Sysmon Image File Name Evasion
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:
The blue arrow points to a call to ZwQueryInformationProcess
with the ProcessBasicInformation
value to retreive a 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:
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:
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:
Within GetProcessParameterString
, it performs the same ProbeForRead
functionality as before:
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:
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:
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
Event ID 23 - FileDelete
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)
Event ID 12 - RegistryEvent (Add or Delete)
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:
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:
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
:
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:
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:
This means that we don't have to modify the original module's entry, we can append a fake one.
The resulting log now reflects the fake process name in the CallTrace value:
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:
An example trigger here would be creating a file in the Downloads directory like so:
Now let's look at the exclusions for this event:
If our image name is C:\Windows\system32\smss.exe
, Sysmon would not log the event. Can we bypass Sysmon from logging?
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:
This function is only triggered when GetProcessInfo
fails. Let's see how it retrieves the image file name:
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:
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...