NDK入门指南
原生开发套件(NDK)是一套工具,能够在Android应用中使用C和C++代码,并提供众多平台库,可以使用这些平台库管理原生Activity和访问物理设备组件。
与NDK密切相关的另一个词汇则是JNI,它是NDK开发中的枢纽,Java与底层交互大多数通过它来完成。
JNI: Java Native Interface 也就是java本地接口 ,它是一个协议,这个协议用来沟通Java代码和C++代码。通过这个协议 ,Java类的某些方法可以用原生实现,同时可以让他们像普通的Java方法一样被调用和使用。
也就是说使用JNI这种协议可以实现Java代码和C++代码的相互调用
那为什么要使用NDK开发呢?
- Java是半解释型语言,容易被反汇编成源码,在开发一些重要协议时,为了安全起见,使用C语言来编写这些重要的部分,来增大系统的安全性
- 在一些复杂性的计算中,要求高性能的场景中,C++更有效率,代码也便于复用
下载NDK和工具
在Android Studio中,点开Config

找到SKD Manager

勾选LLIB NDK和CMake
NDK工程说明

新建一个项目,选择Native C++

语言选择JAVA,后续所有操作默认即可

在默认生成的工程中有一个Native方法stringFromJNI

该方法的实现在native-lib.cpp里
Java_com_example_ndkdemo_MainActivity_stringFromJNI
当前这个函数的名称由以下几部分组成
Java_[包名]_[类名]_[函数名]
这个函数默认有两个参数,如果java中有参数,就继续在两个参数后面加。
其中参数一JNIEnv* env ,是JNI的环境指针,我们用到的jni函数,都在这个指针中;参数二jobject 是java对象的this
JNI数据类型
java在C++中的数据类型,分为两类
- 引用类型,在C++中以j开头,本质都是指针
- 基本数据类型,在C++中以j开头,本质上就是C++中的数据类型重定义
基本数据类型

引用类型

引用类型的继承关系

JNI中的描述符
描述符分为以下几类:类描述符 域描述符 方法描述符
- 类描述符是类的完整名称(包名+类名),将原来的分隔符换成斜杠。例如:
java.lang.String--->java/lang/String
int[]--->[I
float[]--->[F
String[]--->[Ljava/lang/String;
将参数类型的域描述符按照声明顺序放入一对括号中后跟返回值类型的域描述符,例如:
String test()---->()Ljava/lang/String;
int f(int i,Object object)---->(ILjava/lang/Object;)I
void set(byte[] bytes)---->([B)V
JNI基本使用
JAVA代码调用C++代码
先来写一个最简单的Crackme,体验NDK编程的完整流程

界面代码如下
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/edt_user"
android:hint="请在此输入用户名"
android:layout_width="match_parent"
android:layout_height="wrap_content"></EditText>
<EditText
android:id="@+id/edt_pass"
android:hint="请在此输入密码"
android:layout_width="match_parent"
android:layout_height="wrap_content"></EditText>
<Button
android:id="@+id/btn1"
android:text="注册"
android:onClick="onClick"
android:layout_width="match_parent"
android:layout_height="wrap_content"></Button>
</LinearLayout>
接着新增一个Native函数
public native boolean stringFromJNI2(String user,String pass);
然后在C++中编写实现代码
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_example_ndkdemo_MainActivity_stringFromJNI2(JNIEnv *env, jobject thiz, jstring user,
jstring pass) {
bool bRet=false;
const char* pUser=env->GetStringUTFChars(user,JNI_FALSE);
const char* pPass=env->GetStringUTFChars(pass,JNI_FALSE);
if (strcmp(pUser,pPass)==0){
bRet= true;
}
//释放字符串
env->ReleaseStringUTFChars(user,pUser);
env->ReleaseStringUTFChars(pass,pPass);
return bRet;
}
然后在按钮点击事件中调用C接口
public void onClick(View view) {
EditText editText=findViewById(R.id.edt_user);
EditText editText1=findViewById(R.id.edt_pass);
String user=editText.getText().toString();
String pass=editText1.getText().toString();
boolean bRet=stringFromJNI2(user,pass);
if (bRet) {
Toast.makeText(this,"恭喜 注册成功",Toast.LENGTH_LONG).show();
}
else {
Toast.makeText(this,"注册失败",Toast.LENGTH_LONG).show();
}
}
这样就完成了一个完整的JAVA代码调用C代码的过程
C++代码调用JAVA代码
上面的Demo显然安全性不够,因为只要反编译JAVA代码直接修改返回值就能直接破解,不需要分析C++代码,所以再进一步进行修改。
public native void stringFromJNI2(String user,String pass);
首先修改方法原型,让函数返回空
public void onClick(View view) {
EditText editText=findViewById(R.id.edt_user);
EditText editText1=findViewById(R.id.edt_pass);
String user=editText.getText().toString();
String pass=editText1.getText().toString();
stringFromJNI2(user,pass);
}
在onClick方法内直接调用stringFromJNI2 函数
public void ShowText()
{
Toast.makeText(this,"恭喜 注册成功",Toast.LENGTH_LONG).show();
}
然后封装一个ShowText 方法
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndkdemo_MainActivity_stringFromJNI2(JNIEnv *env, jobject thiz, jstring user,
jstring pass) {
const char* pUser=env->GetStringUTFChars(user,JNI_FALSE);
const char* pPass=env->GetStringUTFChars(pass,JNI_FALSE);
if (strcmp(pUser,pPass)==0){
//获取类类型
jclass jclass1=env->GetObjectClass(thiz);
//获取方法ID
jmethodID jmethodId=env->GetMethodID(jclass1,"ShowText","()V");
//调用方法
env->CallVoidMethod(thiz,jmethodId);
}
//释放字符串
env->ReleaseStringUTFChars(user,pUser);
env->ReleaseStringUTFChars(pass,pPass);
}
接着在C++代码中调用JAVA成员函数
C++代码修改JAVA字段
接着再对上面的代码进行修改
public String mString="不好意思 出错了";
public void ShowError()
{
Toast.makeText(this,mString,Toast.LENGTH_LONG).show();
}
首先封装ShowError方法,弹出错误提示。接着修改C++的Native方法,如果输入错误则弹出错误提示,如果正确则将提示字符串修改为注册成功。
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndkdemo_MainActivity_stringFromJNI3(JNIEnv *env, jobject thiz, jstring user,
jstring pass) {
const char* pUser=env->GetStringUTFChars(user,JNI_FALSE);
const char* pPass=env->GetStringUTFChars(pass,JNI_FALSE);
if (strcmp(pUser,pPass)==0){
//修改JAVA中的字段
//获取类类型
jclass jclass1=env->GetObjectClass(thiz);
//获取字段ID
jfieldID jfieldId=env->GetFieldID(jclass1,"mString","Ljava/lang/String;");
//修改字段
env->SetObjectField(thiz,jfieldId,env->NewStringUTF("恭喜 注册成功"));
}
//获取类类型
jclass jclass1=env->GetObjectClass(thiz);
//获取方法ID
jmethodID jmethodId=env->GetMethodID(jclass1,"ShowError","()V");
//调用方法
env->CallVoidMethod(thiz,jmethodId);
//释放字符串
env->ReleaseStringUTFChars(user,pUser);
env->ReleaseStringUTFChars(pass,pPass);
}
这样就完成了用C++代码修改JAVA字段。
但是这样就引出了一个问题

我们只要直接用IDA加载so文件,在导出表中搜索类名,就能很容易找到编写的Native方法

然后直接通过F5查看C++源码。这里我们想让分析人员不那么容易找到对应的Native方法,这样就引入了动态注册的概念
动态注册
public native void Check(String user,String pass);
新增一个Native方法,命名为Check
extern "C"
JNIEXPORT void JNICALL
AAA(JNIEnv *env, jobject thiz, jstring user, jstring pass) {
// TODO: implement Check()
}
然后在生成的C++代码中,将名字修改为AAA。接下来我们利用动态注册的方式将Check方法和AAA进行绑定
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
在C++代码中实现一个onload方法,在so文件加载时,在JNI_OnLoad方法中进行注册,将Check方法和AAA进行绑定,实现代码如下:
extern "C"
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env;
//获取JNI环境指针
jint jret = vm->GetEnv((void **)&env,JNI_VERSION_1_6);
if (jret!=JNI_OK){
return JNI_ERR;
}
//获取类类型 package com.example.ndkdemo;
jclass jclass1=env->FindClass("com/example/ndkdemo/MainActivity");
//准备结构体数组
const JNINativeMethod method={
"Check",
"(Ljava/lang/String;Ljava/lang/String;)V",
(void *)AAA
};
//注册
env->RegisterNatives(jclass1,&method,1);
return JNI_VERSION_1_6;
}
注册完整之后就可以在JAVA代码中直接调用Check方法

这种方法的好处在于直接搜索函数名称是无法搜索到的

通过这种方法动态注册的函数只能通过JNI_Onload找到实现代码
|