安卓逆向:某西到家商品列表so层签名分析

前言

撸袖子干,速战速决。。

分析

    抓包,环境问题,我依旧用的Packet Capture,简单抓个看看。

    signKeyV1就是要找的签名了。

    这个App的Java层的比较简单,没什么大的混淆,都是些老生常谈的常规操作,就快速走一遍了。首先jadx反编译,先搜索一波签名的key "signKeyV1"看看。很幸运,直接搜到了。

从最里层往外层看,先是getParams():

this.params的来源是这个方法:

继续找,走到这里。就是传了一个json,转成了一个字符串

BODY就是字符串body:

猜测就是数据包的JSON。然后外层的formatQueryParaMap方法,就是把Map转成字符串呗。

最后对字符串取了bytes值,传到k2方法里得到sign。k2。。好吧,so层来了。System.loadLibary("jdpdj"), 也就是加载了libjdpdj.so这个so。

    首先hook确认一下是不是走这个方法,如果不是,我们可就白搞了。这里k2的参数是Byte数组类型,所以我们要把它转成字符串输出。

OK,hook到了,那就继续吧。打开IDA反编译一波,打开Exports窗口,可以看到他的导出函数非常之多

    so层函数注册分为两种,静态注册以及动态注册。如果是静态注册,那么这个函数就会出现在导出函数列表中,并且以Java开头,格式是Java_类名_方法名,其中所有"."都用"_"代替。我们搜一下看看:

    并没有k2方法。那么就是动态注册了,一般来说,动态注册的位置可以是init_array或者JNI_Onload,当然他也有可能把这个位置藏的深一点,让你找不到。其会调用registerNatives方法,传入一个结构体进行注册,而结构体里就是我们要的函数信息,包括函数名、签名、以及地址。

    这个so中动态注册的位置并没有藏,直接就在JNI_Onload里。先点击JNI_Onload,跳转到对应的汇编代码位置

    直接F5反编译一波,IDA反编译出来的伪代码非常之不可靠,所以F5的结果仅供参考。。必要时还是要啃汇编。

    我们先对他简单修复一下,这里参数类型如果不熟悉,直接百度就好了。鼠标放在int上,然后按y,手动声明参数类型。这里我们把它改成JavaVM。

    可以看到,伪代码中的GetEnv方法已经被识别出来了。

    但是这个代码逻辑有点问题。他是把a1的指针传给GetEnv,返回的结果,也就是env就保存在a1的地址里,调用NDK的所有函数都需要env这个指针。而他在拿到env之后并没有操作a1,反而进行了一堆v2和v5的操作,这个逻辑是有问题的(所以说,IDA的反编译不靠谱),其实这个地方你猜也猜得到v5就是env,猜不到怎么办?看汇编。

    ARM汇编的基础指令这里就不说了。可以对照着伪代码和汇编一起看,定位的方法可以数函数个数。B代表执行函数,所以执行的第一个函数,34D8A位置执行了R3,也就是伪代码里执行的第一个函数,GetEnv。执行完以后会将env放在第一个寄存器,也就是R0里。

    再往下看,我们知道伪代码里调用这个函数的时候传了两个参数,第一个是v5,第二个"jd/net/z",

    在汇编里也出现了后面的这个字符串,那么下面的BLX就是调用没有被识别出来的(v5+24)这个函数呗。调用时传的两个参数,R0和R1,R1是那个字符串,R0是什么?当然是Env啊。。所以对应一下,v5就是Env。

    按上面的操作我们给v5和v2都改一下类型,设置为JNIEnv *,好了,现在我们已经找到了RegisterNatives方法,可以看到传了4个参数。这里第三个是动态注册的结构体,第四个是要注册的函数个数。

    直接双击&off_117004跳到结构体的地址,我们已经找到k2了。

    每三行就代表一个函数的结构体,还记得我们上面说的吗,结构体里的三个值,分别是函数名、函数签名、地址。也就是gk2+1就是k2的地址,直接点击跳过去。

    F5反编译:

    上面做了一大堆操作,包括了一坨位运算。你要是想挨行分析的话,能看懂的话也可以,反正我是看不懂。那就直接找找我们能看得懂的部分。往下翻,这个j_hmac_sha256就有点意思了。

    然后,你可以选择硬啃挨行分析,或者动态调试,或者去hook。对于这个so而言,很明显最简单的方法是Hook。

    这里可选的hook点有很多,我就直接选j_HMAC_Update这个函数了。先找到他的基址:30068,后面hook要用到。

    so层的hook和java层不太一样,首先要拿到so文件的基址,这个地址是在内存中的,每次打开App都不一样。然后基址+偏移拿到真实函数地址。这里还需要考虑一个处理器类型以及指令集类型的问题,有的app会在lib目录下按CPU类型分出几个文件夹,对应不同的CPU版本。而这个App只有一个so文件,所以不需要关注处理器类型,只需要看一下指令集。如果指令集是thumb类型,需要将地址+1,如果是arm指令集则不需要。这里是arm指令集,具体区分方法百度吧。。

    这里拿到的所有参数和结果,都是地址,如果需要查看字符串内容,可以使用readCString方法,因为这里不是Java的字符串,没法直接读出来。如果地址的值不合法或不是常规的CString,则会报错。这里有三个函数,但是只有args[1]才能读到内容。所以其实args[1]里就是原文,密钥后面会讲。

    我们确实读到了原文值,但是加密结果不对,0x1肯定不是结果的地址。

    有的时候会给函数传过去一个指针,将加密的结果保存在指针指向的地址里。这里我并没有找到。。所以也就想出了另外一个主意,他既然要返回给Java层,必定要把CString转成JString,这一步一定会走NewStringUTF方法,    观察伪代码,在hmacsha256后并没有在做其他操作了,那么我去hook NewStringUTF,拿到的应该就是加密后的值。

    差不多的方法,基址+偏移,只不过这里的基址是libart.so的地址。然后再过滤一下,hmacsha256的结果长度是64位,我们只输出64位的字符串就行了,否则会有很多无关参数。

    出来了。

    然后还有一个没解决的问题,hmacsha256是需要一个原文值和一个密钥的,这里怎么只有一个值?这里你可以Hook顶层的hmac_sha256函数或者其余的几个函数,就会找到32位的密钥了。

    这里有一个更方便的方法。通过分析,可以得到,上面的这个s就是明文值,v23是明文的长度,c语言里很喜欢用这种方法,传一个字符串的同时也传一个长度过去,这个长度不是毫无根据乱给的。他后面也会用到。

    其实密钥和明文他已经绑在一起了,上面我们hook到的参数,切掉后面32位就是需要加密的明文值,而后面的32位就是密钥。

    不信的话我们可以拿上面的结果试验一下:

    和上面hook的结果一模一样。联合java层的hook看看:

    打印的有点多,不过不影响。入参居然没有加盐。。直接原模原样的取哈希了,那么这个so主要的问题就是拿到密钥而已。最后再找到对应的数据包就行了。

总结

    由于是第一次写so层的文章,步骤我尽量写的细一些了,不过不得不说还是花去了几个小时的时间。。这个签名比较简单,依靠frida完全能解决。但是这个app是有xposed和frida检测的,解决的方法。。。自行寻找。不过这个检测不过也没问题,先开App再开frida就行了。

    这种so层不混淆而且用现成算法的,还比较好解决,如果混淆了还可以从秘钥或者结果长度去猜,猜不到,那确实就没啥办法了。。硬刚或者调吧

        声明:本文分析过程仅供学习,并无任何个人以及商业或其他用途,如因参考文章造成违法行为本人概不负责。如有不慎侵权,请联系我删除。