吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 4262|回复: 52
上一主题 下一主题
收起左侧

[Web逆向] 某方数据平台的逆向分析-------学会逆向protobuf

  [复制链接]
跳转到指定楼层
楼主
prince_cool 发表于 2023-1-13 13:12 回帖奖励

某方数据平台的逆向分析-------学会protobuf

声明

本文章中所有内容仅供学习交流,相关链接做了脱敏处理,若有侵权,请联系我立即删除!

一、前期准备:

1.1 了解protobuf协议:

Protobuf(Protocol Buffer)是 Google 开发的一套数据存储传输协议,为二进制序列化格式,可用作 JSON 或 XML 等格式的更有效替代方案。开发人员可以在 .proto 文件中定义消息格式,并使用 protobuf 编译器(protoc)按他们选择的语言生成消息处理程序。Protobuf 编码是二进制的,与 json、xml 不同,它不是可读的,也不容易手动修改。Protobuf 能够把数据压缩得很小,所以传输数据就比 xml 和 json 快几倍,使用其有利于提高效率,但同时增加了分析或修改数据的难度。

序列化 (Serialization) 是指将对象转换为字节序列的过程,在序列化期间,对象将其当前状态写入到临时或持久性存储区,以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

protobuf知识补充见:https://www.52pojie.cn/thread-1735975-1-1.html

1.2 Protobuf 环境配置:

下载protoc文件:找对应版本下载

在这里插入图片描述

然后添加到环境变量,便于之后使用。

二、目标网站:

aHR0cHM6Ly9zLndhbmZhbmdkYXRhLmNvbS5jbi9wYXBlcj9xPSVFNyU4OCVBQyVFOCU5OSVBQg==

打开网站(以第2页为例),找到我们这次需要分析的包。

在这里插入图片描述

返回数据也是乱码:

在这里插入图片描述

经过查资料,观察返回的content-type为application/grpc-web+proto 是以protobuf数据结构传输的。

在这里插入图片描述

我们用fiddler抓包看看(黑色16进制数据是发送的数据,蓝色是请求头数据)

在这里插入图片描述

我们右键将黑色请求数据(第6位开始括)部分保存下来,为bin格式后面分析时候有用,为什么从第6位开始,后面会分析。

三、开始分析:

我们下个xhr断点看看,看看数据是怎么来的。

可以看到数据是以字节集的形式发出的,与fd抓包的16进制数据是一一对应的。

在这里插入图片描述

接下来,我们看看数据是怎么来的?跟e来到上一个栈,可以发现是a赋值给e的。

在这里插入图片描述

我们在e赋值的地方下断点,看看a怎么来的,重新调试一次,主要看a怎么构成的。

在这里插入图片描述

发现其实核心就是9681行到9690行赋值这一段。

for(n = (e = (0,n.a)(e.getRequestMessage())).length,
    f = [0, 0, 0, 0],
    a = new Uint8Array(5 + n),
    s = 3; 0 <= s; s--)
    f[s] = n % 256,
        n >>>= 8;

if (a.set(new Uint8Array(f), 1),
    a.set(e, 5),
    e = a,

我们一句句分析:

先for循环部分:

e = (0,n.a) (e.getRequestMessage())

在这里插入图片描述

得到的是一个unit8Array数组,刚刚好就是我们的数据从5开始往后数。

n = (e = (0,n.a)(e.getRequestMessage())).length

这里得到这个数组长度是26。

`f = [0, 0, 0, 0]``

这里类似设置了一个头,4位空数组

a = new Uint8Array(5 + n)

这里a的初始化,因为我们知道最后a是一个Uint8Array的数组,他的长度是5+n。

s = 3; 0 <= s; s--

这里可以确定整个循环的次数,4次。

因为js是以;为结束的,所以相当于下列语句循环了4次,那这步操作是干嘛的呢?

f[s] = n % 256,
n >>>= 8;

给f数组各位赋值的,刚刚好f数组就是4位。所以说先给f的第四位赋值相当于f[3], 再 n进行>>>=移位操作

得到

f[3] =26%256=26    26 >>>8=0 =>n=0
f[2] = 0%256 =0    0  >>>8=0 =>n=0
f[1] = 0%256 =0    0  >>>8=0 =>n=0
f[0] = 0%256 =0    0  >>>8=0 =>n=0

f=[0, 0, 0, 26]

后面就是给a赋值了:

a.set(new Uint8Array(f), 1)

a是数组,然后查mdn文档,可知set方法的作用:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/set

set() 方法用于从指定数组中读取值,并将其存储在类型化数组中。

有两个参数,typedarray,offset(可选,默认为 0)

typedarray对应  new Uint8Array(f)

offset  对应    1

相当于从下标1开始,给a数组插入值f。

所以得到

在这里插入图片描述

这里核心就是这个set方法,不懂查一下就理解了。

a.set(e, 5)

相当于从数组下标5开始把e的值赋值给a

在这里插入图片描述

ok,我们到这里已经把a怎么来的理解清楚了,也明白了前面5位是怎么来的。

但是,我们还是没接触到protobuf啊,只是知道了数据得到的大概过程。核心就在(0,n.a)(e.getRequestMessage())里面。

在这里插入图片描述

发现了我们的要讲的核心,protobuf。

看文章,看官方文档可以知道要构造这个protobuf格式数据,就是刚刚我们得到的那个数组,我们要先写proto文件,然后生成对应语言的proto版本,然后发包。

四、写proto文件(核心)

跟进n.a方法

在这里插入图片描述

发现了写protobuf数据格式的关键函数:serializeBinary

跟进去

在这里插入图片描述

关键点:

根据serializeBinaryToWriter关键函数,可以得到此结构分层,跟进去可以得到包含几个信息

在这里插入图片描述

write + (信息类型)函数,第一个参数代表是第几个。(如下图圈主部分)

在这里插入图片描述

看到writeMessage,后面有serializeBinaryToWriter关键函数,跟进去,就能再次得到此message的结构层次,一样分析,就能得出完整结构。

packedenum用repeated关键字表示。

一个个分析按顺序写proto文件

syntax = "proto3";

message SearchService {
    enum InterfaceType{
        A=0;
    }
    enum SearchScope{
        B = 0;
    }
    enum SearchFilter {
      C = 0;
   }

   enum Order{
        D=0;
    }

    message SearchSort{
        string Field =1;
        Order Order=2;
    }

    message SecondsList{
        string Field=1;
        string Value=2;
    }
    message Commonrequest{
        string SearchType=1;
        string SearchWord=2;
        SearchSort searchSort=3;
        repeated SecondsList secondslist=4;
        int32 CurrentPage=5;
        int32 PageSize=6;
        SearchScope searchscope=7;
        repeated SearchFilter searchFilter = 8;
        bool LanguageExpand=9;
        bool TopicExpand=10;

    }

    message SearchRequest {
        Commonrequest commonrequest = 1; // 任意变量名
        InterfaceType interfaceType = 2; // 任意变量名
    }
}

命令行生成可python操作的protobuf文件

protoc --python_out=. ./test.proto

数据分析:fd抓包下载bin,然后命令行 protoc --decode_raw < 1.bin执行,解析protobuf数据结构

在这里插入图片描述

然后对应上面的结构找到对应变量,用python赋值运行:

import test_pb2 as pb #导入包
SearchRequest= pb.SearchService.SearchRequest() #实例化对象
#按上面解析数据,按照对应的属性设置值
#字符串,数字型的都是直接赋值
SearchRequest.commonrequest.SearchType='paper' 
SearchRequest.commonrequest.SearchWord='爬虫'
SearchRequest.commonrequest.CurrentPage=2
SearchRequest.commonrequest.PageSize=20
#repeated修饰的messsage类型和enum类型,则需要稍微多几个步骤
SearchRequest.commonrequest.searchFilter.append(0)
SearchRequest.interfaceType=1
form_data = SearchRequest.SerializeToString()
print(form_data)
#保存数据玮bin文件供后续对比使用
with open('me.bin', mode="wb") as f:
    f.write(form_data)

注意:

字符串,数字型的都是直接赋值

repeated修饰的messsage类型和enum类型,则需要稍微多几个步骤

# 可重复message类型,需要调用一个add方法,然后将对应字段赋值
seconds = search_request.commonrequest.Second.add()
seconds.field = "Type"
seconds.value = '"Thesis"'
# 可重复enum枚举类型
search_request.commonrequest.searchFilter.append(0)

自己构造的和上面对比,发现完全一致,说明分析正确。

在这里插入图片描述

用python发包,根据上面的分析,发现需要补头5位

bytes_head = bytes([0, 0, 0, 0, len(form_data)])

完整代码(获得数据):

import requests
import test_pb2 as pb
SearchRequest= pb.SearchService.SearchRequest()
SearchRequest.commonrequest.SearchType='paper'
SearchRequest.commonrequest.SearchWord='爬虫'
SearchRequest.commonrequest.CurrentPage=2
SearchRequest.commonrequest.PageSize=20
SearchRequest.commonrequest.searchFilter.append(0)
SearchRequest.interfaceType=1
form_data = SearchRequest.SerializeToString()
print(form_data)
# with open('me1.bin', mode="wb") as f:
#     f.write(form_data)
# print(SearchRequest.SerializeToString().decode())
bytes_head = bytes([0, 0, 0, 0, len(form_data)])
print(bytes_head+form_data)

headers = {
    "Accept": "*/*",
    "Accept-Language": "zh-CN,zh;q=0.9,zh-TW;q=0.8",
    "Content-Type": "application/grpc-web+proto",
    "Origin": "https://*********",
    "Referer": "https://*********/paper",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
}
url = "https://*********/SearchService.SearchService/search"
response=requests.post(url,headers=headers,data=bytes_head+form_data)
print(response.content)

这篇文章最重要的是要学会怎么构造proto文件,请求头怎么一一对应,下一节,我们将对响应进行处理,找出相应的proto文件。

五、下部分前言

上一节,我们构造了请求的proto文件,并成功用python发包获得了数据,但是得到的数据和f12得到的数据是一样的乱码如下图:

在这里插入图片描述

其实这个也是protobuf格式,发过去的是protobuf格式,收到的也是protobuf格式,只是它是以二进制序列化格式传输的,所以我们看上去像乱码。

那么我们要怎么让它变好看呢?或者怎样好处理呢?

接下来会带来两种方法:①便捷但不太直观,②直观但有点复杂。

大家因人而异选择处理就好,喜欢用哪个就用哪个。

六、介绍处理的两种方法

1.方法一:直接使用第三方库

直接使用python应对protobuf的第三方库:blackboxprotobuf

​        安装命令:pip install blackboxprotobuf

​        调用核心函数 :blackboxprotobuf.decode_message(Byte类型数据)

​        进行解protobuf格式数据变成我们好理解,直观的数据。

​        返回值有两个:第一个是数据(和上一节用命令行解出来的那种格式类似,对应着proto文件的位置),第二个是对应位置的类型

这里我们传response.content进去就好了,但是你会发现报错了:

在这里插入图片描述

因为返回值我们还没分析,它发过来的有一部分是头信息,不在protobuf格式里面,所以解析会报错。
我先展示一下正确结果,给大家看看效果,第二种方法的时候,我会详细讲整个过程:

在这里插入图片描述

所以我们可以分析data的结构,找到我们需要的数据,然后取值。

取值就和取json数据一样:对象[键名] 这里就是 data['2'] ===>爬虫

取你想要的,当然要拿出来数据到json解析的网站解析一下,然后分析:

这里推荐网站:https://spidertools.cn/#/

在这里插入图片描述

你可以看到,虽然拿到了数据,只是位置序号加内容,我们其实要靠猜才能知道是什么。

这就是我所说的便捷但不直观。

当然,blackboxprotobuf库还有其他函数api接口,你们可以查阅其他文章了解使用。

2.方法二:写对应的响应的proto文件

写对应的响应的proto文件,和发包一样。当然可以和发包写在一起。

​        然后同样的用命令行编译成python版本的:

protoc --python_out=. ./test.proto

然后一样的创建实例对象,然后使用.ParseFromString(Byte类型数据)函数就能解析了

search_response = test_proto2_pb2.SearchService_SearchResponse()
search_response.ParseFromString(response.content[5:])
print(search_response)

这里用的是渔歌之前文章的例子做演示,我们看看效果:

在这里插入图片描述

是不是直观了很多,但是核心还是写proto文件,也是难点。然后分析为什么这里是取部分数据做解析。接下来,我们讲怎么构造响应的proto文件,然后为什么数据是从第6位(数组下标5)开始取。

我们f12开始分析,响应其实是最难调试的点,定位也很难,所以我们可以从请求的过程堆栈入手,说不定可以有所启发,发现可疑位置。

下xhr断点,跟着堆栈走,主要看app开头的js,因为chunk开头的是基本库,很少在里面做手脚,一般都是在自写的js里面做加密或其他操作。

在这里插入图片描述

可以发现h这里有点可疑,异步然后获得了值去.toObject,这个toObject就是proto文件转js的时候会产生的一个api函数接口,大家可以简单使用protoc去尝试转化成js看看。

(下载低于3.21.0 的protoc版本,因为原项目已将它独立出来,下载最新版本的protoc,运行js_out会缺少插件)

protoc --js_out=import_style=commonjs,binary:. 你的proto文件名.proto

然后下断点执行,你会看到下面数据如图:

在这里插入图片描述

发现和之前请求的是不是很像,其实这已经是解包好的了,所以应该在前面,但是异步我们不好跟了,或者我们在then那里下一个断点,然后单步跟,我就用另一种方式了,我们复制这一段,全局搜索一下:proto.SearchService.SearchResponse

会找到我这节要说的写响应,返回值proto文件的核心关键词:

deserializeBinary------deserializeBinaryFromReader重点核心

在这里插入图片描述

其实和请求包的关键词很像,就前面加了de,相当于解的意思。这里要多亏了志远大佬的指导,因为其实找响应的时候没一点头绪,然后很多文章都没讲清楚解包的关键词,异步也不太好跟,然后他稍微指点了一下,然后下断点就能定位了。

我们下一个断点看看,放开,重新点击:

在这里插入图片描述

我们成功来到这个位置,和fiddler数据对比一下:

在这里插入图片描述

发现是从第6位开始解包,所以就解决了上面我们的疑问,为什么从第6位开始取数据。

刚刚不是提到了两个关键词吗?

deserializeBinary------deserializeBinaryFromReader重点核心

在这里插入图片描述

和请求的一样理解,只是他现在变成了case语句来表示位置,read后面的类型来表示类型。

在这里插入图片描述

也是一层一层,慢慢的可以跟出来,当然,这个返回的数据量太大了,标号也特别的多,我们刚刚也看到了,所以有没有什么更好的方法得到proto文件呢?

那就是自写ast,然后用ast来处理这种switch语句。先安装babel解析库在当前目录下

npm install @babel/core --save-dev

我对ast还不是很熟悉,我就采用渔歌之前写好的(文末会附上链接),因为网站js也有小更新,之前的使用会报错。

按照文章,我们先把这一个js保存起来,我这里保存成1.js,运行

在这里插入图片描述

发现报错了,我稍微调试修改一下

原来的:

path.parentPath.node.right.body.body[0].body.body[2].cases.length

修改为:

path.parentPath.node.right.body.body[0].body.body[0].cases.length

还加了try处理,因为,有些是取不到length的,我也没太仔细分析原因,只是单纯从报错入手,然后小修改了一下。

proto2改成了proto3,optional关键字删除。

const parser = require("@babel/parser");
// 为parser提供模板引擎
const template = require("@babel/template").default;
// 遍历AST
const traverse = require("@babel/traverse").default;
// 操作节点,比如判断节点类型,生成新的节点等
const t = require("@babel/types");
// 将语法树转换为源代码
const generator = require("@babel/generator");
// 操作文件
const fs = require("fs");

//定义公共函数
function wtofile(path, flags, code) {
    var fd = fs.openSync(path,flags);
    fs.writeSync(fd, code);
    fs.closeSync(fd);
}

function dtofile(path) {
    fs.unlinkSync(path);
}

var file_path = '1.js'; //你要处理的文件
var jscode = fs.readFileSync(file_path, {
    encoding: "utf-8"
});

// 转换为AST语法树
let ast = parser.parse(jscode);
let proto_text = `syntax = "proto3";\n\n// protoc --python_out=. app_proto2.proto\n\n`;

traverse(ast, {
    MemberExpression(path){
        if(path.node.property.type === 'Identifier' && path.node.property.name === 'deserializeBinaryFromReader' && path.parentPath.type === 'AssignmentExpression'){
            let id_name = path.toString().split('.').slice(1, -1).join('_');
            path.parentPath.traverse({
                VariableDeclaration(path_2){
                    if(path_2.node.declarations.length === 1){
                        path_2.replaceWith(t.expressionStatement(
                            t.assignmentExpression(
                                "=",
                                path_2.node.declarations[0].id,
                                path_2.node.declarations[0].init
                            )
                        ))
                    }
                },
                SwitchStatement(path_2){
                    for (let i = 0; i < path_2.node.cases.length - 1; i++) {
                        let item = path_2.node.cases[i];
                        let item2 = path_2.node.cases[i + 1];
                        if(item.consequent.length === 0 && item2.consequent[1].expression.type === 'SequenceExpression'){
                            item.consequent = [
                                item2.consequent[0],
                                t.expressionStatement(
                                    item2.consequent[1].expression.expressions[0]
                                ),
                                item2.consequent[2]
                            ];
                            item2.consequent[1] = t.expressionStatement(
                                item2.consequent[1].expression.expressions[1]
                            )
                        }else if(item.consequent.length === 0){
                            item.consequent = item2.consequent
                        }else if(item.consequent[1].expression.type === 'SequenceExpression'){
                            item.consequent[1] = t.expressionStatement(
                                item.consequent[1].expression.expressions[1]
                            )
                        }
                    }
                }
            });
            let id_text = 'message ' + id_name + ' {\n';
            let let_id_list = [];
            try{
                // console.log(path.parentPath.node.right.body.body[0].body.body[0].cases.length);
                for (let i = 0; i < path.parentPath.node.right.body.body[0].body.body[0].cases.length; i++) {
                    let item = path.parentPath.node.right.body.body[0].body.body[0].cases[i];
                    if(item.test){
                        let id_number = item.test.value;
                        let key = item.consequent[1].expression.callee.property.name;
                        let id_st, id_type;
                        if(key.startsWith("set")){
                            id_st = "";
                        }else if(key.startsWith("add")){
                            id_st = "repeated";
                        }else{
                            // map类型,因为案例中用不到,所以这里省略
                            continue
                        }
                        key = key.substring(3, key.length);
                        id_type = item.consequent[0];
                        if(id_type.expression.right.type === 'NewExpression'){
                            id_type = generator.default(id_type.expression.right.callee).code.split('.').slice(1).join('_');
                        }else{
                            switch (id_type.expression.right.callee.property.name) {
                                case "readString":
                                    id_type = "string";
                                    break;
                                case "readDouble":
                                    id_type = "double";
                                    break;
                                case "readInt32":
                                    id_type = "int32";
                                    break;
                                case "readInt64":
                                    id_type = "int64";
                                    break;
                                case "readFloat":
                                    id_type = "float";
                                    break;
                                case "readBool":
                                    id_type = "bool";
                                    break;
                                case "readPackedInt32":
                                    id_st = "repeated";
                                    id_type = "int32";
                                    break;
                                case "readBytes":
                                    id_type = "bytes";
                                    break;
                                case "readEnum":
                                    id_type = "readEnum";
                                    break;
                                case "readPackedEnum":
                                    id_st = "repeated";
                                    id_type = "readEnum";
                                    break;
                            }
                        }
                        if(id_type === 'readEnum'){
                            id_type = id_name + '_' + key + 'Enum';
                            if(let_id_list.indexOf(id_number) === -1){
                                id_text += '\tenum ' + id_type + ' {\n';
                                for (let j = 0; j < 3; j++) {
                                    id_text += '\t\t' + id_type + 'TYPE_' + j + ' = ' + j + ';\n';
                                }
                                id_text += '\t}\n\n';
                                id_text += '\t' + id_st + ' ' + id_type + ' ' + key + ' = ' + id_number + ';\n';
                                let_id_list.push(id_number)
                            }
                        }else{
                            if(let_id_list.indexOf(id_number) === -1){
                                id_text += '\t' + id_st + ' ' + id_type + ' ' + key + ' = ' + id_number + ';\n';
                                let_id_list.push(id_number)
                            }
                        }
                    }
                }
            }catch(e){
            }

            id_text += '}\n\n';
            proto_text += id_text
        }
    }
});

wtofile('app_proto3.proto', 'w', proto_text);

这个ast代码单纯只是针对这个站点,其他站点也是类似分析。

运行完后,我们得到了app_proto3.proto文件,打开来,我们发现了报错,如下图,渔歌文章也讲清楚了原因,因为对象调用deserializeBinaryFromReader方法的时候,ast代码处理对象无法确定,所以就没加载到。

在这里插入图片描述

我们在调试里面,搜索关键词ExportResponse.deserializeBinaryFromReader

就能找到位置,根据我上面说的上下级关系,然后跟进去

在这里插入图片描述

在这里插入图片描述

就能找到s代表的是什么了。

然后我们依次像这样把报错补好就行了。

得到了proto文件,基本上我们的任务就完成了,我们发包试试吧。

在这里插入图片描述

可以看到很直观,取值也方便。我们的protobuf之旅也基本上结束了!

完整代码:

import requests
import app_proto3_pb2 as pb
import blackboxprotobuf

search_request = pb.SearchService_SearchRequest()
search_request.InterfaceType = 1
search_request.Commonrequest.SearchType = 'paper'
search_request.Commonrequest.SearchWord = '爬虫'
search_request.Commonrequest.CurrentPage = 2
search_request.Commonrequest.PageSize = 20
search_request.Commonrequest.SearchFilterList.append(0)
form_data = search_request.SerializeToString()
# with open('me1.bin', mode="wb") as f:
#     f.write(form_data)
# print(SearchRequest.SerializeToString().decode())
bytes_head = bytes([0, 0, 0, 0, len(form_data)])
# print(bytes_head+form_data)

headers = {
    "Accept": "*/*",
    "Accept-Language": "zh-CN,zh;q=0.9,zh-TW;q=0.8",
    "Content-Type": "application/grpc-web+proto",
    "Origin": "https://**********",
    "Referer": "https://**********/paper",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
}
url = "https://**********/SearchService.SearchService/search"
response=requests.post(url,headers=headers,data=bytes_head+form_data)
# print(response.content)
# deserialize_data, message_type = blackboxprotobuf.decode_message(response.content[5:])
# data, message_type = blackboxprotobuf.decode_message(response.content[5:])
# print(data)
# print(message_type)
search_response = pb.SearchService_SearchResponse()
search_response.ParseFromString(response.content[5:])
print(search_response)

七、注意:

其实这段代码还有点问题,就是最后取的response.content的真实长度是多少,从第6位开始,一共有多长呢,我们只是刚刚好取完是正确的。我们在从fiddler的抓包分析,刚刚不是前面还有5位16进制数:00 00 00 93 E7吗?我们在网站解析一下

在这里插入图片描述

刚刚好就是我们断点时的

在这里插入图片描述

所以前5位十六进制代表的是我们需要的响应数据的长度。

处理方法:

data_len=int.from_bytes(response.content[:5], 'big') #bytes转int
search_response = pb.SearchService_SearchResponse()
search_response.ParseFromString(response.content[5:5+data_len])

希望我们的文章能让你们对protobuf类型网站的逆向有更深的理解,某音直播弹幕其实也是类似原理,抓住这节和上节说的关键词,相信大家都能解决。

deserializeBinary------deserializeBinaryFromReader重点核心

serializeBinary------serializeBinaryFromReader重点核心

有说的不够好的地方欢迎指出,希望你看了有所收获!感谢!

如果想我多出文章,就多点赞转发,谢谢啦,写一篇真的好累,好久啊........

八、参考文章

https://blog.csdn.net/qq_35491275/article/details/111721639

https://mp.weixin.qq.com/s/DzCz66_Szc7vfG6bpl956w

https://bbs.kanxue.com/thread-265537.htm

免费评分

参与人数 22吾爱币 +26 热心值 +19 收起 理由
procurve + 1 + 1 谢谢@Thanks!
coconutwater + 1 + 1 谢谢@Thanks!
shiqiangge + 1 大写的牛
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
love008 + 1 + 1 谢谢@Thanks!
dincia + 1 谢谢@Thanks!
OVVO + 2 + 1 用心讨论,共获提升!
PrincessSnow + 1 + 1 我很赞同!
yixi + 1 + 1 热心回复!
ai474427793 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
笙若 + 1 + 1 谢谢@Thanks!
allspark + 1 + 1 用心讨论,共获提升!
shuaiqi + 1 + 1 谢谢@Thanks!
gaosld + 1 + 1 谢谢@Thanks!
superworker2022 + 1 + 1 我很赞同!
枫叶物语 + 1 + 1 我很赞同!
yx960105 + 1 + 1 谢谢@Thanks!
xhtdtk + 2 + 1 用心讨论,共获提升!
剑来…… + 2 + 1 大佬牛逼!
gksj + 1 + 1 我很赞同!
莫问刀 + 1 + 1 谢谢@Thanks!
mengzuiqiannian + 1 谢谢分享!!!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
shuaiqi 发表于 2023-1-14 17:28
之前爬这个网站因为protobuf 数据格式搞得头疼  最后还是选择用selenium爬了
推荐
闷骚小贱男 发表于 2023-1-16 21:30
好像和我那个H5游戏的二进制wss传输数据是差不多的
沙发
莫问刀 发表于 2023-1-13 14:29
3#
sssssjd 发表于 2023-1-13 17:29
向大佬学习
4#
涛之雨 发表于 2023-1-13 21:01
emmm排版有点问题,给你编辑了一下(结果因为一大堆外链图片把自己ip搞封了=_=)

但是似乎还是有点问题?不过问题不大,你反正再改一下吧,吾爱的话,最好一级标题作为各段落的标题
5#
huiwuwu 发表于 2023-1-14 01:08
这都是大神的牛逼经验,作品用心了。感谢分享。
6#
hxs1 发表于 2023-1-14 06:11
大佬太强大了,争取哪天成为大佬啊&#128514;
7#
QingYi. 发表于 2023-1-14 11:03
"一个个分析按顺序写proto文件" 从这里往后那段proto代码文件就没看懂,不知道咋写出来的了
8#
ppmjb 发表于 2023-1-14 12:38
学习了,多谢分享
9#
qwqwqw9999 发表于 2023-1-14 13:56
学习了,多谢分享
10#
superworker2022 发表于 2023-1-14 15:23
厉害了,跟着研究下
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2023-2-4 06:07

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表