简介本资源是一份面向Python与前端逆向初学者的实战型学习材料聚焦电商平台接口逆向分析核心能力训练适用于希望掌握JS混淆解码、动态签名构造及HTTP请求模拟等关键技术的开发者。压缩包共4个文件含1份HTML页面用于本地运行调试JS解码逻辑、1份Markdown文档记录分析思路与关键参数说明、1个.inscode配置文件支持IDE快速导入环境及.gitignore体现工程规范意识整体仅8KB轻量易读。已有204人学习下载反映出其在小而精的逆向入门场景中具备较高参考价值。读者可直接复用其中Python调用JS函数的Bridge方案、请求头构造模板与签名生成逻辑片段并结合HTML中的可执行JS代码理解加密流程快速建立从抓包→分析→还原→验证的完整逆向思维链路。1. 什么是“盼之代售逆向分析”一个面向电商中间层系统的静态动态双轨拆解实践“盼之代售逆向分析”不是某个公开 SDK 或标准协议而是某电商生态中一类典型代售服务即第三方以自营名义上架、结算、履约但实际库存与履约由上游供应商托管在客户端侧暴露的通信链路与业务逻辑还原工程。它不涉及服务器端源码也不依赖任何越狱或系统级权限——核心目标是搞清App 怎么发现代售商品如何构造下单请求价格/库存/优惠券如何联动计算为什么同一商品在“自营”和“代售”tab 下展示逻辑完全不同这类分析直接服务于合规性审计、多平台比价工具开发、以及自建轻量级代售聚合网关的可行性验证。适合已有 Android/iOS 基础、熟悉 HTTP/HTTPS 抓包与基础 smali/Java/Kotlin 反编译流程的开发者不适合零基础想“一键破解”的用户——这里没有密钥导出也没有自动登录脚本只有对业务语义的耐心拼图。你不会拿到“盼之”的后端 API 文档但能亲手重建出一份比官方文档更贴近真实调用场景的《代售模块行为白皮书》。2. 从 APK 入手定位代售核心模块的三步锚定法2.1 第一步用字符串特征快速圈定业务包名与关键类很多团队误以为要从MainActivity开始逐层跟栈效率极低。实际经验是先搜字符串再反向找调用链。代售场景必然高频出现“代售”“授权销售”“联营”“供货商”等中文关键词也常埋有dai_shou、proxy_sku、consignment等拼音或英文缩写。我们用jadx-gui打开 APK 后全局搜索代售注意引号避免匹配到“代售商品”等长句干扰结果通常集中在资源文件strings.xml和 Java/Kotlin 字节码中。重点看那些被BindView、findViewById或viewBinding引用的字符串 ID 对应的布局文件再顺藤摸到加载该布局的 Fragment 或 Activity 类。# 在 jadx 输出目录中执行Linux/macOS grep -r 代售 ./sources/ --include*.java --include*.kt | head -20提示如果搜不到中文立刻切到resources.arsc的字符串池jadx-gui 左侧树状图 →resources.arsc→String Pool这里存储的是编译后未混淆的原始字符串比代码里更可靠。常见代售标识字段如is_consignment_item、supplier_id、proxy_price往往在此处首次露头。2.2 第二步用网络请求特征锁定代售专属 API 域名与路径代售接口往往与主站域名分离例如主站用api.shop.com代售走proxy.api.shop.com或consign.shop.com。抓包时不要只盯着https://开头的 URL更要关注Host头、Referer、以及请求体中的source_typeconsignment这类标记字段。我们用Charles或mitmproxy抓取一次完整代售商品页浏览 加购 结算流程导出为chls或har文件用 Python 脚本过滤# filter_proxy_api.py import json with open(traffic.har, r, encodingutf-8) as f: har json.load(f) proxy_endpoints set() for entry in har[log][entries]: req entry[request] host req[headers].get(Host, ) url req[url] # 关键同时匹配域名特征 请求体/查询参数含代售语义 if (proxy in host or consign in host or dai in host) and \ (any(kw in url.lower() for kw in [sku, item, detail]) or post in req[method].lower()): proxy_endpoints.add(f{req[method]} {url}) for ep in sorted(proxy_endpoints): print(ep)运行后你会得到类似GET https://proxy.api.shop.com/v2/item/detail?skuId123456sourceconsignment POST https://consign.shop.com/order/create这些就是你要逆向的核心靶点。2.3 第三步用调用栈回溯确认代售逻辑入口类有了域名和关键字符串下一步是找到谁在发起这些请求。在jadx-gui中右键点击任意一个代售相关字符串如proxy.api.shop.com选择Find Usages。结果会列出所有引用它的类。重点关注继承自RetrofitService接口的定义类如ConsignmentApiService.java封装了OkHttpClient并设置baseUrl的网络配置类名称含Consignment、ProxyItem、DaiShou的 ViewModel 或 Repository 类此时不要急着读全类先看其create()或load()方法的调用链谁调用了它传入了什么参数返回值被谁消费用 jadx 的Analyze → Show Call Hierarchy功能从网络请求方法向上追溯 23 层大概率能定位到ConsignmentProductFragment或ProxySkuDetailActivity这类明确命名的入口组件——这正是你后续 Hook 和日志注入的黄金位置。3. 静态分析进阶识别加密签名与参数组装逻辑3.1 代售请求为何总 401签名算法藏在 OkHttp Interceptor 里代售接口几乎全部要求请求头携带X-Signature或Authorization: Proxy token且该签名随时间、参数、设备指纹实时变化。新手常卡在这一步以为要逆向 AES 或 RSA其实绝大多数场景用的是HMAC-SHA256 固定 salt 动态时间戳。签名生成逻辑极少放在业务层而是在网络拦截器Interceptor中统一注入。在jadx-gui中搜索implements Interceptor或new Interceptor() {重点查看intercept(Chain chain)方法。代售专用拦截器通常有明显特征对request.url().toString()做 substring 截取判断是否包含proxy或consign调用某个SignUtil.sign()或AuthHelper.buildSignature()方法传入request.method()、request.url().encodedQuery()、request.body().toString()等最终将结果写入request.newBuilder().addHeader(X-Signature, signature)// 示例典型的代售签名拦截器片段jadx 反编译后 public Response intercept(Chain chain) throws IOException { Request request chain.request(); String url request.url().toString(); if (url.contains(proxy.api.) || url.contains(consign.)) { // 仅对代售域名启用签名 String method request.method(); String query request.url().encodedQuery() ! null ? request.url().encodedQuery() : ; String bodyStr ; RequestBody body request.body(); if (body ! null body.contentType() ! null body.contentType().toString().contains(json)) { Buffer buffer new Buffer(); body.writeTo(buffer); bodyStr buffer.readUtf8(); } String signature SignUtil.generate(method, url, query, bodyStr, System.currentTimeMillis()); request request.newBuilder() .addHeader(X-Signature, signature) .addHeader(X-Timestamp, String.valueOf(System.currentTimeMillis())) .build(); } return chain.proceed(request); }注意SignUtil.generate(...)是关键。继续跟进这个方法你会发现它调用hmacSha256(signData, fixed_salt_2023)—— 这个fixed_salt_2023就是硬编码密钥也是你本地复现签名的唯一钥匙。不要试图爆破它就在同一个类或BuildConfig里搜salt或key字符串。3.2 参数组装陷阱skuId不是数字而是带校验位的 base64 编码代售商品 IDskuId表面看是纯数字如123456789但实测发现直接拿这个数字拼接 URL 会返回400 Invalid SKU。用jadx搜索skuId的 setter 或构造处常看到类似逻辑// 商品实体类中 public void setSkuId(String rawSku) { this.skuId Base64.encodeToString( (rawSku | calculateCheckDigit(rawSku)).getBytes(), Base64.NO_WRAP ); }这意味着前端传给后端的skuId是123456789|7假设校验位是 7再 base64 编码的结果而非原始数字。calculateCheckDigit()通常是简单模运算如(sum of digits) % 10或 CRC16。漏掉这一步所有代售详情请求都会失败。解决方案在 Hook 点如ConsignmentApiService.getItemDetail()中打印原始rawSku和最终encodedSku对比抓包数据反推出校验算法。3.3 优惠券叠加规则JSON Schema 隐式定义在请求体注释里代售订单创建接口/order/create的请求体是 JSON但其字段含义不透明。例如coupon_list数组里每个对象有id、type、scope、discount_value但type3代表什么scope2是否表示“仅限代售商品”翻代码发现这些枚举值定义在CouponType.java中但类里只有public static final int FULL_DISCOUNT 1;没有注释。真正的线索在 Retrofit 接口方法的KDoc 注释Kotlin或JavadocJava里/** * 创建代售订单 * param coupon_list 优惠券列表支持类型 * - type1: 平台通用券 * - type2: 供应商专属券仅对当前代售商品生效 * - type3: 跨店满减券需检查 scope1 表示全场scope2 表示仅代售 */ POST(order/create) fun createOrder(Body order: OrderRequest): CallOrderResponse提示jadx-gui 默认不显示注释需在设置中勾选Show comments from dex。这些注释是开发自留的“活文档”比反编译出的常量定义更有价值。4. 动态分析实战Frida Hook 代售核心方法并导出行为日志4.1 环境准备绕过 SSL Pinning 与检测框架代售 App 普遍启用 SSL Pinning证书固定和 Frida 检测。直接frida -U -f com.xxx.shop -l hook.js会闪退。必须分两步绕过 Pinning使用objection自动 patch推荐# 安装 objection需 Python 3.8 pip install objection # 连接设备自动解除 pinning objection -g com.xxx.shop explore --startup-command android sslpinning disable绕过 Frida 检测HookSystem.getProperty(frida.active)和Process.checkError()等检测点。我们用一个最小化脚本bypass_detection.js// bypass_detection.js Java.perform(() { const System Java.use(java.lang.System); System.getProperty.overload(java.lang.String).implementation function(key) { if (key frida.active) return false; // 伪装未激活 return this.getProperty(key); }; const Process Java.use(android.os.Process); Process.checkError.implementation function() { // 直接返回不抛异常 return; }; });启动命令改为frida -U -f com.xxx.shop -l bypass_detection.js --no-pause注意--no-pause是关键否则 App 启动即暂停无法触发检测逻辑。等控制台输出Started tracing后再在另一终端执行frida-ps -U | grep shop确认进程已跑起来。4.2 Hook 代售商品加载捕获原始 SKU 与价格计算链我们要监控ConsignmentProductFragment加载商品时调用的网络方法。先用frida-trace快速定位frida-trace -U -i *Consignment*.*load* com.xxx.shop输出会显示类似ConsignmentRepository.loadItemDetail的候选。然后编写精准 Hook 脚本hook_consignment.js// hook_consignment.js Java.perform(() { const ConsignmentRepo Java.use(com.xxx.shop.repo.ConsignmentRepository); ConsignmentRepo.loadItemDetail.overload(java.lang.String, java.lang.String).implementation function(sku, otherParam) { console.log([HOOK] loadItemDetail called with sku:, sku); // 打印调用栈确认来源 console.log(Stack:, Java.use(android.util.Log).getStackTraceString(Java.use(java.lang.Exception).$new())); const result this.loadItemDetail(sku, otherParam); // result 是 LiveData 或 Call需进一步处理 return result; }; });但这样只能看到输入看不到后端返回的原始 JSON。更有效的是 Hook OkHttp 的ResponseBody.string()const ResponseBody Java.use(okhttp3.ResponseBody); ResponseBody.string.implementation function() { const str this.string(); if (str.includes(consignment) || str.includes(proxy)) { console.log([RESPONSE] Raw consignment data:, str.substring(0, 500)); // 截断防刷屏 } return str; };将两个 Hook 合并在一个脚本里运行后打开代售商品页控制台会实时输出[HOOK] loadItemDetail called with sku: 123456789 [RESPONSE] Raw consignment data: {sku:MTIzNDU2Nzg5fDM,price:89.9,supplier_name:XX数码专营店,...}这就是你梦寐以求的“未加工”数据流。4.3 日志结构化导出用 Frida 写入手机 SD 卡供后续分析控制台日志易丢失需持久化。Frida 支持Java.use(java.io.FileWriter)但要注意权限。我们改用更稳妥的context.getExternalFilesDir(null)// 在 Java.perform 内部 const Environment Java.use(android.os.Environment); const File Java.use(java.io.File); const FileWriter Java.use(java.io.FileWriter); const logFile File.$new( Environment.getExternalStorageDirectory().getAbsolutePath() /consignment_debug.log ); // 重写 console.log 使其同时写入文件 const originalLog console.log; console.log function() { const msg Array.prototype.slice.call(arguments).map(String).join( ); try { const fw FileWriter.$new(logFile, true); // append mode fw.write(new Date().toISOString() | msg \n); fw.close(); } catch (e) { // 权限不足时降级到 console originalLog.apply(console, arguments); } originalLog.apply(console, arguments); };提示首次运行前手动在手机上授予 App “存储”权限设置 → 应用 → XXX商城 → 权限 → 存储。日志文件路径为/sdcard/Android/data/com.xxx.shop/files/consignment_debug.log可用adb pull导出分析。5. 避坑指南代售逆向中踩过的 5 个真实血泪坑5.1 现象抓包看到代售接口返回 200但 Frida Hook 却收不到响应原因接口使用了 OkHttp 的Cache-Control: only-if-cached响应直接从内存缓存读取未经过Interceptor和ResponseBody因此 Frida 无法捕获。解决在抓包工具中禁用缓存Charles → Proxy → Recording Settings → uncheck Enable Caching或在 Hook 脚本中同时监听Cache.get()方法const Cache Java.use(okhttp3.Cache); Cache.get.implementation function(request) { const result this.get(request); if (result request.url().toString().includes(proxy)) { console.log([CACHE HIT] , request.url().toString()); } return result; };5.2 现象成功 HookloadItemDetail但sku参数是乱码如\u0000\u0000\u0000原因APK 启用了字符串加密String Encryptionsku在传入方法前已被解密但 Frida 读取的是寄存器原始值未触发解密逻辑。解决放弃 Hook 参数改 Hook 解密方法本身。搜索decryptString、deobfuscate等关键词找到解密函数后 Hook 其返回值const CryptoUtil Java.use(com.xxx.shop.util.CryptoUtil); CryptoUtil.decryptString.implementation function(input) { const plain this.decryptString(input); console.log([DECRYPT] input:, input, → output:, plain); return plain; };5.3 现象本地用 Postman 模拟代售下单请求始终返回Invalid signature但 Frida 日志里看到的签名字符串完全一致原因签名计算依赖System.currentTimeMillis()而 Frida 注入时存在毫秒级延迟导致时间戳与签名生成时差 30s服务端校验阈值。解决不在 Postman 里手动生成签名而是用 Frida HookSignUtil.generate()将其封装为 RPC 接口用 Python 脚本实时调用// 在 Frida 脚本中暴露 RPC rpc.exports { generatesign: function(method, url, query, body) { return SignUtil.generate(method, url, query, body, Date.now()); } };Python 端用frida.get_usb_device().attach(com.xxx.shop).rpc.exports.generatesign(...)获取签名确保时间戳与上下文一致。5.4 现象Hook 到ConsignmentProductFragment的onCreateView但findViewById(R.id.price_text)返回 null原因代售模块使用了ViewBinding或Compose传统findViewById已失效。R.id.price_text可能是旧版布局残留新布局中价格控件 ID 已改为binding.priceText或Composable PriceDisplay()。解决在jadx-gui中搜索priceText字符串定位到ConsignmentProductBinding.java其内部有public final TextView priceText;字段。Hook 应针对ConsignmentProductBinding.inflate()的返回值const Binding Java.use(com.xxx.shop.databinding.ConsignmentProductBinding); Binding.inflate.implementation function(inflater, container, attachToRoot) { const binding this.inflate(inflater, container, attachToRoot); console.log([BINDING] priceText text:, binding.priceText.getText()); return binding; };5.5 现象代售商品加入购物车后主 App 购物车页面不显示但 Frida 显示addCart请求返回 success原因代售购物车与主购物车物理隔离数据存在独立数据库表如consignment_cart且 UI 层未监听该表变更。解决Hook 数据库操作定位ConsignmentCartDao.insert()或RoomDatabase的consignment_cart表const Dao Java.use(com.xxx.shop.dao.ConsignmentCartDao); Dao.insert.implementation function(item) { console.log([CART INSERT] , JSON.stringify(item)); return this.insert(item); };确认数据已写入后检查ConsignmentCartFragment是否调用notifyDataSetChanged()或是否存在LiveDataCartData未正确 observe。6. 进阶技巧构建可复用的代售行为模式库与自动化验证流水线6.1 从日志到模式用正则与 AST 提取代售业务规则前面导出的consignment_debug.log是非结构化文本需提炼成机器可读的规则。我们用 Python 构建一个轻量解析器核心是两类提取提取类型正则示例用途API 模式GET https://proxy\.api\.shop\.com/v2/item/detail\?skuId([^\])生成 OpenAPI spec 的paths参数约束price:(\d\.\d),.*?min_order:(\d)提取price必须 0min_order为整数但正则无法处理嵌套 JSON。此时用ast.literal_eval()安全解析日志中的 JSON 片段import re import ast def parse_log_line(line): # 匹配 [RESPONSE] Raw consignment data: {...} 格式 match re.search(r\[RESPONSE\] Raw consignment data: ({.*?}), line) if not match: return None try: # 用 ast.literal_eval 替代 json.loads避免执行恶意代码 data ast.literal_eval(match.group(1)) return { sku: data.get(sku), price: float(data.get(price, 0)), supplier: data.get(supplier_name), stock: data.get(stock, 0), rules: extract_business_rules(data) # 自定义规则提取函数 } except: return None def extract_business_rules(data): rules [] if data.get(price, 0) 0: rules.append(price_must_be_positive) if data.get(stock, 0) data.get(min_order, 1): rules.append(stock_must_ge_min_order) return rules运行后生成consignment_patterns.json{ api_endpoints: [ {method: GET, path: /v2/item/detail, required_params: [skuId]}, {method: POST, path: /order/create, required_fields: [sku, quantity, address_id]} ], business_rules: [ {condition: price_must_be_positive, severity: error}, {condition: stock_must_ge_min_order, severity: warning} ] }这就是你的代售模块“行为契约”。6.2 自动化验证用 pytest Frida 构建回归测试集有了行为契约下一步是防止后续版本变更破坏兼容性。我们写一个test_consignment_api.py用 Frida 启动 App、触发代售流程、校验日志是否符合契约import pytest import frida import time import json class TestConsignmentAPI: def setup_method(self): self.device frida.get_usb_device() self.pid self.device.spawn([com.xxx.shop]) self.session self.device.attach(self.pid) # 加载 Frida 脚本开启日志捕获 with open(hook_consignment.js) as f: script self.session.create_script(f.read()) script.load() self.device.resume(self.pid) time.sleep(3) # 等待 App 启动 def test_sku_loading(self): # 模拟用户点击代售商品 self.device.input_tap(500, 1200) # 坐标需根据实际屏幕调整 time.sleep(2) # 从 Frida 控制台获取最近一条 [RESPONSE] 日志 logs get_recent_frida_logs(self.session, [RESPONSE]) assert len(logs) 0, No response log captured data parse_log_line(logs[-1]) assert data[price] 0, Price must be positive assert supplier in data, Supplier name missing def teardown_method(self): self.session.detach()提示get_recent_frida_logs()需实现 Frida 的onMessage回调收集。完整脚本见 GitHub 仓库consignment-test-suite虚构地址仅作示意。每次发版前运行pytest test_consignment_api.py即可自动验证代售核心链路是否断裂。6.3 我的习惯用 Notion 建立代售模块知识图谱所有逆向成果——APK 版本、Hook 脚本、签名密钥、API 模式、业务规则、已知 Bug——我都不散落在本地文件里而是沉淀到 Notion 数据库。每条记录包含Version2.8.5APK 版本号从build.gradle或jadx的AndroidManifest.xml提取Signature Keyfixed_salt_2023加星号脱敏Critical Hook PointsConsignmentRepository.loadItemDetail,SignUtil.generateKnown Limitationsv2.8.5 不支持跨店优惠券叠加需降级到 v2.7.0数据库视图按Version分组点击任一版本右侧关联显示其所有 Hook 脚本、抓包 HAR 文件、日志样本。当新版本发布我只需新建一条记录复制旧版脚本再运行frida-trace对比差异几分钟内就能定位变更点。这种结构化沉淀让“盼之代售逆向”不再是单次项目而成为可演进的资产。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?