Sep 6, 2022

Attacks on Sysmon Revisited - SysmonEnte

This was originally posted on blogger here.

In this blogpost we demonstrate an attack on the integrity of Sysmon which generates a minimal amount of observable events making this attack difficult to detect in environments where no additional security products are installed.

tl;dr:

  • Suspend all threads of Sysmon.
  • Create a limited handle to Sysmon and elevate it by duplication.
  • Clone the pseudo handle of Sysmon to itself in order to bypass SACL as proposed by James Forshaw.
  • Inject a hook manipulating all events (in particular ProcessAccess events on Sysmon).
  • Resume all threads.

We also release a POC called SysmonEnte.

Background

At Code White we are used to performing complex attacks against hardened and strictly monitored environments. A reasonable approach to stay under the radar of the blue team is to blend in with false positives by adapting normal process- and user behavior, carefully choosing host processes for injected tools and targeting specific user accounts.

However, clients with whom we have been working for a while have reached a high level of maturity. Their security teams strictly follow all the hardening advice we give them and invest a lot of time in collecting and base-lining security related logs while constantly developing and adapting detection rules.

We often see clients making heavy use of Sysmon, along with the Windows Event Logs and a traditional AV solution. For them, Sysmon is the root of trust for their security monitoring and its integrity must be ensured. However, an attacker who has successfully and covertly attacked, compromised the integrity of Sysmon and effectively breaks the security model of these clients.

In order to undermine the aforementioned security-setup, we aimed at attacking Sysmon to tamper with events in a manner which is difficult to detect using Sysmon itself or the Windows Event Logs.

Attacks on Sysmon and Detection

Having done some Googling on how to blind Sysmon, we realized that all publicly documented ways are detectable via Sysmon itself or the Windows Event Logs (at least those we found) :

While we were confident that we can kill Sysmon before throwing Event ID 5 (Process terminated) we thought that a host not sending any events would be suspicious and could be observed in a client’s SIEM. Also, loading a signed, whitelisted and exploitable driver to attack from Kernel land was out of scope to maintain stability.

Since all of these documented attack vectors are somehow detectable via Sysmon itself, the Windows Event Logs or can cause stability issues we needed a new attack vector with the following capabilities:

  1. Not detectable via Sysmon itself
  2. Not detectable via Windows Event Log
  3. Sysmon must stay alive
  4. Attack from usermode

Injecting and manipulating the control flow of Sysmon seemed the most promising.

Attack Description

Similarly to SysmonQuiet or EvtMute, the idea is to inject code into Sysmon which redirects the execution flow in such a way that events can be manipulated before being forwarded to the SIEM.

However, the attack must work in such a way that corresponding ProcessAccess events on Sysmon are not observable via Sysmon or the Event Log.

This presents various problems, but let us first see where such a hook would be applicable.

Manipulating the Execution Flow

Sysmon forwards events to ETW subscribers via the documented function ntdll!EtwEventWrite. This is easily observable by setting an appropriate breakpoint.

The function has the following prototype:

ULONG 
EVNTAPI
EtwEventWrite(
    __in REGHANDLE RegHandle,
    __in PCEVENT_DESCRIPTOR EventDescriptor,
    __in ULONG UserDataCount,
    __in_ecount_opt(UserDataCount) PEVENT_DATA_DESCRIPTOR UserData
    );

The two most important arguments to the function are EventDescriptor and UserData.

typedef struct _EVENT_DESCRIPTOR {
  USHORT    Id;
  UCHAR     Version;
  UCHAR     Channel;
  UCHAR     Level;
  UCHAR     Opcode;
  USHORT    Task;
  ULONGLONG Keyword;
} EVENT_DESCRIPTOR, *PEVENT_DESCRIPTOR;

The Id field of the EVENT_DESCRIPTOR determines the type of event and is important to apply the correct struct definition for the event data pointed to by PEVENT_DATA_DESCRIPTOR. The structs for the different events are obviously different for each Sysmon Event Id, as different fields and information are included. Our injected code must thus be able to apply the correct struct depending on which event is being emitted by Sysmon.

But how do we know the definition of the event structs? Luckily, ETW Explorer has already documented the event definitions:

A definition for the userdata struct describing a ProcessAccess event might therefore look as follows:

typedef struct _ProcessAccess
{
	wchar_t* pRuleName;
	size_t sizeRuleName;
	wchar_t* pUtcTime;
	size_t sizeUtcTime;
	void* psrcGUID;
	size_t sizesrcguid;
	void* ppidsrc;
	size_t sizepidsrc;
	void* ptidsrc;
	size_t sizetidsrc;
	wchar_t* psourceimage;
	size_t sizesourceimage;
	void* ptarGUID;
	size_t sizetarGUID;
	void* ppiddest;
	size_t sizepiddest;
	wchar_t* ptargetimage;
	size_t sizetargetimage;
	PACCESS_MASK pGrantedAccess;
	size_t sizeGrantedAccess;
	wchar_t* pCalltrace;
	size_t sizecalltrace;
	wchar_t* pSourceUser;
	size_t sizeSourceUser;
	wchar_t* pTargetUser;
	size_t sizetargetUser;

} ProcessAccess, *PProcessAccess;

We can validate this in x64dbg by setting a breakpoint at ntdll!EtwEventWrite and applying the said struct definition for a ProcessAccess event.

Faking events

ntdll!EtwEventWrite being responsible for forwarding events is a good place to install a hook to redirect the control flow to injected code which first manipulates the event and then forwards it:

The injected code manipulating the events might look like this:


//Hooked EtwEventWrite Function
ULONG Hook_EtwEventWrite(REGHANDLE RegHandle, PCEVENT_DESCRIPTOR EventDescriptor, ULONG UserDataCount, PEVENT_DATA_DESCRIPTOR UserData) {

    //Get the address of the EtwEventWriteFull Function
    _EtwEventWriteFull EtwEventWriteFull = (_EtwEventWriteFull)getFunctionPtr(CRYPTED_HASH_NTDLL, CRYPTED_HASH_ETWEVENTWRITEFULL);
    if (EtwEventWriteFull == NULL) {
        goto exit;
    }

    //Check if it is a process access event and needs to be tampered with
    switch (EventDescriptor->Id) {
    case EVENT_PROCESSACCESS:
        HandleProcessAccess((PProcessAccess)UserData);
        break;
    default:
        break;
    }

    //Save the event with the EtwEventWriteFull Function
    EtwEventWriteFull(RegHandle, EventDescriptor, 0, NULL, NULL, UserDataCount, UserData);

exit:
    return 0;

}


// Make ProcessAccess events targeting Sysmon itself look benign
VOID HandleProcessAccess(PProcessAccess pProcessAccess) {

	ACCESS_MASK access_mask_benign = 0x1400;
	PCWSTR wstr_sysmon = L"Sysmon";
	PCWSTR wstr_ente = L"Ente";

	//Sysmon check
	psysmon = StrStrIW(pProcessAccess->ptargetimage, wstr_sysmon);
	if (psysmon != NULL) {

		//Replace the access mask with 0x1400
		*pProcessAccess->pGrantedAccess = access_mask_benign;
		pProcessAccess->sizeGrantedAccess = sizeof(access_mask_benign);
		//Replace the Source User with Ente
		lstrcpyW(pProcessAccess->pSourceUser, wstr_ente);
		pProcessAccess->sizeSourceUser = sizeof(wstr_ente);
		
	}

}

Note, how ntdll!EtwEventWriteFull is used to forward every event.

Since we know where to inject the hook and what the UserData structs look like, we are now able to tamper with every Sysmon event before it is forwarded.

However, the injection into Sysmon remains observable and the corresponding ProcessAccess event is the last event we do not control.

Detection of Process Manipulation

OpenProcess Access event

In order to create a handle to Sysmon which allows us to conduct process injection of any kind, we need to open Sysmon with at least the following access mask: PROCESS_VM_OPERATION | PROCESS_VM_WRITE. As Sysmon has not yet been modified while we open this handle, a suspicious ProcessAccess Event is generated which is an IOC defenders could hunt for:

Handle Elevation

Playing with kernel32!DuplicateHandle for another project, we noticed that MSDN states something very interesting:

In some cases, the new handle can have more access rights than the original handle.

Thus, by first creating a handle with a very limited access mask and then duplicating this handle with a new access mask, we technically do not create a new handle with a high access mask.

HANDLE hSysmon = NULL;
HANDLE hhighpriv = NULL;
BOOL bsuccess = FALSE;

hSysmon = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, 3340);
bsuccess = DuplicateHandle(GetCurrentProcess(), hSysmon, GetCurrentProcess(), &hhighpriv, PROCESS_ALL_ACCESS, FALSE, 0);

Sysmon, (to the best of our knowledge) only using OB_OPERATION_HANDLE_CREATE, only sees the benign access mask, but not the duplication of the handle with a higher access mask:

Using handle elevation we can gain handles with arbitrary process access masks to arbitrary (non-ppl) processes while Sysmon only logs the instantiation of the original handle. Great Success!

Unfortunately, there are some problems:

  1. This only works if the targeted process runs as the same user as the duplicating process.

This can be easily circumvented by stealing a token from a System process. We steal the token from an elevated svchost process running as System by only using a PROCESS_QUERY_LIMITED_INFORMATION mask, where we do not need the SE_DEBUG privilege which is often used in detection rules.

  1. System Access Control Lists (SACL). This is a bigger problem

Detection via System Access Control Lists (SACL)

Unfortunately, it is still possible to observe the duplication of the handle by configuring Object Access Auditing using a SACL on Sysmon. The following screenshot shows how ProcessHacker is leveraged to configure the SACL:

With this SACL, event 4656 is generated by the Windows Event Log Service upon creation of a handle to Sysmon allowing to write in its memory.

This event is also emitted, if handle elevation is used.

Note: In the default config, Object Access Auditing is not enabled.

SACL Bypass by James Forshaw

Fortunately for us, James Forshaw published a great blogpost on how to evade SACL.

According to the post, we can duplicate the pseudo handle of a different process to itself to get full access to the process without triggering Object Access Auditing.

A stealthy way to gain a handle suitable for process injection would be the following:

  1. Open a process handle to Sysmon with a very limited access mask (A detection rule based on this would generate too many false positives)
  2. Elevate this handle using ntdll!DuplicateObject to hold the PROCESS_DUP_HANDLE right (Bypasses Sysmon’s telemetry)
  3. Use the elevated handle to duplicate the pseudo Handle of Sysmon (Bypasses SACL).
uPid.UniqueProcess = dwPid;
uPid.UniqueThread = 0;

ntStatus = NtOpenProcess(&hlowpriv, PROCESS_QUERY_LIMITED_INFORMATION, &ObjectAttributes, &uPid);
if (!NT_SUCCESS(ntStatus)) 
	FATAL("[-] Failed to open low priv handle to sysmon\n");

ntStatus = NtDuplicateObject(NtCurrentProcess(), hlowpriv, NtCurrentProcess(), &hduppriv, PROCESS_DUP_HANDLE, FALSE, 0);
if (!NT_SUCCESS(ntStatus)) 
	FATAL("[-] Failed to elevate to handle with PROCESS_DUP_HANDLE rights\n");

ntStatus = NtDuplicateObject(hduppriv, NtCurrentProcess(), NtCurrentProcess(), &hhighpriv, PROCESS_ALL_ACCESS, FALSE, 0);
if (!NT_SUCCESS(ntStatus)) 
	FATAL("[-] Failed to elevate to handle with PROCESS_ALL_ACCESS rights\n");

Doing so we gain a full access handle to Sysmon while bypassing Sysmon’s telemetry and SACL.

Fine Tuning

There was one last IOC we could come up with. Sysmon can only observe the creation of a limited handle to itself, however, following the golden rule of never touching disk, our tool being unpacked or injected into another process will have a broken calltrace containing unknown sections. Since Sysmon has not been tampered with at this point, this would be the last event which we do not have under control and might be sufficient to create a detection rule upon!

We can delay the forwarding of this event by suspending all threads of Sysmon. The events are then queued and dispatched only after we resume the threads, giving us enough time to install a hook manipulating all ProcessAccess events on Sysmon itself. This is possible, because no events for accessing, suspending or resuming a thread exist in Sysmon.

The hook then necessarily spoofs the callstack included in the ProcessAccess event.

Putting It All Together

We combined all of these steps into a tool we call SysmonEnte which you can find on our Github.

SysmonEnte is implemented as fully position independent code (PIC) which can be called using the following prototype:

    DWORD go(DWORD dwPidSysmon);

A sample loader is included and built during compilation when typing make.

Additionally, SysmonEnte uses indirect syscalls to bypass userland hooks while injecting into Sysmon.

The open source variant tampers with process access events to Lsass and Sysmon and sets the access mask to a benign one. Additionally, the source user and the callstack is set to Ente. You can change these to your needs.

Possible Detection Methods

Certain detection ideas exist from our point of view:

ETW TI

The easiest solution would be to subscribe to the Threat Intelligence ETW provider to observe injections or suspicious code manipulations. This however requires a signed ELAM driver.

Kernel Callbacks

If you have the possibility to run as a kernel driver you can probably implement the callback for the OB_OPERATION_HANDLE_DUPLICATE to monitor for Object Access Auditing Bypasses. https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_ob_operation_registration

Object Access Auditing

If you have the possibility to enable Object Access Auditing, you can configure a SACL for Sysmon to monitor the duplication of handles to catch the SACL bypass used to gain a handle to Sysmon. We are not sure about false positives in large environments though.

To the best of our knowledge, and in contrast to SACLs for filesystem- or registry operations, configuring Object Access Auditing on processes is only achievable by writing a custom program. This circumstance makes the detection of handle duplication via SACL non-trivial.

A sample program is included on our GitHub and configures a SACL with ACCESS_SYSTEM_SECURITY + PROCESS_DUP_HANDLE + PROCESS_VM_OPERATION and is applied to the group Everyone. ACCESS_SYSTEM_SECURITY is included as otherwise, attackers can covertly change the SACL.

With this configuration, attempts to duplicate a handle to Sysmon should become visible.

Note: Object Access Auditing is not enabled by default and must be enabled via Group Policy prior the use of the tool.

Final Words

Sysmon on its own is not able to protect itself sufficiently, and it is difficult to observe the described attack with the event log.

We believe that running Sysmon alone, without any protection from a trusted third party tool sitting in kernel land or running as a PPL, is not guaranteed to produce reliable logs with ensured integrity. A possible fix by Microsoft would be to allow running Sysmon as a PPL.

It is noteworthy that the described technique of handle elevation + SACL bypass can also be used to stealthily dump Lsass.

After our talk at X33fcon, nanodump supports handle elevation as well. However, a SACL with PROCESS_VM_READ is configured for Lsass by default. ;-)

References