AmirhosseinHonardoust/Bytecode-Truth-Not-Source
GitHub: AmirhosseinHonardoust/Bytecode-Truth-Not-Source
该项目深入探讨了为何 EVM 字节码才是智能合约安全的真相,揭示了仅依赖已验证 Solidity 源代码所潜藏的巨大风险。
Stars: 15 | Forks: 0
# 为什么字节码比源代码更诚实
### 深入剖析智能合约安全中的验证误区
## 摘要
大多数 Web3 用户,甚至许多开发者,都习惯于信任 Etherscan 上的绿色对勾:
这个对勾制造了一种强大的错觉:
如果源代码可见,合约一定是安全的。
现实更加残酷。
区块链并**不**运行 Solidity。它运行的是 **EVM 字节码**。
字节码是验证者和矿工实际执行的**唯一**内容。
本文将深入探讨:
* Solidity 如何变成字节码
* 为什么源代码 ≠ 行为
* 骗子如何滥用验证
* 如何像以字节码为先的审计员一样思考
* 对比、检查和分析字节码的实用方法
* 为什么安全的未来是**基于字节码**的,而不是基于源代码的
## 目录
1. [思维模型重置:链上实际运行的是什么](#1-mental-model-reset)
2. [从 Solidity 到字节码:编译流水线](#2-compilation-pipeline)
3. [为什么源代码和行为可能不一致](#3-divergence)
4. [“已验证合约”真正意味着什么(以及不意味着什么)](#4-verified)
5. [验证剧场:骗子如何利用信任](#5-theater)
6. [案例研究:仅在字节码中可见的骗局模式](#6-case-studies)
7. [操作码与 EVM 深入剖析:像阅读地图一样阅读字节码](#7-evm-deep-dive)
8. [实用审计员工作流:从源代码到字节码真相](#8-workflow)
9. [工具:如何在实践中检查字节码](#9-tooling)
10. [超越手动审计:字节码图、聚类和机器学习](#10-ml)
11. [威胁模型:攻击者能用字节码技巧做什么](#11-threat-model)
12. [结论:信任机器,而不是营销](#12-conclusion)
# 1. 思维模型重置:链上实际运行的是什么
让我们从一个极其简单的句子开始:
当你:
* 打开 Etherscan
* 浏览 Solidity 代码
* 阅读“已验证”的合约源代码
你**并没有**在阅读机器执行的内容。你是在阅读一个关于合约的、人类友好的故事。
真正的合约是:
* 一系列字节
* 由以太坊虚拟机(EVM)解释
* 表示为 `PUSH1`、`SSTORE`、`JUMPI`、`CALL`、`DELEGATECALL` 等操作码
这就是**执行真相**。
你可以这样理解:
```
Solidity → Compiler → EVM Bytecode → Execution
(marketing) (translator) (real machine language)
```
Solidity 是可选的。
字节码不是。
合约甚至可以在**完全没有 Solidity 源代码**的情况下部署。EVM 并不在乎。
# 2. 从 Solidity 到字节码:编译流水线
在讨论骗局之前,我们需要了解 **Solidity 如何变成字节码**。
典型的流水线(简化版):
```
Solidity Source
↓
Parser & Semantic Analysis
↓
Intermediate Representation (IR)
↓
Yul / EVM-IR
↓
Optimizer Passes
↓
EVM Bytecode
```
### 2.1. 解析与语义分析
编译器:
* 解析 Solidity 语法
* 构建抽象语法树(AST)
* 检查类型、变量使用、可见性、继承等。
在这个层面上,代码看起来仍然像你的 Solidity 源代码。
### 2.2. 中间表示(IR)
编译器将 AST 转换为更低级的 IR。
IR 表达:
* 更显式的操作
* 控制流(分支、循环)
* 存储/内存操作
* 作为跳转和栈操作的函数调用
这更接近汇编语言。
### 2.3. 优化
这是事情变得有趣的地方。
优化器执行如下转换:
* **常量折叠**
uint x = 1 + 2;
变为:
PUSH1 0x03
* **死代码消除**
永远不会执行的代码被移除。
* **公共子表达式消除**
* **分支简化**
* **内联函数**
* **移除冗余检查**
这些转换可以完全改变你的逻辑结构。
### 2.4. 字节码生成
最后,编译器生成 EVM 字节码:
* 操作码
* 跳转目标
* 推入常量
* 函数调度表
* 构造函数代码与运行时代码
现在你拥有了真正的合约:存储在合约地址处的字节码。
# 3. 为什么源代码和行为可能不一致
既然你了解了流水线,这里的关键问题是:
这种“可观察行为”是很微妙的。
人类通常推理的是:
* 简单的控制流(`if`、`for`、`require`)
* 粗略的变量赋值
* 修饰符
但编译器推理的是:
* 精确的逻辑等价性
* Gas 成本
* 存储布局
* 跨函数内联
* 跨合约交互
这开启了一些分歧点。
## 3.1. 优化器可以重构你的代码
考虑一个函数:
```
function fee() public view returns (uint256) {
if (msg.sender == owner) {
return 2;
} else {
return 2;
}
}
```
对人类来说,这看起来可疑但无害。
对编译器来说,两个分支是相同的,所以**条件被擦除了**:
```
PUSH1 0x02
RETURN
```
现在想象更复杂的模式,其中:
* 检查被移动
* 分支被合并
* 失败条件被降低为单个 `REVERT` 路径
最终的控制流图可能是**非显而易见的**。
## 3.2. 内联汇编和 Yul
Solidity 允许开发者降级到 **assembly**:
```
assembly {
// raw EVM
sstore(0xFA, 0x1)
}
```
这直接注入到字节码中,通常**没有高级提示**来说明它的作用。
更糟糕的是:
* 恶意逻辑可以隐藏在汇编深处
* 汇编可以以状态变量中未清晰反映的方式操作存储
* 反编译器难以处理复杂的内联汇编
从源代码的角度来看:
从字节码的角度来看:
## 3.3. 不可达与隐藏逻辑
编译器可能会留下一些不可达的字节码模式,特别是如果是手动注入的。
骗子可以:
* 将恶意逻辑放置在源代码层面看起来不可能但稍后更改某些存储值后可达的分支后面。
* 编码仅在特定状态设置后才出现在正常执行流中的所有者后门。
这在源代码和验证视图中几乎是不可见的。
# 4. “已验证合约”真正意味着什么(以及不意味着什么)
当合约在 Etherscan(或 BscScan 等)上“被验证”时,通常意味着:
1. 开发者上传了源代码。
2. 他们选择了编译器版本 + 优化设置。
3. 浏览器编译该源代码。
4. 生成的字节码与链上字节码匹配(针对运行时部分)。
就是这样。
### 4.1. 它**确实保证**了什么
* 上传的源代码与运行时字节码是**一致的**。
* 至少有一条从源代码 → 编译 → 字节码的路径是匹配的。
### 4.2. 它**不保证**什么
* 源代码是**注释良好**或**可读的**。
* 没有**隐藏函数**是你没有滚动到的。
* 字节码不包含通过以下方式插入的意外行为:
* 内联汇编
* 库
* 代理模式
* **构造函数**或**初始化逻辑**在运行时代码部署之前没有做有害的事情。
* **部署者没有**用误导性的变量名撒谎(`isRenounced` 实际上并不控制你以为的内容)。
最重要的是:
因为你没有审计**实际的机器行为**:字节码。
# 5. 验证剧场:骗子如何利用信任
骗子理解用户的心理学:
* 绿色对勾 = 安全
* 公开源代码 = 透明
* 放弃所有权 = 无控制权
所以他们围绕这些元素构建**剧场**。
让我们看看常见的模式。
## 5.1. 看起来安全的源代码,恶意的字节码
骗子可以:
1. 开发合约的恶意版本。
2. 编译它。部署该字节码。
3. 上传**略有不同**的源代码,这些代码仍然编译成相同的字节码,但看起来无害。
怎么做?
* 使用**不起眼的变量名**。
* 在汇编中隐藏条件。
* 将逻辑移动到具有混淆名称的辅助函数中。
* 使用复杂的继承 / 修饰符来掩盖真正的流程。
用户甚至初级审计员看到:
但字节码揭示:
* 动态的、所有者控制的税费
* 在特定条件下阻止卖出的钩子
* 启用后门的存储槽
## 5.2. 假的“放弃所有权”
DeFi 中的常见梗:
在源代码中:
```
function renounceOwnership() public onlyOwner {
owner = address(0);
}
```
在 UI 中,用户看到:
但在字节码中,我们可能会看到:
```
PUSH20 0xREAL_OWNER
SSTORE 0xFA ; writes to some secret control slot
```
或者像这样的模式:
* `owner` 变量仅用于显示
* 真正的控制权在另一个从未设置为零的槽中
源代码被编写为制造失去控制的**错觉**。
字节码揭示:
* 仍然存在一个活跃的权限
* 或者存在一个特殊的调用者可以做没人预料到的事情
## 5.3. 后来变成 100% 的“税费”
源代码:
```
uint256 public buyTax = 3;
uint256 public sellTax = 3;
function setTaxes(uint256 _buy, uint256 _sell) external onlyOwner {
require(_buy <= 10 && _sell <= 10, "Too high");
buyTax = _buy;
sellTax = _sell;
}
```
看起来合理。
但可能存在:
* 在转账逻辑中使用的隐藏存储
* 汇编中单独的“紧急模式”开关:
```
assembly {
if eq(caller(), sload(0xFA)) {
sstore(0xFB, 0x63) // 99 decimal
}
}
```
所以在一段时间后,开发者设置一个秘密标志并且:
* 每次卖出都被征收 99% 的税
* 代币一夜之间变成跑路骗局
只有通过**跟踪字节码路径**,而不是通过阅读漂亮的公共函数,你才能看到真相。
# 6. 案例研究:仅在字节码中可见的骗局模式
让我们回顾几类**字节码分析至关重要**的骗局。
## 6.1. 通过 Revert / INVALID 的蜜罐
蜜罐模式:买入有效,卖出失败。
源代码看起来没问题:
```
function sell(uint256 amount) external {
_transfer(msg.sender, address(this), amount);
// maybe some liquidity logic
}
```
但在字节码中,在 SELL 路径下:
```
CALLVALUE
PUSH1 0x00
EQ
JUMPI label_safe
INVALID ; or REVERT with no message
label_safe:
...
```
所以在现实条件下,该路径总是导致 `INVALID` / `REVERT`。
用户看到:
* 正常的 `sell` 函数
* 正常的转账代码
字节码揭示:
* 每次卖出尝试都会导致**强制回滚**
## 6.2. 带有隐藏存储的动态费用开关
模式:
* 买入/卖出费用似乎是常量或有界的低值
* 汇编设置另一个控制实际费用的存储槽
示例(简化版):
```
; somewhere in transfer logic
SLOAD 0x10 ; read public sellTax
SLOAD 0xFA ; hidden "multiplier" or flag
MUL ; effective fee = sellTax * hiddenFlag
...
```
如果 `hiddenFlag` 最初为 1 → 安全。
后来开发者设置 `hiddenFlag = 30` → 有效税费飙升。
在源代码中,我们只看到 `sellTax` 是一个低数字。
隐藏乘数仅存在于存储 + 字节码中。
## 6.3. 代理 + DELEGATECALL 滥用
代理是合法的。它们也打开了一扇核大门。
模式:
```
DELEGATECALL
```
这意味着:
恶意设置可能会:
1. 部署一个看起来安全的代理(已验证代码)。
2. 初始实现合约是无害的。
3. 后来,开发者将实现**升级**为恶意合约。
4. 代理(未更改)现在通过 `DELEGATECALL` 路由到骗局逻辑。
验证代理源代码救不了你。
你必须:
* 检查**实现**合约
* 跟踪升级路径
* 分析 `DELEGATECALL` 是如何使用的
字节码对于理解**真正的执行路径**至关重要。
# 7. 操作码与 EVM 深入剖析:像阅读地图一样阅读字节码
要信任字节码,你需要对 EVM 有一个最小的思维模型。
### 7.1. 作为栈机器的 EVM
EVM 是一个**基于栈的虚拟机**。
* 没有寄存器
* 一切都在栈上进行
* 操作码推入和弹出值
示例:
```
PUSH1 0x02
PUSH1 0x03
ADD
```
栈演变:
* 开始:`[]`
* `PUSH1 0x02` → `[2]`
* `PUSH1 0x03` → `[2, 3]`
* `ADD` → `[5]`
这就是你的 Solidity `2 + 3` 变成具体操作的方式。
### 7.2. 控制流:JUMP & JUMPI
高级的 `if`、`for`、`while` 变为:
```
PUSH2 label_true
JUMPI
...
JUMPDEST label_true
...
```
`JUMPI` 是条件跳转:
* 条件在栈顶
* 如果非零 → 跳转到标签
* 否则 → 掉落
这是蜜罐、后门和动态控制经常隐藏的地方:
* “仅所有者”约束
* 切换行为的“模式标志”
* “税费开关”逻辑
你检查:
* 在 `JUMPI` 之前什么进入了栈
* `JUMPDEST` 通向哪里
* 该条件后面是什么代码
### 7.3. 存储:SLOAD & SSTORE
Solidity 中的状态:
```
uint256 public foo;
mapping(address => uint256) public balanceOf;
```
变为存储槽在字节码中:
```
SSTORE ; write to storage
SLOAD ; read from storage
```
要观察的模式:
* 对不寻常槽的写入(`0xFA`、`0xFB` 等)
* 围绕费用或权限检查的存储读/改/写序列
* 修改执行路径的隐藏状态切换
### 7.4. 危险的操作码
一些操作码是**中性的**,一些在声称是“简单代币”的合约中是**可疑的**。
示例:
| 操作码 | 为什么重要 |
| -------------- | ------------------------------------------ |
| `DELEGATECALL` | 使用本地存储执行外部代码 |
| `CALLCODE` | delegatecall 的旧版本 |
| `SELFDESTRUCT` | 可以销毁合约(通常是跑路骗局) |
| `CALLER` | 用于基于调用者的条件 |
| `ORIGIN` | `tx.origin` 问题(钓鱼风险) |
| `INVALID` | 强制停止 |
| `REVERT` | 受控的回滚(蜜罐) |
在**标准 ERC-20 代币**中,大量使用这些是一个危险信号。
# 8. 实用审计员工作流:从源代码到字节码真相
这是一个**以字节码为先的审计员**的思考方式。
## 8.1. 第 1 步,识别合约与编译器
* 在 Etherscan(或链浏览器)上找到合约。
* 注意:
* 编译器版本(`0.8.19`)
* 优化设置(`200 runs`)
* 验证源代码是**完整的**,没有被截断。
## 8.2. 第 2 步,阅读源代码,但不要信任它
做**第一遍**:
* 了解基本逻辑(代币、质押、NFT 等)
* 识别角色:`owner`、`admin`、`feeWallet` 等。
* 寻找:
* `onlyOwner` 门控
* 税费设置
* 反机器人 / 黑名单功能
* 流动性函数
* 放弃模式
但不断重复告诉自己:
## 8.3. 第 3 步,下载 / 检索字节码
使用浏览器:
* 查看“合约字节码”
* 或使用 Web3 或 RPC 调用 `eth_getCode`。
现在你拥有:
* 原始运行时字节码
* 可选地,构造函数字节码(初始化代码)
## 8.4. 第 4 步,反汇编
使用反汇编器:
* 浏览器内置反汇编
* `evm`、`hevm`、`evm-dasm`、`ethervm.io` 等工具
你得到一个操作码视图:
```
60003560e01c8063a9059cbb1461002f57806370a0823114610055575b600080fd5b...
```
变为:
```
PUSH1 0x00
CALLDATALOAD
PUSH1 0xe0
SHR
...
```
## 8.5. 第 5 步,映射关键函数
识别关键入口点:
* `transfer`
* `transferFrom`
* `approve`
* `addLiquidity`
* `setFees`
* `renounceOwnership`
* `excludeFromFee`
* 等等。
然后:
* 找到对应的操作码块
* 验证**源代码承诺的**是字节码实际做的。
示例:
源代码:
```
require(msg.sender == owner, "Not owner");
```
字节码:
```
CALLER
PUSH20 0xDEAD...BEEF
EQ
ISZERO
JUMPI revertLabel
```
这是一致的。
但如果你看到:
```
CALLER
SLOAD 0xFA
EQ
ISZERO
JUMPI revertLabel
```
并且 `SLOAD 0xFA` 是某个*其他*地址,你现在怀疑:
* 有一个秘密所有者
* 或者所有者可以通过另一个路径更改
## 8.6. 第 6 步,寻找控制标志
你想识别:
* 充当模式开关的存储槽
* 检查这些槽的条件
* 设置标志时变为活动的代码路径
模式:
```
SLOAD 0xFB
ISZERO
JUMPI safeLabel
; malicious logic
```
含义:
* 最初槽 `0xFB = 0` → 跳过恶意部分
* 后来开发者设置槽 `0xFB = 1` → 恶意部分激活
如果源代码从未提及 `0xFB`,这是一个红色警报。
## 8.7. 第 7 步,比较行为,而不仅仅是字节相等
浏览器说这些是不够的:
你想检查:
* 每个**关键函数**的行为是否如声明的那样?
* 有没有额外的控制流边?
* 有没有奇怪的回滚模式?
* 不受信任的外部调用(`CALL`、`DELEGATECALL`)?
如果源代码说:
但字节码允许设置一些隐藏乘数,那么:
# 9. 工具:如何在实践中检查字节码
你不需要成为十六进制巫师。你需要一个**最小工具链**:
## 9.1. 浏览器(基础)
* Etherscan 的反汇编器
* BscScan / Arbiscan / Basescan 等效项
步骤:
1. 转到合约。
2. 点击“代码”选项卡。
3. 查看“字节码” → “反汇编”。
你会看到 EVM 操作码。
## 9.2. 本地工具
* `evm`(来自 go-ethereum)
* `hevm`(DappTools)
* `ethervm.io`(在线)
* `panoramix` 或其他反编译器
* `mythril`、`slither`(主要是源代码级别但有用)
你可以:
* 反汇编
* 运行符号执行
* 探索控制流
* 找到危险路径
## 9.3. IDE 集成
* 带有 Solidity 插件的 VSCode
* 解析已部署字节码的 Hardhat/Foundry 任务
* 获取 `eth_getCode` 并作为工作流的一部分反汇编的脚本
你甚至可以编写脚本来:
* 获取地址的字节码
* 解码它
* 在可疑上下文中搜索 `DELEGATECALL`、`SELFDESTRUCT`、`SSTORE` 等操作码。
# 10. 超越手动审计:字节码图、聚类和机器学习
手动审计无法扩展到数千个合约。
这是你的数据科学技能发挥作用的地方。
## 10.1. 字节码作为序列
你可以将字节码视为:
* 操作码序列
* `PUSH1, PUSH1, ADD, SSTORE, JUMP…`
嵌入或特征化它:
* n-gram 频率(像语言模型)
* 操作码直方图
* 双元组 / 三元组
* 序列模型(RNN、Transformers)
用它来:
* 聚类相似的合约
* 检测骗局家族
* 构建分类器:安全与可疑。
## 10.2. 控制流图(CFGs)
你可以将字节码变成**图**:
* 节点 = 基本块
* 边 = JUMP / JUMPI 链接
然后计算:
* 中心性度量
* 结构模式
* 异常分数
骗局合约通常显示:
* 奇怪的分支
* 陷阱般的路径
* 非标准的控制形状
## 10.3. 与部署者图结合
字节码特征 + 部署者图 = 杀手级组合:
* 哪些部署者部署了相似的字节码模式?
* 一些部署者是否对多个骗局字节码集群负责?
* 恶意部署者的跨链聚类
这构建了一个完整的**威胁情报系统**。
# 11. 威胁模型:攻击者能用字节码技巧做什么
让我们总结攻击者在利用源代码与字节码差距时的能力。
### 11.1. 误导用户
* 展示“漂亮”的源代码
* 在以下内容中隐藏真实行为:
* 汇编
* 存储标志
* DELEGATECALL
* 不可达路径
### 11.2. 规避表面审计
许多检查是:
* 基于 Solidity 语法的模式
* 依赖于仅阅读关键函数
但恶意行为可能是:
* 分散在函数中
* 依赖于调用序列
* 仅在特定时间/块/所有者操作后触发
只有**字节码级审计**或**动态执行**才能捕获这些。
### 11.3. 武器化可升级性
一系列可能性:
* 带有看起来安全的升级功能的代理
* 管理员密钥隐藏在不明显的变量中
* `DELEGATECALL` 目标地址可通过单独的函数修改
攻击模式:
1. 启动安全的实现。
2. 吸引流动性和用户。
3. 在选定时间升级到恶意实现。
4. 执行跑路 / 蜜罐 / 排干。
# 12. 结论:信任机器,而不是营销
让我们以核心论点结束:
因为:
* 字节码是 EVM 实际运行的内容。
* 源代码是一种解释,有时诚实,有时具有欺骗性。
* 验证仅证明上传的源代码*可以*编译成链上字节码,而不是合约是安全的。
* 优化、汇编和代理在人类阅读的内容和机器执行的内容之间引入了差距。
如果你关心安全:
* 阅读源代码。
* 但始终验证字节码行为。
* 使用反汇编器和工具检查关键路径。
* 注意 `DELEGATECALL`、`SELFDESTRUCT`、`SSTORE`、`JUMPI` 等操作码。
* 在没有执行证明的情况下,不要信任“放弃所有权”、“safu”、“自动流动性”等营销词汇。
智能合约安全用一句话概括:
标签:Etherscan验证, EVM字节码, Solidity, URL提取, Web3安全, 代理合约, 代码验证, 区块链安全, 反欺诈, 反编译, 合约混淆, 底层汇编, 智能合约安全, 智能合约审计, 智能合约漏洞, 服务器监控, 浏览器安全, 编译器优化, 自动回退, 链上行为分析