【知识库】DDCTF2019官方Write Up——Reverse篇

百家 作者:滴滴安全应急响应中心 2019-04-25 11:33:12

官方writeup公布时间线

对于没有被选手探索到的出题预设路径,出题人将现身说法,带来官方解读。


Reverse作者:奈沙夜影

江苏科技大学/大三/Reverse  Top 1


文章目录

Reverse  Write Up

0x01  :Windows Reverse1(内含出题人视角解析)

0x02  :Windows Reverse2

0x03  :Confused

0x04  :obfuscating macros(内含出题人视角解析)

0x05  :黑盒破解2-时间谜题(内含出题人视角解析)


这两道windows程序的逆向分析难度不大,选手们都做的很好,希望有选手能以此开始入门深入探索逆向分析的其他领域知识。


1

Windows Reverse1


--------------选手Write Up--------------

通过段名发现是UPX壳,upx -d脱壳后进行分析

核心函数只是通过data数组做一个转置,反求index即可

值得一说的是data的地址与实际数组有一些偏移

由于输入的可见字符最小下标就是空格的0x20,因此data这个地址实际上也是真正的表地址(0xb03018)-0x20=0xb02ff8,在实际反查的时候需要稍作处理

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)

2

Windows Reverse 2

用PEID查壳发现是aspack壳,直接运行程序,然后用调试器附加上去

通过调用堆栈来追溯到输入函数

具体方法为:

当程序运行到等待输入时,在调试器中按下暂停,然后选择主线程,观察调用堆栈(Stack Trace / Call Stack)窗口

可以看出调用堆栈中存在scanf函数,这是因为接收输入时程序阻塞,必然是在程序调用接收输入的函数过程中阻塞的。而根据栈帧的机制,函数返回值会被

存放在各个栈帧中,所以scanf的上一个函数就是用户模块了。

双击即可跟进去,此时看到的是一片数据

这是因为代码由动态解密得到,IDA还没有对他们进行分析。

对着它们按C,即可将其作为Code识别,进行分析了

然后重新看调用堆栈窗口的具体地址即可找到目标

也可以直接在Options-general窗口中找到Reanalyse program按钮进行重新分析

关键函数是sub_111f0和sub_11240

跟踪数据变化可以直接看出两个函数分别是hexdecode和base64encode

反向计算得到flag

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


3

Confused

macOS程序

Main函数中没有逻辑,所以通过字符串查找"DDCTF"取两次交叉引用,找到ViewController checkCode:函数

前段主要是校验开头结尾的"DDCTF{}"标志,API的各种命名都很清晰,无需多言


关键部分在msgSend“onSuccess"前的最后一个判断函数sub_1000011d0中

第一个调用sub_100001f60可以看出来是构造函数,将对象的各个成员进行初始化,并把输入即a2的18个字节拷贝到全局变量中


第二个调用sub_100001f00则开始虚拟机的执行循环,初始化IP寄存器后即不断向后执行,直到碰到结尾字节0xf3为止


loop函数内部很清晰,遍历对象的所有opcode与当前IP所指的值相比较,相等时即执行对应函数,函数内部负责后移IP,使VM执行下一条指令

平常做VM可以直接上angr、pintools或者记录运行log来方便地处理,但是本题是mac平台,由于钱包原因就只能乖乖静态逆了

首先整理字节码,位于0x100001984的地方

提出部分以作示例

0xf00x100x660x00x00x0,
0xf8,
0xf20x30,
0xf60xc1,
0xf00x100x630x00x00x0,
0xf8,
0xf20x31,
0xf60xb6,


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

通过structures结构体功能可以将对象的成员重命名、整理结构成比较易读的形式

具体方法为在Structures窗口中按Insert创建结构体,按D创建成员

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

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

然后是0xf8,对应的handler为sub_100001C60

即对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'a')) or v>ord('z'else v),end='')


4

obfuscating macros


本题是仿照ollvm的平坦化思想,由出题人完全独立实现的混淆,可能有些选手会误以为是ollvm这是很正常的事,实际上是使用g++编译的c++代码,因此现成的ollvm反混淆脚本是无法反混淆的,而且在一些细节上面是跟ollvm不一样的。考虑到可能在DDCTF中难度过大,因此特意埋了个逐字节比较的“后门”,结果大家都迅速找到了这个后门来破解,不错不错~ 大家可以期待后续加强版

--------------选手Write Up--------------

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

基础分析

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

有下列几种思路解题:

1.    动态调试

2.    单字节穷举

3.    反混淆


动态调试

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


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

这里判断的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,0x100]

然后要判断key中存在的字母个数,经测试发现每个字母大概会使运行次数增加1681-1683次,将误差消除后比较即可

大概在一小时左右可以得到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 answern'):#输出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年发布的文章https://security.tencent.com/index.php/blog/msg/112),但由于块的分布不同所以还需要调整,由于我并不熟悉angr就不班门弄斧了,等一个师傅指教


5

黑盒破解2-时间谜题


各位同学大家好,今年DDCTF2019的黑盒破解II-时间谜题被2位同学成功破解掉了,我来说一下这次题目的出题意图和解题思路。
主要考察逆向能力和捕获关键信息的学习能力,因为和去年题目兼容也可以作为去年题目来使用。

大家首先需要考虑到是什么样的操作会影响最后flag的输出,通过分析可以看到,题目由2部分组成,前面一部分是VM针对指令的解析过程,后面一部分就不太明显了,是一个运算过程,最后产生一个KEY,用该KEY解密内存数据就可以获得flag输出。题目的意图是利用VM指令中存在异常的处理过程,获得内存地址“写”的能力,通过VM指令泄漏出后面这个算法的参数地址,进而进行修改控制来自动获得Flag的输出。

那么VM和后面的运算过程的关系是什么呢?

仔细分析可以发现这是一个产生1000个总群(随机字符串)的遗传算法,为的是让这个总群进化为目标字符串“rXWZ2OGQwo8Em9r” ,然后选取其中排名靠前的200~300的种子进行KEY的合成工作,但这个运算过程受控于几个参数,1)算法迭代的次数2)可被选优的种子的概率 3)评估最优解的数值 ;也就是这些参数不被修改那么这个算法不会出现最优解,也就不容易了解KEY的合成过程。

逆向过程中会发现这这样的结构,如果到这里通过控制修改下面这三个参数就可以运行出falg了,这里需要注意的一点是因为进化到最优时,排名200~300的种子本身未必每一次都是一样的,也就是和题目中预先设定的KEY未必完全一致,所以需要多运行几次观察。


typedef struct tagKeyofKey {
 int nRandomofNumber; // 产生随机数据的数量,默认1000
 int nRandomofLen;       //产生的字符串的长度,默认15
 int nMakeofDataType;   //产生的数据类型,"A-Z","0-9","a-z" ,三种
 char ch1; // 'a'
 char ch2; // '0'
 char ch3; // 'A'
 int  nSwapofLen;  // 默认是7
 int  nCmpofValue; // 最优的比较值,默认0x147(这样是不会产生最优解的) 。---- 这里需要修改为1
 int  nCount;         //迭代循环的次数,默认100,也就是算法要在100轮运算后结束 ---- 这里需要修改至少1000以上才能保证算法可能会找到最优解,答案不唯一
 double fake1;
 double fake2;
 double fSeed;      //选取的优质总群数量,默认0.0018(这样的数值是不能进化倒最优解出现的)---- 这里需要修改为1.0018比较好,答案不唯一
 double fake4;
 int  nSwapofLen1;   
 int  nCmpofValue1; 
 int  nCount1;  
 char **List;//产生的总群数量
 char *pTarget;//指向进化的目标

}KeyofKey;


剩下的问题是如何利用VM指令构造出异常情况,通常这些指令接收不同输入会产不同的动作,例如add,sub ... , 可能操作的内存空间仅有255字节大小,

rdata:00000001400241A0 off_1400241A0   dq offset sub_140001CB0 ; DATA XREF: sub_1400022A0+29C↑o 函数表存储派发的vm指令,
.rdata:00000001400241A8                 dq offset loc_140001D20
.rdata:00000001400241B0                 dq offset loc_140001DF0
.rdata:00000001400241B8                 dq offset loc_140001E80
.rdata:00000001400241C0                 dq offset sub_140001F40
.rdata:00000001400241C8                 dq offset sub_140002030
.rdata:00000001400241D0                 dq offset sub_140001FF0
.rdata:00000001400241D8                 dq offset sub_140002210  -------- 注意这条指令,
.rdata:00000001400241E0                 dq offset sub_140001C50
.rdata:00000001400241E8                 dq offset loc_140001CF0
.rdata:00000001400241F0                 dq offset loc_140001E20
.rdata:00000001400241F8                 dq offset loc_140001DC0
.rdata:0000000140024200                 dq offset sub_140001F80
.rdata:0000000140024208                 dq offset loc_140001EC0


signed __int64 __fastcall sub_140002210(__int64 a1)
{
 __int64 v1; // r8
 __int64 v2; // rcx
 int v3; // edx
 signed __int64 result; // rax

 v1 = a1;
 if ( !a1 )
   return 0i64;
 v2 = *(signed int *)(a1 + 296);
 *(_DWORD *)(v1 + 296) = v2;
 if ( (signed int)v2 >= *(_DWORD *)(v1 + 300) )     //  step1********** 检测当前pos = v2 的位置是否超过了 v+300 = top ,也就是最大操作空间,初值是top = 255
   return 1i64;
 v3 = *(char *)(v1 + 672) - 48;
 if ( v3 <  42 )                                                       //  step2 ********** 构造这条指令的参数使其 大于42 ,绕过下面这个语句,跳到
 {
   *(_BYTE *)(*(_QWORD *)(v1 + 280) + v2 + *(_QWORD *)v1) = *(_BYTE *)(v2 + v3 + v1 + 16) - 49;
   return 1i64;
 }
 *(_BYTE *)(*(_QWORD *)(v1 + 280) + *(_QWORD *)v1) = *(_BYTE *)(v3 + v1 + v2 + 16) - 49;
 result = 1i64;
 *(_DWORD *)(v1 + 300) = -2;                   //step3 ************* 这里会把 v1+300 -2  ,因为是int 型,导致 运行到这里 top 被修改为 0xfffffffe ,验证失效,可以导致其他很多指令可以执行2个分支条件,
越界写其他地址数据
 return result;
}

类似这样的指令,利用 pos > top 的分支代码,就可以做到泄漏地址,改写算法参数的目的,因为在VM初始化多时候,算法的参数地址放置在了VM所能操作的255字节内存的后面.

signed __int64 __fastcall sub_140001CB0(__int64 a1)
{
 __int64 v1; // rax
 char v2; // dl
 signed __int64 result; // rax
 __int64 v4; // r9
 char v5; // cl
 __int64 v6; // r10
 __int64 v7; // rdx
 char v8; // r8
 unsigned __int8 v9; // cl

 if ( !a1 )
   return 0i64;
 v1 = *(signed int *)(a1 + 296);
 if ( (signed int)v1 >= *(_DWORD *)(a1 + 300) )  
 {
   v4 = a1;
   v5 = *(_BYTE *)(a1 + 672);
   v6 = *(signed int *)(v4 + 296);
   v7 = *(_QWORD *)(v4 + 8);
   v8 = *(_BYTE *)(v7 + v6);
   *(_BYTE *)(v4 + 672) = v8;
  if ( (signed int)v6 >= *(_DWORD *)(v4 + 300) )  
   {
     v9 = v5 - 127;
     *(_BYTE *)(v4 + 672) = v9;
     *(_QWORD *)(v4 + 288) = *(_QWORD *)(v9 + v7);
   }
   else
   {
     *(_BYTE *)(v6 + *(_QWORD *)(v4 + 280) + *(_QWORD *)v4) = *(_BYTE *)(v6 + v8 - 32 + v4 + 16) - 33;
   }
   result = 1i64;
 }
 else
 {
   v2 = *(_BYTE *)(v1 + *(_QWORD *)(a1 + 8));
   result = 1i64;
   *(_BYTE *)(a1 + 673) = v2;
 }
 return result;
}


当分析出所有VM里面可以操作后就可以利用这些指令来改写算法里的参数了。下面就是一个可利用的构造的输入指令,

#[$~0!8"0"8!0%8+0&8<0=8~0>8&0?8]0@8o0A8_0B8(0C8{0M8.0N8!0O8!0P8!

这个输入答案不唯一,这里只做例子演示。

--------------选手Write Up--------------

本题目继承去年的黑盒破解,有大量的冗余无关代码,因此逆起来比较吃力,需要耐心和经常整理思路

整体而言题目很新颖,但难度有些高,需要做题人完全理解遗传算法,大胆、跳出常规思路去解题

个人认为题目的难点主要是正常做题是倒着来,即思考如何能达到目标--使correct标志位为1,进而追溯哪些数据和条件影响该标志位,然后逆向相关的函数并寻找它们和输入的关系,从而反推出输入。
而本题要求首先逆清整个程序的逻辑和几乎所有功能,然后再思考哪些逻辑不合常理,或者说应该被修改,这是一个很反常的思路。
换言之,这是不同于以往找出正确输入的CrackMe,而是一个修复类型的题目


main中主要有两个函数比较重要,分别是负责初始化的init和负责调用VM的check


init里无需多言,主要是各种各样的初始化,里面还有一些混淆,例如随机数、内存交换解密等等,但不受输入影响所以不用关心

check里跟去年一样的方式比对code,然后调用对应的函数
去年的目标是在表中构造出Binggo!的字符串,然后调用输出函数print出来,最后hash校验成功使得flag出现
而今年这个结构仍然保留了,所以构造Binggo!的字符串还是能输出Congratulations!,而题目里却没有说明这个字符串,对做题人来说题目里出现了如果构造一个输出满足hash则能给出正确反馈的状况,所以这是一个很大的坑…
还好我翻出了去年的题目一看hash完全一样,打扰了乖乖看后面那个函数吧

是否输出Congratulations的校验标志有两处交叉引用,一处在VM的handler中,另一处则在这个final进去以后的某部分中

事实上这里才是今年题目的开始
关于VM相关的内容可以参考 去年的WP

https://blog.csdn.net/whklhhhh/article/details/80032874)中re2黑盒破解的部分,不过去年的时候还不怎么会用结构体功能来恢复对象2333
另外handler有一些升级,需要注意到,主要在于当下标寄存器大于char_table的范围即256时,去年是直接return,防止越界读写
而今年刻意地提供了这个功能

随便逆了两个func发现这个VM具备任意写的能力,那么这样可操作的范围就大了,甚至大到让人无所适从的地步
毕竟理论上来说任意写甚至可以直接按照pwn的思路来get shell
找了一圈leak,发现堆地址在init函数中提供了

继续往下看,在final()函数中做了很多事情
首先是随机生成1000个字符串,每个字符串15字节,构成一个对象数组,字符串的字符种类有3种,分别是大写字母、小写字母和数字

然后循环100次


1.    计算每个字符串和标准字符串的相似度,具体计算方法为逐字符做差取绝对值累加,将相似度保存在对象中,然后对整个数组按照相似度排序

2.    求出相似度的平均值,乘以一个比例0.0018(double类型,根据IEEE标准解析),得到值X

3.    遍历所有字符串,相似度小于X的打上标记

4.    随机选取1000对字符串,如果都具备标记则随机交换后半部分,交换完后计算相似度,如果降低则保留,否则还原

5.    遍历带有标记的字符串,随机选取一个值做+1/-1浮动,选择相似度较小的保留

6.    判断相似度最小的字符串的相似度是否小于某个值0x147,若小于则取相似度排名200-300的带有标记的字符串进行两两异或,从尾部开始逐字符计算,若相等则中断并判断下标是否小于某个值Len=0x4E57795F,若小于则判断Len+4是否大于等于30,是则退出,否则从Len开始重复该串的0-Len位,并在最后附加fill_value="5mE9"。这一段逻辑比较绕,建议自己调试两遍看看memmove的作用

根据逻辑想到遗传算法

遗传算法(Genetic Algorithm, GA)是模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的计算模型,是一种通过模拟自然进化过程搜索最优解的方法。

其主要特点是直接对结构对象进行操作,不存在求导和函数连续性的限定;具有内在的隐并行性和更好的全局寻优能力;采用概率化的寻优方法,不需要确定的规则就能自动获取和指导优化的搜索空间,自适应地调整搜索方向。

遗传算法以一种群体中的所有个体为对象,并利用随机化技术指导对一个被编码的参数空间进行高效搜索。其中,选择、交叉和变异构成了遗传算法的遗传操作;参数编码、初始群体的设定、适应度函数的设计、遗传操作设计、控制参数设定五个要素组成了遗传算法的核心内容

摘自简书超详细的遗传算法(Genetic Algorithm)解析

https://www.jianshu.com/p/ae5157c26af9

上述所有参数都是对象的成员

由于我们之前对虚拟机的逆向已经知道有任意写的能力,于是这里的所有参数我们都是可以修改的
那么问题就在于修改哪些了

默认情况下怎么跑都会由于X=平均值*选优比例0.0018=0而没有字符串被标记,于是后面几个函数就都没有用了
所以首先要修改的就是比例0.0018,虽然对象中有几个备选比例成员,测试发现都还是不行,于是大胆给他上了200(……
这个200也是需要用double类型存储的,用matlab转换

octave:3> num2hex(200)
ans = 4069000000000000

发现经过100轮遗传以后产生了很多字符串,但相似度仍然很低,于是再把max_loop遗传代数调大到1000,发现出现了很多跟标准字符串相似的串

注意特征是不同的字符全部位于尾端,猜测是因为交换的算法是两串交换尾端部分
再观察排名200-300的串

确认猜想,就是尾部若干字符不同

那么flag为啥还没出现呢?
因为数据成员中还有一个Len,默认值高达0x4E57795F,会导致任何串都无法进入
而怎么调整也并没有任何线索,它从0-23全部可取,每一个值都会导致解锁密钥不同,且进入解密时会破坏原密文
暴力试了所有值都无果,最终想到了对标准串暴力枚举所有Len和相同的值下标
结果正确的标准是对数据RC4解密后hash的值为36468080

 

class RC4:
    def __init__(self, public_key=None):
        if not public_key:
            public_key = 'none_public_key'
        self.public_key = public_key
        self.index_i = 0;
        self.index_j = 0;
        self._init_box()

    def _init_box(self):
        """
        初始化 置换盒
        """


        self.Box = [i for i in range(256)]
        key_length = len(self.public_key)
        j = 0
        for i in range(256):
            index = ord(self.public_key[(i % key_length)])
            j = (j + self.Box[i] + index) % 256
            self.Box[i], self.Box[j] = self.Box[j], self.Box[i]

    def do_crypt(self, string):
        """
        加密/解密
        string : 待加/解密的字符串
        """


        out = []
        for s in string:
            self.index_i = (self.index_i + 1) % 256
            self.index_j = (self.index_j + self.Box[self.index_i]) % 256
            self.Box[self.index_i], self.Box[self.index_j] = self.Box[self.index_j], self.Box[self.index_i]
            r = (self.Box[self.index_i] + self.Box[self.index_j]*2 + self.index_i + self.index_j) % 256

            # r = (self.Box[self.index_i] + self.Box[self.index_j]) % 256
            R = self.Box[r]  # 生成伪随机数
            out.append(((s) ^ R))

        return (out)
# c = [0x7f, 0x2e, 0x79, 0x56, 0x6, 0xc2, 0xb8, 0x47, 0x52, 0xe1, 0xb9, 0x7f, 0x38, 0x1b, 0xa, 0xcc, 0x18, 0x7a, 0xec, 0xf8, 0xa2, 0x89, 0x91, 0x78, 0xa6, 0x4b, 0x1b, 0x85, 0x93, 0x9a, 0x4c, 0x59, 0x6e, 0xf5, 0xf4, 0x7c, 0xd2, 0xf4, 0x2, 0x6, 0xe4, 0xfb, 0xcb, 0xd7, 0x7c, 0xa9, 0x85, 0xe5, 0x0, 0x15, 0x90, 0x6, 0x4f, 0x1f, 0x52, 0x54, 0xf, 0x5a, 0x3d, 0x87, 0x32, 0x5b, 0xd6, 0xb2]
c = [0xbe0x700x480xc60xa10x600x680xcf0xd20x6e0x9e0x600x810x1a0x5d0x4a0x710x9b0xea0x510x2c0xb70x460xa10x150xb90xee0xc60xc80x00x8f0x100xd0xc00xe20x790xa50x880xcc0x6f0xc20x1d0xd70x8e0x20xf60x1b0x7a0x5f0x5b0x6f0xe30x590xe0x3f0x910x160x3c0x320x950x290xec0xcb0xec]

def hash1(x):
    r = 0
    for i in x:
        if(i>0x7f):
            i = i+0xffffff00
        r = r*16 + i
        v46 = r&0xf0000000
        if(v46!=0):
            r = r^(v46>>24)
        r = (~v46) & r & 0xffffffff
    return r
true_h1 = 36468080

ori = "rXWZ2OGQwo8Em9y"

for final_len in range(24):
    for i in range(len(ori)):
        if(i>final_len):
            continue
        ss = ["" for k in range(48)]
        tmp = ["" for k in range(24)]
        s = ""
        for k in range(len(ori)):
            ss[k] = ori[k]
        ss[i]=""
        for k in range(final_len):
            tmp[k] = ss[k]
        for k in range(final_len):
            ss[final_len+k-1] = tmp[k]
        for k in range(len(ss)):
            if(ss[k]==""):
                ss[k] = "8"
                ss[k+1] = "E"
                ss[k+2] = "m"
                ss[k+3] = "9"
                break
        print(ss)
        for k in ss:
            if(k==""):
                break
            s += k
        print(final_len, i, s)


        rc4 = RC4(s)
        p = rc4.do_crypt(c)
        print("".join(map(chr,p)))
        h1 = hash1(p)
        print((i,s,h1))
        if(h1==36468080):
            print("!"*150)

打印出的值检索"!!!"可以发现flag

事后跟出题老师交流了一下发现问题出在min_value上,期望解法应该是将其改为1,这样当最优解即跟标准串完全一致的串出现时,排名200-300的串才具备N个字符不同的特征
默认的0x147出现时,排名200-300的串可能有N+1,N+2个字符不同,而RC4解密是一次性的,如果第一个进行解密的串不符标准就会破坏密文导致后续正确key即使出现也无法解密出flag

赛时写了一个方便Patch的IDA脚本,在generation之前执行即可Patch各个参数

from ida_bytes import patch_bytes
import struct
base = idaapi.get_imagebase()
GA = Qword(base+0x286a8)+Qword(base+0x286b0)
print(hex(GA))
min_value = GA+0x14
loop_size = GA+0x18
weight = GA+0x30
ptr_str = GA+0x60
final_len = GA+0x40

w = "3ff0000000000000"
#w = "3FF007D4E4D205F3"

w = w.decode("hex")[::-1]
patch_bytes(weight, w)
patch_bytes(final_len, struct.pack(",0xd))
patch_bytes(loop_size,struct.pack(",20000))
patch_bytes(min_value,struct.pack(",1))
#patch_bytes(ptr_str,struct.pack("
print("Patch finished")

查看当前种群

base = idaapi.get_imagebase()
data3 = Qword(base+0x29d90)
GA = Qword(base+0x286a8)+Qword(base+0x286b0)
#min_value = Dword(GA+0x14)
min_value = Dword(base+0x29d80)
for i in range(1000):
  v = Dword(data3+4+24*i)
  if(v>=min_value):
    break
  if(i< 300):
    s = ""
    ptr_s = Qword((data3+16+24*i))
    for j in range(16):
      s += chr(Byte(ptr_s+j))
    print("[%d]:t%dt%s"%(i+1,v,s))
print("alive:%d, v=%d"%(i+1,v))

————— End —————

    延伸阅读    

【知识库】DDCTF2019官方Write Up——Android篇

官网题目仍开放访问,点击“阅读原文“前往

    关于漏洞    

滴滴出行相关漏洞请提交至

http://sec.didichuxing.com/

关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接