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,傳送和接收的部分都大概屬於這個格式:

1
{"type":"widget","widget":"/widget/charmessage","author":{"user":"aaaa#8763","platform":"web"},"recipients":["admin#13371337"],"data":{"message":"abc"}}

其中 widget 部分是在 client 接收的時候會進到 getChatWidget 處理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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/tokennew_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 出來。

這部分可以看我的 xx.pyxx.sh

得到 mapping 之後可以把整個 pyc 重新寫過讓它能在正常的 interpreter 執行,然後自己 dis.dis。得到的 bytecode 簡單讀一下可以發現它就只是取 flag index,然後利用 & 運算和一個 mask 去比較而已,這部分可以很容易的將它還原回能夠通過檢查的 input。

這部分可以參考 rev.py

不過還原之後會發現它的輸入像是亂碼,根本不是 flag,後來還是靠隊友通靈出它可以用這樣的方法還原:

1
2
3
4
5
6
7
8
9
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