
做 Android 开发或性能分析时我们经常遇到一种“想改却改不了”的情况某个功能写在一个老旧 SDK 里源码早就丢了某个第三方库的返回值不符合预期又等不到厂商更新或者你要做兼容性测试需要临时观察一个方法的调用频率、参数和返回值。这些场景下最直接的手段不是去改源码重新编译而是在运行时把方法“截住”插入自己的逻辑再把执行权交回去。这就是 Android Hook。我的判断是Hook 并不是 Android 开发中的黑科技也不只是逆向工程师的专属技能。它本质上是“运行时控制流改造”是理解 ART 虚拟机、动态链接、内存执行权限这些底层机制的一把钥匙。如果你只会照着网上的片段抄一遍却不知道 Hook 到底发生在哪一层、为什么能生效、为什么有的方案需要 root、有的只需要改构建配置那你遇到问题会很难排错。本文希望给你一条相对完整的 Android Hook 技术路线从基础概念到 Java 层与 Native 层的路线对比再到可运行的 Frida 和 Xposed/LSPosed 示例代码最后是验证方法和常见坑。读完以后你至少能独立跑通一个 Hook 实验并能判断一个 Hook 需求应该选哪条路线。需要提前说清楚本文讨论 Hook 的目的是自有应用调试、SDK 兼容性分析、自动化测试和合法授权范围内的安全研究而不是绕过应用保护、窃取数据或做外挂行为。后面会有专门的安全边界提醒但请从始至终保持这个意识。1. 这篇文章真正要解决的问题1.1 三种常见的开发痛点先说一个经典场景。你的 App 接入了某个三方 SDK这个 SDK 内部有一个很关键的方法一直返回错误结果。你没有源码也无法让厂商立刻修复。这时你希望运行时拦截这个方法查看它的参数、返回值甚至暂时把它“替换”成一段自己写的逻辑让业务流程先跑通。另一个场景发生在 SDK 兼容分析中。你负责维护一个运行在 Android 11 到 Android 15 设备上的 App某次闪退出现在 .so 库里但日志只有 segment fault。此时你想在内存层面观察 native 函数被调用时到底传入了什么路径或句柄定位是否访问了受限目录。这已经属于 Native 层 Hook 的范畴。还有一个场景更日常。团队在做自动化测试测试用例需要伪造某些外部环境。比如模拟弱网时你希望 App 里某个网络状态判断方法直接返回false。与其为了测试维护一份内部测试分支代码不如通过 Hook 在运行时注入返回值。这三个场景的共同点是程序已经编译好了没法改源码但你需要对程序行为做临时性、可控的修改。Android Hook 技术就是为解决这类问题而存在的。1.2 一个明确判断选路线比选工具更重要我见过很多初学者一上来就问“Frida 好还是 Xposed 好”这其实问错了。更好的问法是我 Hook 的对象是 Java/Kotlin 方法还是 Native 函数我是否拥有完整权限的测试机我是想动态调试某个 App还是要做一个常驻模块在系统启动或 App 启动时自动生效判断一旦清晰工具自然就清晰了。如果目标是一个只有少量测试场景的动态分析Frida 往往是最高效的选择如果目标是做长期模块比如让某个策略批量生效那么 Xposed 体系会更合适如果目标是分析 own native lib behavior, 需要 native inline hook or PLT/GOT hook.整体来看Android Hook 技术路线可以分成两条主干Java 层 Hook针对 ART 虚拟机中的 ArtMethod。Native 层 Hook针对动态库或进程内存中的机器码。Frida、Xposed、LSPosed、xHook、Dobby 这些新名词的出现不过是在这两条主干上叠了不同的工作模式和使用场景。读到这里你应该知道自己至少要弄懂 ArtMethod、动态符号解析、inline hook 这三个概念。这是后续所有实操的地基。2. Android Hook 的核心概念与分层2.1 Java 层 Hook 的“真身”是 ArtMethodAndroid 应用层代码大多跑在 ART 虚拟机上。Java/Kotlin 方法在运行时并不是一个简单的 C 函数指针而是对应 ART 虚拟机里的一个ArtMethod对象。当你看到一行代码调用obj.getToken()时ART 会先找到这个类对应的 ArtMethod再通过它内部记录的 entry point 进入执行逻辑。Java 层 Hook 的基本原理就是改变这个入口或者调用链让你注册的回调先执行再决定要不要执行原本的getToken()。很多资料用“替换方法”“拦截方法”来形容 Hook听起来很抽象。实际做一次最小实验你就会发现流程非常像操作系统里的拦截器在方法调用前插一个切面在方法返回后插一个切面。这个想法和 AOP 切面编程很像区别在于 AOP 发生在编译期或字节码处理期而 Hook 通常发生在进程运行期。2.2 Java 层与 Native 层的界线要特别记住一点Java/Kotlin 方法调用最终也会进入 Native 代码但你不能因为这一层关系就跳过框架直接去 hookart_quick_generic_jni_trampoline。对绝大多数人和项目来说先掌握 Java 层的可见方法 Hook 就足够了比如方法参数、返回值、异常、调用次数。这类需求用上层 Hook 框架会高效很多。真正需要进入 Native 层的情况是你面对的是.so里的导出函数或像open、read、connect这类 libc 函数。Android App 的文件读写、网络建连最终都会走到这些 Native 调用。此时你不能再用“Java 类名 方法名”定位而要用“模块 导出符号”或“内存地址”定位。这就是 Native Hook 和其他 Native Hookinline hook、PLT/GOT hook的用武之地。2.3 inline hook 和符号 Hook 到底在做什么Native 层 Hook 最常听到的两种实现是 inline hook 和 PLT/GOT hook。inline hook 的核心是改写目标函数机器码开头几条指令。它会把原来的指令替换成一条跳转指令让执行流跳到我们自己写的 stub 中。stub 里会保存现场、执行 Hook 回调、恢复现场并把原始指令执行完再跳回原函数。这个名字里的 inline 意思是“内联补丁”你直接改了函数体。PLT/GOT hook 则不是改函数体而是改动态链接表。Android 上很多.so是通过动态链接调用其他模块函数的例如open可能通过 GOT 表跳到 libc。Hook 框架把 GOT 表里的目标地址替换成我们指定的新函数地址。这样调用方会以为自己在调用原函数实际已经进入你的实现。两条路线适用场景不同。如果目标是自己模块里的内部函数调用PLT/GOT 可能覆盖不到需要 inline hook如果你想保持更小侵入性希望大部分函数继续走原流程PLT/GOT 通常更简单稳定。这也是工程里为什么会有 xHook、Dobby、Frida Interceptor 等多种方案的原因。2.4 Hook 运行边界Hook 不是“JS 反射一下”就可以无限制运行。同一个 Hook 操作在不同环境下会面临不同的问题没有 root 的普通 Android 设备应用进程很难被外部注入所以 Frida Server 运行动态注入通常需要 root 或可调试环境。如果目标 App 本身带 root 检测、反调试、加固保护那么外部注入会失败或崩溃。本文不教你绕过这些保护因为安全测试需要建立在你拥有目标 App 的合法授权之上。系统版本、32 位还是 64 位、ART JIT 是否已预编译都会影响 Hook 的具体效果。后面遇到问题时先从这几个维度检查。结论是理解 Hook 分层理解 ArtMethod 和 inline hook 这类底层机制会让你在 Java 层和 Native 层之间自如切换而不是只会背 Frida 模板。3. 技术路线全景对比在进入环境准备前先给一份路线地图。这样可以帮你看清不同工具在整个方法体系里的位置。路线典型工具Hook 对象是否需要 root常见用途学习门槛Java 层动态 HookFridaJava/Kotlin 方法通常需要 test envApp 运行分析、返回值修改中Java 层模块 HookXposed / LSPosedJava/Kotlin 方法可做系统级root常驻模块、系统组件分析中高Native 层函数 HookFrida Interceptor动态库导出函数通常需要 test env文件、网络、JNI 调用分析高编译期代码插桩ASM / AspectJ / Gradle Transform自己的编译产物不需要自动埋点、方法耗时统计中Native 轻量 Hook 库xHook / DobbyGOT/PLT 表、inline 函数由宿主进程决定自研 SDK 或组件热修复高这张表不是给你背诵的而是想说明一件事Frida 和 Xposed 只是“上层应用”它们下面承担的 Hook 动作并不神秘。选路线时你可以按三个问题来筛选这个 Hook 是否必须在 App 启动前生效是否希望做成可重复加载的模块我能否在目标设备上获得 root 或者注入能力如果只是做临时分析Frida 的交互式体验非常强。如果要做自动化测试中反复执行的逻辑编译期插桩可能更稳定。如果是系统级设备调试、 Framework 调试LSPosed 模块是长久以来被验证过的路线。4. 环境准备与前置条件4.1 Host 环境准备我这里以 Python Frida 为主因为 Frida 是目前最常用的跨平台动态 Hook 工具。无论你用 Windows、macOS 还是 Linux都可以在电脑上安装 Frida 命令行工具pip install frida-tools安装完成后确认版本frida --version如果看到类似版本号输出说明环境已经就绪。注意 Frida 的 Python 客户端版本和服务端版本需要匹配避免出现协议不一致的问题。官方很早就通过 frida-tools 统一了版本但如果你手动下载 frida-server最好下载与frida --version输出一致的版本。如果你还需要编译 Xposed/LSPosed 模块可以打开 Android Studio 建立一个新项目。本文示例不依赖最新版本特性只要你能跑通一个普通 Android 项目即可。版本请以实际安装为准重要的是操作思路。4.2 Android 设备条件Hook 实验需要能自由操作的设备。最容易上手的方案是使用 Android 模拟器中的 AOSP 或 userdebug 镜像而不是自带 Google Play 的正式镜像因为后者通常没有 root也不方便使用 adb root/ frida。操作大体是启动一个 API 26 以上的模拟器。用adb devices确认设备已连接。如果在你自己的测试模拟器上需要 root先执行adb root。如果你使用自己的真机实验请务必使用你有完整权限的测试设备不要在生产环境中尝试 root 和 Hook 操作。4.3 准备目标测试 App为了不让 Hook 对象变成你不了解的黑盒 App我们先自己造一个最小测试 App。假设工程包名是com.example.hooklab里面有一个 Java 类InfoProvider它的两个方法会被 Hook。package com.example.hooklab; public class InfoProvider { public String getToken() { return original-token; } public int getMoney() { return 100; } }再在某个 Activity 里调用这两个方法并把结果显示到界面上。这样我们就能通过界面变化和日志输出来判断 Hook 是否生效。package com.example.hooklab; import android.app.Activity; import android.os.Bundle; import android.widget.TextView; public class MainActivity extends Activity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView textView new TextView(this); InfoProvider provider new InfoProvider(); textView.setText(token provider.getToken() , money provider.getMoney()); setContentView(textView); } }编译安装运行adb install -r app-debug.apk adb shell am start -n com.example.hooklab/.MainActivity这时候你应该能在设备上看到两个原始值tokenoriginal-token, money100。后面我们用 Hook 改变它们。5. 路线一Java 层 Hook用 Frida 跑通最小示例5.1 编写 Hook 脚本在电脑上新建一个文件hook_java.js写入以下内容if (Java.available) { Java.perform(function () { var InfoProvider Java.use(com.example.hooklab.InfoProvider); InfoProvider.getToken.implementation function () { var original this.getToken(); var hooked hooked-token; console.log([*] getToken() original original - hooked); return hooked; }; InfoProvider.getMoney.implementation function () { var original this.getMoney(); console.log([*] getMoney() original original); return original 1000; }; }); }简短解释一下每一段的作用Java.available判断当前进程是否运行在支持 Java 绑定的 ART 环境中。Java.perform是 Frida 在 Java 虚拟机线程中执行代码的入口。很多新手忘记把代码放进去结果 Hook 不生效。Java.use获取目标类相当于拿到运行时的类引用。.implementation function () { ... }是重写方法的实现这就是最直白的 Java 层 Hook。在重写后的函数里调用this.getToken()拿到的是原始方法的真实返回值。如果你想换一个返回值用return覆盖即可。5.2 把脚本注入目标 App假设你的设备已经连接并且 Frida Server 已在测试环境运行。执行frida -U -f com.example.hooklab -l hook_java.js --no-pause参数含义-U通过 USB 连接 Android 设备。-f com.example.hooklab启动指定 App 并在启动时注入。-l hook_java.js加载本地 JS 脚本。--no-pause不等待手动按回车再继续启动 App。如果一切正常目标 App 会重新启动。Frida 控制台会输出 Hook 日志设备界面上的 Token 会变成hooked-token金额会变成1100。从这条路线你可以体会到一个关键结论Java 层 Hook 的使用体验非常“动态”。不需要修改 APK不需要重新编译只要代码在目标类里可被定位就能快速改变方法行为。这也是 Frida 成为当前分析和调试首选工具的原因。5.3 容易出现的问题如果运行后找不到InfoProvider通常是因为你写的包名或类名和实际编译产物不一致。解决方法是先用如下命令列出进程里的 Java 类确认后修改frida -U -f com.example.hooklab -l debug_class.jsdebug_class.js可以写成Java.perform(function () { Java.enumerateLoadedClasses({ onMatch: function (name) { if (name.indexOf(hooklab) ! -1) { console.log(name); } }, onComplete: function () {} }); });如果枚举不到说明目标类还没被加载。此时不要在 attach 模式下等待而是使用-f冷启动或者调用到相关代码后再枚举。6. 路线二模块化 Hook用 Xposed/LSPosedFrida 适合即兴动态分析但如果你希望 Hook 逻辑成为一个模块在每次目标 App 启动时自动生效Xposed/LSPosed 是更常见的方案。这条路线会让你接触真正意义上的“模块化开发”。6.1 Xposed 与 LSPosed 的关系传统 Xposed 曾经是最常用的 Android Hook 框架。随着系统版本不断演进新项目通常转向 LSPosed因为它在 ART Hook 兼容性、作用域管理和模块管理上做得更好。无论是 Xposed 还是 LSPosed开发模块的核心模式是一致的你写一个 APK它不包含 Activity只包含 Hook 逻辑框架负责在目标进程加载这个模块。6.2 模块工程的重要文件一个最小 Xposed/LSPosed 模块通常包含三个关键部分AndroidManifest 中声明模块元数据、入口类、assets/xposed_init文件。assets/xposed_init内容com.example.hookmodule.DemoModuleAndroidManifest.xml 的核心片段application meta-data android:namexposedmodule android:valuetrue / meta-data android:namexposeddescription android:valueHook demo module / meta-data android:namexposedminversion android:value82 / /application注意在 LSPosed 中模块配置项可能不同建议以你接入的 LSPosed 版本为准上述只是一个通用 Xposed 声明。Gradle 依赖通常只需要配置为compileOnly避免模块把 API 打包进 APKdependencies { compileOnly de.robv.android.xposed:api:82 compileOnly de.robv.android.xposed:api:82:sources }6.3 模块入口类接下来写模块入口。它要实现IXposedHookLoadPackage接口框架会在每个应用进程启动后回调。package com.example.hookmodule; import android.util.Log; import de.robv.android.xposed.IXposedHookLoadPackage; import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XposedHelpers; import de.robv.android.xposed.callbacks.XC_LoadPackage; public class DemoModule implements IXposedHookLoadPackage { private static final String TAG HookModule; Override public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) { if (!com.example.hooklab.equals(lpparam.packageName)) { return; } XposedHelpers.findAndHookMethod( com.example.hooklab.InfoProvider, lpparam.classLoader, getToken, new XC_MethodHook() { Override protected void beforeHookedMethod(MethodHookParam param) { Log.i(TAG, before getToken); } Override protected void afterHookedMethod(MethodHookParam param) { param.setResult(module-token); Log.i(TAG, after getToken, result changed); } } ); } }这段代码的核心逻辑先判断当前加载的进程是不是com.example.hooklab避免模块被加载到所有进程里。findAndHookMethod的作用是定位到目标类的getToken()方法。beforeHookedMethod在原始方法执行前执行可以在这里读取参数、修改参数。afterHookedMethod在原始方法执行后执行可以通过param.setResult改变返回值。在这个示例里真正发挥作用的是setResult(module-token)。原方法其实已经执行了但返回值被模块改掉所以调用方拿到的是module-token。6.4 Xposed 路线适合什么如果你只是临时看一次日志Xposed 模块的开发成本比 Frida 高不少。它的优势是稳定性和模块生命周期模块会被框架自动加载到目标进程不需要每次手动输入命令行也不依赖 Python 脚本运行。很多框架类 Hook比如修改系统组件行为、调试系统应用用这条路线会更容易维护。它的劣势同样明显通常要求设备能够 root模块每次改动都需要重新安装并重启目标进程。如果你的目标是做一个 Hook 脚本给团队里的每个测试人员用场景适合模块化如果你只是自己一次性调试Frida 效率更高。7. 路线三Native 层 Hook用 Frida InterceptorJava 层 Hook 能解决很多问题但真实项目并不会所有逻辑都写在 Java 代码里。SDK 的底层、加密库、视频解码器、网络库最终会进入一系列.so文件。当你想分析某个 Native 方法或者观察某个 libc 函数被谁调用、传入了什么路径时可以使用 Frida 的 Interceptor。7.1 确定目标模块与导出函数在 Hook Native 函数前先要知道目标函数在哪个模块里。常见目标包括libc.so里的open、read、write、connect。jni 库导出的自定义函数比如Java_com_example_hooklab_NativeBridge_nativeCall。某个三方 SDK 的加密函数。你可以在 Frida 控制台里用Module.enumerateExports()枚举导出表确认函数确实存在。如果函数没有作为导出符号暴露跟踪难度会更大这种情况下需要更深入的内存逆向可能要用到 inline hook 技术。7.2 Native Hook 脚本新建hook_native.js用来观察 libc 中open函数的调用路径。这个示例很适合做 SDK 文件访问合规测试某个三方库运行时到底访问了哪些路径往往不需要看源码观察 open 就能定位。var openPtr Module.findExportByName(libc.so, open); if (openPtr) { Interceptor.attach(openPtr, { onEnter: function (args) { var path Memory.readUtf8String(args[0]); console.log([open] flags args[1].toInt32() path path); } }); } else { console.log(open not found); }运行注入frida -U -f com.example.hooklab -l hook_native.js --no-pause如果目标 App 在运行过程中访问过某些文件Frida 控制台会打印类似这样的日志[open] flags577 path/data/user/0/com.example.hooklab/files/test.txt7.3 Native Hook 需要注意的细节Native 层 Hook 和 Java 层 Hook 有一个最大区别onEnter里的参数来自 CPU 寄存器或栈而不是 JVM 数据模型。字符串参数是 C 指针所以需要用Memory.readUtf8String(args[0])读取。整数参数也要用.toInt32()做显式转换不能像 Java 世界那样直接拼接字符串。如果你想修改 Native 函数参数请先弄清参数类型和内存布局。无脑把args[0]替换成另一个指针轻则没效果重则导致进程崩溃。通常的做法是只做记录不修改确认必须修改时先在可控的测试 App 上验证。从 Native Hook 这条路线你会发现 Android Hook 的另一层本质程序代码在内存里只是一段可执行的字节流理解模块导出符号、指令跳转、内存读写之后你能操作的范围会大很多。但这也意味着风险一旦写错就可能把目标进程搞崩溃。所以在任何修改型操作之前先备份目标 App 数据和测试环境这比任何防范口诀都实际。8. Hook 生效验证与常见问题排查8.1 验证三步走不管用 Frida 还是 XposedHook 生效与否不能靠“感觉”。我建议你按下面三步验证第一步确认注入进程。在 Frida 里frida-ps -U能看到目标 App 进程如果看不到说明注入或启动方式不对。第二步确认 Hook 日志出现。Frida 控制台或 logcat 中能看到自定义日志尤其是方法参数和返回值。第三步确认行为发生改变。观察 App UI 或日志调用链看返回值是否从original-token变成了 Hook 后的值。如果前两步都没问题第三步却看不出变化很可能是 Hook 方法有多个重载或者类被加固/自定义 ClassLoader 加载目标类并不是你用Java.use拿到的那个类。这时要学会看日志、枚举加载类而不是盲目改写脚本。8.2 常见问题排查表问题现象可能原因排查方式解决建议frida-ps -U看不到设备adb 未授权或连接异常执行adb devices检查 USB 调试授权更换数据线或模拟器注入成功但无 Hook 日志方法还没有被调用或类没有加载在 Activity 中增加一次调用或用-f冷启动使用-f启动目标 App而不是 attach 已有进程Java.use 找不到类包名或类名错误被加固包装枚举已加载的类确认实际名称修正类名必要时找到真实 ClassLoaderHook 后 App 崩溃Hook 逻辑写错如错误修改 Native 参数查看 logcat 中的 Java 栈和 tombstone先在测试 App 上只打印不修改逐步增加逻辑Frida 版本与 frida-server 不匹配两端协议不一致比较frida --version和服务端版本下载相同版本的 frida-serverXposed 模块不生效模块作用域未选择目标 App打开模块管理开启作用域重启 App在 LSPosed 中配置目标作用域排查这类问题第一原则是减少变量。先只 Hook 一个最简单的方法比如直接返回字符串的getToken()。如果连这个方法都 Hook 不了再去查环境问题如果简单方法能 Hook再逐步增加方法的复杂度。这样可以快速把问题定位到“环境”还是“业务代码”上。9. 最佳实践、安全边界与后续学习方向9.1 Hook 代码的工程化建议如果你把 Hook 能力用到自动测试或内部工具链中建议遵循这些工程实践所有 Hook 代码写成独立脚本或模块不要散落在业务代码里。例如hook/目录下区分java_hook.js、native_hook.js。添加环境和 App 版本判断。很多 Hook 方法依赖具体方法签名或 Activity 路径目标 App 升级后可能失效。Hook 回调里做日志标准化输出统一格式比如[Hook][class][method][action]方便后续脚本解析。对可能产生副作用的返回值修改先做灰度实验不要直接改生产数据流的默认行为。涉及 native 层修改时优先采用“只读分析”必要时再考虑编写 stub 和 trampoline。任何修改型 Hook 都要先评估崩溃风险。9.2 不要跨过合规红线Hook 很容易让人产生“什么都能改”的错觉但能力的边界不等于权限的边界。以下几点是我认为每个学习 Hook 的人都应该清楚的底线不要在未经授权的情况下对其他组织或个人的 App 做 Hook尤其是涉及账号、支付、隐私数据的部分。本文不涉及绕过登录、篡改交易、破解验证、抢占库存等行为的实现思路也不建议你往这个方向探索。这类行为轻则违反软件许可协议重则涉嫌违法。在自己的 App 里做插桩和运行时监控也要遵守隐私合规要求。不能因为这是你自己的程序就无限制地收集用户数据。Hook 模块只部署在你有权限的实验设备或内部测试机上不要把动态注入能力打包进普通用户使用的生产 App。技术本身是中性的。本文的核心意义不是让每个人都成为“逆向大师”而是帮助开发者理解运行时机制做好自研 SDK 调试、性能分析和设备兼容性测试。如果你希望长期深耕下一步可以按这个顺序继续学习深入理解 ART 运行时中的 ArtMethod、ClassLinker 和 DexCache 结构。在一个干净的 AOSP 模拟器里编译并验证一个最简单的 Xposed/LSPosed 模块。学习 ELF 文件结构和 GOT/PLT 表再结合 xHook/Dobby 源码去理解 Native Hook 实现差异。把 Hook 能力接入自动化测试链路比如用 Frida 批量伪造参数、模拟异常分支用来提高测试覆盖率。真正掌握了这条技术路线后你会发现 Android Hook 并不是某个框架的“魔法机制”而是围绕虚拟机、内存、系统调用展开的一整套工程能力。如果你能独立写出一份技术选型说明解释同一个需求为什么用 Frida 而不是 Xposed或者为什么 native 层要观察导出符号而不是硬编码地址那这篇路线图就算真正内化成你自己的知识了。建议你现在就按第 4 节和第 5 节跑通一次最小实验然后把这条路线图收藏备用。