应用安全 --- 安卓加固 之 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);