【JS逆向系列】某方数据获取,proto入门

admin 2022年3月30日18:48:24评论172 views字数 9685阅读32分17秒阅读模式

作者坛账号:漁滒


样品网址:aHR0cHM6Ly93d3cud2FuZmFuZ2RhdGEuY29tLmNuL2luZGV4Lmh0bWw=

打开网站后,随便搜索一个关键词,这里以【百度】为例,本次需要分析的是【SearchService.SearchService/search】这个接口

【JS逆向系列】某方数据获取,proto入门

这里可以看到,请求体不再是表单或者json,而是一堆类似乱码的东西,再看看响应体

【JS逆向系列】某方数据获取,proto入门
也是一堆乱码,有的人可能就会想,会不会是有加密呢?当然不排除这个可能。接着再看看请求头,其中有一行是【content-type: application/grpc-web+proto】,这里指明了请求体的类型是proto。

所以这里的乱码并不是有加密,只是用proto这种格式序列化而已。那么接下来的流程就是编写proto文件,然后用protoc编译为对应语言可以调用的类文件。

首先下载protoc,到https://github.com/protocolbuffers/protobuf/下载最新的发行版工具,我这里下载的是win'64版本

【JS逆向系列】某方数据获取,proto入门

下载解压后,将里面的bin目录添加到环境变量,然后在cmd窗口输入【protoc --version】,出现版本好即为成功

【JS逆向系列】某方数据获取,proto入门

因为我们分析的是【SearchService.SearchService/search】这个接口,所以先下一个XHR断点,刷新网页,在断点处断下,返回调用堆栈的上一层

【JS逆向系列】某方数据获取,proto入门
【JS逆向系列】某方数据获取,proto入门

看到一个类似组包的函数,那么在前面加一个断点,再次刷新

【JS逆向系列】某方数据获取,proto入门
接着进入【r.a】

【JS逆向系列】某方数据获取,proto入门
发现这是一个webpack打包的js,并且所有的信息序列化与反序列化的操作都在这个js里面,一般情况下,都是用的标准库的工具,所以首先直接搜索【.deserializeBinaryFromReader = 】,为什么搜索这个呢?这就好比json的数据会搜索【JSON.】是一样的。

【JS逆向系列】某方数据获取,proto入门

这里就获取到每个信息是如何解析的,也就是可以获取信息的结构

如果一个一个来写的话,那就有点麻烦了,而且还怕会出错,那么为了保证准确性,所以这次使用ast来生成proto文件,首先吧这个【app.1d44779a.js】下载到本地,并且执行下面代码

 复制代码 隐藏代码

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 = 'app.1d44779a.js';
var jscode = fs.readFileSync(file_path, {
    encoding: "utf-8"
});

// 转换为AST语法树
let ast = parser.parse(jscode);
let proto_text = `syntax = "proto2";nn// protoc --python_out=. app_proto2.protonn`;

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 = [];
            for (let i = 0; i < path.parentPath.node.right.body.body[0].body.body[2].cases.length; i++) {
                let item = path.parentPath.node.right.body.body[0].body.body[2].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 = "optional";
                    }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 += 'tt' + id_type + 'TYPE_' + j + ' = ' + j + ';n';
                            }
                            id_text += 't}nn';
                            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)
                        }
                    }
                }
            }
            id_text += '}nn';
            proto_text += id_text
        }
    }
});

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

运行后可以得到一个【app_proto2.proto】的文件,开后发现有少量报错

【JS逆向系列】某方数据获取,proto入门
在网页中搜索这个信息结构

【JS逆向系列】某方数据获取,proto入门

这里的o省略了路径名,所以无法获取到完整路径就报错了,手动补充一下即可,往上查找o的来源

【JS逆向系列】某方数据获取,proto入门

o是来自于【e348】,那么搜索这个

【JS逆向系列】某方数据获取,proto入门
一直拉到最下面看看导出的名称是什么

【JS逆向系列】某方数据获取,proto入门

接着补全一下路径

【JS逆向系列】某方数据获取,proto入门

其他的报错都可以如此类推解决,然后在当前目录打开cmd,输入指令编译出python可调用的类

 复制代码 隐藏代码
protoc --python_out=. app_proto2.proto

此时就可以在当前目录的一个【app_proto2_pb2.py】文件

尝试使用这个生成的了进行数据序列化,使用proto文件前,需要先安装依赖库

 复制代码 隐藏代码
pip install protobuf

【JS逆向系列】某方数据获取,proto入门

但是并不是直接序列化后就可以请求,这里可以看到对请求体还有一层包装,序列化的内容被设置到偏移5的位置,而偏移1的位置设置了【l】参数,这里的【l】参数就是后面数据的长度

那么尝试按照这个格式去生成一个请求体去试试能不能获取数据

 复制代码 隐藏代码
import app_proto2_pb2
import requests_html
import struct

def main():
    requests = requests_html.HTMLSession()
    search_request = app_proto2_pb2.SearchService_SearchRequest()
    search_request.InterfaceType = app_proto2_pb2.SearchService_SearchRequest.SearchService_SearchRequest_InterfaceTypeEnum.Value('SearchService_SearchRequest_InterfaceTypeEnumTYPE_0')
    search_request.Commonrequest.SearchType = 'paper'
    search_request.Commonrequest.SearchWord = '百度'
    search_request.Commonrequest.CurrentPage = 1
    search_request.Commonrequest.PageSize = 20
    search_request.Commonrequest.SearchFilterList.append(app_proto2_pb2.SearchService_CommonRequest.SearchService_CommonRequest_SearchFilterListEnum.Value('SearchService_CommonRequest_SearchFilterListEnumTYPE_0'))
    data = search_request.SerializeToString()
    data = bytes([0]) + struct.pack(">i", len(data)) + data
    print(data)

    url = 'https://s.wanfangdata.com.cn/SearchService.SearchService/search'
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4901.0 Safari/537.36',
        'Content-Type': 'application/grpc-web+proto',
    }

    response = requests.post(url, headers=headers, data=data)
    print(response.content)
    print(len(response.content))

【JS逆向系列】某方数据获取,proto入门

看起来返回的数据是正确了,那么接着尝试去反序列化数据

【JS逆向系列】某方数据获取,proto入门
没有报错,非常好,说明编写的proto文件没有问题,本文结束,下面是完整代码

 复制代码 隐藏代码

import app_proto2_pb2
import requests_html
import struct

def main():
    requests = requests_html.HTMLSession()
    search_request = app_proto2_pb2.SearchService_SearchRequest()
    search_request.InterfaceType = app_proto2_pb2.SearchService_SearchRequest.SearchService_SearchRequest_InterfaceTypeEnum.Value('SearchService_SearchRequest_InterfaceTypeEnumTYPE_0')
    search_request.Commonrequest.SearchType = 'paper'
    search_request.Commonrequest.SearchWord = '百度'
    search_request.Commonrequest.CurrentPage = 1
    search_request.Commonrequest.PageSize = 20
    search_request.Commonrequest.SearchFilterList.append(app_proto2_pb2.SearchService_CommonRequest.SearchService_CommonRequest_SearchFilterListEnum.Value('SearchService_CommonRequest_SearchFilterListEnumTYPE_0'))
    data = search_request.SerializeToString()
    data = bytes([0]) + struct.pack(">i", len(data)) + data
    print(data)

    url = 'https://s.wanfangdata.com.cn/SearchService.SearchService/search'
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4901.0 Safari/537.36',
        'Content-Type': 'application/grpc-web+proto',
    }

    response = requests.post(url, headers=headers, data=data)
    data_len = struct.unpack(">i", response.content[1:5])[0]

    search_response = app_proto2_pb2.SearchService_SearchResponse()
    search_response.ParseFromString(response.content[5: 5 + data_len])
    print(search_response)

if __name__ == '__main__':
    main()

附加内容:对于较少的信息结构是,直接手动写也很快。但是多的时候,手动写重复的工作多,还很容易出错,ast的作用就体现出来了。对于web端可以proto文件自动还原可以使用ast,而在app的话,那该如何解决呢?可以参考下面文章使用frida解决

https://github.com/SeeFlowerX/frida-protobuf


--

www.52pojie.cn


--

pojie_52

原文始发于微信公众号(吾爱破解论坛):【JS逆向系列】某方数据获取,proto入门

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年3月30日18:48:24
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【JS逆向系列】某方数据获取,proto入门http://cn-sec.com/archives/855367.html

发表评论

匿名网友 填写信息