声明:本教程仅用于学习交流,有任何侵权问题请联系本人删除
本教程案例请关注微信公众号:逆向小白,联系作者获取
使用fiddler抓取某app登录接口的时候,没有看到任何相关的数据包,猜测app做了防抓包或者不走http协议,尝试proxyDroid、小黄鸟还是不行,最后使用Packet Capture成功抓到数据。看了下登陆接口提交的参数,不知道sign值是怎么来的,所以目标很明确,就是需要逆向分析sign的生成算法。
将apk文件拖到jadx中进行反编译,ctrl+shift+f 搜索关键词sign出来很多信息但没找到有用的,试试搜索"sign",仔细查看发现图示可疑位置,点进去看看。
看到图示内容,可以确定找对了地方,sign值是getSign方法执行后的结果,getSign方法中调用了SignManager类getSign方法,传入了三个参数,第一个参数不确定其值,第二个参数是token值,前面抓包发现这个值是空的,第三个值是时间戳,右击getSign跳到方法声明的地方。
加载so、通过native关键字定义了需要调用的方法getSign,也就是说,它这里调用的是so层的加密算法,so是什么?简单来说,它是c/c++编译后的产物。既然目标app能调用,那我们也可以。在搞之前,还有个参数不确定。这里就用jeb调试一下看看。
将apk拖到jeb中进行反编译,dex反编译成smali,我们可以调试这个smali代码, ctrl+f 搜索"sign", 找到getSign方法,ctrl+b下断点。
确保手机已root并且可调试,如果没有开启调试,到网上去下载相应的mprop, 使用adb推送到手机里,通过如下方法可以临时设置调试状态,这个方法是修改内存种得值,重启手机后恢复不可调试状态。如果你想要永久设置,到网上找相应的教程。然后可以通过adb连接手机,确保jeb可以识别到就行。
点击附加调试,选择需要调试的进程包名附加进程。
手机上输入账号密码点击登陆触发断点
上面分析过,断点处的getSign传入了四个参数,而真正用到的是后面三个,这三个参数值分别放在寄存器v1、v2、 v3中,将int修改为string, 可以看到,v1中的值是空的,v2中的值是空的,v3中的值应该是时间戳,如果你不放心可以多次尝试。
三个参数值确定了,下面要做的工作就是调用so获取sign值,这里参考花哥提到的第一个so调用方案。好的,通过压缩软件打开apk,因为我手机的cpu类型是arm64-v8a, 是向下兼容的,选择使用armeabi-v7a中的so文件是可以的。
将so文件复制到安卓项目的libs->armeabi-v7a下面,并且在app->build.gradle中限制支持的平台,不加这个我这是报错的,加上限制条件,app也不会显得那么臃肿。
在src->main->java下新建一个包名为com.sichuanol.cbgc.util的包,在包下面创建一个名叫SignManager的类,复制源码过来就行。
然后在控制器中调用getSign方法,运行项目安装到手机上,启动服务。
浏览器访问服务接口,返回的是so文件名,竟然翻车了!!!
看了下日志信息,缺少了某个类,尝试复制对应的文件过来,但还是报错缺各种东西,猜测是不是有签名校验,那么调用so的方案就失败了?问题总得解决,换另一个方案,用IDA逆向分析so。跟着大佬学习一下IDA的调试过程,做一个简单的记录。
将so文件拖到IDA中,Exports->**getSign,双击点进去。
这是so反编译出来的汇编代码,看不懂啊,像我这种菜鸡还是去看看伪c代码吧,按F5或者Tab。
**getSign函数中五个参数,第一个参数是JNIEnv类型指针,它指向java环境,通过这个指针就可以对java的代码进行操作,第二个参数是java传递过来的类/类实例,后面参数就是我们上面分析的那三个,有人说,这里的参数类型明明都是int,而且看代码似c/c++不c/c++的,其实是这样的,ida反编译so不可能百分之百还原c/c++代码, 而且这里还用到jni函数,jni又是什么?简单的说,有了jni可以实现java代码和c/c++代码互调。
这些强制类型转换看着太不舒服了,右击hide casts把它们隐藏。
这个看起来就舒服多了,但是看到一些*v5加上数字这又是什么,上面分析过,第一个参数a1是JNIEnv指针,赋值给v5, 那么这一些操作好像是指针偏移,可能是取了jni的函数或者变量。
为了让人能看懂这些jni函数,我们需要导入jni.h头文件,然后改第一个参数为JNIEnv类型指针。
选中第一个参数a1, 右击选择convert to struct*, 选_JNIEnv, 然后再更改指针名为env。
我们将v5的名字也改一下,改成env_,改完之后你会发现,这才是人能看懂的代码啊。
GetStaticMethodID获取java静态方法ID, "getAppSign"为方法名,如果没有取到方法ID就会进行异常处理了。好吧,这就解释了为啥我们调用so报错了。
用jadx搜一下getAppSign方法,我们看到,方法所在的类就是之前报错缺失的,getAppSign方法取了签名值,如果异常返回一个空字符串。要知道,你不改动apk文件的情况下,这个app签名是固定不变的。看c代码中没有涉及到其他java自写方法,那我们只要拿到这个签名值,so调用的方案不就简单了吗?
编写一个hook模块,不会的可以网上搜教程或者参考本人第一篇安卓逆向教程,这个难度不大,最后拿到了这个签名值。
把取签名的方法所在的类文件复制到之前的so调用的项目里,放在同样的包名下,把多余的方法删掉,getAppSign方法只需要返回这个固定字符串就可以了。
运行项目安装apk开启服务,我们拿教程第一张图中的时间戳进行验证,浏览器访问这个接口发现拿到数据了,这个结果跟抓包的结果是一样的。那么简单的so调用就这么搞定了。我们继续看IDA分析调试的方案。
回到伪c代码,大概看看代码你应该知道,取了签名值之后,又有字符串拼接等操作,后面执行MD5Digest函数传入了三个参数,第一个是字符串,第二个是字符串的长度,第三个用于存放运行结果,我们只需要知道这个字符串就好办了,看伪c代码是看不出来的,顶多可以猜一下,不过想着能多讲一些安卓逆向知识,这里还是介绍一下IDA调试。
调试状态下为了效率问题并不会完全解析代码,就会有个问题,我们不好找下断点的地方,因为函数名变成我们不认识的,那么我们需要另一种方法,从函数开始的位置往下数,看MD5Digest位于第几个BLX, 记住这个位置。
到网上下载相应的android_server,用adb将其推送到手机里,具体参考网上教程,然后把名称和端口号最好改一下,有的app会根据这个名称和端口号进行反调试,但是本教程案例没有反调试,默认也可以,然后开启服务,进行端口转发。
重新打开IDA, 远程附加进程。
慢慢等待,出来下图所示的界面点击左上角的启动按钮,让app在调试状态下运行,然后找到Modules, ctrl+f 搜索你需要调试的so, 点进去找到需要调试的函数
一般情况下,当某个函数被调用的时候,R0, R1, R2, R3寄存器存放函数的参数值,因为MD5Digest函数,传入了三个值,所以我们后面调试的时候,只需要关注R0、R1、R2寄存器就可以了,接下来,我们需要找到下断点的位置。
找到前面记住的BLX位置,如果你不确定这个位置是否正确,可以点进去,然后按c键,可以看到这个地方就是我们需要下断点的位置,把方法函数名改一下,方便查看。
按F2添加断点,手机输入账号密码点击登陆触发断点。选中十六进制窗口右击同步R0寄存器
根据R0寄存器的地址找到存放的数据,可以看到该值是一个字符串,字符串前一部分就是我们上面取到的签名值,后面一部分是时间戳,把这个字符串复制一下以遍后面验证使用,
R1寄存器中存放的值是16进制数值2D, 转换为十进制为45,正好是R0寄存器存放的字符串的长度。
同步R2寄存器,因为MD5Digest函数还没有执行,所以这里的值不是最终的结果,我们按F8继续执行
下图标出来的就是计算后的结果,那么这个MD5Digest和python的md5计算结果是否一样呢?
经过验证,发现结果是一样的。本教程案例没有遇到反调试,app加固等情况,加密函数的逻辑比较简单容易处理,即使这样,搞起来还是很费劲的,app逆向需要掌握很多知识,只能慢慢啃,急不得。