警惕科学家钓鱼合约
备注
时间:2024 年 6 月 30 日
作者:[windke]
正文
有个朋友给我发了一个合约 https://etherscan.io/address/0x8876a8cf6e142a0aeb834b824e97870111bb7da1 说有套利可能,可以来研究。我发现这的确是一个有明显漏洞的合约,里面还有 15 ETH 的资金。但仔细研究过后发现这是专门用来钓鱼的合约,想要去攻击的智能合约新手很容易上当。
代码分析
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
|
pragma solidity 0.7.6;
contract smart_bank { function Deposit(uint _unlockTime) public payable { Holder storage acc = Accounts[msg.sender]; acc.balance += msg.value; acc.unlockTime = _unlockTime > block.timestamp ? _unlockTime : block.timestamp; LogFile.AddMessage(msg.sender, msg.value, "Put"); }
function Collect(uint _am) public payable { Holder storage acc = Accounts[msg.sender]; if (acc.balance > MinSum && acc.balance >= _am && block.timestamp > acc.unlockTime) { (bool success, ) = msg.sender.call{value: _am}(""); if (success) { acc.balance -= _am; LogFile.AddMessage(msg.sender, _am, "Collect"); } } }
struct Holder { uint unlockTime; uint balance; }
mapping(address => Holder) public Accounts;
Log LogFile;
uint public MinSum = 1 ether;
constructor(address log) { LogFile = Log(log); }
fallback() external payable { Deposit(0); }
receive() external payable { Deposit(0); } }
contract Log { event Message(address indexed Sender, string Data, uint Val, uint Time);
function AddMessage(address _adr, uint _val, string memory _data) external { emit Message(_adr, _data, _val, block.timestamp); } }
|
这个合约有两个明显的漏洞:
- 在
msg.sender.call 之后调用 acc.balance -= _am, 而且没有重入锁,可以使用重入攻击。
- 在 0.7.6 的 solidity 版本之下使用
acc.balance -= _am,可以使用整数溢出攻击。
但如果你想当然的去用上面的手段攻击合约,就上合约部署者的当了。
这个合约有一个奇怪的方法 LogFile.AddMessage,虽然在下面给出了 contract Log 的源代码,看起来没有问题,但在合约初始化是没有使用 LogFile = new Log() 而是使用了 LogFile = Log(log),这导致了 LogFile.AddMessage 函数调用的代码其实在其他地址。
代码溯源
通过分析合约构造交易的 https://etherscan.io/tx/0x9cb838f7b2eb28951fc2d0b560f8bb98ce32b1789b00735c89385fd9740c96e2 的 data,可以找到 logFile 的真实合约地址是 https://etherscan.io/address/0x441f6fb6e9506082625fe0b973025ef65badf584 ,而这个合约并没有开源。使用反编译工具可以一窥究竟 https://etherscan.io/bytecode-decompiler?a=0x441f6fb6e9506082625fe0b973025ef65badf584
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
|
def storage: stor0 is addr at storage 0 stor1 is addr at storage 1 stor2 is addr at storage 2
def _fallback() payable: revert
def unknown4b906714(addr _param1, uint256 _param2, array _param3) payable: if stor1 != tx.origin: require tx.origin == stor2 call _param1 with: value _param2 wei gas gas_remaining wei args _param3[all] require ext_call.success
def AddMessage(address _adr, uint256 _val, string _data): log 0xb7206ff2: Array(len=_data.length, data=_data[all]), _val, block.timestamp, _adr if caller == stor0: if stor1 != tx.origin: if stor2 != tx.origin: if _val > 0: require 0 < _data.length require Mask(8, 248, cd[(_data + 36)]) != 'C'
|
可以看到 AddMessage 函数的真实逻辑是:
- 检查 caller 是否为 stor0,如果是则进入下一步
- 检查 tx.origin 是否是 stor1 或者 stor2,如果不是则进入下一步
- 检查 _val 是否大于 0,如果是则进入下一步
- _data 长度必须大于 0,并且首字母不能是
C
所以当别人调用 Deposit 往合约里存 ETH 的时候可以正常执行,但调用 Collect 向合约取 ETH 的时候会触发首字母不能是 C 的检查,从而无法执行。
总结
科学家的收入虽然吸引人,但经验不足的新手很容易掉坑里。最近也出现了一些专门钓鱼科学家的合约,大家要谨慎检查。