移动端加密算法逆向实战:从混淆代码到算法还原

📅 2026/7/4 14:32:15
移动端加密算法逆向实战:从混淆代码到算法还原
1. 项目概述一次典型的移动端加密算法逆向之旅最近在分析一些主流应用的网络请求时经常会遇到一些“拦路虎”——那些看似随机、冗长的加密参数。携程APP里的user-dun参数就是这样一个典型代表。它出现在关键的API请求中长度可观结构复杂显然是服务端用于风控和身份验证的核心令牌。对于从事移动安全研究、数据合规分析或自动化流程开发的同行来说理解这类参数的生成逻辑不仅是技术上的挑战更是绕过某些限制、实现合法业务自动化的前提。这次实战我们就来完整地走一遍从发现、定位、逆向到最终还原user-dun算法的全过程。整个过程充满了与代码混淆、反调试机制斗智斗勇的乐趣也沉淀了不少实用的技巧和避坑经验无论你是安全研究员、爬虫工程师还是对安卓逆向感兴趣的开发者相信都能从中获得启发。2. 核心思路与逆向环境搭建2.1 逆向目标与核心挑战拆解我们的终极目标是搞清楚user-dun这个字符串是如何生成的。它不是一个简单的MD5或Base64从抓包观察来看它长度固定或在一定范围内包含数字和字母很像一个经过复杂编码和加密的结果。逆向这种算法通常需要找到生成它的代码位置理解其输入原材料、处理过程算法和输出最终形态。面临的挑战主要来自三个方面代码混淆这是最大的障碍。生产环境的APP尤其是像携程这样的大型应用必然会使用ProGuard或更高级的混淆工具如DexGuard、梆梆加固等对代码进行混淆。类名、方法名、字段名会变成无意义的a,b,c控制流也可能被扁平化或插入垃圾指令极大地增加了静态分析的难度。反调试与动态检测应用可能会检测是否被调试android.os.Debug.isDebuggerConnected()或者运行在模拟器中一旦发现异常环境可能触发算法分支变化甚至直接崩溃导致动态调试失败。算法复杂度user-dun很可能并非单一算法而是多种信息的组合体可能包括设备指纹、时间戳、用户令牌、随机数等再经过一系列加密如AES、RSA、编码Base64、Hex和哈希SHA系列操作生成。我们的逆向策略将是“动静结合”先通过静态分析定位关键代码区域再通过动态调试验证猜想、追踪数据流。2.2 工具链选型与环境准备工欲善其事必先利其器。根据上述挑战我搭建了以下工具链这也是目前移动端逆向的“标配”抓包工具 - HTTP Toolkit / Charles / Fiddler用于拦截和观察网络请求确认user-dun参数的存在、格式和出现时机。我优先推荐HTTP Toolkit它对HTTPS证书的安装和管理非常友好自动化程度高能省去很多配置麻烦。反编译与静态分析 - Jadx-GUI将APK文件反编译成可读的Java/Kotlin代码。Jadx是目前最强大、最易用的开源反编译器其搜索、跳转、查看继承关系等功能对逆向至关重要。对于加固的APK可能需要先脱壳。动态调试 - Frida这是本次实战的“王牌”。Frida是一个动态插桩框架允许我们向目标进程注入JavaScript脚本从而在运行时Hook挂钩关键函数、打印参数和返回值、修改逻辑等。它完美避开了混淆带来的可读性问题让我们直接观察运行时的数据。运行时查看 - Android Studio / Logcat用于查看应用的标准输出日志有时关键信息会通过Log.d打印出来。结合Frida的console.log可以构建完整的运行时信息流。设备环境 - 已Root的安卓真机或高性能模拟器如夜神、雷电动态调试和Frida的某些高级功能如Spawn模式附加通常需要Root权限。使用模拟器可以方便地做快照和回滚但要注意应用可能存在的模拟器检测。重要提示所有逆向分析必须基于合法授权。本文仅用于技术交流与学习旨在提升移动应用安全防护意识请勿将技术用于非法破解、侵犯他人权益或违反服务条款的活动。环境搭建好后第一步是获取目标APK。可以通过官方应用市场下载或者使用一些第三方APK提取工具从已安装的手机中提取。拿到APK后先用jadx-gui打开看看整体代码结构感受一下混淆的强度。3. 静态分析与关键代码定位3.1 从抓包数据到代码搜索首先启动抓包工具和携程APP进行任意一次搜索酒店或机票的交互。在抓包工具中很快就能过滤到携程的API域名如*.ctrip.com或*.trip.com观察请求头或请求体找到那个形如user-dun: xxxxxxxx...的参数。记下它的样子例如它可能看起来像“aBcDeF123...zYxWvU456”。接下来在Jadx中打开反编译后的代码。由于代码被混淆直接搜索“user-dun”字符串可能一无所获因为字符串常量也可能被加密或编码。更有效的方法是搜索与网络请求相关的类或方法。搜索关键词尝试搜索“dun”、“user”、“token”、“header”、“sign”等可能相关的部分单词。有时运气好混淆后的变量名或方法名会保留部分语义。搜索网络库查看APP使用了什么网络库OkHttp, Retrofit, HttpURLConnection等。在Jadx中搜索“OkHttpClient”、“Interceptor”、“Retrofit”、“addHeader”等。网络请求的头部添加逻辑很可能就在一个自定义的Interceptor拦截器中。定位请求构建处找到构建具体API请求的地方查看其头部设置代码。这可能需要一些耐心跟踪调用链。在我的分析中通过搜索“Interceptor”我找到了一个名为c混淆后的类它实现了Interceptor接口。在其intercept方法中发现了一段循环遍历请求头并添加新头的代码。附近有一个方法调用传入了一个Map对象而Map中就包含了user-dun的键值对。这个生成Map的方法就是我们的突破口。3.2. 深入混淆代码理解生成逻辑定位到生成user-dun的方法假设它叫a.a()一个典型的混淆后名称。在Jadx中查看这个方法代码可能类似这样public static String a(Context context) { String b b(context); String c c(); String d d(context); return a(b, c, d); // 最终生成user-dun }虽然名字看不懂但逻辑是清晰的它调用了另外几个方法b(),c(),d()获取一些原材料然后通过a()方法重载进行合成处理。我们需要逐一分析这些子方法。b(context)可能用于获取设备信息。跟进去发现它调用了android.os.Build系列API、获取IMEI/Android ID、屏幕分辨率等。这就是设备指纹的采集过程。c()可能获取时间戳或随机数。跟进去发现它调用了System.currentTimeMillis()并进行了一些格式化。d(context)可能获取用户登录态。跟进去发现它从SharedPreferences或某个管理类中读取了一个access_token。至此我们知道了user-dun的原材料至少包括设备指纹F、时间戳T、用户令牌U。最后那个a(b, c, d)方法就是核心的加密合成函数。3.3. 直面核心加密函数进入最终的a(String, String, String)方法代码混淆程度可能最高。你会看到大量的位操作、循环以及调用一些javax.crypto.*或自定义的本地方法。静态分析到这里会非常吃力因为变量名全是i,j,k,str。控制流可能被switch或if打乱。关键的加密密钥可能被隐藏在字符串常量中并经过了简单的变换如异或、Base64解码。此时不要试图完全用人脑去理解每一行代码。我们的目标是确定算法的类型和关键操作点为动态调试做准备。例如看到Cipher.getInstance(“AES/CBC/PKCS5Padding”)就知道是AES加密。看到MessageDigest.getInstance(“SHA-256”)就知道有SHA256哈希。看到大量的byte[]操作和^异或符号可能是在做自定义的编码或混淆。记下这个核心方法的名字和所在类名例如com.ctrip.foundation.security.a.a(String, String, String)。同时记下它调用的所有可疑的静态方法或获取常量的方法这些都可能成为Frida Hook的切入点。静态分析心得在混淆的代码中不要纠结于变量名。关注方法调用和控制流结构。利用Jadx的“查找用法”功能看哪些地方调用了你感兴趣的方法这有助于理解数据流向。将复杂的代码块用注释标记画出简单的数据流图会清晰很多。4. 动态调试与算法还原静态分析给了我们地图动态调试则是我们行走其间的导航。Frida将在这里大放异彩。4.1. Frida脚本编写基础Hook首先确保Frida服务在手机上运行并且电脑可以adb shell连上。我们编写一个基础的Frida脚本用于Hook我们找到的核心方法。// hook_core.js Java.perform(function () { // 定位核心类 var SecurityClass Java.use(“com.ctrip.foundation.security.a”); // Hook 最终合成的a方法 SecurityClass.a.overload(‘java.lang.String’, ‘java.lang.String’, ‘java.lang.String’).implementation function (str1, str2, str3) { console.log(“[] a()方法被调用”); console.log(“ |- 参数1 (可能为设备信息): ” str1); console.log(“ |- 参数2 (可能为时间戳): ” str2); console.log(“ |- 参数3 (可能为用户令牌): ” str3); // 调用原方法获取结果 var result this.a(str1, str2, str3); console.log(“ |- 返回值 (user-dun): ” result); console.log(“\n”); // 为了分析我们可以把结果和输入保存下来 send({input: [str1, str2, str3], output: result}); return result; }; // 也可以Hook那些获取原材料的方法验证我们的猜想 var UtilsClass Java.use(“com.ctrip.foundation.util.b”); // 假设的类名 if (UtilsClass) { UtilsClass.c.implementation function () { var result this.c(); console.log(“[] c()方法返回时间戳: ” result); return result; }; } });使用命令frida -U -f com.ctrip.android.view -l hook_core.js --no-pause启动APP并注入脚本。触发一个会产生user-dun的网络请求比如刷新首页。在Frida控制台你就能看到实时的参数和返回值打印。4.2. 追踪加密细节与密钥获取通过基础Hook我们确认了输入输出。接下来需要深入加密函数内部。如果核心算法使用了标准加密库我们可以直接HookCipher类的doFinal方法。// hook_cipher.js Java.perform(function () { var Cipher Java.use(‘javax.crypto.Cipher’); Cipher.doFinal.overload(‘[B’).implementation function (inputBytes) { console.log(“[] Cipher.doFinal() 被调用”); // 打印输入字节数组的Hex字符串便于查看明文 console.log(“ |- 输入数据(Hex): ” bytesToHex(inputBytes)); // 打印当前Cipher实例使用的算法需要获取this try { var alg this.getAlgorithm(); console.log(“ |- 算法: ” alg); } catch(e) {} var result this.doFinal(inputBytes); console.log(“ |- 输出数据(Hex): ” bytesToHex(result)); console.log(“\n”); return result; }; // 辅助函数字节数组转Hex function bytesToHex(bytes) { return Array.from(bytes, function(byte) { return (‘0’ (byte 0xFF).toString(16)).slice(-2); }).join(‘’); } });运行这个脚本你可能会看到AES加密前后的数据。但还有一个关键密钥从哪里来密钥可能来自硬编码在代码中经过简单变换。从服务器动态获取但首次或本地应有缓存。由其他参数计算得出。我们需要Hook密钥生成或获取的地方。可以在静态分析时搜索SecretKeySpec、KeyGenerator或一些返回byte[]的疑似密钥生成方法。用Frida Hook这些方法打印其返回值。// 假设我们找到了一个返回密钥字节数组的方法 getKeyBytes() var KeyManager Java.use(‘com.ctrip.foundation.security.d’); KeyManager.getKeyBytes.implementation function () { var keyBytes this.getKeyBytes(); console.log(“[] 获取到密钥字节: ” bytesToHex(keyBytes)); return keyBytes; };4.3. 处理反调试与代码自修改在动态调试过程中应用可能会崩溃或行为异常这可能是触发了反调试。常见的对抗手段有检测调试器Hookandroid.os.Debug.isDebuggerConnected()并使其返回false。检测模拟器Hook一些检测模拟器的方法如检查特定属性文件、IMEI等返回符合真机的值。签名校验在应用启动时校验APK签名。我们可以HookPackageManager.getPackageInfo相关调用返回原始的签名信息。代码自检/内存校验较少见但高级加固会有。可能需要更复杂的Frida脚本或基于内存Patch。一个简单的反调试对抗脚本框架Java.perform(function () { // 反调试检测 var Debug Java.use(‘android.os.Debug’); Debug.isDebuggerConnected.implementation function () { console.log(“[!] isDebuggerConnected() 被调用返回false”); return false; }; // 如果检测到TracerPid另一种调试检测方式 var File Java.use(‘java.io.File’); var FileInputStream Java.use(‘java.io.FileInputStream’); var BufferedReader Java.use(‘java.io.BufferedReader’); var InputStreamReader Java.use(‘java.io.InputStreamReader’); // Hook文件读取如果读取/proc/self/status等文件过滤掉TracerPid // 这里是一个概念示例实际需要更精细的Hook });动态调试避坑指南先静态后动态不要一上来就Frida乱Hook。先通过静态分析缩小目标范围否则海量的日志会让你迷失。分层Hook从最外层的网络请求拦截器开始Hook逐步向内层核心方法深入。像剥洋葱一样一层层揭开。注意时序有些算法依赖于精确的时间戳。Frida的Hook和打印语句会引入微小延迟可能影响时间敏感型参数的生成结果。如果发现Hook后的结果与正常结果不同可以考虑Hook时间获取函数返回一个固定的或可控的时间值。保存上下文使用send()函数将重要的输入输出数据发送到Python端保存下来便于后续分析和算法复现验证。5. 算法复现与验证通过动静结合的分析我们最终梳理出了user-dun的生成流程1. 采集设备指纹(F)包括设备型号、品牌、Android版本、屏幕宽高、IMEI若有权-限、Android ID等经过特定排序和拼接后进行MD5或SHA256哈希得到一个指纹字符串 f_str。 2. 获取时间戳(T)取当前时间戳毫秒级可能除以一个固定值如1000取秒级然后格式化为一个定长字符串 t_str。 3. 获取用户令牌(U)从本地缓存读取登录后的 access_token或一个固定的匿名令牌 anon_token记为 u_str。 4. 拼接原始字符串将 f_str、t_str、u_str 以某个分隔符如 | 或 #拼接起来。 5. AES加密使用一个硬编码或动态生成的AES密钥可能是16/24/32字节以CBC模式对拼接后的字符串进行加密。IV向量可能是固定的或全零。 6. Base64编码将加密后的字节数组进行Base64编码。 7. 二次混淆可选对Base64字符串进行可逆的变换如字符替换、顺序打乱等最终生成 user-dun。现在我们用Python来复现这个算法import hashlib import time import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import pad def generate_user_dun(device_info, access_token, aes_key_hex, aes_iv_hex): “”” 复现user-dun生成算法 :param device_info: 字典包含设备信息 :param access_token: 用户令牌字符串 :param aes_key_hex: AES密钥的Hex字符串 :param aes_iv_hex: AES IV的Hex字符串 :return: user-dun字符串 “”” # 1. 生成设备指纹字符串 fingerprint_parts [ device_info.get(‘brand’, ‘’), device_info.get(‘model’, ‘’), device_info.get(‘android_version’, ‘’), device_info.get(‘screen_resolution’, ‘’), device_info.get(‘android_id’, ‘’), ] # 按特定顺序和格式拼接这里假设用‘#’连接后取MD5 fingerprint_str ‘#’.join(filter(None, fingerprint_parts)) f_str hashlib.md5(fingerprint_str.encode(‘utf-8’)).hexdigest() # 2. 生成时间戳字符串假设取秒级格式化为10位 t_str str(int(time.time()))[-10:].zfill(10) # 取最后10位不足补零 # 3. 用户令牌 u_str access_token # 4. 拼接原始数据假设用‘|’连接 raw_data f“{f_str}|{t_str}|{u_str}” # 5. AES加密 aes_key bytes.fromhex(aes_key_hex) aes_iv bytes.fromhex(aes_iv_hex) cipher AES.new(aes_key, AES.MODE_CBC, aes_iv) # 需要填充PKCS7 padding padded_data pad(raw_data.encode(‘utf-8’), AES.block_size) encrypted_data cipher.encrypt(padded_data) # 6. Base64编码 b64_str base64.b64encode(encrypted_data).decode(‘utf-8’) # 7. 二次混淆假设是简单的字符替换实际需根据分析 # 例如将‘’替换为‘-’‘/’替换为‘_’去掉末尾的‘’ final_dun b64_str.replace(‘’, ‘-’).replace(‘/’, ‘_’).rstrip(‘’) return final_dun # 使用示例密钥和IV需从动态调试中获取 device_info { ‘brand’: ‘Xiaomi’, ‘model’: ‘M2102J2SC’, ‘android_version’: ‘11’, ‘screen_resolution’: ‘1080x2340’, ‘android_id’: ‘a1b2c3d4e5f67890’, } access_token ‘eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...’ # 示例token aes_key ‘0123456789abcdef0123456789abcdef’ # 32字节Hex示例 aes_iv ‘00000000000000000000000000000000’ # 16字节Hex示例 user_dun generate_user_dun(device_info, access_token, aes_key, aes_iv) print(f“生成的 user-dun: {user_dun}”)验证方法在同一个设备上用Frida Hook抓取一组真实的输入参数f_str,t_str,u_str和输出的user-dun。将抓取到的输入参数代入你的复现算法中。比较算法输出与真实抓取的user-dun是否一致。如果不一致检查每一步设备指纹的拼接顺序、哈希算法、时间戳格式、拼接分隔符、AES的模式和填充、Base64后的变换规则等。可能需要反复调整并用Frida Hook中间每一步的输出进行比对。6. 常见问题与排查技巧实录在整个逆向过程中你肯定会遇到各种“坑”。这里记录了一些典型问题及解决思路问题现象可能原因排查与解决思路Jadx反编译失败或卡死APK被加固如梆梆、腾讯乐固1. 使用专门的脱壳工具如Frida脱壳机、DumpDex先脱壳再反编译脱壳后的Dex。2. 尝试其他反编译器如GDA、Bytecode Viewer。Frida附加目标APP后立刻崩溃强反调试保护1. 使用-f参数以Spawn模式启动APPfrida -U -f com.xxx并在脚本最早时机如Java.perform开头就Hook反调试函数。2. 尝试使用frida的—debug模式或更换Frida版本。3. 使用更隐蔽的调试手段如ptrace或基于内核模块的调试。Hook方法时找不到类或方法1. 类名/方法名记错或混淆后变化。2. 类被动态加载。3. 方法签名参数列表不匹配。1. 在Jadx中确认类的完整路径和方法签名包括参数类型。2. 使用Java.enumerateLoadedClasses()在运行时列出已加载的类来查找。3. 使用overload时确保参数类型字符串完全匹配例如‘java.lang.String’和‘[B’字节数组。动态获取的参数与静态分析不一致1. 代码存在多态或条件分支。2. 依赖的某些值如时间、位置在Hook时已变化。3. 算法有多个版本或灰度发布。1. 在调用该方法前Hook其内部调用的其他方法查看更原始的输入。2. Hook系统时间、随机数生成器等函数返回固定的值确保每次执行输入一致。3. 在不同时间、不同账号下多采集几组样本分析差异点。复现的算法结果与真实值差一点1. 编码细节错误如Base64的URL安全模式。2. 填充方式不对PKCS5 vs PKCS7。3. 字符串拼接时末尾空格或不可见字符。4. 设备指纹的某项数据获取方式有误。1.逐字节比对用Frida Hook算法中每一个中间步骤的输出字节数组转Hex与你复现的每一步输出进行严格比对定位第一个出现差异的环节。2. 特别注意字符编码UTF-8, GBK和大小写。网络请求中user-dun偶尔失效1. 算法中使用了有时效性的参数如时间戳过期后服务端拒绝。2. 密钥或盐值定期从服务器更新。1. 分析时间戳的精度和有效期。Hook时间函数确保生成user-dun时使用的时间与请求发出时间非常接近。2. 检查是否有网络请求在获取动态密钥Hook相应的接口。最后的经验之谈逆向工程是一场耐心的较量。面对高度混淆的代码不要指望一蹴而就。最有效的方法是“假设-验证”循环通过静态分析提出一个关于算法步骤的假设然后用Frida动态调试去验证这个假设。当动态结果与静态阅读的代码逻辑不符时往往是发现了混淆带来的“障眼法”或者自己理解有误此时再回头修正静态分析模型。整个过程就像在解一个复杂的谜题每一次成功的Hook和验证都是向最终答案迈进的一步。保持耐心善用工具勤做记录你会发现自己破解混淆的能力在一次次实战中飞速提升。