确定glibc版本,以指定glibc运行程序

前言

CTF题目中经常会给出libc的文件,首先需要做的就是确定libc的版本,对程序进行针对性的攻击。最直接的确定方法就是直接运行该libc./libc.so.6,但通常由于本机ld版本与libc版本不匹配,发生段错误。

确定完大版本号还不够,因为就算是同一版本的libc也会有较大的变动。比方说,与最开始的2.29版本相比,gnu libc库的release/2.29/master分支的tcache_perthread_struct->counts的变量类型都变了;__libc_malloc中取用tcache的判定,也由判断指针是否为空变成了判断counts是否大于0。这些细节很大程度上影响了exploit的书写。

下面以pwnable.tw re-alloc的libc为例,写一写我的经验。

pwnable.tw re-alloc

题目分析

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)
FORTIFY:  Enabled

是一个典型的菜单类程序

1
2
3
4
5
6
7
8
$$$$$$$$$$$$$$$$$$$$$$$$$$$$
🍊      RE Allocator      🍊
$$$$$$$$$$$$$$$$$$$$$$$$$$$$
$   1. Alloc               $
$   2. Realloc             $
$   3. Free                $
$   4. Exit                $
$$$$$$$$$$$$$$$$$$$$$$$$$$$
  1. Alloc: index只能取0或1;size必须<=0x78;然后使用realloc(0, size)分配,通过源码可以看出,这就相当于malloc(size)。这里存在null byte overflow(然而没什么用似乎,一开始我一直卡在这里想如何利用,完全没注意到后一个漏洞。。。
1
2
3
/* realloc of null is supposed to be same as malloc */
if (oldmem == 0)
    return __libc_malloc (bytes);
  1. Realloc: size同样<=0x78。realloc有几种情况:请求大小自己可以满足;尝试向后扩展(top chunk或!inuse (next));__int_malloc请求新块。(这里存在UAF,输入size为0
  2. Free:realloc(ptr, 0)相当于free(ptr),同时将指针数组置空。
1
2
3
4
if (bytes == 0 && oldmem != NULL)
{
    __libc_free (oldmem); return 0;
}

libc_version: glibc 2.29 with tcache。该版本libc的源码。下一篇文章说一下如何确定的libc版本,如何确定的有tcache,以及如何使用指定libc运行程序。

此外,这个版本中出现了对tcache double free的一个检查机制

glibc heap 整理

目前 Linux 标准发行版中使用的堆分配器是 glibc 中的堆分配器:ptmalloc2。ptmalloc2 主要是通过 malloc/free 函数来分配和释放内存块。

需要注意的是,在内存分配与使用的过程中,Linux 有这样的一个基本内存管理思想,只有当真正访问一个地址的时候,系统才会建立虚拟页面与物理页面的映射关系

基本操作

malloc

malloc(size_t n)

  • 当 n=0 时,返回当前系统允许的堆的最小内存块。
  • 当 n 为负数时,由于在大多数系统上,size_t 是无符号数(这一点非常重要),所以程序就会申请很大的内存空间,但通常来说都会失败,因为系统没有那么多的内存可以分配。

free

free(void *p)

  • 当 p 为空指针时,函数不执行任何操作。
  • 当 p 已经被释放之后,再次释放会出现乱七八糟的效果,这其实就是 double free
  • 除了被禁用 (mallopt) 的情况下,当释放很大的内存空间时,程序会将这些内存空间还给系统,以便于减小程序所使用的内存空间。

背后的系统调用

这些函数背后的系统调用主要是 (s)brk 函数以及 mmap, munmap 函数。

How2heap总结

first-fit

如果一个chunk可用且足够大,那么就会使用这个chunk。

calc_tcache_idx

tcache(thread local caching)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* We want 64 entries.  This is an arbitrary limit, which tunables can reduce.  */
# define TCACHE_MAX_BINS		64
# define MAX_TCACHE_SIZE	tidx2usize (TCACHE_MAX_BINS-1)

/* Only used to pre-fill the tunables.  */
# define tidx2usize(idx)	(((size_t) idx) * MALLOC_ALIGNMENT + MINSIZE - SIZE_SZ)

/* When "x" is from chunksize().  */
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
/* When "x" is a user-provided size.  */
# define usize2tidx(x) csize2tidx (request2size (x))

/* With rounding and alignment, the bins are...
   idx 0   bytes 0..24 (64-bit) or 0..12 (32-bit)
   idx 1   bytes 25..40 or 13..20
   idx 2   bytes 41..56 or 21..28
   etc.  */

tcache拥有和fastbin差不多的结构。默认情况下,64个bin,每个bin最多7个chunk

64bit: IDX = (CHUNKSIZE - 0x11) / 0x10

How2heap--tcache_stashing_unlink_attack Hitcon2019--one_punch

在tcache中布置5个chunk,对应大小的small bin中布置2个chunk。修改倒数第二个small chunk的bk指向target,target->bk写入一个可写的位置。使用calloc触发,因为calloc调用_int_malloc不会使用tcache进行分配(除了_int_malloc遍历unsorted bin的尾部),只会往tcache中填充。

可以造成target->bk->fd位置写入大数字,并且target成为tcache中第一个,再次malloc就可以获得。

有点像fastbin reverse into tcache。

variation

在tcache中布置6个chunk,对应大小的small bin中布置2个chunk。(可以用unsorted bin切割的方式布置2个small bin)

修改倒数第二个small chunk的bk,calloc触发,只在bk->fd写入大数字。