Unlink原理
unlink 是当我们去 free 一个 chunk 的时候,如果这个 chunk 的前一个或后一个是 free 的状态,glibc 会把它从链表里面取出来,与现在要 free 的这个合并再放进去,取出来的这个过程就是 unlink
unlink的代码
define unlink(AV, P, BK, FD) {
FD = P->fd;
BK = P->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;
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
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);
if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
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的操作可以简单理解为将P的上一个chunk和P的下一个chunk连接,使P脱离双链表,简化为:
FD=P->fd;
BK=P->bk;
FD->bk=BK;
BK->fd=FD;
wiki的示意图
执行unlink时, 函数会检查当前空闲chunk的前一个chunk的bk指向是不是自己本身或者当前chunk的后一个chunk的fd指向是不是自己,如果不是则会抛出异常。
unlink是利用glibc malloc 的内存回收机制造成攻击的,核心就在于当两个free的堆块在物理上相邻时,会将他们合并,并将原来free的堆块在原来的链表中解链,加入新的链表中,但这样的合并是有条件的,向前或向后合并。但这里的前和后都是指在物理内存中的位置,而不是fd和bk链表所指向的堆块。
以当前的chunk为基准,将preivous free chunk合并到当前chunk称为向后合并,将后面的free chunk合并到当前chunk就称为向前合并。
向后合并
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(p, bck, fwd);
}
#define chunk_at_offset(p, s) ((mchunkptr)(((char*)(p)) + (s)))
首先检测当前free chunk的PREV_INUSE(P)标志位,如果为0表示前一个chunk为free状态,但内存中第一个申请的chunk的前一个chunk一般都被认为在使用中,所以不会发生向后合并。
如果不是内存中的第一个chunk且它的前一个chunk标记为free状态时,发生向后合并:
- 修改chunk的size位大小为两个chunk size之和
- 将指针移动到前一个chunk处
- 调用unlink将前一个chunk从它所在的链表中移除。
向前合并
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大小。
在合并后glbc malloc会将合并后新的chunk加入unsorted bin中,加入第一个可用的chunk之前,更改自己的size字段将前一个chunk标记为已用,再将后一个chunk的previous size改为当前chunk的大小。
利用
-
检查 P 的 size 字段 和 P 的 next_chunk 中记录的 PREV_SIZE 是否一致,不一致则出现corrupted size vs. prev_size的错误
-
检查是否满足 P->fd->bk == P 和 P->bk->fd == P,否则出现corrupted double-linked list错误
为了绕过这个检查,需要以下条件
-
程序中存在一个全局指针变量 ptr
-
ptr 指向的对内存可由用户控制
若具备以上条件,攻击者可在指针 ptr 指向的内存中伪造一个空闲 chunk P,根据 ptr 构造合适的地址覆盖 chunk P 的 fd 和 bk,使得
P->fd->bk == P && P->bk->fd == P
成立。具体如下(64 位):P->fd = ptr - 0x10P->bk = ptr - 0x18
-
实际上就是当 P->fd 或 P->bk 所存地址解释为 chunk 结构时其对应的 fd 或 bk 都为 P 的地址
在执行 unlink(P)时的指针操作如下:
1)FD = P->fd = ptr - 0x10;2)BK = P->bk = ptr - 0x18;// FD->bk = ptr - 0x10 + 0x10 = ptr; BK->fd = ptr -0x18 + 0x18 = ptr// 由于 ptr 指向 P,可成功绕过指针校验3)FD->bk = BK,即 *ptr = ptr - 0x10;4)BK->fd = FD,即 *ptr = ptr - 0x18。
由以上过程可知,借助指向 chunk P 的 ptr 指针可绕过 “corrupted double-linked list” 安全机制,并通过 unlink 攻击实现写内存,最终使得 ptr 指向 ptr - 0x18。
unlink 后,对 ptr 指向的内存进行写入,如
‘A’\*0x18 + free@got
,使得 ptr 指向 free@got,再次对 ptr 指向的内存进行写入,可以把 free@got 修改为 system 的地址,之后调用 free 可任意命令执行。