逆向某短视频App搜索协议:破解加密通信,还原真实数据!
版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/
搜索接口抓包分析
抓包数据如下:
Request 和 Response 的 Body 被加密无法直接查看。
请求地址:
https://***************************/aweme/v2/search/general/single/?klink_egdi=********************************************************&iid=****************&device_id=****************&ac=****&channel=*******************&aid=****&app_name=*****&version_code=******&version_name=******&device_platform=*******&os=*******&ssmix=*&device_type=*****&device_brand=******&language=**&os_api=**&os_version=**&manifest_version_code=******&resolution=*********&dpi=***&update_version_code=********&_rticket=*************&package=************************&first_launch_timestamp=**********&last_deeplink_update_version_code=*&cpu_support64=****&host_abi=*********&is_guest_mode=*&app_type=******&minor_status=*&appTheme=*****&is_preinstall=*&need_personal_recommend=*&is_android_pad=*&is_android_fold=*&ts=**********&cdid=************************************
请求头:
:method: POST
:authority: ***************************
:scheme: https
:path: /aweme/v2/search/general/single/?klink_egdi=********************************************************&iid=****************&device_id=****************&ac=****&channel=*******************&aid=****&app_name=*****&version_code=******&version_name=******&device_platform=*******&os=*******&ssmix=*&device_type=*****&device_brand=******&language=**&os_api=**&os_version=**&manifest_version_code=******&resolution=*********&dpi=***&update_version_code=********&_rticket=*************&package=************************&first_launch_timestamp=**********&last_deeplink_update_version_code=*&cpu_support64=****&host_abi=*********&is_guest_mode=*&app_type=******&minor_status=*&appTheme=*****&is_preinstall=*&need_personal_recommend=*&is_android_pad=*&is_android_fold=*&ts=**********&cdid=************************************
content-length: ****
cookie: ****************************************************
cookie: ************************************************************
cookie: ******************
cookie: ********************
cookie: ************************************************************************************************************************************************************************
cookie: ***************************
cookie: ************************************************
x-tt-dt: ******************************************************************************************************************************************************
activity_now_client: *************
compressed-bcm-chain: ******************************************************************************************************************************************************************************************************************************************************************************
x-ss-req-ticket: *************
sdk-version: *
passport-sdk-version: *****
x-vc-bdturing-sdk-version: ********
content-type: ************************************************
x-ss-stub: ******************************
x-bd-content-encoding: ****
x-tt-store-region: *****
x-tt-store-region-src: ***
x-tt-request-tag: ********
x-ss-dp: ****
x-tt-trace-id: *******************************************************
user-agent: ********************************************************************************************************************************************************************
ttzip-version: **********
ttzip-tlb: *
accept-encoding: ************************
x-argus: ********
x-gorgon: ****************************************************
x-helios: ************************************************
x-khronos: **********
x-ladon: ********
x-medusa: ************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
x-soter: ************************************************************************************************************************
(�/�`�=�{
curl 请求如下:
curl -H "Host: ***************************" -H "Cookie: *********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************" -H "x-tt-dt: ******************************************************************************************************************************************************" -H "activity_now_client: *************" -H "compressed-bcm-chain: ******************************************************************************************************************************************************************************************************************************************************************************" -H "x-ss-req-ticket: *************" -H "sdk-version: *" -H "passport-sdk-version: *****" -H "x-vc-bdturing-sdk-version: ********" -H "content-type: ************************************************" -H "x-ss-stub: ******************************" -H "x-bd-content-encoding: ****" -H "x-tt-store-region: *****" -H "x-tt-store-region-src: ***" -H "x-tt-request-tag: ********" -H "x-ss-dp: ****" -H "x-tt-trace-id: *******************************************************" -H "user-agent: ********************************************************************************************************************************************************************" -H "ttzip-version: **********" -H "ttzip-tlb: *" -H "x-argus: ********" -H "x-gorgon: ****************************************************" -H "x-helios: ************************************************" -H "x-khronos: **********" -H "x-ladon: ********" -H "x-medusa: ************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************" -H "x-soter: ************************************************************************************************************************" --data-binary "(�/�`�=�{
Response:
:status: ***
server: Tengine
content-type: application/json; charset=utf-8
date: *****************************
vary: Accept-Encoding
x-tt-logid: **********************************
status_code: 0
bd-tt-error-code: 0
tt_stable: 1
strict-transport-security: ***********************************
tt-idc-switch: ********************
server-timing: ***************
x-tt-trace-host: ************************************************************************************************************************************************************************************************
x-tt-trace-tag: *****************************
x-tt-trace-id: *******************************************************
content-encoding: ttzip
ttzip-version: **********
server-timing: **********************************************
via: **********************
timing-allow-origin: *
eagleid: **************************
(�/��b� i/
相关文章:
Dex 脱壳与 TTNet 源码分析
使用 frida_dex_dump 脱壳 dex ,再用 dex2jar 把 dex 批量转换成 jar。
TTNet 是****基于 Retrofit 深度改造的闭源网络框架,集成请求加密、拦截器链路、性能埋点和客户端密钥机制,广泛用于其内部 App 的网络通信。
使用 find_in_jars.py 查找 ttnet 包下的类:
(anti-app) PS D:\Python\anti-app\dex2jar> python find_in_jars.py "D:\Python\anti-app\app\douyin\dump_dex\jar" "com.*********.ttnet"
[+] Searching for class prefix: com/*********/ttnet
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes2.jar → com/*********/ttnet/TTNetInit$ENV.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes23.jar → com/*********/ttnet/debug/DebugSetting.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/http/HttpRequestInfo.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/http/RequestContext.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/HttpClient.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/ITTNetDepend.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/TTALog.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/TTMultiNetwork.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/TTNetInit.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/clientkey/ClientKeyManager.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/config/AppConfig.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/config/TTHttpCallThrottleControl$DelayMode.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/config/TTHttpCallThrottleControl.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/cronet/AbsCronetDependAdapter.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/diagnosis/TTNetDiagnosisService.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/priority/TTHttpCallPriorityControl$ModeType.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/priority/TTHttpCallPriorityControl.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/retrofit/SsInterceptor.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/retrofit/SsRetrofitClient.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/throttle/TTNetThrottle.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/tnc/TNCManager$TNCUpdateSource.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/tnc/TNCManager.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/utils/RetrofitUtils$CompressType.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/*********/ttnet/utils/RetrofitUtils.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes47.jar → com/*********/ttnet/diagnosis/IDiagnosisRequest.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes47.jar → com/*********/ttnet/diagnosis/IDiagnosisCallback.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes47.jar → com/*********/ttnet/diagnosis/TTGameDiagnosisService.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes49.jar → com/*********/ttnet/http/IRequestHolder.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes7.jar → com/*********/ttnet/INetworkApi.class
[+] Total 29 match(es) found.
找到 TTNet 的拦截器:com/*********/ttnet/retrofit/SsInterceptor
jar 文件检索工具:打造自己的 Jar 文件分析工具:类名匹配 + 二进制搜索 + 日志输出全搞定
SsInterceptor 方法调用分析
使用 frida hook 一下 SsInterceptor 中的所有方法
/**
* Hook 指定类的所有方法(每个方法所有重载)
* @param {string} className - Java 类的完整名
*/
function hook_all_methods(className) {
Java.perform(function () {
var clazz = Java.use(className);
var methods = clazz.class.getDeclaredMethods(); // 反射获取所有声明的方法
var hooked = new Set(); // 用于避免重复 hook 相同方法名(因为多重载)
methods.forEach(function (m) {
var methodName = m.getName();
// 如果这个方法已经 Hook 过,就跳过
if (hooked.has(methodName)) return;
hooked.add(methodName);
try {
hook_method(className, methodName);
} catch (e) {
console.error("❌ Failed to hook " + methodName + ": " + e);
}
});
});
}
setImmediate(function () {
hook_all_methods("com.*********.ttnet.retrofit.SsInterceptor");
});
// frida -H 127.0.0.1:1234 -F -l hook_class_methods.js
日志输出如下:
================= HOOK START =================
🎯 Class: com.*********.ttnet.retrofit.SsInterceptor
🔧 Method: tryFilterDupQuery
📥 Arguments:
[0]: https://*************/aweme/v1/search/middle_page_tabs/?need_personal_recommend=*&is_login=*&is_vcd=*&request_tag_from=**&klink_egdi=********************************************************&iid=****************&device_id=****************&ac=****&channel=*******************&aid=****&app_name=*****&version_code=******&version_name=******&device_platform=*******&os=*******&ssmix=*&device_type=*****&device_brand=******&language=**&os_api=**&os_version=**&manifest_version_code=******&resolution=*********&dpi=***&update_version_code=********&_rticket=*************&package=************************&first_launch_timestamp=**********&last_deeplink_update_version_code=*&cpu_support64=****&host_abi=*********&is_guest_mode=*&app_type=******&minor_status=*&appTheme=*****&is_preinstall=*&is_android_pad=*&is_android_fold=*&ts=**********&cdid=************************************
📤 Return value: https://*************/aweme/v1/search/middle_page_tabs/?need_personal_recommend=*&is_login=*&is_vcd=*&request_tag_from=**&klink_egdi=********************************************************&iid=****************&device_id=****************&ac=****&channel=*******************&aid=****&app_name=*****&version_code=******&version_name=******&device_platform=*******&os=*******&ssmix=*&device_type=*****&device_brand=******&language=**&os_api=**&os_version=**&manifest_version_code=******&resolution=*********&dpi=***&update_version_code=********&_rticket=*************&package=************************&first_launch_timestamp=**********&last_deeplink_update_version_code=*&cpu_support64=****&host_abi=*********&is_guest_mode=*&app_type=******&minor_status=*&appTheme=*****&is_preinstall=*&is_android_pad=*&is_android_fold=*&ts=**********&cdid=************************************
================== HOOK END ==================
================= HOOK START =================
🎯 Class: com.*********.ttnet.retrofit.SsInterceptor
🔧 Method: intercept
📥 Arguments:
[0]: com.*********.retrofit2.client.Request@1d768f
📤 Return value: com.*********.retrofit2.client.Request@c084623
================== HOOK END ==================
================= HOOK START =================
🎯 Class: com.*********.ttnet.retrofit.SsInterceptor
🔧 Method: intercept
📥 Arguments:
[0]: com.*********.retrofit2.client.Request@c084623
[1]: com.*********.retrofit2.SsResponse@2049c2f
📤 Return value: undefined
================== HOOK END ==================
主要调用了SsInterceptor 的下面 3 个方法:
tryFilterDupQuery:去除重复 query 参数,构造新的 URL
intercept(Request request) 是 “请求拦截器” 方法,用于修改或替换请求内容,添加 headers、加密参数、签名等
intercept(Request, SsResponse) 是 “响应拦截器”,用于观察或处理网络响应(如打点统计、异常分析)。
SsInterceptor、BaseSsInterceptor 与 Interceptor
SsInterceptor、BaseSsInterceptor 与 Interceptor 之间的关系大概如下:
Interceptor (接口)
↑
BaseSsInterceptor(抽象类/实现类,实现了一些公共逻辑)
↑
SsInterceptor(实现类,实现实际请求拦截与处理)
SsInterceptor 代码分析
package com.*********.ttnet.retrofit;
import O.O;
import X.C1465814hj;
import X.C1477714je;
import X.C1478114ji;
import X.C16130FjH;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Pair;
import com.GlobalProxyLancet;
import com.*********.common.profilesdk.ProfileManager;
import com.*********.common.utility.StringUtils;
import com.*********.frameworks.baselib.network.http.retrofit.BaseSsInterceptor;
import com.*********.frameworks.baselib.network.http.util.UrlBuilder;
import com.*********.frameworks.baselib.network.http.util.UrlUtils;
import com.*********.frameworks.core.encrypt.RequestEncryptUtils;
import com.*********.qss.common.catcher.CatcherTrace;
import com.*********.retrofit2.RetrofitMetrics;
import com.*********.retrofit2.client.Header;
import com.*********.retrofit2.client.Request;
import com.*********.ttnet.clientkey.ClientKeyManager;
import com.*********.ttnet.http.HttpRequestInfo;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
// SsInterceptor,用于增强网络请求,包括 query 去重、加密处理、打点统计等
public final class SsInterceptor extends BaseSsInterceptor {
// 是否启用 query 加密(控制开关)
public static volatile boolean sEncryptQueryEnabled;
// 是否已注入 HttpRequestInfo,仅执行一次
public static volatile boolean sHttpInfoInjectedByInterceptor;
// 设置是否开启 query 加密功能
public static void EnableEncryptQuery(boolean z) {
sEncryptQueryEnabled = z;
}
// 去除重复 query 参数,构造新的 URL
private String tryFilterDupQuery(String str) {
List list;
List list2;
if (StringUtils.isEmpty(str)) {
return str;
}
try {
LinkedHashMap linkedHashMap = new LinkedHashMap();
Pair<String, String> parseUrlWithValueList = UrlUtils.parseUrlWithValueList(str, linkedHashMap);
if (parseUrlWithValueList == null) {
return str;
}
// 去重 query
if (!linkedHashMap.isEmpty()) {
for (Map.Entry entry : linkedHashMap.entrySet()) {
if (entry != null && (list2 = (List) entry.getValue()) != null && !list2.isEmpty()) {
LinkedHashSet linkedHashSet = new LinkedHashSet();
Iterator it = list2.iterator();
while (it.hasNext()) {
String str2 = (String) it.next();
if (linkedHashSet.contains(str2)) {
it.remove();
} else {
linkedHashSet.add(str2);
}
}
}
}
}
UrlBuilder urlBuilder = new UrlBuilder(O.C((String) parseUrlWithValueList.first, (String) parseUrlWithValueList.second));
if (!linkedHashMap.isEmpty()) {
for (Map.Entry entry2 : linkedHashMap.entrySet()) {
if (entry2 != null && entry2.getKey() != null && (list = (List) entry2.getValue()) != null && !list.isEmpty()) {
Iterator it2 = list.iterator();
while (it2.hasNext()) {
urlBuilder.addParam((String) entry2.getKey(), (String) it2.next());
}
}
}
}
return urlBuilder.build();
} catch (Throwable th) {
// 异常上报并打印堆栈
CatcherTrace.reportThrowable(th, "c.b.ttn.retrofit.SsInterceptor", "tryFilterDupQuery", "java/lang/Throwable", ProfileManager.VERSION);
GlobalProxyLancet.com_ss_android_ugc_aweme_lancet_ThrowableLancet_thrPrintStackTrace(th);
return str;
}
}
// 拦截请求:对 Request 做加工处理
@Override
public Request intercept(Request request) {
Request intercept = super.intercept(request); // 先调用父类逻辑
if (intercept == null) {
return null;
}
// 只注入一次
if (!sHttpInfoInjectedByInterceptor) {
HttpRequestInfo.injectCreate();
sHttpInfoInjectedByInterceptor = true;
}
long start = SystemClock.uptimeMillis();
Request.Builder newBuilder = intercept.newBuilder();
// 去重 query
String filteredUrl = tryFilterDupQuery(intercept.getUrl());
newBuilder.url(filteredUrl);
if (intercept.getMetrics() != null) {
intercept.getMetrics().filterDupQueryDuration = SystemClock.uptimeMillis() - start;
}
long encryptStart = SystemClock.uptimeMillis();
ArrayList<Header> headers = new ArrayList<>();
if (intercept.getHeaders() != null) {
headers.addAll(intercept.getHeaders());
}
// 是否启用 query 加密
if (sEncryptQueryEnabled) {
try {
LinkedList<Pair<String, String>> encryptedHeaders = new LinkedList<>();
String encryptedUrl = RequestEncryptUtils.tryEncryptRequest(filteredUrl, encryptedHeaders);
if (encryptedUrl != null) {
newBuilder.url(encryptedUrl);
}
for (Pair pair : encryptedHeaders) {
if (pair != null) {
headers.add(new Header((String) pair.first, (String) pair.second));
}
}
} catch (Throwable th) {
CatcherTrace.reportThrowable(th, "c.b.ttn.retrofit.SsInterceptor", "int", "java/lang/Throwable", ProfileManager.VERSION);
GlobalProxyLancet.com_ss_android_ugc_aweme_lancet_ThrowableLancet_thrPrintStackTrace(th);
}
}
if (intercept.getMetrics() != null) {
intercept.getMetrics().encryptRequestDuration = SystemClock.uptimeMillis() - encryptStart;
}
// 添加 X-SS-REQ-TICKET 请求头
long ticketStart = SystemClock.uptimeMillis();
String ticket = null;
try {
if (C1478114ji.LIZIZ) {
if (C16130FjH.LIZIZ(filteredUrl).getHost().endsWith(C1477714je.LIZ())) {
ticket = String.valueOf(System.currentTimeMillis());
}
}
if (!StringUtils.isEmpty(ticket)) {
headers.add(new Header("X-SS-REQ-TICKET", ticket));
}
} catch (Throwable th3) {
CatcherTrace.reportThrowable(th3, "c.b.ttn.retrofit.SsInterceptor", "int", "java/lang/Throwable", ProfileManager.VERSION);
GlobalProxyLancet.com_ss_android_ugc_aweme_lancet_ThrowableLancet_thrPrintStackTrace(th3);
}
if (intercept.getMetrics() != null) {
intercept.getMetrics().genReqTicketDuration = SystemClock.uptimeMillis() - ticketStart;
}
// 添加 X-TT-VERIFY-ID 请求头
long verifyStart = SystemClock.uptimeMillis();
if (C1465814hj.LIZ) {
try {
String verifyId = C1465814hj.LIZ(C16130FjH.LIZIZ(filteredUrl));
if (!TextUtils.isEmpty(verifyId)) {
headers.add(new Header("X-TT-VERIFY-ID", verifyId));
}
} catch (Throwable th4) {
CatcherTrace.reportThrowable(th4, "c.b.ttn.retrofit.SsInterceptor", "preProcessingImpl", "java/lang/Throwable", ProfileManager.VERSION);
GlobalProxyLancet.com_ss_android_ugc_aweme_lancet_ThrowableLancet_thrPrintStackTrace(th4);
}
}
if (intercept.getMetrics() != null) {
intercept.getMetrics().preCdnCacheVerifyDuration = SystemClock.uptimeMillis() - verifyStart;
}
// 添加 x-bd-client-key 与 x-bd-kmsv
ClientKeyManager clientKeyManager = ClientKeyManager.LJFF();
RetrofitMetrics metrics = intercept.getMetrics();
if (ClientKeyManager.LJIIIIZZ != null && ClientKeyManager.LJIIIZ &&
!TextUtils.isEmpty(clientKeyManager.LIZJ) &&
!TextUtils.isEmpty(clientKeyManager.LJFF)) {
long keyStart = SystemClock.uptimeMillis();
headers.add(new Header("x-bd-client-key", clientKeyManager.LIZJ));
headers.add(new Header("x-bd-kmsv", clientKeyManager.LJFF));
if (metrics != null) {
metrics.addClientKeyDuration = SystemClock.uptimeMillis() - keyStart;
metrics.addClientKeySuccess = true;
}
}
newBuilder.headers(headers);
return newBuilder.build();
}
// 响应拦截方法
@Override
public void intercept(Request request, com.*********.retrofit2.SsResponse response) {
...
}
}
所以,通过 hook intercept(Request, SsResponse) 方法就可以拿到请求和响应的数据。
Request
Request 源码如下:
package com.*********.retrofit2.client;
import com.*********.common.profilesdk.ProfileManager;
import com.*********.memoryx.StringBuilderCache;
import com.*********.qss.common.catcher.CatcherTrace;
import com.*********.retrofit2.OptConfig;
import com.*********.retrofit2.RetrofitMetrics;
import com.*********.retrofit2.Utils;
import com.*********.retrofit2.mime.FormUrlEncodedTypedOutput;
import com.*********.retrofit2.mime.TypedOutput;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import okhttp3.RequestBody;
/* loaded from: base.apk_classes37.jar:com/*********/retrofit2/client/Request.class */
public final class Request {
public final boolean addCommonParam;
public final TypedOutput body;
public final int commonParamLevel;
public Object extraInfo;
public final List<Header> headers;
public boolean isBodyEncryptEnabled;
public boolean isQueryEncryptEnabled;
public final int maxLength;
public final String method;
public RetrofitMetrics metrics;
public final int priorityLevel;
public int queryFilterPriority;
public final RequestBody requestBody;
public final int requestPriorityLevel;
public final boolean responseStreaming;
public final String serviceType;
public final Map<Class<?>, Object> tags;
public URI uri;
public final String url;
...
}
SsResponse
Response 源码如下:
package com.*********.retrofit2;
import com.*********.retrofit2.client.Header;
import com.*********.retrofit2.client.Response;
import com.*********.retrofit2.mime.TypedInput;
import java.util.List;
/* loaded from: base.apk_classes28.jar:com/*********/retrofit2/SsResponse.class */
public final class SsResponse<T> {
public final T body;
public final TypedInput errorBody;
public final Response rawResponse;
public RetrofitMetrics retrofitMetrics;
...
}
smali 层深入分析
**的 java 层代码量实在太大了,用 jadx 打开内存直接爆了。
使用 dex2smali 把 jar 批量转换为 smali
把 smali 文件夹导入 Sublime Text,全局查找(Ctrl + Shift + F)“com/google/gson/Gson” 。
找到数据转换工具类 GsonUtils
其中 LIZJ 方法用于把 Object 转换为 String
使用 Gson 把 body 转换成 String
封装一个 toJson 方法使用 GsonUtils 把 Reqeust Body
public final TypedOutput body;
和 Response Body
public final T body;
转换成 String
代码实现如下:
function toJson(obj) {
try {
const GsonUtils = Java.use("com.*********.helios.sdk.utils.GsonUtils");
// 直接调用 LIZJ 静态方法
const jsonString = GsonUtils.LIZJ(obj);
return jsonString ? jsonString.toString() : "";
} catch (err) {
console.error("Error calling GsonUtils.LIZJ:", err);
return "";
}
}
使用 Frida 实时截取请求响应数据
打印 Request
封装一个 printRequest 方法用于打印 Request:
function printRequestContext(context) {
if (!context) return;
const RequestContext = Java.use("com.*********.ttnet.http.RequestContext");
const BaseRequestContext = Java.use("com.*********.frameworks.baselib.network.http.BaseRequestContext");
const ctx = Java.cast(context, RequestContext);
const sb = [];
// 🔷 RequestContext 自身字段
sb.push("🧩 RequestContext:");
sb.push(` - body_is_json: ${ctx.body_is_json.value}`);
sb.push(` - cdn_request_num: ${ctx.cdn_request_num.value}`);
sb.push(` - decode_time: ${ctx.decode_time.value}`);
sb.push(` - force_no_https: ${ctx.force_no_https.value}`);
sb.push(` - https_fail_times: ${ctx.https_fail_times.value}`);
sb.push(` - https_to_http: ${ctx.https_to_http.value}`);
sb.push(` - local_sign: ${ctx.local_sign.value}`);
sb.push(` - ss_sign: ${ctx.ss_sign.value}`);
sb.push(` - using_https: ${ctx.using_https.value}`);
// headers 是 JSONObject,需要转换成字符串打印
const headers = ctx.headers.value;
if (headers !== null) {
sb.push(` - headers: ${headers.toString()}`);
} else {
sb.push(` - headers: null`);
}
// 🔶 BaseRequestContext 父类字段
const baseCtx = Java.cast(context, BaseRequestContext);
sb.push("🔹 BaseRequestContext:");
const tryGet = (fieldName) => {
try {
return baseCtx[fieldName].value;
} catch (e) {
return "<error>";
}
};
sb.push(` - authCredentials: ${tryGet('authCredentials')}`);
sb.push(` - byPassProxy: ${tryGet('byPassProxy')}`);
sb.push(` - bypassCookie: ${tryGet('bypassCookie')}`);
sb.push(` - enable_http_cache: ${tryGet('enable_http_cache')}`);
sb.push(` - force_handle_response: ${tryGet('force_handle_response')}`);
sb.push(` - force_use_okhttp: ${tryGet('force_use_okhttp')}`);
sb.push(` - ignoreCheckMinInputStreamBufferSize: ${tryGet('ignoreCheckMinInputStreamBufferSize')}`);
sb.push(` - input_stream_buffer_size: ${tryGet('input_stream_buffer_size')}`);
sb.push(` - isCustomizedCookie: ${tryGet('isCustomizedCookie')}`);
sb.push(` - okHttpRequestClientBuilderHook: ${tryGet('okHttpRequestClientBuilderHook')}`);
sb.push(` - output_stream_buffer_size: ${tryGet('output_stream_buffer_size')}`);
sb.push(` - protect_timeout: ${tryGet('protect_timeout')}`);
sb.push(` - read_error_response: ${tryGet('read_error_response')}`);
sb.push(` - remoteIp: ${tryGet('remoteIp')}`);
sb.push(` - request_flag: ${tryGet('request_flag')}`);
sb.push(` - request_type_flags: ${tryGet('request_type_flags')}`);
sb.push(` - rotationHostRetryInfoList: ${tryGet('rotationHostRetryInfoList')}`);
sb.push(` - socket_connect_timeout: ${tryGet('socket_connect_timeout')}`);
sb.push(` - socket_read_timeout: ${tryGet('socket_read_timeout')}`);
sb.push(` - socket_write_timeout: ${tryGet('socket_write_timeout')}`);
sb.push(` - status: ${tryGet('status')}`);
sb.push(` - streaming_force_return_response: ${tryGet('streaming_force_return_response')}`);
sb.push(` - throttle_net_speed: ${tryGet('throttle_net_speed')}`);
sb.push(` - timeout_connect: ${tryGet('timeout_connect')}`);
sb.push(` - timeout_read: ${tryGet('timeout_read')}`);
sb.push(` - timeout_write: ${tryGet('timeout_write')}`);
sb.push(` - bypass_network_status_check: ${tryGet('bypass_network_status_check')}`);
sb.push(` - followRedirectInternal: ${tryGet('followRedirectInternal')}`);
sb.push(` - is_need_monitor_in_cancel: ${tryGet('is_need_monitor_in_cancel')}`);
sb.push(` - extraInfo: ${tryGet('extraInfo')}`);
return sb.join("\n");
}
function printRequest(req) {
if (!req) return '⚠️ Request is null';
const Request = Java.use('com.*********.retrofit2.client.Request');
const Header = Java.use('com.*********.retrofit2.client.Header');
const realReq = Java.cast(req, Request);
const sb = [];
sb.push('=== 📦 Request Dump Start ===');
sb.push('➡️ Method: ' + realReq.getMethod());
sb.push('🌐 URL: ' + realReq.getUrl());
sb.push('📌 Path: ' + realReq.getPath());
sb.push('🔐 QueryEncrypt: ' + realReq.isQueryEncryptEnabled());
sb.push('🔐 BodyEncrypt: ' + realReq.isBodyEncryptEnabled());
sb.push('📊 PriorityLevel: ' + realReq.getPriorityLevel());
sb.push('⚙️ ServiceType: ' + realReq.getServiceType());
// 🧾 Headers
const headers = realReq.getHeaders();
if (headers && headers.size() > 0) {
sb.push('🧾 Headers:');
for (let i = 0; i < headers.size(); i++) {
const h = Java.cast(headers.get(i), Header);
sb.push(` - ${h.getName()}: ${h.getValue()}`);
}
}
// 📦 Body info
const body = realReq.getBody();
if (body) {
sb.push('📦 Body mimeType: ' + body.mimeType());
sb.push('📦 Body length: ' + body.length());
sb.push('📦 Body.toString(): ' + body.toString());
sb.push('📦 Body: ' + toJson(body));
}
// 🏷️ Tag
const tag = realReq.tag();
if (tag) {
sb.push('🏷️ Tag: ' + tag.toString());
}
// 📦 ExtraInfo 判断类型
const extraInfo = realReq.getExtraInfo();
if (extraInfo) {
const className = extraInfo.getClass().getName();
sb.push('📦 ExtraInfo class: ' + className);
if (extraInfo.getClass().getName().startsWith("com.*********.ttnet.http.RequestContext")) {
sb.push(printRequestContext(extraInfo));
} else {
sb.push('📦 ExtraInfo.toString(): ' + extraInfo.toString());
}
}
sb.push('=== ✅ Request Dump End ===');
return sb.join('\n');
}
打印 SsResponse
封装一个 printSsResponse 方法用于打印 SsResponse:
function printSsResponse(resp) {
if (!resp) return '⚠️ SsResponse is null';
const SsResponse = Java.use('com.*********.retrofit2.SsResponse');
// const Response = Java.use('com.*********.retrofit2.client.Response');
const Header = Java.use('com.*********.retrofit2.client.Header');
// const TypedInput = Java.use('com.*********.retrofit2.mime.TypedInput');
const ssResp = Java.cast(resp, SsResponse);
const rawResp = ssResp.raw(); // com.*********.retrofit2.client.Response
const sb = [];
sb.push('=== 📥 SsResponse Dump Start ===');
sb.push('✅ isSuccessful: ' + ssResp.isSuccessful());
sb.push('🔢 Code: ' + ssResp.code());
sb.push('📝 Message: ' + ssResp.message());
// Headers
const headers = ssResp.headers();
if (headers && headers.size() > 0) {
sb.push('🧾 Headers:');
for (let i = 0; i < headers.size(); i++) {
const h = Java.cast(headers.get(i), Header);
sb.push(` - ${h.getName()}: ${h.getValue()}`);
}
}
// Body
const body = ssResp.body();
if (body) {
sb.push('📦 Body.toString(): ' + body.toString());
sb.push('📦 Body: ' + toJson(body));
}
// Error body
const errorBody = ssResp.errorBody();
if (errorBody) {
sb.push('❗ ErrorBody mimeType: ' + errorBody.mimeType());
sb.push('❗ ErrorBody length: ' + errorBody.length());
}
// RetrofitMetrics
const metrics = ssResp.getRetrofitMetrics();
if (metrics) {
sb.push('📈 RetrofitMetrics: ' + metrics.toString());
}
sb.push('=== 📥 SsResponse Dump End ===');
// console.log(sb.join('\n'));
return sb.join('\n');
}
Hook intercept(Request, SsResponse)
使用 Frida Hook intercept(Request, SsResponse) 截取请求和响应数据。
ttnet.js
function hookSsInterceptor() {
***************************************************************************
// Hook目标类
*********************************************************
********************************************************************************************************************
*****************************************
**************
**************************************
*******************
***********************
****************************
**************
// 直接打印日志
// console.log(log);
// ✅ 将日志发送给 Python 端而不是 console.log
*******
*********************
*************
****
**********************************
***
}
setImmediate(() => {
Java.perform(function () {
hookSsInterceptor()
});
})
// frida -H 127.0.0.1:1234 -F -l ttnet.js
// frida -H 127.0.0.1:1234 -F -l ttnet.js -o log.txt
在 Python 端接收到数据并输出到 log.txt
ttnet.py
import time
import frida
LOG_FILE = "log.txt"
def on_message(message, data):
if message["type"] == "send":
tag = message["payload"].get("tag")
log = message["payload"].get("payload")
if tag == "ss_intercept":
with open(LOG_FILE, "w", encoding="utf-8") as f:
f.write(f"{log}\n\n")
elif message["type"] == "error":
print("❌ Frida script error:", message["stack"])
def main():
# USB链接
# device: frida.core.Device = frida.get_usb_device()
# 远程链接
device = frida.get_device_manager().add_remote_device("127.0.0.1:1234")
# 附加到当前前台应用
pid = device.get_frontmost_application().pid
session: frida.core.Session = device.attach(pid)
# 加载脚本
with open("ttnet.js", "r", encoding="utf-8") as f:
script = session.create_script(f.read())
script.on("message", on_message)
script.load()
print(f"✅ Script loaded and logging to {LOG_FILE}")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n👋 Exit by user.")
if __name__ == "__main__":
main()
测试
截取下来的数据如下,Reqeust Body 是经过编码加密的二进制数据。
加密字段的解码与数据还原
数据格式说明
{
"content": {
"buf": [40, -75, ...],
"count": 3509
},
"enableRecordFields": false,
"fields": {},
"mBodyMd5Stub": "27237ACA0C**********BEBD640337",
"mIsBodyEncrypted": false,
"mOriginBodySize": 13440,
"mType": "zstd"
}
content.buf 是一个 byte 数组,包含字节值(范围 -128 到 127),这是 Java 的 byte[] 通过 Gson 序列化后的表现形式。我们可以通过 Python 将它转换为字节数组后再解码成字符串。
该数据结构的实际类型是 FormUrlEncodedTypedOutput,Java 源码如下:
package com.*********.retrofit2.mime;
import com.*********.common.profilesdk.ProfileManager;
import com.*********.frameworks.encryptor.EncryptorUtil;
import com.*********.qss.common.catcher.CatcherTrace;
import com.*********.retrofit2.mime.TTRequestCompressManager;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import leakcanary.internal.LeakCanaryFileProvider;
/* loaded from: base.apk_classes37.jar:com/*********/retrofit2/mime/FormUrlEncodedTypedOutput.class */
public final class FormUrlEncodedTypedOutput extends AbsTypedOutput {
public ByteArrayOutputStream content;
public final boolean enableRecordFields;
public final Map<String, List<String>> fields;
public FormUrlEncodedTypedOutput() {
this.content = new ByteArrayOutputStream();
this.fields = new HashMap();
this.enableRecordFields = false;
}
public FormUrlEncodedTypedOutput(boolean z) {
this.content = new ByteArrayOutputStream();
this.fields = new HashMap();
this.enableRecordFields = z;
}
public void addField(String str, String str2) {
addField(str, true, str2, true);
}
public void addField(String str, boolean z, String str2, boolean z2) {
if (str == null) {
throw new NullPointerException(LeakCanaryFileProvider.ATTR_NAME);
}
if (str2 == null) {
throw new NullPointerException("value");
}
if (this.content.size() > 0) {
this.content.write(38);
}
if (this.enableRecordFields) {
if (this.fields.containsKey(str)) {
this.fields.get(str).add(str2);
} else {
ArrayList arrayList = new ArrayList();
arrayList.add(str2);
this.fields.put(str, arrayList);
}
}
String str3 = str;
if (z) {
try {
str3 = URLEncoder.encode(str, "UTF-8");
} catch (IOException e) {
CatcherTrace.reportThrowable(e, "c.b.ret.mim.FormUrlEncodedTypedOutput", "afie", "java/io/IOException", ProfileManager.VERSION);
throw new RuntimeException(e);
}
}
String str4 = str2;
if (z2) {
str4 = URLEncoder.encode(str2, "UTF-8");
}
this.content.write(str3.getBytes("UTF-8"));
this.content.write(61);
this.content.write(str4.getBytes("UTF-8"));
}
@Override // com.*********.retrofit2.mime.AbsTypedOutput
public TTRequestCompressManager.CompressData compressRequestBody(String str, String str2, boolean z) {
byte[] byteArray = this.content.toByteArray();
if (byteArray == null) {
return null;
}
TTRequestCompressManager.CompressData compressBody = TTRequestCompressManager.compressBody(byteArray, byteArray.length, str, str2, z);
if (compressBody != null && compressBody.data != null) {
this.mOriginBodySize = byteArray.length;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(compressBody.data.length);
this.content = byteArrayOutputStream;
byte[] bArr = compressBody.data;
byteArrayOutputStream.write(bArr, 0, bArr.length);
this.mType = compressBody.contentEncoding;
}
return compressBody;
}
public Map<String, List<String>> fields() {
return this.fields;
}
@Override // com.*********.retrofit2.mime.AbsTypedOutput
public String fileName() {
return null;
}
@Override // com.*********.retrofit2.mime.AbsTypedOutput
public byte[] getOriginBody() {
return TTRequestCompressManager.decompressDataByType(this.content.toByteArray(), this.mType, this.mOriginBodySize);
}
@Override // com.*********.retrofit2.mime.AbsTypedOutput
public boolean interceptRequestBody() {
byte[] LIZ;
byte[] byteArray = this.content.toByteArray();
if (byteArray == null || byteArray.length > 102400 || (LIZ = EncryptorUtil.LIZ(byteArray, byteArray.length)) == null) {
return false;
}
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(LIZ.length);
this.content = byteArrayOutputStream;
byteArrayOutputStream.write(LIZ, 0, LIZ.length);
this.mIsBodyEncrypted = true;
return true;
}
@Override // com.*********.retrofit2.mime.AbsTypedOutput
public long length() {
return this.content.size();
}
@Override // com.*********.retrofit2.mime.AbsTypedOutput
public String md5Stub() {
this.mBodyMd5Stub = DigestUtil.md5Hex(this.content.toByteArray());
return this.mBodyMd5Stub;
}
@Override // com.*********.retrofit2.mime.AbsTypedOutput
public String mimeType() {
return "application/x-www-form-urlencoded; charset=UTF-8";
}
@Override // com.*********.retrofit2.mime.AbsTypedOutput
public void writeTo(OutputStream outputStream) {
outputStream.write(this.content.toByteArray());
}
}
buf 解码
这是 Retrofit 发送的请求体中的内容,那么 buf 实际是压缩后的二进制数据(注意 “mType”: “zstd” 表示使用了 zstd 压缩),我们需要:
把 buf 还原为 Uint8Array 或 Buffer。
使用 zstd 解压。
然后解码为字符串(如 UTF-8)。
使用 URL 解码
zstd(Zstandard)是由 Facebook 开发的一种 无损压缩算法,设计目标是提供:
比 zlib 更好的压缩比
更快的压缩和解压速度
代码实现如下:
import zstandard
from urllib.parse import unquote
def decode_zstd_buf_to_text(buf):
"""
将 Retrofit 中 FormUrlEncodedTypedOutput 的 content.buf 解码为文本字符串
参数:
buf (List[int]): 表示 zstd 压缩后的字节数组(有符号 int)
返回:
str: 解压缩后的文本内容
"""
# 将 buf 转换为 bytes 类型(有符号 int8 转换为无符号)
*******************************************************
# 使用 zstandard 解压
************************************
*************************************************
# 解码为字符串(假设 utf-8 编码)
************************************
# URL 解码
********************************
***********************
buf 字段还原并重写
下面是一个完整的 Python 函数,可以:
读取 txt 文件内容
识别包含 buf: [数组] 的字段
解码 buf 中的 byte 数组并转换为字符串
替换原始 buf 字段为可读字符串
覆盖写回原始 txt 文件
完整代码实现如下:
import re
import zstandard
def decode_zstd_buf_to_text(buf):
"""
将 Retrofit 中 FormUrlEncodedTypedOutput 的 content.buf 解码为文本字符串
参数:
buf (List[int]): 表示 zstd 压缩后的字节数组(有符号 int)
返回:
str: 解压缩后的文本内容
"""
# 将 buf 转换为 bytes 类型(有符号 int8 转换为无符号)
*******************************************************
# 使用 zstandard 解压
************************************
*************************************************
# 解码为字符串(假设 utf-8 编码)
************************************
def decode_buf_array(match):
**************************
# 提取所有整数(支持负数)
*************************************************************************
# 解码
***********************************************
*****************************
def replace_buf_array_with_string(file_path: str, encoding='utf-8'):
"""
自动将文件中类似 "buf":[40, -75, 47, ...] 的内容转换为字符串并替换原 buf 值。
:param file_path: txt 文件路径
:param encoding: 读取与写入时的编码(默认 utf-8)
"""
***************************************************
*******************
# 正则匹配 buf: [xx, xx, ...] 的结构,支持跨行
***********************************
# 替换所有匹配的 buf 字段
**************************************************************************
# 写回原文件
***************************************************
*********************
*****************************
if __name__ == '__main__':
replace_buf_array_with_string('log1.txt')
成功还原 buf 字段数据。