智能合约安全系列文章反汇编·上篇
字数 2738 2025-08-22 12:22:48

智能合约反汇编教学文档(上篇)

一、前言

本教学文档基于智能合约安全系列文章中的反汇编上篇内容,旨在帮助学习者掌握智能合约反汇编的基础知识和分析方法。通过本教程,您将学习到:

  1. 智能合约反汇编的基本概念
  2. 如何使用Online Solidity Decompiler进行反汇编
  3. 如何解读反汇编后的指令序列
  4. 理解智能合约的执行流程

二、示例合约

合约源码

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

执行过程:

  1. PUSH1 0x80:将0x80压入栈顶
    • 栈:[0x80]
  2. PUSH1 0x40:将0x40压入栈顶
    • 栈:[0x40, 0x80]
  3. MSTORE:将0x80存储在内存0x40处
    • 栈:[]
  4. PUSH1 0x04:将0x04压入栈顶
    • 栈:[0x04]
  5. CALLDATASIZE:获取调用数据大小
    • 栈:[calldata_size, 0x04]
  6. LT:比较调用数据大小是否小于4字节
    • 若calldata_size < 4:栈:[1]
    • 否则:栈:[0]
  7. PUSH1 0x49:将0x49压入栈顶
    • 栈:[0x49, 比较结果(0或1)]
  8. 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

执行过程:

  1. PUSH1 0x00:将0x00压入栈顶
    • 栈:[0x00]
  2. CALLDATALOAD:从调用数据0x00处读取32字节
    • 栈:[calldata_32bytes]
  3. PUSH29:将29字节的掩码压入栈顶
    • 栈:[0x0100...0000, calldata_32bytes]
  4. SWAP1:交换栈顶两个元素
    • 栈:[calldata_32bytes, 0x0100...0000]
  5. DIV:计算calldata_32bytes / 0x0100...0000
    • 结果:提取calldata的前4字节(函数选择器)
    • 栈:[function_selector]
  6. PUSH4 0xffffffff:将4字节掩码压入栈顶
    • 栈:[0xffffffff, function_selector]
  7. AND:对函数选择器进行掩码操作
    • 栈:[clean_function_selector]
  8. DUP1:复制栈顶元素
    • 栈:[function_selector, function_selector]
  9. PUSH4 0x0dbe671f:压入函数a()的选择器
    • 栈:[0x0dbe671f, function_selector, function_selector]
  10. EQ:比较函数选择器是否匹配
    • 若匹配:栈:[1, function_selector]
    • 否则:栈:[0, function_selector]
  11. PUSH1 0x4e:压入跳转目标
    • 栈:[0x4e, 比较结果, function_selector]
  12. JUMPI:若比较结果为1,跳转到0x4e;否则继续执行

4. 其他函数选择器检查(label_003F)

003F 80 DUP1
0040 63 PUSH4 0x4df7e3d0
0045 14 EQ
0046 60 PUSH1 0x76
0048 57 *JUMPI

执行过程:

  1. DUP1:复制栈顶的函数选择器
    • 栈:[function_selector, function_selector]
  2. PUSH4 0x4df7e3d0:压入函数b()的选择器
    • 栈:[0x4df7e3d0, function_selector, function_selector]
  3. EQ:比较函数选择器是否匹配
    • 若匹配:栈:[1, function_selector]
    • 否则:栈:[0, function_selector]
  4. PUSH1 0x76:压入跳转目标
    • 栈:[0x76, 比较结果, function_selector]
  5. JUMPI:若比较结果为1,跳转到0x76;否则继续执行

5. 默认处理(label_0049)

0049 5B JUMPDEST
004A 60 PUSH1 0x00
004C 80 DUP1
004D FD *REVERT

执行过程:

  1. JUMPDEST:标记跳转目标
  2. PUSH1 0x00:压入0x00
    • 栈:[0x00]
  3. DUP1:复制栈顶元素
    • 栈:[0x00, 0x00]
  4. REVERT:回滚操作,表示没有匹配的函数选择器

四、关键点总结

  1. 函数选择器提取:通过CALLDATALOADDIV操作从调用数据中提取4字节的函数选择器。

  2. 函数分发逻辑

    • 合约首先检查调用数据长度是否小于4字节(函数选择器长度)
    • 然后依次比较提取的函数选择器与合约中函数的签名
    • 匹配则跳转到相应函数代码位置
    • 都不匹配则执行REVERT
  3. 内存布局

    • 初始时将0x80存储在内存0x40处,这是Solidity的内存管理机制
  4. 栈操作

    • 反汇编中大量使用栈操作指令(PUSH, DUP, SWAP等)
    • 理解栈的状态变化是分析反汇编的关键
  5. 跳转逻辑

    • JUMPI指令配合前面的比较指令实现条件跳转
    • 每个函数的入口点都有对应的跳转目标

五、下篇预告

在下篇中,我们将继续分析:

  1. 函数a()和b()的具体实现逻辑
  2. 内部函数self()的递归调用实现
  3. 完整的执行流程串联
  4. 存储变量c的访问方式
  5. 完整的反汇编到源代码的映射关系
智能合约反汇编教学文档(上篇) 一、前言 本教学文档基于智能合约安全系列文章中的反汇编上篇内容,旨在帮助学习者掌握智能合约反汇编的基础知识和分析方法。通过本教程,您将学习到: 智能合约反汇编的基本概念 如何使用Online Solidity Decompiler进行反汇编 如何解读反汇编后的指令序列 理解智能合约的执行流程 二、示例合约 合约源码 合约部署后的opcode 三、反汇编基础指令解析 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) 执行过程: 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) 执行过程: 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) 执行过程: 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) 执行过程: 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的访问方式 完整的反汇编到源代码的映射关系