​ 谷歌的protocol buffer以其轻量级和跨平台的性质,成为了游戏中数据传输的首选。序列化的过程可以参考该文章 ,还有官方的解释https://developers.google.com/protocol-buffers/docs/encoding。

一、反序列化pb数据可行性分析

​ 传统的pb序列化与反序列化:

序列化:	协议文件(.proto)	+	原始数据		==>		序列化后数据
反序列化:	序列化后数据		+	协议文件(.proto)		==> 	原始数据

​ 以传统方式,需要.proto文件才能反序列化出原始数据。但是通过了解序列化的过程,可以知道其实序列化时是按照下列对应类型序列化数据的,那么如果对每个type都能有对应的方法反序列化出原始数据,那么就能够反序列化出整个原始数据。

Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimit string, bytes, embedded messages, packed repeated fields
3 Start group Groups (deprecated)
4 End group Groups (deprecated)
5 32-bit fixed32, sfixed32, float
二、反序列化pb数据例子分析

​ pb的序列化方式采用了如下方式,K-V KEY-VALUE 或 K-L-V KEY-LENGHT-VALUE,好像也有叫 T-V TAG-VALUE 和 T-L-V TAG-LENGTH-VALUE

Protocol Buffer 分析

Key 的定义如下:

(field_number << 3) | wire_type

其中field_nuber 代表pb中的第几个数据,wire_type 从上面的表格可以知道其数值在0-5区间内代表该数据的类型。

Intel CPU的架构是小端模式 ,最低字节在前(Small-Endian)

下面是某个pb数据反序列化分析过程,这个分析过程中需要先了解Varint编码方式,UTF8编码方式:

  • Varint编码方式

Protocol Buffer 分析

  • UTF8编码方式

    Unicode/UCS-4 bit数 UTF-8 byte数 备注
    0000 ~007F 0~7 0XXX XXXX 1
    0080 ~07FF 8~11 110X XXXX10XX XXXX 2
    0800 ~FFFF 12~16 1110XXXX10XX XXXX10XX XXXX 3 基本定义范围:0~FFFF
    1 0000 ~1F FFFF 17~21 1111 0XXX10XX XXXX10XX XXXX10XX XXXX 4 Unicode6.1定义范围:0~10 FFFF
    20 0000 ~3FF FFFF 22~26 1111 10XX10XX XXXX10XX XXXX10XX XXXX10XX XXXX 5 说明:此非unicode编码范围,属于UCS-4 编码早期的规范UTF-8可以到达6字节序列,可以覆盖到31位元(通用字符集原来的极限)。尽管如此,2003年11月UTF-8 被 RFC 3629 重新规范,只能使用原来Unicode定义的区域, U+0000到U+10FFFF。根据规范,这些字节值将无法出现在合法 UTF-8序列中
    400 0000 ~7FFF FFFF 27~31 1111 110X10XX XXXX10XX XXXX10XX XXXX10XX XXXX10XX XXXX
  • 分析过程

45 00 1A 0F 38 35 37 35 34 32 30 35 32 39 2E 33 34 39 36 22 0C E5 B9 BF E4 B8 9C E6 B7 B1 E5 9C B3 08 CB 8A 01 12 20 36 38 61 31 35 36 66 62 38 66 34 65 64 38 31 63 65 61 31 32 37 37 37 61 34 34 30 32 64 38 34 35

45 00 协议长度(小端形式)           00 45 --> 69个字节

1A --> 00011010
3 << 3 | 2	field_number为3,wire_type为2
0F --> 00001111 该值是15个字节
38 --> 00111000 : 56 字符8的char是56	*(这里参与了UTF8来解析,有可能是嵌套,后面分析讨论)*
35 --> 00110101 : 53 字符5
37 --> 00110111 : 55 字符7
35 --> 00110101 : 53 字符5
34 --> 00110100 : 52 字符4
32 --> 00110010 : 50 字符2
30 --> 00110000 : 48 字符0
35 --> 00110101 : 53 字符5
32 --> 00110010 : 50 字符2
39 --> 00111001 : 57 字符9
2E --> 00101110 : 46 字符.
33 --> 00110011 : 51 字符3
34 --> 00110100 : 52 字符4
39 --> 00111001 : 57 字符9
36 --> 00110110 : 54 字符6
pb中第3个的值是:8575420529.3496

22 --> 00100010
4 << 3 | 2 field_number为4,wire_type为2
0C --> 00001100 该值是12字节
E5 --> 11100101 (UTF格式,表明三个字节一起)
B9 --> 10111001 
BF --> 10111111
E5 B9 BF --> 01011110 01111111 --> 24191 字符'广'
E4 --> 11100100 (UTF格式,表明三个字节一起)
B8 --> 10111000
9C --> 10011100
E4 B8 9C --> 01001110 00011100 --> 19996 字符'东'
E6 --> 11100110 (UTF格式,表明三个字节一起)
B7 --> 10110111
B1 --> 10110001
E6 B7 B1 --> 01101101 11110001 --> 28145 字符'深'
E5 --> 11100101 (UTF格式,表明三个字节一起)
9C --> 10011100
B3 --> 10110011
E5 9C B3 --> 01010111 00110011 --> 22323 字符'圳'
pb中第4个的值是:广东深圳

08 --> 00001000
1 << 3 | 0 field_number为1,wire_type为0
CB --> 11001011 (Varint 高位为1表示下个字节是一起的)
8A --> 10001010 (Varint 高位为1表示下个字节是一起的)
01 --> 00000001 
CB 8A 01 --> (PB采用小端)0000001 0001010 1001011 --> 17739
pb中第1个的值是:17739

12 --> 00010010
2 << 3 | 2 field_number为2,wire_type为2
20 --> 00100000 该值是32字节
36 --> 00110110 : 54 字符6
38 --> 00111000 : 56 字符8
61 --> 01100001 : 97 字符a
31 --> 00110001 : 49 字符1
35 --> 00110101 : 53 字符5
36 --> 00110110 : 54 字符6
66 --> 01100110 : 102 字符f
62 --> 01100010 : 98 字符b
38 --> 00111000 : 56 字符8
66 --> 01100110 : 102 字符f
34 --> 00110100 : 52 字符4
65 --> 01100101 : 101 字符e
64 --> 01100100 : 100 字符d
38 --> 00111000 : 56 字符8
31 --> 00110001 : 49 字符1
63 --> 01100011 : 99 字符c
65 --> 01100101 : 101 字符e
61 --> 01100001 : 97 字符a
31 --> 00110001 : 49 字符1
32 --> 00110010 : 50 字符2
37 --> 00110111 : 55 字符7
37 --> 00110111 : 55 字符7
37 --> 00110111 : 55 字符7
61 --> 01100001 : 97 字符a
34 --> 00110100 : 52 字符4
34 --> 00110100 : 52 字符4
30 --> 00110000 : 48 字符0
32 --> 00110010 : 50 字符2
64 --> 01100100 : 100 字符d
38 --> 00111000 : 56 字符8
34 --> 00110100 : 52 字符4
35 --> 00110101 : 53 字符5
pb中第2个的值是:68a156fb8f4ed81cea12777a4402d845

整个pb解析完毕
协议 1001
pb内容:
{
    17739,
    68a156fb8f4ed81cea12777a4402d845,
    8575420529.3496,
    广东深圳
}

该协议.proto如下:
message msg_login_req
{
	optional uint32 uid  		= 1;		// 帐号ID
	optional bytes  key  		= 2;		// 密码		
	optional bytes  deviceid	= 3;		// 设备ID
	optional bytes  city		= 4;		// 城市
}
  • 总结
    1. 序列化后的pb数据能够在没有.proto文件下反序列化出原始数据,但是因为缺少.proto,无法得知每个数据对应的key。
    2. 反序列化过程中遇到了wire_type为2的情况下,直接当成了UTF8形式的数据进行解码,但是wire_type为2,可能是嵌套数据。
三、抓取pb数据

​ Fildder可以抓取HTTP或HTTPS包,但无法抓取TCP包,需要用到另一款工具PacketCapture。安卓下载地址

Protocol Buffer 分析

Packet Capture抓取数据步骤:

  1. 关掉其他进程,打开Packet Caputre
  2. 打开要抓取的游戏
  3. 将抓取的内容保存为后缀名为.pcap的文件
  4. 用Wireshark分析.pcap文件内容

Protocol Buffer 分析

反序列化结果:

Protocol Buffer 分析

四、编写反序列化工具

​ 因为工作中用的是Lua语言,所以这个反序列化工具也想使用Lua语言来编写。可以使用srLua将编写的lua文件编译成.exe文件。srLua 官方下载地址

建议使用支持高版本Lua的srLua,例如在Lua5.1中要使用位操作,需要自己实现/使用quick-cocos2d中的bitExtend.lua/使用LuaBit。而在Lua5.3中是支持位运算的。

  • 例如 test.lua是反序列化的代码文件,将 test.lua编译成 test.exe

Protocol Buffer 分析

  • 编写test.lua文件

    1. 反序列化Varint类型

      根据Varint的编码方式,每次取一个字节,对该字节的最高位进行判断,如果为1,则继续取下一个字节,直到最高位为0。将该过程中所有的字节去掉最高位,按倒序排列。

      十六进制字节 CB 8A 01
      转为二进制 11001011 10001010 00000001
      去掉最高位 x1001011 x0001010 x0000001
      倒序排列 0000001 0001010 1001011
      整合 0000001 0001010 1001011
      结果 17739
      --[[
      	获取varint类型数据
      	pb中wire_type为0(获取key也使用该方式)
      --]]
      function DecodePBData:getVarintData(content,startIndex,tag)
      	if not content then return end
      	local varintTable = {}
      	local strData = ""
      	local varintDataLen = 0
      	while true do
      		if startIndex+varintDataLen+1 > #content then return end
      		local str = string.sub(content,startIndex+varintDataLen,startIndex+varintDataLen+1) 
      		varintDataLen = varintDataLen + 2 + space_byte
      		local d_num = tonumber(str,16)
      		if not d_num then return end
      
      		local hightBit = d_num & (1 << 7)
      		local byte_num = d_num & ((1 << 7) - 1)
      		table.insert(varintTable,byte_num)
      		strData = strData .. str
      		-- 最高位为0,表明结束
      		if hightBit == 0 then
      			break
      		end
      	end
      
      	local varint_num = 0
      	for i = 1, #varintTable do
      		varint_num = varint_num + (varintTable[i] << ((i - 1) * 7))
      	end
      
      	tag = tag or ""
      	output.d("[".. tag .. "getVarintData] strData::" .. strData)
      	output.d("[".. tag .. "getVarintData] varint_num::" .. varint_num)
      	return varint_num, varintDataLen
      end
      

      未解决的点:

      sint32sint64

      sint32,sint64是兼容的,他们都是使用VARINTS(Zigzag(v))进行编码的。但和其他使用VARINTS(v)进行编码的无法区分

      bool值与数字0,1

      反序列化后,0,1无法判断是数字0,1或是bool值false,true

    2. 反序列化64-bit类型

      64-bit即8字节,该类型固定取8字节进行反序列化。

      fixed64,无符号64位数据,取出该数据(小端),对每字节数据进行相应的左移操作,然后将左移的数据进行相加即可。

      function DecodePBData:getFixed64(content,startIndex)
      	if not content then return end
      	local intData = 0
      	local strData = ""
      	local intDataLen = 0
      	for i = 1, 8 do
      		local str = string.sub(content,startIndex+intDataLen,startIndex+intDataLen+1)
      		intDataLen = intDataLen + 2 + space_byte
      		local d_num = tonumber(str,16)
      		intData = intData + (d_num << ((i-1) * 8))
      		strData = strData .. str
      	end
      
      	output.d("[getFixed64] strData:" ..  strData)
      	output.d("[getFixed64] int_num:" .. intData)
      	return intData, intDataLen
      end
      
      

      sfixed64,有符号64位数据,方法与fixed64相似,只是增加判断最高位(符号位),如果是负数(负数在计算机在是以补码的形式存储的),先 -1 求其反码,再异或操作 ~0xFFFFFFFFFFFFFFFF求其原码,再增加符号。-((data - 1) ~ 0xFFFFFFFFFFFFFFFF)

      double,双精度浮点数。1位符号位,11位整数位,52位小数位

      --[[
      	读double
      	bytes 8字节byte数组
      --]]
      function DecodePBData:getDouble(bytes)
      	if not bytes or #bytes < 8 then return end
      	local sign = 1
      	local mantissa = bytes[2] % 2^4
      	for i = 3, 8 do
      		mantissa = mantissa * 256 + bytes[i]
      	end
      	if bytes[1] > 127 then sign = -1 end
      	local exponent = (bytes[1] % 128) * 2^4 + math.floor(bytes[2] / 2^4)
      
      	if exponent == 0 then
      		return 0
      	end
      	mantissa = (math.ldexp(mantissa, -52) + 1) * sign
      	return math.ldexp(mantissa, exponent - 1023)
      end
      

      未解决的点:

      虽然对于fixed64,sfixed64,double都能够单独反序列化。但是对于得到的序列化数据,却无法判断是对应fixed64,sfixed64,double中的哪种类型。

    3. 反序列化Length-delimit类型

      length-delimit中有string, bytes, embedded messages, packed repeated fields,而这几个可以分成几大类。

      • string,bytes UTF8编码类
      • embedded messages,packet repeated fields(没有带【packed = true】)嵌套类
      • packet repeated fields (带【packed = true】)K-L-多个V类型

      UTF8编码类:

      --[[
      	十六进制数据转UTF8
      	hexData 十六进制数据
      --]]
      function DecodePBData:hexToUtf8(hexData)
      	if not hexData then return end
      	output.d("[hexToUtf8] str:" .. hexData)
      	-- 获取单个字节
      	local hexIndex = 1
      	local function getSingleByte()
      		if hexIndex+1 > #hexData then return end
      		local str = string.sub(hexData,hexIndex,hexIndex+1)
      		hexIndex = hexIndex + 2 + space_byte
      		local utf8_num = tonumber(str,16)
      		return utf8_num, str
      	end
      
      	local _utf8Str = ""
      	local singleCharByte = 0
      	local tmpStr = ""
      	while true do
      		local utf8_num, utf8_str = getSingleByte()
      		if not utf8_num then break end
      		tmpStr = tmpStr .. utf8_str
      		-- 偏移位
      		local offsetBit = 7
      		while offsetBit > 0 do
      			local bitNum = utf8_num & (1 << offsetBit)
      			if bitNum == 0 then
      				break
      			end
      			offsetBit = offsetBit - 1
      		end
      		-- utf8字符占用字节数
      		local utf8ByteCount = offsetBit == 7 and 1 or 7 - offsetBit
      		-- 第一个字节对应的真实数字
      		singleCharByte = singleCharByte + (utf8_num & ((1 << (8-utf8ByteCount)) - 1)) << (((utf8ByteCount-1) * 6))
      		if utf8ByteCount > 1 then
      			-- 两个字节以上的
      			for i = 2, utf8ByteCount do
      				local other_utf8_num,other_utf8_str = getSingleByte()
      				singleCharByte = singleCharByte + ((other_utf8_num & 0x3F) << ((utf8ByteCount-i) * 6))
      				tmpStr = tmpStr .. other_utf8_str
      			end
      		end
      
      		output.d("[hexToUtf8] sigle_utf8_str::" .. tmpStr)
      		output.d("[hexToUtf8] sigle_utf8_char::" .. singleCharByte)
      		_utf8Str = _utf8Str .. utf8.char(singleCharByte)
      		singleCharByte = 0
      		tmpStr = ""
      	end
      
      	output.d("[hexToUtf8] _utf8Str::" .. _utf8Str)
      	return _utf8Str
      end
      

      嵌套类:

      ​ 对于嵌套类,只要将其当做是一个全新的pb序列化数据,进行反序列即可。工具中调用decodeMessage(解码单个协议体数据) 方法即可。

      K-L-多个V类型:

      ​ 对于多个V类型,是以相同的wire_type进行解析。可以发现对于UTF8类型,不适用多个V,因为对于UTF8类型没有固定的分界。

      --[[
      	获取packet repeated fields [packed = true]数据 (用于value是varint数据)
      	packet repeated fields可当做是嵌套,调用decodeMessage即可
      --]]
      function DecodePBData:getPackedRepeatedFields(content,startIndex,wire_type)
      	if not content then return end
      	local repeatedPacket = {}
      	while true do
      		if startIndex > #content then break end
      		if wire_type == 0 then
      			-- varint
      			local varint_num, varint_len = DecodePBData:getVarintData(content,startIndex)
      			table.insert(repeatedPacket, varint_num)
      			startIndex = startIndex + varint_len
      		elseif wire_type == 1 then
      			-- 64-bit
      
      		elseif wire_type == 2 then
      			-- length-delimit
      
      		elseif wire_type == 5 then
      			-- 32-bit
      
      		else
      			break
      		end
      	end
      
      	output.d("[getPackedRepeatedFields] wire_type: " .. tostring(wire_type))
      	output.d("[getPackedRepeatedFields] repeatedPacket: " .. table.concat(repeatedPacket,","))
      	return repeatedPacket
      end
      

      未解决的点:

      ​ 判断又K-L-多个V类型解析,但是却无法得知wire_type类型

      需要优化的点:

      ​ 现在对于UTF8,嵌套类,K-L-多个V类型的区分,是先判断是否是嵌套类 先模拟嵌套解析,如果能够解析成功,则表明是嵌套类 ,否则判断是否是UTF8格式,如果是则用UTF8方法解析,否则用K-L-多个V类型解析

    4. 反序列化32-bit类型

      32-bit即4字节,该类型固定取4字节进行反序列化。

      fixed32,无符号32位数据,取出该数据(小端),对每字节数据进行相应的左移操作,然后将左移的数据进行相加即可。

      --[[
      	读取固定的4字节数据
      --]]
      function DecodePBData:getFixed32(content,startIndex)
      	if not content then return end
      	local intData = 0
      	local strData = ""
      	local intDataLen = 0
      	for i = 1, 4 do
      		local str = string.sub(content,startIndex+intDataLen,startIndex+intDataLen+1)
      		intDataLen = intDataLen + 2 + space_byte
      		local d_num = tonumber(str,16)
      		intData = intData + (d_num << ((i-1) * 8))
      		strData = strData .. str
      	end
      
      	output.d("[getFixed32] strData:" ..  strData)
      	output.d("[getFixed32] int_num:" .. intData)
      	return intData, intDataLen
      end
      

      sfixed32,有符号32位数据,方法与fixed32相似,只是增加判断最高位(符号位),如果是负数(负数在计算机在是以补码的形式存储的),先 -1 求其反码,再异或操作 ~0xFFFFFFFF求其原码,再增加符号。-((data - 1) ~ 0xFFFFFFFF)

      float,双精度浮点数。1位符号位,8位整数位,23位小数位

      --[[
      	读float
      	bytes 4字节byte数组
      --]]
      function DecodePBData:getFloat(bytes)
      	if not bytes or #bytes < 4 then return end
      	local sign = 1
      	local mantissa = bytes[2] % 2 ^ 7
      	for i = 3, 4 do
      		mantissa = mantissa * 256 + bytes[i]
      	end
      	if bytes[1] > 127 then sign = -1 end
      	local exponent = (bytes[1] % 128) * 2 + math.floor(bytes[2] / 2 ^ 7)
      
      	if exponent == 0 then
      		return 0
      	end
      	mantissa = (math.ldexp(mantissa, -23) + 1) * sign
      	return math.ldexp(mantissa, exponent - 127)
      end
      

      未解决的点:

      虽然对于fixed32,sfixed32,float都能够单独反序列化。但是对于得到的序列化数据,却无法判断是对应fixed32,sfixed32,float中的哪种类型。

    5. 日志处理

      在早期做解析文件时,需要对每个方法进行测试,了解各个方法解析中的过程,需要较多的日志。后期完成时,只需几个重要的信息日志,模拟了Android中的日志分级打印,加了日志。

      Protocol Buffer 分析

  • 验证test.lua

    在接收到socket数据时,调用编译好的test.exe

    Protocol Buffer 分析

    在游戏运行过程中,对于每个协议的数据,都会调用test.exe进行解析,运行了一段时间后,解析的结果如下:[test.exe解析结果](file://F:\资料\pb分析\游戏中运行一段时间后解析结果.txt)

五、总结

​ 虽然现在的插件已经能够适用于工作中的游戏,但在编写test.lua解析pb插件时,可以看出还有一些点没有解决。下一版本的解析插件可以从以下几个点进行优化:

  1. 对这一版本中未解决的点或需要优化的点就行优化。
  2. 整体的代码可以优化下。
  3. 对apk进行解包,考虑如何运用解包后的xxx.pb文件。

相关文章: