同花顺APP逆向实战:Frida定位加密与协议还原全解析
做行情、金融类 APP 的采集需求时同花顺是比较典型的“硬骨头”Java 层做了大量封装Native 层还有校验接口返回的字段也做过裁剪和签名保护。很多人第一次接触这类 APP 逆向要么卡在抓包看不到数据要么拿到接口却不知道签名怎么生成。本文基于前面的分析基础继续把动态调试、加密定位、采集侧代码还原的实战链路完整走一遍重点落在可落地的源码和数据流上。文章适合有 Android 基础、接触过抓包工具的读者也适合想系统了解“从 APK 到协议还原”这一套工程方法的同学。读完你会掌握一套不依赖具体版本的逆向分析方法换成同花顺后续版本也能自己顺着思路重新定位。1. 逆向分析与采集的边界问题开始动手之前我想先花一点篇幅把边界说清楚。这部分不是客套话而是后面所有操作的前提。1.1 本文适用的研究范围APP 逆向本身是中性的技术能力它可以用于很多合法场景自己开发的 APP 丢失了签名逻辑需要从线上包反推验证。企业委托的安全测试分析自家应用是否存在加密强度不足、参数可篡改等问题。学习 Android 系统机制、Native 层调用约定、Hook 框架原理。对已获得授权的目标进行协议兼容性研究例如老版本客户端无法登录时需要按同样的签名规则重新实现接口调用。文章里涉及的“采集”我统一界定为对你有权访问的数据、已经获得授权或属于公开信息的数据做自动化获取与整理。不要把本文内容理解为绕过收费、盗取用户隐私或批量抓取他人账户数据的“教程”。1.2 同花顺 APP 这类目标有什么特点同花顺作为知名金融信息服务商APP 的技术防护有自己的特点。你随便装一个版本打开会发现它的包结构和普通应用不太一样首次启动会有热修复或动态加载过程部分代码不在 classes.dex 里而是在运行时下载的解压目录中。关键网络请求几乎都走 HTTPS证书校验级别不低。请求体里常见自定义的加密字段和时间戳、随机数绑定在一起。核心算法经常下沉到 .so 动态库Java 层只能看到 JNI 接口名。这些设计决定了「只靠静态反编译」很难真正跑通一个接口。必须把静态分析、动态 Hook、协议抓包三者配合起来。1.3 为什么要强调“授权”很多初学者在网上求“同花顺 API”实际上同花顺官方提供的免费 API 是公开的、可以直接申请的真正被大家热衷讨论的是“模拟客户端请求的私有接口”。这类接口的调用行为一旦超出正常用户频率轻则触发风控重则涉及违反计算机信息系统安全相关法规。这里必须强调不要在生产环境对同花顺或任何第三方系统发起非授权的批量请求。本文代码只用于学习协议分析思路请在本地测试环境或自己搭建的模拟服务上验证。如果要做商业采集请先确认数据源授权优先使用官方开放接口或购买合规数据服务。2. 环境准备一套能用的逆向工具链我把分析环境分成静态分析、动态调试、抓包三组。三组工具共同工作才能完整还原一个加密请求。2.1 基础运行环境本文示例使用以下环境版本不需要完全一致思路不变Windows 10 / macOS / Ubuntu 皆可后面的命令以 Windows 和常见 Linux 风格为主。Python 3.9 及以上用于编写协议重放脚本。Android 模拟器或 Root 真机用于运行目标 APP 和 Frida Hook。一台可以访问互联网的电脑用于 Burp Suite 或 mitmproxy 抓包。手机和电脑处于同一局域网。2.2 静态分析工具静态分析主要用 JADX 看 Java 层逻辑用 frida-dexdump 导出运行时 DEX。JADX 的安装和使用非常简单解压后执行jadx -d output_dir com.example.target.apk关键参数说明-d指定反编译输出目录。--show-bad-code遇到部分反编译失败时尽量显示错误代码有助于理解原始逻辑。-j指定线程数机器性能好时可以调大比如-j 8。反编译结果会保留原有包名和目录结构。在output_dir/sources下可以直接用文本搜索工具检索关键字。2.3 动态 Hook 工具FridaFrida 是目前 Android 动态分析最常用的框架。电脑端安装很简单pip install frida-tools pip install frida手机端需要安装frida-server。注意版本必须和电脑端 frida 一致否则连接时会报错adb push frida-server /data/local/tmp/ adb shell chmod 755 /data/local/tmp/frida-server adb shell /data/local/tmp/frida-server 验证是否连接成功frida-ps -U如果能看到手机进程列表说明环境已经通了。2.4 抓包工具抓 HTTPS 包最常用的是 Burp Suite 和 mitmproxy。两者都需要提前安装 CA 证书到系统信任区。为什么要安装证书因为 APP 的 HTTPS 流量如果不做中间人代理我们只能看到一堆密文。安装 CA 证书后抓包工具可以解密 TLS 流量。对 Android 7.0 以上系统用户信任区默认不被 APP 信任所以需要把证书安装到系统信任区。模拟器可以adb root后直接 push真机则需要 Root 或使用调试模式。安装 mitmproxy 抓包pip install mitmproxy手机配置代理后启动 mitmproxy 的 web 界面mitmweb浏览器访问http://127.0.0.1:8080就能看到实时请求列表。流量不大时这个工具比 Burp 更轻量。3. 从 APK 到代码同花顺静态分析切入点有了工具链后第一步不是打开 JADX 乱翻而是先建立全局认识。3.1 识别壳和加固方式打开 JADX如果看到com.stub.StubApp或com.secneo.apkwrapper这类类名大概率使用了某加固厂商的方案。这时classes.dex通常只是壳真正的业务代码在运行时才解密。处理方式有两种方案 A直接脱壳工具。例如frida-dexdump。在 APP 启动后运行frida-dexdump -U -f com.example.target -o dump.dex-f表示启动指定应用-o输出 DEX 文件。方案 B反射 Dump ClassLoader。更通用但需要写一点 Frida 脚本。对同花顺这种复杂应用建议先用 frida-dexdump 把完整 DEX dump 下来再拖回 Jadx 分析。这样看到的代码才算完整。3.2 抓取搜索关键入口拿到可读源码后不建议从头到尾阅读。体量太大。应该带着目标去搜索。我们的核心目标是找到「网络请求入口」和「参数加密函数」。常见检索关键词signencryptdecryptMD5AESDESRSABase64getInstanceCipherMessageDigest例如在 JADX 的全局搜索中输入sign往往会直接命中拼装请求参数的代码位置。从这里向上回溯就能找到参数来源。同花顺不同版本的实现位置差异较大本文不能给出一个固定不变的类名。你本地分析时应该以自己反编译出的实际结果为准。3.3 区分 Java 层加密和 Native 层加密搜索到加密相关代码后先判断算法写在哪个层。Java 层特征很直观Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding);Native 层特征则是出现了System.loadLibrary并调用了native方法。例如static { System.loadLibrary(hexin); } public static native String getSign(String data);一旦看到这种写法就要把重点转到 .so 文件上。Java 层只是传参和拿结果真正的算法在 Native 二进制里。4. 动态 Hook在运行中抓取真实参数静态分析只能告诉你“可能这么算”不能告诉你“真实运行时的输入输出”。所以我们还需要动态验证。4.1 定位目标方法先回到 Frida。假设我们在 JADX 中找到了一个可疑方法public class RequestManager { public static String buildSign(MapString, String params) { // ... } }为了确认这个方法是不是真正被调用的签名函数可以 Hook 它打印输入参数和返回值。示例脚本如下// hook_sign.js Java.perform(function () { var RequestManager Java.use(替换为实际类名.RequestManager); RequestManager.buildSign.overload(java.util.Map).implementation function (params) { console.log([buildSign] params params.toString()); var result this.buildSign(params); console.log([buildSign] result result); return result; }; });运行命令frida -U -f com.example.target -l hook_sign.js这里把替换为实际类名.RequestManager换成你在 JADX 里看到的完整类名。运行后操作 APP 触发一次网络请求Frida 控制台就会输出签名函数的入参和返回值。通过对比返回值可以判断这个函数是否是最终签名。4.2 Hook Native 方法如果 Java 层方法已经 Hook 到了但返回值在 Java 层只是调用了 native 接口我们需要再往下追一层。假设 Java 层代码如下public static native String nativeSign(String data);Frida 可以拦截 native 方法脚本写法如下// hook_native.js Java.perform(function () { var TargetClass Java.use(替换为实际类名.TargetClass); TargetClass.nativeSign.implementation function (data) { var ret this.nativeSign(data); console.log([nativeSign] data data); console.log([nativeSign] ret ret); return ret; }; });Frida 的 Java.use 对 native 方法同样有效因为从 Java 层看它仍然是一个普通方法。获取到入参和返回值后就能确定 Native 方法的输入格式。4.3 在 libc 层拦截加密函数有时候 .so 内部会直接调用系统加密库比如 OpenSSL 的EVP_EncryptUpdate、AES_cbc_encrypt或者mbedtls_aes_crypt_cbc等。此时可以用 Frida 的Interceptor在 Native 层拦截。以 OpenSSL 的 AES 加密为例拦截伪代码思路// hook_openssl.js var frida null; function hook_openssl() { var AES_cbc_encrypt Module.findExportByName(libcrypto.so, AES_cbc_encrypt); if (AES_cbc_encrypt) { Interceptor.attach(AES_cbc_encrypt, { onEnter: function (args) { console.log([AES_cbc_encrypt] called); console.log(input data hexdump(args[0])); console.log(key length args[2].toInt32()); }, onLeave: function (retval) { console.log([AES_cbc_encrypt] leave); } }); } else { console.log(libcrypto.so not found); } } setTimeout(hook_openssl, 2000);实际拦截时要先把libcrypto.so、libmbedtls.so、libnative-lib.so等拿到本地在 IDA 或 Ghidra 里分析具体调用的导出符号再精确定位 hook 函数。初学阶段不用追求一步到位可以先打印所有可疑动态库的调用栈。4.4 利用“主动调用”思路有些方法在 APP 正常流程里不会频繁触发例如解密本地缓存的函数只在特定界面打开时执行。为了简化采集流程更推荐用 Frida 主动调用减少人工操作。主动调用就是在 Hook 脚本里直接构造参数并调用目标方法Java.perform(function () { var RequestManager Java.use(替换为实际类名.RequestManager); var paramMap Java.use(java.util.HashMap).$new(); paramMap.put(code, 000001); paramMap.put(timestamp, 1712345678000); var sign RequestManager.buildSign(paramMap); console.log(主动调用结果 sign sign); });这种方式可以快速验证不同参数下的签名结果为后续用 Python 重写算法做准备。5. 抓包与协议定位把请求完整还原动态 Hook 解决了「参数怎么算」的问题抓包解决的是「请求发到哪里、返回什么结构」的问题。5.1 用 mitmproxy 找到关键请求启动抓包后打开 APP操作某个查询功能例如查询个股行情、分时数据。然后回到 mitmproxy 过滤请求优先关注这几个特征POST 请求且 Body 不是标准表单格式而是 JSON 或经过编码的字符串。带有大量公共请求头如User-Agent、deviceId、appVersion。URL 路径包含quote、stock、kline、history等语义化关键字。5.2 解析请求参数结构一个典型请求可能长这样POST /api/v1/quote/kline HTTP/1.1 Host: your-target-host Content-Type: application/json User-Agent: THS/8.10.0 Android Device-Id: 86f2b5c3c9cd4a7e Timestamp: 1712345678000 Sign: a1b2c3d4e5f67890... { code: 000001, period: day, count: 320 }注意这里正文参数不复杂复杂的是请求头和 Sign 的生成逻辑。同一套 URL换一个参数后 Sign 完全不同。为了还原协议我习惯把抓到的内容做成“请求记录表”字段示例值来源是否参与签名URL 路径/quote/kline固定是请求体code000001...动态参数是Timestamp1712345678000时间戳是Device-Id86f2b5c3...设备号视版本而定Signa1b2c3...算法生成签名值记录多组不同代码、不同时间的请求对比数据找出发送端签名逻辑中哪些字段参与了计算。5.3 常见抓包失败原因抓包失败通常不是工具问题而是目标 APP 做了防代理检测。常见现象和处理思路现象可能原因处理方案抓不到任何 HTTPS 流量未安装 CA 证书重新安装证书到系统信任区能抓到流量但全是乱码APP 不信任用户 CA需要 Root 将证书移至系统 CA 目录部分接口能抓行情接口失败APP 对特定域名做了 SSL Pinning用 Frida Hook SSL 校验相关方法或使用 JustTrustMe 方案抓包工具崩溃或 APP 闪退检测到代理换成 Tun 模式代理或继续用 Hook 绕过检测SSL Pinning 是逆向中比较常见的知识如果你还不太熟悉建议单独搜一下相关原理。简单来说APP 把服务器的证书指纹写死在代码里抓包工具的证书自然校验不过。用 Frida 绕过或 Hook 校验函数后流量就能正常解密。6. 采集侧 Python 还原从分析结果到可运行代码分析完成后我们进入最后一步把协议还原成 Python 调用脚本。6.1 整体代码结构先看一段典型的采集侧请求骨架代码本身不针对某个真实接口结构可以直接复用# -*- coding: utf-8 -*- 同花顺 APP 协议分析学习示例 仅用于学习和本地测试请勿用于未经授权的批量采集 import hashlib import json import time import random import requests class StockClient: def __init__(self, base_url, device_id): self.base_url base_url self.session requests.Session() self.session.headers.update({ User-Agent: THS/8.10.0 Android, Device-Id: device_id, Content-Type: application/json, }) staticmethod def build_sign(params: dict, secret: str) - str: 根据抓包和反编译还原的签名规则生成 sign。 注意实际算法请根据你自己的分析结果替换 这里只演示“字典排序 拼接 哈希”的常见套路。 # 过滤空值 filtered {k: v for k, v in params.items() if v is not None and v ! } # 字典排序 sorted_keys sorted(filtered.keys()) # 拼接字符串 raw_string for key in sorted_keys: raw_string f{key}{filtered[key]} # 加上密钥 raw_string fsecret{secret} print([sign] raw string:, raw_string) # MD5 / SHA256 视具体算法而定 return hashlib.md5(raw_string.encode(utf-8)).hexdigest() def fetch_kline(self, code: str, period: str day, count: int 320) - dict: 获取 K 线数据。 演示用URL 和字段请按实际抓包结果替换。 timestamp int(time.time() * 1000) params { code: code, period: period, count: count, timestamp: timestamp, } sign self.build_sign(params, secretyour-secret) request_body { code: code, period: period, count: count, timestamp: timestamp, sign: sign, } # 这里仅作演示不会真的发起请求 # resp self.session.post(self.base_url /api/v1/quote/kline, jsonrequest_body) # return resp.json() print([fetch_kline] request url:, self.base_url) print([fetch_kline] request body:, json.dumps(request_body, ensure_asciiFalse)) return request_body if __name__ __main__: client StockClient( base_urlhttps://your-target-host, device_id86f2b5c3c9cd4a7e, ) result client.fetch_kline(code000001, periodday, count10)这段代码故意把真正的发送部分注释掉了。原因是同花顺真实接口的 host、路径、字段结构、签名算法必须在你自己抓包和逆向环境中确认后才能完整替换。盲目运行一个“网上抄来的地址”不仅大概率失败还可能引发不良后果。6.2 从 Hook 结果反推 Python 逻辑拿到 Frida 里 buildSign 的输入和输出后自己写 Python 逻辑时建议把多组样本系统整理出来像下面这样输入参数 code000001perioddaycount320timestamp1712345678000 样本 1 输出1f2c3d... 样本 2 输出a4b5c6...如果算法是简单的字典排序 拼接 MD5Python 侧和 JAVA 侧的字符串拼接顺序必须一致。很多同学在还原签名时失败并不是算法选错而是多了一个结尾的。时间戳传递的是字符串而不是数字。Secret 变量名不同。Join 分隔符不同比如有的用逗号、有的用空字符串。编码时用了 Unicode 字符导致哈希结果不一致。遇到这种情况最有效的排查方式是打印 Python 待签名的原串再和 Frida Hook 到的 Java 原串逐字符对比。6.3 携带 Cookie / Token 的登录态请求如果目标接口需要登录态采集脚本需要先走一遍登录流程把 token 缓存下来。登录流程同样需要逆向分析核心点包括密码是否在本地加密后再传输。验证码是否绑定设备信息。Token 是放在 Header 还是 Body。Token 有效期多久、RefreshToken 如何续期。对同花顺这种应用登录态相关接口一般防护更强。如果是个人学习建议构造一个本地模拟服务来测试不必使用真实账号去频繁请求。6.4 请求频率控制无论目标数据源是否合法采集请求都要有节流。过于密集的请求会给自己带来 IP 封禁风险也会增加目标服务器压力。常见做法import time import random def fetch_with_interval(client, codes): for code in codes: try: client.fetch_kline(code) except Exception as e: print(f[error] {code}: {e}) # 随机等待模拟真实用户访问 wait_seconds random.uniform(1.5, 3.5) time.sleep(wait_seconds)这里的随机等待不是为了防止检测而是让程序的行为更像一个正常用户避免对服务端造成突发压力。商业级采集更应该使用官方 API、消息队列、分布式限流等机制。7. 常见问题与排查清单你动手做的时候大概率会遇到下面这些问题我直接按排查顺序整理出来了。问题现象常见原因排查思路JADX 打开代码后搜不到关键字APK 加固导致业务 DEX 未解出先运行 APP再用 frida-dexdump dump DEXFrida 连接超时frida-server 版本和 PC 端不一致两端统一版本检查 adb 连接Hook 后 APP 闪退目标方法有反调试或调用频率过高降低 Hook 频率延迟 Hook 时间检查是否触发了反调试逻辑抓包只能看到 CONNECT 流量HTTPS 证书未正确安装安装 CA 证书到系统信任区抓包能看到请求但看不到响应证书校验失败Frida Hook SSL 校验或检查 session 复用Python 生成的 sign 和 APP 端不一致排序字段或 secret 不一致打印签名原串逐字符比对请求返回 401/403缺少有效 Token 或签名过期重新运行登录流程确认 timestamp 是否取到毫秒值IP 被封请求频率过高加入延迟、更换代理或直接停止采集排查方法论如果不知道从哪里入手我建议按下面这个顺序走先看网络层。抓包确认请求是否真正发出、服务器是否返回数据。再看参数层。对比两台抓包记录里同一字段的差异尤其是时间和设备标识。再看签名层。用 Frida Hook 打印签名函数的入参和返回值。最后看算法层。把多组输入输出代入 Python确认本地算法实现和 APP 端完全一致。不要跳步。很多同学在签名已经算错的情况下还继续调请求头浪费了不少时间。8. 工程建议与合规提醒逆向采集这类工程往往“能做”和“该做”是两回事。写代码之前工程层面的规范先定好。8.1 代码工程化建议第一配置隔离。host、secret、设备 ID 等不要硬编码到源码里。建议放配置文件或环境变量import os BASE_URL os.getenv(STOCK_BASE_URL, https://your-target-host) SECRET os.getenv(STOCK_SIGN_SECRET, your-secret)这样做的好处是如果你需要切换环境或换设备测试不需要重新改代码。第二日志与请求留痕。每次请求都记录时间、URL、状态码、耗时。不仅方便排查问题也是后续做数据质量评估的依据。可以参考如下结构import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, )第三失败重试必须带退避。服务器返回 5xx 或超时不要立即重试建议采用指数退避避免把自己的出口 IP 打爆。import time def request_with_retry(func, retries3): for attempt in range(retries): try: return func() except Exception as e: wait 2 ** attempt print(f第 {attempt 1} 次失败等待 {wait}s) time.sleep(wait) raise RuntimeError(重试多次仍失败)第四数据落库要做去重和清洗。尤其是 K 线、分时数据不同时间点请求同一只股票可能拿到重复记录。入库时可以用「代码 周期 时间戳」做唯一索引。8.2 同花顺这类 APP 的特殊注意点同花顺版本迭代比较快本文涉及的分析位置可能在新版本中变化。建议在工程里预留适配层独立封装签名函数避免把算法逻辑散落在采集函数中。每次 APP 更新后先分析版本差异再决定是否需要更新签名逻辑。不要在线上环境依赖逆向接口一旦目标调整加密方案你的采集任务会立刻不可用。8.3 合规提醒再强调一次如果你是出于个人学习目的在模拟器、本地测试环境完成本文中的 Hook 和请求还原没有问题。但如果你想把采集系统部署到服务器上批量抓取行情数据做二次分发请务必确认数据源是否允许采集。是否有官方 API 可用。是否有页面协议、Robots 协议或用户协议的限制。你的使用场景是否属于商业化用途。不同金融数据服务商对数据使用都有明确条款行情数据尤其是重资产直接抓取并商用风险很高。技术本身不违法但使用技术的边界一定要自己守住。9. 结语把“逆向”练成系统能力回到开头提到的同花顺实战整条链路可以归纳为一句话静态分析找线索动态 Hook 验逻辑抓包还原定协议Python 代码做落地。这四个环节缺一不可。不同版本的 APP 加密方案可能不同但分析方法始终相通。第一次做的时候会被各种细节卡住多试几次、把 Frida 脚本跑通、把抓包记录对齐你就能慢慢建立起自己的逆向分析体系。如果你拿到的目标是行情资讯类 APP记得优先看看官方开放平台有没有免费接口。很多常规数据不需要逆向就能拿到真正需要逆向的往往是企业合规授权下的兼容性工作。掌握这项能力会让你在协议分析、爬虫工程、安全测试甚至普通的后端开发任务里都更游刃有余。希望这篇实战解析能帮你少走一些弯路也欢迎你把实践中的新问题整理出来后面再针对具体模块继续拆解。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →