iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
Security

從 Detect 到 Proof:智慧合約審計工具實戰系列 第 5

Day 5|Slither 的運作流程拆解

  • 分享至 

  • xImage
  •  

上一篇用 Slither 掃了 FlippazOne,找到沒有加 onlyOwnerownerWithdrawAllTo。這篇來看 Slither 在報出這個結果之前,做了哪些事。

FlippazOne 在做什麼

上一篇沒有仔細介紹 FlippazOne,這邊先補上。它是一個只拍賣一個 NFT 的合約:

  1. owner 呼叫 startAuction 開始拍賣,拍賣時間 24 小時。
  2. 任何人都可以用 bid() 出價,起標價 0.9 ETH,每次加價至少 0.25 ETH,出價的 ETH 會存在合約裡。
  3. 也可以用 buyNow() 直接買下。最高出價低於 14.75 ETH 時直購價是 15 ETH,超過的話是最高出價的兩倍。
  4. 時間到之後,任何人都可以呼叫 endAuction,NFT 會鑄造給最高出價者。
  5. 沒得標的人可以用 bidderWithdraw 拿回出價,或是由 refundBids 一次退給所有人。
  6. owner 用 ownerWithdraw 這幾個函式把得標金額領走。

FlippazOne 自己寫的函式裡,只有 startAuction 和四個修改設定的函式有加 onlyOwner,第 6 步的提款函式都沒有,所以任何人都能把錢領走。

整體流程

Slither 大概會經過這幾個步驟:

  1. 呼叫 solc 編譯合約,拿到 AST (Abstract Syntax Tree)。
  2. 從 AST 整理出合約的繼承關係,還有每個函式的 CFG (Control Flow Graph)。
  3. 把 CFG 裡的 Solidity 敘述轉成 Slither 自己的中介語言 SlithIR,再轉成 SSA (Static Single Assignment) 形式。
  4. 在 SlithIR 上做資料相依、污點分析 (taint analysis) 這些分析,再交給偵測器 (detector) 找問題,或交給 printer 把分析結果印出來。

這些中間產物都可以用 Slither 內建的 printer 印出來看,下面用 ownerWithdrawAllTo 一個一個看。

AST

AST 是編譯器解析原始碼後產生的樹狀結構,函式、變數、運算式都是樹上的節點。Slither 不會自己解析 Solidity,它用的是 solc 輸出的 AST,所以合約要先能用對應版本的編譯器編譯,Slither 才跑得起來。上一篇要先用 solc-select 切到 0.8.15 就是這個原因。

CFG

CFG 會把一個函式拆成好幾個節點,箭頭代表執行的順序。cfg printer 會把每個函式的 CFG 輸出成 .dot 檔:

slither FlippazOne.sol --print cfg

再用 Graphviz 轉成圖片:

dot -Tpng "FlippazOne.sol-FlippazOne-ownerWithdrawAllTo(address).dot" -o ownerWithdrawAllTo.png

https://ithelp.ithome.com.tw/upload/images/20260919/201608897aYmfCW51V.png

ownerWithdrawAllTo 的 CFG 很短,宣告 success 之後就直接轉帳,最後 require(success)

拿有加 onlyOwnerstartAuction 來比較:

https://ithelp.ithome.com.tw/upload/images/20260919/20160889u6Z9g3Utes.png

startAuction 進入點之後的第一個節點是 onlyOwner(),它的 SlithIR 是 MODIFIER_CALL, Ownable.onlyOwner()(),modifier 在 CFG 裡會單獨成一個節點。ownerWithdrawAllTo 沒有這個節點,轉帳之前也沒有檢查是誰在呼叫。

SlithIR

CFG 每個節點裡的 IRs 就是 SlithIR,也可以用 slithir printer 單獨印出來:

slither FlippazOne.sol --print slithir

ownerWithdrawAllTo 轉帳的那一行會變成:

TMP_547 = CONVERT this to address
TMP_548(uint256) = SOLIDITY_CALL balance(address)(TMP_547)
TUPLE_8(bool,bytes) = LOW_LEVEL_CALL, dest:toAddress, function:call, arguments:[''] value:TMP_548
success(bool)= UNPACK TUPLE_8 index: 0

toAddress.call{value: address(this).balance}("") 被拆成四行:把 this 轉成地址、查餘額、低階呼叫,最後從回傳值取出 success。中間的值存在 TMP_547TMP_548 這些暫存變數裡。

偵測器看的是這種拆開後的指令,不用處理 Solidity 的各種寫法。像 arbitrary-send-eth 會去找 LOW_LEVEL_CALL 這類會送出 ETH 的指令,再看 destvalue 從哪裡來。這裡的 desttoAddress,是函式的參數。

SSA

SSA 規定每個變數只能被賦值一次,同一個變數每被寫一次就會多一個新版本。用 slithir-ssa printer 可以看到:

slither FlippazOne.sol --print slithir-ssa
TUPLE_8(bool,bytes) = LOW_LEVEL_CALL, dest:toAddress_1, function:call, arguments:[''] value:TMP_548
success_1(bool)= UNPACK TUPLE_8 index: 0

toAddress 變成 toAddress_1success 變成 success_1。這個函式很短,每個變數都只有一個版本。如果函式比較長,一個變數被改了幾次,就會有 _1_2_3 幾個版本。

每個版本只會有一個來源,所以 Slither 可以往回查一個值是從哪裡來的。toAddress_1 是函式參數,參數是呼叫者傳進來的,所以轉帳的目的地是呼叫者決定的。arbitrary-send-eth 就是用這種資料流分析來判斷目的地。

整理

所以 arbitrary-send-eth 會報 ownerWithdrawAllTo,是因為三個條件都成立:

  1. SlithIR 裡有一個會送出 ETH 的 LOW_LEVEL_CALL
  2. 資料流分析發現 dest 來自函式參數,呼叫者可以控制。
  3. 函式沒有叫 onlyOwner 的 modifier,條件式和呼叫參數裡也沒有用到 msg.sender,所以被判定為沒有保護。

第 3 點是 Slither 原始碼裡 is_protected 的判斷方式,它看的是 modifier 的名字。如果 modifier 叫 onlyOwner 但裡面寫錯,函式還是會被當成有保護。

明日預告

將整理 Slither 的優缺點,並介紹評估這類工具常用的指標。

參考資料


上一篇
Day 4|第一次跑 Slither:FlippazOne 的提款函式
下一篇
Day 6|Slither 的評估指標與特點
系列文
從 Detect 到 Proof:智慧合約審計工具實戰9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言