GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

应用安全 --- 安卓加固 之 apk运行容器dump内存文件

 

直接回答:不能"保证",但可以分情况实现

text
┌──────────────────────────────────────────────────────────┐
│              SO 文件加载难度分级                            │
├─────────┬────────────────────────────────┬───────────────┤
│  难度    │  SO 类型特征                    │  能否成功      │
├─────────┼────────────────────────────────┼───────────────┤
│  ★☆☆☆☆ │  纯计算库,无JNI,无外部依赖      │  ✅ 必定成功   │
│  ★★☆☆☆ │  有依赖SO,但都能提取到           │  ✅ 基本成功   │
│  ★★★☆☆ │  有JNI_OnLoad,注册native方法    │  ⚠️ 需要适配   │
│  ★★★★☆ │  深度依赖原App的Java类和资源      │  ❌ 非常困难   │
│  ★★★★★ │  有反调试/完整性校验/壳保护       │  ❌ 几乎不可能  │
└─────────┴────────────────────────────────┴───────────────┘

为什么不能"保证"? — 5 层障碍

text
加载另一个APK的SO时,会经过这些阶段,每一层都可能出错:

 dlopen("target.so")
 ┌─────────────────────────────────┐
 │ 第1层: ELF 加载                  │  架构是否匹配?
 │ linker 解析 ELF header          │  页面对齐是否兼容?
 └───────────┬─────────────────────┘
 ┌─────────────────────────────────┐
 │ 第2层: 依赖解析 (DT_NEEDED)      │  libfoo.so 能找到吗?
 │ linker 递归加载所有依赖           │  libc++_shared.so 版本对吗?
 └───────────┬─────────────────────┘
 ┌─────────────────────────────────┐
 │ 第3层: 符号重定位                 │  undefined symbol "XXX"
 │ 解析所有外部函数引用              │  其他SO导出的函数找得到吗?
 └───────────┬─────────────────────┘
 ┌─────────────────────────────────┐
 │ 第4层: 构造函数 (.init_array)    │  ⚠️ 自动执行,无法跳过
 │ 初始化全局变量、注册回调等         │  可能访问原App才有的资源
 └───────────┬─────────────────────┘
 ┌─────────────────────────────────┐
 │ 第5层: 运行时调用                 │  调用某个函数时
 │ 函数内部可能回调Java层            │  FindClass/GetMethodID 失败
 │ 可能访问特定文件/网络/硬件         │  SIGSEGV / SIGABRT
 └─────────────────────────────────┘

每层问题的详细分析与解决方案

第 1 层:架构匹配(容易解决)

text
问题:SO 的 CPU 架构必须与当前设备兼容

设备 arm64-v8a 可运行:arm64-v8a ✅, armeabi-v7a ✅(兼容)
设备 armeabi-v7a 可运行:armeabi-v7a ✅, arm64-v8a ❌
设备 x86_64 可运行:     x86_64 ✅, x86 ✅(兼容)

解决: 提取时选对 ABI,前面已讲。


第 2 层:依赖缺失(可以解决)

text
例如 libtarget.so 依赖链:

libtarget.so
  ├── libutils.so        ← 原APK自带,需一起提取
  ├── libc++_shared.so   ← 原APK自带,需一起提取  
  ├── liblog.so          ← 系统库,自动可用
  └── libcrypto.so       ← 原APK自带,需一起提取
        └── libz.so      ← 系统库,自动可用

解决: 从原 APK 把 lib/<abi>/ 下所有 SO 全部提取,按依赖顺序加载。

Java
// 全部提取 + 按顺序加载
for (String dep : loadOrder) {
    dlopen(libDir + "/" + dep, RTLD_LAZY | RTLD_GLOBAL);
}
dlopen(libDir + "/libtarget.so", RTLD_LAZY);

第 3 层:符号解析失败(部分可解决)

text
错误示例:
dlopen failed: cannot locate symbol "_ZN7android6Looper9pollInnerEi" 
referenced by "libtarget.so"

原因:引用了 Android 系统私有符号(非NDK公开API)

这种情况下的选择:

C++
// 方案1: RTLD_LAZY — 延迟到实际调用时才解析
// 如果你不调用使用该符号的函数,就不会崩溃
void *handle = dlopen(path, RTLD_LAZY);  // ✅ 加载不报错

// 方案2: 如果 RTLD_LAZY 还是报错(NEEDED 库缺失)
// 那就真的需要把缺失的库补上

第 4 层:.init_array(最棘手,这是根本障碍)

text
这是 dlopen 时自动执行的代码,你无法阻止:

__attribute__((constructor))
void my_init() {
    // 可能做以下任何事情:
    FILE *f = fopen("/data/data/com.original.app/config.dat", "r");  // ❌ 路径不存在
    check_signature();     // ❌ APK签名不匹配
    init_java_bridge();    // ❌ 找不到原App的Java类
    decrypt_strings();     // 可能OK,也可能依赖其他东西
}

关键事实:dlopen 一定会执行 .init_array 和 __attribute__((constructor)) 中的代码。这无法跳过(除非你自己写 ELF 加载器)。


第 5 层:运行时 JNI 回调(需要构建完整环境)

text
SO函数内部可能这样写:

JNIEXPORT void JNICALL Java_com_original_MyClass_doWork(JNIEnv *env, ...) {
    // 回调 Java
    jclass cls = env->FindClass("com/original/app/Helper");  // ❌ 类不存在
    jmethodID mid = env->GetMethodID(cls, "getData", "()V"); // ❌ 空指针
    env->CallVoidMethod(obj, mid);                           // 💥 崩溃
}

实际可行的完整方案

方案一:纯 dlopen + dlsym(最实用)

适用范围: 目标 SO 中有可以独立调用的 C 函数

text
┌─────────────────────────────────────────────────────────┐
│ 关键原理:                                               │
│                                                         │
│ dlopen() ≠ System.loadLibrary()                         │
│                                                         │
│ • dlopen    → 加载SO,执行.init_array,不调用JNI_OnLoad   │
│ • loadLibrary → 加载SO,执行.init_array,调用JNI_OnLoad   │
│                                                         │
│ 用 dlopen 就天然避开了 JNI_OnLoad 中的 FindClass 崩溃     │
│ 然后用 dlsym 直接拿 C 函数地址来调用                       │
└─────────────────────────────────────────────────────────┘
C++
// safe_direct_loader.cpp

#include <jni.h>
#include <dlfcn.h>
#include <android/log.h>
#include <string>
#include <vector>
#include <dirent.h>
#include <cstring>
#include <algorithm>

#define TAG "DirectLoader"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,  TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)

/**
 * 加载目录中的所有依赖 SO(按字母序,依赖库通常字母序靠前)
 * 然后最后加载目标 SO
 */
extern "C"
JNIEXPORT jlong JNICALL
Java_com_example_soloader_DirectLoader_loadFromDirectory(
        JNIEnv *env, jclass clazz,
        jstring lib_dir_path,
        jstring target_name) {

    const char *dir = env->GetStringUTFChars(lib_dir_path, nullptr);
    const char *target = env->GetStringUTFChars(target_name, nullptr);

    std::string dirStr(dir), targetStr(target);
    std::vector<std::string> allSoFiles;

    // 扫描目录中所有 .so 文件
    DIR *d = opendir(dir);
    if (d) {
        struct dirent *entry;
        while ((entry = readdir(d)) != nullptr) {
            std::string name = entry->d_name;
            if (name.size() > 3 &&
                name.substr(name.size() - 3) == ".so") {
                allSoFiles.push_back(name);
            }
        }
        closedir(d);
    }

    // 排序:让 libc++_shared.so 等基础库排在前面
    std::sort(allSoFiles.begin(), allSoFiles.end());

    // 把目标移到最后
    allSoFiles.erase(
        std::remove(allSoFiles.begin(), allSoFiles.end(), targetStr),
        allSoFiles.end());
    allSoFiles.push_back(targetStr);

    LOGI("=== 加载顺序 (%zu 个文件) ===", allSoFiles.size());

    // 依次加载
    void *targetHandle = nullptr;

    for (const auto &soName : allSoFiles) {
        std::string fullPath = dirStr + "/" + soName;
        bool isTarget = (soName == targetStr);

        dlerror(); // 清除

        // 依赖库用 RTLD_GLOBAL 导出符号给后续库使用
        // 目标库用 RTLD_LAZY 延迟解析
        int flags = isTarget ? RTLD_LAZY : (RTLD_LAZY | RTLD_GLOBAL);

        void *handle = dlopen(fullPath.c_str(), flags);

        if (handle) {
            LOGI("  ✅ %s", soName.c_str());
            if (isTarget) {
                targetHandle = handle;
            }
        } else {
            const char *err = dlerror();
            if (isTarget) {
                LOGE("  ❌ %s: %s", soName.c_str(), err ? err : "?");
            } else {
                // 依赖加载失败只是警告,不阻止继续
                LOGE("  ⚠️ %s: %s", soName.c_str(), err ? err : "?");
            }
        }
    }

    env->ReleaseStringUTFChars(lib_dir_path, dir);
    env->ReleaseStringUTFChars(target_name, target);

    return reinterpret_cast<jlong>(targetHandle);
}

/**
 * 安全调用函数 — 带信号保护
 */
#include <setjmp.h>
#include <signal.h>

static sigjmp_buf s_jump_buf;
static volatile bool s_in_safe_call = false;

static void crash_handler(int sig) {
    if (s_in_safe_call) {
        LOGE("💥 捕获信号 %d,安全返回", sig);
        siglongjmp(s_jump_buf, sig);
    }
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_example_soloader_DirectLoader_safeCallInt(
        JNIEnv *env, jclass clazz,
        jlong handle, jstring func_name) {

    if (!handle) return -1;

    const char *name = env->GetStringUTFChars(func_name, nullptr);
    void *sym = dlsym(reinterpret_cast<void*>(handle), name);

    if (!sym) {
        LOGE("符号 '%s' 不存在", name);
        env->ReleaseStringUTFChars(func_name, name);
        return -1;
    }

    // 安装临时信号处理器
    struct sigaction sa_old_segv, sa_old_abrt, sa_new;
    memset(&sa_new, 0, sizeof(sa_new));
    sa_new.sa_handler = crash_handler;
    sa_new.sa_flags = 0;
    sigaction(SIGSEGV, &sa_new, &sa_old_segv);
    sigaction(SIGABRT, &sa_new, &sa_old_abrt);

    int result = -999;
    s_in_safe_call = true;

    int sig = sigsetjmp(s_jump_buf, 1);
    if (sig == 0) {
        // 正常路径
        typedef int (*func_t)();
        result = ((func_t)sym)();
        LOGI("函数 '%s' 返回: %d", name, result);
    } else {
        // 崩溃恢复路径
        LOGE("函数 '%s' 执行时崩溃 (signal %d)", name, sig);
        result = -sig;
    }

    s_in_safe_call = false;

    // 恢复原信号处理器
    sigaction(SIGSEGV, &sa_old_segv, nullptr);
    sigaction(SIGABRT, &sa_old_abrt, nullptr);

    env->ReleaseStringUTFChars(func_name, name);
    return result;
}

DirectLoader.java

Java
package com.example.soloader;

public class DirectLoader {
    static { System.loadLibrary("sohelper"); }

    public static native long loadFromDirectory(String libDir, String targetSo);
    public static native int  safeCallInt(long handle, String funcName);
}

方案二:自定义 ELF 加载器(跳过 .init_array)

这是唯一能跳过构造函数的方法,但实现复杂度极高:

C++
// custom_elf_loader.cpp
// 简化版:手动解析 ELF 并映射到内存,但跳过 .init_array

#include <jni.h>
#include <elf.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <cstdio>
#include <android/log.h>
#include <map>
#include <string>

#define TAG "CustomELF"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,  TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)

struct LoadedElf {
    void *base;
    size_t size;
    std::map<std::string, void*> symbols;
    bool valid;
};

/**
 * 最小化 ELF 加载器
 * - 手动 mmap PT_LOAD 段
 * - 解析符号表
 * - 不执行 .init_array(这是关键区别)
 * - 不做重定位(所以只能用于查看数据/简单分析)
 *
 * 注意:这个加载器加载的 SO 中的函数如果引用了
 * 外部符号(GOT/PLT),调用时会崩溃。
 * 只适合读取数据或调用纯计算函数。
 */
static LoadedElf custom_load_elf(const char *path) {
    LoadedElf result = {};
    result.valid = false;

    int fd = open(path, O_RDONLY);
    if (fd < 0) {
        LOGE("无法打开: %s", path);
        return result;
    }

    // 获取文件大小
    off_t file_size = lseek(fd, 0, SEEK_END);
    lseek(fd, 0, SEEK_SET);

    // 整个文件映射到内存(只读)
    void *file_map = mmap(nullptr, file_size, PROT_READ,
                          MAP_PRIVATE, fd, 0);
    if (file_map == MAP_FAILED) {
        close(fd);
        return result;
    }

    auto *base = static_cast<uint8_t*>(file_map);

    // 验证 ELF
    if (memcmp(base, "\x7f" "ELF", 4) != 0) {
        LOGE("不是 ELF 文件");
        munmap(file_map, file_size);
        close(fd);
        return result;
    }

#if __LP64__
    auto *ehdr = reinterpret_cast<Elf64_Ehdr*>(base);
    auto *phdrs = reinterpret_cast<Elf64_Phdr*>(base + ehdr->e_phoff);
    auto *shdrs = reinterpret_cast<Elf64_Shdr*>(
        ehdr->e_shoff ? base + ehdr->e_shoff : nullptr);

    // 解析符号表(.dynsym + .dynstr)
    if (shdrs && ehdr->e_shnum > 0) {
        // 先找 section name string table
        const char *shstrtab = nullptr;
        if (ehdr->e_shstrndx < ehdr->e_shnum) {
            shstrtab = reinterpret_cast<const char*>(
                base + shdrs[ehdr->e_shstrndx].sh_offset);
        }

        Elf64_Shdr *dynsym_shdr = nullptr;
        const char *dynstr = nullptr;

        for (int i = 0; i < ehdr->e_shnum; i++) {
            if (shdrs[i].sh_type == SHT_DYNSYM) {
                dynsym_shdr = &shdrs[i];
            }
            if (shdrs[i].sh_type == SHT_STRTAB && shstrtab) {
                const char *name = shstrtab + shdrs[i].sh_name;
                if (strcmp(name, ".dynstr") == 0) {
                    dynstr = reinterpret_cast<const char*>(
                        base + shdrs[i].sh_offset);
                }
            }
        }

        if (dynsym_shdr && dynstr) {
            int sym_count = dynsym_shdr->sh_size / sizeof(Elf64_Sym);
            auto *syms = reinterpret_cast<Elf64_Sym*>(
                base + dynsym_shdr->sh_offset);

            for (int i = 0; i < sym_count; i++) {
                if (syms[i].st_name && syms[i].st_value) {
                    const char *name = dynstr + syms[i].st_name;
                    // 记录符号:注意这里的地址是基于 file mapping 的
                    // 实际使用时需要加上基址偏移
                    result.symbols[name] =
                        reinterpret_cast<void*>(syms[i].st_value);
                }
            }
            LOGI("解析到 %zu 个符号", result.symbols.size());
        }
    }
#endif

    result.base = file_map;
    result.size = file_size;
    result.valid = true;

    close(fd);

    LOGI("自定义加载完成: %s (%ld bytes, %zu symbols)",
         path, (long)file_size, result.symbols.size());
    LOGI("⚠️ 已跳过 .init_array,未执行任何构造函数");
    LOGI("⚠️ 未执行重定位,仅可用于数据读取和符号分析");

    return result;
}

extern "C"
JNIEXPORT jlong JNICALL
Java_com_example_soloader_DirectLoader_customLoadNoInit(
        JNIEnv *env, jclass clazz, jstring path) {

    const char *p = env->GetStringUTFChars(path, nullptr);
    auto *elf = new LoadedElf(custom_load_elf(p));
    env->ReleaseStringUTFChars(path, p);

    if (!elf->valid) {
        delete elf;
        return 0;
    }
    return reinterpret_cast<jlong>(elf);
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_soloader_DirectLoader_listSymbols(
        JNIEnv *env, jclass clazz, jlong handle) {

    auto *elf = reinterpret_cast<LoadedElf*>(handle);
    if (!elf || !elf->valid) return env->NewStringUTF("invalid");

    std::string result;
    char buf[256];
    int count = 0;

    for (auto &pair : elf->symbols) {
        snprintf(buf, sizeof(buf), "0x%08lx  %s\n",
                 (unsigned long)(uintptr_t)pair.second,
                 pair.first.c_str());
        result += buf;
        if (++count > 500) {
            result += "... (truncated)\n";
            break;
        }
    }

    return env->NewStringUTF(result.c_str());
}

方案三:虚拟化运行环境(工业级方案)

这就是 VirtualApp / VirtualXposed / 太极 等框架的原理:

text
┌────────────────────────────────────────────────────────┐
│                  你的 Loader App                       │
│                                                        │
│  ┌────────────────────────────────────────────────────┐ │
│  │              虚拟化容器                              │ │
│  │                                                    │ │
│  │  ┌──────────────┐  ┌───────────────────────────┐   │ │
│  │  │ 虚拟 ClassLoader│  │ 原APK的 classes.dex      │   │ │
│  │  │ (加载原App的  │  │ (提供 Java 类给 SO 回调)  │   │ │
│  │  │  Java 类)     │  │                           │   │ │
│  │  └──────────────┘  └───────────────────────────┘   │ │
│  │                                                    │ │
│  │  ┌──────────────┐  ┌───────────────────────────┐   │ │
│  │  │ 虚拟 Context  │  │ 原APK的 resources/assets  │   │ │
│  │  │ (伪造包名、   │  │ (SO 可能读取的资源文件)    │   │ │
│  │  │  签名、路径)  │  │                           │   │ │
│  │  └──────────────┘  └───────────────────────────┘   │ │
│  │                                                    │ │
│  │  ┌──────────────────────────────────────────────┐  │ │
│  │  │        目标 SO  (在完整环境中加载运行)         │  │ │
│  │  └──────────────────────────────────────────────┘  │ │
│  └────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘

核心代码思路(简化):

Java
/**
 * 虚拟环境加载器 — 加载原 APK 的 DEX + SO
 * 这样 SO 中的 JNI 回调能找到正确的 Java 类
 */
public class VirtualSoLoader {
    
    /**
     * 创建虚拟环境并加载 SO
     */
    public static long loadInVirtualEnv(Context hostContext, 
                                         String apkPath, 
                                         String targetSo) throws Exception {
        
        // 1. 创建 DexClassLoader 加载原 APK 的 Java 类
        String dexOutputDir = hostContext.getDir("vdex", 0).getAbsolutePath();
        String nativeLibDir = hostContext.getDir("vlib", 0).getAbsolutePath();
        
        // 先把原 APK 中的所有 SO 提取到 nativeLibDir
        extractNativeLibs(apkPath, nativeLibDir);
        
        DexClassLoader dexLoader = new DexClassLoader(
            apkPath,                    // 原 APK 路径(包含 dex)
            dexOutputDir,               // dex 优化输出目录
            nativeLibDir,               // native 库搜索路径
            hostContext.getClassLoader() // 父 ClassLoader
        );
        
        // 2. 把这个 ClassLoader 设置到当前线程
        //    这样 JNI FindClass 就能通过这个 loader 找到原 APK 的类
        Thread.currentThread().setContextClassLoader(dexLoader);
        
        // 3. 通过反射用这个 ClassLoader 来加载 SO
        //    利用 Runtime.doLoad 或直接 dlopen
        String soPath = nativeLibDir + "/" + targetSo;
        
        // 方式A: 通过反射调用 System.load(会触发 JNI_OnLoad)
        // 这时 FindClass 会使用 dexLoader,能找到原 APK 的类
        try {
            System.load(soPath);
            Log.i("Virtual", "System.load 成功(含 JNI_OnLoad)");
        } catch (UnsatisfiedLinkError e) {
            Log.e("Virtual", "System.load 失败: " + e.getMessage());
            // 回退到 dlopen(不触发 JNI_OnLoad)
        }
        
        // 方式B: dlopen(不触发 JNI_OnLoad,更安全)
        long handle = NativeBridge.nativeLoadSo(soPath);
        
        // 4. 如果需要,手动调用 JNI_OnLoad
        //    此时 FindClass 应该能通过 dexLoader 找到类
        if (handle != 0) {
            NativeBridge.nativeCallJNIOnLoad(handle);
        }
        
        return handle;
    }
    
    private static void extractNativeLibs(String apkPath, String outDir) 
            throws IOException {
        // ... 提取逻辑同前面的 ApkExtractor
    }
}

但这仍然不能保证 100%: 原 App 的 SO 可能在 JNI_OnLoad 中通过 env->GetJavaVM() 获取 VM,然后通过 VM 拿到 ClassLoader,而这个 ClassLoader 不一定是你设置的那个。


实际成功率评估

我对真实场景做一个诚实的评估:

text
┌───────────────────────────────────────────────────────────────┐
│  实际测试成功率                                                │
├──────────────────────────┬────────┬───────────────────────────┤
│  SO 类型                  │ 成功率  │ 典型例子                   │
├──────────────────────────┼────────┼───────────────────────────┤
│ 开源库编译的 SO           │  95%   │ libsqlite, libcrypto,     │
│ (纯C/C++,无JNI回调)      │        │ libpng, libjpeg, ffmpeg   │
├──────────────────────────┼────────┼───────────────────────────┤
│ 简单 JNI 库              │  70%   │ 游戏引擎的工具库,          │
│ (有JNI但依赖少)           │        │ 加密库, 数据处理库         │
├──────────────────────────┼────────┼───────────────────────────┤
│ 中等复杂 JNI 库           │  30%   │ 大部分商业App的核心SO      │
│ (深度依赖Java类)          │        │ libwechat, libali...      │
├──────────────────────────┼────────┼───────────────────────────┤
│ 有反调试/签名校验的 SO    │   5%   │ 金融App, 游戏反作弊,      │
│                          │        │ 加固壳                    │
├──────────────────────────┼────────┼───────────────────────────┤
│ 自定义 linker/壳保护      │   0%   │ 梆梆/爱加密/娜迦加固      │
│                          │        │ (SO 本身就不是正常 ELF)    │
└──────────────────────────┴────────┴───────────────────────────┘

终极诊断流程

当加载失败时,按这个顺序排查:

Java
/**
 * 系统化诊断工具
 */
public class SoDiagnostics {

    public static String diagnose(Context context, String soPath) {
        StringBuilder report = new StringBuilder();
        
        // Step 1: 文件基本检查
        report.append("═══ 1. 文件检查 ═══\n");
        File f = new File(soPath);
        report.append("存在: ").append(f.exists()).append("\n");
        report.append("大小: ").append(f.length()).append(" bytes\n");
        report.append("可读: ").append(f.canRead()).append("\n");
        report.append("可执行: ").append(f.canExecute()).append("\n");
        
        // Step 2: ELF 架构
        report.append("\n═══ 2. 架构检查 ═══\n");
        String arch = NativeBridge.nativeGetElfArch(soPath);
        report.append("SO 架构: ").append(arch).append("\n");
        report.append("设备支持: ")
              .append(ApkExtractor.getDeviceAbis()).append("\n");
        boolean archOk = ApkExtractor.getDeviceAbis().contains(arch);
        report.append("匹配: ").append(archOk ? "✅" : "❌").append("\n");
        
        if (!archOk) {
            report.append("⛔ 架构不匹配,无法加载!\n");
            return report.toString();
        }
        
        // Step 3: 依赖分析
        report.append("\n═══ 3. 依赖分析 ═══\n");
        String[] deps = NativeBridge.nativeGetDependencies(soPath);
        if (deps != null) {
            for (String dep : deps) {
                // 尝试 dlopen 看系统能否找到
                boolean available = checkLibAvailable(dep, soPath);
                report.append(available ? "  ✅ " : "  ❌ ")
                      .append(dep).append("\n");
            }
        }
        
        // Step 4: 尝试加载
        report.append("\n═══ 4. 加载测试 ═══\n");
        
        // 4a: RTLD_LAZY
        long handle = NativeBridge.nativeLoadSo(soPath);
        if (handle != 0) {
            report.append("✅ dlopen(RTLD_LAZY) 成功\n");
            report.append("句柄: 0x")
                  .append(Long.toHexString(handle)).append("\n");
            
            // 检查 JNI_OnLoad 是否存在
            long onLoadAddr = NativeBridge.nativeGetSymbolAddress(
                handle, "JNI_OnLoad");
            report.append("JNI_OnLoad: ")
                  .append(onLoadAddr != 0 ? "存在" : "不存在")
                  .append("\n");
            
            if (onLoadAddr != 0) {
                report.append("⚠️ 该 SO 有 JNI_OnLoad,")
                      .append("调用时可能因 FindClass 失败而崩溃\n");
                report.append("建议:跳过 JNI_OnLoad,")
                      .append("直接用 dlsym 调用需要的函数\n");
            }
            
            NativeBridge.nativeUnloadSo(handle);
        } else {
            String err = NativeBridge.nativeGetLastError();
            report.append("❌ dlopen 失败: ").append(err).append("\n");
            
            // 诊断具体原因
            if (err.contains("cannot locate symbol")) {
                report.append("\n原因:缺少外部符号\n");
                report.append("建议:确保所有依赖SO都在同目录下,")
                      .append("并先加载它们\n");
            } else if (err.contains("NEEDED")) {
                report.append("\n原因:缺少依赖库\n");
                report.append("建议:从原APK提取所有SO\n");
            } else if (err.contains("init_array") || 
                       err.contains("SIGSEGV")) {
                report.append("\n原因:初始化代码崩溃\n");
                report.append("建议:需要自定义ELF加载器跳过init\n");
            }
        }
        
        return report.toString();
    }
    
    private static boolean checkLibAvailable(String libName, 
                                              String soPath) {
        // 检查同目录
        File sameDir = new File(
            new File(soPath).getParent(), libName);
        if (sameDir.exists()) return true;
        
        // 尝试系统加载
        try {
            void *h = dlopen(libName, RTLD_LAZY);
            if (h != null) { dlclose(h); return true; }
        } catch (Exception e) {}
        
        return false;
    }
}

最终结论

text
┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  Q: 能否在 APK-A 中加载 APK-B 的 SO 并保证不报错?            │
│                                                              │
│  A: 不能"保证",但大部分情况下可以做到:                        │
│                                                              │
│  ✅ 一定能做到的:                                             │
│     • 提取 SO 文件                                           │
│     • 分析依赖关系                                            │
│     • 用 dlopen 尝试加载(不触发 JNI_OnLoad)                  │
│     • 用 dlsym 找到并调用独立的 C 函数                         │
│     • 从内存 dump SO                                         │
│                                                              │
│  ❌ 不能保证的:                                               │
│     • .init_array 中的代码不崩溃(无法跳过,除非自写加载器)     │
│     • 调用涉及 JNI 回调的函数不崩溃(缺少 Java 类)            │
│     • 有完整性校验的 SO 不检测到异常环境                        │
│                                                              │
│  💡 实际建议:                                                 │
│     1. 先用诊断工具分析目标 SO 的复杂度                         │
│     2. 提取全部依赖,按拓扑序用 RTLD_GLOBAL 加载               │
│     3. 目标 SO 用 dlopen(RTLD_LAZY) 加载,不调 JNI_OnLoad    │
│     4. 只 dlsym 调用你需要的特定函数                           │
│     5. 如果还崩溃,用信号处理器捕获并安全返回                    │
│     6. 如果 init 就崩溃,考虑自定义 ELF 加载器或放弃            │
│                                                              │
└──────────────────────────────────────────────────────────────┘

一句话总结: dlopen 能帮你绕过 JNI_OnLoad 的坑,RTLD_LAZY 能帮你绕过符号缺失的坑,但 .init_array 和运行时 JNI 回调是绕不过去的硬伤——这取决于目标 SO 本身的设计,不是你的加载器能完全控制的。

 

 

Android APK — 界面上传 SO 文件并动态加载 + 内存 Dump

项目结构

text
app/src/main/
├── java/com/example/soloader/
│   ├── MainActivity.java          ← 主界面(文件选择/上传)
│   └── NativeLoader.java          ← JNI桥接
├── cpp/
│   ├── CMakeLists.txt
│   ├── native_loader.cpp          ← dlopen/dlsym
│   └── memory_dumper.cpp          ← 内存dump
├── res/layout/
│   └── activity_main.xml
└── AndroidManifest.xml

1. build.gradle (app)

groovy
plugins {
    id 'com.android.application'
}

android {
    namespace 'com.example.soloader'
    compileSdk 34

    defaultConfig {
        applicationId "com.example.soloader"
        minSdk 24
        targetSdk 34
        versionCode 1
        versionName "1.0"

        ndk {
            abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64', 'x86'
        }
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++17 -fexceptions"
                arguments "-DANDROID_STL=c++_shared"
            }
        }
    }

    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.22.1"
        }
    }

    buildFeatures {
        viewBinding true
    }

    buildTypes {
        release { minifyEnabled false }
    }
}

dependencies {
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.11.0'
    implementation 'androidx.activity:activity:1.8.2'
}

2. AndroidManifest.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.soloader">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:label="SO动态加载器"
        android:icon="@mipmap/ic_launcher"
        android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar"
        android:extractNativeLibs="true">

        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:windowSoftInputMode="adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

3. activity_main.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    android:background="#FAFAFA">

    <!-- 标题 -->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="SO 动态加载器"
        android:textSize="22sp"
        android:textStyle="bold"
        android:gravity="center"
        android:textColor="#1565C0"
        android:paddingBottom="8dp" />

    <!-- 状态卡片 -->
    <com.google.android.material.card.MaterialCardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="12dp"
        app:cardElevation="2dp"
        app:cardCornerRadius="8dp"
        xmlns:app="http://schemas.android.com/apk/res-auto">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="12dp">

            <TextView
                android:id="@+id/tv_file_info"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="📂 未选择文件"
                android:textSize="14sp" />

            <TextView
                android:id="@+id/tv_load_status"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="⏳ 未加载"
                android:textSize="14sp"
                android:layout_marginTop="4dp" />
        </LinearLayout>
    </com.google.android.material.card.MaterialCardView>

    <!-- 按钮区域 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginBottom="8dp">

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btn_pick_file"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="📁 选择SO文件"
            android:textSize="13sp"
            android:layout_marginEnd="4dp" />

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btn_load"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="▶ 加载运行"
            android:textSize="13sp"
            android:enabled="false"
            android:layout_marginStart="4dp" />
    </LinearLayout>

    <!-- 函数调用区域 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginBottom="8dp">

        <EditText
            android:id="@+id/et_func_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="输入函数名"
            android:textSize="14sp"
            android:singleLine="true"
            android:layout_marginEnd="4dp"
            android:padding="10dp"
            android:background="@android:drawable/edit_text" />

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btn_call"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="调用"
            android:textSize="13sp"
            android:enabled="false" />
    </LinearLayout>

    <!-- 操作按钮 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginBottom="8dp">

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btn_dump"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="💾 内存Dump"
            android:textSize="12sp"
            android:enabled="false"
            android:layout_marginEnd="2dp"
            style="@style/Widget.MaterialComponents.Button.OutlinedButton" />

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btn_maps"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="🗺 内存映射"
            android:textSize="12sp"
            android:enabled="false"
            android:layout_marginStart="2dp"
            android:layout_marginEnd="2dp"
            style="@style/Widget.MaterialComponents.Button.OutlinedButton" />

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btn_unload"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="⏹ 卸载"
            android:textSize="12sp"
            android:enabled="false"
            android:layout_marginStart="2dp"
            style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
    </LinearLayout>

    <!-- 清除日志 -->
    <com.google.android.material.button.MaterialButton
        android:id="@+id/btn_clear_log"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="🗑 清除日志"
        android:textSize="12sp"
        android:layout_marginBottom="8dp"
        style="@style/Widget.MaterialComponents.Button.TextButton" />

    <!-- 日志输出 -->
    <ScrollView
        android:id="@+id/scroll_log"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="#263238"
        android:padding="8dp">

        <TextView
            android:id="@+id/tv_log"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text=""
            android:textSize="11sp"
            android:textColor="#E0E0E0"
            android:fontFamily="monospace"
            android:lineSpacingExtra="2dp" />
    </ScrollView>
</LinearLayout>

4. NativeLoader.java

Java
package com.example.soloader;

public class NativeLoader {

    static {
        System.loadLibrary("soloader");
    }

    // 加载SO,返回handle(0=失败)
    public static native long loadSo(String soPath);

    // 调用返回字符串的无参函数
    public static native String callFunction(long handle, String funcName);

    // 调用返回int的双参数函数
    public static native int callIntFunction(long handle, String funcName, int a, int b);

    // 调用无返回值无参函数
    public static native void callVoidFunction(long handle, String funcName);

    // 列出SO中所有导出符号
    public static native String listSymbols(long handle, String soPath);

    // 卸载SO
    public static native boolean unloadSo(long handle);

    // 获取基地址
    public static native long getBaseAddress(String soName);

    // 简单dump
    public static native boolean dumpSo(String soName, String outPath);

    // 高级dump(修复ELF)
    public static native boolean dumpAndFixSo(String soName, String outPath);

    // 查看内存映射
    public static native String getMaps(String filter);

    // 获取错误
    public static native String getLastError();
}

5. MainActivity.java — 文件选择 + 全部交互逻辑

Java
package com.example.soloader;

import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.OpenableColumns;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class MainActivity extends AppCompatActivity {

    private TextView tvFileInfo, tvLoadStatus, tvLog;
    private Button btnPickFile, btnLoad, btnCall, btnDump, btnMaps, btnUnload;
    private EditText etFuncName;
    private ScrollView scrollLog;
    private Handler mainHandler;

    // SO 相关状态
    private String soLocalPath = "";   // 复制到私有目录后的路径
    private String soFileName = "";    // 原始文件名
    private long soHandle = 0;         // dlopen 返回的句柄
    private boolean isLoaded = false;

    // 文件选择器
    private final ActivityResultLauncher<Intent> filePickerLauncher =
            registerForActivityResult(
                    new ActivityResultContracts.StartActivityForResult(),
                    result -> {
                        if (result.getResultCode() == RESULT_OK && result.getData() != null) {
                            Uri uri = result.getData().getData();
                            if (uri != null) {
                                handleSelectedFile(uri);
                            }
                        }
                    }
            );

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mainHandler = new Handler(Looper.getMainLooper());
        initViews();
        initListeners();

        log("INFO", "应用已启动");
        log("INFO", "设备ABI: " + Build.SUPPORTED_ABIS[0]);
        log("INFO", "Android版本: " + Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ")");
        log("INFO", "私有目录: " + getFilesDir().getAbsolutePath());
        log("INFO", "点击「选择SO文件」开始使用");
    }

    private void initViews() {
        tvFileInfo = findViewById(R.id.tv_file_info);
        tvLoadStatus = findViewById(R.id.tv_load_status);
        tvLog = findViewById(R.id.tv_log);
        scrollLog = findViewById(R.id.scroll_log);
        btnPickFile = findViewById(R.id.btn_pick_file);
        btnLoad = findViewById(R.id.btn_load);
        btnCall = findViewById(R.id.btn_call);
        btnDump = findViewById(R.id.btn_dump);
        btnMaps = findViewById(R.id.btn_maps);
        btnUnload = findViewById(R.id.btn_unload);
        etFuncName = findViewById(R.id.et_func_name);

        Button btnClearLog = findViewById(R.id.btn_clear_log);
        btnClearLog.setOnClickListener(v -> tvLog.setText(""));
    }

    private void initListeners() {
        // ===== 1. 选择文件 =====
        btnPickFile.setOnClickListener(v -> openFilePicker());

        // ===== 2. 加载SO =====
        btnLoad.setOnClickListener(v -> loadSo());

        // ===== 3. 调用函数 =====
        btnCall.setOnClickListener(v -> callFunction());

        // ===== 4. 内存Dump =====
        btnDump.setOnClickListener(v -> dumpSo());

        // ===== 5. 查看内存映射 =====
        btnMaps.setOnClickListener(v -> showMaps());

        // ===== 6. 卸载 =====
        btnUnload.setOnClickListener(v -> unloadSo());
    }

    // ==========================================
    //  文件选择(从手机中选择 .so 文件上传)
    // ==========================================
    private void openFilePicker() {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("*/*");  // 所有文件类型(.so没有标准MIME)
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);

        // 也可用 ACTION_OPEN_DOCUMENT
        // Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        // intent.setType("*/*");

        try {
            filePickerLauncher.launch(Intent.createChooser(intent, "选择SO文件"));
        } catch (Exception e) {
            log("ERROR", "无法打开文件选择器: " + e.getMessage());
            Toast.makeText(this, "无法打开文件选择器", Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * 处理用户选择的文件:
     *   1. 读取文件名
     *   2. 从 URI 复制到应用私有目录(确保有执行权限)
     *   3. 验证是否是 ELF 文件
     */
    private void handleSelectedFile(Uri uri) {
        try {
            // 获取文件名
            soFileName = getFileNameFromUri(uri);
            log("INFO", "选中文件: " + soFileName);

            if (!soFileName.endsWith(".so") && !soFileName.contains(".so.")) {
                log("WARN", "文件可能不是SO库 (扩展名不是.so),仍然尝试加载");
            }

            // 从URI读取并复制到私有目录
            File destDir = new File(getFilesDir(), "loaded_libs");
            if (!destDir.exists()) destDir.mkdirs();

            File destFile = new File(destDir, soFileName);
            soLocalPath = destFile.getAbsolutePath();

            InputStream is = getContentResolver().openInputStream(uri);
            if (is == null) {
                log("ERROR", "无法读取文件");
                return;
            }

            FileOutputStream fos = new FileOutputStream(destFile);
            byte[] buf = new byte[8192];
            int len;
            long totalSize = 0;
            while ((len = is.read(buf)) > 0) {
                fos.write(buf, 0, len);
                totalSize += len;
            }
            fos.close();
            is.close();

            // 设置权限
            destFile.setReadable(true, false);
            destFile.setExecutable(true, false);
            destFile.setWritable(true, false);

            log("OK", "文件已复制到: " + soLocalPath);
            log("INFO", "文件大小: " + formatSize(totalSize));

            // 验证 ELF 魔术字节
            if (verifyElf(destFile)) {
                log("OK", "ELF格式验证通过 ✓");
                tvFileInfo.setText("📂 " + soFileName + " (" + formatSize(totalSize) + ")");
                btnLoad.setEnabled(true);
                tvLoadStatus.setText("⏳ 就绪,可以加载");
            } else {
                log("ERROR", "文件不是有效的ELF格式!");
                tvFileInfo.setText("❌ 无效文件: " + soFileName);
                btnLoad.setEnabled(false);
            }

        } catch (Exception e) {
            log("ERROR", "处理文件失败: " + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 从 Uri 获取真实文件名
     */
    private String getFileNameFromUri(Uri uri) {
        String name = "unknown.so";

        if ("content".equals(uri.getScheme())) {
            try (Cursor cursor = getContentResolver().query(uri, null, null, null, null)) {
                if (cursor != null && cursor.moveToFirst()) {
                    int idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
                    if (idx >= 0) {
                        name = cursor.getString(idx);
                    }
                }
            }
        }

        if ("unknown.so".equals(name)) {
            String path = uri.getPath();
            if (path != null) {
                int cut = path.lastIndexOf('/');
                if (cut != -1) name = path.substring(cut + 1);
            }
        }

        return name;
    }

    /**
     * 验证文件是否是 ELF 格式
     */
    private boolean verifyElf(File file) {
        try {
            java.io.FileInputStream fis = new java.io.FileInputStream(file);
            byte[] magic = new byte[16];
            int read = fis.read(magic);
            fis.close();

            if (read < 4) return false;

            // ELF 魔术字节: 0x7F 'E' 'L' 'F'
            boolean isElf = (magic[0] == 0x7F && magic[1] == 'E'
                    && magic[2] == 'L' && magic[3] == 'F');

            if (isElf && read >= 5) {
                String elfClass = (magic[4] == 1) ? "32-bit" : "64-bit";
                log("INFO", "ELF类型: " + elfClass);

                String deviceAbi = Build.SUPPORTED_ABIS[0];
                boolean is64Device = deviceAbi.contains("64");
                boolean is64So = (magic[4] == 2);

                if (is64Device != is64So) {
                    log("WARN", "架构可能不匹配! 设备=" + deviceAbi
                            + ", SO=" + elfClass);
                }
            }
            return isElf;

        } catch (Exception e) {
            return false;
        }
    }

    // ==========================================
    //  加载 SO
    // ==========================================
    private void loadSo() {
        if (soLocalPath.isEmpty()) {
            log("ERROR", "未选择文件");
            return;
        }

        if (isLoaded) {
            log("WARN", "已有SO加载,先卸载");
            unloadSo();
        }

        btnLoad.setEnabled(false);
        log("INFO", "正在加载: " + soLocalPath);

        new Thread(() -> {
            long handle = NativeLoader.loadSo(soLocalPath);

            mainHandler.post(() -> {
                if (handle != 0) {
                    soHandle = handle;
                    isLoaded = true;

                    long baseAddr = NativeLoader.getBaseAddress(soFileName);

                    log("OK", "══════════════════════════════");
                    log("OK", "  SO 加载成功!");
                    log("OK", "  Handle : 0x" + Long.toHexString(handle));
                    log("OK", "  基地址 : 0x" + Long.toHexString(baseAddr));
                    log("OK", "══════════════════════════════");

                    tvLoadStatus.setText("✅ 已加载 | Handle: 0x" + Long.toHexString(handle));

                    // 自动列出导出符号
                    listExportedSymbols();

                    updateButtons(true);
                } else {
                    String err = NativeLoader.getLastError();
                    log("ERROR", "加载失败: " + err);
                    tvLoadStatus.setText("❌ 加载失败");
                    btnLoad.setEnabled(true);
                }
            });
        }).start();
    }

    /**
     * 列出 SO 的导出符号(帮助用户知道能调用什么函数)
     */
    private void listExportedSymbols() {
        new Thread(() -> {
            String symbols = NativeLoader.listSymbols(soHandle, soLocalPath);
            mainHandler.post(() -> {
                if (symbols != null && !symbols.isEmpty()) {
                    log("INFO", "── 导出符号列表 ──");
                    log("INFO", symbols);
                }
            });
        }).start();
    }

    // ==========================================
    //  调用函数
    // ==========================================
    private void callFunction() {
        if (!isLoaded) {
            log("ERROR", "SO未加载");
            return;
        }

        String funcName = etFuncName.getText().toString().trim();
        if (funcName.isEmpty()) {
            Toast.makeText(this, "请输入函数名", Toast.LENGTH_SHORT).show();
            return;
        }

        log("INFO", "调用函数: " + funcName + "()");

        new Thread(() -> {
            // 先尝试作为返回字符串的函数
            String strResult = NativeLoader.callFunction(soHandle, funcName);

            mainHandler.post(() -> {
                if (strResult != null && !strResult.startsWith("ERROR:")) {
                    log("OK", funcName + "() 返回: " + strResult);
                } else {
                    log("WARN", funcName + "(): " +
                            (strResult != null ? strResult : "符号未找到"));

                    // 尝试作为void函数调用
                    try {
                        NativeLoader.callVoidFunction(soHandle, funcName);
                        log("OK", funcName + "() 已执行 (void)");
                    } catch (Exception e) {
                        log("ERROR", "调用失败: " + e.getMessage());
                    }
                }
            });
        }).start();
    }

    // ==========================================
    //  内存 Dump
    // ==========================================
    private void dumpSo() {
        if (!isLoaded) {
            log("ERROR", "SO未加载,无法dump");
            return;
        }

        log("INFO", "═══ 开始内存 Dump ═══");

        new Thread(() -> {
            String timestamp = new SimpleDateFormat("HHmmss", Locale.US).format(new Date());

            // 1. 简单dump
            String simplePath = getFilesDir() + "/dump_raw_" + timestamp + "_" + soFileName;
            boolean simpleOk = NativeLoader.dumpSo(soFileName, simplePath);

            mainHandler.post(() -> {
                if (simpleOk) {
                    File f = new File(simplePath);
                    log("OK", "[原始Dump] 成功");
                    log("INFO", "  路径: " + simplePath);
                    log("INFO", "  大小: " + formatSize(f.length()));
                } else {
                    log("ERROR", "[原始Dump] 失败: " + NativeLoader.getLastError());
                }
            });

            // 2. 修复ELF后dump
            String fixedPath = getFilesDir() + "/dump_fixed_" + timestamp + "_" + soFileName;
            boolean fixedOk = NativeLoader.dumpAndFixSo(soFileName, fixedPath);

            mainHandler.post(() -> {
                if (fixedOk) {
                    File f = new File(fixedPath);
                    log("OK", "[ELF修复Dump] 成功");
                    log("INFO", "  路径: " + fixedPath);
                    log("INFO", "  大小: " + formatSize(f.length()));
                } else {
                    log("ERROR", "[ELF修复Dump] 失败: " + NativeLoader.getLastError());
                }
                log("INFO", "═══ Dump 完成 ═══");
            });

        }).start();
    }

    // ==========================================
    //  查看内存映射
    // ==========================================
    private void showMaps() {
        new Thread(() -> {
            String maps = NativeLoader.getMaps(isLoaded ? soFileName : "");
            mainHandler.post(() -> log("INFO", "\n" + maps));
        }).start();
    }

    // ==========================================
    //  卸载 SO
    // ==========================================
    private void unloadSo() {
        if (!isLoaded || soHandle == 0) {
            log("WARN", "没有已加载的SO");
            return;
        }

        new AlertDialog.Builder(this)
                .setTitle("确认卸载")
                .setMessage("确定要卸载 " + soFileName + " ?")
                .setPositiveButton("卸载", (d, w) -> {
                    boolean ok = NativeLoader.unloadSo(soHandle);
                    if (ok) {
                        log("OK", "SO 已卸载: " + soFileName);
                        soHandle = 0;
                        isLoaded = false;
                        tvLoadStatus.setText("⏹ 已卸载");
                        updateButtons(false);
                    } else {
                        log("ERROR", "卸载失败: " + NativeLoader.getLastError());
                    }
                })
                .setNegativeButton("取消", null)
                .show();
    }

    // ==========================================
    //  工具方法
    // ==========================================
    private void updateButtons(boolean loaded) {
        btnLoad.setEnabled(!loaded && !soLocalPath.isEmpty());
        btnCall.setEnabled(loaded);
        btnDump.setEnabled(loaded);
        btnMaps.setEnabled(true);
        btnUnload.setEnabled(loaded);
    }

    private void log(String level, String msg) {
        String time = new SimpleDateFormat("HH:mm:ss", Locale.US).format(new Date());
        String prefix;
        switch (level) {
            case "OK":    prefix = "✅"; break;
            case "ERROR": prefix = "❌"; break;
            case "WARN":  prefix = "⚠️"; break;
            default:      prefix = "ℹ️"; break;
        }

        String line = time + " " + prefix + " " + msg + "\n";

        runOnUiThread(() -> {
            tvLog.append(line);
            scrollLog.post(() -> scrollLog.fullScroll(ScrollView.FOCUS_DOWN));
        });
    }

    private String formatSize(long bytes) {
        if (bytes < 1024) return bytes + " B";
        if (bytes < 1024 * 1024) return String.format(Locale.US, "%.1f KB", bytes / 1024.0);
        return String.format(Locale.US, "%.2f MB", bytes / (1024.0 * 1024));
    }
}

6. CMakeLists.txt

cmake
cmake_minimum_required(VERSION 3.22.1)
project("soloader")

add_library(soloader SHARED
    native_loader.cpp
    memory_dumper.cpp
)

find_library(log-lib log)

target_link_libraries(soloader
    ${log-lib}
    dl          # dlopen/dlsym/dlclose
)

7. native_loader.cpp

C++
#include <jni.h>
#include <dlfcn.h>
#include <string>
#include <cstring>
#include <android/log.h>
#include <fstream>
#include <sstream>
#include <vector>
#include <elf.h>
#include <unistd.h>

#define TAG "NativeLoader"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)

static std::string g_lastError;

// ===================== loadSo =====================
extern "C" JNIEXPORT jlong JNICALL
Java_com_example_soloader_NativeLoader_loadSo(
        JNIEnv *env, jclass, jstring jPath) {

    const char *path = env->GetStringUTFChars(jPath, nullptr);
    LOGI("dlopen: %s", path);

    dlerror(); // 清除旧错误

    void *handle = dlopen(path, RTLD_NOW);
    if (!handle) {
        const char *err = dlerror();
        g_lastError = err ? err : "unknown dlopen error";
        LOGE("dlopen failed: %s", g_lastError.c_str());
        env->ReleaseStringUTFChars(jPath, path);
        return 0;
    }

    LOGI("dlopen OK, handle=%p", handle);
    env->ReleaseStringUTFChars(jPath, path);

    // constructor 函数会在 dlopen 内部自动执行
    // 不需要手动调用

    return reinterpret_cast<jlong>(handle);
}

// ===================== callFunction (返回字符串) =====================
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_soloader_NativeLoader_callFunction(
        JNIEnv *env, jclass, jlong handle, jstring jName) {

    if (!handle) return env->NewStringUTF("ERROR: null handle");

    const char *name = env->GetStringUTFChars(jName, nullptr);
    dlerror();

    typedef const char *(*func_t)();
    func_t fn = (func_t) dlsym(reinterpret_cast<void *>(handle), name);

    if (!fn) {
        const char *err = dlerror();
        g_lastError = err ? err : "symbol not found";
        std::string ret = "ERROR: " + g_lastError;
        env->ReleaseStringUTFChars(jName, name);
        return env->NewStringUTF(ret.c_str());
    }

    LOGI("Calling %s()", name);
    const char *result = fn();
    LOGI("%s() returned: %s", name, result ? result : "(null)");

    env->ReleaseStringUTFChars(jName, name);
    return env->NewStringUTF(result ? result : "(null)");
}

// ===================== callIntFunction =====================
extern "C" JNIEXPORT jint JNICALL
Java_com_example_soloader_NativeLoader_callIntFunction(
        JNIEnv *env, jclass, jlong handle, jstring jName, jint a, jint b) {

    if (!handle) return -1;

    const char *name = env->GetStringUTFChars(jName, nullptr);
    dlerror();

    typedef int (*func_t)(int, int);
    func_t fn = (func_t) dlsym(reinterpret_cast<void *>(handle), name);

    if (!fn) {
        g_lastError = dlerror() ?: "symbol not found";
        env->ReleaseStringUTFChars(jName, name);
        return -1;
    }

    int result = fn((int) a, (int) b);
    LOGI("%s(%d,%d) = %d", name, a, b, result);

    env->ReleaseStringUTFChars(jName, name);
    return result;
}

// ===================== callVoidFunction =====================
extern "C" JNIEXPORT void JNICALL
Java_com_example_soloader_NativeLoader_callVoidFunction(
        JNIEnv *env, jclass, jlong handle, jstring jName) {

    if (!handle) return;

    const char *name = env->GetStringUTFChars(jName, nullptr);
    dlerror();

    typedef void (*func_t)();
    func_t fn = (func_t) dlsym(reinterpret_cast<void *>(handle), name);

    if (fn) {
        LOGI("Calling void %s()", name);
        fn();
    } else {
        g_lastError = dlerror() ?: "symbol not found";
        LOGE("dlsym failed for %s: %s", name, g_lastError.c_str());
    }

    env->ReleaseStringUTFChars(jName, name);
}

// ===================== listSymbols(解析ELF导出符号)=====================
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_soloader_NativeLoader_listSymbols(
        JNIEnv *env, jclass, jlong handle, jstring jPath) {

    const char *path = env->GetStringUTFChars(jPath, nullptr);
    std::ostringstream oss;

    // 读取 ELF 文件解析动态符号表
    FILE *fp = fopen(path, "rb");
    if (!fp) {
        env->ReleaseStringUTFChars(jPath, path);
        return env->NewStringUTF("(无法读取文件)");
    }

    // 读取 ELF 标识
    unsigned char ident[EI_NIDENT];
    fread(ident, 1, EI_NIDENT, fp);

    if (ident[0] != 0x7f || ident[1] != 'E' || ident[2] != 'L' || ident[3] != 'F') {
        fclose(fp);
        env->ReleaseStringUTFChars(jPath, path);
        return env->NewStringUTF("(非ELF文件)");
    }

    bool is64 = (ident[EI_CLASS] == ELFCLASS64);
    fseek(fp, 0, SEEK_SET);

    std::vector<std::string> funcSymbols;

    if (is64) {
        Elf64_Ehdr ehdr;
        fread(&ehdr, sizeof(ehdr), 1, fp);

        // 遍历 section headers 查找 .dynsym 和 .dynstr
        for (int i = 0; i < ehdr.e_shnum; i++) {
            fseek(fp, ehdr.e_shoff + i * ehdr.e_shentsize, SEEK_SET);
            Elf64_Shdr shdr;
            fread(&shdr, sizeof(shdr), 1, fp);

            if (shdr.sh_type == SHT_DYNSYM) {
                // 找到对应的字符串表
                fseek(fp, ehdr.e_shoff + shdr.sh_link * ehdr.e_shentsize, SEEK_SET);
                Elf64_Shdr strShdr;
                fread(&strShdr, sizeof(strShdr), 1, fp);

                // 读取字符串表
                std::vector<char> strtab(strShdr.sh_size);
                fseek(fp, strShdr.sh_offset, SEEK_SET);
                fread(strtab.data(), 1, strShdr.sh_size, fp);

                // 读取符号表
                int symCount = shdr.sh_size / shdr.sh_entsize;
                fseek(fp, shdr.sh_offset, SEEK_SET);

                for (int j = 0; j < symCount; j++) {
                    Elf64_Sym sym;
                    fread(&sym, sizeof(sym), 1, fp);

                    if (sym.st_name != 0 && sym.st_value != 0) {
                        const char *name = strtab.data() + sym.st_name;
                        int type = ELF64_ST_TYPE(sym.st_info);
                        int bind = ELF64_ST_BIND(sym.st_info);

                        if (type == STT_FUNC && bind == STB_GLOBAL) {
                            funcSymbols.push_back(name);
                        }
                    }
                }
                break;
            }
        }
    } else {
        // 32-bit 类似处理
        Elf32_Ehdr ehdr;
        fread(&ehdr, sizeof(ehdr), 1, fp);

        for (int i = 0; i < ehdr.e_shnum; i++) {
            fseek(fp, ehdr.e_shoff + i * ehdr.e_shentsize, SEEK_SET);
            Elf32_Shdr shdr;
            fread(&shdr, sizeof(shdr), 1, fp);

            if (shdr.sh_type == SHT_DYNSYM) {
                fseek(fp, ehdr.e_shoff + shdr.sh_link * ehdr.e_shentsize, SEEK_SET);
                Elf32_Shdr strShdr;
                fread(&strShdr, sizeof(strShdr), 1, fp);

                std::vector<char> strtab(strShdr.sh_size);
                fseek(fp, strShdr.sh_offset, SEEK_SET);
                fread(strtab.data(), 1, strShdr.sh_size, fp);

                int symCount = shdr.sh_size / shdr.sh_entsize;
                fseek(fp, shdr.sh_offset, SEEK_SET);

                for (int j = 0; j < symCount; j++) {
                    Elf32_Sym sym;
                    fread(&sym, sizeof(sym), 1, fp);

                    if (sym.st_name != 0 && sym.st_value != 0) {
                        const char *name = strtab.data() + sym.st_name;
                        int type = ELF32_ST_TYPE(sym.st_info);
                        int bind = ELF32_ST_BIND(sym.st_info);

                        if (type == STT_FUNC && bind == STB_GLOBAL) {
                            funcSymbols.push_back(name);
                        }
                    }
                }
                break;
            }
        }
    }

    fclose(fp);

    if (funcSymbols.empty()) {
        oss << "(未找到导出函数符号)";
    } else {
        oss << "共 " << funcSymbols.size() << " 个导出函数:\n";
        for (size_t i = 0; i < funcSymbols.size(); i++) {
            oss << "  " << (i + 1) << ". " << funcSymbols[i] << "()\n";
        }
    }

    env->ReleaseStringUTFChars(jPath, path);
    return env->NewStringUTF(oss.str().c_str());
}

// ===================== unloadSo =====================
extern "C" JNIEXPORT jboolean JNICALL
Java_com_example_soloader_NativeLoader_unloadSo(
        JNIEnv *env, jclass, jlong handle) {

    if (!handle) return JNI_FALSE;

    dlerror();
    int ret = dlclose(reinterpret_cast<void *>(handle));
    if (ret != 0) {
        g_lastError = dlerror() ?: "dlclose failed";
        return JNI_FALSE;
    }

    LOGI("dlclose OK");
    return JNI_TRUE;
}

// ===================== getLastError =====================
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_soloader_NativeLoader_getLastError(JNIEnv *env, jclass) {
    return env->NewStringUTF(g_lastError.c_str());
}

8. memory_dumper.cpp

C++
#include <jni.h>
#include <string>
#include <fstream>
#include <sstream>
#include <vector>
#include <cstdio>
#include <cstring>
#include <cstdint>
#include <unistd.h>
#include <sys/mman.h>
#include <elf.h>
#include <android/log.h>

#define TAG "MemDumper"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)

extern std::string g_lastError; // 定义在 native_loader.cpp

struct MapRegion {
    uintptr_t start, end;
    char perms[5];
    size_t offset;
    char path[512];
};

static std::vector<MapRegion> readMaps(const char *filter) {
    std::vector<MapRegion> result;
    FILE *fp = fopen("/proc/self/maps", "r");
    if (!fp) return result;

    char line[1024];
    while (fgets(line, sizeof(line), fp)) {
        if (filter && filter[0] && !strstr(line, filter))
            continue;

        MapRegion r = {};
        unsigned long start, end, offset;
        int devMaj, devMin;
        unsigned long inode;

        int n = sscanf(line, "%lx-%lx %4s %lx %x:%x %lu %511s",
                       &start, &end, r.perms, &offset,
                       &devMaj, &devMin, &inode, r.path);
        if (n >= 7) {
            r.start = start;
            r.end = end;
            r.offset = offset;
            if (n < 8) r.path[0] = '\0';
            result.push_back(r);
        }
    }
    fclose(fp);
    return result;
}

// ===================== getBaseAddress =====================
extern "C" JNIEXPORT jlong JNICALL
Java_com_example_soloader_NativeLoader_getBaseAddress(
        JNIEnv *env, jclass, jstring jName) {

    const char *name = env->GetStringUTFChars(jName, nullptr);
    auto regions = readMaps(name);

    uintptr_t base = 0;
    for (auto &r : regions) {
        if (r.offset == 0) { base = r.start; break; }
    }
    if (!base && !regions.empty()) base = regions[0].start;

    env->ReleaseStringUTFChars(jName, name);
    return (jlong) base;
}

// ===================== getMaps =====================
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_soloader_NativeLoader_getMaps(
        JNIEnv *env, jclass, jstring jFilter) {

    const char *filter = env->GetStringUTFChars(jFilter, nullptr);
    auto regions = readMaps(filter);

    std::ostringstream oss;
    oss << "── /proc/self/maps ──\n";
    for (auto &r : regions) {
        char buf[256];
        snprintf(buf, sizeof(buf), "%012lx-%012lx %s off=%07lx %s\n",
                 (unsigned long) r.start, (unsigned long) r.end,
                 r.perms, (unsigned long) r.offset, r.path);
        oss << buf;
    }
    oss << "共 " << regions.size() << " 个区域\n";

    env->ReleaseStringUTFChars(jFilter, filter);
    return env->NewStringUTF(oss.str().c_str());
}

// ===================== dumpSo (原始dump) =====================
extern "C" JNIEXPORT jboolean JNICALL
Java_com_example_soloader_NativeLoader_dumpSo(
        JNIEnv *env, jclass, jstring jName, jstring jOut) {

    const char *name = env->GetStringUTFChars(jName, nullptr);
    const char *out = env->GetStringUTFChars(jOut, nullptr);

    auto regions = readMaps(name);
    if (regions.empty()) {
        g_lastError = "在maps中找不到 " + std::string(name);
        LOGE("%s", g_lastError.c_str());
        env->ReleaseStringUTFChars(jName, name);
        env->ReleaseStringUTFChars(jOut, out);
        return JNI_FALSE;
    }

    uintptr_t base = regions[0].start;
    uintptr_t tail = regions.back().end;
    size_t totalSize = tail - base;

    LOGI("Dump range: 0x%lx-0x%lx (%zu bytes)",
         (unsigned long) base, (unsigned long) tail, totalSize);

    FILE *fp = fopen(out, "wb");
    if (!fp) {
        g_lastError = "打开输出文件失败";
        env->ReleaseStringUTFChars(jName, name);
        env->ReleaseStringUTFChars(jOut, out);
        return JNI_FALSE;
    }

    uint8_t zeros[4096] = {};

    for (auto &r : regions) {
        size_t off = r.start - base;
        size_t sz = r.end - r.start;
        fseek(fp, off, SEEK_SET);

        bool readable = (r.perms[0] == 'r');
        if (readable) {
            fwrite((void *) r.start, 1, sz, fp);
        } else {
            // 尝试 mprotect
            uintptr_t page = r.start & ~0xFFFUL;
            int prot = 0;
            if (r.perms[1] == 'w') prot |= PROT_WRITE;
            if (r.perms[2] == 'x') prot |= PROT_EXEC;

            if (mprotect((void *) page, r.end - page, prot | PROT_READ) == 0) {
                fwrite((void *) r.start, 1, sz, fp);
                mprotect((void *) page, r.end - page, prot);
            } else {
                // 填零
                for (size_t rem = sz; rem > 0;) {
                    size_t w = rem > 4096 ? 4096 : rem;
                    fwrite(zeros, 1, w, fp);
                    rem -= w;
                }
            }
        }
    }

    fclose(fp);
    LOGI("Raw dump done → %s", out);

    env->ReleaseStringUTFChars(jName, name);
    env->ReleaseStringUTFChars(jOut, out);
    return JNI_TRUE;
}

// ===================== dumpAndFixSo (ELF修复dump) =====================
extern "C" JNIEXPORT jboolean JNICALL
Java_com_example_soloader_NativeLoader_dumpAndFixSo(
        JNIEnv *env, jclass, jstring jName, jstring jOut) {

    const char *name = env->GetStringUTFChars(jName, nullptr);
    const char *out = env->GetStringUTFChars(jOut, nullptr);

    auto regions = readMaps(name);
    if (regions.empty()) {
        g_lastError = "maps中找不到 " + std::string(name);
        env->ReleaseStringUTFChars(jName, name);
        env->ReleaseStringUTFChars(jOut, out);
        return JNI_FALSE;
    }

    uintptr_t base = 0;
    for (auto &r : regions) {
        if (r.offset == 0) { base = r.start; break; }
    }
    if (!base) base = regions[0].start;

    uintptr_t tail = regions.back().end;
    size_t totalSize = tail - base;

    // 验证 ELF
    auto *elf = (uint8_t *) base;
    if (elf[0] != 0x7f || elf[1] != 'E' || elf[2] != 'L' || elf[3] != 'F') {
        g_lastError = "基地址处非ELF";
        env->ReleaseStringUTFChars(jName, name);
        env->ReleaseStringUTFChars(jOut, out);
        return JNI_FALSE;
    }

    // 将内存拷贝到 buffer
    std::vector<uint8_t> buf(totalSize, 0);
    for (auto &r : regions) {
        if (r.perms[0] == 'r') {
            size_t off = r.start - base;
            size_t sz = r.end - r.start;
            memcpy(buf.data() + off, (void *) r.start, sz);
        }
    }

    bool is64 = (elf[4] == ELFCLASS64);

    // 修复 ELF
    if (is64) {
        auto *ehdr = (Elf64_Ehdr *) buf.data();
        // Section headers 在内存中通常已被清除
        if (ehdr->e_shoff >= totalSize) {
            ehdr->e_shoff = 0;
            ehdr->e_shnum = 0;
            ehdr->e_shstrndx = 0;
            ehdr->e_shentsize = 0;
        }

        auto *phdr = (Elf64_Phdr *) (buf.data() + ehdr->e_phoff);
        for (int i = 0; i < ehdr->e_phnum; i++) {
            if (phdr[i].p_type == PT_LOAD) {
                // 让 filesz = memsz(BSS 已在内存中初始化为0)
                if (phdr[i].p_filesz < phdr[i].p_memsz)
                    phdr[i].p_filesz = phdr[i].p_memsz;
            }
        }
    } else {
        auto *ehdr = (Elf32_Ehdr *) buf.data();
        if (ehdr->e_shoff >= totalSize) {
            ehdr->e_shoff = 0;
            ehdr->e_shnum = 0;
            ehdr->e_shstrndx = 0;
            ehdr->e_shentsize = 0;
        }
        auto *phdr = (Elf32_Phdr *) (buf.data() + ehdr->e_phoff);
        for (int i = 0; i < ehdr->e_phnum; i++) {
            if (phdr[i].p_type == PT_LOAD) {
                if (phdr[i].p_filesz < phdr[i].p_memsz)
                    phdr[i].p_filesz = phdr[i].p_memsz;
            }
        }
    }

    // 写文件
    FILE *fp = fopen(out, "wb");
    if (!fp) {
        g_lastError = "创建输出文件失败";
        env->ReleaseStringUTFChars(jName, name);
        env->ReleaseStringUTFChars(jOut, out);
        return JNI_FALSE;
    }

    fwrite(buf.data(), 1, buf.size(), fp);
    fclose(fp);
    LOGI("Fixed dump done → %s (%zu bytes)", out, buf.size());

    env->ReleaseStringUTFChars(jName, name);
    env->ReleaseStringUTFChars(jOut, out);
    return JNI_TRUE;
}

9. 测试用 payload.so 源码

C
// payload.c
// 编译: $NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang \
//       -shared -fPIC -o payload.so payload.c

#include <stdio.h>
#include <unistd.h>
#include <android/log.h>

#define LOG(...) __android_log_print(ANDROID_LOG_INFO, "Payload", __VA_ARGS__)

// SO加载时自动执行
__attribute__((constructor))
void auto_run() {
    LOG(">>> payload.so constructor called! pid=%d <<<", getpid());
}

// 导出函数们
const char* hello()       { return "Hello from payload!"; }
const char* get_version() { return "payload v2.0"; }
const char* get_info() {
    static char buf[128];
    snprintf(buf, sizeof(buf), "PID=%d, UID=%d", getpid(), getuid());
    return buf;
}
int add(int a, int b)      { return a + b; }
int multiply(int a, int b) { return a * b; }

完整使用流程

text
┌──────────────────────────────────────────────┐
│              APK 界面                        │
│                                              │
│  ① 点击 [📁 选择SO文件]                      │
│     └─ 弹出系统文件选择器                     │
│     └─ 选择手机上的 .so 文件                  │
│     └─ 自动复制到 /data/data/pkg/files/       │
│     └─ 自动验证 ELF 格式 + 架构匹配           │
│                                              │
│  ② 点击 [▶ 加载运行]                         │
│     └─ dlopen() 加载 SO                      │
│     └─ constructor 自动执行                   │
│     └─ 自动列出所有导出函数名                  │
│                                              │
│  ③ 输入函数名 → 点击 [调用]                   │
│     └─ dlsym() 找到函数                      │
│     └─ 调用并显示返回值                       │
│                                              │
│  ④ 点击 [💾 内存Dump]                        │
│     └─ 读取 /proc/self/maps                  │
│     └─ 从进程内存读取SO数据                   │
│     └─ 修复 ELF Header                       │
│     └─ 保存到文件                             │
│                                              │
│  ⑤ 点击 [⏹ 卸载] → dlclose()                │
└──────────────────────────────────────────────┘

关键点:用户通过系统文件选择器上传 SO → 自动复制到应用私有目录(绕过 Android 7.0+ 的 namespace 限制)→ dlopen 加载 → __attribute__((constructor)) 自动执行 → 用 dlsym 按名调用 → 通过 /proc/self/maps 定位内存并 dump。全过程不需要 root。

 

posted on 2026-02-11 09:08  GKLBB  阅读(20)  评论(0)    收藏  举报