做行情、金融类 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 工具:Frida
Frida 是目前 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(Map<String, 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 | 固定 | 是 |
| 请求体 | code=000001... | 动态参数 | 是 |
| Timestamp | 1712345678000 | 时间戳 | 是 |
| Device-Id | 86f2b5c3... | 设备号 | 视版本而定 |
| Sign | a1b2c3... | 算法生成 | 签名值 |
记录多组不同代码、不同时间的请求,对比数据,找出发送端签名逻辑中哪些字段参与了计算。
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 += f"secret={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, secret="your-secret") request_body = { "code": code, "period": period, "count": count, "timestamp": timestamp, "sign": sign, } # 这里仅作演示,不会真的发起请求 # resp = self.session.post(self.base_url + "/api/v1/quote/kline", json=request_body) # return resp.json() print("[fetch_kline] request url:", self.base_url) print("[fetch_kline] request body:", json.dumps(request_body, ensure_ascii=False)) return request_body if __name__ == "__main__": client = StockClient( base_url="https://your-target-host", device_id="86f2b5c3c9cd4a7e", ) result = client.fetch_kline(code="000001", period="day", count=10)这段代码故意把真正的发送部分注释掉了。原因是:同花顺真实接口的 host、路径、字段结构、签名算法必须在你自己抓包和逆向环境中确认后,才能完整替换。盲目运行一个“网上抄来的地址”不仅大概率失败,还可能引发不良后果。
6.2 从 Hook 结果反推 Python 逻辑
拿到 Frida 里 buildSign 的输入和输出后,自己写 Python 逻辑时,建议把多组样本系统整理出来,像下面这样:
输入参数: code=000001&period=day&count=320×tamp=1712345678000 样本 1 输出:1f2c3d... 样本 2 输出:a4b5c6...如果算法是简单的字典排序 + 拼接 + MD5,Python 侧和 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 DEX |
| Frida 连接超时 | 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( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", )第三,失败重试必须带退避。服务器返回 5xx 或超时,不要立即重试,建议采用指数退避,避免把自己的出口 IP 打爆。
import time def request_with_retry(func, retries=3): 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,记得优先看看官方开放平台有没有免费接口。很多常规数据不需要逆向就能拿到,真正需要逆向的往往是企业合规授权下的兼容性工作。掌握这项能力,会让你在协议分析、爬虫工程、安全测试甚至普通的后端开发任务里都更游刃有余。希望这篇实战解析能帮你少走一些弯路,也欢迎你把实践中的新问题整理出来,后面再针对具体模块继续拆解。