前言

SharpPcap 是.NET 环境中跨平台的抓包框架,对 WinPcap 和 LibPcap 进行了统一的封装,使用 C#语言

本人的毕设需要使用 WinPcap 进行抓包解析,还需要做一个 UI 界面,正好.NET 有这样一个库,同时还有 WPF 这样的 UI 框架,之前参与过 Android 项目,WPF 的 xaml 布局写法和 Android 很类似,上手 WPF 难度应该不算很高,综合考虑下选择使用 C#完成毕设(~~根本原因是 C++ 用不顺手​~💔💔💔)

理想是丰满的,现实是骨感的,当我兴致勃勃准备查找文档开始干的时候,发现怎么网上搜出来的例子跑不通。找到 GitHub 仓库,在 Tutorial 找到一篇文档,但是还是有例子跑不通,猜测是版本的问题,结果发现在 releases 中写“Please see nuget for releases”,这个 nuget 又是啥,咋还跑到那里去发布,后来了解到 nuget 是.NET 的包管理平台,类似 Java 的 Maven。一路搜索过去,倒是找到了 SharpPcap 的 Nuget 地址,但是还是找不到最新的文档,此时我的内心是崩溃的

没办法,只能硬着头皮看 Tutorial 的文档和反编译的源码慢慢调试了,在此记录一下 SharpPcap 新版本的 API 使用,SharpPcap 版本为 6.3.0

SharpPcap 的 GitHub 仓库:dotpcap/sharppcap

PacketDotNet 的 GitHub 仓库:dotpcap/packetnet

NuGet 地址:NuGet Gallery SharpPcap 6.3.0

SharpPcap 安装

SharpPcap 已经发布在 NuGet 上,所以我们可以直接通过 Visual Studio 的 NuGet 管理器获取安装,这里使用 Visual Studio 2022 版本

  1. 安装 SharpPcap

    打开项目的 NuGet 管理器,点击浏览,在搜索框搜索 SharpPcap,点击安装即可

  2. 查看依赖项

    安装完成后,我们可以看到项目中的依赖项已经有了 SharpPcap 的依赖,其中也包含一个叫 PacketDotNet 的库,SharpPcap 主要负责数据包的捕获,而 PacketDotNet 就是负责数据包的解析

  3. 在代码中使用 SharpPcap

    在代码中引入 SharpPcap 命名空间和 PacketDotNet 命名空间即可,打印版本检查是否可以正常使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    using System;
    using SharpPcap;
    using PacketDotNet;

    namespace backend {
    public class Backend {
    public static void Main(string[] args) {
    // Tutorial中获取版本为string ver = SharpPcap.Version.VersionString;
    var version = Pcap.Version;
    var sharpPcapVersion = Pcap.SharpPcapVersion;
    Console.WriteLine(version);
    Console.WriteLine($"SharpPcapVersion = {sharpPcapVersion}");
    }
    }
    }

    可以打印出 Npcap 版本和 SharpPcap 版本,接下来就可以愉快的使用了👏👏👏

获取接口列表

在 SharpPcap 中获取接口列表非常简单,只需要一行代码

1
2
3
4
5
6
var list = CaptureDeviceList.Instance;

// 打印接口信息
foreach (var device in list) {
Console.WriteLine(device);
}

获取到的 CaptureDeviceList 继承了 ReadOnlyCollection<ILiveDevice>,是一个 ILiveDevice 类型的只读集合,由此可见获取到的设备实例是 ILiveDevice 类型的

ILiveDevice 继承了 ICaptureDeviceIInjectionDevice 两个接口,这两个接口都继承了 IPcapDevice 接口,这两个接口中主要包含了各自功能模块的方法接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// ILiveDevice
public interface ILiveDevice : ICaptureDevice , IInjectionDevice {
}

// IInjectionDevice
public interface IInjectionDevice : IPcapDevice {
// 发送数据包
void SendPacket(ReadOnlySpan<byte> p, ICaptureHeader header = null);
}

// ICaptureDevice
public interface ICaptureDevice : IPcapDevice {

// 数据包捕获回调
event PacketArrivalEventHandler OnPacketArrival;

// 停止捕获回调
event CaptureStoppedEventHandler OnCaptureStopped;

// 是否开始捕获
bool Started { get; }

// 捕获超时时间
TimeSpan StopCaptureTimeout { get; set; }

// 开始异步捕获
void StartCapture();

// 停止捕获
void StopCapture();

// 开始同步捕获
void Capture();

// 捕获一个数据包
GetPacketStatus GetNextPacket(out PacketCapture e);

// 捕获统计信息
ICaptureStatistics Statistics { get; }
}

IPcapDevice 中包含了接口设备相关的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface IPcapDevice : IDisposable {

// 设备名
string Name { get; }

// 设备描述
string Description { get; }

// 最后一次发生的错误信息
string LastError { get; }

// 过滤表达式
string Filter { get; set; }

// 设备MAC地址
System.Net.NetworkInformation.PhysicalAddress MacAddress { get; }

// 打开设备
void Open(DeviceConfiguration configuration);

// 关闭设备
void Close();

// 设备的链路层类型
PacketDotNet.LinkLayers LinkType { get; }
}

打开接口并捕获

从上面的接口方法中,可以看到相关的打开、捕获等方法,基本使用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var devices = CaptureDeviceList.Instance;
// 获取第一个接口
var dev = devices[0];
// 设置捕获回调
device.OnPacketArrival += new PacketArrivalEventHandler(OnPacketArrival);
dev.Open(); // 打开接口
dev.StartCapture(); // 开始异步捕获
Console.ReadLine(); // 阻塞主进程
dev.StopCapture(); // 停止捕获
dev.Close(); // 关闭接口

// 回调捕获函数,捕获的包类型为PacketCapture
public static void OnPacketArrival(object sender, PacketCapture p) {
var data = p.Data; // 数据包数据,字节数组
var date = p.Timeval.Date; // 时间戳
// ......
}

实际上这里使用的 Open() 是一个扩展方法,IPcapDevice 接口中的 Open() 接收一个 DeviceConfiguration 类型的参数,表示启动配置,而在 CaptureDeviceExtensions.cs 文件中,对 Open()IInjectionDevice 接口的 SendPacket() 做了扩展

DeviceConfiguration 中有两个常用的属性

  • DeviceModes Mode:接口工作模式
    • None:默认模式
    • Promiscuous:混杂模式
  • int ReadTimeout:捕获超时时间

开启接口混杂模式和设置超时时间,可以调用 CaptureDeviceExtensions.cs 文件中的扩展

1
device.Open(mode: DeviceModes.Promiscuous, read_timeout: 1000);

捕获数据包

与 WinPcap 一样,SharpPcap 也有回调捕获和非回调捕获两种方式,在 SharpPcap 新版本中,将两种方式返回的数据包类型统一为了 PacketCapture 类型

回调捕获

ICaptureDevice 接口中有两个委托属性,可以定义回调函数

  • PacketArrivalEventHandler OnPacketArrival

    数据包捕获回调,函数类型为 void OnPacketArrival(object sender, PacketCapture e)

  • CaptureStoppedEventHandler OnCaptureStopped

    停止捕获回调,函数类型为 void OnCaptureStop(object sender, CaptureStoppedEventStatus status)

在调用 Open() 之前,设置接口的 OnPacketArrival 属性即可设置捕获回调

1
device.OnPacketArrival += new PacketArrivalEventHandler(OnPacketArrival);

非回调捕获

使用 ICaptureDevice 接口中的 GetNextPacket() 获取一个数据包,该函数接收一个 PacketCapture 类型的输出参数,PacketCapture 是数据包类型,返回值是 GetPacketStatus 枚举类型,表示捕获数据包的状态

1
2
3
4
5
6
7
8
9
10
public enum GetPacketStatus {
// 超时
ReadTimeout = 0,
// 捕获到数据包
PacketRead = 1,
// 捕获错误
Error = -1,
// 捕获中止
NoRemainingPackets = -2,
};

在 While 循环中持续获取数据包,不需要调用 StartCapture()

1
2
3
4
5
6
7
8
var device = devices[index];
device.Open(mode: DeviceModes.Promiscuous, read_timeout: 1000);

while (device.GetNextPacket(out PacketCapture packet) == GetPacketStatus.PacketRead) {
Console.WriteLine(packet.GetPacket().GetPacket());
}

device.Close();

过滤数据包

在 SharpPcap 中设置过滤数据包非常简单,只需要设置 IPcapDevice 接口中的 Filter 属性为过滤表达式即可

1
device.Filter = "ip6 and icmp6";  // 过滤ICMPv6包

数据包解析

数据包主要使用到 PacketDotNet 库,PacketDotNet 中将几乎所有类型的数据包都封装了实体类,而它们都继承了 Packet 这个抽象父类,其中也包含一些用于解析的方法,主要用到下面两个方法

  • Packet.ParsePacket():将 PacketCapture 解析为 Packet 对象,
  • Extract<T>():从 Packet 对象中提取指定数据包类型,返回相应的数据包对象

基本使用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static void OnPacketArrival(object s, PacketCapture packetCapture) {
var packet = Packet.ParsePacket(
packetCapture.Device.LinkType,
packetCapture.Data.ToArray()
);
// 也可通过以下方式获取
// var packet = packetCaptrue.GetPacket().GetPacket();

// 解析出来的首先是链路层对象,ParsePacket方法返回Packet父类引用,强转为子类
var ethernet = packet as EthernetPacket ?? throw new NullReferenceException();
var udp = ethernet.Extract<UdpPacket>();
if (udp != null) {
Console.WriteLine(udp);
}
}

数据包解析的部分工作原理详见:SharpPcap 数据包解析原理

堆文件处理

堆文件处理使用到 LibPcap 模块的功能,LibPcap 中接口的父类为 PcapDevice 类,该类实现了 ICaptureDevice 接口

写入堆文件

写入文件主要使用 CaptureFileWriterDevice 类,它继承了 PcapDevice 类,主要使用以下方法

  • CaptureFileWriterDevice():唯一构造器

    传入写入文件名和打开模式,默认为打开并创建

  • Open():打开接口

    传入 DeviceConfiguration,主要参数是链路层类型 LinkLayers,要与捕获接口的链路层类型一致

    CaptureDeviceExtensions.cs 中包含两个 Open 扩展函数

    • void Open(this CaptureFileWriterDevice device, ICaptureDevice captureDevice)
    • void Open(this CaptureFileWriterDevice device, LinkLayers linkLayerType = LinkLayers.Ethernet)
  • Write():写入文件,有两个重载

    • void Write(ReadOnlySpan<byte> p, ref PcapHeader h)
    • void Write(ReadOnlySpan<byte> p)
    • void Write(RawCapture p)

基本使用如下,注意在 Windows 中,文件的相对路径是相对于 .exe 可执行文件的路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 默认模式为打开并创建
CaptureFileWriterDevice writer = new("capture.pcap");
// 打开捕获接口
device.Open(mode: DeviceModes.Promiscuous, read_timeout: 1000);
// 打开写入接口
writer.Open(device);

device.OnPacketArrival += new(OnPacketArrival);
device.StartCapture();
Console.ReadLine();
device.StopCapture();
writer.Close();
device.Close();

private static void OnPacketArrival(object s, PacketCapture packetCapture) {
// 当文件模式不是追加时,在回调中打开写入接口,每次打开会清空文件
writer.Write(packetCapture.GetPacket());
}

读取堆文件

读取文件主要使用 CaptureFileReaderDevice 类,它继承了 PcapDevice 类,主要使用以下方法

  • CaptureFileReaderDevice():唯一构造器,传入读取的文件名
  • Open():打开接口
  • StartCapture():开始读取文件

基本使用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
CaptureFileReaderDevice reader = new("capture.pcap");
reader.Open();
// 设置读取到数据包的回调
reader.OnPacketArrival += new(OnPacketArrival);
// 开始读取
reader.StartCapture();
Console.ReadLine();
reader.StopCapture();
reader.Close();

private static void OnPacketArrival(object s, PacketCapture packetCapture) {
Console.WriteLine(packetCapture.GetPacket().GetPacket().PrintHex());
}

发送数据包

发送单个数据包

使用 IInjectionDevice 接口中的 SendPacket 方法,CaptureDeviceExtensions.cs 中包含该方法的四个扩展方法

  • void SendPacket(ReadOnlySpan<byte> p, ICaptureHeader header = null)
  • void SendPacket(this IInjectionDevice device, byte[] p, int size)
  • void SendPacket(this IInjectionDevice device, Packet p)
  • void SendPacket(this IInjectionDevice device, Packet p, int size)
  • void SendPacket(this IInjectionDevice device, RawCapture p, ICaptureHeader header = null)

基本使用如下,从文件中读取数据包发送

1
2
3
4
5
6
7
8
9
var device = devices[index];
device.Open(mode: DeviceModes.Promiscuous, read_timeout: 1000);

CaptureFileReaderDevice reader = new("capture.pcap");
reader.Open();

while (reader.GetNextPacket(out PacketCapture packet) == GetPacketStatus.PacketRead) {
device.SendPacket(packet.GetPacket());
}

发送队列

发送队列是 WinPcap 扩展功能,使用 LibPcap 模块,主要使用到 SendQueue 类,使用以下方法

  • SendQueue():唯一构造器,传入队列大小,单位 B
  • Add():添加到发送队列,SendQueue.csSendQueueExtensions 中包含它的四个扩展方法
    • bool Add(PcapHeader header, byte[] packet)
    • bool Add(this SendQueue queue, byte[] packet)
    • bool Add(this SendQueue queue, Packet packet)
    • bool Add(this SendQueue queue, RawCapture packet)
    • bool Add(this SendQueue queue, byte[] packet, int seconds, int microseconds)
  • Transmit():发送发送队列,传入 PcapDevice 类型接口对象,返回发送的字节数,有一个重载
    • int Transmit(PcapDevice device, bool synchronized)
    • int Transmit(PcapDevice device, SendQueueTransmitModes transmitMode)

基本使用如下,从文件中读取数据包添加到发送队列并发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CaptureFileReaderDevice reader = new("capture.pcap");
reader.Open();

// 构造发送队列
SendQueue queue = new((int)reader.FileSize);

while (reader.GetNextPacket(out PacketCapture packet) == GetPacketStatus.PacketRead) {
// 添加发送队列,传入RawCapture
queue.Add(packet.GetPacket());
}

device.Open(mode: DeviceModes.Promiscuous, read_timeout: 1000);
// 在WinPcap下,ILiveDevice的运行时类型是LibPcapLiveDevice,强转为子类
// LibPcapLiveDevice继承了PcapDevice类,实现了ILiveDevice接口
// 使用LibPcapLiveDeviceList.Instance可直接获得LibPcapLiveDevice集合
queue.Transmit(device as LibPcapLiveDevice, true);

统计流量信息

使用到 ICaptureDevice 对象的 Statistics 属性,对于 LibPcapLiveDevice 对象,该属性不为 null,该属性为 ICaptureStatistics 类型,包含以下属性

  • ReceivedPackets:已接收的数据包数量
  • DroppedPackets:丢失的数据包数量
  • InterfaceDroppedPackets:接口丢包数

基本使用如下

1
2
3
4
5
6
7
8
9
10
11
12
device.Open(mode: DeviceModes.Promiscuous, read_timeout: 1000);
device.OnPacketArrival += new(OnPacketArrival);
device.StartCapture();
Console.ReadLine();

// 获取统计信息
var statistics = device.Statistics;
Console.WriteLine($"接收{statistics?.ReceivedPackets}个包");
Console.WriteLine($"丢失{statistics?.DroppedPackets}个包");

device.StopCapture();
device.Close();