feature: defender antimalwareengine events

This commit is contained in:
Dobin Rutishauser
2026-01-30 14:33:44 +01:00
parent 54c98e8119
commit 055d74c22d
12 changed files with 187 additions and 126 deletions
+5 -4
View File
@@ -64,7 +64,8 @@ int main(int argc, char* argv[]) {
("k,hook", "Input: Kernel and ntdll hooks", cxxopts::value<bool>()->default_value("false"))
// Input options
("with-unfiltered-etw", "Input option: Enable unfiltered ETW (performance impact)", cxxopts::value<bool>()->default_value("false"))
("with-defendertrace", "Input option Defender: Add MsMpEng.exe access events to target process", cxxopts::value<bool>()->default_value("false"))
("with-antimalwareengine", "Input option Defender: Grab events of ETW Microsoft-Antimalware-Engine related to target process", cxxopts::value<bool>()->default_value("false"))
// Output
("w,web", "Output: Web server", cxxopts::value<bool>()->default_value("true"))
@@ -123,11 +124,11 @@ int main(int argc, char* argv[]) {
g_Config.debug_dllreader = result["dllreader"].as<bool>();
g_Config.hide_full_output = ! result["show"].as<bool>();
g_Config.web_output = result["web"].as<bool>();
g_Config.disable_unfiltered_etw = ! result["with-unfiltered-etw"].as<bool>();
//g_Config.do_dllinjection_ucallstack = result["dllcallstack"].as<bool>();
g_Config.do_defendertrace = result["with-defendertrace"].as<bool>();
g_Config.do_antimalwareengine = result["with-antimalwareengine"].as<bool>();
if (!g_Config.do_etw && !g_Config.do_hook && !g_Config.do_etwti && !g_Config.debug_dllreader) {
printf("Choose at least one of --etw --etwti --hook");
printf("Choose at least one of --etw / --etwti / --hook");
return 1;
}
+4 -1
View File
@@ -21,12 +21,15 @@ public:
bool do_udllinjection = false;
bool debug_dllreader = false;
bool enable_remote_exec = true;
bool disable_unfiltered_etw = false;
// Input selection
bool do_etw = false;
bool do_etwti = false;
bool do_hook = false;
// More input
bool do_defendertrace = false;
bool do_antimalwareengine = false;
bool do_dllinjection_ucallstack = true;
// ETW input selection
+73 -28
View File
@@ -10,24 +10,27 @@
#include "etwreader.h"
#include "process_resolver.h"
#include "config.h"
#include "utils.h"
krabs::user_trace trace_user(L"RedEdrUser");
BOOL use_additional_etw = FALSE;
BOOL is_trace_in_progress = FALSE; // currently unused
HANDLE threadReadynessEtw = NULL; // ready to start tracing
void enable_additional_etw(BOOL use) {
use_additional_etw = use;
void trace_in_progress(BOOL use) {
is_trace_in_progress = use;
}
void event_callback(const EVENT_RECORD& record, const krabs::trace_context& trace_context) {
// Handle ETW events for process monitoring
// - Where ProcessId of EventHeader is our target process
void event_callback_process(const EVENT_RECORD& record, const krabs::trace_context& trace_context) {
try {
krabs::schema schema(record, trace_context.schema_locator);
// This function(-chain) should be high performance, or we lose events.
// Check if we observe the process which emitted this event
DWORD processId = record.EventHeader.ProcessId;
Process* process = g_ProcessResolver.getObject(processId);
if (process == NULL) {
@@ -37,9 +40,12 @@ void event_callback(const EVENT_RECORD& record, const krabs::trace_context& trac
if (!process->observe) {
return;
}
// Convert ETW to JSON
nlohmann::json j = KrabsEtwEventToJsonStr(record, schema);
j["process_name"] = process->name;
j["pid"] = processId;
j["etw_process"] = process->name;
// Emit event
g_EventAggregator.NewEvent(j.dump());
}
catch (const std::exception& e) {
@@ -51,29 +57,68 @@ void event_callback(const EVENT_RECORD& record, const krabs::trace_context& trac
}
void event_callback_nofilter(const EVENT_RECORD& record, const krabs::trace_context& trace_context) {
if (!use_additional_etw) {
// If we dont use additional ETW, we dont want to process these events
return;
}
// Handle ETW events for antimalware monitoring
// - Where "pid" or "filename" or "name" matches our target processes
void event_callback_antimalware(const EVENT_RECORD& record, const krabs::trace_context& trace_context) {
//if (!is_trace_in_progress) {
// return;
//}
try {
krabs::schema schema(record, trace_context.schema_locator);
// Convert ETW to JSON
nlohmann::json j = KrabsEtwEventToJsonStr(record, schema);
// This function(-chain) should be high performance, or we lose events.
// Resolve (source) process name
DWORD processId = record.EventHeader.ProcessId;
Process* process = g_ProcessResolver.getObject(processId);
if (process == NULL) {
LOG_A(LOG_WARNING, "ETW: No process object for pid %lu", processId);
return;
}
j["etw_process"] = process->name;
// This will get information about the process, which may be slow, if not
// done before. It can be done before, e.g. when Kernel event arrived
nlohmann::json j = KrabsEtwEventToJsonStr(record, schema);
j["process_name"] = process->name;
// Check if destination is one of our target processes
if (j.contains("pid") && !j["pid"].is_null()) {
DWORD targetPid = j["pid"].get<DWORD>();
// check if we observe the target process
Process* targetProcess = g_ProcessResolver.getObject(targetPid);
if (targetProcess == NULL) {
LOG_A(LOG_WARNING, "ETW: No target process object for pid %lu", targetPid);
return;
}
if (targetProcess->observe) {
// Emit event
g_EventAggregator.NewEvent(j.dump());
}
}
// Check if filename matches any of our target processes
// Cache MOACLookup 36
else if (j.contains("filename") && !j["filename"].is_null()) {
std::string filename = j["filename"].get<std::string>();
for (const auto& targetProcessName : g_Config.targetProcessNames) {
if (ends_with_case_insensitive(filename, targetProcessName)) {
// Emit event
g_EventAggregator.NewEvent(j.dump());
break;
}
}
}
// Check if name matches any of our target processes
// ExpensiveOperationTaskExpensiveOperationBegin 43
// ExpensiveOperationTaskExpensiveOperationEnd 67
else if (j.contains("name") && !j["name"].is_null()) {
std::string filename = j["name"].get<std::string>();
for (const auto& targetProcessName : g_Config.targetProcessNames) {
if (ends_with_case_insensitive(filename, targetProcessName)) {
// Emit event
g_EventAggregator.NewEvent(j.dump());
break;
}
}
}
}
catch (const std::exception& e) {
LOG_A(LOG_ERROR, "ETW event_callback exception: %s", e.what());
}
@@ -117,7 +162,7 @@ DWORD WINAPI TraceProcessingThread(LPVOID param) {
1 ProcessStart
2 ProcessStop
3 ThreadStart
4 ThreadStop?
4 ThreadStop
5 ImageLoad
6 ImageUnload
11 ProcessFreeze
@@ -126,7 +171,7 @@ DWORD WINAPI TraceProcessingThread(LPVOID param) {
std::vector<unsigned short> process_event_ids = { 1, 2, 3, 4, 5, 6, 11 };
krabs::event_filter process_filter(process_event_ids);
process_provider.trace_flags(process_provider.trace_flags() | EVENT_ENABLE_PROPERTY_STACK_TRACE);
process_filter.add_on_event_callback(event_callback);
process_filter.add_on_event_callback(event_callback_process);
process_provider.add_filter(process_filter);
trace_user.enable(process_provider);
LOG_A(LOG_INFO, "ETW: Microsoft-Windows-Kernel-Process (1, 2, 3, 4, 5, 6, 11)");
@@ -147,7 +192,7 @@ DWORD WINAPI TraceProcessingThread(LPVOID param) {
std::vector<unsigned short> auditapi_event_ids = { 3, 4, 5, 6 };
krabs::event_filter auditapi_filter(auditapi_event_ids);
auditapi_provider.trace_flags(auditapi_provider.trace_flags() | EVENT_ENABLE_PROPERTY_STACK_TRACE);
auditapi_filter.add_on_event_callback(event_callback);
auditapi_filter.add_on_event_callback(event_callback_process);
auditapi_provider.add_filter(auditapi_filter);
trace_user.enable(auditapi_provider);
LOG_A(LOG_INFO, "ETW: Microsoft-Windows-Kernel-Audit-API-Calls (3, 4, 5, 6)");
@@ -174,7 +219,7 @@ DWORD WINAPI TraceProcessingThread(LPVOID param) {
std::vector<unsigned short> kernelfile_event_ids = { 10, 30 };
krabs::event_filter kernelfile_filter(kernelfile_event_ids);
kernelfile_provider.trace_flags(kernelfile_provider.trace_flags() | EVENT_ENABLE_PROPERTY_STACK_TRACE);
kernelfile_filter.add_on_event_callback(event_callback);
kernelfile_filter.add_on_event_callback(event_callback_process);
kernelfile_provider.add_filter(kernelfile_filter);
trace_user.enable(kernelfile_provider);
LOG_A(LOG_INFO, "ETW: Microsoft-Windows-Kernel-File (10, 30)");
@@ -194,7 +239,7 @@ DWORD WINAPI TraceProcessingThread(LPVOID param) {
std::vector<unsigned short> kernelnetwork_event_ids = { 12, 15, 28, 31, 42, 43, 58, 59 };
krabs::event_filter kernelnetwork_filter(kernelnetwork_event_ids);
kernelnetwork_provider.trace_flags(kernelnetwork_provider.trace_flags() | EVENT_ENABLE_PROPERTY_STACK_TRACE);
kernelnetwork_filter.add_on_event_callback(event_callback);
kernelnetwork_filter.add_on_event_callback(event_callback_process);
kernelnetwork_provider.add_filter(kernelnetwork_filter);
trace_user.enable(kernelnetwork_provider);
LOG_A(LOG_INFO, "ETW: Microsoft-Windows-Kernel-Network (12, 15, 28, 31, 42, 43, 58, 59)");
@@ -242,15 +287,15 @@ DWORD WINAPI TraceProcessingThread(LPVOID param) {
/*
krabs::provider<> securityauditing_provider(L"Microsoft-Windows-Security-Auditing");
securityauditing_provider.trace_flags(securityauditing_provider.trace_flags() | EVENT_ENABLE_PROPERTY_STACK_TRACE);
securityauditing_provider.add_on_event_callback(event_callback);
securityauditing_provider.add_on_event_callback(event_callback_process);
trace_user.enable(securityauditing_provider);
*/
// Microsoft-Windows-Threat-Intelligence
/* everything - for the duration of use_additional_etw = true */
if (! g_Config.disable_unfiltered_etw) {
// Microsoft-Antimalware-Engine
// We currently observe all event id's, and filter in the callback
krabs::provider<> antimalwareengine_provider(L"Microsoft-Antimalware-Engine");
antimalwareengine_provider.add_on_event_callback(event_callback_nofilter);
if (g_Config.do_antimalwareengine) {
antimalwareengine_provider.add_on_event_callback(event_callback_antimalware);
trace_user.enable(antimalwareengine_provider);
LOG_A(LOG_INFO, "ETW: Microsoft-Antimalware-Engine (all)");
}
+1 -1
View File
@@ -11,4 +11,4 @@ BOOL InitializeEtwReader(std::vector<HANDLE>& threads);
void EtwReaderStopAll();
BOOL WINAPI ConsoleCtrlHandler(DWORD ctrlType);
DWORD WINAPI TraceProcessingThread(LPVOID param);
void enable_additional_etw(BOOL use);
void trace_in_progress(BOOL use);
+10 -7
View File
@@ -115,18 +115,20 @@ void EventProcessor::AnalyzeEventJson(nlohmann::json& j) {
LOG_A(LOG_WARNING, "No type? %s", j.dump().c_str());
return;
}
if (!j.contains("pid")) {
LOG_A(LOG_WARNING, "No pid? %s", j.dump().c_str());
return;
}
//if (!j.contains("pid")) {
// LOG_A(LOG_WARNING, "No pid? %s", j.dump().c_str());
// return;
//}
// Stats (for UI)
EventStats(j);
Process* process = g_ProcessResolver.getObject(j["pid"].get<DWORD>());
// etw_pid is typically the source process of the event
if (j.contains("etw_pid") && !j["etw_pid"].is_null()) {
Process* process = g_ProcessResolver.getObject(j["etw_pid"].get<DWORD>());
if (process == nullptr) {
// Should not happen
LOG_A(LOG_WARNING, "EventProcessor: Failed to get process object for pid %lu", j["pid"].get<DWORD>());
LOG_A(LOG_WARNING, "EventProcessor: Failed to get process object for pid %lu", j["etw_pid"].get<DWORD>());
return;
}
@@ -140,6 +142,7 @@ void EventProcessor::AnalyzeEventJson(nlohmann::json& j) {
// Augment the JSON Event with memory info
AugmentEventWithMemAddrInfo(j, process);
}
// Print Event
PrintEvent(j);
@@ -214,7 +217,7 @@ void EventProcessor::EventStats(nlohmann::json& j) {
num_dll += 1;
}
else if (j["type"] == "etw") {
if (j["provider_name"] == "Microsoft-Windows-Threat-Intelligence") {
if (j["etw_provider_name"] == "Microsoft-Windows-Threat-Intelligence") {
num_etwti += 1;
}
else {
+3 -3
View File
@@ -20,9 +20,9 @@ function displayEvents(events) {
}
// header
if (key === 'time' || key === 'pid' || key === 'tid' ||
key === 'krn_pid' || key === 'ppid' || key === 'observe' ||
key === 'thread_id' || key === 'provider_name' || key === 'id' || key == 'trace_id'
if (key === 'etw_time' || key === 'etw_pid' || key === 'etw_process' || key === 'etw_tid' ||
key === 'etw_pid' || key === 'etw_event_id' ||
key === 'thread_id' || key === 'etw_provider_name' || key === 'id' || key == 'trace_id'
) {
eventHeader += `<span class="highlight_a">${key}:${value}</span> `;
} else if (key === 'type' || key === 'func' || key === 'event' || key === 'task') {
+6 -13
View File
@@ -199,7 +199,6 @@ DWORD WINAPI WebserverThread(LPVOID param) {
]
*/
try {
res.set_content(g_EventProcessor.GetAllAsJson(), "application/json");
} catch (const std::exception& e) {
LOG_A(LOG_ERROR, "Error getting events: %s", e.what());
@@ -245,7 +244,7 @@ DWORD WINAPI WebserverThread(LPVOID param) {
auto data = json::parse(req.body);
if (data.contains("trace")) {
if (! data["trace"].is_array()) {
LOG_A(LOG_ERROR, "Targets should be an array");
LOG_A(LOG_ERROR, "Trace start: Targets should be an array, but is %s", data["trace"]);
json error_response = { {"error", "trace should be an array"} };
res.status = 400;
res.set_content(error_response.dump(), "application/json");
@@ -259,19 +258,13 @@ DWORD WINAPI WebserverThread(LPVOID param) {
ManagerApplyNewTargets();
/*
// enable Nofilter ETW (experimental)
std::string use_additional_etw;
if (req.has_file("use_additional_etw")) {
auto use_additional_etw_field = req.get_file_value("use_additional_etw");
use_additional_etw = use_additional_etw_field.content;
}
if (use_additional_etw == "true") {
enable_additional_etw(true);
}
else {
enable_additional_etw(false);
}
*/
trace_in_progress(true);
json response = { {"result", "ok"} };
res.set_content(response.dump(), "application/json");
@@ -289,13 +282,13 @@ DWORD WINAPI WebserverThread(LPVOID param) {
}
});
svr.Post("/api/trace/reset", [](const httplib::Request&, httplib::Response& res) {
/*
enable_additional_etw(false);
*/
trace_in_progress(false);
g_EventAggregator.ResetData();
g_EventProcessor.ResetData();
});
svr.Post("/api/trace/stop", [](const httplib::Request&, httplib::Response& res) {
trace_in_progress(false);
});
// Lock management endpoints
svr.Post("/api/lock/acquire", [](const httplib::Request&, httplib::Response& res) {
+2 -1
View File
@@ -30,7 +30,7 @@ nlohmann::json KrabsEtwEventToJsonStr(const EVENT_RECORD& record, krabs::schema
j["event"] = d;
//j["opcode_id"] = schema.event_opcode();
j["event_id"] = schema.event_id();
j["etw_event_id"] = schema.event_id();
// The ProviderId is just the UID of the provider, which is not very useful
// This is a workaround. Alternative would be to use TdhGetEventInformation()?
@@ -52,6 +52,7 @@ nlohmann::json KrabsEtwEventToJsonStr(const EVENT_RECORD& record, krabs::schema
continue;
}
std::string jsonKey = wstring2string((std::wstring&)propertyName);
std::transform(jsonKey.begin(), jsonKey.end(), jsonKey.begin(), ::tolower); // lowercase
// Special cases
if (propertyName == L"ProtectionMask" || propertyName == L"LastProtectionMask") {
+10
View File
@@ -118,6 +118,16 @@ bool contains_case_insensitive(const std::string& haystack, const std::string& n
}
bool ends_with_case_insensitive(const std::string& str, const std::string& suffix) {
if (suffix.size() > str.size()) {
return false;
}
std::string str_lower = to_lowercase2(str);
std::string suffix_lower = to_lowercase2(suffix);
return str_lower.compare(str_lower.size() - suffix_lower.size(), suffix_lower.size(), suffix_lower) == 0;
}
// Dear mother of god whats up with all these goddamn string types
wchar_t* string2wcharAlloc(const std::string& str) {
if (str.empty()) {
+1
View File
@@ -30,6 +30,7 @@ std::string wchar2string(const wchar_t* wstr); // 4
std::wstring string2wstring(const std::string& str); // 3
bool contains_case_insensitive(const std::string& haystack, const std::string& needle); // 5
bool ends_with_case_insensitive(const std::string& str, const std::string& suffix); // 1
void remove_all_occurrences_case_insensitive(std::string& str, const std::string& to_remove); // 3
bool wstring_starts_with(const std::wstring& str, const std::wstring& prefix); // 3
wchar_t* JsonEscape(wchar_t* str, size_t buffer_size); // 9
+56
View File
@@ -0,0 +1,56 @@
# RedEdr Test Script
# Starts RedEdr, traces procexp64.exe, then cleans up
$rededrPath = "C:\RedEdr\RedEdr.exe"
#$targetPath = "D:\toolz\procexp64.exe"
$targetPath = "D:\hacking\some_malware\mimikatz.exe"
#$targetPath = "D:\hacking\malware\cs2025-stageless.exe"
$webserverUrl = "http://localhost:8081"
# Start RedEdr in the background
#Write-Host "Starting RedEdr..."
#$rededrProcess = Start-Process -FilePath $rededrPath -PassThru
# Wait for the webserver to be ready
#Write-Host "Waiting for webserver to start..."
#Start-Sleep -Seconds 3
# Call /api/trace/start to start tracing
# Extract filename without path from targetPath
$targetFilename = [System.IO.Path]::GetFileName($targetPath)
Write-Host "Starting trace for $targetFilename..."
$traceBody = @{
trace = @($targetFilename)
} | ConvertTo-Json
try {
Invoke-RestMethod -Uri "$webserverUrl/api/trace/start" -Method Post -Body $traceBody -ContentType "application/json"
Write-Host "Trace started successfully"
} catch {
Write-Host "Failed to start trace: $_"
}
# Start target executable and wait for it to exit
Write-Host "Starting $targetFilename..."
$procexpProcess = Start-Process -FilePath $targetPath -PassThru
Write-Host "$targetFilename PID: $($procexpProcess.Id) (0x$($procexpProcess.Id.ToString('X')))"
$procexpProcess | Wait-Process
# Call /api/trace/reset to reset the trace
Write-Host "Resetting trace..."
try {
Invoke-RestMethod -Uri "$webserverUrl/api/trace/stop" -Method Post
Write-Host "Trace reset successfully"
} catch {
Write-Host "Failed to reset trace: $_"
}
# Kill RedEdr
#Write-Host "Stopping RedEdr..."
#if ($rededrProcess -and !$rededrProcess.HasExited) {
# Stop-Process -Id $rededrProcess.Id -Force
# Write-Host "RedEdr stopped"
#} else {
# Write-Host "RedEdr process already exited"
#}
-52
View File
@@ -1,52 +0,0 @@
param(
[string]$arg = $args[0] # Default to the first argument if not provided
)
if ($arg -eq "dll") {
# Start notepad.exe in the background
Start-Process notepad
Start-Sleep -Seconds 1
$notepadProcess = Get-Process notepad
$notepadProcessPid = $notepadProcess.Id
Start-Process cmd -ArgumentList @(
"/c",
"timeout /t 1 &"
".\x64\Debug\RedEdrTester.exe 3 $($notepadProcessPid) &"
#"timeout /t 3 &", # Wait for 5 seconds
#"taskkill /im notepad.exe /f" # Kill notepad.exe
)
Start-Process -Wait "C:\rededr\rededr.exe" -ArgumentList "--web --hide --dllreader --trace otepad"
Stop-Process -Name notepad -Force -ErrorAction SilentlyContinue
}
elseif ($arg -eq "kernel") {
Start-Process cmd -ArgumentList @(
"/c",
"timeout /t 2 &",
"start notepad.exe &",
"timeout /t 3 &",
"taskkill /im notepad.exe /f"
)
Start-Process -Wait "C:\rededr\rededr.exe" -ArgumentList "--hide --kernel --inject --trace otepad"
}
elseif ($arg -eq "etw") {
Start-Process cmd -ArgumentList @(
"/c",
"timeout /t 2 &",
"start notepad.exe &",
"timeout /t 3 &",
"taskkill /im notepad.exe /f"
)
Start-Process -Wait "C:\rededr\rededr.exe" -ArgumentList "--etw --trace otepad"
}
elseif ($arg -eq "etwti") {
Start-Process cmd -ArgumentList @(
"/c",
"timeout /t 2 &",
'start C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe &',
"timeout /t 3 &",
"taskkill /im msedge.exe /f"
)
Start-Process -Wait "C:\rededr\rededr.exe" -ArgumentList "--etwti --trace otepad"
}