浅谈glibc-unlink

刚开始看ctf-wiki上的unlink的原理介绍的时候简直一脸懵逼,后来上手这题stkof实战,同时看了其他writeup之后,终于算是有了些浅薄的理解。因此把unlink的过程记录下来,防止以后忘了。

unlink就是把一个空闲chunk从双向链表(如small bin)中拿出来,例如分配新chunk,或是free(p)时和p物理相邻的空闲chunk会和p进行前/后向合并(本文主要讲这种)。unlink的基本过程如下(图来自ctf-wiki):

1553697853937

当有物理地址相邻的两个chunk,按地址从低到高chunk1-chunk2,其中chunk1是空闲状态,chunk2是分配状态,且chunk2为small chunk(large chunk似乎也可以,以后再研究下),这时候执行free(chunk2),就会进行如下检测:

后向合并:

检测chunk2的prev_in_use位,看chunk1是否为空闲,若为空闲,则两个chunk内存合并,指向chunk2的指针改为指向chunk1,接着使用unlink宏,把chunk1从双向链表中移除,chunk1进入unsorted bin。

if (!prev_inuse(p)) {
    prevsize = prev_size(p);
    size += prevsize;
    p = chunk_at_offset(p, -((long) prevsize));
    unlink(av, p, bck, fwd);
}
前向合并:

跟后向合并原理相似,检测chunk2的下个chunk(chunk3,物理地址比chunk2高)是否为空闲,空闲则合并,触发unlink宏,把chunk3从双向链表中移除。

利用:

理想:

了解了unlink的触发机制后,我们就要想怎么利用它了。显然,要把chunk1从链表中移除,最重要的就是fd和bk指针了,所以我们从它下手。回到上面的图中,我们构造:(P为chunk1地址)

  • FD=P->fd = target addr -12
  • BK=P->bk = expect value

根据unlink宏,会有以下操作:

  • FD->bk = BK,即 FD->bk= *(target addr-12+12)=BK=expect value,即 *(target addr)=expect value
  • BK->fd = FD,即 *(expect value +8) = FD = target addr-12

由此可实现任意地址写,例如修改GOT表项。虽然expect value +8地址的值被覆盖了有可能有小问题。

现实:

理想很丰满,现实很骨感,怎么可能随随便便就让你利用。。。实际上,在glibc中还有这个检测机制:

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      
  malloc_printerr (check_action, "corrupted double-linked list", P, AV);  

按照上面构造,则有FD->bk=*(target addr),若为GOT表项则不可能等于P,因此出错。但既然只是地址比较,那我们只要找(或者伪造)一个地址,里面存着P的地址不就解决了?所以我们可以考虑这样绕过检测机制:

先定义chunk1->fd=fakeFD,chunk1->bk=fakeBK,*Q=P

然后构造使得

  • fakeFD->bk==P,即`*(fakeFD+12)=P,Q=fakeFD+12``
  • fakeBK->fd=P,即`*(fakeBK+8)=P,Q=fakeBK+8``
地址
+00: fakeFD
+04: fakeBK
+08
+12: Q P

这样便满足条件,绕过了检测机制,从而调用unlink宏:

  • fakeFD->bk=fakeBK,即*(fakeFD+12)=fakeBK
  • fakeBK->fd=fakeFD,即*(fakeBK+8)=fakeFD

又由上面的构造条件可得:

  • *Q=Q-8
  • *Q=Q-12

至此,Q处的值被改为Q-12。

PS:以上都是以32位系统为前提,若为64位系统,则偏移相应要修改,如+12变为+0x18,+8变为+0x10。

对应题目:2014HITCON stkof

参考资料:

CTF-Wiki Unlink