0%

Ptmalloc算法:Unlink攻击

Ptmalloc算法:Unlink攻击

我们在利用 unlink 所造成的漏洞时,其实就是对 chunk 进行内存布局,然后借助 unlink 操作来达成修改指针(某个chunk的fd指针)的效果

这个指针可以修改为任何地址(fake pointer),那些下一次对该chunk进行操作时,就会向“fake pointer”中写入数据,以实现WAA


Unlink流程

unlink是一个宏操作,用于将某一个空闲chunk从其所处的双向链表中脱链

阉割版代码:

1
2
3
4
5
6
7
8
9
10
11
12
void unlink(malloc_chunk *P, malloc_chunk *BK, malloc_chunk *FD)
{ //p是某个结构体“malloc_chunk”的地址,*p就是结构体本身(进行了降阶)
FD = P->fd; //FD就是指向下一个结构体的指针
BK = P->bk; //BK就是指向上一个结构体的指针
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr(check_action,"corrupted double-linked list",P);
else
{
FD->bk = BK; //FD->bk:下一个结构体中的last
BK->fd = FD;
}
}

流程图:

在程序进行unlink操作时,还有一个检查:

1
2
3
4
5
6
7
8
9
10
11
// 由于'P'已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");

// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);

// 检查 largebin 中 next_size 双向链表完整性检查
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);

简单来说就是:

chunkP的下一个chunk的上一个chunk是不是chunkP

chunkP的上一个chunk的下一个chunk是不是chunkP

Unlink攻击原理

unlink 操作虽然有检查,但并不是那么“智能”,程序只会 根据地址的相对位置 来推测此位置大概是什么数据,比如,程序会认为 chunk_head+0x8 的位置为 presize, 而不会去检查 chunk 本身的“完整性” ,我们就可以利用这一点来欺骗检查程序

buf[0] 中装有 chunk 的首地址,修改 fake chunk->FD 为 buf[0] - 0x18(buf[-3]),修改 fake chunk->BK 为 buf[0] - 0x10(buf[-2])

假设程序要对 fake chunk 进行 unlink 操作,而 FD,BK 中指向的地址显然不合法,看看程序是怎么检查的:

  • 检查chunkP的下一个chunk的上一个chunk是不是chunkP,先获取FD指针指向的chunk,然后检查该chunk的BK指针是不是chunkP:fake chunk 的FD指针指向 buf[-3],而程序会把 buf[-3]+0x18 处当做它的BK指针(指向 buf[0]),符合检查
  • 检查chunkP的上一个chunk的下一个chunk是不是chunkP,先获取BK指针指向的chunk,然后检查该chunk的FD指针是不是chunkP:fake chunk 的BK指针指向 buf[-2],而程序会把 buf[-2]+0x10 处当做它的FD指针(指向 buf[0]),符合检查

这只是一种伪造方法,核心点:程序只会根据地址的相对位置获取数据

Unlink利用姿势

一,针对 chunk_list ,劫持修改模块(两个chunk合并后unlink)

这种攻击的套路比较固定:

  • 申请两个大小相同的chunk(大小不同也可以,我这里为了方便描述就这样规定)
  • 找寻并泄露 chunk_list 的地址(chunk_list 就是存放chunk数据区地址的数组)
  • 在chunk1内部进行伪造:
    • 伪造 presize 为“0”
    • 伪造 size 为“chunk1->size - 0x10”
    • 伪造 FD 为“chunk_list - 0x18”
    • 伪造 BK 为“chunk_list - 0x10”
  • 通过 off-by-one 溢出1字节覆盖 chunk2->size
  • 释放 chunk2

此后Unlink攻击完成,在 unsortedbin 中存储的地址变为“chunk_list - 0x18”,并且在 chunk_list 中残留的chunk1数据区地址也变为“chunk_list - 0x18”

接下来可以用修改模块进行 hook,GOT劫持,也可以把chunk申请到“chunk_list - 0x18”上

二,针对 heap ,实现 overlapping(三个chunk合并后unlink)

这种攻击可以直接在 heap 中打:

  • 申请三个chunk
  • 找寻并泄露 chunk1_head 的地址(chunk1的首地址)
  • 在chunk1内部进行伪造:
    • 伪造 presize 为“0”
    • 伪造 size 为“fake->size+1”(使其可以索引到chunk3)
    • 伪造 FD 为“chunk1_head + 0x18”
    • 伪造 BK 为“chunk1_head + 0x20”
    • 伪造 FD+0x10(BK+0x8)为“chunk1_head + 0x10”(fake chunk首地址)
  • 在chunk2内部最后一个空间写入“fake->size”(原本chunk3->presize的位置,使其可以索引到 fake chunk)
  • 通过 off-by-one 溢出1字节覆盖 chunk3->size
  • 释放 chunk3

注意:“fake->size”的大小为“chunk1->size + chunk2->size - 0x10 ”

libc版本限制

glibc-2.23:

  • 基本的unlink检查

绕过:用上述操作就可以绕过

glibc-2.27:

  • 基本的unlink检查
  • 对“下一个chunk的pre_size”的检查
1
2
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. pre_size");

标准的是申请三个堆 chunk0 chunk1 chunk2(控制 chunk1 为“buf[-3]”)

  • 释放 chunk0,chunk1 的 prev_size 位置会留下 chunk0 的大小

  • 释放 chunk2 glibc 会通过 prev_size 向上索引到 chunk0

  • chunk0 会被检测,这里 “chunk0的size” 会和 “chunk1的prev_size” 比较

注意:chunk2 需要申请一个 chunk3,防止 chunk2 释放后与“top chunk”合并

绕过:需要多伪造一个“fake_chunk0”,并通过“fake_chunk1”索引到“fake_chunk0”

glibc-2.29:

  • 基本的unlink检查
  • 对“上一个chunk的pre_size”的检查
  • 在unlink之前:检查 “chunk0的size” 和 “chunk2的prev_size” 是否相等
1
2
3
4
5
6
7
8
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. pre_size while consolidating");
unlink_chunk (av,p); //unlink的主体操作
}

​ // 这里还没有搞清楚,以后遇到题目了再补充