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

相关推荐
Kapaseker6 小时前
你不看会后悔的2025年终总结
android·kotlin
alexhilton9 小时前
务实的模块化:连接模块(wiring modules)的妙用
android·kotlin·android jetpack
信创天地9 小时前
信创国产化数据库的厂商有哪些?分别用在哪个领域?
数据库·python·网络安全·系统架构·系统安全·运维开发
ji_shuke10 小时前
opencv-mobile 和 ncnn-android 环境配置
android·前端·javascript·人工智能·opencv
秋42711 小时前
防火墙基本介绍与使用
linux·网络协议·安全·网络安全·架构·系统安全
sunnyday042612 小时前
Spring Boot 项目中使用 Dynamic Datasource 实现多数据源管理
android·spring boot·后端
幽络源小助理13 小时前
下载安装AndroidStudio配置Gradle运行第一个kotlin程序
android·开发语言·kotlin
inBuilder低代码平台13 小时前
浅谈安卓Webview从初级到高级应用
android·java·webview
豌豆学姐13 小时前
Sora2 短剧视频创作中如何保持人物一致性?角色创建接口教程
android·java·aigc·php·音视频·uniapp
白熊小北极13 小时前
Android Jetpack Compose折叠屏感知与适配
android