逆向工程实战:58同城App密码加密算法解析与Python复现
1. 项目概述与核心价值
最近在和一些做数据采集、自动化测试的朋友交流时,经常听到一个话题:现在很多App的接口,尤其是涉及用户登录、交易等核心业务的接口,加密和风控做得越来越复杂,逆向分析的难度直线上升。这确实是个痛点,无论是出于安全研究、自动化工具开发,还是单纯的技术好奇心,理解一个成熟商业应用的加密逻辑,都像是一场充满挑战的解谜游戏。
今天,我们就以“58同城App的密码加密”为具体案例,来一场深度的技术拆解。选择58同城,一方面是因为它是一个用户体量巨大、业务场景复杂的国民级应用,其安全策略具有很高的代表性;另一方面,它的加密机制在业内也常被提及,但系统性的分析文章并不多见。通过这个项目,我们不仅能搞清楚“58的密码到底是怎么加密的”,更能掌握一套分析移动端加密的通用方法论。这对于从事移动安全、爬虫开发、自动化测试,甚至是前端逆向的工程师来说,都是一次极佳的实战演练。
简单来说,这个项目的核心价值在于:逆向解析58同城App登录过程中,用户明文密码被转换、加密、传输的全链路逻辑,并复现其加密算法。最终,我们将得到一个可以脱离App环境、独立运行的密码加密函数。这听起来像是一个“黑盒测试”,但我们的目标是以白盒的视角去理解它,整个过程会涉及Android逆向基础、网络抓包、静态/动态分析、算法还原等多个环节。无论你是想提升自己的逆向工程能力,还是需要为你的自动化脚本解决登录难题,相信这篇详尽的复盘都能给你带来直接的帮助。
2. 逆向分析的整体思路与工具选型
面对一个闭源的商业App,我们不可能拿到它的源代码。因此,我们的核心思路是“由外而内,动静结合”。具体来说,就是先观察其外部网络行为,定位关键函数,再深入内部代码逻辑进行还原。
2.1 核心分析思路拆解
整个分析流程可以概括为以下四个步骤,它们环环相扣:
网络行为观测(抓包):这是所有逆向的起点。我们需要捕获App在登录时发送的HTTP/HTTPS请求,重点关注请求体(Body)中密码字段的形态。它是一个长长的、看似随机的字符串吗?它的长度固定吗?每次登录相同密码,这个加密结果会变化吗?这些初步观察能为我们后续的分析提供关键方向。例如,如果密文每次不同,那很可能使用了随机盐(Salt)或时间戳;如果长度固定且较短,可能是哈希(Hash)算法;如果很长且结构复杂,可能是非对称加密或自定义的编码结果。
关键代码定位(Hook/搜索):知道了加密后的“结果”,下一步就是找到生产这个结果的“工厂”。我们有两种主要手段:
- 静态搜索:将App安装包(APK)进行反编译,得到Java/Smali代码。然后,我们可以用加密后的密文(或其特征)作为关键词,在代码中搜索。或者,搜索常见的加密算法关键词,如“AES”、“RSA”、“MD5”、“SHA”、“encrypt”、“encode”等。
- 动态Hook:这是更高效、更精准的方法。我们使用Frida或Xposed等框架,在App运行时,对疑似加密函数进行监控和拦截。通过打印函数的输入(参数)和输出(返回值),我们可以直接确认哪个函数负责将明文密码转换成我们抓包看到的密文。
算法逻辑还原(代码分析):定位到关键函数后,我们需要仔细阅读其反编译后的代码(可能是Java,也可能是更底层的Smali或Native C/C++代码)。分析它的具体实现:调用了哪些系统API或第三方库?盐值(Salt)、密钥(Key)、初始化向量(IV)从哪里来?加密模式是什么?有没有进行额外的编码(如Base64、Hex)?这个过程需要耐心和一定的密码学知识。
算法独立复现(编码实现):理解算法后,最后一步就是用我们熟悉的编程语言(如Python、JavaScript)将这个过程重新实现一遍。用相同的明文密码、盐值、密钥等参数,运行我们编写的函数,看其输出是否与抓包捕获的密文完全一致。只有成功复现,才算真正完成了逆向。
2.2 工具链选型与配置
工欲善其事,必先利其器。以下是本次分析推荐的工具组合,它们覆盖了从抓包到代码还原的整个链条:
抓包工具:Charles / Fiddler / HTTP Toolkit
- 作用:拦截和查看App发出的网络请求。
- 选择理由:Charles和Fiddler是老牌且功能强大的代理工具,支持HTTPS解密(需在手机和电脑上安装证书)。HTTP Toolkit是后起之秀,界面更现代,对移动端抓包非常友好。任选其一即可。
- 关键配置:务必完成手机代理设置和CA证书的安装与信任,否则无法解密HTTPS流量。
逆向分析平台:Android Studio + 模拟器(如夜神、雷电)或 真机
- 作用:运行目标App,并提供调试基础。
- 选择理由:模拟器环境隔离性好,可以随意安装插件、修改系统,不怕搞崩。真机则更贴近用户真实环境。建议初次分析使用模拟器。
- 关键配置:模拟器需要开启Root权限,以便运行Frida-server等高级工具。
反编译工具:Jadx / JEB / Apktool
- 作用:将APK文件反编译成可读的Java代码或Smali中间码。
- 选择理由:Jadx是免费开源首选,能直接将Dex文件转为Java代码,浏览和搜索非常方便。JEB是商业软件,反编译和解析能力更强。Apktool主要用于反编译资源文件和得到Smali代码,适合深度修改。
- 工作流:通常先用Jadx进行全局搜索和浏览,快速定位;复杂逻辑或Jadx解析不佳时,再辅以JEB或直接看Smali。
动态注入框架:Frida
- 作用:在App运行时,动态注入JavaScript脚本,用于Hook(挂钩)Java/Native函数,跟踪参数和返回值。
- 选择理由:Frida是目前移动端动态分析的“神器”,跨平台、脚本编写灵活、社区活跃。相比Xposed,它不需要修改系统,可以随时附着和脱离目标进程,更加轻量和安全。
- 关键准备:需要根据手机架构(通常是arm64)下载对应的
frida-server,并推送到手机中运行。
脚本编写语言:Python + Frida
- 作用:编写控制Frida和进行算法复现的脚本。
- 选择理由:Python有丰富的库(如
frida,requests,hashlib,Crypto)支持整个流程。Frida的API也主要通过Python调用。
注意:本分析仅用于安全研究与学习目的,旨在提升开发者的安全意识和防御能力。请勿将技术用于非法爬取用户数据、攻击系统等违反法律法规和服务条款的行为。尊重知识产权和用户隐私。
3. 实战第一步:网络抓包与加密特征观察
理论说得再多,不如动手一试。我们首先从最外层的网络请求开始。
3.1 配置抓包环境
- 启动抓包代理:以Charles为例,启动后记住电脑的IP地址(如
192.168.1.100)和代理端口(默认为8888)。 - 配置手机网络代理:在手机Wi-Fi设置中,手动配置代理,服务器填写电脑IP,端口填写
8888。 - 安装CA证书:用手机浏览器访问
chls.pro/ssl(Charles)或相应地址,下载并安装Charles的CA证书。在Android高版本中,安装后还需到“设置->安全->加密与凭据->信任的凭据->用户”中确认证书已启用。 - 目标App准备:在手机上安装58同城App(建议从官方渠道下载,分析特定版本)。
3.2 捕获登录请求
- 打开Charles,确保
Proxy -> macOS Proxy(或Windows Proxy)未被勾选,我们只监听手机流量。 - 清空Charles的请求记录。
- 在58同城App上,尝试使用手机号密码登录(可以使用测试账号)。
- 在Charles中,你会看到瞬间出现大量请求。我们需要找到登录接口。通常可以通过URL路径关键词来筛选,如包含
login、passport、auth等。或者直接观察请求体较大的POST请求。
经过抓包,我们很可能找到一个类似https://passport.58.com/api/login的请求。查看其请求体(通常为JSON或Form格式),关键字段如下:
{ “username”: “13800138000”, “password”: “aBcDeFgHiJkL123...(一长串密文)”, “key”: “...”, “timestamp”: “1644567890123”, // ... 其他参数 }关键观察点(以假设的抓包结果为例):
password字段:值是一串非常长的、由字母数字组成的字符串(例如,长度超过100位)。这基本排除了简单的MD5或SHA1哈希(它们输出长度固定为32或40位Hex字符)。key字段:存在一个独立的key字段。这强烈暗示了加密过程可能使用了非对称加密(如RSA)——key很可能是用于加密对称密钥(如AES密钥)的公钥,或者是一个临时的会话密钥。timestamp字段:时间戳的存在,意味着加密结果可能与时间相关,用于防止重放攻击。密文可能包含了时间戳的哈希或签名。- 重复实验:用同一个密码,间隔几分钟再次登录。你会发现,每次提交的
password密文都完全不同!这是一个极其重要的信号。它说明加密算法不是确定性的,其输出依赖于某个随时间变化或随机生成的变量(如时间戳、随机数)。
基于这些观察,我们可以做出初步假设:58同城的密码加密,很可能采用了“随机盐(或时间戳) + 多层哈希/对称加密 + RSA公钥加密”的混合模式。接下来,我们就要进入App内部去验证这个假设。
4. 深入核心:静态分析与关键代码定位
现在,我们有了明确的目标:找到生成那串长长密文的函数。
4.1 反编译与初步搜索
- 获取APK:从手机或模拟器中提取58同城App的安装包(APK文件),或者从可靠的APK下载网站获取对应版本。
- 使用Jadx打开APK:将APK文件拖入Jadx,它会自动进行反编译。这个过程可能需要几分钟。
- 全局搜索:
- 搜索密文特征:如果抓包到的密文有固定前缀(如
ENC_),可以尝试搜索。但通常密文是随机的,此方法效果有限。 - 搜索关键参数名:搜索
password、key、encrypt、encode、login等。 - 搜索加密算法类:搜索
RSA、AES、Cipher、MessageDigest、Security等。
- 搜索密文特征:如果抓包到的密文有固定前缀(如
在Jadx的搜索框中输入“password”,你可能会发现大量相关代码。我们需要更有策略地缩小范围。一个常见思路是寻找网络请求层。可以搜索网络库的类名,如OkHttpClient、Retrofit、Interceptor,或者搜索序列化库如Gson、Fastjson。更直接的是,搜索登录接口的URL路径的一部分,如/api/login。
假设我们找到了一个名为LoginService的接口或LoginRequest的数据模型类。在其附近,很可能存在密码处理的逻辑。
4.2 定位加密函数入口
在商业App中,密码加密通常不会在UI层直接完成,也不会在网络库的最底层。它往往被封装在一个独立的“安全工具类”或“加密模块”中。我们需要找到从登录按钮点击到网络请求发出之间的代码链路。
一个有效的方法是查找负责构建登录请求体的方法。例如,找到一个buildLoginParams(String phone, String password)这样的方法。在这个方法里,你会看到原始密码被传递给另一个方法进行处理,比如:
// 伪代码,基于常见模式推断 public Map<String, String> buildLoginParams(String phone, String password) { Map<String, String> params = new HashMap<>(); params.put(“username”, phone); // 关键行:password被加密 String encryptedPwd = SecurityUtil.encryptPassword(password, System.currentTimeMillis()); params.put(“password”, encryptedPwd); params.put(“timestamp”, String.valueOf(System.currentTimeMillis())); // ... 可能还有其他参数生成逻辑 return params; }这里的SecurityUtil.encryptPassword就是我们要找的关键入口函数。通过Jadx的“查找用法”功能,可以点击这个方法,查看哪些地方调用了它,并最终定位到它的定义。
4.3 静态分析加密逻辑
找到encryptPassword或类似函数后,双击进入查看其Java源码。你可能会看到类似下面的逻辑(这是基于常见模式的推测和整合):
// 再次强调,此为推测还原的伪代码,用于讲解逻辑 public class SecurityUtil { private static final String RSA_PUBLIC_KEY = “MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...(一长串公钥)”; // 静态公钥 public static String encryptPassword(String plainPwd, long timestamp) { // 步骤1:生成随机盐或使用时间戳作为盐的一部分 String dynamicSalt = generateRandomSalt(); // 可能是一个8-16位的随机字符串 // 或者 String dynamicSalt = String.valueOf(timestamp); // 步骤2:第一次哈希或拼接 String step1 = md5(plainPwd + dynamicSalt); // 也可能是 sha256, 或者更复杂的拼接 // 步骤3:二次处理,可能拼接其他固定字符串或再次哈希 String step2 = sha256(step1 + “some_fixed_string”); // 步骤4:将动态盐和第二步的结果组合,然后用RSA公钥加密 String dataToEncrypt = dynamicSalt + “|” + step2; // 组合方式可能是 “$”分隔或JSON byte[] rsaEncryptedData = rsaEncrypt(dataToEncrypt.getBytes(), RSA_PUBLIC_KEY); // 步骤5:将二进制加密结果进行Base64编码,得到最终密文 return Base64.encodeToString(rsaEncryptedData, Base64.NO_WRAP); } private static String generateRandomSalt() { // 生成随机字符串的实现 } private static String md5(String input) { /* ... */ } private static String sha256(String input) { /* ... */ } private static byte[] rsaEncrypt(byte[] data, String publicKeyStr) { /* ... */ } }静态分析要点:
- 理清调用链:从入口函数开始,一步步跟进,画出大致的处理流程图。
- 识别算法:注意
Cipher.getInstance(“RSA/ECB/PKCS1Padding”)、MessageDigest.getInstance(“SHA-256”)这样的调用,它们明确指明了算法。 - 关注密钥和参数:
RSA_PUBLIC_KEY是写死在代码里的吗?还是从网络请求获取的?盐(Salt)是如何生成的?这些是后续复现的关键输入。 - 注意编码:最终输出前,是否经过了
Base64或Hex编码?这决定了我们抓包看到的密文格式。
实操心得:静态分析时,代码可能被混淆(类名、方法名、变量名变成a,b,c等无意义字符)。这时,需要依靠对API的熟悉度(如
Cipher,MessageDigest)和字符串常量(如算法名”AES”, 公钥片段)来推断逻辑。动态Hook在此时的价值就凸显出来了,它可以绕过混淆,直接告诉你函数的输入输出。
5. 动态验证与精准Hook
静态分析给了我们一个蓝图,但代码可能被混淆,或者逻辑分支复杂。动态Hook可以让我们在App运行时,像调试一样直接看到数据流动,是验证猜想、定位关键函数最直接的手段。
5.1 编写Frida Hook脚本
我们的目标是Hook那个疑似加密的函数。假设通过静态分析,我们怀疑com.wuba.security.Encryptor类下的encryptPassword方法是目标。
下面是一个基础的Frida JavaScript脚本模板:
// hook_password.js Java.perform(function () { // 指定要Hook的类 var Encryptor = Java.use(‘com.wuba.security.Encryptor’); // Hook目标方法。需要确认方法签名(参数类型和返回值类型)。 // 假设方法签名是:String encryptPassword(String plainText, long timestamp) Encryptor.encryptPassword.overload(‘java.lang.String’, ‘long’).implementation = function (plainText, timestamp) { console.log(“\n[+] EncryptPassword Hooked!”); console.log(“[*] Plain Text: ” + plainText); console.log(“[*] Timestamp: ” + timestamp); // 调用原方法,获取加密结果 var result = this.encryptPassword(plainText, timestamp); console.log(“[*] Encrypted Result: ” + result); // 打印调用栈,帮助定位是谁调用了这个函数 console.log(“[*] Call Stack:”); console.log(Java.use(“android.util.Log”).getStackTraceString(Java.use(“java.lang.Exception”).$new())); return result; // 返回原结果,不影响App正常运行 }; // 也可以Hook更底层的函数,如MessageDigest var MessageDigest = Java.use(‘java.security.MessageDigest’); MessageDigest.getInstance.overload(‘java.lang.String’).implementation = function (algorithm) { console.log(“\n[+] MessageDigest.getInstance called: ” + algorithm); return this.getInstance(algorithm); }; // Hook Cipher的初始化,看用了什么算法和模式 var Cipher = Java.use(‘javax.crypto.Cipher’); Cipher.getInstance.overload(‘java.lang.String’).implementation = function (transformation) { console.log(“\n[+] Cipher.getInstance called: ” + transformation); return this.getInstance(transformation); }; });5.2 运行Hook脚本并触发登录
- 确保手机/模拟器上已运行
frida-server。 - 在电脑上使用命令行,通过Frida将脚本注入到58同城App的进程:
(frida -U -l hook_password.js -f com.wuba --no-pause-U连接USB设备,-l加载脚本,-f启动应用,–no-pause立即启动) - 在手机上操作58同城App,进入登录页面,输入密码点击登录。
- 观察电脑终端输出的日志。
理想情况下,你会看到类似这样的输出:
[+] EncryptPassword Hooked! [*] Plain Text: myPassword123 [*] Timestamp: 1644567890123 [*] Encrypted Result: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJSc...(很长一串Base64) [*] Call Stack: (一堆类和方法名,显示了调用路径)同时,你还会看到Cipher.getInstance被调用,参数可能是”RSA/ECB/PKCS1Padding”,以及MessageDigest.getInstance被调用,参数是”SHA-256”。
动态分析的价值:
- 确认目标函数:直接验证了
encryptPassword就是我们要找的函数。 - 获取精确输入:看到了原始的明文密码和传入的时间戳。
- 验证输出:确认了该函数的输出与我们抓包得到的
password字段密文完全一致。这是决定性证据。 - 揭示算法细节:通过Hook底层
Cipher和MessageDigest,我们知道了具体使用的算法、模式和填充方式。
注意事项:App可能使用了反调试、反Hook技术。如果Frida脚本注入失败或App崩溃,可能需要考虑使用隐藏Frida、修改特征等方式绕过检测。对于加固的App,还需要先进行脱壳处理才能看到原始代码。这是一个更高级的话题,本次分析假设目标App未做高强度加固。
6. 算法复现与Python实现
经过静态分析和动态验证,我们已经掌握了加密算法的所有细节。现在,用Python将其还原出来。假设我们最终分析出的算法如下(综合了常见模式):
- 生成一个8字节的随机字符串作为动态盐(
dynamic_salt)。 - 将明文密码与动态盐拼接,计算其MD5值(32位小写Hex):
step1 = md5(password + dynamic_salt)。 - 将上一步的结果与一个固定的字符串(如
”wuba_sec_salt”)拼接,计算SHA-256值(64位小写Hex):step2 = sha256(step1 + fixed_salt)。 - 将动态盐和SHA-256结果用竖线
|连接:data_to_encrypt = dynamic_salt + “|” + step2。 - 使用RSA公钥(PKCS1填充,无OAEP),加密上一步得到的字符串。
- 将RSA加密后的二进制数据进行Base64编码,得到最终密文。
下面是完整的Python复现代码:
import hashlib import base64 import os from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 from Crypto import Random class WubaPasswordEncryptor: def __init__(self, rsa_public_key_pem): """ 初始化,传入RSA公钥(PEM格式字符串)。 公钥通常从App静态分析中获得。 """ self.rsa_public_key = RSA.import_key(rsa_public_key_pem) self.cipher = PKCS1_v1_5.new(self.rsa_public_key) self.fixed_salt = “wuba_sec_salt” # 固定盐,从代码中分析得到 def _md5(self, s): """计算字符串的MD5值(32位小写hex)。""" return hashlib.md5(s.encode(‘utf-8’)).hexdigest() def _sha256(self, s): """计算字符串的SHA-256值(64位小写hex)。""" return hashlib.sha256(s.encode(‘utf-8’)).hexdigest() def encrypt_password(self, plain_password, timestamp=None): """ 加密密码的主函数。 :param plain_password: 明文密码 :param timestamp: 时间戳(毫秒)。如果为None,则使用当前时间。 :return: Base64编码的最终加密密文 """ if timestamp is None: import time timestamp = int(time.time() * 1000) # 1. 生成8位随机动态盐(模拟App行为,实际App可能用特定算法生成) # 注意:为了能复现和验证,这里我们可以固定一个盐。但实际App每次登录盐都不同。 # dynamic_salt = os.urandom(8).hex() # 真正的随机 # 为了演示可复现,我们使用一个固定的盐,但用时间戳模拟其变化。 dynamic_salt = hashlib.md5(str(timestamp).encode()).hexdigest()[:8] # 模拟生成 # 2. 第一次MD5:密码+动态盐 step1 = self._md5(plain_password + dynamic_salt) # 3. 第二次SHA256:step1 + 固定盐 step2 = self._sha256(step1 + self.fixed_salt) # 4. 组合待加密数据 data_to_encrypt = f”{dynamic_salt}|{step2}” print(f”[Debug] 动态盐: {dynamic_salt}”) print(f”[Debug] 待加密数据: {data_to_encrypt}”) # 5. RSA加密 # PKCS1_v1_5加密要求数据长度小于密钥长度-11字节 encrypted_bytes = self.cipher.encrypt(data_to_encrypt.encode(‘utf-8’)) # 6. Base64编码 final_ciphertext = base64.b64encode(encrypted_bytes).decode(‘utf-8’) return final_ciphertext # ———————————————————————— # 使用示例 # ———————————————————————— if __name__ == “__main__”: # 从58同城App反编译代码中提取的RSA公钥(PEM格式,此处为示例假密钥) PUBLIC_KEY_PEM = “““—–BEGIN PUBLIC KEY—– MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJScv9lTkL8C7wJxJkZz ... (此处应替换为真实的公钥字符串) ... —–END PUBLIC KEY—–”“” encryptor = WubaPasswordEncryptor(PUBLIC_KEY_PEM) test_password = “myPassword123” test_timestamp = 1644567890123 # 使用抓包时的时间戳 encrypted_pwd = encryptor.encrypt_password(test_password, test_timestamp) print(f”明文密码: {test_password}”) print(f”模拟加密结果: {encrypted_pwd}”) # 验证:与抓包得到的密文对比(需要替换为实际抓包值) captured_ciphertext = “MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJScv9lTkL8C7wJxJkZz...” if encrypted_pwd == captured_ciphertext: print(“[成功] 加密结果与抓包数据一致!”) else: print(“[失败] 加密结果不一致,请检查算法步骤、密钥、盐值等参数。”) # 此时需要返回去检查动态Hook获取的中间值,进行逐步比对调试。复现过程中的关键调试步骤:
- 逐步输出比对:在Python代码的每一步(生成动态盐、第一次MD5、第二次SHA256、组合字符串)都打印出中间结果。
- 与动态Hook日志比对:运行Frida脚本,在App登录时,Hook住加密函数,并打印出相同的中间结果(例如,打印出
dynamic_salt、step1、step2的值)。将Python输出的中间结果与Frida打印的结果逐一比对。 - 确保编码一致:特别注意字符串的编码(UTF-8)和Hex的大小写(通常是小写)。
Base64编码的配置(如是否换行)也要与App端保持一致(Android的Base64.NO_WRAP对应Python的standard_b64encode)。 - 确认RSA细节:RSA的公钥格式(PEM)、加密填充方案(PKCS1_v1_5还是OAEP)、以及是否需要处理密钥长度限制,都必须与App端完全一致。
7. 常见问题、排查技巧与安全思考
在逆向和复现的过程中,你几乎一定会遇到各种问题。下面是一些常见坑点和排查思路的实录。
7.1 常见问题速查表
| 问题现象 | 可能原因 | 排查思路 |
|---|---|---|
| 抓包看不到HTTPS请求 | 手机未正确安装/信任CA证书;App使用了证书绑定(SSL Pinning) | 1. 检查证书是否安装并启用。 2. 尝试使用JustTrustMe等Xposed模块,或使用Frida脚本绕过SSL Pinning。 |
| Frida注入失败或App闪退 | App有反调试/反Frida检测 | 1. 使用隐藏Frida的脚本或修改过的frida-server。 2. 尝试在App启动后再注入( -f改成-n附加进程)。3. 检查是否因为加固导致,需先脱壳。 |
| Hook不到目标函数 | 函数名被混淆;方法签名(参数)猜测错误 | 1. 扩大Hook范围,Hook加密相关基类或接口。 2. 通过调用栈反推。先Hook网络请求库,看提交参数前调用了哪些方法。 3. 使用Frida的 Java.choose()枚举已加载类,搜索特征字符串。 |
| 加密结果长度不一致 | 算法步骤错误;编码方式错误;盐或密钥不对 | 1.逐环节比对:用Frida打印出每个中间变量,与Python代码输出对比。 2.检查编码:确认Hex、Base64的编码解码无误,无多余空格换行。 3.验证密钥:确认使用的公钥与App内嵌的完全一致(包括头尾和换行)。 |
| RSA加密出错(如长度错误) | 待加密数据超长;填充模式不对 | 1. PKCS1_v1_5要求数据长度 < 密钥长度 – 11。数据过长需分块或改用其他方式。 2. 确认App使用的是 PKCS1Padding还是OAEPWithSHA-256AndMGF1Padding。 |
| 每次运行结果都与抓包不同 | 动态盐或时间戳未正确模拟 | 1. 确保Python代码中生成动态盐的逻辑与App完全一致。可能需要Hook盐生成函数。 2. 确保使用的时间戳与抓包请求中的 timestamp字段完全相同。 |
7.2 独家避坑技巧
- 从结果反推,用Hook验证:不要一头扎进混淆的代码里。先通过抓包明确最终密文的形态,然后用Frida广泛Hook所有看似相关的加密函数(
Cipher.getInstance,MessageDigest.getInstance, 所有encrypt,encode方法),快速缩小范围。 - 重视“调用栈”:Frida打印的调用栈是黄金信息。它能告诉你加密函数是被谁调用的,从而理解整个加密流程在业务代码中的位置,有时甚至能直接定位到参数组装的地方。
- 固定随机数:如果加密过程中有随机数(如动态盐),为了调试方便,可以在Hook脚本中拦截随机数生成函数(如
java.util.Random),让其返回一个固定值。这样每次加密结果就固定了,便于比对。 - 分步替换,隔离测试:在Python复现时,不要一次性写完全部逻辑。可以先从App中Hook出第一步(如MD5)的输入和输出,在Python中只实现这一步并验证。成功后再加入第二步,如此递进,能快速定位问题环节。
- 关注Native层:复杂的加密逻辑可能不在Java层,而是写在so库(Native C/C++代码)中。如果Java层只看到一个
native方法的调用,那么就需要使用Frida去Hook Native函数,或者使用IDA Pro等工具分析so文件,难度会大增。
7.3 关于加密与安全的思考
通过这次对58同城App密码加密的深度分析,我们可以窥见一个大型互联网应用在安全设计上的考量:
- 前端加密的意义:很多人认为HTTPS已经足够安全,前端加密是多此一举。但实际上,前端加密(尤其是非对称加密)主要目的是防止密码在客户端侧被恶意软件窃取(键盘记录器等),以及避免密码明文出现在客户端的日志或内存中。它构成了纵深防御的一环。
- 动态盐与防重放:使用时间戳或随机数作为盐,确保了每次登录请求的密文都不同,有效防止了网络抓包后的“重放攻击”(即攻击者直接发送截获的密文进行登录)。
- 混合加密策略:采用哈希(MD5, SHA256)与非对称加密(RSA)结合的方式。哈希用于保护密码本身和生成固定长度的摘要,RSA用于保护传输过程。这种组合在安全性和性能上取得了平衡。
- 密钥管理:公钥硬编码在客户端虽然存在被提取的风险,但这在移动端是常见做法。真正的安全依赖于服务端的私钥保管、风控系统(识别异常登录)以及定期更新密钥对。
作为开发者,从防御角度,我们应该理解这种设计思路,在自己的项目中合理应用;从安全研究角度,这个过程锻炼了我们的逆向工程、密码学应用和问题排查能力。记住,技术的刀刃朝向哪里,取决于使用它的人。