某大学生常用APP抓包数据加密流程分析

华盟原创文章投稿奖励计划

文章作者:988hvjnkj

文章来源:https://www.freebuf.com/articles/mobile/453346.html

文章转载/排版来自于神农Sec


01

0x1 RPA机器人自动化公众号文章发布

一、原始抓包

1.1抓包

自动草稿自动草稿

1.2 抓包结果分析

  • 核心发现:请求参数仅存在1个——参数d,且整个数据包已被加密,无法直接读取原始业务数据(如账号、密码),需通过逆向分析还原加密流程。

二、逆向分析:定位核心代码与参数

2.1 登录接口定位

通过搜索登录相关关键词(如“login/phone”),定位到APP源码中登录功能的核心常量与请求发送方法:

--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

// 登录接口URL常量 public static final String f6071I4 = "login/phone";  // 登录请求发送方法:str=账号,str2=原始密码,httpCallBack=回调对象 public static void m10245e(String str, String str2, HttpCallBack httpCallBack) {     HashMap<String, String> hashMap = C2165k.getHashMap();     hashMap.put("account", str); // 存入账号参数     hashMap.put("pwd", C2428c.m6288d(str2)); // 存入经加密处理的密码     // 发送POST请求:传入接口URL、参数集合、回调函数     BaseRequest.post(C2165k.getHostUrlV2(InterfaceC2418c.f6071I4), hashMap, httpCallBack.mCallback); }

2.2 逆向核心目标

需解决3个关键问题,逐步还原加密流程:

  1. 1.

    C2165k.getHashMap():获取的通用参数集合包含哪些内容?

  2. 2.

    C2428c.m6288d(str2):该方法对原始密码(str2)进行了何种加密处理?

  3. 3.

    BaseRequest.post():如何利用传入的hashMap构建最终加密参数d?

三、关键参数解析:通用hashMap集合

3.1 C2165k.getHashMap()方法源码

该方法用于封装APP所有请求的通用参数,源码如下:

--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

public static HashMap<String, String> getHashMap() {     HashMap<String, String> hashMap = new HashMap<>();     // 未登录状态下,uid和token为空(登录后会添加)     if (C5772a.m10514g() != null && !TextUtils.isEmpty(C5772a.m10509b()) && !TextUtils.isEmpty(C5772a.m10513f())) {         hashMap.put(InterfaceC2417b.f5966a, C5772a.m10509b()); // 添加uid(未登录时无值)         hashMap.put("token", C5772a.m10513f()); // 添加用户凭证token(未登录时无值)     }     // 添加极光推送ID(jPushId),推测用于推送服务(如广告、通知)     String registrationID = JPushInterface.getRegistrationID(App.getContext());     if (!TextUtils.isEmpty(registrationID)) {         hashMap.put("jPushId", registrationID);         InterfaceC2423h.f6552e.mo6554c("jpush_registrationID = " + registrationID, new Object[0]);     }     // 添加APP版本号     hashMap.put("version", C2108a.f5088e);     return hashMap; }

3.2 未登录状态下的hashMap值获取(Hook脚本)

因未登录时uidtoken为空,仅需获取jPushIdversion,可通过Hook脚本直接捕获方法返回值:

--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

setTimeout(function() {     Java.perform(function() {         console.log("开始Hook C2165k.getHashMap()");         const KClass = Java.use("你的类名"); // 需替换为实际C2165k类名         KClass.getHashMap.implementation = function() {             const app = this.getHashMap(); // 执行原方法并获取返回值             console.log("原始通用hashmap:" + app);             return app;         };     }); }, 8000); // 延迟8秒执行,避免APP未加载完成

自动草稿

原始通用hashmap:{jPushId=100d8559086045b508a, version=4.8.3}

四、密码加密逻辑:C2428c.m6288d()方法解析

4.1 方法源码与核心问题

该方法用于处理原始密码,源码中存在关键错误,需结合逻辑分析修正:

--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

import java.security.Key; import java.security.Security; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import org.bouncycastle.jce.provider.BouncyCastleProvider;  public class C2428c {     private static final String f6621a = "514345744E41596C4E41496C"; // 24字节3DES密钥(示例)     private static final String f6622b = "DESede/ECB/PKCS7Padding"; // 3DES加密配置(算法+模式+填充)     private static C2428c f6623c;      // 密码加密方法:调用m6299e()实现加密     public static String m6288d(String str) {         try {             return m6291i().m6299e(str, f6621a);         } catch (Exception e2) {             e2.printStackTrace();             return str; // 异常时返回原始密码(安全性缺陷)         }     }      // 单例模式获取C2428c实例,添加BouncyCastle加密提供者     public static C2428c m6291i() {         if (f6623c == null) {             synchronized (C2428c.class) {                 if (f6623c == null) {                     Security.addProvider(new BouncyCastleProvider());                     f6623c = new C2428c();                 }             }         }         return f6623c;     }      // 密钥创建方法(核心错误点)     private Key m6292j(String str) {         // 错误:SecretKeySpec需传入纯算法名("DESede"),而非含模式/填充的完整字符串         return new SecretKeySpec(str.getBytes(), f6622b);      }      private Key m6293k(byte[] bArr) {         // 同上述错误:算法参数传入错误         return new SecretKeySpec(bArr, f6622b);      }      // 加密核心逻辑:3DES加密+十六进制转换     public String m6299e(String str, String str2) throws Exception {         Cipher.getInstance(f6622b).init(1, m6292j(str2));         return m6296n(m6300f(str.getBytes(), str2.getBytes()));     }      // 字节数组转十六进制字符串     private String m6296n(byte[] bArr) {         return new String(m6295m(bArr));     }      private byte[] m6295m(byte[] bArr) {         byte[] bArr2 = new byte[bArr.length * 2];         for (int i2 = 0; i2 < bArr.length; i2++) {             byte b = (byte) ((bArr[i2] >> 4) & 15);             byte b2 = (byte) (bArr[i2] & 15);             int i3 = i2 * 2;             bArr2[i3] = (byte) (b < 10 ? b + 48 : b + 55);             bArr2[i3 + 1] = (byte) (b2 < 10 ? b2 + 48 : b2 + 55);         }         return bArr2;     }      // 执行3DES加密     private byte[] m6300f(byte[] bArr, byte[] bArr2) throws Exception {         Cipher cipher = Cipher.getInstance(f6622b);         cipher.init(1, m6293k(bArr2));         return cipher.doFinal(bArr);     } }

4.2 代码错误分析(结合AI解读)

需从“原本意图→具体错误→后果→实际加密方式”四维度拆解:

1. 原本意图

实现标准3DES加密(Triple DES),配置如下:

  • 算法:DESede(3DES)

  • 模式:ECB(电子密码本模式)

  • 填充:PKCS7Padding

  • 密钥:24字节字符串(符合3DES密钥长度要求)

2. 具体错误

SecretKeySpec算法参数传入错误
创建密钥时,需传入纯算法名("DESede"),但代码中传入了含模式/填充的完整字符串("DESede/ECB/PKCS7Padding")。
SecretKeySpec仅负责定义密钥对应的算法,模式和填充是Cipher(加密器)的配置项,与密钥无关。

3. 错误后果

  • 密钥初始化异常:虽Java加密框架可能容错(忽略模式/填充,提取"DESede"),但不符合规范,可能导致加密强度退化(如等效单DES)。

  • 加密结果异常:标准3DES密文长度应为8字节整数倍(对应十六进制16字符整数倍),但代码加密"111111"后得到15字符结果(如C3EAB353AA425226),长度异常。

4. 实际加密方式

仍为错误实现的3DES加密:核心算法基于3DES,但因密钥参数错误,流程不标准,安全性降低。

4.3 Python还原错误3DES加密逻辑

通过Python模拟代码中的错误加密流程,还原密码加密结果:

--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

import pyDes import binascii  def java_like_hex_encode(byte_array):     """模拟Java中m6295m()的字节数组转十六进制逻辑"""     hex_chars = []     for b in byte_array:         # 模拟Java有符号byte(pyDes返回无符号字节)         b_signed = b if b < 128 else b - 256         high = (b_signed >> 4) & 0x0F         low = b_signed & 0x0F         # 转十六进制字符(0-9→48-57,A-F→65-70)         high_char = chr(high + 48) if high < 10 else chr(high + 55)         low_char = chr(low + 48) if low < 10 else chr(low + 55)         hex_chars.append(high_char + low_char)     return ''.join(hex_chars)  def encrypt_111111():     # 1. 明文:原始密码"111111"(UTF-8编码)     plaintext = "111111".encode('utf-8')     # 2. 密钥:对应Java中的f6621a(24字节)     key_str = "你的密钥"     key = key_str.encode('utf-8')     # 3. 初始化3DES加密器(ECB模式+PKCS7填充)     cipher = pyDes.triple_des(key, mode=pyDes.ECB, pad=None, padmode=2)     # 4. 执行加密(自动处理PKCS7填充)     ciphertext = cipher.encrypt(plaintext)     # 5. 模拟Java十六进制转换     encrypted_hex = java_like_hex_encode(ciphertext)     return encrypted_hex  # 测试:加密"111111" result = encrypt_111111() print("加密结果:", result)  # 输出示例:C3EAB353AA425226(因错误实现,长度可能异常)

五、最终请求参数d构建:BaseRequest.post()方法解析

该方法是加密流程核心,负责将通用参数、密码、时间戳、签名整合为加密参数d,分两阶段解析:

5.1 阶段一:添加timestamp与signToken

1. 源码关键逻辑

--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

public static InterfaceC9173e post(String str, Map<String, String> map, InterfaceC9175f interfaceC9175f) {     // 日志:打印请求URL与原始参数     InterfaceC2423h.f6548a.mo6554c("Request " + str + "\nParameters " + map, new Object[0]);      // 1. 添加时间戳(timestamp):毫秒级时间戳,防重复请求     map.put(InterfaceC2421f.f6488f0, String.valueOf(System.currentTimeMillis()));     // 2. 添加签名(signToken):防数据篡改,通过C2434i.m6346d()生成     map.put(InterfaceC2421f.f6486e0, C2434i.m6346d(map));     // 3. 加密生成最终参数d:调用C2430e.m6301a()     Map<String, String> m6301a = C2430e.m6301a(map);      // 后续为HTTP请求头构建、请求发送逻辑(略)     return mo28839a; }

2. signToken生成逻辑(C2434i.m6346d())

signToken通过SHA-512哈希+多步字符串处理生成,确保参数未被篡改,源码与步骤如下:

--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

public static String m6346d(Map<String, String> map) {     // 步骤1:将无序HashMap转为有序TreeMap(按Key自然排序,避免顺序影响哈希结果)     TreeMap treeMap = new TreeMap();     for (Map.Entry<String, String> entry : map.entrySet()) {         treeMap.put(entry.getKey(), entry.getValue() == null ? "" : entry.getValue());     }      // 步骤2:序列化TreeMap为字节数组     byte[] bytes = C2451f.m6453f(treeMap).getBytes();      try {         // 步骤3:计算SHA-512哈希值         MessageDigest messageDigest = MessageDigest.getInstance(MessageDigestAlgorithms.SHA_512);         messageDigest.update(bytes);         byte[] digestBytes = messageDigest.digest();          // 步骤4:多步处理哈希结果(m6343a→m6344b→m6345c→m6696r)         return C2480d0.m6696r(m6345c(m6344b(m6343a(digestBytes))));     } catch (Exception unused) {         return null;     } }

3. 哈希结果处理四步曲

步骤

方法

输入

处理逻辑

输出

1

m6343a

64字节SHA-512原始哈希

字节数组转128位十六进制字符串(1字节=2位)

128位十六进制字符串

2

m6344b

128位字符串

提取奇数位索引(0开始)字符

64位字符串

3

m6345c

64位字符串

提取偶数位索引(0开始)字符

32位字符串

4

C2480d0.m6696r

32位字符串

计算MD5哈希→转32位十六进制→大写

32位大写MD5字符串

5.2 阶段二:加密生成参数d(C2430e.m6301a())

该方法是参数d的核心加密逻辑,需拆分为四步还原:

1. 源码核心片段

--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

public static Map<String, String> m6301a(Map<String, String> map) {     // 步骤1:TreeMap排序(同signToken生成,确保顺序一致)     TreeMap treeMap = new TreeMap();     Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();     while (true) {         String str2 = "";         if (!it.hasNext()) break;         Map.Entry<String, String> next = it.next();         String key = next.getKey();         if (next.getValue() != null) str2 = next.getValue();         treeMap.put(key, str2);     }      HashMap hashMap = new HashMap();     String m6281f = AbstractC2426a.m6281f(); // 步骤2:生成16位随机串(AES密钥)      try {         // 步骤3:RSA加密随机串(用Native层获取的RSA公钥)         String str = C2433h.m6337f(C2433h.m6340i(C2450e.m6420m().f6667k, C2450e.m6420m().f6668l), m6281f);     } catch (Exception e2) {         e2.printStackTrace();         str = "";     }      try {         // 步骤4:AES加密业务数据(用随机串做密钥,IV从Native层获取)         String m6277b2 = AbstractC2426a.m6277b(C2451f.m6453f(treeMap), m6281f, C2450e.m6420m().f6669m);         // 拼接:AES加密结果 + 空字符串 + RSA加密结果         StringBuilder sb2 = new StringBuilder();         sb2.append(m6277b2).append("").append(str);         // 编码处理:调用m6286d()(Base64),存入参数d         hashMap.put(App.get().getResources().getString(C2404f.p.request_parment), AbstractC2427b.m6286d(sb2.toString().getBytes()));     } catch (Exception e3) {         e3.printStackTrace();     }     return hashMap; }

2. 四步还原参数d加密逻辑

步骤1:生成16位随机串(AbstractC2426a.m6281f())

用于作为AES加密的密钥(符合AES-128对16字节密钥的要求),源码如下:

--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

public static String m6281f() {     Random random = new Random();     StringBuffer stringBuffer = new StringBuffer();     // 字符池:大小写字母+数字(共62个字符)     String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";     for (int i2 = 0; i2 < 16; i2++) {         // 随机选择字符,拼接为16位串         stringBuffer.append(chars.charAt(random.nextInt(62)));     }     return stringBuffer.toString(); }
步骤2:RSA公钥加密随机串

  • RSA参数来源f6667k(模,Module)和f6668l(公钥指数,Exponent)从Native层(C/C++)获取,通过NativeUtil.getNetSignModuleKey()NativeUtil.getNetSignExponent()调用,提升反编译难度。


  • 公钥构建:通过C2433h.m6340i()将模和指数转为RSAPublicKey:

    --javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

    public static RSAPublicKey m6340i(String str, String str2) {     try {         return (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(new BigInteger(str), new BigInteger(str2)));     } catch (Exception e2) {         e2.printStackTrace();         return null;     } }
  • 分段加密:因RSA加密长度限制,对16位随机串分段加密(每段≤117字节),结果转Base64:

    --javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

    public static final String f6626a = "RSA/NONE/PKCS1Padding"; public static final String f6627b = "BC";  public static String m6337f(PublicKey publicKey, String str) throws Exception {     Cipher cipher = Cipher.getInstance(f6626a, f6627b);     cipher.init(1, publicKey);     byte[] bytes = str.getBytes("UTF-8");     int length = bytes.length;     ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();     int i2 = 0;     while (true) {         int i4 = length - i2;         if (i4 <= 0) {             byte[] byteArray = byteArrayOutputStream.toByteArray();             byteArrayOutputStream.close();             return new String(Base64.encode(byteArray, 2));         }         // 分段加密:超过117字节则截取,否则直接加密         byte[] doFinal = i4 > 117 ? cipher.doFinal(bytes, i2, 117) : cipher.doFinal(bytes, i2, i4);         byteArrayOutputStream.write(doFinal, 0, doFinal.length);         i2 += 117;     } }
步骤3:Hook获取RSA参数(模+指数)

因RSA参数存于Native层,需通过Hook脚本在APP运行时捕获:

--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

Java.perform(function() {     const EncryptClass = Java.use("你的类名"); // 替换为实际加密类名     EncryptClass.a.implementation = function(map) {         console.log("======================================");         console.log(" 触发请求,获取RSA关键参数:");          // 1. 获取Native层的RSA模、指数、IV         const EClass = Java.use("你的类名"); // 替换为C2450e相关类名         const eInstance = EClass.m();         const eCls = EClass.class;          // 工具函数:获取类字段值         const getFieldValue = (fieldName) => {             try {                 const field = eCls.getDeclaredField(fieldName);                 field.setAccessible(true); // 突破访问权限                 return field.get(eInstance);             } catch (e) {                 return "获取失败:" + e.message;             }         };          // 获取RSA模(j)、指数(k)、AES IV(l/m)         const rsaModule = getFieldValue("j"); // RSA模数         const rsaExponent = getFieldValue("k"); // RSA指数         const aesIV1 = getFieldValue("l");         const aesIV2 = getFieldValue("m");          console.log(` RSA模数(j): ${rsaModule}`);         console.log(` RSA指数(k): ${rsaExponent}`);         console.log(` AES IV值(l): ${aesIV1}`);         console.log(` AES IV值(m): ${aesIV2}`);          // 2. 执行原方法,返回结果         const result = this.a(map);         console.log("======================================\n");         return result;     };     console.log(" RSA参数Hook脚本就绪!发起请求即可捕获"); });

自动草稿

步骤4:AES加密业务数据与Base64编码

  • AES加密:用步骤1生成的16位随机串做密钥,f6669m(从Native层获取)做IV(在步骤三中的HOOK脚本里有获取IV的操作),加密序列化后的业务数据(TreeMap):

    --javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

    public static String m6277b(String str, String str2, String str3) throws Exception {     // 校验密钥:非空且长度为16字节(AES-128)     if (str2 == null) {         System.out.print("Key为空null");         return null;     }     if (str2.length() != 16) {         System.out.print("Key长度不是16位");         return null;     }     // 初始化AES密钥与加密器(CBC模式+PKCS5Padding)     SecretKeySpec secretKeySpec = new SecretKeySpec(str2.getBytes(), "AES");     Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");     cipher.init(1, secretKeySpec, new IvParameterSpec(str3.getBytes()));     // 加密+Base64编码(调用m6286d())     return AbstractC2427b.m6286d(cipher.doFinal(str.getBytes())); }

  • Base64编码(AbstractC2427b.m6286d()):自定义Base64编码实现,将AES加密结果与RSA加密结果的拼接串转为最终参数d的值,核心是通过字符池(ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/)完成字节与字符串的映射。

六、整体加密流程总结

APP登录请求的完整加密链路如下:

  1. 1.

    参数准备:通过C2165k.getHashMap()获取通用参数(jPushId、version),添加账号(account)与3DES加密后的密码(pwd)。

  2. 2.

    添加防篡改与防重放参数


    • 加入timestamp(毫秒级时间戳),防重复请求。


    • 生成signToken(SHA-512→多步字符串处理→MD5),防数据篡改。

  3. 3.

    生成参数d


    • 生成16位随机串(AES密钥)。


    • RSA加密随机串(用Native层公钥)。


    • AES加密业务参数(用随机串+Native层IV)。


    • 拼接AES结果与RSA结果,Base64编码后作为参数d。

  4. 4.

    发送请求:以参数d为核心,发起POST请求,服务器解密验证后响应。


文章来源:HACK之道

本文来源HACK之道,经授权后由华盟君发布,观点不代表华盟网的立场,转载请联系原作者。

发表评论