Android逆向之旅—抖音火山视频的Native注册混淆函数获取方法

一、静态分析

最近在小密圈中有很多同学都在咨询有时候有些应用的动态注册Native函数,在分析so之后发现找不到真的实现函数功能地方,我们知道有时候为了安全考虑会动态注册Native函数,但是如果只是这么做的话就会非常简单,比如这样的:

这样的我们熟知ReigsterNatives函数的参数结构立马就可以获取到Native的实现逻辑函数功能了,所以有的应用为了安全就把这些信息加密或者混淆了,而无意中发现了抖音火山的so就是这么做的,我们用IDA打开他的so文件:

直接找到JNI_OnLoad函数发现他用了OLLVM混淆啥的,先不管这些了,往下看找到RegisterNatives:

这里大家如果看到类似于vXX+YYY这样的,选中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函数查看他的实参值:

然后就可以看到具体参数值了:

点击第三个参数查看注册方法体信息:

可惜这里的数据被加密或者混淆了,而且我们点击函数地址发现也都是常量数据了。

 

二、解决方案

所以到这里我们发现抖音为了防止静态分析就做了这一策略,有的同学会说那就动态调试呀,之前说过了他加了OLLVM混淆,动态调试其实很费劲的,那么我们真的没办法了?所以本文的目的就一个把这些注册方法弄出来,其实最重要的是获取到本地的实现函数,我们现在能想到的有哪些方案呢?

第一个方案:动态调试给RegisterNatives函数下断点查看参数值即可

这个方案首先抖音肯定内部有反调试操作,虽然之前有文章可以把这个解决掉:Android中破解抖音签名算法;但是刚看到了内部用了混淆动态调试很恶心的。

第二个方案:用Frida去Hook这个RegisterNatives函数打印参数值即可

这个方案靠谱方便,但是有个问题就是我们可以知道这个函数的地址所以可以hook他,但是hook之后获取到了参数是JNINativeMethod类型,Frida的api中没有提供这个结构体信息,所以需要自己计算地址打印,这一点有点麻烦,不过本文也会介绍这部分内容。但是不会采用这个方案。当然这个方案的其他选择是使用CydiaSubstrate框架即可,因为本人设备问题没法用这个框架,当然这个是最好的选择,留给大家自己动手学习一下吧。

第三个方案:使用应用内免Root的Hook技术获取参数值即可

这个方案的核心技术在之前的文章已经介绍过了:Android中免Root修改运行时指令代码;我们可以编译好这个模块就是在so中hook系统的RegisterNatives函数,然后打印所有的参数值,最后把这个模块编译成so,以及Java层的调用代码然后添加到抖音应用中,前提是过掉他的签名校验等信息,不然二次打包运行报错的。当然我们还可以直接调用他的so文件在我们的案例项目中,不过这个也是需要解决一些防护问题这个在之前的文章也介绍过了。

 

三、应用内Hook获取信息

从上面的分析本文采用第三个方案,但是也会介绍第二个方案,因为有个知识点需要告诉大家,我们先来看看第三个方案实现吧,先不说了这个核心技术采用的是:https://github.com/ele7enxxh/Android-Inline-Hook;这个框架比较简单,他的代码中是hook系统的puts函数,我们改成RegisterNatives函数即可:

这里hook操作需要三个参数:hook的函数地址,hook之后的我们自己实现的函数,hook的原函数;这里的后面两个函数比较简单就是定义声明:

这个从jni.h头文件中就可以找到,其实这里最难的是如何获取到这个函数的地址,我们知道一般我们都会通过dlopen和dlsym函数通过指定的so中找到函数地址的,但是这里这么做就很麻烦了,那么怎么获取到呢?其实这个也就是后面使用Frida去hook操作需要解决的问题,这个之前有人说通过JNIEnv指针逐个计算得到这个函数地址,因为我们通过jni.h文件中可以看到JNIEnv结构体定义可以这么计算的,但是这么做兼容性很差而且不稳定,这里是否可以直接得到这个函数地址呢?答案是肯定的,我们通过查看jni.h文件内容发现JNIEnv内部有一个变量:

看到这里维护了一个JNINativeInterface结构体变量,然后JNIEnv内部的所有函数调用都是通过这个结构体来完成的,那么我们可以获取到这个变量,然后直接调用RegisterNatives就是函数地址了:

在JNI_OnLoad函数中获取到这个functions变量指针后面直接调用函数就可以了,当然Frida中的处理方式和这个不一样,但是原理都差不多,后面在介绍。所以这里大家一定要记住了以后需要hook JNIEnv的函数都用这种方式去操作即可。然后我们看看我们拦截之后的函数实现:

这里主要涉及到三点内容,我们拦截这个方法之后,首先需要通过jclass获取到类名,这里发了一个错误就是一心想着通过底层去获取,然后走了很多弯路,其实以后我们记住一点如果Native有时候做不了的事情,你有JNIEnv变量,就可以反射调用Java方法,因为有时候Java层做一件事非常容易的,比如这里jclass类型其实就是Java层的Class类型,在Java层用Class获取类名就一行代码的事情非常简单。大家一定要记住这个知识点。然后我们获取到了注册函数信息之后需要打印实际实现的函数地址,但是这个是绝对地址,所以我们需要获取so的基地址然后用绝对地址减去这个基地址就是这个函数的相对地址,有了相对地址就可以去IDA中分析so函数功能了,有了绝对地址也可以hook操作了。最后当然是一定要记得调用一下原始的函数,不改变原始调用逻辑即可。

然后接下来我们直接编译一下这个so即可,但是通过测试发现,我们还需要一个触发逻辑,就是hook操作不要放在JNI_OnLoad中做,因为应用在JNI_OnLoad中调用注册函数的,我们没法获取调用顺序,所以添加一个手动触发逻辑其实也很简单,就是上次一个Native方法即可:

这里然后编译我们的项目库得到了我们想要的so文件,然后在新建一个工程,这个工程主要是调用这个hook的so和需要被hook的so文件,这里当然是抖音的libcms.so文件了:

这个项目中主要就是为了hook住需要被操的应用的so文件,当然签名也说了也可以把这个项目编译弄成smali代码插入到被操的应用中也是可以的,这里为了简单就是直接调用他们的so文件dump出注册函数信息,然后我们运行看看效果:

这里看到我们成功的获取到了抖音的libcms.so文件中动态注册的几个函数名和类名,同时也把函数地址打印出来了,这样我们就成功了,同时我们这个工具可以用于其他应用的这类问题,也就是说以后大家如果遇到这类混淆加密了动态注册函数可以用这个项目工具去解决,那这个一定是成功的吗?其实不然,因为假如人家在JNI_OnLoad中加了检测信息,那么我们这个就歇菜了,因为我们的hook时机在JNI_OnLoad之后了,但是我们可以把这个工具编译到人家应用中然后二次打包调用就可以啦,前提能二次打包运行成功,可以用我的kstools工具啦。

 

四、Frida的Hook获取信息

其实本文的目的很简单,本来是为了解决抖音的混淆问题,但是最后发现这个东西可以做成通用的工具解决这类问题,所以大家以后遇到这类问题就用这种方式操作即可,当然到这里本文并没有结束,因为你看文章不容易,不能让你没有收获就走了,所以接下来我们继续前面介绍的内容就是frida中去hook这个函数然后打印效果,想必大家在frida使用的时候都遇到这种情况就是如何hook系统的JNIEnv函数或者是调用JNIEnv函数,之前有人是通过获取到了JNIEnv指针之后按照指定函数的偏移值计算得到函数地址然后调用或者hook操作,其实我们通过查看frida的env.js源码可以看到他的实现:

其实他内部也是通过函数的偏移地址计算的,这里的frida的env.js源码是在frida-java部分中,这个在官网中没有,需要下载编译,这里后面会给大家:

看到这里的确是通过偏移值计算函数地址,所以我们把他的这种方式拷贝出来就可以得到RegisterNatives函数地址了:

这样我们就可以在Frida中获取到RegisterNatives函数地址了,然后开始hook操作了,但是可惜我们hook之后却很难打印出结构体信息,因为frida中无法找到这个结构体的定义了,要是打印只能靠自己慢慢计算了,这里我不往下走了太折腾了,感兴趣的同学可以自己手动操作一番,那有的同学好奇没结果你说这些有啥用?说这些其实是为了跟你们介绍额外的知识点就是以后可以在Frida中调用JNIEnv函数或者获取函数地址的功能。这一点一定要记住不要在自己去计算了。

好了到这里我们就算把本文的内容全部介绍完了,也获取到了抖音的那几个函数地址,后续会继续静态分析加密算法函数功能,接下来总结一下本文学到的知识点:

第一、有时候在Native层做事情很难,为啥不考虑反射调用Java层代码去获取结果呢?

第二、在hook系统JNIEnv函数的时候可以借助内部的JNINativeInterface*变量进行操作获取指定函数地址即可

第三、在使用Frida进行hook操作的时候如果想调用JNIEnv的函数,可以直接调用不需要自己手动去计算偏移地址,具体可以查看frida源码

第四、以后如果遇到类似的注册函数混淆加密了,请想到我这个工具项目,直接替换你想要hook的so文件即可


发表评论

电子邮件地址不会被公开。 必填项已用*标注