公司动态

Android应用动态Hook实战:从Frida环境搭建到函数拦截

📅 2026/7/19 10:19:43
Android应用动态Hook实战:从Frida环境搭建到函数拦截
1. 项目概述为什么从“Hello World”到Hook是必经之路如果你对移动安全、逆向工程或者应用行为分析感兴趣那么“Frida”这个名字你一定不陌生。它被誉为动态插桩的“瑞士军刀”但很多新手在第一次接触时往往会被其庞大的生态和看似复杂的脚本编写劝退。最常见的困境就是教程看了很多环境也搭好了但面对一个真实的App却不知道从哪里下笔写第一个真正有用的脚本。这正是我们这次实战要解决的问题——跳过空洞的理论直接动手在Android模拟器上用Frida-Tools 17.3.2完成从打印一句“Hello World”到成功Hook一个目标函数的全过程。这个过程的本质是建立一个正向的、可验证的学习循环。单纯的“Hello World”只能证明环境通了但毫无成就感而直接挑战复杂Hook又容易因挫败感而放弃。我们选取的路径是先确保Frida与模拟器“握手”成功Hello World阶段然后立即转向一个明确、简单且有即时反馈的实战目标。这就像学游泳先在浅水区扶着池边蹬腿Hello World然后马上在教练指导下游出五米实战Hook这种即时的成功反馈是持续学习的最佳动力。本次实战我们将使用雷电模拟器兼容性好对Frida支持稳定作为我们的“实验沙盒”目标应用就是一个最简单的、我们自己编写的Demo App。你将清晰地看到一个脚本如何从探测环境到定位目标再到注入逻辑并改变其运行行为。无论你是安全研究员、应用测试工程师还是单纯对技术好奇的开发者跟着走完这一趟你获得的将不仅仅是一段可运行的代码更是一套可复用的、解决问题的实战方法论。2. 环境搭建与核心工具链解析工欲善其事必先利其器。一个稳定、干净的环境是后续所有操作的基础。这里我们不追求最全的配置而是追求最直接、最不容易出错的“一条龙”方案。2.1 模拟器选型为什么是雷电模拟器Android模拟器选择众多如Genymotion、官方模拟器、夜神、MuMu等。我们选择雷电模拟器LDPlayer进行本次实战主要基于以下几点考量兼容性与稳定性雷电模拟器基于Android 7.1/9.0等经典版本其内核与Frida的兼容性经过大量社区实践验证出现莫名闪退或连接失败的概率较低。Root权限获取便捷雷电模拟器在设置中可直接开启Root权限这对于需要高权限操作的Frida至关重要免去了手动刷机、修补Boot镜像的繁琐过程。网络与端口配置简单其网络模式通常为桥接或NAT便于我们从主机Windows/Mac直接访问模拟器内部的Frida服务端口。性能与资源占用平衡对于Frida脚本调试这类场景不需要极高的图形性能雷电模拟器在资源占用和流畅度上取得了较好平衡。注意请务必从官网下载雷电模拟器避免使用修改版以防内置环境不纯净导致Frida运行异常。安装后第一件事是进入模拟器“设置”-“高级设置”中开启“Root权限”。2.2 Frida生态组件详解与安装Frida不是一个单一工具而是一个工具链。理解每个组件的职责能让你在出现问题时快速定位。Frida-Server (核心服务端)这是一个需要运行在目标设备即我们的Android模拟器上的守护进程。它的职责是接收来自外部的Frida脚本指令并将其注入到目标进程中。你可以把它想象成在目标设备上安装的一个“代理”或“后门”。安装从Frida官方GitHub Releases页面下载与你的模拟器架构通常是x86或x86_64对应的frida-server可执行文件。例如frida-server-17.3.2-android-x86.xz。操作解压后得到frida-server文件通过adb push命令将其上传到模拟器的/data/local/tmp/目录并赋予可执行权限。Frida-Tools (Python客户端工具包)这是一组运行在**你的开发电脑主机**上的Python命令行工具。frida-tools包含了frida、frida-ps、frida-ls-devices等非常实用的命令行工具。我们通过它们来发现设备、列出进程、注入脚本。安装在主机上使用pip安装指定版本pip install frida-tools17.3.2。指定版本可以确保与frida-server版本严格一致避免因版本不匹配导致的协议错误。Frida Python Binding (编程接口)当你用Python写Frida脚本时import frida所使用的就是这个库。它通常随frida-tools一起安装。它提供了与Frida-Server通信的所有底层API。ADB (Android调试桥)连接主机与模拟器的桥梁。我们用它来推送文件、开启端口转发、进入Shell环境。确保你的系统PATH中包含了ADB命令。版本一致性的黄金法则frida-server、frida-tools以及Python脚本中引用的Frida库这三者的主版本号如17.3.2必须完全一致。这是避免出现“Protocol error”、“Unexpected message format”等诡异错误的第一要务。2.3 环境配置实操步骤下面是一步步的配置命令请在你的主机命令行中执行# 1. 启动雷电模拟器并确保ADB已连接 adb devices # 你应该能看到类似 emulator-5554 device 的输出 # 2. 将下载好的frida-server推送到模拟器 adb push /你的本地路径/frida-server /data/local/tmp/ # 3. 进入模拟器shell并启动frida-server adb shell # 进入模拟器后 su # 获取root权限雷电模拟器默认已开启此命令会切换为#提示符 cd /data/local/tmp chmod 755 frida-server # 赋予执行权限 ./frida-server # 后台运行 # 注意此时不要关闭这个shell窗口否则进程会终止。可以按 CtrlZ然后输入 bg 使其在后台继续运行或者新开一个终端窗口进行后续操作。 # 4. 在主机上验证连接 # 新开一个主机终端 frida-ps -U # 参数-U表示连接USB设备包括模拟器。如果一切正常你将看到模拟器上正在运行的进程列表。如果frida-ps -U成功列出了进程那么恭喜你最困难的环境搭建部分已经完成。如果失败请按以下顺序排查ADB连接是否正常模拟器Root是否开启frida-server是否在运行可用ps | grep frida在模拟器shell中查看主机与服务器版本是否一致3. 从“Hello World”到进程附着建立通信桥梁环境通了我们就要开始写代码了。第一个脚本的目标不是做复杂的事而是验证“我们的代码能否在目标设备上执行”。这是建立信心的关键一步。3.1 编写第一个“Hello World”脚本创建一个名为hello.js的JavaScript文件。这是将被注入到目标进程的脚本逻辑。// hello.js Java.perform(function () { // 这是Frida脚本的入口点确保在Java虚拟机上下文中执行 console.log([*] Hello from Frida!); // 我们也可以尝试调用一些系统API来证明脚本有执行能力 var StringClass Java.use(java.lang.String); var exampleString StringClass.$new(Frida Demo); console.log([*] Created String: exampleString); // 打印当前进程的ID console.log([*] Process ID: Process.id); console.log([*] Script is running successfully!); });这个脚本做了三件事1) 打印日志2) 演示如何使用Frida的Java.useAPI来操作Java类3) 打印进程信息。它没有任何Hook操作纯粹是“打招呼”。3.2 使用Python加载器控制脚本生命周期光有JS脚本不行我们需要一个Python程序作为“发射器”负责连接设备、启动脚本、并接收脚本输出的日志。创建loader.py# loader.py import frida import sys def on_message(message, data): # 处理从JS脚本发回的消息 if message[type] send: print(f[*] {message[payload]}) else: print(message) # 连接到USB设备上的Frida-server device frida.get_usb_device() # 附加到目标进程。这里我们先找一个系统进程做测试比如system_server系统核心进程一直存在 # 使用 frida-ps -U 可以查看进程列表 target_process system_server try: session device.attach(target_process) print(f[*] Attached to process: {target_process}) except Exception as e: print(f[!] Failed to attach to {target_process}: {e}) sys.exit(1) # 读取我们刚才写的JS脚本 with open(hello.js, r, encodingutf-8) as f: js_code f.read() # 创建脚本实例 script session.create_script(js_code) # 绑定消息回调函数 script.on(message, on_message) # 加载并执行脚本 script.load() # 保持脚本运行等待用户输入后退出 print([*] Script loaded. Press Enter to exit...) sys.stdin.read() session.detach()运行这个Python脚本python loader.py。如果一切顺利你将在主机终端看到来自模拟器system_server进程内部打印出的“Hello from Frida!”等信息。实操心得第一次运行最常见的错误是“Failed to attach: unable to find process with name system_server”或权限错误。请确保frida-server是以root权限运行的在#提示符下。你附加的进程名完全正确大小写敏感。对于用户App需要其进程正在运行。可以通过frida-ps -U再次确认进程列表。成功了吗如果你看到了来自目标进程的问候那么你已经完成了从主机到模拟器再到特定进程内部的完整通信链搭建。这个“Hello World”的价值远大于一句输出它证明了你的整个工具链是畅通无阻的。4. 目标定位与Hook实战拦截一个简单函数现在我们进入真正的Hook实战。我们将目标从一个系统进程转移到一个我们自己可控的Demo App上。这样做的原因是1) 系统进程函数复杂不易找到简单目标2) 自己编写的App我们完全清楚其内部逻辑可以精准验证Hook效果。4.1 创建目标Demo Android应用我们使用Android Studio快速创建一个项目其中包含一个非常简单的目标函数。核心代码如下// MainActivity.java package com.example.fridademo; import android.os.Bundle; import android.util.Log; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity { private static final String TAG FridaDemo; // 这是我们即将要Hook的目标函数 public String getSecret() { String secret ThisIsTheOriginalSecret; Log.d(TAG, getSecret() called, returning: secret); return secret; } // 另一个函数用于触发目标函数 public void showSecret() { String s getSecret(); TextView tv findViewById(R.id.textView); tv.setText(Secret is: s); } Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 添加一个按钮点击后调用showSecret findViewById(R.id.button).setOnClickListener(v - showSecret()); } }这个App有一个按钮点击后调用showSecret()进而调用getSecret()最终在TextView上显示“Secret is: ThisIsTheOriginalSecret”。我们的Hook目标就是拦截getSecret()函数修改其返回值。将App安装到雷电模拟器中adb install app-debug.apk。安装后启动它点击按钮确认它能正常显示原始字符串。4.2 编写Hook脚本定位、拦截与修改现在我们编写Frida JavaScript脚本hook_demo.js来Hook这个函数。// hook_demo.js Java.perform(function () { console.log([*] Starting Hook Demo Script...); // 1. 定位我们的目标类 // 如果类名不确定可以使用模糊搜索但这里我们明确知道类名 var MainActivity Java.use(com.example.fridademo.MainActivity); // 2. Hook目标函数getSecret() MainActivity.getSecret.implementation function () { console.log([] getSecret() Hooked! This function is about to be called.); // 3. 调用原函数可选获取原始返回值 var originalResult this.getSecret(); console.log([*] Original result was: originalResult); // 4. 伪造我们想要的返回值 var fakeSecret HackedByFrida123!; console.log([] Returning fake secret: fakeSecret); // 5. 返回修改后的值替换原函数的返回值 return fakeSecret; }; console.log([*] Hook placed successfully. Waiting for function to be called...); });脚本解析Java.use(“类名”)获取一个JavaScript包装器用于操作目标Java类。函数名.implementation这是Frida Hook的核心。我们将目标函数的实现替换为我们自己定义的JavaScript函数。在我们的实现里this.getSecret()可以调用原始的函数逻辑这需要小心递归但这里函数名不同没问题。我们记录原值然后返回一个伪造的字符串。4.3 注入脚本并观察效果修改之前的loader.py将目标进程改为我们的Demo App。首先运行Demo App然后使用frida-ps -U找到它的进程名通常是包名com.example.fridademo。更新Python加载器# loader_hook.py import frida import sys def on_message(message, data): if message[type] send: print(f[*] {message[payload]}) else: print(message) device frida.get_usb_device() # 改为你的App包名 target_process com.example.fridademo try: # 注意这里使用attach要求App已启动。也可以使用spawn在App启动前注入。 session device.attach(target_process) print(f[*] Attached to App: {target_process}) except Exception as e: print(f[!] Failed: {e}) # 尝试使用spawn方式启动并附加 print(f[*] Trying to spawn the app...) pid device.spawn([target_process]) session device.attach(pid) device.resume(pid) print(f[*] App spawned and attached (PID: {pid})) with open(hook_demo.js, r, encodingutf-8) as f: js_code f.read() script session.create_script(js_code) script.on(message, on_message) script.load() print([*] Hook script injected! Now go to your app and click the button.) sys.stdin.read()运行python loader_hook.py。脚本会提示Hook已放置。此时切回雷电模拟器点击Demo App上的按钮。见证奇迹的时刻观察两个地方。你的主机终端会输出类似下面的日志证明Hook被触发函数被拦截。[*] Attached to App: com.example.fridademo [*] Hook script injected! Now go to your app and click the button. [] getSecret() Hooked! This function is about to be called. [*] Original result was: ThisIsTheOriginalSecret [] Returning fake secret: HackedByFrida123!模拟器上的App界面TextView显示的文本将从“Secret is: ThisIsTheOriginalSecret”变成“Secret is: HackedByFrida123!”。至此你完成了第一个完整的、有实际效果的Frida Hook脚本你成功地拦截了一个Java函数监听了它的调用并修改了它的返回值。5. 核心原理与高级Hook技巧初探成功了一次之后我们有必要深入一层理解背后的原理并学习如何处理更复杂的场景。5.1 Frida Hook的核心原理动态插桩Frida实现Hook的核心技术称为“动态二进制插桩”Dynamic Binary Instrumentation, DBI。它不像静态修改APK文件而是在目标进程运行时将自定义的代码我们的JavaScript注入到进程内存中。注入frida-server作为守护进程具备高权限。当我们的Python控制端通过USB发送脚本时server会通过ptrace等机制将Frida的运行时一个包含JavaScript引擎的小型库注入到目标进程。拦截对于Java层函数Frida利用了Android的ART/Dalvik虚拟机机制。它通过修改Java方法对应的底层结构如ArtMethod将其入口指向一段“桥接代码”。当该方法被调用时控制权首先转到我们的JavaScript代码。执行与恢复我们的JS代码执行完毕后可以选择调用原函数通过.implementation包装器内的this.原函数名也可以直接返回一个新值。最终控制权会返回给原调用方。这个过程完全是动态的、内存中的不需要重启App或修改任何文件。这也是Frida在逆向分析和动态测试中如此强大的原因。5.2 处理重载函数与参数现实中的函数常常有重载Overload。例如我们的getSecret可能有一个接收int参数的重载版本。如何Hook特定的一个Java.perform(function () { var MainActivity Java.use(com.example.fridademo.MainActivity); // Hook 无参数的 getSecret() MainActivity.getSecret.overload().implementation function() { console.log([] Hooked getSecret()); return Hooked no-arg version; }; // Hook 接收一个int参数的 getSecret(int type) MainActivity.getSecret.overload(int).implementation function(type) { console.log([] Hooked getSecret(int): type type); // 我们可以修改参数也可以修改返回值 var newType type 100; var result this.getSecret(newType); // 调用原函数但传入修改后的参数 return Modified: result; }; });使用.overload(“参数类型签名”)来指定具体要Hook的重载方法。参数类型签名遵循JNI格式如int,java.lang.String,[B(byte数组)等。5.3 主动调用与对象构造除了拦截我们还可以主动调用函数甚至创建新的Java对象。Java.perform(function () { // 主动调用静态方法 Java.choose(com.example.fridademo.MainActivity, { onMatch: function(instance) { // 找到已存在的实例 console.log([*] Found instance: instance); // 主动调用实例方法 var secret instance.getSecret(); console.log([*] Actively called getSecret: secret); }, onComplete: function() {} }); // 构造一个新的Java对象 var StringClass Java.use(java.lang.String); var newString StringClass.$new(Hello from Frida Constructor); console.log([*] New String object: newString); });Java.choose用于在堆上枚举已存在的对象实例这在需要与特定UI组件交互时非常有用。$new是Frida提供的构造器调用方法。6. 实战问题排查与进阶调试技巧即使按照步骤操作你也可能会遇到各种问题。这里汇总了常见坑点及解决方案。6.1 常见错误与解决方案速查表错误现象可能原因解决方案Failed to attach: unable to find process with name ‘xxx’1. 进程名错误。2. 进程未运行。3. Frida-server未运行或权限不足。1. 用frida-ps -U确认精确进程名。2. 启动目标App。3. 检查adb shell中psTypeError: cannot read property ‘implementation’ of undefined1. 类名错误。2. 类尚未被加载。3. 混淆导致类名变化。1. 检查包名、类名拼写。2. 在Java.perform内使用setTimeout延迟Hook或先触发类加载。3. 使用Java.enumerateLoadedClasses()搜索类名。Error: access violation accessing 0x…(内存访问错误)1. Hook了错误的函数地址Native层常见。2. 脚本逻辑错误导致非法内存访问。1. 确认函数签名参数、返回类型。2. 简化脚本分步调试。对于Native Hook需格外小心。脚本注入后App闪退1. Hook的函数是关键系统函数或频繁调用函数实现逻辑太慢或出错。2. 脚本存在死循环或内存泄漏。3. 与App其他保护机制冲突。1. 优化Hook函数逻辑避免耗时操作。2. 使用try-catch包裹可能出错的部分。3. 尝试绕过反调试进阶话题。frida-ps -U无输出或超时1. ADB连接不稳定。2. Frida-server版本与客户端不匹配。3. 模拟器网络模式导致端口不通。1. 重启ADBadb kill-server adb start-server。2.严格检查版本一致性。3. 尝试在主机上adb forward tcp:27042 tcp:27042然后使用frida-ps -H 127.0.0.1:27042连接。6.2 使用console.log与send()进行调试调试Frida脚本主要靠打印。除了console.log输出到Frida控制台还可以用send()将结构化数据发回Python端处理。// JS端 var complexData { pid: Process.id, modules: Process.enumerateModules() }; send({ type: info, payload: complexData }); // Python端 on_message 函数 def on_message(message, data): if message[type] send: payload message[payload] if isinstance(payload, dict) and payload.get(type) info: print(f[Info] PID: {payload[payload][pid]}) for module in payload[payload][modules]: print(f - {module[name]})6.3 枚举与搜索定位未知目标面对一个陌生的App如何找到要Hook的类和方法// 1. 枚举所有已加载的类 Java.enumerateLoadedClasses({ onMatch: function(className) { if (className.includes(secret) || className.includes(auth)) { // 关键词过滤 console.log([*] Found interesting class: className); } }, onComplete: function() { console.log([*] Enumeration complete.); } }); // 2. 枚举某个类的所有方法 var TargetClass Java.use(com.xxx.xxx.TargetClass); var methods TargetClass.class.getDeclaredMethods(); methods.forEach(function(method) { console.log([*] Method: method.toString()); });这些方法在逆向分析中至关重要是“探索”阶段的必备工具。走到这里你已经掌握了Frida进行Android Java层Hook的完整工作流从环境搭建、连接验证到编写脚本、Hook函数、修改逻辑再到问题排查和基础逆向。这个从“Hello World”到“实战Hook”的旅程其核心价值在于建立了一套可复用的模式。下次当你面对一个新的App时你不再会无从下手而是会自然地遵循“确认环境 - 附加进程 - 探索类与方法 - 编写测试Hook - 验证效果”的流程。记住所有复杂的分析都始于一个简单的Hook。试着用今天学到的方法去Hook一下你手机上的计算器App看看能否改变一次加法运算的结果。这种“小胜利”会不断驱动你走向更深入的技术领域。