从 Android N 开始,对 NDK 调用私有 API 的行为做了限制。在 Android 7.0 行为变更中明确提到:
从 Android 7.0 开始,系统将阻止应用动态链接非公开 NDK 库,这种库可能会导致您的应用崩溃。此行为变更旨在为跨平台更新和不同设备提供统一的应用体验。即使您的代码可能不会链接私有库,但您的应用中的第三方静态库可能会这么做。因此,所有开发者都应进行相应检查,确保他们的应用不会在运行 Android 7.0 的设备上崩溃。如果您的应用使用原生代码,则只能使用公开 NDK API。
通常使用私有 API 的做法是,使用 dlopen 加载 API 所在的动态库,然后通过 dlsym 获取函数地址并调用之。例如如下代码通过不被 NDK 推荐的方式,解析私有函数 JNI_GetCreatedJavaVMs 获取 jvm 实例:
int result;
jsize nVMs = 0;
JavaVM *buffer[1];
void *handle = dlopen("libart.so", RTLD_NOW);
if (!handle) {
ALOGE(dlerror());
} else {
GetCreatedJavaVMs JNI_GetCreatedJavaVMs = (GetCreatedJavaVMs)dlsym(handle, "JNI_GetCreatedJavaVMs");
if (JNI_GetCreatedJavaVMs) {
result = JNI_GetCreatedJavaVMs(buffer, 1, &nVMs);
// ..
}
}
Android N 对此的处理方式是,使用 classloader-namespace 限制了 dlopen 参数允许加载的路径。对于 NDK 公开可用的链接库 (如 libandroid, libc, libcamera2ndk, libdl, libGLES, libjnigraphics, liblog, libm, libmediandk, libOpenMAXAL, libOpenSLES, libstdc++, libvulkan, libz 等),以及 apk 自带的 libs 下的共享对象,可以自由地加载。但尝试 dlopen 其他路径的文件则会返回 null 并在 logcat 中打印类似这样的错误:
03–10 12:06:22.981 14193–14193/com.example.app W/linker: library “libutils.so” (“/system/lib/libutils.so”) needed or dlopened by “/data/app/com.example.app-1/lib/arm/libsqlcipher_android.so” is not accessible for the namespace “classloader-namespace” — the access is temporarily granted as a workaround for http://b/26394120
为了兼容性,在应用的 target API level 小于 24 时,linker 输出一个警告并弹出 toast。
03-21 17:07:51.502 31234 31234 W linker : library "libart.so"
("/system/lib64/libart.so") needed or dlopened by
"/data/app/com.example.app-1/lib/arm64/libjvmfun.so" is not accessible
for the namespace "classloader-namespace" - the access is temporarily granted
as a workaround for http://b/26394120
如果 target API level 为 Android N 甚至更高版本,那么 dlopen 将返回 null,并可能抛出 java.lang.UnsatisfiedLinkError:
Android 的链接器源码在 platform/bionic/linker/linker.cpp 实现。执行 C 标准库的 dlopen 函数时,在内部函数 load_library 中加入了对链接库合法性的判断:
https://android.googlesource.com/platform/bionic/+/master/linker/linker.cpp#1210
小于 API Level 24 时允许加载的“灰名单”如下:
static const char* const kLibraryGreyList[] = {
"libandroid_runtime.so",
"libbinder.so",
"libcrypto.so",
"libcutils.so",
"libexpat.so",
"libgui.so",
"libmedia.so",
"libnativehelper.so",
"libskia.so",
"libssl.so",
"libstagefright.so",
"libsqlite.so",
"libui.so",
"libutils.so",
"libvorbisidec.so",
nullptr
};
上面废话铺垫了这么多,其实绕过的办法巨弱智……只要不传入 NULL 作为 dlopen 的文件名就行了。
typedef jint (JNICALL *GetCreatedJavaVMs)(JavaVM **, jsize, jsize *);
void *handle = dlopen(NULL, RTLD_NOW);
JNI_GetCreatedJavaVMs = (GetCreatedJavaVMs) dlsym(handle, "JNI_GetCreatedJavaVMs");
if (JNI_GetCreatedJavaVMs) {
jsize nVMs = 0;
JavaVM *buffer[1];
int result = JNI_GetCreatedJavaVMs(buffer, 1, &nVMs);
if (result != JNI_OK) {
stream << "failed to call JNI_GetCreatedJavaVMs" << endl;
}
stream << "jvms: " << nVMs << endl;
stream << "jvm: " << buffer[0] << endl;
}
局限性是只能加载系统库中的共享对象,而不能指定使用某一个具体的库文件,或者从任意路径加载 shared object
[课程]FART 脱壳王!加量不加价!FART作者讲授!