name: multiversx-code-analysis description: MultiversX智能合约的全面代码分析工具包。涵盖差异审查(版本比较、升级安全性)、修复验证(验证补丁、回归测试)和变体分析(在代码库中查找类似漏洞)。适用于审查PR、验证安全补丁或查找漏洞变体。
MultiversX代码分析
用于分析MultiversX智能合约代码变更、验证修复和在代码库中查找漏洞变体的工具包。
何时使用
- 审查包含合约变更的拉取请求
- 部署前审计升级提案
- 验证安全补丁和漏洞修复
- 在代码库中查找类似漏洞
- 创建全面的漏洞报告
第1节:差异审查
分析两个版本MultiversX代码库之间的差异,重点关注变更的安全影响、存储布局兼容性和升级安全性。
1.1 可升级性检查(MultiversX特定)
存储布局兼容性
关键:存储布局变更可能损坏现有数据。
结构体字段顺序
// v1 - 原始结构体
#[derive(TopEncode, TopDecode, TypeAbi)]
pub struct UserData {
pub balance: BigUint, // 偏移量 0
pub last_claim: u64, // 偏移量 1
}
// v2 - 危险:字段重排序
pub struct UserData {
pub last_claim: u64, // 现在在偏移量 0 - 破坏现有数据
pub balance: BigUint, // 现在在偏移量 1 - 已损坏
}
// v2 - 安全:仅追加新字段
pub struct UserData {
pub balance: BigUint, // 偏移量 0 - 未变
pub last_claim: u64, // 偏移量 1 - 未变
pub new_field: bool, // 偏移量 2 - 新字段(安全)
}
存储映射器键变更
// v1
#[storage_mapper("user_balance")]
fn user_balance(&self, user: &ManagedAddress) -> SingleValueMapper<BigUint>;
// v2 - 危险:更改存储键
#[storage_mapper("userBalance")] // 不同键 = 新的空存储!
fn user_balance(&self, user: &ManagedAddress) -> SingleValueMapper<BigUint>;
升级时的初始化
关键:#[init] 在升级时不会调用。只有 #[upgrade] 会运行。
// v2 - 添加了新存储映射器
#[storage_mapper("newFeatureEnabled")]
fn new_feature_enabled(&self) -> SingleValueMapper<bool>;
// 错误:假设init运行
#[init]
fn init(&self) {
self.config().set(DefaultConfig::new());
self.new_feature_enabled().set(true); // 升级时永不运行!
}
// 正确:在升级中初始化
#[upgrade]
fn upgrade(&self) {
self.new_feature_enabled().set(true); // 正确初始化
}
破坏性变更清单
| 变更类型 | 风险 | 缓解措施 |
|---|---|---|
| 结构体字段重排序 | 关键 | 永远不要重排序,仅追加 |
| 存储键重命名 | 关键 | 保留旧键,迁移数据 |
| 新必需存储 | 高 | 在 #[upgrade] 中初始化 |
| 移除端点 | 中 | 确保没有外部依赖 |
| 更改端点签名 | 中 | 版本化API或保持兼容性 |
| 新验证规则 | 中 | 考虑现有状态有效性 |
1.2 回归分析
新功能影响
- 新功能是否破坏现有不变量?
- 是否引入了新的攻击向量?
- 气体成本是否显著变化?
删除代码分析
当代码被移除时,验证:
- 这是否是故意的安全修复?
- 是否移除了验证检查(潜在漏洞)?
- 是否有其他代码路径依赖于此?
// v1 - 有余额检查
fn withdraw(&self, amount: BigUint) {
require!(amount <= self.balance().get(), "余额不足");
// ... 提款逻辑
}
// v2 - 检查被移除 - 为什么?
fn withdraw(&self, amount: BigUint) {
// 缺少余额检查!这是故意的吗?
// ... 提款逻辑
}
修改逻辑分析
对于更改的代码,验证:
- 边界情况是否仍正确处理
- 错误消息是否适当更新
- 相关代码路径是否一致更新
1.3 审查工作流
步骤1:生成干净差异
# 在git标签/提交之间
git diff v1.0.0..v2.0.0 -- src/
# 忽略格式化更改
git diff -w v1.0.0..v2.0.0 -- src/
# 关注特定文件
git diff v1.0.0..v2.0.0 -- src/lib.rs
步骤2:分类变更
## 变更摘要
### 存储变更
- [ ] user_data结构体:添加了 `reward_multiplier` 字段(安全 - 追加)
- [ ] 新映射器:`feature_flags`(验证:在升级中初始化)
### 端点变更
- [ ] deposit():添加了代币验证(安全修复)
- [ ] withdraw():更改了气体计算(验证:无DoS向量)
### 移除代码
- [ ] legacy_claim():移除整个端点(验证:无外部调用者)
### 新代码
- [ ] batch_transfer():新端点(需要完整审查)
步骤3:跟踪数据流
对于每个更改的数据结构:
- 查找所有读取位置
- 查找所有写入位置
- 验证跨变更的一致性
步骤4:验证测试覆盖率
# 检查新代码路径是否被测试
sc-meta test
# 生成测试覆盖率报告
cargo tarpaulin --out Html
1.4 安全特定的差异检查
访问控制变更
// v1 - 仅所有者
#[only_owner]
#[endpoint]
fn sensitive_action(&self) { }
// v2 - 危险:移除访问控制
#[endpoint] // 现在公开!这是故意的吗?
fn sensitive_action(&self) { }
支付处理变更
// v1 - 已验证代币
#[payable]
fn deposit(&self) {
let payment = self.call_value().single();
require!(payment.token_identifier == self.accepted_token().get(), "错误的代币");
}
// v2 - 危险:移除验证
#[payable]
fn deposit(&self) {
let payment = self.call_value().single();
// 缺少代币验证!现在接受任何代币
}
算术变更
// v1 - 安全算术
let result = a.checked_add(&b).unwrap_or_else(|| sc_panic!("溢出"));
// v2 - 危险:移除溢出保护
let result = a + b; // 可能溢出!
1.5 差异审查输出模板
# 差异审查报告
**比较版本**:v1.0.0 → v2.0.0
**审查者**:[姓名]
**日期**:[日期]
## 摘要
[变更的概述段落]
## 关键发现
1. [发现及其严重性和建议]
## 存储兼容性
- [ ] 无结构体字段重排序
- [ ] 新映射器在 #[upgrade] 中初始化
- [ ] 存储键未变
## 破坏性变更
| 变更 | 影响 | 是否需要迁移 |
|--------|--------|-------------------|
| ... | ... | ... |
## 建议
1. [具体的可操作建议]
常见陷阱
- 假设init在升级时运行:始终检查
#[upgrade]函数 - 缺少存储迁移:重命名键丢失现有数据
- 移除验证:可能是故意的安全修复或意外的漏洞
- 更改数学精度:可能影响现有计算
- 修改访问控制:可能暴露敏感函数
第2节:修复验证
严格验证报告的漏洞是否已被消除,且未引入回归或新问题。
2.1 验证循环
步骤1:复现漏洞
创建演示漏洞的测试场景:
// scenarios/exploit_before_fix.scen.json
{
"name": "演示漏洞 - 修复前应失败",
"steps": [
{
"step": "scCall",
"comment": "攻击者利用漏洞",
"tx": {
"from": "address:attacker",
"to": "sc:vulnerable_contract",
"function": "vulnerable_endpoint",
"arguments": ["...exploit_payload..."],
"gasLimit": "5,000,000"
},
"expect": {
"status": "0",
"message": "*"
}
}
]
}
步骤2:应用修复
审查解决漏洞的代码修改。
步骤3:验证修复有效性
运行漏洞场景 — 现在必须失败(或行为正确):
# 漏洞场景现在应通过(漏洞被阻止)
sc-meta test --scenario scenarios/exploit_before_fix.scen.json
步骤4:运行回归套件
所有现有测试必须仍通过:
# 完整测试套件
sc-meta test
# 或使用cargo
cargo test
2.2 常见修复失败
部分修复
修复处理了一条路径但错过了变体:
// 漏洞:缺少金额验证
#[endpoint]
fn deposit(&self) {
let amount = self.call_value().egld();
// 没有检查金额 > 0
}
// 部分修复:只修复了deposit,未修复transfer
#[endpoint]
fn deposit(&self) {
let amount = self.call_value().egld();
require!(amount > 0, "金额必须为正"); // 已修复!
}
#[endpoint]
fn transfer(&self, amount: BigUint) {
// 仍缺少金额 > 0检查! <- 变体未修复
}
验证:使用变体分析(第3节)查找所有类似代码路径。
移动漏洞(修复创建新问题)
// 漏洞:重入
#[endpoint]
fn withdraw(&self) {
let balance = self.balance().get();
self.tx().to(&caller).egld(&balance).transfer(); // 状态更新前外部调用
self.balance().clear();
}
// 错误修复:防止重入但创建DoS
#[endpoint]
fn withdraw(&self) {
self.locked().set(true); // 添加锁定
let balance = self.balance().get();
self.tx().to(&caller).egld(&balance).transfer();
self.balance().clear();
// 缺少:self.locked().set(false); <- 锁定从未释放!
}
// 正确修复:检查-效果-交互模式
#[endpoint]
fn withdraw(&self) {
let balance = self.balance().get();
self.balance().clear(); // 外部调用前状态更新
self.tx().to(&caller).egld(&balance).transfer();
}
不完整验证
// 漏洞:整数溢出
let total = amount1 + amount2; // 可能溢出
// 不完整修复:检查一个但不检查两个
require!(amount1 < MAX_AMOUNT, "Amount1过大");
let total = amount1 + amount2; // 如果amount2大,仍会溢出!
// 正确修复:BigUint是任意精度(无溢出),但验证边界
let total = &amount1 + &amount2;
require!(total <= BigUint::from(MAX_ALLOWED), "金额超过最大值");
2.3 验证清单
代码审查
- [ ] 修复解决根本原因,而不仅仅是症状
- [ ] 所有类似模式的代码路径都被修复(变体分析)
- [ ] 修复未引入新漏洞
- [ ] 修复遵循MultiversX最佳实践
测试
- [ ] 创建漏洞场景,在漏洞代码上失败
- [ ] 漏洞场景在修复代码上通过(被阻止)
- [ ] 所有现有测试通过(无回归)
- [ ] 测试边界情况(边界值、空输入、最大值)
文档
- [ ] 修复提交清晰描述漏洞
- [ ] 测试场景记录攻击向量
- [ ] 任何行为变更被记录
2.4 测试场景模板
{
"name": "验证修复 [VULNERABILITY_ID]",
"comment": "此场景验证 [DESCRIPTION] 是否已正确修复",
"steps": [
{
"step": "setState",
"comment": "设置漏洞状态",
"accounts": {
"address:attacker": { "nonce": "0", "balance": "1000" },
"sc:contract": { "code": "file:output/contract.wasm" }
}
},
{
"step": "scCall",
"comment": "尝试利用 - 修复后应失败",
"tx": {
"from": "address:attacker",
"to": "sc:contract",
"function": "vulnerable_function",
"arguments": ["exploit_input"]
},
"expect": {
"status": "4",
"message": "str:预期错误消息"
}
},
{
"step": "checkState",
"comment": "验证状态未变(利用被阻止)",
"accounts": {
"sc:contract": {
"storage": {
"str:sensitive_value": "original_value"
}
}
}
}
]
}
2.5 验证报告模板
# 修复验证报告
## 漏洞参考
- **ID**:[CVE/内部ID]
- **严重性**:[关键/高/中/低]
- **描述**:[简要描述]
## 修复详情
- **提交**:[git提交哈希]
- **更改的文件**:[文件列表]
- **方法**:[修复方法描述]
## 验证结果
### 漏洞复现
- [ ] 创建漏洞场景:`scenarios/[名称].scen.json`
- [ ] 场景在漏洞代码上失败(提交:[哈希])
- [ ] 场景在修复代码上通过(提交:[哈希])
### 回归测试
- [ ] 所有现有测试通过
- [ ] 无新警告来自 `cargo clippy`
- [ ] 气体成本在可接受范围内
### 变体分析
- [ ] 使用变体分析搜索相似模式
- [ ] 所有变体已解决:[列表或“未找到”]
## 结论
**状态**:[已验证 / 需要改进 / 拒绝]
**备注**:[任何额外观察]
**签名**:[审查者姓名, 日期]
2.6 验证期间的危险信号
- 修复对于问题过于复杂
- 修复更改了无关代码
- 未添加针对具体漏洞的测试
- 修复依赖外部假设
- 气体成本显著增加
- 访问控制修改无明确理由
第3节:变体分析
通过系统地在代码库其他地方定位类似问题,倍增单个漏洞发现的价值。
3.1 变体分析过程
1. 找到初始漏洞 → 具体漏洞实例
2. 抽象模式 → 什么使其成为漏洞?
3. 创建搜索 → Grep/Semgrep查询
4. 找到变体 → 所有类似出现
5. 验证每个 → 确认真正正例
6. 报告所有 → 记录漏洞类别
3.2 常见MultiversX变体模式
模式:缺少支付验证
变体搜索:
# 查找所有可支付端点
grep -rn "#\[payable" src/
# 检查缺少代币验证
grep -A 30 "#\[payable" src/*.rs > payable_endpoints.txt
# 手动审查每个代币标识符验证
Semgrep规则:
rules:
- id: mvx-payable-no-token-check
patterns:
- pattern: |
#[payable]
$ANNOTATIONS
fn $FUNC(&self, $...PARAMS) {
$...BODY
}
- pattern-not: |
#[payable]
$ANNOTATIONS
fn $FUNC(&self, $...PARAMS) {
<... token_identifier ...>
}
模式:无界迭代
# 查找所有存储映射器的.iter()调用
grep -rn "\.iter()" src/
# 查找所有存储上的for循环
grep -rn "for.*in.*self\." src/
每个的清单:
- [ ] 迭代是否受限?
- [ ] 用户能否增长集合?
- [ ] 是否有分页?
模式:回调状态假设
# 查找所有回调
grep -rn "#\[callback\]" src/
# 检查正确处理结果
grep -A 20 "#\[callback\]" src/*.rs | grep -c "ManagedAsyncCallResult"
所有回调需要:
#[callback]
fn any_callback(&self, #[call_result] result: ManagedAsyncCallResult<T>) {
match result {
ManagedAsyncCallResult::Ok(_) => { /* 成功 */ },
ManagedAsyncCallResult::Err(_) => { /* 处理失败! */ }
}
}
模式:缺少访问控制
# 查找修改类似管理员存储的函数
grep -rn "admin\|owner\|config\|fee" src/ | grep "\.set("
# 交叉参考访问控制
grep -B 10 "admin.*\.set\|config.*\.set" src/*.rs | grep -v "only_owner"
模式:算术无检查
# 查找所有算术操作
grep -rn " + \| - \| \* " src/*.rs
# 排除测试文件和注释
grep -rn " + \| - \| \* " src/*.rs | grep -v "test\|//"
3.3 系统变体搜索
步骤1:特征化漏洞
回答这些问题:
- 什么是漏洞代码模式?
- 什么使其可被利用?
- 修复应是什么样?
步骤2:创建检测查询
基于Grep:
# 模式:[具体代码模式]
grep -rn "[pattern]" src/
# 负面模式(应该存在但不存在)
grep -L "[expected_pattern]" src/*.rs
基于Semgrep:
rules:
- id: variant-pattern
patterns:
- pattern: <vulnerable pattern>
- pattern-not: <fixed pattern>
步骤3:分类结果
| 结果 | 分类 | 行动 |
|---|---|---|
| 明确易受攻击 | 真正正例 | 报告 |
| 需要上下文 | 调查 | 手动审查 |
| 有缓解措施 | 假正例 | 记录原因 |
| 不同模式 | 不是变体 | 跳过 |
步骤4:记录发现
## 变体分析:[漏洞类别名称]
### 初始发现
- 位置:[文件:行]
- 描述:[错误之处]
### 模式描述
[抽象描述什么使其成为漏洞]
### 搜索方法
```bash
[grep/semgrep命令使用]
找到的变体
| 位置 | 状态 | 备注 |
|---|---|---|
| file1.rs:23 | 确认 | 相同模式 |
| file2.rs:45 | 确认 | 轻微变体 |
| file3.rs:67 | 假正例 | 其他地方有验证 |
修复
[如何修复所有实例]
### 3.4 未来预防的自动化
#### 转换为CI/CD检查
```yaml
# .github/workflows/security.yml
- name: 检查漏洞模式
run: |
# 运行semgrep与自定义规则
semgrep --config rules/mvx-security.yaml src/
# 基于Grep的检查
if grep -rn "unsafe_pattern" src/; then
echo "找到潜在漏洞"
exit 1
fi
3.5 变体分析清单
找到任何漏洞后:
- [ ] 抽象模式(什么使其成为漏洞?)
- [ ] 创建搜索查询(grep, semgrep)
- [ ] 搜索整个代码库
- [ ] 分类每个结果(TP/FP/需调查)
- [ ] 验证真正正例可被利用
- [ ] 记录所有变体
- [ ] 为CI/CD创建预防规则
- [ ] 建议修复所有实例
3.6 常见变体类别
输入验证变体
- 一个端点缺少 → 检查所有端点
- 一个参数缺少 → 检查所有参数
访问控制变体
- 一个管理员函数缺少 → 检查所有管理员函数
- 不一致的角色检查 → 审计整个角色系统
状态管理变体
- 一个函数中的重入 → 检查所有外部调用
- 缺少回调处理 → 检查所有回调
算术变体
- 一个计算中的溢出 → 检查所有数学操作
- 一个公式中的精度损失 → 检查所有除法操作
3.7 报告多个变体
整合报告
# 漏洞类别:[名称]
## 摘要
在代码库中找到 [N] 个 [漏洞描述] 实例。
## 根本原因
[为什么此模式易受攻击]
## 实例
### 实例1 (file1.rs:23)
[详情]
### 实例2 (file2.rs:45)
[详情]
## 推荐修复
[通用修复模式]
## 预防
[如何预防此类漏洞未来出现]
严重性聚合
| 个体严重性 | 计数 | 聚合严重性 |
|---|---|---|
| 关键 | 3+ | 关键 |
| 高 | 5+ | 关键 |
| 中 | 10+ | 高 |
| 低 | 任意 | 低 |
3.8 示例:完整变体分析
初始漏洞:stake()中缺少金额验证
// 在stake.rs:45找到
#[payable("EGLD")]
fn stake(&self) {
let payment = self.call_value().egld();
// 漏洞:没有检查金额 > 0
self.staked().update(|s| *s += payment.amount.as_big_uint());
}
模式:可支付端点缺少金额 > 0检查
注意:使用SDK v0.64+ Payment类型时,
amount是NonZeroBigUint—> 0检查对于类型化支付是冗余的。以下模式适用于旧SDK版本或原始call_value()使用。
搜索:
grep -rn "#\[payable" src/ | cut -d: -f1 | sort -u | while read file; do
echo "=== $file ==="
grep -A 30 "#\[payable" "$file" | head -40
done > payable_review.txt
找到的变体:
stake.rs:45- stake() - 确认stake.rs:78- add_stake() - 确认rewards.rs:23- deposit_rewards() - 确认fees.rs:12- pay_fee() - 假正例(第15行有检查)
应用到所有的修复:
#[payable("EGLD")]
fn stake(&self) {
let payment = self.call_value().egld();
require!(payment.amount.as_big_uint() > 0, "金额必须为正");
self.staked().update(|s| *s += payment.amount.as_big_uint());
}
创建的CI规则:rules/mvx-amount-validation.yaml