文件来自于前几天CyBRICS 2021中的lx100题目,因为做题时候被IP分片坑到了,发现自己对于网络这一块的知识掌握的并不好,所以写一篇文章来理一下。为了省事就直接用比赛的pcap文件做样例了:点击。
从Wireshark抓包来看IP分片
UDP/lPv4分片
一般来说我们都知道MTU是1500字节,因此超过1500字节的数据就需要进行ip分片。包含源和目的端口号的UDP头部只出现在第一个分片里,分片由IPv4头部中的标识(Identification)、分片偏移(Fragment offiet)和更多分片(More Fragments, MF)字段控制。
- 这些分片的标识(Identification)都是同样的而且分片偏移(Fragment offiet)是以8字节为单位的偏移。同样的标识和不同的偏移让接收方可以对分片进行重组。
- 而更多分片(More Fragments, MF)标志指明该数据报后面是否还有更多的分组, 只有最后一个分片才应置成0。当MF = 0的分片被接收到时, 重组程序才能确定原始数据报的长度, 它等于分片偏移字段的值(乘以8)加上当前分片IPv4总长度字段的值。
下图是一个长度为3000字节的udp包(包括了udp包头的8个字节)。原始数据报要加上ipv4包头为3020字节,它超过了MTU的限制,因此发送的时候会进行分片。
- 第一个分片中实际传送了1472字节的udp数据和8字节udp包头。此时偏移为0,因为接下来还有数据包要发因此MF置为1
- 第二个分片传送1480字节的udp数据,因为第一个分片已经传送了1480,因此这里的偏移为1480/8 = 185,因为接下来还有数据包要发因此MF置为1
- 最后一个分片传送剩下的40字节数据,并将MF置为0,此时接收方可以通过370*8+60来算出原始数据报的长度,因此也知道了udp包的大小为3000字节(减掉ip包头的20字节)。
wireshark中的视角
比赛用的这个pcap文件实际上抓的是照相机用udp传输图片的过程,我们以一个udp传输为例: 我们发现它和我们上一节讲的不太一样,UDP包按理应该是第一个出现,之后跟着一串分片后的IPv4包。而打开上面截图里所有的IPv4包我们发现它们的MF标志全都是1,且最后的udp包的length明显大于了MTU。这是因为wireshark的首选项中自动开启了重组分片数据的选项。这个默认开启的选项能直接帮我们重组原udp数据包并用它替换了MF=0的那个IPv4数据包,因此在开启该选项时我们是找不到MF=0的数据包的。 为了更好的理解这个过程我们关闭这个选项。可以看到下面的样子: 我们打开第一个UDP包,它的数据如下: 我们可以知道虽然它包头中的长度给出的Length为8537(包含8个字节包头,实际数据为8529字节),但是真实的Data里只有1472字节,1472+8(UDP包头)+20(IP包头) = MTU。我们再看这一组最后一个IPv4包: 我们可以看到它的MF=0,因此计算925*8+1157 = 8557,这是原始的数据报文,减掉20字节的IP包头,恰好为8537字节。与UDP包中所给的Length值一样。
回到这是一道ctf题目
上文也说了这是一道照相机传图片的题,因此一个udp包就是一张图,我们只需从pcap文件里提取图片就行。 用pyshark库对pcap文件进行处理,注意首选项中的改动是会影响到pyshark这个库的设置的。是否打开重组IPv4数据包选项会影响到UDP流的追踪结果(这很显然啦,毕竟两个选项下udp的DATA大小都不一样了)。 直接贴一手队友的代码(开启重组数据包的选项):
import pyshark
def get_jpeg(data, cnt):
start = data.find('ffd8ff')
data = data[start:]
l = len(data)//2
bs = int(data, base=16).to_bytes(l, 'big')
with open(f'jpgs/{cnt}.jpeg', 'wb') as f:
f.write(bs)
pcap = pyshark.FileCapture('lx100.pcap', display_filter='udp', include_raw=True, use_json=True)
cnt = 0
for p in pcap:
if p.highest_layer == 'DATA_RAW':
data = str(p['UDP'].get('payload_raw')[0])
if data.endswith('ffd9'):
cnt += 1
get_jpeg(data, cnt)
|