前言
学过计算机网络协议的人应该都深有体会,网络协议知识点多而复杂,小编在刚开始学习的时候也是被各种协议搞得晕头转向,各层协议,各种协议报文格式,各个字段代表的含义...,小编目前所接触到的学习网络协议的方法有以下三种:
(1)通过看书和看视频。这是很普遍的方式了,学校教育基本这种,适合考试
(2)除了看书和视频,利用抓包软件(wireshark,科来网络分析系统)抓包,看看数据包到底长啥样,再对照书和视频来理解。有一定实操,适合工作,是比较有效的方式
(3)就是小编最近参加的一个星球活动,直接手写一个抓包软件,要求对计算机网络协议有一定了解且有一定的编程基础。个人觉得这是史上最强的学习网络协议的方式了,适合想对网络协议有更深理解或者想提升编程能力的人。
活动形式是这样的:星主把整个任务分成了三个阶段,每个阶段的任务再合理的细分到每天的量,每天发布学习任务和对应的资料,再布置作业,然后参加者根据给出的资料自己动手、思考完成作业并在星球提交。目前已结束第一阶段。
本系列文章将记录参与活动的整个过程(希望能全程跟下来),作为学习笔记和自己成长历程的记录。由于本人非科班专业出身,故选择易学的python作为编程语言(本人代码水平很烂,大佬勿喷)
附上一张星主此次活动的宣传海报:
大家可以关注下大佬的微信公众号:编程技术宇宙
内容绝对干货,以故事形式给你说清网络协议、操作系统、CPU这些计算机中枯燥晦涩的知识点,关注快两年了,推荐给大家。
第一阶段历时三周(周六周日不另外发布任务,自己用来总结和回顾,完善代码),包括13天的作业布置,外加最后的收官竞赛(神仙打架):
https://mp.weixin.qq.com/s/cQKq6H983Xy9cuYaVpdDeg
如果有和我一样想对网络协议有更深理解且想提升编程能力的师傅们,欢迎加入进来,人多才好玩~(嘿哈)
今天的内容是第一阶段的收官竞赛,花了我一个下午 + 晚上的时间来写代码和修改代码。
第一阶段收官竞赛
任务:解析第8-9天的pcap文件,遍历文件中的所有数据包,解析以太网、IP协议、ARP协议、UDP协议、DNS协议,解析结果分别输出到4个文件:
> - arp.txt > - ip.txt > - udp.txt > - dns.txt 按以下格式输出:
竞赛规则:
优化程序,追求性能和准确率两项指标。性能:在启动解析时打印时间戳,在解析结束时再打印时间戳,计算解析耗费的时间(精确到毫秒) 准确率:将分别统计查阅四个文件的输出内容是否完备和准确。
将使用大家的程序去分析一个1GB的数据包文件,评估程序的性能。
奖励:综合表现最佳的同学,将获得《大话计算机》系列图书,价值400+RMB。
可以看到奖励是相当丰厚的,但得能者才能拿到。我用的是python,运行速度自然是比不上C/C ++,go大佬的,所以一开始就躺平了,Life is short,using python~,只要能跑进行。下面就来说说这道题我是怎么做的吧~
拿到题目时的想法是解析完DNS协议后,如果只要求能跑起来,那这道题其实并不难
(1)只需要将前面的代码做个整合,把之前的打印语句换成题目中所要求的,整合完代码,看下是否能正常运行,若能正常运行,进行第(2)步;
(2)在解析前获得一个系统的时间,解析完之后再获得一个系统时间,两个时间相减,就是解析花费的时间
(3)前两步没问题之后,再讲打印语句换成写入到文件的操作就大功告成
于是经过一番思索,代码实现的总体思路如下:
根据这个思路,搭起了框架:
注:第一个else应为若没有DNS协议报文,图为copy后忘记改了。
当我将这个框架发到群里时,引来了各位大佬的发言:
轩辕之风大佬给我举了个简单例子:
并给出了箴言:
没有开发经验的我,代码写得实在是太烂了,一开始就没想着要用面向对象的思想。由于作业是赶上来的,没时间大改了,于是我完成了如下shi山堆积的代码:
import mmap,binascii,time,os,datetime
def big_small_convert(data): #小端模式转化为大端模式
return binascii.hexlify(binascii.unhexlify(data)[::-1])
def time_stamp_convert(hexstr): #16进制字符串时间戳转化为年月日时分秒
stamp = int(hexstr,16)
timeArray = time.localtime(stamp)
otherStyleTime = time.strftime("%Y-%m-%d %H:%M:%S", timeArray)
return otherStyleTime
def mac_address_format(start,end): #格式化MAC地址
j = 1
s = ""
for i in range(start,end):
if j % 6 == 0:
s += (m.read(1).hex().upper() + "")
else:
s += (m.read(1).hex().upper() + "-")
j += 1
i += 1
return s
def eth_protocol_identify(start): #识别以太网帧中type字段所代表的协议类型
type_code = m[start:start + 2].hex()
if type_code == "0800":
s = "IPV4"
elif type_code == "0806":
return "ARP"
elif type_code == "0835":
s = "RARP"
elif type_code == "86dd":
s = "IPV6"
else:
s = "unknow"
return s
def ip_convert(start,end): #将十六进制ip转换为点分十进制
j = 1
s = ""
for i in range(start,end):
if j % 4 == 0:
hex_num = m.read(1).hex()
num = int(hex_num,16)
s += (str(num) + "")
else:
hex_num = m.read(1).hex()
num = int(hex_num,16)
s += (str(num) + ".")
j += 1
i += 1
return s
def dns_qr_identity(dns_qr_index): #判断DNS数据包是查询还是响应
result = m[dns_qr_index:dns_qr_index + 1].hex()
result = result[0]
result = bin(int(result,16))[2:]
result = result[0]
return result
def dns_quer_convert(dns_quer_index): #将对应十六进制段转换为dns查询的域名,
#返回查询名区域长度末尾(回答区域头部)字节序号和域名
domain = ""
while True:
domain_length = m[dns_quer_index:dns_quer_index + 1].hex()
if domain_length == "00":
break
domain_length = int(domain_length,16)
s = m[dns_quer_index + 1:dns_quer_index + 1 + domain_length].hex()
s = hexstr_to_asciistr(s) + "."
domain += s
dns_quer_index = dns_quer_index + 1 + domain_length
dns_resp_head_index = dns_quer_index + 5
return [domain, dns_resp_head_index]
def hexstr_to_asciistr(hexstr): #将16进制字符串转化为ascii字符串
asciistr = "".join([chr(int(b, 16)) for b in [hexstr[i:i+2] for i in range(0, len(hexstr), 2)]])
return asciistr
def dns_resp_ip_address(dns_answer_rrs_index,dns_resp_head_index): #读取dns响应包中回答区域中的A记录IP
dns_answer_rrs_number = m[dns_answer_rrs_index:dns_answer_rrs_index + 2].hex()
dns_answer_rrs_number = int(dns_answer_rrs_number,16)
ip_list = []
for i in range(0,dns_answer_rrs_number):
quer_type_index = dns_resp_head_index + 2
data_length_index = quer_type_index + 8
quer_type = m[quer_type_index:quer_type_index + 2].hex()
quer_type = int(quer_type,16)
data_length = m[data_length_index:data_length_index + 2].hex()
data_length = int(data_length,16)
if quer_type == 1:
ip_address_index = quer_type_index + 10
m.seek(ip_address_index)
ip_address = ip_convert(ip_address_index,ip_address_index + 4)
ip_list.append(ip_address)
dns_resp_head_index = data_length_index + 2 + data_length
else:
dns_resp_head_index = data_length_index + 2 + data_length
i += 1
return ip_list
if __name__ == '__main__':
f1 = open("arp.txt",'w')
f2 = open("ip.txt",'w')
f3 = open("udp.txt",'w')
f4 = open("dns.txt",'w')
start_time = datetime.datetime.now().microsecond
print("开始解析pcap文件,当前时间为: {}".format(start_time))
size = os.path.getsize('day8.pcap')
with open('day8.pcap','rb') as f:
with mmap.mmap(f.fileno(),0,access = mmap.ACCESS_READ) as m:
type_index = 52 #数据包Ethernet II报文的Type字段字节序号,第一个为52
total = 0 #记录总数据包数
arp_number = 0 #记录arp协议报文数
ipv4_number = 0 #记录ipv4协议报文数
ipv4_udp_number = 0 #记录ipv4 udp协议报文数
dns_number = 0 #记录ipv4基于udp的dns协议报文数
domain_ip_map = {} #域名与IP的对应关系
while True: #循环遍历数据包
total += 1
protocol_type = eth_protocol_identify(type_index) #识别Ethernet报文中Type字段代表的协议类型
if(protocol_type != "ARP" and protocol_type != "IPV4"): #若为其他协议则跳到下一个数据包EthernetII报文的Type字段
packet_filed_index = type_index - 20
packet_length = m[packet_filed_index:packet_filed_index + 4].hex() #读取包头中Caplen(4B)字段
packet_length = big_small_convert(packet_length) #小端模式转化为大端模式
packet_length = int(packet_length,16) #16进制转十进制
type_index = type_index + packet_length + 16
if type_index < size:
continue
else:
break
else: #若为ARP或IPV4,则先求出时间和包的长度,再分别进入解析
packet_filed_index = type_index - 20 #当前数据包包头中Caplen(4B)字段所在的起始位置
packet_length = m[packet_filed_index:packet_filed_index + 4].hex() #读取包头中Caplen(4B)字段
packet_length = big_small_convert(packet_length) #小端模式转化为大端模式
packet_length = int(packet_length,16) #16进制转十进制
time_filed_index = type_index - 28
data = m[time_filed_index:time_filed_index + 4].hex() #读取包头中Timestamp(4B)字段
result = big_small_convert(data) #小端模式转化为大端模式
standard_time = time_stamp_convert(result) #16进制字符时间戳转化为年月日时分秒
s = "> - [{}] {} Bytes ".format(standard_time, str(packet_length))
if (protocol_type == "ARP"): #若为ARP则进入到ARP协议解析
arp_number += 1
arp_smac_index = type_index + 10 # 当前数据包目的MAC、源MAC、目的IP、源IP的起始位置
arp_src_ip_index = arp_smac_index + 6
arp_dmac_index = arp_src_ip_index + 4
arp_dst_ip_index = arp_dmac_index + 6
opcode_index = type_index + 8 #ARP包的操作类型字段的起始位置
m.seek(arp_smac_index) # 读取目的MAC、源MAC、目的IP、源IP并按格式输出
arp_smac_address = mac_address_format(arp_smac_index, arp_src_ip_index)
arp_src_ip = ip_convert(arp_src_ip_index, arp_dmac_index)
arp_dmac_address = mac_address_format(arp_dmac_index, arp_dst_ip_index)
arp_dst_ip = ip_convert(arp_dst_ip_index, arp_dst_ip_index + 4)
opcode = m[opcode_index:opcode_index + 2].hex()
opcode = int(opcode,16)
s_arp = s + "{} {} ".format(arp_smac_address,arp_dmac_address)
if opcode == 1:
# print(s_arp + "查询{}的MAC地址".format(arp_dst_ip))
f1.write(s_arp + "查询{}的MAC地址n".format(arp_dst_ip))
else:
# print(s_arp + "响应{}的MAC地址".format(arp_src_ip))
f1.write(s_arp + "响应{}的MAC地址n".format(arp_src_ip))
type_index = type_index + packet_length + 16 #解析完成,跳到下一个数据包的Ethernet II报文的Type字段
if type_index < size:
continue
else:
break
else: #若为IPV4,则进入到IPV4协议报文的解析
ipv4_number += 1
dmac_index = packet_filed_index + 8 #当前数据包包头目的MAC、源MAC的起始位置
smac_index = dmac_index + 6
src_ip_index = smac_index + 20
dst_ip_index = src_ip_index + 4
m.seek(dmac_index)
dmac_address = mac_address_format(dmac_index,smac_index)
smac_address = mac_address_format(smac_index,smac_index + 6)
m.seek(src_ip_index)
src_ip = ip_convert(src_ip_index,dst_ip_index)
dst_ip = ip_convert(dst_ip_index,dst_ip_index + 4)
s_ipv4 = s + "{} {} ".format(smac_address,dmac_address)
# print(s_ipv4 + "{} {}".format(src_ip,dst_ip))
f2.write(s_ipv4 + "{} {}n".format(src_ip,dst_ip))
ipv4_protocol_index = type_index + 11
ipv4_protocol = m[ipv4_protocol_index:ipv4_protocol_index + 1].hex()
ipv4_protocol = int(ipv4_protocol,16)
if(ipv4_protocol == 17): #若有UDP协议报文,则继续进入解析
ipv4_udp_number += 1
src_port_index = dst_ip_index + 4
dst_port_index = src_port_index + 2
src_port = m[src_port_index:dst_port_index].hex()
src_port = int(src_port,16)
dst_port = m[dst_port_index:dst_port_index + 2].hex()
dst_port = int(dst_port,16)
s_udp = s_ipv4 + "IPV4 {} {} {} {} ".format(src_ip,dst_ip,str(src_port),str(dst_port))
# print(s_udp)
f3.write(s_udp + "n")
if(src_port == 53 or dst_port == 53): #若有DNS协议报文,则继续进入解析
dns_number += 1
dns_qr_index = dst_port_index + 8
dns_quer_index = dns_qr_index + 10
dns_qr = dns_qr_identity(dns_qr_index)
dns_quer_result = dns_quer_convert(dns_quer_index)
dns_quer_domain = dns_quer_result[0].strip(".")
if dns_qr == "0":
s_dns_req = s_udp + "DNS请求 查询域名{}的地址".format(dns_quer_domain)
# print(s_dns_req)
f4.write(s_dns_req + "n")
else:
if dns_quer_domain not in domain_ip_map:
dns_answer_rrs_index = dns_qr_index + 4
dns_resp_head_index = dns_quer_result[1]
dns_resp_ip_list = dns_resp_ip_address(dns_answer_rrs_index,dns_resp_head_index)
domain_ip_map[dns_quer_domain] = dns_resp_ip_list
s_dns_rep = s_udp + "DNS响应 域名{}的地址是{}".format(dns_quer_domain,", ".join(domain_ip_map[dns_quer_domain]))
# print(s_dns_rep)
f4.write(s_dns_rep + "n")
type_index = type_index + packet_length + 16 #DNS协议解析完成,跳到下一个数据包的Ethernet II报文的Type字段
if type_index < size:
continue
else:
break
else:
type_index = type_index + packet_length + 16 #若无DNS协议报文,则跳到下一个数据包的Ethernet II报文的Type字段
if type_index < size:
continue
else:
break
else:
type_index = type_index + packet_length + 16 #若没有UDP协议报文,跳到下一个数据包的Ethernet II报文的Type字段
if type_index < size:
continue
else:
break
f1.close()
f2.close()
f3.close()
f4.close()
f.close()
end_time = datetime.datetime.now().microsecond
spend_time = end_time - start_time
print("解析结束,结束时间为:{},花费时间为{}us,合{}ms".format(end_time,spend_time,spend_time / 1000))
print("总共{}个数据包,其中ARP协议报文{}个,IPV4协议报文{}个,UDP协议报文{}个,DNS协议报文{}个".format(str(total),
str(arp_number),str(ipv4_number),str(ipv4_udp_number),str(dns_number)))
这份代码自己看着都难受,尤其是在sublime中改代码时(所有代码都是在sublime中完成的,一是练习手写代码的能力,二是代码在sublime中看着花花绿绿的,就感觉漂亮),放在pycharm中会针对if...else这类关键字进行折叠,对改代码还是比较好的。
运行结果准确无误:
使用1G数据包进行测试:
每次时间可能不太一样,但基本在10-11s之间
注:当使用datetime.datetime.now().microsecond获取时间时,1G数据包下可能跑出负数:
改为datetime.datetime.now()就不会出现这样的情况。
考核最终总共10位同学提交作业,在Linux 2核4GB内存的虚拟机上,分析同一个300MB,23W+数据包的pcap文件
程序正常运行+输出结果正确,才能作为有效成绩。最后对所有的有效成绩,根据分析所花的时间进行排名。
经过激烈比拼,排名结果如下:
[ ]Annihilation_choice:未成功运行
[ ]橘子🍊:未成功运行
第八名:[Python]厉害:11906 ms 11705 ms
第七名:[Java]小满同学:5787 ms 5829 ms
第六名:[Python]Walker: 1731 ms 1742 ms
第五名:[C]letangers: 925 ms 935 ms
第四名:[Go]我的朋友叫垃圾呆: 778 ms 794 ms
第三名:[Python]Mr.欧谢特 629 ms 631 ms
第二名:[C++]endless: 317 ms 318 ms
第一名:[C++]月: 146 ms 148 ms
语言的优势还是很明显的,最令人感到意外的是python大佬经过优化后,挤进前三甲,真的是太强了!
python大佬优化了两天,并分享了优化经验:
反正我是没完全听懂,听完感受就是:python虽然封装了底层的东西,但当我们对python代码进行优化时就不得不需要用到底层知识了,同样是敲代码,懂底层的可以走得更远,不懂底层的就是个码农。
此次竞赛的结果并不具有普遍性哦,程序能跑多快一方面跟使用的语言有关,另一方面也要看你是怎么写的,写得好的代码时间复杂度低,使用python做得好的话,也是可以快过go的。从我的视角来看,这次角逐是这样的:
C++大佬:C++本来就很快啊,我代码写得很烂的(太凡尔赛了)
Python大佬:花了两天做的优化,挤进前三甲,叫板C++(太强了)
C大佬:花时间造轮子,后面没时间做优化了
Java大佬:面向对象,封装得太深,运行速度慢了
我:Life is short,using python!能跑就行
此系列文章就暂时告一段落了,下一篇就等第二阶段了,大概是学生暑假时候。透露下第二阶段就要解析常用的TCP,HTTP协议了,会更复杂,更难解析。后续我会优化自己的代码,也采用封装、模块化的思想,以适应多协议、复杂协议的解析。
如果喜欢小编的文章,记得点赞+关注支持一下哦~
原文始发于微信公众号(沃克学安全):跟着轩辕之风大佬手写一个抓包软件|第一阶段:解析静态网络数据包文件(收官之战)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论