Android逆向-NDK开发初试

何为NDK

​ 安卓开发分SDK和NDK,SDK运行在ART/Dalvik虚拟机中,NDK运行在原生系统中,虚拟机执行效率不如原生效率高,要谈两者的区别,还是直接放一个SDK调用NDK的图,通过实际的调用流程来了解一下吧。

SDK调用NDK Zygote 是安卓系统启动时就会创建这么一个进程,它是所有安卓应用的父进程。Zygote在启动时,会加载系统核心框架库(如framework.jar,包含Activity、Service等核心类的实现),每启动一个应用,Zygote就会通过Fork自身来生成新进程,前面那些预加载的资源就会被所有子进程共享,避免重复加载。

​ 所以Zygote是创建ART/Dalvik虚拟机的进程,它会完成虚拟机的初始化,确保fork出来的应用进程都继承一个准备就绪的、标准化的虚拟机环境。应用启动后,就会在这个虚拟机中加载APK中的Dex码,进行初始化Application,然后就是四大组件的初始化。

​ 那图中的JNI又是个啥呢,在安卓中,JNI 是指 Java 原生接口,是一个规范。它定义了 Android 从受管理代码(使用 Java 或 Kotlin 编程语言编写)编译的字节码与原生代码(使用 C/C++ 编写)互动的方式。SDK调用NDK,是通过JNI与NDK进行调用映射和数据类型转换等操作,完成JAVA层到NATIVE层的通信。

​ 话说回来,NDK就是为了高运行效率、跨平台复用、底层逻辑操作,按照JNI规范在原生空间中运行编译C\C++代码的Android原生开发工具集。

NDK开发初试

​ 扔一点命令

shell
1
2
3
4
5
6
7
8
//查看当前界面Activity
adb shell dumpsys window | grep mCurrentFocus
//查看应用完整包名
adb shell pm list package
//列出所有应用包,并对应完整路径
adb shell pm list package -f
//显示系统详细信息
adb shell uname -a

​ 通过Android Studio创建一个Native开发项目,在java层中写进行声明

1
2
3
4
5
public static native String mdString(String string);
//如果使用static进行声明,那么native层中的this对象要使用jclass类型,反之使用jobject类型
//并且,访问其对象也应使用对应的方法
//非 static 方法(jobject):可通过env->GetObjectField/CallObjectMethod访问实例成员;
//static 方法(jclass):需通过env->GetStaticObjectField/CallStaticObjectMethod访问静态成员。

​ 对应的,native层的定义(C版)

1
2
3
4
5
6
7
8
JNIEXPORT jstring JNICALL
Java_com_example_myndk_MainActivity_stringFromJNI(
JNIEnv* env, // JNI环境指针,提供JNI操作函数
jobject thiz) { // 对应Java中的this对象
const char* hello = "Hello from C Native Code!";
// 把C字符串转为Java的String对象
return (*env)->NewStringUTF(env, hello);
}

​ 其中,如果是已CPP进行native层的编写,就需要在最开始进行一个name mangling,因为CPP可能会进行重载,抽空去研究一下编译区别(咕咕咕)。

1
2
3
4
5
6
7
8
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myndk_MainActivity_stringFromJNI(
JNIEnv* env,
jobject thiz) {
std::string hello = "Hello from C++ Native Code!";
// C++中env直接调用方法,无需(*env)->
return env->NewStringUTF(hello.c_str());
}

1. JNI 基本类型对照表 (Java Primitive Types)

Java 基本类型 JNI 对应类型 C/C++ 等效类型 占用字节 说明
boolean jboolean unsigned char 1 0=false,非0=true
byte jbyte signed char 1 8位有符号整数
char jchar unsigned short 2 UTF-16编码的字符
short jshort short 2 16位有符号整数
int jint int 4 32位有符号整数
long jlong long long / int64_t 8 64位有符号整数
float jfloat float 4 32位浮点数
double jdouble double 8 64位浮点数

2. JNI 引用类型对照表 (Java Reference Types)

Java 引用类型 JNI 对应类型 说明/使用场景
Object jobject 所有引用类型的父类
Class jclass 对应 Java 的 Class 对象 (用于静态方法/反射)
String jstring 对应 Java 的 String
Throwable jthrowable 对应 Java 的异常类
Object[] jobjectArray 对象数组 (如 String[], User[])
boolean[] jbooleanArray 布尔数组
byte[] jbyteArray 字节数组 (常用作二进制数据传输)
char[] jcharArray 字符数组
short[] jshortArray 短整型数组
int[] jintArray 整型数组
long[] jlongArray 长整型数组
float[] jfloatArray 浮点数组
double[] jdoubleArray 双精度浮点数组
自定义类/接口 jobject 统统映射为 jobject

3. JNI 环境与 ID 类型 (JNI Special Types)

用途 JNI 类型 说明
JNI 环境指针 JNIEnv* 核心接口指针 (C使用 (*env)->, C++使用 env->)
虚拟机指针 JavaVM* 跨线程获取 JNIEnv 时使用
字段 ID jfieldID 访问 Java 属性的凭据
方法 ID jmethodID 调用 Java 方法的凭据

​ 直接抄仨对照表扔这,用到的时候再看。

​ 写完对应的代码,再扔一个命令行编译的命令

1
2
3
./gradlew assembleDebug

#编译好后,通过 find . -name "*.apk" 找到对应的debug文件

JNI_HOOK

这里使用frida进行native层的被动调用,需要找到要hook的libso名称,还有要hook的函数名称,以及入参和出参类型,使用以下命令进行查找。

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
Using USB device `KB2000`
Agent injected and responds ok!

_ _ _ _
___| |_|_|___ ___| |_|_|___ ___
| . | . | | -_| _| _| | . | |
|___|___| |___|___|_| |_|___|_|_|
|___|(object)inject(ion) v1.11.0

Runtime Mobile Exploration
by: @leonjza from @sensepost

[tab] for command suggestions
com.example.ezmd5 on (OnePlus: 11) [usb] # memory list modules
libezmd5.so 0x718b26b000 45056 (44.0 KiB) /data/app/~~fmgadrx_ut4G29FifjXEmw==/com.example.ezmd5-vLBLnfw0npqVhTLBZNLi...
//查找加载的libso名称

com.example.ezmd5 on (OnePlus: 11) [usb] # memory list exports libezmd5.so
Save the output by adding `--json exports.json` to this command
Type Name Address
-------- -------------------------------------------- ------------
function Java_com_example_ezmd5_MainActivity_mdString 0x71da5e9a10
function _ZN7_JNIEnv17GetStringUTFCharsEP8_jstringPh 0x71da5e9b30
function _Z7MD5InitP7MD5_CTX 0x71da5e9b6c
function _Z9MD5UpdateP7MD5_CTXPhj 0x71da5e9bcc
function _Z8MD5FinalP7MD5_CTX 0x71da5e9d8c
function _ZN7_JNIEnv12NewStringUTFEPKc 0x71da5ea0bc

//查看libso文件的导出表,找到导出的函数名

//使用c++filt工具,查看函数传参类型
➜ frida-scripts # c++filt -n _ZN7_JNIEnv17GetStringUTFCharsEP8_jstringPh
_JNIEnv::GetStringUTFChars(_jstring*, unsigned char*)

找到我们想hook的JNI,通过ida或者c++filt工具查看传参

ida

​ 由于该jni是在启动时后就进行直接调用的,这里改用了lasting_yang师傅写的 frida_dump去hookdlopen,非常给力的fridahook项目,抽空研究研究(咕咕咕~)

https://github.com/lasting-yang/frida_dump

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
var can_hook_libart = false;

function hook_dlopen() {
Interceptor.attach(Module.findExportByName(null, "dlopen"), {
onEnter: function(args) {
var pathptr = args[0];
this.targeet = false;
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
//console.log("dlopen:", path);
if (path.indexOf("libezmd5.so") >= 0) {
this.target = true;
console.log("[dlopen:]", path);
}
}
},
onLeave: function(retval) {
if(this.target){
console.log("find Exports => libezmd5.so", JSON.stringify(Module.enumerateSymbols()));
}

// if (this.can_hook_libart && !is_hook_libart) {
// dump_dex();
// is_hook_libart = true;
// }
}
})

Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function(args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
console.log("android_dlopen_ext:", path);
if(path.indexOf("libezmd5.so") >= 0){
this.target = true;
}



// if (path.indexOf("libart.so") >= 0) {
// this.can_hook_libart = true;
// console.log("[android_dlopen_ext:]", path);
// }
}
},
onLeave: function(retval) {

if(this.target){
// console.log("find Exports => libezmd5.so", JSON.stringify(Module.enumerateSymbols()));

console.log("find Exports => ", JSON.stringify(Module.enumerateExports("libezmd5.so")));
const matched_export = Module.enumerateExports("libezmd5.so").filter(function(exported) {
return exported.name.indexOf("Java_com_example_ezmd5_MainActivity_mdString") >= 0;
});
const matched_addr = matched_export[0]["address"];
console.log("find Export addr => ", matched_addr);
var md5String = new NativeFunction(matched_addr, 'pointer', ['pointer', 'pointer', 'pointer']);
Interceptor.replace(matched_addr, new NativeCallback((env, jclass, jstring)=>{
console.log("111>")
console.log("jstring =>", Java.vm.getEnv().getStringUtfChars(jstring, null).readCString())
var string1 = "ghbbhc";
var jstring1 = Java.vm.getEnv().newStringUtf(string1);
var retval = md5String(env, jclass, jstring1);
console.log("new md5 => ", Java.vm.getEnv().getStringUtfChars(retval,null).readCString());
return retval;

}, 'pointer', ['pointer', 'pointer', 'pointer']))

}
// if (this.can_hook_libart && !is_hook_libart) {
// dump_dex();
// is_hook_libart = true;
// }
}
});
}


setImmediate(hook_dlopen);

fridahook

JNI主动调用

1
2
3
4
5
6
7
8
9
10
11
setTimeout(()=>{
Java.perform(()=>{
var native = Module.findExportByName("libezmd5.so", "Java_com_example_ezmd5_MainActivity_mdString");
console.log("find native address =>", native);
var md5String = new NativeFunction(native, 'pointer', ['pointer', 'pointer', 'pointer']);
console.log("============= 主动调用 =============");
console.log("String =>", "123456");
var result = md5String(Java.vm.tryGetEnv(), Java.vm.tryGetEnv(), Java.vm.getEnv().newStringUtf("123456"));
console.log("MD5 =>", Java.vm.getEnv().getStringUtfChars(result, null).readCString());
})
}, 1000)

ab26fd33d8d12a11db2544bce6e5a076