Detecting Parent Process Spoofing using KrabsETW

This blog post covers how to build a simple PoC program that will use the KrabsETW library to subscribe to an ETW provider in order to detect parent process spoofing.

Introduction

Parent-child relationships are a valuable telemetry source for blue teams and security products to identify potentially malicious process chains. For example, the parent-child relationship of winword.exe launching powershell.exe or cmd.exe is often monitored as it can be indicative of office macro execution which is commonly used as an initial access technique by commodity malware and APTs alike.

The scrutiny of parent-child relationships relating to commonly abused windows programs had led to attackers looking for ways to obscure or break these relationships. As such there are plenty tools and resources available online to allow for parent process spoofing, and the technique has also been included as a feature in Cobalt Strike since version 3.8.

The rest of this blog post details how to use ETW providers to provide a detection mechanism for parent PID spoofing. This is not a new detection technique, it is just my own experimentation of the technique discussed in the original detection blog post by F-Secure Countercept.

Event Tracing for Windows (ETW)

Event Tracing for Windows (ETW) is a tracing facility that allows a user log and consume events from various applications and APIs. During the course of their normal operation, windows applications and APIs (providers) will log information to ETW "feeds" which can then be subscribed to by consumers, who can then parse and act on the ingested information.

Given the stability, realtime nature, and visibility that ETW offers, it has quickly been adopted by blue teams and security products as a rich telemetry source. EDR products will commonly subscribe to ETW "feeds" to enrich in their detection capabilities, enabling greater visibility into memory related events, registry events, and more. To simplify the process of subscribing to ETW feeds, Microsoft released a C++ library called KrabsETW which provides a very simple way of subscribing to single or multiple ETW feeds.

The Power of (Krabs)ETW

As detailed in the Countercept blog post, the Microsoft-Windows-Kernel-Provider ETW provider offers a powerful method of detection for parent PID spoofing. This kernel provider provides real-time information about windows processes, including various information such as the process ID of a created process, the parent process ID, the process image name, etc.

To explore what other information is available from this provider we can easily subscribe to this feed using the KrabsETW library:

#include "packages\Microsoft.O365.Security.Krabsetw.4.1.18\lib\native\include\krabs.hpp"
#include <iostream>
#include <tlhelp32.h>

void start_etw_trace()
{
    krabs::user_trace trace;

    krabs::provider<> provider(L"Microsoft-Windows-Kernel-Process");
    provider.any(0x10);  // WINEVENT_KEYWORD_PROCESS

    auto process_callback = [](const EVENT_RECORD& record, const krabs::trace_context& trace_context) {
        krabs::schema schema(record, trace_context.schema_locator);
        krabs::parser parser(schema);
		    uint32_t ppid = parser.parse<uint32_t>(L"ParentProcessID");
	    	uint32_t pid = parser.parse<uint32_t>(L"ProcessID");
	    	std::wstring image_name = parser.parse<std::wstring>(L"ImageName");

	    	std::wcout << L"[>] Process Name: " << image_name << std::endl;
	    	std::wcout << L"[>] ParentProcess ID: " << ppid << std::endl;
	    	std::wcout << L"[>] ProcessID " << pid << std::endl;
	    	std::wcout << std::endl;
    };

    // real-time process start events
    krabs::event_filter process_filter(krabs::predicates::id_is(1));
    process_filter.add_on_event_callback(process_callback);
    provider.add_filter(process_filter);

    trace.enable(provider);
    trace.start();
}

int main()
{
    std::cout << "[+] Monitoring Starting!\n" << std::endl;
    start_etw_trace();
}

As the Microsoft-Windows-Kernel-Process is a kernel trace, subscribing requires admin rights. In this case I used PsExec to spawn a SYSTEM shell before executing the compiled binary.

Parent Process or Creator Process

As explained in an excellent blog post by Pavel Yosifovich there is no dependency between a child and a parent process in any way. The 'mapping' between a process and its parent is not enforced and can be altered from user-land. For example, via the lpStartupInfo structure when calling CreateProcessA.

The parent process ID that is displayed from our current PoC code will match the parent specified in the lpStartupInfo structure. This means that it will display the spoofed parent and not the real parent. Fortunately the Microsoft-Windows-Kernel-Provider ETW provider allows us to see the creator process for each new process that is created, the creator "process ID" is not affected by changes to the lpStartupInfo structure and therefore presents a detection opportunity for us.

We can access the "creator process" ID from the record EventHeader for events provided by the Microsoft-Windows-Kernel-Provider ETW provider. The EventHeader is basically just a wrapper around the actual ETW record and contains various pieces of information relating to how the event was created. One of these pieces of information is the ProcessID of the process that has spawned the new process, in other words this is the real parent process ID that will be unaffected by PPID spoofing techniques.

Building our PoC program

Now that we have all of the pieces of the puzzle, we can extend our previous PoC code to detect a common examples of parent PID spoofing:

#include "packages\Microsoft.O365.Security.Krabsetw.4.1.18\lib\native\include\krabs.hpp"
#include <iostream>
#include <tlhelp32.h>

int Error(const char* msg) {
    std::cout << "[+] Error: " << msg << " " << GetLastError() << std::endl;
	  return 1;
}

// Resolve a PID to a process name
std::wstring GetProcessName(uint32_t pid) {
	HANDLE hProcessSnap;
	PROCESSENTRY32 pe32;

	hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (hProcessSnap == INVALID_HANDLE_VALUE)
	{
		Error("Error in CreateToolhelp32Snapshot");
		return L"<Error>";
	}
	pe32.dwSize = sizeof(PROCESSENTRY32);

	if (!Process32First(hProcessSnap, &pe32))
	{
		Error("Error in Process32First\n");
		CloseHandle(hProcessSnap);
		return L"<Error>";
	}
	do
	{
		if (pid == pe32.th32ProcessID){ 
            return pe32.szExeFile;
		}

	} while (Process32Next(hProcessSnap, &pe32));
	CloseHandle(hProcessSnap);
	return L"<Error>";
}

void begin_trace()
{
    krabs::user_trace trace;

    krabs::provider<> provider(L"Microsoft-Windows-Kernel-Process");
    provider.any(0x10);  // WINEVENT_KEYWORD_PROCESS

    auto process_callback = [](const EVENT_RECORD& record, const krabs::trace_context& trace_context) {
        krabs::schema schema(record, trace_context.schema_locator);
        krabs::parser parser(schema);
		    uint32_t ppid = parser.parse<uint32_t>(L"ParentProcessID");
        // record.EventHeader.ProcessId is the creator process ID
        // Check whether the PPID is the same as the creator PID
        if (ppid != record.EventHeader.ProcessId) { 
            uint32_t pid = parser.parse<uint32_t>(L"ProcessID");
            std::wstring image_name = parser.parse<std::wstring>(L"ImageName");

            // Get the image name from the real parent process ID
            std::wstring real_parent_process_name = GetProcessName(record.EventHeader.ProcessId);
            std::wstring fake_parent_process_name = GetProcessName(ppid);

            std::wcout << L"[!] Process PPID Spoofing Detected" << std::endl;
            std::wcout << L"[>] Real Parent: " << real_parent_process_name << std::endl;
            std::wcout << L"[>] Fake Parent: " << fake_parent_process_name << std::endl;
            std::wcout << L"[>] Process Name: " << image_name << std::endl;
            std::wcout << L"[>] ProcessID " << pid << std::endl;
            std::wcout << std::endl;
        }
    };

    // real-time process start events
    krabs::event_filter process_filter(krabs::predicates::id_is(1));
    process_filter.add_on_event_callback(process_callback);
    provider.add_filter(process_filter);

    trace.enable(provider);
    trace.start();
}

int main()
{
    std::cout << "[+] Monitoring Starting!\n" << std::endl;
    begin_trace();
}

As the Microsoft-Windows-Kernel-Process is a kernel trace, subscribing requires admin rights. In this case I used PsExec to spawn a SYSTEM shell before executing the compiled binary.

Detection Demo

References

Last updated