FRIDA入门指南:从零编写第一个Android Hook脚本

发布时间:2026/7/17 8:16:22

FRIDA入门指南:从零编写第一个Android Hook脚本 1. 项目概述为什么是FRIDA如果你对移动安全、逆向工程或者应用动态分析感兴趣那么“FRIDA”这个名字你一定不陌生。它就像一个功能强大的“手术刀”允许你在应用运行时动态地观察、修改甚至控制其内部逻辑。无论是想分析某个App的加密算法还是想绕过某个烦人的验证FRIDA都能提供一种相对优雅的解决方案。这篇指南的目标非常明确让一个完全没有接触过FRIDA的人能够亲手写出并运行自己的第一个Hook脚本。我们不谈高深的理论只聚焦于最核心的“动手”环节让你在最短的时间内感受到FRIDA的魅力。为什么选择FRIDA作为入门工具在移动端动态分析领域Xposed、Cydia Substrate等框架同样知名。但FRIDA有几个难以比拟的优势跨平台支持Android、iOS、Windows、macOS等、脚本化使用Python或JavaScript编写逻辑无需编译重启、易用性相比其他框架其API设计对新手更友好。更重要的是它的社区活跃遇到问题容易找到解决方案。对于初学者而言从FRIDA入手能更快地建立起对Hook技术的直观理解避免在复杂的环境配置和底层原理中迷失方向。2. 核心概念与准备工作在开始写代码之前我们需要先理清几个核心概念并准备好“手术台”和“手术刀”。2.1 什么是Hook你可以把Hook理解为一种“拦截”技术。想象一下一个应用程序就像一条流水线数据参数从A点传到B点经过处理得到结果。Hook技术允许你在A点和B点之间悄悄地插入一个你自己的“监听站”和“加工厂”。当数据流经时你的“监听站”能先看到原始数据参数然后你的“加工厂”可以决定是原封不动地放行是修改一下再传递还是直接返回一个假的结果。在FRIDA的语境里我们主要Hook的是函数Function。无论是Java层的类方法还是Native层C/C的库函数都可以成为我们的目标。通过Hook我们能获取函数调用时的参数值、修改返回值、甚至阻止函数执行。这是动态分析中最核心、最强大的能力。2.2 环境搭建安装FRIDA与配置设备工欲善其事必先利其器。FRIDA环境分为两部分服务端frida-server和客户端frida-tools。1. 客户端安装在你的电脑上客户端就是我们写Python脚本调用FRIDA API的地方。安装非常简单使用Python的包管理工具pip即可。建议使用Python 3.7及以上版本。pip install frida-tools安装完成后在命令行输入frida --version如果能正确显示版本号如16.1.11说明客户端安装成功。2. 服务端部署在目标设备上服务端需要运行在你要分析的目标设备上比如一台Android手机或模拟器。这里以Android为例。步骤一下载frida-server访问FRIDA的官方GitHub发布页根据你设备的CPU架构下载对应的frida-server文件。对于大多数现代Android手机是arm64架构对于模拟器如雷电模拟器通常是x86_64。文件命名类似frida-server-16.1.11-android-arm64.xz。步骤二推送并启动将下载的.xz文件解压得到frida-server可执行文件。通过ADBAndroid Debug Bridge工具推送到设备并赋予执行权限。# 将frida-server推送到设备的/data/local/tmp目录 adb push frida-server /data/local/tmp/ # 连接到设备的shell adb shell # 进入目录并赋予执行权限 cd /data/local/tmp chmod 755 frida-server # 以后台方式启动frida-server ./frida-server 注意部分系统尤其是高版本Android有严格的SELinux策略直接运行可能会报错。可以尝试先进入root模式su或者使用setenforce 0临时关闭SELinux需要root权限。对于模拟器通常root权限是默认开启的操作会更简单。3. 连接验证在电脑上打开一个新的命令行窗口运行frida-ps -U这个命令会列出通过USB-U参数连接的设备上所有正在运行的进程。如果能看到一长串进程列表如com.android.chrome,system_server等恭喜你FRIDA环境已经成功搭建如果报错请检查设备USB调试是否开启、frida-server是否在运行、以及电脑和设备是否在同一个网络环境如果使用网络连接。2.3 工具选型为什么选择PythonJavaScriptFRIDA的Hook脚本核心逻辑使用JavaScript编写。这是因为FRIDA内置了一个高效的JavaScript运行时Duktape或V8能够直接在目标进程的内存中执行JS代码实现对函数逻辑的注入和操作。JS语法灵活对于处理对象、数组等数据结构非常方便。而我们用Python作为“外壳”或“控制器”。Python脚本负责启动FRIDA、将JS脚本附加到目标进程、接收JS脚本传回的数据并进行处理、以及提供更友好的人机交互界面。Python拥有丰富的生态库方便我们进行数据存储、网络通信或更复杂的逻辑控制。这种“Python搭台JavaScript唱戏”的模式结合了Python的易用性和JavaScript在动态执行方面的优势是FRIDA标准的工作方式。3. 第一个Hook脚本从定位到实现理论准备就绪让我们开始实战。我们的第一个目标不求复杂但求完整地走通流程。假设我们想Hook一个Android应用里最常见的函数android.widget.Toast.makeText()。这个函数用于弹出提示框我们可以通过Hook它来监控App在何时、何地弹出了什么内容的Toast。3.1 目标分析与定位在Hook之前我们需要知道目标的“准确地址”。对于Java函数这个“地址”就是它的类名、方法名和参数签名。Toast.makeText是一个静态方法。我们通过查阅Android官方文档或源码可以知道它的完整签名是public static Toast makeText (Context context, CharSequence text, int duration)类名android.widget.Toast方法名makeText参数签名(Landroid/content/Context; Ljava/lang/CharSequence; I)Landroid/widget/Toast;这是JNI签名格式Landroid/content/Context;表示一个Context对象Ljava/lang/CharSequence;表示一个CharSequence对象String是其子类I表示int类型返回值Landroid/widget/Toast;表示一个Toast对象。对于我们的第一个脚本我们不需要手动计算这么复杂的签名。FRIDA的Java API提供了更简单的方式。但理解这个概念很重要因为当你Hook系统API或第三方库时可能需要自己确定签名。3.2 JavaScript Hook脚本编写现在我们来编写核心的JavaScript Hook代码。创建一个文件命名为hook_toast.js。// hook_toast.js Java.perform(function () { // 确保代码在Java虚拟机上下文中执行 console.log([*] 开始Hook Toast.makeText...); // 1. 获取目标类的引用 var Toast Java.use(android.widget.Toast); // 2. Hook指定的方法重载 // 这里我们Hook参数为 (Context, CharSequence, int) 的这个重载 Toast.makeText.overload(android.content.Context, java.lang.CharSequence, int).implementation function (context, text, duration) { // 3. 在函数执行前打印传入的参数 console.log(\n[] Toast.makeText 被调用); console.log( 上下文 Context: context); console.log( 提示文本 Text: text); console.log( 显示时长 Duration: duration (0短1长)); // 4. 可选修改参数。例如把所有Toast文本都加上前缀 var modifiedText [Hooked] text; // 注意text是CharSequence我们需要将其转换为String进行操作再转回去 // 这里简单处理如果直接赋值字符串在某些情况下可能类型不匹配。 // 更稳妥的方式是使用 Java.String 对象 // var newText Java.use(java.lang.String).$new([Hooked] text); // 5. 调用原函数 (original method)并获取其返回值 var originalResult this.makeText(context, text, duration); // 这里使用未修改的text // 如果要使用修改后的文本则调用this.makeText(context, newText, duration); console.log([*] 原始函数执行完毕。); // 6. 返回原始结果当然你也可以返回一个自己伪造的Toast对象 return originalResult; }; console.log([*] Hook设置完成等待触发...); });代码逐行解析Java.perform()这是一个关键函数它确保其内部的代码在Android应用的Java线程上下文中执行。所有对Java类的操作都必须包裹在这个函数里。Java.use(“android.widget.Toast”)获取对Toast类的引用。之后我们可以通过这个引用Toast来访问类的静态方法或Hook实例方法。.overload(…)因为Java支持方法重载makeText可能有多个版本。我们需要用.overload并指定参数类型来精确选择要Hook的那个重载。这里我们指定了参数类型为Context,CharSequence,int。.implementation function (…) { … }这是Hook的核心。我们将目标方法的实现替换为我们自己定义的函数。当App调用原始的makeText时实际上会执行我们这里的代码。在我们的实现函数里context,text,duration就是原始调用传入的参数。我们可以打印、修改它们。this.makeText(…)在Hook函数内部this.makeText指向的是原始的函数。通过调用它我们执行了应用原本的逻辑。这是一个非常重要的技巧确保了应用在Hook后依然能正常工作除非我们故意不想让它工作。return originalResult;最后将原始函数执行的结果返回。这样调用方感知不到函数被Hook过除了我们打印的日志和可能的参数修改。3.3 Python控制脚本编写接下来编写Python脚本负责将JS脚本注入到目标进程。创建一个文件命名为runner.py。# runner.py import frida import sys # 定义从JS脚本接收消息的回调函数 def on_message(message, data): if message[type] send: # 打印JS脚本通过send()函数发送的日志 print(f[JS Log] {message[payload]}) elif message[type] error: # 打印JS脚本执行错误 print(f[JS Error] {message[stack]}) # 主函数 def main(): # 1. 连接到设备USB连接 try: device frida.get_usb_device() print(f[*] 已连接到设备: {device}) except Exception as e: print(f[!] 连接设备失败: {e}) print([!] 请检查1. USB调试已开启 2. frida-server正在运行 3. 电脑已授权设备) sys.exit(1) # 2. 选择目标进程 # 这里我们以系统UI进程为例它经常弹出Toast。你也可以换成任何其他App的包名。 target_package com.android.systemui # 如果你想Hook某个具体App比如一个测试App替换为它的包名即可例如com.example.myapp print(f[*] 正在附加到进程: {target_package}) try: # 附加到目标进程 session device.attach(target_package) except frida.ProcessNotFoundError: print(f[!] 未找到进程: {target_package}请确保应用正在运行。) sys.exit(1) # 3. 读取并创建JS脚本 with open(hook_toast.js, r, encodingutf-8) as f: js_code f.read() script session.create_script(js_code) script.on(message, on_message) # 设置消息回调 # 4. 加载并执行JS脚本 print([*] 正在加载Hook脚本...) script.load() print([*] Hook脚本加载成功) print([*] 现在请操作设备触发Toast弹出比如在设置中点击某个选项。) print([*] 按 CtrlC 停止脚本。\n) # 5. 保持脚本运行等待输入 try: sys.stdin.read() except KeyboardInterrupt: print(\n[*] 用户中断正在分离...) finally: session.detach() print([*] 已从进程分离。) if __name__ __main__: main()3.4 运行与验证确保你的Android设备或模拟器已连接电脑且frida-server正在运行。在设备上确保目标进程是活跃的。对于com.android.systemui系统界面它始终在运行。你也可以打开一个会弹出Toast的App。在电脑的命令行进入存放hook_toast.js和runner.py的目录。运行Python脚本python runner.py。如果一切正常你会看到脚本输出连接成功、附加成功、脚本加载成功的提示。现在去操作你的设备。例如在设置中关闭Wi-Fi或者在任何App里执行一个会弹出Toast的操作比如“已复制到剪贴板”。观察运行runner.py的命令行窗口。你应该能看到类似下面的输出[JS Log] [*] 开始Hook Toast.makeText... [JS Log] [*] Hook设置完成等待触发... [JS Log] [] Toast.makeText 被调用 [JS Log] 上下文 Context: android.app.ContextImpla1b2c3d [JS Log] 提示文本 Text: Wi-Fi已关闭 [JS Log] 显示时长 Duration: 0(0短1长) [JS Log] [*] 原始函数执行完毕。恭喜你的第一个FRIDA Hook脚本已经成功运行了。你成功地拦截了一个系统级的Java函数调用并捕获了它的调用参数。4. 脚本进阶常见模式与技巧成功运行第一个脚本只是起点。在实际分析中你会遇到更复杂的情况。下面介绍几个必备的进阶技巧。4.1 Hook重载方法与构造函数很多方法有多个重载。你需要Hook所有可能被调用的版本或者精确指定。使用.overload()来区分。var StringClass Java.use(java.lang.String); // Hook String.indexOf(String) 重载 StringClass.indexOf.overload(java.lang.String).implementation function(str) { console.log(indexOf被调用参数: ${str}); return this.indexOf(str); // 调用原方法 }; // Hook String.indexOf(String, int) 重载 StringClass.indexOf.overload(java.lang.String, int).implementation function(str, fromIndex) { console.log(indexOf被调用参数: ${str}, ${fromIndex}); return this.indexOf(str, fromIndex); };Hook构造函数使用$init。var Intent Java.use(android.content.Intent); Intent.$init.overload(android.content.Context, java.lang.Class).implementation function(context, cls) { console.log(Intent被创建目标Class: ${cls}); // 调用原始构造函数 return this.$init(context, cls); };4.2 操作对象与调用方法在Hook函数内部你获取到的参数可能是复杂的Java对象。你需要知道如何访问其字段和方法。Java.perform(function () { var Activity Java.use(android.app.Activity); Activity.onCreate.overload(android.os.Bundle).implementation function(bundle) { // 调用原方法 this.onCreate(bundle); // this 在这里指向当前的Activity实例 // 1. 获取当前Activity的类名 var className this.getClass().getName(); console.log(当前Activity: ${className}); // 2. 调用实例的方法 var window this.getWindow(); // 3. 修改对象的字段如果有权限 // this.mTitle Java.use(java.lang.String).$new(Hooked Title); // 4. 使用Java.choose在堆上查找特定类的实例非常有用 Java.choose(com.example.target.DataClass, { onMatch: function(instance) { // 每当在堆上找到一个DataClass实例就会进入这里 console.log(找到DataClass实例: ${instance}); console.log( secret字段值: ${instance.secret.value}); // 可以修改它 instance.secret.value HackedValue; }, onComplete: function() { console.log(堆遍历完成。); } }); }; });4.3 处理Native层C/C函数FRIDA同样可以Hook Native层的函数这需要使用Interceptor.attachAPI。// 假设我们要Hook libc.so 中的 strlen 函数 Interceptor.attach(Module.findExportByName(libc.so, strlen), { onEnter: function(args) { // args[0] 是第一个参数即字符串指针 this.inputString Memory.readUtf8String(args[0]); console.log([Native] strlen被调用字符串: ${this.inputString}); }, onLeave: function(retval) { // retval 是返回值 console.log([Native] strlen返回长度: ${retval}); // 甚至可以修改返回值 // retval.replace(10); // 让strlen总是返回10 } });4.4 脚本的健壮性与错误处理在实际环境中目标类或方法可能不存在例如在不同系统版本上。你的脚本应该能处理这些情况。Java.perform(function () { try { var targetClass Java.use(com.some.obscure.Class); console.log([*] 目标类存在准备Hook...); // ... Hook逻辑 } catch (e) { console.log([!] 无法使用目标类错误: ${e}); // 可以尝试备用方案或优雅退出 } // 另一种方式检查类是否存在 if (Java.available) { Java.enumerateLoadedClasses({ onMatch: function(className) { if (className.includes(MyTarget)) { console.log([*] 发现疑似目标类: ${className}); } }, onComplete: function() { console.log([*] 类枚举完成。); } }); } });5. 实战演练Hook一个简单App的登录函数让我们用一个更贴近实战的例子来巩固。假设我们有一个简单的测试App包名是com.example.demoapp它有一个登录功能调用了一个方法com.example.demoapp.LoginUtil.verifyLogin(String username, String password)返回一个布尔值。我们的目标是Hook这个verifyLogin方法打印出每次登录尝试的用户名和密码并且无论密码是什么都让登录成功。步骤1分析目标我们已知类名和方法名。假设我们不知道参数签名但通过常识或动态观察可以猜测是两个String参数。步骤2编写JS脚本 (hook_login.js)Java.perform(function () { console.log([*] 脚本注入成功开始寻找登录逻辑...); try { var LoginUtil Java.use(com.example.demoapp.LoginUtil); console.log([*] 成功找到LoginUtil类。); // 尝试Hook verifyLogin。我们假设它有两个String参数。 // 使用overload指定参数类型为两个java.lang.String LoginUtil.verifyLogin.overload(java.lang.String, java.lang.String).implementation function(username, password) { console.log(\n[] 登录验证被触发); console.log( 用户名: ${username}); console.log( 密码: ${password}); // 关键在这里我们直接返回true绕过密码验证 console.log([*] 强制返回 true登录成功); return true; // 如果你想调用原函数并获取结果可以这样做 // var originalResult this.verifyLogin(username, password); // console.log([*] 原始验证结果: ${originalResult}); // return originalResult; // 或者返回修改后的结果 }; console.log([*] LoginUtil.verifyLogin Hook设置完成。); } catch (e) { console.log([!] Hook过程中出现错误: ${e}); // 可能是类名错误、方法签名不匹配等 // 可以尝试枚举类的方法来确认签名 // var methods LoginUtil.class.getDeclaredMethods(); // ... } });步骤3修改Python控制脚本修改runner.py中的target_package为com.example.demoapp并将加载的JS文件改为hook_login.js。步骤4运行与测试在设备上安装并运行com.example.demoapp。运行python runner.py。在Demo App的登录界面输入任意用户名和密码点击登录。观察命令行输出你应该能看到捕获到的用户名和密码并且App会显示登录成功因为我们强制返回了true。这个例子展示了FRIDA在安全测试中的一个典型应用绕过客户端逻辑验证。当然这只是一个教学演示真实场景中的验证逻辑会复杂得多可能涉及网络请求、本地加密等。6. 常见问题与排查技巧实录在实际操作中你肯定会遇到各种各样的问题。下面是我在无数次“踩坑”后总结的一些常见问题及解决方法。6.1 连接与进程附加问题问题现象可能原因解决方案frida-ps -U无输出或报错Unable to connect to remote frida-server1. frida-server未运行。2. 设备USB调试未开启。3. 电脑未授权设备。4. 设备CPU架构与frida-server版本不匹配。5. 端口冲突默认27042。1. 进入设备shell检查/data/local/tmp/frida-server进程是否存在ps | grep frida。2. 在设备开发者选项中确认USB调试已开启。3. 拔插USB线查看设备是否弹出“允许USB调试”提示。4. 重新下载正确架构的frida-server。5. 尝试使用frida -U -p PID指定进程ID连接。Failed to spawn: unable to find application with identifier ‘xxx’目标应用未启动或者包名错误。1. 使用frida-ps -U确认应用进程是否存在。2. 检查包名拼写是否正确。3. 先手动启动应用再使用attach而非spawn。TypeError: cannot read property ‘implementation’ of undefinedJS脚本中.overload()指定的方法签名与实际方法不匹配或者方法名写错。1. 使用Java.use(“ClassName”).class.getDeclaredMethods()在脚本中枚举类的方法查看准确的签名。2. 如果不确定重载可以尝试不写.overload()直接HookFRIDA有时能自动处理但这不是好习惯。6.2 脚本执行与逻辑问题问题现象可能原因解决方案Hook后应用崩溃或行为异常1. 在Hook函数中没有调用原方法this.originalMethod导致程序逻辑链断裂。2. 修改了不可变对象或关键参数导致后续逻辑出错。3. Native层Hook时错误地修改了内存或寄存器。1.黄金法则除非你明确知道后果否则在implementation函数中最后一定要调用原方法并返回其结果。2. 对于参数和返回值的修改要格外小心最好先只做读操作。3. 在Native Hook的onLeave中修改retval要清楚其类型和含义。打印出的对象是[object Object]或无意义ID直接打印Java对象JavaScript会调用其toString()默认输出可能不友好。1. 对于已知类可以调用其特定的方法获取信息如obj.toString()、obj.getClass().getName()。2. 使用JSON.stringify()通常对Java对象无效需要使用FRIDA的Java.cast或直接访问字段。Hook似乎没有生效没有日志输出1. 目标方法没有被调用。2. Hook的时机太晚方法在脚本注入前已经被调用过了。3. 脚本有语法错误但未在on_message回调中捕获。1. 确认你的操作确实会触发目标方法。2. 尝试使用frida -U -f com.package.name –no-pause -l script.js在应用启动时立即注入spawn模式。3. 在Python脚本的on_message回调中确保处理了error类型的消息。6.3 性能与稳定性问题脚本导致应用卡顿如果你的Hook函数逻辑非常复杂比如在每次调用时都遍历堆Java.choose会严重拖慢应用速度。尽量将耗时的操作如查找实例放在Hook设置阶段而不是每次调用时都执行。内存泄漏在JavaScript中持有大量Java对象的全局引用可能导致Java垃圾回收器无法回收它们造成内存泄漏。使用完后及时将引用置为null。多线程问题Hook的函数可能被多个线程同时调用。确保你的Hook代码是线程安全的避免使用共享的全局变量而不加锁。6.4 一个实用的调试技巧使用console.log和send()在开发Hook脚本时console.log()是你的最佳伙伴。但要注意console.log输出在FRIDA的日志中而通过send()函数可以将结构化数据发回Python端处理。// 在JS中 var result this.originalMethod(...args); // 发送一个复杂对象回Python send({type: data, payload: {arg1: args[0], result: result}}); return result;# 在Python的on_message回调中 def on_message(message, data): if message[type] send: payload message[payload] if isinstance(payload, dict) and payload.get(type) data: print(f收到数据: 参数{payload[payload][arg1]}, 结果{payload[payload][result]})这个技巧在你需要将Hook到的数据保存到文件或进行进一步分析时非常有用。从环境搭建到第一个脚本运行再到进阶技巧和问题排查我希望这篇指南为你打开了一扇通往移动安全动态分析世界的大门。FRIDA的强大远不止于此比如还有RPC远程过程调用允许你在Python端直接调用设备上的Java方法还有内存扫描、动态修改so库等高级功能。但所有这些高级应用都建立在扎实的基础之上——即理解如何定位和Hook一个目标函数。

相关新闻