某品会APP逆向分析:通信加密与签名算法全解析

发布时间:2026/7/2 22:15:36
某品会APP逆向分析:通信加密与签名算法全解析 1. 项目概述与逆向价值最近在逆向分析圈子里讨论某品会这类大型电商APP的声音一直没停过。这类APP集成了复杂的业务逻辑、风控策略和通信加密对逆向工程师来说既是挑战也是检验技术成色的绝佳“试金石”。我花了大概两周时间从零开始对某品会APP的某个版本进行了深度逆向分析整个过程就像在解一个层层嵌套的密码盒既有柳暗花明的兴奋也有陷入泥潭的困惑。今天这篇文章我就把这次逆向分析的完整思路、核心发现和踩过的坑毫无保留地分享出来。无论你是想学习APP逆向的入门新手还是想了解大型商业APP安全机制的同行相信都能从中找到有价值的信息。我们这次的目标很明确不搞破坏不涉及任何灰色操作纯粹从技术研究的角度拆解其核心的通信加密、签名算法以及部分关键业务逻辑的实现方式理解其安全设计的精妙之处。2. 逆向环境与工具链搭建工欲善其事必先利其器。一个稳定、高效的逆向分析环境是成功的一半。这次分析我主要采用了动态分析与静态分析相结合的策略工具链的选择也围绕这个核心展开。2.1 核心工具选型与配置我的主力分析机是一台运行macOS的MacBook Pro同时通过Docker和虚拟机搭建了辅助的Windows和Linux环境以应对不同工具的需求。以下是核心工具清单及其作用抓包与流量分析工具Charles/ Fiddler/ HTTP Toolkit: 用于拦截和查看HTTP/HTTPS流量。我最终选择了HTTP Toolkit因为它对非标准端口和WebSocket的支持更好且界面更现代化。关键在于配置手机代理和安装CA证书以解密HTTPS流量。某品会大量使用HTTPS这是观察其网络请求入口的第一步。Wireshark: 作为底层流量抓包的补充用于分析Charles可能漏掉的非HTTP协议流量或验证证书握手过程。动态调试与运行时分析工具Frida:本次分析的绝对核心。它是一个动态插桩工具可以在APP运行时注入JavaScript脚本实现函数Hook、参数打印、方法替换等。我主要用它来Hook Java层和Native层的加密函数实时获取输入输出。Objection: 基于Frida的命令行工具可以快速进行内存搜索、绕过SSL Pinning证书绑定等常见操作。对于快速验证和初步探索非常高效。Android Studio 模拟器/真机: 用于运行目标APP。我推荐使用Google Pixel系列的官方系统镜像创建模拟器兼容性最好。真机则需要开启USB调试并Root对于深度逆向几乎是必须的。静态反编译与分析工具Jadx-GUI: 反编译APK中DEX文件的首选工具可以将字节码转换为可读性极高的Java代码。它的图形化界面支持搜索、跳转、查看交叉引用是分析Java层逻辑的利器。IDA Pro/Ghidra: 用于分析APP中的原生库.so文件。某品会的核心加密逻辑很可能放在Native层。IDA Pro的交互式反汇编和调试功能强大而Ghidra作为免费开源工具其反编译质量也非常高我通常会两者结合使用。Apktool: 用于解包APK获取资源文件、清单文件以及未被混淆的XML布局文件有时能从中发现一些端倪比如引用的第三方SDK。注意所有工具请从官方网站或可信的GitHub仓库下载避免使用来历不明的破解版以防植入后门或分析环境被污染。2.2 关键环境配置步骤这里重点讲一下Frida环境的搭建因为后续很多操作都依赖它。安装Frida: 在电脑端使用pip安装pip install frida-tools。同时需要根据手机或模拟器的CPU架构从Frida的GitHub Releases页面下载对应的frida-server二进制文件。部署frida-server: 将下载的frida-server通过adb推送到设备上并赋予可执行权限。adb push frida-server /data/local/tmp/ adb shell cd /data/local/tmp chmod 755 frida-server ./frida-server 验证连接: 在电脑端新开一个终端运行frida-ps -U如果能看到设备上的进程列表说明连接成功。绕过SSL Pinning: 某品会这类APP肯定使用了SSL Pinning来防止中间人攻击。使用Objection可以快速绕过objection -g com.xxx.pinhui explore然后在Objection的REPL中运行android sslpinning disable。但这只是通用方法对于自定义的证书验证逻辑可能无效届时需要手动分析并Hook相关的验证函数。环境搭好之后建议先不要急着深入而是用抓包工具完整地浏览一遍APP记录下主要的API域名、请求参数结构和常见的响应格式建立一个初步的“地图”。3. 初步侦察与协议入口分析逆向分析就像侦探破案第一步永远是现场勘查。对于APP来说就是观察它的网络行为。3.1 网络流量抓取与特征观察配置好HTTP Toolkit代理后打开某品会APP进行登录、浏览商品、加入购物车、查看个人中心等关键操作。很快我在抓包工具中看到了大量的请求。经过筛选我重点关注了以下几个特征明显的请求登录请求 (/api/member/login/v2) 显然是认证入口请求体和响应体都被加密了看起来像是Base64编码的密文。商品列表请求 (/api/goods/list) 即使是不需要登录的公开数据请求的URL和参数中也包含了一些类似sign、timestamp、nonce的字段响应体同样是加密的。心跳或初始化请求 (/api/app/init) APP启动时调用返回了包括公钥、算法标识在内的配置信息。这是一个非常重要的突破口我整理了一下初步观察到的规律全链路加密 不仅敏感信息登录密码几乎所有业务API的请求体body和响应体都进行了加密传输明文可见的只有URL和少数几个Query参数。签名机制 大部分请求的URL中都会携带sign、timestamp等参数这是典型的防重放和参数防篡改签名机制。密钥协商 从/api/app/init的响应可以推测客户端与服务器可能采用了非对称加密如RSA协商对称加密密钥如AES后续通信再用对称加密。3.2 静态入口点定位面对加密的流量直接硬猜算法是不可能的。下一步就是找到执行加密和解密的代码位置。这里我采用了“由外到内”的搜索策略。首先用Jadx打开APK进行全局搜索。搜索关键词的选择很重要搜索类名 搜索“AES”、“RSA”、“DES”、“Cipher”、“Encrypt”、“Decrypt”、“Crypto”、“Security”等关键词。很快我发现了一个名为com.xxx.pinhui.security.EncryptUtils的类看起来很有希望。搜索初始化API的响应字段 从抓包看到/api/app/init返回了publicKey和algorithm。我直接在Jadx中搜索字符串publicKey和algorithm找到了解析这个响应的代码位置通常是在某个Callback或Model类里。顺藤摸瓜就能找到使用这个publicKey的代码。搜索网络库 某品会很可能使用了OkHttp或Retrofit作为网络框架。搜索Interceptor拦截器这个关键词因为加密和签名逻辑通常被封装在自定义的Interceptor中在请求发出前和响应收到后统一处理。我找到了一个SecurityInterceptor类这几乎就是加密通信的“总闸”。定位到SecurityInterceptor后我看到了类似下面的伪代码结构public class SecurityInterceptor implements Interceptor { Override public Response intercept(Chain chain) throws IOException { Request originalRequest chain.request(); // 1. 对请求体进行加密并添加签名参数 Request encryptedRequest encryptRequest(originalRequest); // 2. 发送加密后的请求 Response response chain.proceed(encryptedRequest); // 3. 对响应体进行解密 Response decryptedResponse decryptResponse(response); return decryptedResponse; } }至此我们找到了加密解密的入口函数encryptRequest和decryptResponse。接下来的任务就是深入这两个函数还原完整的算法流程。4. 核心加密与签名算法逆向这是整个逆向过程中最硬核、也最有趣的部分。我们需要像剥洋葱一样一层层揭开加密和签名的面纱。4.1 请求体加密流程剖析进入encryptRequest方法结合Jadx的反编译代码和Frida的动态Hook我逐步理清了请求体的加密过程生成随机对称密钥 每次请求都会随机生成一个16字节128位的AES密钥我们称之为sessionKey。加密业务数据 将原始的JSON请求体例如{phone:13800138000, password:...}用上一步生成的sessionKey以AES-128-CBC模式进行加密生成密文encryptedBody。这里还涉及PKCS7填充和随机生成IV初始化向量。加密会话密钥 上一步的sessionKey本身需要用服务器的公钥进行加密以确保只有持有私钥的服务器才能解密。从/api/app/init获取的publicKey被用于RSA加密。这里注意为了适应密钥长度通常还会对sessionKey进行分段加密或使用RSA/ECB/PKCS1Padding模式。组装最终请求体 最终上传的请求体是一个新的JSON结构大致如下{ encryptedKey: Base64编码的(RSA加密后的sessionKey), encryptedData: Base64编码的(AES加密后的原始请求体), iv: Base64编码的(AES IV), algorithm: AES/CBC/PKCS7Padding_RSA/ECB/PKCS1Padding }这个新的JSON会被转换成字符串作为HTTP请求的Body通常是application/json格式。为了验证这个过程我写了一个Frida脚本Hook了javax.crypto.Cipher类的getInstance、init、doFinal方法。当APP发起登录请求时脚本打印出了如下关键信息Cipher.getInstance: AES/CBC/PKCS7Padding Cipher.init: ENCRYPT_MODE, keyxxxx..., ivxxxx... Cipher.doFinal (输入): {phone:138...,password:...} Cipher.doFinal (输出): [密文字节数组] Cipher.getInstance: RSA/ECB/PKCS1Padding Cipher.init: ENCRYPT_MODE, keyxxxx... (公钥) Cipher.doFinal (输入): [sessionKey字节数组] Cipher.doFinal (输出): [加密后的sessionKey]动态Hook的结果与静态分析的逻辑完全吻合证实了我们的分析。4.2 URL签名算法解析请求体加密解决了数据保密性问题但URL本身和Query参数还是明文的。为了防止参数被篡改或请求被重放签名算法登场了。在SecurityInterceptor中encryptRequest方法里除了加密Body还会调用一个generateSign的方法来生成sign参数。分析这个方法参数排序与拼接 将所有待签名的参数包括timestamp、nonce、appVersion、deviceId以及业务参数如goodsId按照参数名ASCII码从小到大排序字典序然后使用keyvalue的格式用连接起来形成一个字符串paramStr。例如appVersion10.2.1deviceIdabc123noncexyz×tamp1678888888添加密钥 在paramStr的最后拼接上一个固定的secretKey这个Key通常硬编码在APP中或由服务器在初始化时下发。形成signStr paramStr secretxxxxxx。计算哈希 对signStr计算MD5哈希值有时也可能是SHA256并将结果转换为小写十六进制字符串这就是最终的sign值。添加到URL 将sign、timestamp、nonce等参数作为Query参数附加到请求URL中。签名算法的核心在于secretKey和排序规则。只要规则一致任何一方都能计算出相同的sign。服务器收到请求后会以同样的算法重新计算一遍sign并与客户端传来的sign对比不一致则拒绝请求。实操心得寻找secretKey时不要只搜索明文字符串。它可能被分割成几段、经过简单的位运算或Base64编码后存储。可以尝试Hook签名函数直接打印出参与计算的完整signStr这样secretKey就暴露无遗了。4.3 响应体解密流程响应体的解密是加密的逆过程。在decryptResponse方法中解析响应体 服务器返回的响应体其结构通常与加密请求体类似包含encryptedData和iv等字段。获取会话密钥 由于一个会话中可能使用同一个sessionKey或者服务器会用客户端的公钥加密一个新的sessionKey返回。在这个案例中我发现它复用了请求阶段的sessionKey。这意味着我们需要在客户端内存中缓存这个sessionKey用于解密后续的响应。解密业务数据 使用缓存的sessionKey和响应中的iv以AES-128-CBC模式解密encryptedData得到原始的JSON响应字符串。这里有一个关键点sessionKey的生命周期管理。是每次请求都更换还是一个会话期内不变通过Hook和多次请求观察我确认在某品会的这个版本中sessionKey在用户登录成功后的一段较长时间内甚至整个APP生命周期是复用的这简化了逆向后的模拟请求过程。5. 算法还原与本地模拟实现分析清楚算法后下一步就是用代码这里以Python为例在电脑上复现整个流程从而能够模拟APP发送合法的请求。这是验证逆向成果的关键一步。5.1 密钥与参数的提取首先我们需要从APP中提取出必要的“原料”RSA公钥 从/api/app/init的响应中直接获取。或者从反编译的代码中找到硬编码的公钥字符串。通常是一个Base64编码的PKCS#8格式公钥。签名SecretKey 通过HookgenerateSign函数从拼接的字符串中提取。或者在Jadx中搜索相关字符串或常量可能被混淆但最终会参与MD5计算。其他固定参数 如appVersion、deviceId生成规则、nonce生成算法等。deviceId通常是根据手机设备信息生成的一个唯一标识在模拟时可以随机生成一个符合规则的固定值。5.2 Python模拟代码编写假设我们已提取到公钥PUBLIC_KEY_BASE64和签名密钥SECRET_KEY。import json import base64 import hashlib import time import random import string from Crypto.Cipher import AES, PKCS1_v1_5 from Crypto.PublicKey import RSA from Crypto.Util.Padding import pad, unpad from urllib.parse import urlencode # 1. 准备RSA公钥 PUBLIC_KEY RSA.import_key(base64.b64decode(PUBLIC_KEY_BASE64)) # 2. 生成随机AES会话密钥和IV def generate_aes_key_iv(): session_key .join(random.choices(string.ascii_letters string.digits, k16)).encode() iv .join(random.choices(string.ascii_letters string.digits, k16)).encode() return session_key, iv # 3. 加密请求体 def encrypt_request_data(original_data: dict): 原始业务数据 - 加密后的请求体JSON字符串 # 3.1 生成AES密钥和IV session_key, iv generate_aes_key_iv() # 3.2 AES加密原始数据 cipher_aes AES.new(session_key, AES.MODE_CBC, iv) original_json json.dumps(original_data, separators(,, :), ensure_asciiFalse) encrypted_body cipher_aes.encrypt(pad(original_json.encode(utf-8), AES.block_size)) # 3.3 RSA加密AES密钥 cipher_rsa PKCS1_v1_5.new(PUBLIC_KEY) encrypted_key cipher_rsa.encrypt(session_key) # 3.4 组装最终请求体 encrypted_payload { encryptedKey: base64.b64encode(encrypted_key).decode(utf-8), encryptedData: base64.b64encode(encrypted_body).decode(utf-8), iv: base64.b64encode(iv).decode(utf-8), algorithm: AES/CBC/PKCS7Padding_RSA/ECB/PKCS1Padding } return json.dumps(encrypted_payload, separators(,, :)), session_key, iv # 4. 生成URL签名 def generate_sign(params: dict, secret_key: str): 生成请求签名 # 4.1 参数排序并拼接 sorted_params sorted(params.items(), keylambda x: x[0]) param_str .join([f{k}{v} for k, v in sorted_params]) # 4.2 拼接密钥 sign_str param_str fsecret{secret_key} # 4.3 计算MD5 md5 hashlib.md5() md5.update(sign_str.encode(utf-8)) return md5.hexdigest().lower() # 5. 解密响应体 def decrypt_response_data(encrypted_response_json: str, session_key: bytes, iv: bytes): 服务器响应 - 解密后的业务数据字典 resp_data json.loads(encrypted_response_json) encrypted_data base64.b64decode(resp_data[encryptedData]) iv base64.b64decode(resp_data[iv]) # 通常使用响应中的新IV cipher_aes AES.new(session_key, AES.MODE_CBC, iv) decrypted_bytes unpad(cipher_aes.decrypt(encrypted_data), AES.block_size) return json.loads(decrypted_bytes.decode(utf-8)) # 6. 模拟一个完整的请求 def simulate_login(phone, password): # 6.1 准备业务数据 biz_data {phone: phone, password: password, loginType: 1} # 6.2 加密请求体并保留session_key用于解密响应 encrypted_body_str, session_key, iv encrypt_request_data(biz_data) # 6.3 准备URL参数并生成签名 timestamp int(time.time() * 1000) nonce .join(random.choices(string.ascii_letters string.digits, k8)) url_params { timestamp: timestamp, nonce: nonce, appVersion: 10.2.1, deviceId: simulated_device_123, } sign generate_sign(url_params, SECRET_KEY) url_params[sign] sign # 6.4 构建最终请求 final_url fhttps://api.xxx.com/api/member/login/v2?{urlencode(url_params)} headers { Content-Type: application/json; charsetUTF-8, User-Agent: PinHui/10.2.1 (Android; ...) } # 这里使用requests库发送POST请求body为encrypted_body_str # response requests.post(final_url, dataencrypted_body_str, headersheaders) # decrypted_resp decrypt_response_data(response.text, session_key, iv) # print(decrypted_resp) print(f请求URL: {final_url}) print(f加密后Body: {encrypted_body_str[:100]}...) return session_key # 返回session_key供后续请求使用 # 使用示例 if __name__ __main__: # 这些值需要从逆向分析中获取并替换 PUBLIC_KEY_BASE64 你的RSA公钥Base64 SECRET_KEY 你的签名密钥 session_key simulate_login(13800138000, your_encrypted_password)这段代码完整还原了从数据加密、签名生成到请求发送的客户端流程。运行它并与抓包工具中捕获的真实请求进行对比检查URL参数结构、签名值以及请求体格式是否一致。如果一致恭喜你核心通信协议已被成功攻破。6. 深入Native层与混淆对抗如果一切顺利Java层的分析就完成了。但很多APP会把最核心的算法如自定义的哈希、白盒AES放在Native层C/C代码编译的.so库中并加以混淆以增加逆向难度。6.1 定位Native加密函数在Java层的EncryptUtils或类似类中你可能会看到native声明的方法public static native byte[] nativeEncrypt(byte[] data, int type); public static native String nativeGenerateSign(String paramStr);这表明实际的加密和签名计算是在Native库中完成的。你需要找到加载这个库的代码通常是System.loadLibrary(security)然后在解压的APK的lib目录下找到对应的libsecurity.so文件可能有armeabi-v7a, arm64-v8a等多个版本。6.2 使用IDA Pro/Ghidra分析.so文件将libsecurity.so拖入IDA Pro。分析Native层比Java层更底层挑战更大寻找JNI函数 在导出函数列表中查找以Java_开头的函数这些是JNI接口函数。例如Java_com_xxx_pinhui_security_EncryptUtils_nativeEncrypt。定位到它们就找到了分析的起点。理解函数逻辑 Native层代码可能被严重混淆控制流平坦化、指令替换、字符串加密。你需要耐心地跟踪参数传递、识别关键的加密函数调用如寻找OPENSSL的函数符号或识别AES的S盒、RSA的大数运算等特征。动态调试 使用IDA Pro或Ghidra的调试功能附加到运行中的APP进程在JNI函数入口处下断点直接观察传入的参数和返回的结果与Java层的输入输出进行验证。Frida也可以Hook Native函数但需要知道函数地址或符号。6.3 常见的混淆与对抗技巧字符串加密 代码中的关键字符串如算法名、密钥是加密存储的运行时解密。可以通过Hook内存分配函数如malloc或字符串操作函数在解密后获取明文。控制流平坦化 将正常的if-else、switch逻辑打乱用一个大switch-case和状态变量来调度极大地增加静态分析的难度。动态调试可以帮你理清实际执行路径。符号表剥离 发布版的.so文件通常移除了函数和变量名等调试符号所有函数都显示为sub_XXXX。需要通过交叉引用、特征码或动态调试来猜测其功能。反调试检测 APP会检测是否被调试如检查ptrace、TracerPid等一旦发现就退出或执行错误逻辑。需要使用反反调试技巧如修改内核参数、Hook检测函数等。面对深度混淆我的策略是动态分析为主静态分析为辅。优先使用Frida Hook住JNI函数的入口和出口打印输入输出。如果算法逻辑不复杂甚至可以直接在Frida脚本中调用这些Native函数让APP自己为我们计算我们只关心结果。如果必须还原算法再结合动态调试去理解其内部逻辑。7. 常见问题与排查实录在逆向过程中我遇到了无数个坑。这里记录下几个最具代表性的问题及其解决方法希望能帮你节省大量时间。7.1 抓包工具看不到HTTPS流量现象 配置好代理后APP无法联网或只能看到乱码/证书错误。原因 APP启用了SSL Pinning证书绑定只信任自己的证书或特定CA不信任用户安装的抓包工具证书。解决使用Objection一键绕过 如前所述android sslpinning disable。这对使用标准库如OkHttp的Pinning有效。手动Hook 如果Objection无效说明APP有自定义的证书验证逻辑。需要反编译找到相关的TrustManager或X509Certificate验证代码用Frida Hook并使其始终返回true或有效的证书链。刷入系统级证书 在已Root的设备上将抓包工具的CA证书移动到系统证书目录/system/etc/security/cacerts/并修改权限。这样证书会被系统完全信任。7.2 Frida脚本注入失败或进程崩溃现象 执行frida -U -f com.xxx.pinhui时进程崩溃或脚本注入后无输出。原因反Frida检测 APP检测到了Frida的存在如检测frida-server进程、端口、特征文件等。架构或版本不匹配frida-server的版本与电脑端frida-tools版本不兼容或frida-server的CPU架构与设备不符。脚本错误 JavaScript脚本语法错误或Hook了不存在的类/方法。解决对抗检测 使用修改版的frida-server如frida-server的某些变种或使用Frida的--no-pause参数或在APP启动后再附加frida -U com.xxx.pinhui。也可以写脚本先Hook掉APP的反调试检测函数。检查版本 确保frida --version与设备上的frida-server版本一致。使用adb shell getprop ro.product.cpu.abi查看设备架构。调试脚本 先写一个简单的脚本如Java.perform(function() { console.log(Script loaded!); })测试注入是否成功。再逐步增加Hook逻辑。7.3 算法还原后签名依然不匹配现象 自己模拟生成的sign与APP生成的sign不同服务器返回签名错误。原因参数遗漏或多余 参与签名的参数列表不完整或包含了不该参与签名的参数如sign本身。编码问题 参数值可能进行了URL编码或Unicode处理拼接前需要统一。排序规则不一致 不仅是参数名排序如果参数值本身是复杂对象如JSON字符串其内部的排序规则也可能有要求。密钥错误 使用的secretKey不正确或者密钥在拼接前经过了额外处理如MD5哈希一次后再用。哈希算法不同 可能不是MD5而是SHA1、SHA256甚至可能是自定义的哈希函数。排查精准Hook 用Frida Hook到签名函数的最内部打印出即将被计算哈希的最终字符串。将这个字符串与你本地拼接的字符串进行逐字符对比包括空格、换行、特殊字符。二分法验证 先固定所有参数timestamp,nonce等只让一个参数变化对比APP和你本地计算的sign看是否一致。逐步增加变量定位到是哪个参数或环节出了问题。算法验证 将Hook得到的最终字符串分别用MD5、SHA1、SHA256等常见算法计算看结果是否匹配。7.4 Native层函数地址找不到现象 知道Native函数名但在Frida中用Module.findExportByName(null, 函数名)返回null。原因符号被剥离 发布版的.so文件没有导出函数符号。函数是静态的 函数没有导出只在库内部使用。加载时机 库可能还没有被加载。解决通过偏移地址查找 在IDA Pro中查看目标函数的相对虚拟地址RVA。在Frida中先获取模块基地址然后加上RVA得到绝对地址。var base Module.findBaseAddress(libsecurity.so); var func_addr base.add(0x1234); // 0x1234是函数在IDA中的偏移 Interceptor.attach(func_addr, { onEnter: function(args) { ... }, onLeave: function(retval) { ... } });通过特征码搜索 在内存中搜索函数开头的一段独特的机器码操作码来定位函数。确保库已加载 在Java.perform中操作或监听dlopen事件确保库加载后再Hook。整个逆向分析某品会APP的过程是一次对耐心、细心和技术深度的综合考验。从最外层的流量观察到Java层的逻辑梳理再到可能存在的Native层攻防每一步都需要严谨的推理和反复的验证。最终的目标不是破解而是理解其设计思路。当你能够用自己写的代码完美模拟出APP的合法请求时那种成就感是无与伦比的。这份经验不仅适用于某品会其分析思路和方法论对于绝大多数采用类似安全方案的Android APP都具有很高的参考价值。记住逆向工程的核心是学习和理解请务必在法律和道德允许的范围内进行技术研究。