【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捕获完整信息。
相关推荐
Suckerbin2 小时前
Gaara: 1靶场渗透
安全·web安全·网络安全
lin62534222 小时前
Android右滑解锁UI,带背景流动渐变动画效果
android·ui
Kapibalapikapi3 小时前
工具 | netcat, netstat
网络·笔记·逆向
小白勇闯网安圈4 小时前
Training-WWW-Robots、command_execution、baby_web、xff_referer
网络安全·web
鹏多多5 小时前
Flutter输入框TextField的属性与实战用法全面解析+示例
android·前端·flutter
2501_916008895 小时前
iOS 开发者工具全景图,构建从编码、调试到性能诊断的多层级工程化工具体系
android·ios·小程序·https·uni-app·iphone·webview
Winter_Sun灬6 小时前
CentOS 7 编译安卓 arm64-v8a 版 OpenSSL 动态库(.so)
android·linux·centos
柯南二号6 小时前
【大前端】【Android】用 Python 脚本模拟点击 Android APP —— 全面技术指南
android·前端·python
龚礼鹏6 小时前
图像显示框架六——SurfaceFlinger的初始化以及任务调度(基于Android 15源码分析)
android