├── docs ├── screencap.png ├── LIMITATIONS.md ├── INSTALLATION.md ├── BUFFERING.md ├── COMPARISION.md ├── PARSING_DATA.md ├── CONFIGURATION.md ├── FILTERING.md └── SCENARIOS.md ├── sealighter ├── MSG00001.bin ├── sealighter_provider.rc ├── sealighter_providerTEMP.BIN ├── sealighter_json.h ├── sealighter_controller.h ├── sealighter_krabs.h ├── resource.h ├── sealighter_main.cpp ├── util.h ├── sealighter_errors.h ├── util.cpp ├── sealighter_handler.h ├── sealighter.vcxproj.filters ├── sealighter_util.h ├── sealighter_provider.man ├── sealighter.vcxproj ├── sealighter_util.cpp ├── sealighter_predicates.h ├── sealighter_provider.h ├── sealighter_handler.cpp └── sealighter_controller.cpp ├── .gitmodules ├── .gitignore ├── example_config.json ├── example_config_event_log.json ├── print_output_file.py ├── .github └── workflows │ ├── main.yml │ └── release.yml ├── sealighter.sln └── README.md /docs/screencap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pathtofile/Sealighter/HEAD/docs/screencap.png -------------------------------------------------------------------------------- /sealighter/MSG00001.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pathtofile/Sealighter/HEAD/sealighter/MSG00001.bin -------------------------------------------------------------------------------- /sealighter/sealighter_provider.rc: -------------------------------------------------------------------------------- 1 | LANGUAGE 0x9,0x1 2 | 1 11 "MSG00001.bin" 3 | 1 WEVT_TEMPLATE "sealighter_providerTEMP.BIN" 4 | -------------------------------------------------------------------------------- /sealighter/sealighter_providerTEMP.BIN: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pathtofile/Sealighter/HEAD/sealighter/sealighter_providerTEMP.BIN -------------------------------------------------------------------------------- /sealighter/sealighter_json.h: -------------------------------------------------------------------------------- 1 | // This is just a "healper" header to add json functionality 2 | #pragma once 3 | #include "nlohmann/json.hpp" 4 | using json = nlohmann::json; 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "krabsetw"] 2 | path = krabsetw 3 | url = https://github.com/microsoft/krabsetw.git 4 | branch = master 5 | [submodule "json"] 6 | path = json 7 | url = https://github.com/nlohmann/json.git 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | .vscode/ 3 | x64/ 4 | packages/ 5 | sealighter/x64 6 | *.vcxproj.user 7 | 8 | # Some testing might create this file: 9 | out.json 10 | run.bat 11 | config.json 12 | test_sealighter_provider.man 13 | -------------------------------------------------------------------------------- /sealighter/sealighter_controller.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | /* 5 | * Start Sealighter 6 | */ 7 | int run_sealighter 8 | ( 9 | std::string config_string 10 | ); 11 | 12 | /* 13 | * Stop Sealighter 14 | */ 15 | void stop_sealighter(); 16 | -------------------------------------------------------------------------------- /sealighter/sealighter_krabs.h: -------------------------------------------------------------------------------- 1 | // This is just a "healper" header to deal with the warning in Krabs' header 2 | #pragma once 3 | // Disable Warning for KrabsETW 4 | // and it pollutes our build script with a warning 5 | // we won't deal with because its not our code 6 | #pragma warning( push ) 7 | #pragma warning( disable : 4244 ) 8 | #include "krabs.hpp" 9 | #pragma warning( pop ) 10 | 11 | using namespace krabs; 12 | -------------------------------------------------------------------------------- /sealighter/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by sealighter.rc 4 | // 5 | 6 | // Next default values for new objects 7 | // 8 | #ifdef APSTUDIO_INVOKED 9 | #ifndef APSTUDIO_READONLY_SYMBOLS 10 | #define _APS_NEXT_RESOURCE_VALUE 102 11 | #define _APS_NEXT_COMMAND_VALUE 40001 12 | #define _APS_NEXT_CONTROL_VALUE 1001 13 | #define _APS_NEXT_SYMED_VALUE 101 14 | #endif 15 | #endif 16 | -------------------------------------------------------------------------------- /example_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_properties": { 3 | "session_name": "Sealighter-Trace", 4 | "output_format": "stdout" 5 | }, 6 | "user_traces": [ 7 | { 8 | "trace_name": "ProcTrace01", 9 | "provider_name": "Microsoft-Windows-Kernel-Process", 10 | "keywords_any": 16, 11 | "filters": { 12 | "any_of": { 13 | "event_id_is": [ 1, 2 ] 14 | } 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /example_config_event_log.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_properties": { 3 | "session_name": "Sealighter-Trace", 4 | "output_format": "event_log" 5 | }, 6 | "user_traces": [ 7 | { 8 | "trace_name": "ProcTrace01", 9 | "provider_name": "Microsoft-Windows-Kernel-Process", 10 | "keywords_any": 16, 11 | "filters": { 12 | "any_of": { 13 | "event_id_is": [ 1, 2 ] 14 | } 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /print_output_file.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper script to print an file outputted by Sealighter 3 | """ 4 | import argparse 5 | import json 6 | 7 | 8 | def main(): 9 | parser = argparse.ArgumentParser("Print Sealighter output files") 10 | parser.add_argument("output_file", help="Path to output file") 11 | args = parser.parse_args() 12 | with open(args.output_file, "r") as f: 13 | text = f.read() 14 | 15 | for line in text.split("\n"): 16 | if line.strip() != "": 17 | j = json.loads(line) 18 | print(json.dumps(j, indent=2)) 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: windows-2019 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | with: 17 | submodules: recursive 18 | 19 | - name: Setup MsBuild 20 | uses: microsoft/setup-msbuild@v1.1 21 | 22 | - name: Build Debug 23 | run: msbuild.exe /nologo /m /t:Rebuild /p:Configuration=Debug sealighter.sln 24 | 25 | - name: Build Release 26 | run: msbuild.exe /nologo /m /t:Rebuild /p:Configuration=Release sealighter.sln 27 | -------------------------------------------------------------------------------- /sealighter/sealighter_main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "sealighter_handler.h" 5 | #include "sealighter_errors.h" 6 | #include "sealighter_util.h" 7 | #include "sealighter_controller.h" 8 | /* 9 | Main entrypoint 10 | */ 11 | int main 12 | ( 13 | int argc, 14 | char* argv[] 15 | ) 16 | { 17 | int status = 0; 18 | if (2 != argc) { 19 | log_messageA("usage: %s \n", argv[0]); 20 | return SEALIGHTER_ERROR_NOCONFIG; 21 | } 22 | 23 | std::string config_path = argv[1]; 24 | 25 | if (!file_exists(config_path)) { 26 | log_messageA("Error: Config file doesn't exist\n"); 27 | return SEALIGHTER_ERROR_MISSING_CONFIG; 28 | } 29 | std::ifstream config_stream(config_path); 30 | std::string config_string((std::istreambuf_iterator(config_stream)), 31 | (std::istreambuf_iterator())); 32 | config_stream.close(); 33 | 34 | status = run_sealighter(config_string); 35 | 36 | return status; 37 | } 38 | -------------------------------------------------------------------------------- /sealighter/util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "etw_handler.h" 3 | 4 | struct cmp_wstr 5 | { 6 | bool operator()(const wchar_t* a, const wchar_t* b) const 7 | { 8 | return std::wcscmp(a, b) < 0; 9 | } 10 | }; 11 | 12 | enum class Output_format 13 | { 14 | output_stdout, 15 | output_event_log, 16 | output_all 17 | }; 18 | static Output_format g_output_format = Output_format::output_stdout; 19 | 20 | 21 | void threaded_println(const char* to_print); 22 | 23 | std::string convert_wstr_str 24 | ( 25 | const std::wstring& from 26 | ); 27 | 28 | std::wstring convert_str_wstr( 29 | const std::string& from 30 | ); 31 | 32 | std::string convert_bytes_filetimestring 33 | ( 34 | const std::vector& bytes 35 | ); 36 | 37 | std::string convert_bytes_systemtimestring 38 | ( 39 | const std::vector& bytes 40 | ); 41 | 42 | std::string convert_guid_str 43 | ( 44 | const GUID& in_guid 45 | ); 46 | 47 | std::string convert_bytes_sidstring 48 | ( 49 | const std::vector& bytes 50 | ); 51 | 52 | 53 | std::string convert_bytes_hexstring 54 | ( 55 | const std::vector& bytes 56 | ); 57 | 58 | int convert_bytes_int 59 | ( 60 | const std::vector& bytes 61 | ); -------------------------------------------------------------------------------- /sealighter/sealighter_errors.h: -------------------------------------------------------------------------------- 1 | // Error list 2 | #pragma once 3 | 4 | // No Config file supplied 5 | #define SEALIGHTER_ERROR_NOCONFIG 1 6 | 7 | // Failed to register event log 8 | #define SEALIGHTER_ERROR_EVENTLOG_REGISTER 2 9 | 10 | // Failed to register ctrl+c handler 11 | #define SEALIGHTER_ERROR_CTRL_C_REGISTER 3 12 | 13 | // Failed to parse session properties 14 | #define SEALIGHTER_ERROR_PARSE_CONFIG_PROPS 4 15 | 16 | // Failed to parse Kernel Provider in config 17 | #define SEALIGHTER_ERROR_PARSE_KERNEL_PROVIDER 6 18 | 19 | // No user or kernel providers in config 20 | #define SEALIGHTER_ERROR_PARSE_NO_PROVIDERS 7 21 | 22 | // Bad output format in config 23 | #define SEALIGHTER_ERROR_OUTPUT_FORMAT 8 24 | 25 | // Failed to parse filters in config 26 | #define SEALIGHTER_ERROR_PARSE_FILTER 9 27 | 28 | // Failed to parse User Provider in config 29 | #define SEALIGHTER_ERROR_PARSE_USER_PROVIDER 10 30 | 31 | // Failed to define any ETW Session 32 | #define SEALIGHTER_ERROR_NO_SESSION_CREATED 11 33 | 34 | // Config file doesn't exit 35 | #define SEALIGHTER_ERROR_MISSING_CONFIG 12 36 | 37 | // Counld't open output stream to output file 38 | #define SEALIGHTER_ERROR_OUTPUT_FILE 13 39 | 40 | // Failed to resolve the specified provider 41 | #define SEALIGHTER_ERROR_NO_PROVIDER 14 42 | -------------------------------------------------------------------------------- /docs/LIMITATIONS.md: -------------------------------------------------------------------------------- 1 | # Things Sealighter doesn't do well or at all: 2 | 3 | ## Handle Non-Ascii strings 4 | Things may work with non-ascii filtering, config, or events, but they may not. 5 | 6 | ## Work on 32Bit 7 | Sealighter works only on 64Bit, and no effort will be made to make it work on 32Bit. 8 | 9 | ## Be production-ready 10 | Sealighter is a research tool first, so I would **not** deploy it to 1000s of computer, or as an EDR replacement. 11 | 12 | ## Be consistent between c++ and c code conventions 13 | I mainly work in C, and resort to it more than I probably should, so the C++ parts were thrown together a bit hastily. 14 | 15 | ## Auto-parse WPP traces 16 | The format of WPP events have to be 'manually' defined or extracted from a pdb file, we can't extract them from 17 | the session at runtime. 18 | 19 | Instead set `dump_raw_event` to `true` on the provider to get the hex-encoded bytes, and parse its aftet the fact from there. 20 | 21 | I could in the future enable a user to specify the event format in a tmf or json struct, and then auto-parse the events, 22 | but this isn't currently on the cards. 23 | 24 | # Potential future work: 25 | * Add [Related Activity IDs](https://github.com/microsoft/krabsetw/issues/64) parsing 26 | * Option to pretty-print to file 27 | * Code cleanup (ha) 28 | * Take in a format specification to parse WPP events 29 | -------------------------------------------------------------------------------- /sealighter.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29806.167 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "sealighter", "sealighter\sealighter.vcxproj", "{6624B356-3AA7-4A88-BDFC-B525697ECB38}" 7 | ProjectSection(ProjectDependencies) = postProject 8 | {ED4E6027-541F-440A-A5EE-15DBB7B89423} = {ED4E6027-541F-440A-A5EE-15DBB7B89423} 9 | EndProjectSection 10 | EndProject 11 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.O365.Security.Native.ETW", "krabsetw\Microsoft.O365.Security.Native.ETW\Microsoft.O365.Security.Native.ETW.vcxproj", "{ED4E6027-541F-440A-A5EE-15DBB7B89423}" 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|x64 = Debug|x64 16 | Release|x64 = Release|x64 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {6624B356-3AA7-4A88-BDFC-B525697ECB38}.Debug|x64.ActiveCfg = Debug|x64 20 | {6624B356-3AA7-4A88-BDFC-B525697ECB38}.Debug|x64.Build.0 = Debug|x64 21 | {6624B356-3AA7-4A88-BDFC-B525697ECB38}.Release|x64.ActiveCfg = Release|x64 22 | {6624B356-3AA7-4A88-BDFC-B525697ECB38}.Release|x64.Build.0 = Release|x64 23 | {ED4E6027-541F-440A-A5EE-15DBB7B89423}.Debug|x64.ActiveCfg = Debug|x64 24 | {ED4E6027-541F-440A-A5EE-15DBB7B89423}.Debug|x64.Build.0 = Debug|x64 25 | {ED4E6027-541F-440A-A5EE-15DBB7B89423}.Release|x64.ActiveCfg = Release|x64 26 | {ED4E6027-541F-440A-A5EE-15DBB7B89423}.Release|x64.Build.0 = Release|x64 27 | EndGlobalSection 28 | GlobalSection(SolutionProperties) = preSolution 29 | HideSolutionNode = FALSE 30 | EndGlobalSection 31 | GlobalSection(ExtensibilityGlobals) = postSolution 32 | SolutionGuid = {9CF83EF4-315E-4120-8BEB-6930E74F7739} 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | 9 | jobs: 10 | build: 11 | runs-on: windows-2019 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | with: 16 | submodules: recursive 17 | 18 | - name: Setup MsBuild 19 | uses: microsoft/setup-msbuild@v1.1 20 | 21 | - name: Build Debug 22 | run: msbuild.exe /nologo /m /t:Rebuild /p:Configuration=Debug sealighter.sln 23 | 24 | - name: Build Release 25 | run: msbuild.exe /nologo /m /t:Rebuild /p:Configuration=Release sealighter.sln 26 | 27 | - name: Create Release 28 | id: create_release 29 | uses: actions/create-release@v1 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | with: 33 | tag_name: ${{ github.ref }} 34 | release_name: Release ${{ github.ref }} 35 | draft: false 36 | prerelease: false 37 | 38 | - name: Upload Debug Build 39 | uses: actions/upload-release-asset@v1 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | with: 43 | upload_url: ${{ steps.create_release.outputs.upload_url }} 44 | asset_path: ./x64/Debug/sealighter.exe 45 | asset_name: sealighter.debug.exe 46 | asset_content_type: application/octet-stream 47 | 48 | - name: Upload Release Build 49 | uses: actions/upload-release-asset@v1 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | with: 53 | upload_url: ${{ steps.create_release.outputs.upload_url }} 54 | asset_path: ./x64/Release/sealighter.exe 55 | asset_name: sealighter.exe 56 | asset_content_type: application/octet-stream 57 | 58 | - name: Upload Manifest 59 | uses: actions/upload-release-asset@v1 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | with: 63 | upload_url: ${{ steps.create_release.outputs.upload_url }} 64 | asset_path: ./sealighter/sealighter_provider.man 65 | asset_name: sealighter_provider.man 66 | asset_content_type: application/xml 67 | -------------------------------------------------------------------------------- /docs/INSTALLATION.md: -------------------------------------------------------------------------------- 1 | 2 | # Setup 3 | 4 | Sealighter runs on Windows 7+, x64. 5 | 6 | First, grab the latest binary from [Github Releases](https://github.com/pathtofile/Sealighter/releases).\ 7 | 8 | You might also need the latest Visual C runtime: https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads 9 | 10 | ## Log to Standard Output or a File 11 | 12 | Simply run `sealighter.exe` as Administrator, passing in the path to a JSON config file, e.g.: 13 | ```batch 14 | sealighter.exe path\to\config.json 15 | ``` 16 | 17 | For details on what the config file looks like see **[Configuration](CONFIGURATION.md)**, but a simple example to log all process creations and terminations looks like: 18 | ```json 19 | { 20 | "session_properties": { 21 | "session_name": "My-Process-Trace", 22 | "output_format": "stdout" 23 | }, 24 | "user_traces": [ 25 | { 26 | "trace_name": "proc_trace", 27 | "provider_name": "Microsoft-Windows-Kernel-Process", 28 | "keywords_any": 16 29 | } 30 | ] 31 | } 32 | ``` 33 | 34 | 35 | # Log to Windows Event Log 36 | For high-volume providers, it might be more efficient to log to the Windows Event Log, to ensure events aren't dropped. Prior to being able to write to the Windows Event Log, we need parse some data to the Event Log service. 37 | 38 | Download the latest `sealighter_provider.man` manifest from [Github Releases](https://github.com/pathtofile/Sealighter/releases). 39 | 40 | Open the manifest in a text edit, and replace `!!SEALIGHTER_LOCATION!!` with full path to sealighter.exe Then run the following from an elevated PowerShell or Command Prompt: 41 | ```batch 42 | wevtutil im path/to/sealighter_provider.man 43 | ``` 44 | 45 | Once installed, in the Event Viewer UI under "Applications and Service Logs" you should see an "Sealighter" folder, and "Operational" log. You can also confirm by running this PowerShell: 46 | ```powershell 47 | (Get-WinEvent -LogName "Sealighter/Operational").Length 48 | ``` 49 | 50 | If you move Sealighter, run the command again. To uninstall the log, run: 51 | ```batch 52 | wevtutil um path/to/sealighter_provider.man 53 | ``` 54 | 55 | You can also change the size of the Log on disk in the Event Viewer UI, if you plan on recording many events. 56 | -------------------------------------------------------------------------------- /docs/BUFFERING.md: -------------------------------------------------------------------------------- 1 | # Buffering 2 | Buffering enables the reporting of many similar events in a time period as one with a count. 3 | 4 | Events are buffered together by collapsing events with a matching set of properties. 5 | 6 | For example, in a Process Trace, you could buffer all process starts of `cmd.exe` together, 7 | only reporting one event per minute. That event with contain a `buffered_count` field with the 8 | number of `cmd.exe`s seen that minute. 9 | 10 | 11 | There are two things to configure to enable buffering: 12 | - [Buffering Timeout](#Buffering-Timeout) 13 | - [Per-Provider Buffers](#Per-Provider-Buffers) 14 | 15 | ## Buffering Timeout 16 | By setting the `buffering_timout_seconds` option in the `session_properties` config 17 | header sets the time between 'flushes' of events, and therefore the time period to 18 | report similar events as one: 19 | ```json 20 | "session_properties": { 21 | "buffering_timout_seconds": 10 22 | }, 23 | // ... 24 | ``` 25 | If not set, the default is 30 seconds. 26 | 27 | 28 | ## Per-Provider Buffers 29 | To enable buffering for a provider, add the `buffer` key: 30 | ```json 31 | "user_traces": [ 32 | { 33 | "trace_name": "ProcTrace01", 34 | "provider_name": "Microsoft-Windows-Kernel-Process", 35 | "keywords_any": 16, 36 | "buffers": [ 37 | { 38 | "event_id": 1, 39 | "max_before_buffering": 1, 40 | "fields": [ 41 | "ImageName" 42 | ] 43 | } 44 | ] 45 | } 46 | ``` 47 | 48 | Buffers are configured per event ID, they have the following fields: 49 | - [event_id](#event_id) 50 | - [max_before_buffering](#max_before_buffering) 51 | - [properties_to_match](#properties_to_match) 52 | 53 | 54 | ### event_id 55 | The Event ID to buffer 56 | 57 | 58 | ### max_before_buffering 59 | The number of events to report as-is before starting to buffer. 60 | This enables you to specify "No more than X matching events per Y seconds". 61 | If set to `0` then all matching events will be buffered together. 62 | 63 | 64 | ### properties_to_match 65 | This is an array of the names of the event properties to buffer events on. 66 | Events that have the same values in all of these fields are considered 'the same' 67 | and will be buffered together. 68 | -------------------------------------------------------------------------------- /sealighter/util.cpp: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | #include 3 | 4 | static std::mutex g_print_mutex; 5 | 6 | void threaded_println(const char* to_print) 7 | { 8 | g_print_mutex.lock(); 9 | printf("%s\n", to_print); 10 | g_print_mutex.unlock(); 11 | } 12 | 13 | std::string convert_wstr_str 14 | ( 15 | const std::wstring& from 16 | ) 17 | { 18 | std::string to(from.begin(), from.end()); 19 | return to; 20 | } 21 | 22 | std::wstring convert_str_wstr 23 | ( 24 | const std::string& from 25 | ) 26 | { 27 | std::wstring to(from.begin(), from.end()); 28 | return to; 29 | } 30 | 31 | std::string convert_guid_str 32 | ( 33 | const GUID& in_guid 34 | ) 35 | { 36 | char guid_string[39]; // 2 braces + 32 hex chars + 4 hyphens + null terminator 37 | snprintf( 38 | guid_string, sizeof(guid_string), 39 | "{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", 40 | in_guid.Data1, in_guid.Data2, in_guid.Data3, 41 | in_guid.Data4[0], in_guid.Data4[1], in_guid.Data4[2], 42 | in_guid.Data4[3], in_guid.Data4[4], in_guid.Data4[5], 43 | in_guid.Data4[6], in_guid.Data4[7]); 44 | 45 | return std::string(guid_string); 46 | } 47 | 48 | std::string convert_bytes_systemtimestring 49 | ( 50 | const std::vector& bytes 51 | ) 52 | { 53 | // TODO: Convert SYSTEMTIMEs 54 | // Could call "SystemTimeToFileTime" first then use that function 55 | return convert_bytes_hexstring(bytes); 56 | } 57 | std::string convert_bytes_filetimestring 58 | ( 59 | const std::vector& bytes 60 | ) 61 | { 62 | // TODO: Convert FILETIMEs 63 | return convert_bytes_hexstring(bytes); 64 | } 65 | 66 | std::string convert_bytes_sidstring 67 | ( 68 | const std::vector& bytes 69 | ) 70 | { 71 | // TODO: Convert SIDs 72 | return convert_bytes_hexstring(bytes); 73 | } 74 | 75 | std::string convert_bytes_hexstring 76 | ( 77 | const std::vector& bytes 78 | ) 79 | { 80 | std::ostringstream ss; 81 | ss << std::hex << std::uppercase << std::setfill('0'); 82 | for (int c : bytes) { 83 | ss << std::setw(2) << c; 84 | } 85 | return ss.str(); 86 | } 87 | 88 | int convert_bytes_int 89 | ( 90 | const std::vector& bytes 91 | ) 92 | { 93 | int ret = 0; 94 | if (bytes.size() == 4) { 95 | ret = (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | (bytes[0]); 96 | } 97 | return ret; 98 | } 99 | -------------------------------------------------------------------------------- /sealighter/sealighter_handler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "sealighter_krabs.h" 3 | #include "sealighter_json.h" 4 | 5 | 6 | struct event_buffer_t { 7 | event_buffer_t() 8 | {} 9 | 10 | json json_event; 11 | }; 12 | 13 | 14 | struct event_buffer_list_t { 15 | event_buffer_list_t 16 | ( 17 | std::uint32_t id, 18 | std::uint32_t max 19 | ) 20 | : event_id(id) 21 | , max_before_buffering(max) 22 | , event_count(0) 23 | {} 24 | 25 | const std::uint32_t event_id; 26 | const std::uint32_t max_before_buffering; 27 | 28 | std::uint32_t event_count; 29 | std::vector properties_to_compare; 30 | std::vector json_event_buffered; 31 | }; 32 | 33 | struct sealighter_context_t { 34 | sealighter_context_t 35 | ( 36 | std::string name, 37 | bool dump_event 38 | ) 39 | : trace_name(name) 40 | , dump_raw_event(dump_event) 41 | {} 42 | 43 | const std::string trace_name; 44 | const bool dump_raw_event; 45 | }; 46 | 47 | /* 48 | Parse incoming events into JSON and output 49 | */ 50 | void handle_event 51 | ( 52 | const EVENT_RECORD& record, 53 | const trace_context& trace_context 54 | ); 55 | 56 | /* 57 | Parse incoming events into JSON and output 58 | */ 59 | void handle_event_context 60 | ( 61 | const EVENT_RECORD& record, 62 | const trace_context& trace_context, 63 | std::shared_ptr event_context 64 | ); 65 | 66 | /* 67 | Hold whether we should be outputting the parsed JSON event 68 | */ 69 | enum Output_format 70 | { 71 | output_stdout, 72 | output_event_log, 73 | output_file 74 | }; 75 | 76 | /* 77 | Log an event to stdout, file, or Event log 78 | */ 79 | void log_event 80 | ( 81 | std::string event_string 82 | ); 83 | 84 | 85 | /* 86 | Create stream to write to output file 87 | */ 88 | int setup_logger_file 89 | ( 90 | std::string filename 91 | ); 92 | 93 | /* 94 | Close stream to output file 95 | */ 96 | void teardown_logger_file(); 97 | 98 | /* 99 | Stores the global output format 100 | */ 101 | void set_output_format 102 | ( 103 | Output_format format 104 | ); 105 | 106 | void add_buffered_list 107 | ( 108 | std::string trace_name, 109 | event_buffer_list_t buffered_list 110 | ); 111 | 112 | void set_buffer_lists_timeout 113 | ( 114 | uint32_t timeout 115 | ); 116 | 117 | void start_bufferring(); 118 | 119 | void stop_bufferring(); 120 | -------------------------------------------------------------------------------- /sealighter/sealighter.vcxproj.filters: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | {733f676c-28ca-463f-a9e8-6d54c396614d} 18 | 19 | 20 | 21 | 22 | Source Files 23 | 24 | 25 | Source Files 26 | 27 | 28 | Source Files 29 | 30 | 31 | Source Files 32 | 33 | 34 | 35 | 36 | ExternalHeaders 37 | 38 | 39 | Header Files 40 | 41 | 42 | Header Files 43 | 44 | 45 | Header Files 46 | 47 | 48 | Header Files 49 | 50 | 51 | Header Files 52 | 53 | 54 | Header Files 55 | 56 | 57 | Header Files 58 | 59 | 60 | Header Files 61 | 62 | 63 | Header Files 64 | 65 | 66 | 67 | 68 | Resource Files 69 | 70 | 71 | 72 | 73 | Resource Files 74 | 75 | 76 | -------------------------------------------------------------------------------- /sealighter/sealighter_util.h: -------------------------------------------------------------------------------- 1 | // Utility helper functions 2 | #pragma once 3 | #include "sealighter_json.h" 4 | 5 | 6 | /* 7 | Helper to convert json to string 8 | */ 9 | std::string convert_json_string 10 | ( 11 | json item, 12 | bool pretty_print 13 | ); 14 | 15 | 16 | /* 17 | Helper to convert string to a lowercase version of that string 18 | */ 19 | std::string convert_str_str_lowercase( 20 | const std::string& from 21 | ); 22 | 23 | /* 24 | Helper to convert wstring to a lowercase version of that string 25 | */ 26 | std::wstring convert_wstr_wstr_lowercase( 27 | const std::wstring& from 28 | ); 29 | 30 | /* 31 | Helper to convert widestring to string 32 | */ 33 | std::string convert_wstr_str 34 | ( 35 | const std::wstring& from 36 | ); 37 | 38 | 39 | /* 40 | Helper to convert string to widestring 41 | */ 42 | std::wstring convert_str_wstr( 43 | const std::string& from 44 | ); 45 | 46 | /* 47 | Helper to convert string to widestring lowercase 48 | */ 49 | std::wstring convert_str_wstr_lowercase( 50 | const std::string& from 51 | ); 52 | 53 | /* 54 | Helper to convert string to lowercase byte vector 55 | */ 56 | std::vector convert_str_bytes_lowercase( 57 | const std::string& from 58 | ); 59 | 60 | /* 61 | Helper to convert string to lowercase widechar byte vector 62 | */ 63 | std::vector convert_str_wbytes_lowercase( 64 | const std::string& from 65 | ); 66 | 67 | /* 68 | Helper to convert LARGE_INTEGER timestamp to string 69 | MSDN States this is: 70 | Time at which the information in this structure was updated, 71 | in 100-nanosecond intervals since midnight, January 1, 1601. 72 | 73 | This is the same as a FILETIME 74 | */ 75 | std::string convert_timestamp_string 76 | ( 77 | const LARGE_INTEGER from 78 | ); 79 | 80 | 81 | /* 82 | Helper to convert FILETIME to string 83 | */ 84 | std::string convert_filetime_string 85 | ( 86 | const FILETIME from 87 | ); 88 | 89 | 90 | /* 91 | Helper to convert SYSTEMTIME to string 92 | */ 93 | std::string convert_systemtime_string 94 | ( 95 | const SYSTEMTIME from 96 | ); 97 | 98 | 99 | /* 100 | Helper to convert byte array to string, 101 | treating the bytes as a GUID 102 | */ 103 | std::string convert_guid_str 104 | ( 105 | const GUID& from 106 | ); 107 | 108 | /* 109 | Helper to convert widestring to GUID. 110 | If this fails the GUID will be NULL_GUID 111 | */ 112 | GUID convert_wstr_guid 113 | ( 114 | std::wstring from 115 | ); 116 | 117 | /* 118 | Helper to convert string to GUID. 119 | If this fails the GUID will be NULL_GUID 120 | */ 121 | GUID convert_str_guid 122 | ( 123 | std::string from 124 | ); 125 | 126 | 127 | /* 128 | Helper to convert byte array to string, 129 | treating the bytes as a SID 130 | */ 131 | std::string convert_bytes_sidstring 132 | ( 133 | const std::vector& from 134 | ); 135 | 136 | 137 | /* 138 | Helper to convert byte vector to a 139 | hexidecimal string representation 140 | (will *not* have a leading "0x") 141 | */ 142 | std::string convert_bytevector_hexstring 143 | ( 144 | const std::vector& from 145 | ); 146 | 147 | /* 148 | Helper to convert byte array to a 149 | hexidecimal string representation 150 | (will *not* have a leading "0x") 151 | */ 152 | std::string convert_bytearray_hexstring 153 | ( 154 | BYTE* from, 155 | int len 156 | ); 157 | 158 | /* 159 | Helper to convert a ULONG64 to a 160 | hex string representation 161 | */ 162 | std::string convert_ulong64_hexstring 163 | ( 164 | const ULONG64 from 165 | ); 166 | 167 | 168 | /* 169 | Helper to convert byte array to an int 170 | */ 171 | int convert_bytes_sint32 172 | ( 173 | const std::vector& from 174 | ); 175 | 176 | 177 | /* 178 | Helper to convert byte array to a boolean 179 | Simply converts to an int, then checks does 180 | "if(value)..." 181 | */ 182 | bool convert_bytes_bool 183 | ( 184 | const std::vector& from 185 | ); 186 | 187 | /* 188 | Checks if a file exits on disk 189 | */ 190 | bool file_exists 191 | ( 192 | std::string fileName 193 | ); 194 | 195 | VOID log_messageA(const CHAR* format, ...); 196 | VOID log_messageW(const WCHAR* format, ...); 197 | -------------------------------------------------------------------------------- /sealighter/sealighter_provider.man: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /docs/COMPARISION.md: -------------------------------------------------------------------------------- 1 | # Comparison to SilkETW 2 | 3 | [SilkETW](https://github.com/fireeye/SilkETW) is another Open Source tool designed to help understand the power of ETW and the data it can produce. 4 | 5 | I tested it out when FuzzySec first released it, but came across a couple of issue that mean it didn't replace my personal tooling as a research platform. 6 | 7 | I really want to stress, my goal and primary use case of Sealighter is as a *research* platform. SilkETW has some nice features to make it more suitable in a production environment. But for a platform to research new ETW, TraceLogging, and WPP providers and events, The following were challenges that led me to turn my tooling into a proper project: 8 | 9 | 10 | ## Production Vs Research 11 | Sealighter is designed to help researchers understand and dig into ETW. 12 | I do not advise anyone using it in a production environment to protect their network, mostly due to it being a single person growing/maintaing this 13 | project. Use the ideas you learn from using Sealighter to enhanse your own tools or situational awareness. 14 | 15 | 16 | ## Parsing of some Events 17 | Under the hood, Silk uses the .NET library [Microsoft.Diagnostics.Tracing.TraceEvent](https://github.com/microsoft/perfview/blob/master/documentation/TraceEvent/TraceEventLibrary.md) for its base ETW handling. 18 | In the past, I've had issues with this library not fully parsing events, reporting only a fraction of the event's properties. An example of this was the `Event(1001)` in the `Microsoft-Windows-WFP` Provider. 19 | 20 | Additionally, this library is only .NET, and only provides basic ETW Session controlling, meaning FuzzySec had to implement his own filtering logic. 21 | 22 | Comparatively, Sealighter is built on the [KrabsETW](https://github.com/microsoft/krabsetw) Library. This library has both a C++ native library (with Sealighter uses), and a .NET wrapper. Additionally, it provides a lot of extremely useful and efficient event filtering options, which makes up the bulk of Sealighter's filtering (i.e. I didn't have to write them, just fix a couple of bugs in them, which I got merged back into Krabs). 23 | 24 | 25 | ## Filtering 26 | ETW can be extremely verbose, making very difficult to find useful data in sea of events. 27 | SilkETW, provides a few basic ways to filter events, alongside a powerful Yara rule matching engine. 28 | The Yara rule matching is super powerful - run against the JSON serialised event, you can write any filter to match any part of the event. For Sealighter I decided this was not the right approach for two main reasons: 29 | 30 | 31 | ### 1. Performance 32 | Silk has to first received the event, parse it into JSON, *then* run the Yara Scanning over it. This comes at a performance cost, as you are spending time parsing and converting events to JSON only to drop them. 33 | 34 | Sealighter instead filters events as early as possible using a static list of filters you can read about [here](FILTERING.md), before converting them to JSON. This ensures much higher performance, particularly for high-volume providers. Additionally, Yara scanning the entire event it much less efficient compared to searching just the event property you want to filter. 35 | 36 | ### 2. Separating filters and config 37 | Silk separates the Yara filters from the config, reading them in from a separate file/folder. I'm not a fan of this, and prefer to more tightly couple the Provider of interest, and the filter I am running of it, in a single place inside the config. 38 | 39 | 40 | ## Performance and resource usage 41 | Silk is written in .NET, whereas Sealighter is in native C++. 42 | For some high-volume providers, I have found a difference in memory and CPU usage, with the lower-level C++ performing better, mostly due to the reduced amount of memory allocating it does before filtering events. 43 | 44 | 45 | ## Opcode parsing wrong 46 | This is an issue that is easily fixed, and I only realised as I was writing this I forgot to raise an issue on SilkETW's tracker, despite hitting it when I first checked it out, so I've made sure to [add it](https://github.com/fireeye/SilkETW/issues/13). 47 | 48 | In Silk, when creating a filter by Opcode, Silk manually checks this Opcode is between 0-9. 49 | However, Opcodes are a UCHAR, so they can actually be up to 255. 50 | 51 | 52 | ## Stack Traces 53 | SilkETW doesn't enable reporting of Event Stack Traces, a very useful set of data to get in some circumstances. 54 | Sealighter has the ability to do this for user mode providers by enabling the `report_stacktrace` option on a 55 | per-provider basis 56 | 57 | ## Buffering 58 | ETW can produce lots and lots of events. I added the ability to buffer many similar events in time period into 59 | one event with a count. 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sealighter - Easy ETW Tracing for Security Research 2 | 3 | ![CI](https://github.com/pathtofile/Sealighter/workflows/CI/badge.svg?branch=master) 4 | 5 | I created this project to help non-developers dive into researching Event Tracing for Windows (ETW) and Windows PreProcessor Tracing (WPP). 6 | 7 | # Features 8 | - Subscribe to multiple ETW and WPP Providers at once 9 | - Automatically parse events into JSON without needing to know format 10 | - Robust Event filtering including filter chaining and filter negation 11 | - Output to Standard out, File, or Windows Event Log (to be ingested by other tools) 12 | - Get event stack traces 13 | - Configurable Buffering many events in a time period into one with a count, to reduce the number of events generated 14 | 15 | 16 | ![Screenshot of Sealighter running](docs/screencap.png) 17 | 18 | # Overview 19 | Sealighter leverages the feature-rich [Krabs ETW](https://github.com/microsoft/krabsetw) Library to enable detailed filtering and triage of ETW and WPP Providers and Events. 20 | 21 | You can subscribe and filter multiple providers, including User mode Providers, Kernel Tracing, and WPP Tracing, and output events as JSON to either stdout, a file, or the Windows Event Log (useful for high-volume traces like `FileIO`). No knowledge of the events the provider may produce, or their format, is necessary, Sealighter automatically captures and parses any events it is asked. 22 | 23 | Events can then be parsed from JSON in Python, PowerShell, or forwarded to Splunk or ELK for further searching. 24 | 25 | Filtering can be done on various aspects of an Event, from its ID or Opcode, to matching a property value, to doing an arbitrary string search across the entire event (Useful in WPP traces or when you don't know the event structure, but have an idea of its contents). You can also chain multiple filters together, or negate the filter. You can also filter the maximum events per ID, useful to investigate a new provider without being flooded by similar events. 26 | 27 | 28 | # Why this exists 29 | ETW is an incredibly useful system for both Red and Blue teams. Red teams may glean insight into the inner workings of Windows components, and Blue teams might get valuable insight into suspicious activity. 30 | 31 | A common research loop would be: 32 | 1. Identify interesting ETW Providers using `logman query providers` or Looking for WPP Traces in Binaries 33 | 2. Start a Session with the interesting providers enable, and capture events whilst doing something 'interesting' 34 | 3. Look over the results, using one or more of: 35 | - Eyeballing each event/grepping for words you expect to see 36 | - Run a script in Python or PowerShell to help filter or find interesting captured events 37 | - Ingesting the data into Splunk or an ELK stack for some advanced UI-driven searching 38 | 39 | Doing this with ETW Events can be difficult, without writing code to interact with and parse events from the obtuse ETW API. If you're not a strong programmer (or don't want to deal with the API), your only other options are to use a combination of older inbuilt windows tools to write to disk as binary `etl` files, then dealing with those. WPP traces compounds the issues, providing almost no easy-to-find data about provider and their events. 40 | 41 | Projects like [JDU2600's Event List ](https://github.com/jdu2600/Windows10EtwEvents) and [ETWExplorer](https://github.com/zodiacon/EtwExplorer) and give some static insight, but Providers often contain obfuscated event names like `Event(1001)`, meaning the most interesting data only becomes visible by dynamically running a trace and observing the output. 42 | 43 | 44 | # So like SilkETW? 45 | In a way, this plays in a similar space as FuzzySec's [SilkETW](https://github.com/fireeye/SilkETW). But While Silk is more production-ready for defenders, this is designed for researchers like myself, and as such contains a number of features that I couldn't get with Silk, mostly due to the different Library they used to power the tool. Please see [Here](docs/COMPARISION.md) for more information. 46 | 47 | # Intended Audience 48 | Probably someone who understands the basic of ETW, and really wants to dive into discovering what data you can glean from it, without having to write code or manually figure out how to get and parse events. 49 | 50 | # Getting Started 51 | 52 | Please read the following pages: 53 | 54 | **[Installation](docs/INSTALLATION.md)** - How to start running Sealighter, including a simple config, and how to set up Windows Event logging if required. 55 | 56 | **[Configuration](docs/CONFIGURATION.md)** - How to configure Sealighter, including how to specify what Providers to Log, and where to log to. 57 | 58 | **[Filtering](docs/FILTERING.md)** - Deep dive into all the types of filtering Sealighter provides. 59 | 60 | **[Buffering](docs/BUFFERING.md)** - How to use buffering to report many similar events as one 61 | 62 | **[Parsing Data](docs/PARSING_DATA.md)** - How to get and parse data from Sealighter. 63 | 64 | **[Scenarios](docs/SCENARIOS.md)** - Walkthrough example scenarios of how I've used Sealighter in my research. 65 | 66 | **[Limitations](docs/LIMITATIONS.md)** - Things Sealighter doesn't do well or at all. 67 | 68 | # Why it's called Sealighter 69 | The name is a contraction of [Seafood Highlighter](https://en.wikipedia.org/wiki/Seafood_extender), which is what we call fake crab meat in Oz. As it's built on Krabs ETW, I thought the name was funny. 70 | 71 | # Found problems? 72 | Feel free to raise an issue, although as I state in the [comparison docs](docs/COMPARISION.md) I'm only a single person, and this is a research-ready 73 | tool, not a production-ready. 74 | 75 | # Props and further reading 76 | - [Great Blog on ETW and WPP from Matt Graeber](https://posts.specterops.io/data-source-analysis-and-dynamic-windows-re-using-wpp-and-tracelogging-e465f8b653f7) 77 | - [JDU2600's Event List ](https://github.com/jdu2600/Windows10EtwEvents) 78 | - [ETWExplorer](https://github.com/zodiacon/EtwExplorer) 79 | - [Krabs ETW, the library that powers Sealighter](https://github.com/microsoft/krabsetw) 80 | - [SilkETW](https://github.com/fireeye/SilkETW) 81 | -------------------------------------------------------------------------------- /sealighter/sealighter.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | x64 7 | 8 | 9 | Release 10 | x64 11 | 12 | 13 | 14 | 16.0 15 | {6624B356-3AA7-4A88-BDFC-B525697ECB38} 16 | Win32Proj 17 | SimpleETW 18 | 10.0 19 | sealighter 20 | 21 | 22 | 23 | Application 24 | true 25 | v142 26 | Unicode 27 | Static 28 | 29 | 30 | Application 31 | false 32 | v142 33 | true 34 | Unicode 35 | Static 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | true 51 | $(SolutionDir)$(Platform)\$(Configuration)\ 52 | 53 | 54 | false 55 | $(SolutionDir)$(Platform)\$(Configuration)\ 56 | 57 | 58 | 59 | 60 | 61 | Level4 62 | true 63 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 64 | true 65 | $(ProjectDir)..\krabsetw\krabs;$(ProjectDir)..\krabsetw\Microsoft.O365.Security.Native.ETW;$(ProjectDir)..\json\single_include 66 | 67 | 68 | Console 69 | true 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | Level3 81 | true 82 | true 83 | true 84 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 85 | true 86 | $(ProjectDir)..\krabsetw\krabs;$(ProjectDir)..\krabsetw\Microsoft.O365.Security.Native.ETW;$(ProjectDir)..\json\single_include 87 | 88 | 89 | Console 90 | true 91 | true 92 | true 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | {ed4e6027-541f-440a-a5ee-15dbb7b89423} 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /docs/PARSING_DATA.md: -------------------------------------------------------------------------------- 1 | # Parsing Data 2 | - [Event Format](#Event-Format) 3 | - [Parsing Data](#Parsing-Data) 4 | 5 | 6 | ## Event Format 7 | Events are outputted as JSON, for example: 8 | ```json 9 | { 10 | "header": { 11 | "activity_id": "{00000000-0000-0000-0000-000000000000}", 12 | "event_flags": 576, 13 | "event_id": 1, 14 | "event_name": "", 15 | "event_opcode": 1, 16 | "event_version": 3, 17 | "process_id": 17964, 18 | "provider_name": "Microsoft-Windows-Kernel-Process", 19 | "task_name": "ProcessStart", 20 | "thread_id": 25932, 21 | "timestamp": "2020-05-17 11:54:24Z", 22 | "trace_name": "proc_trace", 23 | "buffered_count": 2 24 | }, 25 | "properties": { 26 | "CreateTime": "2020-05-17 11:54:24Z", 27 | "Flags": 0, 28 | "ImageChecksum": 219193, 29 | "ImageName": "\\Device\\HarddiskVolume4\\Windows\\System32\\notepad.exe", 30 | "MandatoryLabel": "Mandatory Label\\Medium Mandatory Level", 31 | "PackageFullName": "", 32 | "PackageRelativeAppId": "", 33 | "ParentProcessID": 17964, 34 | "ParentProcessSequenceNumber": 15206, 35 | "ProcessID": 25752, 36 | "ProcessSequenceNumber": 33013, 37 | "ProcessTokenElevationType": 3, 38 | "ProcessTokenIsElevated": 0, 39 | "SessionID": 4, 40 | "TimeDateStamp": 587902357 41 | }, 42 | "property_types": { 43 | "CreateTime": "FILETIME", 44 | "Flags": "UINT32", 45 | "ImageChecksum": "UINT32", 46 | "ImageName": "STRINGW", 47 | "MandatoryLabel": "SID", 48 | "PackageFullName": "STRINGW", 49 | "PackageRelativeAppId": "STRINGW", 50 | "ParentProcessID": "UINT32", 51 | "ParentProcessSequenceNumber": "UINT64", 52 | "ProcessID": "UINT32", 53 | "ProcessSequenceNumber": "UINT64", 54 | "ProcessTokenElevationType": "UINT32", 55 | "ProcessTokenIsElevated": "UINT32", 56 | "SessionID": "UINT32", 57 | "TimeDateStamp": "UINT32" 58 | }, 59 | "stack_trace": [ 60 | "0x7FFA18BAB944", 61 | "0x7FFA1868902A", 62 | "0x773817C3" 63 | ] 64 | } 65 | ``` 66 | 67 | 68 | There are 3 sections to the JSON: 69 | - [header](#header) 70 | - [properties](#properties) 71 | - [property_types](#property_types) 72 | 73 | If the `report_stacktrace` option in the provider configuration is used, 74 | there will also be a [stack_trace](#stack_trace) array. 75 | 76 | 77 | ## header 78 | This section is the same for every event, and contains the event metadata. 79 | It will always contain these fields, which are taken from the Event Header: 80 | - activity_id 81 | - event_flags 82 | - event_id 83 | - event_name 84 | - event_opcode 85 | - event_version 86 | - process_id 87 | - provider_name 88 | - task_name 89 | - thread_id 90 | - timestamp 91 | - trace_name 92 | 93 | If [Buffering](BUFFERING.md) is being used and events were buffered together, 94 | the header will contain an extra field `buffered_count` with the number of events 95 | that were seen in the `buffering_timout_seconds` period 96 | 97 | ## properties 98 | This is the meat of the event. Unique to every event type, and sometimes even events of the same event ID can have different properties. 99 | 100 | Sealighter will attempt to parse every property of every event based upon the property's `TDH_INTYPE`. If the Event does not supply this information, or it's `INTYPE` isn't a simple number, GUID, or string, the property data will be the hex encoded bytes. 101 | 102 | ## property_types 103 | To assist in writing [filters](FILTERING.md), we also log the `TDH_INTYPE` of each property. This can also help if you do wish to write your own code to parse specific events. 104 | 105 | ## stack_trace 106 | If the `report_stacktrace` option in the provider configuration is used, 107 | there will also be a [stack_trace](#stack_trace) array. This is the array of the memory addresses` 108 | of functions that generated the event. 109 | 110 | 111 | _____________ 112 | 113 | 114 | # Parsing Data 115 | 116 | ## Parsing Output Files 117 | If the `output_format` was `file`, the events written to the file on a single line, 1 event per line. You can parse this file with Python: 118 | ```python 119 | import json 120 | events = list() 121 | 122 | with open("output.json", "r") as f: 123 | for line in f: 124 | events.append(json.loads(line)) 125 | 126 | # We we can do stuff with the json 127 | for event in events: 128 | print(json.dumps(event, indent=True)) 129 | 130 | ``` 131 | 132 | ## Parsing Event Logs 133 | If the `output_format` was `event_log`, the events are written to the `Sealighter/Operational` Event Log. You can use PowerShell to get and parse these Events: 134 | ```powershell 135 | $events = Get-WinEvent -LogName "Sealighter/Operational" 136 | ``` 137 | 138 | The event `.message` contains the full JSON, but you can also access the event's `.Properties` array. 139 | This array has the JSON at index 0, then the rest of the `header` as the remaining properties (except for the optional `buffered_count` field), i.e.: 140 | ``` 141 | 0. json 142 | 1. activity_id 143 | 2. event_flags 144 | 3. event_id 145 | 4. event_name 146 | 5. event_opcode 147 | 6. event_version 148 | 7. process_id 149 | 8. provider_name 150 | 9. task_name 151 | 10. thread_id 152 | 11. timestamp 153 | 12. trace_name 154 | ``` 155 | For example in PowerShell 156 | ```powershell 157 | foreach ($event in $events) { 158 | $provider_name = $event.Properties[8].Value 159 | $task_name = $event.Properties[9].Value 160 | Write-Host "$provider_name - $task_name" 161 | } 162 | ``` 163 | But you and also just read in the message as JSON: 164 | ```powershell 165 | foreach ($event in $events) { 166 | $event_json = ConvertFrom-Json $event.Message 167 | $provider_name = $event_json.header.provider_name 168 | $task_name = $event_json.header.task_name 169 | Write-Host "$provider_name - $task_name" 170 | } 171 | ``` 172 | 173 | 174 | ## With Splunk or Kibana 175 | You may also wish to ingest the data into a Splunk or ELK stack. 176 | 177 | You can either write to a file, then upload it, or for streaming write to the event log and use a forwarder like WinLogbeat to forward the data. 178 | 179 | 180 | For super quick experiments, I quickly set up a temporary Splunk instance in a docker container by: 181 | ```bash 182 | docker run --rm --name splunk -d -p 8000:8000 -e 'SPLUNK_START_ARGS=--accept-license' -e 'SPLUNK_PASSWORD=sealighter' splunk/splunk:latest 183 | ``` 184 | After the container started up, I ran Sealighter writing to a file, then afterwards ingested the file and started searching. When I was done for the day, I tore down the container, so I never hit the upload limits. 185 | -------------------------------------------------------------------------------- /sealighter/sealighter_util.cpp: -------------------------------------------------------------------------------- 1 | #include "sealighter_krabs.h" 2 | #include 3 | #include 4 | #include 5 | #include "sealighter_json.h" 6 | #include "sealighter_util.h" 7 | 8 | 9 | std::string convert_json_string 10 | ( 11 | json item, 12 | bool pretty_print 13 | ) 14 | { 15 | if (pretty_print) { 16 | return item.dump(4, ' ', false, nlohmann::detail::error_handler_t::ignore); 17 | } 18 | else { 19 | return item.dump(-1, ' ', false, nlohmann::detail::error_handler_t::ignore); 20 | } 21 | } 22 | 23 | std::string convert_str_str_lowercase( 24 | const std::string& from 25 | ) 26 | { 27 | std::string to = from; 28 | std::transform(to.begin(), to.end(), to.begin(), 29 | [](unsigned char c) { return std::tolower(c); }); 30 | return to; 31 | } 32 | 33 | 34 | std::wstring convert_wstr_wstr_lowercase( 35 | const std::wstring& from 36 | ) 37 | { 38 | std::wstring to = from; 39 | std::transform(to.begin(), to.end(), to.begin(), 40 | [](unsigned char c) { return std::tolower(c); }); 41 | return to; 42 | } 43 | 44 | std::string convert_wstr_str 45 | ( 46 | const std::wstring& from 47 | ) 48 | { 49 | std::string to(from.begin(), from.end()); 50 | return to; 51 | } 52 | 53 | 54 | std::wstring convert_str_wstr 55 | ( 56 | const std::string& from 57 | ) 58 | { 59 | std::wstring to(from.begin(), from.end()); 60 | return to; 61 | } 62 | 63 | std::wstring convert_str_wstr_lowercase( 64 | const std::string& from 65 | ) 66 | { 67 | std::string from_lower = convert_str_str_lowercase(from); 68 | std::wstring to = convert_str_wstr(from_lower); 69 | return to; 70 | } 71 | 72 | std::vector convert_str_bytes_lowercase( 73 | const std::string& from 74 | ) 75 | { 76 | std::string from_lower = convert_str_str_lowercase(from); 77 | std::vector to(from_lower.begin(), from_lower.end()); 78 | return to; 79 | } 80 | 81 | std::vector convert_str_wbytes_lowercase( 82 | const std::string& from 83 | ) 84 | { 85 | std::wstring from_wide_lower = convert_str_wstr_lowercase(from); 86 | BYTE* from_bytes = (BYTE*)from_wide_lower.c_str(); 87 | // Size returns string len, so double as they are widechars 88 | // But don't copy in trailing NULL so we can match mid-string 89 | size_t from_bytes_size = from_wide_lower.size() * sizeof(WCHAR); 90 | 91 | std::vector to(from_bytes, from_bytes + from_bytes_size); 92 | return to; 93 | } 94 | 95 | std::string convert_guid_str 96 | ( 97 | const GUID& from 98 | ) 99 | { 100 | char guid_string[39]; // 2 braces + 32 hex chars + 4 hyphens + null terminator 101 | snprintf( 102 | guid_string, sizeof(guid_string), 103 | "{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", 104 | from.Data1, from.Data2, from.Data3, 105 | from.Data4[0], from.Data4[1], from.Data4[2], 106 | from.Data4[3], from.Data4[4], from.Data4[5], 107 | from.Data4[6], from.Data4[7]); 108 | std::string to(guid_string); 109 | return to; 110 | } 111 | 112 | /* 113 | Helper to convert byte array to string, 114 | treating the bytes as a GUID 115 | */ 116 | GUID convert_wstr_guid 117 | ( 118 | std::wstring from 119 | ) 120 | { 121 | GUID to = GUID_NULL; 122 | (void)CLSIDFromString(from.c_str(), (LPCLSID)&to); 123 | 124 | return to; 125 | } 126 | GUID convert_str_guid 127 | ( 128 | std::string from 129 | ) 130 | { 131 | GUID to = GUID_NULL; 132 | (void)CLSIDFromString(convert_str_wstr(from).c_str(), (LPCLSID)&to); 133 | return to; 134 | } 135 | 136 | std::string convert_timestamp_string 137 | ( 138 | const LARGE_INTEGER from 139 | ) 140 | { 141 | // Both LARGE_INTEGER timestamps and FILETIMES are 142 | // "100-nanosecond intervals since midnight, January 1, 1601" 143 | FILETIME ft; 144 | 145 | ft.dwHighDateTime = from.HighPart; 146 | ft.dwLowDateTime = from.LowPart; 147 | 148 | std::string to = convert_filetime_string(ft); 149 | return to; 150 | } 151 | 152 | std::string convert_filetime_string 153 | ( 154 | const FILETIME from 155 | ) 156 | { 157 | SYSTEMTIME stime; 158 | ::FileTimeToSystemTime(std::addressof(from), std::addressof(stime)); 159 | std::string to = convert_systemtime_string(stime); 160 | return to; 161 | } 162 | 163 | 164 | std::string convert_systemtime_string 165 | ( 166 | const SYSTEMTIME from 167 | ) 168 | { 169 | std::ostringstream stm; 170 | const auto w2 = std::setw(2); 171 | stm << std::setfill('0') << std::setw(4) << from.wYear << '-' << w2 << from.wMonth 172 | << '-' << w2 << from.wDay << ' ' << w2 << from.wHour 173 | << ':' << w2 << from.wMinute << ':' << w2 << from.wSecond << 'Z'; 174 | 175 | std::string to = stm.str(); 176 | return to; 177 | } 178 | 179 | 180 | std::string convert_bytes_sidstring 181 | ( 182 | const std::vector& from 183 | ) 184 | { 185 | #define MAX_NAME 256 186 | char domain_name[MAX_NAME] = ""; 187 | char user_name[MAX_NAME] = ""; 188 | DWORD user_name_size = MAX_NAME; 189 | DWORD domain_name_size = MAX_NAME; 190 | SID_NAME_USE name_use; 191 | const BYTE* data = (from.data()); 192 | std::string to; 193 | if (LookupAccountSidA(NULL, (PSID)data, user_name, &user_name_size, domain_name, &domain_name_size, &name_use)) { 194 | to = domain_name; 195 | to += "\\"; 196 | to += user_name; 197 | } 198 | else { 199 | // Fallback to printing the raw bytes 200 | to = convert_bytevector_hexstring(from); 201 | } 202 | return to; 203 | } 204 | 205 | 206 | std::string convert_bytevector_hexstring 207 | ( 208 | const std::vector& from 209 | ) 210 | { 211 | std::ostringstream ss; 212 | ss << std::hex << std::uppercase << std::setfill('0'); 213 | for (int c : from) { 214 | ss << std::setw(2) << c; 215 | } 216 | 217 | std::string to = ss.str(); 218 | return to; 219 | } 220 | 221 | 222 | std::string convert_bytearray_hexstring 223 | ( 224 | BYTE* from, 225 | int len 226 | ) 227 | { 228 | std::ostringstream ss; 229 | ss << std::hex << std::uppercase << std::setfill('0'); 230 | for (int i = 0; i < len; i++) { 231 | ss << std::setw(2) << (int)from[i]; 232 | 233 | } 234 | 235 | std::string to = ss.str(); 236 | return to; 237 | } 238 | 239 | std::string convert_ulong64_hexstring 240 | ( 241 | const ULONG64 from 242 | ) 243 | { 244 | std::stringstream to; 245 | to << "0x" << std::uppercase << std::hex << from; 246 | return to.str(); 247 | } 248 | 249 | 250 | int convert_bytes_sint32 251 | ( 252 | const std::vector& from 253 | ) 254 | { 255 | int to = 0; 256 | if (from.size() == 4) { 257 | to = (from[3] << 24) | (from[2] << 16) | (from[1] << 8) | (from[0]); 258 | } 259 | return to; 260 | } 261 | 262 | bool convert_bytes_bool 263 | ( 264 | const std::vector& from 265 | ) 266 | { 267 | if (convert_bytes_sint32(from)) { 268 | return true; 269 | } 270 | return false; 271 | } 272 | 273 | 274 | bool file_exists 275 | ( 276 | std::string fileName 277 | ) 278 | { 279 | std::ifstream infile(fileName); 280 | return infile.good(); 281 | } 282 | 283 | 284 | #define MAX_SIZE 4096 285 | 286 | VOID log_messageA(const CHAR* format, ...) 287 | { 288 | CHAR message[MAX_SIZE]; 289 | 290 | va_list arg_ptr; 291 | va_start(arg_ptr, format); 292 | _vsnprintf_s(message, MAX_SIZE, MAX_SIZE-1, format, arg_ptr); 293 | va_end(arg_ptr); 294 | OutputDebugStringA(message); 295 | printf("%s", message); 296 | } 297 | 298 | VOID log_messageW(const WCHAR* format, ...) 299 | { 300 | WCHAR message[MAX_SIZE]; 301 | va_list arg_ptr; 302 | va_start(arg_ptr, format); 303 | _vsnwprintf_s(message, MAX_SIZE, MAX_SIZE-1, format, arg_ptr); 304 | va_end(arg_ptr); 305 | OutputDebugStringW(message); 306 | wprintf(L"%s", message); 307 | } 308 | -------------------------------------------------------------------------------- /docs/CONFIGURATION.md: -------------------------------------------------------------------------------- 1 | # Configuration File 2 | 3 | The Sealighter config file is how you specify what events from what providers to log, how to log them, and other ETW session properties. 4 | 5 | The file is in JSON. An example config file looks like this: 6 | ```json 7 | { 8 | "session_properties": { 9 | "session_name": "My-Process-Trace", 10 | "output_format": "stdout", 11 | "buffering_timout_seconds": 10 12 | }, 13 | "user_traces": [ 14 | { 15 | "trace_name": "proc_trace", 16 | "provider_name": "Microsoft-Windows-Kernel-Process", 17 | "keywords_any": 16 18 | }, 19 | { 20 | "trace_name": "guid_trace", 21 | "provider_name": "{382b5e24-181e-417f-a8d6-2155f749e724}", 22 | "filters": { 23 | "any_of": { 24 | "opcode_is": [1, 2] 25 | } 26 | }, 27 | "buffers": [ 28 | { 29 | "event_id": 1, 30 | "max_before_buffering": 1, 31 | "fields": [ 32 | "ImageName" 33 | ] 34 | } 35 | ] 36 | }, 37 | ], 38 | "kernel_traces": [ 39 | { 40 | "trace_name": "kernel_proc_trace", 41 | "provider_name": "process", 42 | } 43 | ] 44 | } 45 | ``` 46 | 47 | Config Files have 3 Parts: 48 | - [session_properties](#session_properties) 49 | - [user_traces](#user_traces) 50 | - [kernel_traces](#kernel_traces) 51 | 52 | _____________ 53 | 54 | # session_properties 55 | These are where you specify properties of the ETW Session, e.g: 56 | ```json 57 | "session_properties": { 58 | "session_name": "My-Trace", 59 | "output_format": "stdout", 60 | "output_filename": "path/to/output.json", 61 | }, 62 | ``` 63 | You can specify the following options: 64 | 65 | ### session_name 66 | The name of the ETW Session. 67 | Default: Sealighter 68 | 69 | ### output_format 70 | Where to output the events to. Can be one of: 71 | - stdout 72 | - event_log 73 | - file 74 | 75 | If specifying file, also specify `output_filename`: 76 | ```json 77 | "session_properties": { 78 | "output_format": "stdout", 79 | "output_filename": "path/to/output.json", 80 | }, 81 | ``` 82 | 83 | ### output_filename 84 | If outputting to a file, the path to write the output events to. 85 | 86 | The following are advanced session properties: 87 | ### buffer_size 88 | The Size of the in-memory buffer. 89 | Default: 256 90 | 91 | ### minimum_buffers 92 | Minimum Buffers to allocate. Default 12 93 | 94 | ### maximum_buffers 95 | Max Buffers to allocate. Default 48 96 | 97 | ### flush_timer 98 | Buffer Flush timer in seconds. Default 1 99 | 100 | ### buffering_timout_seconds 101 | If using [Buffering](BUFFERING.md), this specifies how often to flush 102 | the events, reporting on a group of events as one with a `buffered_count`. 103 | If using buffering, default is 30 seconds. 104 | 105 | _____________ 106 | 107 | # user_traces 108 | This is an array of the User mode, TraceLogging, or WPP providers you want to subscribe to, e.g.: 109 | ```json 110 | "user_traces": [ 111 | { 112 | "trace_name": "proc_trace", 113 | "provider_name": "Microsoft-Windows-Kernel-Process", 114 | "keywords_any": 16 115 | }, 116 | { 117 | "trace_name": "guid_trace", 118 | "provider_name": "{382b5e24-181e-417f-a8d6-2155f749e724}", 119 | "report_stacktrace": true, 120 | "filters": { 121 | "any_of": { 122 | "opcode_is": [1, 2] 123 | } 124 | } 125 | }, 126 | ] 127 | ``` 128 | 129 | User Providers have the following options, all are optional except for `trace_name` and `provider_name`: 130 | 131 | ### trace_name 132 | Unique Name to give this provider, that will appear in the reported events. 133 | If running multiple `user_traces` that use the same provider, this will tell you which set of 134 | filters the event hit on. 135 | 136 | ### provider_name 137 | The name or GUID of the Provider to enable. 138 | For TraceLogging and WPP Traces, this *must* be the GUID. 139 | 140 | ### keywords_any 141 | Only report on Events that has these at least some of these keyword flags. See [Scenarios](SCENARIOS.md) for examples on finding information on a provider's keywords. 142 | 143 | Whilst you can also use filters to filter based on keywords (filters explainer later), the `keywords_any` filtering happens in the Kernel, instead of in user land inside Sealighter, and is therefore much more efficient to filter. 144 | 145 | It is advices to `keywords_any` as much as possible to ensure you don't drop any events. 146 | 147 | ### keywords_all 148 | Similar to `keywords_any`, but an event must match all the keywords. 149 | 150 | If neither `keywords_any` or `keywords_all` is specified, all events will be passed onto the filters to be reported on. 151 | 152 | `keywords_any` and `keywords_all` take precedence of filters. 153 | 154 | 155 | ### Level 156 | Only report if events are at least this logging level. 157 | Like the `keywords_*` options, it is more efficient use this instead of a Filter, and this will take precedence of a Filter 158 | 159 | ### trace_flags 160 | Any advanced flags 161 | 162 | ### report_stacktrace 163 | If set to `true`, events will also include a stack trace array of the memory addresses 164 | of functions that generated the event. 165 | 166 | ### filters 167 | An array of filters to further filter the events to report on. These can be quite complex, so read the [Filtering](FILTERING.md) section for details. 168 | 169 | ### buffers 170 | Buffering enables the reporting of many similar events in a time period as one with a count. 171 | For details, read [Buffering](BUFFERING.md). 172 | 173 | ### dump_raw_event 174 | ```json 175 | { 176 | "trace_name": "tracelogging_trace", 177 | "provider_name": "{75697175-75e2-4d85-83bc-7278acc12de4}", 178 | "dump_raw_event": true 179 | }, 180 | ``` 181 | If `dump_raw_event` is true, instead of attempting to parse the event, return the hex-encoded raw bytes of the event. 182 | The bytes will be in the `raw` tag. This is useful for WPP traces, where the event data structure is not known. 183 | 184 | _____________ 185 | 186 | # kernel_traces 187 | 188 | This is an array of the special sub-providers of the Special `NT Kernel Trace` that you wish to log, e.g.: 189 | ```json 190 | "kernel_traces": [ 191 | { 192 | "trace_name": "kernel_proc_trace", 193 | "provider_name": "process", 194 | "filters": { 195 | "any_of": { 196 | "opcode_is": [1, 2] 197 | } 198 | } 199 | }, 200 | { 201 | "trace_name": "kernel_image_trace", 202 | "provider_name": "image_load", 203 | } 204 | ] 205 | ``` 206 | Kernel Providers have three options, all are required: 207 | 208 | ### trace_name 209 | Unique Name to give this provider, that will appear in header of reported events. 210 | If running multiple `user_traces` that use the same provider, this will tell you which set of 211 | filters the event hit on. 212 | 213 | ### provider_name 214 | The kernel provider to log. Must be one of: 215 | - process 216 | - thread 217 | - image_load 218 | - process_counter 219 | - context_switch 220 | - dpc 221 | - interrupt 222 | - system_call 223 | - disk_io 224 | - disk_file_io 225 | - disk_init_io 226 | - thread_dispatch 227 | - memory_page_fault 228 | - memory_hard_fault 229 | - virtual_alloc 230 | - network_tcpip 231 | - registry 232 | - alpc 233 | - split_io 234 | - driver 235 | - profile 236 | - file_io 237 | - file_init_io 238 | - debug_print 239 | - vamap_provider 240 | - object_manager 241 | 242 | 243 | ### filters 244 | Like `user_traces`, this is a list of filters to filter the events to report on. These can be quite complex, so read the [Filtering](FILTERING.md) section for details. 245 | 246 | ### buffers 247 | Like `user_traces`, buffering enables the reporting of many similar events in a time period as one with a count. 248 | For details, read [Buffering](BUFFERING.md). 249 | -------------------------------------------------------------------------------- /docs/FILTERING.md: -------------------------------------------------------------------------------- 1 | # Filtering 2 | - [Filtering Overview](#Filtering-Overview) 3 | - [Filter Lists](#Filter-Lists) 4 | - [Filter Types](#Filter-Types) 5 | 6 | 7 | # Filtering Overview 8 | Filters provide a way to stem the flood of events that ETW can generate, and only 9 | report on events of interest. 10 | 11 | Filters are applied per-provider after checking the `keyword_any` and `keyword_all` fields. The are defined in the config file, e.g.: 12 | ```json 13 | "user_traces": [ 14 | { 15 | "trace_name": "proc_trace", 16 | "provider_name": "Microsoft-Windows-Kernel-Process", 17 | "keywords_any": 16 18 | }, 19 | { 20 | "trace_name": "guid_trace", 21 | "provider_name": "{382b5e24-181e-417f-a8d6-2155f749e724}", 22 | "filters": { 23 | "any_of": { 24 | "opcode_is": [1, 2] 25 | } 26 | } 27 | }, 28 | ] 29 | ``` 30 | 31 | _____________ 32 | 33 | # Filter Lists 34 | 35 | Filters are added inside the `filter` key, into one of 3 lists. You do not have to specify each list. The lists are: 36 | - [any_of](#any_of) 37 | - [all_of](#all_of) 38 | - [none_of](#none_of) 39 | 40 | 41 | ## any_of 42 | ```json 43 | "filters": { 44 | "any_of": { 45 | "opcode_is": 1, 46 | "event_id_is": 1 47 | } 48 | } 49 | ``` 50 | An event will be reported if it matches *any* of the filters in the `any_of` list. In the example above, and event with opcode `1` but id `2` will still be reported. 51 | 52 | 53 | ## all_of 54 | ```json 55 | "filters": { 56 | "all_of": { 57 | "opcode_is": 1, 58 | "event_id_is": 1 59 | } 60 | } 61 | ``` 62 | An event will be reported if it matches *all* the filters in the `all_of` list. In the example above, and event with opcode `1` but id `2` will not be reported, but an event with opcode `1` and id `1` will. 63 | 64 | If a field is an array, e.g.: 65 | ```json 66 | "filters": { 67 | "all_of": { 68 | "opcode_is": [1, 2], 69 | "event_id_is": 1 70 | } 71 | } 72 | ``` 73 | Then an event only has to match one of the items in the array, as an event can only have one of each field. But it still has to match the rest of the items in the list: An event with id `2` and opcode `1` will match, but not id `2` opcode `2`. 74 | 75 | 76 | ## none_of 77 | ```json 78 | "filters": { 79 | "none_of": { 80 | "opcode_is": 1, 81 | "event_id_is": 1 82 | } 83 | } 84 | ``` 85 | An event will be reported only if it *does not* match any of the filters in the `none_of` list. In the above example, an event with id `1` and opcode `2` not be reported, but an event with id `2` and opcode `2` will be. 86 | 87 | Like `all_of`, if a field is an array then an event only has to 'fail' a match of one of the items in the array, as an event can only have one of each field. 88 | 89 | _____________ 90 | 91 | # Filter Types 92 | There is a lot of different filters you can apply. 93 | For each filter key, you can either set it to a value (either a number or string), or an array, e.g.: 94 | ```json 95 | "filters": { 96 | "none_of": { 97 | "process_name_contains": "notepad.exe", 98 | "event_id_is": [1, 2] 99 | } 100 | } 101 | ``` 102 | 103 | If an array, it is the same as if the array is a mini `any_of` list, where the event only has to match one of them to match. 104 | 105 | The possible filters are: 106 | - [Header Filters](#Header-Filters) 107 | - [event_id_is](#event_id_is) 108 | - [opcode_is](#opcode_is) 109 | - [process_id_is](#process_id_is) 110 | - [version_is](#version_is) 111 | - [activity_id_is](#activity_id_is) 112 | - [process_name_contains](#process_name_contains) 113 | - [Property Filters](#Property-Filters) 114 | - [property_is](#property_is) 115 | - [property_equals/property_iequals](#property_equals/property_iequals) 116 | - [property_contains/property_icontains](#property_contains/property_icontains) 117 | - [property_starts_with/property_istarts_with](#property_starts_with/property_istarts_with) 118 | - [property_ends_with/property_iends_with](#property_ends_with/property_iends_with) 119 | - [Discovery Filters](#Discovery-Filters) 120 | - [any_field_contains](#any_field_contains) 121 | - [max_events_total](#max_events_total) 122 | - [max_events_id](#max_events_id) 123 | 124 | _____ 125 | 126 | ## Header Filters 127 | These filters filter based upon the Event Header Metadata: 128 | 129 | 130 | ### event_id_is 131 | ```json 132 | "any_of": { 133 | "event_id_is": 1 134 | } 135 | ``` 136 | An event will be reported if its Event ID matches this number. 137 | 138 | ### opcode_is 139 | ```json 140 | "any_of": { 141 | "opcode_is": 1 142 | } 143 | ``` 144 | An event will be reported if its Opcode matches this number. 145 | 146 | ### process_id_is 147 | ```json 148 | "any_of": { 149 | "process_id_is": 1 150 | } 151 | ``` 152 | An event will be reported if the PID of the process that generated the event matches this number. 153 | 154 | ### version_is 155 | ```json 156 | "any_of": { 157 | "version_is": 1 158 | } 159 | ``` 160 | An event will be reported if its Event version matches this number. 161 | 162 | ### activity_id_is 163 | ```json 164 | "any_of": { 165 | "activity_id_is": "{00000000-0000-0000-0000-000000000000}" 166 | } 167 | ``` 168 | An event will be reported if its Event Activity ID matches this number. If an Event does not have an Activity ID, it will be a NULL GUID (i.e. all zeros). You can therefore use `activity_id_is` in the `none_of` list to filter out any events that do *not* have an activity ID. 169 | 170 | ### process_name_contains 171 | ```json 172 | "any_of": { 173 | "process_name_contains": "notepad.exe" 174 | } 175 | ``` 176 | An event will be reported if the Image Name of the Process that generated the event contains this value. 177 | 178 | _____ 179 | 180 | 181 | ## Property Filters: 182 | These filters apply to the properties within an Event. 183 | Each takes 3 parameters: 184 | - `name`: The Name of the property to filter 185 | - `value`: The value to filter on 186 | - `type`: The `TDH_INTYPE` that the property is. 187 | 188 | e.g.: 189 | ```json 190 | "any_of": { 191 | "property_is": { 192 | "name": "ImageName", 193 | "value": "notepad.exe", 194 | "type": "STRINGA", 195 | } 196 | } 197 | ``` 198 | You are required to specify the `type` to reduce the runtime parsing of each property and event, doing it once before the ETW Session starts instead of checking every event. You can check a property type by first running another trace and looking in the events `property_type` array. The possible types are: 199 | - STRINGA 200 | - STRINGW 201 | - INT8 202 | - UINT8 203 | - INT16 204 | - UINT16 205 | - INT32 206 | - UINT32 207 | - INT64 208 | - UINT64 209 | 210 | These are the types of property filters: 211 | 212 | ### property_is 213 | ```json 214 | "any_of": { 215 | "property_is": { 216 | "name": "ExitCode", 217 | "value": 0, 218 | "type": "INT32", 219 | } 220 | } 221 | ``` 222 | An event will be reported if the property exists, matches the type and value. 223 | 224 | 225 | ### property_equals/property_iequals 226 | ```json 227 | "any_of": { 228 | "property_equals": { 229 | "name": "ImageName", 230 | "value": "notepad.exe", 231 | "type": "STRINGA", 232 | } 233 | } 234 | ``` 235 | An event will be reported if the property exists, matches the type, and is equal to the value. `property_iequals` is the case-insensitive version. 236 | 237 | ### property_contains/property_icontains 238 | ```json 239 | "any_of": { 240 | "property_contains": { 241 | "name": "ImageName", 242 | "value": "notepad.exe", 243 | "type": "STRINGA", 244 | } 245 | } 246 | ``` 247 | An event will be reported if the property exists, matches the type, and contains the value. `property_icontains` is the case-insensitive version. 248 | 249 | ### property_starts_with/property_istarts_with 250 | ```json 251 | "any_of": { 252 | "property_starts_with": { 253 | "name": "ImageName", 254 | "value": "notepad.exe", 255 | "type": "STRINGA", 256 | } 257 | } 258 | ``` 259 | An event will be reported if the property exists, matches the type, and starts with the value. `property_istarts_with` is the case-insensitive version. 260 | 261 | ### property_ends_with/property_iends_with 262 | ```json 263 | "any_of": { 264 | "property_ends_with": { 265 | "name": "ImageName", 266 | "value": "notepad.exe", 267 | "type": "STRINGA", 268 | } 269 | } 270 | ``` 271 | An event will be reported if the property exists, matches the type, and ends with the value. `property_iends_with` is the case-insensitive version. 272 | 273 | ------- 274 | 275 | ## Discovery Filters 276 | These are some special filters to help with discovery of interesting events withing a provider 277 | 278 | ### any_field_contains 279 | ```json 280 | "any_of": { 281 | "any_field_contains": "notepad" 282 | } 283 | ``` 284 | This searches all properties of all events (including property names) for this value. It also searches for Wide and ANSI string versions. This is extremely useful when you don't know what events a provider has, but have some idea of the data it may generate. (See [Secenarios](SCENARIOS.md) for an example of using this). 285 | 286 | 287 | ### max_events_total 288 | ```json 289 | "any_of": { 290 | "max_events_total": 100 291 | } 292 | ``` 293 | This limits the total number of events to report on. Useful for getting a snapshot of possible events generated by a high-volume provider. 294 | 295 | 296 | ### max_events_id 297 | ```json 298 | "any_of": { 299 | "max_events_id": { 300 | "event_id_is": 1, 301 | "max_events": 1 302 | } 303 | } 304 | ``` 305 | This limits the total number of events matching a specific ID to report on. Useful for getting a snapshot of possible events generated by a high-volume provider. 306 | -------------------------------------------------------------------------------- /sealighter/sealighter_predicates.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "sealighter_krabs.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | /** 9 | * 10 | * Accepts an event if any of the predicates in the vector matches 11 | * 12 | */ 13 | struct sealighter_any_of : predicates::details::predicate_base { 14 | sealighter_any_of(std::vector> list) 15 | : list_(list) 16 | {} 17 | 18 | bool operator()(const EVENT_RECORD& record, const trace_context& trace_context) const { 19 | for (auto& item : list_) { 20 | if (item->operator()(record, trace_context)) { 21 | return true; 22 | }; 23 | } 24 | return false; 25 | } 26 | private: 27 | std::vector> list_; 28 | }; 29 | 30 | /** 31 | * 32 | * Accepts an event if all of the predicates in the vector matches 33 | * 34 | */ 35 | struct sealighter_all_of : predicates::details::predicate_base { 36 | sealighter_all_of(std::vector> list) 37 | : list_(list) 38 | {} 39 | 40 | bool operator()(const EVENT_RECORD& record, const trace_context& trace_context) const { 41 | if (list_.empty()) { 42 | return false; 43 | } 44 | for (auto& item : list_) { 45 | if (!item->operator()(record, trace_context)) { 46 | return false; 47 | }; 48 | } 49 | return true; 50 | } 51 | private: 52 | std::vector> list_; 53 | }; 54 | 55 | /** 56 | * 57 | * Accepts an event only if none of the predicates in the vector match 58 | * 59 | */ 60 | struct sealighter_none_of : predicates::details::predicate_base { 61 | sealighter_none_of(std::vector> list) 62 | : list_(list) 63 | {} 64 | 65 | bool operator()(const EVENT_RECORD& record, const trace_context& trace_context) const { 66 | for (auto& item : list_) { 67 | if (item->operator()(record, trace_context)) { 68 | return false; 69 | }; 70 | } 71 | return true; 72 | } 73 | private: 74 | std::vector> list_; 75 | }; 76 | 77 | /** 78 | * 79 | * Accepts an event if its ID matches the expected value, 80 | But only until we see a maximum number of events. 81 | * 82 | */ 83 | struct sealighter_max_events_id: predicates::details::predicate_base { 84 | sealighter_max_events_id(uint64_t id_expected, uint64_t max_events) 85 | : id_expected_(USHORT(id_expected)) 86 | , max_events_(max_events) 87 | {} 88 | 89 | bool operator()(const EVENT_RECORD& record, const trace_context&) const { 90 | // Match correct id first 91 | if (record.EventHeader.EventDescriptor.Id == id_expected_) { 92 | if (count_ < max_events_) { 93 | // Increment count 94 | count_++; 95 | return true; 96 | } 97 | } 98 | return false; 99 | } 100 | private: 101 | USHORT id_expected_; 102 | uint64_t max_events_; 103 | mutable uint64_t count_ = 0; 104 | }; 105 | 106 | /** 107 | * 108 | * Accepts an event if its ID matches the expected value, 109 | But only until we see a maximum number of events. 110 | * 111 | */ 112 | struct sealighter_max_events_total : predicates::details::predicate_base { 113 | sealighter_max_events_total(UINT64 max_events) 114 | : max_events_(max_events) 115 | {} 116 | 117 | bool operator()(const EVENT_RECORD&, const trace_context&) const { 118 | if (count_ < max_events_) { 119 | // Increment count 120 | count_++; 121 | return true; 122 | } 123 | return false; 124 | } 125 | private: 126 | UINT64 max_events_; 127 | mutable UINT64 count_ = 0; 128 | }; 129 | 130 | 131 | /** 132 | * 133 | * Returns true if the event property matches the expected value. 134 | * 135 | */ 136 | template 137 | struct sealighter_property_is : predicates::details::predicate_base { 138 | sealighter_property_is(const std::wstring& property, const T& expected) 139 | : property_(property) 140 | , expected_(expected) 141 | {} 142 | 143 | bool operator()(const EVENT_RECORD& record, const trace_context& trace_context) const 144 | { 145 | schema schema(record, trace_context.schema_locator); 146 | parser parser(schema); 147 | 148 | try { 149 | return (expected_ == parser.parse(property_)); 150 | } 151 | catch (...) { 152 | return false; 153 | } 154 | } 155 | 156 | private: 157 | const std::wstring property_; 158 | const T expected_; 159 | }; 160 | 161 | 162 | /** 163 | * 164 | * Accepts if 'to_find' is it the event at all, either in a property name, 165 | Or in a STRINGA or STRINGW field 166 | * 167 | */ 168 | struct sealighter_any_field_contains : predicates::details::predicate_base { 169 | sealighter_any_field_contains(std::string to_find) 170 | : to_findW_(convert_str_wstr_lowercase(to_find)) 171 | , to_findA_(convert_str_str_lowercase(to_find)) 172 | , to_find_bytesA_(convert_str_bytes_lowercase(to_find)) 173 | , to_find_bytesW_(convert_str_wbytes_lowercase(to_find)) 174 | {} 175 | 176 | bool operator()(const EVENT_RECORD& record, const trace_context& trace_context) const { 177 | schema schema(record, trace_context.schema_locator); 178 | parser parser(schema); 179 | 180 | for (property& prop : parser.properties()) 181 | { 182 | // First check the property name 183 | if (convert_wstr_wstr_lowercase(prop.name()).find(to_findW_) != std::string::npos) { 184 | return true; 185 | }; 186 | 187 | switch (prop.type()) 188 | { 189 | case TDH_INTYPE_ANSISTRING: 190 | if (convert_str_str_lowercase(parser.parse(prop.name())).find(to_findA_) != std::string::npos) { 191 | return true; 192 | }; 193 | break; 194 | case TDH_INTYPE_UNICODESTRING: 195 | if (convert_wstr_wstr_lowercase(parser.parse(prop.name())).find(to_findW_) != std::string::npos) { 196 | return true; 197 | }; 198 | break; 199 | // These *might* contains something of use 200 | // If we search the raw bytes 201 | case TDH_INTYPE_MANIFEST_COUNTEDSTRING: 202 | case TDH_INTYPE_MANIFEST_COUNTEDANSISTRING: 203 | case TDH_INTYPE_RESERVED24: 204 | case TDH_INTYPE_MANIFEST_COUNTEDBINARY: 205 | case TDH_INTYPE_COUNTEDSTRING: 206 | case TDH_INTYPE_COUNTEDANSISTRING: 207 | case TDH_INTYPE_HEXINT32: 208 | case TDH_INTYPE_HEXINT64: 209 | case TDH_INTYPE_REVERSEDCOUNTEDSTRING: 210 | case TDH_INTYPE_REVERSEDCOUNTEDANSISTRING: 211 | case TDH_INTYPE_NONNULLTERMINATEDSTRING: 212 | case TDH_INTYPE_NONNULLTERMINATEDANSISTRING: 213 | case TDH_INTYPE_UNICODECHAR: 214 | case TDH_INTYPE_ANSICHAR: 215 | case TDH_INTYPE_BINARY: 216 | case TDH_INTYPE_HEXDUMP: 217 | case TDH_INTYPE_NULL: 218 | // See if we can find it in the raw bytes 219 | if (check_bytes(parser.parse(prop.name()).bytes())) { 220 | return true; 221 | } 222 | break; 223 | // These are just numbers or arbitrary bytes, they won't contain text 224 | case TDH_INTYPE_INT8: 225 | case TDH_INTYPE_UINT8: 226 | case TDH_INTYPE_INT16: 227 | case TDH_INTYPE_UINT16: 228 | case TDH_INTYPE_INT32: 229 | case TDH_INTYPE_UINT32: 230 | case TDH_INTYPE_INT64: 231 | case TDH_INTYPE_UINT64: 232 | case TDH_INTYPE_FLOAT: 233 | case TDH_INTYPE_DOUBLE: 234 | case TDH_INTYPE_BOOLEAN: 235 | case TDH_INTYPE_GUID: 236 | case TDH_INTYPE_FILETIME: 237 | case TDH_INTYPE_SID: 238 | case TDH_INTYPE_WBEMSID: 239 | case TDH_INTYPE_POINTER: 240 | case TDH_INTYPE_SYSTEMTIME: 241 | case TDH_INTYPE_SIZET: 242 | default: 243 | continue; 244 | } 245 | } 246 | return false; 247 | } 248 | private: 249 | bool check_bytes(std::vector bytes) const { 250 | // Check if big enough. The ANSI bytearray is always smaller or 251 | // equal to the WIDECHAR one 252 | if (bytes.size() < to_find_bytesA_.size()) { 253 | return false; 254 | } 255 | 256 | // Try to find ANSI string first 257 | auto it = std::search( 258 | std::begin(bytes), std::end(bytes), 259 | std::begin(to_find_bytesA_), std::end(to_find_bytesA_)); 260 | if (it != std::end(bytes)) { 261 | return true; 262 | } 263 | else if (bytes.size() >= to_find_bytesW_.size()) { 264 | // See if you can find the Wide string 265 | it = std::search( 266 | std::begin(bytes), std::end(bytes), 267 | std::begin(to_find_bytesW_), std::end(to_find_bytesW_)); 268 | if (it != std::end(bytes)) { 269 | return true; 270 | } 271 | } 272 | return false; 273 | } 274 | 275 | std::wstring to_findW_; 276 | std::string to_findA_; 277 | std::vector to_find_bytesA_; 278 | std::vector to_find_bytesW_; 279 | }; 280 | 281 | /** 282 | * 283 | * Accepts an event if it the Process name of the process that 284 | created this event matches. 285 | * 286 | */ 287 | struct sealighter_process_name_contains : predicates::details::predicate_base { 288 | sealighter_process_name_contains(std::string process_name) 289 | : process_name_(process_name) 290 | {} 291 | 292 | bool operator()(const EVENT_RECORD& record, const trace_context& trace_context) const { 293 | schema schema(record, trace_context.schema_locator); 294 | unsigned int pid = schema.process_id(); 295 | 296 | // As we're running as Admin, we *should* be able to open a handle to the process 297 | HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); 298 | if (NULL != hProcess) { 299 | // Have to get the full image name, which is the full path 300 | CHAR image_name_chars[1024]; 301 | DWORD ret = GetProcessImageFileNameA(hProcess, image_name_chars, 1024); 302 | if (ret != 0) { 303 | std::string image_name(image_name_chars, image_name_chars + ret); 304 | if (image_name.find(process_name_) != std::string::npos) { 305 | return true; 306 | } 307 | } 308 | } 309 | 310 | return false; 311 | } 312 | private: 313 | std::string process_name_; 314 | }; 315 | 316 | /** 317 | * 318 | * Accepts an event if it the Process name of the process that 319 | created this event matches. 320 | * 321 | */ 322 | struct sealighter_activity_id_is : predicates::details::predicate_base { 323 | sealighter_activity_id_is(std::string guid_match) 324 | : guid_match_(convert_str_guid(guid_match)) 325 | {} 326 | 327 | bool operator()(const EVENT_RECORD& record, const trace_context& trace_context) const { 328 | schema schema(record, trace_context.schema_locator); 329 | if (guid_match_ == schema.activity_id()) { 330 | return true; 331 | } 332 | return false; 333 | } 334 | private: 335 | GUID guid_match_; 336 | }; 337 | -------------------------------------------------------------------------------- /sealighter/sealighter_provider.h: -------------------------------------------------------------------------------- 1 | //**********************************************************************` 2 | //* This is an include file generated by Message Compiler. *` 3 | //* *` 4 | //* Copyright (c) Microsoft Corporation. All Rights Reserved. *` 5 | //**********************************************************************` 6 | #pragma once 7 | #include 8 | #include 9 | #include "evntprov.h" 10 | // 11 | // Initial Defs 12 | // 13 | #if !defined(ETW_INLINE) 14 | #define ETW_INLINE DECLSPEC_NOINLINE __inline 15 | #endif 16 | 17 | #if defined(__cplusplus) 18 | extern "C" { 19 | #endif 20 | 21 | // 22 | // Allow disabling of code generation 23 | // 24 | #ifndef MCGEN_DISABLE_PROVIDER_CODE_GENERATION 25 | #if !defined(McGenDebug) 26 | #define McGenDebug(a,b) 27 | #endif 28 | 29 | 30 | #if !defined(MCGEN_TRACE_CONTEXT_DEF) 31 | #define MCGEN_TRACE_CONTEXT_DEF 32 | typedef struct _MCGEN_TRACE_CONTEXT 33 | { 34 | TRACEHANDLE RegistrationHandle; 35 | TRACEHANDLE Logger; 36 | ULONGLONG MatchAnyKeyword; 37 | ULONGLONG MatchAllKeyword; 38 | ULONG Flags; 39 | ULONG IsEnabled; 40 | UCHAR Level; 41 | UCHAR Reserve; 42 | USHORT EnableBitsCount; 43 | PULONG EnableBitMask; 44 | const ULONGLONG* EnableKeyWords; 45 | const UCHAR* EnableLevel; 46 | } MCGEN_TRACE_CONTEXT, *PMCGEN_TRACE_CONTEXT; 47 | #endif 48 | 49 | #if !defined(MCGEN_LEVEL_KEYWORD_ENABLED_DEF) 50 | #define MCGEN_LEVEL_KEYWORD_ENABLED_DEF 51 | FORCEINLINE 52 | BOOLEAN 53 | McGenLevelKeywordEnabled( 54 | _In_ PMCGEN_TRACE_CONTEXT EnableInfo, 55 | _In_ UCHAR Level, 56 | _In_ ULONGLONG Keyword 57 | ) 58 | { 59 | // 60 | // Check if the event Level is lower than the level at which 61 | // the channel is enabled. 62 | // If the event Level is 0 or the channel is enabled at level 0, 63 | // all levels are enabled. 64 | // 65 | 66 | if ((Level <= EnableInfo->Level) || // This also covers the case of Level == 0. 67 | (EnableInfo->Level == 0)) { 68 | 69 | // 70 | // Check if Keyword is enabled 71 | // 72 | 73 | if ((Keyword == (ULONGLONG)0) || 74 | ((Keyword & EnableInfo->MatchAnyKeyword) && 75 | ((Keyword & EnableInfo->MatchAllKeyword) == EnableInfo->MatchAllKeyword))) { 76 | return TRUE; 77 | } 78 | } 79 | 80 | return FALSE; 81 | 82 | } 83 | #endif 84 | 85 | #if !defined(MCGEN_EVENT_ENABLED_DEF) 86 | #define MCGEN_EVENT_ENABLED_DEF 87 | FORCEINLINE 88 | BOOLEAN 89 | McGenEventEnabled( 90 | _In_ PMCGEN_TRACE_CONTEXT EnableInfo, 91 | _In_ PCEVENT_DESCRIPTOR EventDescriptor 92 | ) 93 | { 94 | 95 | return McGenLevelKeywordEnabled(EnableInfo, EventDescriptor->Level, EventDescriptor->Keyword); 96 | 97 | } 98 | #endif 99 | 100 | 101 | // 102 | // EnableCheckMacro 103 | // 104 | #ifndef MCGEN_ENABLE_CHECK 105 | #define MCGEN_ENABLE_CHECK(Context, Descriptor) (Context.IsEnabled && McGenEventEnabled(&Context, &Descriptor)) 106 | #endif 107 | 108 | #if !defined(MCGEN_CONTROL_CALLBACK) 109 | #define MCGEN_CONTROL_CALLBACK 110 | 111 | DECLSPEC_NOINLINE __inline 112 | VOID 113 | __stdcall 114 | McGenControlCallbackV2( 115 | _In_ LPCGUID SourceId, 116 | _In_ ULONG ControlCode, 117 | _In_ UCHAR Level, 118 | _In_ ULONGLONG MatchAnyKeyword, 119 | _In_ ULONGLONG MatchAllKeyword, 120 | _In_opt_ PEVENT_FILTER_DESCRIPTOR FilterData, 121 | _Inout_opt_ PVOID CallbackContext 122 | ) 123 | /*++ 124 | 125 | Routine Description: 126 | 127 | This is the notification callback for Windows Vista and later. 128 | 129 | Arguments: 130 | 131 | SourceId - The GUID that identifies the session that enabled the provider. 132 | 133 | ControlCode - The parameter indicates whether the provider 134 | is being enabled or disabled. 135 | 136 | Level - The level at which the event is enabled. 137 | 138 | MatchAnyKeyword - The bitmask of keywords that the provider uses to 139 | determine the category of events that it writes. 140 | 141 | MatchAllKeyword - This bitmask additionally restricts the category 142 | of events that the provider writes. 143 | 144 | FilterData - The provider-defined data. 145 | 146 | CallbackContext - The context of the callback that is defined when the provider 147 | called EtwRegister to register itself. 148 | 149 | Remarks: 150 | 151 | ETW calls this function to notify provider of enable/disable 152 | 153 | --*/ 154 | { 155 | PMCGEN_TRACE_CONTEXT Ctx = (PMCGEN_TRACE_CONTEXT)CallbackContext; 156 | ULONG Ix; 157 | #ifndef MCGEN_PRIVATE_ENABLE_CALLBACK_V2 158 | UNREFERENCED_PARAMETER(SourceId); 159 | UNREFERENCED_PARAMETER(FilterData); 160 | #endif 161 | 162 | if (Ctx == NULL) { 163 | return; 164 | } 165 | 166 | switch (ControlCode) { 167 | 168 | case EVENT_CONTROL_CODE_ENABLE_PROVIDER: 169 | Ctx->Level = Level; 170 | Ctx->MatchAnyKeyword = MatchAnyKeyword; 171 | Ctx->MatchAllKeyword = MatchAllKeyword; 172 | Ctx->IsEnabled = EVENT_CONTROL_CODE_ENABLE_PROVIDER; 173 | 174 | for (Ix = 0; Ix < Ctx->EnableBitsCount; Ix += 1) { 175 | if (McGenLevelKeywordEnabled(Ctx, Ctx->EnableLevel[Ix], Ctx->EnableKeyWords[Ix]) != FALSE) { 176 | Ctx->EnableBitMask[Ix >> 5] |= (1 << (Ix % 32)); 177 | } else { 178 | Ctx->EnableBitMask[Ix >> 5] &= ~(1 << (Ix % 32)); 179 | } 180 | } 181 | break; 182 | 183 | case EVENT_CONTROL_CODE_DISABLE_PROVIDER: 184 | Ctx->IsEnabled = EVENT_CONTROL_CODE_DISABLE_PROVIDER; 185 | Ctx->Level = 0; 186 | Ctx->MatchAnyKeyword = 0; 187 | Ctx->MatchAllKeyword = 0; 188 | if (Ctx->EnableBitsCount > 0) { 189 | RtlZeroMemory(Ctx->EnableBitMask, (((Ctx->EnableBitsCount - 1) / 32) + 1) * sizeof(ULONG)); 190 | } 191 | break; 192 | 193 | default: 194 | break; 195 | } 196 | 197 | #ifdef MCGEN_PRIVATE_ENABLE_CALLBACK_V2 198 | // 199 | // Call user defined callback 200 | // 201 | MCGEN_PRIVATE_ENABLE_CALLBACK_V2( 202 | SourceId, 203 | ControlCode, 204 | Level, 205 | MatchAnyKeyword, 206 | MatchAllKeyword, 207 | FilterData, 208 | CallbackContext 209 | ); 210 | #endif 211 | 212 | return; 213 | } 214 | 215 | #endif 216 | #endif // MCGEN_DISABLE_PROVIDER_CODE_GENERATION 217 | //+ 218 | // Provider Sealighter Event Count 1 219 | //+ 220 | EXTERN_C __declspec(selectany) const GUID SEALIGHTER_PROVIDER = {0xcdd5f0cc, 0xab0c, 0x4abe, {0x97, 0xb2, 0xcc, 0x82, 0xb7, 0xe6, 0x8f, 0x30}}; 221 | 222 | // 223 | // Channel 224 | // 225 | #define SEALIGHTER_OPERATIONAL 0x10 226 | 227 | // 228 | // Opcodes 229 | // 230 | #define SEALIGHTER_REPORT_OPCODE 0xa 231 | 232 | // 233 | // Tasks 234 | // 235 | #define SEALIGHTER_REPORT_TASK 0x1 236 | EXTERN_C __declspec(selectany) const GUID ReportId = {0xf87d5c2b, 0xe51e, 0x466b, {0xac, 0x10, 0x54, 0xd2, 0x31, 0x22, 0x0f, 0x98}}; 237 | // 238 | // Keyword 239 | // 240 | #define SEALIGHTER_REPORT_KEYWORD 0x1 241 | 242 | // 243 | // Event Descriptors 244 | // 245 | EXTERN_C __declspec(selectany) const EVENT_DESCRIPTOR SEALIGHTER_REPORT_EVENT = {0x1, 0x1, 0x10, 0x4, 0xa, 0x1, 0x8000000000000001}; 246 | #define SEALIGHTER_REPORT_EVENT_value 0x1 247 | 248 | // 249 | // Note on Generate Code from Manifest for Windows Vista and above 250 | // 251 | //Structures : are handled as a size and pointer pairs. The macro for the event will have an extra 252 | //parameter for the size in bytes of the structure. Make sure that your structures have no extra padding. 253 | // 254 | //Strings: There are several cases that can be described in the manifest. For array of variable length 255 | //strings, the generated code will take the count of characters for the whole array as an input parameter. 256 | // 257 | //SID No support for array of SIDs, the macro will take a pointer to the SID and use appropriate 258 | //GetLengthSid function to get the length. 259 | // 260 | 261 | // 262 | // Allow disabling of code generation 263 | // 264 | #ifndef MCGEN_DISABLE_PROVIDER_CODE_GENERATION 265 | 266 | // 267 | // Globals 268 | // 269 | 270 | 271 | // 272 | // Event Enablement Bits 273 | // 274 | 275 | EXTERN_C __declspec(selectany) DECLSPEC_CACHEALIGN ULONG SealighterEnableBits[1]; 276 | EXTERN_C __declspec(selectany) const ULONGLONG SealighterKeywords[1] = {0x8000000000000001}; 277 | EXTERN_C __declspec(selectany) const UCHAR SealighterLevels[1] = {4}; 278 | EXTERN_C __declspec(selectany) MCGEN_TRACE_CONTEXT SEALIGHTER_PROVIDER_Context = {0, 0, 0, 0, 0, 0, 0, 0, 1, SealighterEnableBits, SealighterKeywords, SealighterLevels}; 279 | 280 | EXTERN_C __declspec(selectany) REGHANDLE SealighterHandle = (REGHANDLE)0; 281 | 282 | #if !defined(McGenEventRegisterUnregister) 283 | #define McGenEventRegisterUnregister 284 | #pragma warning(push) 285 | #pragma warning(disable:6103) 286 | DECLSPEC_NOINLINE __inline 287 | ULONG __stdcall 288 | McGenEventRegister( 289 | _In_ LPCGUID ProviderId, 290 | _In_opt_ PENABLECALLBACK EnableCallback, 291 | _In_opt_ PVOID CallbackContext, 292 | _Inout_ PREGHANDLE RegHandle 293 | ) 294 | /*++ 295 | 296 | Routine Description: 297 | 298 | This function registers the provider with ETW USER mode. 299 | 300 | Arguments: 301 | ProviderId - Provider ID to be register with ETW. 302 | 303 | EnableCallback - Callback to be used. 304 | 305 | CallbackContext - Context for this provider. 306 | 307 | RegHandle - Pointer to registration handle. 308 | 309 | Remarks: 310 | 311 | If the handle != NULL will return ERROR_SUCCESS 312 | 313 | --*/ 314 | { 315 | ULONG Error; 316 | 317 | 318 | if (*RegHandle) { 319 | // 320 | // already registered 321 | // 322 | return ERROR_SUCCESS; 323 | } 324 | 325 | Error = EventRegister( ProviderId, EnableCallback, CallbackContext, RegHandle); 326 | 327 | return Error; 328 | } 329 | #pragma warning(pop) 330 | 331 | 332 | DECLSPEC_NOINLINE __inline 333 | ULONG __stdcall 334 | McGenEventUnregister(_Inout_ PREGHANDLE RegHandle) 335 | /*++ 336 | 337 | Routine Description: 338 | 339 | Unregister from ETW USER mode 340 | 341 | Arguments: 342 | RegHandle this is the pointer to the provider context 343 | Remarks: 344 | If provider has not been registered, RegHandle == NULL, 345 | return ERROR_SUCCESS 346 | --*/ 347 | { 348 | ULONG Error; 349 | 350 | 351 | if(!(*RegHandle)) { 352 | // 353 | // Provider has not registerd 354 | // 355 | return ERROR_SUCCESS; 356 | } 357 | 358 | Error = EventUnregister(*RegHandle); 359 | *RegHandle = (REGHANDLE)0; 360 | 361 | return Error; 362 | } 363 | #endif 364 | // 365 | // Register with ETW Vista + 366 | // 367 | #ifndef EventRegisterSealighter 368 | #define EventRegisterSealighter() McGenEventRegister(&SEALIGHTER_PROVIDER, McGenControlCallbackV2, &SEALIGHTER_PROVIDER_Context, &SealighterHandle) 369 | #endif 370 | 371 | // 372 | // UnRegister with ETW 373 | // 374 | #ifndef EventUnregisterSealighter 375 | #define EventUnregisterSealighter() McGenEventUnregister(&SealighterHandle) 376 | #endif 377 | 378 | // 379 | // Enablement check macro for SEALIGHTER_REPORT_EVENT 380 | // 381 | 382 | #define EventEnabledSEALIGHTER_REPORT_EVENT() ((SealighterEnableBits[0] & 0x00000001) != 0) 383 | 384 | // 385 | // Event Macro for SEALIGHTER_REPORT_EVENT 386 | // 387 | #define EventWriteSEALIGHTER_REPORT_EVENT(json, activity_id, event_flags, event_id, event_name, event_opcode, event_version, process_id, provider_name, task_name, thread_id, timestamp, trace_name)\ 388 | EventEnabledSEALIGHTER_REPORT_EVENT() ?\ 389 | Template_sshhzccqzzqis(SealighterHandle, &SEALIGHTER_REPORT_EVENT, json, activity_id, event_flags, event_id, event_name, event_opcode, event_version, process_id, provider_name, task_name, thread_id, timestamp, trace_name)\ 390 | : ERROR_SUCCESS\ 391 | 392 | #endif // MCGEN_DISABLE_PROVIDER_CODE_GENERATION 393 | 394 | 395 | // 396 | // Allow Diasabling of code generation 397 | // 398 | #ifndef MCGEN_DISABLE_PROVIDER_CODE_GENERATION 399 | 400 | // 401 | // Template Functions 402 | // 403 | // 404 | //Template from manifest : SEALIGHTER_REPORT_TEMPLATE 405 | // 406 | #ifndef Template_sshhzccqzzqis_def 407 | #define Template_sshhzccqzzqis_def 408 | ETW_INLINE 409 | ULONG 410 | Template_sshhzccqzzqis( 411 | _In_ REGHANDLE RegHandle, 412 | _In_ PCEVENT_DESCRIPTOR Descriptor, 413 | _In_opt_ LPCSTR _Arg0, 414 | _In_opt_ LPCSTR _Arg1, 415 | _In_ const unsigned short _Arg2, 416 | _In_ const unsigned short _Arg3, 417 | _In_opt_ PCWSTR _Arg4, 418 | _In_ const UCHAR _Arg5, 419 | _In_ const UCHAR _Arg6, 420 | _In_ const unsigned int _Arg7, 421 | _In_opt_ PCWSTR _Arg8, 422 | _In_opt_ PCWSTR _Arg9, 423 | _In_ const unsigned int _Arg10, 424 | _In_ signed __int64 _Arg11, 425 | _In_opt_ LPCSTR _Arg12 426 | ) 427 | { 428 | #define ARGUMENT_COUNT_sshhzccqzzqis 13 429 | 430 | EVENT_DATA_DESCRIPTOR EventData[ARGUMENT_COUNT_sshhzccqzzqis]; 431 | 432 | EventDataDescCreate(&EventData[0], 433 | (_Arg0 != NULL) ? _Arg0 : "NULL", 434 | (_Arg0 != NULL) ? (ULONG)((strlen(_Arg0) + 1) * sizeof(CHAR)) : (ULONG)sizeof("NULL")); 435 | 436 | EventDataDescCreate(&EventData[1], 437 | (_Arg1 != NULL) ? _Arg1 : "NULL", 438 | (_Arg1 != NULL) ? (ULONG)((strlen(_Arg1) + 1) * sizeof(CHAR)) : (ULONG)sizeof("NULL")); 439 | 440 | EventDataDescCreate(&EventData[2], &_Arg2, sizeof(const unsigned short) ); 441 | 442 | EventDataDescCreate(&EventData[3], &_Arg3, sizeof(const unsigned short) ); 443 | 444 | EventDataDescCreate(&EventData[4], 445 | (_Arg4 != NULL) ? _Arg4 : L"NULL", 446 | (_Arg4 != NULL) ? (ULONG)((wcslen(_Arg4) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"NULL")); 447 | 448 | EventDataDescCreate(&EventData[5], &_Arg5, sizeof(const UCHAR) ); 449 | 450 | EventDataDescCreate(&EventData[6], &_Arg6, sizeof(const UCHAR) ); 451 | 452 | EventDataDescCreate(&EventData[7], &_Arg7, sizeof(const unsigned int) ); 453 | 454 | EventDataDescCreate(&EventData[8], 455 | (_Arg8 != NULL) ? _Arg8 : L"NULL", 456 | (_Arg8 != NULL) ? (ULONG)((wcslen(_Arg8) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"NULL")); 457 | 458 | EventDataDescCreate(&EventData[9], 459 | (_Arg9 != NULL) ? _Arg9 : L"NULL", 460 | (_Arg9 != NULL) ? (ULONG)((wcslen(_Arg9) + 1) * sizeof(WCHAR)) : (ULONG)sizeof(L"NULL")); 461 | 462 | EventDataDescCreate(&EventData[10], &_Arg10, sizeof(const unsigned int) ); 463 | 464 | EventDataDescCreate(&EventData[11], &_Arg11, sizeof(signed __int64) ); 465 | 466 | EventDataDescCreate(&EventData[12], 467 | (_Arg12 != NULL) ? _Arg12 : "NULL", 468 | (_Arg12 != NULL) ? (ULONG)((strlen(_Arg12) + 1) * sizeof(CHAR)) : (ULONG)sizeof("NULL")); 469 | 470 | return EventWrite(RegHandle, Descriptor, ARGUMENT_COUNT_sshhzccqzzqis, EventData); 471 | } 472 | #endif 473 | 474 | #endif // MCGEN_DISABLE_PROVIDER_CODE_GENERATION 475 | 476 | #if defined(__cplusplus) 477 | }; 478 | #endif 479 | 480 | #define MSG_SEALIGHTER_PROVIDER_Keyword_SEALIGHTER_REPORT_message 0x10000001L 481 | #define MSG_SEALIGHTER_PROVIDER_opcode_SEALIGHTER_REPORT_OPCODE_message 0x3000000AL 482 | #define MSG_level_Informational 0x50000004L 483 | #define MSG_Sealighter_task_SEALIGHTER_REPORT_TASK_message 0x70000001L 484 | #define MSG_SEALIGHTER_PROVIDER_channel_SEALIGHTER_OPERATIONAL_message 0x90000001L 485 | #define MSG_Sealighter_event_1_message 0xB0010001L 486 | -------------------------------------------------------------------------------- /docs/SCENARIOS.md: -------------------------------------------------------------------------------- 1 | # Scenarios 2 | 3 | # ETW and WPP Overviews and examples 4 | These great blogs provide a great example of the power of ETW, TraceLogging, and WPP: 5 | - [Data Source Analysis and Dynamic Windows RE using WPP and TraceLogging](https://posts.specterops.io/data-source-analysis-and-dynamic-windows-re-using-wpp-and-tracelogging-e465f8b653f7) 6 | - [Hidden Treasure: Intrusion Detection with ETW](https://zacbrown.org/2017/04/11/hidden-treasure-intrusion-detection-with-etw-part-1) 7 | - [Tampering with Windows Event Tracing: Background, Offense, and Defense](https://medium.com/palantir/tampering-with-windows-event-tracing-background-offense-and-defense-4be7ac62ac63) 8 | 9 | 10 | # Example uses using Sealighter 11 | - [Tracking process execution](#Tracking-process-execution) 12 | - [Find data in any field](#Find-data-in-any-field) 13 | - [Find correlated Events](#Find-correlated-Events) 14 | - [Use Stack Traces](#Use-Stack-Traces) 15 | - [Use Buffering](#Use-Buffering) 16 | - [WPP Tracing](#WPP-Tracing) 17 | 18 | # Tracking process execution 19 | Lets trace a program using [Zac Brown's ideas](https://zacbrown.org/2017/04/11/hidden-treasure-intrusion-detection-with-etw-part-1). Create the Following Config: 20 | ```json 21 | { 22 | "session_properties": { 23 | "session_name": "Sealighter-Trace", 24 | "output_format": "file", 25 | "output_filename": "output.json" 26 | }, 27 | "user_traces": [ 28 | { 29 | "trace_name": "powershell_dns", 30 | "provider_name": "Microsoft-Windows-DNS-Client", 31 | "filters": { 32 | "any_of": { 33 | "process_name_contains": "powershell.exe" 34 | } 35 | } 36 | }, 37 | { 38 | "trace_name": "powershell_wmi", 39 | "provider_name": "Microsoft-Windows-WMI", 40 | "filters": { 41 | "any_of": { 42 | "process_name_contains": "powershell.exe" 43 | } 44 | } 45 | }, 46 | { 47 | "trace_name": "powershell_winpshell", 48 | "provider_name": "Microsoft-Windows-PowerShell", 49 | "filters": { 50 | "any_of": { 51 | "process_name_contains": "powershell.exe" 52 | } 53 | } 54 | } 55 | ], 56 | "kernel_traces": [ 57 | { 58 | "trace_name": "powershell_kern_image_load", 59 | "provider_name": "image_load", 60 | "filters": { 61 | "any_of": { 62 | "process_name_contains": "powershell.exe" 63 | } 64 | } 65 | }, 66 | { 67 | "trace_name": "powershell_kern_tcpip", 68 | "provider_name": "network_tcpip", 69 | "filters": { 70 | "any_of": { 71 | "process_name_contains": "powershell.exe" 72 | } 73 | } 74 | } 75 | ] 76 | } 77 | ``` 78 | 79 | We can now start Sealighter, Open PowerShell, run mimikatz in it, then stop the trace. 80 | 81 | Then we can use Python to parse the events: 82 | ```python 83 | import json 84 | events = list() 85 | 86 | with open("output.json", "r") as f: 87 | for line in f: 88 | events.append(json.loads(line)) 89 | 90 | for event_string in events: 91 | event = json.loads(event_string) 92 | if event["provider_name"] == "MSNT_SystemTrace" and event["task_name"] == "Image": 93 | # Image Load: 94 | image_filename = event["properties"]["FileName"] 95 | print(f"[A] Image Loaded: {image_filename})") 96 | 97 | elif event["provider_name"] == "Microsoft-Windows-PowerShell" and event["event_id"] == 4104: 98 | # PowerShell Script 99 | script = event["properties"]["ScriptBlockText"] 100 | print(f"[B] Script: {script})") 101 | ``` 102 | 103 | 104 | ---------- 105 | 106 | 107 | # Find data in any field 108 | Let's investigate a TraceLogging trace using [Matt Graeber's blog](https://posts.specterops.io/data-source-analysis-and-dynamic-windows-re-using-wpp-and-tracelogging-e465f8b653f7) as a guide. 109 | 110 | Load up his `TLGMetadataParser.psm1` Script in PowerShell, and let's see if there's any TraceLogging providers in `Shell32.dll`: 111 | ```powershell 112 | Import-Module .\TLGMetadataParser.psm1 113 | $shell32 = Get-TraceLoggingMetadata -Path C:\Windows\System32\shell32.dll 114 | $shell32.Providers | Format-List 115 | ``` 116 | 117 | In the list of providers, you should see this one: 118 | ``` 119 | ProviderGUID : 382b5e24-181e-417f-a8d6-2155f749e724 120 | ProviderName : Microsoft.Windows.ShellExecute 121 | ProviderGroupGUID : 4f50731a-89cf-4782-b3e0-dce8c90476ba 122 | ``` 123 | 124 | This appears to be something to do with `ShellExecute`, a common way malware can launch programs from VBA, or other scripts. 125 | For example, here's how you can use it from PowerShell: 126 | ```powershell 127 | $shellobj = New-Object -ComObject Shell.Application 128 | $shellobj.ShellExecute("notepad.exe", "test.txt") 129 | ``` 130 | 131 | Let's Create a Sealighter trace with two providers: 132 | 1. A Process Trace, where we'll get all process starts (Event Id 2) 133 | 2. A TraceLogging trace for the ShellExecute provider, searching for any event that contains the string "cmd.exe" 134 | The config will look like this: 135 | ```json 136 | { 137 | "session_properties": { 138 | "session_name": "Sealighter-Trace", 139 | "output_format": "stdout" 140 | }, 141 | "user_traces": [ 142 | { 143 | "trace_name": "proc_trace", 144 | "provider_name": "Microsoft-Windows-Kernel-Process", 145 | "keywords_any": 16, 146 | "filters": { 147 | "any_of": { 148 | "event_id_is": 1 149 | } 150 | } 151 | }, 152 | { 153 | "trace_name": "shell32_trace", 154 | "provider_name": "{382b5e24-181e-417f-a8d6-2155f749e724}", 155 | "filters": { 156 | "any_of": { 157 | "any_field_contains": "notepad.exe" 158 | } 159 | } 160 | } 161 | ] 162 | } 163 | ``` 164 | 165 | Start the Sealighter trace, run the PowerShell `.ShellExecute` script, and you should see 2 events similar to the following: 166 | ```json 167 | { 168 | "header": { 169 | "activity_id": "{636D6229-2DBC-0001-0F21-6E63BC2DD601}", 170 | "event_flags": 577, 171 | "event_id": 0, 172 | "event_name": "ShellExecuteExW", 173 | "event_opcode": 1, 174 | "event_version": 0, 175 | "process_id": 15724, 176 | "provider_name": "Microsoft.Windows.ShellExecute", 177 | "task_name": "ShellExecuteExW", 178 | "thread_id": 16392, 179 | "timestamp": "2020-05-19 12:08:08Z", 180 | "trace_name": "shell32_trace" 181 | }, 182 | "properties": { 183 | "PartA_PrivTags": 0, 184 | "dwHotKey": 0, 185 | "fMask": 0, 186 | "hMonitor": "0000000000000000", 187 | "hkeyClass": 0, 188 | "hwnd": "0000000000000000", 189 | "lpClass": "", 190 | "lpDirectory": "", 191 | "lpFile": "notepad.exe", 192 | "lpIDList": "0000000000000000", 193 | "lpParameters": "test.txt", 194 | "lpVerb": "", 195 | "nShow": 1, 196 | "site": 0, 197 | "wilActivity": "08400000" 198 | } 199 | } 200 | { 201 | "header": { 202 | "activity_id": "{00000000-0000-0000-0000-000000000000}", 203 | "event_flags": 576, 204 | "event_id": 1, 205 | "event_name": "", 206 | "event_opcode": 1, 207 | "event_version": 3, 208 | "process_id": 15724, 209 | "provider_name": "Microsoft-Windows-Kernel-Process", 210 | "task_name": "ProcessStart", 211 | "thread_id": 4564, 212 | "timestamp": "2020-05-19 12:08:08Z", 213 | "trace_name": "proc_trace" 214 | }, 215 | "properties": { 216 | "CreateTime": "2020-05-19 12:08:08Z", 217 | "Flags": 0, 218 | "ImageChecksum": 219193, 219 | "ImageName": "\\Device\\HarddiskVolume4\\Windows\\System32\\notepad.exe", 220 | "MandatoryLabel": "Mandatory Label\\High Mandatory Level", 221 | "PackageFullName": "", 222 | "PackageRelativeAppId": "", 223 | "ParentProcessID": 15724, 224 | "ParentProcessSequenceNumber": 7866, 225 | "ProcessID": 16144, 226 | "ProcessSequenceNumber": 8055, 227 | "ProcessTokenElevationType": 2, 228 | "ProcessTokenIsElevated": 1, 229 | "SessionID": 1, 230 | "TimeDateStamp": 587902357 231 | } 232 | } 233 | ``` 234 | 235 | In my example, the PID of the PowerShell that ran the command was `15724`. 236 | We can see that PowerShell triggered an event 237 | 238 | We can see a generated event that we didn't know existed called `ShellExecuteExW`, which contains the program we ran (`lpFile`) and the arguments (`lpParameters`). 239 | 240 | We can also see the information also matches up with the `Microsoft-Windows-Kernel-Process` trace, confirming that this was us that triggered the event. 241 | 242 | 243 | ---------- 244 | 245 | ## Find correlated Events 246 | Activity IDs are unique GUIDs in the event header for a chain of events, and enable correlation of events across time and even across different providers. 247 | 248 | To look for providers that use activity IDs, you could use the following filter, to filter out any events that *don't* have an activity ID: 249 | ```json 250 | "filters": { 251 | "none_of": { 252 | "activity_id_is": "{00000000-0000-0000-0000-000000000000}" 253 | } 254 | } 255 | ``` 256 | 257 | Then it would be a case of running various ETW, TraceLogging, or WPP providers, doing "stuff", and seeing if any events get emitted. 258 | 259 | 260 | ### Use Stack Traces 261 | To demonstrate the use of Stack Traces, let's make and trace our own C program that also does `ShellExecute`: 262 | First we compile the following simple x86 C program and name it `shellcaller.exe`: 263 | ```c++ 264 | #include 265 | #include 266 | #include 267 | 268 | __declspec(noinline) 269 | static void call_com() 270 | { 271 | // Print the base address to make RE faster 272 | // You could also find this out by degging the process 273 | printf("Base address: 0x%p\n", (void*)GetModuleHandleA(NULL)); 274 | ShellExecuteA(NULL, "open", "notepad.exe", NULL, NULL, SW_SHOWNORMAL); 275 | } 276 | 277 | int main() 278 | { 279 | call_com(); 280 | return 0; 281 | } 282 | ``` 283 | 284 | Then, we'll run Sealighter with the same sort of config to trace the Shell32 Events, 285 | but also use `report_stacktrace` to get stack traces: 286 | ```json 287 | { 288 | "session_properties": { 289 | "session_name": "Sealighter-Trace", 290 | "output_format": "stdout" 291 | }, 292 | "user_traces": [ 293 | { 294 | "trace_name": "shell32_trace", 295 | "provider_name": "{382b5e24-181e-417f-a8d6-2155f749e724}", 296 | "report_stacktrace": true, 297 | "filters": { 298 | "any_of": { 299 | "any_field_contains": "shellcaller.exe" 300 | } 301 | } 302 | } 303 | ] 304 | } 305 | ``` 306 | 307 | Start Sealighter, and once the trace has started run `shellcaller.exe` - It should launch 308 | a `notepad.exe` process, and you should get an output similar to below (but with different numbers): 309 | ``` 310 | Base address: 0x00B80000 311 | ``` 312 | 313 | Sealighter should also output an event similar to below (but again with different numbers): 314 | ```json 315 | { 316 | "header": { 317 | "activity_id": "{6D9D4EDB-33CC-0002-7D4A-A16DCC33D601}", 318 | "event_flags": 545, 319 | "event_id": 0, 320 | "event_name": "ShellExecuteExW", 321 | "event_opcode": 1, 322 | "event_version": 0, 323 | "process_id": 12016, 324 | "provider_name": "Microsoft.Windows.ShellExecute", 325 | "task_name": "ShellExecuteExW", 326 | "thread_id": 5964, 327 | "timestamp": "2020-05-28 10:20:07Z", 328 | "trace_name": "ShellExecute-Stacktrace" 329 | }, 330 | "properties": { 331 | "PartA_PrivTags": 0, 332 | "dwHotKey": 0, 333 | "fMask": 5376, 334 | "hMonitor": "00000000", 335 | "hkeyClass": 0, 336 | "hwnd": "00000000", 337 | "lpClass": "", 338 | "lpDirectory": "", 339 | "lpFile": "notepad.exe", 340 | "lpIDList": "00000000", 341 | "lpParameters": "", 342 | "lpVerb": "open", 343 | "nShow": 1, 344 | "site": 7340143, 345 | "wilActivity": "4C170000" 346 | }, 347 | "property_types": { 348 | "PartA_PrivTags": "UINT64", 349 | "dwHotKey": "UINT32", 350 | "fMask": "UINT32", 351 | "hMonitor": "OTHER", 352 | "hkeyClass": "UINT32", 353 | "hwnd": "OTHER", 354 | "lpClass": "STRINGW", 355 | "lpDirectory": "STRINGW", 356 | "lpFile": "STRINGW", 357 | "lpIDList": "OTHER", 358 | "lpParameters": "STRINGW", 359 | "lpVerb": "STRINGW", 360 | "nShow": "INT32", 361 | "site": "UINT32", 362 | "wilActivity": "OTHER" 363 | }, 364 | "stack_trace": [ 365 | "0x7FFA18BAB944", 366 | "0x7FFA18692E9F", 367 | "0x7FFA1868902A", 368 | "0x773817C3", 369 | "0x773811B9", 370 | "0x7FFA186838C9", 371 | "0x7FFA186832BD", 372 | "0x7FFA18BE266E", 373 | "0x7FFA18BD12F1", 374 | "0x7FFA18B84543", 375 | "0x7FFA18B844EE", 376 | "0x7740169C", 377 | "0x773F451B", 378 | "0x773F43D9", 379 | "0x76DE9F06", 380 | "0x76DAEA61", 381 | "0x76DAE21B", 382 | "0x76DADFF2", 383 | "0x76EE05F4", 384 | "0x76EE0591", 385 | "0xB8106E", 386 | "0x7653F989", 387 | "0x773F7084", 388 | "0x773F7054" 389 | ] 390 | } 391 | ``` 392 | So we see that `shellcaller.exe` did indeed call `ShellExecute` to launch `notepad.exe` 393 | We now also get a stack trace. 394 | 395 | In this example, The base in-memory address of `shellcaller.exe` is `0x00B80000`. 396 | We can also see one of the addresses in the stack is `0xB8106E`, 0x106E bytes into the image. 397 | 398 | Now, if We open up a tool like [Ghidra](https://ghidra-sre.org), import `shellcaller.exe` into it, 399 | and go `0x106E` bytes into the image, we should see our call to `SHELL32.DLL::ShellExecuteA`. 400 | 401 | 402 | 403 | ## Use Buffering 404 | Buffering enables the reporting of many similar events in a time period as one with a count. 405 | 406 | For example, Let's create a trace to log process starts, but buffer all process that have the same ImageFileName together, reporting in groups of every 10 seconds. We will use the following config: 407 | ```json 408 | { 409 | "session_properties": { 410 | "session_name": "Sealighter-Trace", 411 | "output_format": "stdout", 412 | "buffering_timout_seconds": 10 413 | }, 414 | "user_traces": [ 415 | { 416 | "trace_name": "ProcTrace01", 417 | "provider_name": "Microsoft-Windows-Kernel-Process", 418 | "keywords_any": 16, 419 | "filters": { 420 | "any_of": { 421 | "event_id_is": 1 422 | } 423 | }, 424 | "buffers": [ 425 | { 426 | "event_id": 1, 427 | "max_before_buffering": 0, 428 | "properties_to_match": [ 429 | "ImageName" 430 | ] 431 | } 432 | ] 433 | } 434 | ] 435 | } 436 | ``` 437 | We have created a `Microsoft-Windows-Kernel-Process` to only look at Event ID 1, i.e. `ProcessStart` events. 438 | We have set the `buffering_timout_seconds` option to 10 seconds. 439 | We have set 1 buffer for Event 1, to match in the Property `ImageName`. 440 | 441 | Running this trace, we will get reports once every 10 seconds. Any matching ImageNames will be 442 | rolled into a single event with a `buffered_count` field. 443 | 444 | Process with different ImageNames will be their own events. 445 | 446 | 447 | ## WPP Tracing 448 | WPP Tracing is special, as the format of the events are not contained in the trace at runtime. 449 | The format is usually compiled into the pdb file, or in a seperate TMF file. 450 | 451 | Curretly Sealighter cannot auto-parse WPP events, however we can get the raw event data, and parse them 452 | after the fact with Python. 453 | 454 | As an example, lets trace the OLE32 COM provider. Using the microsoft documentation [here](https://support.microsoft.com/en-us/help/926098/how-to-enable-com-and-com-diagnostic-tracing), first enable OLE32 tracing by runnig the following command in an elevated prompt: 455 | ``` 456 | reg add HKEY_LOCAL_MACHINE\Software\Microsoft\OLE\Tracing /v ExecutablesToTrace /t REG_MULTI_SZ /d * /f 457 | ``` 458 | Now we can run a sealighter trace. 459 | 460 | We need to log not only the WPP provider GUID, but also the message GUID, as WPP messages get delivered 461 | to their own provider. We know these GUIDS by reverse engineering ole32.dll. Keywords and levels in WPP traces are CHARs, so to get all events we will set both to 0xff, i.e. `255`. 462 | We will use `dump_raw_event` to dump the raw hex-encoded bytes, and also limit it to just 10 messages, 463 | to prevent the trace from flooding us with too much information. The end config shoud look something like this: 464 | ```json 465 | { 466 | "session_properties": { 467 | "session_name": "Sealighter-Trace", 468 | "output_format": "file", 469 | "output_filename": "ole.json" 470 | }, 471 | "user_traces": [ 472 | { 473 | "trace_name": "ole32", 474 | "provider_name": "{BDA92AE8-9F11-4D49-BA1D-A4C2ABCA692E}", 475 | "keywords_any": 255, 476 | "level": 255 477 | }, 478 | { 479 | "trace_name": "ole32-message", 480 | "provider_name": "{0F480EA8-F109-39A3-8A27-36DC7E84A294}", 481 | "dump_raw_event": true, 482 | "filters": { 483 | "any_of": { 484 | "max_events_total": 10 485 | } 486 | }, 487 | } 488 | ] 489 | } 490 | ``` 491 | Note the GUIDs might be different with different versions of the dll. 492 | Start the trace, wait 30 seconds, and you should have 10 events in `ole.json`. 493 | We know from reverse engineering that the messages we logged are just a ANSI string, so 494 | we can parse the file with this simple python script: 495 | ```python 496 | import json 497 | import binascii 498 | 499 | with open("ole.json", "r") as f: 500 | for line in f: 501 | # Get the event in JSON format 502 | event = json.loads(line) 503 | process_id = event["header"]["process_id"] 504 | # Extract and convert the raw bytes 505 | raw_hex = event["raw"] 506 | raw_bytes = binascii.unhexlify(raw_hex) 507 | raw_string = raw_bytes.decode("utf16") 508 | # Print the string 509 | print(f"[{process_id}] - {raw_string}") 510 | ``` 511 | Run the script, and you should see output like the following: 512 | ``` 513 | [1516] - (onecore\com\combase\common\internal\comtrace.cxx):(InitializeTracing):(159) Starting OLE32 tracing for: C:\WINDOWS\system32\conhost.exe 514 | [1316] - (onecore\com\combase\rpcss\objex\manager.cxx):(_ServerAllocateOIDs):(2968) process:000001634C6BC9C0 PID:618 C:\WINDOWS\System32\svchost.exe OXID:AAC34D291DA574F0 515 | [1316] - (onecore\com\combase\rpcss\objex\manager.cxx):(ServerAllocateOIDsInternal):(2866) process:000001634C6BC9C0 PID:618 C:\WINDOWS\System32\svchost.exe Server OXID:AAC34D291DA574F0 516 | [1316] - (onecore\com\combase\rpcss\objex\idtable.hxx):(CIdKey::CIdKey):(42) this:000000247377E8F0 ID:AAC34D291DA574F0 517 | ``` 518 | 519 | Once finished, we can stop OLE tracing by delting the reg key we created: 520 | ``` 521 | reg delete HKEY_LOCAL_MACHINE\Software\Microsoft\OLE\Tracing /v ExecutablesToTrace /f 522 | ``` 523 | 524 | In the future Sealighter might enable you to define the event structure at runtime so events look like other ETW events, but not right now. 525 | For more information on WPP tracing, again see [Matt Graeber's blog](https://posts.specterops.io/data-source-analysis-and-dynamic-windows-re-using-wpp-and-tracelogging-e465f8b653f7) as a guide. 526 | -------------------------------------------------------------------------------- /sealighter/sealighter_handler.cpp: -------------------------------------------------------------------------------- 1 | #include "sealighter_krabs.h" 2 | #include "sealighter_handler.h" 3 | #include "sealighter_errors.h" 4 | #include "sealighter_util.h" 5 | #include "sealighter_provider.h" 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | // ------------------------- 12 | // GLOBALS - START 13 | // ------------------------- 14 | 15 | // Output file to write events to 16 | static std::ofstream g_outfile; 17 | 18 | // Helper mutex to ensure threaded functions 19 | // print a whole event without interruption 20 | static std::mutex g_print_mutex; 21 | 22 | // Holds format 23 | static Output_format g_output_format; 24 | 25 | // Hold data for buffering 26 | static std::map> g_buffer_lists; 27 | // Default to 30 seconds 28 | static std::uint32_t g_buffer_lists_timeout_seconds = 5; 29 | static std::mutex g_buffer_lists_mutex; 30 | static std::thread g_buffer_list_thread; 31 | static std::atomic_bool g_buffer_thread_stop = false; 32 | static std::condition_variable g_buffer_list_con_var; 33 | 34 | // ------------------------- 35 | // GLOBALS - END 36 | // ------------------------- 37 | // PRIVATE FUNCTIONS - START 38 | // ------------------------- 39 | 40 | 41 | /* 42 | Print a line to stdout, using a mutex 43 | to ensure we print each event wholey before 44 | another can 45 | */ 46 | void threaded_print_ln 47 | ( 48 | std::string event_string 49 | ) 50 | { 51 | g_print_mutex.lock(); 52 | log_messageA("%s\n", event_string.c_str()); 53 | g_print_mutex.unlock(); 54 | } 55 | 56 | 57 | /* 58 | Write to Event Log 59 | */ 60 | void write_event_log 61 | ( 62 | json json_event, 63 | std::string trace_name, 64 | std::string event_string 65 | ) 66 | { 67 | DWORD status = ERROR_SUCCESS; 68 | 69 | // TODO: Make sure we didn't break this 70 | // Also fix up schema, no need to to all the str_wstr converting 71 | // Also fix up timestamp string 72 | status = EventWriteSEALIGHTER_REPORT_EVENT( 73 | event_string.c_str(), 74 | json_event["header"]["activity_id"].get().c_str(), 75 | (USHORT)json_event["header"]["event_flags"].get(), 76 | (USHORT)json_event["header"]["event_id"].get(), 77 | convert_str_wstr(json_event["header"]["event_name"].get()).c_str(), 78 | (UCHAR)json_event["header"]["event_opcode"].get(), 79 | (UCHAR)json_event["header"]["event_version"].get(), 80 | json_event["header"]["process_id"].get(), 81 | convert_str_wstr(json_event["header"]["provider_name"].get()).c_str(), 82 | convert_str_wstr(json_event["header"]["task_name"].get()).c_str(), 83 | json_event["header"]["thread_id"].get(), 84 | 0, // schema.timestamp().quadPart 85 | trace_name.c_str() 86 | ); 87 | 88 | if (status != ERROR_SUCCESS) { 89 | log_messageA("Error %ul line %d\n", status, __LINE__); 90 | return; 91 | } 92 | } 93 | 94 | 95 | /* 96 | Print a line to an output file, using a mutex 97 | to ensure we print each event wholey before 98 | another can 99 | */ 100 | void threaded_write_file_ln 101 | ( 102 | std::string event_string 103 | ) 104 | { 105 | g_print_mutex.lock(); 106 | g_outfile << event_string << std::endl; 107 | g_print_mutex.unlock(); 108 | } 109 | 110 | 111 | /* 112 | Convert an ETW Event to JSON 113 | */ 114 | json parse_event_to_json 115 | ( 116 | const EVENT_RECORD& record, 117 | const trace_context&, 118 | std::shared_ptr sealighter_context, 119 | krabs::schema schema 120 | ) 121 | { 122 | std::string trace_name = sealighter_context->trace_name; 123 | json json_properties; 124 | json json_properties_types; 125 | json json_header = { 126 | { "event_id", schema.event_id() }, 127 | { "event_name", convert_wstr_str(schema.event_name()) }, 128 | { "task_name", convert_wstr_str(schema.task_name()) }, 129 | { "thread_id", schema.thread_id() }, 130 | { "timestamp", convert_timestamp_string(schema.timestamp()) }, 131 | { "event_flags", schema.event_flags() }, 132 | { "event_opcode", schema.event_opcode() }, 133 | { "event_version", schema.event_version() }, 134 | { "process_id", schema.process_id()}, 135 | { "provider_name", convert_wstr_str(schema.provider_name()) }, 136 | { "activity_id", convert_guid_str(schema.activity_id()) }, 137 | { "trace_name", trace_name}, 138 | }; 139 | 140 | json json_event = { {"header", json_header} }; 141 | 142 | // Check if we are just dumping the raw event, or attempting to parse it 143 | if (sealighter_context->dump_raw_event) { 144 | std::string raw_hex = convert_bytearray_hexstring((BYTE*)record.UserData, record.UserDataLength); 145 | json_event["raw"] = raw_hex; 146 | } 147 | else { 148 | krabs::parser parser(schema); 149 | for (krabs::property& prop : parser.properties()) { 150 | std::wstring prop_name_wstr = prop.name(); 151 | std::string prop_name = convert_wstr_str(prop_name_wstr); 152 | 153 | try 154 | { 155 | switch (prop.type()) 156 | { 157 | case TDH_INTYPE_ANSISTRING: 158 | json_properties[prop_name] = parser.parse(prop_name_wstr); 159 | json_properties_types[prop_name] = "STRINGA"; 160 | break; 161 | case TDH_INTYPE_UNICODESTRING: 162 | json_properties[prop_name] = convert_wstr_str(parser.parse(prop_name_wstr)); 163 | json_properties_types[prop_name] = "STRINGW"; 164 | break; 165 | case TDH_INTYPE_INT8: 166 | json_properties[prop_name] = parser.parse(prop_name_wstr); 167 | json_properties_types[prop_name] = "INT8"; 168 | break; 169 | case TDH_INTYPE_UINT8: 170 | json_properties[prop_name] = parser.parse(prop_name_wstr); 171 | json_properties_types[prop_name] = "UINT8"; 172 | break; 173 | case TDH_INTYPE_INT16: 174 | json_properties[prop_name] = parser.parse(prop_name_wstr); 175 | json_properties_types[prop_name] = "INT16"; 176 | break; 177 | case TDH_INTYPE_UINT16: 178 | json_properties[prop_name] = parser.parse(prop_name_wstr); 179 | json_properties_types[prop_name] = "UINT16"; 180 | break; 181 | case TDH_INTYPE_INT32: 182 | json_properties[prop_name] = parser.parse(prop_name_wstr); 183 | json_properties_types[prop_name] = "INT32"; 184 | break; 185 | case TDH_INTYPE_UINT32: 186 | json_properties[prop_name] = parser.parse(prop_name_wstr); 187 | json_properties_types[prop_name] = "UINT32"; 188 | break; 189 | case TDH_INTYPE_INT64: 190 | json_properties[prop_name] = parser.parse(prop_name_wstr); 191 | json_properties_types[prop_name] = "INT64"; 192 | break; 193 | case TDH_INTYPE_UINT64: 194 | json_properties[prop_name] = parser.parse(prop_name_wstr); 195 | json_properties_types[prop_name] = "UINT64"; 196 | break; 197 | case TDH_INTYPE_FLOAT: 198 | json_properties[prop_name] = parser.parse(prop_name_wstr); 199 | json_properties_types[prop_name] = "FLOAT"; 200 | break; 201 | case TDH_INTYPE_DOUBLE: 202 | json_properties[prop_name] = parser.parse(prop_name_wstr); 203 | json_properties_types[prop_name] = "DOUBLE"; 204 | break; 205 | case TDH_INTYPE_BOOLEAN: 206 | json_properties[prop_name] = convert_bytes_bool(parser.parse(prop_name_wstr).bytes()); 207 | json_properties_types[prop_name] = "BOOLEAN"; 208 | break; 209 | case TDH_INTYPE_BINARY: 210 | json_properties[prop_name] = 211 | convert_bytevector_hexstring(parser.parse(prop_name_wstr).bytes()); 212 | json_properties_types[prop_name] = "BINARY"; 213 | break; 214 | case TDH_INTYPE_GUID: 215 | json_properties[prop_name] = 216 | convert_guid_str(parser.parse(prop_name_wstr)); 217 | json_properties_types[prop_name] = "GUID"; 218 | break; 219 | case TDH_INTYPE_FILETIME: 220 | json_properties[prop_name] = convert_filetime_string( 221 | parser.parse(prop_name_wstr)); 222 | json_properties_types[prop_name] = "FILETIME"; 223 | break; 224 | case TDH_INTYPE_SYSTEMTIME: 225 | json_properties[prop_name] = convert_systemtime_string( 226 | parser.parse(prop_name_wstr)); 227 | json_properties_types[prop_name] = "SYSTEMTIME"; 228 | break; 229 | case TDH_INTYPE_SID: 230 | json_properties[prop_name] = convert_bytes_sidstring( 231 | parser.parse(prop_name_wstr).bytes()); 232 | json_properties_types[prop_name] = "SID"; 233 | break; 234 | case TDH_INTYPE_WBEMSID: 235 | // *Supposedly* like SID? 236 | json_properties[prop_name] = convert_bytevector_hexstring( 237 | parser.parse(prop_name_wstr).bytes()); 238 | json_properties_types[prop_name] = "WBEMSID"; 239 | break; 240 | case TDH_INTYPE_POINTER: 241 | json_properties[prop_name] = 242 | convert_ulong64_hexstring(parser.parse(prop_name_wstr).address); 243 | json_properties_types[prop_name] = "POINTER"; 244 | break; 245 | case TDH_INTYPE_HEXINT32: 246 | case TDH_INTYPE_HEXINT64: 247 | case TDH_INTYPE_MANIFEST_COUNTEDSTRING: 248 | case TDH_INTYPE_MANIFEST_COUNTEDANSISTRING: 249 | case TDH_INTYPE_RESERVED24: 250 | case TDH_INTYPE_MANIFEST_COUNTEDBINARY: 251 | case TDH_INTYPE_COUNTEDSTRING: 252 | case TDH_INTYPE_COUNTEDANSISTRING: 253 | case TDH_INTYPE_REVERSEDCOUNTEDSTRING: 254 | case TDH_INTYPE_REVERSEDCOUNTEDANSISTRING: 255 | case TDH_INTYPE_NONNULLTERMINATEDSTRING: 256 | case TDH_INTYPE_NONNULLTERMINATEDANSISTRING: 257 | case TDH_INTYPE_UNICODECHAR: 258 | case TDH_INTYPE_ANSICHAR: 259 | case TDH_INTYPE_SIZET: 260 | case TDH_INTYPE_HEXDUMP: 261 | case TDH_INTYPE_NULL: 262 | default: 263 | json_properties[prop_name] = 264 | convert_bytevector_hexstring(parser.parse(prop_name_wstr).bytes()); 265 | json_properties_types[prop_name] = "OTHER"; 266 | break; 267 | } 268 | } 269 | catch (...) 270 | { 271 | // Failed to parse, default to hex 272 | // Try hex, if something even worse is up return empty 273 | try 274 | { 275 | json_properties[prop_name] = 276 | convert_bytevector_hexstring(parser.parse(prop_name_wstr).bytes()); 277 | json_properties_types[prop_name] = "ERROR"; 278 | } 279 | catch (...) {} 280 | } 281 | } 282 | json_event["property_types"] = json_properties_types; 283 | json_event["properties"] = json_properties; 284 | } 285 | 286 | // Check if we're meant to parse any extended data 287 | if (record.ExtendedDataCount != 0) { 288 | // At the moment we only support EVENT_HEADER_EXT_TYPE_STACK_TRACE64 289 | // The extra field is TRACE64 (and not TRACE32) even in the event the 290 | // process that generated the event is 32Bit 291 | for (USHORT i = 0; i < record.ExtendedDataCount; i++) 292 | { 293 | EVENT_HEADER_EXTENDED_DATA_ITEM data_item = record.ExtendedData[i]; 294 | 295 | if (data_item.ExtType == EVENT_HEADER_EXT_TYPE_STACK_TRACE64) { 296 | PEVENT_EXTENDED_ITEM_STACK_TRACE64 stacktrace = 297 | (PEVENT_EXTENDED_ITEM_STACK_TRACE64)data_item.DataPtr; 298 | uint32_t stack_length = (data_item.DataSize - sizeof(ULONG64)) / sizeof(ULONG64); 299 | 300 | json json_stacktrace = json::array(); 301 | for (size_t x = 0; x < stack_length; x++) 302 | { 303 | // Stacktraces make more sense in hex 304 | json_stacktrace.push_back(convert_ulong64_hexstring(stacktrace->Address[x])); 305 | } 306 | // We're ignoring the MatchId, which if not 0 then the stack is split across events 307 | // But stiching it together would be too much of a pain for the mostly-stateless 308 | // Sealighter. So we'll just collect what we've got. 309 | json_event["stack_trace"] = json_stacktrace; 310 | } 311 | } 312 | } 313 | 314 | return json_event; 315 | } 316 | 317 | void output_json_event 318 | ( 319 | json json_event 320 | ) 321 | { 322 | // If writing to a file, don't pretty print 323 | // This makes it 1 line per event 324 | bool pretty_print = (Output_format::output_file != g_output_format); 325 | std::string event_string = convert_json_string(json_event, pretty_print); 326 | std::string trace_name = json_event["header"]["trace_name"]; 327 | 328 | // Log event if we successfully parsed it 329 | if (!event_string.empty()) { 330 | switch (g_output_format) 331 | { 332 | case output_stdout: 333 | threaded_print_ln(event_string); 334 | break; 335 | case output_event_log: 336 | write_event_log(json_event, trace_name, event_string); 337 | break; 338 | case output_file: 339 | threaded_write_file_ln(event_string); 340 | break; 341 | } 342 | } 343 | } 344 | 345 | void handle_event_context 346 | ( 347 | const EVENT_RECORD& record, 348 | const trace_context& trace_context, 349 | std::shared_ptr sealighter_context 350 | ) 351 | { 352 | json json_event; 353 | schema schema(record, trace_context.schema_locator); 354 | bool buffered = false; 355 | 356 | std::string trace_name = sealighter_context->trace_name; 357 | json_event = parse_event_to_json(record, trace_context, sealighter_context, schema); 358 | 359 | // Only care about event buffering if required 360 | if (g_buffer_lists.size() > 0 && g_buffer_lists.find(trace_name) != g_buffer_lists.end()) { 361 | // Lock Mutex for safety 362 | g_buffer_lists_mutex.lock(); 363 | 364 | for (event_buffer_list_t& buffer : g_buffer_lists[trace_name]) { 365 | if (buffer.event_id != (uint32_t)schema.event_id()) { 366 | continue; 367 | } 368 | if (buffer.event_count < buffer.max_before_buffering) { 369 | // Increment counter but report event 370 | buffer.event_count += 1; 371 | break; 372 | } 373 | 374 | // We're buffering. See if we already have the matching event 375 | bool matched_event = false; 376 | for (json& json_event_buffered : buffer.json_event_buffered) { 377 | bool matched_field = true; 378 | for (std::string prop_to_compare: buffer.properties_to_compare) { 379 | auto field_event = convert_json_string(json_event["properties"][prop_to_compare], false); 380 | auto field_buffered = convert_json_string(json_event_buffered["properties"][prop_to_compare], false); 381 | if (field_event != field_buffered) { 382 | // Not a match 383 | matched_field = false; 384 | break; 385 | } 386 | } 387 | if (matched_field) { 388 | // Matched, increase event count 389 | auto old_count = json_event_buffered["header"]["buffered_count"].get(); 390 | json_event_buffered["header"]["buffered_count"] = old_count + 1; 391 | matched_event = true; 392 | } 393 | } 394 | if (!matched_event) { 395 | // Event wasn't in the list, add it 396 | json_event["header"]["buffered_count"] = 1; 397 | buffer.json_event_buffered.push_back(json_event); 398 | } 399 | // As we're buffering don't report event 400 | buffered = true; 401 | break; 402 | } 403 | g_buffer_lists_mutex.unlock(); 404 | } 405 | 406 | // Report event only if not buffering 407 | if (!buffered) { 408 | output_json_event(json_event); 409 | } 410 | } 411 | 412 | 413 | 414 | void handle_event 415 | ( 416 | const EVENT_RECORD& record, 417 | const trace_context& trace_context 418 | ) 419 | { 420 | auto dummy_context = std::make_shared("", false); 421 | handle_event_context(record, trace_context, dummy_context); 422 | } 423 | 424 | int setup_logger_file 425 | ( 426 | std::string filename 427 | ) 428 | { 429 | g_outfile.open(filename.c_str(), std::ios::out | std::ios::app); 430 | if (g_outfile.good()) { 431 | return ERROR_SUCCESS; 432 | } 433 | else { 434 | return SEALIGHTER_ERROR_OUTPUT_FILE; 435 | } 436 | } 437 | 438 | void teardown_logger_file() 439 | { 440 | if (g_outfile.is_open()) { 441 | g_outfile.close(); 442 | } 443 | } 444 | 445 | 446 | void set_output_format(Output_format format) 447 | { 448 | g_output_format = format; 449 | } 450 | 451 | 452 | void add_buffered_list 453 | ( 454 | std::string trace_name, 455 | event_buffer_list_t buffered_list 456 | ) 457 | { 458 | if (g_buffer_lists.find(trace_name) == g_buffer_lists.end()) { 459 | g_buffer_lists[trace_name] = std::vector(); 460 | } 461 | g_buffer_lists[trace_name].push_back(buffered_list); 462 | } 463 | 464 | void set_buffer_lists_timeout 465 | ( 466 | uint32_t timeout 467 | ) 468 | { 469 | g_buffer_lists_timeout_seconds = timeout; 470 | } 471 | 472 | void flush_buffered_lists() 473 | { 474 | g_buffer_lists_mutex.lock(); 475 | for (auto& buffer_list : g_buffer_lists) { 476 | for (auto& buffer : buffer_list.second) { 477 | for (auto& json_event : buffer.json_event_buffered) { 478 | output_json_event(json_event); 479 | } 480 | buffer.json_event_buffered.clear(); 481 | buffer.event_count = 0; 482 | } 483 | } 484 | g_buffer_lists_mutex.unlock(); 485 | } 486 | 487 | void bufferring_thread() 488 | { 489 | std::mutex thread_mutex; 490 | std::unique_lock lock(thread_mutex); 491 | auto time_point = std::chrono::system_clock::now() + 492 | std::chrono::seconds(g_buffer_lists_timeout_seconds); 493 | while (!g_buffer_thread_stop) { 494 | while (g_buffer_list_con_var.wait_until(lock, time_point) == std::cv_status::timeout) { 495 | flush_buffered_lists(); 496 | time_point = std::chrono::system_clock::now() + 497 | std::chrono::seconds(g_buffer_lists_timeout_seconds); 498 | } 499 | } 500 | 501 | // Flush one last time before ending 502 | flush_buffered_lists(); 503 | } 504 | 505 | void start_bufferring() 506 | { 507 | // Only start buffer thread if we need to 508 | if (g_buffer_lists.size() != 0 && !g_buffer_thread_stop.load()) { 509 | g_buffer_list_thread = std::thread(bufferring_thread); 510 | } 511 | } 512 | 513 | 514 | void stop_bufferring() 515 | { 516 | if (g_buffer_lists.size() != 0 && !g_buffer_thread_stop.load()) { 517 | g_buffer_thread_stop = true; 518 | g_buffer_list_con_var.notify_one(); 519 | g_buffer_list_thread.join(); 520 | } 521 | } 522 | -------------------------------------------------------------------------------- /sealighter/sealighter_controller.cpp: -------------------------------------------------------------------------------- 1 | #include "sealighter_krabs.h" 2 | #include 3 | #include 4 | #include 5 | #include "sealighter_errors.h" 6 | #include "sealighter_util.h" 7 | #include "sealighter_json.h" 8 | #include "sealighter_predicates.h" 9 | #include "sealighter_handler.h" 10 | #include "sealighter_provider.h" 11 | 12 | // ------------------------- 13 | // GLOBALS - START 14 | // ------------------------- 15 | 16 | // Add aliases to make code cleaner 17 | namespace kpc = krabs::predicates::comparers; 18 | namespace kpa = krabs::predicates::adapters; 19 | 20 | 21 | // Holds a KrabsETW User Session Trace if needed 22 | static user_trace* g_user_session = NULL; 23 | 24 | // Holds a KrabsETW Kernel Session Trace if needed 25 | static kernel_trace* g_kernel_session = NULL; 26 | 27 | // ------------------------- 28 | // GLOBALS - END 29 | // ------------------------- 30 | // PRIVATE FUNCTIONS - START 31 | // ------------------------- 32 | 33 | /* 34 | Adds a single property comparer filter to a list 35 | */ 36 | template < 37 | typename ComparerA, 38 | typename ComparerW 39 | > 40 | void add_filter_to_vector_property_compare_item 41 | ( 42 | json item, 43 | std::vector>& list 44 | ) 45 | { 46 | if (!item["name"].is_null() && !item["value"].is_null() && !item["type"].is_null()) { 47 | std::wstring name = convert_str_wstr(item["name"].get()); 48 | std::string type = item["type"].get(); 49 | if (type == "STRINGA") { 50 | std::string val = item["value"].get(); 51 | auto pred = std::shared_ptr< 52 | predicates::details::property_view_predicate< 53 | std::string, 54 | kpa::generic_string, 55 | ComparerA 56 | > 57 | >(new predicates::details::property_view_predicate< 58 | std::string, 59 | kpa::generic_string, 60 | ComparerA 61 | >( 62 | name, 63 | val, 64 | kpa::generic_string(), 65 | ComparerA() 66 | )); 67 | list.emplace_back(pred); 68 | } 69 | else if (type == "STRINGW") { 70 | std::wstring val = convert_str_wstr(item["value"].get()); 71 | 72 | auto pred = std::shared_ptr< 73 | predicates::details::property_view_predicate< 74 | std::wstring, 75 | kpa::generic_string, 76 | ComparerW 77 | > 78 | >(new predicates::details::property_view_predicate< 79 | std::wstring, 80 | kpa::generic_string, 81 | ComparerW 82 | >( 83 | name, 84 | val, 85 | kpa::generic_string(), 86 | ComparerW() 87 | )); 88 | list.emplace_back(pred); 89 | } 90 | else { 91 | // Raise a parse error, type has to be a string 92 | throw nlohmann::detail::exception( 93 | nlohmann::detail::parse_error::create(0, 0, 94 | "The 'type' of a Property Comparer must be 'STRINGA' or 'STRINGW'")); 95 | } 96 | } 97 | else { 98 | // Raise a parse error, properites *must* have all these fields 99 | throw nlohmann::detail::exception( 100 | nlohmann::detail::parse_error::create( 101 | 0, 0, "Properties must have a 'name', 'type' AND 'value' keys ")); 102 | } 103 | } 104 | 105 | 106 | /* 107 | Adds a property comparer filter to a list 108 | */ 109 | template < 110 | typename ComparerA, 111 | typename ComparerW 112 | > 113 | void add_filter_to_vector_property_compare 114 | ( 115 | json root, 116 | std::string element, 117 | std::vector>& pred_vector 118 | ) 119 | { 120 | std::vector> list; 121 | if (!root[element].is_null()) { 122 | log_messageA(" %s: %s\n", element.c_str(), convert_json_string(root[element], false).c_str()); 123 | if (root[element].is_array()) { 124 | for (json item : root[element]) { 125 | add_filter_to_vector_property_compare_item(item, list); 126 | } 127 | if (!list.empty()) { 128 | pred_vector.emplace_back(std::shared_ptr(new sealighter_any_of(list))); 129 | } 130 | } 131 | else { 132 | add_filter_to_vector_property_compare_item(root[element], pred_vector); 133 | } 134 | } 135 | } 136 | 137 | 138 | /* 139 | Add a single "property is" filter to a list. 140 | */ 141 | void add_filter_to_vector_property_is_item 142 | ( 143 | json item, 144 | std::vector>& list 145 | ) 146 | { 147 | if (!item["name"].is_null() && !item["value"].is_null() && !item["type"].is_null()) { 148 | std::wstring name = convert_str_wstr(item["name"].get()); 149 | std::string type = item["type"].get(); 150 | if (type == "STRINGA") { 151 | auto val = item["value"].get(); 152 | list.emplace_back(std::shared_ptr> 153 | (new sealighter_property_is(name, val))); 154 | } 155 | else if (type == "STRINGW") { 156 | auto val = convert_str_wstr(item["value"].get()); 157 | list.emplace_back(std::shared_ptr> 158 | (new sealighter_property_is(name, val))); 159 | } 160 | else if (type == "INT8") { 161 | auto val = item["value"].get(); 162 | list.emplace_back(std::shared_ptr> 163 | (new sealighter_property_is(name, val))); 164 | } 165 | else if (type == "UINT8") { 166 | auto val = item["value"].get(); 167 | list.emplace_back(std::shared_ptr> 168 | (new sealighter_property_is(name, val))); 169 | } 170 | else if (type == "INT16") { 171 | auto val = item["value"].get(); 172 | list.emplace_back(std::shared_ptr> 173 | (new sealighter_property_is(name, val))); 174 | } 175 | else if (type == "UINT16") { 176 | auto val = item["value"].get(); 177 | list.emplace_back(std::shared_ptr> 178 | (new sealighter_property_is(name, val))); 179 | } 180 | else if (type == "INT32") { 181 | auto val = item["value"].get(); 182 | list.emplace_back(std::shared_ptr> 183 | (new sealighter_property_is(name, val))); 184 | } 185 | else if (type == "UINT32") { 186 | auto val = item["value"].get(); 187 | list.emplace_back(std::shared_ptr> 188 | (new sealighter_property_is(name, val))); 189 | } 190 | else if (type == "INT64") { 191 | auto val = item["value"].get(); 192 | list.emplace_back(std::shared_ptr> 193 | (new sealighter_property_is(name, val))); 194 | } 195 | else if (type == "UINT64") { 196 | auto val = item["value"].get(); 197 | list.emplace_back(std::shared_ptr> 198 | (new sealighter_property_is(name, val))); 199 | } 200 | } 201 | else { 202 | // Raise an parse error, properites *must* have all these fields 203 | throw nlohmann::detail::exception( 204 | nlohmann::detail::parse_error::create 205 | (0, 0, "Properties must have a 'name', 'type' AND 'value' keys ")); 206 | } 207 | } 208 | 209 | 210 | /* 211 | Add the "Property Is" filter to a list, 212 | if availible. This is a different type of predicate 213 | to both the basic predicates and the other property comparer ones 214 | */ 215 | void add_filter_to_vector_property_is 216 | ( 217 | json root, 218 | std::vector>& pred_vector 219 | ) 220 | { 221 | std::vector> list; 222 | if (!root.is_null()) { 223 | log_messageA(" Property Is: %s\n", convert_json_string(root, false).c_str()); 224 | if (root.is_array()) { 225 | for (json item : root) { 226 | add_filter_to_vector_property_is_item(item, list); 227 | } 228 | if (!list.empty()) { 229 | pred_vector.emplace_back(std::shared_ptr(new sealighter_any_of(list))); 230 | } 231 | } 232 | else { 233 | add_filter_to_vector_property_is_item(root, pred_vector); 234 | } 235 | } 236 | }; 237 | 238 | /* 239 | Adds a basic filter with two values 240 | */ 241 | template < 242 | typename TPred, 243 | typename TJson1=std::uint64_t, 244 | typename TJson2=std::uint64_t 245 | > 246 | void add_filter_to_vector_basic_pair 247 | ( 248 | json root, 249 | std::string element, 250 | std::string item1_name, 251 | std::string item2_name, 252 | std::vector>& pred_vector 253 | ) 254 | { 255 | std::vector> list; 256 | if (!root[element].is_null()) { 257 | log_messageA(" %s: %s\n", element.c_str(), convert_json_string(root[element], false).c_str()); 258 | if (root[element].is_array()) { 259 | for (json item : root[element]) { 260 | if (!item[item1_name].is_null() && !item[item2_name].is_null()) { 261 | TJson1 item1 = item[item1_name].get(); 262 | TJson2 item2 = item[item2_name].get(); 263 | list.emplace_back(std::shared_ptr(new TPred(item1, item2))); 264 | } 265 | } 266 | if (!list.empty()) { 267 | pred_vector.emplace_back(std::shared_ptr(new sealighter_any_of(list))); 268 | } 269 | } 270 | else { 271 | if (!root[element][item1_name].is_null() && !root[element][item2_name].is_null()) { 272 | TJson1 item1 = root[element][item1_name].get(); 273 | TJson2 item2 = root[element][item2_name].get(); 274 | pred_vector.emplace_back(std::shared_ptr(new TPred(item1, item2))); 275 | } 276 | } 277 | } 278 | } 279 | 280 | 281 | /* 282 | Parse JSON to create a KrabsETW filter and add it to a list 283 | JSON can be a single item, or an array of items that we will 'OR' 284 | */ 285 | template < 286 | typename TPred, 287 | typename TJson1=std::uint64_t 288 | > 289 | void add_filter_to_vector_basic 290 | ( 291 | json root, 292 | std::string element, 293 | std::vector>& pred_vector 294 | ) 295 | { 296 | std::vector> list; 297 | if (!root[element].is_null()) { 298 | log_messageA(" %s: %s\n", element.c_str(), convert_json_string(root[element], false).c_str()); 299 | // If a list, filter can be any of them 300 | if (root[element].is_array()) { 301 | for (json item : root[element]) { 302 | list.emplace_back(std::shared_ptr(new TPred(item.get()))); 303 | } 304 | if (!list.empty()) { 305 | pred_vector.emplace_back(std::shared_ptr(new sealighter_any_of(list))); 306 | } 307 | } 308 | else { 309 | pred_vector.emplace_back(std::shared_ptr 310 | (new TPred(root[element].get()))); 311 | } 312 | } 313 | } 314 | 315 | 316 | /* 317 | Parse JSON to add filters to a vector list 318 | */ 319 | int add_filters_to_vector 320 | ( 321 | std::vector>& pred_vector, 322 | json json_list 323 | ) 324 | { 325 | int status = ERROR_SUCCESS; 326 | try { 327 | // Add the basic single-value filters 328 | add_filter_to_vector_basic 329 | (json_list, "event_id_is", pred_vector); 330 | add_filter_to_vector_basic 331 | (json_list, "opcode_is", pred_vector); 332 | add_filter_to_vector_basic 333 | (json_list, "process_id_is", pred_vector); 334 | add_filter_to_vector_basic 335 | (json_list, "version_is", pred_vector); 336 | 337 | // Add all the property filters 338 | add_filter_to_vector_property_is(json_list["property_is"], pred_vector); 339 | 340 | add_filter_to_vector_property_compare< 341 | kpc::equals::value_type>>, 342 | kpc::equals::value_type>> 343 | >(json_list, "property_equals", pred_vector); 344 | 345 | add_filter_to_vector_property_compare< 346 | kpc::equals::value_type>>, 347 | kpc::equals::value_type>> 348 | >(json_list, "property_iequals", pred_vector); 349 | 350 | add_filter_to_vector_property_compare< 351 | kpc::contains::value_type>>, 352 | kpc::contains::value_type>> 353 | >(json_list, "property_contains", pred_vector); 354 | 355 | add_filter_to_vector_property_compare< 356 | kpc::contains::value_type>>, 357 | kpc::contains::value_type>> 358 | >(json_list, "property_icontains", pred_vector); 359 | 360 | add_filter_to_vector_property_compare< 361 | kpc::starts_with::value_type>>, 362 | kpc::starts_with::value_type>> 363 | >(json_list, "property_starts_with", pred_vector); 364 | 365 | add_filter_to_vector_property_compare< 366 | kpc::starts_with::value_type>>, 367 | kpc::starts_with::value_type>> 368 | >(json_list, "property_istarts_with", pred_vector); 369 | 370 | add_filter_to_vector_property_compare< 371 | kpc::ends_with::value_type>>, 372 | kpc::ends_with::value_type>> 373 | >(json_list, "property_ends_with", pred_vector); 374 | 375 | add_filter_to_vector_property_compare< 376 | kpc::ends_with::value_type>>, 377 | kpc::ends_with::value_type>> 378 | >(json_list, "property_iends_with", pred_vector); 379 | 380 | // Add own own created Predicates 381 | add_filter_to_vector_basic 382 | (json_list, "max_events_total", pred_vector); 383 | add_filter_to_vector_basic_pair 384 | (json_list, "max_events_id", "id_is", "max_events", pred_vector); 385 | add_filter_to_vector_basic 386 | (json_list, "any_field_contains", pred_vector); 387 | add_filter_to_vector_basic 388 | (json_list, "process_name_contains", pred_vector); 389 | add_filter_to_vector_basic 390 | (json_list, "activity_id_is", pred_vector); 391 | } 392 | catch (const nlohmann::detail::exception& e) { 393 | log_messageA("failed to add filters from config to provider\n"); 394 | log_messageA("%s\n", e.what()); 395 | status = SEALIGHTER_ERROR_PARSE_FILTER; 396 | } 397 | return status; 398 | } 399 | 400 | 401 | /* 402 | Add Krabs filters to an ETW provider 403 | */ 404 | template 405 | int add_filters 406 | ( 407 | details::base_provider* pNew_provider, 408 | std::shared_ptr sealighter_context, 409 | json json_provider 410 | ) 411 | { 412 | int status = ERROR_SUCCESS; 413 | 414 | if (json_provider["filters"].is_null() || 415 | (json_provider["filters"]["any_of"].is_null() && 416 | json_provider["filters"]["all_of"].is_null() && 417 | json_provider["filters"]["none_of"].is_null() 418 | ) 419 | ) { 420 | // No filters, log everything 421 | log_messageA(" No event filters\n"); 422 | pNew_provider->add_on_event_callback([sealighter_context](const EVENT_RECORD& record, const krabs::trace_context& trace_context) { 423 | handle_event_context(record, trace_context, sealighter_context); 424 | }); 425 | } 426 | else { 427 | // Build top-level list 428 | // All 3 options will eventually be ANDed together 429 | std::vector> top_list; 430 | if (!json_provider["filters"]["any_of"].is_null()) { 431 | log_messageA(" Filtering any of:\n"); 432 | std::vector> list; 433 | status = add_filters_to_vector(list, json_provider["filters"]["any_of"]); 434 | if (ERROR_SUCCESS == status) { 435 | top_list.emplace_back(std::shared_ptr(new sealighter_any_of(list))); 436 | } 437 | } 438 | if (ERROR_SUCCESS == status && !json_provider["filters"]["all_of"].is_null()) { 439 | log_messageA(" Filtering all of:\n"); 440 | std::vector> list; 441 | status = add_filters_to_vector(list, json_provider["filters"]["all_of"]); 442 | if (ERROR_SUCCESS == status) { 443 | top_list.emplace_back(std::shared_ptr(new sealighter_all_of(list))); 444 | } 445 | } 446 | if (ERROR_SUCCESS == status && !json_provider["filters"]["none_of"].is_null()) { 447 | log_messageA(" Filtering none of:\n"); 448 | std::vector> list; 449 | status = add_filters_to_vector(list, json_provider["filters"]["none_of"]); 450 | if (ERROR_SUCCESS == status) { 451 | top_list.emplace_back(std::shared_ptr(new sealighter_none_of(list))); 452 | } 453 | } 454 | 455 | // Add top level list to a filter 456 | if (ERROR_SUCCESS == status) { 457 | sealighter_all_of top_pred = sealighter_all_of(top_list); 458 | event_filter filter(top_pred); 459 | 460 | filter.add_on_event_callback([sealighter_context](const EVENT_RECORD& record, const krabs::trace_context& trace_context) { 461 | handle_event_context(record, trace_context, sealighter_context); 462 | }); 463 | pNew_provider->add_filter(filter); 464 | } 465 | } 466 | 467 | return status; 468 | } 469 | 470 | 471 | /* 472 | Add Kernel Providers and Create Kernel ETW Session 473 | */ 474 | int add_kernel_traces 475 | ( 476 | json json_config, 477 | EVENT_TRACE_PROPERTIES session_properties 478 | ) 479 | { 480 | int status = ERROR_SUCCESS; 481 | kernel_provider* pNew_provider; 482 | std::string provider_name; 483 | 484 | // Initialize Session and props 485 | g_kernel_session = new kernel_trace(); 486 | g_kernel_session->set_trace_properties(&session_properties); 487 | 488 | // Add any Kernel providers 489 | try { 490 | for (json json_provider : json_config["kernel_traces"]) { 491 | if (json_provider["provider_name"].is_null()) { 492 | log_messageA("Invalid Provider, missing provider name\n"); 493 | status = SEALIGHTER_ERROR_PARSE_KERNEL_PROVIDER; 494 | break; 495 | } 496 | provider_name = json_provider["provider_name"].get(); 497 | 498 | if (provider_name == "process") { 499 | pNew_provider = new kernel::process_provider(); 500 | } 501 | else if (provider_name == "thread") { 502 | pNew_provider = new kernel::thread_provider(); 503 | } 504 | else if (provider_name == "image_load") { 505 | pNew_provider = new kernel::image_load_provider(); 506 | } 507 | else if (provider_name == "process_counter") { 508 | pNew_provider = new kernel::process_counter_provider(); 509 | } 510 | else if (provider_name == "context_switch") { 511 | pNew_provider = new kernel::context_switch_provider(); 512 | } 513 | else if (provider_name == "dpc") { 514 | pNew_provider = new kernel::dpc_provider(); 515 | } 516 | else if (provider_name == "debug_print") { 517 | pNew_provider = new kernel::debug_print_provider(); 518 | } 519 | else if (provider_name == "interrupt") { 520 | pNew_provider = new kernel::interrupt_provider(); 521 | } 522 | else if (provider_name == "system_call") { 523 | pNew_provider = new kernel::system_call_provider(); 524 | } 525 | else if (provider_name == "disk_io") { 526 | pNew_provider = new kernel::disk_io_provider(); 527 | } 528 | else if (provider_name == "disk_file_io") { 529 | pNew_provider = new kernel::disk_file_io_provider(); 530 | } 531 | else if (provider_name == "disk_init_io") { 532 | pNew_provider = new kernel::disk_init_io_provider(); 533 | } 534 | else if (provider_name == "thread_dispatch") { 535 | pNew_provider = new kernel::thread_dispatch_provider(); 536 | } 537 | else if (provider_name == "memory_page_fault") { 538 | pNew_provider = new kernel::memory_page_fault_provider(); 539 | } 540 | else if (provider_name == "memory_hard_fault") { 541 | pNew_provider = new kernel::memory_hard_fault_provider(); 542 | } 543 | else if (provider_name == "virtual_alloc") { 544 | pNew_provider = new kernel::virtual_alloc_provider(); 545 | } 546 | else if (provider_name == "network_tcpip") { 547 | pNew_provider = new kernel::network_tcpip_provider(); 548 | } 549 | else if (provider_name == "registry") { 550 | pNew_provider = new kernel::registry_provider(); 551 | } 552 | else if (provider_name == "alpc") { 553 | pNew_provider = new kernel::alpc_provider(); 554 | } 555 | else if (provider_name == "split_io") { 556 | pNew_provider = new kernel::split_io_provider(); 557 | } 558 | else if (provider_name == "driver") { 559 | pNew_provider = new kernel::driver_provider(); 560 | } 561 | else if (provider_name == "profile") { 562 | pNew_provider = new kernel::profile_provider(); 563 | } 564 | else if (provider_name == "file_io") { 565 | pNew_provider = new kernel::file_io_provider(); 566 | } 567 | else if (provider_name == "file_init_io") { 568 | pNew_provider = new kernel::file_init_io_provider(); 569 | } 570 | else if (provider_name == "vamap") { 571 | pNew_provider = new kernel::vamap_provider(); 572 | } 573 | else if (provider_name == "object_manager") { 574 | pNew_provider = new kernel::object_manager_provider(); 575 | } 576 | else { 577 | log_messageA("Invalid Provider: %s\n", provider_name.c_str()); 578 | status = SEALIGHTER_ERROR_PARSE_KERNEL_PROVIDER; 579 | break; 580 | } 581 | 582 | // Create context with trace name 583 | if (json_provider["trace_name"].is_null()) { 584 | log_messageA("Invalid Provider, missing trace name\n"); 585 | status = SEALIGHTER_ERROR_PARSE_KERNEL_PROVIDER; 586 | break; 587 | } 588 | 589 | std::string trace_name = json_provider["trace_name"].get(); 590 | auto sealighter_context = 591 | std::shared_ptr(new sealighter_context_t(trace_name, false)); 592 | for (json json_buffers : json_provider["buffers"]) { 593 | auto event_id = json_buffers["event_id"].get(); 594 | auto max = json_buffers["max_before_buffering"].get(); 595 | auto buffer_list = event_buffer_list_t(event_id, max); 596 | for (json json_buff_prop: json_buffers["properties_to_match"]) { 597 | buffer_list.properties_to_compare.push_back(json_buff_prop.get()); 598 | } 599 | 600 | add_buffered_list(trace_name, buffer_list); 601 | } 602 | 603 | 604 | // Add any filters 605 | log_messageA("Kernel Provider: %s\n", provider_name.c_str()); 606 | status = add_filters(pNew_provider, sealighter_context, json_provider); 607 | if (ERROR_SUCCESS == status) { 608 | g_kernel_session->enable(*pNew_provider); 609 | } 610 | else { 611 | log_messageA("Failed to add filters to: %s\n", provider_name.c_str()); 612 | break; 613 | } 614 | } 615 | } 616 | catch (const nlohmann::detail::exception & e) { 617 | log_messageA("invalid kernel provider in config file\n"); 618 | log_messageA("%s\n", e.what()); 619 | status = SEALIGHTER_ERROR_PARSE_KERNEL_PROVIDER; 620 | } 621 | return status; 622 | } 623 | 624 | /* 625 | Add User providers and create User ETW Session 626 | */ 627 | int add_user_traces 628 | ( 629 | json json_config, 630 | EVENT_TRACE_PROPERTIES session_properties, 631 | std::wstring session_name 632 | ) 633 | { 634 | int status = ERROR_SUCCESS; 635 | // Initialize Session and props 636 | g_user_session = new user_trace(session_name); 637 | g_user_session->set_trace_properties(&session_properties); 638 | try { 639 | // Parse the Usermode Providers 640 | for (json json_provider : json_config["user_traces"]) { 641 | GUID provider_guid; 642 | provider<>* pNew_provider; 643 | std::wstring provider_name; 644 | std::string trace_name; 645 | if (json_provider["provider_name"].is_null()) { 646 | log_messageA("Invalid Provider\n"); 647 | status = SEALIGHTER_ERROR_PARSE_USER_PROVIDER; 648 | break; 649 | } 650 | 651 | if (json_provider["trace_name"].is_null()) { 652 | log_messageA("Invalid Provider, missing trace name\n"); 653 | status = SEALIGHTER_ERROR_PARSE_KERNEL_PROVIDER; 654 | break; 655 | } 656 | trace_name = json_provider["trace_name"].get(); 657 | 658 | // If provider_name is a GUID, use that 659 | // Otherwise pass it off to Krabs to try to resolve 660 | provider_name = convert_str_wstr(json_provider["provider_name"].get()); 661 | provider_guid = convert_wstr_guid(provider_name); 662 | 663 | if (provider_guid != GUID_NULL) { 664 | pNew_provider = new provider<>(provider_guid); 665 | } 666 | else { 667 | try { 668 | pNew_provider = new provider<>(provider_name); 669 | } 670 | catch (const std::exception & e) { 671 | log_messageA("%s\n", e.what()); 672 | status = SEALIGHTER_ERROR_NO_PROVIDER; 673 | break; 674 | } 675 | } 676 | log_messageW(L"User Provider: %s\n", provider_name.c_str()); 677 | log_messageA(" Trace Name: %s\n", trace_name.c_str()); 678 | 679 | // If no keywords_all or keywords_any is set 680 | // then set a default 'match anything' 681 | if (json_provider["keywords_all"].is_null() && json_provider["keywords_any"].is_null()) { 682 | log_messageA(" Keywords: All\n"); 683 | } 684 | else { 685 | if (!json_provider["keywords_all"].is_null()) { 686 | uint64_t data = json_provider["keywords_all"].get(); 687 | log_messageA(" Keywords All: 0x%llx\n", data); 688 | pNew_provider->all(data); 689 | } 690 | 691 | if (!json_provider["keywords_any"].is_null()) { 692 | uint64_t data = json_provider["keywords_any"].get(); 693 | log_messageA(" Keywords Any: 0x%llx\n", data); 694 | pNew_provider->any(data); 695 | } 696 | } 697 | if (!json_provider["level"].is_null()) { 698 | uint64_t data = json_provider["level"].get(); 699 | log_messageA(" Level: 0x%llx\n", data); 700 | pNew_provider->level(data); 701 | } 702 | else { 703 | // Set Max Level 704 | pNew_provider->level(0xff); 705 | } 706 | 707 | if (!json_provider["trace_flags"].is_null()) { 708 | uint64_t data = json_provider["trace_flags"].get(); 709 | log_messageA(" Trace Flags: 0x%llx\n", data); 710 | pNew_provider->trace_flags(data); 711 | } 712 | 713 | // Check if we want a stacktrace 714 | // This is just a helper option, you could also set this in the trace_flags 715 | if (!json_provider["report_stacktrace"].is_null() && json_provider["report_stacktrace"].get()) { 716 | // Add the stacktrace trace flag 717 | pNew_provider->trace_flags(pNew_provider->trace_flags() | EVENT_ENABLE_PROPERTY_STACK_TRACE); 718 | } 719 | 720 | // Create context with trace name 721 | 722 | // Check if we're dumping the raw event, or attempting to parse it 723 | bool dump_raw_event = false; 724 | if (!json_provider["dump_raw_event"].is_null()) { 725 | dump_raw_event = json_provider["dump_raw_event"].get(); 726 | if (dump_raw_event) { 727 | log_messageA(" Recording raw events\n"); 728 | } 729 | } 730 | 731 | auto sealighter_context = 732 | std::shared_ptr(new sealighter_context_t(trace_name, dump_raw_event)); 733 | for (json json_buffers : json_provider["buffers"]) { 734 | auto event_id = json_buffers["event_id"].get(); 735 | auto max = json_buffers["max_before_buffering"].get(); 736 | auto buffer_list = event_buffer_list_t(event_id, max); 737 | for (json json_buff_prop : json_buffers["properties_to_match"]) { 738 | buffer_list.properties_to_compare.push_back(json_buff_prop.get()); 739 | } 740 | 741 | add_buffered_list(trace_name, buffer_list); 742 | } 743 | 744 | // Add any filters 745 | status = add_filters(pNew_provider, sealighter_context, json_provider); 746 | if (ERROR_SUCCESS == status) { 747 | g_user_session->enable(*pNew_provider); 748 | } 749 | else { 750 | log_messageW(L"Failed to add filters to: %s\n", provider_name.c_str()); 751 | break; 752 | } 753 | } 754 | } 755 | catch (const nlohmann::detail::exception & e) { 756 | log_messageA("invalid providers in config file\n"); 757 | log_messageA("%s\n", e.what()); 758 | status = SEALIGHTER_ERROR_PARSE_USER_PROVIDER; 759 | } 760 | 761 | // If everything is good, also add a default handler 762 | if (ERROR_SUCCESS == status) { 763 | g_user_session->set_default_event_callback(handle_event); 764 | } 765 | 766 | return status; 767 | } 768 | 769 | 770 | /* 771 | Parse the Config file, setup 772 | ETW Session and Krabs filters 773 | */ 774 | int parse_config 775 | ( 776 | std::string config_string 777 | ) 778 | { 779 | int status = ERROR_SUCCESS; 780 | EVENT_TRACE_PROPERTIES session_properties = { 0 }; 781 | std::wstring session_name = L"Sealighter-Trace"; 782 | json json_config; 783 | 784 | try { 785 | // Read in config file 786 | json_config = json::parse(config_string); 787 | 788 | // Set defaults 789 | session_properties.BufferSize = 256; 790 | session_properties.MinimumBuffers = 12; 791 | session_properties.MaximumBuffers = 48; 792 | session_properties.FlushTimer = 1; 793 | session_properties.LogFileMode = EVENT_TRACE_REAL_TIME_MODE | EVENT_TRACE_INDEPENDENT_SESSION_MODE; 794 | 795 | // Parse the config json for any custom properties 796 | json json_props = json_config["session_properties"]; 797 | if (!json_props.is_null()) 798 | { 799 | if (!json_props["session_name"].is_null()) { 800 | session_name = convert_str_wstr(json_props["session_name"].get()); 801 | log_messageW(L"Session Name: %s\n", session_name.c_str()); 802 | } 803 | 804 | if (!json_props["buffer_size"].is_null()) { 805 | session_properties.BufferSize = json_props["buffer_size"].get(); 806 | } 807 | 808 | if (!json_props["minimum_buffers"].is_null()) { 809 | session_properties.MinimumBuffers = 810 | json_props["minimum_buffers"].get(); 811 | } 812 | 813 | if (!json_props["maximum_buffers"].is_null()) { 814 | session_properties.MaximumBuffers = 815 | json_props["maximum_buffers"].get(); 816 | } 817 | 818 | if (!json_props["flush_timer"].is_null()) { 819 | session_properties.FlushTimer = 820 | json_props["flush_timer"].get(); 821 | } 822 | 823 | if (!json_props["output_format"].is_null()) { 824 | std::string format = json_props["output_format"].get(); 825 | if ("stdout" == format) { 826 | set_output_format(Output_format::output_stdout); 827 | } 828 | else if ("event_log" == format) { 829 | set_output_format(Output_format::output_event_log); 830 | } 831 | else if ("file" == format) { 832 | if (json_props["output_filename"].is_null()) { 833 | log_messageA("When output_format == 'file', also set 'output_filename'\n"); 834 | status = SEALIGHTER_ERROR_OUTPUT_FILE; 835 | } 836 | else { 837 | //g_output_format = Output_format::output_file; 838 | set_output_format(Output_format::output_file); 839 | status = setup_logger_file(json_props["output_filename"].get()); 840 | } 841 | } 842 | else { 843 | log_messageA("Invalid output_format\n"); 844 | status = SEALIGHTER_ERROR_OUTPUT_FORMAT; 845 | } 846 | log_messageA("Outputs: %s\n", format.c_str()); 847 | } 848 | 849 | if (!json_props["buffering_timout_seconds"].is_null()) { 850 | auto timeout = json_props["buffering_timout_seconds"].get(); 851 | set_buffer_lists_timeout(timeout); 852 | } 853 | } 854 | } 855 | catch (const nlohmann::detail::exception& e) { 856 | log_messageA("invalid session properties in config file\n"); 857 | log_messageA("%s\n", e.what()); 858 | status = SEALIGHTER_ERROR_PARSE_CONFIG_PROPS; 859 | } 860 | 861 | if (ERROR_SUCCESS == status) { 862 | if (json_config["user_traces"].is_null() && json_config["kernel_traces"].is_null()) { 863 | log_messageA("No User or Kernel providers in config file\n"); 864 | status = SEALIGHTER_ERROR_PARSE_NO_PROVIDERS; 865 | } 866 | else { 867 | if (!json_config["user_traces"].is_null()) { 868 | status = add_user_traces(json_config, session_properties, session_name); 869 | } 870 | 871 | // Add kernel providers if needed 872 | if (ERROR_SUCCESS == status && !json_config["kernel_traces"].is_null()) { 873 | status = add_kernel_traces(json_config, session_properties); 874 | } 875 | } 876 | } 877 | 878 | return status; 879 | } 880 | 881 | 882 | /* 883 | Run a trace, and ensure we stop if something goes wrong 884 | */ 885 | template 886 | void run_trace(trace* trace) 887 | { 888 | if (NULL != trace) { 889 | // Ensure we always stop the trace afterwards 890 | try { 891 | trace->start(); 892 | } 893 | catch (const std::exception & e) { 894 | log_messageA("%s\n", e.what()); 895 | trace->stop(); 896 | throw; 897 | } 898 | catch (...) { 899 | trace->stop(); 900 | throw; 901 | } 902 | } 903 | } 904 | 905 | 906 | /* 907 | Stop any running trace 908 | */ 909 | void stop_sealighter() 910 | { 911 | if (NULL != g_user_session) { 912 | g_user_session->stop(); 913 | } 914 | if (NULL != g_kernel_session) { 915 | g_kernel_session->stop(); 916 | } 917 | } 918 | 919 | 920 | /* 921 | Handler for Ctrl+C cancel events. 922 | Makes sure we stop our ETW Session when shutting down 923 | */ 924 | BOOL WINAPI crl_c_handler 925 | ( 926 | DWORD fdwCtrlType 927 | ) 928 | { 929 | switch (fdwCtrlType) 930 | { 931 | case CTRL_C_EVENT: 932 | stop_sealighter(); 933 | return TRUE; 934 | } 935 | return FALSE; 936 | } 937 | 938 | 939 | // ------------------------- 940 | // PRIVATE FUNCTIONS - END 941 | // ------------------------- 942 | // PUBLIC FUNCTIONS - START 943 | // ------------------------- 944 | int run_sealighter 945 | ( 946 | std::string config_string 947 | ) 948 | { 949 | int status = ERROR_SUCCESS; 950 | 951 | // Setup Event Logging 952 | status = EventRegisterSealighter(); 953 | if (ERROR_SUCCESS != status) { 954 | log_messageA("Error registering event log: %ul\n", status); 955 | return SEALIGHTER_ERROR_EVENTLOG_REGISTER; 956 | } 957 | 958 | // Add ctrl+C handler to make sure we stop the trace 959 | if (!SetConsoleCtrlHandler(crl_c_handler, TRUE)) { 960 | log_messageA("failed to set ctrl-c handler\n"); 961 | return SEALIGHTER_ERROR_CTRL_C_REGISTER; 962 | } 963 | 964 | // Parse config file 965 | status = parse_config(config_string); 966 | if (ERROR_SUCCESS != status) { 967 | return status; 968 | } 969 | if (NULL == g_user_session && NULL == g_kernel_session) { 970 | log_messageA("Failed to define any ETW Session\n"); 971 | return SEALIGHTER_ERROR_NO_SESSION_CREATED; 972 | } 973 | else { 974 | // Setup Buffering thread if needed 975 | start_bufferring(); 976 | 977 | // Start Trace we've configured 978 | // Don't run multithreaded if we don't have to 979 | if (NULL != g_user_session && NULL == g_kernel_session) { 980 | log_messageA("Starting User Trace...\n"); 981 | log_messageA("-----------------------------------------\n"); 982 | run_trace(g_user_session); 983 | } 984 | else if (NULL == g_user_session && NULL != g_kernel_session) { 985 | log_messageA("Starting Kernel Trace...\n"); 986 | log_messageA("-----------------------------------------\n"); 987 | run_trace(g_kernel_session); 988 | } 989 | else { 990 | // Have to multi-thread it 991 | log_messageA("Starting User and Kernel Traces...\n"); 992 | log_messageA("-----------------------------------------\n"); 993 | std::thread user_thread = std::thread(run_trace, g_user_session); 994 | std::thread kernel_thread = std::thread(run_trace, g_kernel_session); 995 | 996 | // Call join, blocking until both have shut down 997 | user_thread.join(); 998 | kernel_thread.join(); 999 | } 1000 | 1001 | // Teardown and cleanup 1002 | stop_bufferring(); 1003 | teardown_logger_file(); 1004 | (void)EventUnregisterSealighter(); 1005 | } 1006 | 1007 | return status; 1008 | } 1009 | --------------------------------------------------------------------------------