电商App签名加密逆向实战:JS定位与Python复现抓取纯净数据 1. 项目概述与核心价值最近在分析一个主流电商App的v3.0版本时发现其数据接口的签名加密逻辑又升级了。和以往直接在请求参数里加个sign字段不同这次他们把签名生成的核心逻辑藏得更深用上了更复杂的密钥派生和哈希算法组合。对于做数据分析和市场研究的朋友来说直接拿到结构化的JSON数据至关重要但面对这种加密常规的抓包工具就束手无策了。这个项目就是带你一步步拆解这个新版签名sign的生成过程并用Python完整复现其逻辑最终实现绕过App直接模拟请求抓取纯净的JSON数据。整个过程不依赖任何第三方付费服务或硬件纯粹是技术层面的分析与还原。这不仅仅是破解一个签名更是一次完整的JS逆向实战。你会接触到如何定位移动端App中的加密入口、如何追踪JavaScript核心代码、如何分析混淆后的算法逻辑以及最终如何用Python这门“胶水语言”将零散的算法片段粘合成一个可用的签名生成器。无论你是想学习JS逆向的进阶技巧还是急需获取某个加密接口的数据这套方法都能给你提供一个清晰的思路和可复现的路径。接下来我们就从最关键的突破口——如何找到签名算法的位置开始。2. 逆向工程入口定位与抓包分析逆向的第一步永远是观察。在直接动刀分析代码之前我们必须先弄清楚这个签名“长什么样”以及它出现在网络请求的哪个环节。2.1 请求特征与签名初步观察我使用抓包工具如Charles或Fiddler对目标电商App进行流量拦截。配置好代理和SSL证书解密后浏览几个商品列表页和详情页很快就能在抓包记录中看到大量的api.xxx.com的请求。重点观察其中一个典型的商品列表请求GET /api/v3/product/list?categoryId123page1pageSize20timestamp1734567890123sign7a8f9e3d2c1b0a5f4e6d7c8b9a0f1e2d HTTP/1.1 Host: api.xxx.com User-Agent: Dalvik/2.1.0 (Linux; U; Android 11; ...) Authorization: Bearer eyJhbGciOiJ...可以看到几个关键特征签名位置sign作为一个独立的查询参数Query Param出现在URL中而不是放在请求头Header或请求体Body里。这是一种常见的做法方便服务端统一校验。其他参数除了业务参数categoryId,page等还有一个timestamp时间戳。签名算法极大概率会将这些参数一起纳入计算。请求方法这里是GET。对于POST请求签名算法通常还会处理请求体Body的内容。初步判断签名sign是一个32位的十六进制字符串hexdigest这强烈暗示它可能是MD5或SHA-256等哈希算法的结果。但直接对看到的参数如categoryId123page1...进行MD5计算得到的签名与抓包得到的sign完全不匹配。这说明算法内部还做了其他处理比如密钥参与、参数排序、或者对参数值进行了额外的编码或转换。注意在实际操作中务必使用测试账号或在不违反用户协议的前提下对自己可控的账号相关数据进行抓包分析。避免高频请求干扰正常服务。2.2 关键突破口搜索与Hook知道签名特征后下一步就是在App的代码里找到它。对于Android App我们可以将APK文件进行反编译使用工具如Jadx-GUI。反编译后我们面临的是成千上万个Java/Kotlin类文件。直接阅读犹如大海捞针。这里的关键技巧是搜索和Hook。字符串搜索在Jadx中全局搜索关键词“sign”。你可能会找到几十上百个结果。我们需要筛选出那些看起来像是网络请求构建或签名生成的地方。例如搜索“.sign”、“sign”或者签名参数名“sign”。有时签名方法名可能不是简单的sign而是generateSign、getSignature、calcSign等需要结合上下文判断。定位网络库现代App大多使用OkHttp、Retrofit等网络库。找到负责构建最终请求URL或添加公共参数的拦截器Interceptor代码是发现签名逻辑的捷径。在Jadx中搜索“Interceptor”、“OkHttpClient”、“addQueryParameter”等关键词。使用Frida进行动态Hook静态分析遇到混淆时动态调试威力巨大。我们可以编写Frida脚本Hook我们怀疑的签名生成方法。例如如果我们怀疑一个名为com.xxx.security.SignUtils.generate()的方法可以写如下脚本Java.perform(function() { var SignUtils Java.use(com.xxx.security.SignUtils); SignUtils.generate.overload(java.lang.String, java.util.Map).implementation function(param1, paramMap) { console.log([] generate() called!); console.log(param1: param1); console.log(paramMap: JSON.stringify(paramMap)); var result this.generate(param1, paramMap); console.log(result (sign): result); return result; }; });运行这个脚本后在App内触发网络请求如果Hook成功控制台就会打印出签名方法的输入参数和输出结果。这能让我们瞬间理解签名算法需要哪些原材料。在我的这次分析中正是通过Frida Hook发现签名方法接收两个参数一个是固定的“v3”字符串可能是版本标识另一个是一个包含了timestamp、page等所有业务参数的Map对象。3. 核心算法逆向与JavaScript代码分析通过Hook我们知道了签名函数的输入输出。下一步就是深入函数内部看它到底做了什么。由于App可能对核心算法进行了JavaScript封装尤其是在Hybrid或React Native开发的应用中或者将算法放在so库里我们可能需要追踪到JavaScript层。3.1 追踪至JavaScript环境在反编译的代码中如果发现调用链最终指向了WebView、evaluateJavascript或者ReactNative相关模块那么签名算法很可能实现在JavaScript中。我们可以尝试从App的assets资源目录或代码中查找.js、.jsbundle文件。更有效的方法是在App运行时利用浏览器的开发者工具对于WebView或React Native调试器直接查看加载的JavaScript代码。对于加固较强的App这一步可能比较困难。另一种思路是既然我们能用Frida Hook到Java层的方法那么也可以尝试用Frida去Hook JavaScript引擎的执行直接打印或修改JavaScript函数的逻辑但这需要更高级的技巧。在这次分析的案例中我幸运地在assets里找到了一个名为security.js的压缩文件。格式化代码后发现了一个非常可疑的函数function generateSignV3(params, secretKey) { // 1. 参数排序 var keys Object.keys(params).sort(); var sortedStr ; for (var i 0; i keys.length; i) { var key keys[i]; var value params[key]; // 2. 对值进行URL编码注意空格转为号 sortedStr key encodeURIComponent(value).replace(/%20/g, ); if (i ! keys.length - 1) { sortedStr ; } } // 3. 拼接密钥 var strToSign sortedStr secret secretKey; // 4. 计算MD5并转为小写 var md5Hash md5(strToSign); return md5Hash.toLowerCase(); }这段代码逻辑非常清晰但有个关键点secretKey从哪里来它没有硬编码在JS里。继续搜索secretKey的赋值发现它是在App启动时通过一个Java Native Interface (JNI) 调用从本地so库中获取的。这增加了难度意味着密钥是动态生成的或者隐藏在so库中。3.2 算法逻辑拆解尽管密钥获取被保护但签名算法本身已经明朗。我们将其逻辑拆解为以下几步这是后续用Python复现的蓝图参数收集与过滤收集所有需要参与签名的请求参数GET参数、POST的Form参数或JSON Body。通常需要排除sign参数本身。有些系统还会排除file等特殊参数。字典序排序将所有参数名按照字母顺序ASCII码进行排序。这是保证服务端和客户端以相同顺序拼接字符串的关键。键值对拼接将排序后的参数以keyvalue的形式用符号连接起来形成一个字符串。这里需要特别注意value的处理空值value是空字符串或null时如何处理通常是保留key的形式。编码value是否需要像encodeURIComponent那样编码是否需要将%20替换为这必须和原算法完全一致。拼接密钥将上一步得到的参数字符串后面加上secret和密钥secretKey形成待签名的原始字符串。哈希计算使用指定的哈希算法这里是MD5计算上一步字符串的哈希值。输出格式化将计算出的二进制哈希结果通常转换为十六进制字符串hexdigest。注意大小写这里是全小写。实操心得在分析JS代码时要特别注意JavaScript和Java/Python在字符串处理、编码、排序上的细微差异。例如JavaScript的encodeURIComponent和Python的urllib.parse.quote默认行为有差别。必须通过对比抓包得到的原始请求和算法输出进行反复验证确保每一步的还原都精确无误。4. Python复现签名生成逻辑算法逻辑清晰后用Python复现就相对直接了。但魔鬼藏在细节里我们必须确保每一步都和JavaScript版本的行为完全一致。4.1 环境准备与依赖首先创建一个Python项目并安装必要的库。我们主要会用到hashlib进行MD5计算以及urllib.parse进行URL编码。# 建议使用虚拟环境 python -m venv venv source venv/bin/activate # Linux/Mac # venv\Scripts\activate # Windows # 安装依赖 (实际上标准库已足够这里列出以备其他需要) # pip install requests # 用于最终发送请求4.2 密钥问题的解决思路这是复现过程中最大的障碍。密钥secretKey没有直接出现在JS中。有几种解决思路静态分析so库使用IDA Pro、Ghidra等工具逆向Android的so库找到生成或返回密钥的函数。难度较高。动态获取既然App运行时能拿到密钥我们也可以用Frida写一个脚本在App启动后Hook那个返回密钥的JNI函数将密钥打印出来并保存。只要App版本不变这个密钥在一段时间内通常是固定的。这是最实用的方法。模拟推导观察密钥是否与设备信息、时间、或其他固定种子相关。有时密钥可能是App版本号 设备ID 固定盐值的某种哈希。这需要大量猜测和验证。假设我们通过方法2利用Frida脚本获取到了当前的secretKey为d8f7a9c3b4e5f6a7。请注意这是一个示例真实密钥需自行获取。4.3 核心代码实现现在我们可以编写Python版的generate_sign_v3函数了。import hashlib from urllib.parse import quote def generate_sign_v3(params, secret_key): 复现电商App v3.0签名算法 :param params: dict, 所有请求参数不包含sign本身 :param secret_key: str, 从App中获取的密钥 :return: str, 计算得到的sign值 # 1. 参数排序按照键的ASCII码升序 sorted_items sorted(params.items(), keylambda x: x[0]) # 2. 构建待签名字符串 parts [] for key, value in sorted_items: # 关键模拟JavaScript的encodeURIComponent并将%20替换为 # Python的quote默认会对空格编码为%20我们需要替换为 encoded_value quote(str(value), safe) encoded_value encoded_value.replace(%20, ) parts.append(f{key}{encoded_value}) param_str .join(parts) # 3. 拼接密钥 str_to_sign param_str secret secret_key print(f[DEBUG] 待签名字符串: {str_to_sign}) # 调试用发布时可删除 # 4. 计算MD5并转为小写十六进制 md5_hash hashlib.md5() # 注意需要将字符串编码为字节流 md5_hash.update(str_to_sign.encode(utf-8)) sign md5_hash.hexdigest().lower() return sign # 测试用例 if __name__ __main__: # 模拟抓包得到的参数不含sign test_params { categoryId: 123, page: 1, pageSize: 20, timestamp: 1734567890123 } test_secret_key d8f7a9c3b4e5f6a7 # 示例密钥 calculated_sign generate_sign_v3(test_params, test_secret_key) print(f计算得到的sign: {calculated_sign}) # 与抓包得到的sign对比 captured_sign 7a8f9e3d2c1b0a5f4e6d7c8b9a0f1e2d # 示例 if calculated_sign captured_sign: print(✅ 签名验证成功) else: print(❌ 签名验证失败请检查算法或密钥。)代码关键点解析sorted(params.items(), keylambda x: x[0])这确保了参数按键名进行ASCII排序与JavaScript的Object.keys(params).sort()行为一致。quote(str(value), safe)urllib.parse.quote的safe参数表示不对任何字符进行特殊保留全部编码这最接近encodeURIComponent的行为。默认的safe/会保留斜杠等字符。.replace(%20, )这是为了精确匹配目标JS代码中将空格编码为号的行为。很多Web签名规范中空格确实被处理为而非%20。str_to_sign.encode(utf-8)哈希函数需要字节输入必须指定编码。UTF-8是最通用的选择。运行这段测试代码如果输出的sign与抓包中看到的sign一致那么恭喜你核心算法复现成功。5. 构建完整的数据抓取流程有了签名生成能力我们就可以组装一个完整的、能绕过App直接获取数据的Python脚本了。5.1 请求参数自动化组装一个完整的请求除了业务参数通常还需要公共参数如timestamp当前毫秒时间戳、appVersion、deviceId等。我们需要模拟App的行为自动组装这些参数。import time import uuid def build_common_params(): 构建公共请求参数 params { timestamp: str(int(time.time() * 1000)), # 13位毫秒时间戳 appVersion: 3.0.1, # 需与目标App版本一致 platform: android, deviceId: get_device_id(), # 模拟一个设备ID可以是固定值或随机生成 # ... 可能还有其他参数需根据抓包补充 } return params def get_device_id(): 模拟生成设备ID。实际中可能更复杂这里简单返回一个UUID # 可以返回一个固定的模拟ID以保持会话一致性 # return simulated_device_id_12345 return str(uuid.uuid4()).replace(-, )[:16] def build_full_params(business_params): 合并公共参数和业务参数 common build_common_params() full_params {**common, **business_params} # 通常需要移除sign参数如果有的话 full_params.pop(sign, None) return full_params5.2 集成签名与发送请求我们将签名函数集成到请求流程中使用requests库发送HTTP请求。import requests def fetch_data_from_api(api_path, business_params, secret_key): 向目标API发送请求并获取JSON数据 :param api_path: str, API路径如 /api/v3/product/list :param business_params: dict, 业务参数 :param secret_key: str, 签名密钥 :return: dict, 解析后的JSON响应数据 # 1. 构建完整参数 full_params build_full_params(business_params) # 2. 生成签名 sign generate_sign_v3(full_params, secret_key) full_params[sign] sign # 将签名加入请求参数 # 3. 构建请求URL和Headers base_url https://api.xxx.com # 替换为真实域名 url base_url api_path headers { User-Agent: Dalvik/2.1.0 (Linux; U; Android 11; ...模拟), # 模拟App的UA Authorization: Bearer ..., # 如果需要Token这里也需要处理 Content-Type: application/x-www-form-urlencoded; charsetUTF-8, } # 注意如果Authorization是动态的也需要通过逆向获取其生成或刷新逻辑 # 4. 发送请求 print(f[INFO] 请求URL: {url}) print(f[INFO] 请求参数: {full_params}) try: # 如果是GET请求 response requests.get(url, paramsfull_params, headersheaders, timeout10) # 如果是POST请求参数在body中则使用 # response requests.post(url, datafull_params, headersheaders, timeout10) response.raise_for_status() # 检查HTTP错误 # 5. 解析响应 json_data response.json() print(f[INFO] 请求成功数据条数: {len(json_data.get(data, [])) if isinstance(json_data.get(data), list) else N/A}) return json_data except requests.exceptions.RequestException as e: print(f[ERROR] 网络请求失败: {e}) return None except ValueError as e: print(f[ERROR] JSON解析失败: {e}, 响应文本: {response.text[:200]}) return None # 使用示例 if __name__ __main__: SECRET_KEY d8f7a9c3b4e5f6a7 # 替换为真实密钥 API_PATH /api/v3/product/list BUSINESS_PARAMS { categoryId: 123, page: 1, pageSize: 20 } data fetch_data_from_api(API_PATH, BUSINESS_PARAMS, SECRET_KEY) if data: # 处理获取到的数据例如保存到文件 import json with open(product_list.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) print(数据已保存至 product_list.json)5.3 处理授权与Token很多API除了签名还需要Authorization头部的Token。这个Token通常通过登录接口获得。你需要分析App的登录流程抓取登录请求分析登录请求的参数和响应。响应体里通常包含一个access_token和refresh_token。模拟登录用Python复现登录请求获取Token。注意登录请求本身可能也有签名需要先破解登录接口的签名算法可能和业务接口不同。Token管理实现Token的缓存和刷新逻辑。Token有过期时间过期后需要用refresh_token去刷新。这又是一个独立的逆向点。注意事项Token和签名密钥都可能与设备、账号绑定。频繁更换设备信息或在高并发下使用同一个Token/密钥可能会触发服务器的风控机制导致IP或账号被封禁。在自动化脚本中建议加入合理的请求间隔如time.sleep(1)并模拟真实用户的行为轨迹。6. 常见问题排查与实战技巧在实际操作中你几乎一定会遇到各种问题导致签名校验失败。下面是一个排查清单和对应的解决思路。6.1 签名校验失败排查表问题现象可能原因排查步骤与解决方案签名完全不匹配1. 密钥错误。2. 参数集合错误多参、漏参。3. 算法根本不对不是MD5或哈希前还做了其他操作。1.核对密钥用Frida多次Hook确认密钥是否动态变化或检查是否传错。2.参数对比将Python构建的参数字典与Frida Hook到的参数Map进行逐项对比确保完全一致。特别注意timestamp的精度秒/毫秒。3.算法验证在JS代码中在md5()函数调用前将strToSign打印出来。在Python中也打印str_to_sign进行逐字符比对包括不可见字符。签名偶尔成功经常失败1. 参数排序规则不一致。2. 参数值编码不一致。3. 有非必传参数未参与签名但有时又被传入了。1.确认排序确保Python和JS都按ASCII码升序排序。对于包含数字、字母、下划线的键名通常没问题。但要小心中文参数名。2.精确编码重点检查encodeURIComponent的还原。对于特殊字符如!,(,),*等看JS和Python的编码结果是否一致。使用print(repr(encoded_value))查看Python中的真实字符。3.检查参数源确保每次请求构建的参数来源稳定没有随机或动态生成的干扰项。请求返回“签名过期”timestamp参数与服务端时间不同步。1. 检查本地系统时间是否准确。2. 检查App使用的timestamp是秒还是毫秒。3. 服务器可能有时间容差如±5分钟如果本地时间偏差太大就会失败。同步网络时间。请求返回“非法请求”或“参数错误”1. 缺少某些必传的公共参数如appKey,version。2.User-Agent或Header被服务器校验。1. 仔细对比抓包中成功请求的所有参数一个都不能少。2. 完全模拟抓包中的请求头包括User-Agent,X-Requested-With等字段。登录成功但业务接口401/403Token失效或未正确附加。1. 检查Authorization头是否正确添加了Bearer前缀和Token。2. Token可能已过期需要实现刷新逻辑。3. Token可能与当前请求的设备信息或签名绑定确保这些信息在登录和后续请求中保持一致。6.2 高级技巧与心得对抗代码混淆如果核心JS代码被Webpack打包或严重混淆函数名和变量名都变成了a, b, c。这时不要慌可以搜索关键常量字符串如secret、MD5的初始化常量0x67452301等来定位函数。或者在已知的入口函数处下断点单步跟踪执行流。使用Node.js进行算法验证在复现算法过程中可以创建一个简单的Node.js环境直接执行被还原的JavaScript代码片段与Python计算结果进行交叉验证。这能快速定位是密钥问题还是算法还原问题。参数白名单与黑名单不是所有请求参数都参与签名。有些系统会排除sign本身、file文件上传等参数。这需要分析JS代码中的过滤逻辑或者在Hook时观察传入签名函数的Map到底包含了哪些键。应对算法变种v3.0签名可能只是当前版本。服务端可能会升级到v3.1加入新的盐值或更换哈希算法。你的脚本应该设计得易于修改将算法版本、密钥等配置项提取到外部配置文件或数据库里。法律与道德边界所有逆向分析工作都应仅限于学习、研究和法律允许的范围内。切勿用于恶意爬取、侵犯隐私、干扰服务正常运行或进行商业牟利。在开始项目前请务必阅读目标App的用户协议和robots.txt如果有的话明确其数据使用政策。通过以上步骤你不仅能够破解特定App的签名更能掌握一套通用的JS逆向和算法复现的方法论。从抓包观察、动态Hook、静态分析到代码复现和完整流程搭建每一步都需要耐心和细致的对比验证。当你的脚本成功抓取到第一份JSON数据时那种成就感就是对所有努力的最好回报。记住逆向工程的精髓在于理解系统的设计逻辑而不仅仅是破解一个障碍。