CTF相关
-
常见的二进制程序漏洞
- 栈溢出,堆溢出,UAF,这个UAF比较经常被问到
-
上述漏洞怎么利用
一、栈溢出
1.1 基础栈结构
进入函数时:
- CALLXXX
- PUSH retadddr(call的下一条指令,目的:知道怎么回来)
- JMP XXX
- 进入到XXX
- PUSH RBP(保存栈基址,好恢复)
- MOV RBP,RSP(栈底抬上去)
- SUB RSP,XXh(抬高栈顶给局部函数预留空间)
退出函数时:
- leave
- mov rsp,rbp(把rsp弄回来)
- pop rbp(把rbp弄回来)
- ret
- pop rip(这个时候RIP就被retaddr的值取代了)
1.2 栈溢出
发生栈溢出的基本前提:
- 程序必须向栈上写入数据
- 写入的数据大小没有被良好地控制
1.3 主要分类
1.3.1 对抗DEP/NX保护技术
-
ret2text
- 即控制程序执行程序本身已有的的代码 (.text),我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码 (也就是 gadgets),这就是我们所要说的 ROP
-
ret2shellcode
- 即控制程序执行 shellcode 代码。shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell
- shellcode指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的shell。在栈溢出的基础上,shellcode所在的区域具有可执行权限
-
ROP
- 利用程序中已经有的小片段(gadgets),控制程序的执行流,以ret结尾的指令,方便连续地控制程序地的执行流
- 满足条件:
- 程序存在溢出,并且可以控制返回地址
- 可以找到满足条件的gadgets以及相应gadgets的地址(如果不固定,就需要想办法动态获取对应的地址)
- 过程:
- 精心构造栈结构
- 利用返回地址ret的跳转特点
- 不在栈中或bss段执行代码,而是在程序的可执行段寻找可以执行的小组件(gadget)
- 把小组件串起来,构造而成的就叫ROP链
-
ret2syscall
- ret2syscall,即控制程序执行系统调用,获取 shell
-
ret2libc
- 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)。一般情况下,我们会选择执行 system("/bin/sh"),故而此时我们需要知道 system 函数的地址
- libc中应有尽有
- 然而ASLR/PIE技术使得程序基地址和libc基地址每次加载的都不一样
- 延迟绑定机制
- 原理:
- 因为动态链接库的加载机制是lazy原则(got表),即用时加载,第二次就不用加载
- 注意:只能泄露已经执行过一次的函数的libc地址
- 利用思路:
- 泄露GOT表中某个函数的libc地址
- 在libc中找到system,“bin/sh”这个函数的相对偏移
- 得到system的地址和“bin/sh”的地址
- 构造ROP链,成功利用
- 具体过程:
- 在执行了一次某函数之后,GOT表中就会把一个函数在程序中的终极偏移存起来
- 终极偏移=libc基址(每次加载都不一样)+库内函数相对偏移
- System=libc基址+system在库中的相对偏移
- 如果开启了PIE
- 结合其他漏洞进行先行泄露
1.3.2 对抗Canary保护技术
-
泄露Canary的值
- 泄露:fs:28h内的值(IDA静态分析)
-
覆写副本值
- 需要进行位置的爆破
- mapped段和glibc的偏移是固定的
-
劫持stack_chk_fail
-
可以修改全局偏移表(GOT)中存储的_stack_chk_fail函数地址,便可以在触发Canary检查失败时,跳转到指定的地址继续执行
-
stack smashing
- 当Canary被覆盖之后,会call到_stack_chk_fail打印argv[0]这个指针指向的字符串,默认是程序的名字
- 如果我们把它覆盖为其他的地址时,它就会把其他内存地址的信息给打印出来
-
逐字节爆破(BROP)
-
攻击条件:
- 远程程序必须先存在一个已知得stack overflow的漏洞,而且攻击者知道如何触发这个漏洞
- 服务程序在crash之后会重新复活,并且复活的进程不会被re-rand(意味着虽然有ASLR保护,但是复活的进程和之前的进程的地址随机化与Canary是一样的)。这个需求其实是合理的,因为当前像Nginx,MySQL,Apache、OpenSSH,Samba等服务器应用都是符合这种特性的
-
核心就是想办法泄露程序的更多信息
- 由于我们不知道被攻击程序的内存布局,所以首先要做的事情就是通过某种方法从远程服务器dump出该程序的内存到本地
write(int sock, void *buf, int len)
-
BROP基本思路
-
判断栈溢出的长度
- 直接暴力枚举,因为Canary被覆盖或者返回地址被覆盖,会导致程序Crash
-
逐字节爆破Canary(如果没有开,就跳过这一步)
- 一个一个字节顺序地进行尝试来还原出真实的Canary
-
寻找stop gadget
-
寻找useful gadget(尤其是Brop gadget)
-
寻找可用的PLT表项
-
利用PLT表中的puts(或者是write)函数,配合useful gadget,来远程dump信息
-
寻找stop gadget
- 但目前为止,已经得到了合适的Canary来绕开stack canary的保护,但如果我们把返回地址覆盖成某些我们随意选取的内存地址的话,程序有很大可能性会crash
- 存在另外一种情况,即该return address指向了一块代码区域,当程序的执行流跳到那段区域之后,程序并不会crash,而是进入了无限循环,这时程序仅仅是hang在了那里,攻击者能够一直保持连接状态。于是,我们把这种类型的gadget,称位stop gadget,可以不断爆破覆盖返回地址尝试stop gadget
-
-
对于Windows可以计算出来
- Canary = __security_cookie ^ ebp
-
1.3.3 New
-
栈溢出长度不够:栈劫持
- 如果可以在.bss段等已知位置进行写入,就可以提前进行栈布局。通过覆盖栈上存储的saved rbp和saced rip,将栈进行劫持
- leave
- mov rsp,rbp(把rsp弄回来)
- pop rbp(把rbp弄回来)
- ret
- pop rip(这个时候rip就被retaddr的值取代了)
-
SROP
-
SROP的全称是Sigreturn Oriented Programming。在这里‘sigreturn’是一个系统调用,它在unix系统发生signal的时候会被间接地调用
-
signal机制
- signal机制是类unix系统中进程之间相互传递信息地一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用kill来发送软中断信号
- 内核会为该进程保存对应的上下文,主要是将所有寄存器压入栈中,以及压入signal信息,以及指向sigreturn的系统调用地址。此时栈的结构,我们称ucontext以及siginfo这一段为Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的sinal handler中处理相应的signal。因此,当signal handler执行完之后,就会执行sigreturr代码
- 仔细回顾一下内核在signal信号处理的过程中的工作,我们可以发现,内核主要做的工作就是为进程保存上下文,并且恢复上下文。这个主要的变动都在Signal Frame中。但是徐娅注意的是:
- Signal Frame被保存在用户的地址空间中,所以用户是可以读写的
- 由于内核与信号处理程序无关,它并不会去记录这个signal对应的Signal Frame,所以当执行sigreturn系统调用时,此时的Signal Frame并不一定是之前内核为用户进程保存的Signal Frame
-
利用思路:
-
如果系统执行一系列函数,只需要做两处修改即可
- 控制栈指针
- 把原来RIP指向的syscall gadget换成syscall;ret gadget
-
在构造ROP攻击时,需要满足下面的条件:
-
可以通过栈溢出来控制栈的内容
-
需要知道相应的地址
-
“/bin/sh”
-
Signal Frame
-
syscall
-
sigreturn
-
-
需要有足够大的空间塞下整个sigal frame
-
-
-
二、堆溢出
2.1 基础
- malloc
- malloc(size_t n)
- 当n = 0时,返回当前系统允许的堆的最小内存块
- 当n为负数时,由于在大多数系统中,size_t是无符号数(这一点非常重要),所以程序就会申请很大的内存空间,但通常来说都会失败,因为系统没有那么多的内存可以分配
- free
- free(void* p)
- 当p为空指针时,函数不执行任何操作
- 当p已经释放之后,再次释放会出现乱七八糟的效果,这其实就是double free
- 除了被禁用(mallopt)的情况下,当释放很大的内存空间时,程序会将这些内存空间还给系统,以便于减少程序所使用的空间
- 内存分配后的系统调用
- 在前面提到的函数中,无论是malloc还是free函数,我们动态申请和释放内存时,都经常会使用,但是它们并不是真正与系统交互的函数,这些函数背后的系统调用主要是(s)brk函数以及mmap、munmap函数
2.2 堆相关数据结构
堆的操作相当的复杂,那么在glibc内部必然也有精心设计的数据结构来管理它。与堆相应的数据结构主要分为:
- 宏观结构,包含堆的宏观信息,可以通过这些数据结构索引堆的基本信息
- 微观结构,用于具体处理堆的分配与回收中的内存块
2.2.1 微观结构 malloc_chunk
在程序的执行过程中,我们称由malloc申请的内存为chunk。这块内存在ptmalloc内部用malloc_chunk结构体来表示。当程序申请的chunk被free后,会被加入到相应的空闲管理列表中
非常有意思的是,无论一个chunk的大小如何,处于分配状态还是释放状态,它们都使用一个统一的结构,虽然它们使用了同一个数据结构,但是根据是否被释放,它们的表现形式会有所不同:
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
- prev_size
- 如果该chunk的物理相邻的前一地址chunk(两个指针的地址差值为前一个chunk的大小)是空闲的话,那该字段记录的是前一个chunk的大小(包括chunk头)。否则,该字段可以用来存储物理相邻的前一个chunk的数据。这里的前一chunk指的是较低地址的chunk
- size
- 该chunk的大小,必须是2*SIZE_SZ的整数倍。如果申请的内存大小不是2*SIZE_SZ的整数倍,会被转换满足大小的最小的2*SIZE_SZ的倍数。32位系统中,SIZE_SZ是4;64位系统中,SIZE_SZ是8。该字段的低三个比特位对chunk的大小没有影响,它们从高到低分别表示:
- NO_MAIN_ARENA
- 记录当前chunk是否属于主线程,1表示不属于,0表示属于
- IS_MAPPED
- 记录当前chunk是否由mmap分配的
- PREV_INUSE
- 记录当前一个chunk块是否被分配。一般来说,堆中第一个被分配的内存块的size字段的P位都会被置为1,以便于防止访问前面的非法内存。当一个chunk的size的P位为0时,我们能通过prev_size字段来获取上一个chunk的大小以及地址。这也方便进行chunk间的合并
- NO_MAIN_ARENA
- 该chunk的大小,必须是2*SIZE_SZ的整数倍。如果申请的内存大小不是2*SIZE_SZ的整数倍,会被转换满足大小的最小的2*SIZE_SZ的倍数。32位系统中,SIZE_SZ是4;64位系统中,SIZE_SZ是8。该字段的低三个比特位对chunk的大小没有影响,它们从高到低分别表示:
- fd、bk
- chunk处于分配时,从fd字段开始是用户的数据。chunk空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下:
- fd:指向下一个(非物理相邻)空闲的chunk
- bk:指向上一个(非物理相邻)空闲的chunk
- 通过fd和bk可以将空闲的chunk块加入到空闲的chunk块链表中进行统一管理
- chunk处于分配时,从fd字段开始是用户的数据。chunk空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下:
- fd_nextsize、bk_nextsize,也是只有chunk空闲的时候才使用,不过其用于较大的chunk(large chunk)
- fd_nextsize:指向前一个与当前chunk大小不同的第一个空闲块,不包含bin的头指针
- bk_nextsize:指向后一个与当前chunk大小不同的第一个空闲块,不包含bin的头指针
- 一半空闲的large chunk在fd的遍历顺序中,按照由大到小的顺序排列,这样做可以避免在寻找合适的chunk时挨个遍历
2.2.1.2 已分配的chunk
一个已经分配的chunk的样子如下,我们称前两个字段称为chunk header,后面的部分称位user data。每次malloc申请内得到内存指针,其实指向user data的起始处
当一个chunk处于使用状态时,它的下一个chunk的prev_size域无效,所以下一个chunk的该部分也可以被当前chunk使用,这就是chunk中的空间复用
An allocated chunk looks like this:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2.2.1.2 已释放的chunk
被释放的chunk被记录在链表中(可能是循环双向链表,可能是单向链表),具体结构如下:
Free chunks are stored in circular doubly-linked lists, and look like this:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | Size of chunk, in bytes |A|0|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Forward pointer to next chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Back pointer to previous chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unused space (may be 0 bytes long) .
. .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' | Size of chunk, in bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|0|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2.2.2 bin
我们曾经说过,用户释放掉的chunk不会马上归还给系统,ptmalloc会统一管理heap和mmap映射区域中的空闲的chunk。当用户再一次请求分配内存时,ptmalloc分配器会试图在空闲的chunk中挑选一块合适的给用户。这样可以避免频繁的系统调用,降低内存分配时的开销
在具体的实现中,ptmalloc采用分箱式方法对空闲的chunk进行管理。首先,它会根据空闲的chunk的大小,以及使用状态将chunk初步分为4类:
- fast bins
- small bins
- large bins
- unsorted bin
每类中仍然有更细的划分,相似大小的chun会用双向链表链接起来,也就是说,每类bin的内部仍然会有多个互不相关的链表来保存不同大小的chunk
对于small bins、large bins、unsorted bin来说,ptmalloc将它们维护在同一个数组中,这些bin对应的数据结构在malloc_state中
- unsorted bin,字如其面,这里面的chunk没有进行排序,存储的chunk比较杂
- 索引从2到63的bin称位small bin,同一个small bin链表中的大小相同。两个相邻索引的small bin链表中的chunk大小相差的字节数为2个机器字长,即32位相差8个字节,64位相差16字节
- small bins后面的bin被称作large bins。large bins中的每一个bin都包含一定范围内的chunk,其中的chunk按fd指针的顺序从大到小排列。相同大小的chunk同样按照最近使用顺序排列
- ptmalloc为了提供分配的速度,会把一些小的chunk先放到fast bins的容器内。而且,fastbin容器中的chunk的使用标记总是被置位的,所以不满足上面的原则
2.2.3 宏观结构arena
struct malloc_state
{
/* Serialize access. */
__libc_lock_define (, mutex);
/* Flags (formerly in max_fast). */
int flags;
/* Set if the fastbin chunks contain recently inserted free blocks. */
/* Note this is a bool but not all targets support atomics on booleans. */
int have_fastchunks;
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];
/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];
/* Linked list */
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;
};
-
__libc_lock_define (, mutex);
- 该变量用于控制程序串行访问同一个分配区,当一个线程获取了分配区之后,其它线程要想访问该分配区,就必须等待该线程完成后才能够使用
-
flags
- flags记录了分配区的一些标志,不如bit 0记录了分配区是否有fastbin chunk,bit 1标识分配区是否能返回连续的虚拟地址空间
-
fastbinsY[NFASTBINS]
- 存放每个fast chunk链表头部分指针
-
top
- 指向分配区的top chunk
-
last_remainder
- 最新的chunk分割之后剩下的那部分
-
bins
- 用于存储unstored bin,small bins和large bins的chunk链表
-
binmap
- ptmalloc用一个bit来标识某一个bin中是否包含空闲chunk
2.3 Use After Free
简单地说,Use After Free就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况:
- 内存块被释放后,其对应的指针被设置为NULL,然后再次使用,自然程序会崩溃
- 内存块被释放后,其对应的指针没有被设置为NULL,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转
- 内存块被释放后,其对应的指针没有被设置为NULL,但是在它下一次使用之前,有代码块对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能出现奇怪的问题
- 而我们一般所指的Use After Free漏洞主要是后两种。此外,我们一般称被释放后没有被设置为NULL的内存指针为dangling pointer
2.4 Fastbin Attack
Fastbin Attack是一类漏洞的利用方法,是指所有基于fastbin机制的漏洞利用方法,这类利用的前提是:
- 存在堆溢出、use-after-free等能控制chunk内容的漏洞
- 漏洞发生于fastbin类型的chunk中
如果细分的话,可以做如下的分类:
- Fastbin Double Free
- House of Spirit
- Alloc to Stack
- Arbitrary Alloc
其中,前两种主要漏洞侧重于利用free函数释放真的chunk或伪造的chunk,然后再次申请chunk进行攻击,后两种侧重于故意修改fd指针,直接利用malloc申请指定位置chunk进行攻击
fastbin attack存在的原因在于fastbin是使用单链表来维护释放的堆块的,并且由fastbin管理的chunk即使被释放,其next_chunk的prev_inuse位也不会被清空。我们来看一下fastbin是怎样管理空闲chunk的
int main(){
void *chunk1, *chunk2, *chunk3;
chunk1 = malloc(0x30);
chunk2 = malloc(0x30);
chunk3 = malloc(0x30);
//进行释放
free(chunk1);
free(chunk2);
free(chunk3);
return 0;
}
程序执行完后的内存情况:
0x602000: 0x0000000000000000 0x0000000000000041 <== chunk1
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000041
0x602050: 0x0000000000602000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000041
0x602090: 0x0000000000602040 0x0000000000000000
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000020f41
2.4.1 Fastbin Double Free
Fastbin Double Free是指fastbin的chunk可以被多次释放,因此可以再fastbin链表中存在多次。这样导致的后果是多次分配可以从fastbin链表中取出同一个堆块,相当于多个指针指向同一个堆块,结合推块的数据内容可以实现类似于类型混淆(type confused)的效果
Fastbin Double Free能够成功利用主要有两部分的原因:
- fastbin的堆块释放后next_chunk的pre_inuse位不会被清空
- fastbin在执行free的时候仅验证了main_arena直接指向的块,即链表指针头部的块。对于链表后面的块,并没有进行验证
/* Another simple check: make sure the top of the bin is not the record we are going to add(i.e.,double free).*/
if(_builtin_expect(old == p, 0)){
errstr = "double free or corruption(fasttop)";
goto errout;
}
简单绕过:
int main(void){
void *chunk1, *chunk2, *chunk3;
chunk1 = malloc(0x30);
chunk2 = malloc(0x30);
//进行释放
free(chunk1);
free(chunk2);
free(chunk1);
return 0;
}
2.5 Unlink
我们在利用unlink所造成的漏洞时,其实就是对进行unlink chunk进行内存布局,然后借助unlink操作来达成修改指针的效果
我们先来回顾一下unlink的目的与过程,其目的是把一个双向链表中的空闲块拿出来(例如free时和目前物理相邻的free chunk进行合并)。其基本的过程是:
2.5.1 古老的unlink
假设有空间连续的两个chunk(Q,Nextchunk),其中Q处于使用状态、Nextchunk处于释放状态。那么如果我们通过某种方式(比如溢出)将Nextchunk的fd和bk指针修改为指定的值。则当我们free(Q)时:
- glibc判断这个块是small chunk
- 判断向前合并,发现前一个chunk处于使用状态,不需要前向合并
- 判断后向合并,发现后一个chunk处于空闲状态,需要合并
- 继而对Nextchunk采取unlink操作
那么unlink具体执行的效果是什么样子呢?我们可以来分析一下(这里P即为Nextchunk):
- FD = P->fd = target addr -12
- BK = P->bk = expect value
- FD->bk - BK,即*(target addr - 12 + 12) = BK = expect value
- BK->fd = FD,即*(expect value + 8) = FD = target addr -12
最后实现的效果就是:*(target addr) = expect value,可以将恶意代码存放在expect value处,将target addr里面存的值为expect value的内存地址
看起来我们似乎可以通过 unlink 直接实现任意地址读写的目的,但是我们还是需要确保 expect value +8 地址具有可写的权限。
比如说我们将 target addr 设置为某个 got 表项,那么当程序调用对应的 libc 函数时,就会直接执行我们设置的值(expect value)处的代码。需要注意的是,expect value+8 处的值被破坏了,需要想办法绕过。
对于musl libc就是利用的上述双向链表算法,没有任何检查
2.5.2 当前的unlink(glibc)
// fd bk
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
此时
- FD->bk = target addr - 12 + 12=target_addr
- BK->fd = expect value + 8
相当于我们是释放Q,来触发unlink Nextchunk,然后利用unlink Nextchunk实现任意地址写
那么我们上面所利用的修改 GOT 表项的方法就可能不可用了。但是我们可以通过伪造的方式绕过这个机制,相当于抽象虚拟了两个chunk,一个是fakeFD当作Nexchunk的下一个chunk用来存放伪造的bk指针,一个是fakeBK当作Nexchunk的上一个chunk用来存放伪造的fd指针
首先我们通过覆盖,将 nextchunk 的 FD 指针指向了 fakeFD,将 nextchunk 的 BK 指针指向了 fakeBK 。那么为了通过验证,我们需要
-
fakeFD -> bk == P
<=>*(fakeFD + 12) == P
-
fakeBK -> fd == P
<=>*(fakeBK + 8) == P
当满足上述两式时,可以进入 Unlink 的环节,进行如下操作:
-
fakeFD -> bk = fakeBK
<=>*(fakeFD + 12) = fakeBK
-
fakeBK -> fd = fakeFD
<=>*(fakeBK + 8) = fakeFD
如果让 fakeFD + 12 和 fakeBK + 8 指向同一个指向 P 的指针,那么:
*P = P - 8
*P = P - 12
利用思路:
- 条件
- UAF ,可修改 free 状态下 smallbin 或是 unsorted bin 的 fd 和 bk 指针
- 已知位置存在一个指针指向可进行 UAF 的 chunk
- 效果
使得已指向 UAF chunk 的指针 ptr 变为 ptr - 0x18
- 思路
设指向可 UAF chunk 的指针的地址为 ptr
- 修改 fd 为 ptr - 0x18
- 修改 bk 为 ptr - 0x10
- 触发 unlink
ptr 处的指针会变为 ptr - 0x18
2.6 堆中的Off-By-One
严格来说Off-By-One漏洞是一种特殊的溢出漏洞,off-By-One指程序向缓冲区中写入时,写入的字节数超过了这个缓冲区本身所申请的字节数并且只越界了一个字节
off-by-one是指单字节缓冲区溢出,这种漏洞的产生往往与边界验证不严和字符串操作有关,当然也不排除写入的size正好就只多了一个字节的情况。其中边界验证不严通常包括:
- 使用循环语句向堆块中写入数据时,循环的次数设置错误(这在 C 语言初学者中很常见)导致多写入了一个字节。
- 字符串操作不合适
一般来说,单字节溢出被认为是难以利用的,但是因为 Linux 的堆管理机制 ptmalloc 验证的松散性,基于 Linux 堆的 off-by-one 漏洞利用起来并不复杂,并且威力强大。 此外,需要说明的一点是 off-by-one 是可以基于各种缓冲区的,比如栈、bss 段等等,但是堆上(heap based) 的 off-by-one 是 CTF 中比较常见的。我们这里仅讨论堆上的 off-by-one 情况
2.7 Unsorted Bin Attack
- 当一个较大的chunk被分割成两半后,如果剩下的部分大于MINSIZE,就会被放到Unsorted Bin中
- 释放一个不属于fastbin的chunk,并且该chunk不和top chunk紧邻时,该chunk会被首先放到unsorted bin中
- 当进行malloc_consolidate时,可能会把合并后的chunk放到unsorted bin中,如果不是和top chunk近邻的话
基本使用情况 :
- Unsorted Bin 在使用的过程中,采用的遍历顺序是 FIFO,即插入的时候插入到 unsorted bin 的头部,取出的时候从链表尾获取。
- 在程序 malloc 时,如果在 fastbin,small bin 中找不到对应大小的 chunk,就会尝试从 Unsorted Bin 中寻找 chunk。如果取出来的 chunk 大小刚好满足,就会直接返回给用户,否则就会把这些 chunk 分别插入到对应的 bin 中
在 glibc/malloc/malloc.c 中的 _int_malloc
有这么一段代码,当将一个 unsorted bin 取出的时候,会将 bck->fd
的位置写入本 Unsorted Bin 的位置:
/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
换而言之,如果我们控制了 bk 的值,我们就能将 unsorted_chunks (av)
写到任意地址
2.8 House of Orange
house of orange利用方法来自于 Hitcon CTF 2016 中的一道同名题目。由于这种利用方法在此前的 CTF 题目中没有出现过,因此之后出现的一系列衍生题目的利用方法我们称之为 House of Orange
首先需要知道目标漏洞是堆上的漏洞但是特殊之处在于题目中不存在free函数或其他释放堆块的函数,我们一般知道想要利用堆漏洞,需要对堆块进行malloc和free操作,但是在House of Orange利用中无法使用free函数,因此House of Orange核心就是通过漏洞利用来获得free的效果
假设目前的top chunk已经不满足malloc的分配需求,首先我们在程序中的malloc调用会执行到_int_malloc函数中,在_int_malloc函数中,会依次检验fastbin、small bins、unsorted bin、large bins是否可以满足分配要求,假如都不能满足用户申请堆内存的要求,需要执行sysmalloc来向系统申请更多的空间。但是对于堆来说有mmap和brk两种分配方式,我们需要让堆以brk的形式拓展,之后原有的top chunk会被置于unsorted bin中
有以下要求:
- 伪造的size必须要对齐到内存页
- size要大于MINISIZE(0x10)
- size要小于之后申请的chunksize + MINISIZE(0x10)
- size的prev inuse位必须为1
2.9 tcache
在2.26及以后的glibc版本中加入了tcache机制管理长度较小的堆块,其优先级很高,会先于全部的bin来处理。每个链表的个数是一定的,当缓存链表装满时,分配方式就与之前版本的malloc相同
- 单链表结构
- LIFO分配策略
- 申请堆块时不检查size字段
- 不检查double free
可以看出来tcache相当于一种弱化的fastbin,fastbin上的所有攻击方法都可以在tcache中实现,并且攻击者无需伪造堆块,利用起来更加容易
2.10 IO_FILE Related
2.10.1 FILE结构体概述
FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。 FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中。我们常定义一个指向 FILE 结构的指针来接收这个返回值
FILE 结构定义在 libio.h 中,如下所示
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
进程中的 FILE 结构会通过_chain 域彼此连接形成一个链表,链表头部用全局变量_IO_list_all 表示,通过这个值我们可以遍历所有的 FILE 结构。
在标准 I/O 库中,每个程序启动时有三个文件流是自动打开的:stdin、stdout、stderr。因此在初始状态下,_IO_list_all 指向了一个有这些文件流构成的链表,但是需要注意的是这三个文件流位于 libc.so 的数据段。而我们使用 fopen 创建的文件流是分配在堆内存上的
我们可以在 libc.so 中找到 stdin\stdout\stderr 等符号,这些符号是指向 FILE 结构的指针,真正结构的符号是
- _IO_2_1_stderr_
- _IO_2_1_stdout_
- _IO_2_1_stdin_
但是事实上_IO_FILE 结构外包裹着另一种结构_IO_FILE_plus,其中包含了一个重要的指针 vtable 指向了一系列函数指针
在 libc2.23 版本下,32 位的 vtable 偏移为 0x94,64 位偏移为 0xd8
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}
vtable 是 IO_jump_t 类型的指针,IO_jump_t 中保存了一些函数指针,在后面我们会看到在一系列标准 IO 函数中会调用这些函数指针
void * funcs[] = {
1 NULL, // "extra word"
2 NULL, // DUMMY
3 exit, // finish
4 NULL, // overflow
5 NULL, // underflow
6 NULL, // uflow
7 NULL, // pbackfail
8 NULL, // xsputn #printf
9 NULL, // xsgetn
10 NULL, // seekoff
11 NULL, // seekpos
12 NULL, // setbuf
13 NULL, // sync
14 NULL, // doallocate
15 NULL, // read
16 NULL, // write
17 NULL, // seek
18 pwn, // close
19 NULL, // stat
20 NULL, // showmanyc
21 NULL, // imbue
};
2.10.2 伪造vtable劫持程序流程
前面我们介绍了 Linux 中文件流的特性(FILE),我们可以得知 Linux 中的一些常见的 IO 操作函数都需要经过 FILE 结构进行处理。尤其是_IO_FILE_plus 结构中存在 vtable,一些函数会取出 vtable 中的指针进行调用
因此伪造 vtable 劫持程序流程的中心思想就是针对_IO_FILE_plus 的 vtable 动手脚,通过把 vtable 指向我们控制的内存,并在其中布置函数指针来实现
因此 vtable 劫持分为两种:
- 一种是直接改写 vtable 中的函数指针,通过任意地址写就可以实现
- 另一种是覆盖 vtable 的指针指向我们控制的内存,然后在其中布置函数指针
在2.24版本的glibc中,全新加入了针对_IO_FILE_plus的vtable的劫持的检测措施,glibc会在调用虚函数之前首先检查vtable地址的合法性。首先会验证vtable是否位于_IO_vtable段中,如果满足条件就执行,否则会调用_IO_vtable_check做进一步检查
泄露flags信息,覆盖flags泄露信息,用这个方法leak的核心就是_IO_IS_APPENDING这个flag值,将这个flag搞成1之后,就可以通过修改_IO_buf_base来完成leak
而在赛题中,只需要爆破半个字节就可以拿到stdout的地址,然后从stdout开头开始覆盖,改掉flag之后再低位覆盖_IO_buf_base就可以完成leak,中间的三个变量在输出的过程中都不怎么用得到,直接盖成0就行了
2.11 Glibc-2.29新特性
2.11.1 tcache新增防护机制
typedef struct tcache_entry{
struct tcache entry *next;
struct tcache perthread_struct *key;
}tcache_entry;
/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = tmp->next)
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}
这里会对tcache
链表上的所有chunk进行对比,检测是否有重复,这让原本在glibc-2.27
和glibc-2.28
肆虐的tcache double free
攻击很难实施,但是鉴于tcache
的特性,tcache
的利用还是要比其他的bins
方便很多
2.11.2 unsorted bin
原本这里仅仅有size检查。
bck = victim->bk;
size = chunksize (victim);
mchunkptr next = chunk_at_offset (victim, size);
if (__glibc_unlikely (size <= 2 * SIZE_SZ)
|| __glibc_unlikely (size > av->system_mem))
malloc_printerr ("malloc(): invalid size (unsorted)");
if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ)
|| __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
malloc_printerr ("malloc(): invalid next size (unsorted)");
if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
if (__glibc_unlikely (bck->fd != victim)
|| __glibc_unlikely (victim->fd != unsorted_chunks (av)))
malloc_printerr ("malloc(): unsorted double linked list corrupted");
if (__glibc_unlikely (prev_inuse (next)))
malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");
然后在glibc-2.29
中,增加了如下检查:
- 对下一个相邻chunk的size检查
- 对下一个相邻chunk的
prev_size
进行检查 - 检查
unsorted bin
双向链表的完整性,对unsorted bin attack
可以说是很致命的检查 - 对下一个chunk的
prev_inuse
位进行检查
这么多的检查,将使得unsorted bin
更难利用。
下面这个检查早在glibc-2.28
就有了,这里提一下,主要是防护unsorted bin attack
2.11.3 top chunk
对于top chunk
增加了size
检查,遏制了House of Force
攻击。
victim = av->top;
size = chunksize (victim);
if (__glibc_unlikely (size > av->system_mem))
malloc_printerr ("malloc(): corrupted top size");
三、Linux内核
- Linux保护机制更多,限制更多
- SMEP
- 即Supervisor Mode Execution Protection(管理模式执行保护)。如果处理器处于ring0模式,并试图执行有user数据的内存时,就会触发一个页错误,用来保护内核使其不允许执行用户空间代码
3.1 ret2user
简单举例:
- 由内核栈溢出
- 没有开启KALSR、Canary、SMEP
驱动通过_copy_from_user
将用户输入的数据读入到了内核栈中的buffer,由于没有限制长度,所以是简单的栈溢出,类似ret2shellcode(没开SMEP)
-
ret2usr 攻击利用了 用户空间的进程不能访问内核空间,但内核空间能访问用户空间 这个特性来定向内核代码或数据流指向用户控件,以
ring 0
特权执行用户空间代码完成提权等操作 -
溢出后,直接将返回地址覆写为用户态调用
commit_creds(prepare_kernel_cred(0));
的函数的地址进行提权,然后使用iretq指令从内核态返回到用户态,从而get shell -
所以还需要伪造好之后执行iretq指令会弹回到寄存器中的值,这里有两种伪造的思路:
- 在存放全局变量的bss段上进行伪造,并修改rsp指针进行栈劫持
- 直接在内核栈上进行伪造
-
从核心态拿到用户态的shell
3.2 内核ROP
再举一例:
- 有内核栈溢出
- 开启了SMEP
- 没有开启KALSR、Canary
系统根据cr4寄存器的值判断是否开启了smep,然而rc4寄存器可以使用mov指令进行修改,这提供了两种思路:
- 利用ROP,直接执行
commit_creds(prepare_kernel_cred(0));
,然后iret返回用户空间 - 利用ROP设置cr4寄存器的值,关闭smep,然后进行ret2user攻击
qwd2018_core
3.3 Double_fetch
Double Fetch 从漏洞原理上属于条件竞争漏洞,是一种内核态与用户态之间的数据访问竞争
在 Linux 等现代操作系统中,虚拟内存地址通常被划分为内核空间和用户空间。内核空间负责运行内核代码、驱动模块代码等,权限较高。而用户空间运行用户代码,并通过系统调用进入内核完成相关功能。通常情况下,用户空间向内核传递数据时,内核先通过通过 copy_from_user 等拷贝函数将用户数据拷贝至内核空间进行校验及相关处理,但在输入数据较为复杂时,内核可能只引用其指针,而将数据暂时保存在用户空间进行后续处理。此时,该数据存在被其他恶意线程篡改风险,造成内核验证通过数据与实际使用数据不一致,导致内核代码执行异常
3.4 UAF
内核的UAF和用户态的差不多,不同的是内核使用的是slab/slub分配器来管理堆块
四、格式化字符串漏洞利用技术
格式化字符串漏洞(format string)主要是printf函数家族的问题。printf、fprintf、sprintf、snprintf等格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数
重要特性:
- printf()函数的参数个数不固定
- printf()函数的参数分成两个部分(第一个参数中的格式化字符串的数量决定了后面参数的数量)
Printf()的栈操作:
五、Linux保护手段
【面试原题】:上述漏洞有什么防护措施?
常见的保护机制:
- Canary
- Fortify
- NX/DEP
- PIE/ASLR
- RELRO
5.1 NX保护
作用:
将数据(堆、栈)所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令
编译选项:
- 关闭:-z execstack
- 开启:-z noexecstack
5.2 PIE保护
作用:
使得程序地址空间分布随机化,增加ROP等利用的难度(因为地址都是不确定的了)
编译选项:
- 关闭:-no-pie
- 开启:-pie -fPIC
5.3 Canary保护
作用:
函数开始执行的时候会先往栈里插入Canary的值,当函数真正返回的时候会验证Canary值是否合法,如果不合法就停止程序运行。可以防止栈溢出覆盖返回地址
编译选项:
- 关闭:-fno-stack-protector
- 启用(只为局部变量中含有char的函数插入保护代码):-fstack-protector
- 启用(为所有函数插入保护代码):-fstack-protector-all
5.4 Fortify保护
作用:
主要用来防止格式化字符串漏洞。包含%n的格式化字符串不能位于程序内存中的可写地址。当使用位置参数时,必须使用范围内的所有参数,如果要使用%7$x,必须同时使用1$,2$,3$,4$,5$,6$
编译选项:
- 关闭:-D_FORTIFY_SOURCE = 0
- 开启:-D_FORTIFY_SOURCE = 2
5.5 RELRO保护
作用:
设置符号重定向表为只读并在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)表攻击
编译选项:
- 开启(部分):-z lazy
- 开启(完全):-z now