unlink原理
unlink这种利用方式源于glibc堆管理的一种特性,即当free一个chunk时,若该chunk处于双向链表中,则会检测其相邻块是否空闲,若处于空闲状态,则会将该空闲块从双向链表中取出,然后合并,这个取出操作就是unlink。unlink其实就是删除双向链表中的目标结点,这个删除过程本质上是对其前后结点的指针重新赋值,若我们对其指针进行巧妙的设置,则可能控制任意内存地址空间,我们会有任意地址改写能力。
合并
向前合并:
查看下一个块是不是空闲的 ,下一个块是空闲的,如果下下个块(距离当前空闲块)的PREV_INUSE§位没有设置。为了访问下下个块,将当前块的大小加到它的块指针,再将下一个块的大小加到下一个块指针。
如果是空闲的,合并它。
现在将合并后的块添加到 unsorted bin 中。
向后合并:
查看前一个块是不是空闲的 –如果当前空闲块的PREV_INUSE§位没有设置, 则前一个块是空闲的。
如果空闲,合并它
基本过程如下:
源码
利用unlink的时候,有一个检查机制,这就要看看unlink的源码
#!c
1413 /* Take a chunk off a bin list */
1414 #define unlink(AV, P, BK, FD) {
1415 FD = P->fd;
1416 BK = P->bk;
1417 if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
1418 malloc_printerr (check_action, "corrupted double-linked list", P, AV);
1419 else {
1420 FD->bk = BK;
1421 BK->fd = FD;
1422 if (!in_smallbin_range (P->size)
1423 && __builtin_expect (P->fd_nextsize != NULL, 0)) {
1424 if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
1425 || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
1426 malloc_printerr (check_action,
1427 "corrupted double-linked list (not small)",
1428 P, AV);
1429 if (FD->fd_nextsize == NULL) {
1430 if (P->fd_nextsize == P)
1431 FD->fd_nextsize = FD->bk_nextsize = FD;
1432 else {
1433 FD->fd_nextsize = P->fd_nextsize;
1434 FD->bk_nextsize = P->bk_nextsize;
1435 P->fd_nextsize->bk_nextsize = FD;
1436 P->bk_nextsize->fd_nextsize = FD;
1437 }
1438 } else {
1439 P->fd_nextsize->bk_nextsize = P->bk_nextsize;
1440 P->bk_nextsize->fd_nextsize = P->fd_nextsize;
1441 }
1442 }
1443 }
1444 }
1445
1446 /*
检查机制
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
在unlink之前会检查一下当前结点的前一个结点的后一个节点和当前结点的后一个节点的前一个节点是不是同一个结点,要想成功unlink,必须绕过这个检测,一种思路是将chunk->fd设置为(&chunk_addr - 0x18),因为:
chunk->fd->bk == *(chunk->fd + 0x18) == *(&chunk_addr - 0x18 + 0x18) == chunk_addr
这样就满足了检测条件,同理需要将chunk->bk设置为(&chunk_addr-0x10)。检测之后进行unlink操作
chunk->bk->fd = chunk->fd ==> *(chunk->bk + 0x10) = chunk->fd
==> *(&chunk_addr) = (&chunk_addr - 0x18)
chunk->fd->bk = chunk->bk ==> *(chunk->fd + 0x18) = chunk->bk
==> *(&chunk_addr) = (&chunk_addr - 0x10)
由于进行了两次赋值操作,所以只有第二次的有效,这样好像也看不出什么东西。这里我们假设有一个字符串数组strings,其第一个元素为strings[0],是char类型,假设我们已经构造好一个chunk,它满足chunk->fd == &strings[0] - 0x10,chunk->bk == &strings[0] - 0x18,unlink进行strings[0] = &strings[0] - 0x10,若我们本来可以对strings[0]的内容进行修改,那么unlink后我们实际上修改的是&strings[0] - 0x10处,进而覆盖到(&string[0]),若向*(&strings[0])写入目标地址,再进行strings[0]的写操作就能写入数据到目标地址内存中。
当然,上述利用存在一些细节问题,如string[0]和chunk的地址并非一致,这导致无法绕过检测,所以必须伪造chunk写入string[0]中才行,在具体利用的例子中可以看到如何伪造chunk。
伪造chunk
伪造chunk首先我们要知道chunk在free前和free后是不同的状态,这也是chunk的神奇之处。
正常情况下unlink之后会把P从双链表中解链,如果我们伪造了一个chunk,将fd=mem-0x18,bk=mem-0x10就可以绕过这个检查,还有一个要注意的是伪造chunk的fake_size位的最后一个标志为P的设置,size的最后一个标志位P表示了前一个chunk的使用状态,P=1表示前一个chunk处于使用状态,P=0表示前一个chunk处于空闲状态,我们通过控制P位就可以控制合并,当我们将FD=mem-0x18,BK=mem-0x10时,就表示我们已经拥有了任意地址写的能力,例如我们用合适的payload将[email protected]写入。p就变成了[email protected],那么再改一次p,把[email protected]改为shellcode的地址或者说system的地址都可以。之后再调用free功能,就可以任意命令执行。