shadowhook 是一个 Android inline hook 库。 它的目标是:
- 稳定 - 可以稳定的用于 production app 中。
- 兼容 - 始终保持新版本 API 和 ABI 向后兼容。
- 性能 - 持续降低 API 调用耗时和 hook 后引入的额外运行时耗时。
- 功能 - 除了基本的 hook 功能以外,还提供“hook 引起或相关”问题的通用解决方案。
如果你需要的是 Android PLT hook 库,建议试试 ByteHook。
- 支持 armeabi-v7a 和 arm64-v8a。
- 支持 Android
4.1-16(API level16-36)。 - 支持 hook 和 intercept。
- 支持通过“地址”或“库名 + 函数名”指定 hook 和 intercept 的目标位置。
- 自动完成对“新加载 ELF”的 hook 和 intercept,执行完成后调用可选的回调函数。
- 自动避免代理函数之间形成的递归环形调用。
- 支持 hook 和 intercept 操作记录,操作记录可随时导出。
- 支持注册 linker 调用新加载 ELF 的
.init+.init_array和.fini+.fini_array前后的回调函数。 - 支持绕过 linker namespace 的限制,查询进程中所有 ELF 的
.dynsym和.symtab中的符号地址。 - 在 hook 代理函数和 intercept 拦截器函数中,兼容 CFI unwind 和 FP unwind。
- 使用 MIT 许可证授权。
Caution
下面的「快速开始」能让你的 DEMO 运行起来。但是 shadowhook 并不仅仅是几个 hook API 这么简单,想要在 production app 中稳定的使用 shadowhook,并且充分发挥它的能力,请务必阅读「shadowhook 手册」。
shadowhook 发布在 Maven Central 上。为了使用 native 依赖项,shadowhook 使用了从 Android Gradle Plugin 4.0 开始支持的 Prefab 包格式。
allprojects {
repositories {
mavenCentral()
}
}android {
buildFeatures {
prefab true
}
}
dependencies {
implementation 'com.bytedance.android:shadowhook:x.y.z'
}x.y.z 请替换成版本号,建议使用最新的 release 版本。
注意:shadowhook 使用 prefab package schema v2,它是从 Android Gradle Plugin 7.1.0 开始作为默认配置的。如果你使用的是 Android Gradle Plugin 7.1.0 之前的版本,请在 gradle.properties 中加入以下配置:
android.prefabVersion=2.0.0
CMakeLists.txt
find_package(shadowhook REQUIRED CONFIG)
add_library(mylib SHARED mylib.c)
target_link_libraries(mylib shadowhook::shadowhook)Android.mk
include $(CLEAR_VARS)
LOCAL_MODULE := mylib
LOCAL_SRC_FILES := mylib.c
LOCAL_SHARED_LIBRARIES += shadowhook
include $(BUILD_SHARED_LIBRARY)
$(call import-module,prefab/shadowhook)
android {
defaultConfig {
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
}
}shadowhook 包含两个 .so 文件:libshadowhook.so 和 libshadowhook_nothing.so。
如果你是在一个 SDK 工程里使用 shadowhook,你需要避免把 libshadowhook.so 和 libshadowhook_nothing.so 打包到你的 AAR 里,以免 app 工程打包时遇到重复的 .so 文件问题。
android {
packagingOptions {
exclude '**/libshadowhook.so'
exclude '**/libshadowhook_nothing.so'
}
}另一方面, 如果你是在一个 APP 工程里使用 shadowhook,你可能需要增加一些选项,用来处理重复的 .so 文件引起的冲突。但是,这可能会导致 APP 使用错误版本的 shadowhook。
android {
packagingOptions {
pickFirst '**/libshadowhook.so'
pickFirst '**/libshadowhook_nothing.so'
}
}shadowhook 支持三种模式(shared,multi,unique),你可以先试试 unique 模式。
import com.bytedance.shadowhook.ShadowHook;
public class MySdk {
public void init() {
ShadowHook.init(new ShadowHook.ConfigBuilder()
.setMode(ShadowHook.Mode.UNIQUE)
.build());
}
}hook 作用于函数整体。你需要编写一个代理函数,这个代理函数需要以和原函数同样的方式接收参数和传递返回值,这通常意味着代理函数需要定义成和原函数同样的类型(包括:参数个数、参数顺序、参数类型、返回值类型)。当 hook 成功后,执行到被 hook 的函数时,会先执行代理函数,在代理函数中你可以自己决定是否调用原函数。
举例:hook libart.so 中的 art::ArtMethod::Invoke() 函数。
void *orig = NULL;
void *stub = NULL;
// 被 hook 函数的类型定义
typedef void (*artmethod_invoke_func_type_t)(void *, void *, uint32_t *, uint32_t, void *, const char *);
// 代理函数
void artmethod_invoke_proxy(void *thiz, void *thread, uint32_t *args, uint32_t args_size, void *result, const char *shorty) {
// do something
((artmethod_invoke_func_type_t)orig)(thiz, thread, args, args_size, result, shorty);
// do something
}
void do_hook() {
stub = shadowhook_hook_sym_name(
"libart.so",
"_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc",
(void *)artmethod_invoke_proxy,
(void **)&orig);
if(stub == NULL) {
int err_num = shadowhook_get_errno();
const char *err_msg = shadowhook_to_errmsg(err_num);
LOG("hook error %d - %s", err_num, err_msg);
}
}
void do_unhook() {
int result = shadowhook_unhook(stub);
if(result != 0) {
int err_num = shadowhook_get_errno();
const char *err_msg = shadowhook_to_errmsg(err_num);
LOG("unhook error %d - %s", err_num, err_msg);
}
}_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc是art::ArtMethod::Invoke在 libart.so 中经过 C++ Name Mangler 处理后的函数符号名,可以使用 readelf 查看。C 函数没有 Name Mangler 的概念。art::ArtMethod::Invoke在 Android M 之前版本中的符号名有所不同,这个例子仅适用于 Android M 及之后的版本。如果要实现更好的 Android 版本兼容性,你需要自己处理函数符号名的差异。
intercept 作用于指令。可以是函数的第一条指令,也可以是函数中间的某条指令。你需要编写一个拦截器函数。当 intercept 成功后,执行到被 intercept 的指令时,会先执行拦截器函数。在拦截器函数中,你可以读取和修改寄存器的值。当拦截器函数返回后,会继续执行被 intercept 的指令。intercept 类似于调试器的断点调试功能。
举例:intercept libart.so 中的 art::ArtMethod::Invoke() 函数中的某条指令。
void *stub;
#if defined(__aarch64__)
void artmethod_invoke_interceptor(shadowhook_cpu_context_t *ctx, void *data) {
// 当 x19 等于 0 时,修改 x20 和 x21 的值
if (ctx->regs[19] == 0) {
ctx->regs[20] = 1;
ctx->regs[21] = 1000;
LOG("interceptor: found x19 == 0");
}
// 当 q0 等于 0 时,修改 q0,q1,q2,q3 的值
if (ctx->vregs[0].q == 0) {
ctx->vregs[0].q = 1;
ctx->vregs[1].q = 0;
ctx->vregs[2].q = 0;
ctx->vregs[3].q = 0;
LOG("interceptor: found q0 == 0");
}
}
void do_intercept(void) {
// 查询 art::ArtMethod::Invoke 的地址
void *handle = shadowhook_dlopen("libart.so");
if (handle == NULL) {
LOG("handle not found");
return;
}
void *sym_addr = shadowhook_dlsym(handle, "_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc");
shadowhook_dlclose(handle);
if (sym_addr == NULL) {
LOG("symbol not found");
return;
}
// 定位 art::ArtMethod::Invoke 中的某条指令的地址
void *instr_addr = (void *)((uintptr_t)sym_addr + 20);
stub = shadowhook_intercept_instr_addr(
instr_addr,
artmethod_invoke_interceptor,
NULL,
SHADOWHOOK_INTERCEPT_WITH_FPSIMD_READ_WRITE);
if(stub == NULL) {
int err_num = shadowhook_get_errno();
const char *err_msg = shadowhook_to_errmsg(err_num);
LOG("intercept failed: %d - %s", err_num, err_msg);
}
}
void do_unintercept() {
int result = shadowhook_unintercept(stub);
if (result != 0) {
int err_num = shadowhook_get_errno();
const char *err_msg = shadowhook_to_errmsg(err_num);
LOG("unintercept failed: %d - %s", err_num, err_msg);
}
}
#endif- 为了简化示例代码,这里的
instr_addr固定为sym_addr + 20。在真实的场景中,一般会结合内存扫描等手段来确定需要 intercept 的指令的地址。 - 由于 aarch32 和 aarch64 的寄存器不同,同一个函数的指令也不同,所以 intercept 逻辑一般需要分别编写。这里只包含了针对 aarch64 的示例代码。
ShadowHook 使用 MIT 许可证 授权。
ShadowHook 使用了以下第三方源码或库:
- queue.h
BSD 3-Clause License
Copyright (c) 1991, 1993 The Regents of the University of California. - tree.h
BSD 2-Clause License
Copyright (c) 2002 Niels Provos provos@citi.umich.edu - linux-syscall-support
BSD 3-Clause License
Copyright (c) 2005-2011 Google Inc. - xDL
MIT License
Copyright (c) 2020-2025 HexHacking Team