原文: https://flowus.cn/share/9263eee2-f20a-436c-ae7c-75c86e2424b6
【FlowUs 息流】如何在android N之后的native使用系统私有库
前言
系统私有库指的是,存放在android系统/system/lib/和/vendor/lib下面,但是Android NDK中没有公开API的lib库。
从Android N开始(SDK >= 24),通过dlopen
打开系统私有库,或者lib库中依赖系统私有库,都会产生异常,甚至可能导致app崩溃。具体可以阅读官方文档说明。
具体请看这篇文章: Android Native禁止使用系统私有库详解
今天我们就针对这篇文章的思路来实操一把.
环境准备:
小米10手机(64位, android12)
windows11 64位, ndk 21 sdk: 34
创建一个native样式的android项目
按模板来就可以了
1_创建工程1.png 1_创建工程2.png
按照上面文章中的思路,绕过dlopen的限制.
逐个判断下面几个目录下是否有要加载的目标so文件
static const char *const kSystemLibDir = "/system/lib/";
static const char *const kOdmLibDir = "/odm/lib/";
static const char *const kVendorLibDir = "/vendor/lib/";
static const char *const kApexLibDir = "/apex/com.android.runtime/lib/";
static const char *const kApexArtNsLibDir = "/apex/com.android.art/lib/";
比如libart.so, 在/apex/com.android.art/lib/目录下.
读取/proc/xxx/maps文件, 从中找到目标so文件加载的地址
3_maps_loc.png对应到代码中, 就是逐行读取, 判断是否包含:
maps = fopen("/proc/self/maps", "r");
if (!maps) fatal("failed to open maps");
while (!found && fgets(buff, sizeof(buff), maps)) {
if (strstr(buff, libpath) && (strstr(buff, "r-xp") || strstr(buff, "r--p"))) found = 1;
}
找到之后把地址信息保存下来, 并且进一步对这块内存按照elf格式进行解析, 重点将动态字符串表(Dynamic String Table)和 动态符号表(Dynamic Symbol Table)解析并保存下来,后面要用. 因为我们之所以打开目标so文件, 肯定后面是要调用其中定义的某个函数, 这里保存解析的数据就是为了下一步打开函数用的.
实现自定义的函数mydlsym
找到要使用的函数地址.
比如我们尝试调用art中打印java线程堆栈的功能函数.
void Thread::DumpJavaStack(std::ostream& os, bool checksuspended, bool dumplocks)
首先定义对应的函数指针:
typedef void (*DumpJavaStackFunc)(void *,std::ostream& os, bool param1, bool param2);
函数符号:
#define DUMP_JAVA_STACK_SIG "_ZNK3art6Thread13DumpJavaStackERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEEbb"
然后就可以通过上一步保存的符号表信息, 逐个进行字符串比对, 找到目标地址
for (k = 0; k < ctx->nsyms; k++, sym++)
if (strcmp(strings + sym->st_name, name) == 0) {
/* NB: sym->st_value is an offset into the section for relocatables,
but a VMA for shared libs or exe files, so we have to subtract the bias */
void *ret = (char *) ctx->load_addr + sym->st_value - ctx->bias;
log_info("%s found at %p", name, ret);
return ret;
}
调用目标函数
注意到要调用art::Thread::DumpJavaStack()
函数, 需要四个参数, 第一个参数就是当前线程, 这个有两种方式得到.
- 使用汇编语言, 从当前线程局部存储(TLS)中查询
#define __get_tls()
({
void **__val;
__asm__("mrs %0, tpidr_el0" : "=r"(__val));
__val;
})
#elif defined(__arm__)
#define TLS_SLOT_ART_THREAD_SELF 7
__get_tls()[TLS_SLOT_ART_THREAD_SELF];
- 使用java层线程来获得, 注意到Thread.java类有个变量:
private volatile long nativePeer;
这个实际上就是对应了在art虚拟机中的线程, 我们可以通过反射方便的得到这个字段值.
这里我们选择简单的b方案, 得到这个字段值,并通过jni调用传入native层
val nativePeer: Long = ReflectHelper.findField(Thread::class.java, "nativePeer") .getLong(Thread.currentThread()) val strFromNative = stringFromJNI(nativePeer)
external fun stringFromJNI(self: Long): String
于是, 最终我们的native代码是这样的:
jstring * native*stringFromJNI( JNIEnv *env, jclass, jlong self /* this */) { std::string hello = "Hello from C++"; void *handle = mydlopen("libart.so", RTLD_NOW);
if (handle == nullptr) {
// 处理打开库失败的情况
return env->NewStringUTF("Failed to open library");
}
DumpJavaStackFunc originFunc = reinterpret_cast<DumpJavaStackFunc>(mydlsym(handle, DUMP_JAVA_STACK_SIG));
if (originFunc == nullptr) {
log_info("这里为空", 1);
return env->NewStringUTF("定位DumpJavaStack失败");
}
std::ostringstream oss;
struct ctx *ctx = (struct ctx *) handle;
oss <<"libart.so加载在位置:"<< ctx->load_addr<<"\r\n";
oss <<"当前线程堆栈:\r\n";
// 调用原始函数,将输出写入 buffer
originFunc(reinterpret_cast<void *>(self), oss, true, false);
return env->NewStringUTF(oss.str().c_str());
}
成功运行后, 界面显示:
4_screen.png
总结
demo虽然简单, 但是演示了几个比较重要的功能:
- 如何在N版本以上打开非公开的native函数.
-
需要注意的是, 不同的android版本, 绕过这个限制的方法可能是不同的, 这里演示使用的办法只在android12上验证过. 在我们开发apm工具时, 对不同版本的适配也是最麻烦的.
-
其实还有另外的办法可以解决这个问题, 比如Android9.0 hook dlopen问题/如何hook dlopen相关函数, 但是因为本文只是对最开始时思路的演示, 这里就先跳过, 后面再另开一个demo单独演示.
-
通过读取maps文件, 解析so加载地址, 并进一步分析出函数表数据.
-
如何获取当前线程.
demo已经上传到github: https://github.com/shaopx/MyDLOpenDemo
网友评论