本任務內容是關於 StarkNet 合約的 calldata。calldata 是指函數調用時傳遞給函數的數據,在 StarkNet 中,傳入的數據可能是各種類型,但最終都會轉換為多個 felt252 組成的 snapshot,具體見call_contract_syscall。
有 2 個合約,PortalSpell 合約中的 cast 接受的是 1 個Array<PortalData>
,DrunkenMage 合約中的 cast 是傳入 2 個Array<felt252>
。需要你傳入 2 個 Array 構成的 calldata,和Array<PortalData>
前 2 個 PortalData 是相同的(驗證見合約代碼)。
struct PortalData {
location: felt252,
details: Array<felt252>
}
// 預期的函數簽名
cast(portal_data: Array<PortalData>)
// Mage 的函數簽名
cast(origin: Array<felt252>, destination: Array<felt252>)
上面的 cast 會通過 dispatcher 跨合約調用,無需驗證參數是否一樣。
思路#
首先,先需要了解 StarkNet 合約是怎麼解析 calldata 的,任務的 walkthough 裡說得很清楚了。
編碼的目標是 Array<PortalData>
,值是
[ 0: { location: 'TAVERN', details: [ 'OPEN', 'PORTAL' ] }, 1: { location: 'HOME', details: [ 'CLOSE', 'PORTAL' ] } ]
由於實現了 Serde,可以對 struct 進行 serialize,在測試中打印後,單個 struct 是這樣編碼的
[DEBUG] TAVERN (raw: 0x54415645524e
[DEBUG] (raw: 0x2
[DEBUG] OPEN (raw: 0x4f50454e
[DEBUG] PORTAL (raw: 0x504f5254414c
2 個 PortalData 加在一起,前面再加個 2,就是最終編碼。
drunk_spell.cast(origin, destination)
傳入是分開的,我們必須找到 origin
和 destination
的值,以便當它被解釋為 Array<PortalData>
時,它反映了所需的 calldata。
需要驗證的只是前 2 個 Portal,是 ['TAVERN', 2, 'OPEN', 'PORTAL', 'HOME', 2, 'CLOSE', 'PORTAL']
就行。
如果再加一個PortalData {location: 'VOID', details: ['ANY']}
,編碼會變成[3, 'TAVERN', 2, 'OPEN', 'PORTAL', 'HOME', 2, 'CLOSE', 'PORTAL','VOID', 1 , 'ANY']
,也是可以通過驗證的。
但如果要拆分成 2 個 array 分別傳入,就是 [3, 'TAVERN', 2, 'OPEN', 'PORTAL']
和 ['PORTAL', 'HOME', 2, 'CLOSE', 'PORTAL','VOID', 1 , 'ANY']
,'PORTAL' 轉為 felt 的十進制數字會非常大,想要加長 details 滿足長度要求,或添加更多的 portal,會消耗很多 gas,在測試時運行會報錯。
那如果繼續增加 Portal 的個數,直到第二個參數 array 的長度是個非常小的數字的話,就不用消耗很多 Gas 了。接下來你應該知道怎麼寫了。
我通過了測試後,準備部署合約去破解。先用 starkli invoke 發現不能傳 array,snforge invoke --calldata
可以傳 calldata,但又不知道怎麼傳入帳戶等額外參數。如果部署 hack 合約有點麻煩了。最終發現直接走 voyager 瀏覽器發送兩個 calldata 是最簡單的。
總結#
StarkNet 的 calldata 組成規則很簡單,這題燒腦的地方是需要添加更多 Portal 去組成特定的 calldata,蠻有意思的 CTF 題目。