1. 项目概述从登录请求到算法核心做移动端安全分析的朋友对登录协议的逆向分析应该都不陌生。这几乎是进入一个APP安全研究领域的“敲门砖”也是理解其客户端与服务器交互逻辑最直接的切入点。最近我花了些时间深入分析了“车智赢”这款APP的登录流程目标很明确找到其登录请求中核心参数的生成算法特别是那个关键的签名sign或令牌token是如何被构造出来的。为什么是登录协议因为登录环节往往是安全机制最集中、最典型的地方。一个设计良好的登录协议会包含设备指纹、请求签名、数据加密、防重放攻击等多种防护手段。逆向分析它不仅能让我们理解其安全架构更能为后续的自动化测试、协议模拟或安全审计打下坚实基础。对于“车智赢”这类涉及车辆控制、状态查询等敏感操作的APP其安全性的强弱直接关系到用户隐私和资产安全因此分析其底层实现具有很高的参考价值。本次分析将聚焦于“核心算法篇”这意味着我们会绕过基础的抓包、定位等步骤直接深入到最关键的加密/签名函数内部剖析其输入、输出、逻辑流程以及关键的密钥或盐值salt是如何参与运算的。整个过程会涉及静态分析反编译、动态调试Hook、算法还原等多个环节。无论你是移动安全研究员、爬虫工程师还是对安卓逆向感兴趣的开发者相信这篇详尽的拆解都能给你带来直接的帮助和启发。2. 分析环境与工具链搭建工欲善其事必先利其器。一套稳定、高效的分析环境是逆向工程成功的基础。对于安卓APP的协议分析我的工具链通常分为几个层次抓包层、动态调试层、静态分析层和辅助工具层。2.1 核心工具选型与配置首先抓包工具我首选Charles或Fiddler两者都能很好地完成HTTPS流量的拦截和解密。关键在于手机端证书的安装与信任。这里有个细节在安卓高版本特别是7.0以上系统中系统不再信任用户安装的证书导致无法解密HTTPS流量。解决方案通常有两种一是将抓包工具的证书安装到系统证书目录需要Root权限二是使用像VirtualXposed、太极这类无需Root即可修改APP运行环境从而使其信任用户证书的工具。对于“车智赢”我采用了Root过的真机环境直接将Charles证书推送为系统证书一劳永逸。注意部分APP会启用SSL Pinning证书绑定技术检测到非预期的证书会中断连接。此时就需要使用JustTrustMe、SSLUnpinning等Xposed模块或者使用Frida脚本在内存中绕过证书校验。在分析初期如果发现抓包工具无法捕获到登录请求大概率就是遇到了证书绑定。动态调试和代码注入方面Frida是目前当之无愧的王者。它是一个动态代码插桩工具可以让我们在运行时拦截、修改函数调用或者直接调用APP内的任何函数。我通常会准备一套Frida Server运行在手机端、Frida Python库运行在电脑端以及一系列常用的脚本。对于登录算法最常用的就是Hook Java层函数打印其输入参数和返回值。静态分析则依赖于反编译工具。Jadx-GUI是我的首选它能够将APK文件中的DEX字节码反编译成可读性相当高的Java代码并且支持全局搜索、跳转引用效率极高。对于混淆严重的代码有时还需要结合IDA Pro或Ghidra来分析底层的so库Native层代码。我的完整工作流是先用抓包工具捕获到登录请求定位到关键的加密参数如sign然后在Jadx中搜索这个参数名或者搜索其可能出现的URL路径、接口名找到疑似函数后编写Frida脚本进行Hook验证最后结合静态分析理清整个算法的逻辑并用Python或JavaScript进行还原。2.2 关键环境配置细节Root环境选择我使用了一台刷了Magisk的Pixel手机。Magisk的Systemless Root特性对APP的隐藏性更好可以绕过一些基础的Root检测。Frida版本匹配务必确保电脑端frida、frida-tools的版本与手机端frida-server的版本一致否则会出现连接失败或无法识别API的问题。我当前使用的是Frida 16.x版本。抓包过滤器设置在Charles中我会设置一个Focus主机比如*.che-zhi-ying.com这样能过滤掉大量无关的静态资源请求让登录请求一目了然。Jadx的优化在Jadx的设置中开启“反混淆”选项如果支持并调整反编译器为“Fallback”模式有时能获得更好的代码。对于大型APK首次反编译和索引会较慢耐心等待即可。这套环境搭建起来可能需要一些时间但一旦就绪它就是一个强大的、可复用的分析平台能够应对大多数安卓APP的逆向需求。3. 登录请求抓包与关键参数定位一切准备就绪后我们启动“车智赢”APP进行登录操作可以使用测试账号同时在Charles中观察捕获到的网络请求。很快我们就能锁定登录接口。通常它的路径会是类似于/api/v1/user/login或/auth/login这样的形式。以我抓取到的请求为例请求方法 POST请求URLhttps://api.chezhijing.com/mobile/login请求头 包含常见的Content-Type: application/jsonUser-Agent等。请求体(JSON格式){ “username”: “13800138000”, “password”: “加密后的字符串”, “timestamp”: “1646389200000”, “nonce”: “a1b2c3d4e5”, “sign”: “7f8e9a0b1c2d3e4f5a6b7c8d9e0f1a2b”, “deviceId”: “android_xxxxxx” }从这个请求体中我们可以立刻识别出几个关键角色username 明文用户名或手机号。password 明显是经过加密处理后的密文这是首要分析目标。timestamp 时间戳常用于防重放攻击。nonce 随机数同样用于防重放确保每次请求唯一。sign 签名这通常是整个请求安全性的核心。它由其他参数可能包括请求体、请求头、甚至一个密钥按照特定算法生成用于服务端验证请求的完整性和合法性。deviceId 设备标识符。我们的核心目标就是password的加密算法和sign的签名算法。通常password的加密可能相对独立如RSA公钥加密、AES加密而sign的生成则会综合多个参数。定位技巧 在Jadx中我们可以使用全局搜索快捷键Shift Shift。首先搜索sign这个关键词会找到很多地方。我们需要结合上下文判断比如查找赋值语句sign 或者查找包含Map、TreeMap常用于参数排序以及MD5、SHA256、HMAC等加密相关字符串的代码。另一个有效方法是搜索接口URL中的关键字如“/mobile/login”直接定位到处理登录请求的代码附近。4. 静态分析与算法函数定位通过搜索“sign”和登录URL我在Jadx中找到了一个名为com.chezhijing.network.security.SignGenerator的类。这个类名非常直观很可能就是我们要找的目标。打开这个类发现其核心是一个generateSign方法。代码经过了混淆但关键逻辑依然可辨。该方法接收一个MapString, String参数存放所有待签名的参数和一个String参数可能是密钥或盐值返回计算出的签名。public class SignGenerator { public static String generateSign(MapString, String params, String secret) { // 1. 参数排序 ListString keys new ArrayList(params.keySet()); Collections.sort(keys); // 2. 拼接键值对 StringBuilder sb new StringBuilder(); for (String key : keys) { String value params.get(key); if (value ! null !value.isEmpty()) { sb.append(key).append(“”).append(value).append(“”); } } // 去掉最后一个“” if (sb.length() 0) { sb.deleteCharAt(sb.length() - 1); } // 3. 拼接密钥 sb.append(secret); // 注意这里是直接拼接也可能是其他方式 // 4. 进行哈希计算 String signStr sb.toString(); return md5(signStr); // 这里看到是MD5也可能是SHA256等 } private static String md5(String input) { try { MessageDigest md MessageDigest.getInstance(“MD5”); byte[] digest md.digest(input.getBytes(“UTF-8”)); StringBuilder hexString new StringBuilder(); for (byte b : digest) { String hex Integer.toHexString(0xff b); if (hex.length() 1) hexString.append(‘0’); hexString.append(hex); } return hexString.toString(); } catch (Exception e) { e.printStackTrace(); return “”; } } }从这段代码可以清晰还原出签名算法参数排序将所有待签名参数不包括sign本身按键名进行字典序排序。拼接字符串将排序后的参数按keyvalue格式拼接成一个字符串。添加密钥在拼接好的字符串末尾直接追加上一个密钥secret。这个secret是分析的关键它可能硬编码在代码中也可能从服务器动态获取。计算MD5对最终的字符串进行MD5哈希得到32位小写的十六进制字符串作为签名。接下来需要找到secret是什么。继续在代码中搜索SignGenerator.generateSign的调用处发现在一个网络请求拦截器或封装类里调用时传入的secret是一个从SecurityConfig.getAppSecret()获取的值。追踪SecurityConfig类发现appSecret是一个静态常量其值为“CheZhiYing_2023Sec”此处为示例实际值需分析确认。这里是一个重要的注意事项很多APP会将密钥进行简单的编码或拆分不会明文写在代码里可能需要分析初始化流程或通过动态调试来获取。同时我们也需要分析password的加密。搜索password的赋值处可能找到类似params.put(“password”, EncryptUtil.rsaEncrypt(rawPassword))的代码。追踪EncryptUtil发现它使用了RSA加密并且公钥PUBLIC_KEY也以字符串常量形式存储在代码中。RSA加密通常采用分段加密和Base64输出。至此通过静态分析我们已经初步掌握了两个核心算法的逻辑框架签名是排序拼接MD5密码加密是RSA公钥加密。但这还不够我们需要用动态调试来验证这些逻辑并获取确切的密钥和参数。5. 动态调试验证与密钥获取静态分析给出的代码逻辑是“应该是这样”而动态调试能告诉我们“实际上就是这样”。我们使用Frida来验证SignGenerator.generateSign函数。编写一个Frida脚本Java.perform(function() { var SignGenerator Java.use(‘com.chezhijing.network.security.SignGenerator’); var overloads SignGenerator.generateSign.overloads; for (var i 0; i overloads.length; i) { if (overloads[i].hasOwnProperty(‘argumentTypes’)) { // Hook所有重载 SignGenerator.generateSign.overloads[i].implementation function(params, secret) { console.log(“[SignGenerator.generateSign] called!”); console.log(“Params: “ JSON.stringify(params)); console.log(“Secret: “ secret); var result this.generateSign(params, secret); console.log(“Result Sign: “ result); console.log(“---”); return result; }; } } });运行脚本触发登录操作。在Frida控制台我们看到了真实的调用[SignGenerator.generateSign] called! Params: {“username”:“13800138000”, “password”:“xxx”, “timestamp”:“1646389200000”, “nonce”:“a1b2c3d4e5”, “deviceId”:“android_xxxx”} Secret: CheZhiYing_2023Sec Result Sign: 7f8e9a0b1c2d3e4f5a6b7c8d9e0f1a2b动态Hook完美验证了我们的静态分析传入的参数Map、使用的密钥secret以及计算出的sign值都与抓包数据吻合。这证实了签名算法无误。接下来验证密码加密。同样用Frida HookEncryptUtil.rsaEncrypt方法打印出明文密码和加密后的结果确认其与请求中的password字段一致并验证使用的公钥。实操心得在动态调试时可能会遇到函数重载overload的情况。上面的脚本通过遍历.overloads来处理所有重载版本是一种稳妥的做法。另外如果APP启用了反调试或Frida检测可能需要使用一些对抗手段比如使用定制版的Frida、或者先使用其他工具如Objection进行附着。6. 核心算法还原与Python实现经过静态和动态分析我们已经掌握了所有细节。现在用Python将这两个核心算法还原出来。这不仅是分析的成果也是后续进行协议模拟、自动化测试的基础。6.1 签名算法Sign还原签名算法的关键在于参数的排序和拼接顺序必须与APP端完全一致。import hashlib import time import uuid def generate_sign(params, secret): “”” 生成请求签名 :param params: dict, 待签名的参数字典 :param secret: str, 密钥 :return: str, 32位小写MD5签名 “”” # 1. 参数排序 sorted_keys sorted(params.keys()) # 2. 拼接键值对 sign_str_parts [] for key in sorted_keys: value params.get(key) if value is not None and str(value) ! ‘’: # 注意value需要转换为字符串拼接格式为 keyvalue sign_str_parts.append(f“{key}{value}”) # 用‘’连接所有键值对 sign_str “”.join(sign_str_parts) # 3. 拼接密钥 sign_str secret # 4. 计算MD5 md5 hashlib.md5() md5.update(sign_str.encode(‘utf-8’)) return md5.hexdigest().lower() # 示例使用 login_params { “username”: “13800138000”, “password”: “RSA加密后的密文”, # 此处先占位下面会生成 “timestamp”: str(int(time.time() * 1000)), # 毫秒级时间戳 “nonce”: uuid.uuid4().hex[:8], # 生成8位随机字符串 “deviceId”: “android_test_123” } app_secret “CheZhiYing_2023Sec” # 此为示例密钥实际分析中获取 signature generate_sign(login_params, app_secret) print(f“生成的签名: {signature}”)6.2 密码加密算法RSA还原密码通常使用RSA公钥加密。我们需要将从代码中提取的公钥字符串通常是PEM格式但可能去掉了头尾标记正确加载。from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 import base64 def rsa_encrypt_password(plain_password, public_key_str): “”” 使用RSA公钥加密密码 :param plain_password: str, 明文密码 :param public_key_str: str, PEM格式的公钥字符串可能不含头尾 :return: str, Base64编码后的密文 “”” # 1. 处理公钥字符串如果缺少头尾标记则加上 if not public_key_str.startswith(‘—–BEGIN PUBLIC KEY—–‘): public_key_str ‘—–BEGIN PUBLIC KEY—–\n‘ public_key_str ‘\n—–END PUBLIC KEY—–‘ # 2. 导入公钥 public_key RSA.import_key(public_key_str) # 3. 创建加密器使用PKCS1_v1_5填充模式这是最常见的 cipher PKCS1_v1_5.new(public_key) # 4. 加密。RSA加密有长度限制需要分段。但密码通常较短直接加密。 # 输入需要是bytes plaintext plain_password.encode(‘utf-8’) ciphertext cipher.encrypt(plaintext) # 5. Base64编码 encrypted_b64 base64.b64encode(ciphertext).decode(‘utf-8’) return encrypted_b64 # 示例使用公钥为示例非真实 sample_public_key “““MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1W1…””” # 省略部分 password “my_password_123” encrypted_password rsa_encrypt_password(password, sample_public_key) print(f“加密后的密码: {encrypted_password}”) # 将加密后的密码填入上面的login_params中 login_params[‘password’] encrypted_password6.3 完整登录请求组装现在我们可以组装一个完整的、可发送的登录请求了。import requests import json def simulate_login(username, password): # 1. 准备参数 params { “username”: username, “timestamp”: str(int(time.time() * 1000)), “nonce”: uuid.uuid4().hex[:8], “deviceId”: “android_模拟设备ID” } # 2. RSA加密密码 encrypted_pwd rsa_encrypt_password(password, REAL_PUBLIC_KEY) # 替换为真实公钥 params[‘password’] encrypted_pwd # 3. 生成签名注意签名时通常不包含sign字段本身 sign generate_sign(params, REAL_APP_SECRET) # 替换为真实密钥 params[‘sign’] sign # 4. 发送请求 headers { ‘Content-Type’: ‘application/json; charsetUTF-8’, ‘User-Agent’: ‘Dalvik/2.1.0 (Linux; U; Android 11; Pixel 5 Build/RQ3A.210805.001.A1)’ } login_url “https://api.chezhijing.com/mobile/login” try: response requests.post(login_url, datajson.dumps(params), headersheaders, timeout10) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f“请求失败: {e}”) return None # 测试调用 # result simulate_login(“13800138000”, “123456”) # print(result)注意事项在实际还原时务必注意字符串编码UTF-8、参数排序规则字典序、空值处理是否参与签名、以及密钥的准确性和完整性。一个字符的差异都会导致签名校验失败。建议将Hook到的原始参数和计算中间结果与自己的还原算法进行逐行对比调试。7. 常见问题排查与深度避坑指南在算法还原和模拟请求的过程中你几乎一定会遇到各种问题导致请求失败。下面是我总结的常见问题排查清单和避坑经验。7.1 签名校验失败Sign Error这是最常见的问题。服务端返回“签名错误”或“sign invalid”。请按以下顺序排查参数遗漏或多余检查参与签名的参数是否齐全。关键点有些签名会包含所有请求参数包括URL查询参数而有些只包含Body参数。甚至可能包含一些隐藏的固定参数。通过HookgenerateSign函数对比你组装的参数字典和APP实际传入的字典确保键值对完全一致。参数值格式不一致时间戳是字符串还是数字deviceId的格式是否完全一致布尔值true/false是字符串还是布尔类型在拼接签名串时所有值都必须转换为字符串且格式要与APP端完全一致。Hook时打印出拼接前的paramsMap仔细核对每个值的类型和字符串形式。拼接顺序与分隔符确认是keyvalue还是key:value\n末尾的是否去除密钥是直接拼接还是在前面加其他字符如secret最可靠的方法是在Hook时打印出最终传入MD5函数的那个原始字符串signStr然后在你自己的代码里还原出完全一样的字符串。可以使用print(repr(sign_str))来查看不可见字符。密钥错误确认使用的secret是否正确且完整。它可能不是简单的字符串而是经过一次MD5哈希后的结果或者需要从其他接口动态获取。动态Hook是获取其真实值的不二法门。编码问题确保拼接和哈希计算时使用的编码是UTF-8。Python的hashlib.md5().update()默认接受bytes用.encode(‘utf-8’)转换。如果参数值包含中文等非ASCII字符编码不一致会导致签名不同。7.2 密码解密失败Password Error如果服务端提示密码错误但确认明文密码正确问题出在加密环节。公钥格式从代码中提取的公钥字符串可能不是标准的PEM格式。它可能去掉了—–BEGIN PUBLIC KEY—–头尾标记或者是以X.509格式存储。你需要根据代码中加载密钥的方式例如使用KeyFactory.getInstance(“RSA”)和X509EncodedKeySpec来判断其格式并在Python中用相应方式加载。有时公钥是Base64编码的需要先解码。加密填充模式RSA加密必须指定填充模式。最常见的是PKCS1_v1_5这也是Java默认的。但有些APP可能使用OAEP填充。在Java代码中查找Cipher.getInstance()的传参如果是“RSA/ECB/PKCS1Padding”则对应PKCS1_v1_5如果是“RSA/ECB/OAEPWithSHA-256AndMGF1Padding”则对应OAEP。Python的Crypto库需要指定对应的填充模式。分段加密如果密码很长可能需要分段加密。但登录密码通常不会超过RSA密钥长度如2048位所能加密的最大明文长度例如PKCS1_v1_5填充下约245字节。如果遇到超长密码需要查看APP代码是否实现了分段加密逻辑。7.3 其他常见防御与绕过设备指纹deviceId这个deviceId可能不是简单的Android ID或IMEI它可能是由多个设备参数如品牌、型号、序列号、MAC地址等组合后经过特定算法如MD5生成的。你需要找到生成这个ID的代码并模拟生成一个固定的或符合规则的ID。频繁更换可能触发风控。非对称加密协商更复杂的方案是客户端先请求一个临时的公钥或会话密钥再用这个临时密钥来加密密码。这意味着public_key不是写死在客户端的。你需要分析登录前的握手流程。算法混淆与Native层实现核心算法可能被移到C/C编写的so库中以增加逆向难度。此时需要分析JNI接口或者使用Frida直接Hook Native层的函数使用Interceptor.attach。这需要更高的技巧但思路不变定位函数、Hook输入输出、分析逻辑。请求重放与时效性timestamp和nonce就是用于防止重放攻击的。确保你的时间戳与服务器时间同步可以取服务器时间nonce每次请求必须不同。7.4 调试与验证技巧本地验证在完全模拟发送网络请求前可以先进行本地验证。用Frida Hook到APP计算签名和加密密码的瞬间记录下① 输入参数 ② 输出的签名/密文。然后暂停你的Python脚本用记录下的输入参数运行你的算法看输出是否与Hook到的结果完全一致。这是最有效的调试方法。分步替换在模拟请求时可以先尝试用抓包工具如Charles的“断点”或“重写”功能将原始请求中的sign或password替换成你自己生成的然后发送。观察服务器响应。这样可以隔离问题确定是哪个参数计算错误。日志比对在APP代码中可能会在调试模式下输出日志。可以尝试Hookandroid.util.Log类查看是否有关于网络请求或安全模块的日志输出这能提供宝贵线索。逆向分析是一个需要耐心和细致的过程每一个细节都可能成为突破口或绊脚石。对于“车智赢”登录协议的分析从抓包定位到算法还原整套流程体现了移动端协议分析的典型方法论。掌握这套方法后面对其他APP的类似需求你都能有条不紊地层层深入最终达成目标。记住动态验证是检验真理的唯一标准多Hook多比对成功还原就在眼前。
车智赢APP登录协议逆向分析:签名算法与RSA加密还原实战
发布时间:2026/7/4 11:39:13
1. 项目概述从登录请求到算法核心做移动端安全分析的朋友对登录协议的逆向分析应该都不陌生。这几乎是进入一个APP安全研究领域的“敲门砖”也是理解其客户端与服务器交互逻辑最直接的切入点。最近我花了些时间深入分析了“车智赢”这款APP的登录流程目标很明确找到其登录请求中核心参数的生成算法特别是那个关键的签名sign或令牌token是如何被构造出来的。为什么是登录协议因为登录环节往往是安全机制最集中、最典型的地方。一个设计良好的登录协议会包含设备指纹、请求签名、数据加密、防重放攻击等多种防护手段。逆向分析它不仅能让我们理解其安全架构更能为后续的自动化测试、协议模拟或安全审计打下坚实基础。对于“车智赢”这类涉及车辆控制、状态查询等敏感操作的APP其安全性的强弱直接关系到用户隐私和资产安全因此分析其底层实现具有很高的参考价值。本次分析将聚焦于“核心算法篇”这意味着我们会绕过基础的抓包、定位等步骤直接深入到最关键的加密/签名函数内部剖析其输入、输出、逻辑流程以及关键的密钥或盐值salt是如何参与运算的。整个过程会涉及静态分析反编译、动态调试Hook、算法还原等多个环节。无论你是移动安全研究员、爬虫工程师还是对安卓逆向感兴趣的开发者相信这篇详尽的拆解都能给你带来直接的帮助和启发。2. 分析环境与工具链搭建工欲善其事必先利其器。一套稳定、高效的分析环境是逆向工程成功的基础。对于安卓APP的协议分析我的工具链通常分为几个层次抓包层、动态调试层、静态分析层和辅助工具层。2.1 核心工具选型与配置首先抓包工具我首选Charles或Fiddler两者都能很好地完成HTTPS流量的拦截和解密。关键在于手机端证书的安装与信任。这里有个细节在安卓高版本特别是7.0以上系统中系统不再信任用户安装的证书导致无法解密HTTPS流量。解决方案通常有两种一是将抓包工具的证书安装到系统证书目录需要Root权限二是使用像VirtualXposed、太极这类无需Root即可修改APP运行环境从而使其信任用户证书的工具。对于“车智赢”我采用了Root过的真机环境直接将Charles证书推送为系统证书一劳永逸。注意部分APP会启用SSL Pinning证书绑定技术检测到非预期的证书会中断连接。此时就需要使用JustTrustMe、SSLUnpinning等Xposed模块或者使用Frida脚本在内存中绕过证书校验。在分析初期如果发现抓包工具无法捕获到登录请求大概率就是遇到了证书绑定。动态调试和代码注入方面Frida是目前当之无愧的王者。它是一个动态代码插桩工具可以让我们在运行时拦截、修改函数调用或者直接调用APP内的任何函数。我通常会准备一套Frida Server运行在手机端、Frida Python库运行在电脑端以及一系列常用的脚本。对于登录算法最常用的就是Hook Java层函数打印其输入参数和返回值。静态分析则依赖于反编译工具。Jadx-GUI是我的首选它能够将APK文件中的DEX字节码反编译成可读性相当高的Java代码并且支持全局搜索、跳转引用效率极高。对于混淆严重的代码有时还需要结合IDA Pro或Ghidra来分析底层的so库Native层代码。我的完整工作流是先用抓包工具捕获到登录请求定位到关键的加密参数如sign然后在Jadx中搜索这个参数名或者搜索其可能出现的URL路径、接口名找到疑似函数后编写Frida脚本进行Hook验证最后结合静态分析理清整个算法的逻辑并用Python或JavaScript进行还原。2.2 关键环境配置细节Root环境选择我使用了一台刷了Magisk的Pixel手机。Magisk的Systemless Root特性对APP的隐藏性更好可以绕过一些基础的Root检测。Frida版本匹配务必确保电脑端frida、frida-tools的版本与手机端frida-server的版本一致否则会出现连接失败或无法识别API的问题。我当前使用的是Frida 16.x版本。抓包过滤器设置在Charles中我会设置一个Focus主机比如*.che-zhi-ying.com这样能过滤掉大量无关的静态资源请求让登录请求一目了然。Jadx的优化在Jadx的设置中开启“反混淆”选项如果支持并调整反编译器为“Fallback”模式有时能获得更好的代码。对于大型APK首次反编译和索引会较慢耐心等待即可。这套环境搭建起来可能需要一些时间但一旦就绪它就是一个强大的、可复用的分析平台能够应对大多数安卓APP的逆向需求。3. 登录请求抓包与关键参数定位一切准备就绪后我们启动“车智赢”APP进行登录操作可以使用测试账号同时在Charles中观察捕获到的网络请求。很快我们就能锁定登录接口。通常它的路径会是类似于/api/v1/user/login或/auth/login这样的形式。以我抓取到的请求为例请求方法 POST请求URLhttps://api.chezhijing.com/mobile/login请求头 包含常见的Content-Type: application/jsonUser-Agent等。请求体(JSON格式){ “username”: “13800138000”, “password”: “加密后的字符串”, “timestamp”: “1646389200000”, “nonce”: “a1b2c3d4e5”, “sign”: “7f8e9a0b1c2d3e4f5a6b7c8d9e0f1a2b”, “deviceId”: “android_xxxxxx” }从这个请求体中我们可以立刻识别出几个关键角色username 明文用户名或手机号。password 明显是经过加密处理后的密文这是首要分析目标。timestamp 时间戳常用于防重放攻击。nonce 随机数同样用于防重放确保每次请求唯一。sign 签名这通常是整个请求安全性的核心。它由其他参数可能包括请求体、请求头、甚至一个密钥按照特定算法生成用于服务端验证请求的完整性和合法性。deviceId 设备标识符。我们的核心目标就是password的加密算法和sign的签名算法。通常password的加密可能相对独立如RSA公钥加密、AES加密而sign的生成则会综合多个参数。定位技巧 在Jadx中我们可以使用全局搜索快捷键Shift Shift。首先搜索sign这个关键词会找到很多地方。我们需要结合上下文判断比如查找赋值语句sign 或者查找包含Map、TreeMap常用于参数排序以及MD5、SHA256、HMAC等加密相关字符串的代码。另一个有效方法是搜索接口URL中的关键字如“/mobile/login”直接定位到处理登录请求的代码附近。4. 静态分析与算法函数定位通过搜索“sign”和登录URL我在Jadx中找到了一个名为com.chezhijing.network.security.SignGenerator的类。这个类名非常直观很可能就是我们要找的目标。打开这个类发现其核心是一个generateSign方法。代码经过了混淆但关键逻辑依然可辨。该方法接收一个MapString, String参数存放所有待签名的参数和一个String参数可能是密钥或盐值返回计算出的签名。public class SignGenerator { public static String generateSign(MapString, String params, String secret) { // 1. 参数排序 ListString keys new ArrayList(params.keySet()); Collections.sort(keys); // 2. 拼接键值对 StringBuilder sb new StringBuilder(); for (String key : keys) { String value params.get(key); if (value ! null !value.isEmpty()) { sb.append(key).append(“”).append(value).append(“”); } } // 去掉最后一个“” if (sb.length() 0) { sb.deleteCharAt(sb.length() - 1); } // 3. 拼接密钥 sb.append(secret); // 注意这里是直接拼接也可能是其他方式 // 4. 进行哈希计算 String signStr sb.toString(); return md5(signStr); // 这里看到是MD5也可能是SHA256等 } private static String md5(String input) { try { MessageDigest md MessageDigest.getInstance(“MD5”); byte[] digest md.digest(input.getBytes(“UTF-8”)); StringBuilder hexString new StringBuilder(); for (byte b : digest) { String hex Integer.toHexString(0xff b); if (hex.length() 1) hexString.append(‘0’); hexString.append(hex); } return hexString.toString(); } catch (Exception e) { e.printStackTrace(); return “”; } } }从这段代码可以清晰还原出签名算法参数排序将所有待签名参数不包括sign本身按键名进行字典序排序。拼接字符串将排序后的参数按keyvalue格式拼接成一个字符串。添加密钥在拼接好的字符串末尾直接追加上一个密钥secret。这个secret是分析的关键它可能硬编码在代码中也可能从服务器动态获取。计算MD5对最终的字符串进行MD5哈希得到32位小写的十六进制字符串作为签名。接下来需要找到secret是什么。继续在代码中搜索SignGenerator.generateSign的调用处发现在一个网络请求拦截器或封装类里调用时传入的secret是一个从SecurityConfig.getAppSecret()获取的值。追踪SecurityConfig类发现appSecret是一个静态常量其值为“CheZhiYing_2023Sec”此处为示例实际值需分析确认。这里是一个重要的注意事项很多APP会将密钥进行简单的编码或拆分不会明文写在代码里可能需要分析初始化流程或通过动态调试来获取。同时我们也需要分析password的加密。搜索password的赋值处可能找到类似params.put(“password”, EncryptUtil.rsaEncrypt(rawPassword))的代码。追踪EncryptUtil发现它使用了RSA加密并且公钥PUBLIC_KEY也以字符串常量形式存储在代码中。RSA加密通常采用分段加密和Base64输出。至此通过静态分析我们已经初步掌握了两个核心算法的逻辑框架签名是排序拼接MD5密码加密是RSA公钥加密。但这还不够我们需要用动态调试来验证这些逻辑并获取确切的密钥和参数。5. 动态调试验证与密钥获取静态分析给出的代码逻辑是“应该是这样”而动态调试能告诉我们“实际上就是这样”。我们使用Frida来验证SignGenerator.generateSign函数。编写一个Frida脚本Java.perform(function() { var SignGenerator Java.use(‘com.chezhijing.network.security.SignGenerator’); var overloads SignGenerator.generateSign.overloads; for (var i 0; i overloads.length; i) { if (overloads[i].hasOwnProperty(‘argumentTypes’)) { // Hook所有重载 SignGenerator.generateSign.overloads[i].implementation function(params, secret) { console.log(“[SignGenerator.generateSign] called!”); console.log(“Params: “ JSON.stringify(params)); console.log(“Secret: “ secret); var result this.generateSign(params, secret); console.log(“Result Sign: “ result); console.log(“---”); return result; }; } } });运行脚本触发登录操作。在Frida控制台我们看到了真实的调用[SignGenerator.generateSign] called! Params: {“username”:“13800138000”, “password”:“xxx”, “timestamp”:“1646389200000”, “nonce”:“a1b2c3d4e5”, “deviceId”:“android_xxxx”} Secret: CheZhiYing_2023Sec Result Sign: 7f8e9a0b1c2d3e4f5a6b7c8d9e0f1a2b动态Hook完美验证了我们的静态分析传入的参数Map、使用的密钥secret以及计算出的sign值都与抓包数据吻合。这证实了签名算法无误。接下来验证密码加密。同样用Frida HookEncryptUtil.rsaEncrypt方法打印出明文密码和加密后的结果确认其与请求中的password字段一致并验证使用的公钥。实操心得在动态调试时可能会遇到函数重载overload的情况。上面的脚本通过遍历.overloads来处理所有重载版本是一种稳妥的做法。另外如果APP启用了反调试或Frida检测可能需要使用一些对抗手段比如使用定制版的Frida、或者先使用其他工具如Objection进行附着。6. 核心算法还原与Python实现经过静态和动态分析我们已经掌握了所有细节。现在用Python将这两个核心算法还原出来。这不仅是分析的成果也是后续进行协议模拟、自动化测试的基础。6.1 签名算法Sign还原签名算法的关键在于参数的排序和拼接顺序必须与APP端完全一致。import hashlib import time import uuid def generate_sign(params, secret): “”” 生成请求签名 :param params: dict, 待签名的参数字典 :param secret: str, 密钥 :return: str, 32位小写MD5签名 “”” # 1. 参数排序 sorted_keys sorted(params.keys()) # 2. 拼接键值对 sign_str_parts [] for key in sorted_keys: value params.get(key) if value is not None and str(value) ! ‘’: # 注意value需要转换为字符串拼接格式为 keyvalue sign_str_parts.append(f“{key}{value}”) # 用‘’连接所有键值对 sign_str “”.join(sign_str_parts) # 3. 拼接密钥 sign_str secret # 4. 计算MD5 md5 hashlib.md5() md5.update(sign_str.encode(‘utf-8’)) return md5.hexdigest().lower() # 示例使用 login_params { “username”: “13800138000”, “password”: “RSA加密后的密文”, # 此处先占位下面会生成 “timestamp”: str(int(time.time() * 1000)), # 毫秒级时间戳 “nonce”: uuid.uuid4().hex[:8], # 生成8位随机字符串 “deviceId”: “android_test_123” } app_secret “CheZhiYing_2023Sec” # 此为示例密钥实际分析中获取 signature generate_sign(login_params, app_secret) print(f“生成的签名: {signature}”)6.2 密码加密算法RSA还原密码通常使用RSA公钥加密。我们需要将从代码中提取的公钥字符串通常是PEM格式但可能去掉了头尾标记正确加载。from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 import base64 def rsa_encrypt_password(plain_password, public_key_str): “”” 使用RSA公钥加密密码 :param plain_password: str, 明文密码 :param public_key_str: str, PEM格式的公钥字符串可能不含头尾 :return: str, Base64编码后的密文 “”” # 1. 处理公钥字符串如果缺少头尾标记则加上 if not public_key_str.startswith(‘—–BEGIN PUBLIC KEY—–‘): public_key_str ‘—–BEGIN PUBLIC KEY—–\n‘ public_key_str ‘\n—–END PUBLIC KEY—–‘ # 2. 导入公钥 public_key RSA.import_key(public_key_str) # 3. 创建加密器使用PKCS1_v1_5填充模式这是最常见的 cipher PKCS1_v1_5.new(public_key) # 4. 加密。RSA加密有长度限制需要分段。但密码通常较短直接加密。 # 输入需要是bytes plaintext plain_password.encode(‘utf-8’) ciphertext cipher.encrypt(plaintext) # 5. Base64编码 encrypted_b64 base64.b64encode(ciphertext).decode(‘utf-8’) return encrypted_b64 # 示例使用公钥为示例非真实 sample_public_key “““MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1W1…””” # 省略部分 password “my_password_123” encrypted_password rsa_encrypt_password(password, sample_public_key) print(f“加密后的密码: {encrypted_password}”) # 将加密后的密码填入上面的login_params中 login_params[‘password’] encrypted_password6.3 完整登录请求组装现在我们可以组装一个完整的、可发送的登录请求了。import requests import json def simulate_login(username, password): # 1. 准备参数 params { “username”: username, “timestamp”: str(int(time.time() * 1000)), “nonce”: uuid.uuid4().hex[:8], “deviceId”: “android_模拟设备ID” } # 2. RSA加密密码 encrypted_pwd rsa_encrypt_password(password, REAL_PUBLIC_KEY) # 替换为真实公钥 params[‘password’] encrypted_pwd # 3. 生成签名注意签名时通常不包含sign字段本身 sign generate_sign(params, REAL_APP_SECRET) # 替换为真实密钥 params[‘sign’] sign # 4. 发送请求 headers { ‘Content-Type’: ‘application/json; charsetUTF-8’, ‘User-Agent’: ‘Dalvik/2.1.0 (Linux; U; Android 11; Pixel 5 Build/RQ3A.210805.001.A1)’ } login_url “https://api.chezhijing.com/mobile/login” try: response requests.post(login_url, datajson.dumps(params), headersheaders, timeout10) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f“请求失败: {e}”) return None # 测试调用 # result simulate_login(“13800138000”, “123456”) # print(result)注意事项在实际还原时务必注意字符串编码UTF-8、参数排序规则字典序、空值处理是否参与签名、以及密钥的准确性和完整性。一个字符的差异都会导致签名校验失败。建议将Hook到的原始参数和计算中间结果与自己的还原算法进行逐行对比调试。7. 常见问题排查与深度避坑指南在算法还原和模拟请求的过程中你几乎一定会遇到各种问题导致请求失败。下面是我总结的常见问题排查清单和避坑经验。7.1 签名校验失败Sign Error这是最常见的问题。服务端返回“签名错误”或“sign invalid”。请按以下顺序排查参数遗漏或多余检查参与签名的参数是否齐全。关键点有些签名会包含所有请求参数包括URL查询参数而有些只包含Body参数。甚至可能包含一些隐藏的固定参数。通过HookgenerateSign函数对比你组装的参数字典和APP实际传入的字典确保键值对完全一致。参数值格式不一致时间戳是字符串还是数字deviceId的格式是否完全一致布尔值true/false是字符串还是布尔类型在拼接签名串时所有值都必须转换为字符串且格式要与APP端完全一致。Hook时打印出拼接前的paramsMap仔细核对每个值的类型和字符串形式。拼接顺序与分隔符确认是keyvalue还是key:value\n末尾的是否去除密钥是直接拼接还是在前面加其他字符如secret最可靠的方法是在Hook时打印出最终传入MD5函数的那个原始字符串signStr然后在你自己的代码里还原出完全一样的字符串。可以使用print(repr(sign_str))来查看不可见字符。密钥错误确认使用的secret是否正确且完整。它可能不是简单的字符串而是经过一次MD5哈希后的结果或者需要从其他接口动态获取。动态Hook是获取其真实值的不二法门。编码问题确保拼接和哈希计算时使用的编码是UTF-8。Python的hashlib.md5().update()默认接受bytes用.encode(‘utf-8’)转换。如果参数值包含中文等非ASCII字符编码不一致会导致签名不同。7.2 密码解密失败Password Error如果服务端提示密码错误但确认明文密码正确问题出在加密环节。公钥格式从代码中提取的公钥字符串可能不是标准的PEM格式。它可能去掉了—–BEGIN PUBLIC KEY—–头尾标记或者是以X.509格式存储。你需要根据代码中加载密钥的方式例如使用KeyFactory.getInstance(“RSA”)和X509EncodedKeySpec来判断其格式并在Python中用相应方式加载。有时公钥是Base64编码的需要先解码。加密填充模式RSA加密必须指定填充模式。最常见的是PKCS1_v1_5这也是Java默认的。但有些APP可能使用OAEP填充。在Java代码中查找Cipher.getInstance()的传参如果是“RSA/ECB/PKCS1Padding”则对应PKCS1_v1_5如果是“RSA/ECB/OAEPWithSHA-256AndMGF1Padding”则对应OAEP。Python的Crypto库需要指定对应的填充模式。分段加密如果密码很长可能需要分段加密。但登录密码通常不会超过RSA密钥长度如2048位所能加密的最大明文长度例如PKCS1_v1_5填充下约245字节。如果遇到超长密码需要查看APP代码是否实现了分段加密逻辑。7.3 其他常见防御与绕过设备指纹deviceId这个deviceId可能不是简单的Android ID或IMEI它可能是由多个设备参数如品牌、型号、序列号、MAC地址等组合后经过特定算法如MD5生成的。你需要找到生成这个ID的代码并模拟生成一个固定的或符合规则的ID。频繁更换可能触发风控。非对称加密协商更复杂的方案是客户端先请求一个临时的公钥或会话密钥再用这个临时密钥来加密密码。这意味着public_key不是写死在客户端的。你需要分析登录前的握手流程。算法混淆与Native层实现核心算法可能被移到C/C编写的so库中以增加逆向难度。此时需要分析JNI接口或者使用Frida直接Hook Native层的函数使用Interceptor.attach。这需要更高的技巧但思路不变定位函数、Hook输入输出、分析逻辑。请求重放与时效性timestamp和nonce就是用于防止重放攻击的。确保你的时间戳与服务器时间同步可以取服务器时间nonce每次请求必须不同。7.4 调试与验证技巧本地验证在完全模拟发送网络请求前可以先进行本地验证。用Frida Hook到APP计算签名和加密密码的瞬间记录下① 输入参数 ② 输出的签名/密文。然后暂停你的Python脚本用记录下的输入参数运行你的算法看输出是否与Hook到的结果完全一致。这是最有效的调试方法。分步替换在模拟请求时可以先尝试用抓包工具如Charles的“断点”或“重写”功能将原始请求中的sign或password替换成你自己生成的然后发送。观察服务器响应。这样可以隔离问题确定是哪个参数计算错误。日志比对在APP代码中可能会在调试模式下输出日志。可以尝试Hookandroid.util.Log类查看是否有关于网络请求或安全模块的日志输出这能提供宝贵线索。逆向分析是一个需要耐心和细致的过程每一个细节都可能成为突破口或绊脚石。对于“车智赢”登录协议的分析从抓包定位到算法还原整套流程体现了移动端协议分析的典型方法论。掌握这套方法后面对其他APP的类似需求你都能有条不紊地层层深入最终达成目标。记住动态验证是检验真理的唯一标准多Hook多比对成功还原就在眼前。