案例:小H书,版本:6.90.0
本文主要讲述了小H书的shield算法的分析过程,以及用py复原算法时所遇到的坑。关于调试方面,frida足以,虽然麻烦了点。如果能用ida调试就最好,不过他的反调试我没有过多的研究,就没有走ida动态调试这条路。看到还有很多大佬用unidbg的,这个工具我还没有做过多的了解。因为看到了一个改内核可以绕过所有反调试的,之前尝到了ida动态调试的甜头,所以打算试试这一招。话不多说,开始分析。
复原shield,两个native函数,initialize、intercept是关键,核心算法就在这两个函数中,进入shield.so。
搜索一下,发现只有JNI_OnLoad,是动态注册的,进来找一下函数的偏移地址。没有加ollvm,很容易就可以定位到(或者直接用gitlub上面大佬的开源代码,hook nativeregister也可以知道,就算加了ollvm用这个也可以hook到,大佬的东西就是牛哇)。
先来分析一下intercept,因为我一开始没有找到是从哪里开始加密的,intercept里面写了好多东西,我看了半天,只看到了最后拼接XY的地方,其它的地方完全看不懂在干嘛。所以我就从最后往上追,最后追到了所有流程。
这里XY和v53进行拼接,得到了v54就是最终的shield的结果,往上看函数40CEC,看v53是怎么来的
这里函数只传进来一个参数,是用来存储最终结果的,完全看不懂怎么传值过来的,所以这里我也是从下往上看的。不断地hook入参出参,最后在函数40EA8中,找到了关键点。
45258函数的第一个参数的返回值是我们最终的结果,参与计算的是第二个参数v3
进到45258函数,可以看到很清晰的算法逻辑,先循环后判断,以及byte_A8740的值,可以很容易判断出,这是一个base64(因为之前做过一个魔改的base64,所以看到没有混淆的代码,几乎可以一眼判断出是什么)。通过py验证,将之前hook到的v3的值做base64,结果与hook到的结果是一致的。
接下来看一下v3的值是从何而来。回到intercept继续往上看。
函数11920和函数119BC,根本看不出来是啥东西,先不管,往上看函数406FC。
这里40C74的第三个参数的返回值就是后面传入base64中的值(少了16位,但是经过不同手机设备不同账号的hook结果,那16位是不变的,所以我就写死了)。进入函数40C74。
发现关键函数4A94C,第一个参数是一个很长的值(经过测试,这个值是不变的,换设备和账号也不会变,于是我把它写死。。。)。第二个参数是一个数(其实就是第三个参数的len),第三个参数是一个拼接的值,里面包含了device_id、build等信息。第四个参数是用来存储最终结果的。ok进入函数看算法。
有两个for循环,但是看每一段都是一样的,所以可以用py复原成一个for循环,里面也不用写那么长。比如第一个for循环里面有八段,一共循环了十次,用py复原就可以写成一段循环80次。第二个循环有判断,由一个变量控制,每走一段就减一,为0就结束循环。通过hook发现第二个循环第一轮走到第三个if时结束了。所以这个函数一共循环了83次,传进来的第二个参数0x53转成10进制,正好是83。所以可以知道循环次数和第三个参数的长度有关。
这里说一下frida inline hook
这里我想要知道程序运行时,变量v5和init_index1的值,切换到汇编里,对应偏移地址4A9B8,寄存器r0的值就是v5,r1就是init_index1。如果要看相加后的值,需要hook下一条指令的偏移地址,查看r0。inline hook代码如下
这样就可以慢慢看每一处的变量的值了(不知道是我的电脑问题还是frida的版本问题,我用frida inline hook的时候,改代码后必须把控制台关掉重启才会生效。我感觉这是个bug。。。不科学啊)。
这里有几个坑,来说一下。
这里的init+2,其实是内存地址加8字节,看汇编就知道了。
还有一点,在给res赋值时,按这个逻辑会发现init_add2下标越界了,这里我的解决方案是,如果越界,就把下标值减去init_add2的长度。结果就很神奇的对上了。经过多次测试,按照这样的逻辑结果确实是对的,也不会再出现越界的情况。
到这里算法4A94C就分析完成了,用py复原了一下,只有十行左右的代码,还是比较容易的。(不过我踩坑调试,研究了一下午才完成这个函数的复原。。。)接下来函数4A94C的第三个参数是如何生成的,是我们需要继续分析的。
来看一下结构,一共分为四段,第一段看起来像个定值(通过对不同设备和账号进行hook,还真是个定值),中间两段是build和device_id的拼接,最后一段看起来像一个md5的值。接下来主要看一下这个md5的值是从何而来。
继续向上看,在函数404E8的第七个参数v27,发现了md5的值,通过静态分析往上追,不难发现函数105B0的第一个参数,返回了md5的值。
接下来的算法部分如果想要复原,最好先熟悉一下md5,用py撸一遍标准的md5算法源码。复原魔改的算法,就在原本的标准算法结构上去改,这样只需要去看重要的运算逻辑,搞起来会方便一点。不然一行一行抠会怀疑人生…
说一下md5和hmac_md5的区别,这里用两个公式,就可以很清晰的表达。
md5: md5(message)
hmac_md5:md5((key ^ 0x5c) + md5((key ^ 0x36) + message))
hamc其实是在hash算法上套了一个结构。他们的计算结果都是32位的哈希字符串,实际上计算结果的长度取决于hash算法的选择,hamc可以与任意hash算法组合。
进入105B0继续看,他的第四个参数通过hook查看是一个内存地址,往上追溯这个地址是java层调用so的另一个函数然后传给intercept的。
进入函数44418,逻辑不是很复杂,主要看重要参数调用的函数4C818。进入4C818可以看到核心逻辑0x36, 0x5c,通过inline hook查看与0x36, 0x5c异或的值,发现正是之前传入intercept的内存地址中的值,也就是hmac的key。
函数4C4F4中,注意看最后一步,这里通过内存地址执行了一个函数,inline hook查看函数是4C2D4(因为arm和thumb的关系,这里的内存地址其实是+1的,也就是说在ida中对应的偏移地址应该是hook到的-1,所以hook到的是4C2D5,实际函数应该是4C2D4),在ida中找到函数进行查看。
dword_B4D58跳过去,发现是md5 Transform的运算常量(好像和标准的md5相比也有改动,这里没有验证,直接复制出来用)
进入函数4E2D6,发现了md5 init的四个常量,这里做了魔改,init是倒序的。
返回到函数4C818继续向下看
函数4C5C0,返回地址B1EA0,静态分析发现关键函数4CFAA。通过hook入参可以判断这里是md5的update函数。这里4CFAA一共调用了4次,根据hmac_md5结构,可以知道4次调用分别update了opad,ipad,message,md5(ipad+message)。
进入4D108就来到了md5的核心运算逻辑Transform。
Transform部分的魔改主要做了循环左移位数和部分执行顺序的改变,常量因为我是直接复制过来用的,并没有仔细看他有没有改。
md5的部分到这就结束了,关于md5的最后一步final填充值并没有进行魔改直接用标准的就好,xhs方面的md5只改了init和Transform,懂源码的话稍微看一下就能复原了。
如果能把md5复原,就可以单账号去请求了,因为hmac的key是复用的,只有在app启动时执行一次initialize。
关于hmac的key的生成部分,下一篇文章再继续分析。
关于标准版md5,py版和C++版的代码,以及py抠C++时遇到的坑,后续都会在公众号里更新,感兴趣的同学可以关注一波。
最后欢迎各位老司机进群交流:546452230