Android 的 SDK 主要是基于 Java 的,同时,开发者可以通过 JNI 调用自己的 C/C++ 动态库,于是 NDK 应运而生。本文主要介绍 NDK 和 JNI 的一些概念以及 JNI 接口的使用和 hook 方法。
Google 对 NDK 的介绍如下
Android NDK 是一个工具集,可让您使用 C 和 C++ 等语言以原生代码实现应用的各个部分。对于特定类型的应用,这可以帮助您重复使用以这些语言编写的代码库。
原生开发套件 (NDK) 是一套工具,使您能够在 Android 应用中使用 C 和 C++ 代码,并提供众多平台库,您可使用这些平台库管理原生 Activity 和访问实体设备组件,例如传感器和触摸输入。NDK 可能不适合大多数 Android 编程初学者,这些初学者只需使用 Java 代码和框架 API 开发应用。然而,如果您需要实现以下一个或多个目标,那么 NDK 就能派上用场
- 进一步提升设备性能,以降低延迟或运行游戏或物理模拟等计算密集型应用。
- 重复使用自己或其他开发者的 C 或 C++ 库。
- 对代码的保护。APK 的 Java 层代码很容易被反编译,而 C/C++ 库反编译难度大。
不同的 Android 设备使用不同的 CPU,而不同的 CPU 支持不同的指令集。CPU 与指令集的每种组合都有专属的应用二进制接口 (ABI)。NDK 为我们提供了各种 ABI,确保针对正确的架构和 CPU 进行编译。
ABI |
支持的指令集 |
备注 |
armeabi-v7a |
armeabi Thumb-2 VFPv3-D16 |
与 ARMv5/v6 设备不兼容。 |
arm64-v8a |
AArch64 |
|
x86 |
x86 (IA-32) MMX SSE/2/3 SSSE3 |
不支持 MOVBE 或 SSE4。 |
x86_64 |
x86-64 MMX SSE/2/3 SSSE3 SSE4.1、4.2 POPCNT |
|
注意:NDK 以前支持 ARMv5 (armeabi) 以及 32 位和 64 位 MIPS,但 NDK r17 已不再支持。
我们可以新建一个 Android 项目,使用以下代码即可打印出当前设备支持的 ABI 信息。
1
2
3
4
5
|
TextView tv = binding.sampleText;
tv.setText("Supported ABI:\n");
for (int i = 0; i < Build.SUPPORTED_ABIS.length; i++) {
tv.append(Build.SUPPORTED_ABIS[i] + "\n");
}
|
默认情况下,Gradle(无论是通过 Android Studio 使用,还是从命令行使用)会针对所有非弃用 ABI 进行构建。要限制应用支持的 ABI 集,请使用 abiFilters
。例如,要仅针对 64 位 ABI 进行构建,请在 build.gradle
中设置以下配置:
1
2
3
4
5
6
7
|
android {
defaultConfig {
ndk {
abiFilters 'arm64-v8a', 'x86_64'
}
}
}
|
JNI 是 Java 调用 C++ 的规范,并非 Android 自创。一般的 Java 程序使用的 JNI 标准与 Android 存在差异,Android 的 JNI 更简单。
JNI,全称为 Java Native Interface,JNI 是 Java 调用 Native 语言的一种特性。通过 JNI 可以使得 Java 与 C/C++ 进行交互,即可以在 Java 代码中调用 C/C++ 等语言的代码,反之亦可。由于 JNI 是 JVM 规范的一部分,因此我们写的 JNI 程序可以在任何实现了 JNI 规范的 Java 虚拟机中运行。同时,这个特性使我们可以复用以前用 C/C++ 写的大量代码。
1
2
3
4
5
6
7
|
extern "C" JNIEXPORT jstring JNICALL
Java_dev_svip_easymd5_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
|
- jstring 是返回值类型
- Java_dev_svip_easymd5 是包名
- MainActivity 是类名
- stringFromJNI 是方法名
上述代码在 Java 中的声明如下
1
|
public native String stringFromJNI();
|
我们可以看到,在 JNI 方法中返回值是 jstring,而在 Java 代码中返回值为 String,二者之间具有对应关系,其他类型的对应关系如下
◎ JNI 与 Java 类型对照
此处我们使用的项目为 Android Studio 自动生成,新建 Native C++ 项目即可。
使用 objection 查看模块名
1
2
|
objection -g dev.svip.easymd5 explore
memory list modules
|
搜索包名可以得到我们需要的模块名
使用 jnitrace 进行追踪,代码中方法定义如下,可以看出,成功追踪到方法调用。
1
2
3
4
5
6
7
|
extern "C" JNIEXPORT jstring JNICALL
Java_dev_svip_easymd5_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
|
◎ jnitrace结果
代码主要使用 https://github.com/itechbear/AndroidNativeMD5/blob/master/jni/md5.c
将 md5.c 的内容移植到本地项目后,编译运行即可,本文所用项目请点击此处。
注意:前面有提到,Gradle 会针对所有非弃用 ABI 进行构建,但在 Android Studio 中直接点击运行时,只会构建当前设备需要的 ABI,我的手机默认构建的 ABI 为 arm64-v8a
,而此处我们所用代码对架构有要求,导致 md5 计算错误,此处我们只需指定要构建的 ABI 即可使结果正确。
1
2
3
4
5
6
7
|
android {
defaultConfig {
ndk {
abiFilters 'armeabi-v7a'
}
}
}
|
另一种修复方式:
修改源码中定义的 UINT4 类型即可(第 46 行)
1
2
3
|
/* typedef a 32 bit type */
//typedef unsigned long int UINT4;
typedef uint32_t UINT4;
|
本项目中的 md5 实现有 C/C++ 两套版本(仅有轻微改动,分别对应 native-lib.c 和 native-lib.cpp),在 CMakeLists.txt 中修改引入的文件名即可指定所用版本。使用 objection 查看导出函数,可发现以下区别。如图所示,后者我们可以通过 c++filt 命令查看函数的具体声明。
◎ C版本导出函数
◎ C++版本导出函数
使用刚才创建的 easymd5 项目。Frida 脚本使用 TypeScript,与 JavaScript 语法仅有很少的差异,这也是 Frida 官方推荐的方式。
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
|
function hook_dlopen() {
//Android低版本使用dlopen
//Android高版本使用android_dlopen_ext
//我使用的是 Android 11,所以代码只写了android_dlopen_ext部分,如果使用较低版本,可将代码复制到dlopen方法中
var dlopen = Module.findExportByName(null, "dlopen");
if (dlopen) {
Interceptor.attach(dlopen, {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = pathptr.readCString();
console.log("dlopen =>", path);
}
}, onLeave: function (_retval) {
}
});
}
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
if (android_dlopen_ext) {
Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
var pathptr = args[0];
this.target = false;
if (pathptr !== undefined && pathptr != null) {
var path = pathptr.readCString();
// console.log("android_dlopen_ext =>", path);
if (path) {
if (path.indexOf("libeasymd5.so") >= 0) {
this.target = true;
console.log("find android_dlopen_ext =>", path);
}
}
}
}, onLeave: function (_retval) {
if (this.target) {
var mdString_addr = Module.findExportByName("libeasymd5.so", "Java_dev_svip_easymd5_MainActivity_mdString");
// console.log("mdString address =>", mdString_addr);
// if (mdString_addr) {
// Interceptor.attach(mdString_addr, {
// onEnter: function (args) {
// console.log("jstring =>", Java.vm.tryGetEnv().getStringUtfChars(args[2]).readCString());
// }, onLeave: function (retval) {
// console.log("retval is =>", Java.vm.tryGetEnv().getStringUtfChars(retval).readCString());
// }
// });
// }
if (mdString_addr) {
var mdString = new NativeFunction(mdString_addr, "pointer", ["pointer", "pointer", "pointer"]);
var mymdString = new NativeCallback(function (env, jobject, jstring) {
//打印原始参数
console.log("jstring =>", Java.vm.tryGetEnv().getStringUtfChars(jstring).readCString());
var newJstring = Java.vm.tryGetEnv().newStringUtf("111");
//修改参数
var retval = mdString(env, jobject, newJstring);
//打印原来的返回值
console.log("retval =>", Java.vm.tryGetEnv().getStringUtfChars(retval).readCString());
//直接返回自定义的值
return newJstring;
}, "pointer", ["pointer", "pointer", "pointer"]);
Interceptor.replace(mdString, mymdString);
}
}
}
});
}
}
|
将 C/C++ 代码中的函数 Java_dev_svip_easymd5_MainActivity_mdString 注释掉,然后添加以下代码
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
|
#define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
//原Java_dev_svip_easymd5_MainActivity_mdString的实现
jstring ffff(JNIEnv *env, jobject obj, jstring str) {
MD5_CTX mdContext;
char *inString = const_cast<char *>(env -> GetStringUTFChars(str, NULL));
unsigned int len = strlen(inString);
MD5Init(&mdContext);
MD5Update(&mdContext, reinterpret_cast<unsigned char *>(inString), len);
MD5Final(&mdContext);
int i;
char dest[32] = {0};
for (i = 0; i < 16; i++) {
sprintf(dest + i * 2, "%02x", mdContext . digest[i]);
}
return env -> NewStringUTF(dest);
}
static JNINativeMethod method_table[] = {
//这里的第二个值可以通过jadx查看原app获得,详情见下面的说明
{"mdString", "(Ljava/lang/String;)Ljava/lang/String;", (void *) ffff},
};
static int
registerMethods(JNIEnv *env, const char *className, JNINativeMethod *gMethods, int numMethods) {
jclass clazz = env -> FindClass(className);
if (clazz == nullptr) {
return JNI_FALSE;
}
if (env -> RegisterNatives(clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
if (vm -> GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
//注册native方法
if (!registerMethods(env, "dev/svip/easymd5/MainActivity", method_table, NELEM(method_table))) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
|
变量 method_table[] 中的第二个值可以使用 jadx 查看之前静态注册生成的 apk,在 Smali 中,搜索方法名,后面的内容即为需要的内容。
查看导出函数,可以发现已无法找到函数 mdString
◎ 静态注册后的导出函数
1、https://developer.android.com/ndk/guides
2、https://www.jianshu.com/p/87ce6f565d37
3、https://github.com/itechbear/AndroidNativeMD5
4、https://github.com/svipblog/easymd5