从Inspeckage到Python脚本:一次完整的安卓APP通信协议逆向实战

从Inspeckage到Python脚本:一次完整的安卓APP通信协议逆向实战 1. 为什么我们需要逆向APP通信协议大家好我是老张一个在移动安全和数据抓包领域折腾了十多年的“老油条”。今天我想和你聊聊一个非常实用的话题如何从零开始完整地逆向一个安卓APP的通信协议并最终用Python脚本把它复现出来。你可能遇到过这样的情况公司业务需要对接某个第三方APP的数据但对方不提供API文档或者你对某个APP的运行机制特别好奇想看看它背后是怎么和服务端“对话”的又或者你是一名安全研究员需要评估一个APP的数据传输是否安全。这些场景都指向同一个核心需求——搞清楚APP的网络请求到底发了什么以及服务器回了什么。这个过程我们称之为“协议逆向”。听起来很高大上好像需要破解什么高深莫测的加密算法。其实不然很多主流APP的通信协议其核心逻辑往往就是几种常见的加密和签名方式的组合。关键在于你得有一套系统的方法论和顺手的工具把散落在各处的“拼图”找出来再拼成一幅完整的画面。我这次选择的实战对象是“步道乐跑”APP一个校园跑步打卡应用。选择它一是因为它具有一定的代表性用到了常见的AES加密和MD5签名二来它的协议复杂度适中非常适合作为教学案例。整个流程我会用到两个核心工具Inspeckage和HttpCanary。前者用来动态分析APP运行时的加密、哈希等操作后者用来捕获原始的网络请求和响应。最后我们会把分析结果用Python代码完整地复现出来形成一个可以自动化运行的脚本。这不仅仅是一次技术演练更是一次完整的“从数据到代码”的思维训练。跟着我的步骤走一遍你不仅能学会工具的使用更能掌握一套应对未知APP协议的分析框架。准备好了吗我们开始吧。2. 搭建你的移动端分析实验室工欲善其事必先利其器。逆向分析的第一步是搭建一个稳定、可控的分析环境。直接在真机上折腾风险高且不方便。我的方案是使用虚拟手机系统在真机里创建一个独立的沙箱环境所有分析操作都在这个沙箱里进行安全又方便。2.1 核心工具全家桶你需要准备以下软件我已经把它们打包好了你可以从我的蓝奏云仓库下载链接和密码在文末备注。别担心都是常见的开源或免费工具。VMOS Pro这是一个安卓虚拟机APP。你可以把它理解为你手机里的“第二部手机”。我们所有的分析操作都将在这个虚拟手机里进行这样即使搞崩了也不会影响你真实的手机系统。Inspeckage这是一个基于Xposed框架的动态分析工具。它的强大之处在于能够钩住HookAPP对加解密、哈希、SharedPreferences等关键API的调用并把调用参数和结果清晰地展示在网页上。这是我们窥探APP内部逻辑的“透视镜”。JustTrustMe同样是一个Xposed模块。它的作用是禁用APP内部的SSL证书绑定SSL Pinning校验。现在很多APP为了安全会校验服务器的证书导致像HttpCanary这样的抓包工具无法解密HTTPS流量。JustTrustMe能帮我们绕过这个限制。HttpCanary一款功能强大的手机抓包工具。它就像网络请求的“录音机”能详细记录下APP发出的每一个请求和服务器返回的每一个响应包括完整的URL、请求头、请求体等。目标APP本例中的“步道乐跑”APP。2.2 详细配置步骤图解很多教程在这一步讲得不清不楚导致新手卡在环境配置上。我带你一步步走把每个坑都提前填平。第一步安装VMOS Pro并创建虚拟机在真机上安装VMOS Pro打开它。我为了更好的性能和兼容性开通了会员非必须但免费版可能有广告。在虚拟机选择页面我推荐使用那个安卓7.1精简版的镜像体积小纯净对Xposed框架支持好。等待虚拟机加载完成你会进入一个全新的手机桌面。点击桌面上类似“文件传输”或“我的文件”的图标。第二步导入并安装分析工具在VMOS的文件传输功能里选择“我要导入”。找到你事先下载好的Inspeckage和JustTrustMe的安装包.apk文件选中并确认。VMOS会自动将它们安装到虚拟系统中。安装“步道乐跑”APP。这里有个小技巧先在真机上安装好步道乐跑然后同样通过VMOS的“我要导入” - “应用”功能选择“步道乐跑”将其克隆到虚拟机中。这比在虚拟机里重新下载要快得多。第三步激活Xposed模块回到VMOS的桌面找到并打开“Xposed”这个应用通常图标是一个机器人。点击左上角菜单进入“模块”管理页面。你会看到刚刚安装的Inspeckage和JustTrustMe。把这两个模块右边的开关都勾选上。最关键的一步勾选后Xposed会提示你需要重启虚拟机以激活模块。一定要重启不重启模块是不会生效的。重启虚拟机后再次进入Xposed的模块页面确认两个模块都已激活。通常JustTrustMe默认就是激活状态而Inspeckage需要进一步配置。第四步配置并启动Inspeckage服务在虚拟机里打开Inspeckage应用。你会看到一个非常简洁的界面。如果一切正常你应该能看到两行关键的绿色日志Module enable和Server start。这表示Inspeckage模块已加载并且在本地的8008端口启动了一个Web服务。记住这个状态Inspeckage的配置到这里就完成了先把它放在后台运行。第五步配置真机上的HttpCanary在你的真机上安装并打开HttpCanary。点击左上角菜单进入设置 - SSL证书设置。点击“安装证书”按照提示为HttpCanary安装一个抓包所需的CA证书。这一步是为了让HttpCanary能够解密HTTPS流量是抓包成功的前提。返回设置页找到“目标应用”选项。点击右上角的“”号在弹出的应用列表里选择“VMOS Pro”。这一点非常重要我们是要抓虚拟机里APP的包所以目标应用是虚拟机容器本身而不是步道乐跑。至此我们的移动端分析实验室就搭建完毕了。简单总结一下虚拟机VMOS里运行着目标APP和监控工具Inspeckage真机上运行着抓包工具HttpCanary并且配置好了去监听虚拟机的网络流量。接下来就是让它们联动起来捕获数据。3. 双线作战捕获网络请求与运行时行为环境准备好了就像猎人布好了陷阱现在要引诱猎物出场了。我们的策略是“双线作战”让HttpCanary记录下所有进出的网络数据包同时让Inspeckage记录下APP在运行时调用了哪些加密函数、传递了什么参数。两相对照真相自然浮出水面。3.1 启动监测与抓包这个流程需要一点耐心和顺序我画个简单的脑图帮你理解[真机] 浏览器打开 127.0.0.1:8008 -- [虚拟机] Inspeckage Web服务 ^ | | | | v | [虚拟机] 步道乐跑APP | | | v -- [真机] HttpCanary 抓包 ------ 网络请求启动Inspeckage Web界面在真机上打开任意浏览器输入地址http://127.0.0.1:8008。如果看到Inspeckage的网页控制台恭喜你服务连接成功。此时页面左上角的开关是“OFF”并且“App is running”显示为false。先保持这个页面不要关闭。开始全局抓包打开真机上的HttpCanary点击右下角的“小飞机”图标让它变成绿色。这表示HttpCanary已经开始在系统层面监听网络流量了。联动启动目标APP不要清理后台直接从屏幕边缘调出VMOS的小窗或返回VMOS应用。在VMOS的Inspeckage应用界面你会看到一个应用列表。找到并点击“步道乐跑”。Inspeckage会帮你启动这个APP并自动将其选定为监控目标。验证监控状态此时步道乐跑APP开始运行并发送网络请求。迅速切换回真机的浏览器刷新127.0.0.1:8008这个页面。如果看到“App is running”变成了true说明Inspeckage已经成功附着Attach到步道乐跑进程上了。这时把网页左上角的开关从“OFF”拨到“ON”。成功状态检查HttpCanary的抓包列表中应该开始出现大量来自api2.lptiyu.com等域名的HTTPS请求。Inspeckage的网页控制台在“Overview”、“Crypto”、“Hash”等标签页下应该开始滚动刷出新的日志信息。如果两者都有数据流动那么恭喜你最关键的抓取环节已经成功现在让APP完成一次完整的登录操作或者多进行几个页面跳转以生成足够我们分析的网络流量。然后可以停止HttpCanary的抓包再次点击小飞机并关闭Inspeckage的监控开关网页上拨回OFF准备进入下一阶段——数据分析。3.2 如何从海量数据中找到关键线索抓包完成后你可能会面对HttpCanary里上百条请求记录感到无从下手。别慌我有几个屡试不爽的筛选技巧按域名过滤首先在HttpCanary中过滤出目标API域名如api2.lptiyu.com的请求排除掉图片、统计等干扰请求。寻找“起点”APP启动或登录时的第一个认证请求往往是协议的核心。把抓包列表拉到底部从最早的请求开始看。关注关键词在请求URL或请求体中寻找像login、auth、token、user这样的关键词。这些通常是认证、获取用户信息等关键接口。观察请求/响应结构重点查看那些请求体不是明文JSON或者响应体是一串Base64编码的请求。这往往是加密的迹象。在我的这次抓包中很快就锁定了一个关键的请求https://api2.lptiyu.com/v3/api.php/Login/RefreshToken。看名字就知道这是刷新令牌的接口。点开它的“请求”部分在预览视图里我发现了几个老朋友token、timestamp、nonce以及一个非常显眼的sign字段。凭经验看timestamp时间戳和nonce随机数通常是防重放攻击的算法上不需要逆向。而sign签名字段其值是一串32位的十六进制字符串这强烈暗示它可能是一个MD5哈希值。签名的作用是确保请求数据在传输过程中未被篡改服务器端会用同样的规则计算一遍进行比对。所以破解sign的生成规则是我们逆向协议的第一道关卡。再看“响应”部分data字段是一长串Base64编码的字符串。尝试用Base64解码得到的是乱码。这说明什么说明在Base64编码之前原始数据已经被加密过了。这成了我们的第二道关卡破解响应数据的加密算法。线索已经找到接下来就该请出我们的“内应”——Inspeckage来看看APP在发出这个请求和解析这个响应时到底偷偷干了些什么。4. 抽丝剥茧在Inspeckage日志中定位加密逻辑Inspeckage的网页控制台是我们的“上帝视角”。它记录了APP运行时所有被Hook到的加密、哈希、编码等操作。但日志信息可能非常多我们需要像侦探一样从中找出与目标请求相关的蛛丝马迹。4.1 破解响应加密寻找AES的密钥和IV我们的目标是那个被加密后又Base64的data字段。在Inspeckage页面我们主要关注两个标签页Crypto加解密操作和Hash哈希操作。锁定搜索范围由于RefreshToken请求是较早发生的对应的加密调用也应该在日志的前面部分。我们把Crypto页面的日志滚动到最底部最早记录开始查找。使用浏览器查找功能这是提高效率的关键按下CtrlF(Windows) 或CmdF(Mac)在查找框里输入data字段Base64串的前6-8个字符。为什么不全输因为Inspeckage页面可能对长字符串进行了截断显示用开头片段更容易匹配成功。分析找到的记录很快我找到了一条关键记录。日志清晰地显示了以下信息SecretKeySpec(Wet2C8d34f62ndi3, AES) Cipher[AES/CBC/PKCS5Padding] IV: K6iv85jBD8jgf32D这简直是“自报家门”它告诉我们加密算法AES模式是CBC填充方式是PKCS5Padding。密钥KeyWet2C8d34f62ndi3初始化向量IVK6iv85jBD8jgf32D在这条记录附近通常还能看到加密前的原始明文很可能就是一个JSON字符串包含了uid、access_token等信息。这样一来响应数据的加密方式就被我们完全掌握了。4.2 破解请求签名验证MD5猜想接下来处理请求中的sign。我们切换到Hash标签页。同样使用查找功能在Hash日志里搜索sign值的前几位字符。展开完整信息Inspeckage的Hash记录默认可能折叠显示点击记录旁边的符号可以展开详情。解读结果找到的记录显示Algorithm(MD5)后面跟着一长串拼接起来的字符串最后是冒号和计算出的MD5值。这证实了我们的猜想sign就是一个MD5哈希值。 关键在于冒号前面的那一长串字符串它就是生成签名的“原料”。仔细看这串原料里包含了请求中的所有字段access_token、jpush_id、timestamp、nonce等等并且是按照字母顺序排列后以字段名字段值的形式直接拼接起来的最后还追加了一个固定的字符串rDJiNB9j7vD2我们称之为“盐”或salt。至此通过HttpCanary和Inspeckage的配合我们就像拿到了协议的“设计图纸”响应解密使用AES-CBC算法密钥和IV已知先用Base64解码再用AES解密即可得到明文JSON。请求签名将所有必要参数按字母顺序拼接成一个字符串末尾加盐然后计算这个字符串的MD5值作为sign参数。逆向分析的核心部分已经完成。剩下的就是把这些规则翻译成Python代码。5. 从分析到实现编写Python自动化脚本分析得再透彻不落地成代码都是空谈。用Python复现协议不仅能验证我们的分析是否正确更能将其转化为一个可重复使用的工具。我会把代码拆解成几个核心模块并解释其中的关键点。5.1 构建加解密与签名工具类首先我们创建两个基础工具类分别处理AES加解密和MD5签名。AES加解密模块 (crypto.py)这里我们使用pycryptodome库它是Python下强大的加密工具库。安装命令pip install pycryptodome。from Crypto.Cipher import AES import base64 import json import re class Crypto: def __init__(self): # 从Inspeckage日志中获取的密钥和IV self.key Wet2C8d34f62ndi3.encode(utf-8) # 密钥需要转为bytes self.iv bK6iv85jBD8jgf32D # 初始化向量 self.mode AES.MODE_CBC # 加密模式 self.bs AES.block_size # AES块大小16字节 # PKCS5/PKCS7填充函数确保明文长度是块大小的整数倍 self._pad lambda s: s (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs) # 对应的去填充函数 self._unpad lambda s: s[:-ord(s[len(s)-1:])] def encrypt(self, text): 加密明文文本返回Base64编码的字符串 # 1. 对文本进行PKCS5填充 raw self._pad(text) # 2. 创建AES加密器 cipher AES.new(self.key, self.mode, self.iv) # 3. 加密 encrypted cipher.encrypt(raw.encode(utf-8)) # 4. 将加密后的bytes进行Base64编码并转为字符串 return base64.b64encode(encrypted).decode(utf-8) def decrypt(self, enc): 解密Base64编码的密文返回解密后的JSON对象字典 # 1. Base64解码 enc base64.b64decode(enc) # 2. 创建AES解密器 cipher AES.new(self.key, self.mode, self.iv) # 3. 解密 decrypted cipher.decrypt(enc) # 4. 去除PKCS5填充 plain_text self._unpad(decrypted.decode(utf-8, errorsignore)) # 5. 处理可能的转义字符和特殊控制字符根据实际响应调整 # 有时服务器返回的数据里会包含转义的斜杠或不可见字符需要清洗 plain_text plain_text.replace(r\/, /) # 使用更健壮的方式加载JSON去除可能存在的非法字符 try: return json.loads(plain_text) except json.JSONDecodeError: # 如果直接解析失败尝试清理一些常见控制字符 cleaned_text re.sub(r[\x00-\x1f\x7f], , plain_text) return json.loads(cleaned_text)MD5签名模块 (signature.py)这个就简单多了Python标准库hashlib就够用。import hashlib def generate_sign(params_dict, saltrDJiNB9j7vD2): 根据参数字典生成签名 :param params_dict: 包含所有请求参数的字典 :param salt: 从日志中发现的固定盐值 :return: 32位小写MD5签名字符串 # 1. 将字典按键进行字母顺序排序 sorted_items sorted(params_dict.items()) # 2. 拼接成 key1value1key2value2... 格式的字符串 sign_string .join([f{k}{v} for k, v in sorted_items]) # 3. 在末尾加上盐 sign_string salt # 4. 计算MD5 m hashlib.md5() m.update(sign_string.encode(utf-8)) return m.hexdigest()5.2 组装请求与处理响应有了工具我们就可以模拟APP构造一个完整的请求了。以刷新Token的接口为例。import time import random from crypto import Crypto from signature import generate_sign class LePaoClient: def __init__(self, uid, token, refresh_token, student_num, device_info): 初始化客户端 :param device_info: 包含设备标识信息的字典如jpush_id, mobileDeviceId等 self.uid uid self.token token self.refresh_token refresh_token self.student_num student_num self.device_info device_info self.crypto Crypto() self.base_params { version: 86, version_name: 3.3.6, mobileModel: device_info.get(mobileModel, BRQ-AN00), mobileDeviceId: device_info.get(mobileDeviceId), mobileOsVersion: 7.1.2, ostype: 1, student_num: student_num, school_id: ***, # 需要替换为实际值 uid: uid, jpush_id: device_info.get(jpush_id), } def build_refresh_token_request(self): 构造刷新Token的请求参数 # 1. 准备动态参数 timestamp str(int(time.time())) # 当前时间戳 nonce str(random.randint(100000, 999999)) # 6位随机数 # 2. 合并所有参数 request_params self.base_params.copy() request_params.update({ token: self.token, access_token: self.token, # 观察发现这个接口里access_token和token相同 refresh_token: self.refresh_token, timestamp: timestamp, nonce: nonce, }) # 3. 生成签名注意签名时不需要包含sign字段本身 sign_params request_params.copy() sign generate_sign(sign_params) request_params[sign] sign return request_params def refresh_token(self): 执行刷新Token的请求并解析响应 import requests url https://api2.lptiyu.com/v3/api.php/Login/RefreshToken headers { Content-Type: application/x-www-form-urlencoded;charsetUTF-8, User-Agent: Dalvik/2.1.0(Linux;U;Android7.1.2;BRQ-AN00Build/NZH54D), } # 构造请求数据 data self.build_refresh_token_request() # 发送POST请求注意数据需要以form-urlencoded格式发送 response requests.post(url, headersheaders, datadata) resp_json response.json() # 解密响应数据 if data in resp_json: decrypted_data self.crypto.decrypt(resp_json[data]) print(刷新Token成功返回数据, decrypted_data) # 更新本地的token信息 self.token decrypted_data.get(access_token, self.token) self.refresh_token decrypted_data.get(refresh_token, self.refresh_token) return decrypted_data else: print(请求失败, resp_json) return None5.3 实战演练与踩坑记录代码写好了跑起来试试。在实际运行中我遇到了几个典型的“坑”这里分享给你帮你避雷参数顺序是签名的关键generate_sign函数中必须先对参数字典进行排序sorted(params_dict.items())。服务器端校验签名时也是按同样的顺序拼接字符串。顺序错一个字符得到的MD5值就完全不同导致签名验证失败。注意字段的包含与排除构造用于签名的字典时要仔细对照Inspeckage日志里的原始字符串。有时候并不是请求体里的所有字段都需要参与签名。同样sign字段本身是签名计算的结果它当然不能参与自身的计算。AES解密后的数据清洗服务器返回的JSON字符串有时会包含一些控制字符如\u0004,\u0007或转义问题。直接json.loads()可能会失败。我的Crypto.decrypt方法中加入了简单的清洗逻辑如果遇到更复杂的情况你可能需要根据具体的报错信息来调整正则表达式或清洗策略。请求头的模仿除了Content-TypeUser-Agent也很重要。有些服务器会校验UA。直接从HttpCanary抓到的请求中复制完整的UA字符串是最稳妥的。时间戳与随机数的时效性timestamp和nonce是为了防止请求被重放。所以你的脚本每次发起请求时都必须生成新的、当前的时间戳和一个随机数。不能使用固定值。当你成功运行脚本打印出解密后的用户信息或新的token时那种成就感是无与伦比的。这意味着你完全理解了这套通信协议并且拥有了用代码与之对话的能力。6. 扩展思路构建更健壮的自动化工具一个简单的接口调用脚本只是开始。在实际项目中我们可能需要一个更健壮、功能更完整的工具。比如自动处理登录、token过期刷新、多接口调用、数据持久化等。这里分享一些我的扩展思路。你可以将上面的代码模块进一步封装形成一个客户端SDK。这个SDK可以包含以下功能会话管理自动维护uid,token,refresh_token等会话状态。自动重试与刷新当接口返回“token过期”等特定错误码时自动调用刷新token接口并用新的token重试原请求。数据持久化将设备信息、用户token等保存到本地文件或数据库下次启动时无需重新登录。多接口支持为APP的各个功能模块如登录、获取用户信息、查询跑步记录、提交成绩等封装对应的方法。异常处理与日志加入完善的异常捕获和日志记录方便调试和排查问题。例如一个增强版的客户端初始化可能包含自动登录逻辑class EnhancedLePaoClient(LePaoClient): def __init__(self, config_fileconfig.json): # 尝试从配置文件加载已有会话 if os.path.exists(config_file): self.load_config(config_file) # 验证token是否有效 if not self.validate_token(): print(Token已失效需要重新登录。) self.login_interactive() else: print(未找到配置文件开始首次登录。) self.login_interactive() super().__init__(self.uid, self.token, self.refresh_token, self.student_num, self.device_info) def login_interactive(self): # 交互式获取手机号和验证码可通过其他方式自动获取 phone input(请输入手机号: ) code input(请输入验证码: ) login_data self._perform_login(phone, code) self._update_session(login_data) self.save_config() def _perform_login(self, phone, code): # 构造登录请求登录接口的签名规则可能不同需要单独分析 # ... 登录逻辑 ... pass通过这样的迭代你的脚本就从一次性的分析验证工具进化成了一个可以持续集成、提供稳定服务的自动化组件。这才是协议逆向工程的最终价值所在——将黑盒的通信过程转化为白盒的、可控的代码逻辑。整个流程走下来从环境搭建、数据抓取、动态分析到代码实现我们完成了一个完整的闭环。技术本身在不断更新但这套“观察 - 假设 - 验证 - 实现”的方法论是通用的。希望这次以步道乐跑为例的实战能为你打开安卓APP协议逆向这扇门。下次当你遇到一个新的、未知的APP时不妨也拿起Inspeckage和HttpCanary这套组合工具按照这个思路试一试。