智能合约安全系列文章反汇编·上篇
字数 2738 2025-08-22 12:22:48
智能合约反汇编教学文档(上篇)
一、前言
本教学文档基于智能合约安全系列文章中的反汇编上篇内容,旨在帮助学习者掌握智能合约反汇编的基础知识和分析方法。通过本教程,您将学习到:
- 智能合约反汇编的基本概念
- 如何使用Online Solidity Decompiler进行反汇编
- 如何解读反汇编后的指令序列
- 理解智能合约的执行流程
二、示例合约
合约源码
pragma solidity ^0.4.24;
contract Tee {
uint256 private c;
function a() public returns (uint256) {
self(2);
}
function b() public {
c++;
}
function self(uint n) internal returns (uint256) {
if (n <= 1) {
return 1;
}
return n * self(n - 1);
}
}
合约部署后的opcode
0x6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630dbe671f14604e5780634df7e3d0146076575b600080fd5b348015605957600080fd5b506060608a565b6040518082815260200191505060405180910390f35b348015608157600080fd5b5060886098565b005b60006094600260ab565b5090565b6000808154809291906001019190505550565b600060018211151560be576001905060cd565b60c86001830360ab565b820290505b9190505600a165627a7a7230582003f585ad588850fbfba4e8d96684e2c3fa427daf013d4a0f8e78188d4d475ee80029
三、反汇编基础指令解析
1. 基本指令说明
| 指令 | 名称 | 功能描述 |
|---|---|---|
| PUSH1-PUSH32 | 压入字节 | 将1-32字节数据压入栈顶 |
| MSTORE | 内存存储 | 从栈中取出两个值arg0和arg1,将arg1存放在内存的arg0处 |
| CALLDATASIZE | 调用数据大小 | 获取msg.data调用数据的大小 |
| LT | 小于比较 | 比较栈顶两个值,若先出栈的值小于后出栈的值则把1入栈,否则0入栈 |
| JUMPI | 条件跳转 | 从栈中取出两个值arg0和arg1,若arg1为真则跳转到arg0处 |
| CALLDATALOAD | 调用数据加载 | 从指定索引处读取32字节的调用数据 |
| SWAP1 | 交换 | 交换栈顶元素与之后的第一个元素 |
| DIV | 除法 | 计算栈中第一个元素除以第二个元素的值 |
| AND | 与运算 | 对栈顶两个值进行按位与运算 |
| DUP1 | 复制栈顶 | 复制当前栈顶元素 |
| EQ | 相等比较 | 比较栈顶两个值是否相等,相等则1入栈,否则0入栈 |
| JUMPDEST | 跳转目标 | 标记跳转目标位置 |
| REVERT | 回滚 | 停止执行并回滚状态 |
2. 初始指令分析(label_0000)
0000 60 PUSH1 0x80
0002 60 PUSH1 0x40
0004 52 MSTORE
0005 60 PUSH1 0x04
0007 36 CALLDATASIZE
0008 10 LT
0009 60 PUSH1 0x49
000B 57 *JUMPI
执行过程:
PUSH1 0x80:将0x80压入栈顶- 栈:[0x80]
PUSH1 0x40:将0x40压入栈顶- 栈:[0x40, 0x80]
MSTORE:将0x80存储在内存0x40处- 栈:[]
PUSH1 0x04:将0x04压入栈顶- 栈:[0x04]
CALLDATASIZE:获取调用数据大小- 栈:[calldata_size, 0x04]
LT:比较调用数据大小是否小于4字节- 若calldata_size < 4:栈:[1]
- 否则:栈:[0]
PUSH1 0x49:将0x49压入栈顶- 栈:[0x49, 比较结果(0或1)]
JUMPI:若比较结果为1,跳转到0x49;否则继续执行
3. 函数选择器分析(label_000C)
000C 60 PUSH1 0x00
000E 35 CALLDATALOAD
000F 7C PUSH29 0x0100000000000000000000000000000000000000000000000000000000
002D 90 SWAP1
002E 04 DIV
002F 63 PUSH4 0xffffffff
0034 16 AND
0035 80 DUP1
0036 63 PUSH4 0x0dbe671f
003B 14 EQ
003C 60 PUSH1 0x4e
003E 57 *JUMPI
执行过程:
PUSH1 0x00:将0x00压入栈顶- 栈:[0x00]
CALLDATALOAD:从调用数据0x00处读取32字节- 栈:[calldata_32bytes]
PUSH29:将29字节的掩码压入栈顶- 栈:[0x0100...0000, calldata_32bytes]
SWAP1:交换栈顶两个元素- 栈:[calldata_32bytes, 0x0100...0000]
DIV:计算calldata_32bytes / 0x0100...0000- 结果:提取calldata的前4字节(函数选择器)
- 栈:[function_selector]
PUSH4 0xffffffff:将4字节掩码压入栈顶- 栈:[0xffffffff, function_selector]
AND:对函数选择器进行掩码操作- 栈:[clean_function_selector]
DUP1:复制栈顶元素- 栈:[function_selector, function_selector]
PUSH4 0x0dbe671f:压入函数a()的选择器- 栈:[0x0dbe671f, function_selector, function_selector]
EQ:比较函数选择器是否匹配- 若匹配:栈:[1, function_selector]
- 否则:栈:[0, function_selector]
PUSH1 0x4e:压入跳转目标- 栈:[0x4e, 比较结果, function_selector]
JUMPI:若比较结果为1,跳转到0x4e;否则继续执行
4. 其他函数选择器检查(label_003F)
003F 80 DUP1
0040 63 PUSH4 0x4df7e3d0
0045 14 EQ
0046 60 PUSH1 0x76
0048 57 *JUMPI
执行过程:
DUP1:复制栈顶的函数选择器- 栈:[function_selector, function_selector]
PUSH4 0x4df7e3d0:压入函数b()的选择器- 栈:[0x4df7e3d0, function_selector, function_selector]
EQ:比较函数选择器是否匹配- 若匹配:栈:[1, function_selector]
- 否则:栈:[0, function_selector]
PUSH1 0x76:压入跳转目标- 栈:[0x76, 比较结果, function_selector]
JUMPI:若比较结果为1,跳转到0x76;否则继续执行
5. 默认处理(label_0049)
0049 5B JUMPDEST
004A 60 PUSH1 0x00
004C 80 DUP1
004D FD *REVERT
执行过程:
JUMPDEST:标记跳转目标PUSH1 0x00:压入0x00- 栈:[0x00]
DUP1:复制栈顶元素- 栈:[0x00, 0x00]
REVERT:回滚操作,表示没有匹配的函数选择器
四、关键点总结
-
函数选择器提取:通过
CALLDATALOAD和DIV操作从调用数据中提取4字节的函数选择器。 -
函数分发逻辑:
- 合约首先检查调用数据长度是否小于4字节(函数选择器长度)
- 然后依次比较提取的函数选择器与合约中函数的签名
- 匹配则跳转到相应函数代码位置
- 都不匹配则执行
REVERT
-
内存布局:
- 初始时将0x80存储在内存0x40处,这是Solidity的内存管理机制
-
栈操作:
- 反汇编中大量使用栈操作指令(PUSH, DUP, SWAP等)
- 理解栈的状态变化是分析反汇编的关键
-
跳转逻辑:
JUMPI指令配合前面的比较指令实现条件跳转- 每个函数的入口点都有对应的跳转目标
五、下篇预告
在下篇中,我们将继续分析:
- 函数a()和b()的具体实现逻辑
- 内部函数self()的递归调用实现
- 完整的执行流程串联
- 存储变量c的访问方式
- 完整的反汇编到源代码的映射关系