某TV抓包和jce响应解析
免责声明
1、本贴仅作为技术讨论,本人不会利用以下技术盈利、或从事任何侵害该网站的行为。
2、如觉得此贴不妥,请联系本人将第一时间删除。
样本地址
aHR0cHM6Ly90di5xcS5jb20v
前言
大概是今年上半年,某视频TV端的软件就开始抓不到包,旧版的仍然能抓到。
使用sunny,抓包的时候发现,相比旧版的,新版多了许多UDP连接。
参考之前很多的app,猜测这是quic协议,根据**的习惯,找了找开源的项目。
发去年年底开源了一个叫做tquic项目
看样子多半就是quic协议了,毕竟它就喜欢这样干,参考trpc,tars。
既然已经猜到是quic,复盘友商成功案例,迭代旧有传统打法。
一、禁用UDP抓包
毕竟quic还是相对比较新的东西,大部分软件为了兼容旧版本,同时支持多种协议,毕竟TV很多还是安卓5。
参考下面用抓包软件来禁用udp。
二、hook请求信息
参考https://github.com/TencentCloud/libtquic-sdk/blob/main/src/android/TnetQuicRequest.java里面代码,
hook nativeSendRequest 看堆栈一步步往上找。
let TnetQuicRequest = Java.use("com.tencent.qqlive.modules.vb.tquic.impl.TnetQuicRequest");
TnetQuicRequest["sendRequest"].implementation = function (bArr, i10, z10) {
console.log(`TnetQuicRequest.sendRequest is called: bArr=${bArr}, i10=${i10}, z10=${z10}`);
let result = this["sendRequest"](bArr, i10, z10);
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
console.log(`TnetQuicRequest.sendRequest result=${result}`);
return result;
};
下面图中performRequest传入了request, headers和请求头,同样hook一下
let TQuicStack = Java.use("com.ktcp.tencent.volley.toolbox.TQuicStack");
TQuicStack["performRequest"].implementation = function (request, map) {
var url = request.getUrl();
var Headers = request.getHeaders();
var keys = Headers.keySet().toArray();
console.log("Request URL: " + url);
for (var i = 0; i < keys.length; i++) {
console.log("Request Header: " + keys[i] + " : " + Headers.get(keys[i]));
}
keys=map.keySet().toArray();
for (var i = 0; i < keys.length; i++) {
console.log("Request Header: " + keys[i] + " : " + map.get(keys[i]));
}
var Cookie=request.getCookie();
console.log("Request Cookie: " + Cookie);
var methodid = request.getMethod();
var method = function (id) {
switch (id) {
case 0:
return "GET";
case 1:
return "POST";
case 2:
return "PUT";
case 3:
return "DELETE";
default:
return "UNKNOWN";
}
}(methodid);
console.log("Request Method: " + method);
if (method === "POST") {
var body = request.getBody();
var bodystr = Java.use("java.lang.String").$new(body);
console.log("Request Body: " + bodystr);
}
let result = this["performRequest"](request, map);
return result;
};
这里只hook了请求,响应是一样的就在下面几个函数。
三、hook降级
根据上面hook nativeSendRequest打印的堆栈信息,可以判断出app使用的是OKHttp,添加了拦截器。
参考https://juejin.cn/post/7308909489154015247,利用addInterceptor函数嵌入了tquic。
同样往上找堆栈,发现一个可疑函数selectHttpStack和chooseProtocol,尝试hook,同时抓包。
chooseProtocol返回2的时候就是tquic,无法抓包,为1时能够抓到。
直接hook返回1就行了
let MultiStackNetwork = Java.use("com.ktcp.tencent.volley.toolbox.MultiStackNetwork");
MultiStackNetwork["chooseProtocol"].implementation = function (request) {
return 1;
};
四、jce响应解析
在抓包过程中会发现,响应大多数是二进制,不是json。在请求参数里面有个jce,大部分修改为json能够返回json格式,下面图片中的接口,修改后仍然是二进制的。
4.1 jce是什么
jce 是一个类似于 pb 的二进制编码协议,最早是在腾讯 rpc 框架 tars 设计的,不过现在都统一为tars。
类似.proto文件,.jce/.tars文件是腾讯tars rpc框架的基础(现在又出一个trpc pb协议)。
目前 tars 支持 C++,Java,PHP,Nodejs,Go ;所以解析jce最简单方法还是直接把app里面代码扣出来调用。
4.2 .jce/.tars基本格式
参考官网文档
4.3 从java代码中还原.jce/.tars文件
建议自己先多看看官方文档,文档里面有点就不说了。
根据官方提供的示例
StatF.tars
StatMicMsgHead.java
两个文件对照看可以找到对应关系
java |
.jce/.tars |
true |
require |
false |
optional |
boolean |
bool |
byte |
byte |
String |
string |
int |
int |
long |
long |
float |
float |
double |
double |
short |
short |
[] |
vector< > |
Map |
map< , > |
顺序在readFrom和TarsStructProperty里面都有,但是在app里面只能通过readFrom里面的参数来了。
参考上面的关系很容易手动还原,不多写了。
4.4 快速定位java代码
根据官方文件在java中函数readString,能够读取字符串,直接hook
let JceInputStream = Java.use("com.qq.taf.jce.JceInputStream");
JceInputStream["readString"].implementation = function (i10, z10) {
console.log(`JceInputStream.readString is called: i10=${i10}, z10=${z10}`);
let result = this["readString"](i10, z10);
console.log(`JceInputStream.readString result=${result}`);
return result;
};
根据打印堆栈信息判断出所需的代码在SpecPageContentResp
然后还原出tars文件,附件里面提供我还原后的文件,仅供参考,需要注意的是module qqlivetv,module 最好写一样的。
4.5 利用go解析响应
根据官方文件配置tarsgo
go install github.com/TarsCloud/TarsGo/tars/tools/tarsgo@latest
go install github.com/TarsCloud/TarsGo/tars/tools/tars2go@latest
创建一个新的.tars文件,包含还原出来的全部.tars文件
#include "tras/ottProto/OttHead.tars"
.................
#include "tras/tvVideoSuper/SingleLinePlayerViewInfo.tars"
然后生成对应go代码,这里建议先看看官方文档,生成对应的服务,先了解一下rpc整个流程。
tars2go all.tars
根据官方提供的文件server.go,同样是在ReadFrom函数中解析
// Invoke process request and send response
func (s *serverProtocol) Invoke(ctx context.Context, reqBytes []byte) []byte {
req := &requestf.RequestPacket{}
rsp := &requestf.ResponsePacket{}
is := codec.NewReader(reqBytes[4:])
if err := req.ReadFrom(is); err != nil {
rsp.IRet = 1
rsp.SResultDesc = "decode request package error"
} else {
rsp.IVersion = req.IVersion
rsp.CPacketType = req.CPacketType
rsp.IRequestId = req.IRequestId
if req.SFuncName != "tars_ping" {
rspData := s.s.OnConnect(ctx, tools.Int8ToByte(req.SBuffer))
rsp.SBuffer = tools.ByteToInt8(rspData)
}
}
return response2Bytes(rsp)
}
很容易写出我们需要的代码
u := "https://tv.t002.ottcn.com/i-tvbin/qtv_video/home_page/hp_real_waterfall"
headers := map[string]string{
**********
}
cookies := map[string]string{
**********
}
params := map[string]string{
"format": "jce",
"req_type": "spec",
"area_id": "zq_80485272",
"Q-UA": "**********",
}
req := url.NewRequest()
req.Params = url.ParseParams(params)
req.Headers = url.ParseHeaders(headers)
req.Cookies = url.ParseCookies(u, cookies)
r, err := requests.Get(u, req)
if err != nil {
fmt.Println(err)
}
var SpecPageContentResp qqlivetv.SpecPageContentResp
reader := codec.NewReader(r.Content)
err = SpecPageContentResp.ReadFrom(reader)
if err != nil {
fmt.Println(err)
}
CurPageContents := SpecPageContentResp.Data.PageContent.CurPageContent
var items []qqlivetv.ItemInfo
for _, v := range CurPageContents {
for _, group := range v.Groups {
for _, line := range group.Lines {
for _, component := range line.Components {
for _, grid := range component.Grids {
for _, item := range grid.Items {
items = append(items, item)
}
}
}
}
}
}
for _, item := range items {
voiceTitle := item.ExtraData["voiceTitle"].StrVal
s := item.Action.PageSnapshot.Url
if s != "" {
var VideoInfo VideoInfo
r, err := requests.Get(s, nil)
if err != nil {
fmt.Println(err)
}
content := r.Content
err = json.Unmarshal(content, &VideoInfo)
if err != nil {
fmt.Println(err)
}
for _, v := range VideoInfo.VideoList.Videos {
fmt.Println(v.Vid, v.Title)
}
continue
}
fmt.Println(voiceTitle)
}
五、后记
总的来说都比较简单,jce和proto类似,主要是还原.tars文件。如果app没加固可以尝试直接反编译导出.java文件,然后递归遍历,生成全部或者需要的.tars,基本jce java文件都在一个目录下面。
附件
只有本地本文档,和还原出来的tras文件
https://nicaicai.lanzouo.com/i1PFO2guyx2d