TSC CTF 2025 - writeup
Author: 堇姬Naup
Rank: 5th
總排名第五名,解了26題,這場解的題目有點多,這邊只簡單寫一些有趣的題目,Pwn我應該會全寫
Ranked 5th overall, solving 26 challenges. Since we solved quite a lot of challenges in this competition, I’ll only briefly write about some interesting ones here. But I should write all the Pwn challenges.
btw, this is my pwn template: https://github.com/Naupjjin/MyCTFLib
Pwn
gamble_bad_bad
1 | struct GameState { |
這裡在輸入時沒有控制輸入長度,導致可以蓋到 jackpot_value
只要蓋 jackpot_value 成 777 就可以 get flag
There is no input length restriction here, allowing us to overwrite jackpot_value.
By simply overwriting jackpot_value to 777, we can get the flag.
1 | strcmp(game.jackpot_value, "777") |
My script
1 | from pwn import * |
Localstack
1 |
|
漏洞點非常明顯,array 在 stack 上
pop 跟 push 沒有管控,導致可以控制top成很大或很小的數字來 oob,這題直接 oob 寫 ret address 就好了
另外show 可以用來 leakPIE 跳 print_flag
另外我這邊在蓋的時候,剛好可以蓋到 top,所以我直接把 top 改成可以推寫入到 save rbp,再往下寫入就可以了 (注意不要蓋到 stack canary)
The vulnerability is very obvious: the array is located on the stack.
By using pop and push, we can manipulate top to a very large or very small number, leading to oob condition. In this challenge, directly using OOB to overwrite the return address is sufficient.
Additionally, show can be used to leak PIE and jump to print_flag.
When overwriting, I happened to be able to overwrite top, so I directly changed top to the saved rbp. From there, I continued writing downward and write return address (making sure not to overwrite the stack canary).
My script
1 | from pwn import * |
babyheap
1 | void edit(void){ |
edit function 有 heap overflow 很明顯,通過 unsorted bin leak libc,malloc 兩塊,free掉下面那塊,heap overflow 蓋 tcache fd 到 free_hook,把 free_hook 改成 system,並將想要 Free 的 chunk 是 /bin/sh,就 get shell 了
PS: 記得要讓你在alloc free hook 的 fake chunk 時,tcache_pthread 的 cnt 要是 > 0 的
The edit function has a very obvious heap overflow. Using the unsorted bin, we can leak libc.
Allocate two chunks, then free the lower chunk.
Use heap overflow to overwrite the tcache fd to point to free_hook, then overwrite free_hook with system.
Finally, free a chunk containing /bin/sh to get a shell.
PS: Remember that when allocating the fake chunk for free_hook, the tcache_pthread count must be greater than 0.
My script
1 | from pwn import * |
窗戶麵包
1 | __int64 __fastcall main() |
有 buffer overflow 蓋 return address 控 rip 跳到 magic function(有給 main function address,通過計算 magic function 跟 main function offset 算出 magic address)
There is a buffer overflow that overwrites the return address, controlling the rip to jump to the magic function. The address of the main function is provided, and by calculating the offset between the magic function and the main function, we can determine the magic function’s address.
1 | void __cdecl magic(const char *param1, int param2, const char *param3) |
這邊記得往後跳一點,跳過判斷式,就可以 get shell 了
如果有亂碼就下 chcp 65001
Remember to jump a bit further ahead to skip the conditional check, and you’ll be able to get the shell.
If you encounter any garbled characters, run chcp 65001
to set the code page to UTF-8.
My script
1 | from pwn import * |
Globalstack
1 | int64_t stack[MAX_STACK_SIZE]; |
跟 localstack
,不過這題不在 stack 上,GOT 上有libc,通過show leak libc,之後改 top (改成任意記憶體位置可以有任意寫) 到 free hook,並把 free hook 寫 one gadget,跳上去 get shell
This is similar to localstack
, but in this case, it’s not on the stack. The GOT contains a libc address. By using the show function, we can leak libc.
After that, we modify top (which allows us to write to any memory location) to point to the free_hook, then overwrite the free_hook with a one-gadget.
Finally, we jump to the one-gadget to get the shell.
My script
1 | from pwn import * |
BabyStack
1 | puts("| Show your skills !"); |
這裡有任意寫可以控 libc 裡面的 GOT,將 rsp 往上搬 0x58
we have arbitrary write, which allows us to control the GOT inside libc. By moving the rsp up by 0x58
1 | puts("| Do you know how the stack works ?"); |
接下來在原本讓你輸入的地方會剛好在 rsp 所指的地方,ret繼續執行,這邊寫 onegadget(前面兩個 gadget 用來調整成 one gadget 條件)
Next, the place where you were originally allowed to input will happen to be at the location pointed to by rsp. After the ret, execution continues. Here, we write the one-gadget (with the previous two gadgets used to adjust the conditions for the one-gadget).
1 | from pwn import * |
Babyrust
buffer overflow 打 ROP,通過 gadget 把 /bin/sh 寫到記憶體上,並把 rax、rdi、rsi、rdx 堆成 syscall table 的值,唯一遇到的問題是沒有syscall,不過可以在這裡看到封裝過的 syscall (我是去追 gdb 發現他不是一般的 syscall)
不過這裡的 syscall 會把你的參數位移,按照這個位移就行了
In this case, a buffer overflow is used to perform a ROP attack. Through gadgets, we write /bin/sh to memory and set up the values for rax, rdi, rsi, and rdx to match the syscall table. The only issue encountered is the absence of a direct syscall, but by inspecting the code in GDB, we can see that the syscall is encapsulated (it’s not a regular syscall).
However, this encapsulated syscall shifts the parameters, so we just need to account for the offset in the parameters to make it work correctly.
1 | ENTRY (syscall) |
https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/x86_64/syscall.S.html
My script
1 | from pwn import * |
no view (賽後解)
1 | void delete(void){ |
這邊先了解 _IO_2_1_stdout_
該如何leaklibc
當 _IO_2_1_stdout_
這個 file structure flags 加上 _IO_CURRENTLY_PUTTING
和 _IO_IS_APPENDING
,並把 _IO_write_base
改小,就可以 leaklibc
先看這個 POC
To understand how to leak libc using _IO_2_1_stdout_
, we need to focus on the file structure, specifically the flags _IO_CURRENTLY_PUTTING
and _IO_IS_APPENDING
. By modifying the flags of the file structure and shrinking _IO_write_base
, we can exploit this to leak libc.
Let’s take a look at the POC
1 |
|
Detail:
https://reinject.top/posts/ctf-pwn/leaklibc/overwrite__io_2_1_stdout_to_leak_libc/
delete 把 chunk free 掉後沒有清空 pointer,導致有 UAF
首先是先 malloc 兩塊 chunk,一塊大的會進 unsorted bin
一塊小的防止 unsorted bin chunk 被合併到 top chunk
接下來 malloc 兩塊,他會從 unsorted bin chunk 切空間來用
第一塊 chunk 會有 main arena(double linklist 會指回 main arena)
把第二塊 free 掉讓他掉入 tcache
通過 UAF 將第一塊 chunk (有 main_arena) 低位改成 0xd6a0,並將他 copy 到被 free 掉的 tcache chunk
_IO_2_1_stdout_
的 offset 是 0x1ed6a0
main arena 的 offset 是 0x1ecfd0
扣掉低位是 0x000直接改就行,和最高位兩位相同基本上不會有問題(除非有進位但先省略)
那就會有 1/16 (0x0 ~ 0xf) 機會拿到 stdout
拿到 stdout 後就改 flags 成 0xfbad1800,write base 低位改為 0x00
拿到 libcbase 後就跟 babyheap 一樣打 tcache poinsoning 了
After deleting and freeing the chunk, the pointer is not cleared, leading to UAF vulnerability.
First, we malloc two chunks: one large chunk goes into the unsorted bin, and one small chunk prevents the unsorted bin chunk from being merged into the top chunk.
Next, malloc two more chunks. These will be allocated from the unsorted bin chunk. The first chunk will contain a main_arena pointer (since the double-linked list points back to main_arena).
By freeing the second chunk, it will go into the tcache. Through the UAF, we overwrite the lower part of the first chunk (which contains main_arena) with 0xd6a0 and copy it into the freed tcache chunk.
The offset of _IO_2_1_stdout_
is 0x1ed6a0, and the offset of main_arena is 0x1ecfd0.
By subtracting the lower bits (which are 0x000), we can directly modify the value, and since the highest bits are the same, there won’t be any issues. This gives us a 1/16 chance (0x0 ~ 0xf) of obtaining the stdout address.
Once we have stdout, we change the flags to 0xfbad1800 and set the lower bits of write_base to 0x00.
After obtaining the libc base address, we proceed with tcache poisoning, similar to the babyheap technique.
1 | from pwn import * |
TSCCTF{w0w_y0u_hav3_succ3ss4_1nsp3ct_th3_s3cr3t_congrats}
Crypto
2DES
DES 弱密鑰對
1 | from pwn import * |
我從來都不覺得算密碼學開心過
枚舉其中一個 r 看解不解的出來
1 | from Crypto.Util.number import getPrime, long_to_bytes |
AES Encryption Oracle
難的是寫腳本
知道這張圖在幹嘛就行
我們可以 leak 相鄰兩個 block 及 flag
所以只要 第 n 個block(第一個是 IV,之後是 C) xor DEC(第 n+1 個block, key) = m
1 | #!/usr/bin/env python3 |
Misc
calc
No builtins 跟過濾 []
用這條 chain + getitem + unicode 就可以了
https://lingojam.com/ItalicTextGenerator
1 | ().__𝘤𝘭𝘢𝘴𝘴__.__𝘮𝘳𝘰__.__𝘨𝘦𝘵𝘪𝘵𝘦𝘮__(-1).__𝘴𝘶𝘣𝘤𝘭𝘢𝘴𝘴𝘦𝘴__().__𝘨𝘦𝘵𝘪𝘵𝘦𝘮__(121).𝘭𝘰𝘢𝘥_𝘮𝘰𝘥𝘶𝘭𝘦("\157\163").𝘴𝘺𝘴𝘵𝘦𝘮("\57\142\151\156\57\163\150") |
Web
E4sy SQLi
1 |
|
這裡有 SQL injection 問題,通過看前端會不會被重新導向來確認 flag 是否正確,一個一個 leak,也就是 SQLinjection Boolean base leak flag
1 | import requests |
after all
打的很開心,題目很好玩,雖然沒有辦法打有獎金賽區很可惜QQ