某方数据protobuf协议逆向
目标网站:DQpodHRwczovL3Mud2FuZmFuZ2RhdGEuY29tLmNu
目标接口:aHR0cHM6Ly9zLndhbmZhbmdkYXRhLmNvbS5jbi9TZWFyY2hTZXJ2aWNlLlNlYXJjaFNlcnZpY2Uvc2VhcmNo
一、定义
‘Protobuf 是 Google 开发的一种语言无关、平台无关的序列化数据结构,相当于更小、更快、更严格的 JSON。’
整体流程如下。

二、特征
我们先来看看响应,是一堆乱码。

表单数据也是乱码

注意下图的content-type,很明显已经告诉咱们是protobuf了。

三、重放与排查
我们来解析一下curl,发现并没有啥相关加密参数,可见除了protobuf,没啥别的反爬手段,唯一要改的就是个时间戳。cookie里的fingerprint貌似也有点说法,先放一下😋
data = { "\\u0000\\u0000\\u0000\\u0000": "\n$\n\\u0005paper\\u0012\\u0006爬虫(\\u00010\\u0014B\\u0001\\u0000H\\u0001b\\u0002pcj\\u0006search\\u0010\\u0001\"\\u0007AI_READ\"\nAI_EXTRACT"}解析出来要携带的表单如下,这是json转义后的数据。内容也是个二进制。
我们在py里利用blackboxprotobuf来还原一下这个数据。
import blackboxprotobuf
import codecs
# 示例 protobuf 字节串protobuf_bytes = b'\x08\x96\x01'message_data = { '1': { # 嵌套对象 '1': b'paper', # 字符串用 bytes '2': '爬虫'.encode('utf-8'), # 中文必须编码为 UTF-8 '5': 1, # int '6': 20, # int '8': b'\x00', # 空字节 '9': 1, # int '12': { # 嵌套对象 '14': 99 # int (ascii 'c') }, '13': b'search', # 字符串 }, '2': 1, # int '4': [ # 数组 b'AI_READ', b'AI_EXTRACT' ]}s = "\n$\n\\u0005paper\\u0012\\u0006爬虫(\\u00010\\u0014B\\u0001\\u0000H\\u0001b\\u0002pcj\\u0006search\\u0010\\u0001\"\\u0007AI_READ\"\nAI_EXTRACT"decoded = codecs.decode(s, 'unicode_escape') # 把 \\u0005 变成真正的 \x05mybytes = decoded.encode('latin-1')# 解码 protobuf 消息decoded_message, type_definition = blackboxprotobuf.decode_message(mybytes)print('type:',type_definition)print("Decoded Message:", decoded_message)
# 重新编码消息encoded_bytes = blackboxprotobuf.encode_message(decoded_message, type_definition)newencoded_bytes = blackboxprotobuf.encode_message(message_data, type_definition)print("Encoded Bytes:", encoded_bytes)print('new encoded:',newencoded_bytes)拿到的结果就像7-25行的样子,我们可以明文修改然后再加密放到post请求里。
四、寻找逻辑
我们来看看网页上是如何处理protobuf的。

for (o = (t = (0, o.a)(t.getRequestMessage())).length, d = [0, 0, 0, 0], s = new Uint8Array(5 + o), a = 3; 0 <= a; a--) d[a] = o % 256, o >>>= 8; if (s.set(new Uint8Array(d), 1), s.set(t, 5), t = s, "text" == e.a)讲一下整个流程吧。
a(t.getRequestMessage())这里是把表单转成了uint8Array,总共61字节
o被赋值成t的长度,也就是61
d = [0, 0, 0, 0]定义了一个缓冲数组。
s = new Uint8Array(5 + o)申请缓冲区,共66字节。
a = 3; 0 <= a; a--) d[a] = o % 256, o >>>= 8;接着走了四次for循环,o是61,先把d[3]赋值为61,然后o位数右移8位变成0。
循环结束后d就是[0,0,0,61]
s.set(new Uint8Array(d), 1)
接着在下标1的索引位置,将d数组复制到s中。
s.set(t, 5)
同样的操作,偏移到5复制t。
那s就变成了
[0, 0, 0, 0, 61, 10, 36, 10, 5, 112, 97, 112, 101, 114, 18, 6, 231, 136, 172, 232, 153, 171, 40, 1, 48, 20, 66, 1, 0, 72, 1, 98, 2, 112, 99, 106, 6, 115, 101, 97, 114, 99, 104, 16, 1, 34, 7, 65, 73, 95, 82, 69, 65, 68, 34, 10, 65, 73, 95, 69, 88, 84, 82, 65, 67, 84]我们可以看到,前五个数字几乎是固定的,这里被用作了帧头
[0, 0, 0, 0, 61]0(第1字节):压缩标志 = 0(未压缩)[0,0,0,61](第2-5字节):长度字段,大端序,值为61
五、伪造帧头
根据网站逻辑,直接伪造一下尝试结果可行。
def encode_varint(value): result = [] while value > 0x7f: result.append((value & 0x7f) | 0x80) value >>= 7 result.append(value & 0x7f) return bytes(result)
def encode_length_delimited(field_number, data): tag = (field_number << 3) | 2 return encode_varint(tag) + encode_varint(len(data)) + data
def encode_varint_field(field_number, value): tag = (field_number << 3) | 0 return encode_varint(tag) + encode_varint(value)
query = '美女'.encode('utf-8')inner = b''inner += encode_length_delimited(1, b'paper')inner += encode_length_delimited(2, query)inner += encode_varint_field(5, 1)inner += encode_varint_field(6, 20)inner += encode_length_delimited(8, b'\x00')inner += encode_varint_field(9, 1)inner12 = encode_varint_field(14, 99)inner += encode_length_delimited(12, inner12)inner += encode_length_delimited(13, b'search')
body = b''body += encode_length_delimited(1, inner)body += encode_varint_field(2, 1)body += encode_length_delimited(4, b'AI_READ')body += encode_length_delimited(4, b'AI_EXTRACT')
grpc_frame = b'\x00' + struct.pack('>I', len(body)) + body根据之前的表单伪造一下帧头。
六、解密数据
可以使用那个黑盒库,我直接交给ai解析,反而更快一点。
import jsonimport structimport re
with open('response_raw.json', 'r', encoding='utf-8') as f: raw_data = json.load(f)
data = bytes.fromhex(raw_data['content_hex'])
# 解析 gRPC 帧头msg_len = struct.unpack('>I', data[1:5])[0]proto_data = data[5:5 + msg_len]
def read_varint(data, pos): result = 0 shift = 0 while pos < len(data): byte = data[pos] pos += 1 result |= (byte & 0x7f) << shift if not (byte & 0x80): break shift += 7 return result, pos
def parse_protobuf(data, max_depth=5): if max_depth <= 0: return {} result = {} pos = 0 while pos < len(data): try: tag, pos = read_varint(data, pos) except: break field_num = tag >> 3 wire_type = tag & 0x07
if wire_type == 0: value, pos = read_varint(data, pos) elif wire_type == 2: length, pos = read_varint(data, pos) value = data[pos:pos + length] pos += length try: text = value.decode('utf-8') if any('\u4e00' <= c <= '\u9fff' for c in text[:50]): value = text else: sub = parse_protobuf(value, max_depth - 1) value = sub if sub else text except: sub = parse_protobuf(value, max_depth - 1) value = sub if sub else value else: break
if field_num in result: if not isinstance(result[field_num], list): result[field_num] = [result[field_num]] result[field_num].append(value) else: result[field_num] = value return result
top = parse_protobuf(proto_data)papers = top.get(4, [])
print(f"共 {len(papers)} 篇论文:\n")
for i, paper in enumerate(papers): inner = paper.get(101, {}) if not isinstance(inner, dict) or not inner: continue
# 标题:优先取中文,否则取第一个 title_list = inner.get(2, '') if not isinstance(title_list, list): title_list = [title_list] title = '' for t in title_list: t_str = str(t) if any('\u4e00' <= c <= '\u9fff' for c in t_str): title = t_str break if not title and title_list: title = str(title_list[0]) title = re.sub(r'<[^>]+>', '', title)
# 作者 authors = inner.get(3, []) if not isinstance(authors, list): authors = [authors] authors = [str(a) for a in authors if isinstance(a, str) and any('\u4e00' <= c <= '\u9fff' for c in a)]
# 期刊 journal_list = inner.get(23, '') if not isinstance(journal_list, list): journal_list = [journal_list] journal = '' for j in journal_list: j_str = str(j) if any('\u4e00' <= c <= '\u9fff' for c in j_str): journal = j_str break if not journal and journal_list: journal = str(journal_list[0])
year = inner.get(33, '') page = inner.get(36, '') doi = inner.get(41, '')
print(f"{i + 1}. {title}") print(f" 作者: {', '.join(authors[:3]) if authors else '未知'}") print(f" 期刊: {journal}") print(f" 年份: {year} 页码: {page}") print(f" DOI: {doi}") print()部分信息可能已经过时









