diff --git a/src/NdisEtl2Pcap.sln b/src/NdisEtl2Pcap.sln new file mode 100644 index 0000000..0042152 --- /dev/null +++ b/src/NdisEtl2Pcap.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28010.2019 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NdisEtl2Pcap", "NdisEtl2Pcap\NdisEtl2Pcap.csproj", "{E62E0034-A80B-4604-B803-F9DA3FCA7C73}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NdisEtl2PcapLib", "NdisEtl2PcapLib\NdisEtl2PcapLib.csproj", "{1861E72D-E4B7-43DF-BCC0-49E3FDD40DA2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E62E0034-A80B-4604-B803-F9DA3FCA7C73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E62E0034-A80B-4604-B803-F9DA3FCA7C73}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E62E0034-A80B-4604-B803-F9DA3FCA7C73}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E62E0034-A80B-4604-B803-F9DA3FCA7C73}.Release|Any CPU.Build.0 = Release|Any CPU + {1861E72D-E4B7-43DF-BCC0-49E3FDD40DA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1861E72D-E4B7-43DF-BCC0-49E3FDD40DA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1861E72D-E4B7-43DF-BCC0-49E3FDD40DA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1861E72D-E4B7-43DF-BCC0-49E3FDD40DA2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {07C12DCE-39C9-45B8-9165-EE2D8181E54D} + EndGlobalSection +EndGlobal diff --git a/src/NdisEtl2Pcap/App.config b/src/NdisEtl2Pcap/App.config new file mode 100644 index 0000000..787dcbe --- /dev/null +++ b/src/NdisEtl2Pcap/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/NdisEtl2Pcap/ExceptionTrapper.cs b/src/NdisEtl2Pcap/ExceptionTrapper.cs new file mode 100644 index 0000000..f1fb80a --- /dev/null +++ b/src/NdisEtl2Pcap/ExceptionTrapper.cs @@ -0,0 +1,56 @@ +using System; +using System.Text; + +namespace NdisEtl2Pcap +{ + internal static class ExceptionTrapper + { + public static void UnhandledExceptionTrapper(object sender, UnhandledExceptionEventArgs e) + { + var ex = (Exception)e.ExceptionObject; + var exceptionInfoText = BuildExceptionInformationText(ex); + Console.Error.WriteLine("**** EXCEPTION ****"); + Console.Error.WriteLine(exceptionInfoText); + } + + public static string BuildExceptionInformationText(Exception exception) + { + var builder = new StringBuilder(); + + var ex = exception; + while (true) + { + builder.AppendLine(string.Format("{0}: {1}", ex.GetType().FullName, ex.Message)); + + // Special handling for each exception type. + switch (ex) + { + case System.ComponentModel.Win32Exception win32Exception: + builder.AppendLine(string.Format("NativeErrorCode: {0}", win32Exception.NativeErrorCode)); + builder.AppendLine(string.Format("ErrorCode: 0x{0:x8}", win32Exception.ErrorCode)); + builder.AppendLine(string.Format("HResult: 0x{0:x8}", win32Exception.HResult)); + break; + } + + if (ex.Data.Count != 0) + { + builder.AppendLine("Data:"); + foreach (string key in ex.Data.Keys) + { + builder.AppendLine(string.Format(" {0}: {1}", key, ex.Data[key])); + } + } + + builder.AppendLine("Stack Trace:"); + builder.AppendLine(ex.StackTrace); + + if (ex.InnerException == null) break; + + ex = ex.InnerException; + builder.AppendLine(@"--- Inner exception is below ---"); + } + + return builder.ToString(); + } + } +} diff --git a/src/NdisEtl2Pcap/NdisEtl2Pcap.csproj b/src/NdisEtl2Pcap/NdisEtl2Pcap.csproj new file mode 100644 index 0000000..fab8e9f --- /dev/null +++ b/src/NdisEtl2Pcap/NdisEtl2Pcap.csproj @@ -0,0 +1,55 @@ + + + + + Debug + AnyCPU + {E62E0034-A80B-4604-B803-F9DA3FCA7C73} + Exe + NdisEtl2Pcap + NdisEtl2Pcap + v4.7.1 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + {1861e72d-e4b7-43df-bcc0-49e3fdd40da2} + NdisEtl2PcapLib + + + + \ No newline at end of file diff --git a/src/NdisEtl2Pcap/Program.cs b/src/NdisEtl2Pcap/Program.cs new file mode 100644 index 0000000..3a47eaa --- /dev/null +++ b/src/NdisEtl2Pcap/Program.cs @@ -0,0 +1,79 @@ +using System; +using System.IO; +using System.Diagnostics; +using System.Reflection; +using NdisEtl2PcapLib; +using NdisEtl2PcapLib.Pcap; + +namespace NdisEtl2Pcap +{ + internal class Program + { + private static ResultSummary ResultSummary { get; set; } + + static void Main(string[] args) + { + AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(ExceptionTrapper.UnhandledExceptionTrapper); + + if (args.Length < 2) + { + PrintUsage(); + return; + } + var etlFilePath = args[0]; + var pcapFilePath = args[1]; + + ResultSummary = new ResultSummary(); + + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + var packets = LoadNdisRecords(etlFilePath); + PcapFile.WritePcapFile(pcapFilePath, packets); + + stopwatch.Stop(); + ResultSummary.Elapsed = stopwatch.Elapsed; + + PrintResultSummary(); + } + + private static void PrintUsage() + { + var exeName = Path.GetFileName(Assembly.GetEntryAssembly().Location); + Console.WriteLine("Usage: {0} ", exeName); + } + + private static NdisEventRecord[] LoadNdisRecords(string etlFilePath) + { + NdisEventRecord[] records; + using (var ndisEtlFile = NdisEtlFile.Load(etlFilePath)) + { + records = ndisEtlFile.Records; + + ResultSummary.TotalEventRecordCount = ndisEtlFile.TotalEventRecordCount; + ResultSummary.TotalNdisEventRecordCount = ndisEtlFile.TotalNdisEventRecordCount; + ResultSummary.OldestNdisEventRecordTimestamp = ndisEtlFile.OldestNdisEventRecordTimestamp; + ResultSummary.NewestNdisEventRecordTimestamp = ndisEtlFile.NewestNdisEventRecordTimestamp; + } + return records; + } + + private static void PrintResultSummary() + { + Console.WriteLine("TotalEventRecordCount: {0}", ResultSummary.TotalEventRecordCount); + Console.WriteLine("TotalNdisEventRecordCount: {0}", ResultSummary.TotalNdisEventRecordCount); + Console.WriteLine("OldestNdisEventRecordTimestamp: {0}", ResultSummary.OldestNdisEventRecordTimestamp); + Console.WriteLine("NewestNdisEventRecordTimestamp: {0}", ResultSummary.NewestNdisEventRecordTimestamp); + Console.WriteLine("Elapsed: {0}", ResultSummary.Elapsed); + } + } + + internal class ResultSummary + { + public long TotalEventRecordCount { get; set; } + public long TotalNdisEventRecordCount { get; set; } + public DateTime OldestNdisEventRecordTimestamp { get; set; } + public DateTime NewestNdisEventRecordTimestamp { get; set; } + public TimeSpan Elapsed { get; set; } + } +} diff --git a/src/NdisEtl2Pcap/Properties/AssemblyInfo.cs b/src/NdisEtl2Pcap/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7bc36f1 --- /dev/null +++ b/src/NdisEtl2Pcap/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("NdisEtl2Pcap")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("NdisEtl2Pcap")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("e62e0034-a80b-4604-b803-f9da3fca7c73")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/NdisEtl2PcapLib/NativeApi/EventTracing.cs b/src/NdisEtl2PcapLib/NativeApi/EventTracing.cs new file mode 100644 index 0000000..806aec6 --- /dev/null +++ b/src/NdisEtl2PcapLib/NativeApi/EventTracing.cs @@ -0,0 +1,369 @@ +using System; +using System.Runtime.InteropServices; + +namespace NdisEtl2PcapLib.NativeApi.EventTracing +{ + // + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724950(v=vs.85).aspx + // + + [StructLayout(LayoutKind.Sequential)] + internal struct SYSTEMTIME + { + public ushort wYear; + public ushort wMonth; + public ushort wDayOfWeek; + public ushort wDay; + public ushort wHour; + public ushort wMinute; + public ushort wSecond; + public ushort wMilliseconds; + } + + // + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx + // + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct TIME_ZONE_INFORMATION + { + public int Bias; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string StandardName; + + public SYSTEMTIME StandardDate; + public int StandardBias; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string DaylightName; + + public SYSTEMTIME DaylightDate; + public int DaylightBias; + } + + // + // https://docs.microsoft.com/en-us/windows/desktop/etw/event-trace-header + // + + [StructLayout(LayoutKind.Sequential)] + internal struct EVENT_TRACE_HEADER + { + public ushort Size; + public ushort FieldTypeFlags; // Reserved. + + public byte ClassType; + public byte ClassLevel; + public byte ClassVersion; + + public uint ThreadId; + public uint ProcessId; + public ulong TimeStamp; + + public Guid Guid; + + public uint ClientContext; // Reserved. + public uint Flags; + } + + // + // https://docs.microsoft.com/en-us/windows/desktop/etw/event-trace + // + + [StructLayout(LayoutKind.Sequential)] + internal struct EVENT_TRACE + { + public EVENT_TRACE_HEADER Header; + public uint InstanceId; + public uint ParentInstanceId; + public Guid ParentGuid; + public IntPtr MofData; + public uint MofLength; + public uint ClientContext; // Reserved. + } + + // + // https://docs.microsoft.com/en-us/windows/desktop/etw/trace-logfile-header + // + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct TRACE_LOGFILE_HEADER + { + public uint BufferSize; + + public byte MajorVersion; + public byte MinorVersion; + public byte SubVersion; // Reserved. + public byte SubMinorVersion; // Reserved. + + public uint ProviderVersion; + public uint NumberOfProcessors; + public long EndTime; + public uint TimerResolution; + public uint MaximumFileSize; + public uint LogFileMode; + public uint BuffersWritten; + + public uint StartBuffers; // Reserved. + public uint PointerSize; + public uint EventsLost; + public uint CpuSpeedInMHz; + + public IntPtr LoggerName; // Do not use. + public IntPtr LogFileName; // Do not use. + public TIME_ZONE_INFORMATION TimeZone; + public long BootTime; + public long PerfFreq; + public long StartTime; + public uint ReservedFlags; + public uint BuffersLost; + } + + // + // PEVENT_TRACE_BUFFER_CALLBACK callback function + // https://docs.microsoft.com/en-us/windows/desktop/etw/buffercallback + // + + internal delegate uint BufferCallback([In] ref EVENT_TRACE_LOGFILE buffer); + + // + // PEVENT_RECORD_CALLBACK callback function + // https://docs.microsoft.com/en-us/windows/desktop/etw/eventrecordcallback + // + + internal delegate void EventRecordCallback([In] ref EVENT_RECORD eventRecord); + + // + // https://docs.microsoft.com/en-us/windows/desktop/etw/event-trace-logfile + // + + [StructLayout(LayoutKind.Sequential)] + internal struct EVENT_TRACE_LOGFILE + { + [MarshalAs(UnmanagedType.LPWStr)] + public string LogFileName; + + [MarshalAs(UnmanagedType.LPWStr)] + public string LoggerName; + + public ulong CurrentTime; + public uint BuffersRead; + public uint ProcessTraceMode; // This app uses only this field within the union. + public EVENT_TRACE CurrentEvent; + public TRACE_LOGFILE_HEADER LogfileHeader; + + [MarshalAs(UnmanagedType.FunctionPtr)] + public BufferCallback BufferCallback; + + public uint BufferSize; + public uint Filled; + public uint EventsLost; // Not used. + + [MarshalAs(UnmanagedType.FunctionPtr)] + public EventRecordCallback EventCallback; + + [MarshalAs(UnmanagedType.Bool)] + public bool IsKernelTrace; + public IntPtr Context; + } + + // + // https://docs.microsoft.com/en-us/windows/desktop/api/evntprov/ns-evntprov-_event_descriptor + // + + [StructLayout(LayoutKind.Sequential)] + internal struct EVENT_DESCRIPTOR + { + public ushort Id; + public byte Version; + public byte Channel; + public byte Level; + public byte Opcode; + public ushort Task; + public ulong Keyword; + } + + // + // https://docs.microsoft.com/en-us/windows/desktop/api/evntcons/ns-evntcons-_event_header + // + + [StructLayout(LayoutKind.Sequential)] + internal struct EVENT_HEADER + { + public ushort Size; + public ushort HeaderType; + public ushort Flags; + public ushort EventProperty; + public uint ThreadId; + public uint ProcessId; + public long TimeStamp; + public Guid ProviderId; + public EVENT_DESCRIPTOR EventDescriptor; + public ulong ProcessorTime; + public Guid ActivityId; + } + + // + // https://docs.microsoft.com/en-us/windows/desktop/api/relogger/ns-relogger-_etw_buffer_context + // + + [StructLayout(LayoutKind.Sequential)] + internal struct ETW_BUFFER_CONTEXT + { + public byte ProcessorNumber; + public byte Alignment; + public ushort LoggerId; + } + + // + // https://docs.microsoft.com/en-us/windows/desktop/api/evntcons/ns-evntcons-_event_record + // + + [StructLayout(LayoutKind.Sequential)] + internal struct EVENT_RECORD + { + public EVENT_HEADER EventHeader; + public ETW_BUFFER_CONTEXT BufferContext; + public ushort ExtendedDataCount; + public ushort UserDataLength; + public IntPtr ExtendedData; + public IntPtr UserData; + public IntPtr UserContext; + } + + // + // _EVENT_HEADER_EXTENDED_DATA_ITEM structure + // https://docs.microsoft.com/en-us/windows/desktop/api/evntcons/ns-evntcons-_event_header_extended_data_item + // + + [StructLayout(LayoutKind.Explicit)] + internal struct EVENT_HEADER_EXTENDED_DATA_ITEM + { + [FieldOffset(0)] + public ushort Reserved1; // Reserved. + + [FieldOffset(2)] + public ushort ExtType; + + [FieldOffset(4)] + public ushort Linkage; + + [FieldOffset(6)] + public ushort DataSize; + + [FieldOffset(8)] + public ulong DataPtr; + } + + // + // The header of NDIS packet fragment record. + // + + [StructLayout(LayoutKind.Sequential)] + internal struct NdisEventRecordPacketFragmentHeader + { + public uint MiniportIfIndex; + public uint LowerIfIndex; + public uint FragmentSize; + } + + internal static class EventTracingApi + { + // ProcessTraceMode in EVENT_TRACE_LOGFILE structure. + public const uint PROCESS_TRACE_MODE_EVENT_RECORD = 0x10000000; + + // The ETW provider ID of Microsoft-Windows-NDIS-PacketCapture provider. + public static Guid EtwNdisPacketCaptureProviderId = new Guid("2ED6006E-4729-4609-B423-3EE7BCD678EF"); + + // The event record ID of packet fragment by Microsoft-Windows-NDIS-PacketCapture provider. + public const ushort EtwNdisPacketFragmentRecordId = 1001; + + // + // OpenTrace function + // https://docs.microsoft.com/en-us/windows/desktop/etw/opentrace + // + + [DllImport("sechost.dll", ExactSpelling = true, EntryPoint = "OpenTraceW", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern ulong OpenTraceSechost(ref EVENT_TRACE_LOGFILE logfile); + + [DllImport("advapi32.dll", ExactSpelling = true, EntryPoint = "OpenTraceW", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern ulong OpenTraceAdvapi32(ref EVENT_TRACE_LOGFILE logfile); + + private delegate ulong OpenTraceNativeApiFunction(ref EVENT_TRACE_LOGFILE logfile); + private static readonly OpenTraceNativeApiFunction OpenTraceNative; + + // + // CloseTrace function + // https://docs.microsoft.com/en-us/windows/desktop/etw/closetrace + // + + [DllImport("sechost.dll", ExactSpelling = true, EntryPoint = "CloseTrace")] + private static extern int CloseTraceSechost(ulong traceHandle); + + [DllImport("advapi32.dll", ExactSpelling = true, EntryPoint = "CloseTrace")] + private static extern int CloseTraceAdvapi32(ulong traceHandle); + + private delegate int CloseTraceNativeApiFunction(ulong traceHandle); + private static readonly CloseTraceNativeApiFunction CloseTraceNative; + + // + // ProcessTrace function + // https://docs.microsoft.com/en-us/windows/desktop/etw/processtrace + // + + [DllImport("sechost.dll", ExactSpelling = true, EntryPoint = "ProcessTrace")] + private static extern uint ProcessTraceSechost(ulong[] handleArray, uint handleCount, IntPtr startTime, IntPtr endTime); + + [DllImport("advapi32.dll", ExactSpelling = true, EntryPoint = "ProcessTrace")] + private static extern uint ProcessTraceAdvapi32(ulong[] handleArray, uint handleCount, IntPtr startTime, IntPtr endTime); + + private delegate uint ProcessTraceNativeApiFunction(ulong[] handleArray, uint handleCount, IntPtr startTime, IntPtr endTime); + private static readonly ProcessTraceNativeApiFunction ProcessTraceNative; + + static EventTracingApi() + { + // Use sechost.dll on Windows 8.1 and Windows Server 2012 R2 and later. Otherwise, use advapi32.dll. + // Because the DLL is different that exports the event tracing functions. + var version = Environment.OSVersion.Version; + var useSechostDll = version.Major > 6 || (version.Major == 6 && version.Minor >= 3); + if (useSechostDll) + { + OpenTraceNative = OpenTraceSechost; + CloseTraceNative = CloseTraceSechost; + ProcessTraceNative = ProcessTraceSechost; + } + else + { + OpenTraceNative = OpenTraceAdvapi32; + CloseTraceNative = CloseTraceAdvapi32; + ProcessTraceNative = ProcessTraceAdvapi32; + } + } + + public static ulong OpenTrace(ref EVENT_TRACE_LOGFILE logfile) + { + return OpenTraceNative(ref logfile); + } + + public static int CloseTrace(ulong traceHandle) + { + return CloseTraceNative(traceHandle); + } + + public static uint ProcessTrace(ulong[] handleArray, uint handleCount, IntPtr startTime, IntPtr endTime) + { + return ProcessTraceNative(handleArray, handleCount, startTime, endTime); + } + + public static bool IsInvalidProcessTraceHandle(ulong traceHandle) + { + // If OpenTrace function fails, + // returns 0xFFFFFFFFFFFFFFFF, if application is 64-bit and OS is Vista and later. + // returns 0x00000000FFFFFFFF, if application is 32-bit and OS is Vista and later. + // Ref: https://docs.microsoft.com/en-us/windows/desktop/etw/opentrace + return (traceHandle == 0) || + (Environment.Is64BitProcess && (traceHandle == 0xFFFFFFFFFFFFFFFF)) || + (!Environment.Is64BitProcess && (traceHandle == 0x00000000FFFFFFFF)); + } + } +} diff --git a/src/NdisEtl2PcapLib/NativeApi/Win32Error.cs b/src/NdisEtl2PcapLib/NativeApi/Win32Error.cs new file mode 100644 index 0000000..9a8de19 --- /dev/null +++ b/src/NdisEtl2PcapLib/NativeApi/Win32Error.cs @@ -0,0 +1,8 @@ +namespace NdisEtl2PcapLib.NativeApi +{ + internal static class Win32ErrorCode + { + public const uint ERROR_SUCCESS = 0; + public const uint ERROR_FILE_CORRUPT = 1392; + } +} diff --git a/src/NdisEtl2PcapLib/NdisEtl2PcapLib.csproj b/src/NdisEtl2PcapLib/NdisEtl2PcapLib.csproj new file mode 100644 index 0000000..701d389 --- /dev/null +++ b/src/NdisEtl2PcapLib/NdisEtl2PcapLib.csproj @@ -0,0 +1,47 @@ + + + + + Debug + AnyCPU + {1861E72D-E4B7-43DF-BCC0-49E3FDD40DA2} + Library + Properties + NdisEtl2PcapLib + NdisEtl2PcapLib + v4.7.1 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NdisEtl2PcapLib/NdisEtlFile.cs b/src/NdisEtl2PcapLib/NdisEtlFile.cs new file mode 100644 index 0000000..6d9b3c0 --- /dev/null +++ b/src/NdisEtl2PcapLib/NdisEtlFile.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using NdisEtl2PcapLib.NativeApi; +using NdisEtl2PcapLib.NativeApi.EventTracing; + +namespace NdisEtl2PcapLib +{ + public sealed class NdisEtlFile : IDisposable + { + public string EtlFilePath { get; private set; } + private ulong TraceHandle { get; set; } + + private List NdisEventRecords { get; set; } + public NdisEventRecord[] Records { get { return NdisEventRecords.ToArray(); } } + + public long TotalEventRecordCount { get; private set; } + public long TotalNdisEventRecordCount { get { return NdisEventRecords.LongCount(); } } + + public DateTime OldestNdisEventRecordTimestamp { get; private set; } + public DateTime NewestNdisEventRecordTimestamp { get; private set; } + + private int CallCountOfBufferCallback { get; set; } + private int CallCountOfEventRecordCallback { get; set; } + private int CallCountOfEventRecordCallbackNdis { get; set; } + + public static NdisEtlFile Load(string etlFilePath) + { + var ndisEtlFile = new NdisEtlFile(etlFilePath); + ndisEtlFile.ReadAllNdisEventRecords(); + return ndisEtlFile; + } + + public void Dispose() + { + if (!EventTracingApi.IsInvalidProcessTraceHandle(TraceHandle)) + { + EventTracingApi.CloseTrace(TraceHandle); + } + } + + private NdisEtlFile(string etlFilePath) + { + EtlFilePath = etlFilePath; + NdisEventRecords = new List(); + TotalEventRecordCount = 0; + OldestNdisEventRecordTimestamp = DateTime.MaxValue; + NewestNdisEventRecordTimestamp = DateTime.MinValue; + } + + private void ReadAllNdisEventRecords() + { + OpenEtlFile(); + ProcessEtlFile(); + } + + private EventRecordCallback EventRecordCallbackRetainer { get; set; } + private BufferCallback BufferCallbackRetainer { get; set; } + + private void OpenEtlFile() + { + // Retained the delegate objects to avoid GC. + EventRecordCallbackRetainer = new EventRecordCallback(EventRecordCallback); + BufferCallbackRetainer = new BufferCallback(BufferCallback); + + // Open the ETL log file. + EVENT_TRACE_LOGFILE traceLogFile = new EVENT_TRACE_LOGFILE + { + LogFileName = EtlFilePath, + ProcessTraceMode = EventTracingApi.PROCESS_TRACE_MODE_EVENT_RECORD, + BufferCallback = BufferCallbackRetainer, + EventCallback = EventRecordCallbackRetainer, + }; + TraceHandle = EventTracingApi.OpenTrace(ref traceLogFile); + + if (EventTracingApi.IsInvalidProcessTraceHandle(TraceHandle)) + { + var win32ErrorCode = Marshal.GetLastWin32Error(); + + string exceptionMessage; + if (win32ErrorCode == Win32ErrorCode.ERROR_FILE_CORRUPT) + { + exceptionMessage = string.Format("OpenTrace function failed. The file '{0}' is corrupted.", EtlFilePath); + } + else + { + exceptionMessage = string.Format("OpenTrace function failed. The file tried to open was '{0}'.", EtlFilePath); + } + + throw new Win32Exception(win32ErrorCode, exceptionMessage); + } + } + + private void ProcessEtlFile() + { + // Start trace processing. + ulong[] traceHandles = new ulong[] { TraceHandle }; + uint result = EventTracingApi.ProcessTrace(traceHandles, (uint)traceHandles.Length, IntPtr.Zero, IntPtr.Zero); + + if (result != Win32ErrorCode.ERROR_SUCCESS) + { + throw new Win32Exception((int)result, string.Format("ProcessTrace function failed with 0x{0:x8}. The file that was open was '{1}'.", result, EtlFilePath)); + } + } + + internal uint BufferCallback(ref EVENT_TRACE_LOGFILE buffer) + { + Debug.Write(nameof(BufferCallback)); + Debug.WriteLine(""); + + CallCountOfBufferCallback++; + return 1; + } + + internal void EventRecordCallback(ref EVENT_RECORD eventRecord) + { + Debug.Write(nameof(EventRecordCallback)); + Debug.Write(string.Format(": TotalEventRecordCount: {0}", TotalEventRecordCount)); + Debug.WriteLine(""); + + // for statistics. + TotalEventRecordCount++; + + // We handle only the NDIS provider's record and packet fragment event. + if (eventRecord.EventHeader.ProviderId == EventTracingApi.EtwNdisPacketCaptureProviderId && + eventRecord.EventHeader.EventDescriptor.Id == EventTracingApi.EtwNdisPacketFragmentRecordId) + { + var timestamp = GetNdisEventRecordTimestampUtc(eventRecord); + var packetFragment = GetNdisEventRecordPacketFragment(eventRecord); + NdisEventRecords.Add(new NdisEventRecord(timestamp, packetFragment)); + + // for statistics. + if (timestamp < OldestNdisEventRecordTimestamp) + { + OldestNdisEventRecordTimestamp = timestamp; + } + + if (timestamp > NewestNdisEventRecordTimestamp) + { + NewestNdisEventRecordTimestamp = timestamp; + } + } + } + + private static DateTime GetNdisEventRecordTimestampUtc(EVENT_RECORD eventRecord) + { + return DateTime.FromFileTime(eventRecord.EventHeader.TimeStamp); + } + + private static byte[] GetNdisEventRecordPacketFragment(EVENT_RECORD eventRecord) + { + var userData = GetEventRecordUserData(eventRecord); + var packetFragmentHeader = GetNdisEventRecordPacketFragmentHeader(userData); + + var packetFragment = new byte[packetFragmentHeader.FragmentSize]; + Buffer.BlockCopy(userData, Marshal.SizeOf(), packetFragment, 0, packetFragment.Length); + return packetFragment; + } + + private static byte[] GetEventRecordUserData(EVENT_RECORD eventRecord) + { + // Copy the unmanaged bytes to the managed byte array. + var userData = new byte[eventRecord.UserDataLength]; + Marshal.Copy(eventRecord.UserData, userData, 0, userData.Length); + return userData; + } + + private static NdisEventRecordPacketFragmentHeader GetNdisEventRecordPacketFragmentHeader(byte[] eventRecordUserData) + { + // Allocate the user data object's handle and disable GC for the user data memory. + var gcHandle = GCHandle.Alloc(eventRecordUserData, GCHandleType.Pinned); + + NdisEventRecordPacketFragmentHeader header; + try + { + // Create a new header structure object from the head bytes of user data memory. + header = (NdisEventRecordPacketFragmentHeader)Marshal.PtrToStructure(gcHandle.AddrOfPinnedObject(), typeof(NdisEventRecordPacketFragmentHeader)); + } + finally + { + // Enable GC for the user data object. + gcHandle.Free(); + } + + return header; + } + } +} diff --git a/src/NdisEtl2PcapLib/NdisEventRecord.cs b/src/NdisEtl2PcapLib/NdisEventRecord.cs new file mode 100644 index 0000000..1676279 --- /dev/null +++ b/src/NdisEtl2PcapLib/NdisEventRecord.cs @@ -0,0 +1,16 @@ +using System; + +namespace NdisEtl2PcapLib +{ + public sealed class NdisEventRecord + { + public DateTime TimestampUtc { get; private set; } + public byte[] PacketFragment { get; private set; } + + internal NdisEventRecord(DateTime timestampUtc, byte[] packetFragment) + { + TimestampUtc = timestampUtc; + PacketFragment = packetFragment; + } + } +} diff --git a/src/NdisEtl2PcapLib/Pcap/PcapFile.cs b/src/NdisEtl2PcapLib/Pcap/PcapFile.cs new file mode 100644 index 0000000..be6120c --- /dev/null +++ b/src/NdisEtl2PcapLib/Pcap/PcapFile.cs @@ -0,0 +1,135 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace NdisEtl2PcapLib.Pcap +{ + public static class PcapFile + { + // + // NPF structures and definitions + // https://www.winpcap.org/docs/docs_412/html/group__NPF__include.html + // + // PCAP-DumpFileFormat + // https://www.winpcap.org/ntar/draft/PCAP-DumpFileFormat_ts.html + // + + // pcap file header contants. + private const uint TCPDUMP_MAGIC = 0xa1b2c3d4; // pcap file header magic. + private const ushort PCAP_VERSION_MAJOR = 2; + private const ushort PCAP_VERSION_MINOR = 4; + private const uint PcapFileHeaderSnapLengthValue = 262144; // Wireshark used this value. Reason is unknown. + private const uint LINKTYPE_ETHERNET = 1; + + // pcap_file_header structure + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct PcapFileHeader + { + public uint Magic; // Magic. + public ushort VersionMajor; // Libpcap major version. + public ushort VersionMinor; // Libpcap minor version. + public int ThisZone; // GMT to local correction. + public uint Sigfigs; // accuracy of timestamps. + public uint SnapLength; // Max length saved portion of each packet. + public uint LinkType; // Data link type. + } + + // timeval structure + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct TimeInterval + { + public int Seconds; + public int Microseconds; + } + + // sf_pkthdr structure + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct PacketHeader + { + public TimeInterval Timestamp; // Timestamp + public uint CapturedPacketLength; // Length of captured portion. + public uint OriginalPacketLength; // Length of the original packet (off wire). + } + + public static void WritePcapFile(string pcapFilePath, NdisEventRecord[] packets) + { + using (var stream = new FileStream(pcapFilePath, FileMode.Create, FileAccess.Write, FileShare.None)) + using (var writer = new BinaryWriter(stream)) + { + WriteFileHeader(writer); + + foreach (var packet in packets) + { + WritePacket(writer, packet); + } + } + } + + private static void WriteFileHeader(BinaryWriter writer) + { + PcapFileHeader pcapFileHeader = new PcapFileHeader() + { + Magic = TCPDUMP_MAGIC, + VersionMajor = PCAP_VERSION_MAJOR, + VersionMinor = PCAP_VERSION_MINOR, + ThisZone = 0, + Sigfigs = 0, + SnapLength = PcapFileHeaderSnapLengthValue, + LinkType = LINKTYPE_ETHERNET, + }; + + var buffer = GetStructAsByteArray(pcapFileHeader); + writer.Write(buffer); + } + + private static void WritePacket(BinaryWriter writer, NdisEventRecord capturedPacket) + { + PacketHeader packetHeader = new PacketHeader() + { + Timestamp = GetTimeInterval(capturedPacket.TimestampUtc), + CapturedPacketLength = (uint)capturedPacket.PacketFragment.Length, + OriginalPacketLength = (uint)capturedPacket.PacketFragment.Length, + }; + + var buffer = GetStructAsByteArray(packetHeader); + writer.Write(buffer); + + writer.Write(capturedPacket.PacketFragment); + } + + private static TimeInterval GetTimeInterval(DateTime timestamp) + { + var unixTimeTicks = timestamp.Subtract(new DateTime(1970, 1, 1)).Ticks; + var seconds = (int)(unixTimeTicks / TimeSpan.TicksPerSecond); + var microseconds = (int)((unixTimeTicks % TimeSpan.TicksPerSecond) / 10); + return new TimeInterval() + { + Seconds = seconds, + Microseconds = microseconds, + }; + } + + private static byte[] GetStructAsByteArray(T structure) + { + // Create a byte array object for the buffer. + var sizeOfStruct = Marshal.SizeOf(structure); + var buffer = new byte[sizeOfStruct]; + + // Allocate the buffer's handle and disable GC for that memory. + var gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + + try + { + // Copy the structure bytes to the allocated buffer. + Marshal.StructureToPtr(structure, gcHandle.AddrOfPinnedObject(), false); + } + finally + { + // Enable GC for the buffer memory. + gcHandle.Free(); + } + + return buffer; + } + } +} diff --git a/src/NdisEtl2PcapLib/Properties/AssemblyInfo.cs b/src/NdisEtl2PcapLib/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..0e2749b --- /dev/null +++ b/src/NdisEtl2PcapLib/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("NdisEtl2PcapLib")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("NdisEtl2PcapLib")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("1861e72d-e4b7-43df-bcc0-49e3fdd40da2")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")]