Crosslink《CTF搶旗賽》解題全攻略(三)- randomIndex / EXTCODESIZE / Unexpected Ether

從實作及比賽中,一探智能合約可能存在的種種漏洞

Robin Pan 潘宣任
CryptoCow
8 min readDec 13, 2019

--

目前 CTF 網站已轉移至:

https://cypherpunks-core.github.io/cypherpunks-ctf/

本文假設讀者對智能合約的操作、語法有一定程度的瞭解,若對 Metamask 不熟悉或是不了解如何與 CTF 網站互動,建議先參考這篇引導,內容包括了安裝 Metamask 、連結至 CTF 網站,到一步步與合約互動,最終解開關卡 0 。

系列文目錄

 (一)關卡 1、2、3 - Underflow / XOR / Reentrancy
(二)關卡 4、5  - getStorageAt() / delegatecall
▶(三)關卡 6、7、8 - randomIndex / EXTCODESIZE / Unexpected Ether
(四)關卡 9
(五)關卡 10
(六)關卡 11
(七)關卡 12

本文目錄

關卡6 - PaperScissorsStone
關卡7 - Fomo4D
關卡8 - MorganStark

關卡 6 — PaperScissorsStone

關卡 6 — PaperScissorsStone.sol

初始狀態:winTimes = 0

目標:winTimes = 3

-

合約解析

這是一個猜拳的合約,如果能連續猜贏三次,就算過關。

合約中的隨機數是由以太坊鏈上某個區塊的哈希值計算出來的,雖然看似隨機,但其實每個人在發送交易前都能算得出來。

解題關鍵

randomIndex

guess(uint8) 中其實就是判斷傳入的 _guess 是不是等於 rps[randomIndex] +1 再 mod 3 ,若是,則算猜中一次。

其中一個作法是,預設要在某 A 區塊時發送交易,並先計算出以 A 區塊高度為基準的 randomIndex_guess,待 A 區塊到來時再發送交易。

然而,其實我們可以不用預先人工計算,只要再寫另一個 Attack 合約,透過這個合約再去呼叫 guess(uint8) 作攻擊,由於兩個 function 會在同一個區塊內執行,只要在 Attack 合約中複製一模一樣的程式,就能在攻擊前得到一樣的 randomIndex 並算出 _guess

透過這種方式,完全不用煩惱要在哪個時間點攻擊,也不需要知道 block.number 和 blockhash,只要呼叫 Attack 合約中的 attack() ,就能直接猜中答案。連續呼叫三次之後,即可過關!

關卡 7 — Fomo4D

關卡 7 — Fomo4D.sol

起始狀態:owner = “BoYu Chen”

目標:owner = “Bill Hsu”

-

合約解析

這關的重點在於讓 changeOwner() 執行成功即可過關。

changeOwner() 的第一行即要求必須符合兩個條件,才能繼續執行下去:

  1. msg.sender != tx.origin
  2. isContract(msg.sender) == false

** msg.sender 為當前調用的發起者,而 tx.origin 則是整筆交易(整條調用鏈)的發起者。

若用戶是從 EOA(Externally Owned Account)直接調用 changeOwner() ,由於沒有經過合約轉手,msg.sendertx.origin 會是相同的,不符合條件,交易會直接失敗。因此,為了通過這個限制,必須部署另一個合約,並在新合約中再回去調用 changeOwner() ,此時 msg.sender 會變成新合約的地址,而 tx.origin 則還是用戶的 EOA 地址。

完成第一個條件後,接著會調用 isContract(address) 檢查剛剛的 msg.sender 是不是一個合約,如果是的話會回傳 true ,這將導致條件不符,並使得交易再次失敗。

看到這邊,你可能會想:兩個條件相互矛盾,一個需要通過合約轉手調用,另一個又會驗證調用者必須不是合約,怎麼可能通過?

沒錯,兩個條件相互衝突就是為了讓你無法修改 owner 。然而,在放棄之前,我們應該先細看一下 isContract(address) 是如何判斷地址的合約與否…

解題關鍵

EXTCODESIZE

extcodesize(a) 是一段 EVM 的 opcode (operation code),用來取得位於地址 a 的程式碼大小。而這也就是 isContract(address) 判斷傳入地址是否為合約的標準 — 如果這個地址上的程式碼大小大於 0 ,那就是合約;反之,沒有程式碼,就是 EOA。

然而,使用 constructor 去調用時, extcodesize(a) 會回傳 0 !

這是由於合約執行 constructor 時程式碼尚未完成部署,區塊也尚未完成打包,對 extcodesize(a) 來說 a 地址還未包含程式碼,因此回傳 0 。

了解這些後,我們直接部署一個攻擊合約:

在合約的 constructor 中傳入被攻擊目標 Fomo4D 的地址,並呼叫其 changeOwner() 。在攻擊者合約部署完成後, Fomo4D 的 owner 也會跟著改變,過關!

關卡 8 — MorganStark

關卡 8 — MorganStark.sol

起始狀態: 合約餘額 = 0

目標:合約餘額 > 0

-

合約解析

這關的過關條件是讓合約地址的餘額大於 0 。

解析第三關時有介紹過,合約中的 function 要接收 ether 必須註明 payable ,因此我們直接找有 payable 的 function 開始看。

  1. constructor:有標明 payable ,但在其第一行又要求 msg.value == 0 ,使得我們無法帶著 ether 創建這個合約。
  2. collegeFund():這的確是個收款 function ,但唯有 owner 可以呼叫。而 owner 並不是我們,是幫我們部署這個合約 instance 的 Level 合約。

看完了合約中僅有的兩個 payable function ,似乎都無法讓我們捐款成功。由於沒有 payable fallback function,因此也無法透過一般交易把 ether 轉進合約。

然而,有兩種例外可以強制將 ether 轉進合約,不論目標合約是否有 payable function ,且不會執行任何程式碼(包含 fallback function)。

解題關鍵

Unexpected Ether

這兩種例外分別為:

  1. selfdestruct
  2. 預傳送 Ether

方法一

第一種方式是使用 selfdestruct function 。selfdestruct(target) 會將程式碼從合約地址移除,並將合約剩餘的所有 ether 轉到 target 地址,無論 target 是 EOA 或 contract ,都必須無條件接收。

為了使用這個方法,我們需要部署另一個含有 selfdestruct 的合約,並使它擁有大於 0 的 ether:

只要帶著 ether 呼叫 kill 並將 MorganStark 的地址傳入,在 selfdestruct 後就會發現 ether 確實進了 MorganStark 合約,第一種方式過關!

方法二

第二種方式預傳送 ether ,是在合約被部署前預測出合約即將被部署到的地址,並將 ether 預先轉入,由於地址上尚未有程式碼, ether 的轉送不會受到阻擋。

但合約的地址要如何預知呢?

其實合約的地址是確定性的(deterministic),可以經由合約部署者的地址(sender及它在部署當下的 nonce 計算出來 :

new_address = keccak256(rlp([sender, nonce]))[12:]

對 sender 和 nonce 做 RLP 編碼後,計算其 keccak256 的值,再取最後 20 bytes。下方是以 node.js 實作的一段 script:

擷取自 https://ethereum.stackexchange.com/a/46960

使用前,記得先將 nonce 和 sender 依需求修改後再執行。注意, nonce 的值是十六進制而非十進制。舉個例子:

  • address: 0x6a250e76b574b5b26b8953a5229b623f5e68eab3
  • nonce: 0x26 (十進制為 38)
  • result: 0x45bd566b3fa2e7b4268988d307cbb4a98ecce2d0

還要注意的一點是,在這個 CTF 中,關卡 instance 都是由 Level 合約部署的,並不是玩家本身的 EOA 。Level 合約的 nonce 可以透過 web3.eth.getTransactionCount 來取得。

計算出即將被部署的地址後,只要先將 ether 轉過去,再去獲取關卡 instance ,即可直接過關!

註:在以太坊 Constantinople 硬分叉時,加入了 CREATE2 opcode,與原本的 CREATE 一樣是用來部署合約的,不同的地方在於,CREATE2 讓你更有彈性的控制合約產生時的地址。細節可以參考 EIP-1014Constantinople硬分叉內容介紹,以及有趣的應用

如果您有其他有趣的解法,歡迎在下方一起留言討論!

--

--