libc 源码下载:

http://ftp.gnu.org/gnu/glibc/

# 1.Unlink

这里参考了博客:

https://hollk.blog.csdn.net/article/details/108481889?spm=1001.2014.3001.5502

unlink 是一个宏,定义在 malloc.c 里

c
#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 是在进行 free 操作时执行的,看上面的源码知道是对 链表 的操作,这里是修改指针的效果(unlink 目的 就是将一个空闲块 (在链表中) 拿出来,例如 free 时和目前物理相邻的 free chunk 进行合并),我的理解是 unlink 是将空闲的块在有新释放的块满足条件要合并时进行的操作,因为想要合并,就需要先将空闲的块从链表里取下来, unlink 就是在进行这个取下来的操作

执行 unlink 的函数(在 free函数 执行了 int_free()函数 ,其中调用了 unlink 宏)

c
#define unlink(AV, P, BK, FD)
static void _int_free (mstate av, mchunkptr p, int have_lock)
free(){
	_int_free(){
		unlink();
	}
}

ctfwiki 上的图片:

这里就能发现是将中间的 P 给取了出来,修改了 BKFD 的指针

图里面的执行顺序为:

  1. P->fd=FD
  2. P->bk=BK
  3. FD->bk=BK
  4. BK->fd=FD

上面的释放顺序为:

  1. free(FD)

  2. free(P)

  3. free(BK)

    所以在 bin 中为: BK->P>FD 的顺序

# 2.unlink 检查机制

由于 unlink 是在 free 函数中调用的,所以只需要检查 chunk 是否为空闲

其检查机制有三个:

  1. 检查被释放的 chunksize 的值是否与相邻高地址的 chunkpre_size 的大小相同(这里忽略 p 标志位, p标志位 为 size 最低位)【一个块为空闲时,相邻高地址块的 pre_size 为前一个块的大小(=size)】
  2. 检查被释放 chunk 与相邻高地址的 chunksizeP标志位 是否为 0, p为0则表示前一个chunk空闲
  3. 检查前后被释放 chunk 的 fd 和 bk

# 3.unlink 绕过

我们想要利用 unlink,就需要绕过其检查,而上面的三个检查机制里前面两个通过溢出直接修改即可,后面的则需要我们进行一番操作

# 1. 关键检查:

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

上面的是检查在 空闲的链表 中前后释放的 chunk的指针 是否对应正确

# 2. 绕过

下面的图里我们可以看到空闲链表的结构:

这里我们可以知道各个指针指向的位置:

c
P->fd=FD
P->bk=BK
FD->bk=P   // 关键检查
BK->fd=P    // 关键检查

1. 这里我们知道 FDpre_size 位到 bk 位需要 0x18 个大小(32 位是 12 个),也就是说 &FD+0x18=&bk , 而 P->fd=FD 并且 FD的bk 的值为 P 的地址,所以 P->fd->bk=P<=>*(P->fd+0x18)=P

这里 *(P->fd+0x18)=P >P->fd+0x18=&P> P->fd=&P-0X18

2. 同理知道 BK 的 pre_size 位到 fd 位需要 0x10 个大小(32 位是 8 个),即 &BK+0X10=&fd ,而 P->bk=BK 并且 BK 的 fd 的值为 P,所以 P->bk->fd=P<=> *(P->bk+0x10)=P

这里 *(P->bk+0x10)=P >P->bk+0x10=&P> P->bk=&P-0X10

也就是说我们最终通过:

c
P->fd->bk == P <=> *(P->fd + 0x18) == P 
p->bk->fd == P <=> *(p->bk + 0x10) == P

得到了:

c
P->fd = &P - 0x18 
P->bk = &P - 0x10

这意味着当我们的 P 中的 fd=&P - 0x18 , bk=&P - 0x10 就能绕过检查 (这里是将 fd 的 内容 设置为 (&p-0x18),将 bk 的 内容 设置为 (&p-0x10) )

# 3. 不同链表的 unlink(fd_nextchunk 和 bk_prechunk 分别为了方便在 large bins 中快速地管理 chunk 块)

因为 P 的脱链操作只能在 smallbinlargebin 中(无法在 fastbin 中)进行,而这两个 bin 都是 双向链表 ,所以我们必须修改 前后 chunk 的 fd 和 bk 指针

上面已经得出了结论,这里有别人更详细的推导:
https://cloud.tencent.com/developer/article/1557872

# 对于 smallbin 来说:

smallbin 通过上面的方式直接就可以完成脱链,因为 smallbin 中的 chunk 的 fd_nextsizebk_nextsize 是没有意义的

# 对于 largebin 来说:

对于 smallbin 来说,脱链操作上面就已经完成了,但是对于 largebin 来说,还有未完成的工作,因为 largebin 中还有 fdnextsize 以及 bknextsize 指针需要修改。

在 largebin 中,也只有在相同尺寸的同一组 chunks 中的第一个 chunk 中 fd_nextsize 以及 bk_nextsize 才有意义。

c
if (!in_smallbin_range (P->size)                \
    && __builtin_expect (P->fd_nextsize != NULL, 0)) {
    ...
}

P->fdnextsize!=null 时才需要修改(这里意味着 P 是 这一组 相同尺寸 chunk 的第一块 chunk),如果 P->fdnextsize == null ,说明 P 是尺寸相同的一组 chunks 的非第一个 chunk,此时 P 的 fdnextsizebknextsize 是没有意义的,自然没有修改的必要

c
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;             
           }

如果 FD->fd_nextsize == NULL ,那么 P 脱链(P 为 这一组 第一个 chunk)后 FD 即成为当前尺寸相同的 chunks 的第一个 chunk。

接着判断 P->fd_nextsize == P ,因为当 P 为仅有的唯一一组尺寸相同的 chunks 的第一个 chunk 的话,是需要特别对待的,P 不为第一个时 FD 直接继承 P 的 fdnextsize 以及 bknextsize 即可。

如果 FD->fd_nextsize != NULL ,说明 FD 是下一组尺寸相同的 chunks 的第一个 chunk。(这里是每一个组的第一个都是满足这个条件)

# 4.Unlink 利用

由上面的绕过可以得知:

c
P->fd = &P - 0x18 
P->bk = &P - 0x10

而在 unlink 宏中:

c
#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=P->bk					      
        BK->fd = FD;	        // 在这里 FD=P->fd
	....
     }
}

通过我们进行的绕过手段导致这里也就相当于:

FD->bk = BK  并且  BK=P->bk
所以FD->bk=P->bk

BK->fd = FD   并且  FD=P->fd
所以BK->fd =P->fd

因此这也就是:

c
FD->bk =&P - 0x10  // 事实上 FD->bk=BK (这里实际上是因为已经在链表中去除 P 后会根据之前 P 的 fd 和 bk 来确定修改后的指针的指向)
BK->fd  =&P - 0x18  // 事实上 BK->fd=FD

前面构造的为了进行绕过:

c
P=&P-0x10   //P->bk
P=&P-0x18   //P->fd

这里的执行是有顺序的,所以 BK->fd = FD 一定是后执行的,所以结果就为: P=&P-0x18

也就是说可以往 P 里写入值去修改 &P-0x18 的内容!(这里是我们往 P 里写值,其会去修改 &P-0x18 地方的值)

# P 地址的寻找:

然而这里还有一个问题是我们如何去找到 P 的地址,这里我们就需要找到堆的管理数组 (一般在 bbs 段处,会有个数组来记录每个 chunk 的地址),我们可以在这里找到我们伪造的堆块的 数组 的地址,通过这个地址来减 0x18 放入伪造的堆块的 fd 中即可

这样我们后续改变就可以根据这个数组来修改地址内部的值

【个人理解】

我对于 unlink 漏洞的理解是认为他其实是对 对应的堆块的管理数组进行了修改,将我们想要修改的地址作为堆块给添加了上去,其中的一个数组会存放这个想要修改值的地址,这就会导致我们后面可以对其当作堆块一样进行修改

参考:

https://www.bilibili.com/video/BV1Uv411j7fr/?p=20

https://cloud.tencent.com/developer/article/1557872

https://hollk.blog.csdn.net/article/details/108481889?spm=1001.2014.3001.5502

https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unlink/

https://www.secpulse.com/archives/115388.html

堆的数据结构:

https://www.cnblogs.com/L0g4n-blog/p/12965187.html