首页
论坛
专栏
课程

Android逆向新手答疑解惑篇——JNI与动态注册

葫芦娃 发布于 移动端那些事 2018-02-15 05:00

Android逆向新手答疑解惑篇——JNI与动态注册

何为JNI

JNI全称为Java Native Interface,是使Java方法与C\C++函数互通的一座桥梁。通俗的讲,它的作用就是使Java可以调用C\C++写的函数、使C\C++可以调用Java写的方法。

JNI的情景应用

性能

众所周知,Android开发一般采用Java语言,虽Google推出了Kotlin语言的开发方案,但其实Kotlin的本质亦是基于Java虚拟机,那么在Android上系统,亦是基于Dalvik虚拟机的,所以性能上,与跟采用Java开发是没有任何区别的。由于Java是虚拟机语言(指需要被编译成虚拟机代码,由虚拟机执行的语言),所以无论是JVM(Java虚拟机)还是Dalvik(Android定制版JVM),其程序性能在性能需求较高的情况下,就显得有些不足了。
那么这个时候就需要编译型语言出马了,编译型语言将源代码编译为机器码直接由CPU执行代码,使性能大幅提升。

代码安全性

Java代码的安全性很弱! 如果你没有逆向Java或者Android程序的经验,那么可以请你写一个简单的Java程序或者Android程序,然后在Github或者其他地方下载一个jadx,打开jadx-gui或者使用命令行,反编译你编译出来的程序,你可能会发现这是一个新世界,噢天哪,代码逻辑清晰可见,简直就跟在看源码一样!当然,这些只是反编译器生成的伪代码,但也足以惊人。
这个时候,你就可以开始考虑将关键代码放到C\C++里面写了,因为其编译之后就只有机器码,机器码可以反编译成汇编,但汇编比高级语言更加的晦涩难懂,没有一定技术功底的人无法直观的理解汇编代码。虽可通过一些神器(如:IDA F5)来获取伪码,但这些伪码相比Java的伪码,简直不堪入目。
所以编写原生代码,不但可以拥有更高的性能,还可获得一定的代码安全性保障。

JNI的使用

Google为Android的原生开发提供了开发者工具NDK(Native Development Kit),用来编译C/C++项目。起初的时候构建一个NDK项目还需一番配置,现在随着Android Studio的不断更新,已经可以在Android Studio的项目中直接编写、编译了。

配置Android Studio & SDK

需要先对Android Studio进行一番配置。首先打开Android Studio的设置页面,File-Settings,搜索Android SDK,勾选上CMake(编译C\C++源码的程序)、LLDB(调试器)、NDK,然后点击Apply进行更新。
配置Android SDK
此处我没有勾选NDK是因为我使用自行下载的NDK版本,每个项目自行选择NDK路径。

新建项目

打开Android Studio新建一个Project,并第一步勾选Include C++ support:

其余选项可按需改动。新建完成后,就是一个完整的JNI的Hello World了。

项目分析

在左侧的Andorid视图中,可以看到比正常的项目多了一个cpp目录,这就是我们存放C\C++源码的地方了:
Android视图
生成的这个函数声明看起来有点反人类,其实他是这样子的

JNIEXPORT jstring JNICALL Java_cn_hluwa_demo01_MainActivity_stringFromJNI(JNIEnv *env,jobject /* this */)

Ctrl单击JNIEXPORT可以看到其宏定义,是一个defalut属性,而JNICALL则是个空定义,所以其实这两个是可以忽略的。
重点关注的是返回类型jstring函数名Java_cn_hluwa_demo01_MainActivity_stringFromJNI参数列表JNIEnv和jobject

JNI中数据类型

大家伙知道,Java中的基本数据类型是int、long、short、float、double、char、byte、boolean这些,为了避免与C语言的基本数据类型冲突,在JNI中,将JAVA的基本数据类型重定义成了:jint、jlong、jshort、jfloat、jdouble、jchar、jbyte、jboolean。那jstring又是怎么回事呢?虽然String不是Java基本数据类型,但它实在是太常用了,所以便有了jstring;对于数组,则是再后面再加个Array,如:jintArray、jbyteArray,但是没有jstringArray,欸,那如何表示呢?还有其他的非基本类型呢? 除了上述以及jclass、jthrowable、jarray这些有专用重定义之外其他类型均使用jobject表示,所以String数组就是jobjectArray啦。Ctrl+单击jstring就可跳到jni.h头文件查看各个定义了。

JNI函数命名规则

可以看到这个函数名非常的长,这是因为JNI函数的绑定需要依赖于一个函数命名规则,让Java层一下子就可以找到对应的原生函数。可以先看到java层的代码:

package cn.hluwa.demo01;
...
public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("native-lib");
    }
...
    public native String stringFromJNI();
}

stringFromJNI 加了一个native描述符,表示是一个原生函数,MainActivity是类名,cn.hluwa.demo01是包名,Java_cn_hluwa_demo01_MainActivity_stringFromJNI是对应的C函数名,那么这个规则就很显而易见了,将包名的.替换成_(因为.不能用于函数命名),然后Java_PackName_CLassName_MethodName。运行时,JNI就会依赖此规则来对函数进行绑定。

 

至于Native层调用Java层呢,JNI提供里一系列函数,比如:

    jclass      (*FindClass)(JNIEnv*, const char*);
    jclass      (*GetObjectClass)(JNIEnv*, jobject);
    jboolean    (*IsInstanceOf)(JNIEnv*, jobject, jclass);
    jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
    jobject     (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...);

同样在jni.h中可以看到,或可自行查阅文档。

JNI的逆向

JNI的加载流程

在上述的Java代码中,可以看到static代码块中多了一个System.loadLibrary("native-lib");,在Android开发中,原生代码一般使用C\C++编写,然后编译为一个动态链接库,即文件后缀为".so"的ELF文件loadLibrary的作用就是加载这个动态链接库,这样后面的代码调用才能成功的找到对应的原生函数。而静态代码块的执行时机非常早,比什么构造函数、onCreate都要早,在类加载的时候就被调用库加载并非一定要在当前类、static块中!。加载库还有其他方法,比如使用System.load(String)方法,其传入链接库的具体路径;甚至有的是在Native层中使用dlopen、mmap等方式来进行加载,就相当于自己实现了一个loadLibrary,但是最终的目的都是一样的:将代码加载入内存中
Android编译后的Apk其实只是个zip压缩包,打开后在其lib目录中可以看到那些被loadLibrary加载的库(lib中可能有多个文件夹,对应多种CPU架构)。

初始化函数

  1. 在Android系统中,对链接库进行加载的程序叫做linker,文件路径为/system/bin/linker。linker加载so的时候会依次调用其init_array中的函数来执行开发者的初始化代码,可在IDA中按shift+f7打开Segmentation视图,若有.init_array项,那么其中的函数就会被依次执行,这些函数都没有参数。
    注:更多精彩可看linker的源代码。:)
    init_array内容
  2. linker中加载so的函数叫做dlopen,而loadLibrary跟load其实也是基于dlopen,但其添加了一个回调就是JNI_OnLoad,只要在代码中定义一个名为JNI_OnLoad的函数,dlopen完成之后就会将其调用。JNI_OnLoad的定义如下:
    jint JNI_OnLoad(JavaVM* vm, void* reserved)
    
    vm参数一般只是用来获取env,以便调用一系列JNI函数。在IDA中,如果使用F5看到的是一个没有参数或者参数类型不对的JNI_OnLoad,比如这样:
    JNI_OnLoad
    这是因为IDA不能准确的识别函数声明或变量类型,请点击函数名或者相应的变量名,然后按下y键,修改成正确的声明\类型即可。

    JNI函数的参数

    根据stringFromJNI的例子可知,Native层多了两个接收参数JNIEnv*和jobject,然后后面才是java层传递过来的参数。IDA经常不能正确识别参数列表,所以手动y的时候一定要正确的修改,就像这样:
    stringFromJNI

动态注册

如今许多开发者都出于安全性考虑或其他需求,不愿使用函数名规则绑定,而是自己动态注册来绑定native函数。方法也很简单,只需调用RegisterNatives函数即可。其申明如下:

jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)

clazz 就是native函数所在的类,可通过FindClass获取(将.换成/);methods是一个数组,其中包含注册信息,nMethods是数量。实例代码如下:

JNIEXPORT jstring JNICALL stringFromJNI(JNIEnv *env,jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}


jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv * env;
    vm->GetEnv((void**)&env,JNI_VERSION_1_6);
    JNINativeMethod methods[] = {
            {"stringFromJNI","()Ljava/lang/String;",(void*)stringFromJNI},
    };
    env->RegisterNatives(env->FindClass("cn/hluwa/demo01/MainActivity"),methods,1);
    return JNI_VERSION_1_6;
}

JNINativeMethod结构体有三个成员,第一个是java层的方法名,第二个是方法签名(括号内是参数类型括号后是返回类型,具体可搜索JNINativeMethod signature这里暂不多讲),第三个是C函数指针。这样三个参数就便成了一组注册信息。

 

反编译的时候可能会是这样子的(C++编译。C编译出来函数名只有RegisterNatives):

哇塞为什么有四个参数?不要慌..第一个其实就是JNIEnv,第二个是class,第三个是methods。
所以如果在逆向过程中看到这个函数的调用,那么直接查看第三个参数即可得到具体的注册信息。

最后

祝大家2018新年快乐,万事如意。
前面涉及的一些理论知识,废话稍多..见谅
若还有何新手常见的问题可留言提出,然后顺便点一下关注谢谢:)


分享到:
最新评论 (0)
登录后即可评论