DDCTF2019官方Write Up——Reverse篇

194 阅读9分钟
原文链接: www.anquanke.com

 

第三届DDCTF高校闯关赛鸣锣开战,DDCTF是滴滴针对国内高校学生举办的网络安全技术竞技赛,由滴滴出行安全产品与技术部顶级安全专家出题,已成功举办两届。在过去两年,共有一万余名高校同学参加了挑战,其中部分优胜选手选择加入滴滴,参与到了解决出行领域安全问题的挑战中。通过这样的比赛,我们希望挖掘并培养更多的国际化创新型网络安全人才,共同守护亿万用户的出行安全。

 

Windows Reverse1

通过段名发现是UPX壳,upx -d脱壳后进行分析 核心函数只是通过data数组做一个转置,反求index即可 值得一说的是data的地址与实际数组有一些偏移 由于输入的可见字符最小下标就是空格的0x20,因此data这个地址实际上也是真正的表地址(0xb03018)-0x20=0xb02ff8,在实际反查的时候需要稍作处理

标题: fig:

from ida_bytes import get_bytes
data = get_bytes(0xb03018,0xb03078-0xb03018)
r = "DDCTF{reverseME}"
s = ""
for i in range(len(r)):
  s += chr(data.index(r[i])+0xb03018-0xb02ff8)
print(s)

 

Windows Reverse 2

用PEID查壳发现是aspack壳,直接运行程序,然后用调试器附加上去 通过调用堆栈来追溯到输入函数 具体方法为: 当程序运行到等待输入时,在调试器中按下暂停,然后选择主线程,观察调用堆栈(Stack Trace / Call Stack)窗口

标题: fig:

可以看出调用堆栈中存在scanf函数,这是因为接收输入时程序阻塞,必然是在程序调用接收输入的函数过程中阻塞的。而根据栈帧的机制,函数返回值会被存放在各个栈帧中,所以scanf的上一个函数就是用户模块了。 双击即可跟进去,此时看到的是一片数据

标题: fig:

这是因为代码由动态解密得到,IDA还没有对他们进行分析。 对着它们按C,即可将其作为Code识别,进行分析了 然后重新看调用堆栈窗口的具体地址即可找到目标 也可以直接在Options-general窗口中找到Reanalyse program按钮进行重新分析

标题: fig:

关键函数是sub111f0和sub11240 跟踪数据变化可以直接看出两个函数分别是hexdecode和base64encode 反向计算得到flag

from base64 import b64decode
print(b64decode(b"reverse+").hex().upper())

 

Confused

ios程序 Main函数中没有逻辑,所以通过字符串查找”DDCTF”取两次交叉引用,找到ViewController checkCode:函数 前段主要是校验开头结尾的”DDCTF{}”标志,API的各种命名都很清晰,无需多言。关键部分在msgSend“onSuccess”前的最后一个判断函数sub_1000011d0中

第一个调用sub100001f60可以看出来是构造函数,将对象的各个成员进行初始化,并把输入即a2的18个字节拷贝到全局变量中 第二个调用sub100001f00则开始虚拟机的执行循环,初始化IP寄存器后即不断向后执行,直到碰到结尾字节0xf3为止 loop函数内部很清晰,遍历对象的所有opcode与当前IP所指的值相比较,相等时即执行对应函数,函数内部负责后移IP,使VM执行下一条指令

平常做VM可以直接上angr、pintools或者记录运行log来方便地处理,但是本题是mac平台,由于钱包原因就只能乖乖静态逆了 首先整理字节码,位于0x100001984的地方 提出部分以作示例

0xf0, 0x10, 0x66, 0x0, 0x0, 0x0,
0xf8,
0xf2, 0x30,
0xf6, 0xc1,
0xf0, 0x10, 0x63, 0x0, 0x0, 0x0,
0xf8,
0xf2, 0x31,
0xf6, 0xb6,

第一个code是0xf0,对应的handler为sub_100001d70

标题: fig:

通过structures结构体功能可以将对象的成员重命名、整理结构成比较易读的形式 具体方法为在Structures窗口中按Insert创建结构体,按D创建成员

标题: fig:

最后将a1的类型按y重定义成结构体指针vm*即可

可以看出来f0根据第二个字节来决定将后4个字节的Int存入某个寄存器中,例如0x10表示a 然后是0xf8,对应的handler为sub_100001C60

标题: fig:

即对a寄存器的值在字母域内加2,相当于ROT2吧

以此类推,0xf2是判断a寄存器内的值是否和全局变量中以后一个字节为偏移的值相等,实际上也就是刚才memcpy进来的input 0xf6则是根据0xf2判断的结果来决定是否跳转,下同循环 因此整个算法实际上就是逐字符判断定值+2是否与输入相等,只需要将code_array中的定值取出即可 例如一个正则表达式

code="""0xf0, 0x10, ... 略"""
import re
data = re.findall(r"\n?0xf0, \n?0x10, \n?0x(..)", code)

print(data)
for i in data:
    v = int(i,16)+2
    print(chr(v-26 if (v>ord('Z') and v<ord('a')) or v>ord('z') else v),end='')

 

obfuscating macros

本题是一个经过OLLVM混淆的较简单算法的程序

基础分析

主函数中通过cin接受输入,传入两个函数中处理后要求皆返回True 两个函数都被OLLVM了 第一个函数通过黑盒可以知道是HexDecode,输出仍保存在原来的位置里,若输入超出数字和大写ABCDEF则Decode失败返回False 第二个函数则是对输入进行了一些比较,输出比较的结果

可供尝试有下列几种方法:

  1. 动态调试
  2. 符号执行
  3. 单字节穷举

动态调试

对于被控制流平坦化处理过的程序,执行流完全打散,所以很难知道各个代码块之间的关系,再加上虚假执行流会污染代码块,使得同一个真实块出现多次,难以分辨真实代码 因此在不deflat的情况下最好的办法就是单步执行慢慢跟,等到执行真实代码,尤其是一些运算的时候稍作注意 本题中通过这样的办法发现了这样一处代码

标题: fig:

指针++后取值,很符合字符串的逐字符处理逻辑 点进去看一下可以发现正是hexdecode过后的输入,而v26与输入产生了联系,所以我们下一步要跟着v26的数据走

标题: fig:

这里判断的v26的值是否为0,等价于cmp data,input; jz xxx; 因此可以知道要求第一个值为79,同理继续往下跟即可获得所有flag 另外快速一点的方法是使用断点脚本,在0x405fc6处下断并设置下述脚本,则会在output窗口打印出所需值

v = GetRegValue("ecx")
SetRegValue(v,"eax")
print("%X"%v)

另外算法实际上也并不复杂,简单来说是通过一个data数组和另一个数组异或产生的数据,前一个数组是逐个赋值的,所以并不好找出顺序来静态解出flag

单字节穷举

由于该程序的算法是逐字节校验,并且当某一个值错误时就会退出,因此可以应用pintools类的侧信道攻击 但并不能直接上轮子,因为在check的前面还有一个hexdecode,使得输入必须两个一组,并且由于数字和字母处理逻辑不同所以也会产生执行次数的跃变,要做特殊修正 首先字典调整成[“%02x”%i for i in range(1,0×100] 然后要判断key中存在的字母个数,经测试发现每个字母大概会使运行次数增加1681-1683次,将误差消除后比较即可

标题: fig:

大概在一小时左右可以得到flag,效率虽然比较感人,但优势在于期间不用关注该题,只等躺着拿flag就行了233

在之前的轮子上做了微调的脚本如下

#-*- coding:utf-8 -*-
import popen2,string

INFILE = "test"
CMD = "/root/pin/pin -t /root/pin/source/tools/ManualExamples/obj-intel64/inscount1.so -- /root/Project/obfuscating_macros.out <" + INFILE
#choices = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&'()*+,-./:;<=>?@[\]^_`{|}~"#自定义爆破字典顺序,将数字和小写字母提前可以使得速度快一些~
choices = ["%02X"%i for i in range(1,0x100)]

def execlCommand(command):
    global f
    fin,fout = popen2.popen2(command)
    result1 = fin.readline()#获取程序自带打印信息,wrong或者correct
    print result1
    if(result1 != 'wrong answer\n'):#输出Correct时终止循环
        f = 0
    result2 = fin.readline()#等待子进程结束,结果输出完成
    fin.close()



def writefile(data):
    fi = open(INFILE,'w')
    fi.write(data)
    fi.close()

def pad(data, n, padding):
    return data + padding * (n - len(data))

flag = ''
f = 1
while(f):
    dic = {}
    l = 0#初始化计数器
    for i in choices:
        key = flag + i#测试字符串
        print ">",key
        writefile(pad(key, 8, '0'))
        execlCommand(CMD)
        fi = open('./inscount.out', 'r')
        # 管道写入较慢,读不到内容时继续尝试读
        while(1):
            try:
                n = int(fi.read().split(' ')[1], 10)
                break
            except IndexError:
                continue
        fi.close()
        c = 0
        for ch in key:
            if(ch in string.ascii_uppercase):
                c += 1682
        print n-c
        if(n-c-l>50 and l ):
            flag += i
            break
        else:
            l = n-c
print flag

毫无疑问的是对于被OLLVM混淆过的程序,纯静态分析难度较大 而本题的混淆方案经过处理,网上现成的轮子只有TSRC于17年发布的文章,但由于块的分布不同所以还需要调整,由于我并不熟悉angr就不班门弄斧了,等一个师傅指教 所以所谓的静态分析其实还是要在动态的基础之上进行一定操作的