Files
2025-11-26 20:11:44 +01:00

716 lines
28 KiB
C++

#include <krabs.hpp>
#include "helpers/json.hpp"
#include <iostream>
#include <cstdlib>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "Ws2_32.lib")
#include "globals.h"
#include "profile.h"
#include "utils.h"
#include "etwparser.h"
// all events
std::vector<json> std_events = {};
std::vector<json> misc_events = {};
std::vector<json> etw_ti_events = {};
std::vector<json> hook_events = {};
std::map<std::string, std::vector<UINT64>> time_diffs_ns = { // differences in nanoseconds between ETW time and system time
{ EDRi_PROVIDER, {} },
{ ATTACK_PROVIDER, {} },
{ HOOK_PROVIDER, {} }
};
int null_events = 0; // errors
// globals
ProcInfo null_proc = { 0, 0, 0, "", false };
ProcInfo g_attack_proc = null_proc;
std::vector<ProcInfo> g_attack_and_injected_procs = {};
bool g_start_marked_detected = false;
bool g_hooker_started = false;
bool g_attack_terminated = false;
// when check is needed, they are set to false again
bool g_misc_trace_started = true;
bool g_etw_ti_trace_started = true;
bool g_hook_trace_started = true;
// procs to check for hook init msg
int detected_hook_start_markers = 0;
// static
static bool cleaned_events = false;
// keys that get merged together
MergeCategory ppid_keys = {
PPID,
{"parentpid"}
};
MergeCategory tpid_keys = { // all refer to yet another pid (event has an emitter (process_id), original proc (pid), and the target proc (tpid))
TARGET_PID,
{"processid", "tpid", "targetprocessid", "frozenprocessid"} // processid in kernel means tpid, processid in antimalware means pid (but only event 95 has this)
};
MergeCategory ttid_keys = { // both refer to yet another pid (event has an emitter (process_id), original proc (pid), and the target proc (tpid))
TARGET_TID,
{"targetthreatid", "ttid"} // yes, threat with a t, this is a typo in the property name
};
MergeCategory filepath_keys = {
FILEPATH,
{"basepath", "filename", "imagename", "path", "reasonimagepath"}
};
std::vector<MergeCategory> key_categories_to_merge = { ppid_keys, tpid_keys, ttid_keys, filepath_keys };
// get time difference statistics
std::map<std::string, std::vector<UINT64>> get_time_diffs() {
return time_diffs_ns;
}
// potentially corrupted events, or complete parsing errors
int get_null_events_count() {
return null_events;
}
// -------------- EVENT PARSING -------------- //
// parses attack events
json parse_custom_etw_event(const EVENT_RECORD& record, const krabs::schema& schema) {
try {
krabs::parser parser(schema);
json j;
std::string provider = wchar2string(schema.provider_name());
__int64 etw_timestamp_filetime = static_cast<__int64>(record.EventHeader.TimeStamp.QuadPart);
j[TIMESTAMP_ETW] = filetime_to_iso8601(etw_timestamp_filetime);
j[TYPE] = "myETW";
j[PID] = record.EventHeader.ProcessId;
std::string task = wchar2string(schema.task_name());
j[TASK] = task;
j[PROVIDER_NAME] = wchar2string(schema.provider_name());
if (j[PROVIDER_NAME] == EDRi_PROVIDER) {
j[EVENT_ID] = EDRi_PROVIDER_EVENT_ID;
}
else if (j[PROVIDER_NAME] == ATTACK_PROVIDER) {
j[EVENT_ID] = ATTACK_PROVIDER_EVENT_ID;
}
else if (j[PROVIDER_NAME] == HOOK_PROVIDER) {
j[EVENT_ID] = HOOK_PROVIDER_EVENT_ID;
}
else { // unknown, return header info only
if (g_debug) {
std::cerr << "[!] ETW: Custom event with unknown provider: " << j.dump() << "\n";
}
j[EVENT_ID] = -1;
return j;
}
// custom parsing when not using manifest based ETW --> cannot use property parsing
// get payload size
const BYTE* data = (const BYTE*)record.UserData;
ULONG size = record.UserDataLength;
// PARSE MESSAGE
const char* msg = reinterpret_cast<const char*>(data); // read until first null byte
size_t msg_len = strnlen(msg, size);
if (msg_len == 0) {
msg = "(empty message)";
std::cout << "[*] ETW: Warning: Custom event with empty message " << j.dump() << "\n";
}
j[MESSAGE] = msg;
const BYTE* ptr_field = data + msg_len + 1;
// PARSE NS_SINCE_EPOCH
UINT64 system_timestamp_ns_since_unix_epoch = 0;
if (ptr_field + sizeof(UINT64) <= data + size) {
memcpy(&system_timestamp_ns_since_unix_epoch, ptr_field, sizeof(UINT64));
UINT64 etw_ns_since_unix_epoch = (static_cast<UINT64>(etw_timestamp_filetime) - WINDOWS_TICKS_TO_UNIX_EPOCH) * NS_PER_WINDOWS_TICK;
if (system_timestamp_ns_since_unix_epoch > 0) { // only diff if valid timestamp
INT64 diff = static_cast<INT64>(etw_ns_since_unix_epoch) - static_cast<INT64>(system_timestamp_ns_since_unix_epoch); // etw time should be greater, but do not trust time...
time_diffs_ns[j[PROVIDER_NAME]].push_back(std::abs(diff));
}
ptr_field += sizeof(UINT64);
}
else if (g_debug) {
std::cerr << "[!] ETW: Custom event with no " << TIMESTAMP_NS << " field: " << j.dump() << "\n";
}
if (system_timestamp_ns_since_unix_epoch == 0) { // falback to ETW timestamp if invalid
j[TIMESTAMP_NS] = filetime_to_unix_epoch_ns(etw_timestamp_filetime);
j[TIMESTAMP_SYS] = j[TIMESTAMP_ETW];
if (g_debug) {
std::cerr << "[!] ETW: Custom event with invalid " << TIMESTAMP_NS << " field: " << j.dump() << "\n";
}
}
else { // use timestamp right before etw was emitted (more exact for hook events)
j[TIMESTAMP_NS] = system_timestamp_ns_since_unix_epoch;
j[TIMESTAMP_SYS] = unix_epoch_ns_to_iso8601(system_timestamp_ns_since_unix_epoch);
}
if (provider == HOOK_PROVIDER) {
// PARSE TARGETPID
UINT64 targetpid = -1;
if (ptr_field + sizeof(UINT64) <= data + size) {
memcpy(&targetpid, ptr_field, sizeof(UINT64));
}
else if (g_debug) {
std::cerr << "[!] ETW: Hook event with no " << TARGET_PID << " field: " << j.dump() << "\n";
}
j[TARGET_PID] = targetpid;
}
return j;
}
catch (const std::exception& ex) {
std::cerr << "[!] ETW: Custom Trace Exception: " << ex.what() << "\n";
return json();
}
}
std::string get_kernel_api_task_name(int event_id) {
if (event_id == 1)
return "PsSetLoadImageNotifyRoutineEx";
if (event_id == 2)
return "NtTerminateProcess";
if (event_id == 3)
return "NtCreateSymbolicLinkObject";
if (event_id == 4)
return "NtSetContextThread";
if (event_id == 5)
return "PsOpenProcess";
if (event_id == 6)
return "PsOpenThread";
if (event_id == 7)
return "IoRegisterLastChanceShutdownNotification";
if (event_id == 8)
return "IoRegisterShutdownNotification";
return "unmapped kernel api event id";
}
std::string get_val(const json& j, const std::string& key) {
if (!j.contains(key)) {
return ""; // key not present
}
if (j[key].is_string()) {
return j[key].get<std::string>();
}
else if (j[key].is_number()) {
return std::to_string(j[key].get<double>());
}
else if (j[key].is_boolean()) {
return j[key].get<bool>() ? "true" : "false";
}
else if (j[key].is_null()) {
return "null";
}
else {
// array or object
return j[key].dump();
}
}
// parses all other ETW events
json parse_etw_event(const EVENT_RECORD& record, const krabs::schema& schema) {
try {
std::string provider_name = wchar2string(schema.provider_name());
if (provider_name == EDRi_PROVIDER || provider_name == ATTACK_PROVIDER || provider_name == HOOK_PROVIDER) {
return parse_custom_etw_event(record, schema);
}
json j;
__int64 etw_timestamp_filetime = static_cast<__int64>(record.EventHeader.TimeStamp.QuadPart);
j[TIMESTAMP_NS] = filetime_to_unix_epoch_ns(etw_timestamp_filetime);
j[TIMESTAMP_ETW] = filetime_to_iso8601(etw_timestamp_filetime);
j[TIMESTAMP_SYS] = j[TIMESTAMP_ETW]; // normal ETW events only have their timestamp, not the custom system timestamp
j[PID] = record.EventHeader.ProcessId;
j[TID] = record.EventHeader.ThreadId;
j[EVENT_ID] = schema.event_id(); // opcode is also an ID, but not documented?
j[PROVIDER_NAME] = provider_name;
if (j[PROVIDER_NAME] == THREAT_INTEL_PROVIDER) {
j[TYPE] = "ETW-TI";
}
else {
j[TYPE] = "ETW";
}
if (provider_name == KERNEL_API_PROVIDER) {
// special handling for kernel api provider to get better task names
std::string task_name = get_kernel_api_task_name(schema.event_id());
j[TASK] = task_name;
}
else {
std::wstring combined = std::wstring(schema.task_name()) + L" " + std::wstring(schema.opcode_name());
j[TASK] = wstring2string(combined);
}
krabs::parser parser(schema);
// parse all properties defined in the schema
for (const auto& property : parser.properties()) {
std::string last_key;
std::string original_key = "";
int last_type;
try {
// get property name and type
const std::wstring& property_name = property.name();
const auto property_type = property.type();
// create key and convert it to lowercase
std::string key = wstring2string((std::wstring&)property_name);
std::transform(key.begin(), key.end(), key.begin(),
[](unsigned char c) { return std::tolower(c); });
// for tracking potential overwrites & error messages
std::string overwritten_value = "";
last_key = key;
last_type = property_type;
// check if it's a merged key --> write value to merged_key
for (const auto& cat : key_categories_to_merge) {
if (std::find(cat.keys_to_merge.begin(), cat.keys_to_merge.end(), key) != cat.keys_to_merge.end()) {
original_key = key;
key = cat.merged_key;
}
}
if (j.contains(key)) {
overwritten_value = get_val(j, key);
}
// Special cases
if (key == "protectionmask" || key == "lastprotectionmask") {
uint32_t protection_mask = parser.parse<uint32_t>(property_name);
j[key] = get_memory_region_protect(protection_mask);
continue;
}
switch (property_type) {
case TDH_INTYPE_UNICODESTRING:
{
std::wstringstream wss;
wss << parser.parse<std::wstring>(property_name);
std::string s = wstring2string((std::wstring&)wss.str());
j[key] = s;
break;
}
case TDH_INTYPE_ANSISTRING:
j[key] = parser.parse<std::string>(property_name);
break;
case TDH_INTYPE_INT8:
j[key] = (int32_t)parser.parse<CHAR>(property_name);
break;
case TDH_INTYPE_UINT8:
j[key] = (uint32_t)parser.parse<UCHAR>(property_name);
break;
case TDH_INTYPE_INT16:
j[key] = (int32_t)parser.parse<SHORT>(property_name);
break;
case TDH_INTYPE_UINT16:
j[key] = (int32_t)parser.parse<USHORT>(property_name);
break;
case TDH_INTYPE_INT32:
j[key] = (int32_t)parser.parse<int32_t>(property_name);
break;
case TDH_INTYPE_UINT32:
j[key] = (uint32_t)parser.parse<uint32_t>(property_name);
break;
case TDH_INTYPE_INT64:
j[key] = (int64_t)parser.parse<int64_t>(property_name);
break;
case TDH_INTYPE_UINT64:
j[key] = (uint64_t)parser.parse<uint64_t>(property_name);
break;
case TDH_INTYPE_BOOLEAN:
{
try {
j[key] = (bool)parser.parse<bool>(property_name);
}
catch (...) {
// fallback: dump raw bytes
auto bin = parser.parse<krabs::binary>(property_name);
const auto& bytes = bin.bytes();
std::ostringstream oss;
oss << "0x" << std::hex << std::setfill('0');
for (auto b : bytes)
oss << std::setw(2) << static_cast<int>(b);
j[key] = oss.str();
}
break;
}
case TDH_INTYPE_POINTER:
j[key] = (uint64_t)parser.parse<PVOID>(property_name);
break;
case TDH_INTYPE_BINARY:
{
try {
auto bin = parser.parse<krabs::binary>(property_name);
const auto& bytes = bin.bytes();
const auto size = bytes.size();
const auto data = bytes.empty() ? nullptr : bytes.data();
if (size == 4 && data) {
char ipStr[INET_ADDRSTRLEN] = { 0 };
if (inet_ntop(AF_INET, data, ipStr, sizeof(ipStr)))
j[key] = std::string(ipStr);
else
j[key] = "<inet_ntop_AF_INET_failed>";
}
else if (size == 16 && data) {
// detect IPv4-mapped IPv6 ::ffff:a.b.c.d (first 10 bytes 0, then 0xff,0xff)
bool ipv4_mapped = (std::equal(bytes.begin(), bytes.begin() + 10, std::vector<BYTE>(10, 0).begin())
&& bytes[10] == 0xff && bytes[11] == 0xff);
if (ipv4_mapped) {
// convert last 4 bytes as IPv4
char ipStr[INET_ADDRSTRLEN] = { 0 };
if (inet_ntop(AF_INET, data + 12, ipStr, sizeof(ipStr)))
j[key] = std::string("::ffff:") + std::string(ipStr); // or ipStr alone if you prefer
else
j[key] = "<inet_ntop_mapped_failed>";
}
else {
char ipStr[INET6_ADDRSTRLEN] = { 0 };
if (inet_ntop(AF_INET6, data, ipStr, sizeof(ipStr)))
j[key] = std::string(ipStr);
else
j[key] = "<inet_ntop_AF_INET6_failed>";
}
}
else {
// fallback: hex dump
std::ostringstream oss;
oss << "0x" << std::hex << std::setfill('0');
for (BYTE b : bytes) {
oss << std::setw(2) << static_cast<int>(b);
}
j[key] = oss.str();
}
}
catch (...) {
// ignore conversion errors
j[key] = "<parse error>";
}
break;
}
case TDH_INTYPE_GUID:
{
GUID guid = parser.parse<GUID>(property_name);
std::ostringstream oss;
oss << std::hex << std::setfill('0')
<< std::setw(8) << guid.Data1 << "-"
<< std::setw(4) << guid.Data2 << "-"
<< std::setw(4) << guid.Data3 << "-";
for (int i = 0; i < 2; i++)
oss << std::setw(2) << static_cast<int>(guid.Data4[i]);
oss << "-";
for (int i = 2; i < 8; i++)
oss << std::setw(2) << static_cast<int>(guid.Data4[i]);
j[key] = oss.str();
break;
}
case TDH_INTYPE_FILETIME:
{
FILETIME fileTime = parser.parse<FILETIME>(property_name);
ULARGE_INTEGER uli;
uli.LowPart = fileTime.dwLowDateTime;
uli.HighPart = fileTime.dwHighDateTime;
j[key] = uli.QuadPart;
break;
}
case TDH_INTYPE_SID:
{
std::vector<uint8_t> raw;
if (parser.try_parse(property_name, raw)) {
// try to convert raw bytes to a SID string
if (!raw.empty() && IsValidSid((PSID)raw.data())) {
LPWSTR sidString = nullptr;
if (ConvertSidToStringSidW((PSID)raw.data(), &sidString)) {
std::wstring ws(sidString);
j[key] = wstring2string(ws);
LocalFree(sidString);
}
else {
j[key] = "invalid_sid";
}
}
else {
// fallback: output raw data as hex
std::ostringstream oss;
oss << "0x";
for (auto b : raw) {
oss << std::hex << std::setw(2) << std::setfill('0') << (int)b;
}
j[key] = oss.str();
}
}
else {
// parsing failed, fallback: empty hex
j[key] = "0x";
}
break;
}
case TDH_INTYPE_HEXINT32:
{
std::ostringstream oss;
oss << "0x" << std::hex << std::uppercase << parser.parse<uint32_t>(property_name);
j[key] = oss.str();
break;
}
case TDH_INTYPE_HEXINT64:
{
std::ostringstream oss;
oss << "0x" << std::hex << std::uppercase << parser.parse<uint64_t>(property_name);
j[key] = oss.str();
break;
}
default:
std::cout << "[*] ETW: Warning: Unsupported property type " << property_type << " for " << j[TASK] << "'s " << key << "\n";
j[key] = "unsupported";
break;
}
/*
if (key == ORIGINATING_PID && j[ORIGINATING_PID] == 0) {
j[ORIGINATING_PID] = -1; // orginating pid=0 does not make sense?
}
*/
if (overwritten_value != "") {
if (overwritten_value != j[key]) { // only warn if the values differ
std::cerr << "[!] ETW: Warning, " << j[PROVIDER_NAME] << ":" << j[EVENT_ID] <<
", overwritten '" << key << ":" << overwritten_value <<
"' with '" << key << ":" << j[key] << "'";
if (original_key != "") { // include name of merged key (if overwrite was b.c. of a merge)
std::cerr << " because of merged key '" << original_key << "'";
}
std::cerr << "\n";
}
}
}
catch (const std::exception& ex) {
std::cerr <<
"[!] ETW: parse_etw_event failed to parse " << j[TASK] <<
", key: " << last_key <<
", type: " << last_type <<
", error: " << ex.what() << "\n";
j[last_key] = PARSE_ERROR; // write error and continue to next prop
}
}
// callstack
if (include_stacktrace) {
try {
j["stacktrace"] = json::array();
int idx = 0;
for (auto& return_address : schema.stack_trace()) {
// only add non-kernelspace addresses
if (return_address < 0xFFFF080000000000) {
j["stacktrace"].push_back(return_address);
idx++;
}
}
}
catch (const std::exception& ex) {
std::cerr << "[!] ETW: Failed to parse " << j[TASK] << "'s call stack: " << ex.what() << "\n";
}
}
return j;
}
catch (const std::exception& ex) {
std::cerr << "[!] ETW: parse_etw_event general exception: " << ex.what() << "\n";
return json();
}
}
// -------------- POST PARSING CHECKS -------------- //
// check proc started via kernel etw
int check_new_proc(json& j) {
if (j[PROVIDER_NAME] == KERNEL_PROCESS_PROVIDER && j[EVENT_ID] == KERNEL_PROC_START_EVENT_ID && j.contains(TARGET_PID)) {
return j[TARGET_PID]; // kernel proc uses this key
}
return 0;
}
// check proc ended via kernel etw
int check_proc_termination(json& j) {
if (j[PROVIDER_NAME] == KERNEL_PROCESS_PROVIDER && j[EVENT_ID] == KERNEL_PROC_STOP_EVENT_ID && j.contains(TARGET_PID)) {
return j[TARGET_PID]; // kernel proc uses this key
}
return 0;
}
// check when the first EDRi event is registered --> trace running
bool check_traces_started(json& j) {
if (j.contains(MESSAGE)) {
return j[MESSAGE] == EDRi_TRACE_START_MARKER;
}
return false;
}
// check if the hooker emits ETW messages --> hooks installed
bool check_hooker_started(json& j) {
if (j[PROVIDER_NAME] == HOOK_PROVIDER && j.contains(MESSAGE)) {
if (j[MESSAGE] == NTDLL_HOOKER_TRACE_START_MARKER) {
detected_hook_start_markers++;
if (g_debug) {
std::cout << "[+] ETW: Detected hook initialization in " << j[PID] << "\n";
}
}
}
return detected_hook_start_markers == g_newly_hooked_procs.size();
}
// monitors events: mark procs as started or terminated (also attack and injected on their own), and check traces startes
void post_parsing_checks(json& j) {
// add a newly spawned procs to process map
int pid = check_new_proc(j);
if (pid != 0) {
if (!j.contains(FILEPATH)) {
if (g_debug) {
std::cout << "[!] ETW: New proc without a " << FILEPATH << ", " << j.dump() << "\n";
}
}
else {
std::string exe_path = j[FILEPATH].get<std::string>();
std::string exe_name = exe_path.substr(exe_path.find_last_of("\\") + 1);
UINT64 timestamp_ns = j[TIMESTAMP_NS];
// check if this is a process to be tracked
bool to_track = false;
for (auto& e : g_exes_to_track) {
if (_stricmp(exe_name.c_str(), e.c_str()) == 0) {
to_track = true;
}
}
// also check if this is an attack or injected proc
if (g_attack_proc.PID == 0) {
if (filepath_match(j[FILEPATH], g_attack_exe_path)) { // depends on the attack path, but this is fixed
g_attack_proc = ProcInfo{ pid, timestamp_ns, MAX_PROC_END, exe_name, true };
g_attack_and_injected_procs.push_back(g_attack_proc);
std::cout << "[+] ETW: Got attack PID: " << pid << "\n";
to_track = true;
}
}
if (filepath_match(j[FILEPATH], injected_path) || filepath_match(j[FILEPATH], cs_injected_path)) {
g_attack_and_injected_procs.push_back(ProcInfo{ pid, timestamp_ns, MAX_PROC_END, exe_name, true });
std::cout << "[+] ETW: Got injected PID: " << pid << "\n";
to_track = true;
}
add_proc(pid, exe_name, timestamp_ns, to_track);
}
}
// or mark termination of process
pid = check_proc_termination(j);
if (pid != 0) {
UINT64 timestamp_ns = j[TIMESTAMP_NS];
mark_termination(pid, timestamp_ns);
// also check if the attack is done
if (!g_attack_terminated && pid == g_attack_proc.PID) {
std::cout << "[+] ETW: Attack termination detected\n";
g_attack_terminated = true;
}
// no need to check for injected termination, only g_attack_terminated is relevant
}
// checks if the trace started
if (!g_start_marked_detected && check_traces_started(j)) {
if (g_debug) {
std::cout << "[+] ETW: Start marker detected\n";
}
g_start_marked_detected = true;
}
}
void post_parsing_checks_hooks(json& j) {
// checks if the hooker is started (only if new procs were hooked, and the hooker is not started yet)
if (!g_hooker_started && g_newly_hooked_procs.size() > 0 && check_hooker_started(j)) {
g_hooker_started = true;
}
}
// hand over schema for parsing
void event_callback_std(const EVENT_RECORD& record, const krabs::trace_context& trace_context) {
json ev = parse_etw_event(record, krabs::schema(record, trace_context.schema_locator));
if (ev.is_null()) {
null_events++;
return; // ignore empty events
}
post_parsing_checks(ev);
std_events.push_back(ev);
}
// hand over schema for parsing
void event_callback_misc(const EVENT_RECORD& record, const krabs::trace_context& trace_context) {
g_misc_trace_started = true;
json ev = parse_etw_event(record, krabs::schema(record, trace_context.schema_locator));
if (ev.is_null()) {
null_events++;
return; // ignore empty events
}
misc_events.push_back(ev);
}
// hand over schema for parsing
void event_callback_etw_ti(const EVENT_RECORD& record, const krabs::trace_context& trace_context) {
// g_etw_ti_trace_started = true; // when there are no "threat" events, there are no etw-tievents --> must be set in etwreader, even if it's not super accurate there
json ev = parse_etw_event(record, krabs::schema(record, trace_context.schema_locator));
if (ev.is_null()) {
null_events++;
return; // ignore empty events
}
etw_ti_events.push_back(ev);
}
// hand over schema for parsing
void event_callback_hooks(const EVENT_RECORD& record, const krabs::trace_context& trace_context) {
// g_hook_trace_started = true; // when there are no hooked binaries, there are no events --> must be set in etwreader, even if it's not super accurate there
json ev = parse_etw_event(record, krabs::schema(record, trace_context.schema_locator));
if (ev.is_null()) {
null_events++;
return; // ignore empty events
}
post_parsing_checks_hooks(ev);
hook_events.push_back(ev);
}
// get all events as one flat vector
std::vector<json> concat_all_etw_events() {
std::vector<json> out;
std::vector<std::vector<json>*> event_traces = { &std_events, &misc_events, &etw_ti_events, &hook_events };
size_t events_count = 0;
for (auto v_ptr : event_traces) {
events_count += v_ptr->size();
}
if (g_debug) {
std::cout << "[*] ETW: Flattening all " << events_count << " recorded events\n";
}
out.clear();
out.reserve(events_count);
for (auto v_ptr : event_traces) {
out.insert(out.end(),
std::make_move_iterator(v_ptr->begin()),
std::make_move_iterator(v_ptr->end()));
v_ptr->clear(); // original vectors are now empty
v_ptr->shrink_to_fit(); // release the ~kraken~ memory
}
return out;
}