某大学生常用APP抓包数据加密流程分析
文章作者:988hvjnkj
文章来源:https://www.freebuf.com/articles/mobile/453346.html
文章转载/排版来自于神农Sec
01
0x1 RPA机器人自动化公众号文章发布
一、原始抓包
1.1抓包


1.2 抓包结果分析
二、逆向分析:定位核心代码与参数
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.
C2165k.getHashMap():获取的通用参数集合包含哪些内容?
-
2.
C2428c.m6288d(str2):该方法对原始密码(str2)进行了何种加密处理?
-
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脚本)
因未登录时uid和token为空,仅需获取jPushId和version,可通过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.
参数准备:通过C2165k.getHashMap()获取通用参数(jPushId、version),添加账号(account)与3DES加密后的密码(pwd)。
-
2.
添加防篡改与防重放参数:
-
加入timestamp(毫秒级时间戳),防重复请求。
-
生成signToken(SHA-512→多步字符串处理→MD5),防数据篡改。
-
3.
生成参数d:
-
生成16位随机串(AES密钥)。
-
RSA加密随机串(用Native层公钥)。
-
AES加密业务参数(用随机串+Native层IV)。
-
拼接AES结果与RSA结果,Base64编码后作为参数d。
-
4.
发送请求:以参数d为核心,发起POST请求,服务器解密验证后响应。
文章来源:HACK之道
华盟君