diff --git a/utils/stream_parser/.gitignore b/utils/stream_parser/.gitignore new file mode 100644 index 000000000..34c8dee45 --- /dev/null +++ b/utils/stream_parser/.gitignore @@ -0,0 +1,388 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Nuget personal access tokens and Credentials +nuget.config + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +.idea/ +*.sln.iml diff --git a/utils/stream_parser/common/Daybreak/Connection.cs b/utils/stream_parser/common/Daybreak/Connection.cs new file mode 100644 index 000000000..470e2b17a --- /dev/null +++ b/utils/stream_parser/common/Daybreak/Connection.cs @@ -0,0 +1,609 @@ +using Ionic.Zlib; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace StreamParser.Common.Daybreak +{ + public class Connection : IConnection + { + private class EncodeType + { + public const int None = 0; + public const int Compression = 1; + public const int XOR = 4; + } + + private enum SequenceOrder + { + Past, + Current, + Future + } + + private readonly IParser _owner; + private readonly IPAddress _srcAddr; + private readonly int _srcPort; + private readonly IPAddress _dstAddr; + private readonly int _dstPort; + private readonly Util.Crc32 _crc_generator = new Util.Crc32(); + private readonly Guid _id = Guid.NewGuid(); + private uint _connect_code = 0; + private int _encode_key = 0; + private int _crc_bytes = 0; + private int[] _encode_pass = new int[2] { 0, 0 }; + private ConnectionStream[] _client_streams = new ConnectionStream[4] { + new ConnectionStream(), + new ConnectionStream(), + new ConnectionStream(), + new ConnectionStream() + }; + private ConnectionStream[] _server_streams = new ConnectionStream[4] { + new ConnectionStream(), + new ConnectionStream(), + new ConnectionStream(), + new ConnectionStream() + }; + + public IConnection.OnPacketRecvHandler OnPacketRecv { get; set; } + + public IPAddress ClientAddress => _srcAddr; + public int ClientPort => _srcPort; + public IPAddress ServerAddress => _dstAddr; + public int ServerPort => _dstPort; + public Guid Id => _id; + + public ConnectionType ConnectionType { + get + { + //World servers used to be coded to always be 9000 but live started using dynamic ports + //I've seen from 9000 to 9008 on live + if (_dstPort >= 9000 && _dstPort <= 9010) + { + return ConnectionType.World; + } + else if (_encode_pass[0] == EncodeType.None && _encode_pass[1] == EncodeType.None) + { + return ConnectionType.Login; + } + else if (_encode_pass[0] == EncodeType.XOR && _encode_pass[1] == EncodeType.None) + { + return ConnectionType.Chat; + } + else if (_encode_pass[0] == EncodeType.Compression && _encode_pass[1] == EncodeType.None) + { + return ConnectionType.Zone; + } + + return ConnectionType.Unknown; + } + } + + public Connection(IParser owner, IPAddress srcAddr, int srcPort, IPAddress dstAddr, int dstPort) + { + _owner = owner; + _srcAddr = srcAddr; + _srcPort = srcPort; + _dstAddr = dstAddr; + _dstPort = dstPort; + } + + public void ProcessPacket(IPAddress srcAddr, int srcPort, DateTime packetTime, ReadOnlySpan data) + { + if (data.Length < 1) + { + return; + } + + var opcode = data[1]; + if (data[0] == 0 && (opcode == Opcode.KeepAlive || opcode == Opcode.OutboundPing)) + { + return; + } + + if (PacketCanBeDecoded(data)) + { + if (!ValidateCRC(data)) + { + return; + } + + if (_encode_pass[0] == EncodeType.None && _encode_pass[1] == EncodeType.None) + { + ProcessDecodedPacket(srcAddr, srcPort, packetTime, data.Slice(0, data.Length - _crc_bytes)); + } + else + { + //unfortunately we can't avoid a copy here + var temp = data.Slice(0, data.Length - _crc_bytes).ToArray(); + for (int i = 1; i >= 0; --i) + { + switch(_encode_pass[i]) + { + case EncodeType.Compression: + if(temp[0] == 0) + { + temp = Decompress(temp, 2, temp.Length - 2); + } + else + { + temp = Decompress(temp, 1, temp.Length - 1); + } + break; + case EncodeType.XOR: + if (temp[0] == 0) + { + temp = Decode(temp, 2, temp.Length - 2); + } + else + { + temp = Decode(temp, 1, temp.Length - 1); + } + break; + } + } + + ProcessDecodedPacket(srcAddr, srcPort, packetTime, temp); + } + } + else + { + ProcessDecodedPacket(srcAddr, srcPort, packetTime, data); + } + } + + private void ProcessDecodedPacket(IPAddress srcAddr, int srcPort, DateTime packetTime, ReadOnlySpan data) + { + if (data.Length < 1) + { + return; + } + + if (data[0] == 0) + { + if (data.Length < 2) + { + return; + } + + var opcode = data[1]; + switch (opcode) + { + case Opcode.SessionResponse: + if (_connect_code == 0) + { + _connect_code = BitConverter.ToUInt32(data.Slice(2, 4)); + _encode_key = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(data.Slice(6, 4))); + _crc_bytes = data[10]; + _encode_pass[0] = data[11]; + _encode_pass[1] = data[12]; + _owner.OnNewConnection?.Invoke(this, packetTime); + } + break; + case Opcode.SessionDisconnect: + if(_connect_code != 0) + { + _connect_code = 0; + _encode_key = 0; + _crc_bytes = 0; + _encode_pass[0] = 0; + _encode_pass[1] = 0; + _owner.OnLostConnection?.Invoke(this, packetTime); + } + break; + case Opcode.Combined: + { + int current = 2; + int end = data.Length; + while (current < end) + { + byte subpacket_length = data[current]; + current += 1; + + if (end < current + subpacket_length) + { + return; + } + + var subpacket = data.Slice(current, subpacket_length); + ProcessDecodedPacket(srcAddr, srcPort, packetTime, subpacket); + current += subpacket_length; + } + } + break; + case Opcode.AppCombined: + { + int current = 2; + int end = data.Length; + while (current < end) + { + int subpacket_length = 0; + if (data[current] == 0xff) + { + if (end < current + 3) + { + return; + } + + if (data[current + 1] == 0xff && data[current + 2] == 0xff) + { + if (end < current + 7) + { + return; + } + + subpacket_length = + ((data[current + 3]) << 24) | + ((data[current + 4]) << 16) | + ((data[current + 5]) << 8) | + (data[current + 6]); + + current += 7; + } + else + { + subpacket_length = + ((data[current + 1]) << 8) | + (data[current + 2]); + + current += 3; + } + } + else + { + subpacket_length = data[current]; + current += 1; + } + + var subpacket = data.Slice(current, subpacket_length); + ProcessDecodedPacket(srcAddr, srcPort, packetTime, subpacket); + } + } + break; + case Opcode.Packet: + case Opcode.Packet2: + case Opcode.Packet3: + case Opcode.Packet4: + { + var stream_id = opcode - Opcode.Packet; + var stream = FindStream(srcAddr, srcPort, stream_id); + var sequence = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data.Slice(2, 2))); + var order = CompareSequence(stream.Sequence, sequence); + if (order == SequenceOrder.Future) + { + if (!stream.PacketQueue.ContainsKey(sequence)) + { + stream.PacketQueue.Add(sequence, new ConnectionStream.QueuedPacket + { + Data = data.ToArray(), + PacketTime = packetTime + }); + } + } + else if (order == SequenceOrder.Current) + { + if (stream.PacketQueue.ContainsKey(sequence)) + { + stream.PacketQueue.Remove(sequence); + } + + stream.Sequence++; + ProcessDecodedPacket(srcAddr, srcPort, packetTime, data.Slice(4)); + ProcessQueue(srcAddr, srcPort, stream_id); + } + } + break; + case Opcode.Fragment: + case Opcode.Fragment2: + case Opcode.Fragment3: + case Opcode.Fragment4: + { + var stream_id = opcode - Opcode.Fragment; + var stream = FindStream(srcAddr, srcPort, stream_id); + var sequence = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data.Slice(2, 2))); + var order = CompareSequence(stream.Sequence, sequence); + if (order == SequenceOrder.Future) + { + if (!stream.PacketQueue.ContainsKey(sequence)) + { + stream.PacketQueue.Add(sequence, new ConnectionStream.QueuedPacket + { + Data = data.ToArray(), + PacketTime = packetTime + }); + } + } + else if (order == SequenceOrder.Current) + { + if (stream.PacketQueue.ContainsKey(sequence)) + { + stream.PacketQueue.Remove(sequence); + } + + stream.Sequence++; + + if (stream.TotalFragmentedBytes == 0) + { + stream.TotalFragmentedBytes = (uint)IPAddress.NetworkToHostOrder(BitConverter.ToInt32(data.Slice(4, 4))); + stream.CurrentFragmentedBytes = (uint)(data.Length - 8); + + if(stream.FragmentBuffer == null || stream.FragmentBuffer.Length < (stream.TotalFragmentedBytes + 512)) + { + stream.FragmentBuffer = new byte[stream.TotalFragmentedBytes + 512]; + } + + var target = stream.FragmentBuffer.AsSpan(); + data.Slice(8).CopyTo(target); + } else + { + var target = stream.FragmentBuffer.AsSpan((int)stream.CurrentFragmentedBytes); + data.Slice(4).CopyTo(target); + + stream.CurrentFragmentedBytes += (uint)(data.Length - 4); + + if (stream.CurrentFragmentedBytes >= stream.TotalFragmentedBytes) + { + ProcessDecodedPacket(srcAddr, srcPort, packetTime, + stream.FragmentBuffer.AsSpan(0, (int)stream.TotalFragmentedBytes)); + stream.CurrentFragmentedBytes = 0; + stream.TotalFragmentedBytes = 0; + } + } + + ProcessQueue(srcAddr, srcPort, stream_id); + } + + } + break; + case Opcode.Padding: + OnPacketRecv?.Invoke(this, GetDirection(srcAddr, srcPort), packetTime, data.Slice(1)); + break; + default: + break; + } + } else + { + OnPacketRecv?.Invoke(this, GetDirection(srcAddr, srcPort), packetTime, data); + } + } + + private void ProcessQueue(IPAddress srcAddr, int srcPort, int stream_id) + { + var stream = FindStream(srcAddr, srcPort, stream_id); + var sequence = stream.Sequence; + + //try to get the current sequence in the queue, if it exists then process it + ConnectionStream.QueuedPacket value; + if(stream.PacketQueue.TryGetValue(sequence, out value)) + { + ProcessDecodedPacket(srcAddr, srcPort, value.PacketTime, value.Data); + } + } + + public bool Match(IPAddress srcAddr, int srcPort, IPAddress dstAddr, int dstPort) + { + var p1 = _srcAddr.Equals(srcAddr) && _srcPort == srcPort && _dstAddr.Equals(dstAddr) && _dstPort == dstPort; + var p2 = _srcAddr.Equals(dstAddr) && _srcPort == dstPort && _dstAddr.Equals(srcAddr) && _dstPort == srcPort; + return p1 || p2; + } + + private SequenceOrder CompareSequence(ushort expected, ushort actual) + { + int diff = (int)actual - (int)expected; + + if (diff == 0) + { + return SequenceOrder.Current; + } + + if (diff > 0) + { + if (diff > 10000) + { + return SequenceOrder.Past; + } + + return SequenceOrder.Future; + } + + if (diff < -10000) + { + return SequenceOrder.Future; + } + + return SequenceOrder.Past; + } + + private bool PacketCanBeDecoded(ReadOnlySpan p) + { + if (p.Length < 2) + { + return false; + } + + if (p[0] != 0) + { + return true; + } + + var opcode = p[1]; + + if (opcode == Opcode.SessionRequest || opcode == Opcode.SessionResponse || opcode == Opcode.OutOfSession) + { + return false; + } + + return true; + } + + private bool ValidateCRC(ReadOnlySpan p) + { + if (_crc_bytes == 0) + { + return true; + } + + int actual = 0; + int calculated = _crc_generator.Calculate(p.Slice(0, p.Length - _crc_bytes), _encode_key); + switch (_crc_bytes) + { + case 2: + actual = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(p.Slice(p.Length - 2, 2))) & 0xFFFF; + calculated = calculated & 0xFFFF; + break; + case 4: + actual = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(p.Slice(p.Length - 4, 4))); + break; + default: + return false; + } + + return actual == calculated; + } + + private byte[] Decompress(byte[] p, int offset, int length) + { + if (length < 2) + { + return p; + } + + Span header = p.AsSpan(0, offset); + byte flag = p[offset]; + Span payload = p.AsSpan(offset + 1, length - 1); + + if (flag == 0x5a) + { + var pl = payload.ToArray(); + var inflated = Inflate(payload.ToArray()); + byte[] ret = new byte[offset + inflated.Length]; + Array.Copy(p, 0, ret, 0, offset); + Array.Copy(inflated, 0, ret, offset, inflated.Length); + return ret; + } + else if (flag == 0xa5) + { + byte[] ret = new byte[offset + length - 1]; + Array.Copy(p, 0, ret, 0, offset); + Array.Copy(p, offset + 1, ret, offset, length - 1); + return ret; + } + else + { + return p; + } + } + + private byte[] Inflate(byte[] p) + { + try + { + using (var out_stream = new MemoryStream()) + { + using (var in_stream = new MemoryStream(p)) + { + var buffer = new byte[512]; + using (var zs = new ZlibStream(in_stream, CompressionMode.Decompress)) + { + int r = 0; + do + { + r = zs.Read(buffer, 0, 512); + out_stream.Write(buffer, 0, r); + } while (r == 512); + } + } + + var ret = out_stream.ToArray(); + return ret; + } + } + catch (Exception) + { + return null; + } + } + + private byte[] Decode(byte[] p, int offset, int length) + { + int key = _encode_key; + Span buffer = p.AsSpan(offset, length); + int i = 0; + for (i = 0; i + 4 <= length; i += 4) + { + int pt = BitConverter.ToInt32(buffer.Slice(i)) ^ key; + key = BitConverter.ToInt32(buffer.Slice(i)); + + if(BitConverter.TryWriteBytes(buffer.Slice(i), pt) == false) + { + throw new Exception("Error writing bytes back in decode."); + } + } + + byte kc = (byte)(key & 0xFF); + for (; i < length; i++) + { + buffer[i] = (byte)(buffer[i] ^ kc); + } + + return p; + } + + private Direction GetDirection(IPAddress srcAddr, int srcPort) + { + if(srcAddr.Equals(_srcAddr) && srcPort == _srcPort) + { + return Direction.ClientToServer; + } else + { + return Direction.ServerToClient; + } + } + + private ConnectionStream FindStream(IPAddress srcAddr, int srcPort, int index) + { + if (index < 0 || index > 3) + { + return null; + } + + var dir = GetDirection(srcAddr, srcPort); + + if(dir == Direction.ClientToServer) + { + return _client_streams[index]; + } else + { + return _server_streams[index]; + } + } + + private class ConnectionStream + { + public class QueuedPacket + { + public byte[] Data { get; set; } + public DateTime PacketTime { get; set; } + } + + public ConnectionStream() + { + Sequence = 0; + CurrentFragmentedBytes = 0; + TotalFragmentedBytes = 0; + FragmentBuffer = null; + PacketQueue = new Dictionary(); + } + + public ushort Sequence { get; set; } + public uint CurrentFragmentedBytes { get; set; } + public uint TotalFragmentedBytes { get; set; } + public byte[] FragmentBuffer { get; set; } + public Dictionary PacketQueue { get; set; } + } + } +} diff --git a/utils/stream_parser/common/Daybreak/GamePacket.cs b/utils/stream_parser/common/Daybreak/GamePacket.cs new file mode 100644 index 000000000..af73c831b --- /dev/null +++ b/utils/stream_parser/common/Daybreak/GamePacket.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StreamParser.Common.Daybreak +{ + public ref struct GamePacket + { + private readonly ReadOnlySpan _data; + + public GamePacket(ReadOnlySpan data) + { + _data = data; + } + + public readonly override string ToString() + { + return ToString(16); + } + + public readonly string ToString(int columns) + { + int rows = _data.Length / columns; + if (_data.Length % columns != 0) + { + rows += 1; + } + + int expected = (10 + columns * 4) * rows; + var sb = new StringBuilder(expected); + + for(var i = 0; i < rows; ++i) + { + sb.AppendFormat("{0} |", (i * columns).ToString("X5")); + + for(var j = 0; j < columns; ++j) + { + var index = (i * 16) + j; + if (index >= _data.Length) + { + sb.Append(" "); + } else + { + var c = _data[index]; + sb.AppendFormat("{0,3}", c.ToString("X2")); + } + } + + sb.Append(" | "); + + for (var j = 0; j < columns; ++j) + { + var index = (i * 16) + j; + if (index >= _data.Length) + { + sb.Append(" "); + } + else + { + var c = _data[index]; + var ch = (char)c; + if (char.IsLetterOrDigit(ch) || char.IsPunctuation(ch) || char.IsSymbol(ch) || (ch == ' ')) + { + sb.Append(ch); + } + else + { + sb.Append("."); + } + } + } + + sb.AppendLine(); + } + + return sb.ToString(); + } + + public readonly string ToModelString(int max_taken, bool hex) + { + int expected = Math.Min(_data.Length, max_taken) * (hex ? 2 : 1); + if(expected <= 0) + { + return string.Empty; + } + + var sb = new StringBuilder(expected); + + for(var i = 0; i < Math.Min(_data.Length, max_taken); ++i) + { + var c = _data[i]; + if(hex) + { + sb.Append(c.ToString("X2")); + } else + { + var ch = (char)c; + if (char.IsLetterOrDigit(ch) || char.IsPunctuation(ch) || char.IsSymbol(ch) || (ch == ' ')) + { + sb.Append(ch); + } + else + { + sb.Append("."); + } + } + } + + return sb.ToString(); + } + } +} diff --git a/utils/stream_parser/common/Daybreak/IConnection.cs b/utils/stream_parser/common/Daybreak/IConnection.cs new file mode 100644 index 000000000..3a422a2df --- /dev/null +++ b/utils/stream_parser/common/Daybreak/IConnection.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace StreamParser.Common.Daybreak +{ + public enum Direction + { + ClientToServer, + ServerToClient + } + + public enum ConnectionType + { + Unknown, + Login, + World, + Chat, + Zone + } + + public interface IConnection + { + delegate void OnPacketRecvHandler(Connection connection, Direction direction, DateTime packetTime, ReadOnlySpan data); + OnPacketRecvHandler OnPacketRecv { get; set; } + + void ProcessPacket(IPAddress srcAddr, int srcPort, DateTime packetTime, ReadOnlySpan data); + bool Match(IPAddress srcAddr, int srcPort, IPAddress dstAddr, int dstPort); + + ConnectionType ConnectionType { get; } + IPAddress ClientAddress { get; } + int ClientPort { get; } + IPAddress ServerAddress { get; } + int ServerPort { get; } + Guid Id { get; } + } +} diff --git a/utils/stream_parser/common/Daybreak/IParser.cs b/utils/stream_parser/common/Daybreak/IParser.cs new file mode 100644 index 000000000..28d85e05a --- /dev/null +++ b/utils/stream_parser/common/Daybreak/IParser.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StreamParser.Common.Daybreak +{ + public interface IParser + { + delegate void ConnectionHandler(IConnection connection, DateTime connectionTime); + ConnectionHandler OnNewConnection { get; set; } + ConnectionHandler OnLostConnection { get; set; } + void Parse(string filename); + } +} diff --git a/utils/stream_parser/common/Daybreak/Opcode.cs b/utils/stream_parser/common/Daybreak/Opcode.cs new file mode 100644 index 000000000..3239851f6 --- /dev/null +++ b/utils/stream_parser/common/Daybreak/Opcode.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StreamParser.Common.Daybreak +{ + public class Opcode + { + public const byte Padding = 0; + public const byte SessionRequest = 1; + public const byte SessionResponse = 2; + public const byte Combined = 3; + public const byte SessionDisconnect = 5; + public const byte KeepAlive = 6; + public const byte SessionStatRequest = 7; + public const byte SessionStatResponse = 8; + public const byte Packet = 9; + public const byte Packet2 = 10; + public const byte Packet3 = 11; + public const byte Packet4 = 12; + public const byte Fragment = 13; + public const byte Fragment2 = 14; + public const byte Fragment3 = 15; + public const byte Fragment4 = 16; + public const byte OutOfOrderAck = 17; + public const byte OutOfOrderAck2 = 18; + public const byte OutOfOrderAck3 = 19; + public const byte OutOfOrderAck4 = 20; + public const byte Ack = 21; + public const byte Ack2 = 22; + public const byte Ack3 = 23; + public const byte Ack4 = 22; + public const byte AppCombined = 23; + public const byte OutboundPing = 28; + public const byte OutOfSession = 29; + } +} diff --git a/utils/stream_parser/common/Daybreak/Parser.cs b/utils/stream_parser/common/Daybreak/Parser.cs new file mode 100644 index 000000000..8c95dd6f9 --- /dev/null +++ b/utils/stream_parser/common/Daybreak/Parser.cs @@ -0,0 +1,109 @@ +using Microsoft.Extensions.Logging; +using SharpPcap; +using SharpPcap.LibPcap; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace StreamParser.Common.Daybreak +{ + public class Parser : IParser + { + /** + * Dependencies + */ + private readonly ILogger _logger; + private readonly List _connections = new List(); + + public IParser.ConnectionHandler OnNewConnection { get; set; } + public IParser.ConnectionHandler OnLostConnection { get; set; } + + public Parser(ILogger logger) + { + _logger = logger; + } + + public void Parse(string filename) + { + ICaptureDevice device = null; + try + { + device = new CaptureFileReaderDevice(filename); + device.Open(); + + device.OnPacketArrival += new PacketArrivalEventHandler(OnPacketCapture); + + device.Capture(); + device.Close(); + } catch(Exception ex) + { + _logger.LogError(ex, "Error reading device capture."); + } + } + + private void OnPacketCapture(object sender, PacketCapture capture) + { + var raw = capture.GetPacket(); + if (raw.LinkLayerType == PacketDotNet.LinkLayers.Ethernet) + { + var packet = PacketDotNet.Packet.ParsePacket(raw.LinkLayerType, raw.Data); + var ipPacket = packet.Extract(); + var udpPacket = packet.Extract(); + + if (ipPacket != null && udpPacket != null) + { + try + { + ProcessPacket(ipPacket.SourceAddress, + udpPacket.SourcePort, + ipPacket.DestinationAddress, + udpPacket.DestinationPort, + raw.Timeval.Date, + udpPacket.PayloadData); + } catch(Exception ex) + { + _logger.LogError(ex, "Error processing packet"); + } + } + } + } + + private void ProcessPacket(IPAddress srcAddr, int srcPort, IPAddress dstAddr, int dstPort, DateTime packetTime, ReadOnlySpan data) + { + if(data.Length < 2) + { + _logger.LogTrace("Tossing packet, {0} was less than minimum packet size", data.Length); + return; + } + + var c = FindConnection(srcAddr, srcPort, dstAddr, dstPort); + + if(c != null) + { + c.ProcessPacket(srcAddr, srcPort, packetTime, data); + } + else if (data[0] == 0 && data[1] == Opcode.SessionRequest) + { + c = new Connection(this, srcAddr, srcPort, dstAddr, dstPort); + _connections.Add(c); + c.ProcessPacket(srcAddr, srcPort, packetTime, data); + } + } + + private IConnection FindConnection(IPAddress srcAddr, int srcPort, IPAddress dstAddr, int dstPort) + { + foreach (var c in _connections) + { + if (c.Match(srcAddr, srcPort, dstAddr, dstPort)) + { + return c; + } + } + + return null; + } + } +} diff --git a/utils/stream_parser/common/Util/Crc32.cs b/utils/stream_parser/common/Util/Crc32.cs new file mode 100644 index 000000000..ae0b863bb --- /dev/null +++ b/utils/stream_parser/common/Util/Crc32.cs @@ -0,0 +1,103 @@ +using System; + +namespace StreamParser.Common.Util +{ + public class Crc32 + { + private uint[] _encodeTable = + { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, + 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, + 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, + 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, + 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, + 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, + 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, + 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, + 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, + 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, + 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, + 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, + 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, + 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, + 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, + 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, + 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, + 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, + 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, + 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, + 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, + 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, + 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, + 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, + 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, + 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, + 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, + 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, + 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, + 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, + 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, + 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, + 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, + 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, + 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, + 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, + 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, + 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, + 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, + 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, + 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, + 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, + 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, + 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, + 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, + 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, + 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, + 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, + 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, + 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, + 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, + 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D + }; + + public int Calculate(ReadOnlySpan data) + { + uint crc = 0xffffffff; + + for (int i = 0; i < data.Length; ++i) + { + crc = ((crc >> 8) & 0x00FFFFFF) ^ _encodeTable[(crc ^ data[i]) & 0x000000FF]; + } + + return (int)~crc; + } + + public int Calculate(ReadOnlySpan data, int key) + { + uint crc = 0xffffffff; + for (int i = 0; i < 4; ++i) + { + crc = ((crc >> 8) & 0x00FFFFFF) ^ _encodeTable[(crc ^ ((key >> (i * 8)) & 0xff)) & 0x000000FF]; + } + + for (int i = 0; i < data.Length; ++i) + { + crc = ((crc >> 8) & 0x00FFFFFF) ^ _encodeTable[(crc ^ data[i]) & 0x000000FF]; + } + + return (int)~crc; + } + } +} diff --git a/utils/stream_parser/common/common.csproj b/utils/stream_parser/common/common.csproj new file mode 100644 index 000000000..5d289096f --- /dev/null +++ b/utils/stream_parser/common/common.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + StreamParser.Common + + + + + + + + + diff --git a/utils/stream_parser/stream_parser/ConsoleHostedService.cs b/utils/stream_parser/stream_parser/ConsoleHostedService.cs new file mode 100644 index 000000000..69e071072 --- /dev/null +++ b/utils/stream_parser/stream_parser/ConsoleHostedService.cs @@ -0,0 +1,317 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using StreamParser.Common.Daybreak; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using CommandLine; +using System.Globalization; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Modes; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; + +namespace StreamParser +{ + public class ConsoleHostedService : IHostedService + { + private class ParsedPacket { + public byte[] Data { get; set; } + public Direction Direction { get; set; } + public DateTime Time { get; set; } + } + + private class ParsedConnection + { + public IPAddress ClientAddress { get; set; } + public int ClientPort { get; set; } + public IPAddress ServerAddress { get; set; } + public int ServerPort { get; set; } + public ConnectionType ConnectionType { get; set; } + public List Packets { get; set; } + public DateTime ConnectedTime { get; set; } + public DateTime? DisconnectedTime { get; set; } + } + + private readonly ILogger _logger; + private readonly IHostApplicationLifetime _applicationLifetime; + private readonly IParser _parser; + private readonly Dictionary _connections = new Dictionary(); + + public ConsoleHostedService(ILogger logger, + IHostApplicationLifetime applicationLifetime, + IParser parser) + { + _logger = logger; + _applicationLifetime = applicationLifetime; + _parser = parser; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _applicationLifetime.ApplicationStarted.Register(() => + { + var args = Environment.GetCommandLineArgs(); + + CommandLine.Parser.Default.ParseArguments(args) + .WithParsed(o => + { + _parser.OnNewConnection += OnNewConnection; + _parser.OnLostConnection += OnLostConnection; + + foreach(var f in o.Input) + { + _logger.LogInformation("Parsing {0}...", f); + _parser.Parse(f); + } + + foreach(var c in _connections) + { + if(o.Dump) + { + DumpConnectionToTextFile(c.Value, o.Output, o.Decrypt); + } + + if(o.Csv) + { + DumpConnectionToCsvFile(c.Value, o.Output, o.Decrypt); + } + } + + _applicationLifetime.StopApplication(); + }) + .WithNotParsed(e => + { + bool stops_processing = false; + foreach(var err in e) + { + _logger.LogError("Error: {0}", err.Tag); + stops_processing = stops_processing || err.StopsProcessing; + } + + if(stops_processing) + { + _applicationLifetime.StopApplication(); + } + }); + }); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + private void OnNewConnection(IConnection connection, DateTime connectionTime) + { + _logger.LogTrace("New connection {0}:{1} <-> {2}:{3} of type {4}", + connection.ClientAddress, + connection.ClientPort, + connection.ServerAddress, + connection.ServerPort, + connection.ConnectionType); + connection.OnPacketRecv += OnPacketRecv; + + _connections.Add(connection.Id, new ParsedConnection + { + ClientAddress = connection.ClientAddress, + ClientPort = connection.ClientPort, + ServerAddress = connection.ServerAddress, + ServerPort = connection.ServerPort, + ConnectionType = connection.ConnectionType, + Packets = new List(), + ConnectedTime = connectionTime, + }); + } + + private void OnLostConnection(IConnection connection, DateTime connectionTime) + { + _logger.LogTrace("Lost connection {0}:{1} <-> {2}:{3}", + connection.ClientAddress, + connection.ClientPort, + connection.ServerAddress, + connection.ServerPort); + connection.OnPacketRecv -= OnPacketRecv; + + var parsedConnection = _connections.GetValueOrDefault(connection.Id); + parsedConnection.DisconnectedTime = connectionTime; + } + + private void OnPacketRecv(IConnection connection, Direction direction, DateTime packetTime, ReadOnlySpan data) + { + var parsedConnection = _connections.GetValueOrDefault(connection.Id); + parsedConnection.Packets.Add(new ParsedPacket + { + Data = data.ToArray(), + Direction = direction, + Time = packetTime + }); + } + + private void DumpConnectionToTextFile(ParsedConnection c, string output, bool decrypt) + { + try + { + var path = output + string.Format("{0}-{1}.txt", c.ConnectionType.ToString().ToLower(), c.ConnectedTime.ToString("yyyyMMddHHmmssfff")); + if (File.Exists(path)) + { + File.Delete(path); + } + + File.AppendAllText(path, string.Format("### type: {0}\n", c.ConnectionType)); + File.AppendAllText(path, string.Format("### started: {0}\n", c.ConnectedTime.ToString("s"))); + File.AppendAllText(path, string.Format("### ended: {0}\n", c.DisconnectedTime.HasValue ? c.DisconnectedTime.Value.ToString("s") : "unknown")); + File.AppendAllText(path, string.Format("### client: {0}:{1}\n", c.ClientAddress.ToString(), c.ClientPort)); + File.AppendAllText(path, string.Format("### server: {0}:{1}\n\n", c.ServerAddress.ToString(), c.ServerPort)); + + foreach(var p in c.Packets) + { + ReadOnlySpan data = p.Data; + string dir = p.Direction == Direction.ClientToServer ? "Client -> Server" : "Server -> Client"; + + switch (c.ConnectionType) + { + case ConnectionType.Login: + { + int opcode = BitConverter.ToUInt16(data.Slice(0, 2)); + { + File.AppendAllText(path, + string.Format("{0} [Opcode: 0x{1}, Size: {2}] ({3})\n", dir, opcode.ToString("X4"), data.Length - 2, p.Time.ToString("s"))); + + var gp = new GamePacket(data.Slice(2)); + File.AppendAllText(path, string.Format("{0}\n", gp.ToString())); + + if(decrypt && opcode == 2 || opcode == 24) + { + var encrypted_block = data.Slice(12, data.Length - 12); + var dec = EQDecrypt(encrypted_block); + + if(dec != null) + { + File.AppendAllText(path, string.Format("[Decrypted Data, Offset: {0}, Size: {1}]\n", 10, dec.Length)); + gp = new GamePacket(dec); + File.AppendAllText(path, string.Format("{0}\n", gp.ToString())); + } + } + } + } + break; + case ConnectionType.Chat: + { + int opcode = data[0]; + File.AppendAllText(path, + string.Format("{0} [Opcode: 0x{1}, Size: {2}] ({3})\n", dir, opcode.ToString("X2"), data.Length - 1, p.Time.ToString("s"))); + + var gp = new GamePacket(data.Slice(1)); + File.AppendAllText(path, string.Format("{0}\n", gp.ToString())); + } + break; + default: + { + int opcode = BitConverter.ToUInt16(data.Slice(0, 2)); + File.AppendAllText(path, + string.Format("{0} [Opcode: 0x{1}, Size: {2}] ({3})\n", dir, opcode.ToString("X4"), data.Length - 2, p.Time.ToString("s"))); + + var gp = new GamePacket(data.Slice(2)); + File.AppendAllText(path, string.Format("{0}\n", gp.ToString())); + } + break; + } + } + } + catch(Exception ex) + { + _logger.LogError(ex, "Error dumping connection {0} to txt file", c.ConnectedTime.ToString("s")); + } + } + + private class CsvRow + { + public int Index { get; set; } + public string Direction { get; set; } + public string Opcode { get; set; } + public int Size { get; set; } + public string Data { get; set; } + } + + private void DumpConnectionToCsvFile(ParsedConnection c, string output, bool decrypt) + { + try + { + var path = output + string.Format("{0}-{1}.csv", c.ConnectionType.ToString().ToLower(), c.ConnectedTime.ToString("yyyyMMddHHmmssfff")); + if (File.Exists(path)) + { + File.Delete(path); + } + + var rows = new List(); + var i = 0; + foreach (var p in c.Packets) + { + var row = new CsvRow(); + row.Index = i++; + ReadOnlySpan data = p.Data; + row.Direction = p.Direction == Direction.ClientToServer ? "0" : "1"; + + switch (c.ConnectionType) + { + case ConnectionType.Chat: + { + row.Opcode = data[0].ToString(); + var gp = new GamePacket(data.Slice(1)); + row.Size = data.Length - 1; + row.Data = gp.ToModelString(512, false); + } + break; + default: + { + + row.Opcode = BitConverter.ToUInt16(data.Slice(0, 2)).ToString(); + var gp = new GamePacket(data.Slice(2)); + row.Size = data.Length - 2; + row.Data = gp.ToModelString(512, false); + } + break; + } + + rows.Add(row); + } + + using (var writer = new StreamWriter(path)) + using (var csv = new CsvHelper.CsvWriter(writer, CultureInfo.InvariantCulture)) + { + csv.WriteRecords(rows); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error dumping connection {0} to csv file", c.ConnectedTime.ToString("s")); + } + } + + private byte[] EQDecrypt(ReadOnlySpan data) + { + try + { + var desEngine = new DesEngine(); + var cbcBlockCipher = new CbcBlockCipher(desEngine); + var bufferedBlockCipher = new BufferedBlockCipher(cbcBlockCipher); + bufferedBlockCipher.Init(false, new ParametersWithIV(new KeyParameter(new byte[16]), new byte[8])); + var cipherData = new byte[bufferedBlockCipher.GetOutputSize(data.Length)]; + var outputLength = bufferedBlockCipher.ProcessBytes(data.ToArray(), 0, data.Length, cipherData, 0); + bufferedBlockCipher.DoFinal(cipherData, outputLength); + return cipherData; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error decrypting EQ Datablock"); + return null; + } + } + } +} diff --git a/utils/stream_parser/stream_parser/ConsoleHostedServiceOptions.cs b/utils/stream_parser/stream_parser/ConsoleHostedServiceOptions.cs new file mode 100644 index 000000000..ae7e5da00 --- /dev/null +++ b/utils/stream_parser/stream_parser/ConsoleHostedServiceOptions.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CommandLine; + +namespace StreamParser +{ + public class ConsoleHostedServiceOptions + { + [Option("input", Required = true, HelpText = "Input pcap files to be processed.")] + public IEnumerable Input { get; set; } + + [Option("output", Default = "output/", HelpText = "Directory to put output files")] + public string Output { get; set; } + + [Option("text", Default = true, HelpText = "Dump connections to text files.")] + public bool Dump { get; set; } + + [Option("csv", Default = false, HelpText = "Dump connections to csv files appropriate for building ML models.")] + public bool Csv { get; set; } + + [Option("decrypt", Default = false, HelpText = "Decrypt the \"Encrypted\" packets.")] + public bool Decrypt { get; set; } + } +} diff --git a/utils/stream_parser/stream_parser/Program.cs b/utils/stream_parser/stream_parser/Program.cs new file mode 100644 index 000000000..44fee4d80 --- /dev/null +++ b/utils/stream_parser/stream_parser/Program.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using StreamParser.Common.Daybreak; +using System; + +namespace StreamParser +{ + class Program + { + static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services.AddScoped(); + services.AddHostedService(); + }); + } + } +} diff --git a/utils/stream_parser/stream_parser/Properties/launchSettings.json b/utils/stream_parser/stream_parser/Properties/launchSettings.json new file mode 100644 index 000000000..59fe04b2f --- /dev/null +++ b/utils/stream_parser/stream_parser/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "stream_parser": { + "commandName": "Project", + "commandLineArgs": "--input input/test.pcap --csv --text --decrypt", + "workingDirectory": "E:\\Projects\\stream_parser\\stream_parser\\bin\\Debug\\net5.0" + } + } +} \ No newline at end of file diff --git a/utils/stream_parser/stream_parser/stream_parser.csproj b/utils/stream_parser/stream_parser/stream_parser.csproj new file mode 100644 index 000000000..20cd75e79 --- /dev/null +++ b/utils/stream_parser/stream_parser/stream_parser.csproj @@ -0,0 +1,24 @@ + + + + Exe + net6.0 + StreamParser + + + + false + + + + + + + + + + + + + +