DEF CON CTF Qualifier 2022 WriteUps
這次在 Balsn.217@TSJ.tw 的聯隊中拿下了第二名,不過身為一個 web/crypto player 在這場比賽中真的覺得做不了什麼事,因為題目都太 rev/pwn heavy 了。這邊也稍微粗略紀錄些有參與的題目的解法,因為很多題目也都是靠多人合作才完成的。
sameold
這題要找到一個字串 x
以隊伍名稱的 punycode 為開頭,後面可以接任意的 alphanumeric 字元,要使得 crc32(x) == crc32("the")
。
最簡單的解法就是直接 bruteforce,這個也是預期解法。不過有趣的一個地方是這次比賽有隊伍名稱就叫 the
,所以可以直接輕鬆解掉這題。
不過我後來還有弄個使用 crc 性質的數學解法,需要一些線性代數去處理。對於 m bits -> n bits 的 crc 來說
其中 是 維向量,而 是 矩陣和 是 維向量。這部分可以直接把 crc function 視為 blackbox 嘗試去 recover 和 ,可以避免處理麻煩的 padding 的細節。
我有為這個寫了個 sage script: crcmat.sage
所以將 concat 後的字串視為 ,其中 padded prefix,而 是要 concat 的東西。當目標 crc 為 的時候有 ,所以解 就能得到 。不過這個情況下找到的 一般不是 alphanumeric,所以還得另外處理。
可知當正常情況下 的時候 的 right kernel 的 rank 很高,再來是 加上任何 right kernel 中的向量也都還是能讓 crc 保持相同,所以要找個方法讓 和那些向量的組合變成一個 alphanumeric 的向量。
去問了 Utaha 才知道可以利用 ASCII 0~7
字元的 ascii code 都是 00110???
的格式,所以每個字元都能視為是一個 00110000
和 3 個向量的組合,append n 個字元的話就是個常數 k
向量 (0011000000110000....
),然後和多出來的 3n 個自由向量。這邊暫且把那些自由向量稱做 basis
,所以有 d + span(kernel) = k + span(basis)
,因此 kernel 和 basis 整理到同一邊去解這個 linear system,然後取 sol[:dim(kernel)] * kernel + d
就會是個 alphanumeric 的向量,同時也能保持 crc 相同。
Solver: DEF CON Quals 2022- sameold
Hash it
reverse 它的 binary 可以知道它會拿 input bytes 兩兩 hash,取 hash 的第一個 byte 組成 shellcode 最後執行。hash 部分是 md5, sha1, sha256, sha512 輪迴,所以就按照它的邏輯寫個腳本把 shellcode 爆破回原本的 input bytes 即可。
腳本: DEF CON Quals 2022 - Hash It
discoteq
這題關於 Flutter Web/Desktop 寫的一個聊天軟體,admin (bot) 使用了 desktop 版本接收你的訊息。
通訊部分使用的是 websocket,傳送和接收的部分都大概屬於這個格式:
{"type":"widget","widget":"/widget/charmessage","author":{"user":"aaaa#8763","platform":"web"},"recipients":["admin#13371337"],"data":{"message":"abc"}}
其中 widget
部分是在 client 接收的時候會進到 getChatWidget
處理:
getChatWidget(url) {
var $async$goto = 0,
$async$completer = A._makeAsyncAwaitCompleter(type$.RemoteChatWidget),
$async$returnValue, t1, decoder, result, t2, widget, existing;
var $async$getChatWidget = A._wrapJsFunctionForAsync(function($async$errorCode, $async$result) {
if ($async$errorCode === 1)
return A._asyncRethrow($async$result, $async$completer);
while (true)
switch ($async$goto) {
case 0:
// Function start
existing = $.chatWidgetCache.$index(0, url);
if ($.config.$index(0, "auto") !== true && existing != null) {
$async$returnValue = existing;
// goto return
$async$goto = 1;
break;
}
$async$goto = 3;
return A._asyncAwait(A.makeRequest("GET", url, null, true, null), $async$getChatWidget);
case 3:
// returning from await.
t1 = $async$result.bodyBytes;
t1 = A.NativeByteData_NativeByteData$view(t1.buffer, t1.byteOffset, t1.byteLength);
decoder = new A._BlobDecoder(t1);
decoder.expectSignature$1(B.List_254_82_70_87);
result = new A.RemoteWidgetLibrary(decoder._readImportList$0(), decoder._readDeclarationList$0());
if (decoder._binary$_cursor < t1.byteLength)
A.throwExpression(B.FormatException_gkJ);
t1 = A.LinkedHashMap_LinkedHashMap$_empty(type$.LibraryName, type$.WidgetLibrary);
t2 = type$.FullyQualifiedWidgetName;
t2 = new A.Runtime(t1, A.LinkedHashMap_LinkedHashMap$_empty(t2, type$.nullable__ResolvedConstructor), A.LinkedHashMap_LinkedHashMap$_empty(t2, type$._CurriedWidget), A.List_List$filled(0, null, false, type$.nullable_void_Function));
widget = new A.RemoteChatWidget(result, t2);
t1.$indexSet(0, B.LibraryName_List_core_widgets, new A.LocalWidgetLibrary(A._coreWidgetsDefinitions()));
t2._clearCache$0();
t1.$indexSet(0, B.LibraryName_List_core_material, new A.LocalWidgetLibrary(A._materialWidgetsDefinitions()));
t2._clearCache$0();
t1.$indexSet(0, B.LibraryName_List_local, new A.LocalWidgetLibrary(A.LinkedHashMap_LinkedHashMap$_literal(["URLImage", A.main_URLImage_FromSource$closure(), "ApiMapper", A.main_ApiMapper_FromSource$closure()], type$.String, type$.Widget_Function_BuildContext_DataSource)));
t2._clearCache$0();
t1.$indexSet(0, B.LibraryName_List_remote, result);
t2._clearCache$0();
$.chatWidgetCache.$indexSet(0, url, widget);
$async$returnValue = widget;
// goto return
$async$goto = 1;
break;
case 1:
// return
return A._asyncReturn($async$returnValue, $async$completer);
}
});
return A._asyncStartSync($async$getChatWidget, $async$completer);
},
url
部分是直接 concat 的,所以如果 widget
是 @attacker.host/malicious_file
就能控制進入 RemoteWidgetLibrary
的 payload。
這個功能是 Flutter 的 Remote Flutter Widgets 功能,它算是個 serialization 格式。把那個 library 抓下來跑,然後嘗試把它原本的 rfw 檔案也抓下來解析並比較看看它有什麼功能。
總之最後可以知道它有自己加個 ApiMapper
可以讓你 get json 然後抓某個 key 出來設定到 data 中,另外還有 post_api
的 event 可以 POST data 的資料出去到其他地方,url 一樣能透過 @
讓它將資料傳到自己的 server。
所以就想辦法組合讓 admin 把 /api/token
的 new_token
傳到自己的 server,然後拿 token 去 POST /api/flag
即可。
Payload 和其他相關的腳本: DEF CON Quals 2022 - discoteq
admad
這題就一個 Python 的 pyc reverse 題目,flag checker 類型的,但是它提供了自己編譯的一個 python interpreter,還有很多的額外 library 也都被移除掉了。
題目的 Python 版本它有直接說是 702e0da000bf28aa20cb7f3893b575d977506495,所以可以抓下來編譯,不過編譯之後嘗試跑它的 chall.pyc
會直接 segfault,只能用它給的 python 跑才行。
splitline 說這題會炸是因為它的 bytecode 有置換過,但 binary 是 stripped 的很難直接 reverse 出來哪些有改過。解決辦法是參考 kholia/dedrop 去寫個包含(幾乎)所有 opcode 的 python 檔,然後讓他們生成兩個 pyc,之後透過比較 opcode 去造一個 mapping 出來。
得到 mapping 之後可以把整個 pyc 重新寫過讓它能在正常的 interpreter 執行,然後自己 dis.dis
。得到的 bytecode 簡單讀一下可以發現它就只是取 flag index,然後利用 &
運算和一個 mask 去比較而已,這部分可以很容易的將它還原回能夠通過檢查的 input。
這部分可以參考 rev.py。
不過還原之後會發現它的輸入像是亂碼,根本不是 flag,後來還是靠隊友通靈出它可以用這樣的方法還原:
test = b'\x8e\x86\x8d\x95\xbb\xaa\xb9\xb2\xc8\xb3\xba\xc5\xaf\xc3\xc8\xc7\xc5\xb9\xcf\xe3\xe3\xe6\xd3\xd3\xd1\xe4\xe1\xcd\xe3\xf2\xed\xfa\xe0\xe9\xea\xddC0EH\xe7\xf3\x0f\xed\x06\x17\x02\xf5_Z^\x13`\x16\x15fhj)'
now=56
now2=72
for i in test:
print(chr((i-now)%128),end='')
print(chr((i-now2)%128))
now+=2
now2+=2
# then guess FLAG{helping_adam_customize_cpython_3.12_is_fun_702e0da000}
結果後來才知道它的 interpreter 也有把 bytes
修改過,就是如果參數是 list 且 list[0] == ord('F')
的話就會把它的值亂搞,使用了類似上面的公式去輸出的。
router-niii
這題是 router 系列的第三題,主要是要去打某個從 /vm
下載的 binary,它是處理 update 資料的程式,實際上也是個 interpreter 有自己的 opcode。
我在這邊一開始有注意到它的 binary 的 call
都被混淆成了一些奇怪的東西,所以就嘗試利用 capstone 和 pwntools 把它修回正常的 call
,也能正常執行和在 IDA 中反編譯: vm_patch.py。
然後後來 orange 成功將它 reverse 出來,然後生出可以讀 /flag3
的 payload,然而它的 vm 還會接受個 seed
參數 (./vm <seed>
) 當作 srand(seed)
,然後利用輸出的值亂序執行 opcode,所以必須要知道 server 端是怎麼執行 ./vm
的才能知道 seed。
不過要做到這件事基本上需要能利用 OOB 讀取 (/ping?id=???
) 把 server binary dump 下來然後 reverse 才行。不過由於它 dump 的時候一次 4096 bytes,沒能讓我把 ELF header 也 dump 回來去拚回一個可以 reverse 的 ELF,所以也沒能 reverse 成功,這也是解第二題 router-nii 所需的關鍵。
不過後來亂試之後發現 /ping?id=1000
的地方有奇怪的資料在,發現它基本上除了前四個 bytes 以外大部分都是 null,所以我就通靈說這該不會就是 ./vm <seed>
的 seed。然後寫了個腳本整合 orange 的 payload 就發現說真的是這樣,然後就很幸運的拿到了第三題的 flag ==。
腳本可以參考 solve.py。