当前位置: 首页 > news >正文

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

1. 项目概述:一次典型的移动端加密算法逆向之旅

最近在分析一些主流应用的网络请求时,经常会遇到一些“拦路虎”——那些看似随机、冗长的加密参数。携程APP里的user-dun参数就是这样一个典型代表。它出现在关键的API请求中,长度可观,结构复杂,显然是服务端用于风控和身份验证的核心令牌。对于从事移动安全研究、数据合规分析或自动化流程开发的同行来说,理解这类参数的生成逻辑,不仅是技术上的挑战,更是绕过某些限制、实现合法业务自动化的前提。这次实战,我们就来完整地走一遍从发现、定位、逆向到最终还原user-dun算法的全过程。整个过程充满了与代码混淆、反调试机制斗智斗勇的乐趣,也沉淀了不少实用的技巧和避坑经验,无论你是安全研究员、爬虫工程师还是对安卓逆向感兴趣的开发者,相信都能从中获得启发。

2. 核心思路与逆向环境搭建

2.1 逆向目标与核心挑战拆解

我们的终极目标是搞清楚user-dun这个字符串是如何生成的。它不是一个简单的MD5或Base64,从抓包观察来看,它长度固定(或在一定范围内),包含数字和字母,很像一个经过复杂编码和加密的结果。逆向这种算法,通常需要找到生成它的代码位置,理解其输入(原材料)、处理过程(算法)和输出(最终形态)。

面临的挑战主要来自三个方面:

  1. 代码混淆:这是最大的障碍。生产环境的APP,尤其是像携程这样的大型应用,必然会使用ProGuard或更高级的混淆工具(如DexGuard、梆梆加固等)对代码进行混淆。类名、方法名、字段名会变成无意义的a,b,c,控制流也可能被扁平化或插入垃圾指令,极大地增加了静态分析的难度。
  2. 反调试与动态检测:应用可能会检测是否被调试(android.os.Debug.isDebuggerConnected()),或者运行在模拟器中,一旦发现异常环境,可能触发算法分支变化甚至直接崩溃,导致动态调试失败。
  3. 算法复杂度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”字符串可能一无所获,因为字符串常量也可能被加密或编码。更有效的方法是搜索与网络请求相关的类或方法。

  1. 搜索关键词:尝试搜索“dun”、“user”、“token”、“header”、“sign”等可能相关的部分单词。有时运气好,混淆后的变量名或方法名会保留部分语义。
  2. 搜索网络库:查看APP使用了什么网络库(OkHttp, Retrofit, HttpURLConnection等)。在Jadx中搜索“OkHttpClient”、“Interceptor”、“Retrofit”、“addHeader”等。网络请求的头部添加逻辑,很可能就在一个自定义的Interceptor(拦截器)中。
  3. 定位请求构建处:找到构建具体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
  • 控制流可能被switchif打乱。
  • 关键的加密密钥可能被隐藏在字符串常量中,并经过了简单的变换(如异或、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加密前后的数据。但还有一个关键:密钥从哪里来?密钥可能来自:

  1. 硬编码在代码中(经过简单变换)。
  2. 从服务器动态获取(但首次或本地应有缓存)。
  3. 由其他参数计算得出。

我们需要Hook密钥生成或获取的地方。可以在静态分析时,搜索SecretKeySpecKeyGenerator或一些返回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 });

动态调试避坑指南

  1. 先静态,后动态:不要一上来就Frida乱Hook。先通过静态分析缩小目标范围,否则海量的日志会让你迷失。
  2. 分层Hook:从最外层的网络请求拦截器开始Hook,逐步向内层核心方法深入。像剥洋葱一样,一层层揭开。
  3. 注意时序:有些算法依赖于精确的时间戳。Frida的Hook和打印语句会引入微小延迟,可能影响时间敏感型参数的生成结果。如果发现Hook后的结果与正常结果不同,可以考虑Hook时间获取函数,返回一个固定的或可控的时间值。
  4. 保存上下文:使用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}”)

验证方法

  1. 在同一个设备上,用Frida Hook抓取一组真实的输入参数(f_str,t_str,u_str)和输出的user-dun
  2. 将抓取到的输入参数,代入你的复现算法中。
  3. 比较算法输出与真实抓取的user-dun是否一致。如果不一致,检查每一步:设备指纹的拼接顺序、哈希算法、时间戳格式、拼接分隔符、AES的模式和填充、Base64后的变换规则等。可能需要反复调整,并用Frida Hook中间每一步的输出进行比对。

6. 常见问题与排查技巧实录

在整个逆向过程中,你肯定会遇到各种“坑”。这里记录了一些典型问题及解决思路:

问题现象可能原因排查与解决思路
Jadx反编译失败或卡死APK被加固(如梆梆、腾讯乐固)1. 使用专门的脱壳工具(如Frida脱壳机、DumpDex)先脱壳,再反编译脱壳后的Dex。
2. 尝试其他反编译器,如GDA、Bytecode Viewer。
Frida附加目标APP后立刻崩溃强反调试保护1. 使用-f参数以Spawn模式启动APP(frida -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和验证,都是向最终答案迈进的一步。保持耐心,善用工具,勤做记录,你会发现自己破解混淆的能力在一次次实战中飞速提升。

http://www.jsqmd.com/news/1122322/

相关文章:

  • NextGenAI联盟:5000万美元如何重塑大模型研发范式
  • KNN算法超参数调优实战与鸢尾花分类应用
  • 意识觉醒的源头:丘脑中央核!!!
  • 基于深度学习的单目视觉FCW系统实现与优化
  • 大数据处理性能优化实战:从理论到实践
  • AI工具助力研究生开题报告写作:9款实用工具与技巧
  • 2022年8月AI趋势:大模型轻量化与生成式AI工业化落地
  • 浅谈SQL Server中的事务日志(一)----事务日志的物理和逻辑构架
  • STM32F070RB与MC6470 IMU的硬件协同与运动控制实践
  • 深度学习算法速查表:类型、应用与典型示例
  • 基于YOLOv12的香蕉成熟度自动识别系统开发
  • 生成式AI模型选型决策地图:显式与隐式密度模型深度解析
  • Mac Mouse Fix终极指南:让你的普通鼠标在macOS上超越苹果触控板体验
  • 国产大模型写代码实战指南:GLM、Kimi、Minimax、豆包四大引擎选型对比
  • 【JAVA毕设源码分享】基于springboot云山幼儿园管理系统的设计与实现(程序+文档+代码讲解+一条龙定制)
  • ColabFold终极指南:零基础快速预测蛋白质3D结构
  • Trilium中文版:解决知识管理三大痛点的开源笔记神器
  • C语言实现SM3国密算法:从原理到工程实践完整指南
  • 如何免费加速百度网盘下载:PDown下载器完整使用指南
  • DCT与小波变换结合的图像压缩技术实践
  • 多维数据聚合实战:从OLAP立方体到动态重切片
  • Spring Boot+Vue旅游分享小程序毕业设计:从通用模板到业务化改造实战
  • AI正在接管的五大开发岗位:内容生成、测试、数据清洗、DBA与DevOps
  • OAuth2.0与JWT实战:从授权原理到微服务安全架构落地
  • 告别链接失效!5分钟搭建网易云音乐永久解析服务
  • stltostp:专业STL到STEP格式转换的终极解决方案
  • 零代码AI智能体创建工具实战指南
  • 三层内网渗透实战:从Web突破到核心区提权全流程解析
  • UEFI安全监控与Peacock框架实战解析
  • 基于ResNet和PyTorch的花卉分类系统设计与实现