首页
论坛
课程
招聘
[原创]HITB CTF 2018 gundam分析
2021-8-22 23:18 9177

[原创]HITB CTF 2018 gundam分析

2021-8-22 23:18
9177

这道题主要考察tcache poisoning技术(修改tcache 中chunk的next指针),涉及到内存地址泄露、double free等技术,题目难度不大,但是对于入门选手来说,要跨过各种坑,成功利用漏洞获得系统控制权也不是简单容易的事。
一、gundam结构分析
二、内存泄露
三、双重释放漏洞(double free)
四、总结遇到的各种坑

 

一、gundam结构分析
(一) 基本结构:
通过逆向分析,可以知道,gundam结构如下:

1
2
3
4
5
6
struct gundam{
    uint32_t flag;
    char *name;
    char type[24];
}gundam;
struct gundam *factory[9]

包含一个结构体,命名为gundam,一个指向该结构体的指针factory。实际上在建立gundam的过程中,有两次malloc过程:

1
2
s = malloc(0x28#1
buf = malloc(0x100uLL); #2

语句#1申请0x28字节的堆内存,用于创建结构体gundam(factory指针指向该结构体)。
语句#2申请0x100uLL字节的内存,用于保存gundam的name,返回地址buf指向该chunk的用户数据部分。
使用命令gdb gundam启动pwndbg,创建8个gundam之后,使用heap命令查看chunk的状态如下:
图片描述
每个gundam包含两个chunk,一个大小为0x31的factory,另一个大小为0x111的name。
创建8个gundam,再释放,可以看到前7个进入tcache,最后一个进入unsortedbin:
图片描述
(二)gundam中的关键函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
__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;
}

该函数在读入用户输入的gundam name时,没有对字符串末尾进行处理(加上'\x00')。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 sub_D32()
{
  unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]
 
  v2 = __readfsqword(0x28u);
  if ( dword_20208C ) //gundam数目
  {
    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;
}

destroy函数删除gundam,首先将gundam->flag置0,然后释放gundam->name。而在释放name后,并没有将name指针置空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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_2020A0[i]&&!*(_DWORD *)qword_2020A0[i] )
    {
      free((void *)qword_2020A0[i]);
      qword_2020A0[i] = 0LL;
      --dword_20208C;
    }
  }
  puts("Done!");
  return __readfsqword(0x28u) ^ v2;
}

blow_up函数将已经flag置0的factroy释放,并将factroy指针置空,减少gundam的数量,但是仍然没有将gundam->name指针置空,存在double free漏洞。

 

二、内存泄露
由前面分析得知,由于tcache机制本身的限制,当释放的chunk数量超过7个,会充满tcache bin ,超出的chunk根据大小放到unsorted bin。当chunk被free放入tcache bin时,会在该chunk对应的用户数据区(fd区域)写入tcache_entry, 指向下一个位于tcache bin 中的chunk的用户数据,从而构成单链表。
前面分析,函数构造gundam时,对于用户的输入字符串没有进行处理,即末尾增加截断字符“\x00”,而申请的堆空间有0x100字节,没有初始化,导致存在泄露信息的可能。
图片描述
通过gdb.attach()动态跟进调试gundam,在构造8个gundam处设置断点,前7个的name 是‘AAAAAAA’,最后一个的name是'BBBBBBB',查看最后一个gundam->name的内存,可以看到0x7fe02bafbc78这个地址,而该地址是main_arena+88地址:
图片描述
我们知道,tcache结构位于heap的最前端,也是一个堆块,其中包含数组entries,用于放置64个bins的地址,数组counts存放每个bins中chunk的数量。
通过vmmap命令,可以看到heap的开始位置为0x55e22cd98000:
图片描述
使用x/26gx 0x55e22cd98000+0x10继续查看该地址处的值:
图片描述
可以看到,tcache中已经存放了7个chunk,所以第8个chunk放入unsorted bin。
认真观察可以发现,地址0x55e22cd980c8处的指针0x000055e22cd98a10正好就是tcache bin头结点指向的第一个chunk,也就是最后被释放加入tcache的chunk(第7个chunk):
图片描述
我们知道,一个gundam包含两个chunk,大小为0x30,另一个为0x110。我们由此出发,寻找第8个chunk。
通过连续申请创建8个gundam,第7个gundam的地址加上2个chunk的大小,就是第8个chunk的地址。
使用x/26gx 0x000055e22cd98a10+0x30+0x110-0x10,可以看到第8个chunk的地址为0x55e22cd98b40,其bk和fd指针都指向0x7f566befac78(unsortedbin头结点,是main_arena+88,也是泄露的地址):

 

图片描述
思考为什么会泄露地址0x7f566befac78(main_arena+88)?因为在反复的gundam创建和释放过程中,伴随着0x55e22cd98b40+0x10(基址会变,偏移不变)处数据的变化。在创建第8个chunk时,地址0x55e22cd98b40+0x10(基址会变,偏移不变)存放gundam->name,由于我们输入name字符串为'BBBBBBB',只有7个字节(加上末尾的0x0a正好8个字节),0x55e22cd98b40+0x18后面内存地址处的值没有专门处理。在释放第8个chunk时,通过前面的分析可以知道第8个chunk的地址0x55e22cd98b40处,其bk和fd指针都指向0x7f566befac78(unsortedbin头结点,是main_arena+88,也是泄露的地址)。而再次创建第8个chunk时,由于gundam构造函数没有对用户输入的字符串末尾进行处理,也没有对其申请的chunk进行过初始化,导致上次释放时写入的bk指针的值0x7f566befac78(unsortedbin头结点,是main_arena+88,也是泄露的地址)仍然存在。利用者就可以通过查找'BBBBBBB'字符串进而找到泄露的地址0x7f566befac78(unsortedbin头结点,是main_arena+88,也是泄露的地址)。前提是,必须至少有两次创建和释放8个以上gundam的过程,才能使第八个chunk地址的偏移0x18处存在泄露地址。
知道了泄露地址,我们再查找libc基地址,紧挨着heap的libc-2.26.so对应的地址0x7f566bb4f000就是libc基地址:
图片描述

 

泄露地址0x7f566befac78-libc基地址0x7f566bb4f000=偏移0x3ac78是一个固定值。由于每次运行时程序加载到地址空间的基址会随机变化,可以用泄露地址减去该偏移得到对应的libc的基地址,进而计算出free_hook_addr和system_addr地址。

 

三、双重释放漏洞
libc-2.26没有对tcache 中的double free进行安全性检查,直到libc-2.28,才增加了对tcache 中的double free的安全性检查。前面我们在分析gundam的关键函数时知道,destroy函数和blow_up函数都没有将gundam->name置空,存在着双重释放的漏洞。
对于前面我们创建的8个gundam,我们先依次destroy序号为2、1、0的gundam,可以看到有3个空闲的chunk进入tcache:
图片描述
使用heap命令观察chunk状态:
图片描述
0x5627336a1a00地址处chunk为0号gundam的name对应的chunk,最后进入tcache,位于tcache指向的第一个位置。此时factroy还没有释放,大小为0x31的chunk还没有free。
再次destroy序号为0的gundam,可以看到:
图片描述
此时,tcache中只有一个chunk,且该chunk的fd指针指向自身。为什么重复destroy了0号gundam,反而造成tcache中空闲chunk减少2个?
图片描述
如上图所示,1表示destroy3个gundam后,tcache bin的状态。
当free一个chunk时,该chunk的fd指针会指向tcache bin中的第一个chunk,然后tcache bin的头结点指向刚free掉的chunk,依次类推,保证tcache bin的头结点指向的chunk永远是最后freed的。
当再次destroy 0号gundam时,对应的chunk(已在tcache中)会再次被free,并修改fd指针,从而使chunk 0的fd指向自身,从而完成double free。
通过前面计算出的泄露地址与libc基地址的偏移,可以在获得泄露地址的前提下,动态算出libc基地址的,进而计算出free_hook_addr和system_addr地址。
再次构造gundam,分别以free_hook_addr、'/bin/sh\x00'字符串和system_addr作为name参数:
1.构造第1个gundam时,参数为free_hook_addr的地址,glibc会从tcache bin 中找到空闲的chunk,此时为chunk0,tcache_get 操作会将tcache->entries[tc_idx]指向的第一个chunk返回,并使entries[tc_idx]指向下一个chunk。由于chunk0的fd指向自身,tcache->entries[tc_idx]仍然指向chunk0,同时chunk0的fd被改写成free_hook_addr;
图片描述
从上图可以看到,chunk0的fd指向_free_hook地址。
2.构造第2个gundam时,参数为'/bin/sh\x00'字符串,glibc会从tcache bin 中找到空闲的chunk,此时仍然为chunk0,tcache_get 操作会使chunk0返回,使tcache->entries[tc_idx]指向前面chunk0的fd,即free_hook_addr,同时使chunk0的fd改写为'/bin/sh\x00'字符串;
图片描述
从上图可以看到,tcache->entries[tc_idx]已经指向_free_hook地址。
图片描述
从上图可以看到,chunk0的fd处为'/bin/sh\x00'字符串。
3.构造第3个gundam时,参数为system_addr地址,glibc会从tcache bin 中找到空闲的chunk,因为此时tcache->entries[tc_idx]指向free_hook_addr,就在free_hook_addr处写上system_addr。
图片描述
图片描述
从上图可以看到,free_hook_addr处已经写上system_addr。
通过以上3步操作,成功使system_addr与free_hook绑定,再执行1个destroy操作,即可启动system。
因此,选择destroy含有'/bin/sh\x00'字符串的1号gundam,成功执行shell:
图片描述

 

四、总结遇到的各种坑
调试过程遇到不少问题,首先是如何在本地系统上调试针对libc-2.26.so的程序gundam。通过在网上查找资料,将libc.so.6分别作为LD_PRELOAD环境变量和ELF()的参数进行设置,根据libc.so.6在内存地址空间的基地址和泄露地址的偏移量计算出偏移,exploit脚本能够执行到最后,但是无法执行用户输入命令,输入命令就会报出segment fault。vmmap显示的内存中的链接器仍然是本地系统glibc-2.27的链接器ld-2.27.so。同时,使用gdb直接加载gundam,在提示符下通过语句设置环境变量的方式,根本无法启动调试。最后想到是libc版本的问题,从服务器下载glibc的源码,进行编译,生成libc-2.26.so,解决了编译的问题。再就是动态跟进调试的问题。使用gdb直接加载gundam,并逐句调试的方式效率太低,最后想到在python脚本中使用gdb.attach()和pause()语句,可以动态跟进调试,效率提高不少。最后就是为什么会出现泄漏地址的问题?而这恰恰是解决问题的关键。查了网上很多资料,只讲了可以找到该泄漏地址,但没有讲这个问题的原因,给利用造成了很多不便。经过反复调试,弄清了造成地址泄露的原因是:两次以上构造和销毁gundam造成第8个chunk的bk处保留了原来指向unsortedbin的指针,导致再次构造gundam时,关键地址被泄露。


[注意] 欢迎加入看雪团队!base上海,招聘CTF安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!

最后于 2021-8-23 16:48 被dolphindiv编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回