【Frida Android】实战篇14:非标准算法场景 Hook 教程

文章目录

  • [1 前言](#1 前言)
  • [2 非标准加密源码解析](#2 非标准加密源码解析)
    • [2.1 代码作用分析](#2.1 代码作用分析)
  • [3 未混淆APK的Hook流程](#3 未混淆APK的Hook流程)
    • [3.1 定位目标方法](#3.1 定位目标方法)
    • [3.2 编写Hook脚本](#3.2 编写Hook脚本)
    • [3.3 脚本执行效果](#3.3 脚本执行效果)
  • [4 混淆APK的Hook流程](#4 混淆APK的Hook流程)
    • [4.1 混淆带来的问题](#4.1 混淆带来的问题)
    • [4.2 第一步:通过系统方法定位加密逻辑](#4.2 第一步:通过系统方法定位加密逻辑)
      • [4.2.1 定位脚本](#4.2.1 定位脚本)
      • [4.2.2 分析定位结果](#4.2.2 分析定位结果)
    • [4.3 第二步:验证疑似方法](#4.3 第二步:验证疑似方法)
      • [4.3.1 测试脚本](#4.3.1 测试脚本)
      • [4.3.2 验证结果](#4.3.2 验证结果)
    • [4.4 第三步:补全加密方法Hook](#4.4 第三步:补全加密方法Hook)
      • [4.4.1 完整Hook脚本](#4.4.1 完整Hook脚本)
      • [4.4.2 最终效果](#4.4.2 最终效果)
  • [5 本章总结](#5 本章总结)
    • [5.1 未混淆场景Hook要点](#5.1 未混淆场景Hook要点)
    • [5.2 混淆场景Hook要点](#5.2 混淆场景Hook要点)

⚠️本博文所涉安全渗透测试技术、方法及案例,仅用于网络安全技术研究与合规性交流,旨在提升读者的安全防护意识与技术能力。任何个人或组织在使用相关内容前,必须获得目标网络 / 系统所有者的明确且书面授权,严禁用于未经授权的网络探测、漏洞利用、数据获取等非法行为。

1 前言

在现代企业应用开发中,加密技术是保障数据安全的核心手段。除了AES、RSA等广泛认可的标准加密算法外,许多企业会针对特殊业务场景设计并实现非标准加密算法------这些算法可能是基于异或、位移、自定义置换等基础操作的组合,也可能是对标准算法的魔改(如修改密钥生成规则、调整分组模式等)。

学习对非标准算法进行Hook的必要性在于:这类算法没有统一规范,逆向分析时无法像前几个章节通过"识别标准算法特征"快速破解,必须通过动态Hook捕获输入输出、密钥等关键信息。

掌握非标准算法的Hook技巧,能帮助开发者在安全测试、漏洞分析、兼容性验证等场景中高效定位加密逻辑,是逆向工程与应用调试的核心技能之一。

本章节使用的示例 APK、相关源码如下:

链接: https://pan.baidu.com/s/1bu9TqgfT5GCisXJ3lA2EWQ?pwd=ti1e

提取码: ti1e

2 非标准加密源码解析

以下是示例 APK 中的非标准加密工具类实现,包含加密(encrypt)与解密(decrypt)两个核心方法:

kotlin 复制代码
package com.example.fridaapk

import android.util.Base64

object CustomEncryptionUtils {
    private const val DEFAULT_KEY = "FridaCustomKey"
    
    fun encrypt(plaintext: String, key: String = DEFAULT_KEY): String {
        // 将明文和密钥转换为UTF-8字节数组
        val plainBytes = plaintext.toByteArray(Charsets.UTF_8)
        val keyBytes = key.toByteArray(Charsets.UTF_8)
        
        val encryptedBytes = ByteArray(plainBytes.size)
        
        for (i in plainBytes.indices) {
            // 密钥循环复用(取模运算实现密钥字节重复)
            val keyByte = keyBytes[i % keyBytes.size]
            // 异或操作:明文字节与密钥字节异或
            var encryptedByte = plainBytes[i].toInt() xor keyByte.toInt()
            // 左移1位+右移7位(循环位移)增加复杂度
            encryptedByte = (encryptedByte shl 1) or (encryptedByte shr 7)
            // 确保结果在byte范围内(0-255)
            encryptedBytes[i] = (encryptedByte and 0xFF).toByte()
        }
        
        // 加密结果通过Base64编码为字符串
        return Base64.encodeToString(encryptedBytes, Base64.NO_WRAP)
    }
    
    fun decrypt(encryptedData: String, key: String = DEFAULT_KEY): String {
        // 将Base64编码的密文解密为字节数组
        val encryptedBytes = Base64.decode(encryptedData, Base64.NO_WRAP)
        val keyBytes = key.toByteArray(Charsets.UTF_8)
        
        val decryptedBytes = ByteArray(encryptedBytes.size)
        
        for (i in encryptedBytes.indices) {
            // 密钥循环复用(同加密逻辑)
            val keyByte = keyBytes[i % keyBytes.size]
            // 逆向位移:先还原循环位移操作
            var decryptedByte = encryptedBytes[i].toInt() and 0xFF
            decryptedByte = (decryptedByte shr 1) or (decryptedByte shl 7)
            // 逆向异或:用密钥字节还原明文
            decryptedByte = decryptedByte xor keyByte.toInt()
            decryptedBytes[i] = (decryptedByte and 0xFF).toByte()
        }
        
        // 将字节数组转换为UTF-8字符串(明文)
        return String(decryptedBytes, Charsets.UTF_8)
    }
}

2.1 代码作用分析

该工具类的核心逻辑基于"异或+循环位移"的组合:

  • 加密过程:明文字节与密钥字节循环异或后,进行左移1位+右移7位的循环位移操作,最终通过Base64编码输出;
  • 解密过程:先对Base64密文解码,再逆向执行位移操作(右移1位+左移7位),最后通过异或还原明文;
  • 密钥机制:支持自定义密钥,默认密钥为"FridaCustomKey",通过取模运算实现密钥字节的循环复用(适合任意长度明文)。

3 未混淆APK的Hook流程

当APK未经过混淆处理时,类名、方法名、参数列表等信息会完整保留,此时可直接通过静态分析定位目标方法并编写Hook脚本。

3.1 定位目标方法

通过反编译工具(如jadx)打开APK,可直接搜索到com.example.fridaapk.CustomEncryptionUtils类,以及其中的encryptdecrypt方法(方法名与源码一致)。

3.2 编写Hook脚本

核心目标是捕获加密/解密的输入(明文/密文、密钥)和输出(密文/明文),同时兼容默认密钥的调用场景(即调用方法时未传入密钥,使用DEFAULT_KEY的情况)。

javascript 复制代码
import Java from "frida-java-bridge";

Java.perform(() => {
    // 获取目标类的引用(类名与源码一致)
    const CustomEncryptionUtils = Java.use("com.example.fridaapk.CustomEncryptionUtils");

    // Hook encrypt方法
    CustomEncryptionUtils.encrypt.overload('java.lang.String', 'java.lang.String').implementation = function (plaintext, key) {
        console.log("\n=========== 加密方法(encrypt)调用 ===========");
        console.log("[输入] 明文 (plaintext):", plaintext);
        console.log("[输入] 密钥 (key):", key);
        
        // 调用原方法获取加密结果
        const ciphertext = this.encrypt(plaintext, key);
        
        console.log("[输出] 密文 (ciphertext):", ciphertext);
        console.log("============================================\n");
        
        return ciphertext; // 返回原方法结果,不影响程序运行
    };

    // Hook decrypt方法
    CustomEncryptionUtils.decrypt.overload('java.lang.String', 'java.lang.String').implementation = function (encryptedData, key) {
        console.log("\n=========== 解密方法(decrypt)调用 ===========");
        console.log("[输入] 密文 (encryptedData):", encryptedData);
        console.log("[输入] 密钥 (key):", key);
        
        // 调用原方法获取解密结果
        const plaintext = this.decrypt(encryptedData, key);
        
        console.log("[输出] 明文 (plaintext):", plaintext);
        console.log("============================================\n");
        
        return plaintext;
    };
});

3.3 脚本执行效果

运行脚本后,当应用调用加密/解密方法时,会输出完整的输入输出日志,例如:

通过日志可直接获取明文、密文、密钥的对应关系,完成对非标准加密的动态分析。

4 混淆APK的Hook流程

当APK经过ProGuard/R8等工具混淆后,自定义类名、方法名会被替换为无意义的短字母(如a.bab),直接定位目标方法变得困难。此时需要通过"间接定位"技巧逐步缩小范围,最终实现Hook。

4.1 混淆带来的问题

混淆工具会对自定义代码进行如下处理:

  • 类名:com.example.fridaapk.CustomEncryptionUtils → 任意字母(例如a.b
  • 方法名:encrypt → 任意字母(例如b)、decrypt → 任意字母(例如a
  • 参数/变量名:全部替换为无意义名称(如p0p1

但系统类(如java.lang.Stringandroid.util.Base64)不会被混淆,这是后续Hook的关键突破口。

4.2 第一步:通过系统方法定位加密逻辑

加密/解密过程必然涉及字符串与字节数组的转换 (如String.getBytes()),因此HookString.getBytes()方法可捕获所有字符串的字节转换操作,结合调用栈分析定位加密相关代码。

4.2.1 定位脚本

该脚本用于探测可能存在加密的地方。

javascript 复制代码
import Java from "frida-java-bridge";

// 打印调用栈的工具函数(用于追踪方法调用链)
function showStacks() {
  var Exception = Java.use("java.lang.Exception");
  var ins = Exception.$new("Exception"); // 通过异常对象获取调用栈
  var straces = ins.getStackTrace();
  if (undefined == straces || null == straces) {
    return;
  }
  console.log("============================= Stack start=======================");
  console.log("");
  for (var i = 0; i < straces.length; i++) {
    var str = " " + straces[i].toString();
    console.log(str);
  }
  console.log("");
  console.log("============================= Stack end=======================\r\n");
  Exception.$dispose(); // 释放对象,避免内存泄漏
}

Java.perform(() => {
  // Hook String类的getBytes方法
  var str = Java.use("java.lang.String");
  str.getBytes.overload()
    .implementation = function () {
      var result = this.getBytes(); // 调用原方法
      var newStr = str.$new(result); // 将字节数组转回字符串,便于查看内容
      console.log("str.getBytes result: ", newStr); // 打印转换后的字符串
      showStacks(); // 打印调用栈
      return result;
    }
  // Hook String类的getBytes方法
  str.getBytes.overload('java.lang.String')
    .implementation = function (a) {
      var result = this.getBytes(a);
      var newStr = str.$new(result, a);
      console.log("str.getBytes result: ", newStr);
      showStacks();
      return result;
    }
});

4.2.2 分析定位结果

运行脚本后,操作应用中涉及加密/解密的功能(如点击"加密"或"解密"按钮),会输出如下日志:

shell 复制代码
# 启动时打印的日志,忽略

str.getBytes result:  AAAAAAAgAgwgLA==
============================= Stack start=======================

 java.lang.String.getBytes(Native Method)
 android.util.Base64.decode(Base64.java:120)	# 涉及Base64解码(可能是解密步骤)
 z2.c.a(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:7) # 自定义方法,紧跟Base64.decode,疑似解密方法
 z2.i.c(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:28)
 n.e1.l(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:139)
 h3.a.m(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:9)
 w3.w.n(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:83)
 w3.f.p(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:114)
 w3.f.D(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:33)
 w3.f.m(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:17)
 b1.w.b0(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:51)
 b1.w.i(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:31)
 m.k.i(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:105)
 b1.f.h(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:150)
 b1.f.h(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:129)
 b1.f.h(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:129)
 b1.f.h(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:129)
 b1.f.h(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:129)
 a1.d.e(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:36)
 b1.q.c(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:144)
 i1.v.E(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:81)
 i1.v.l(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:385)
 i1.v.dispatchTouchEvent(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:76)
 android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3120)
 android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2801)
 android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3120)
 android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2801)
 android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3120)
 android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2801)
 android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3120)
 android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2801)
 com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:498)
 com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1900)
 android.app.Activity.dispatchTouchEvent(Activity.java:4203)
 com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:456)
 android.view.View.dispatchPointerEvent(View.java:14858)
 android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:6468)
 android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:6269)
 android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5734)
 android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5791)
 android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:5757)
 android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:5931)
 android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:5765)
 android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:5988)
 android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5738)
 android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5791)
 android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:5757)
 android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:5765)
 android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5738)
 android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:8742)
 android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:8693)
 android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:8662)
 android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:8865)
 android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:259)
 android.os.MessageQueue.nativePollOnce(Native Method)
 android.os.MessageQueue.next(MessageQueue.java:335)
 android.os.Looper.loopOnce(Looper.java:161)
 android.os.Looper.loop(Looper.java:288)
 android.app.ActivityThread.main(ActivityThread.java:8062)
 java.lang.reflect.Method.invoke(Native Method)
 com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571)
 com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1091)

============================= Stack end=======================

关键信息解读:

  • str.getBytes result: AAAAAAAgAgwgLA==:被转换的字符串是Base64格式,符合加密后的数据特征;
  • 调用栈中android.util.Base64.decode后紧跟z2.c.a并且带了r8-map-id-xxx:说明z2.c.a方法可能在调用Base64解码后进行解密处理,并且经过了R8代码混淆,因此z2.c.a是疑似解密方法。

4.3 第二步:验证疑似方法

通过反编译工具找到z2.c类的a方法,观察其逻辑是否与解密流程一致(如包含逆向位移、异或等操作)。确认后编写测试脚本验证。

4.3.1 测试脚本

javascript 复制代码
import Java from "frida-java-bridge";

Java.perform(() => {
  try {
    // 定位疑似解密方法所在的类
    const DecryptClass = Java.use("z2.c");
    // Hook z2.c类的a方法(参数为密文字符串)
    DecryptClass.a.overload("java.lang.String").implementation = function (encryptedData) {
      console.log("Hook到解密方法 z2.c.a:");
      console.log("[输入] 密文:", encryptedData);
      
      // 调用原方法获取结果
      const result = this.a(encryptedData);
      console.log("[输出] 明文:", result);
      
      return result;
    };
  } catch (error) {
    console.error("Hook执行出错:", error.message); // 捕获类或方法不存在的错误
  }
});

4.3.2 验证结果

运行脚本后,操作应用的非标准算法区域按钮,输出如下日志,说明z2.c.a确实是解密方法:

4.4 第三步:补全加密方法Hook

观察上述反编译代码,继续可定位到加密方法(z2.c.b),最终编写完整脚本。

4.4.1 完整Hook脚本

javascript 复制代码
import Java from "frida-java-bridge";

Java.perform(() => {
  try {
    // 定位加密/解密所在的类(z2.c)
    const CustClass = Java.use("z2.c");
    
    // Hook解密方法 z2.c.a
    CustClass.a.overload("java.lang.String").implementation = function (encryptedData) {
      console.log("Hook到解密方法 z2.c.a:");
      console.log("[输入] 密文:", encryptedData);
      
      const result = this.a(encryptedData);
      console.log("[输出] 明文:", result);
      
      return result;
    };

    // Hook加密方法 z2.c.b
    CustClass.b.implementation = function () {
      console.log("Hook到加密方法 z2.c.b:");
      
      // 获取明文(根据业务场景确定,此处为示例值)
      const plaintext = "FridaStudy";
      console.log("[输入] 明文:", plaintext);
      
      const ciphertext = this.b();
      console.log("[输出] 密文:", ciphertext);
      
      return ciphertext;
    };

  } catch (error) {
    console.error("Hook执行出错:", error.message);
  }
});

4.4.2 最终效果

运行脚本后,加密与解密的输入输出均被成功捕获:

5 本章总结

5.1 未混淆场景Hook要点

  • 核心思路:直接通过类名+方法名定位目标(依赖反编译后可见的代码结构);
  • 关键操作:
    1. 反编译APK,找到加密工具类(如CustomEncryptionUtils);
    2. 确定加密/解密方法的参数列表(如encrypt(String, String));
    3. 编写Hook脚本,打印方法的输入参数(明文/密文、密钥)和返回值;
  • 优势:操作简单,定位精准,适合快速验证。

5.2 混淆场景Hook要点

  • 核心思路:利用"系统类不混淆"的特性,通过Hook基础方法(如String.getBytes()Base64.encode())追踪调用栈,间接定位混淆后的目标方法;
  • 关键步骤:
    1. Hook与加密相关的系统方法(字符串转换、Base64编解码等);
    2. 操作应用触发加密/解密流程,通过日志捕获可疑数据(如Base64字符串);
    3. 分析调用栈,定位可疑的自定义方法(如z2.c.a);
    4. 反编译验证方法逻辑,编写测试脚本确认;
    5. 补全加密/解密方法的Hook,获取完整数据;
  • 优势:不受混淆影响,通用性强,适合复杂场景。
相关推荐
bleach-2 小时前
内网渗透之横向移动&持久化远程控制篇——利用ipc、sc、schtasks、AT,远程连接的winrm,wmic的使用和定时任务的创建
网络·windows·安全·web安全·网络安全·系统安全·安全威胁分析
小虎牙0072 小时前
关于Android Compose架构的思考
android·前端·mvvm
2501_915909063 小时前
手机崩溃日志导出的工程化体系,从系统级诊断到应用行为分析的多工具协同方法
android·ios·智能手机·小程序·uni-app·iphone·webview
木风小助理3 小时前
MySQL内存监控深度解析与故障排查实践
android·adb
灰鲸广告联盟3 小时前
APP广告变现定制化解决方案,助力收益提升与用户体验平衡
android·flutter·搜索引擎·ux
帅得不敢出门4 小时前
精简Android SDK(AOSP)的git项目提高git指令速度
android·java·开发语言·git·elasticsearch
2501_937189234 小时前
神马 9.0 2025 最新版源码系统:安全加固 + 二次开发友好
android·源码·开源软件·源代码管理·机顶盒
モンキー・D・小菜鸡儿4 小时前
Android 中 StateFlow 的使用
android·kotlin
二川bro5 小时前
字符串特性解析:Python不可变性引发的错误
android·开发语言·python