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

文章目录

  • [1. 前言](#1. 前言)
  • [2. Hook](#2. Hook)
    • [2.1 核心加密源码分析](#2.1 核心加密源码分析)
    • [2.2 Hook 完整脚本](#2.2 Hook 完整脚本)
    • [2.3 Hook脚本详细分析](#2.3 Hook脚本详细分析)
      • [2.3.1 `bytesToString(byteArr)`](#2.3.1 bytesToString(byteArr))
      • [2.3.2 `getHexBytes(byteArr)`](#2.3.2 getHexBytes(byteArr))
      • [2.3.3 `encodeBase64(data)`](#2.3.3 encodeBase64(data))
      • [2.3.4 核心Hook逻辑(`Cipher.doFinal`)](#2.3.4 核心Hook逻辑(Cipher.doFinal))
  • [3. 本章总结](#3. 本章总结)
    • [3.1 重要知识点](#3.1 重要知识点)
    • [3.2 Hook思路](#3.2 Hook思路)

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

1. 前言

非对称加密通过公钥(公开可访问)与私钥(严格保密)的密钥对实现数据交互,其中RSA作为应用最广泛的非对称算法,在企业场景中承担着核心安全职责,其主要用途包括:

  • 密钥交换:安全传输对称加密密钥(如AES密钥通过RSA公钥加密后传输,避免明文泄露);
  • 数字签名:通过私钥对数据哈希值签名,接收方用公钥验签,确保数据未被篡改且来源可信(如API接口防伪造、软件安装包合法性验证)。

大厂对RSA的安全规范要求极为严格,具体如下:

  • 密钥长度:最低强制2048位(随着算力提升,该标准可能进一步提高);金融、政务等核心敏感场景必须使用4096位(密钥越长破解难度指数级增加,但运算效率略有降低,需在安全与性能间平衡)。
  • 数字签名配套算法:单独使用RSA仅能保证机密性,无法防止数据篡改,因此必须结合SHA-256(最低要求)或SHA-512等强哈希算法------流程为"数据哈希→私钥签名→公钥验签+哈希比对",双重保障完整性。
  • 密钥管理:私钥必须离线存储(如硬件安全模块HSM),禁止硬编码在代码中;密钥需定期轮换(通常90-180天),避免长期使用导致泄露风险。
  • 合规性:金融领域需符合PCI DSS标准,政务领域需满足等保2.0三级及以上要求,确保算法实现符合国家或行业加密标准。

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

链接: https://pan.baidu.com/s/1M2sVSNuvFFz4VmPIRNc8yw?pwd=qnxa

提取码: qnxa

2. Hook

2.1 核心加密源码分析

以下是示例 APK 中 RSA 加密工具类RSAUtils的核心逻辑解析,该类采用企业级RSA-2048方案,适配大文本分段加解密。

  1. 算法选型 :采用RSA/ECB/OAEPWithSHA-256AndMGF1Padding,是企业级RSA的"安全标配"------相比基础的PKCS1Padding,OAEP填充抗破解能力更强,结合SHA-256哈希算法,完全符合大厂"强填充+强哈希"的安全基线。
  2. 密钥规格:强制使用2048位密钥(企业非对称加密最低要求,金融/政务场景可升级至4096位),密钥对动态生成(无硬编码),符合"密钥动态管理"的企业规范。
  3. 分段加解密:RSA存在明文长度限制(2048位密钥的加密块大小=2048/8 - 42=214字节),因此代码实现了分段加密 / 解密逻辑。
kotlin 复制代码
package com.example.fridaapk

import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.PublicKey
import android.util.Base64
import javax.crypto.Cipher

object RSAUtils {
    private const val KEY_SIZE_2048 = 2048
    private const val TRANSFORMATION = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"

    fun generateRSA2048KeyPair(): KeyPair {
        val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
        keyPairGenerator.initialize(KEY_SIZE_2048)
        return keyPairGenerator.generateKeyPair()
    }

    fun encryptRSA2048(plaintext: String, publicKey: PublicKey): String {
        val cipher = Cipher.getInstance(TRANSFORMATION)
        cipher.init(Cipher.ENCRYPT_MODE, publicKey)

        val plainBytes = plaintext.toByteArray(Charsets.UTF_8)
        val maxBlockSize = getMaxEncryptBlockSize()

        val encryptedData = mutableListOf<ByteArray>()
        var offset = 0

        while (offset < plainBytes.size) {
            val blockSize = minOf(maxBlockSize, plainBytes.size - offset)
            val block = plainBytes.copyOfRange(offset, offset + blockSize)
            val encryptedBlock = cipher.doFinal(block)
            encryptedData.add(encryptedBlock)
            offset += blockSize
        }

        val result = encryptedData.flatten()
        return Base64.encodeToString(result, Base64.NO_WRAP)
    }

    fun decryptRSA2048(encryptedData: String, privateKey: PrivateKey): String {
        val cipher = Cipher.getInstance(TRANSFORMATION)
        cipher.init(Cipher.DECRYPT_MODE, privateKey)

        val encryptedBytes = Base64.decode(encryptedData, Base64.NO_WRAP)
        val maxDecryptBlockSize = getMaxDecryptBlockSize()

        val decryptedData = mutableListOf<ByteArray>()
        var offset = 0

        while (offset < encryptedBytes.size) {
            val blockSize = minOf(maxDecryptBlockSize, encryptedBytes.size - offset)
            val block = encryptedBytes.copyOfRange(offset, offset + blockSize)
            val decryptedBlock = cipher.doFinal(block)
            decryptedData.add(decryptedBlock)
            offset += blockSize
        }

        val result = decryptedData.flatten()
        return String(result, Charsets.UTF_8)
    }

    private fun getMaxEncryptBlockSize(): Int {
        return (KEY_SIZE_2048 / 8) - 42
    }

    private fun getMaxDecryptBlockSize(): Int {
        return KEY_SIZE_2048 / 8
    }

    private fun List<ByteArray>.flatten(): ByteArray {
        val totalSize = this.sumOf { it.size }
        val result = ByteArray(totalSize)
        var offset = 0
        for (array in this) {
            System.arraycopy(array, 0, result, offset, array.size)
            offset += array.size
        }
        return result
    }
}

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, 2);
  } 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");

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

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

        console.log(`\n=== 操作详情 ===`);
        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;
    };
  });
} catch (e) {
  console.log(`[Frida] 脚本执行异常: ${e.message}`);
}

2.3 Hook脚本详细分析

脚本基于Frida框架,通过Hookjavax.crypto.Cipher类的doFinal方法捕获加密解密过程,核心方法及功能如下:

2.3.1 bytesToString(byteArr)

  • 功能:将字节数组转换为可读字符串,优先尝试UTF-8编码(适用于文本类明文/解密结果);若转换失败(如密文字节非文本格式),则调用getHexBytes转为16进制字符串。
  • 作用:解决字节数组直接打印不可读的问题,方便直观查看明文或解析异常数据。
javascript 复制代码
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);
  }
}

2.3.2 getHexBytes(byteArr)

  • 功能:将字节数组转换为16进制字符串(如[0x1F, 0x2A]转为"1f2a")。
  • 作用:处理非文本类字节数据(如密文、二进制协议数据),确保字节信息可完整展示(16进制是字节的通用可读格式)。
javascript 复制代码
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;
}

2.3.3 encodeBase64(data)

  • 功能:将字节数组转为Base64字符串,兼容Android(android.util.Base64)和Java(java.util.Base64)两种实现。
  • 作用:密文通常以Base64格式传输或存储,该方法确保hook中捕获的密文字节能正确转换为业务中实际使用的Base64格式。
javascript 复制代码
function encodeBase64(data) {
  try {
    return Java.use("android.util.Base64").encodeToString(data, 2); // 2对应NO_WRAP模式
  } catch (e) {
    try {
      const base64Encoder = Java.use("java.util.Base64").getEncoder();
      return base64Encoder.encodeToString(data);
    } catch (e2) {
      return "<Base64编码失败>";
    }
  }
}

2.3.4 核心Hook逻辑(Cipher.doFinal

  • 目标:HookCipher类的doFinal(byte[])方法(加密/解密的最终执行步骤)。
  • 流程:
    1. 调用原方法获取结果(this.doFinal(input));
    2. 通过getAlgorithm()获取算法(如AES/GCM/NoPadding),通过opmode判断模式(1为加密,2为解密);
    3. 针对加密模式:打印明文(input)和Base64密文(result);
    4. 针对解密模式:打印Base64密文(input)和明文(result)。
javascript 复制代码
Java.perform(() => {
  const Cipher = Java.use("javax.crypto.Cipher");
  Cipher.doFinal.overload('[B').implementation = function (input) {
    const result = this.doFinal(input);
    try {
      const algorithm = this.getAlgorithm();
      const mode = this.opmode.value;
      console.log(`\n=== 操作详情 ===`);
      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;
  };
});

RSA脚本无需Hook Base64兜底的原因(对比AES)

与上一篇教程的AES的Hook需额外Hook Base64不同,本篇RSA无需兜底,核心差异在于数据结构和处理流程:

  1. 数据结构复杂度不同

    • AES加密后的数据是复合结构 (如GCM模式中需拼接IV和密文,IV是解密必需的元数据),通常需要Base64编码后传输,此时Base64处理的是"IV+密文"的组合数据,若仅HookdoFinal可能遗漏完整信息。
    • RSA加密输出是单一密文块(无额外元数据),加密结果本身就是可直接使用的完整数据,无需拼接其他信息。
  2. Base64编码的作用不同

    • AES中Base64编码是为了处理"二进制+元数据"的复合结构(确保传输兼容性),编码对象是组合数据,可能在doFinal之后单独调用Base64方法,因此需要Hook Base64兜底。
    • RSA中Base64仅用于单一密文的"二进制→文本"转换(方便传输),编码对象就是doFinal的输出结果,通过HookdoFinal已能捕获完整密文,无需额外拦截Base64。
  3. 操作流程的直接性

    • AES的加密流程通常是"加密(doFinal)→拼接IV和密文→Base64编码",多步骤处理可能导致doFinal的输出不是最终传输的密文。
    • RSA的加密流程通常是"加密(doFinal)→Base64编码",doFinal的输出是核心密文,Base64仅为格式转换,不影响数据完整性,因此doFinal足以捕获关键信息。

启动 Hook 脚本后操作应用的非对称加密区域,执行效果如下图:

3. 本章总结

3.1 重要知识点

  1. RSA安全规范:密钥长度最低2048位(核心场景4096位),必须结合SHA-256及以上哈希算法实现数字签名,私钥需安全存储并定期轮换。
  2. AES-GCM实现细节:采用256位密钥,依赖12字节IV(随机生成)和128位Tag(完整性校验),加密后需拼接IV和密文再Base64编码。
  3. Frida Hook核心 :通过拦截javax.crypto.CipherdoFinal方法,可捕获加密/解密的输入(明文/密文)和输出(密文/明文),结合数据转换工具(字节→字符串/16进制/Base64)实现过程可视化。

3.2 Hook思路

  1. 目标选择 :加密算法的核心处理方法(如Cipher.doFinal)是Hook的关键,因其是加密/解密的最终执行步骤,能直接获取原始输入输出。
  2. 数据解析 :通过工具方法(bytesToStringgetHexBytesencodeBase64)处理字节数组,将不可读的二进制数据转换为可读格式(文本、16进制、Base64)。
  3. 模式区分 :利用Cipheropmode属性区分加密(1)和解密(2)模式,分别打印对应的数据流向(明文→密文或密文→明文)。
  4. 场景适配 :针对不同加密算法(AES/RSA)的特点调整Hook策略------AES需考虑复合数据结构可能需Hook Base64,RSA因数据结构简单可直接通过doFinal捕获完整信息。
相关推荐
Kapaseker1 天前
你不看会后悔的2025年终总结
android·kotlin
alexhilton1 天前
务实的模块化:连接模块(wiring modules)的妙用
android·kotlin·android jetpack
信创天地1 天前
信创国产化数据库的厂商有哪些?分别用在哪个领域?
数据库·python·网络安全·系统架构·系统安全·运维开发
ji_shuke1 天前
opencv-mobile 和 ncnn-android 环境配置
android·前端·javascript·人工智能·opencv
秋4271 天前
防火墙基本介绍与使用
linux·网络协议·安全·网络安全·架构·系统安全
sunnyday04261 天前
Spring Boot 项目中使用 Dynamic Datasource 实现多数据源管理
android·spring boot·后端
幽络源小助理1 天前
下载安装AndroidStudio配置Gradle运行第一个kotlin程序
android·开发语言·kotlin
inBuilder低代码平台1 天前
浅谈安卓Webview从初级到高级应用
android·java·webview
豌豆学姐1 天前
Sora2 短剧视频创作中如何保持人物一致性?角色创建接口教程
android·java·aigc·php·音视频·uniapp
白熊小北极1 天前
Android Jetpack Compose折叠屏感知与适配
android