Android Native Reverse(...ing)
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 | package com.example.task; //包名 |
在xxxx/jni/com/example/task
目录下编译java文件
javac 编译test.java
文件生成test.class
进入到 xxx/jni
目录下使用命令: javah com.example.task.test 会生成 com_example_task_test.h 文件,内容如下。
1 | /* DO NOT EDIT THIS FILE - it is machine generated */ |
可以观察 函数是 java_包名_类名_函数名 参数是 JNIEnv* 和 jobject + 额外参数
将com_example_task_test.h
修改为Myjni.h
或其他 按照自己喜好👨💻。
此时我们只拿到了在java中声明的native函数,还没有给出函数体,故下一步是通过c/c++完善函数并编译出so文件。
1 |
|
之后通过gcc 编译MyJni.c
生成so文件
1 | gcc -fPIC -shared -o libMyJni.so MyJni.c -I xxxx/include |
-I xxxx/include 指定头文件搜索目录,xxx为jdk的安装路径,要用到Jni.h文件 , 并且输出so文件必须以lib开头
如果要想实例运行,test.java
中需要调用so文件,如下。
1 | package com.example.task; //包名 |
完成上述步骤后可通过 java -Djava.library.path=xxx/jni/com/example/task/ com.example.task.test进行运行测试。
上述是静态注册的方法,静态注册的函数名都按照java_包名_类名_方法名
的格式来命名,长度比较长,并且类名或包名修改后还要重复上述步骤生成新的so文件。
动态注册
动态注册需要将native方法构造成JNINativeMethod数组,JNINativeMethod结构体定义如下。
1 | typedef struct { |
之后需要重写JNI_OnLoad函数,该函数会在 System.loadLibrary加载完so文件后被调用,在其中完成动态注册。
一个MyJni.c的结构如下。
1 | #include<jni.h> |
因为JNI_OnLoad
是导出函数,所以IDA在exports中可以找到。
其中,主要用RegisterNatives解析
1 | jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)//参数依次是 native所在类 函数数组 和 函数个数 |
上述只是对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 | jint JNI_OnLoad(JavaVM *vm, void *reserved) |
修改之后,即可直观看出是对一个函数进行了动态注册,跟进method变量,观察其3个值,是经过smc修改的,手动修一下,如下。
动态注册的check函数已经找到,接下来就要逆向分析函数逻辑,不过在此之前,.init_array的内容需要解决,因为本题中发现其中存在解密代码,对一些静态数据进行了修改。
根据jadx中check函数的参数和JNI函数的结构修复check函数。
对于Init段存在字符解密的so文件,可以用frida hook dump出so,这样拿到就是字符串解密后的so文件,可惜👦手上没有root机,暂且硬怼。
首先对传入字符进行base64解密,解密函数也是通过索引表映射来实现的,打印出索引表,b64表被修改,这和RE2 shellcode中所用函数一致。
1 | a=[...] #删除末尾重复0 |
之后进入第一个处理函数,pre_getstr
通过交叉引用知是java层调用prenative函数传入的字符串,分析知该函数为AES128的秘钥扩展。
不过这里的sbox经过换表了,接下来是sub_2DE8
函数,是AES的加密主体。由控制流程图知,他在进行加密的时候没有用循环,代码复用导致函数比较大,比较难读。
类似这种块主要进行轮秘钥加和行位移,之后sub_2668
执行列混淆,用到了一个数组
通过IDAPYTHON
dump出数组,根据数组去github上搜一下,找到了类似符号的源码aes_c.c - github,这段逻辑也主要用于列混淆中。
1 | s1=[0x00,0x02,0x04,0x06,0x08,0x0a,0x0c,0x0e,0x10,0x12,0x14,0x16,0x18,0x1a,0x1c,0x1e,0x20,0x22,0x24,0x26,0x28,0x2a,0x2c,0x2e,0x30,0x32,0x34,0x36,0x38,0x3a,0x3c,0x3e,0x40,0x42,0x44,0x46,0x48,0x4a,0x4c,0x4e,0x50,0x52,0x54,0x56,0x58,0x5a,0x5c,0x5e,0x60,0x62,0x64,0x66,0x68,0x6a,0x6c,0x6e,0x70,0x72,0x74,0x76,0x78,0x7a,0x7c,0x7e,0x80,0x82,0x84,0x86,0x88,0x8a,0x8c,0x8e,0x90,0x92,0x94,0x96,0x98,0x9a,0x9c,0x9e,0xa0,0xa2,0xa4,0xa6,0xa8,0xaa,0xac,0xae,0xb0,0xb2,0xb4,0xb6,0xb8,0xba,0xbc,0xbe,0xc0,0xc2,0xc4,0xc6,0xc8,0xca,0xcc,0xce,0xd0,0xd2,0xd4,0xd6,0xd8,0xda,0xdc,0xde,0xe0,0xe2,0xe4,0xe6,0xe8,0xea,0xec,0xee,0xf0,0xf2,0xf4,0xf6,0xf8,0xfa,0xfc,0xfe,0x1b,0x19,0x1f,0x1d,0x13,0x11,0x17,0x15,0x0b,0x09,0x0f,0x0d,0x03,0x01,0x07,0x05,0x3b,0x39,0x3f,0x3d,0x33,0x31,0x37,0x35,0x2b,0x29,0x2f,0x2d,0x23,0x21,0x27,0x25,0x5b,0x59,0x5f,0x5d,0x53,0x51,0x57,0x55,0x4b,0x49,0x4f,0x4d,0x43,0x41,0x47,0x45,0x7b,0x79,0x7f,0x7d,0x73,0x71,0x77,0x75,0x6b,0x69,0x6f,0x6d,0x63,0x61,0x67,0x65,0x9b,0x99,0x9f,0x9d,0x93,0x91,0x97,0x95,0x8b,0x89,0x8f,0x8d,0x83,0x81,0x87,0x85,0xbb,0xb9,0xbf,0xbd,0xb3,0xb1,0xb7,0xb5,0xab,0xa9,0xaf,0xad,0xa3,0xa1,0xa7,0xa5,0xdb,0xd9,0xdf,0xdd,0xd3,0xd1,0xd7,0xd5,0xcb,0xc9,0xcf,0xcd,0xc3,0xc1,0xc7,0xc5,0xfb,0xf9,0xff,0xfd,0xf3,0xf1,0xf7,0xf5,0xeb,0xe9,0xef,0xed,0xe3,0xe1,0xe7,0xe5] |
如下图,用到了两个表来列混淆,特征比较明显,一般这种使用数组来实现的,可以在github上找到到蛛丝马迹。
通过交叉引用和第三个参数128可知调用了9次,对应AES中间的9轮,所以上述AES单纯魔改了sbox,其余算法不变。
到此,一个动态注册的函数分析完毕,从修复JNI和注册信息,再到定位函数逆向逻辑,最后逆向还原即可💪。
😴至于后续如何,未完待续。。。。
参考: