译文出自:登链翻译计划[1] 译者:翻译小组[2] 校对:Tiny 熊[3]
这是通过逆向和调试深入 EVM 最后一篇,我们将讨论与其他智能合约的交互。EVM 是如何处理这个问题的?让我们拭目以待!
这里是我们的(最后一个)测试智能合约:
// 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;
}
}
你可以通过以下链接找到该函数:https://ethervm.io/decompile/ropsten/0x7dCb3545d535D2DF5752F389c34E346f333d907b
我们将从函数选择器的跳转后的 0x6D 开始:
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 的指针) 。我们可以把它翻译成汇编代码:
addr := sload(0x00)
下面是下一个部分的反汇编:
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
:
|0x80|0x40|addr|0x00|RET|
。|0x80|0x40|addr|0x00|RET|
。|0x00|0xa0|0x80|0x40|addr|0x00|RET|
。这里是反编译的结果:
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 后的内存
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|
由于这里有很多汇编看起来很熟悉,我不会描述所有的指令。
注意0x03155a67 是 test()函数的签名!
反编译结果:
mstore(0xa0,0x03155a6700000..00)
free_pointer4 = mload(0x40) // load a4
在 92 和 9E 之间:
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 是什么?别担心,我以后会解释的 :)
反编译:
addr = addr & 0x000..00fff
func_012d(0xa4,0x80) // return values aren't known now.
内存:
让我们跟随执行流程:
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
我们可以对这段汇编进行如下拆解:
var1 := mload(arg2) // loads 0x04, arg2 = 080
if (!(var1 < 0x00)) {
JUMP 0x014e
}
经过分析,如果 0x00不小于内存[0x80:0xa0],这段代码 JUMP 到 0x14e。如果不是,就执行下面的代码。由于 0x00 比 0x04少,所以代码不会 JUMP。
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|...
反编译:
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。
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|
反编译:
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 次,为什么会这样?这个函数的目的是什么?
请耐心等待,我将回答你所有的问题 :)
这个函数可以被完全解构:
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
}
函数的终点在 0x167,EVM 跳到 RET 地址(0xba)
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 在堆栈中推了很多值,正如我们在上面看到的。下面是堆栈中每个参数的含义。
下面是调用前的内存内容:
我们看到memory[0xa4:0xa4+0x04] = memory[0xa4:0xa8] = 0x0c55699c。这就是函数test() 的签名。
由于没有任何参数,msg.data 只包含4 个字节。
调用完成后:
返回值根据 CALL 中提供的参数存储在内存中。
不要忘记 step over 和 step in 的区别!
在 RED 中,这意味着 step in。(进入下一个指令)
绿色框起来的,表示 step over。(步入下一条指令,并跳过函数,如果有对函数的调用,无论这是一个内部或外部函数。)
当你在字节 0xC7 时,例外地点击绿色按钮,跳过调用(你也可以测试一下,当你点击红色按钮时会发生什么)。
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 。
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位置之后,智能合约没有包含太多的代码。代码只是结束了...
我们怎么能总结所有这些汇编代码呢?
有 3 个主要部分:
在本文的第 2 部分字节 0xCD 和 B9 之间,EVM 在内存中存储必要的值。(msg.data 的长度和返回值的长度)
在本文的第 3 部分(函数 func_012d)似乎在内存中做一些验证......
在本文的第 4/5/6 部分,EVM 将所有参数推入堆栈,调用函数并检查返回数据的大小。
在这 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