diff --git a/.github/workflows/krabsetw.yml b/.github/workflows/krabsetw.yml
index a8c6565..43c9ecc 100644
--- a/.github/workflows/krabsetw.yml
+++ b/.github/workflows/krabsetw.yml
@@ -13,6 +13,9 @@ jobs:
steps:
- uses: actions/checkout@v2
+ - uses: actions/setup-dotnet@v4
+ with:
+ global-json-file: global.json
- uses: microsoft/setup-msbuild@v1
- uses: NuGet/setup-nuget@v1.0.5
- uses: darenm/Setup-VSTest@v1
diff --git a/Microsoft.O365.Security.Native.ETW/EventRecordMetadata.hpp b/Microsoft.O365.Security.Native.ETW/EventRecordMetadata.hpp
index 7515a78..a368be9 100644
--- a/Microsoft.O365.Security.Native.ETW/EventRecordMetadata.hpp
+++ b/Microsoft.O365.Security.Native.ETW/EventRecordMetadata.hpp
@@ -7,6 +7,11 @@
#include "IEventRecordMetadata.hpp"
#include "Guid.hpp"
+// Windows SDK may not define this constant on older SDK versions.
+#ifndef EVENT_HEADER_EXT_TYPE_PROCESS_START_KEY
+#define EVENT_HEADER_EXT_TYPE_PROCESS_START_KEY 0x000B
+#endif
+
using namespace System;
using namespace System::Runtime::InteropServices;
@@ -232,6 +237,31 @@ namespace Microsoft { namespace O365 { namespace Security { namespace ETW {
return false;
}
+ ///
+ /// If the event's extended data contains a process start key
+ /// (enabled via EVENT_ENABLE_PROPERTY_PROCESS_START_KEY), retrieve it.
+ ///
+ virtual bool TryGetProcessStartKey([Out] uint64_t% result)
+ {
+ auto extended_data_count = record_->ExtendedDataCount;
+ for (USHORT i = 0; i < extended_data_count; i++)
+ {
+ auto& extended_data = record_->ExtendedData[i];
+
+ if (extended_data.ExtType == EVENT_HEADER_EXT_TYPE_PROCESS_START_KEY)
+ {
+ if (extended_data.DataPtr != 0)
+ {
+ result = *reinterpret_cast(extended_data.DataPtr);
+ return true;
+ }
+ }
+ }
+
+ result = 0;
+ return false;
+ }
+
#pragma endregion
};
diff --git a/Microsoft.O365.Security.Native.ETW/IEventRecordMetadata.hpp b/Microsoft.O365.Security.Native.ETW/IEventRecordMetadata.hpp
index eccaa56..0a07b75 100644
--- a/Microsoft.O365.Security.Native.ETW/IEventRecordMetadata.hpp
+++ b/Microsoft.O365.Security.Native.ETW/IEventRecordMetadata.hpp
@@ -149,6 +149,17 @@ namespace Microsoft { namespace O365 { namespace Security { namespace ETW {
///
bool TryGetContainerId([Out] System::Guid% result);
+ ///
+ /// If the event's extended data contains a process start key
+ /// (enabled via EVENT_ENABLE_PROPERTY_PROCESS_START_KEY), retrieve it.
+ /// The process start key uniquely identifies a process instance across
+ /// the lifetime of a boot session, unlike PID which can be recycled.
+ ///
+ ///
+ /// True if a process start key was present. False if not.
+ ///
+ bool TryGetProcessStartKey([Out] uint64_t% result);
+
#pragma endregion
};
diff --git a/Microsoft.O365.Security.Native.ETW/Testing/RecordBuilder.hpp b/Microsoft.O365.Security.Native.ETW/Testing/RecordBuilder.hpp
index 96db2ce..76e28fe 100644
--- a/Microsoft.O365.Security.Native.ETW/Testing/RecordBuilder.hpp
+++ b/Microsoft.O365.Security.Native.ETW/Testing/RecordBuilder.hpp
@@ -67,6 +67,11 @@ namespace Microsoft { namespace O365 { namespace Security { namespace ETW { name
///
void AddContainerId(System::Guid container_id);
+ ///
+ /// Adds a process start key extended data item
+ ///
+ void AddProcessStartKey(System::UInt64 process_start_key);
+
internal:
NativePtr builder_;
@@ -185,4 +190,10 @@ namespace Microsoft { namespace O365 { namespace Security { namespace ETW { name
builder_->add_container_id_extended_data(ConvertGuid(container_id));
}
+ inline void RecordBuilder::AddProcessStartKey(System::UInt64 process_start_key)
+ {
+ ULONG64 key = process_start_key;
+ builder_->add_process_start_key_extended_data(key);
+ }
+
} /* namespace Testing */ } /* namespace ETW */ } /* namespace Security */ } /* namespace O365 */ } /* namespace Microsoft */
\ No newline at end of file
diff --git a/examples/ManagedExamples/Program.cs b/examples/ManagedExamples/Program.cs
index f2f3981..6193b52 100644
--- a/examples/ManagedExamples/Program.cs
+++ b/examples/ManagedExamples/Program.cs
@@ -17,6 +17,7 @@ static void Main(string[] args)
//UserTrace005.Start();
//UserTrace006_Rundown.Start();
//UserTrace007_StackTrace.Start();
+ //UserTrace008_ProcessStartKey.Start();
//FakingEvents001.Start();
//WppTrace001.Start();
}
diff --git a/examples/ManagedExamples/UserTrace008_ProcessStartKey.cs b/examples/ManagedExamples/UserTrace008_ProcessStartKey.cs
new file mode 100644
index 0000000..51b7da0
--- /dev/null
+++ b/examples/ManagedExamples/UserTrace008_ProcessStartKey.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+// This example demonstrates reading the ProcessStartKey from ETW extended data items.
+// The ProcessStartKey uniquely identifies a process instance across a boot session
+// (unlike PID which can be recycled).
+
+using System;
+using System.Security.Principal;
+using System.Threading;
+using Microsoft.O365.Security.ETW;
+
+namespace ManagedExamples
+{
+ public static class UserTrace008_ProcessStartKey
+ {
+ public static void Start()
+ {
+ if (!(new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)))
+ {
+ Console.WriteLine("Microsoft-Windows-Kernel-Process provider requires Administrator privileges.");
+ return;
+ }
+
+ var trace = new UserTrace("UserTrace008_ProcessStartKey");
+ var provider = new Provider("Microsoft-Windows-Kernel-Process");
+ provider.Any = 0x10; // WINEVENT_KEYWORD_PROCESS
+ provider.TraceFlags |= TraceFlags.IncludeProcessStartKey;
+
+ int eventCount = 0;
+ int eventsWithKey = 0;
+ const int maxEvents = 10;
+
+ // Listen for ProcessStart (1) and ProcessStop (2)
+ var filter = new EventFilter(Filter.EventIdIs(1).Or(Filter.EventIdIs(2)));
+ filter.OnEvent += (record) =>
+ {
+ var pid = record.GetUInt32("ProcessID");
+ string imageName;
+ try { imageName = record.GetUnicodeString("ImageName"); }
+ catch { try { imageName = record.GetAnsiString("ImageName"); } catch { imageName = ""; } }
+
+ ulong processStartKey = 0;
+ bool hasKey = record.TryGetProcessStartKey(out processStartKey);
+
+ if (hasKey && processStartKey != 0)
+ Interlocked.Increment(ref eventsWithKey);
+
+ int count = Interlocked.Increment(ref eventCount);
+
+ Console.WriteLine($"[{record.TaskName}] PID={pid} ImageName={imageName} " +
+ $"HasProcessStartKey={hasKey} ProcessStartKey=0x{processStartKey:X}");
+
+ if (count >= maxEvents)
+ {
+ Console.WriteLine($"\nReceived {count} events, {eventsWithKey} had a non-zero ProcessStartKey.");
+ if (eventsWithKey > 0)
+ Console.WriteLine("PASS: ProcessStartKey is being populated in extended data.");
+ else
+ Console.WriteLine("FAIL: No events had a ProcessStartKey set.");
+ trace.Stop();
+ }
+ };
+
+ provider.AddFilter(filter);
+ trace.Enable(provider);
+
+ Console.WriteLine("Listening for process start/stop events (will stop after 10 events)...");
+ Console.WriteLine("Tip: Start or stop some processes to generate events.\n");
+ trace.Start();
+ }
+ }
+}
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..391ba3c
--- /dev/null
+++ b/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "version": "8.0.100",
+ "rollForward": "latestFeature"
+ }
+}
diff --git a/krabs/krabs/schema.hpp b/krabs/krabs/schema.hpp
index 34cc9ba..9016f7b 100644
--- a/krabs/krabs/schema.hpp
+++ b/krabs/krabs/schema.hpp
@@ -18,6 +18,11 @@
#include "compiler_check.hpp"
#include "schema_locator.hpp"
+// Windows SDK may not define this constant on older SDK versions.
+#ifndef EVENT_HEADER_EXT_TYPE_PROCESS_START_KEY
+#define EVENT_HEADER_EXT_TYPE_PROCESS_START_KEY 0x000B
+#endif
+
#pragma comment(lib, "tdh.lib")
@@ -282,6 +287,24 @@ namespace krabs {
*/
std::vector stack_trace() const;
+ /**
+ *
+ * Retrieves the process start key from the extended data, if enabled.
+ * The process start key is a ULONG64 that uniquely identifies a process
+ * instance across the lifetime of a boot session (unlike PID which can
+ * be recycled). Requires EVENT_ENABLE_PROPERTY_PROCESS_START_KEY.
+ * Returns 0 if the extended data item is not present.
+ *
+ *
+ * void on_event(const EVENT_RECORD &record, const krabs::trace_context &trace_context)
+ * {
+ * krabs::schema schema(record, trace_context.schema_locator);
+ * ULONG64 psk = schema.process_start_key();
+ * }
+ *
+ */
+ ULONG64 process_start_key() const;
+
private:
const EVENT_RECORD &record_;
const TRACE_EVENT_INFO *pSchema_;
@@ -299,6 +322,8 @@ namespace krabs {
friend int event_id(const schema &);
friend std::vector stack_trace(const schema&);
friend std::vector stack_trace(const EVENT_RECORD&);
+ friend ULONG64 process_start_key(const schema&);
+ friend ULONG64 process_start_key(const EVENT_RECORD&);
friend class parser;
friend class property_iterator;
@@ -461,4 +486,18 @@ namespace krabs {
return call_stack;
}
+
+ inline ULONG64 schema::process_start_key() const
+ {
+ for (USHORT i = 0; i < record_.ExtendedDataCount; i++)
+ {
+ auto& item = record_.ExtendedData[i];
+ if (item.ExtType == EVENT_HEADER_EXT_TYPE_PROCESS_START_KEY)
+ {
+ if (item.DataPtr != 0)
+ return *reinterpret_cast(item.DataPtr);
+ }
+ }
+ return 0;
+ }
}
diff --git a/krabs/krabs/testing/extended_data_builder.hpp b/krabs/krabs/testing/extended_data_builder.hpp
index 3eeadf2..e0ae9b3 100644
--- a/krabs/krabs/testing/extended_data_builder.hpp
+++ b/krabs/krabs/testing/extended_data_builder.hpp
@@ -17,6 +17,10 @@
#define EVENT_HEADER_EXT_TYPE_CONTAINER_ID 16
#endif
+#ifndef EVENT_HEADER_EXT_TYPE_PROCESS_START_KEY
+ #define EVENT_HEADER_EXT_TYPE_PROCESS_START_KEY 0x000B
+#endif
+
namespace krabs { namespace testing {
class extended_data_builder;
@@ -65,6 +69,9 @@ namespace krabs { namespace testing {
// Mocks a container ID type extended data item.
void add_container_id(const GUID& container_id);
+ // Mocks a process start key type extended data item.
+ void add_process_start_key(const ULONG64& process_start_key);
+
// This generates a contiguous buffer holding all of the data for
// the extended data items. Non-trivial because the actual structs
// have to be a contiguous array, and they each contain pointers,
@@ -107,6 +114,14 @@ namespace krabs { namespace testing {
items_.emplace_back(static_cast(EVENT_HEADER_EXT_TYPE_CONTAINER_ID), guid_data, GUID_STRING_LENGTH_NO_BRACES);
}
+ inline void extended_data_builder::add_process_start_key(const ULONG64& process_start_key)
+ {
+ items_.emplace_back(
+ static_cast(EVENT_HEADER_EXT_TYPE_PROCESS_START_KEY),
+ const_cast(reinterpret_cast(&process_start_key)),
+ sizeof(ULONG64));
+ }
+
inline std::pair, size_t> extended_data_builder::pack() const
{
// Return null for buffer if there are no extended data items.
diff --git a/krabs/krabs/testing/record_builder.hpp b/krabs/krabs/testing/record_builder.hpp
index ded8993..f165e8b 100644
--- a/krabs/krabs/testing/record_builder.hpp
+++ b/krabs/krabs/testing/record_builder.hpp
@@ -144,6 +144,13 @@ namespace krabs { namespace testing {
*/
void add_container_id_extended_data(const GUID& container_id);
+ /**
+ *
+ * Adds extended data representing a process start key
+ *
+ */
+ void add_process_start_key_extended_data(const ULONG64& process_start_key);
+
/**
*
* Gives direct access to the EVENT_HEADER that will be packed into
@@ -308,6 +315,11 @@ namespace krabs { namespace testing {
extended_data_.add_container_id(container_id);
}
+ inline void record_builder::add_process_start_key_extended_data(const ULONG64& process_start_key)
+ {
+ extended_data_.add_process_start_key(process_start_key);
+ }
+
inline std::pair, std::vector>
record_builder::pack_impl(const EVENT_RECORD &record) const
{
diff --git a/tests/ManagedETWTests/Events/PowerShellEvent.cs b/tests/ManagedETWTests/Events/PowerShellEvent.cs
index 0538de6..10aaffc 100644
--- a/tests/ManagedETWTests/Events/PowerShellEvent.cs
+++ b/tests/ManagedETWTests/Events/PowerShellEvent.cs
@@ -48,5 +48,23 @@ public static SynthRecord CreateRecordWithContainerId(
return rb.Pack();
}
}
+
+ public static SynthRecord CreateRecordWithProcessStartKey(
+ string userData,
+ string contextInfo,
+ string payload,
+ ulong processStartKey)
+ {
+ using (var rb = new RecordBuilder(ProviderId, EventId, Version))
+ {
+ rb.AddUnicodeString(UserData, userData);
+ rb.AddUnicodeString(ContextInfo, contextInfo);
+ rb.AddUnicodeString(Payload, payload);
+
+ rb.AddProcessStartKey(processStartKey);
+
+ return rb.Pack();
+ }
+ }
}
}
diff --git a/tests/ManagedETWTests/describe_EventRecord.cs b/tests/ManagedETWTests/describe_EventRecord.cs
index 249e81e..011a42c 100644
--- a/tests/ManagedETWTests/describe_EventRecord.cs
+++ b/tests/ManagedETWTests/describe_EventRecord.cs
@@ -226,6 +226,39 @@ public void it_should_read_container_id()
proxy.PushEvent(PowerShellEvent.CreateRecordWithContainerId(
"Test data", String.Empty, String.Empty, guid));
}
+
+ [TestMethod]
+ public void it_should_read_process_start_key()
+ {
+ ulong expectedKey = 0x123456789ABCDEF0;
+ var provider = new Provider(PowerShellEvent.ProviderId);
+ provider.OnEvent += e =>
+ {
+ ulong processStartKey;
+ Assert.IsTrue(e.TryGetProcessStartKey(out processStartKey));
+ Assert.AreEqual(expectedKey, processStartKey);
+ };
+
+ trace.Enable(provider);
+ proxy.PushEvent(PowerShellEvent.CreateRecordWithProcessStartKey(
+ "Test data", String.Empty, String.Empty, expectedKey));
+ }
+
+ [TestMethod]
+ public void it_should_return_false_when_process_start_key_not_present()
+ {
+ var provider = new Provider(PowerShellEvent.ProviderId);
+ provider.OnEvent += e =>
+ {
+ ulong processStartKey;
+ Assert.IsFalse(e.TryGetProcessStartKey(out processStartKey));
+ Assert.AreEqual(0UL, processStartKey);
+ };
+
+ trace.Enable(provider);
+ proxy.PushEvent(PowerShellEvent.CreateRecord(
+ "Test data", String.Empty, String.Empty));
+ }
}
[TestClass]
diff --git a/tests/krabstests/test_record_builder.cpp b/tests/krabstests/test_record_builder.cpp
index 51ddc66..ee19452 100644
--- a/tests/krabstests/test_record_builder.cpp
+++ b/tests/krabstests/test_record_builder.cpp
@@ -223,6 +223,70 @@ namespace krabstests
Assert::IsTrue(CONTAINER_GUID == krabs::guid(parsed_guid));
}
+ TEST_METHOD(pack_should_include_process_start_key_extended_data)
+ {
+ krabs::guid powershell(L"{A0C1853B-5C40-4B15-8766-3CF1C58F985A}");
+ krabs::testing::record_builder builder(powershell, krabs::id(7942), krabs::version(1));
+ ULONG64 key = 0x123456789ABCDEF0;
+ builder.add_process_start_key_extended_data(key);
+
+ auto synth_record = builder.pack_incomplete();
+ const EVENT_RECORD& record = synth_record;
+
+ Assert::AreEqual((unsigned int)record.ExtendedDataCount, 1u);
+ Assert::IsNotNull(record.ExtendedData);
+ Assert::AreEqual((unsigned int)record.ExtendedData[0].ExtType, (unsigned int)EVENT_HEADER_EXT_TYPE_PROCESS_START_KEY);
+ }
+
+ TEST_METHOD(process_start_key_should_be_read_correctly_after_packing)
+ {
+ const ULONG64 EXPECTED_KEY = 0x123456789ABCDEF0;
+
+ krabs::guid powershell(L"{A0C1853B-5C40-4B15-8766-3CF1C58F985A}");
+ krabs::testing::record_builder builder(powershell, krabs::id(7942), krabs::version(1));
+ ULONG64 key = EXPECTED_KEY;
+ builder.add_process_start_key_extended_data(key);
+
+ auto synth_record = builder.pack_incomplete();
+ const EVENT_RECORD& record = synth_record;
+
+ Assert::AreEqual((unsigned int)record.ExtendedDataCount, 1u);
+ Assert::IsNotNull(record.ExtendedData);
+
+ auto& extended_data = record.ExtendedData[0];
+ Assert::AreEqual((unsigned int)extended_data.ExtType, (unsigned int)EVENT_HEADER_EXT_TYPE_PROCESS_START_KEY);
+ Assert::AreEqual((size_t)extended_data.DataSize, sizeof(ULONG64));
+
+ auto parsed_key = *reinterpret_cast(extended_data.DataPtr);
+ Assert::AreEqual(EXPECTED_KEY, parsed_key);
+ }
+
+ TEST_METHOD(process_start_key_should_be_read_via_schema)
+ {
+ const ULONG64 EXPECTED_KEY = 0xDEADBEEFCAFEBABE;
+
+ krabs::guid powershell(L"{A0C1853B-5C40-4B15-8766-3CF1C58F985A}");
+ krabs::testing::record_builder builder(powershell, krabs::id(7942), krabs::version(1));
+ ULONG64 key = EXPECTED_KEY;
+ builder.add_process_start_key_extended_data(key);
+
+ auto synth_record = builder.pack_incomplete();
+ krabs::schema schema(synth_record, schema_locator_);
+
+ Assert::AreEqual(EXPECTED_KEY, schema.process_start_key());
+ }
+
+ TEST_METHOD(process_start_key_should_return_zero_when_not_present)
+ {
+ krabs::guid powershell(L"{A0C1853B-5C40-4B15-8766-3CF1C58F985A}");
+ krabs::testing::record_builder builder(powershell, krabs::id(7942), krabs::version(1));
+
+ auto synth_record = builder.pack_incomplete();
+ krabs::schema schema(synth_record, schema_locator_);
+
+ Assert::AreEqual((ULONG64)0, schema.process_start_key());
+ }
+
private:
krabs::schema_locator schema_locator_;
};