One of my side projects for understanding the Windows kernel and driver development includes research into the Sysmon driver. After having read some weird methods of how other drivers access processes' image file names on Twitter and in Bill Demirkapi's How to use Trend Micro's Rootkit Remover to Install a Rootkit blog post, I decided I should investigate further into what Sysmon did too. And so, the result is this post which looks at how Sysmon does it and what it does is mind-boggling. As for the how, I hope someone else can provide that for me!

Software versions and testing environments:

  • SysmonDrv version 11.0
  • SysmonDrv version 10.42
  • Windows 10 x64 version 2004

Discovery

My research into the Sysmon driver begins at version 10.42 (just a little bit outdated). I was trying to look into how Sysmon handles process access events in the ObRegisterCallbacks' post operation routine. This eventually led me to the function - that I will name GetProcessInfo - which is called in the event that a process has been detected to access another process:

GetProcessInfo to query the source process' image name

The blue arrow points to a call to ZwQueryInformationProcess with the ProcessBasicInformation value to retreive a PROCESS_BASIC_INFORMATION structure:

typedef struct _PROCESS_BASIC_INFORMATION {
    PVOID Reserved1;
    PPEB PebBaseAddress;
    PVOID Reserved2[2];
    ULONG_PTR UniqueProcessId;
    PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;
PROCESS_BASIC_INFORMATION structure

At offset +8 (rbp-11h) in the ProcessInformation return value parameter (rbp-19h) lies the PebBaseAddress member which points to the Process Environment Block (PEB). This is passed into the GetProcessInfo function along with three other UNICODE_STRING values that indicate the source process' image file name, current directory, and command line. The last two strings are NULL so they do not return any value.

Inside the GetProcessInfo function, the driver attaches to the source process and reads the PEB:

Reading and copying the process' PEB structure

In the blue, the rbx register gets set to the PebBaseAddress value and then it is read from using ProbeForRead and what looks to be an optimised RtlCopyMemory. Next, it will read and copy the ProcessParameters member from the PEB structure:

Reading and copying the process' PEB's ProcessParameters member

After this, it calls an internal function that I've named GetProcessParameterString which takes both the recently read PEB and its ProcessParameters member. This specific call shown here also retrieves the ProcessParameters' ImagePathName member:

Retrieves ImagePathName from the PEB's ProcessParameters.

Within GetProcessParameterString, it performs the same ProbeForRead functionality as before:

GetProcessParameterString reads and copies ProcessParameters members

Returning back to the function that called GetProcessInfo, we can see that the CurrentProcessImageFileName variable is copied into the event data structure to be logged:

Copying source image process name into event structure for logging

POC

The PoC is as simple as:

PPEB Peb = (PPEB)__readgsqword(0x60);
PRTL_USER_PROCESS_PARAMETERS ProcessParameters = Peb->ProcessParameters;
UNICODE_STRING FakeImagePathName = { 
    0x8, 0x8,
    L"Test"
};
    
ProcessParameters->ImagePathName = FakeImagePathName;

That's literally it.

Affected Events

So now that I know this function exists and takes multiple parameters (some are NULL'd out in what I've shown), I thought that surely there must be more uses of it elsewhere. Lo and behold, it is:

Affected events using GetProcessInfo

IDA's proximity view comes especially in handy here showing which functions lead to GetProcessInfo. File events, registry, and process access events are all affected, with varying degrees of impact on the event data which we shall see soon. Although the thread notification callback routine uses this function, the reason I've not highlighted is that the process cannot change its PEB data before control can be gained by the process (unless someone knows a way to do this too).

Luckily, there haven't been many changes between Sysmon 10.42 and 11.0 - I believe most of them were for the new file archiving functionality - so the issue persists.

Event ID 11 - FileCreate

FileCreate event with faked image

Event ID 23 - FileDelete

FileDelete event with faked image

While both of these file events can have false image values, the target file object's path cannot be modified.

Event ID 23 - RegistryEvent (Set Value)

Registry set value event with faked image

Event ID 12 - RegistryEvent (Add or Delete)

Registry add or delete event with fake image

Similar to the file events, the target registry object cannot be changed.

ProcessAccess

The ProcessAccess event has an additional CallTrace element that tracks the call stack. If we try the same PEB trick, we get the following:

ProcessAccess event with faked source image

Here, the CallTrace values reveal the true source image path. These values are obtained with the RtlWalkFrameChain function. The reason why I have included \Downloads\ in the string is so that Sysmon will trigger the event for demonstration purposes.

To get the image names, Sysmon enumerates the linked list of modules within the PEB for each address within the call stack. It will read the PEB's PEB_LDR_DATA structure first:

Read PEB's PEB_LDR_DATA structure

Then it will call the function - I named it GetBackTraceModuleInfo - passing in the PEB_LDR_DATA, InMemoryOrderModuleList list, the address to query (BackTraceEntry), and ModuleInfo:

Calling GetBackTraceModuleInfo

The proprietary ModuleInfo structure contains the following information:

typedef struct _MODULE_INFO {
    /*   0 */ WCHAR FullDllName[260];   // Module name.
    /* 208 */ PVOID Reserved;
    /* 210 */ PVOID DllBase;
    /* 218 */ ULONG SizeOfImage;
    /* 21C */ BOOLEAN IsWow64;
    /* 220 */ PVOID BackTraceAddress;
} MODULE_INFO, * PMODULE_INFO;

This function performs the module list enumeration to locate the module which contains the BackTraceEntry address. It is tested against DllBase and DllBase + SizeOfImage obtained from the LDR_DATA_TABLE_ENTRY structure:

Checking BackTraceEntry's address

Since this depends on user-mode data, it can be falsified just like the PEB's image file name. An interesting discovery is that the module list is iterated until either the end of the list is reached or if the enumeration hits 512 entries, whichever first:

List enumeration loop condition

This means that we don't have to modify the original module's entry, we can append a fake one.

PLIST_ENTRY MemList = Peb->Ldr->InMemoryOrderModuleList.Flink;
PLDR_DATA_TABLE_ENTRY SelfTableEntry = NULL;

for (ULONG_PTR i = 0; MemList != &Peb->Ldr->InMemoryOrderModuleList; i++) {
	PLDR_DATA_TABLE_ENTRY Ent = CONTAINING_RECORD(
		MemList,
		LDR_DATA_TABLE_ENTRY,
		InMemoryOrderLinks
	);
    
	if (!_wcsicmp(Ent->FullDllName.Buffer, argv[0])) {
		SelfTableEntry = Ent;
	}

	MemList = MemList->Flink;
}

LDR_DATA_TABLE_ENTRY FakeTableEntry;
if (SelfTableEntry) {
	//
	// Copy own module's image size and DLL base
	// to trick Sysmon.
	//
	FakeTableEntry.DllBase = SelfTableEntry->DllBase;
	//
	// SizeOfImage.
	//
	FakeTableEntry.Reserved3[1] = SelfTableEntry->Reserved3[1];
	//
	// Fake the image name.
	//
	FakeTableEntry.FullDllName = FakeImagePathName;

	//
	// Append to module list.
	//
	FakeTableEntry.InMemoryOrderLinks.Blink = MemList;
	FakeTableEntry.InMemoryOrderLinks.Flink = &Peb->Ldr->InMemoryOrderModuleList;
	MemList->Blink->Flink = &FakeTableEntry.InMemoryOrderLinks;
	Peb->Ldr->InMemoryOrderModuleList.Blink = &FakeTableEntry.InMemoryOrderLinks;
}
Appending fake LDR_DATA_TABLE_ENTRY module entry

The resulting log now reflects the fake process name in the CallTrace value:

Fake image name in CallTrace

Bypassing Logging

In the event where events have exclusions based on image names, it's possible to forge the image names using the above technique and stop Sysmon from logging the event entirely. Let's look at an example.

SwiftOnSecurity's Sysmon configuration file contains the following inclusions for the FileCreate event:

FileCreate event inclusion triggers

An example trigger here would be creating a file in the Downloads directory like so:

Creating a file in the Downloads directory logged by Sysmon

Now let's look at the exclusions for this event:

FileCreate event exclusions

If our image name is C:\Windows\system32\smss.exe, Sysmon would not log the event. Can we bypass Sysmon from logging?

Bypassing Sysmon's FileCreate event with faked image

We can see that Sysmon doesn't log the FileCreate event. Success!

Bonus Process Access Method

One of the sections in Bill Demirkapi's Trend Micro post discusses the EPROCESS ImageFileName Offset. If we have a look at the disassembly after the GetProcessInfo call from the ObRegisterCallbacks' post operation, we can see the GetProcessImageFileNameByHandle function:

GetProcessImageFileNameByHandle fallback method

This function is only triggered when GetProcessInfo fails. Let's see how it retrieves the image file name:

GetProcessImageFileNameByHandle function

The blue highlights the CurrentProcessImageFileName parameter which can be seen to receive a UNICODE_STRING pool buffer at the bottom. In the red, we can see that the global variable g_ProcessNameOffset is added onto the PEPROCESS object returned by ObReferenceObjecyByHandle. If we trace the origin of g_ProcessNameOffset, we get the following:

g_ProcessNameOffset origin

This essentially translates to:

PEPROCESS Process = IoGetCurrentProcess();

for (int g_ProcessNameOffset = 0; g_ProcessNameOffset < 0x3000; g_ProcessNameOffset++) {
    if (!strncmp("System", (PUCHAR)Process + g_ProcessNameOffset, strlen("System")) {
    	break;
    }
}

Of course there is proper API to access the ImageFileName offset in the EPROCESS structure (ZwQueryInformationProcess with ProcessImageFileName). So why does it exist? @analyzev notes on this Twitter thread that this function dates back to this RegMon source and it matches exactly with Sysmon's.

I believe this method of retrieving the image name can produce false results. If the file on disk is renamed or moved, the changes may not be reflected in the EPROCESS structure.

Conclusion

It's interesting to see critical data being retrieved in an unreliable and user-controlled way. I'm curious as to what impact this may have from a detection and forensics point of view. Events may slip by unnoticed or certain alerts may not fire if rules do not match where image names are used. Of course Sysmon shouldn't be the only source of logging and some of the affected events are not entirely untrustworthy so the overall effect may not be so concerning. But something to think about...