本文为ConsenSys CTF,Ethereum Sandbox相关的一篇文章。 在了解这个题目前需要我们对以太坊和Solidity的基本概念进行理解。

题目一

我们的目标部署0x68cb858247ef5c4a0d0cde9d6f68dce93e49c02a的一个合约上。该合约没有经过代码验证操作,所以我们需要对该合约进行逆向从而获取源代码信息。

代码信息如下:

// Decompiled at www.contract-library.com

// Data structures and variables inferred from the use of storage instructions
uint256[] stor_write_what_where_gadget; // STORAGE[0x0]
uint256[] stor_owners; // STORAGE[0x1]

// Note: The function selector is not present in the original solidity code.
// However, we display it for the sake of completeness.
function __function_selector__(uint256 function_selector) public {
  MEM[0x40] = 0x80;
  if ((msg.data.length() >= 0x4)) {
    if ((0x25e7c27 == function_selector)) owners(uint256)(function_selector);
    if ((0x2918435f == function_selector)) fun_sandbox(address)();
    if ((0x4214352d == function_selector)) write_what_where_gadget(uint256,uint256)();
    if ((0x74e3fb3e == function_selector)) 0x74e3fb3e(function_selector);
  }
  throw();
}

function write_what_where_gadget() public {
  require(!msg.value);
  require(((msg.data.length() - 0x4) >= 0x40));
  v1200x149 = msg.data[v1200x131];
  v1200x14d = v1200x131 + 32;
  require((msg.data[v1200x14d] < stor_write_what_where_gadget.length));
  stor_write_what_where_gadget[msg.data[v1200x14d]] = v1200x149;
  exit();
}

function 0x74e3fb3e() public {
  require(!msg.value);
  require(((msg.data.length() - 0x4) >= 0x20));
  v1650x18e = msg.data[v1650x176];
  require((v1650x18e < stor_write_what_where_gadget.length));
  v1650x1a1 = MEM[0x40];
  MEM[v1650x1a1] = stor_write_what_where_gadget[v1650x18e][0];
  return(MEM[MEM[0x40]:MEM[0x40] + (v1650x1a1 + 32 - MEM[0x40])]);
}

function owners() public {
  require(!msg.value);
  require(((msg.data.length() - 0x4) >= 0x20));
  v610x8a = msg.data[v610x72];
  require((v610x8a < stor_owners.length));
  v610x1ef = address(stor_owners[v610x8a][0] >> 0);
  v610x9d = MEM[0x40];
  MEM[v610x9d] = address(v610x1ef);
  return(MEM[MEM[0x40]:MEM[0x40] + (v610x9d + 32 - MEM[0x40])]);
}

function fun_sandbox(address varg0) public {
  require(((msg.data.length() - 0x4) >= 0x20));
  v289_1 = 0x0;
  v20b_0 = 0x0;
  while (true) {
    if ((v20b_0 >= stor_owners.length)) break;
    require((v20b_0 < stor_owners.length));
    if ((address(msg.sender) == address(stor_owners[v20b_0][0] >> 0))) {
      v289_1 = 0x1;
    }
    v20b_0 = v20b_0 + 1;
    continue;
  }
  require(v289_1);
  v29c = extcodesize(varg0);
  v2a1 = MEM[0x40];
  MEM[0x40] = (v2a1 + (v29c + 63 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0));
  MEM[v2a1] = v29c;
  EXTCODECOPY(varg0, v2a1 + 32, 0x0, v29c);
  v2cf_0 = 0x0;
  while (true) {
    if ((v2cf_0 >= MEM[v2a1])) break;
    if ((v2cf_0 < MEM[v2a1])) break;
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xf0 << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xf1 << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xf2 << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xf4 << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xfa << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xff << 248));
    v2cf_0 = v2cf_0 + 1;
    continue;
  }
  v714 = address(varg0).delegatecall(MEM[MEM[0x40] : MEM[0x40] + ((0x0 + MEM[0x40]) - MEM[0x40])]).gas(msg.gas);
  if ((RETURNDATASIZE != 0x0)) {
    vdc0x724 = MEM[0x40];
    MEM[0x40] = (vdc0x724 + (RETURNDATASIZE + 63 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0));
    MEM[vdc0x724] = RETURNDATASIZE;
    RETURNDATACOPY(vdc0x724 + 32, 0x0, RETURNDATASIZE);
  }
  vdc0x753 = !vdc0x714;
  require(vdc0x714);
  exit();
}

合约未经验证,因此我们将使用合约库的方式对其进行逆向工程https://contract-library.com/contracts/Ethereum/0x68cb858247ef5c4a0d0cde9d6f68dce93e49c02a

我们发现在功能0x2918435f中有一个被更改过的调用函数。如果我们可以指定delegatecall使用的地址,那么我们基本上就能拥有合约。 让我们来看看为了触发此漏洞必须满足哪些条件。

首先我们来看先决条件:

function 0x74e3fb3e() public {
  require(!msg.value);
  require(((msg.data.length() - 0x4) >= 0x20));
  v1650x18e = msg.data[v1650x176];
  require((v1650x18e < stor_write_what_where_gadget.length));
  v1650x1a1 = MEM[0x40];
  MEM[v1650x1a1] = stor_write_what_where_gadget[v1650x18e][0];
  return(MEM[MEM[0x40]:MEM[0x40] + (v1650x1a1 + 32 - MEM[0x40])]);
}

在该函数中,我们需要满足:

require(((msg.data.length - 0x4) >= 0x20));

即消息data长度必须至少为32字节。

我们对合约进行一些细微的修改,存储偏移量0x01存储了一个数组。 此代码实质上检查调用者是否在该数组中。 在代码开始处,此数组等于[0xf339084e9838281c953f3e812f32a6e145f64bff]。

bool foundOwner = false;
for (int index = 0; index < owners.length; index++) {
    if (msg.sender == owners[index]) {
        foundOwner = true;
    }
}
require(foundOwner);

之后我们再看下面的内容:

while (true) {
    if ((v2cf_0 >= MEM[v2a1])) break;
    if ((v2cf_0 < MEM[v2a1])) break;
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xf0 << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xf1 << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xf2 << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xf4 << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xfa << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xff << 248));
    v2cf_0 = v2cf_0 + 1;
    continue;

在合约中我们看到了上述一堆条件,然而这些条件经过分析可以简化为:

bytes memory code = address(target).code;
for (int index = 0; index < code.length; index++) {
    require(code[index] != 0xf0);
    require(code[index] != 0xf1);
    require(code[index] != 0xf2);
    require(code[index] != 0xf4);
    require(code[index] != 0xfa);
    require(code[index] != 0xf4);
}

这个前提条件很容易理解。 根据黑名单(CREATE,CALL,CALLCODE,DELEGATECALL,STATICCALL和SELFDESTRUCT)检查目标合同代码的每个字节。 这就是类似于沙箱的某种操作。

前提条件1很简单,由于msg.data中的内容是我们给出的,所以其长度很容易满足。然而前提条件2比较棘手。由于我们没有直接修改所有者数组的函数。 因此,我们需要寻找其他的方法来满足上述条件。 唯一的具有修改内容的函数如下:

if ((0x4214352d == function_selector)) write_what_where_gadget(uint256,uint256)();
function write_what_where_gadget() public {
  require(!msg.value);
  require(((msg.data.length() - 0x4) >= 0x40));
  v1200x149 = msg.data[v1200x131];
  v1200x14d = v1200x131 + 32;
  require((msg.data[v1200x14d] < stor_write_what_where_gadget.length));
  stor_write_what_where_gadget[msg.data[v1200x14d]] = v1200x149;
  exit();
}

看起来这个函数并没有什么危险,但实际上此函数隐藏了一个任意的写原语的接口,我们可以用它将合约的owner所有权转让给我们自己。

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖