最近有位同学发了一个样本给我,主要是有一个解密方法,把字符串加密了,加解密方法都放在so中,所以之前也没怎么去给大家介绍arm指令和解密算法等知识,正好借助这个样本给大家介绍一些so加密方法的破解,首先我们直接在Java层看到加密信息,这个是这位同学直接告诉我这个类,我没怎么去搜了:
这个应用不知道干嘛的,但是他的防护做的还挺厉害的,之前我们介绍过小黄车应用内部也用了这种中文混淆变量和方法等操作,这里就不多解释了,这里主要看那个加密算法:
看到这里有一个加解密方法,传入字符串字节,返回加解密之后的字节数据,我们直接用IDA打开这个libwechat.so文件:
这里可惜没有收到Java_xxx这样的函数,说明他可能用了动态注册,所以就去搜JNI_OnLoad函数,所以这里注意大家以后如果打开so之后发现没有Java_xxx这样的函数开头一般都是在JNI_OnLoad中采用了动态注册方式,所以只需要找到JNI_OnLoad函数,然后找到RegisterNatives函数即可,不过在这个过程中我们需要转换JNIEnv指针信息:
这里大家如果看到类似于vXX+YY这样的,选中vXX变量,然后按Y按键,然后替换成JNIEnv*即可,我们如果手动注册过Native方法,都知道RegisterNatives函数的三个参数含义:
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods) 第一个参数:需要注册native函数的上层Java类 第二个参数:注册的方法结构体信息 第三个参数:需要注册的方法个数这里当然是重点看第二个参数,这里当然也需要知道方法结构体信息:
typedef struct {const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
结构体包含三部分分别是:方法名、方法的签名、对应的native函数地址;那么这里我们肯定重点看第三部分,因为要找到具体的解密函数,这时候我们需要去对RegisterNatives函数查看他的实参值:
这里选中RegisterNatives函数名,然后右键选择Force call type即可:
这时候就看到了RegisterNatives的三个参数值,其实这里看到是四个,这个主要是调用方式的区别,因为我们还会看到有这种调用方式:(*JNIEnv)->RegisterNatives(JNIEnv env…),所以第一个参数其实是JNIEnv变量,这里就看第三个参数的地址就是需要注册方法的结构体信息,点击进入查看:
这里看到了方法名,方法签名以及对应的具体函数,这里主要看解密函数,找到decryptData即可,然后点击进入查看:
按下F5查看C语言代码:
继续点击进入查看:
这里就是实际的解密算法的地方了,大致看一下其实还是很简单的,就是有一个AES_CBC_128算法加解密的,我们用过这个算法都知道需要key和iv值,因为是128位的,所以这两个值肯定是16(128/8)个字节,这个是基础知识也是非常关键的知识,知道是16个字节对于后面分析破解非常关键。然后需要从解密之后的字节数组的最后一位获取实际字节的长度,最后构建byte数组返回给Java层即可。所以这里我们看到最重要的是如何获取aes解密的key和iv值。这里有很多种方式可以动态调试,可以hook。但是我们先不介绍这两种解密方式,我们先来看看另外一个问题。
二、调用so功能函数(修改指令)我们在之前是不是有时候解密一个so算法,其实没不要真的知道他的解密算法,而是可以调用他的so然后直接解密出来数据即可,所以我们本文也来尝试做一下,为什么这么做因为在这个过程中我想给大家介绍一些知识点比如修改arm指令等,我们把这个应用的so拷贝到项目中,然后构建一个native类和方法,最后调用解密方法,发现调用直接出现崩溃信息:这时候我们发现在进入JNI_OnLoad挂了,说明JNI_OnLoad中做了一些东西检测:
看到JNI_OnLoad函数中有这两个函数调用,第一个我们都知道为了防止自己的进程被人恶意附加,就自己先占坑,这样别人就附加失败了,第二个看似也是类似功能,不过不用关心内部实现,我们为了后面动态调试成功,这里还是先把这两个函数干掉吧,这里干掉简单直接改成NOP空指令就可以了,就相当于没调用了。因为这两个函数的执行逻辑和返回结果和后面的逻辑是没任何关系的,所以可以这么做,如果有关系那只能修改返回值了。修改指令之前其实介绍过了,很简单先找到指令对应的偏移地址:
然后用010Editor工具打开so文件,找到这个地址:
怎么修改成NOP指令呢?有一个牛逼的网站在线转换arm为hex值: http://armconverter.com :
这里看到转换BLX指令的HEX正好和上面看到的HEX值对应上了,这里修改NOP指令:
看到NOP指令对应的HEX值是C046,那就修改吧:
这里注意需要把那两条指令的所有HEX全部改成NOP指令,保存再用IDA打开查看:
修改成功,这两个函数就等于没调用了,在运行调用so还是崩溃,这时候需要想到的是有签名校验,而巧合的是在搜索JNI的时候无意发现了这个函数:
当然如果大家想知道so中有没有签名校验,可以直接Shift+F12查找字符串内容”signatures”:
一般有这类字符串信息都有签名校验功能了,我们继续看上面那个签名校验函数:
果然这里会获取签名信息,然后比对返回1表示正确的签名信息,这里我们不要直接修改返回值和那个v5变量值,因为我们知道strcmp函数执行的结果是-1,0,1;这里明显是需要让返回值是0才可以,那不如直接修改v3的初始值为1即可,修改方法和上面的指令修改类似:
记住这个便宜地址,然后去010Editor工具中查看:
然后把赋值修改成1:
然后去010Editor修改即可:
修改之后保存,用IDA打开so:
看到已经修改成功了,然后在F5查看伪代码:
这里不管签名对不对,都直接返回1了,修改了之后我们在运行发现还是报错,这个需要再去看JNI_OnLoad函数了:
这里需要获取一个Java层的类,所以我们在工程中新建这个类即可,这个类可以没有任何方法:
然后运行成功,看看解密之后的内容是啥:
看到解密之后的内容是个字符串version内容,到此我们就成功的过掉了so中的一些检测调用so解密出来内容了,那么在这个过程中我们依然可以学到很多东西:
第一、修改指令,如果不想让一个函数执行,只需要把跳转指令修改成NOP空指令即可,前提是这个函数的执行结果和后面的逻辑没有半毛钱的关系,如果有那么就需要修改函数的返回值,一般需要修改跳转指令之后的MOVS指令的寄存器值,如果简单点可以直接修改变量的初始化值,比如这里的过掉签名校验。 第二、如果快速的知道so中是否有签名校验功能,可以直接在字符串列表中搜索”signatures”即可,现在也有很多应用会在so中调用Java层的类信息,所以需要去看JNI_OnLoad中arm指令,或者直接搜索字符串列表,因为一般Java层类信息,都是xxx/yyy/zzz/MMM这样的字符串格式,通过肉眼排查也是可以的。 三、动态调试so获取解密算法虽然我们成功的调用了so解密出内容了,但是这个不是本文的重点,本文的重点是把这个解密算法弄出来,不过在之前已经分析了大概,我们只需要弄到aes的key和iv值即可,这里有两种方式一种是用Frida进行hook操作,一种是动态调试,这里动态调试非常简单,前提是用我们上面已经修改过指令的so包,不然内部有一些反调试检测。为了方便用我们的demo工程进行动态调试即可:
第一步:运行手机端的android_server第二步:端口转发 adb forward tcp:23946 tcp:23946 第三步:调试运行程序 adb shell am start -D -n cn.wjdiankong.awwechathack/.MainActivity 第四步:打开IDA附加进程