通过逆向和调试深入EVM 6 - 完整的智能合约布局
在这个合约中,我们将逆向一个完整的智能合约。这一部分的目标是全面了解智能合约布局,全面了解智能合约的布局,并通过手动的方式对其进行反编译。
这是我们通过逆向和调试深入EVM的第 6 篇,在这里你可以找到之前和接下来的部分。
下面的代码就是要分析的智能合约,用以下设置编译它。
// 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
(不要相信这个反编译。有一些错误将在这篇文章中强调,请使用文章末尾的反汇编)
在每一个程序中(不仅仅是在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函数中的内容。我们需要首先 "映射 "智能合约的所有函数,看看它们在智能合约中的位置。
一个智能合约,仅仅是由不同的函数构成的。每一块汇编代码都在一个函数中,而且每个函数在智能合约代码中都是并排的。
这意味着,如果有一些函数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到一个未...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!