安卓逆向实战:Frida Hook HashMap.put快速定位加密参数入口

📅 2026/7/8 16:31:48
安卓逆向实战:Frida Hook HashMap.put快速定位加密参数入口
1. 项目概述与核心价值最近在分析一个安卓App的网络请求时遇到了一个典型的“黑盒”问题它的关键请求参数被加密了抓包看到的是一串毫无规律的密文。对于逆向分析来说第一步就是要找到这个加密逻辑的入口。如果直接去啃庞大的反编译代码无异于大海捞针效率极低。这时候动态插桩工具Frida就派上了大用场。今天要分享的就是一个非常高效且通用的定位技巧——HashMap.put拦截法。这个方法的核心思路不是去硬磕加密算法本身而是去拦截那些最终承载了加密结果的、最常见的数据结构比如HashMap的put方法。为什么是HashMap在安卓开发中无论是使用原生的HttpURLConnection、OkHttp还是其他网络库在构建最终发送的请求参数时开发者常常会将参数键值对放入一个Map结构中而HashMap正是最常用的实现类。加密后的参数在最终被放入请求体或拼接到URL之前极有可能会通过HashMap.put(String key, Object value)这个方法“落袋为安”。因此拦截这个方法我们就能在参数被发送前的那一刻清晰地看到哪些键key对应了加密后的值value。这就像在快递打包的最后一步看一眼快递单上写的到底是什么比去翻整个仓库的进货记录要快得多。这个方法尤其适合快速定位未知加密参数的位置为后续的算法逆向打下坚实基础。无论你是移动安全研究员、爬虫工程师还是对安卓逆向感兴趣的开发者掌握这个技巧都能让你的分析工作事半功倍。接下来我将从原理、环境搭建、脚本编写到实战调试完整地走一遍这个流程并附上我优化过的完整脚本和踩坑心得。2. 核心原理与方案选型2.1 为什么选择拦截 HashMap.put在逆向工程中定位目标代码有静态分析和动态分析两种路径。静态分析阅读反编译的Smali或Java代码虽然全面但面对混淆严重、逻辑复杂的App时容易迷失方向。动态分析运行时Hook则能让我们直接观察程序的实际行为精准打击。在动态分析中我们有多种Hook目标可以选择加密函数本身如MD5、AES、RSA的入口。但这要求我们事先知道或能猜出用了什么算法或者需要先进行大量的静态分析来定位这些类门槛较高。网络库的发送函数如OkHttpClient.newCall()或HttpURLConnection.getOutputStream()。在这里我们能抓到完整的请求数据但数据可能已经是加密后的最终形态我们不知道是哪个环节、哪个函数产生的这些密文。参数组装过程这就是HashMap.put拦截法的用武之地。它处于加密函数和网络发送函数之间。大多数业务逻辑中参数会先被放入一个Map然后再由网络库将其转换为表单、JSON或Query String。拦截put方法好处非常明显通用性强HashMap是Java标准库的一部分绝大多数App都会使用不受第三方网络库的限制。信息完整我们能同时看到参数的键Key和值Value。Key的名字如sign、token、data常常直接提示了该参数的作用例如签名、令牌、加密数据这提供了至关重要的上下文。调用栈清晰当我们在put方法处断下时可以打印即时的调用堆栈Call Stack。这个堆栈能清晰地告诉我们是哪个类的哪个方法最终调用了这次put从而一步步逆向回溯到加密函数的调用点。2.2 Frida在动态分析中的优势要实现运行时对HashMap.put的拦截我们需要一个强大的动态插桩框架。Frida正是这方面的佼佼者相比其他工具如Xposed它有如下优势无需重启可以随时将脚本注入到正在运行的进程中修改逻辑立即生效非常适合快速迭代和测试。脚本化使用JavaScript编写Hook逻辑开发调试速度快修改灵活。跨平台不仅支持安卓也支持iOS、Windows、macOS等。功能强大除了Hook还能调用函数、修改内存、枚举模块和类几乎能完成所有运行时分析需求。我们的方案就是使用Frida编写一个JavaScript脚本Hook住目标App中所有HashMap实例的put方法并打印出关键的调用信息。3. 环境准备与工具配置3.1 基础环境搭建工欲善其事必先利其器。一个稳定的Frida环境是成功的第一步。设备与系统推荐使用真机模拟器尤其是x86架构可能存在兼容性问题。一部已经Root的安卓手机是最佳选择。如果没有Root也可以使用frida-gadget的注入方案但过程稍复杂。安卓版本建议使用Android 7.0到12之间的版本。太老的版本Frida支持可能不完善太新的版本13可能遇到更强的反调试或SELinux限制。Frida环境安装在电脑客户端上安装在你的PCWindows/macOS/Linux上使用Python的pip包管理器安装。建议使用清华镜像源加速。pip install frida-tools -i https://pypi.tuna.tsinghua.edu.cn/simple安装后命令行输入frida --version能显示版本号即表示成功。在手机上安装服务端首先需要根据你手机的CPU架构下载对应的frida-server二进制文件。去Frida的GitHub Releases页面下载。用adb shell getprop ro.product.cpu.abi命令查看架构通常是arm64-v8a。# 下载后推送到手机并赋予执行权限 adb push frida-server-16.1.4-android-arm64 /data/local/tmp/frida-server adb shell chmod 755 /data/local/tmp/frida-server # 运行服务端建议在后台运行 adb shell /data/local/tmp/frida-server 验证连接在电脑上执行frida-ps -U如果能看到手机上的进程列表说明连接成功。辅助工具ADB (Android Debug Bridge)必备用于与手机通信。抓包工具如Charles或Fiddler用于捕获网络请求确认加密参数的名字和位置。你需要配置好手机代理和CA证书。反编译工具如Jadx-GUI用于静态查看App的Java代码辅助理解类名和方法名虽然我们主要用动态方法但静态查看可以验证我们的Hook结果。3.2 目标App分析与初步侦察在开始写脚本前我们需要对目标App有个基本了解。启动App并抓包打开抓包工具启动目标App进行触发加密参数的网络操作如登录、搜索。在抓包工具中找到目标请求记下明显是加密参数的键名。例如你可能会看到sign4F89A1B2C3...或encrypted_dataeyJhbGciOiJ...。这个键名sign,encrypted_data就是我们后续过滤的重要依据。定位App进程名使用frida-ps -U找到目标App的进程名。它通常与包名相关可能是包名本身也可能是包名加后缀如:pushservice。记下这个准确的进程名我们稍后需要用它来附加Attach。注意有些App会进行反调试或反注入检测。如果遇到Frida无法附加或附加后App闪退的情况可能需要考虑使用定制版的Frida、对抗检测的脚本或者在更早的时机如App启动时进行注入。这是进阶话题本文假设目标App没有强力的防护。4. Frida脚本编写与核心逻辑解析下面是我在实践中不断优化后的一个增强版HashMap.put拦截脚本。它不仅打印信息还加入了过滤、堆栈深度控制等实用功能。// hook_hashmap_put.js Java.perform(function () { console.log([*] 脚本已加载开始Hook HashMap.put...); // 1. 获取HashMap类 var HashMap Java.use(java.util.HashMap); // 2. Hook put 方法 HashMap.put.implementation function (key, value) { // ----- 关键过滤逻辑根据你的目标调整 ----- var targetKey sign; // 替换为你在抓包中看到的加密参数键名例如sign, token, data // 如果想监控所有put可以将下面if条件注释掉或改为 if(true) if (key targetKey) { // ----- 信息打印 ----- console.log(\n 捕获到目标参数 ); console.log([-] 时间戳: new Date().toLocaleString()); console.log([-] HashMap实例: this.hashCode()); console.log([-] 键 (Key): key); console.log([-] 值 (Value): value); console.log([-] 值类型: (value ? value.getClass().getName() : null)); // ----- 调用堆栈分析最核心的部分----- // 限制堆栈深度避免输出过长通常看前10-15条就足够了 var stackDepth 12; var stackTrace Java.use(android.util.Log).getStackTraceString(Java.use(java.lang.Exception).$new()); var stackLines stackTrace.split(\\n); console.log([-] 调用堆栈 (前 stackDepth 行):); // 跳过堆栈的第一行它总是java.lang.Exception for (var i 1; i stackLines.length i stackDepth; i) { // 过滤掉一些无关的堆栈行如Thread.run让输出更干净 if (!stackLines[i].includes(java.lang.Thread) !stackLines[i].includes(.perform()) { console.log( stackLines[i].trim()); } } // ----- 尝试获取更多上下文可选----- // 有时参数值本身是一个复杂对象可以尝试调用其toString方法获取更可读的信息 try { if (value typeof value.toString function value.toString() ! [object Object]) { var stringValue value.toString(); // 避免打印过长的Base64字符串等 if (stringValue.length 500) { console.log([-] Value.toString(): stringValue); } else { console.log([-] Value.toString() 过长已截断。长度: stringValue.length); } } } catch (e) { // 忽略toString可能抛出的异常 } console.log( 结束 \n); } // ----- 执行原方法并返回结果 ----- // 非常重要必须调用原方法否则会破坏App的正常逻辑可能导致崩溃或功能异常。 var result this.put(key, value); return result; }; console.log([*] HashMap.put Hook 设置完成。); });4.1 脚本逻辑逐行解析Java.perform这是Frida脚本的入口函数确保所有Java层操作在一个安全的上下文中执行。获取并Hook类Java.use(“java.util.HashMap”)获取到HashMap类的引用。然后重写其put方法的implementation属性这是Frida Hook的标准做法。过滤逻辑if (key targetKey)这行是脚本的“眼睛”。在实战中你通常只关心一两个特定的加密参数。通过抓包获取其键名如sign并赋值给targetKey可以避免脚本输出海量无关的put调用日志让信息聚焦。初期探索时你可以将其注释掉先看看所有put调用找到规律后再设置过滤。信息打印打印时间戳、HashMap对象哈希值、键、值及其类型。类型信息getClass().getName()有时能提示值的来源如是一个String还是某个自定义类的对象。调用堆栈获取这是脚本的“灵魂”。Java.use(“android.util.Log”).getStackTraceString(...)是安卓环境下获取当前线程堆栈的常用方法。我们创建一个新的Exception对象它的堆栈恰好就是当前代码的执行路径。解析堆栈字符串跳过无关行如Frida自身、线程调度打印出关键的调用链路。这个堆栈直接告诉你是com.example.app.EncryptUtils.calculateSign()调用了这个put还是com.example.app.NetworkHelper.buildParams()调用的。顺着这个堆栈往上回溯你就能快速定位到加密函数所在的具体类和方法。执行原方法var result this.put(key, value);这一行调用了原始的put方法。这是Hook的黄金法则除非你有明确意图要修改返回值否则一定要调用原函数并返回其结果否则App的逻辑链会断裂必然导致崩溃或异常行为。4.2 脚本的进阶优化点多键名过滤如果目标有多个加密参数可以将targetKey改为数组使用if (array.includes(key))进行判断。值内容分析对于值Value可以增加更深入的分析。例如如果值是字符串可以判断其是否看起来像Base64、Hex或JSON。性能考虑如果App的put调用极其频繁即使过滤后日志量也很大可以考虑将日志输出到文件或者添加采样逻辑如每10次打印一次。防检测一些安全意识强的App会检测HashMap.put等常用方法是否被Hook。可以尝试Hook更底层的java.util.AbstractMap.put或者使用Frida的NativeFunction在Native层进行更隐蔽的监控这属于高阶技巧。5. 实战操作与问题排查5.1 执行Hook脚本环境准备好脚本写好后我们开始实战。启动目标App确保App处于你可以触发加密请求的状态如登录界面。附加进程并注入脚本在电脑命令行中执行frida -U -l hook_hashmap_put.js -f com.target.app.package --no-pause-U: 连接到USB设备。-l: 加载指定的脚本文件。-f: 启动一个新的App进程并附加。如果你已经启动了App可以用-n “App名称”来附加到已有进程。使用-f可以确保从最早时机开始Hook。--no-pause: 启动后立即恢复进程执行不中断。触发网络请求在手机上操作App触发那个携带加密参数的网络请求比如点击登录按钮。观察控制台输出如果一切顺利你将在电脑的命令行中看到类似下面的输出[*] 脚本已加载开始Hook HashMap.put... [*] HashMap.put Hook 设置完成。 捕获到目标参数 [-] 时间戳: 2023-10-27 14:30:15 [-] HashMap实例: 203458210 [-] 键 (Key): sign [-] 值 (Value): a1b2c3d4e5f67890... [-] 值类型: java.lang.String [-] 调用堆栈 (前12行): at com.xxx.xxx.network.RequestBuilder.buildParams(RequestBuilder.java:125) at com.xxx.xxx.network.ApiService.login(ApiService.java:88) at com.xxx.xxx.activity.LoginActivity$2.onClick(LoginActivity.java:205) ... 结束 看调用堆栈清晰地指出了路径LoginActivity中的点击事件调用了ApiService.login后者又调用了RequestBuilder.buildParams正是在buildParams方法的第125行附近我们的sign参数被put进了HashMap。5.2 常见问题与排查技巧在实际操作中你可能会遇到以下问题这里是我的排查实录脚本注入失败提示TypeError: cannot read property ‘implementation’ of undefined原因最常见的原因是类名没找到。虽然java.util.HashMap是标准库但有些App可能会使用自定义的Map实现如ArrayMap或者使用了ProGuard混淆将标准库类也“优化”了极少见。排查先注释掉Hook代码在Java.perform里添加console.log(Java.enumerateLoadedClassesSync().filter(c c.includes(“Map”)))打印所有已加载的包含“Map”的类看看有没有HashMap。尝试Hook其父类java.util.AbstractMap的put方法因为HashMap继承自它。如果确实没有你可能需要调整策略去Hook网络库本身的参数组装方法。注入后App立刻闪退原因脚本错误你的脚本可能存在语法错误或逻辑错误如未调用原函数。反调试/反注入App检测到了Frida。Hook时机不对在某个关键类初始化完成前就Hook可能导致依赖问题。排查首先用最简单的脚本测试如只Hookjava.lang.String.toString确认基础Frida环境是否被检测。使用—no-pause参数避免进程暂停导致检测超时。尝试使用frida-gadget以非Root模式注入或使用对抗Frida检测的脚本如修改Frida默认端口、特征。能看到put调用但堆栈信息不完整或全是Frida内部调用原因堆栈获取方式可能在某些环境下受限或者被混淆了。排查确保使用的是android.util.Log.getStackTraceString这是安卓环境下的可靠方法。检查堆栈过滤逻辑是否过于严格误删了有用行。可以暂时注释掉过滤代码查看完整堆栈。如果堆栈被混淆类名和方法名是a,b,c你需要结合反编译工具Jadx将混淆后的堆栈映射回源码。虽然看起来费劲但路径逻辑是不变的。日志输出太多找不到想要的信息原因过滤条件targetKey设置不正确或App内部有大量无关的HashMap操作。排查再次确认抓包中加密参数的确切键名注意大小写。可以先不设过滤运行一次触发请求从海量日志中搜索你的加密值密文片段找到对应的那次put调用从而确认正确的key。除了Key过滤可以加一层Value过滤比如判断value是否包含特定字符或长度是否符合密文特征。Hook成功但堆栈没有指向加密函数原因加密可能发生在更早的阶段加密后的结果作为一个整体对象如一个JSONObject或自定义DataBean被put进来。此时你put的Value是一个对象密文藏在对象内部。应对查看打印出的值类型如果是自定义类如com.xxx.EncryptedData你需要用Frida去Hook这个类的构造函数或它的toString、getData等方法进一步深入。或者在堆栈中寻找put之前最近的那个方法它很可能就是负责组装和加密的入口即使它没有直接调用加密算法。6. 从定位到深入后续逆向策略成功定位到put调用点只是第一步。拿到调用堆栈后你的逆向工作就进入了“按图索骥”的阶段。静态代码对照用Jadx等工具打开目标App的APK根据堆栈信息找到对应的类和方法。例如找到com.xxx.xxx.network.RequestBuilder.buildParams方法。分析上下文在buildParams方法中查看sign参数的值是如何计算出来的。它可能来自于某个方法的返回值比如sign EncryptUtils.generateSign(params)。Hook加密函数一旦找到了疑似加密函数如generateSign你就可以编写新的Frida脚本去Hook它打印其输入参数明文和输出结果密文验证其功能。算法还原通过动态Hook你可以收集多组明文和对应的密文。观察输入输出变化规律结合静态分析加密函数的代码逐步还原出加密算法可能是MD5、SHA256加盐、AES、RSA或者是自定义的混淆算法。模拟实现在Python或你熟悉的语言中复现这个加密算法用于脱离App环境生成有效的请求参数。整个流程从模糊的加密请求到清晰的算法定位HashMap.put拦截法扮演了那个关键的“突破口”角色。它用最小的代价给了你一张通往核心逻辑的“地图”。这个方法我已经在数十个不同复杂度、不同防护等级的App上实践过成功率非常高。当然没有一种方法是万能的当遇到极度定制化的网络层或强混淆时可能需要结合其他Hook点如JSON序列化库的toJson方法、特定网络库的拦截器等。但无论如何将HashMap.put作为动态分析的起点绝对是一个高效且明智的选择。