这里借助一个 CrackMe.apk 实例来帮忙分析。反编译 apk 文件得到 smali 代码,提取关键代码如下:
# direct methods .method static constructor()V .locals 2 .prologue .line 15 sget v0, Landroid/os/Build$VERSION;->SDK_INT:I const/16 v1, 0x14 if-ge v0, v1, :cond_0 .line 16 const-string v0, "qtfreet" invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V .line 20 :cond_0 return-void .end method ... # virtual methods .method public native check(Ljava/lang/String;)Z .end method
分析后可以看到调用了 .so 文件里定义的 check 函数,然后这个 check 函数又和 button 的 OnClick 事件绑定,可以判断 check 则是判断用户输入 key 是否正确的函数。
用 IDA 对 libqtfreet.so 进行分析,首先先看看有没有 JAVA_com_qtfreet_crackme001_check 这样类似的函数,如果存在的话就说明 .so 文件里面没有对 check 函数进行动态注册,JAVA_com_qtfreet_crackme001_check 就是 smali 里面调用的 check 函数原型,直接分析即可。
如果没有发现上面类似函数的话则说明 smali 调用的 check 函数进行了动态注册,此时需要找到注册的位置。
这里需要注意一个问题,就是安卓加载 .so 文件的流程。安卓对 .so 文件加载时首先会查看 .init 或 .init_array 段是否存在,如果存在那么就先运行这段的内容,如果不存在的话那么就检查是否存在 JNI_Onload 存在则执行。
另外,因为 .init 或者 .init_array 在 IDA 动态调试的时候是不会显示出来的,所以需要静态分析出这两段的偏移量然后动态调试的时候计算出绝对位置,然后在 make code(快捷键:c),这样才可以看到该段内的代码内容。
查看 .init_array 段的地址有两种办法:
1.可以使用 IDA 加载 .so 文件,新建 “Segments” 视图,这里会列出不同类型的代码段信息,如下图所示。
2.可以使用二进制工具 readelf 来查看 .so 文件的结构,在 OS X 上面可以使用 greadelf 代替。
可以看到这里两个都指示 .init_array 存在,并且偏移量为 0x20790,IDA 定位到该位置查看如图。
定义了一些东西,这个不必太在意具体是什么内容后面用到的话再看就行了, .init_array 执行完之后就跳到 JNI_Onload 里面执行,IDA 跳到 JNI_Onload 之后 F5 查看伪代码。
这里会有一些小问题,就是 jni.h 头文件里的结构体因为 IDA 里面缺乏参考所以不能正确的对应到他们的类型,需要手动修改数据类型(IDA 快捷键 Y),例如 JNI_Onload(int a1) 这里的 a1 就应该修改成 JavaVM*,后面的一些则需要修改成 JNIEnv*。修改好之后的代码如下图所示:
看到高亮的 RegisterNatives,进去第三个参数 off_21458 位置查看保存的内容。
可以看到这里最后执行了一个 memcmp 所以可以确定 sub_8A64 开始是关键的判断代码,直接将 memcmp 的返回值修改即可成功破解 crackme。如果需要逆出 key 则需要结合之前的初始化变量和函数来看,可以自己试一试逆向 key 不过多阐述了。
ps: 文中省略了一些 Anti Anti-Debug 的内容,因为这个 crackme 的反调试并不复杂而且通过单一函数实现,可以直接将反调试函数 “movs r0,r0” 掉。
* crackme.apk 可以在 Github 上找到。