【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 问题,该脚本都能提供直观的监控数据,帮助开发者把控加密环节的安全性与正确性。

相关推荐
音视频牛哥14 小时前
大牛直播SDK(SmartMediaKit)Android平台Unity3D RTSP/RTMP播放器集成实践
android·unity3d·rtsp播放器·rtmp播放器·unity3d rtmp播放器·安卓unity rtsp播放器·安卓unity rtmp播放器
w1wi14 小时前
安卓抓包完全指南(一):从入门到 SSL Pinning 绕过
android·网络协议·ssl
aqi0016 小时前
一文理清 HarmonyOS 6.0.2 涵盖的十个升级点
android·华为·harmonyos·鸿蒙·harmony
路baby16 小时前
RCE漏洞的原理详细讲解并基于pikachu靶场的实战演戏
安全·web安全·网络安全·系统安全·网络攻击模型·安全威胁分析·rce
赏金术士17 小时前
Jetpack Compose 状态提升(State Hoisting)完全指南
android·kotlin·compose
谪星·阿凯17 小时前
第三方应用软件提权全解析
windows·网络安全
BoomHe17 小时前
git Rebase 为任意一笔提交补上 Change-Id
android·git·android studio
TDengine (老段)18 小时前
TDengine 超级表/子表/普通表 — 设计理念与内部表示
android·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
shuaiqinke18 小时前
【分享】Edge浏览器|内置扩展仓库|支持油猴|上网无限制
android·前端·人工智能·edge
Carson带你学Android19 小时前
见证历史!Swift 6.3 官方支持 Android,跨平台要变天了?
android