SDK、NDK 和 JNI、SO、ELF 的关系

NDK是安卓原生开发工具包,作用:把 C/C++ 代码 → 编译成 .so(ELF 文件)
SDK是安卓Java/Kotlin 开发工具包 ,作用:写 APP 界面、逻辑、四大组件
JNIJava ↔ C/C++ 的调用桥梁

概念 作用
NDK 安卓原生开发工具包,作用:把 C/C++ 代码 → 编译成 .so(ELF 文件)
SDK 安卓Java/Kotlin 开发工具包 ,作用:提供 API、编译器、模拟器
JNI Java ↔ C/C++ 的调用桥梁

关系链

1
2
3
4
5
6
7
Java 代码 (SDK 编写)
↓ (需要调用 C 功能)
JNI 桥梁

C/C++ 代码 (NDK 编译)

.so 文件 (ELF 格式)

JNI 基础开发流程

完整流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Kotlin/Java 声明 native 方法 
↓(告诉Java:这个方法我不实现,交给C/C++)
编写 C/C++ 实现(严格遵守 JNI 函数名规则)
↓(C++ 写真正逻辑)
CMakeLists.txt 配置编译
↓(告诉编译器:要编译哪个文件、生成什么so)
build.gradle 关联 NDK + CMake
↓(告诉Android Studio:用NDK工具编译)
NDK 交叉编译 → libxxx.so
↓(C++ → 二进制so库)
运行时 System.loadLibrary 加载
↓(把so装进虚拟机)
调用 native 方法
↓(Java ↔ C 互相通信)

第一步:Kotlin声明 native 方法

1
2
3
4
5
6
7
8
9
10
11
12
class MainActivity : AppCompatActivity() {

// 声明 native 方法,- Kotlin 用 external,Java 用 native
external fun stringFromJNI(): String
external fun add(a: Int, b: Int): Int

override fun onCreate(...) {
System.loadLibrary("mylib") // 加载 libmylib.so
Log.d("JNI", stringFromJNI())
}
}
class MainActivity : AppCompatActivity()
  • 这是安卓的 页面(Activity)
  • 所有页面都继承自 AppCompatActivity

`System.loadLibrary(“mylib”);

  • 加载 C/C++ 编译好的 libmylib.so 动态库文件

Log.d(标签, 内容)

  • 把返回的字符串打印到 Logcat 日志,你就能看到 C++ 传回来的内容

第二步:编写 C/C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <jni.h>
#include <string>

// 命名规范:Java_包名_类名_方法名(点换下划线)
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
return env->NewStringUTF("Hello from C++!");
}

extern "C" JNIEXPORT jint JNICALL
Java_com_example_myapp_MainActivity_add(
JNIEnv* env,
jobject /* this */,
jint a,
jint b) {
return a + b;
}

第一个方法:stringFromJNI

extern "C"

  • 告诉编译器:按 C 语言格式编译
  • 不加的话,函数名会被打乱,Kotlin 找不到方法
    JNIEXPORT
  • JNI 标记:这个函数可以被外部调用
    jstring
  • 返回值类型= Java/Kotlin 里的 String
    JNICALL
  • JNI 调用约定(固定写法,不用管)
    参数
1
JNIEnv* env
  • JNI 环境指针
  • 用来创建字符串、操作数组等
1
jobject
  • 相当于 Kotlin 里的 this
  • 指向调用该方法的 Activity 对象
1
return env->NewStringUTF("Hello from C++!");
  • 创建一个 C++ 字符串
  • 转成 JNI 字符串(jstring)
  • 返回给 Kotlin

第二个方法:add

就是实现了一个a + b

第三步:CMakeLists.txt

编译 C/C++ 代码成 .so 库的配置文件

1
2
3
4
5
6
7
8
9
10
11
cmake_minimum_required(VERSION 3.22.1)//指定 CMake 最低版本
project("mylib")//项目名称,对应前面写的:System.loadLibrary("mylib")
//创建库
add_library(
mylib // 库名 → 生成 libmylib.so
SHARED // 动态库
native-lib.cpp //要编译的 C++ 文件
)

find_library(log-lib log)//找到 Android 日志库
target_link_libraries(mylib ${log-lib})//链接 Android 日志库

第四步:build.gradle 关联

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
android {
// 1. C++ 编译配置
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-std=c++17" // 使用 C++17 标准
}
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt" // CMake 文件路径
version "3.22.1" // CMake 版本
}
}
}

JNI 数据类型对照表

签名符号 C/C++ Java/Ketlin
V void void
Z jboolean boolean
I jint int
J jlong long
D jdouble double
F jfloat float
B jbyte byte
C jchar char
S jshort short
[Z jbooleanArray boolean[]
[I jintArray int[]
[J jlongArray long[]
[D jdoubleArray double[]
[F jfloatArray float[]
[B jbyteArray byte[]
[C jcharArray char[]
[S jshortArray short[]
Ljava/lang/String; jstring String
L 完整包名加类名; jobject class

静态注册与动态注册

JVM查找Native方法有两种方式:
1、按照JNI规范的命名规则进行查找,这种方式叫静态注册。
2、调用JNI提供的RegisterNatives函数,将本地函数注册到JVM中,这种方式叫动态注册

这两种方法就是JNI将Java方法与Native的方法对应联系起来的

静态注册

静态注册指的是在编译时或程序启动时,就已经确定了注册的内容。也就是说,静态注册发生在编译或程序启动时,且通常是在源代码中显式声明的。

C语言的函数和Java层native方法的对应关系,在函数名上就可以体现,逆向很方便,直接在so文件的导出函数里面以Java*包名*类名_方法名就可以找到对应java层中声明的native方法

静态注册流程

1
2
3
4
5
6
7
8
System.loadLibrary("mylib")

JVM 加载 libmylib.so

调用 native 方法时,按命名规则在 SO 中查找符号

找到 → 执行
未找到 → UnsatisfiedLinkError

例题:huhstsec平台的Native层反编译(静态注册)

首先将下载好的APK附件拖入jadx中进行查看,对主要函数进行分析
1

由此我们可以知道summertrain就是所加载的so文件
对APK文件进行解压,我们可以直接在lib里面看到几个文件夹
1
这些文件夹分别代表不同的设备类型
关于Android系统整体架构,请看:Android系统整体架构
我们点进V8a,将唯一的一个so文件拖入IDA中进行分析,按照刚刚所述,那么这个题目我们要找的函数名为:Java_com_swdd_summertrain_MainActivity_Check
找到对应函数即可直接获得flag

动态注册

在 SO 加载时,通过 JNI_OnLoad 函数手动建立 Java 方法与 C 函数的映射关系

动态注册流程

1
2
3
4
5
6
7
8
9
10
11
System.loadLibrary("mylib")

JVM 加载 libmylib.so

自动调用 JNI_OnLoad()

RegisterNatives() 把映射表注册到虚拟机

调用 native 方法时,直接通过映射表找到函数

执行(无需再查找符号)

动态注册的优点是可以自由命名 Native 方法,缺点是如果 Native 方法过多,操作比较麻烦。

例题:huhstsec平台的动态注册

1很明显让我们到so文件中去找
丢到IDA中反编译没有查到Java+包名+类名+Native方法名类型的函数
查找JNI_OnLoad函数
1
点开off_37A90函数
1
点开sub_172E0函数去看
1
这里_mm_xor_si128 是 Intel 提供的 SIMD指令集
在函数sub_172E0 中,使用 mm_xor_si128 函数时传入了 -1LL 作为其中一个参数。在计算机中,整数 -1 在二进制补码表示下是全 1。所以这里异或就相当于取反操作

整个函数讲的就是:获取一个字符串,对其进行处理(按字节取反),与特定内存区域比较,创建一个 Java 的 Boolean 对象,最后清理资源并返回该对象的引用。不过,memcmp 的结果未被使用。

找到密文后进行取反操作就可以得到明文

二者之间的差异

1