Android Native Reverse
Android native 逆向
前置知识
NDK
NDK 即 Native Development Kit,是 Android 中的一个开发工具包,使您能够在 Android 应用中使用 C 和 C++ 代码。
使用NDK可以快速开发 C、 C++ 的动态库,并自动将 so 和应用一起打包成 APK。即可通过 NDK使 Java 与 Native 代码(如 C、C++)交互。

JNI
JNI 即 Java Native Interface,是一种编程框架,使得 Java 虚拟机中的 Java 程序可以调用本地应用或库,也可以被其他程序调用。 本地程序一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序
使用JNI 首先要声明native方法,之后实现native方法,并生成so文件,最终加载so文件,调用native方法。
JNI 是一个编程框架,是一个抽象的东西,NDK 是一个工具包
ABI
ABI 即 Application Binary Interface。我们上面说了,每个CPU系统只能使用相对应的二进制文件,不同的 Android 设备使用不同的 CPU,而不同的 CPU 支持不同的指令集。


在 Android 手机上安装一个应用时,只有手机CPU架构支持的ABI架构对应的.so文件会被安装。如果支持多个ABI架构,会按照优先级进行安装。x86架构的模拟器也能运行arm的程序是因为其中间对so文件进行了转换,所以在调试或者是用frida hook 时会因此出现不匹配或找不到的问题。
JNI方法的使用
静态注册
native 层的方法名为:Java_<包名>*<类名>*<方法名>(__<参数>)
linux: mkdir -p jni/com/example/task
注册步骤如下
1 | |
在xxxx/jni/com/example/task目录下编译java文件
javac 编译test.java文件生成test.class
进入到 xxx/jni 目录下使用命令: javah com.example.task.test 会生成 com_example_task_test.h 文件,内容如下。
1 | |
可以观察 函数是 java_包名_类名_函数名 参数是 JNIEnv* 和 jobject + 额外参数
将com_example_task_test.h 修改为Myjni.h 或其他 按照自己喜好👨💻。
此时我们只拿到了在java中声明的native函数,还没有给出函数体,故下一步是通过c/c++完善函数并编译出so文件。
1 | |
之后通过gcc 编译MyJni.c生成so文件
1 | |
-I xxxx/include 指定头文件搜索目录,xxx为jdk的安装路径,要用到Jni.h文件 , 并且输出so文件必须以lib开头
如果要想实例运行,test.java中需要调用so文件,如下。
1 | |
完成上述步骤后可通过 java -Djava.library.path=xxx/jni/com/example/task/ com.example.task.test进行运行测试。
上述是静态注册的方法,静态注册的函数名都按照java_包名_类名_方法名的格式来命名,长度比较长,并且类名或包名修改后还要重复上述步骤生成新的so文件。
动态注册
动态注册需要将native方法构造成JNINativeMethod数组,JNINativeMethod结构体定义如下。
1 | |
之后需要重写JNI_OnLoad函数,该函数会在 System.loadLibrary加载完so文件后被调用,在其中完成动态注册。
一个MyJni.c的结构如下。
1 | |
因为JNI_OnLoad是导出函数,所以IDA在exports中可以找到。
其中,主要用RegisterNatives解析
1 | |
上述只是对JNI编程结构有一定的了解,如果要深入的话还要系统学习一下Android开发和java等😔。
libxxx.so文件函数修复
👀易忽略点:so文件是一个elf格式的文件,在so被加载之前,会执行init段的代码,在结束的时候,会执行fini段的代码。所以在init_array中可能会有smc数据解密的代码,往往存在着数据解密。
静态注册的native函数因为命名规范很容易识别,所以难点在动态注册的函数。
jadx中查看native声明,并且之后会调用check函数。

打开lib.so文件,其中native的check函数通过动态注册,因为导出表中无xxxx_check函数。可见JNI_OnLoad函数本身的参数是正确的,但是其中的局部变量类型却不正确😭。

根据上文所述动态注册的步骤(获取JNI的env变量,获取包含native的类,通过RegisterNatives和method数组来动态注册),调整变量修复函数。
1 | |
修改之后,即可直观看出是对一个函数进行了动态注册,跟进method变量,观察其3个值,是经过smc修改的,手动修一下,如下。

动态注册的check函数已经找到,接下来就要逆向分析函数逻辑,不过在此之前,.init_array的内容需要解决,因为本题中发现其中存在解密代码,对一些静态数据进行了修改。

根据jadx中check函数的参数和JNI函数的结构修复check函数。

对于Init段存在字符解密的so文件,可以用frida hook dump出so,这样拿到就是字符串解密后的so文件,可惜👦手上没有root机,暂且硬怼。

首先对传入字符进行base64解密,解密函数也是通过索引表映射来实现的,打印出索引表,b64表被修改,这和RE2 shellcode中所用函数一致。
1 | |
之后进入第一个处理函数,pre_getstr通过交叉引用知是java层调用prenative函数传入的字符串,分析知该函数为AES128的秘钥扩展。

不过这里的sbox经过换表了,接下来是sub_2DE8函数,是AES的加密主体。由控制流程图知,他在进行加密的时候没有用循环,代码复用导致函数比较大,比较难读。

类似这种块主要进行轮秘钥加和行位移,之后sub_2668执行列混淆,用到了一个数组

通过IDAPYTHON dump出数组,根据数组去github上搜一下,找到了类似符号的源码aes_c.c - github,这段逻辑也主要用于列混淆中。
1 | |
如下图,用到了两个表来列混淆,特征比较明显,一般这种使用数组来实现的,可以在github上找到到蛛丝马迹。

通过交叉引用和第三个参数128可知调用了9次,对应AES中间的9轮,所以上述AES单纯魔改了sbox,其余算法不变。
到此,一个动态注册的函数分析完毕,从修复JNI和注册信息,再到定位函数逆向逻辑,最后逆向还原即可💪。
😴至于后续如何,未完待续。。。。
参考:
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!