1069 lines
40 KiB
C#
1069 lines
40 KiB
C#
//
|
|
// Copyright (c) 2022-present, Trail of Bits, Inc.
|
|
// All rights reserved.
|
|
//
|
|
// This source code is licensed in accordance with the terms specified in
|
|
// the LICENSE file found in the root directory of this source tree.
|
|
//
|
|
using BrightIdeasSoftware;
|
|
using NtApiDotNet;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.IO;
|
|
|
|
namespace RpcInvestigator.Util
|
|
{
|
|
using static NativeTraceConsumer;
|
|
using static TraceLogger;
|
|
|
|
public class ParsedEtwEvent
|
|
{
|
|
//
|
|
// This struct needs to be kept tidy. Some potentially useful fields have
|
|
// been purposefully left out, as ETW data can be a firehose and memory
|
|
// consumed quickly. Instances of these class are kept in memory as long
|
|
// as the window containing the listview displays them.
|
|
//
|
|
// Further, this field list is reduced to only those observed as meaningful
|
|
// for the Microsoft-Windows-RPC provider. Any other provider will probably
|
|
// want other fields added, like EventId, Version, Provider, etc. To save
|
|
// space, they are removed here instead of keeping them without the [OLVColumn]
|
|
// attribute.
|
|
//
|
|
[OLVColumn]
|
|
public uint ProcessId { get; set; }
|
|
[OLVColumn]
|
|
public long ProcessStartKey { get; set; }
|
|
[OLVColumn]
|
|
public uint ThreadId { get; set; }
|
|
[OLVColumn]
|
|
public Sid UserSid { get; set; }
|
|
[OLVColumn]
|
|
public Guid ActivityId { get; set; }
|
|
[OLVColumn]
|
|
public DateTime Timestamp { get; set; }
|
|
[OLVColumn]
|
|
public string Level { get; set; }
|
|
[OLVColumn]
|
|
public string Channel { get; set; }
|
|
[OLVColumn]
|
|
public List<string> Keywords { get; set; }
|
|
[OLVColumn]
|
|
public string Task { get; set; }
|
|
[OLVColumn]
|
|
public string Opcode { get; set; }
|
|
public Dictionary<string, string> UserDataProperties { get; set; }
|
|
|
|
public override string ToString()
|
|
{
|
|
StringBuilder sb = new StringBuilder();
|
|
//sb.AppendLine("Event ID: " + EventId);
|
|
//sb.AppendLine("Version: " + Version);
|
|
sb.AppendLine("Timestamp: " + Timestamp.ToString());
|
|
sb.AppendLine("Process: " + ProcessId + " (Key=" + ProcessStartKey + ")");
|
|
sb.AppendLine("TID: " + ThreadId);
|
|
if (UserSid != null)
|
|
{
|
|
sb.AppendLine("User SID: " + UserSid.ToString());
|
|
}
|
|
if (ActivityId != null)
|
|
{
|
|
sb.AppendLine("Activity ID: " + ActivityId.ToString());
|
|
}
|
|
sb.AppendLine("Level: " + Level);
|
|
sb.AppendLine("Channel: " + Channel);
|
|
sb.AppendLine("Keywords: ");
|
|
foreach (var k in Keywords)
|
|
{
|
|
sb.AppendLine(" " + k);
|
|
}
|
|
sb.AppendLine("Task: " + Task);
|
|
sb.AppendLine("Opcode: " + Opcode);
|
|
sb.AppendLine("UserData Properties:");
|
|
if (UserDataProperties != null)
|
|
{
|
|
foreach (var kvp in UserDataProperties)
|
|
{
|
|
sb.AppendLine(" " + kvp.Key + " : " + kvp.Value);
|
|
}
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
|
|
public class EtwEventParserBuffers : IDisposable
|
|
{
|
|
public EVENT_RECORD m_Event;
|
|
public TRACE_EVENT_INFO m_TraceEventInfo;
|
|
public EVENT_MAP_INFO m_MapInfo;
|
|
public IntPtr m_TraceEventInfoBuffer;
|
|
public IntPtr m_TdhMapBuffer;
|
|
public IntPtr m_TdhOutputBuffer;
|
|
public IntPtr m_EventBuffer;
|
|
public static int ETW_MAX_EVENT_SIZE = 65536; // 65K
|
|
public static int MAP_SIZE = 1024 * 4000; // 4MB
|
|
public static int TDH_STR_SIZE = 1024 * 4000; // 4MB
|
|
public static int TRACE_EVENT_INFO_SIZE = 1024 * 4000; // 4MB
|
|
private bool m_Disposed;
|
|
|
|
public EtwEventParserBuffers()
|
|
{
|
|
//
|
|
// Pre-allocate large buffers to re-use across all events that
|
|
// reuse this parser. This helps performance tremendously.
|
|
//
|
|
// The map buffer holds manifest data that can be ulong.max size.
|
|
// We pick a decently large size here (4MB)
|
|
//
|
|
// The TDH output buffer contains arbitrary unicode string data,
|
|
// and can be as large as the ETW provider wants AFAIK.
|
|
//
|
|
// Event buffer can never exceed 65k.
|
|
//
|
|
// The TRACE_EVENT_INFO structure is variable-length and the total
|
|
// size depends on the ETW provider's manifest.
|
|
//
|
|
m_TdhMapBuffer = Marshal.AllocHGlobal(MAP_SIZE);
|
|
m_TdhOutputBuffer = Marshal.AllocHGlobal(TDH_STR_SIZE);
|
|
m_EventBuffer = Marshal.AllocHGlobal(ETW_MAX_EVENT_SIZE);
|
|
m_TraceEventInfoBuffer = Marshal.AllocHGlobal(TRACE_EVENT_INFO_SIZE);
|
|
}
|
|
|
|
~EtwEventParserBuffers()
|
|
{
|
|
Dispose(false);
|
|
}
|
|
|
|
private void Dispose(bool disposing)
|
|
{
|
|
if (m_Disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Information,
|
|
"Disposing EtwEventParserBuffers");
|
|
|
|
m_Disposed = true;
|
|
|
|
if (m_TdhMapBuffer != IntPtr.Zero)
|
|
{
|
|
Marshal.FreeHGlobal(m_TdhMapBuffer);
|
|
}
|
|
|
|
if (m_TdhOutputBuffer != IntPtr.Zero)
|
|
{
|
|
Marshal.FreeHGlobal(m_TdhOutputBuffer);
|
|
}
|
|
|
|
if (m_EventBuffer != IntPtr.Zero)
|
|
{
|
|
Marshal.FreeHGlobal(m_EventBuffer);
|
|
}
|
|
|
|
if (m_TraceEventInfoBuffer != IntPtr.Zero)
|
|
{
|
|
Marshal.FreeHGlobal(m_TraceEventInfoBuffer);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
public void SetEvent(EVENT_RECORD Event)
|
|
{
|
|
m_Event = Event;
|
|
Marshal.StructureToPtr(Event, m_EventBuffer, false);
|
|
}
|
|
|
|
public void SetTraceInfo()
|
|
{
|
|
m_TraceEventInfo = (TRACE_EVENT_INFO)Marshal.PtrToStructure(
|
|
m_TraceEventInfoBuffer, typeof(TRACE_EVENT_INFO));
|
|
}
|
|
|
|
public void SetMapInfoBuffer()
|
|
{
|
|
m_MapInfo = (EVENT_MAP_INFO)Marshal.PtrToStructure(
|
|
m_TdhMapBuffer, typeof(EVENT_MAP_INFO));
|
|
}
|
|
}
|
|
|
|
public class EtwEventParser
|
|
{
|
|
private EtwEventParserBuffers m_Buffers;
|
|
private long m_UniqueId;
|
|
private ParsedEtwEvent m_ParsedEvent;
|
|
private long m_PerfFreq;
|
|
|
|
public EtwEventParser(
|
|
EVENT_RECORD Event,
|
|
EtwEventParserBuffers Buffers,
|
|
long PerfFreq)
|
|
{
|
|
m_Buffers = Buffers;
|
|
m_Buffers.SetEvent(Event);
|
|
m_UniqueId = Event.EventHeader.TimeStamp;
|
|
m_PerfFreq = PerfFreq;
|
|
}
|
|
|
|
public
|
|
void
|
|
DumpDiagnosticInfo(string DestinationFolder)
|
|
{
|
|
var size = m_Buffers.m_Event.EventHeader.Size;
|
|
var rawEvent = new byte[size];
|
|
Marshal.Copy(m_Buffers.m_EventBuffer, rawEvent, 0, size);
|
|
string target = Path.Combine(DestinationFolder, "Event-raw-" +
|
|
m_UniqueId.ToString("X") + ".bin");
|
|
File.WriteAllBytes(target, rawEvent);
|
|
target = Path.Combine(DestinationFolder, "Event-text-" +
|
|
m_UniqueId.ToString("X") + ".txt");
|
|
File.WriteAllText(target, m_ParsedEvent.ToString());
|
|
}
|
|
|
|
public
|
|
ParsedEtwEvent
|
|
Parse()
|
|
{
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Verbose,
|
|
"Parsing event 0x" + m_UniqueId.ToString("X"));
|
|
|
|
//
|
|
// Ignore string-only events and those generated by WPP.
|
|
//
|
|
if (m_Buffers.m_Event.EventHeader.Flags.HasFlag(
|
|
EventHeaderFlags.StringOnly) ||
|
|
m_Buffers.m_Event.EventHeader.Flags.HasFlag(
|
|
EventHeaderFlags.TraceMessage))
|
|
{
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Warning,
|
|
"Ignoring string-only or WPP-generated event");
|
|
return null;
|
|
}
|
|
//
|
|
// Ignore events that require legacy WMI MOF manifest to parse user data.
|
|
//
|
|
if (m_Buffers.m_Event.EventHeader.EventProperty.HasFlag(
|
|
EventHeaderPropertyFlags.LegacyEventLog))
|
|
{
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Warning,
|
|
"Ignoring legacy event log style event");
|
|
return null;
|
|
}
|
|
|
|
m_ParsedEvent = new ParsedEtwEvent();
|
|
|
|
//
|
|
// Get stuff hanging right off the EVENT_RECORD's header
|
|
//
|
|
m_ParsedEvent.ProcessId = m_Buffers.m_Event.EventHeader.ProcessId;
|
|
m_ParsedEvent.ThreadId = m_Buffers.m_Event.EventHeader.ThreadId;
|
|
m_ParsedEvent.ActivityId = m_Buffers.m_Event.EventHeader.ActivityId;
|
|
//
|
|
// Event timestamp is stored in QPC format, convert to a scaled
|
|
// 100-ns standard system time that DateTime can cope with.
|
|
//
|
|
var scaledTimestamp = (long)
|
|
(m_Buffers.m_Event.EventHeader.TimeStamp * 10000000.0 / m_PerfFreq);
|
|
m_ParsedEvent.Timestamp = DateTime.FromFileTime(scaledTimestamp);
|
|
|
|
//parsedEvent.ProviderGuid = m_Buffers.m_Event.EventHeader.ProviderId;
|
|
|
|
//
|
|
// The remaining parser steps require TRACE_EVENT_INFO. We cache the
|
|
// resulting buffer internally for the remainder of processing this
|
|
// event.
|
|
//
|
|
if (!GetTraceEventInfo())
|
|
{
|
|
return null;
|
|
}
|
|
|
|
//
|
|
// Use TDH to parse meta information from the event descriptor
|
|
// inside the event header (keywords, opcode, task, etc)
|
|
//
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Information,
|
|
"Parsing trace event meta information");
|
|
if (!ParseMetaInformation())
|
|
{
|
|
return null;
|
|
}
|
|
|
|
//
|
|
// Parse "extended data" which is stuff added by the OS to every
|
|
// ETW event - like SID, stack trace, process start key,
|
|
// and so on (some of these must be turned on when you call
|
|
// EnableTraceEx)
|
|
//
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Information,
|
|
"Parsing trace extended data");
|
|
if (!ParseExtendedData())
|
|
{
|
|
return null;
|
|
}
|
|
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Information,
|
|
m_ParsedEvent.ToString());
|
|
|
|
//
|
|
// Use TDH and the manifest to parse custom "user data" which
|
|
// has information unique to this provider.
|
|
//
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Information,
|
|
"Parsing trace user data");
|
|
if (m_Buffers.m_Event.UserDataLength <= 0)
|
|
{
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Warning,
|
|
"Event contained no user data.");
|
|
return m_ParsedEvent;
|
|
}
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Information,
|
|
"Event has " + m_Buffers.m_Event.UserDataLength +
|
|
" bytes of user data.");
|
|
|
|
//
|
|
// Instantiate a PropertyParser to reuse the pre-allocated buffers.
|
|
//
|
|
var propertyParser = new PropertyParser(m_Buffers);
|
|
if (!propertyParser.Initialize())
|
|
{
|
|
return null;
|
|
}
|
|
|
|
//
|
|
// Parsing begins at the top level properties. The PropertyParser.Parse
|
|
// method will recurse as needed.
|
|
//
|
|
var properties = new Dictionary<string, string>();
|
|
var success = propertyParser.Parse(
|
|
0,
|
|
m_Buffers.m_TraceEventInfo.TopLevelPropertyCount,
|
|
ref properties,
|
|
new StringBuilder());
|
|
m_ParsedEvent.UserDataProperties = properties;
|
|
if (!success)
|
|
{
|
|
return null;
|
|
}
|
|
return m_ParsedEvent;
|
|
}
|
|
|
|
private
|
|
bool
|
|
GetTraceEventInfo()
|
|
{
|
|
uint bufferSize = (uint)EtwEventParserBuffers.TRACE_EVENT_INFO_SIZE;
|
|
var status = TdhGetEventInformation(
|
|
m_Buffers.m_EventBuffer,
|
|
0,
|
|
IntPtr.Zero,
|
|
m_Buffers.m_TraceEventInfoBuffer,
|
|
ref bufferSize);
|
|
if (status != NtApiDotNet.Win32.Win32Error.SUCCESS)
|
|
{
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Error,
|
|
"TdhGetEventInformation failed: " +
|
|
status.ToString("X"));
|
|
return false;
|
|
}
|
|
m_Buffers.SetTraceInfo();
|
|
return true;
|
|
}
|
|
|
|
private
|
|
bool
|
|
ParseMetaInformation()
|
|
{
|
|
var buffer = m_Buffers.m_TraceEventInfoBuffer;
|
|
|
|
try
|
|
{
|
|
/*if (m_Buffers.m_TraceEventInfo.ProviderNameOffset > 0)
|
|
{
|
|
ParsedEvent.Provider = Marshal.PtrToStringUni(
|
|
IntPtr.Add(buffer, m_Buffers.m_TraceEventInfo.ProviderNameOffset));
|
|
}*/
|
|
|
|
if (m_Buffers.m_TraceEventInfo.LevelNameOffset > 0)
|
|
{
|
|
m_ParsedEvent.Level = Marshal.PtrToStringUni(
|
|
IntPtr.Add(buffer, m_Buffers.m_TraceEventInfo.LevelNameOffset));
|
|
}
|
|
else
|
|
{
|
|
m_ParsedEvent.Level = m_Buffers.m_Event.EventHeader.Level.ToString();
|
|
}
|
|
|
|
if (m_Buffers.m_TraceEventInfo.ChannelNameOffset > 0)
|
|
{
|
|
m_ParsedEvent.Channel = Marshal.PtrToStringUni(
|
|
IntPtr.Add(buffer, m_Buffers.m_TraceEventInfo.ChannelNameOffset));
|
|
}
|
|
else
|
|
{
|
|
m_ParsedEvent.Channel =
|
|
m_Buffers.m_Event.EventHeader.Channel.ToString();
|
|
}
|
|
|
|
m_ParsedEvent.Keywords = new List<string>();
|
|
if (m_Buffers.m_TraceEventInfo.KeywordsNameOffset > 0)
|
|
{
|
|
for (int offset = m_Buffers.m_TraceEventInfo.KeywordsNameOffset; ;)
|
|
{
|
|
var str = Marshal.PtrToStringUni(IntPtr.Add(buffer, offset));
|
|
if (string.IsNullOrEmpty(str))
|
|
{
|
|
break;
|
|
}
|
|
m_ParsedEvent.Keywords.Add(str);
|
|
offset += Encoding.Unicode.GetByteCount(str) + 2;
|
|
}
|
|
}
|
|
|
|
if (m_Buffers.m_TraceEventInfo.TaskNameOffset > 0)
|
|
{
|
|
m_ParsedEvent.Task = Marshal.PtrToStringUni(
|
|
IntPtr.Add(buffer, m_Buffers.m_TraceEventInfo.TaskNameOffset));
|
|
}
|
|
else
|
|
{
|
|
m_ParsedEvent.Task = m_Buffers.m_Event.EventHeader.Task.ToString();
|
|
}
|
|
|
|
if (m_Buffers.m_TraceEventInfo.OpcodeNameOffset > 0)
|
|
{
|
|
m_ParsedEvent.Opcode = Marshal.PtrToStringUni(
|
|
IntPtr.Add(buffer, m_Buffers.m_TraceEventInfo.OpcodeNameOffset));
|
|
}
|
|
else
|
|
{
|
|
m_ParsedEvent.Opcode =
|
|
m_Buffers.m_Event.EventHeader.Opcode.ToString();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Error,
|
|
"An exception occurred when marshaling the " +
|
|
"TRACE_EVENT_INFO struct: " + ex.Message);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private
|
|
bool
|
|
ParseExtendedData()
|
|
{
|
|
if (!m_Buffers.m_Event.EventHeader.Flags.HasFlag(
|
|
EventHeaderFlags.ExtendedInfo) ||
|
|
m_Buffers.m_Event.ExtendedDataCount <= 0)
|
|
{
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Warning,
|
|
"Event contained no extended data.");
|
|
return true;
|
|
}
|
|
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Information,
|
|
"Event has " + m_Buffers.m_Event.ExtendedDataCount +
|
|
" extended data items.");
|
|
|
|
IntPtr buffer = m_Buffers.m_Event.ExtendedData;
|
|
for (int i = 0; i < m_Buffers.m_Event.ExtendedDataCount; i++)
|
|
{
|
|
var item = (EVENT_HEADER_EXTENDED_DATA_ITEM)Marshal.PtrToStructure(
|
|
buffer, typeof(EVENT_HEADER_EXTENDED_DATA_ITEM));
|
|
if (item.DataSize == 0)
|
|
{
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Error,
|
|
"Corrupt extended data, zero-length " +
|
|
"extended data item.");
|
|
return false;
|
|
}
|
|
unsafe
|
|
{
|
|
IntPtr data = new IntPtr((void*)item.DataPtr);
|
|
try
|
|
{
|
|
switch (item.ExtType)
|
|
{
|
|
case EventHeaderExtendedDataType.Sid:
|
|
{
|
|
m_ParsedEvent.UserSid = new Sid(data);
|
|
break;
|
|
}
|
|
case EventHeaderExtendedDataType.RelatedActivityId:
|
|
{
|
|
//
|
|
// This is always 0 for RPC events but could
|
|
// be useful in the future.
|
|
//
|
|
//ParsedEvent.ParentActivityId =
|
|
// (Guid)Marshal.PtrToStructure(data, typeof(Guid));
|
|
break;
|
|
}
|
|
case EventHeaderExtendedDataType.ProcessStartKey:
|
|
{
|
|
m_ParsedEvent.ProcessStartKey = Marshal.ReadInt64(data);
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Error,
|
|
"Failed to cast extended data item #" + i +
|
|
", type " + item.ExtType + ", length " + item.DataSize +
|
|
", pointer 0x" + item.DataPtr + ": " + ex.Message);
|
|
return false;
|
|
}
|
|
var size = Marshal.SizeOf(typeof(EVENT_HEADER_EXTENDED_DATA_ITEM));
|
|
buffer = IntPtr.Add(buffer, size);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public class PropertyParser
|
|
{
|
|
private EtwEventParserBuffers m_Buffers;
|
|
private Dictionary<int, ushort> m_PropertyIndexLookup;
|
|
private List<EVENT_PROPERTY_INFO> m_PropertyInfo;
|
|
private IntPtr m_UserDataCurrentPosition;
|
|
private IntPtr m_UserDataEndPosition;
|
|
private ushort m_UserDataRemaining;
|
|
|
|
public PropertyParser(EtwEventParserBuffers Buffers)
|
|
{
|
|
m_Buffers = Buffers;
|
|
m_PropertyIndexLookup = new Dictionary<int, ushort>();
|
|
m_UserDataCurrentPosition = m_Buffers.m_Event.UserData;
|
|
m_UserDataEndPosition =
|
|
IntPtr.Add(m_UserDataCurrentPosition, m_Buffers.m_Event.UserDataLength);
|
|
m_UserDataRemaining = m_Buffers.m_Event.UserDataLength;
|
|
}
|
|
|
|
public
|
|
bool
|
|
Initialize()
|
|
{
|
|
//
|
|
// Parse the property array from the TRACE_EVENT_INFO for this event.
|
|
//
|
|
try
|
|
{
|
|
int offset = Marshal.OffsetOf(typeof(TRACE_EVENT_INFO),
|
|
"EventPropertyInfoArray").ToInt32();
|
|
IntPtr arrayStart = IntPtr.Add(m_Buffers.m_TraceEventInfoBuffer, offset);
|
|
m_PropertyInfo = MarshalHelper.MarshalArray<EVENT_PROPERTY_INFO>(arrayStart,
|
|
(uint)m_Buffers.m_TraceEventInfo.PropertyCount);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Error,
|
|
"Failed to retrieve property array: " + ex.Message);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public
|
|
bool
|
|
Parse(
|
|
int PropertyIndexStart,
|
|
int PropertyIndexEnd,
|
|
ref Dictionary<string, string> Properties,
|
|
StringBuilder ParentStruct)
|
|
{
|
|
for (int i = PropertyIndexStart; i < PropertyIndexEnd; i++)
|
|
{
|
|
var propertyInfo = m_PropertyInfo[i];
|
|
string propertyName = "(Unnamed)";
|
|
if (propertyInfo.NameOffset != 0)
|
|
{
|
|
propertyName = Marshal.PtrToStringUni(
|
|
IntPtr.Add(m_Buffers.m_TraceEventInfoBuffer,
|
|
propertyInfo.NameOffset));
|
|
}
|
|
|
|
bool isArray = false;
|
|
var arrayCount = GetArrayLength(i);
|
|
|
|
if (arrayCount > 1 || propertyInfo.Flags.HasFlag(
|
|
PROPERTY_FLAGS.ParamCount | PROPERTY_FLAGS.ParamFixedCount))
|
|
{
|
|
isArray = true;
|
|
}
|
|
|
|
//
|
|
// If this property is a scalar integer, remember the value
|
|
// in case it is needed for a subsequent property's length
|
|
// or count.
|
|
//
|
|
if (!propertyInfo.Flags.HasFlag(
|
|
PROPERTY_FLAGS.Struct | PROPERTY_FLAGS.ParamCount) &&
|
|
propertyInfo.CountOrCountIndex == 1)
|
|
{
|
|
StorePropertyLookup(i);
|
|
}
|
|
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Verbose,
|
|
"Parsing property #" + i + " [" + propertyName + "]" +
|
|
(isArray ? " - Array with " + arrayCount + " elements" : ""));
|
|
|
|
//
|
|
// For simplicity, non-array properties are treated like 1-length
|
|
// arrays.
|
|
//
|
|
for (int j = 0; j < arrayCount; j++)
|
|
{
|
|
var usePropertyName = propertyName;
|
|
|
|
if (isArray)
|
|
{
|
|
usePropertyName += "-" + j;
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Verbose,
|
|
usePropertyName);
|
|
}
|
|
|
|
StringBuilder structValue = new StringBuilder();
|
|
|
|
if (propertyInfo.Flags.HasFlag(PROPERTY_FLAGS.Struct))
|
|
{
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Verbose,
|
|
" Property is a struct (start index = " +
|
|
propertyInfo.StructStartIndex + ", count = " +
|
|
propertyInfo.NumOfStructMembers + "), recursing...");
|
|
|
|
//
|
|
// Recurse structs.
|
|
//
|
|
structValue.AppendLine(usePropertyName);
|
|
int startIndex = propertyInfo.StructStartIndex;
|
|
int endIndex = startIndex + propertyInfo.NumOfStructMembers;
|
|
if (!Parse(startIndex, endIndex, ref Properties, structValue))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (structValue.Length > 0)
|
|
{
|
|
//
|
|
// Returning from struct recursion? Use that accumulated
|
|
// value for this property.
|
|
//
|
|
AddProperty(usePropertyName,
|
|
structValue.ToString(),
|
|
new StringBuilder(),
|
|
ref Properties);
|
|
}
|
|
else if (!GetPropertyValue(i, j, ref Properties, ParentStruct))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private
|
|
bool
|
|
GetPropertyValue(
|
|
int PropertyIndex,
|
|
int ArrayIndex,
|
|
ref Dictionary<string, string> Properties,
|
|
StringBuilder ParentStruct
|
|
)
|
|
{
|
|
var propertyInfo = m_PropertyInfo[PropertyIndex];
|
|
var useMap = false;
|
|
var propertyName = "(Unnamed)";
|
|
var propertyLength = GetPropertyLength(PropertyIndex);
|
|
var traceEventInfoBuffer = m_Buffers.m_TraceEventInfoBuffer;
|
|
var eventBuffer = m_Buffers.m_EventBuffer;
|
|
|
|
if (propertyInfo.NameOffset != 0)
|
|
{
|
|
propertyName = Marshal.PtrToStringUni(
|
|
IntPtr.Add(traceEventInfoBuffer, propertyInfo.NameOffset));
|
|
}
|
|
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Verbose,
|
|
" Property named " + propertyName + " (length=" +
|
|
propertyLength + ")");
|
|
|
|
//
|
|
// If the property has an associated map (i.e. an enumerated type),
|
|
// try to look up the map data. (If this is an array, we only need
|
|
// to do the lookup on the first iteration.)
|
|
//
|
|
if (propertyInfo.MapNameOffset != 0 && ArrayIndex == 0)
|
|
{
|
|
switch (propertyInfo.InType)
|
|
{
|
|
case TdhInputType.UInt8:
|
|
case TdhInputType.UInt16:
|
|
case TdhInputType.UInt32:
|
|
case TdhInputType.HexInt32:
|
|
{
|
|
var mapName = Marshal.PtrToStringUni(
|
|
IntPtr.Add(traceEventInfoBuffer, propertyInfo.MapNameOffset));
|
|
uint sizeNeeded = (uint)EtwEventParserBuffers.MAP_SIZE;
|
|
var status = TdhGetEventMapInformation(
|
|
eventBuffer,
|
|
mapName,
|
|
m_Buffers.m_TdhMapBuffer,
|
|
ref sizeNeeded).MapDosErrorToStatus();
|
|
if (status != NtStatus.STATUS_SUCCESS)
|
|
{
|
|
//
|
|
// We could retry, but I want to avoid frequent memory
|
|
// allocations. If this happens, investigate increasing
|
|
// the static buffer size.
|
|
//
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Error,
|
|
"TdhGetEventMapInformation() failed: 0x" +
|
|
status.ToString("X"));
|
|
break;
|
|
}
|
|
|
|
useMap = true;
|
|
m_Buffers.SetMapInfoBuffer();
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Loop because we may need to retry the call to TdhFormatProperty.
|
|
//
|
|
for ( ; ; )
|
|
{
|
|
if (IsEmptyProperty(propertyLength, PropertyIndex))
|
|
{
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Verbose,
|
|
" Property has an empty value.");
|
|
AddProperty(propertyName,
|
|
"<empty>",
|
|
ParentStruct,
|
|
ref Properties);
|
|
return true;
|
|
}
|
|
|
|
uint outputBufferSize = 65535; // max is 64kb.
|
|
ushort dataConsumed = 0;
|
|
var result = TdhFormatProperty(
|
|
traceEventInfoBuffer,
|
|
useMap ? m_Buffers.m_TdhMapBuffer : IntPtr.Zero,
|
|
(uint)(m_Buffers.m_Event.EventHeader.Flags.HasFlag(
|
|
EventHeaderFlags.Is32BitHeader) ? 4 : 8),
|
|
propertyInfo.InType,
|
|
propertyInfo.OutType == TdhOutputType.NoPrin ?
|
|
TdhOutputType.Null : propertyInfo.OutType,
|
|
propertyLength,
|
|
m_UserDataRemaining,
|
|
m_UserDataCurrentPosition,
|
|
ref outputBufferSize,
|
|
m_Buffers.m_TdhOutputBuffer,
|
|
ref dataConsumed);
|
|
var status = result.MapDosErrorToStatus();
|
|
if (status == NtStatus.STATUS_BUFFER_TOO_SMALL)
|
|
{
|
|
//
|
|
// We could retry, but I want to avoid frequent memory
|
|
// allocations. However, this should never happen because
|
|
// we allocate the max ushort size the prototype allows.
|
|
//
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Error,
|
|
"TdhFormatProperty() failed, buffer too small: 0x" +
|
|
status.ToString("X"));
|
|
return false;
|
|
}
|
|
else if (result == NtApiDotNet.Win32.Win32Error.ERROR_EVT_INVALID_EVENT_DATA && useMap)
|
|
{
|
|
//
|
|
// If the value isn't in the map, TdhFormatProperty treats it
|
|
// as an error instead of just putting the number in. We'll
|
|
// try again with no map.
|
|
//
|
|
useMap = false;
|
|
continue;
|
|
}
|
|
else if (!status.IsSuccess())
|
|
{
|
|
var error = "TdhFormatProperty() failed: 0x" + status.ToString("X");
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Error,
|
|
error);
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
var propertyValue = Marshal.PtrToStringUni(
|
|
m_Buffers.m_TdhOutputBuffer);
|
|
AddProperty(propertyName,
|
|
propertyValue,
|
|
ParentStruct,
|
|
ref Properties);
|
|
AdvanceBufferPosition(dataConsumed);
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Verbose,
|
|
" Property value is "+propertyValue);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private
|
|
bool
|
|
IsEmptyProperty(int PropertyLength, int PropertyIndex)
|
|
{
|
|
var propertyInfo = m_PropertyInfo[PropertyIndex];
|
|
|
|
//
|
|
// Null data or null-terminated strings are not supported by TdhFormatProperty
|
|
//
|
|
return PropertyLength == 0 &&
|
|
(propertyInfo.Flags.HasFlag(PROPERTY_FLAGS.ParamLength |
|
|
PROPERTY_FLAGS.ParamFixedLength)) &&
|
|
(propertyInfo.InType == TdhInputType.UnicodeString ||
|
|
propertyInfo.InType == TdhInputType.AnsiString);
|
|
}
|
|
|
|
private
|
|
void
|
|
StorePropertyLookup(int PropertyIndex)
|
|
{
|
|
var propertyInfo = m_PropertyInfo[PropertyIndex];
|
|
|
|
if (m_PropertyIndexLookup.ContainsKey(PropertyIndex))
|
|
{
|
|
//
|
|
// Properties that are repeated in arrays will be revisited for each
|
|
// array element. The index lookup is updated each time.
|
|
//
|
|
m_PropertyIndexLookup.Remove(PropertyIndex);
|
|
}
|
|
//
|
|
// Note: The integer values read here from the Marshaler are
|
|
// read from the current position in the buffer and the buffer
|
|
// should NOT be advanced.
|
|
//
|
|
switch (propertyInfo.InType)
|
|
{
|
|
case TdhInputType.Int8:
|
|
case TdhInputType.UInt8:
|
|
{
|
|
if (CanSeek(1))
|
|
{
|
|
var value = Marshal.ReadByte(m_UserDataCurrentPosition);
|
|
m_PropertyIndexLookup.Add(
|
|
PropertyIndex,
|
|
value);
|
|
}
|
|
else
|
|
{
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Error,
|
|
" Unable to read 1 byte for PropertyIndex " +
|
|
PropertyIndex);
|
|
}
|
|
break;
|
|
}
|
|
case TdhInputType.Int16:
|
|
case TdhInputType.UInt16:
|
|
{
|
|
if (CanSeek(2))
|
|
{
|
|
var value = (ushort)Marshal.ReadInt16(
|
|
m_UserDataCurrentPosition);
|
|
m_PropertyIndexLookup.Add(PropertyIndex, value);
|
|
}
|
|
else
|
|
{
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Error,
|
|
" Unable to read 2 bytes for PropertyIndex " +
|
|
PropertyIndex);
|
|
}
|
|
break;
|
|
}
|
|
case TdhInputType.Int32:
|
|
case TdhInputType.UInt32:
|
|
case TdhInputType.HexInt32:
|
|
{
|
|
if (CanSeek(4))
|
|
{
|
|
var value = Marshal.ReadInt32(m_UserDataCurrentPosition);
|
|
var asUshort = (ushort)(value > 0xffff ? 0xff : value);
|
|
m_PropertyIndexLookup.Add(PropertyIndex, asUshort);
|
|
}
|
|
else
|
|
{
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Error,
|
|
" Unable to read 4 bytes for PropertyIndex " +
|
|
PropertyIndex);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private
|
|
ushort
|
|
GetPropertyLength(int PropertyIndex)
|
|
{
|
|
var propertyInfo = m_PropertyInfo[PropertyIndex];
|
|
|
|
if (propertyInfo.OutType == TdhOutputType.Ipv6 &&
|
|
propertyInfo.InType == TdhInputType.Binary &&
|
|
propertyInfo.LengthOrLengthIndex == 0 &&
|
|
!propertyInfo.Flags.HasFlag(
|
|
PROPERTY_FLAGS.ParamLength | PROPERTY_FLAGS.ParamFixedLength))
|
|
{
|
|
//
|
|
// special case for incorrectly-defined IPV6 addresses
|
|
//
|
|
return 16;
|
|
}
|
|
|
|
if (propertyInfo.Flags.HasFlag(PROPERTY_FLAGS.ParamLength))
|
|
{
|
|
//
|
|
// The length of the property was previously parsed, so
|
|
// look up that value now.
|
|
//
|
|
return m_PropertyIndexLookup[propertyInfo.LengthOrLengthIndex];
|
|
}
|
|
|
|
//
|
|
// The length of the property is directly in the field.
|
|
//
|
|
return propertyInfo.LengthOrLengthIndex;
|
|
}
|
|
|
|
private
|
|
ushort
|
|
GetArrayLength(int PropertyIndex)
|
|
{
|
|
var propertyInfo = m_PropertyInfo[PropertyIndex];
|
|
|
|
if (propertyInfo.Flags.HasFlag(PROPERTY_FLAGS.ParamCount))
|
|
{
|
|
//
|
|
// The length of the array was previously parsed, so
|
|
// look up that value now.
|
|
//
|
|
return m_PropertyIndexLookup[propertyInfo.CountOrCountIndex];
|
|
}
|
|
|
|
//
|
|
// The length of the array is directly in the field.
|
|
//
|
|
return propertyInfo.CountOrCountIndex;
|
|
}
|
|
|
|
private
|
|
bool
|
|
CanSeek(int NumBytes)
|
|
{
|
|
return m_UserDataEndPosition.ToInt64() -
|
|
m_UserDataCurrentPosition.ToInt64() >= NumBytes;
|
|
}
|
|
|
|
private
|
|
void
|
|
AdvanceBufferPosition(ushort NumBytes)
|
|
{
|
|
m_UserDataCurrentPosition = IntPtr.Add(
|
|
m_UserDataCurrentPosition, NumBytes);
|
|
m_UserDataRemaining -= NumBytes;
|
|
}
|
|
|
|
private
|
|
void
|
|
AddProperty(
|
|
string PropertyName,
|
|
string PropertyValue,
|
|
StringBuilder ParentStruct,
|
|
ref Dictionary<string, string> Properties)
|
|
{
|
|
if (ParentStruct.Length > 0)
|
|
{
|
|
//
|
|
// We're not directly inserting this property value into the UserData
|
|
// properties because it's actually a field of a struct property we're
|
|
// recursing. Append it to that value.
|
|
//
|
|
ParentStruct.AppendLine("." + PropertyName + " = " + PropertyValue);
|
|
}
|
|
else
|
|
{
|
|
if (Properties.ContainsKey(PropertyName))
|
|
{
|
|
//
|
|
// This is unexpected. ETW event manifests must specify unique
|
|
// property names IIUC. If this turns out to be untrue, we can
|
|
// fudge a unique name here. For now, we'll simply drop this
|
|
// property entirely.
|
|
//
|
|
Trace(TraceLoggerType.EtwEventParser,
|
|
TraceEventType.Error,
|
|
"The property " + PropertyName + " already exists in the list "+
|
|
"of UserData properties parsed from this event. Ignoring this "+
|
|
"discovered property value.");
|
|
return;
|
|
}
|
|
Properties.Add(PropertyName, PropertyValue);
|
|
}
|
|
}
|
|
}
|
|
}
|