Android逆向7-Native逆向

来自吾爱破解-正己

https://www.52pojie.cn/thread-1701353-1-1.html

NDK(Native Development Kit)是一套用于开发Android应用程序的工具集,它允许在C/C++中编写性能关键的部分代码,并将这些代码与Java代码进行连接。

下载NDK和CMake

下面是cmakelist.txt和native-lib.cpp文件的作用以及简要说明:

文件名 作用 说明
CMakeLists.txt 构建配置文件 CMakeLists.txt是用于配置NDK项目的构建系统的文件。它指定了构建所需的源文件、依赖项、编译选项等。在构建过程中,CMake会根据该文件的指示生成对应的构建脚本,用于编译本地代码并生成本地库。
native-lib.cpp 本地代码实现文件 native-lib.cpp是包含本地代码实现的文件。它定义了通过Java和本地代码之间进行通信的本地方法。该文件中的函数实现将被编译为本地库,供Java代码调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MainActivity extends AppCompatActivity {

// Used to load the 'ndkdemo' library on application startup.
static {
System.loadLibrary("ndkdemo"); // 加载名为"ndkdemo"的库
}

private ActivityMainBinding binding; // 声明一个ActivityMainBinding变量

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

binding = ActivityMainBinding.inflate(getLayoutInflater()); // 使用ViewBinding将布局文件解析为一个ActivityMainBinding对象
setContentView(binding.getRoot()); // 将Activity的布局设置为根布局

// Example of a call to a native method
TextView tv = binding.sampleText; // 获取布局文件中的TextView控件
tv.setText(stringFromJNI()); // 调用本地方法stringFromJNI()并将其返回的字符串设置为TextView的文本内容
}

/**
* A native method that is implemented by the 'ndkdemo' native library,
* which is packaged with this application.
*/
public native String stringFromJNI(); // 声明一个native方法stringFromJNI()
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# 设置构建本地库所需的CMake的最小版本要求
cmake_minimum_required(VERSION 3.22.1)

# 声明和命名项目
project("ndkdemo")

# 创建并命名一个库,设置其类型为STATIC或SHARED,并指定源代码的相对路径
# 可以定义多个库,CMake会为您构建它们
# Gradle会自动将共享库打包到APK中
add_library(
# 设置库的名称
ndkdemo

# 设置库类型为共享库
SHARED

# 提供源文件的相对路径
native-lib.cpp)

# 搜索指定的预构建库并将路径存储为变量。
# 由于CMake默认在搜索路径中包含系统库,因此您只需指定要添加的公共NDK库的名称。
# CMake会在完成构建之前验证该库是否存在。
find_library(
# 设置路径变量的名称
log-lib

# 指定要让CMake定位的NDK库的名称
log)

# 指定CMake应链接到目标库的库。
# 您可以链接多个库,例如在此构建脚本中定义的库、预构建的第三方库或系统库。
target_link_libraries(
# 指定目标库
ndkdemo

# 将目标库链接到NDK中包含的log
${log-lib})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <jni.h> // JNI头文件,提供了JNI函数和数据类型的定义
#include <string> // C++标准库的string类

// 声明一个jni函数,该函数将会被Java代码调用
// JNIEXPORT表示这个函数是可导出的,并且可以被其他代码使用
// jstring表示这个函数返回的是一个Java字符串对象
// JNICALL是JNI函数的调用约定
// Java_com_example_ndkdemo_MainActivity_stringFromJNI是JNI函数的命名规则,与Java中对应的方法名对应
// Java打头,1包名,2类名,3方法名字;"_"号隔开
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndkdemo_MainActivity_stringFromJNI(
JNIEnv* env, // JNIEnv是指向JNI环境的指针,可以用来访问JNI提供的功能
jobject /* this */) { // jobject是指向Java对象的指针,在本例中并没有使用

std::string hello = "Hello from C++"; // 创建一个C++字符串对象
return env->NewStringUTF(hello.c_str()); // 将C++字符串对象转换为Java字符串对象并返回
}

NDK是开发套件,JNI才是调用的框架。所以与其说是NDK开发,不如说是JNI的开发。不过NDK是Android提供的开发套件。JNI可不是,JNI全称Java Native Interface,即Java本地接口,JNI是Java调用Native 语言的一种特性。通过JNI可以使得Java与C/C++机型交互。即可以在Java代码中调用C/C++等语言的代码或者在C/C++代码中调用Java代码。

JNI的两种注册方式

jni静态注册方式
  • 优点: 理解和使用方式简单, 属于傻瓜式操作, 使用相关工具按流程操作就行, 出错率低
  • 缺点: 当需要更改类名,包名或者方法时, 需要按照之前方法重新生成头文件, 灵活性不高

jni动态注册方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <jni.h>
#include <string>

extern "C" {

JNIEXPORT jstring JNICALL Java_com_example_ndkdemo_MainActivity_nativeGetStringFromJNI(JNIEnv* env, jobject obj) {
std::string hello = "Hello wuaipojie";
return env->NewStringUTF(hello.c_str());
}

// 定义本地方法注册函数
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return -1;
}

// 定义要注册的本地方法
JNINativeMethod methods[] = {
{"nativeGetStringFromJNI", "()Ljava/lang/String;", reinterpret_cast<void*>(Java_com_example_ndkdemo_MainActivity_nativeGetStringFromJNI)}
};

// 获取类引用
jclass clazz = env->FindClass("com/example/ndkdemo/MainActivity");
if (clazz == nullptr) {
return -1;
}

// 注册本地方法
if (env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
return -1;
}

return JNI_VERSION_1_6;
}

} // extern "C"

下面是一些常见的C++数据类型和它们在Java中的对应关系,以及它们在JNI动态注册中的数据类型签名(signature):

C++ 数据类型 Java 数据类型 JNI 数据类型签名
jint int “I”
jboolean boolean “Z”
jbyte byte “B”
jchar char “C”
jshort short “S”
jlong long “J”
jfloat float “F”
jdouble double “D”
jobject Object “Ljava/lang/Object;”
jstring String “Ljava/lang/String;”
jarray Array “[elementType”
jobjectArray Object[] “[Ljava/lang/Object;”
jbooleanArray boolean[] “[Z”
jbyteArray byte[] “[B”
jcharArray char[] “[C”
jshortArray short[] “[S”
jintArray int[] “[I”
jlongArray long[] “[J”
jfloatArray float[] “[F”
jdoubleArray double[] “[D”

在JNI动态注册中,需要使用正确的数据类型签名来声明本地方法。例如,如果你要注册一个返回int类型的本地方法,其数据类型签名应为I

image-20250402160343383

ARM基础知识

常见寻址方式

寻址方式 描述
立即数寻址 直接使用立即数值作为操作数,例如:MOV R0, #5
寄存器直接寻址 使用寄存器中的值作为操作数,例如:MOV R0, R1
寄存器间接寻址 使用寄存器中的值作为内存地址,访问该地址中的数据,例如:LDR R0, [R1]
寄存器相对寻址 使用寄存器中的值加上一个立即偏移量作为内存地址,例如:LDR R0, [R1, #4]
寄存器变址寻址 使用两个寄存器中的值相加作为内存地址,例如:LDR R0, [R1, R2]
带有变址寄存器的寄存器相对寻址 使用寄存器中的值加上另一个寄存器的值乘以一个比例因子作为内存地址,例如:LDR R0, [R1, R2, LSL #2]
堆栈寻址 使用堆栈指针寄存器(如SP)进行操作,例如:PUSH {R0, R1}POP {R0, R1}

压栈和出栈指令

指令类型 指令示例 描述
压栈 PUSH {R0, R1} 将寄存器R0和R1的内容压入堆栈中
压栈 PUSH {R0-R5} 将寄存器R0到R5的内容压入堆栈中
压栈 STMDB SP!, {R0-R5} 将寄存器R0到R5的内容压入堆栈中(与PUSH等效)
出栈 POP {R0, R1} 从堆栈中弹出数据,恢复到寄存器R0和R1中
出栈 POP {R0-R5} 从堆栈中弹出数据,恢复到寄存器R0到R5中

跳转指令

指令类型 指令示例 描述
无条件跳转 B label 无条件跳转到标签label指向的位置
子程序调用 BL label 调用子程序,将当前指令的下一条指令地址存入链接寄存器(LR),然后跳转到标签label指向的位置
子程序返回 BX LR 返回子程序调用前的位置,跳转到链接寄存器(LR)中存储的地址
寄存器跳转 BX Rn 跳转到寄存器Rn中存储的地址

算术运算指令

汇编中也可以进行算术运算, 比如加减乘除,常用的运算指令用法如表 所示:

指令 计算公式 备注
ADD Rd, Rn, Rm Rd = Rn + Rm 加法运算,指令为 ADD
ADD Rd, Rn, #immed Rd = Rn + #immed 加法运算,指令为 ADD
ADC Rd, Rn, Rm Rd = Rn + Rm + 进位 带进位的加法运算,指令为 ADC
ADC Rd, Rn, #immed Rd = Rn + #immed + 进位 带进位的加法运算,指令为 ADC
SUB Rd, Rn, Rm Rd = Rn - Rm 减法
SUB Rd, #immed Rd = Rd - #immed 减法
SUB Rd, Rn, #immed Rd = Rn - #immed 减法
SBC Rd, Rn, #immed Rd = Rn - #immed - 借位 带借位的减法
SBC Rd, Rn ,Rm Rd = Rn - Rm - 借位 带借位的减法
MUL Rd, Rn, Rm Rd = Rn * Rm 乘法 (32 位)
UDIV Rd, Rn, Rm Rd = Rn / Rm 无符号除法
SDIV Rd, Rn, Rm Rd = Rn / Rm 有符号除法

image-20250402163000485