WindivertDotnet快速發Ping

2022-10-19 21:00:15

1 前言

WindivertDotnet是物件導向的WinDivert的dotnet非同步封裝,其提供如下的傳送資料方法:

ValueTask<int> SendAsync(
    WinDivertPacket packet, 
    WinDivertAddress addr,
    CancellationToken cancellationToken)

在修改包的場景,我們通過RecvAsync()方法獲取具有內容的WinDivertPacketWinDivertAddress物件範例,簡單修改這兩個物件的一些值之後,就可以傳送出去。

但在注入的場景,我們需要無中生成WinDivertPacketWinDivertAddress兩個物件,前者是IP包的完整資料,後者主要指示資料要經過的網路介面卡的索引、資料是入口還是出口方向、是否為loopback等資訊,下面我將使用WindivertDotnet來開發一個批次Ping功能的範例來教大家怎麼注入封包。

2 發出Ping包

2.1 路由計算

在發Ping的場景中,我們只知道目的地IP地址,WinDivertRouter物件可以幫們提前算出路由資訊,得到以下表格的內容:

屬性 說明
IPAddress DstAddress 目的地IP地址
IPAddress SrcAddress 源IP地址
int InterfaceIndex 經過的網路介面卡的索引
bool IsOutbound 是否為出口方向
// 使用dstAddr建立router
var router = new WinDivertRouter(dstAddr); 

2.2 建立WinDivertAddress

WinDivertAddress的如下屬性必須要設定正確,它是IP封包構建鏈路封包必須的項:

屬性 說明
WinDivertAddress.NetWork->IfIdx 發包的網路介面卡的索引
WinDivertAddress.Flags.OutboundFlag 是否為出口方向
WinDivertAddress.Flags.LoopbackFlag 是否為迴環
// 使用router建立WinDivertAddress 
using WinDivertAddress addr = router.CreateAddress();

2.3 建立WinDivertPacket

因為從router裡知道了源IP和目標IP,所以建立ICMP ping功能的WinDivertPacket就比較容易。

/// <summary>
/// 建立icmp的echo包
/// </summary>
/// <param name="srcAddr"></param>
/// <param name="dstAddr"></param>
/// <returns></returns>
private unsafe WinDivertPacket CreateIPV4EchoPacket(IPAddress srcAddr, IPAddress dstAddr)
{
    // ipv4頭
    var ipHeader = new IPV4Header
    {
        TTL = 128,
        Version = 4,
        DstAddr = dstAddr,
        SrcAddr = srcAddr,
        Protocol = ProtocolType.Icmp,
        HdrLength = (byte)(sizeof(IPV4Header) / 4),
        Id = ++this.id,
        Length = (ushort)(sizeof(IPV4Header) + sizeof(IcmpV4Header))
    };

    // icmp頭
    var icmpHeader = new IcmpV4Header
    {
        Type = IcmpV4MessageType.EchoRequest,
        Code = default,
        Identifier = ipHeader.Id,
        SequenceNumber = ++this.sequenceNumber,
    };

    // 將資料寫到packet緩衝區
    var packet = new WinDivertPacket(ipHeader.Length);

    var writer = packet.GetWriter();
    writer.Write(ipHeader);
    writer.Write(icmpHeader);

    return packet;
}

2.4 發出封包

現在我們可使用Windivert物件,將為每個目的地IP建立的WinDivertPacketWinDivertAddress兩個物件傳送出去:

/// <summary>
/// 傳送icmp的echo請求包
/// </summary>
/// <param name="dstAddrs"></param>
/// <returns></returns>
private async Task SendEchoRequestAsync(IEnumerable<IPAddress> dstAddrs)
{
    foreach (var address in dstAddrs)
    {
        // 使用router計算將進行通訊的本機地址
        var router = new WinDivertRouter(address);
        using var addr = router.CreateAddress();
        using var packet = this.CreateIPV4EchoPacket(router.SrcAddress, router.DstAddress);

        packet.CalcChecksums(addr);     // 計算checksums,因為建立包時沒有計算

        await this.divert.SendAsync(packet, addr);
    }
}

3 接收回復包

3.1 Filter

我們可以使用過濾器,將接收的內容過濾為icmp,並且資料是入口方向,必要不必要的資料到達我們的應用層而增加了處理負擔:

// 只接受進入系統的icmp
var filter = Filter.True.And(f => f.IsIcmp && f.Network.Inbound);
this.divert = new WinDivert(filter, WinDivertLayer.Network);

3.2 接收資料

接收資料這個就簡單了,這是WindivertDotnet最擅長的技能:

/// <summary>
/// 監聽ping的回覆
/// </summary>
/// <param name="cancellationToken">取消令牌</param>
/// <returns></returns>
private async Task<HashSet<IPAddress>> RecvEchoReplyAsync(CancellationToken cancellationToken)
{
    var results = new HashSet<IPAddress>();
    using var packet = new WinDivertPacket();
    using var addr = new WinDivertAddress();

    while (cancellationToken.IsCancellationRequested == false)
    {
        try
        {
            await this.divert.RecvAsync(packet, addr, cancellationToken);
            if (TryGetEchoReplyAddr(packet, out var value))
            {
                results.Add(value);
            }
            // 把packet發出,避免系統其它軟體此刻也有ping而收不到回覆
            await this.divert.SendAsync(packet, addr, cancellationToken);
        }
        catch (OperationCanceledException)
        {
            break;
        }
    }
    return results;
}

3.3 解析回覆的IP

/// <summary>
/// 解析出icmp回覆資訊
/// </summary>
/// <param name="packet">封包</param>
/// <param name="value">回覆的IP</param>
/// <returns></returns>
private unsafe static bool TryGetEchoReplyAddr(WinDivertPacket packet, [MaybeNullWhen(false)] out IPAddress value)
{
   var result = packet.GetParseResult();
   if (result.IcmpV4Header != null &&
       result.IcmpV4Header->Type == IcmpV4MessageType.EchoReply)
   {
       value = result.IPV4Header->SrcAddr;
       return true;
   }
   else if (result.IcmpV6Header != null &&
       result.IcmpV6Header->Type == IcmpV6MessageType.EchoReply)
   {
       value = result.IPV6Header->SrcAddr;
       return true;
   }

   value = null;
   return false;
}

4 整合資料

我們需要一個執行緒來開啟接收ping回覆,同時另一個執行緒把所有ping發出去,最後拿ping的所有IP和ping回覆的所有IP求交集,就是我們需要的結果。


/// <summary>
/// Ping所有地址
/// 佔用兩個執行緒
/// </summary>
/// <param name="dstAddrs">目標地址</param>
/// <param name="delay">最後一個IP發出ping之後的等待回覆時長</param>
/// <returns></returns>
public async Task<IPAddress[]> PingAllAsync(IEnumerable<IPAddress> dstAddrs, TimeSpan delay)
{
    // 開始監聽ping的回覆
    using var cts = new CancellationTokenSource();
    var recvTask = this.RecvEchoReplyAsync(cts.Token);

    // 對所有ip發ping
    await this.SendEchoRequestAsync(dstAddrs);

    // 延時取消監聽
    cts.CancelAfter(delay);
    var results = await recvTask;

    // 清洗資料
    return results.Intersect(dstAddrs).ToArray();
}

後記

通過WindivertDotnet的路由,無中生有IP封包,並可以將其正確的傳送的指定的目的地IP地址。像本範例的這個Ping方式,10秒ping完1萬個IP並拿到其回覆的IP是非常輕鬆的。