首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >通过逆向和调试深入EVM #7 - 与其他智能合约的交互

通过逆向和调试深入EVM #7 - 与其他智能合约的交互

作者头像
Tiny熊
发布2023-01-09 17:21:00
发布2023-01-09 17:21:00
68100
代码可运行
举报
运行总次数:0
代码可运行

译文出自:登链翻译计划[1] 译者:翻译小组[2] 校对:Tiny 熊[3]

这是通过逆向和调试深入 EVM 最后一篇,我们将讨论与其他智能合约的交互。EVM 是如何处理这个问题的?让我们拭目以待!

1. 调用介绍

这里是我们的(最后一个)测试智能合约:

代码语言:javascript
代码运行次数:0
运行
复制
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^ 0.8 .0;

contract Caller {
  address _addr;

  function setAddr(address x) external {
    _addr = x;
  }

  function test() external {
    _addr.call {
      value: 0 ether,
      gas: 1000000
    }(abi.encodeWithSignature("x()"));
  }
}

contract Called {

  function x() external {
    uint a = 2 + 3;
    return a;
  }

}
  1. 你需要编译和部署 2 个合约 CallerCalled (启动优化, runs 为 200, solidity 0.8.7)。
  2. 之后,调用函数 Caller.setAddr(x),x=被调用合约的地址,将地址设置为被调用合约。
  3. 现在我们可以通过调用函数 "test()"并进行反汇编来分析该函数。

你可以通过以下链接找到该函数:https://ethervm.io/decompile/ropsten/0x7dCb3545d535D2DF5752F389c34E346f333d907b

2. 全面反编译

我们将从函数选择器的跳转后的 0x6D 开始:

代码语言:javascript
代码运行次数:0
运行
复制
006D    5B  JUMPDEST     |0xf8a8fd6d| (discarded)
006E    61  PUSH2 0x006b |0x006b|
0071    60  PUSH1 0x00   |0x00|0x006b|
0073    80  DUP1         |0x00|0x00|0x006b|
0074    54  SLOAD        |addr|0x00|0x006b|

在 0x6E,PUSH2 0x006b是一个指向 0x6B 的指针,在那里函数STOP,在else if函数选择器的结束时,EVM 将跳转到这个字节。

之后,EVM SLOAD在 0x00 槽的地址,这当然是智能合约中的 _addr 变量。在 74 字节之后堆栈为:|addr|0x00|RET| (RET 是 0x6b,指向 6B 的指针) 。我们可以把它翻译成汇编代码:

代码语言:javascript
代码运行次数:0
运行
复制
addr := sload(0x00)

下面是下一个部分的反汇编:

代码语言:javascript
代码运行次数:0
运行
复制
0075    60  PUSH1 0x40  |0x40|addr|0x00|0x006b|
0077    80  DUP1        |0x40|0x40|addr|0x00|0x006b|
0078    51  MLOAD       |0x80|0x40|addr|0x00|0x006b|
0079    60  PUSH1 0x04  |0x04|0x80|0x40|addr|0x00|0x006b|
007B    81  DUP2        |0x80|0x04|0x80|0x40|addr|0x00|0x006b|
007C    52  MSTORE      |0x80|0x40|addr|0x00|0x006b|
007D    60  PUSH1 0x24  |0x24|0x80|0x40|addr|0x00|0x006b|
007F    81  DUP2        |0x80|0x24|0x80|0x40|addr|0x00|0x006b|
0080    01  ADD         |0xa4|0x80|0x40|addr|0x00|0x006b|
0081    82  DUP3        |0x40|0xa4|0x80|0x40|addr|0x00|0x006b|
0082    52  MSTORE      |0x80|0x40|addr|0x00|0x006b|
0083    60  PUSH1 0x20  |0x20|0x80|0x40|addr|0x00|0x006b|
0085    81  DUP2        |0x80|0x20|0x80|0x40|addr|0x00|0x006b|
0086    01  ADD         |0xa0|0x80|0x40|addr|0x00|0x006b|
0087    80  DUP1        |0xa0|0xa0|0x80|0x40|addr|0x00|0x006b|
0088    51  MLOAD       |0x00|0xa0|0x80|0x40|addr|0x00|0x006b|

EVM 继续进行一系列的MLOAD/MSTORE

  1. 在第 78 字节,它在 0x40 处 MLOAD 内存,结果是 0x80(空闲内存指针),堆栈为:|0x80|0x40|addr|0x00|RET|
  2. 在第 7C 字节,EVM 在空闲内存指针(这里是 0x80)处 MSTORE 0x04,堆栈保持不变|0x80|0x40|addr|0x00|RET|
  3. 在 7D-82 字节,EVM 将 24 加到 0x80,并将结果 0xa4 存入 40,这是新的空闲内存指针,堆栈保持完全相同。这是正常的,因为 0x80 这次不是空闲的。
  4. 在第 88 个字节,EVM 将 0x20 加到 0x80 中,这次 MLOAD 的结果=0xa0。当然内存是空的,堆栈是:|0x00|0xa0|0x80|0x40|addr|0x00|RET|

这里是反编译的结果:

代码语言:javascript
代码运行次数:0
运行
复制
uint free_pointer := mload(0x40) // equalts 0x80
mstore(free_pointer,0x04)
free_pointer2 := free_pointer + 0x24
mstore(0x40,free_pointer2)
free_pointer3 := free_pointer + 0x20
result := mload(free_pointer3) // equals 0

因此,现在的内存是:

图:字节 0x88 后的内存

代码语言:javascript
代码运行次数:0
运行
复制
0089  60  PUSH1 0x01  |0x01|0x00|0xa0|0x80|0x40|addr|0x00|.
008B  60  PUSH1 0x01  |0x01|0x01|0x00|0xa0|0x80|0x40|addr|0x00|.
008D  60  PUSH1 0xe0  |0xe0|0x01|0x01|0x00|0xa0|.. (hidden)
008F  1B  SHL         |0x00..00100..000|0x01|0x00|0xa0|..
0090  03  SUB         |0x00..000ff..fff|0x00|0xa0|...
0091  16  AND         |0x00|0xa0|...

0092  63  PUSH4 0x03155a67 |0x03155a67|0x00|0xa0|...
0097  60  PUSH1 0xe2       |0xe2|0x03155a67|0x00|0xa0|...
0099  1B  SHL    |0x03155a6700000..00|0x00|0xa0|...
009A  17  OR     |0x03155a6700000..00|0xa0|...
009B  90  SWAP1  |0xa0|0x03155a6700000..00|...
009C  52  MSTORE |0x80|0x40|addr|0x00|RET|
009D  90  SWAP1  |0x40|0x80|addr|0x00|RET|
009E  51  MLOAD  |0xa4|0x80|addr|0x00|RET|

由于这里有很多汇编看起来很熟悉,我不会描述所有的指令。

  1. 在 89 和 91 字节之间,EVM 不做任何处理(这里代码可以优化)。
  2. 在 92 和 9B 字节之间,EVM 在堆栈中 "创建了" 0x03155a6700000...00
  3. 在字节 9C 处:EVM 将这个 ( 0x03155a6700000..00) 存储在 0xa0
  4. 在第 9E 字节:EVM MLOAD 自由内存指针。

注意0x03155a67 是 test()函数的签名!

反编译结果:

代码语言:javascript
代码运行次数:0
运行
复制
mstore(0xa0,0x03155a6700000..00)
free_pointer4 = mload(0x40) // load a4

在 92 和 9E 之间:

代码语言:javascript
代码运行次数:0
运行
复制
009F    60  PUSH1 0x01  |0x01|0xa4|0x80|addr|0x00|RET|
00A1    60  PUSH1 0x01  |0x01|0x01|0xa4|0x80|addr|0x00|RET|
00A3    60  PUSH1 0xa0  |0xa0|0x01|0x01|0xa4|0x80|addr|0x00|RET|
00A5    1B  SHL   |0x0..0100..00|0x01|0xa4|0x80|addr|0x00|RET|
00A6    03  SUB   |0x0..0ff..ff|0xa4|0x80|addr|0x00|RET|
00A7    90  SWAP1 |0xa4|0x0..0ff..ff|0x80|addr|0x00|RET|
00A8    92  SWAP3 |addr|0x0..0ff..ff|0x80|0xa4|0x00|RET|
00A9    16  AND   |addr (cleaned) |0x80|0xa4|0x00|RET|
00AA    92  SWAP3 |0x00|0x80|0xa4|addr|RET|
00AB    62  PUSH3 0x0f4240 |0x0f4240|0x00|0x80|0xa4|addr|RET|
00AF    92  SWAP3          |0xa4|0x00|0x80|0x0f4240|addr|RET|
00B0    90  SWAP1          |0x00|0xa4|0x80|0x0f4240|addr|RET|
00B1    91  SWAP2          |0x80|0xa4|0x00|0x0f4240|addr|RET|
00B2    61  PUSH2 0x00ba   |0x00ba|0x80|0xa4|0x00|0x0f4240|addr|RET|
00B5    91  SWAP2          |0xa4|0x80|0x00ba|0x00|0x0f4240|addr|RET|
00B6    61  PUSH2 0x012d   |0x012|0xa4|0x80|0x00ba|0x00|0x0f4240|
00B9    56  *JUMP          |0xa4|0x80|0x00ba|0x00|0x0f4240|addr|RET|

在 9F 和 A9 之间,我们也已经知道这个模式,目的是用 0x000...00ffff 作为掩码来 "清理 "地址。

例如,这里是一个 "清理过的"地址(32 个字节):0x000000000000000000000000aaC5322e456d45E7b6c452038836C5631C2AeBc0

而这里是没有被清理的相同地址:0x10000000000b000000000000aaC5322e456d45E7b6c452038836C5631C2AeBc0

目标是去除 "1 "和 "b"。

在 AA 和 B1 之间,没有什么有趣的东西。

这个部分以对位于 0x012d 的函数的调用结束,参数是0xa4 和 0x80

除了 "地址清理 "和内存中的签名外,没有什么可说的,至少现在是这样......

内存[0x80:0xa0]中的 4 是什么?别担心,我以后会解释的 :)

反编译:

代码语言:javascript
代码运行次数:0
运行
复制
addr = addr & 0x000..00fff
func_012d(0xa4,0x80)   // return values aren't known now.

内存:

3. 在 0x12D 的函数里有什么?

让我们跟随执行流程:

代码语言:javascript
代码运行次数:0
运行
复制
012D    5B  JUMPDEST     |0xa4|0x80|RET|....
012E    60  PUSH1 0x00   |0x00|0xa4|0x80|RET|....
0130    82  DUP3         |0x80|0x00|0xa4|0x80|RET|....
0131    51  MLOAD        |0x04|0x00|0xa4|0x80|RET|....
0132    60  PUSH1 0x00   |0x00|0x04|0x00|0xa4|0x80|RET|....
0134    5B  JUMPDEST     |0x00|0x04|0x00|0xa4|0x80|RET|....
0135    81  DUP2         |0x04|0x00|0x04|0x00|0xa4|0x80|RET|....
0136    81  DUP2         |0x00|0x04|0x00|0x04|0x00|0xa4|0x80|RET|...
0137    10  LT           |0x01|0x00|0x04|0x00|0xa4|0x80|RET|....
0138    15  ISZERO       |0x00|0x00|0x04|0x00|0xa4|0x80|RET|....
0139    61  PUSH2 0x014e
013C    57  *JUMPI

我们可以对这段汇编进行如下拆解:

代码语言:javascript
代码运行次数:0
运行
复制
var1 := mload(arg2)  // loads 0x04, arg2 = 080
if (!(var1 < 0x00)) {
     JUMP 0x014e
}

经过分析,如果 0x00小于内存[0x80:0xa0],这段代码 JUMP 到 0x14e。如果不是,就执行下面的代码。由于 0x00 比 0x04,所以代码不会 JUMP

代码语言:javascript
代码运行次数:0
运行
复制
013D    60  PUSH1 0x20 |0x20|0x00|0x04|0x00|0xa4|0x80|RET|....
013F    81  DUP2   |0x00|0x20|0x00|0x04|0x00|0xa4|0x80|RET|....
0140    86  DUP7   |0x80|0x00|0x20|0x00|0x04|...
0141    01  ADD    |0x80|0x20|0x00|0x04|...
0142    81  DUP2   |0x20|0x80|0x20|0x00|0x04|...
0143    01  ADD    |0xa0|0x20|0x00|0x04|...
0144    51  MLOAD  |0x0c55699c|0x20|0x00|0x04|...
0145    85  DUP6   |0xa4|0x0c55699c|0x20|0x00|0x04|...
0146    83  DUP4   |0x00|0xa4|0x0c55699c|0x20|0x00|0x04|...
0147    01  ADD    |0xa4|0x0c55699c|0x20|0x00|0x04|...
0148    52  MSTORE |0x20|0x00|0x04|...
0149    01  ADD    |0x20|0x04|...
014A    61  PUSH2 0x0134 |0x134|0x20|0x04|...
014D    56  *JUMP  |0x20|0x04|...

反编译:

代码语言:javascript
代码运行次数:0
运行
复制
signature := mload(0xa0)
mstore(0xa4, signature)
jump 0x134

这段代码加载在内存[0xa0:0xc0] 的函数x() 签名,并在内存[0xa4:0xc4] 中存储 MStore。

这段代码JUMP 到 0x134,但是在函数_12d(uint,uint)的开头,参数不是 0x80 和 0x04,而是 0x20 和 0x04。

下一次内存[0x20:0x40]将被加载作为结果,但是这个块是空的(=0x00),所以内存[0x20:0x40]=0x0, 不小于 0x0,因此代码将在 0x14e 处 JUMP。

代码语言:javascript
代码运行次数:0
运行
复制
014E    5B  JUMPDEST  |0x20|0x04|0x00|0xa4|0x80|0xba|0x00|..
014F    81  DUP2  |0x04|0x20|0x04|0x00|0xa4|0x80|0xba|0x00|..
0150    81  DUP2  |0x20|0x04|0x20|0x04|0x00|0xa4|0x80|0xba|0x00|
0151    11  GT    |0x01|0x20|0x04|0x00|0xa4|0x80|0xba|0x00|
0152    15  ISZERO |0x00|0x20|0x04|0x00|0xa4|0x80|0xba|0x00|
0153    61  PUSH2 0x015d
0156    57  *JUMPI0157    60  PUSH1 0x00 |0x00|0x20|0x04|0x00|0xa4|0x80|0xba|0x00|
0159    82  DUP3  |0x04|0x00|0x20|0x04|0x00|0xa4|0x80|0xba|0x00|
015A    85  DUP6  |0xa4|0x04|0x00|0x20|0x04|0x00|0xa4|0x80|...|
015B    01  ADD   |0xa8|0x00|0x20|0x04|0x00|0xa4|0x80|...|
015C    52  MSTORE   |0x20|0x04|0x00|0xa4|0x80|...|
015D    5B  JUMPDEST |0x20|0x04|0x00|0xa4|0x80|...|
015E    50  POP      |0x04|0x00|0xa4|0x80|...|
015F    91  SWAP2    |0xa4|0x00|0x04|0x80|...|
0160    90  SWAP1    |0x00|0xa4|0x04|0x80|...|
0161    91  SWAP2    |0x04|0xa4|0x00|0x80|...|
0162    01  ADD      |0xa8|0x00|0x80|0xba (RET)|
0163    92  SWAP3    |0xba|0x00|0x80|0xa8|
0164    91  SWAP2    |0x80|0x00|0xba|0xa8|
0165    50  POP
0166    50  POP
0167    56  *JUMP    |0xa8|

反编译:

代码语言:javascript
代码运行次数:0
运行
复制
if ((mload(0x80) > 20)) {
    mstore(var1+arg1,0x00) //arg1= a4 var1 = mload(0x80) = 4
}

开始的时候。这段代码会验证0x20是否大于0x04。如果是,0x00被存储在memory[first_argument+var1] (var1 等于 4)。否则,继续执行流程,函数结束。

内存几乎和以前一样,只有一个区别。函数签名在memory[0xa0:0xa8] 出现了 2 次,为什么会这样?这个函数的目的是什么?

请耐心等待,我将回答你所有的问题 :)

这个函数可以被完全解构:

代码语言:javascript
代码运行次数:0
运行
复制
var1 := mload(arg2)  // loads 0x04, arg2 = 080
if (!(var1 < 0x00)) {
     signature := mload(0xa0)
     mstore(0xa4, signature)
     jump 0x134
}

if ((mload(0x80) > 20)) {
    mstore(var1+arg1,0x00) //arg1= a4 var1 = mload(0x80) = 4
}

4. 调用指令

函数的终点在 0x167,EVM 跳到 RET 地址(0xba)

代码语言:javascript
代码运行次数:0
运行
复制
00BA    5B  JUMPDEST    |0xa8|0x00|0xf4240|address|0x06b|..
00BB    60  PUSH1 0x00  |0x00|0xa8|0x00|0xf4240|address|0x06b|..
00BD    60  PUSH1 0x40
|0x40|0x00|0xa8|0x00|0xf4240|address|0x06b|..
00BF    51  MLOAD  |0xa4|0x00|0xa8|0x00|0xf4240|address|0x06b|..
00C0    80  DUP1   |0xa4|0xa4|0x00|0xa8|0x00|0xf4240|addr|..
00C1    83  DUP4   |0xa8|0xa4|0xa4|0x00|0xa8|0x00|0xf4240|addr|..
00C2    03  SUB    |0x04|0xa4|0x00|0xa8|0x00|0xf4240|addr|..
00C3    81  DUP2   |0xa4|0x04|0xa4|0x00|0xa8|0x00|0xf4240|addr|..
00C4    85  DUP6   |0x00|0xa4|0x04|0xa4|0x00|0xa8|0x00|0xf4240|..
00C5    88  DUP9   |addr|0x00|0xa4|0x04|0xa4|0x00|0xa8|0x00|...
00C6    88  DUP9   |0xf4240|addr|0x00|0xa4|0x04|0xa4|0x00|0xa8|..
00C7    F1  CALL   |0x01|0xa8|..

由于 CALL 指令在堆栈中需要 7 个参数,EVM 在堆栈中推了很多值,正如我们在上面看到的。下面是堆栈中每个参数的含义。

  • Stack(0)=Max gas. (=0xf4240 = 十进制 100.000 )
  • Stack(1) = 智能合约的地址
  • Stack(2) = msg.value (发送的以太币数量 = 0)
  • Stack(3) = 内存中 msg.data 的偏移量(=0xa4)
  • Stack(4) = msg.data 的长度(=0x04)
  • Stack(5) = 内存中返回值的偏移量(=0xa4)
  • Stack(6) = 返回值的长度(=0x04)

下面是调用前的内存内容:

我们看到memory[0xa4:0xa4+0x04] = memory[0xa4:0xa8] = 0x0c55699c。这就是函数test() 的签名。

由于没有任何参数,msg.data 只包含4 个字节

调用完成后:

  • 0x00出现在Stack(0) 中,如果调用失败。
  • 如果调用成功,0x01会出现在Stack(0) 中。

返回值根据 CALL 中提供的参数存储在内存中。

5. Step over 与 Step in

不要忘记 step over 和 step in 的区别!

在 RED 中,这意味着 step in。(进入下一个指令)

绿色框起来的,表示 step over。(步入下一条指令,并跳过函数,如果有对函数的调用,无论这是一个内部或外部函数。)

当你在字节 0xC7 时,例外地点击绿色按钮,跳过调用(你也可以测试一下,当你点击红色按钮时会发生什么)

6. RETURNDATASIZE

代码语言:javascript
代码运行次数:0
运行
复制
00C8    93  SWAP4
00C9    50  POP
00CA    50  POP
00CB    50  POP
00CC    50  POP             |0x01|0x6b|0xf8a8fd6d|
00CD    3D  RETURNDATASIZE  |0x00|0x01|0x6b|0xf8a8fd6d|
00CE    80  DUP1            |0x00|0x00|0x01|0x6b|0xf8a8fd6d|
00CF    60  PUSH1 0x00      |0x00|0x00|0x00|0x01|0x6b|0xf8a8fd6d|
00D1    81  DUP2            |0x00|0x00|0x00|0x00|0x01|0x6b|0xf8a8fd.
00D2    14  EQ              |0x01|0x00|0x00|0x01|0x6b|0xf8a8fd.
00D3    61  PUSH2 0x00f8
00D6    57  *JUMPI          |0x00|0x00|0x01|0x6b|0xf8a8fd.

RETURNDATASIZE,我们从未见过这个操作码。它只是在堆栈中返回上次调用时返回的数据的大小。

由于在最后一次调用中没有返回数据,大小为零,因此这个指令返回 0。

接下来在代码中,EVM 将这个值与 0 进行比较,如果RETURNDATASIZE=0,则跳到0xf8

代码语言:javascript
代码运行次数:0
运行
复制
00F8    5B  JUMPDEST  |0x00|0x00|0x01|0x6b|0xf8a8fd.
00F9    50  POP
00FA    50  POP
00FB    50  POP       |0x6b|0xf8a8fd.
00FC    56  *JUMP     |0xf8a8fd006B    5B  JUMPDEST
006C    00  *STOP

0xf8位置之后,智能合约没有包含太多的代码。代码只是结束了...

7. 总结一下

我们怎么能总结所有这些汇编代码呢?

有 3 个主要部分:

在本文的第 2 部分字节 0xCD 和 B9 之间,EVM 在内存中存储必要的值。(msg.data 的长度和返回值的长度)

在本文的第 3 部分(函数 func_012d)似乎在内存中做一些验证......

在本文的第 4/5/6 部分,EVM 将所有参数推入堆栈,调用函数并检查返回数据的大小。

8. 其他 CALL 派生的操作码

  • STATICCALL 这个操作码和 CALL 完全一样,不同的是 msg.value 永远是 0,而且STATICALL不能修改被调用合约的状态。我不会在逆向用STATICCALL来调用一个合约,因为它将花费太多的时间来写,而且其结果与CALL几乎一样。
  • DELEGATECALL,也和CALL一样,但不同的是,所有的状态变化都会在调用者的合约中。(例如,如果槽 0x02 在DELEGATECALL中被设置为 0x10,槽 0x02 在调用方合约中等于 10,而在被调用方合约中不等于 10)。msg.value 和 msg.sender 与未调用智能合约时相同。(如果addr调用智能合约 A,该 A 合约DELEGATECALL到智能合约 Bmsg.sender仍将是addrmsg.value将保持不变)
  • CALLCODE,与DELEGRATECALL非常相似,但 msg.sender 和msg.value被改变为智能合约的。所以在上一个例子中(msg.sender将是合约 A,而msg.value将由合约 A选择发送的金额)。

9. 总结

在这 7 篇文章里,我们几乎学到了 EVM 汇编的每一个指令,更重要的是:逆向智能合约的方法论。

我希望你喜欢这些系列,并学到了很多关于 EVM 的知识!


本翻译由 Duet Protocol[4] 赞助支持。

原文链接:https://trustchain.medium.com/reversing-and-debugging-the-evm-part-7-2a20a44a555e

参考资料

[1]

登链翻译计划: https://github.com/lbc-team/Pioneer

[2]

翻译小组: https://learnblockchain.cn/people/412

[3]

Tiny 熊: https://learnblockchain.cn/people/15

[4]

Duet Protocol: https://duet.finance/?utm_souce=learnblockchain

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-11-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 调用介绍
  • 2. 全面反编译
  • 3. 在 0x12D 的函数里有什么?
  • 4. 调用指令
  • 5. Step over 与 Step in
  • 6. RETURNDATASIZE
  • 7. 总结一下
  • 8. 其他 CALL 派生的操作码
  • 9. 总结
    • 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档