吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 12985|回复: 100
收起左侧

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

    [复制链接]
漁滒 发表于 2022-3-27 17:24

@TOC

样品网址:aHR0cHM6Ly93d3cud2FuZmFuZ2RhdGEuY29tLmNuL2luZGV4Lmh0bWw=

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

1.png

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

2.png
也是一堆乱码,有的人可能就会想,会不会是有加密呢?当然不排除这个可能。接着再看看请求头,其中有一行是【content-type: application/grpc-web+proto】,这里指明了请求体的类型是proto。

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

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

3.png

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

4.png

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

5.png
6.png

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

7.png
接着进入【r.a】

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

9.png

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

如果一个一个来写的话,那就有点麻烦了,而且还怕会出错,那么为了保证准确性,所以这次使用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";\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 = [];
            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 += '\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)
                        }
                    }
                }
            }
            id_text += '}\n\n';
            proto_text += id_text
        }
    }
});

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

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

10.png
在网页中搜索这个信息结构

11.png

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

12.png

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

13.png
一直拉到最下面看看导出的名称是什么

14.png

接着补全一下路径

15.png

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

protoc --python_out=. app_proto2.proto

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

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

pip install protobuf

16.png

但是并不是直接序列化后就可以请求,这里可以看到对请求体还有一层包装,序列化的内容被设置到偏移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))

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

18.png
没有报错,非常好,说明编写的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

免费评分

参与人数 42吾爱币 +42 热心值 +40 收起 理由
树先生诶 + 1 + 1 谢谢@Thanks!
msk2001 + 1 谢谢@Thanks!
shanhu5235 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
imstone + 1 + 1 谢谢@Thanks!
yunji + 1 用心讨论,共获提升!
放手一搏09 + 1 + 1 鼓励转贴优秀软件安全工具和文档!
godehi + 1 + 1 谢谢@Thanks!
Anekys + 1 + 1 热心回复!
159198 + 1 我看不懂,但我大受震撼
红烧排骨 + 1 热心回复!
努力加载中 + 1 + 1 热心回复!
OYyunshen + 1 + 1 我很赞同!
love2334163717 + 1 + 1 热心回复!
itaotao + 1 我很赞同!
lecat + 1 + 1 热心回复!
zjun777 + 1 + 1 用心讨论,共获提升!
jokerxin + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
CodingZhang + 1 + 1 谢谢@Thanks!
xionghaizi + 1 我很赞同!
JinxBoy + 1 谢谢@Thanks!
bjznhxy + 1 + 1 我很赞同!
Quincy379 + 1 + 1 已经处理,感谢您对吾爱破解论坛的支持!
ezerear + 1 + 1 谢谢@Thanks!
szjzxm4321 + 1 + 1 热心回复!
MagicHen + 1 我很赞同!
seei + 1 用心讨论,共获提升!
c7128 + 1 + 1 谢谢@Thanks!
annye + 1 用心讨论,共获提升!
独行风云 + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
MFC + 1 + 1 谢谢@Thanks!
ofo + 3 + 1 越来越看不懂了....
mufeng007 + 1 热心回复!
xinjun_ying + 1 我很赞同!
victos + 1 + 1 谢谢@Thanks!
zhyerh + 1 + 1 谢谢@Thanks!
笙若 + 1 + 1 谢谢@Thanks!
yxpp + 1 + 1 我很赞同!
ShyGW + 1 + 1 热心回复!
风绕柳絮轻敲雪 + 4 + 1 我很赞同!
ncu.xxy + 1 + 1 热心回复!
兜兜风f + 4 + 1 谢谢@Thanks!

查看全部评分

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

unmask 发表于 2022-3-27 20:27
一般情况下,都是用的标准库的工具,所以首先直接搜索【.deserializeBinaryFromReader = 】,为什么搜索这个呢?这就好比json的数据会搜索【JSON.】是一样的。


用的标准库工具就是搜索【.deserializeBinaryFromReader = 】,这个还是很勉强,用的哪个标准库?每个标准库都是这个funcName?这个不一定吧。
根据我自己的经验,顶多会搜索【deserialize/unserialize】,可能会瞎猫碰死耗子刚好找到了这个deserializeBinaryFromReader,所以这中间还是缺很多东西的,像我这种小白还是看不明白。
但是大佬既然是科普,希望能再补一补这种缺失的东西。

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
漁滒 + 1 + 1 热心回复!

查看全部评分

oyfj 发表于 2022-4-8 17:18
跟着楼主的干货研究了好几天,总算搞懂了。因为没学过python,也看不懂struct.pack和unpack.... 所以写了个nodejs版的...
[JavaScript] 纯文本查看 复制代码
const proto = require('./app_proto2_pb')

var request = require('request');

const searchreq = new proto.SearchService_SearchRequest()
const commonreq = new proto.SearchService_CommonRequest();
// data.setInterfacetype (proto.SearchService_SearchRequest.SearchService_SearchRequest_InterfaceTypeEnum.SEARCHSERVICE_SEARCHREQUEST_INTERFACETYPEENUMTYPE_0)
commonreq.setSearchtype('paper')
commonreq.setSearchword('百度')
commonreq.setCurrentpage(1)
commonreq.setPagesize(20)
// commonreq.setSearchfilterlistList ( [proto.SearchService_CommonRequest.SearchService_CommonRequest_SearchFilterListEnum.SearchService_CommonRequest_SearchFilterListEnumTYPE_0])

searchreq.setCommonrequest(commonreq)
var data = searchreq.serializeBinary()
var a = new Uint8Array(5 + data.length)
a.set(new Uint8Array([0, 0, 0, 0, data.length]), 0)
a.set(data, 5)
// console.log('body',data)
// console.log('wrap',a)
// 0,0,0,0,长度,proto2进制包
//前5位其实是代表长度,不满5位就补0
request.post(
	{
		url: 'https://s.wanfangdata.com.cn/SearchService.SearchService/search',
		encoding: null,// nodejs如果不指定编码会默认把返回值转为字符串,会把数据搞错乱
		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',
		},
		body: a

	}, function (_, httpResponse, body) {
		if (httpResponse.statusCode != 200) {
			console.error('请求出错', body.toString())
			return
		}
		// 返回的是buffer类型
		console.log('body size', body.length);
		var length = parseInt(body.slice(0, 5).toString('hex'), 16)
		console.log('data length', length);
		var result = body.slice(5, 5 + length)
		var obj = proto.SearchService_SearchResponse.deserializeBinary(result)
		console.log(obj.toObject());
	})


QQ截图20220408165440.jpg

大概讲一下组包和解包的思路:
组包:前5位是代表request body被proto序列化后得到uint8array的长度,第6位开始就是序列化后的uint8array了。
解包:截取前5位转成16进制后,再转10进制就得到了长度了,然后就可以截取真实的包了。(Buffer里是10进制的)
那个ast生成proto的代码如果是我来写我估计要调试好几天才写得出来。。膜拜大神
逗逗苍穹 发表于 2022-3-27 17:30
13248101888 发表于 2022-3-27 17:33
我看限制不了
kdkdkdkd 发表于 2022-3-27 18:37
感谢大佬,互相学习互相进步
ciker_li 发表于 2022-3-27 19:51
好难啊,学习学习
muyejianghu 发表于 2022-3-27 20:14
哇偶,好复杂的样子
tukuai88ya 发表于 2022-3-27 20:39
好难啊,学习学习
 楼主| 漁滒 发表于 2022-3-27 20:39
unmask 发表于 2022-3-27 20:27
用的标准库工具就是搜索【.deserializeBinaryFromReader = 】,这个还是很勉强,用的哪个标准库?每个 ...

目前我遇到的格式基本是两种,这个是其中一种。还有一种类似于某音的,是路径后面【.decode】,所以基本搜索这两种就可以了,也算是一种经验吧

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
unmask + 1 + 1 用心讨论,共获提升!

查看全部评分

yxpp 发表于 2022-3-27 21:21
看大佬知识,长见识
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2024-11-15 14:02

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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