开发一个无root的Android SO动态加载与内存Dump工具
我将为你开发一个完整的Android APK,支持图形化选择SO文件、动态加载到内存、以及内存dump到本地文件。
项目结构
text
SOLoader/
├── app/
│ ├── src/main/
│ │ ├── java/com/soloader/
│ │ │ ├── MainActivity.java
│ │ │ ├── SOFileAdapter.java
│ │ │ ├── SOLoaderEngine.java
│ │ │ ├── MemoryDumper.java
│ │ │ ├── LoadedSOInfo.java
│ │ │ ├── LoadedSOAdapter.java
│ │ │ └── FilePickerActivity.java
│ │ ├── jni/
│ │ │ ├── Android.mk
│ │ │ ├── Application.mk
│ │ │ └── native_loader.c
│ │ ├── res/
│ │ │ ├── layout/
│ │ │ │ ├── activity_main.xml
│ │ │ │ ├── activity_file_picker.xml
│ │ │ │ ├── item_so_file.xml
│ │ │ │ ├── item_loaded_so.xml
│ │ │ │ └── dialog_memory_info.xml
│ │ │ ├── values/
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── styles.xml
│ │ │ └── drawable/
│ │ │ ├── bg_card.xml
│ │ │ ├── bg_button_primary.xml
│ │ │ ├── bg_button_danger.xml
│ │ │ └── bg_button_secondary.xml
│ │ └── AndroidManifest.xml
│ └── build.gradle
├── build.gradle
└── settings.gradle
1. AndroidManifest.xml
XML
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.soloader">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".FilePickerActivity"
android:theme="@style/AppTheme" />
</application>
</manifest>
2. build.gradle (app)
groovy
plugins {
id 'com.android.application'
}
android {
compileSdkVersion 33
defaultConfig {
applicationId "com.soloader"
minSdkVersion 24
targetSdkVersion 33
versionCode 1
versionName "1.0"
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'
}
}
buildTypes {
release {
minifyEnabled false
}
}
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.recyclerview:recyclerview:1.3.1'
implementation 'androidx.cardview:cardview:1.0.0'
}
3. JNI Native代码
jni/Android.mk
Makefile
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := native_loader
LOCAL_SRC_FILES := native_loader.c
LOCAL_LDLIBS := -llog -ldl
LOCAL_CFLAGS := -Wall -Wextra
include $(BUILD_SHARED_LIBRARY)
jni/Application.mk
Makefile
APP_ABI := arm64-v8a armeabi-v7a x86 x86_64
APP_PLATFORM := android-24
APP_STL := c++_static
jni/native_loader.c
C
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mman.h>
#include <android/log.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__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define MAX_LOADED_LIBS 64
typedef struct {
void* handle;
char path[512];
unsigned long base_addr;
unsigned long size;
int is_loaded;
} LoadedLib;
static LoadedLib loaded_libs[MAX_LOADED_LIBS];
static int lib_count = 0;
// 从 /proc/self/maps 中解析SO的基地址和大小
static int find_lib_in_maps(const char* lib_name, unsigned long* base, unsigned long* end) {
FILE* fp = fopen("/proc/self/maps", "r");
if (!fp) {
LOGE("Cannot open /proc/self/maps: %s", strerror(errno));
return -1;
}
char line[1024];
*base = 0;
*end = 0;
int found = 0;
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, lib_name)) {
unsigned long start, e;
if (sscanf(line, "%lx-%lx", &start, &e) == 2) {
if (!found) {
*base = start;
found = 1;
}
*end = e; // 持续更新end以获取最大范围
}
}
}
fclose(fp);
if (found) {
LOGI("Found %s: base=0x%lx, end=0x%lx, size=%lu",
lib_name, *base, *end, *end - *base);
return 0;
}
return -1;
}
// 加载SO文件
JNIEXPORT jlong JNICALL
Java_com_soloader_SOLoaderEngine_nativeLoadSO(JNIEnv* env, jobject thiz, jstring path) {
const char* lib_path = (*env)->GetStringUTFChars(env, path, NULL);
if (!lib_path) {
LOGE("Failed to get path string");
return 0;
}
LOGI("Loading SO: %s", lib_path);
// 使用 dlopen 加载
void* handle = dlopen(lib_path, RTLD_NOW);
if (!handle) {
LOGE("dlopen failed: %s", dlerror());
(*env)->ReleaseStringUTFChars(env, path, lib_path);
return 0;
}
LOGI("SO loaded successfully, handle=%p", handle);
// 查找基地址
unsigned long base = 0, end = 0;
// 提取文件名用于在maps中搜索
const char* filename = strrchr(lib_path, '/');
if (filename) {
filename++;
} else {
filename = lib_path;
}
if (find_lib_in_maps(filename, &base, &end) == 0) {
if (lib_count < MAX_LOADED_LIBS) {
loaded_libs[lib_count].handle = handle;
strncpy(loaded_libs[lib_count].path, lib_path, sizeof(loaded_libs[lib_count].path) - 1);
loaded_libs[lib_count].base_addr = base;
loaded_libs[lib_count].size = end - base;
loaded_libs[lib_count].is_loaded = 1;
lib_count++;
}
}
(*env)->ReleaseStringUTFChars(env, path, lib_path);
return (jlong)(uintptr_t)handle;
}
// 卸载SO文件
JNIEXPORT jboolean JNICALL
Java_com_soloader_SOLoaderEngine_nativeUnloadSO(JNIEnv* env, jobject thiz, jlong handle) {
void* h = (void*)(uintptr_t)handle;
for (int i = 0; i < lib_count; i++) {
if (loaded_libs[i].handle == h && loaded_libs[i].is_loaded) {
int ret = dlclose(h);
if (ret == 0) {
loaded_libs[i].is_loaded = 0;
LOGI("SO unloaded: %s", loaded_libs[i].path);
return JNI_TRUE;
} else {
LOGE("dlclose failed: %s", dlerror());
return JNI_FALSE;
}
}
}
// 如果没在记录中找到,直接尝试关闭
int ret = dlclose(h);
return ret == 0 ? JNI_TRUE : JNI_FALSE;
}
// 获取SO的基地址
JNIEXPORT jlong JNICALL
Java_com_soloader_SOLoaderEngine_nativeGetBaseAddress(JNIEnv* env, jobject thiz, jlong handle) {
void* h = (void*)(uintptr_t)handle;
for (int i = 0; i < lib_count; i++) {
if (loaded_libs[i].handle == h && loaded_libs[i].is_loaded) {
return (jlong)loaded_libs[i].base_addr;
}
}
return 0;
}
// 获取SO的内存大小
JNIEXPORT jlong JNICALL
Java_com_soloader_SOLoaderEngine_nativeGetSOSize(JNIEnv* env, jobject thiz, jlong handle) {
void* h = (void*)(uintptr_t)handle;
for (int i = 0; i < lib_count; i++) {
if (loaded_libs[i].handle == h && loaded_libs[i].is_loaded) {
return (jlong)loaded_libs[i].size;
}
}
return 0;
}
// Dump内存到文件
JNIEXPORT jboolean JNICALL
Java_com_soloader_MemoryDumper_nativeDumpMemory(JNIEnv* env, jobject thiz,
jlong address, jlong size, jstring output_path) {
const char* out_path = (*env)->GetStringUTFChars(env, output_path, NULL);
if (!out_path) {
LOGE("Failed to get output path");
return JNI_FALSE;
}
unsigned long addr = (unsigned long)address;
unsigned long dump_size = (unsigned long)size;
LOGI("Dumping memory: addr=0x%lx, size=%lu, output=%s", addr, dump_size, out_path);
FILE* fp = fopen(out_path, "wb");
if (!fp) {
LOGE("Cannot create output file: %s, error: %s", out_path, strerror(errno));
(*env)->ReleaseStringUTFChars(env, output_path, out_path);
return JNI_FALSE;
}
// 逐页读取,跳过不可读的页
unsigned long page_size = sysconf(_SC_PAGESIZE);
unsigned long current = addr;
unsigned long remaining = dump_size;
unsigned long written = 0;
unsigned long skipped = 0;
while (remaining > 0) {
unsigned long chunk = remaining < page_size ? remaining : page_size;
// 检查内存是否可读 - 通过尝试读取 mincore 或直接尝试
// 使用 mincore 检查页面是否映射
unsigned char vec;
unsigned long aligned_addr = current & ~(page_size - 1);
// 尝试直接写入,如果失败则写入零
// 使用信号处理来捕获 SIGSEGV 太复杂,改用 /proc/self/mem
FILE* mem_fp = fopen("/proc/self/mem", "rb");
if (mem_fp) {
if (fseeko(mem_fp, (off_t)current, SEEK_SET) == 0) {
unsigned char* buf = (unsigned char*)malloc(chunk);
if (buf) {
size_t read_bytes = fread(buf, 1, chunk, mem_fp);
if (read_bytes == chunk) {
fwrite(buf, 1, chunk, fp);
written += chunk;
} else {
// 不可读,写零
memset(buf, 0, chunk);
fwrite(buf, 1, chunk, fp);
skipped += chunk;
}
free(buf);
}
}
fclose(mem_fp);
}
current += chunk;
remaining -= chunk;
}
fclose(fp);
LOGI("Dump complete: written=%lu, skipped=%lu", written, skipped);
(*env)->ReleaseStringUTFChars(env, output_path, out_path);
return JNI_TRUE;
}
// Dump指定SO的内存
JNIEXPORT jboolean JNICALL
Java_com_soloader_MemoryDumper_nativeDumpSO(JNIEnv* env, jobject thiz,
jlong handle, jstring output_path) {
void* h = (void*)(uintptr_t)handle;
for (int i = 0; i < lib_count; i++) {
if (loaded_libs[i].handle == h && loaded_libs[i].is_loaded) {
return Java_com_soloader_MemoryDumper_nativeDumpMemory(