【Frida Android】实战篇12:企业常用对称加密场景 Hook 教程

文章目录

    • [1 前言](#1 前言)
      • [1.1 对称加密:AES的企业级通用规范](#1.1 对称加密:AES的企业级通用规范)
    • [2. Hook技术](#2. Hook技术)
      • [2.1 核心源码](#2.1 核心源码)
      • [2.2 Hook脚本](#2.2 Hook脚本)
      • [2.3 脚本设计细节](#2.3 脚本设计细节)
        • [2.3.1 辅助函数:字节数组处理与Base64兼容](#2.3.1 辅助函数:字节数组处理与Base64兼容)
        • [2.3.2 Hook Cipher.doFinal():获取加密/解密的核心数据](#2.3.2 Hook Cipher.doFinal():获取加密/解密的核心数据)
        • [2.3.3 Hook Base64.encodeToString():捕获最终传输的密文](#2.3.3 Hook Base64.encodeToString():捕获最终传输的密文)
    • [3. 总结](#3. 总结)

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

1 前言

在企业级应用中,数据安全是业务稳定运行的基石,加密技术作为数据安全的核心手段,对称加密因高效性被广泛应用。其中,AES(高级加密标准)是最主流的对称加密算法,凭借高强度、高兼容性的特点,成为支付信息传输、用户凭证存储、API通信加密等场景的首选方案。

1.1 对称加密:AES的企业级通用规范

AES加密解密使用相同密钥,运算效率远超非对称加密,特别适合处理大量数据。其企业级通用规范确保在各类场景下的安全性:

  1. 密钥管理:禁止硬编码在代码中,必须通过KMS(密钥管理系统)动态获取,且需定期轮换(降低密钥泄露后的风险);
  2. 工作模式:优先使用GCM(认证加密模式,同时保证机密性、完整性、真实性),禁用ECB(相同明文生成相同密文,易被攻击者分析规律);
  3. IV/Nonce(初始化向量):需12-16字节随机生成,同一密钥下绝对不可重复(避免通过密文对比泄露明文信息)。

2. Hook技术

以下是针对Java环境中AES加密体系的Hook脚本及详细解析,可通用监控各类应用的AES加密行为。

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

链接: https://pan.baidu.com/s/10yENJwouZqa41qkcB0QEAQ?pwd=vsu1 提取码: vsu1

2.1 核心源码

示例APK核心代码简要分析

以下是示例APK中AES加密工具类AESUtils的核心逻辑解析,该类采用企业主流的AES-256-GCM方案。

  1. 算法选型 :采用AES/GCM/NoPadding,是企业级加密的首选组合------GCM模式同时保证机密性(加密)、完整性(防篡改)、真实性(防伪造),无需额外填充逻辑。
  2. 关键参数:12字节IV(Nonce)+ 256位密钥 + 128位认证标签,完全符合"随机IV+强密钥+认证校验"的安全规范。
  3. 数据结构 :加密后最终输出Base64(IV+密文),这是Hook时需要重点捕获的"完整密文"------仅抓doFinal返回的密文(不含IV)无法解密,需通过Base64 Hook或IV提取获取完整数据。
java 复制代码
package com.example.fridaapk

import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import android.util.Base64

object AESUtils {
    private const val GCM_IV_LENGTH = 12
    private const val GCM_TAG_LENGTH = 128
    private const val ALGORITHM = "AES/GCM/NoPadding"

    // 注意:在实际应用中,应通过 KMS 系统获取密钥而不是本地生成
    fun generateKey(): SecretKey {
        val keyGenerator = KeyGenerator.getInstance("AES")
        keyGenerator.init(256)
        return keyGenerator.generateKey()
    }

    fun encrypt(plaintext: String, key: SecretKey): String {
        val cipher = Cipher.getInstance(ALGORITHM)
        val iv = ByteArray(GCM_IV_LENGTH)
        SecureRandom().nextBytes(iv)

        val spec = GCMParameterSpec(GCM_TAG_LENGTH, iv)
        cipher.init(Cipher.ENCRYPT_MODE, key, spec)
        val ciphertext = cipher.doFinal(plaintext.toByteArray(Charsets.UTF_8))

        val encryptedData = ByteArray(iv.size + ciphertext.size)
        System.arraycopy(iv, 0, encryptedData, 0, iv.size) // 前12字节:IV
        System.arraycopy(ciphertext, 0, encryptedData, iv.size, ciphertext.size)

        return Base64.encodeToString(encryptedData, Base64.NO_WRAP)
    }

    fun decrypt(encryptedData: String, key: SecretKey): String {
        val decodedData = Base64.decode(encryptedData, Base64.NO_WRAP) 
        val iv = ByteArray(GCM_IV_LENGTH)
        val ciphertext = ByteArray(decodedData.size - GCM_IV_LENGTH)

        System.arraycopy(decodedData, 0, iv, 0, GCM_IV_LENGTH)
        System.arraycopy(decodedData, GCM_IV_LENGTH, ciphertext, 0, ciphertext.size)

        val cipher = Cipher.getInstance(ALGORITHM)
        val spec = GCMParameterSpec(GCM_TAG_LENGTH, iv)
        cipher.init(Cipher.DECRYPT_MODE, key, spec)
        val plaintext = cipher.doFinal(ciphertext)
        return String(plaintext, Charsets.UTF_8)
    }
}

2.2 Hook脚本

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

// 字节数组转明文
function bytesToString(byteArr) {
  if (!byteArr || byteArr.length === 0) return "空";
  try {
    const str = Java.use("java.lang.String").$new(byteArr, "UTF-8");
    return str.toString();
  } catch (e) {
    return getHexBytes(byteArr);
  }
}

// 字节数组转16进制
function getHexBytes(byteArr) {
  if (!byteArr || byteArr.length === 0) return "空";
  let hex = "";
  for (let i = 0; i < byteArr.length; i++) {
    const b = byteArr[i] & 0xFF;
    hex += (b < 16 ? "0" : "") + b.toString(16);
  }
  return hex;
}

// Base64编码(兼容android.util.Base64和java.util.Base64)
function encodeBase64(data) {
  try {
    return Java.use("android.util.Base64").encodeToString(data, 0);
  } catch (e) {
    try {
      const base64Encoder = Java.use("java.util.Base64").getEncoder();
      return base64Encoder.encodeToString(data);
    } catch (e2) {
      return "<Base64编码失败>";
    }
  }
}

try {
  Java.perform(() => {
    const Cipher = Java.use("javax.crypto.Cipher");
    const Base64Android = Java.use("android.util.Base64");
    const Base64Java = Java.use("java.util.Base64");

    // Hook doFinal 方法
    Cipher.doFinal.overload('[B').implementation = function (input) {
      const result = this.doFinal(input);

      try {
        const algorithm = this.getAlgorithm();
        const mode = this.opmode.value;

        if (algorithm && algorithm.includes("AES")) {
          console.log(`=== 操作详情 ===`);
          console.log(`[加密/解密] 算法名称: ${algorithm}`);
          
          if (mode == 1) { // 加密模式
            console.log(`[加密] 明文: ${bytesToString(input)}`);
            console.log(`[加密] 密文(Base64): ${encodeBase64(result)}`);
          } else if (mode == 2) { // 解密模式
            console.log(`[解密] 密文(Base64): ${encodeBase64(input)}`);
            console.log(`[解密] 明文: ${bytesToString(result)}`);
          }
        }
      } catch (e) {
        console.log(`[Cipher] 解析数据出错: ${e.message}`);
      }
      return result;
    };

    // Hook Base64.encodeToString 方法
    Base64Android.encodeToString.overloads.forEach(func => {
      func.implementation = function (data, flags) {
        const result = this.encodeToString(data, flags);
        console.log(`[Base64编码] 疑似密文: ${result}`);
        return result;
      };
    });

    // Hook java.util.Base64.encoder.encodeToString 方法
    try {
      const encoder = Base64Java.getEncoder();
      encoder.encodeToString.overloads.forEach(func => {
        func.implementation = function (data) {
          const result = this.encodeToString(data);
          console.log(`[Base64编码] 疑似密文: ${result}`);
          return result;
        };
      });
    } catch (e) {
      // 如果不可用,忽略
    }

  });
} catch (e) {
  console.log(`[Frida] 脚本执行异常: ${e.message}`);
}

启动脚本run.py和前面一样:

python 复制代码
import frida
import sys

# 目标应用包名
PACKAGE_NAME = "com.example.fridaapk"

def main():
    """
    启动目标应用并注入 Frida 脚本。
    """
    try:
        # 获取连接的 USB 设备
        print("正在寻找 USB 设备...")
        device = frida.get_usb_device(timeout=10)
        print(f"成功连接到设备: {device.name}")

        # 启动应用并附加
        print(f"正在启动应用: {PACKAGE_NAME}...")
        pid = device.spawn([PACKAGE_NAME])
        session = device.attach(pid)
        print(f"应用启动成功,进程 ID (PID): {pid}")

        # 加载并注入 JS 脚本
        print("正在注入 Frida 脚本...")
        with open("./js/compiled_hook.js", "r", encoding="utf-8") as f:
            js_code = f.read()
        
        script = session.create_script(js_code)
        
        script.load()
        print("脚本注入成功!")

        # 恢复应用运行
        device.resume(pid)
        print("应用已恢复运行,开始监控 Frida 输出...")

        # 保持脚本运行,直到用户按下 Ctrl+C
        try:
            sys.stdin.read()
        except KeyboardInterrupt:
            print("用户中断,正在退出...")

    except frida.TimedOutError:
        print("错误:未找到连接的 USB 设备。请确保设备已连接并开启 USB 调试。")
    except frida.ProcessNotFoundError:
        print(f"错误:无法找到或启动应用 '{PACKAGE_NAME}'。请确保应用已安装。")
    except FileNotFoundError:
        print(f"错误:Frida 脚本文件未找到。请检查路径 './js/compiled_hook.js' 是否正确。")
    except Exception as e:
        print(f"发生未知错误: {e}")
    finally:
        print("程序已退出。")

if __name__ == "__main__":
    main()

运行启动脚本后,操作示例 APK 的对称加密区域,输出结果如下:

可以看到AES加密密文,与 Hook 住的第2个 Base64 的密文不同,因为我在 APK 中输出的最终密文是随机 IV 和 AES密文的拼接,可以看到AES加密密文是最终密文的后半部分。

2.3 脚本设计细节

Java中所有AES加密/解密操作均通过javax.crypto.Cipher类完成,而密文通常会经过Base64编码后用于传输或存储。脚本通过监控关键节点,实现对AES加密全流程的可视化。

2.3.1 辅助函数:字节数组处理与Base64兼容
  • bytesToString 函数

    作用:将加密/解密过程中的字节数组转为UTF-8字符串,方便直接查看明文(如用户输入的密码、API请求参数等)。

    必要性:加密操作的输入输出都是字节数组,直接打印会显示乱码,通过该函数可直观呈现可读的明文内容。

  • getHexBytes 函数

    作用:当字节数组无法转为UTF-8字符串(如二进制密文、随机生成的IV)时,转为十六进制字符串展示。

    必要性:确保所有字节数据都能以人类可读的形式呈现,避免因编码问题导致关键信息丢失。

  • encodeBase64 函数

    作用:兼容Android和Java平台的Base64编码逻辑,将字节数组转为Base64字符串。

    必要性:实际应用中,密文常以Base64形式传输(如接口中的encryptedData字段),且不同平台可能使用android.util.Base64(Android)或java.util.Base64(Java),兼容两者才能确保获取完整的最终密文(例如示例APK中可能采用"IV+密文"拼接后再Base64编码的格式,需通过该函数正确解析)。

2.3.2 Hook Cipher.doFinal():获取加密/解密的核心数据

作用:doFinal是AES实际执行加密/解密的方法------加密时输入明文字节数组、输出密文字节数组;解密时输入密文字节数组、输出明文字节数组。通过Hook该方法,可直接获取:

  • 加密前的原始明文和加密后的密文(字节数组及Base64形式);
  • 解密前的密文(字节数组及Base64形式)和解密后的原始明文。

必要性:这是加密流程的"结果输出口",不Hook此方法就无法获取实际参与传输/存储的密文和原始明文,无法验证加密逻辑是否正确(如明文密文是否匹配)。

2.3.3 Hook Base64.encodeToString():捕获最终传输的密文

作用:监控所有Base64编码操作,输出编码后的字符串(疑似密文)。

必要性:

  1. 多数场景下,AES生成的二进制密文会经过Base64编码后才用于传输(如HTTP请求体、数据库存储),仅HookCipher只能拿到二进制密文,无法获取实际传输的"最终密文";
  2. 部分应用会对"IV+密文""密文+校验值"等组合数据进行Base64编码,Hook该方法可捕获完整的拼接后密文,便于还原加密数据结构;
  3. 兼容Android和Java的Base64实现,避免因平台差异漏掉关键密文(例如某些应用在不同版本中切换Base64工具类,不全面Hook会导致监控断层)。

3. 总结

该Hook脚本通过监控AES加密流程的核心节点,实现了对加密行为的全链路可视化:

  1. 借助bytesToStringgetHexBytes确保字节数据可读;
  2. 通过encodeBase64兼容多平台Base64编码,获取最终传输的密文;
  3. HookCipher.doFinal获取加密/解密的原始数据,验证明文密文对应关系;
  4. 监控Base64编码过程,捕获实际传输的密文形式。

无论是验证AES是否符合"GCM模式+随机IV+动态密钥"的企业规范,还是调试加密逻辑中的数据 mismatch 问题,该脚本都能提供直观的监控数据,帮助开发者把控加密环节的安全性与正确性。

相关推荐
モンキー・D・小菜鸡儿1 小时前
Android15 新特性与适配指南
android·kotlin·安卓新特性
星环处相逢2 小时前
MySQL数据库索引与事务:从基础到实践的全面解析
android
Kin__Zhang2 小时前
随手记录 UE4/CARLA 仿真器 segmentation fault
android·java·ue4
明君879972 小时前
Flutter横向树形选择器实现方案
android·ios
CrazyQ12 小时前
flutter_easy_refresh在3.38.3配合NestedScrollView的注意要点。
android·flutter·dart
北京耐用通信3 小时前
阀岛的“超级大脑”:耐达讯自动化网关让EtherNet/IP转DeviceNet“说同一种语言”
人工智能·物联网·网络协议·网络安全·自动化·信息与通信
三七吃山漆3 小时前
攻防世界——fakebook
android·网络安全·web·ctf
二川bro4 小时前
类型错误详解:Python TypeError排查手册
android·java·python
TeleostNaCl4 小时前
在小米 Hyper OS 2 上使用开发者选项关闭视频彩铃功能
android·经验分享