3015 words
15 minutes
注入篇二 spawn模式的实现

注入篇二 spawn模式的实现#

前言:在上一篇文章中大概解释了attach模式注入so的实现思路,但是实战中spawn模式注入也是很常用的

0x01 什么是spawn注入模式#

我的理解是 在目标进程启动时对其进行注入。这可以帮助我们绕过部分反调试,同时可以监听一些初始化函数

0x02 大致的实现思路#

spawn翻译过来是产卵的意思,刚接触frida的时候我不理解spawn这个单词和注入这个操作之间的联系,后来才知道这个操作是需要注入zygote的,而zygote翻译过来刚好是受精卵的意思,现在感觉还挺有意思的,spawn这个名字确实很恰当

我们想要在进程启动时就注入,那么时机很重要,这个时机当然越早越好…吗?我们知道app进程都是由zygote进程fork而来的,fork之后才会有app进程,这里差不多就是app进程刚出生的时候了,我们在这时,把自己的so注入就可以实现上述的效果了

0x03 开始实现#

由于我们要hook zygote进程里的一些函数,所以首先要把hook模块注入进zygote进程,关于如何通过ptrace注入目标进程,上一篇文章已经讲的比较清晰了,这里就不过多赘述了,这里先来讲讲hook模块应该实现哪些功能吧。这里的hook框架依旧是使用dobby

首先要明确hook点

上面说了,我们是hook fork函数,所以目标so是/system/lib64/libc.so,查找函数地址可以先dlopen libc.so,然后再dlsym fork函数,这里dlopen是为了拿到对应so的handle,正常进程里libc.so 99%是已经被加载过的,你再打开一次也无所谓,因为linker会直接返回solist里的handle给你,不会浪费很多时间。当然也可以使用elf_utils解析磁盘上的libc.so拿到偏移,然后查找基地址两个相加得到真实的地址

其次编写hook函数

static std::string getCurrentProcessName() {
char process_name[64] = {0};
int fd = open("/proc/self/cmdline", O_RDONLY);
if (fd != -1) {
ssize_t len = read(fd, process_name, sizeof(process_name) - 1);
if (len > 0) {
process_name[len] = '\0';
}
close(fd);
}
return std::string(process_name);
}
static pid_t hooked_fork() {
if (in_hook_fork) {
return orig_fork();
}
LOGD("enter fork func!");
pid_t result = orig_fork();
if (result == 0) {
LOGD("enter child process");
std::string current_process = getCurrentProcessName();
if (!current_process.empty()) {
LOGD("Child process started: %s", current_process.c_str());
//todo 然后判断进程名是否为目标进程,后续再dlopen自己的so
}
}
return result;
}

我刚开始是这么写的,我觉得没啥毛病,但是一运行就有问题了,各位可以先思考一下为什么这样不行哈哈哈哈

运行时输出了如下日志:

Terminal window
07-09 10:52:08.046 19197 19197 I [Zygote_Hook]: enter child process
07-09 10:52:08.047 19197 19197 I [Zygote_Hook]: Child process started: zygote64

子进程的进程名依然是zygote64,为什么会这样呢???其实是因为获取进程名的时机太早了,这时子进程还没初始化完全,所以获取到的名字还是父进程的

为了解决这个问题,我们就得先研究一下android中app进程的进程名是如何被设置的,查阅了一番资料后得知android.os.Process.setArgv0函数负责设置进程名,它是一个native函数,长这样

void android_os_Process_setArgV0(JNIEnv* env, jobject clazz, jstring name)
{
if (name == NULL) {
jniThrowNullPointerException(env, NULL);
return;
}
const jchar* str = env->GetStringCritical(name, 0);
String8 name8;
if (str) {
name8 = String8(reinterpret_cast<const char16_t*>(str),
env->GetStringLength(name));
env->ReleaseStringCritical(name, str);
}
if (!name8.empty()) {
AndroidRuntime::getRuntime()->setArgv0(name8.c_str(), true /* setProcName */);
}
}

它的第三个参数就是我们需要的进程名,所以我们可以在子进程中hook这个函数,当函数执行时取出参数,与我们的目标进程名作比较,如果一致,则加载我们自己的so。加载so使用dlopen就行了,但是dlopen只能从那几个路径加载,虽然本文不涉及注入痕迹的隐藏,但简单的能直接避免还是就避免一下吧。如果从app私有目录加载,那么app自己是有权限检查该路径下的文件的,不是很安全。如果从/system/lib下加载,那每次注入不同的so都需要重启手机重新挂载文件,这太麻烦了。所以还是从/data/local/tmp下加载吧,当然这个路径也不安全,但相对好一点。但如果选择这个路径,就会有新的问题。普通app进程是没办法访问这个目录的,所以dlopen肯定会失败。解决方案也比较简单,把selinux设置为宽容模式即可。那么新的问题又来了,hook代码是运行在app进程的,app进程没有权限设置selinux,这里只有Injector进程有这个权限,所以需要设计跨进程通信模块,用于在dlopen前后通知Injector模块修改selinux模式

设计通信模块

为了方便,本来想直接用信号的,因为这个通信场景本身也不复杂,但是后面写完才发现权限不够,于是又重写,这里直接用UDS。为了后续开发方便,就把模块单拎出来写了,另外两个模块共享通信模块

#include "socket_comm.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <thread>
#include <android/log.h>
#include <sys/stat.h>
#include <errno.h>
#define LOG_TAG "[Socket_Comm]"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
namespace comm {
SocketServer::SocketServer() : server_fd_(-1), running_(false), background_thread_(nullptr) {
}
SocketServer::~SocketServer() {
Stop();
}
bool SocketServer::Start() {
if (server_fd_ != -1) {
LOGE("Server already started");
return false;
}
unlink(COMM_SOCKET_PATH);
// 创建Unix Domain Socket
server_fd_ = socket(AF_UNIX, SOCK_STREAM, 0);
if (server_fd_ < 0) {
LOGE("Failed to create socket: %s", strerror(errno));
return false;
}
struct sockaddr_un server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
strncpy(server_addr.sun_path, COMM_SOCKET_PATH, sizeof(server_addr.sun_path) - 1);
if (bind(server_fd_, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
LOGE("Failed to bind socket: %s", strerror(errno));
close(server_fd_);
server_fd_ = -1;
return false;
}
// 设置socket文件权限,允许所有用户访问
chmod(COMM_SOCKET_PATH, 0666);
if (listen(server_fd_, 1) < 0) {
LOGE("Failed to listen on socket: %s", strerror(errno));
close(server_fd_);
server_fd_ = -1;
unlink(COMM_SOCKET_PATH);
return false;
}
running_ = true;
LOGI("Unix Domain Socket server started at %s", COMM_SOCKET_PATH);
return true;
}
void SocketServer::Stop() {
running_ = false;
if (server_fd_ != -1) {
shutdown(server_fd_, SHUT_RDWR);
close(server_fd_);
server_fd_ = -1;
}
unlink(COMM_SOCKET_PATH);
if (background_thread_) {
if (background_thread_->joinable()) {
background_thread_->join();
}
delete background_thread_;
background_thread_ = nullptr;
}
LOGI("Socket server stopped");
}
std::string SocketServer::WaitForMessage(int timeout_ms) {
if (server_fd_ == -1) {
LOGE("Server not started");
return "";
}
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(server_fd_, &readfds);
struct timeval* tv_ptr = nullptr;
struct timeval tv;
if (timeout_ms >= 0) {
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
tv_ptr = &tv;
}
int ret = select(server_fd_ + 1, &readfds, nullptr, nullptr, tv_ptr);
if (ret <= 0) {
if (ret < 0 && errno != EINTR) {
LOGE("select error: %s", strerror(errno));
}
return "";
}
struct sockaddr_un client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd_, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
if (errno != EINTR && errno != EAGAIN) {
LOGE("Failed to accept connection: %s", strerror(errno));
}
return "";
}
char buffer[256] = {0};
ssize_t bytes_read = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
close(client_fd);
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
LOGI("Received message: %s", buffer);
return std::string(buffer);
}
return "";
}
void SocketServer::RunInBackground(std::function<void(const std::string&)> callback) {
if (background_thread_) {
LOGE("Background thread already running");
return;
}
background_thread_ = new std::thread(&SocketServer::AcceptLoop, this, callback);
}
void SocketServer::AcceptLoop(std::function<void(const std::string&)> callback) {
LOGI("Background accept loop started");
while (running_) {
std::string msg = WaitForMessage(1000);
if (!msg.empty() && callback) {
callback(msg);
}
}
LOGI("Background accept loop ended");
}
bool SocketClient::SendMessage(const std::string& message) {
int client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (client_fd < 0) {
LOGE("Failed to create client socket: %s", strerror(errno));
return false;
}
struct sockaddr_un server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
strncpy(server_addr.sun_path, COMM_SOCKET_PATH, sizeof(server_addr.sun_path) - 1);
if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
LOGE("Failed to connect to server: %s (path: %s)", strerror(errno), COMM_SOCKET_PATH);
close(client_fd);
return false;
}
ssize_t bytes_sent = send(client_fd, message.c_str(), message.length(), 0);
close(client_fd);
if (bytes_sent < 0) {
LOGE("Failed to send message: %s", strerror(errno));
return false;
}
LOGI("Message sent: %s", message.c_str());
return true;
}
}

这样就解决权限的问题了:) 是不是就可以加载so了呢,直接dlopen(SO_PATH, FLAG),那么SO_PATH从哪来?当然完善上面的跨进程通信模块,也能把path传过来,但是有点麻烦了,这里我的解决方案是在hook模块里导出一个函数用于被Injector模块远程调用,参数里传递一些必要的信息,比如SO_PATH

// 全局变量存储目标信息
static char g_target_process_name[256] = {0};
static char g_target_library_path[512] = {0};
extern "C" void setTargetInfo(char* target_process_name, char* target_library_path) {
strcpy(g_target_process_name, target_process_name);
strcpy(g_target_library_path, target_library_path);
LOGI("Target process name: %s", g_target_process_name);
LOGI("Target library path: %s", g_target_library_path);
}

总的代码就是这样子的

#include <sys/mman.h>
#include <dlfcn.h>
#include <sys/types.h>
#include <string>
#include <jni.h>
#include "utils.h"
#include "../ipc/socket_comm.h"
#define LIBC "/system/lib64/libc.so"
#define LIBANDROID_RUNTIME "/system/lib64/libandroid_runtime.so"
// 全局变量存储目标信息
static char g_target_process_name[256] = {0};
static char g_target_library_path[512] = {0};
typedef pid_t (*Fork_t)();
static Fork_t orig_fork = nullptr;
void* fork_addr = nullptr;
typedef void (*SetArgV0_t)(JNIEnv*, jobject, jstring);
static SetArgV0_t orig_setArgV0 = nullptr;
void* setArgV0_addr = nullptr;
static volatile int* inject_flag = nullptr;
static pid_t g_zygote_pid = 0;
static bool g_injection_done = false;
void unhook() {
if(fork_addr)
DobbyDestroy(fork_addr);
if(setArgV0_addr)
DobbyDestroy(setArgV0_addr);
LOGI("All hooks removed");
}
extern "C" void cleanup_hooks() {
unhook();
if (inject_flag && inject_flag != MAP_FAILED) {
LOGI("Cleaning up shared memory");
munmap((void*)inject_flag, sizeof(int));
inject_flag = nullptr;
}
memset(g_target_process_name, 0, sizeof(g_target_process_name));
memset(g_target_library_path, 0, sizeof(g_target_library_path));
g_injection_done = false;
LOGI("cleanup_hooks completed");
}
static void hook_setArgV0(JNIEnv* env, jobject clazz, jstring name) {
std::string process_name;
if (name && env) {
const char* name_utf = env->GetStringUTFChars(name, nullptr);
if (name_utf) {
process_name = name_utf;
env->ReleaseStringUTFChars(name, name_utf);
LOGD("Process name: %s", process_name.c_str());
}
}
if (!process_name.empty() && process_name == g_target_process_name && !g_injection_done) {
// 标记已完成,避免重复注入
g_injection_done = true;
// 在子进程中 unhook
if(setArgV0_addr) {
DobbyDestroy(setArgV0_addr);
setArgV0_addr = nullptr;
}
LOGI("Target process detected: %s, injecting library: %s",
process_name.c_str(), g_target_library_path);
void* handle = dlopen(g_target_library_path, RTLD_NOW | RTLD_NODELETE | RTLD_GLOBAL);
bool inject_success = (handle != nullptr);
if (inject_success) {
LOGI("Successfully loaded library: %s", g_target_library_path);
comm::SocketClient::NotifySuccess();
} else {
LOGE("Failed to load library: %s, error: %s",
g_target_library_path, dlerror());
comm::SocketClient::NotifyFailed();
}
if (inject_flag && inject_flag != MAP_FAILED) {
*inject_flag = 1;
LOGI("Set inject_flag to 1 in shared memory");
}
}
if (orig_setArgV0) {
orig_setArgV0(env, clazz, name);
}
}
static void setup_setArgV0_hook() {
void* handle = dlopen(LIBANDROID_RUNTIME, RTLD_NOW);
if (!handle) {
LOGE("Failed to open libandroid_runtime.so: %s", dlerror());
return;
}
const char* symbol = "_Z27android_os_Process_setArgV0P7_JNIEnvP8_jobjectP8_jstring";
setArgV0_addr = dlsym(handle, symbol);
if (!setArgV0_addr) {
LOGE("Failed to find %s: %s", symbol, dlerror());
return;
}
LOGI("Found %s at %p", symbol, setArgV0_addr);
orig_setArgV0 = (SetArgV0_t)InlineHook(setArgV0_addr, (void*)hook_setArgV0);
if (orig_setArgV0) {
LOGI("Successfully hooked %s", symbol);
} else {
LOGE("Failed to hook %s", symbol);
}
}
static pid_t hook_fork() {
LOGD("enter fork func!");
pid_t result = orig_fork();
if (result == 0) {
LOGD("enter child process");
setup_setArgV0_hook();
} else if (result > 0) {
if (getpid() == g_zygote_pid && inject_flag && inject_flag != MAP_FAILED && *inject_flag == 1) {
LOGI("Injection completed in child process, unhooking fork in parent");
if (fork_addr) {
DobbyDestroy(fork_addr);
fork_addr = nullptr;
}
munmap((void*)inject_flag, sizeof(int));
inject_flag = nullptr;
}
}
return result;
}
void setup_fork_hook() {
void* handle = dlopen(LIBC, RTLD_NOW);
if (!handle) {
LOGE("Failed to open libc.so");
return;
}
const char* fork_symbol = "fork";
fork_addr = dlsym(handle, fork_symbol);
if (!fork_addr) {
LOGE("Failed to find fork");
return;
}
LOGI("Found fork at %p", fork_addr);
orig_fork = (Fork_t)InlineHook(fork_addr, (void*)hook_fork);
if (orig_fork) {
LOGI("Successfully hooked fork");
} else {
LOGE("Failed to hook fork");
}
}
extern "C" void setTargetInfo(char* target_process_name, char* target_library_path) {
strcpy(g_target_process_name, target_process_name);
strcpy(g_target_library_path, target_library_path);
LOGI("Target process name: %s", g_target_process_name);
LOGI("Target library path: %s", g_target_library_path);
g_injection_done = false;
if (inject_flag && inject_flag != MAP_FAILED) {
*inject_flag = 0;
LOGI("Reset inject_flag to 0");
}
if (fork_addr == nullptr && orig_fork == nullptr) {
LOGI("Re-hooking fork for new injection");
setup_fork_hook();
}
}
__attribute__((constructor))
void initialize_hook() {
LOGI("Zygote hook module loaded");
g_zygote_pid = getpid();
inject_flag = (volatile int*)mmap(NULL, sizeof(int),
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (inject_flag != MAP_FAILED) {
*inject_flag = 0;
LOGI("Created shared memory for inject_flag");
} else {
LOGE("Failed to create shared memory");
inject_flag = nullptr;
}
setup_fork_hook();
}
__attribute__((destructor))
void cleanup_hook() {
LOGD("Cleaning up hooks in destructor");
cleanup_hooks();
}

自己写时记得及时unhook,不然有的软件会进不去

0x04 小结#

在学校时,用完frida之后去食堂吃饭,付钱的软件老是打不开,当时不明白为什么我明明调试的是别的app,这个app凭什么打不开,其实是用完之后zygote进程里的一些函数没有被unhook,app启动会fork zygote进程,所以就被检测到了:(

再来聊聊检测和过检测吧,就说我写的这个工具吧,如果不开源,可能就是maps里有可疑路径的so,soinfo list里也会有,/proc/self/fd/ 中的文件描述符,还有就是通信时的痕迹,别的地方感觉也没啥了(注入留下的痕迹,hook的不归我管)。前面两个,目前用户层有的解决方案就是:

  • 遍历 /proc/<pid>/maps,找到目标库的所有内存段
  • 对每个段,远程分配匿名内存、远程 memcpy 覆盖内容、调用 mremap 将匿名内存映射到原来的地址
  • 最后还原原有的内存保护权限

但这样还是解决不了问题的,因为正常app基本没用可执行的匿名内存段,但是一些加固之后的似乎有,我之前看36O是有的

solist里的痕迹就是通过找到solist_remove_soinfo函数的地址,然后调用它,移除目标so的soinfo,当然这里还涉及获取目标so的soinfo,遍历solist等操作,不赘述了

使用自定义linker的方案可以在so的路径上多一些选择,而且不会在solist留下痕迹,但也并非一点痕迹没有的:( 所以还是配合内核模块一起用吧

注入篇二 spawn模式的实现
https://yuuki.cool/posts/injectso2/
Author
Yuuki
Published at
2025-07-11
License
CC BY-NC-SA 4.0