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