前言:先说下今天遇到的一个问题。和外部门同学对接消息记录拉取,采用的是pb协议,接口机的L5暴露给对方用以拉取。对方请求后表示IO超时没有收到对应回包,于是对方质疑我方有问题。现需要排查原因,更准确说如何找证据打脸对方。
分析:我方业务是相对成熟的,这种问题大概率是对方发过来的请求不对,例如缺少某些字段导致接口机无法将请求转发给后方业务机。注:经过沟通发现对方不能打印请求体(具体原因也不管了)。思路无非如下(其中第二点是此次研究的重点):
①首先tcpdump抓包看看我方究竟有没有收到对方的请求;
②如果收到请求尝试将抓包的数据反解得到对方实际发送过来的结构体,打脸对方。
已知数据组包格式如下,关于head和body的pb结构体这里不做详述。下面重点演示第二个问题。
STX_C_0x5B = 0x5b #这个就是'['的16进制ascii码
ETX_C_0x5D = 0x5d #这个就是']'的16进制ascii码
STX_C_0x5B+cmd(4)+seq(4)+headlen(4)+bodylen(4)+Head(pb)+Body(pb)+ETX_C_0x5D
1、tcpdump在接口机上抓取数据包(对方发过来的请求),并输出为.cap文件。
对应cap文件参见 这里
tcpdump -i eth1 host 9.138.238.218 -X -w aa.cap
2、将.cap文件用wiresharks打开进行分析如下:
此时祭出如下这张图(将TCP Head换成此处的UDP Head),一毛一样。观察上图可以看到各个方面都符合预期,接下来要做的事情就是把其中的数据部分转换解析并打印出来。
3、解析并打印data部分
(0)选中date域→右键→copy→ 总共有如下选项,各个选项对应的数据如下:
坑点:这里是在 是在实际传输数据处copy,如果在 阴影处 复制的话实际上赋值的是完整的数据包(这种情况你要自己截取data域)!!
#as Hex Dump
0000 5b 00 04 a0 73 c0 71 1f 14 00 00 00 16 00 00 00
0010 11 08 02 12 12 08 01 18 f3 c0 12 20 94 be c4 83
0020 0c 42 04 08 00 20 00 9a 87 94 01 0c 08 00 10 00
0030 18 00 20 00 28 0f 30 01 5d
#as Printable text
[ sÀqóÀ ¾ÄB (0]
#as a Hex Stream
5b0004a073c0711f14000000160000001108021212080118f3c0122094bec4830c4204080020009a8794010c0800100018002000280f30015d
#as a raw binary
#as a Escaped string
"\x5b\x00\x04\xa0\x73\xc0\x71\x1f\x14\x00\x00\x00\x16\x00\x00\x00" \
"\x11\x08\x02\x12\x12\x08\x01\x18\xf3\xc0\x12\x20\x94\xbe\xc4\x83" \
"\x0c\x42\x04\x08\x00\x20\x00\x9a\x87\x94\x01\x0c\x08\x00\x10\x00" \
"\x18\x00\x20\x00\x28\x0f\x30\x01\x5d"
显然其中的escaped string(转义字串)应该就是我们之前所说的二进制流了。
(1)将二进制流的换行、引号等去掉,赋值给buf
buf=b'\x5b\x00\x04\xa0\x73\xc0\x71\x1f\x14\x00\x00\x00\x16\x00\x00\x00\x11\x08\x02\x12\x12\x08\x01\x18\xf3\xc0\x12\x20\x94\xbe\xc4\x83\x0c\x42\x04\x08\x00\x20\x00\x9a\x87\x94\x01\x0c\x08\x00\x10\x00\x18\x00\x20\x00\x28\x0f\x30\x01\x5d'
(2)然后进入python交互模式进行解析
import struct
import com.tencent.epc.innerprocess.cloudcc_pb2 as cloudcc
import com.tencent.epc.innerprocess_pb2 as innerprocess
buf=b'\x5b\x00\x04\xa0\x73\xc0\x71\x1f\x14\x00\x00\x00\x16\x00\x00\x00\x11\x08\x02\x12\x12\x08\x01\x18\xf3\xc0\x12\x20\x94\xbe\xc4\x83\x0c\x42\x04\x08\x00\x20\x00\x9a\x87\x94\x01\x0c\x08\x00\x10\x00\x18\x00\x20\x00\x28\x0f\x30\x01\x5d'
prefix_len = 1 + 4 + 4 + 4 + 4
suffix_len = 1
str_cmd_seq_len = buf[1:17]
(cmd, seq, head_len, body_len) = struct.unpack('!IIII', str_cmd_seq_len)
如下图(cmd,seq,head_len,body_len)都是可以正确解析出来的。
接下来解析head和body。
head = innerprocess.Head()
reqbody = cloudcc.ReqBody()
str_head = buf[prefix_len:prefix_len + head_len]
head.ParseFromString(str_head)
str_body = buf[prefix_len + head_len:prefix_len + head_len + body_len]
reqbody.ParseFromString(str_body)
如下图可以看到head、reqbody也都被解析出来了。
显然字段乱填,成功打脸对方!!!!
注意事项:
1、在解析过程中时刻留意下wireshark上显示的各部分数据长度如何,和自己copy下来的len(buf)是否相等。
2、在处理过程中如果没能按照自己预想成功解析。可以借助别的手段辅助定位。
例如:对于xrpc协议打包格式如下。
对于抓到的一个具体数据包wireshark看到data长度为186。
且赋值data域得到二进制流如下。
buf="\x09\x30\x00\x00\x00\x00\x00\xba\x00\x94\x00\x00\x00\x00\x00\x00\x18\xb6\xec\xee\xfb\x03\x2a\x0e\x75\x63\x5f\x6d\x73\x67\x5f\x65\x74\x6c\x5f\x73\x76\x72\x32\x2a\x62\x61\x73\x69\x63\x2e\x61\x63\x63\x6f\x75\x6e\x74\x2e\x71\x69\x64\x69\x61\x6e\x5f\x61\x63\x63\x6f\x75\x6e\x74\x2e\x51\x69\x64\x69\x61\x6e\x41\x63\x63\x6f\x75\x6e\x74\x3a\x41\x2f\x62\x61\x73\x69\x63\x2e\x61\x63\x63\x6f\x75\x6e\x74\x2e\x71\x69\x64\x69\x61\x6e\x5f\x61\x63\x63\x6f\x75\x6e\x74\x2e\x51\x69\x64\x69\x61\x6e\x41\x63\x63\x6f\x75\x6e\x74\x2f\x47\x65\x74\x4d\x61\x73\x74\x65\x72\x42\x79\x53\x75\x62\x43\x6f\x72\x70\x75\x69\x6e\x4a\x0d\x0a\x09\x69\x31\x38\x6e\x5f\x6c\x61\x6e\x67\x12\x00\x08\xf2\xba\x84\xd0\x0a\x12\x0e\x75\x63\x5f\x6d\x73\x67\x5f\x65\x74\x6c\x5f\x73\x76\x72"
其实我们就可以对这个buf进行遍历都按照unsigned int的方式去解析看看究竟哪里能解出来186,进而辅助我们定位原因。工具机→/data/home/shuozhuo/myInner → find_pos.py
#!/usr/bin/python
#coding:utf-8
import struct
import random
import socket
import sys
import time
buf="\x09\x30\x00\x00\x00\x00\x00\xba\x00\x94\x00\x00\x00\x00\x00\x00\x18\xb6\xec\xee\xfb\x03\x2a\x0e\x75\x63\x5f\x6d\x73\x67\x5f\x65\x74\x6c\x5f\x73\x76\x72\x32\x2a\x62\x61\x73\x69\x63\x2e\x61\x63\x63\x6f\x75\x6e\x74\x2e\x71\x69\x64\x69\x61\x6e\x5f\x61\x63\x63\x6f\x75\x6e\x74\x2e\x51\x69\x64\x69\x61\x6e\x41\x63\x63\x6f\x75\x6e\x74\x3a\x41\x2f\x62\x61\x73\x69\x63\x2e\x61\x63\x63\x6f\x75\x6e\x74\x2e\x71\x69\x64\x69\x61\x6e\x5f\x61\x63\x63\x6f\x75\x6e\x74\x2e\x51\x69\x64\x69\x61\x6e\x41\x63\x63\x6f\x75\x6e\x74\x2f\x47\x65\x74\x4d\x61\x73\x74\x65\x72\x42\x79\x53\x75\x62\x43\x6f\x72\x70\x75\x69\x6e\x4a\x0d\x0a\x09\x69\x31\x38\x6e\x5f\x6c\x61\x6e\x67\x12\x00\x08\xf2\xba\x84\xd0\x0a\x12\x0e\x75\x63\x5f\x6d\x73\x67\x5f\x65\x74\x6c\x5f\x73\x76\x72"
for i in range(len(buf)-4):
print('pos:',i, struct.unpack('!I', buf[i:i+4]))
对于我的这个buf,执行结果如下。显然[4:8)这四个细节就是示意图中的“数据总大小”。
3、如果发送的请求数据包看起来都正常但是依然没能收到相应怎么办?
答:在被请求机器上抓取一个正常业务的好用的请求,然后解析出来原模原样的填写请求参数,这种情况肯定是能通的。接下来就可以分析究竟是哪个参数导致的没响应。
注:可能会有些我们不知道或者没留意到的约定,例如对于xrpc协议示意图中的魔数就要写死2x5x;否则接受方就会认为是不合法的请求直接丢掉。
魔数的作用:其实就是快速的识别字节流是否能够被程序处理。如果不能处理立马放弃,而不是白白浪费资源。
举个例子如果开发了一个服务器自定义了一个协议没有魔数,那么任何数据报传递到服务器服务器都会根据自定义协议来进行处理,包括不符合自定义协议规范的数据包。服务器闭着眼解析,解析完了后才发现不对劲就白白浪费了资源。相反服务器如果能先阅读到魔数第一时间判断是否是有效数据包。另外为了安全一般对于不符合预期的数据包会直接关闭连接以节省资源。
4、对照着ascii码表,我们可以对每个字节进行验证。
背景:我以为同样的请求参数但是python脚本发送的请求是有回包的,但是C++程序发出去的缺没有回包。
#python发出去的buf是好用的
pbuf=b"\x09\x30\x00\x00\x00\x00\x00\xff\x00\x87\x00\x00\x00\x00\x00\x00\x18\x80\xee\xe0\xb4\x03\x2a\x17\x75\x63\x5f\x6d\x73\x67\x5f\x72\x65\x63\x6f\x72\x64\x5f\x77\x72\x69\x74\x65\x5f\x73\x76\x72\x32\x24\x74\x72\x70\x63\x2e\x6b\x6e\x6f\x63\x6b\x6e\x6f\x63\x6b\x2e\x61\x75\x74\x68\x43\x65\x6e\x74\x65\x72\x2e\x41\x75\x74\x68\x43\x65\x6e\x74\x65\x72\x3a\x31\x2f\x74\x72\x70\x63\x2e\x6b\x6e\x6f\x63\x6b\x6e\x6f\x63\x6b\x2e\x61\x75\x74\x68\x63\x65\x6e\x74\x65\x72\x2e\x41\x75\x74\x68\x43\x65\x6e\x74\x65\x72\x2f\x47\x65\x74\x41\x75\x74\x68\x49\x6e\x66\x6f\x4a\x0d\x0a\x09\x69\x31\x38\x6e\x5f\x6c\x61\x6e\x67\x12\x00\x0a\x24\x34\x32\x37\x63\x35\x37\x35\x63\x2d\x37\x36\x38\x33\x2d\x34\x33\x64\x30\x2d\x39\x30\x30\x35\x2d\x39\x33\x66\x33\x34\x30\x33\x39\x62\x35\x30\x31\x12\x40\x34\x62\x39\x39\x37\x32\x31\x38\x39\x63\x65\x62\x61\x36\x37\x64\x34\x30\x35\x32\x37\x31\x63\x62\x39\x39\x63\x38\x61\x64\x63\x39\x65\x30\x32\x38\x32\x31\x38\x61\x61\x62\x63\x65\x32\x64\x64\x65\x66\x36\x61\x34\x31\x62\x63\x39\x66\x36\x63\x38\x62\x63\x65\x32"
#C++发出去的buf就一直报错
cbuf=b"\x09\x30\x00\x00\x00\x00\x00\xff\x00\x87\x00\x00\x00\x00\x00\x00\x18\x80\xee\xe0\xb4\x03\x2a\x17\x75\x63\x5f\x6d\x73\x67\x5f\x72\x65\x63\x6f\x72\x64\x5f\x77\x72\x69\x74\x65\x5f\x73\x76\x72\x32\x24\x74\x72\x70\x63\x2e\x6b\x6e\x6f\x63\x6b\x6e\x6f\x63\x6b\x2e\x61\x75\x74\x68\x43\x65\x6e\x74\x65\x72\x2e\x41\x75\x74\x68\x43\x65\x6e\x74\x65\x72\x3a\x31\x2f\x74\x72\x70\x63\x2e\x6b\x6e\x6f\x63\x6b\x6e\x6f\x63\x6b\x2e\x61\x75\x74\x68\x43\x65\x6e\x74\x65\x72\x2e\x41\x75\x74\x68\x43\x65\x6e\x74\x65\x72\x2f\x47\x65\x74\x41\x75\x74\x68\x49\x6e\x66\x6f\x4a\x0d\x0a\x09\x69\x31\x38\x6e\x5f\x6c\x61\x6e\x67\x12\x00\x0a\x24\x34\x32\x37\x63\x35\x37\x35\x63\x2d\x37\x36\x38\x33\x2d\x34\x33\x64\x30\x2d\x39\x30\x30\x35\x2d\x39\x33\x66\x33\x34\x30\x33\x39\x62\x35\x30\x31\x12\x40\x34\x62\x39\x39\x37\x32\x31\x38\x39\x63\x65\x62\x61\x36\x37\x64\x34\x30\x35\x32\x37\x31\x63\x62\x39\x39\x63\x38\x61\x64\x63\x39\x65\x30\x32\x38\x32\x31\x38\x61\x61\x62\x63\x65\x32\x64\x64\x65\x66\x36\x61\x34\x31\x62\x63\x39\x66\x36\x63\x38\x62\x63\x65\x32"
使用python工具直接发送C++对应的buf报了和C++程序同样的错误提示。显然,就是cbuf有问题。经过和ascii码表比对可以逐个字节解释含义。如下pbuf中"c"对应到cbuf中是“C”,对cbuf修改对应位置的参数即解决问题。
↓
t r p c . k n o c k n 0 c k . a u t h c/C e n t e r . A u t h C e n
\x2f\x74\x72\x70\x63\x2e\x6b\x6e\x6f\x63\x6b\x6e\x6f\x63\x6b\x2e\x61\x75\x74\x68\x63\x65\x6e\x74\x65\x72\x2e\x41\x75\x74\x68\x43\x65\x6e\ pbuf相关片段
\x2f\x74\x72\x70\x63\x2e\x6b\x6e\x6f\x63\x6b\x6e\x6f\x63\x6b\x2e\x61\x75\x74\x68\x43\x65\x6e\x74\x65\x72\x2e\x41\x75\x74\x68\x43\x65\x6e\ cbuf相关片段