作者Editor

看雪.WiFi万能钥匙 CTF 2017第十二 点评及解题思路

发布于:2017-06-28 18:17:17 阅读:363 回帖:0


看雪CTF 2017 比赛进行至第十二题

截止至今天中午12点,第十二题破解人数为 5 人!

防守方 hsadkhk 依然位居首位~,第十二题作者冲进第二位!

此题过后,风间仁依然处于第一位,前十名排名基本保持不变。


比赛进入尾声,仅剩 3 题。

期待ing......

接下来我们来回顾一下第十二题

看看 看雪评委和出题者是怎么说的ヾ(๑╹◡╹)ノ"。


看雪评委 netwind 点评


作为一道安卓平台破解题,作者在反调试和加密算法上对apk文件进行了很好的防护。通过对dex文件进行hash校验,隐藏函数名,在so层检查是否被附加调试来进行反调试。在算法上采用了DES和RC6算法,需要暴力枚举来破解此题。


作者简介


liuluping,四川大学信息电子信息学院在读研究生,目前研究方向为软件安全和漏洞分析,正在进行符号执行和程序自动化分析分析相关的研究,从硕士开始接触软件安全,从事程序分析,恶意代码分析和检测的相关方向的研究。

在程序分析上比较擅长协议逆向分析,常见加密算法识别以及恶意代码分析等,在实验室的日常工作中,接触较多各种零散类的工作,在安全研究路上还有待成长。


看雪 CTF 2017 第十二题设计思路


题目Flag

Flag:kxuectf{D3crypted1sV3rylntere5tin91}

题目设置时需要说明flag格式:kxuectf{},否则后面涉及到暴力破解,可能会出现题目无解。

安全策略

1. DEX 修改和反调试检测

(1)对DEX文件的SHA-1值进行检测:从/data/dalvik-cache/目录中读出应用安装存储的DEX文件,获取里面DEX文件的SHA-1值,并与应用原始的SHA-1值比较,不相等则退出,隐藏在动态注册中;

(2)为了防止解题者直接nop掉exit()函数,在进入so中的check函数后首先再进行一次DEX是否修改和反调试检测,如果检测到任意一个就将DES算法密钥的第5个和第6个值改掉。

2. 隐藏MainActivity,使得程序不执行配置文件中的MainActivity 。

安全策略

1.采用符号隐藏策略,将函数名隐藏

2.动态注册

使用动态注册将java层的check函数动态注册为ab函数

Java层算法

1. 在配置文件的MainActivity 即Main类中添加了多余的迷惑解题者的签名验证,实际并没有执行。

2. Java层算法一共三个,ab.a()、ba.a()、ca.a(),但是真正有用的是ab.a(),一个简单的移位加密。

Main类中的调用顺序是:

  str=ca.a(input)+ba.a(input)+ab.a(input)

uv类中的调用顺序是:

  str= ab.a(input)+ba.a(input)+ca.a(input)

ab.a()、ba.a()函数密文和输入长度一样,ca.a()函数密文与输入长度有关,所以算法需要对输入位数的判断,来确定输入为多少位。

3. 真正用于so中验证的只取前36位,也就是真正有用的加密函数只有ab.a()。


Native 层算法

1. Native层算法思路简单,使用两层加密算法,分别是RC6和DES。

2. 算法基本步骤如下:

3. 首先将输入利用DES进行加密,其中DES密钥硬编码在程序中,需要进行5轮。

4. 将加密后的数据分成两部分,前4轮组成0x20个字节作为第一部分,而后面一轮的前4个字节作为另外一部分。

5. 将步骤2中得到的最后一轮前4个字节替换掉硬编码的0x10个字节后作为下一步骤进行加密的RC6密钥。

6. 利用步骤3中得到的RC6密钥对步骤1中加密后得到的前半部分(0x20)进行加密后和硬编码的数据进行比较。

7. 最后根据比较结果得到True或者False并返回。

8. Native层的加密算法整个流程见PPT第二页。


相关思路

1. 在步骤1中使用的DES加密算法为标准的加密算法,使用的Paddting填充模式,填充数据为(8-(加密长度%8))

2. 在RC6算法中,对两个固定参数中的一个稍微进行了修改,参数Q由0x9E3779B9变成了0x61c88647。

3. 为了增加一点难度,这里没有直接将输入经过两次加密后直接和硬编码数据进行比较,而是将第一次加密后的部分作为第二次密钥的一部分。这里不会造成无解。因为在程序中已经进行了提示flag开头为kxuectf{,因此前8个字节固定,经过java层相关变换以及到了native层第一次DES加密时,由于DES也是8字节分组加密,因此DES加密后输出8字节也是固定的,为“\x4c\xd9\xa3\xe6\xed\xfe\xd1\x05”。

这样在进行解题中,由于知道最后比较结果,知道RC6密钥的前12个字节,以及知道RC6加密前的明文的前8个字节格式,因此这里一般思路是可以暴力枚举4个字节,将比较结果进行解密判断前8个字节是否等于上面的结果,从而得到RC6的密钥,进而逆推还原。

4. 但其实只需要破解3个字节就ok,通过分析程序可知,flag为36个字符,最后一个为},经过java层加密后得到固定字符‘{’,在进行DES加密时,采用padding模式,因此最后一组肯定是”xxx{\x04\x04\x04\x04”。

从这个地方推过去,由于最后一组进行DES加密后会去前4个字节来替换掉RC6的密钥,因此枚举最后一组前的3个字符即可,这样将最后一组进行枚举并一次进行DES加密然后将结果中的前四个字节取出来替换到硬编码的RC6密钥,然后利用RC6密钥对最后比较的0x20个字节进行解密,如果接的得到的前8个字节为“\x4c\xd9\xa3\xe6\xed\xfe\xd1\x05”则表示最后flag的最后四个字符已经确定,并且RC6的密钥也确定,这样在再对硬编码数据进行解密并依次逆推后即可顺利得到最后的flag:kxuectf{D3crypted1sV3rylntere5tin91}。


下面选取攻击者 loudy 的破解分析



使用工具:IDA6.8(so修改、反汇编分析、动态调试) notepad(记录) vc6.0(编程解码) android sdk emulator(android4.4虚拟机)、jeb(dex反编译)ApkIDE(修改DEX、重打包)

步    骤:

一、反逆向手段绕过

(1)dex层

直接JEB分析,如下图3处红线标注的地方有MD5校验。


绕过方法也很简单,ApkIDE反编译,修改下图61行的if-eqz为if-neq,并重新打包,即可直接跳过MD5检测。打包时,也许会提示某个styles.xml的几行有问题,删除那几行即可。


(2)so层

用IDA连接,程序直接退出,故肯定有反调试手段。用winrar对apk文件解包后,发现lib目录有3个不同的so文件,如下,其中armeabi-v7a文件夹下的libenjoy.so文件才是我的android4.4系统需要的,分析此文件。


IDA载入,静态分析,没有发现.init表,那么我们重点分析JNI_OnLoad函数。其中sub_24B8和sub_281C是两个主要的反调试函数。


而sub_2350和sub_2378都是同一种形式的解码函数,即按字节依次与0x16异或,解密结果都是字符串,这种函数后面还有很多,不一一介绍。

JNI_OnLoad中第14行解码结果为“check”,也即dex调用的本地函数,再看17行,跟踪到如下位置,发现check函数的实际地址为sub_3748。

而此处作者也用了一个小手段,自己新建了一个名为.mytext的可执行段,sub_3748在其中实现,导致IDA在默认的.text段找不到该函数,有一定的反逆向效果。

再看JNI_OnLoad函数调用的sub_24B8函数,该函数的主要作用是读取/data/dalvik-cache/目录下的data@app@com.game.kxctf-1.apk@classes.dex文件或者data@app@com.game.kxctf-2.apk@classes.dex文件的校验值,结果与“E07784C2A2923354F18952BBDB5D81916D24D2F5”比较,不等于则exit(0)退出程序。

绕过方法也很简单直接将JNI_OnLoad中对该函数的调用以0x00覆盖即可。

JNI_OnLoad函数调用的sub_281C函数,实际就是调用sub_1B04如下,该函数创建了一个线程。

跟进后,发现线程主体如下。其中sub_25A8函数获取/proc/%d/status的State,看其中是否包含“R/S/T”。sub_2628获取/proc/%d/status的TracerPid,看是否为非0值。sub_2688执行cat/proc/%d/wchan,看结果中是否包含sys_epoll或者ptrace_stop。sub_24B8之前已经分析。

从分析可以知道,该线程没有影响算法流程,因此也可以直接在JNI_OnLoad中将对sub_281C的调用以0x00覆盖,实现绕过。

最后在check函数sub_3748开头还有一处反调试,如下,分析发现sub_27B4功能和sub_24B8一模一样,不再累述,直接将调用处用0x00覆盖实现绕过。


二、算法流程分析

反逆向手段清除后就可以大胆调试,跟踪算法流程了。流程分dex层和so层两个方面讲。

(1)dex层

JEB分析结果很直观,将三个函数分别对输入字符串的处理结果连接起来,送入so层处理,如果so层处理结果为1,则正确。

ca.a是base64编码,ba.a、ab.a也分别是一种编码,虽然有三种编码,但实际分析后发现,ca.a和ba.a都不起作用,实际只有ab.a参与了算法流程,该函数如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
publicstaticStringa(Stringinput) {
returnab.encrypt(input);
    }
publicstaticStringencrypt(Stringinput) {
Stringv3 = "";
intv2;
for(v2 = 0; v2<input.length(); ++v2) {
intv0 = input.charAt(v2);
if(v0<97 v0="">109) {
if(v0>= 65&&v0<= 77) {
v3 = String.valueOf(v3) + (((char)(v0 + 13)));
gotolabel_20;
                }
if(v0>= 110&&v0<= 122) {
v3 = String.valueOf(v3) + (((char)(v0 - 13)));
gotolabel_20;
                }
if(v0>= 78&&v0<= 90) {
v3 = String.valueOf(v3) + (((char)(v0 - 13)));
gotolabel_20;
                }
if(v0>= 48&&v0<= 57) {
v3 = String.valueOf(v3) + (((char)(v0 ^ 7)));
gotolabel_20;
                }
v3 = String.valueOf(v3) + (((char)(v0 ^ 6)));
            }
else {
v3 = String.valueOf(v3) + (((char)(v0 + 13)));
            }
label_20:
        }
returnv3;
    }

此时无需更加深入分析,可以等so反推之后再爆破。

(2)so层

so层流程其实很简单。

1、   判断dex层传来的数据是否大于0x78(其实此时就是0x78),大于则over。

2、   截取前0x24位(即dex层ab.a函数处理结果),并在结尾补4个4成0x28位。

3、   用密钥{0xFD, 0xB4, 0x68, 0x54, 0x08, 0xCD, 0x56, 0x4E}对此0x28位进行DES加密(非标准)(实际操作时,密钥转换为子密钥总有一两位不同,故直接在内存提取子密钥表,取消密钥生成过程)

4、   DES结果第0x20、0x21、0x22,0x23位替换密钥{0x65, 0x48, 0x32, 0xEF, 0xBA, 0xCD, 0x56, 0x4E, 0x0F, 0x9B, 0x1D,0x27, 0x0, 0x0, 0x0, 0x0}的最后四位。

5、   用上一步的密钥对DES结果前0x20位进行RC6加密。

6、   RC6加密(非标准)结果与0x42D3C3C2, 0xF12AE92D, 0x66C92822, 0x2CEB540E,0x9407E577, 0x4A92B792, 0x2E5DFDF0, 0xF3549FC6比较,全部相等则注册成功。

三、破解过程

流程梳理清楚就好了,首先通过调试修改标准DES和RC6与程序所用一致。

其中DES修改较小,RC6修改较大,分别如下。一般RC6加密Q值为0x9E3779B9,而静态分析可知此程序为0x61C88647(即-0x9E3779B9)。

另外RC6的密钥会有个反馈,不是每次块(0x10字节)加密都用同一个密钥,对照IDA将C代码做如下修改,其中kerr为下一次加解密的密钥。


这两个函数调整好后(这是一个漫长的跟踪过程),事情就好办了。

由于DES为ECB模式,8字节独立加密。根据已知条件前8字节为“kxuectf{”得到加密结果0x4C,0xD9, 0xA3, 0xE6, 0xED, 0xFE, 0xD1,0x05。

由最后一个字节为“}”, ab.a函数处理后为“{”,所以DES加密的最后8字节为x,x,x,{,4,4,4,4,其中x为未知数。编写如下代码爆破,得到结果“a>6”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for(char i=0x20;i<0x7f;i++)< code="">
         for(char j=0x20;j<0x7f;j++)< code="">
              for(char k=0x20;k<0x7f;k++)< code="">
              {
                       str[0] = i;
                       str[1] = j;
                       str[2] = k;
                       memset ( str2, 0, sizeof ( str2 ) );
                       Des_Run ( str2, str, ENCRYPT );
                       key1[12]=str2[0];
                       key1[13]=str2[1];
                       key1[14]=str2[2];
                       key1[15]=str2[3];
                       rc6_key_setup(key1, 16);
                       rc6_block_decrypt(ct, pt);
                       if(pt[0]==stro[0] &&pt[1]==stro[1])
                       {
                            printf("%c%c%c%c\n",str[0],str[1],str[2],str[3]);
                       }
              }

由此得到正确RC6密钥{0x65, 0x48, 0x32,0xEF, 0xBA, 0xCD, 0x56, 0x4E, 0x0F, 0x9B, 0x1D, 0x27, 0x13, 0x6a, 0x7e, 0x1F}。

利用此密钥对0x42D3C3C2, 0xF12AE92D, 0x66C92822, 0x2CEB540E,0x9407E577, 0x4A92B792, 0x2E5DFDF0, 0xF3549FC6解密,得到结果“xkhrpgs}Q4pelcgrq6fI4elyagrer2gv”,加上“a>6{”,即“xkhrpgs}Q4pelcgrq6fI4elyagrer2gv a>6{”为正确的进入so的字符串。

通过如下函数爆破。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include 
char test[] = "xkhrpgs}Q4pelcgrq6fI4elyagrer2gva>6{";
char getit(char a){
    if(a<97 a="">109){
            if(a>=48 && a <=57){< code="">
                return  a^7;
            }
            else if(a>=65&&a<=77){< code="">
                return a+13;
            }
            else if(a>=78&&a<=90){< code="">
                return a-13;
            }
            else if(a>=110 && a<=122){< code="">
                return a-13;
            }
            else{
                return a^6;
            }
    }
    else{
            return a+13;
    }   
}
  
int main(){
    for(int j=0;j<36;j++){< code="">
        printf("%d ",j);
        for(char a=0x20;;a++){
            if(getit(a)==test[j]){
                printf("%c ",a);
            }
            if(a==0x7f) break;
        }
        printf("\n");
    }
    return 0;
}

得到结果如下。

如果剔除掉特殊字符,得到注册码为


“kxuectf{D3crypted1sV3rylntere5tin91}”输入模拟器,正确。




最后感谢 WiFi 万能钥匙安全应急响应中心的赞助支持,

接下来的比赛大家一定要使出洪荒之力哦!↖(^ω^)↗

比心 ❤



赞助商

上海连尚网络科技有限公司成立于 2013 年,是一家专注于提供免费上网和内容服务的移动互联网企业。连尚网络自主研发的核心产品 WiFi 万能钥匙,以分享经济的模式,通过云计算和大数据技术,利用热点主人分享的闲置WiFi资源,为用户提供免费、稳定、安全的上网服务,以帮助更多的人上网,找到属于他们的机会,改变自己的命运。



返回