# SWSEC HW2 Writeup ###### tags: `swsec` `writeup` <style> p:has(img) { text-align: center; } .markdown-body img { max-width: 75%; } </style> ## crackme_vectorization 一開始先吃一個 size,然後讀入 size 個 int。 malloc 一塊 0x10 大的 memory,並將 `ceil(sqrt(size))` 存入,且透過 SIMD 指令 (`_mm_shuffle_epi32`) 複製成兩份。 malloc 另一塊 memory 放前面提到的 int 們。 ![image](https://hackmd.io/_uploads/ByhEgBpEp.png) ![image](https://hackmd.io/_uploads/BJjWWrTN6.png =40%x) Create Struct: ![image](https://hackmd.io/_uploads/Sk7tbraEa.png =50%x) 下面檢查 `sqrt_ceil` = 7,才會傳入 `sub_1300` (`matrix_mul`) 做檢查。 ![image](https://hackmd.io/_uploads/SkdQZSpEp.png =60%x) malloc 另一塊 memory 作為回傳值: ![image](https://hackmd.io/_uploads/rkikfSaNT.png) 接下來因為 `sqrt_ceil` 固定為 7,因此有些計算結果為定值 (`const*` 變數們),下面有一連串 SIMD 操作: ![image](https://hackmd.io/_uploads/HJ6U7rTEp.png) ![image](https://hackmd.io/_uploads/B1R_XHpV6.png) ![image](https://hackmd.io/_uploads/SynWBHTE6.png) 可以參考 https://www.officedaytime.com/simd512e/ 來看 SIMD 指令的行為圖解。 中間有許多 if 會被跳過,可以用 `Numpad -` 把那些 branch 隱藏起來。 觀察 `chunk_idx` 與 `buffer_cnt`,可以發現 input 會被切成 7 個 chunk,然後透過 `chunk_idx` 來存取 chunk 中的第幾個數字,而 key 則是直接線性存取。 另外觀察 LABEL_21 沒有 SIMD 的部分,他把上面 SIMD 沒處理掉的第 5、6、7 個 chunk 也做了 reduce。 最後直接通出來是矩陣乘法,`input @ key = res`,直接解反矩陣得到: `crackme_vectorization/solve.py` ```python key_mat = numpy.array(key, dtype=int).reshape(7, 7) res_mat = numpy.array(res, dtype=int).reshape(7, 7) input_mat = numpy.linalg.solve(key_mat, res_mat).round().astype(int) assert numpy.equal(key_mat @ input_mat, res_mat).all() # sanity check print(*input_mat.flatten().tolist()) print(*map(chr, input_mat.flatten().tolist()), sep="") ``` 要記得用 `.round()` 來把浮點數誤差修正,不能直接轉 int,否則有些 char 會無條件捨去造成錯誤。 <img src="https://hackmd.io/_uploads/rk4-qtnf6.png" style="width: 65%"> <img src="https://hackmd.io/_uploads/ByF9KtnMT.png" style="width: 35%"> FLAG: `FLAG{yOu_kn0w_hOw_to_r3v3r53_4_m47riX!}` ## Banana Donut Verifier 將 scanf 的參數 rename 為 `input` ![image](https://hackmd.io/_uploads/HJRUJXTVa.png) 觀察 xref,可以發現他只會被上面的迴圈拿 intermediate state 做 XOR,以及傳入某個 function (`sub_1A05`,renamed to `some_kind_of_hash`)。 ![image](https://hackmd.io/_uploads/By0u1QpVp.png) 另外底下可以看到其驗證邏輯,XOR 過後的 input 之 hash 要跟 `off_6050` 算出來的一樣。 ![image](https://hackmd.io/_uploads/SkGllmaEa.png) 若按下 `Ctrl+D`,則可直接傳 EOF 給 scanf,讓 input 內全為 0,這樣就能得到 XOR 的 mask,且 input = mask $\oplus$ ans (`off_6050`)。 `mask.txt` 與 `ans.txt` 是在做 `some_kind_of_hash(input, 1024uLL);` 前下斷點,透過 gdb dump memory 資料,將兩者 XOR 則可取得 session 以交換 flag。 `Banana Donut Verifier/solve.py` ```python def parse(f): with open(f, "r") as f: ret = sum([ list(map(lambda x: int(x, 16), l.strip()[16:].split(" "))) for l in f.readlines() ], []) return ret mask = parse("mask.txt") # input, $rbp-0x480 ans = parse("ans.txt") # 0x2010 (.rodata) val = [a ^ b for a, b in zip(mask, ans)] print(*map(chr, val), sep="") ``` <img src="https://hackmd.io/_uploads/BJdTk8pGT.png" style="width: 55%"> <img src="https://hackmd.io/_uploads/By_6JLaza.png" style="width: 45%"> FLAG: `FLAG{d0_Y0u_l1k3_b4n4Na_d0Nut?}` ## Baby Ransom 1 -- Next Stage Payload Entry point -> `start` 裡面呼叫了 `sub_140001131`,重命名為 `main`。 下方有複製 `argv` 的部分,並將其傳入 `sub_140001DBB` (`load_payload`)。 ![image](https://hackmd.io/_uploads/Hyj9fQp4a.png) 裡面檢查 `data_140001DBB` (`encrypted_command`) 是否是可以存取的 web url,若是則不載入。 ![image](https://hackmd.io/_uploads/BkTAzmaN6.png) 此資料實際上為 XOR 加密的其他 payload。 ![image](https://hackmd.io/_uploads/SJnzXXpVT.png) ![image](https://hackmd.io/_uploads/SJ3JI76V6.png) 跟入 `sub_140001B0A` (`hide_self`): ![image](https://hackmd.io/_uploads/SkGAEmpVa.png) 前面先呼叫 `SHGetKnownFolderPath` API 拿了一個路徑,其為使用者的下載資料夾,參考文件如下: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid <img src=https://hackmd.io/_uploads/BJAYN7pE6.png style="width: 50%"> <img src=https://hackmd.io/_uploads/Hk3947aE6.png style="width: 50%"> 而後透過 `PathAllocCombine` 將 XOR 解密後的路徑與使用者下載資料夾做組合。 ![image](https://hackmd.io/_uploads/Bk9FrQTN6.png) 下方將自己移到該資料夾,並將自己設成 `FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM` 以隱藏自身。最後呼叫 `sub_140001992` (`schtasks_create`)。 ![image](https://hackmd.io/_uploads/ryArLX6Na.png) `schtasks_create` 會把自己透過 `schtasks` 來排程,解密後指令為 `schtasks /create /sc minute /mo 5 /tn "Microsoft Windows Update" /tr "%s thisisthestart!!" /f`,可以看到指定了參數 `thisisthestart!!`。 ![image](https://hackmd.io/_uploads/rJWCIXaNa.png) 後面若檔案是在正確的路徑 (`ppszPathOut`) 被執行,則呼叫 `sub_14000197A`,裡面最終呼叫 `sub_1400018DA`,並檢查 `IsDebuggerPresent` 與參數,若檢查通過即開始惡意行為 (`sub_140001653`,`start_payload`)。 ![image](https://hackmd.io/_uploads/SyPNv7a4T.png) 裡面呼叫 `sub_14000155A` (`decrypt_next_stage_payload`) 來取得 next stage payload,並透過 `sub_1400013B4` (`migrate`) 將 payload inject 到其他 process (`calc`)。 ![image](https://hackmd.io/_uploads/SycKPQaVa.png) 跟進 `decrypt_next_stage_payload`,可以看到 payload 是透過 `FindResourceA` 取得。 ![image](https://hackmd.io/_uploads/BkTIX_8Va.png) 並用 `0x8711` 做了 XOR 加密。 ![image](https://hackmd.io/_uploads/SJ1K7_UNT.png) 用 Resource Hacker dump 加密的 payload 並自行解密。 ![image](https://hackmd.io/_uploads/H1ig7uUV6.png) `Baby Ransom 1 -- Next Stage Payload/decrypt.py` ```python import itertools key = b"\x11\x87" payload = b"" with open("next_stage.bin", "rb") as f: encrypted_payload = f.read() for a, b in zip(encrypted_payload, itertools.cycle(key)): payload += (a ^ b).to_bytes(1, 'little') with open("payload.dll", "wb") as f: f.write(payload) ``` ![image](https://hackmd.io/_uploads/BywO8dU4a.png) FLAG: `FLAG{e6b77096375bcff4c8bc765e599fbbc0}` ## Baby Ransom 2 -- Encrypted File `start` -> `__scrt_common_main_seh` -> `WinMain` -> `sub_140002870` ![image](https://hackmd.io/_uploads/SJYst7pEp.png =40%x) `sub_140001B10` (`my_start`) retype `p_InLoadOrderModuleList` -> `_LDR_DATA_TABLE_ENTRY *` `DllNameBuffer` -> check loading module retype `v17` -> `IMAGE_EXPORT_DIRECTORY *` => `v5`: `sub_140002810(Function Name)` ![image](https://hackmd.io/_uploads/B1VTtXp4T.png) `sub_140002810` (`FNV_1a`) ![image](https://hackmd.io/_uploads/H1_A5XTE6.png =40%x) `v5` 傳入 switch case,若 hash 符合則將對應的 address 設為 `v8` (`AddressOfFunction`)。 寫個腳本 parse address 與其 hash value,以快速計算並透過 `MakeName` 重新命名該 address。 此處 `*.dll.txt` 存有 export table 所有 function name,`cpp` 則為 IDA 複製出來的 switch case decompile 結果。 `Baby Ransom 2 – Encrypted File/brute.py` ```python import re import os import glob # FNV-1a def name_hash(a1): v2 = 0xCBF29CE484222325 for c in a1: v3 = (ord(c) ^ v2) & ((1 << 64) - 1) v2 = (0x100000001B3 * v3) & ((1 << 64) - 1) return v2 for fn in glob.glob("*.dll.txt"): dll = fn.split(".")[0] cpp = f"{dll}.cpp" if not os.path.exists(cpp): continue db = dict() with open(fn) as f: for l in f.readlines(): db[name_hash(l.strip())] = l.strip() with open(cpp, "r", encoding="utf-8") as f: try: while True: h = int(re.findall("case (0x\w+)i64:", f.readline())[0], 16) a = re.findall("qword_(\w+) = v", f.readline())[0] f.readline() # break print(f'MakeName(0x{a}, "{db[h]}_{a[-4:]}");') except: pass ``` 效果: ![image](https://hackmd.io/_uploads/ByYE2QpVa.png =40%x) 繼續跟 `sub_140001660` (`do_encryption`): ![image](https://hackmd.io/_uploads/SJ0qhXTN6.png) 其從 `advapi32.dll` 載入了 `SystemFunction033`,並把資料夾底下所有檔案丟到 `sub_140001960` (`encrypt_file`) 來加密。 `sub_1400028B0` (`download_robots_txt`) 裡面下載了 `https://shouldhavecat.com/robots.txt`,並取 `flag.txt[2687:2705]` 作為 key (length = 19)。 ![image](https://hackmd.io/_uploads/r1kphm6V6.png) `sub_140001960` (`encrypt_file`) ![image](https://hackmd.io/_uploads/S1bYCXpVp.png) 參考 https://osandamalith.com/2022/11/10/encrypting-shellcode-using-systemfunction032-033/ ,得知 `SystemFunction033` 為 RC4,以及其參數結構: ![image](https://hackmd.io/_uploads/SkuT1Np4a.png =40%x) 另外能看到 L23,key length 實際上只取了 8 bytes。 寫腳本解密 `enc_flag.txt`。 `Baby Ransom 2 – Encrypted File/solve.py` ```python from Crypto.Cipher import ARC4 import requests as r url = "https://shouldhavecat.com/robots.txt" res = r.get(url) key = res.content[2687:2687+8] cipher = ARC4.new(key) with open("enc_flag.txt", "rb") as f: flag = cipher.decrypt(f.read()) print(flag.decode()) ``` FLAG: `FLAG{50_y0u_p4y_7h3_r4n50m?!hmmmmm}` ## Evil Flag Checker 此題有許多 anti debugger 機制,如 `IsDebuggerPresent` 與 `SEH` 跳 EIP 等,但如果能直接跳到驗證邏輯的 function,就不需要動態跑一個一個跟,也就不會被 anti debugger 噁心到。 ![image](https://hackmd.io/_uploads/SkbFlNaVa.png) ![image](https://hackmd.io/_uploads/S1p3l4pE6.png) 從字串回推驗證 function 位置: `sub_4013C0` (原本有點壞掉,對著 function 按 y 重跑他就解好了 ??) ![image](https://hackmd.io/_uploads/B1q1bETVT.png =40%x) 驗證邏輯在 `sub_4012A0` (`validate_flag`): ![image](https://hackmd.io/_uploads/HkvNWE6NT.png) 前半段在計算 hash。 ![image](https://hackmd.io/_uploads/rJKPWVT4T.png) 後半段看起來像優化過的 `strcmp`,所以不管他。 照著 hash 邏輯重新計算,寫個腳本解密。 `Evil Flag Checker/solve.py` ```python unk_403400 = [ 0xED, 0x03, 0x81, 0x69, 0x7B, 0x84, 0xA6, 0xA0, 0x5B, 0x2B, 0xB6, 0xE6, 0x5C, 0x57, 0xC9, 0x99, 0xE8, 0xB2, 0x20, 0x72, 0x38, 0xF1, 0x58 #,0x00 ] def ror4(n, b): bits = f"{n:>032b}" return int(bits[-b:] + bits[:-b], 2) & ((1 << 32) - 1) l = len(unk_403400) key = 0xE0C92EAB flag = [] for i, b in enumerate(unk_403400[:l]): v = b ^ (key & 0xFF) flag.append(v) # key = (l + (v ^ ror4(key, 3)) - i) & ((1 << 32) - 1) key = (l + (b ^ ror4(key, 3)) - i) & ((1 << 32) - 1) # v should be b here print(bytes(flag).decode()) ``` 這裡要注意 `unk_403400` 最後的 NULL 不能納入,因為他是 string terminator。 若照著 decompile 結果,key 更新是 `key = (l + (v ^ ror4(key, 3)) - i) & ((1 << 32) - 1)`,但若在寫腳本解密,要把 `v` 換成 `b`,因為實際上在程式解密當下,v 的值會是最後的輸出,即 `b`。 FLAG: `FLAG{jmp1ng_a1l_ar0und}` ## TrashCan ![image](https://hackmd.io/_uploads/BkI7UEaNT.png =40%x) 前面先 allocate 了兩個 0x10 大的 memory,其中 `off_140004468` 是 function table。 第一塊 memory 會將第二塊 memory 的值存到 `tree + 8`。 ![image](https://hackmd.io/_uploads/S1K4UVpEp.png) `sub_1400013B0` (`traverse_tree`)、`sub_1400015B0` (`build_tree`) 下面再 cin 收 input,並呼叫 function table 中的第二個 function 處理各 input 字元。 最後呼叫 function table 中的第一個 function 來 validate flag。 ![image](https://hackmd.io/_uploads/HJXxS4TVT.png) 跟入 `sub_1400015B0` (`build_tree`),可以發現若符合條件,則會 dereference 某個 pointer、呼叫 `sub_140001290` (`node_append`)、並把結果存回去。 若該 ptr 為空,則 alloc 一個新的 0x18 大的 memory,並初始化值。 ![image](https://hackmd.io/_uploads/BkgCBUNT4a.png) `sub_140001290` (`node_append`) 是一個遞迴函數,行為與 `sub_1400015B0` (`build_tree`) 中呼叫他的部分相同。 ![image](https://hackmd.io/_uploads/r1hd8E6Np.png) 仔細觀察這段: ![image](https://hackmd.io/_uploads/Sk1XKE6NT.png) 其中 `a2` 是由 main 傳入的 character,`v5` 則在 main 中第二塊 alloc 的記憶體裡,通靈出來這是一個建 tree 的過程,`v5[1]` 為 node 的 data,`v5 + 1` 與 `v5 + 2` 則分別為 left,right child。 故推測 `sub_140001290` (`node_append`) 就是做 node append。 另外觀察 `v7`,可以看到其中除了 character 以外,還有 `v4`,且每次呼叫 `sub_1400015B0` (`build_tree`) 就會 +1,故推測此為 input 的 index。 Create struct: ![image](https://hackmd.io/_uploads/rk2L9EaV6.png) 重新標 type 與重命名: `main` ![image](https://hackmd.io/_uploads/ryZsNNTNa.png =45%x) ![image](https://hackmd.io/_uploads/SJdXBNaVT.png) `build_tree` ![image](https://hackmd.io/_uploads/B1DnBVpVT.png) `node_append` ![image](https://hackmd.io/_uploads/BJqKUNpVa.png) 回去看 `traverse_tree`: ![image](https://hackmd.io/_uploads/H1lj2ETV6.png =50%x) 裡面呼叫 `sub_140001320` (`preorder_traverse`),且也為遞迴函數。 ![image](https://hackmd.io/_uploads/r127nEpV6.png) 先走訪 `v4`、再來是 left、再來是 right,推論為 preorder traversal。 `a3` 會被傳入 `sub_140001F60` (`vector_push_expand`),裡面呼叫了 `sub_1400020F0`,會丟出 `vector too long` 的錯誤,故推測 `a3` 是個 vector。 參考此篇文章 https://www.msreverseengineering.com/blog/2021/9/21/automation-in-reverse-engineering-c-template-code ,建立 vector 與 vector_item struct: ![image](https://hackmd.io/_uploads/ByX_nE646.png) `v6` 為 push 到 vector 的東西,為 64 bits 大,剛好是兩個 int,一個為 index 一個為 data,這裡因為 little endian 的關係,higher address 是較後面的 field。 retype & rename: ![image](https://hackmd.io/_uploads/BywPa4aVT.png) 推測 `a3` 是存遍歷路徑,`vector_push_expand` 是在 vector 滿 (`last == vector->end`) 的時候會做 expand 以存放更多資料。 `traverse_tree` ![image](https://hackmd.io/_uploads/ByFzCNa4a.png) 上面透過 SIMD 一次載入多筆資料 (retype `data` 與 `index` 為 `int[22]`) ![image](https://hackmd.io/_uploads/rkVLRNT46.png =40%x) 下面會一一對照路徑是否正確 (檢查 `data` 與 `index`)。 實際上 binary 裡的 flag 只是打亂順序,並沒有做其他處理,因此可以直接透過 index 回推。 `Trashcan/solve.py` ```python # setup data and index... node = [] for d, dep in zip(data, index): node.append((d, dep)) flag = "" for n in sorted(node, key=lambda x: (x[1], data.index(x[0]))): flag += chr(n[0]) print(flag) ``` ![image](https://hackmd.io/_uploads/HJN12pjET.png) FLAG: `FLAG{s1mpl3_tr4S5_caN}`