从零构建APK到Frida动态Hook:Android应用运行时篡改实战指南

📅 2026/7/18 15:32:05
从零构建APK到Frida动态Hook:Android应用运行时篡改实战指南
1. 项目概述为什么我们要亲手构建APK并Hook它在移动安全研究、应用逆向分析甚至是自动化测试领域Frida 早已不是个陌生的名字。它就像一把瑞士军刀能让我们在应用运行时“为所欲为”——查看、修改函数逻辑甚至凭空注入代码。但很多教程都停留在“如何Hook一个现成的App”上这就像只教你怎么开别人家的锁却不告诉你锁的内部结构。一旦遇到加固、混淆或者复杂的业务逻辑新手往往会一头雾水不知道从何下手。所以我决定换一个思路带大家走一条更扎实的路从零开始亲手构建一个目标APK然后亲手用Frida去Hook它实现动态篡改逻辑。这个过程的魅力在于你既是“造物主”也是“破解者”。你清楚地知道每一行代码在哪里、每一个方法叫什么、参数是什么然后你再亲自去拦截和修改它。这种“自产自销”的实战能让你透彻理解Hook技术的每一个环节从源码到字节码从静态分析到动态注入建立起完整的知识闭环。无论是为了深入理解Android机制还是为后续分析第三方复杂应用打下坚实基础这都是一次不可多得的深度练习。2. 环境搭建与目标APK构建工欲善其事必先利其器。我们的第一步是准备好所有工具并创建一个用于“实验”的目标应用。2.1 核心工具链准备你需要准备以下环境我强烈建议在实体机或性能较好的模拟器如Android Studio自带的AVD上进行某些轻量模拟器可能对Frida支持不佳。Android Studio这是我们的主要开发环境用于编写和构建目标APK。从官网下载并安装最新稳定版即可。安装时注意勾选Android SDK和相应的平台工具。Java Development Kit (JDK)Android Studio通常会捆绑或提示安装确保版本为JDK 11或17根据你的Gradle插件版本兼容性选择。Python 3.xFrida的客户端脚本主要使用Python编写。确保已安装并将pip添加到系统环境变量。Frida这是我们的主角。安装分为两部分客户端 (Client)在你的电脑上安装。打开命令行执行pip install frida-tools。这会同时安装frida和frida-tools包含有用的命令行工具如frida-ps。服务端 (Server)在你的Android设备真机或模拟器上运行。首先通过adb shell getprop ro.product.cpu.abi查看设备架构通常是arm64-v8a或x86_64。然后去Frida的GitHub Releases页面下载对应架构的frida-server-xx.x.x-android-xx.xz文件。ADB (Android Debug Bridge)通常包含在Android SDK的platform-tools目录下。确保adb命令可以在终端中直接调用。注意下载Frida-server时版本号尽量与frida-tools的版本保持一致或接近以避免潜在的兼容性问题。使用frida --version查看客户端版本。2.2 创建并理解我们的“实验靶场”APK我们将创建一个极其简单但功能点清晰的Android应用它包含我们后续要Hook的所有逻辑。新建项目打开Android Studio选择“Empty Activity”模板语言选择Java为了更直观地展示Hook过程Kotlin原理相同。项目名称为HookDemo包名可设为com.example.hookdemo。设计核心逻辑我们修改MainActivity.java添加一些“靶子”方法。package com.example.hookdemo; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.widget.Toast; public class MainActivity extends AppCompatActivity { private static final String TAG HookDemo; // 靶子方法1一个简单的加法函数 public int add(int a, int b) { int result a b; Log.d(TAG, “add: “ a “ “ b “ “ result); return result; } // 靶子方法2一个验证登录的函数 public boolean verifyLogin(String username, String password) { Log.d(TAG, “verifyLogin called with: “ username “/“ password); // 简单的硬编码验证 boolean isValid “admin“.equals(username) “123456“.equals(password); Toast.makeText(this, “Login “ (isValid ? “Success“ : “Failed“), Toast.LENGTH_SHORT).show(); return isValid; } // 靶子方法3一个静态方法 public static String getSecret() { return “ThisIsASecretString“; } Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 调用一下我们的方法方便观察 int sum add(5, 3); boolean loginResult verifyLogin(“user“, “pass“); String secret getSecret(); Log.d(TAG, “onCreate: Sum“ sum “, Login“ loginResult “, Secret“ secret); } }构建APK点击Android Studio工具栏的“Build” - “Build Bundle(s) / APK(s)” - “Build APK(s)”。构建完成后将生成的app-debug.apk文件安装到你的测试设备上。使用命令adb install path/to/your/app-debug.apk在设备上找到并打开HookDemo应用观察Logcat输出确认应用正常运行并看到了我们预设的日志。这个APK就是我们的“靶场”。我们知道add方法在MainActivity类里它接收两个int参数返回它们的和。我们知道verifyLogin的校验逻辑是写死的。我们的目标就是在不修改这个APK源码、不重新编译的情况下用Frida改变这些行为。3. Frida基础与设备端部署在开始Hook之前我们必须让Frida在设备端“跑起来”。3.1 部署Frida-Server到Android设备假设你下载的frida-server文件是frida-server-16.1.11-android-arm64.xz。解压并推送首先解压.xz文件得到二进制文件frida-server-16.1.11-android-arm64。# 将文件推送到设备的可执行目录/data/local/tmp 通常具有执行权限 adb push frida-server-16.1.11-android-arm64 /data/local/tmp/赋予执行权限并运行adb shell # 进入目录 cd /data/local/tmp # 赋予可执行权限 chmod 755 frida-server-16.1.11-android-arm64 # 以后台方式运行frida-server注意这里的文件名要和你推送的一致 ./frida-server-16.1.11-android-arm64 符号让其在后台运行。你可以使用ps | grep frida来检查进程是否在运行。端口转发重要Frida-server默认监听TCP端口27042。我们需要通过ADB将这个端口转发到本地以便电脑上的Frida客户端能够连接。# 退出设备的shell如果还在的话在电脑的终端执行 adb forward tcp:27042 tcp:270423.2 验证连接与基础命令现在从你的电脑终端尝试以下命令验证环境是否通畅# 列出设备上运行的所有进程 frida-ps -U # 列出设备上运行的所有Android应用包名格式 frida-ps -Ua如果能看到一长串进程列表恭喜你Frida环境已经搭建成功。-U参数代表连接到USB设备。4. Hook实战从定位到动态篡改现在进入最核心的部分。我们将编写Python脚本利用Frida来Hook我们自己编写的APK。4.1 编写第一个Hook脚本拦截并修改加法结果创建一个Python文件例如hook_add.py。import frida import sys # 定义我们的JavaScript Hook代码 jscode “““ Java.perform(function () { console.log(“[] Hook脚本已加载“); // 定位我们的目标类 var MainActivity Java.use(‘com.example.hookdemo.MainActivity’); // Hook ‘add’ 实例方法 MainActivity.add.implementation function (a, b) { console.log(“[] Hook到 add 方法被调用“); console.log(“ 原始参数: a“ a “, b“ b); // 我们可以篡改参数 var newA a * 2; // 例如把第一个参数乘以2 var newB b 10; // 把第二个参数加10 console.log(“ 篡改后参数: newA“ newA “, newB“ newB); // 调用原始方法但传入篡改后的参数 var originalResult this.add(newA, newB); // 注意这里调用原始实现 console.log(“ 原始方法结果基于篡改参数: “ originalResult); // 我们甚至可以篡改返回值 var finalResult originalResult - 100; console.log(“ 最终返回结果: “ finalResult); return finalResult; }; console.log(“[] add 方法Hook设置完成“); }); “““ def on_message(message, data): if message[‘type’] ‘send’: print(“[APP LOG]: “ message[‘payload’]) else: print(message) # 连接到设备 device frida.get_usb_device() print(“[] 已连接到设备: “ str(device)) # 附加到目标进程我们的HookDemo应用 # 首先需要启动应用或者确保它正在运行。这里我们通过包名来附加。 try: # 方式一附加到已存在的进程 pid device.spawn([“com.example.hookdemo“]) # spawn会启动应用但暂停它 session device.attach(pid) device.resume(pid) # 恢复应用执行 print(“[] 已启动并附加到进程 PID: “ str(pid)) except Exception as e: # 方式二如果应用已在运行直接附加 print(“[!] 尝试附加到运行中的进程...“) session device.attach(“com.example.hookdemo“) # 创建并加载脚本 script session.create_script(jscode) script.on(‘message‘, on_message) print(“[] 正在加载Hook脚本...“) script.load() # 保持脚本运行等待Hook事件触发 print(“[] Hook脚本加载成功请操作App触发add方法或等待onCreate调用。“) print(“[] 按 CtrlC 停止脚本。“) sys.stdin.read()脚本解析与核心技巧Java.perform这是Frida在Android上执行Java层Hook的入口函数所有相关操作必须包裹在其中以确保在正确的线程上下文中执行。Java.use(‘完整类名’)用于获取一个JavaScript wrapper来代表目标Java类。这是定位类的关键。.implementation这是Frida最强大的特性之一。通过重写一个方法的implementation属性我们就替换了它的原始实现。在重写的函数里this指向当前对象实例。this.add(...)在Hook函数内部通过this.方法名(...)可以调用方法的原始实现。这是实现“先执行原逻辑再修改结果”或“修改参数后执行原逻辑”的基础。参数与返回值你可以任意读取、修改方法的参数和返回值。这是动态篡改逻辑的核心。运行与观察确保设备上的HookDemo应用已经关闭。在终端运行python hook_add.py。脚本会启动应用并附加。此时在设备上打开HookDemo应用。观察Python脚本控制台的输出。你会看到add方法被onCreate调用时我们的Hook脚本成功拦截打印了原始参数(5, 3)篡改后参数(10, 13)原始结果23以及最终返回的篡改结果-77。同时观察Android Studio的Logcat你会发现应用自己打印的日志add: 5 3 -77。看返回值已经被我们篡改了但应用对此一无所知它以为add(5,3)真的返回了-77。4.2 深入Hook篡改登录验证逻辑接下来我们HookverifyLogin方法实现一个“万能密码”。创建新脚本hook_login.pyJavaScript部分修改如下Java.perform(function () { console.log(“[] 开始Hook登录验证...“); var MainActivity Java.use(‘com.example.hookdemo.MainActivity’); MainActivity.verifyLogin.implementation function (username, password) { console.log(“[] 登录验证被调用“); console.log(“ 用户名: “ username); console.log(“ 密码: “ password); // 动态篡改逻辑无论输入什么只要密码是“frida“就算成功 var isMagicPassword “frida“.equals(password); if (isMagicPassword) { console.log(“[!] 检测到万能密码 ‘frida‘强制验证通过“); // 直接返回true完全绕过原始验证逻辑 return true; } // 如果不是万能密码则执行原始验证逻辑 console.log(“ 执行原始验证逻辑...“); var originalResult this.verifyLogin(username, password); console.log(“ 原始验证结果: “ originalResult); return originalResult; }; console.log(“[] verifyLogin 方法Hook设置完成“); });技巧与思考在这个例子中我们没有调用原始方法就返回了true这意味着原始的业务逻辑检查admin/123456被完全短路Bypass了。这在安全测试中常用于绕过客户端校验。你可以尝试修改脚本在调用原始方法this.verifyLogin时将参数篡改为正确的admin和123456这样无论用户输入什么最终都会以管理员身份登录。这演示了另一种攻击向量。4.3 挑战Hook静态方法Hook静态方法略有不同因为this在静态上下文中没有意义。我们修改hook_static.py的JS代码Java.perform(function () { console.log(“[] 开始Hook静态方法...“); var MainActivity Java.use(‘com.example.hookdemo.MainActivity’); // Hook静态方法 MainActivity.getSecret.implementation function () { // 注意这里没有参数 console.log(“[] 静态方法 getSecret 被调用“); // 调用原始静态方法 var originalSecret MainActivity.getSecret(); // 注意调用方式变了用类名而不是this console.log(“ 原始秘密: “ originalSecret); // 篡改返回值 var fakeSecret “HackedByFrida!“; console.log(“ 返回假秘密: “ fakeSecret); return fakeSecret; }; console.log(“[] getSecret 静态方法Hook设置完成“); });关键区别在Hook静态方法的实现里调用原始方法使用的是类名.方法名()即MainActivity.getSecret()而不是this.getSecret()。因为this指向实例对象而静态方法属于类本身。运行这个脚本再次打开应用你会发现Logcat中打印的Secret变成了我们返回的HackedByFrida!。5. 高级技巧与实战问题排查掌握了基本Hook后我们面对真实场景会更复杂。以下是一些进阶技巧和常见问题的解决方案。5.1 枚举与Hook重载方法如果一个类中有多个同名但参数不同的方法重载我们需要精确指定。假设我们的MainActivity有另一个add方法接收double类型。public double add(double a, double b) { return a b; }在Frida中我们可以使用.overload(‘参数类型‘)来指定var MainActivity Java.use(‘com.example.hookdemo.MainActivity’); // Hook int add(int, int) MainActivity.add.overload(‘int‘, ‘int‘).implementation function(a, b) { // ... hook logic for int version }; // Hook double add(double, double) MainActivity.add.overload(‘double‘, ‘double‘).implementation function(a, b) { // ... hook logic for double version };参数类型使用JNI签名简写如int,double,java.lang.String,[B(byte数组),[Ljava.lang.String;(String数组)等。5.2 主动调用Java方法Frida不仅能拦截调用还能主动发起调用。这在需要触发特定逻辑或查询对象状态时非常有用。Java.perform(function () { // 使用Java.choose在堆上查找已存在的对象实例 Java.choose(‘com.example.hookdemo.MainActivity‘, { “onMatch“: function(instance) { console.log(“[] 找到MainActivity实例: “ instance); // 主动调用实例方法 var result instance.add(100, 200); console.log(“[] 主动调用add(100,200)结果: “ result); }, “onComplete“: function() { console.log(“[] 实例搜索完成。“); } }); // 主动调用静态方法 var secret Java.use(‘com.example.hookdemo.MainActivity‘).getSecret(); console.log(“[] 主动调用静态方法getSecret(): “ secret); });5.3 常见问题排查实录在实际操作中你几乎一定会遇到下面这些问题。问题1Java.perform错误或类找不到现象Error: java.lang.ClassNotFoundException: com.example.hookdemo.MainActivity。排查包名/类名拼写错误这是最常见的原因。使用frida-ps -Ua确认应用包名使用反编译工具如JADX或查看R.java确认完整类名。类加载器问题有些类由自定义ClassLoader加载。可以尝试枚举所有ClassLoader来查找类Java.enumerateClassLoaders({ onMatch: function(loader){ try { if (loader.findClass(“com.example.hookdemo.MainActivity“)) { console.log(“[] 找到目标ClassLoader: “ loader); Java.classFactory.loader loader; // 切换ClassLoader } } catch(e) {} }, onComplete: function(){} });应用未启动或进程错误确保脚本附加的是正确的进程。对于多进程应用UI主进程的包名通常就是应用包名。问题2Hook后应用崩溃或行为异常现象Hook后应用闪退或功能不正常。排查参数/返回值类型不匹配在Hook函数中返回的类型必须与原方法声明的类型完全一致。int不能返回String。未调用原始方法如果你替换了implementation但某些关键逻辑如初始化、状态设置在原始方法中不调用它可能导致后续崩溃。除非你明确想阻断该逻辑。递归调用在Hook函数内部如果你又调用了同一个被Hook的方法且没有条件限制会导致无限递归和栈溢出。确保你的调用逻辑有终止条件或使用一个标志位来避免。var isHooked false; MainActivity.someMethod.implementation function(arg) { if (isHooked) { return this.someMethod(arg); // 调用原始实现 } isHooked true; // ... 你的hook逻辑 var result this.someMethod(arg); // 这里会再次进入本函数但被标志位拦截 isHooked false; return result; };问题3Frida-server连接失败或进程列表为空现象frida-ps -U无输出或报错。排查ADB连接确保adb devices能看到你的设备。端口转发确认执行了adb forward tcp:27042 tcp:27042。Frida-server未运行重新进入adb shell检查/data/local/tmp/下的进程是否存在并用ps | grep frida确认。如果没有重新执行运行命令。权限问题确保是以root权限运行frida-server在已root的设备或模拟器上。非root环境需要一些额外配置且能力受限。版本不匹配确保电脑端的frida和frida-tools版本与设备端的frida-server版本大致兼容。问题4如何Hook Native (SO库) 函数Frida同样强大。使用Interceptor.attach来Hook Native函数。// 假设有一个libnative.so里面导出了函数int add(int a, int b) var nativeAdd Module.findExportByName(“libnative.so“, “_Z3addii“); // 需要函数修饰名 Interceptor.attach(nativeAdd, { onEnter: function(args) { console.log(“[] Native add called.“); console.log(“ arg[0] (a): “ args[0].toInt32()); console.log(“ arg[1] (b): “ args[1].toInt32()); }, onLeave: function(retval) { console.log(“ returning: “ retval.toInt32()); // 篡改返回值 retval.replace(999); } });获取准确的函数符号名是Native Hook的关键可以使用objdump -T或nm工具查看SO库。6. 项目总结与扩展思路通过这个从构建APK到实现动态Hook的完整流程我们亲手验证了Frida的核心能力在运行时对已知和未知的代码逻辑进行观察、拦截与篡改。我们从一个清晰的“靶场”开始避免了逆向分析中“黑盒”的迷茫专注于掌握Hook技术本身。我个人在实际操作中的体会是理解“上下文”至关重要。无论是Java层的this与类名调用区别还是Native层的参数传递约定亦或是多ClassLoader环境本质上都是代码执行环境的问题。Frida给了我们上帝视角但我们必须理解这个世界的运行规则才能有效地施加影响。这个项目可以轻松地扩展复杂参数处理尝试Hook接收或返回复杂对象如List、自定义类的方法学习如何使用Frida的API去构造和解析这些对象。批量Hook与RPC编写更复杂的脚本一次性Hook多个类或方法。甚至利用Frida的rpc.exports功能将Hook到的函数暴露给Python端调用实现双向通信。对抗简单反调试一些应用会检测Frida。你可以尝试在自定义的APK中加入简单的反Frida代码如检测端口27042、检测frida-server进程名然后编写Frida脚本来绕过这些检测这是一场有趣的攻防练习。结合自动化将你的Frida脚本与UI自动化工具如Appium结合模拟用户操作触发特定功能同时进行动态数据篡改实现更复杂的自动化测试场景。记住技术本身无分对错。在合法授权的范围内用这些技能去加固你自己的应用、进行安全评估或自动化测试才是它最大的价值所在。希望这篇长文能成为你探索Android动态分析世界的一块坚实跳板。