同花顺APP逆向实战:Frida定位加密与协议还原全解析
2026/9/5 15:52:36 网站建设 项目流程

做行情、金融类 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.StubAppcom.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 抓取搜索关键入口

拿到可读源码后,不建议从头到尾阅读。体量太大。应该带着目标去搜索。

我们的核心目标是找到「网络请求入口」和「参数加密函数」。常见检索关键词:

  • sign
  • encrypt
  • decrypt
  • MD5
  • AES
  • DES
  • RSA
  • Base64
  • getInstance
  • Cipher
  • MessageDigest

例如在 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_EncryptUpdateAES_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.solibmbedtls.solibnative-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-AgentdeviceIdappVersion
  • URL 路径包含quotestockklinehistory等语义化关键字。

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...动态参数
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 += 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&timestamp=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 被封请求频率过高加入延迟、更换代理或直接停止采集

排查方法论

如果不知道从哪里入手,我建议按下面这个顺序走:

  1. 先看网络层。抓包确认请求是否真正发出、服务器是否返回数据。
  2. 再看参数层。对比两台抓包记录里同一字段的差异,尤其是时间和设备标识。
  3. 再看签名层。用 Frida Hook 打印签名函数的入参和返回值。
  4. 最后看算法层。把多组输入输出代入 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,记得优先看看官方开放平台有没有免费接口。很多常规数据不需要逆向就能拿到,真正需要逆向的往往是企业合规授权下的兼容性工作。掌握这项能力,会让你在协议分析、爬虫工程、安全测试甚至普通的后端开发任务里都更游刃有余。希望这篇实战解析能帮你少走一些弯路,也欢迎你把实践中的新问题整理出来,后面再针对具体模块继续拆解。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询