XCTF_MOBILE10_easy-dex

初见

附件为一个apk,先到模拟器中运行一下。

这里要注意,这道题不能选用CPU指令集为x86的模拟器,需要用CPU指令集为ARM的模拟器,因为这个apk里面用到了native库,并且apk只提供了ARM指令集的native库。

可以看到,我是有两个模拟器,一个ARM指令集的,一个Intel指令集的,这里要用ARM指令集的模拟器。

打开模拟器后,把apk拖进去进行安装,安装好后,就能看到这道题的app了:

 app名字上给我们了一个小提示:要找到dex。

运行一下app,主界面就是全黑,没有任何东西:

 初步体验,没有更多有用信息了。下面反编译静态分析看看。

静态分析

将apk重命名为zip并解压。发现竟然没有dex文件:

这又和模拟器应用列表里的app的名字联系起来了,要找到dex。

先看看AndroidManifest.xml,看看这个app的整体信息。

AndroidManifest.xml分析

直接使用zip解压后,AndroidManifest.xml是二进制格式的,无法直接阅读,需要用apktool对apk进行反编译:

apktool.bat d apk路径

反编译之后,会生成一个目录,目录名和apk名相同。这个目录里的XML就都是解码后的了。

通过查看AndroidManifest.xml内容可以发现,这是一个NativeActivity的app:

<activity android:configChanges="keyboardHidden|orientation" android:label="@string/app_name" android:name="android.app.NativeActivity">
   <meta-data android:name="android.app.lib_name" android:value="native"/>
   <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
   </intent-filter>
</activity>

关于NativeActivity,可以查看这篇博客。

NativeActivity的app的主函数在lib目录下的Native库中,对于这道题就是在\6ee9ecdb39b5492aba053a73aeebb25b1\lib\armeabi-v7a\libnative.so中。

通过PE文件工具可知,这是一个32位的ELF文件:

接下来我们就用IDA对这个文件进行分析。

libnative.so

用IDA加载libnative.so,在导出函数中,可以找到android_main函数:

根据讲NativeActivity的博客我们知道,如果导出了android_main函数,那这就是这个app的主函数。

在这个函数中可以看到log函数调用,形如:

_android_log_print(4, "FindMyDex", "Can you shake your phone 100 times in 10 seconds?");

故尝试使用Logcat看看能不能捕获这些字符串,发现是可以的:

上图最后一行字符串就是app打印的,提示要求我们10秒内点100下鼠标。这道题考的是手速??

这手速要求,反正我是达不到,只能继续看看反汇编代码了。

android_main

在android_main代码后面部分,可以看到恭喜你成功的代码分支:

else
{
    v20 = time_before;
    if ( uncompress(DstBuf, &destLen, (const Bytef *)SrcBuf, ::dw_0x3CA10) )
      _android_log_print(5, "FindMyDex", "Dangerous operation detected.");
    v21 = open(filename, 577, 511);
    if ( !v21 )
      _android_log_print(5, "FindMyDex", "Something wrong with the permission.");
     write(v21, DstBuf, destLen);
     close(v21);
     free(DstBuf);
     free(SrcBuf);
     if ( access(name, 0) && mkdir(name, 0x1FFu) )
       _android_log_print(5, "FindMyDex", "Something wrong with the permission..");
     sub_2368((int)a1);
     remove(filename);
     _android_log_print(4, "FindMyDex", "Congratulations!! You made it!");
     sub_2250(a1);
     v10 = 0x80000000;
     time_before = v20;
}

这里面有些变量我改了名的,可能和你的IDA输出不一样。

这段的大致流程就是,使用uncompress函数解压一段内存,然后将解压后的内容写文件。之后又把这个文件删除。这个解压后的内容很可能就是dex文件。

根据uncompress函数的参数,解压前的内存为变量SrcBuf,长度为0x3CA10。我们向上找找这个SrcBuf内容哪来的。

在函数的最开始,可以找到SrcBuf申请空间和赋初值的代码:

SrcBuf = (char *)malloc(::dw_0x3CA10);
qmemcpy(SrcBuf, byte_7004, dw_0x3CA10);

就是将0x7004处的,长0x3CA10的内容复制到SrcBuf中。

之后有一段代码对SecBuf内容进行了解密:

        v10 = NumOfSharke;
        if ( (unsigned int)(NumOfSharke - 1) <= 88 )
        {
          v10 = NumOfSharke;
          BlockNum = NumOfSharke / 10;
          if ( NumOfSharke % 10 == 9 )
          {
            dw_0x3CA10_1 = ::dw_0x3CA10;
            BlockSize = (int)::dw_0x3CA10 / 10;
            v18 = (BlockNum + 1) * ((int)::dw_0x3CA10 / 10);
            if ( (int)::dw_0x3CA10 / 10 * BlockNum < v18 )
            {
              v19 = &SrcBuf[BlockSize * BlockNum];
              do
              {
                --BlockSize;
                *v19++ ^= NumOfSharke;
              }
              while ( BlockSize );
            }
            if ( NumOfSharke == 89 )
            {
              while ( v18 < dw_0x3CA10_1 )
                SrcBuf[v18++] ^= 89u;
            }
            v10 = NumOfSharke + 1;
          }
        }

核心就是将0x3CA10这么长的内容平均分成10份,前8份的内容分别异或9/19/29/39/49/59/69/79,最后两份内容异或89。就是一个异或解密。

所以整体解密流程分三步:

  1. 从0x7001获取0x3CA10长度的内容
  2. 进行异或解密
  3. 使用uncompress函数解压

解密dex文件

针对解密的三步流程,下面用三段python代码实现dex文件解密。

这里要注意,0x7001是加载进内存的地址,由于内存加载基址为0x1000,所以在文件中被加密的内容从0x6001开始。

下面第一段python代码用于获取加密后内容并写入一个文件:

fLib = open("C:\\Users\\leo\\Desktop\\6ee9ecdb39b5492aba053a73aeebb25b\\lib\\armeabi-v7a\\libnative.so","rb");
fLib.seek(0x6004, 0);
read_buf = fLib.read(0x3CA10)
fDump = open("C:\\Users\\leo\\Desktop\\dump.dat", "wb");
fDump.write(read_buf);
fDump.close();
fLib.close();

第二段代码读入上面生成的文件,对加密后内容进行异或解密,并将解密后内容写入文件:

import os
f=open("dump",'rb').read()
ws=open('de_dump','wb')
length = 0x3CA10
data = list(f) 
data1 = [];  
#print hex(len(data)) 
for j in range(0,90):
    if j%10 == 9:
        ls = int(j/10)
        ls_ = int(length/10)
        start = int(ls*ls_)
        end = int((ls+1)*ls_)
        for i in range(start,end):
            ws.write(((data[i])^j).to_bytes(length=1,byteorder='big',signed=False))
        if j == 89:
            for i in range(end,length):
                ws.write(((data[i])^89).to_bytes(length=1,byteorder='big',signed=False))
ws.close()

第三段代码uncompress函数对异或解密后的内容进行解压:

import zlib
buf=open("de_dump",'rb').read()
buf_de = zlib.decompress((buf))
ff=open('class.dex','wb')
ff.write(buf_de)
ff.close()

当然也可以把三步解密过程放在一个python代码中,但为了方便分析解密过程的中间结果,我是写了三段代码。

dex文件逆向分析

获得dex文件后,使用dex2jar对dex文件进行反编译:

用jd-gui查看反编译得到的jar包。

在MainActivity类中可以看到一串字节串:

  private static byte[] m = new byte[] { 
      -120, 77, -14, -38, 17, 5, -42, 44, -32, 109, 
      85, 31, 24, -91, -112, -83, 64, -83, Byte.MIN_VALUE, 84, 
      5, -94, -98, -30, 18, 70, -26, 71, 5, -99, 
      -62, -58, 117, 29, -44, 6, 112, -4, 81, 84, 
      9, 22, -51, 95, -34, 12, 47, 77 };

还可以在构造函数中看到,类a负责按钮事件的响应:

((Button)findViewById(2131427413)).setOnClickListener(new a(this, (EditText)findViewById(2131427412), (Context)this));

类a中的按钮响应函数为:

public void onClick(View paramView) {
    if (Arrays.equals(MainActivity.b(this.a.getText().toString(), this.c.getString(2131099683)), MainActivity.m)) {
      Toast.makeText(this.b, this.c.getString(2131099685), 1).show();
      return;
    } 
    Toast.makeText(this.b, this.c.getString(2131099682), 1).show();
}

其中核心是Arrays.equeal函数调用,该函数有两个参数,一个为MainAcitvity.b函数的返回值,一个为MainAcitvity.m字节串。如果这两个参数相等,就表示成功。

所以,这道题就是要寻找正确的参数,让MainAcitvity.b函数返回MainAcitvity.m字节串。

MainAcitvity.b函数

MainAcitvity.b函数有两个参数,一个是输入字符串,一个是固定字符串。

这个固定字符串的ID为2131099683(0x7f060023),这个ID对应的资源名在public.xml中可以找到:

<public type="string" name="two_fish" id="0x7f060023" />

在strings.xml中可以找到资源名对应的字符串:

<string name="two_fish">I have a male fish and a female fish.</string>

也就是MainAcitvity.b函数的第二个参数为“I have a male fish and a female fish.”。

这里暗示有twofish加密算法,如果MainAcitvity.b是twofish加密函数的话,第二个参数,这个固定字符串“I have a male fish and a female fish.”就应该是它的秘钥。

尝试用twofish算法对MainAcitvity.m字节串进行解密。但是MainAcitvity.m字节串里面有负数,先用python转化为16进制字节串:

a = [-120, 77, -14, -38, 17, 5, -42, 44, -32, 109, 85, 0x1F, 24, -91, -112, -83, 0x40, -83, -128, 84, 5, -94, -98, -30, 18, 70, -26, 71, 5, -99, -62, -58, 0x75, 29, -44, 6, 0x70, -4, 81, 84, 9, 22, -51, 0x5F, -34, 12, 0x2F, 77]
for i in a:
	print("%02x"%(i & 0xff), end = '')

输出为:

884df2da1105d62ce06d551f18a590ad40ad805405a29ee21246e647059dc2c6751dd40670fc51540916cd5fde0c2f4d

然后到twofish在线解密网站进行解密:

得到结果:

qwb{TH3y_Io<e_EACh_OTh3r_FOrEUER}

———————————————————————————————————————————

欢迎关注我的微博:大雄_RE。专注软件逆向,分享最新的好文章、好工具,追踪行业大佬的研究成果。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值