# 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 們。


Create Struct:

下面檢查 `sqrt_ceil` = 7,才會傳入 `sub_1300` (`matrix_mul`) 做檢查。

malloc 另一塊 memory 作為回傳值:

接下來因為 `sqrt_ceil` 固定為 7,因此有些計算結果為定值 (`const*` 變數們),下面有一連串 SIMD 操作:



可以參考 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`

觀察 xref,可以發現他只會被上面的迴圈拿 intermediate state 做 XOR,以及傳入某個 function (`sub_1A05`,renamed to `some_kind_of_hash`)。

另外底下可以看到其驗證邏輯,XOR 過後的 input 之 hash 要跟 `off_6050` 算出來的一樣。

若按下 `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`)。

裡面檢查 `data_140001DBB` (`encrypted_command`) 是否是可以存取的 web url,若是則不載入。

此資料實際上為 XOR 加密的其他 payload。


跟入 `sub_140001B0A` (`hide_self`):

前面先呼叫 `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 解密後的路徑與使用者下載資料夾做組合。

下方將自己移到該資料夾,並將自己設成 `FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM` 以隱藏自身。最後呼叫 `sub_140001992` (`schtasks_create`)。

`schtasks_create` 會把自己透過 `schtasks` 來排程,解密後指令為 `schtasks /create /sc minute /mo 5 /tn "Microsoft Windows Update" /tr "%s thisisthestart!!" /f`,可以看到指定了參數 `thisisthestart!!`。

後面若檔案是在正確的路徑 (`ppszPathOut`) 被執行,則呼叫 `sub_14000197A`,裡面最終呼叫 `sub_1400018DA`,並檢查 `IsDebuggerPresent` 與參數,若檢查通過即開始惡意行為 (`sub_140001653`,`start_payload`)。

裡面呼叫 `sub_14000155A` (`decrypt_next_stage_payload`) 來取得 next stage payload,並透過 `sub_1400013B4` (`migrate`) 將 payload inject 到其他 process (`calc`)。

跟進 `decrypt_next_stage_payload`,可以看到 payload 是透過 `FindResourceA` 取得。

並用 `0x8711` 做了 XOR 加密。

用 Resource Hacker dump 加密的 payload 並自行解密。

`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)
```

FLAG: `FLAG{e6b77096375bcff4c8bc765e599fbbc0}`
## Baby Ransom 2 -- Encrypted File
`start` -> `__scrt_common_main_seh` -> `WinMain` -> `sub_140002870`

`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)`

`sub_140002810` (`FNV_1a`)

`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
```
效果:

繼續跟 `sub_140001660` (`do_encryption`):

其從 `advapi32.dll` 載入了 `SystemFunction033`,並把資料夾底下所有檔案丟到 `sub_140001960` (`encrypt_file`) 來加密。
`sub_1400028B0` (`download_robots_txt`) 裡面下載了 `https://shouldhavecat.com/robots.txt`,並取 `flag.txt[2687:2705]` 作為 key (length = 19)。

`sub_140001960` (`encrypt_file`)

參考 https://osandamalith.com/2022/11/10/encrypting-shellcode-using-systemfunction032-033/ ,得知 `SystemFunction033` 為 RC4,以及其參數結構:

另外能看到 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 噁心到。


從字串回推驗證 function 位置: `sub_4013C0` (原本有點壞掉,對著 function 按 y 重跑他就解好了 ??)

驗證邏輯在 `sub_4012A0` (`validate_flag`):

前半段在計算 hash。

後半段看起來像優化過的 `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

前面先 allocate 了兩個 0x10 大的 memory,其中 `off_140004468` 是 function table。
第一塊 memory 會將第二塊 memory 的值存到 `tree + 8`。

`sub_1400013B0` (`traverse_tree`)、`sub_1400015B0` (`build_tree`)
下面再 cin 收 input,並呼叫 function table 中的第二個 function 處理各 input 字元。
最後呼叫 function table 中的第一個 function 來 validate flag。

跟入 `sub_1400015B0` (`build_tree`),可以發現若符合條件,則會 dereference 某個 pointer、呼叫 `sub_140001290` (`node_append`)、並把結果存回去。
若該 ptr 為空,則 alloc 一個新的 0x18 大的 memory,並初始化值。

`sub_140001290` (`node_append`) 是一個遞迴函數,行為與 `sub_1400015B0` (`build_tree`) 中呼叫他的部分相同。

仔細觀察這段:

其中 `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:

重新標 type 與重命名:
`main`


`build_tree`

`node_append`

回去看 `traverse_tree`:

裡面呼叫 `sub_140001320` (`preorder_traverse`),且也為遞迴函數。

先走訪 `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:

`v6` 為 push 到 vector 的東西,為 64 bits 大,剛好是兩個 int,一個為 index 一個為 data,這裡因為 little endian 的關係,higher address 是較後面的 field。
retype & rename:

推測 `a3` 是存遍歷路徑,`vector_push_expand` 是在 vector 滿 (`last == vector->end`) 的時候會做 expand 以存放更多資料。
`traverse_tree`

上面透過 SIMD 一次載入多筆資料 (retype `data` 與 `index` 為 `int[22]`)

下面會一一對照路徑是否正確 (檢查 `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)
```

FLAG: `FLAG{s1mpl3_tr4S5_caN}`