通过逆向和调试深入EVM #6 - 完整的智能合约布局

通过逆向和调试深入EVM 6 - 完整的智能合约布局

在这个合约中,我们将逆向一个完整的智能合约。这一部分的目标是全面了解智能合约布局,全面了解智能合约的布局,并通过手动的方式对其进行反编译。

这是我们通过逆向和调试深入EVM的第 6 篇,在这里你可以找到之前和接下来的部分。

下面的代码就是要分析的智能合约,用以下设置编译它。

  • solidity版本:0.8.7
  • 优化器:200 runs
// SPDX-License-Identifier: UNLICENSED  
pragma solidity ^ 0.8 .0;

contract Test {
  address owner;

  uint data;

  function setOwner(address _addr) external {
    owner = _addr;
  }

  function returnAdd(uint x, uint y) internal view returns(uint) {
    return x + y;
  }

  function setBalance(uint x) external {
    uint var1 = 10;
    data = returnAdd(x, var1);
  }
}

这个智能合约比之前的合约要长一些,但不用担心,难度不高。

以下是该智能合约的完整拆解:https://ethervm.io/decompile/ropsten/0xd3ac4c6028484a0f101f835e9e5dab72a2fe1b97

(不要相信这个反编译。有一些错误将在这篇文章中强调,请使用文章末尾的反汇编)

1. 反汇编函数main

在每一个程序中(不仅仅是在EVM上)都有一个所谓的入口点,这就是被执行的第一行代码。

例如,当你在C或C++中创建一个程序时,入口点是函数main()

但在solidity中有些不同,入口点是智能合约的开始。 每当你在区块链上调用一个智能合约时,这个入口点就会被首先执行。我们将其称为函数main,它的位置当然是在字节0处。

通过查看指令0和17字节之间的反汇编,我们可以很容易地推断出函数main由这段代码开始:

function main() {
  mstore(0x40, 0x80)
  if (msg.value > 0) {
    revert();
  }
  if (msg.data.size < 4) {
    revert();
  }
}

(我们已经在本系列的第一篇分析了智能合约的开始,如果你不记得了,请随时刷新你的知识,如果有什么遗漏的话)

通过查看第18和36字节之间指令,我们可以很容易地看到一个 "else if "语句,这是一个函数选择器:

function main() {
  mstore(0x40, 0x80)
  if (msg.value > 0) {
    revert();
  }
  if (msg.data.size < 4) {
    revert();
  }
  byte4 selector = msg.data[0:4]
  switch (selector) {
    case 0x13af4035:
      // JUMP to 37  

    case 0xfb1669ca:
      // JUMP to 66  
    default:
      revert(0);
  }

值得注意的是,"switch" 语句在solidity中并不存在,而是由 "else if " 组成。

我们将在后面的文章中倒转else if函数中的内容。我们需要首先 "映射 "智能合约的所有函数,看看它们在智能合约中的位置。

2. 函数布局

一个智能合约,仅仅是由不同的函数构成的。每一块汇编代码都在一个函数中,而且每个函数在智能合约代码中都是并排的。

这意味着,如果有一些函数A的代码位于字节1和5之间,一些函数B的代码位于字节6和9之间,函数A不能在字节10之后继续,因为这些代码不是 "并排的",它们被B分开了。

鉴于这些信息,我们将尝试重建智能合约的所有函数,注意有 "用户创建" 的函数,也有 "编译器" 创建的函数。

请注意,在这篇文章中,我们将使用十六进制偏移量而不是十进制

要做到这一点,我们现在只看3条指令:JUMP、JUMPDEST、JUMPI(有时也看PUSH)。

我们知道,在第0字节和第66字节之间(至少),我们是在函数main()中,现在我们不知道函数main()在哪里结束。

但是在第70字节...

67 PUSH1 0x64 
69 PUSH1 0x71  
6B CALLDATASIZE  
6C PUSH1 0x04  
6E PUSH1 0xba  
70 JUMP
71 JUMPDEST

我们可以很容易地看到对0xBA的函数调用,参数0x04和CALLDATASIZE(这是一个将msg.data的大小推入堆栈的函数)被推入堆栈中。

0x71是调用完成后继续执行流程的保存地址(这就是为什么在0x71字节有一个JUMPDEST)。

这样看来,0xba是开始一个新函数的地址,让我们去看看0xba吧

00BA JUMPDEST  
00BB PUSH1 0x00  
00BD PUSH1 0x20  
00BF DUP3  
00C0 DUP5  
00C1 SUB  
00C2 SLT  
00C3 ISZERO  
00C4 PUSH1 0xcb  
00C6 *JUMPI

在0xC6处有一些条件,如果条件得到满足,EVM就会跳到CB,否则代码就会继续进行,不久就会返回。

00C7 PUSH1 0x00  
00C9 DUP1  
00CA *REVERT00CB JUMPDEST 
00CC POP  
00CD CALLDATALOAD  
00CE SWAP2  
00CF SWAP1  
00D0 POP  
00D1 *JUMP

在0xD1函数JUMP到一个未知的目的地,很可能是调用之前保存的0x71地址。(可以通过计算0xba和0xd1之间每条指令的堆栈元素数量来验证)所以这是一个以0xba开始的函数的结束。

这样看来,我们发现的第一个函数位于BA和D1之间,而且我们知道它需要两个参数。

我们将它命名为 "function_0BA(a,b)",因为我们不知道这个函数的 "真实 "名称。

让我们深入研究一下,因为智能合约只是由函数组成。还有一个函数从0xD2开始(在0xD1之后),让我们把它拆解一下。

D2 JUMPDEST  | D3 PUSH1 0x00 | D5  DUP3 | D6 NOT | D7 DUP3 | 00D8 GT | D9 ISZERO | DA PUSH1 0xf2 | DC *JUMPI

在0xDC,那里的函数有一个条件,如果条件得到满足,EVM就会跳转到0xF2,否则就会执行0xDD和0xF1(该代码会被回退)之间的代码。

00DD    63  PUSH4 0x4e487b71  
00E2    60  PUSH1 0xe0  
00E4    1B  SHL  
00E5    60  PUSH1 0x00  
00E7    52  MSTORE  
00E8    60  PUSH1 0x11  
00EA    60  PUSH1 0x04  
00EC    52  MSTORE  
00ED    60  PUSH1 0x24  
00EF    60  PUSH1 0x00  
00F1    FD  *REVERT

00F2    5B  JUMPDEST  
00F3    50  POP  
00F4    01  ADD  
00F5    90  SWAP1  
00F6    56  *JUMP

在0xF2,函数JUMP到一个未...

剩余50%的内容订阅专栏后可查看

0 条评论

请先 登录 后评论
翻译小组
翻译小组

首席翻译官

167 篇文章, 29224 学分