[原创]基础so注入的实现

[复制链接]
查看955 | 回复0 | 2025-7-25 15:39:33 | 显示全部楼层 |阅读模式

立即注册,突破游客限制,与技术大牛交流切磋,畅享海量资源,让你的技术之路不再孤单!

您需要 登录 才可以下载或查看,没有账号?立即注册

×
android平台的so注入
前言:由于安卓的进程隔离机制,我们在hook或操作其他进程时,往往需要先把so注入到目标进程
0x01 什么是so注入
一句话概括下来就是 把自己的so加载到目标进程的地址空间
0x02 主流的注入方式
据我所知,目前有两种:
  • 注入zygote进程
  • 通过ptrace直接注入目标进程
先来聊聊第一点吧,我们知道安卓中所有应用进程都fork自zygote进程,所以直接把so注入zygote进程,app在启动时会fork zygote,从而达到注入的目。那zygisk是如何注入进zygote进程的呢,其实也是通过ptrace哈哈哈。所以写zygisk模块可以轻松帮我们注入自己的so。同理,用xposed插件也可以,而且更简单。这里不细说,我们主要聊一聊第二种方式
0x03 如何使用ptrace注入so
既然要使用ptrace,那么我们就得先知道ptrace是什么东西
0x03.1 什么是ptrace
Ptrace是Linux提供的进程调试接口,可以让一个进程去控制另一个进程。通过ptrace,我们能附加到目标进程上,读写它的内存和寄存器。有了这些能力,远程调用目标进程的函数就成为可能了。
主要用到的ptrace操作包括:
PTRACE_ATTACH:附加目标进程
PTRACE_GETREGSET/SETREGSET:读写寄存器
PTRACE_PEEKDATA/POKEDATA:读写内存
0x03.2 使用ptrace注入so
知道了ptrace是干什么的,现在就能来注入so了。既然要把自己的so塞到目标进程里,那么直接远程调用对应进程的dlopen加载我们自己的so不就好了吗~。这就是我们的终极目标了
那我们就先来实现一下远程调用:



  1. bool ProcessUtils::SetupRemoteCall(RemoteCallContext* ctx, uint64_t func_addr,
  2.                                    const std::vector<uint64_t>& args) {
  3.     // 复制原始寄存器
  4.     ctx->regs = ctx->orig_regs;
  5.     // 设置栈指针 - 确保16字节对齐
  6.     ctx->regs.sp = (ctx->orig_regs.sp - 0x100) & ~0xF;
  7.     // 设置参数(ARM64前8个参数通过x0-x7传递)
  8.     for (size_t i = 0; i < args.size() && i < 8; i++) {
  9.         ctx->regs.regs[i] = args[i];
  10.     }
  11.     // 如果参数超过8个,需要压栈
  12.     if (args.size() > 8) {
  13.         MemoryUtils memory_utils;
  14.         uint64_t stack_addr = ctx->regs.sp;
  15.         for (size_t i = 8; i < args.size(); i++) {
  16.             if (!memory_utils.WriteProcessMemory(ctx->pid, stack_addr, &args[i], sizeof(uint64_t))) {
  17.                 LOGE("Failed to write stack argument %zu", i);
  18.                 return false;
  19.             }
  20.             stack_addr += sizeof(uint64_t);
  21.         }
  22.     }
  23.     // 设置PC指向目标函数
  24.     ctx->regs.pc = func_addr;
  25.     // 设置返回地址为0,这样函数返回时会触发SIGSEGV
  26.     ctx->regs.regs[30] = 0;  // x30是链接寄存器(LR)
  27.     LOGD("Setting up remote call: PC=0x%lx, SP=0x%lx", ctx->regs.pc, ctx->regs.sp);
  28.     return SetRegisters(ctx->pid, &ctx->regs);
  29. }
复制代码
原理就是通过设置pc(程序计数器)指向目标函数的地址。比如我们要远程调用dlopen,就得先拿到dlopen的地址,然后将参数写入对应寄存器,再修改pc指针指向dlopen的地址即可
现在远程调用是解决了,该解决dlopen的地址问题了:
我们知道,内存中的函数地址是由基地址(函数所在的so的起始地址)+偏移地址(函数相对于so的偏移)确定的,而同一个so的函数在不同进程里和在磁盘中的偏移是一样的。所以我们有两种办法拿到函数的真实地址(准确来说是相对于so的偏移地址)
  • 直接解析磁盘文件,比如dlopen函数,我们可以解析磁盘里的libc.so,找到dlopen的偏移,然后加上基地址,就是真实地址了,即 target_real_addr = target_base + offset
  • 在自己的进程中dlopen目标函数所在的so,然后使用dlsym查找,但是这里dlsym查找到的是对应函数在自己进程的绝对地址,所以需要额外的计算,即 target_real_addr = dlsym_res - my_base + target_base,这种方式就无需解析elf了,更简单实用
获取基地址就没啥好说的了,用户层直接读/proc/{pid}/maps就行了
  1. uint64_t ProcessUtils::GetModuleBase(pid_t pid, const std::string& module_name) {
  2.         char maps_path[256];
  3.         snprintf(maps_path, sizeof(maps_path), "/proc/%d/maps", pid);
  4.         LOGD("GetModuleBase: pid=%d, module=%s", pid, module_name.c_str());
  5.         std::ifstream maps(maps_path);
  6.         if (!maps.is_open()) {
  7.             LOGE("Failed to open %s: %s", maps_path, strerror(errno));
  8.             return 0;
  9.         }
  10.         std::string line;
  11.         bool found = false;
  12.         while (std::getline(maps, line)) {
  13.             if (line.find(module_name) != std::string::npos &&
  14.                 line.find(" r-xp ") != std::string::npos) {  // 只查找可执行段
  15.                 // 解析基址
  16.                 uint64_t base;
  17.                 if (sscanf(line.c_str(), "%lx", &base) == 1) {
  18.                     maps.close();
  19.                     LOGD("  Found module %s at base 0x%lx", module_name.c_str(), base);
  20.                     LOGD("  Map line: %s", line.c_str());
  21.                     return base;
  22.                 }
  23.             }
  24.         }
  25.         maps.close();
  26.         LOGD("  Module %s not found in process %d", module_name.c_str(), pid);
  27.         return 0;
  28.     }
复制代码
  1. uint64_t Injector::GetRemoteAddress(pid_t pid, const std::string& module_name,
  2.                                         const std::string& func_name) {
  3.         LOGD("GetRemoteAddress: module=%s, function=%s", module_name.c_str(), func_name.c_str());
  4.         // 获取目标进程中的模块基址
  5.         uint64_t remote_base = process_utils_.GetModuleBase(pid, module_name);
  6.         if (remote_base == 0) {
  7.             LOGD("  Module %s not found in process %d", module_name.c_str(), pid);
  8.             return 0;
  9.         }
  10.         // 获取本地进程中的模块基址
  11.         uint64_t local_base = process_utils_.GetModuleBase(getpid(), module_name);
  12.         if (local_base == 0) {
  13.             LOGD("  Module %s not found in local process", module_name.c_str());
  14.             // 对于loader模块,尝试动态加载
  15.             if (module_name.find("yuuki_transit") != std::string::npos ||
  16.                 module_name == LOADER_PATH) {
  17.                 LOGD("  Trying to load loader module locally to get function offset");
  18.                 void* handle = dlopen(LOADER_PATH, RTLD_NOW | RTLD_LOCAL);
  19.                 if (handle) {
  20.                     void* func = dlsym(handle, func_name.c_str());
  21.                     if (func) {
  22.                         // 再次获取本地基址
  23.                         local_base = process_utils_.GetModuleBase(getpid(), "yuuki_transit.so");
  24.                         if (local_base == 0) {
  25.                             local_base = process_utils_.GetModuleBase(getpid(), LOADER_PATH);
  26.                         }
  27.                         if (local_base != 0) {
  28.                             uint64_t offset = (uint64_t)func - local_base;
  29.                             uint64_t remote_addr = remote_base + offset;
  30.                             dlclose(handle);
  31.                             LOGD("  Found %s at offset 0x%lx, remote addr: 0x%lx",
  32.                                  func_name.c_str(), offset, remote_addr);
  33.                             return remote_addr;
  34.                         }
  35.                     }
  36.                     dlclose(handle);
  37.                 }
  38.             }
  39.             return 0;
  40.         }
复制代码
写入so路径:
值得注意的是,我们在使用dlopen的时候需要传入so的路径,这个值是一个字符串,更准确的来讲,dlopen接收到的字符串的首地址。所以我们需要远程把so的路径写入到目标进程中。我们可以借助ptrace的PTRACE_POKEDATA实现写入,在此之前,我们得获取一块稳定已知的可写内存,所以还需要远程调用一次mmap,远程调用函数的逻辑和上面一样,直接用就行

  1. bool MemoryUtils::WriteWord(pid_t pid, uint64_t addr, long value) {
  2.     if (ptrace(PTRACE_POKEDATA, pid, addr, value) == -1) {
  3.         LOGE("PTRACE_POKEDATA failed at 0x%lx: %s", addr, strerror(errno));
  4.         return false;
  5.     }
  6.     return true;
  7. }
复制代码
  1. bool MemoryUtils::WriteProcessMemory(pid_t pid, uint64_t addr, const void* buf, size_t size) {
  2.     LOGD("WriteProcessMemory: pid=%d, addr=0x%lx, size=%zu", pid, addr, size);
  3.     const uint8_t* src = (const uint8_t*)buf;
  4.     size_t remaining = size;
  5.     while (remaining > 0) {
  6.         size_t to_write = (remaining > sizeof(long)) ? sizeof(long) : remaining;
  7.         long data = 0;
  8.         if (to_write < sizeof(long)) {
  9.             // 需要先读取原始数据,保持未修改的字节
  10.             if (!ReadWord(pid, addr, &data)) {
  11.                 LOGE("  Failed to read original data at 0x%lx", addr);
  12.                 return false;
  13.             }
  14.         }
  15.         memcpy(&data, src, to_write);
  16.         if (!WriteWord(pid, addr, data)) {
  17.             LOGE("  Failed to write at 0x%lx", addr);
  18.             return false;
  19.         }
  20.         src += to_write;
  21.         addr += to_write;
  22.         remaining -= to_write;
  23.     }
  24.     LOGD("  Successfully wrote %zu bytes", size);
  25.     return true;
  26. }
复制代码
selinux模式切换:
这样主要的逻辑就全都实现了,剩下的就是处理selinux相关代码,通过修改/sys/fs/selinux/enforce文件的值实现enforce和permissive模式的切换
  1. bool SELinuxUtils::SetEnforcing() {
  2.     LOGI("Setting SELinux to enforcing mode");
  3.     return SetEnforceStatus(1);
  4. }
  5. bool SELinuxUtils::SetPermissive() {
  6.     LOGI("Setting SELinux to permissive mode");
  7.     return SetEnforceStatus(0);
  8. }
  9. int SELinuxUtils::GetEnforceStatus() {
  10.     LOGD("Checking SELinux enforce status...");
  11.     std::ifstream enforce_file("/sys/fs/selinux/enforce");
  12.     if (!enforce_file.is_open()) {
  13.         LOGD("  /sys/fs/selinux/enforce not found, trying old path...");
  14.         // 尝试旧路径
  15.         enforce_file.open("/selinux/enforce");
  16.         if (!enforce_file.is_open()) {
  17.             LOGD("  SELinux appears to be disabled");
  18.             return -1;
  19.         }
  20.     }
  21.     int status;
  22.     enforce_file >> status;
  23.     enforce_file.close();
  24.     LOGD("  SELinux enforce status: %d (%s)", status,
  25.          status == 1 ? "enforcing" : status == 0 ? "permissive" : "unknown");
  26.     return status;
  27. }
  28. bool SELinuxUtils::SetEnforceStatus(int status) {
  29.     // 需要root权限
  30.     int fd = open("/sys/fs/selinux/enforce", O_WRONLY);
  31.     if (fd < 0) {
  32.         // 尝试旧路径
  33.         fd = open("/selinux/enforce", O_WRONLY);
  34.         if (fd < 0) {
  35.             LOGE("Failed to open SELinux enforce file: %s", strerror(errno));
  36.             return false;
  37.         }
  38.     }
  39.     char status_str[2];
  40.     snprintf(status_str, sizeof(status_str), "%d", status);
  41.     ssize_t written = write(fd, status_str, 1);
  42.     close(fd);
  43.     if (written != 1) {
  44.         LOGE("Failed to write SELinux enforce status: %s", strerror(errno));
  45.         return false;
  46.     }
  47.     LOGI("SELinux enforce status set to %d", status);
  48.     return true;
  49. }
复制代码
0x04 小结
这只是实现了最简单最基础的attach模式so注入,会留下痕迹,虽然痕迹不多,但是都比较致命,基本上有检测的app注入进去就会挂掉。spawn模式下一次再介绍吧,至于隐藏痕迹,我感觉用户层也没啥好隐藏的,Memory Remapping依然会带来新的检测点,处理solist和maps里注入so的路径完全可以通过自定义linker来加载规避掉,但这会带来较差的兼容性和比较大的工程量。最简单的就是进到内核层操作seq_file来处理maps里的信息,但是我感觉只要路径正常,so名称随机,maps和solist不处理也没啥问题,但是dlopen只能加载那几个路径下的so哈哈哈哈



这个人还没有签名
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则