事情是这样的,前阵子接了个App的数据采集需求。对方给的目标App不是H5套壳,是正经的原生加密接口,请求头里带签名,参数里带加密字段,Charles一抓全是密文。浏览器里能看的网页版接口,换到移动端就给你返回一堆看不懂的字节。
这种局,纯靠Python发请求是死路一条,你得先搞定两件事:第一,让App的HTTPS流量在Charles里变成明文;第二,让App在运行时把加密逻辑“演”给你看。前者是Charles抓包的本职工作,后者是Frida Hook的看家本事。这篇文章就把这两条线串起来,讲清楚从环境搭建到实战采集的完整链路。
这套组合适合谁?想入门移动端逆向的Python开发者、需要从App取数但不想死磕汇编的爬虫工程师、还有做安全测试的朋友。前置要求不高,懂一点Python语法,知道requests怎么发请求,剩下我一步步带。
1. 先说结论:这套组合到底在解决什么问题
1.1 抓包工具负责“看”,Hook框架负责“骗”
很多新手上来就问“Charles怎么破解App的加密接口”,其实这个理解是错的。Charles干的活是中间人代理,它能让你看到App和服务器之间传输的明文内容,但它不会帮你解密业务层的数据。真实的工作流是两层配合:
-
Charles解决“传输层明文”的问题。它把自己伪装成服务器,跟App建立一条TLS连接,再跟真正的服务器建立另一条TLS连接,两条连接拼在一起,中间的数据就被它看光了。这是经典的中间人攻击思路,只不过用在了调试场景。
-
Frida解决“业务层逻辑”的问题。App内部的加密算法、签名生成、参数拼接,这些发生在内存里,Charles看不到。Frida的做法是把JS脚本注入到App进程里,Hook住关键函数,把函数入参、返回值、调用栈全部打印出来,相当于在App的代码里安了一个窃听器。
一句话总结:Charles让你能看到网络层发生了什么,Frida让你能看到代码层发生了什么。 两个配合,才能从“抓到一堆密文”推进到“知道密文怎么生成、怎么还原”。
1.2 什么场景下你才需要这套配置
不是所有App都需要上Frida,很多情况Charles单独就够用了。我一般按下表来判断:
| 场景 | 需要Charles | 需要Frida |
|---|---|---|
| 接口请求头里只有一个固定Token | 是 | 否 |
| 请求参数是明文JSON,只是包了一层TLS | 是 | 否 |
| 请求参数带时间戳+签名 | 是 | 是 |
| 响应内容整体加密,字段不可读 | 是 | 是 |
| App做了证书校验(SSL Pinning) | 否 | 是 |
| 核心算法写在so层(Native层) | 辅助 | 是 |
上表最关键的一行是最后两行。如果App做了SSL Pinning,Charles能抓到包但全是握手失败的红色警告,这时候必须用Frida去Hook掉证书校验逻辑。如果App把签名算法下沉到so层,Charles同样无能为力,Frida配合Native Hook才能拿到结果。
1.3 我为什么不推荐一上来就啃源码
市面上的逆向教程有个通病,一上来就让你反编译APK、看smali、分析so文件。这条路太慢,而且对Python背景的开发者极不友好。你用反编译工具看半天,不如直接Hook一个函数来得快。
反编译适合“静态分析”——理清整体结构、找关键类名、看权限声明。但真正要弄明白“这个参数怎么来的”,动态Hook的效率高几个数量级。Frida让你在App运行时直接看到函数调用,比对着汇编猜含义快太多了。所以我的建议是:先用Charles+Frida把链路跑通,反编译只在需要定位函数的时候用来辅助。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:版本匹配是最大的隐性坑
这一节不啰嗦,直接给环境清单和验证方法。很多人卡在第一步装环境,不是操作不对,是版本不匹配。
2.1 Python虚拟环境与依赖安装
Python这边需要安装的是frida-tools和frida两个包。这里有个细节,frida和frida-tools是两个独立的包,很多人都搞混了。frida是Python绑定库,负责跟frida-server通信;frida-tools才是命令行工具集,包括frida-ps、frida-trace这些命令。两个版本必须保持同步,否则会报版本不一致的错误。
bash复制python -m venv venv
source venv/bin/activate # Windows下执行 venv\Scripts\activate
pip install frida frida-tools
装完以后验证一下:
bash复制python -c "import frida; print(frida.__version__)"
frida --version
这两个命令输出的版本号必须一致。比如都是16.4.5,对不上就重新装。我见过太多人卡在这一步,装了半天发现是frida和frida-tools版本冲突。
2.2 Charles安装与代理配置的基本功
Charles的安装没什么好说的,官网下载对应系统的安装包,一路下一步。Mac用户注意一下,Charles的证书要装到“系统”钥匙串里,不只是“登录”钥匙串,否则Chrome会持续报证书不受信任。
手机或模拟器端的代理配置,重点就三个参数:代理地址、代理端口、代理协议。代理地址填你电脑的局域网IP,端口默认8888,协议选HTTP。这里有个坑,如果你电脑开了防火墙,模拟器可能连不上Charles的8888端口,需要在防火墙里放行Charles。
提示:用MuMu、雷电这类模拟器时,不要直接在模拟器设置里填代理。模拟器内的代理要设置Wi-Fi的代理,而且很多国产模拟器的“网络设置”入口藏得很深。最稳妥的办法是直接在模拟器设置里搜索“代理”,或者用
adb shell settings put global http_proxy IP:端口命令设置。
2.3 模拟器/真机的Frida环境:版本匹配原则
Frida的版本匹配有三层,每一层不匹配都会出问题:
- frida(Python端)版本要和frida-server(设备端)版本一致。
- frida-server的架构要匹配设备CPU架构。模拟器常见的是
x86_64,真机大部分是arm64。 - frida-server的Android版本要兼容设备系统版本。
下载frida-server的时候,官方GitHub Release页面会提供很多文件,命名规则是frida-server-版本-android-架构.xz。比如frida-server-16.4.5-android-x86_64.xz。下载后先解压:
bash复制xz -d frida-server-16.4.5-android-x86_64.xz
然后推送到设备上:
bash复制adb push frida-server-16.4.5-android-x86_64 /data/local/tmp/
adb shell
cd /data/local/tmp
chmod 755 frida-server-16.4.5-android-x86_64
./frida-server-16.4.5-android-x86_64 &
启动成功后,回到电脑端执行frida-ps -U,如果能列出设备上的进程列表,说明通信正常。
2.4 一个容易被忽略的校验点:Android版本与SSL层差异
这里多说一句,Android 7.0(API 24)开始,App默认不再信任用户安装的CA证书。这意味着即使你把Charles的证书装到了手机上,Android 7.0以上的App也可能拒绝信任它,表现为Charles里能看到握手失败。
解决办法是改App的network_security_config.xml配置,但这需要重打包APK,对大部分场景来说太重了。另一个办法是用Frida直接Hook掉证书校验逻辑,这个后面实战部分细讲。你现在只需要记住:Android版本越高,SSL层的坑越深,越依赖Frida救场。
3. Charles抓包要点:从“能看到包”到“能读懂包”
3.1 代理与证书配置:为什么证书装完还是连不上
Charles抓包的标准流程是三步:
- 设置电脑端代理监听端口(默认8888)。
- 设备连上同一Wi-Fi,设置代理指向电脑IP和8888端口。
- 手机浏览器访问
chls.pro/ssl下载CA证书并安装。
这三步做完,大部分HTTPS网站应该都能解出明文了。但很多人卡在第三步:证书装了,Charles里还是显示红叉。
这个问题的根源,九成是证书安装的位置不对。Android 7.0以上要求证书是“CA证书”类型,装到“受信任的凭据”里的“系统”分区才行。普通安装默认装到“用户”分区,而App默认不信任用户分区的证书。你可以去设置里搜“证书与凭据”,看一下Charles证书在哪个分区。
还有一个容易忽略的点:有些App在检测到Charles代理后,会主动拒绝联网。 这不是技术问题,是App内部的网络策略。遇到这种情况,用Charles看包的意义就不大了,直接跳转到Frida阶段,或者用Iptables做透明代理绕过检测。
3.2 HTTPS解密原理:Charles是怎么当中间人的
理解Charles的解密原理,有助于你在遇到问题时快速定位。Charles做的事,本质上是生成一张自己的CA根证书,然后为每个域名动态签发一张子证书。具体流程:
- 设备代理请求指向Charles。
- Charles收到App的TLS握手请求后,用自己的证书跟App完成一次TLS握手。
- Charles再以客户端身份,跟真正的服务器完成第二次TLS握手。
- 之后App发给服务器的数据,先到Charles,Charles解密后重新加密发给服务器。
所以对App来说,Charles就是服务器;对服务器来说,Charles就是App。这就是“中间人”三个字的来源。
为什么Charles能解密?因为App信任了Charles的CA根证书。 如果App不信任,TLS握手就会失败,体现在Charles上就是握手错误。这就引出了SSL Pinning。
3.3 Android 7以上必须处理的网络配置问题
我说一下实际项目里最常见的两种处理姿势:
姿势一:App没有做SSL Pinning。 这种情况只需要把Charles的CA证书装进系统分区就能抓。可以用Magisk模块或者一个叫Move Certificates的工具,把用户分区的证书复制到系统分区。不用重打包App,简单直接。
姿势二:App做了SSL Pinning。 证书校验在代码层写死,即使装了系统证书也会被拒。这种情况Charles单独搞不定,必须依赖Frida Hook掉校验函数。这也是本文的核心场景。
我给个判断标准:如果Charles的抓包记录里,某个请求显示“Handshake failed”或者“Connection aborted”,十有八九是SSL Pinning。 此时不必在Charles上死磕,继续看第四章。
3.4 定位目标接口的三个实用方法
Charles能抓到包以后,真正的难点是怎么从几十上百个请求里找到你需要的那个接口。三个方法按效率排序:
方法一:过滤域名。 在Charles的Filter栏输入目标域名,比如api.example.com。第一个看的接口往往是/api/config、/api/user/info这样的“初始化接口”,App启动后会先请求这些。
方法二:按包名筛选。 Charles支持按进程名过滤,在Proxy设置里打开Record过滤,填上App的包名。这个功能能帮你把其他App的流量全部过滤掉。
方法三:抓启动链路。 清空Charles记录,杀掉App重新启动,观察从冷启动到首页数据加载完成这期间的请求序列。通常第一个请求是配置,第二个是用户信息,第三个就是首页数据,逐一点开看返回内容,找到你需要的字段。
提示:找到目标接口后,右键复制cURL请求,直接粘贴到Python的
requests里就能模拟。但注意,如果请求头里有签名参数,cURL里的值已经过期,必须通过Frida动态获取新的签名。
4. Frida Hook的核心机制与Python端集成
4.1 Frida的工作方式:注入脚本与RPC调用
Frida全称是“Dynamic instrumentation toolkit”,它的工作方式分两端:
- 设备端:运行一个叫
frida-server的守护进程。它负责接收电脑端发来的命令,把JS代码注入到目标App进程里执行。 - 电脑端:Python脚本通过
frida库连接frida-server,发起注入请求,并接收注入脚本回传的数据。
这个架构最巧妙的一点是——你在电脑上用Python写业务逻辑,用JS写Hook逻辑,两者通过Frida的桥接通道互相通信。JS脚本负责在App内部“偷看”,Python负责“处理结果”。对爬虫场景来说非常友好,采集逻辑和Hook逻辑天然分离。
4.2 Hook一个函数到底发生了什么
Hook的核心概念是:在目标函数执行前后插入你的代码。Frida的Interceptor就是干这个的。看一个最简单的例子,Hook一个Java静态方法:
javascript复制Java.perform(function () {
var targetClass = Java.use("com.example.app.CryptoHelper");
targetClass.md5.overload('java.lang.String').implementation = function (input) {
console.log("md5 called, input = " + input);
var result = this.md5(input);
console.log("md5 result = " + result);
return result;
};
});
这段代码用Java.use找到目标类,然后重写md5方法的实现。原来的逻辑依然会执行(调用this.md5(input)),但我们加了一层日志,能看到入参和返回值。
为什么Hook不用改App的代码? 因为Frida在运行时修改了App进程内存中的方法表,让方法调用指向了你的JS实现。对App来说,它感知不到自己被“篡改”了。
4.3 Python如何调用Frida:从进程列表到脚本注入
Python端的代码模板是固定的,核心就三步:连接设备、附加进程、加载脚本。我封装了一个可复用的骨架:
python复制import frida
import sys
def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)
js_code = """
Java.perform(function () {
var targetClass = Java.use("com.example.app.CryptoHelper");
targetClass.md5.overload('java.lang.String').implementation = function (input) {
send({before: input});
var result = this.md5(input);
send({after: result});
return result;
};
});
"""
device = frida.get_usb_device()
pid = device.spawn(["com.example.app"])
session = device.attach(pid)
script = session.create_script(js_code)
script.on('message', on_message)
script.load()
device.resume(pid)
sys.stdin.read()
这段代码的细节值得讲一下:
device.spawn是在App启动时就注入,比attach先跑到。App的启动阶段往往是初始化加密逻辑的关键时期,所以用spawn而不是attach。send函数是JS往Python端回传数据的通道。on_message里接收到的就是JS里send出来的内容。device.resume(pid)必须放在script.load()之后,否则App的代码已经跑过了Hook点,Hook就不生效了。
4.4 多dex场景下如何定位目标函数
这是实际项目中很容易踩的坑。很多App用了MultiDex,核心代码不在默认的classes.dex里,Frida的Java.use会报“ClassNotFoundException”。
解决办法是主动遍历所有已加载的类,找到目标类。Frida提供了Java.enumerateLoadedClasses,但是要注意,类在第一次使用时才会被加载。如果你发现Hook不到目标类,先触发一下目标功能,让App把类加载进内存,再Hook。
一个更靠谱的方法是HookClassLoader的loadClass方法,抓取所有类的加载过程:
javascript复制Java.perform(function () {
var ClassLoader = Java.use("java.lang.ClassLoader");
ClassLoader.loadClass.overload('java.lang.String').implementation = function (name) {
if (name.indexOf("com.example") !== -1) {
console.log("Loading class: " + name);
}
return this.loadClass(name);
};
});
这个hook脚本配合操作日志,能快速定位到目标类在哪个ClassLoader、什么时候加载。定位到以后,再用Java.use去Hook目标方法。实测下来,多dex场景下这个方案比瞎猜快得多。
5. 组合实战:绕过SSL Pinning拿到加密参数
5.1 目标分析:先看包,再决定Hook什么
我拿一个典型的场景举例。假设目标App是com.example.app,Charles抓到的请求长这样:
code复制POST /api/v1/order/list
Headers:
sign: 9f2d0e1c...
timestamp: 1735123456
Body:
{"uid": "123456", "page": 1}
sign明显是动态生成的签名,每次请求都不一样。现在的问题是:sign是怎么来的?
先用反编译工具(我习惯用JADX)打开APK,全局搜索"sign"。搜出来的结果会有一堆,快速筛选出哪些类名包含sign、crypto、security、encrypt这些关键词。不用深入分析代码逻辑,只需要定位关键类和方法的名称。
这一步的目的是给Frida提供靶子。有了类名和方法名,Frida才能精准Hook。
5.2 通过Hook证书校验函数绕过Pinning
针对SSL Pinning,Frida有一个现成的思路:Hook SSLContext和TrustManager相关的函数,让App不再校验证书。下面是一段兼容性比较好的脚本:
javascript复制Java.perform(function () {
var SSLContext = Java.use("javax.net.ssl.SSLContext");
SSLContext.init.overload(
'[Ljavax.net.ssl.KeyManager;',
'[Ljavax.net.ssl.TrustManager;',
'java.security.SecureRandom'
).implementation = function (keyManagers, trustManagers, secureRandom) {
console.log("Bypassing SSLContext.init()");
this.init(keyManagers, trustManagers, secureRandom);
};
var X509TrustManager = Java.use("javax.net.ssl.X509TrustManager");
var TrustManagerImpl = Java.use("com.android.org.conscrypt.TrustManagerImpl");
TrustManagerImpl.verifyChain.implementation = function (untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData) {
console.log("Bypassing TrustManagerImpl.verifyChain()");
return untrustedChain;
};
});
这段脚本干的活是:让App在握手时信任任何证书。 TrustManagerImpl.verifyChain是Android系统层校验证书链的方法,返回原始链就相当于告诉App“证书没问题”。
Hook完以后再回Charles看,握手失败的问题消失,HTTPS流量正常解密。
注意:Frida绕过SSL Pinning的方案有很多,没有万能钥匙。不同的App用了不同的网络库,需要Hook不同的函数。常见的Target有
okhttp3.CertificatePinner(OkHttp)、TrustManagerImpl(系统库)、NSURLSession(iOS)等。先抓报错日志,看它在哪一步挂的,再决定Hook什么。
5.3 Hook加密函数:拿参数和返回值
绕过SSL Pinning只是第一步,真正的难点是拿到签名算法的入参和返回值。回到5.1的场景,在JADX里搜索到的关键类可能是com.example.app.security.SignHelper,方法名是generateSign。现在用Frida把它Hook住:
javascript复制Java.perform(function () {
var SignHelper = Java.use("com.example.app.security.SignHelper");
SignHelper.generateSign.overload('java.lang.String', 'java.lang.String').implementation = function (params, timestamp) {
var result = this.generateSign(params, timestamp);
console.log("params = " + params);
console.log("timestamp = " + timestamp);
console.log("sign = " + result);
return result;
};
});
把这段脚本加载进Python,触发一次App的请求,你就能在控制台看到完整的签名生成过程:传入的参数是什么、时间戳是什么、最终的签名是什么。
这一步的价值在于:你不需要理解签名算法的具体实现,你只需要在Python端复现它的输入输出。 把App里调用generateSign时传入的参数,原样搬到Python里,再用Hook到的算法生成签名,就能拼出合法的请求。
5.4 写一个可复用的Python采集脚本
到这里,Charles和Frida已经帮你搞定了“如何生成合法请求”的问题。最后一步是把这些能力封装成Python采集脚本。我建议的做法是:Frida脚本常驻,通过RPC调用动态生成签名,而不是在Python里复刻一遍算法。
Frida的RPC(Remote Procedure Call)机制允许Python端直接调用JS里定义的函数。JS代码可以这样改造:
javascript复制rpc.exports = {
getsign: function (params, timestamp) {
var result = null;
Java.perform(function () {
var SignHelper = Java.use("com.example.app.security.SignHelper");
result = SignHelper.generateSign(params, timestamp);
});
return result;
}
};
Python端就可以这样调用:
python复制sign = script.exports_sync.getsign(json_data, current_timestamp)
然后直接用requests发请求:
python复制import requests
import json
import time
url = "https://api.example.com/v1/order/list"
timestamp = str(int(time.time()))
params = json.dumps({"uid": "123456", "page": 1})
sign = script.exports_sync.getsign(params, timestamp)
headers = {
"sign": sign,
"timestamp": timestamp,
"Content-Type": "application/json"
}
resp = requests.post(url, headers=headers, data=params)
print(resp.json())
为什么推荐RPC而不是纯Python复现算法? 因为真实项目里的签名算法经常嵌套好几层,复现成本高,而且算法一更新你就要跟着改。RPC的方式把“生成签名”这件事留给App自己做,天然免疫算法变化。只要Frida连上App,你永远能拿到合法签名。
提示:RPC方案的前提是设备上一直跑着frida-server,而且要保证App进程不被杀掉。实际落地时,我会用一台专门的测试机当“签名机”,脚本内部做了重连逻辑,frida-server断了就自动重启,App被杀就自动拉起。
6. 盘点我踩过的坑与排查思路
6.1 “证书已安装但显示不安全连接”的排查链
这是我被问得最多的问题。网上90%的答案都是“证书没装好”,但实际情况分好几种。我总结了排查链路:
第一步,先确认证书是否真的装进了系统分区。去设置里查“受信任的凭据”,看有没有Charles的证书条目。如果没有,说明你只装到了用户分区,需要走Magisk模块或Move Certificates工具。
第二步,确认代理是否生效。Charles的会话列表里能不能看到CONNECT请求?如果连CONNECT都没有,说明代理压根没生效,检查模拟器或手机的Wi-Fi代理设置。
第三步,确认是不是SSL Pinning。如果CONNECT成功、证书也装了,但某个请求还是握手失败,点开Charles的Info窗口看错误信息。如果提示证书校验失败,基本可以断定是SSL Pinning。
前面三步走完,问题基本定位清楚了。多数情况下,不是证书的问题,是SSL Pinning。
6.2 “frida-server启动失败”的原因分类
frida-server在设备上跑不起来,原因我可以列个表,方便你对照排查:
| 现象 | 原因 | 解决办法 |
|---|---|---|
cannot find "frida_server" |
二进制没有推送到设备,或路径不对 | 检查/data/local/tmp/下有没有文件,确认推送时没有改名 |
failed to open |
没有执行权限 | chmod 755 frida-server |
segmentation fault |
架构不匹配 | 用adb shell getprop ro.product.cpu.abi确认架构,重新下载对应版本 |
| 进程启动后立刻退出 | 设备上已有旧版frida-server | 先pkill frida-server再启动,或者观察输出日志 |
电脑端frida-ps -U超时 |
电脑和设备的frida版本不一致 | 统一Python端的frida和frida-tools版本 |
这里要特别注意:frida版本不匹配的报错往往不是你想象的“版本号不匹配”,而是各种玄学错误。 我之前遇到过一遍遍报unable to connect to device,折腾半天发现电脑端frida是16.0,设备端frida-server是15.2,版本差一个小版本就通信不了。
6.3 “Hook不到目标函数”的四种可能
Hook脚本写了,也加载成功了,但目标函数就是没被执行,日志里一条输出都没有。这种情况我归纳为四种原因:
第一种,目标类还没被加载。 Java的类是在第一次使用时才加载到内存的,而Frida的Hook发生在负载时。解决办法是先触发目标功能,让App把目标类加载进来,再重新Hook。或者在脚本里加一个延迟重试机制。
第二种,目标方法有重载。 generateSign可能有多个重载版本,你的overload参数类型写得不匹配。先删掉overload,直接用implementation = function() {},让Frida自动匹配所有重载。
第三种,函数在Native层实现。 如果方法体是native关键词,纯Java Hook抓不到。这时候要用Interceptor.attach,Hook的是so层导出函数。
第四种,App检测了Frida。 有些App有反调试机制,检测到Frida的特征就直接退出或者走假的执行路径。这种需要先绕过反调试,比如修改frida-server的文件名、隐藏注入特征。
6.4 设备选型与效率建议
拿模拟器还是真机?这是个老生常谈的问题。我的习惯是:调试阶段用模拟器,采集上线用真机。
模拟器的优势是方便,快照、多开、环境干净,适合开发调试。但模拟器有两个隐患:第一,部分App做了模拟器检测,运行环境不合法直接拒绝服务;第二,模拟器的CPU架构和真机不同,有些so层的加密逻辑在模拟器上会走不同的分支。所以模拟器上调通的Hook脚本,拿到真机上跑,有时候行为会不一样。
真机采集更稳定,但要解决设备管理的问题。我的做法是买几台便宜的Android小主机,固定放在一个台子上,统一刷好系统,装上frida-server,用Python脚本统一调度。这些设备不需要屏幕,不需要新系统,稳定就行。
成本方面,模拟器零成本,真机一台几百到一千不等。如果是个人项目,模拟器完全够用;如果是正式的采集业务,建议直接上真机,省下的调试时间远超设备成本。
另外强调一点,所有测试行为都要在授权范围内进行。 我自己的原则是:只测自己开发或者有明确授权的App,不拿这套东西去打别人家的服务。逆向技术本身是中性的,用在调试、安全审计、数据合规方向上很有价值,但越过了授权的边界就是另一个性质的问题了。
