美文网首页
[APM学习]如何在android N之后dlopen使用系统私

[APM学习]如何在android N之后dlopen使用系统私

作者: vb12 | 来源:发表于2023-11-17 22:18 被阅读0次

    原文: 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()函数, 需要四个参数, 第一个参数就是当前线程, 这个有两种方式得到.

    1. 使用汇编语言, 从当前线程局部存储(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];
    
    
    1. 使用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虽然简单, 但是演示了几个比较重要的功能:

    1. 如何在N版本以上打开非公开的native函数.
    • 需要注意的是, 不同的android版本, 绕过这个限制的方法可能是不同的, 这里演示使用的办法只在android12上验证过. 在我们开发apm工具时, 对不同版本的适配也是最麻烦的.

    • 其实还有另外的办法可以解决这个问题, 比如Android9.0 hook dlopen问题/如何hook dlopen相关函数, 但是因为本文只是对最开始时思路的演示, 这里就先跳过, 后面再另开一个demo单独演示.

    1. 通过读取maps文件, 解析so加载地址, 并进一步分析出函数表数据.

    2. 如何获取当前线程.

    demo已经上传到github: https://github.com/shaopx/MyDLOpenDemo

    资源

    1. https://github.com/avs333/Nougat_dlfunctions

    2. https://github.com/KwaiAppTeam/KOOM

    相关文章

      网友评论

          本文标题:[APM学习]如何在android N之后dlopen使用系统私

          本文链接:https://www.haomeiwen.com/subject/wpsuwdtx.html