【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,获取完整数据;
  • 优势:不受混淆影响,通用性强,适合复杂场景。
相关推荐
阿巴斯甜6 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker7 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95278 小时前
Andorid Google 登录接入文档
android
黄林晴9 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android