~ruther/NosSmooth

5c10978909817964303732e9031b6cefa61af137 — Rutherther 2 years ago ab84d2f
feat(pcap): add pcap client support
A Pcap/NosSmooth.Pcap/ConnectionData.cs => Pcap/NosSmooth.Pcap/ConnectionData.cs +19 -0
@@ 0,0 1,19 @@
//
//  ConnectionData.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Net;

namespace NosSmooth.Pcap;

public record ConnectionData
(
    IPAddress SourceAddress,
    int SourcePort,
    IPAddress DestinationAddress,
    int DestinationPort,
    List<byte[]> SniffedData,
    DateTimeOffset FirstObservedAt
);
\ No newline at end of file

A Pcap/NosSmooth.Pcap/NosSmooth.Pcap.csproj => Pcap/NosSmooth.Pcap/NosSmooth.Pcap.csproj +18 -0
@@ 0,0 1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="SharpPcap" Version="6.2.5" />
    </ItemGroup>

    <ItemGroup>
      <ProjectReference Include="..\..\Core\NosSmooth.Core\NosSmooth.Core.csproj" />
      <ProjectReference Include="..\..\Core\NosSmooth.Cryptography\NosSmooth.Cryptography.csproj" />
    </ItemGroup>

</Project>

A Pcap/NosSmooth.Pcap/PcapNostaleClient.cs => Pcap/NosSmooth.Pcap/PcapNostaleClient.cs +232 -0
@@ 0,0 1,232 @@
//
//  PcapNostaleClient.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Packets;
using NosSmooth.Cryptography;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using PacketDotNet;
using Remora.Results;
using SharpPcap;
using SharpPcap.LibPcap;

namespace NosSmooth.Pcap;

/// <summary>
/// A NosTale client that works by capturing packets.
/// </summary>
/// <remarks>
/// Sending packets means the same number of packet will appear twice.
/// That may be detected by the server and the server may suspect
/// something malicious is going on.
/// </remarks>
public class PcapNostaleClient : BaseNostaleClient
{
    private readonly Process _process;
    private readonly PcapNostaleManager _pcapManager;
    private readonly ProcessTcpManager _processTcpManager;
    private readonly IPacketHandler _handler;
    private CryptographyManager _crypto;
    private int _localPort;
    private long _localAddr;
    private CancellationToken? _stoppingToken;

    /// <summary>
    /// Initializes a new instance of the <see cref="PcapNostaleClient"/> class.
    /// </summary>
    /// <param name="process">The process to look for.</param>
    /// <param name="encryptionKey">The current encryption key of the world connection, if known. Zero if unknown.</param>
    /// <param name="pcapManager">The pcap manager.</param>
    /// <param name="processTcpManager">The process manager.</param>
    /// <param name="handler">The packet handler.</param>
    /// <param name="commandProcessor">The command processor.</param>
    public PcapNostaleClient
    (
        Process process,
        int encryptionKey,
        PcapNostaleManager pcapManager,
        ProcessTcpManager processTcpManager,
        IPacketHandler handler,
        CommandProcessor commandProcessor
    )
        : base(commandProcessor)
    {
        _process = process;
        _pcapManager = pcapManager;
        _processTcpManager = processTcpManager;
        _handler = handler;
        _crypto = new CryptographyManager();
        _crypto.EncryptionKey = encryptionKey;
    }

    /// <inheritdoc />
    public override async Task<Result> RunAsync(CancellationToken stopRequested = default)
    {
        _stoppingToken = stopRequested;
        TcpConnection? lastConnection = null;
        TcpConnection? reverseLastConnection = null;
        try
        {
            await _processTcpManager.RegisterProcess(_process.Id);
            _pcapManager.AddClient();

            while (!stopRequested.IsCancellationRequested)
            {
                if (_process.HasExited)
                {
                    break;
                }

                var connection = (await _processTcpManager.GetConnectionsAsync(_process.Id)).Cast<TcpConnection?>()
                    .FirstOrDefault();

                if (lastConnection != connection)
                {
                    if (lastConnection is not null)
                    {
                        _pcapManager.UnregisterConnection(lastConnection.Value);
                        _crypto.EncryptionKey = 0;
                    }
                    if (reverseLastConnection is not null)
                    {
                        _pcapManager.UnregisterConnection(reverseLastConnection.Value);
                    }

                    if (connection is not null)
                    {
                        var conn = connection.Value;
                        var reverseConn = new TcpConnection
                            (conn.RemoteAddr, conn.RemotePort, conn.LocalAddr, conn.LocalPort);

                        _localAddr = conn.LocalAddr;
                        _localPort = conn.LocalPort;

                        _pcapManager.RegisterConnection(conn, this);
                        _pcapManager.RegisterConnection(reverseConn, this);

                        lastConnection = conn;
                        reverseLastConnection = reverseConn;
                    }
                    else
                    {
                        lastConnection = null;
                        reverseLastConnection = null;
                    }
                }

                await Task.Delay(TimeSpan.FromMilliseconds(1), stopRequested);
            }
        }
        catch (OperationCanceledException)
        {
            // ignored
        }
        catch (Exception e)
        {
            return e;
        }
        finally
        {
            await _processTcpManager.UnregisterProcess(_process.Id);
            _pcapManager.RemoveClient();
            if (lastConnection is not null)
            {
                _pcapManager.UnregisterConnection(lastConnection.Value);
            }

            if (reverseLastConnection is not null)
            {
                _pcapManager.UnregisterConnection(reverseLastConnection.Value);
            }
        }

        return Result.FromSuccess();
    }

    /// <inheritdoc />
    public override Task<Result> SendPacketAsync(string packetString, CancellationToken ct = default)
    {
        throw new NotImplementedException();
    }

    /// <inheritdoc />
    public override Task<Result> ReceivePacketAsync(string packetString, CancellationToken ct = default)
    {
        throw new NotImplementedException();
    }

    /// <summary>
    /// Called when an associated packet has been obtained.
    /// </summary>
    /// <param name="connection">The connection that obtained the packet.</param>
    /// <param name="payloadData">The raw payload data of the packet.</param>
    internal void OnPacketArrival(TcpConnection connection, byte[] payloadData)
    {
        string data;
        PacketSource source;
        bool containsPacketId = false;

        if (connection.LocalAddr == _localAddr && connection.LocalPort == _localPort)
        { // sent packet
            source = PacketSource.Client;
            if (_crypto.EncryptionKey == 0)
            {
                var worldDecrypted = _crypto.ServerWorld.Decrypt(payloadData, Encoding.Default);

                var splitted = worldDecrypted.Split(' ');
                if (splitted.Length == 2 && int.TryParse(splitted[1], out var encryptionKey))
                { // possibly first packet from world
                    _crypto.EncryptionKey = encryptionKey;
                    data = worldDecrypted;
                    containsPacketId = true;
                }
                else
                { // doesn't look like first packet from world, so assume login.
                    data = _crypto.ServerLogin.Decrypt(payloadData, Encoding.Default);
                }
            }
            else
            {
                data = _crypto.ServerWorld.Decrypt(payloadData, Encoding.Default);
                containsPacketId = true;
            }
        }
        else
        { // received packet
            source = PacketSource.Server;
            if (_crypto.EncryptionKey == 0)
            { // probably login
                data = _crypto.ClientLogin.Decrypt(payloadData, Encoding.Default);
            }
            else
            {
                data = _crypto.ClientWorld.Decrypt(payloadData, Encoding.Default);
            }
        }

        if (data.Length > 0)
        {
            foreach (var line in data.Split('\n'))
            {
                var linePacket = line;
                if (containsPacketId)
                {
                    linePacket = line.Substring(line.IndexOf(' ') + 1);
                }

                Console.WriteLine(linePacket);

                // _handler.HandlePacketAsync(this, source, linePacket, _stoppingToken ?? default);
            }

        }
    }
}
\ No newline at end of file

A Pcap/NosSmooth.Pcap/PcapNostaleManager.cs => Pcap/NosSmooth.Pcap/PcapNostaleManager.cs +182 -0
@@ 0,0 1,182 @@
//
//  PcapNostaleManager.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net;
using SharpPcap;
using SharpPcap.LibPcap;

namespace NosSmooth.Pcap;

/// <summary>
/// Captures packets, distributes them to Pcap clients.
/// </summary>
public class PcapNostaleManager
{
    private readonly ConcurrentDictionary<TcpConnection, ConnectionData> _connections;
    private readonly ConcurrentDictionary<TcpConnection, PcapNostaleClient> _clients;
    private int _clientsCount;
    private bool _started;

    /// <summary>
    /// Initializes a new instance of the <see cref="PcapNostaleManager"/> class.
    /// </summary>
    public PcapNostaleManager()
    {
        _connections = new ConcurrentDictionary<TcpConnection, ConnectionData>();
        _clients = new ConcurrentDictionary<TcpConnection, PcapNostaleClient>();
    }

    /// <summary>
    /// Add a pcap client.
    /// </summary>
    internal void AddClient()
    {
        var count = Interlocked.Increment(ref _clientsCount);

        if (count == 1)
        {
            StartCapturing();
        }
    }

    /// <summary>
    /// Remove a pcap client.
    /// </summary>
    /// <remarks>
    /// When no clients are left, packet capture will be stopped.
    /// </remarks>
    internal void RemoveClient()
    {
        var count = Interlocked.Decrement(ref _clientsCount);

        if (count == 0)
        {
            Stop();
        }
    }

    /// <summary>
    /// Associate the given connection with the given client.
    /// </summary>
    /// <param name="connection">The connection to associate.</param>
    /// <param name="client">The client to associate the connection with.</param>
    internal void RegisterConnection(TcpConnection connection, PcapNostaleClient client)
    {
        _clients.AddOrUpdate(connection, (c) => client, (c1, c2) => client);

        if (_connections.TryGetValue(connection, out var data))
        {
            foreach (var sniffedPacket in data.SniffedData)
            {
                client.OnPacketArrival(connection, sniffedPacket);
            }
        }
    }

    /// <summary>
    /// Disassociate the given connection.
    /// </summary>
    /// <param name="connection">The connection to disassociate.</param>
    internal void UnregisterConnection(TcpConnection connection)
    {
        _clients.TryRemove(connection, out _);
    }

    private void Stop()
    {
        if (!_started)
        {
            return;
        }

        _started = false;
        foreach (var device in LibPcapLiveDeviceList.Instance)
        {
            device.StopCapture();
        }
    }

    /// <summary>
    /// Start capturing packets from all devices.
    /// </summary>
    public void StartCapturing()
    {
        if (_started)
        {
            return;
        }

        _started = true;

        foreach (var device in LibPcapLiveDeviceList.Instance)
        {
            if (!device.Opened)
            {
                device.Open();
            }

            device.Filter = "ip and tcp";
            device.OnPacketArrival += DeviceOnOnPacketArrival;
            device.StartCapture();
        }
    }

    private void DeviceOnOnPacketArrival(object sender, PacketCapture e)
    {
        var rawPacket = e.GetPacket();

        var packet = PacketDotNet.Packet.ParsePacket(rawPacket.LinkLayerType, rawPacket.Data);

        var tcpPacket = packet.Extract<PacketDotNet.TcpPacket>();
        if (tcpPacket is null)
        {
            return;
        }

        if (!tcpPacket.HasPayloadData || tcpPacket.PayloadData.Length == 0 || tcpPacket.PayloadData.Length > 500)
        {
            return;
        }

        var ipPacket = (PacketDotNet.IPPacket)tcpPacket.ParentPacket;
        System.Net.IPAddress srcIp = ipPacket.SourceAddress;
        System.Net.IPAddress dstIp = ipPacket.DestinationAddress;
        int srcPort = tcpPacket.SourcePort;
        int dstPort = tcpPacket.DestinationPort;

        var tcpConnection = new TcpConnection(srcIp.Address, srcPort, dstIp.Address, dstPort);

        if (!_connections.ContainsKey(tcpConnection))
        {
            _connections.TryAdd
            (
                tcpConnection,
                new ConnectionData
                (
                    srcIp,
                    srcPort,
                    dstIp,
                    dstPort,
                    new List<byte[]>(),
                    DateTimeOffset.Now
                )
            );
        }

        var data = _connections[tcpConnection];
        if (data.SniffedData.Count < 5)
        {
            data.SniffedData.Add(tcpPacket.PayloadData);
        } // TODO: clean up the sniffed data in case they are not needed.

        if (_clients.TryGetValue(tcpConnection, out var client))
        {
            client.OnPacketArrival(tcpConnection, tcpPacket.PayloadData);
        }
    }
}
\ No newline at end of file

Do not follow this link