一、样本静态分析
最近有位同学发了一个样本给我,主要是有一个解密方法,把字符串加密了,加解密方法都放在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附加进程
第五步:设置Debugger Option中勾选上加载so断点
这里其实是为了防止JNI_OnLoad函数中有一些检测,我们需要断点调试找到这些检测,但是因为我们之前已经手动的把检测给弄掉了,所以这里勾不勾选都无所谓了。
第六步:等待调试jdb连接
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8600
这里的端口号需要从DDMS中查看,是个红色小蜘蛛:
第七步:IDA中运行程序或者按F9
这里可以一路往下按,因为会加载很多系统so文件,一路按下去直到加载到了我们自己的so,这里没有检测直接过去即可
运行成功就jdb连接成功了。
第八步:在Module中找到需要调试的so文件
找到之后双击进入so文件。
第九步:找到需要调试的函数
找到需要调试的函数之后,点击进入查看即可,到这里我们就给我们需要调试的函数下好了断点信息,上面的几个步骤都是基本操作,因为之前已经介绍了很多遍了,这里就不再多介绍了,如果想了解更多内容可以购买本人的逆向大黄书《Android应用安全防护和逆向分析》;然后我们在通过静态分析获取到我们那个aes解密算法地方:
我们可以通过静态分析so找到那个需要调试的函数地址:
看到这里双开IDA的好处是动静结合非常方便,这时候我们只需要查看函数调用前的参数值也就是那几个MOV指令的寄存器值:
R0寄存器保存的是需要解密的内容:
这里看到后面的几个参数很可能就是key或者是iv值,我们点击查看R3寄存器的详细值:
看到这里的数据有点特别,首先是16个字节的不知道是啥可能是iv或者是key值,但是后面接着是一个16字节的值,所以这里看到这两个16字节的值可能就是我们想要的key和iv值了,所以这里有一个重要的知识点就是key和iv肯定是16字节值,因为用的是aes_cbc_128的加密方式。可以继续往下看:
看到这个不确定是iv还是key的值,接着往下走:
看到这个解密后的内容就开心多了,而且看到后面的值是9,也就是总长度16-9=7也就是version的长度,这里的总长度是固定的16,因为Java层传递的字节数组长度是16。
四、Hook获取解密算法
好了到这里我们成功的获取到了需要的key值和iv值,但是不确定他们是具体的值,这个简单,我们用Java代码写一个然后尝试互换彼此即可,但是到这里就结束了吗?其实不然,因为按照我的性格我会通过一个案例把我知道的我会的统统告诉大家,所以我们用另外一种方式获取key和iv值,因为看到上面的动态调试虽然靠谱但是过于繁琐,所以这里就用之前介绍过的Frida框架来hook这个解密函数直接打印他的几个参数值,关于Frida来hook功能不了解的同学可以查看这一篇文章:Hook神器Frida介绍;这里我们看到这个加密函数是导出的:
然后写一下frida的hook脚本:
然后我们就开始运行了,当然前提是你得安装好Frida环境,具体内容看我的那一篇文章即可:
第一步:运行手机端的frida-server
第二步:转发端口
第三步:运行hook脚本
运行之后我们就看到数据了,通过和上面的IDA动态调试出来的数据也是一致的,而且发现这种hook方式太无敌了,非常方便快捷,太好用了。
五、Java代码实现解密算法
接下来我们就把这个key和iv放到Java代码中运行一下吧:
这里不确定key和iv值,互换一下尝试就弄出来了:
然后运行就成功解密了:
六、技术总结
到这里我们终于把so中的加密算法解密出来了,本文的内容非常多,因为我不想给大家只是一个结果,我在这个过程中我遇到的问题和解决办法以及我学到的东西我都想告诉你们,接下来我们总结本文的内容:
第一、修改指令,如果不想让一个函数执行,只需要把跳转指令修改成NOP空指令即可,前提是这个函数的执行结果和后面的逻辑没有半毛钱的关系,如果有那么就需要修改函数的返回值,一般需要修改跳转指令之后的MOVS指令的寄存器值,如果简单点可以直接修改变量的初始化值,比如这里的过掉签名校验。
第二、如果快速的知道so中是否有签名校验功能,可以直接在字符串列表中搜索”signatures”即可,现在也有很多应用会在so中调用Java层的类信息,所以需要去看JNI_OnLoad中arm指令,或者直接搜索字符串列表,因为一般Java层类信息,都是xxx/yyy/zzz/MMM这样的字符串格式,通过肉眼排查也是可以的。
第三、动态调试so的步骤还是需要熟悉的,但是有时候为了方便快捷Frida直接hook得到结果也是一个非常不错的选择。
第四、对于一些常规的加密算法的特点一定要知道,这个是对于一个破解者最基本的素质要求,本文如果知道了aes_cbc_128的加密方式的key和iv都是16个字节对于本文来说是非常关键的。没事别看那些步兵骑兵啥的,多看看加密算法的特点。
本文的目的只有一个就是学习更多的逆向技巧和思路,如果有人利用本文技术去进行非法商业获取利益带来的法律责任都是操作者自己承担,和本文以及作者没关系,本文涉及到的代码项目可以去编码美丽小密圈自取,欢迎加入小密圈一起学习探讨技术
七、总结
感谢这位同学提供的样本,每一次样本我们都可以学到很多东西,本文通过动态调试,Hook操作都是可以获取到解密的key信息,所以问题只有一个,办法有很多种。写这一篇文章真的不容易光截图就几十张了,看完希望大家多多转发支持!
《Android应用安全防护和逆向分析》
点击立即购买:京东 天猫
更多内容:点击这里
关注微信公众号,最新技术干货实时推送