From 5c10978909817964303732e9031b6cefa61af137 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sat, 11 Feb 2023 17:33:46 +0100 Subject: [PATCH] feat(pcap): add pcap client support --- Pcap/NosSmooth.Pcap/ConnectionData.cs | 19 ++ Pcap/NosSmooth.Pcap/NosSmooth.Pcap.csproj | 18 ++ Pcap/NosSmooth.Pcap/PcapNostaleClient.cs | 232 ++++++++++++++++++++++ Pcap/NosSmooth.Pcap/PcapNostaleManager.cs | 182 +++++++++++++++++ 4 files changed, 451 insertions(+) create mode 100644 Pcap/NosSmooth.Pcap/ConnectionData.cs create mode 100644 Pcap/NosSmooth.Pcap/NosSmooth.Pcap.csproj create mode 100644 Pcap/NosSmooth.Pcap/PcapNostaleClient.cs create mode 100644 Pcap/NosSmooth.Pcap/PcapNostaleManager.cs diff --git a/Pcap/NosSmooth.Pcap/ConnectionData.cs b/Pcap/NosSmooth.Pcap/ConnectionData.cs new file mode 100644 index 0000000..1a81c76 --- /dev/null +++ b/Pcap/NosSmooth.Pcap/ConnectionData.cs @@ -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 SniffedData, + DateTimeOffset FirstObservedAt +); \ No newline at end of file diff --git a/Pcap/NosSmooth.Pcap/NosSmooth.Pcap.csproj b/Pcap/NosSmooth.Pcap/NosSmooth.Pcap.csproj new file mode 100644 index 0000000..d1458a0 --- /dev/null +++ b/Pcap/NosSmooth.Pcap/NosSmooth.Pcap.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/Pcap/NosSmooth.Pcap/PcapNostaleClient.cs b/Pcap/NosSmooth.Pcap/PcapNostaleClient.cs new file mode 100644 index 0000000..4bf4559 --- /dev/null +++ b/Pcap/NosSmooth.Pcap/PcapNostaleClient.cs @@ -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; + +/// +/// A NosTale client that works by capturing packets. +/// +/// +/// 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. +/// +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; + + /// + /// Initializes a new instance of the class. + /// + /// The process to look for. + /// The current encryption key of the world connection, if known. Zero if unknown. + /// The pcap manager. + /// The process manager. + /// The packet handler. + /// The command processor. + 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; + } + + /// + public override async Task 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() + .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(); + } + + /// + public override Task SendPacketAsync(string packetString, CancellationToken ct = default) + { + throw new NotImplementedException(); + } + + /// + public override Task ReceivePacketAsync(string packetString, CancellationToken ct = default) + { + throw new NotImplementedException(); + } + + /// + /// Called when an associated packet has been obtained. + /// + /// The connection that obtained the packet. + /// The raw payload data of the packet. + 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 diff --git a/Pcap/NosSmooth.Pcap/PcapNostaleManager.cs b/Pcap/NosSmooth.Pcap/PcapNostaleManager.cs new file mode 100644 index 0000000..def3ac1 --- /dev/null +++ b/Pcap/NosSmooth.Pcap/PcapNostaleManager.cs @@ -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; + +/// +/// Captures packets, distributes them to Pcap clients. +/// +public class PcapNostaleManager +{ + private readonly ConcurrentDictionary _connections; + private readonly ConcurrentDictionary _clients; + private int _clientsCount; + private bool _started; + + /// + /// Initializes a new instance of the class. + /// + public PcapNostaleManager() + { + _connections = new ConcurrentDictionary(); + _clients = new ConcurrentDictionary(); + } + + /// + /// Add a pcap client. + /// + internal void AddClient() + { + var count = Interlocked.Increment(ref _clientsCount); + + if (count == 1) + { + StartCapturing(); + } + } + + /// + /// Remove a pcap client. + /// + /// + /// When no clients are left, packet capture will be stopped. + /// + internal void RemoveClient() + { + var count = Interlocked.Decrement(ref _clientsCount); + + if (count == 0) + { + Stop(); + } + } + + /// + /// Associate the given connection with the given client. + /// + /// The connection to associate. + /// The client to associate the connection with. + 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); + } + } + } + + /// + /// Disassociate the given connection. + /// + /// The connection to disassociate. + 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(); + } + } + + /// + /// Start capturing packets from all devices. + /// + 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(); + 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(), + 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 -- 2.49.0