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

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

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

1. 调用介绍

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

// 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开始:

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

  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|

这里是反编译的结果:

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

因此,现在的内存是:

img

图:字节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|

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

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

注意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 know...

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

点赞 0
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
翻译小组
翻译小组
0x9e64...7c84
大家看到好的文章可以在 GitHub 提 Issue: https://github.com/lbc-team/Pioneer/issues 欢迎关注我的 Twitter: https://twitter.com/UpchainDAO