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_; };