附件为一个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的整体信息。
直接使用zip解压后,AndroidManifest.xml是二进制格式的,无法直接阅读,需要用apktool对apk进行反编译:
apktool.bat d apk路径
反编译之后,会生成一个目录,目录名和apk名相同。这个目录里的XML就都是解码后的了。
通过查看AndroidManifest.xml内容可以发现,这是一个NativeActivity的app:
关于NativeActivity,可以查看这篇博客。
NativeActivity的app的主函数在lib目录下的Native库中,对于这道题就是在\6ee9ecdb39b5492aba053a73aeebb25b1\lib\armeabi-v7a\libnative.so中。
通过PE文件工具可知,这是一个32位的ELF文件:
接下来我们就用IDA对这个文件进行分析。
用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代码后面部分,可以看到恭喜你成功的代码分支:
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。就是一个异或解密。
所以整体解密流程分三步:
针对解密的三步流程,下面用三段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文件后,使用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函数有两个参数,一个是输入字符串,一个是固定字符串。
这个固定字符串的ID为2131099683(0x7f060023),这个ID对应的资源名在public.xml中可以找到:
在strings.xml中可以找到资源名对应的字符串:
I have a male fish and a female fish.
也就是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 ——————————————————————————————————————————— 欢迎关注我的微博:大雄_RE。专注软件逆向,分享最新的好文章、好工具,追踪行业大佬的研究成果。