Linux下堆溢出 1-unlink基本原理
1 前言
在CTF比赛PWN常有关于unlink的题目,看了网上大佬的文章,有些难点都是一言带过,对于刚入门的同学有难度,个人认为难点如下:
- free chunk向前和向后合并
- 两个检查点的绕过
- unlink后chunk1和chunk1[3]的同为一体
- unlink后chunk1的移形换位
2 基础知识
2.1 向前和向后合并
当两个free的堆块在物理上相邻时,会将他们合并,并将原来free的堆块在原来的链表中解链,加入新的链表中,但这样的合并是有条件的,向前或向后合并。以当前的chunk为基准,将preivous free chunk合并到当前chunk称为向后合并,将后面的free chunk合并到当前chunk就称为向前合并。注意这里的前后是指物理内存而非fd、bk所指的链表堆块。
2.1.1 向后合并
/*malloc.c int_free函数中*//*这里p指向当前malloc_chunk结构体,bck和fwd分别为当前chunk的向后和向前一个free chunk*/
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;//修改指向当前chunk的指针,指向前一个chunk。
p = chunk_at_offset(p, -((long) prevsize));
unlink(p, bck, fwd);
}
#define chunk_at_offset(p, s) ((mchunkptr)(((char*)(p)) + (s)))
unlink函数可简单理解定义如下:
#define unlink(P, BK, FD) {
FD = P->fd;
BK = P->bk;
FD->bk=BK;
BK->fd=FD;
...
}
当要free chunk2 前发现上一个块chunk1也是free状态的,就抱大腿合并起来,指向chunk2的ptr指针现在指向chunk1,size也变为size+presize也就是这样:
新的free的chunk1不会很快回到操作系统,于是需要从所在的free的chunk链中进行unlink(有fd指针和bk指针)再放到unsorted bin中保存。
2.1.2 向前合并
if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
/* consolidate forward */
if (!nextinuse) {
unlink(nextchunk, bck, fwd);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);
#define clear_inuse_bit_at_offset(p, s)
(((mchunkptr)(((char*)(p)) + (s)))->size &= ~(PREV_INUSE))
向前合并不修正P的指针,只增加size大小。合并后会将合并后新的chunk加入unsorted bin中,加入第一个可用的chunk之前,更改自己的size字段将前一个chunk标记为已用,再将后一个chunk的previous size改为当前chunk的大小。
2.2 chunk结构体
2.2.1 使用中的chunk
一个使用中的 chunk(就是还没有 free 掉)在内存中的样子如下(从上到下,地址越来越大),通过 malloc 返回的是 mem 指向的地址。
- size of previous chunk
这是前面一个 chunk 的大小,这里的前面一个指的是低地址的那一个 - size of chunk
这个 chunk 的大小。而且这个 chunk 的大小一定是 8 的倍数。所以低三位是 0,由于低三位是 0,是固定值,可以将这些固定值,用来表示其他的含义,反正计算大小的时候,统一把他们当成 0 就好了。下面从高到低介绍这些标志的意思:
- A:是不是「主分配区」分配的内存 1 表示不是主分配区分配的,0 表示是主分配区分配的
- M:是不是 Memory Mapped 分配的内存,1 表示是,0 表示是 heap
- P:表示前一个 chunk 是否在使用,在初始化的时候通常为 1,防止使用不能访问的内存
- user data
2.2.2 空闲的chunk
- 正在被使用,则pre_size被用来内存赋值。否则则是上个chunk的大小。
size指的是本chunk的大小(包括chunk头)。
- P指PREV_INUSE,如果此位被设置为1(即allocated),则表示上个chunk已被分配,如果是0,则表示上个堆块是空闲状态。
- fd指向链表中前一个堆块的指针,该指针指向的是chunk的head。
- bk指向链表中后一个堆块的指针,该指针也是指向chunk的head,通过 fd 和 bk 可以将空闲的 chunk 块加入到空闲的 chunk 块链表进行统一管理。
- fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
- bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适chunk 时挨个遍历。
2.3 unlink检查机制
以前的unlink没检查,现在多了两项检查机制,在4.1和4.3中再讲解如何绕过这两项检查。
- unlink 检查是否P->fd->bk == P 以及 P->bk->fd == P
- unlink chunk size是否等于next chunk(内存意义上的)的prev_size.
3 程序和调试环境准备
3.1 测试环境
- Linux ubuntu 16.04.1
- glibc 2.23(这个攻击方法对>2.25版本的glibc无效)
- gdb 7.11.1
- pwndbg 1.1.0
3.2 演示程序
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
struct chunk_structure {
size_t prev_size;
size_t size;
struct chunk_structure *fd;
struct chunk_structure *bk;
char buf[10]; // padding
};
int main() {
unsigned long long *chunk1, *chunk2;
struct chunk_structure *fake_chunk, *chunk2_hdr;
char data[20];
// First grab two chunks (non fast)
chunk1 = malloc(0x80);
chunk2 = malloc(0x80);
printf("&chunk1 :%p\n", &chunk1);
printf("chunk1 :%p\n", chunk1);
printf("chunk2 :%p\n", chunk2);
// Assuming attacker has control over chunk1's contents
// Overflow the heap, override chunk2's header
// First forge a fake chunk starting at chunk1
// Need to setup fd and bk pointers to pass the unlink security check
fake_chunk = (struct chunk_structure *)chunk1;
fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P
fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P
// Next modify the header of chunk2 to pass all security checks
chunk2_hdr = (struct chunk_structure *)(chunk2 - 2);
chunk2_hdr->prev_size = 0x80; // chunk1's data region size
chunk2_hdr->size &= ~1; // Unsetting prev_in_use bit
// Now, when chunk2 is freed, attacker's fake chunk is 'unlinked'
// This results in chunk1 pointer pointing to chunk1 - 3
// i.e. chunk1[3] now contains chunk1 itself.
// We then make chunk1 point to some victim's data
free(chunk2);
printf("chunk1 :%p\n", chunk1);
printf("chunk1[3] :%x\n", chunk1[3]);
chunk1[3] = (unsigned long long)data;
strcpy(data, "Victim's data");
// Overwrite victim's data using chunk1
chunk1[0] = 0x002164656b636168LL;
printf("%s\n", data);
return 0;
}
3.3 编译命令
sudo gcc -o unlink unlink.c -g
需要安装插件 pwndbg进行程序调试
4 调试程序
在程序第20行处下断点,运行到chunk1 = malloc(0x80);处,这时还未执行。
执行n单步执行之后,第一个malloc执行完毕。
在第32行下断点
b 32
c
fake_chunk被赋值,可以看到fake_chunk和chunk1指向同一个地址空间0x602010。但是需要注意的是。chunk1是malloc分配出来的,指向的是用户数据区域,没有包含chunk header,而fake_chunk是由chunk1强制转换过来的,是伪造的chunk,此时chunk结构见下图:
通过将chunk1强制转换为struct chunk_structure结构体,就伪造出了一个chunk。即fake_chunk。此时如下图:
4.1 过检查点1
首先看一下chunk1和chunk2
fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P
fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P
我们看见&chunk1-3。这里是指针运算。&chunk1占8个字节。所以减3即减3*8共24个字节(0x18)。即
&chunk1-3 = 0x7fffffffddd0-0x18=0x7fffffffddb8。
然后再强制转换为chunk_structure 结构体。赋值给了fake_chunk->fd。fake_chunk->bk同理,&chunk1-2 = 0x7fffffffddd0-0x10=0x7fffffffddc0。
执行完之后看一下现在的chunk1和chunk2。和上图对比一下就发现fake_chunk->fd和fake_chunk->bk已经改变为0x7fffffffddb8和0x7fffffffddc0。
32行和33行的代码主要是绕过检查点1的。保证P->fd->bk == P 以及 P->bk->fd ==P,此时这里的P就是fake_chunk(之后会细解释为什么)。
即fake_chunk->fd->bk == fake_chunk 和 fake_chunk->bk->fd == fake_chunk。
fake_chunk->fd =0x7fffffffddb8,是chunk_structure 结构体。
fake_chunk->bk =0x7fffffffddc0,是chunk_structure 结构体。
以下是fake_chunk->fd结构体
fake_chunk->fd->bk是0x602010,所以本质上&fake_chunk->fd->bk = = &chunk1 == 0x7fffffffddd0,又因为chunk1指针指向0x602010,并且指针fake_chunk指向地址也0x602010,这样就保证了fake_chunk->fd->bk == fake_chunk。
以下是fake_chunk->bk结构体:
fake_chunk->bk->fd是0x602010。&fake_chunk->bk->fd = = &chunk1== 0x7fffffffddd0 , 并且指针fake_chunk指向地址也是0x602010,这样就保证了fake_chunk->bk->fd == fake_chunk,现在已经绕过检查点1了。
4.2 过检查点2
此时来分析下面这行代码。chunk2的指向地址是0x6020a0。并且是unsigned long long类型。即占8字节,所以chunk2-2 = 0x6020a0-16 =0x602090。并强制转换为chunk_structure结构体。赋值给chunk2_hdr。
n //单步执行
下面这两句代码是绕过检查点2的。第一行是将chunk_hdr->prev_szie设置为0x80。第二行是将chunk_hdr->size的P位设置位0。这样就会以为上一个堆块是空闲状态并且大小是0x80。
chunk2_hdr->prev_size = 0x80; // chunk1's data region size
chunk2_hdr->size &= ~1; // Unsetting prev_in_use bit
n
n
b 45
c
n
当free的时候就会触发unlink,此时free(chunk2),发现P位是0,则表示chunk1是空闲状态。则chunk1会向后合并。即P->fd->bk = P->bk 和 P->bk->fd = P->fd。这里的P指fake_chunk。为什么指fake_chunk呢?因为当free(chunk2)的时候,看见P位是0,此时需要找到前一个chunk。当前chunk的mem地址减去当前chunk的prev_size。即chunk2的地址减去0x80。则是0x6020a0-0x80=0x602020。此时0x602020是fake_chunk->fd的位置。
而此时栈结构,chunk1指针值被改变为0x7fffffffddb8,改变是因为P->fd->bk = P->bk.和P->bk->fd = P->fd。
4.3 chunk1和chunk1[3]同为一体
x/10gx 0x7fffffffddb8
因为&chunk1==0x7fffffffddd0,即为图中红框处,而chunk1[3]地址也是0x7fffffffddd0,所以修改chunk1[3]就相当于修改chunk1。
4.4 chunk1的移形换位
chunk1[3] = (unsigned long long)data;
strcpy(data, “Victim’s data”);
将chunk1[3]修改为data即0x7fffffffddf0,则此chuan1也已经指向0x7fffffffddf0,看一下这里存的什么,Chunk1指针指向0x7fffffffddf0,注意此chunk1指针的值已经变化了,此时给chunk1[0]赋值即给data赋值。注意这里data其实是我们模拟出来的攻击对象。
x/s 0x7fffffffddf0
n
可以看到data被成功修改了,hack成功!
持续更新漏洞利用案例…
5 参考链接
https://blog.csdn.net/qq_41918771/article/details/100917350?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-3
https://www.jianshu.com/p/4438e7b3b0ed