Inside Windows Defender System Guard Runtime Monitor
What is System Guard Runtime Monitor? (SGRM)
System Guard Runtime Monitor (SGRM) is a component of Windows Defender (WD), that was introduced in the Windows 10 1709 update and has been present since as a key component to ensure system integrity.
Another name for this component is Octagon, which is assumed to be an internal project name for Microsoft, where System Guard Runtime Monitor is used as the public name for marketing Windows Defender. For SGRM to work, a device must have Virtual Secure Mode enabled on their system, as the protection makes use of Virtual Trust Levels to minimize the attack surface on the core attestation Lua engine. Microsoft, in this 2018 blog, describe SGRM as "If important security features should fail, users should be aware. Windows Defender System Guard runtime attestation, a new Windows platform security technology, fills this need"
. Simply put, SGRM is an anti-tampering mechanism for your modern Windows device.
In this post, I'll go into the details behind how SGRM works, the Lua component, integrity checks performed, the RPC service, and more from a brief reverse engineering standpoint of this WD component.
Components
We'll quickly take a look at some of the components of SGRM, to provide some context before going through the details of each individual component.
Component | Usage |
---|---|
SgrmBroker.exe | Provides a client API, exposing assists to the SGRM runtime when doing assertions. |
Sgrm.sys | The agent driver, exposes functionality for use within the assertion assists wrappers used by SgrmBroker. |
SgrmEnclave.dll | Lua assertion engine, also called the enclave controller shim, contains the Lua runtime, SgrmEnclave_secure.dll runs in VTL-1, or another mode of operation. Talks to SgrmBroker.exe via the API. |
SgrmLpac.exe | A local RPC service, which exposes a method to send an HTTP POST request to a specified endpoint |
SgrmAgent.sys
Driver Boot
The agent driver, %WINDIR%\System32\drivers\SgrmAgent.sys
, provides kernel-level assists to the SgrmBroker assist engine, which runs in user-mode.
Upon driver boot new secure device is created under \\Device\\MSSGRMAGENTSYS
, with a symbolic link pointing to this device under \\??\\MSSGRMAGENTSYS
. An Event Tracing (Etw) provider is registered under Microsoft.Windows.Oct.Driver
. The provider receives violations for checks the driver implements are sent to throughout the execution.
After this has completed, the driver requests a kernel extension host from ntoskrnl, using the (mostly undocumented) API ExRegisterHost
, under PspOctExtensionHost
, from PspOctExtensionInterface
. The registering of this host provides straight up, direct kernel access to certain functions, almost as if the agent code was running within ntoskrnl itself. A table of extension functions is initialized with a pool tag of 0x2000D
:
.data:00000001C00088A8 g_ExtensionFunctions dq 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0; 0
.data:00000001C00088A8 ; DATA XREF: OctpInitialize:loc_1C000B25D↓o
.data:00000001C00088A8 ; OctpInitialize+26E↓w ...
Initialization
To initialize the driver, an IOCTL 0x9C402480
must be sent to the driver. This then calls the internal function OctpHandleInitRequest
.
The driver has several checks in place to ensure the integrity of the user-mode caller:
- The device is bound to the SgrmBroker (OctBroker) service SID upon creation with
WdmlibIoCreateDeviceSecure
"D:P(A;;GRGWGX;;;S-1-5-80-3706850399-3459138796-2835936764-562029542-397710147)
- Within the dispatch routine, the driver can only be initialized once, resulting only in a single handle at a time effectively being open
- Only Windows TCB signed processes (not to be confused with Thread Control Block) can open a
HANDLE
(Windows Defender runs as a Protected Process Light (PPL) under the anti-malware context, the trust level would not suffice)
For example, below, if the driver has already been initialized in OctpHandleInitRequest
, the driver will write an event to the abovementioned Etw provider and deny access.
// Global g_initialized flag check ≠ 0
if ( _InterlockedCompareExchange(&g_initialized, 1, 0) == 1 )
{
EtwWriteTransfer(1, g_SgrmDescriptor, {"OctpHandleInitRequest", "SGRM device was already initialized!"}, ...);
}
Agent Mailbox
As previously mentioned, the driver exposes functionality to SgrmBroker
. The IOCTL 0x9C402484
is used to dispatch to the specified function within the driver mailbox.
struct _SGRM_MAILBOX_REQ {
UINT8 u8Index;
DWORD dwArgNumb;
[function arguments]
} *PSGRM_MAILBOX_REQ, SGRM_MAILBOX_REQ;
The list of driver assist functions can be observed in the table below.
The specific mailbox value is passed in a struct, to SgrmAgent.sys's OctpMailboxDispatcher
. Below, is an excerpt of the function targets that can be accessed by the user-mode agent for utility:
Idx | Mailbox Target | Description |
---|---|---|
0x1 | OctpHandleMapVirtualAddress | Map a specified virtual address (including kernel mode) |
0x2 | OctpHandleUnmapVirtualAddress | Unmap a specified virtual address |
0x13 | OctpHandleGetThreadContext | Get the context of specified thread |
0x15 | OctpHandleGetMsrValue | Read the value of a specified MSR |
0x12 | OctpHandleGetMemoryRegionInfo | Get the information of a specified memory region |
0xD | OctpHandleFreezeThread | Freeze a specified thread |
0xE | OctpHandleThawThread | Similar to freeze thread |
0xF | OctpHandleCopyMemory | Copy memory (including physical), destination, source, and size. |
0x10 | OctpHandleMapFile | Map a specified file |
0x11 | OctpHandleUnmapFile | Unmap a specified file |
0xC | OctpHandleGetStructureOffsetSize | Get the offset of a specified structure, e.g. EPROCESS |
Along with providing these utilities, the agent provides access to these objects to the broker through the mailbox for runtime inspection via the engine on demand:
EPROCESS
ETHREAD
DRIVER_OBJECT
TOKEN
SgrmBroker.exe
Service Start
SgrmBroker.exe
is one of the user-mode components of the System Guard Runtime Monitor, and runs as a service named OctBroker
(or, SgrmBroker
). The service is started as a delayed service for performance reasons, and has a trigger to spawn the client API service upon boot. The process runs as a signed Windows Trusted Computing Base (TCB) PPL.
When the service starts, the following is applied to the process, for performance and security:
- Set the current thread priority to
THREAD_MODE_BACKGROUND_BEGIN
- Set the
PROCESS_INFORMATION_CLASS
of the current process toProcessEnableLogging
- Enable ASLR using
ProcessASLRPolicy
- Enable handle checking using
ProcessStrictHandleCheckPolicy
- Set the heap to terminate upon corruption using
HeapEnableTerminationOnCorruption
The broker then does some core initialization for general functionality, such as setting up an Etw handler. The LAPC component (SgrmLpac.exe
) is programmatically spawned within the initialization function, all within the LpacHost.exe
class. A job is created for it through CreateJobObjectA
, and the handle of the LpacHost.exe
process is bound to it. This class provides the functionality to spawn, terminate, and use the HTTP POST RPC endpoint. After the RPC server has spawned, if the process is terminated, LpacHost::LpacTerminatedCallback
is called which closes all of the RPC connections. The details of this component are examined in the SgrmLpac section of this blog post.
Once the core initialization has completed, a path to %WINDIR%\System32\Sgrm
is constructed. To ensure the integrity of this directory, an event is created DirectoryWatcher
class to watch for tampering efforts that may take place to the files in this directory. A thread is then spawned in the background to watch the directory under DirectoryWatcher::StartWatch
. If one of the watched files is altered, an Etw write event is triggered. The files in this directory are detailed below:
File | Usage |
---|---|
SgrmAssertions.bin | Compiled Lua script, with Microsoft-defined header, which contains the assertions |
SgrmAssertions.cat | Certificate used to verify the assertions that the engine makes, to ensure they are legitimate |
Then, the RPC client API is initialized as a local service (ncalrpc
) within OctagonRpcInitialize
.
Initialization
The broker attempts to open a handle to the agent driver - \\\\.\\MSSGRMAGENTSYS
. If it is not successful, possibly indicating a malicious handle to the driver, an Etw event is triggered. If this is successful, the initialization IOCTL is sent (0x9C402480
) via to the fast I/O control port in the driver. If the driver has already been initialized, as discussed in the previous section, this will fail.
The number of bytes returned from the DeviceIoControl
call must be >= 0x11D
, if not, a ValidateResponseSize
Etw event is triggered. A structure is populated with information from the OctpGetNtosInfo
within the driver. The OctpGetNtosInfo
queries the SystemModuleInformationEx
class by calling ZwQuerySystemInformation
to populate the undocumented RTL_PROCESS_MODULE_INFORMATION_EX
structure, only certain applicable members are used in the resultant buffer.
// Credit: ProcessHacker
typedef struct _RTL_PROCESS_MODULE_INFORMATION_EX
{
USHORT NextOffset;
struct _RTL_PROCESS_MODULE_INFORMATION
{
HANDLE Section;
PVOID MappedBase;
PVOID ImageBase;
ULONG ImageSize;
ULONG Flags;
USHORT LoadOrderIndex;
USHORT InitOrderIndex;
USHORT LoadCount;
USHORT OffsetToFileName;
UCHAR FullPathName[256];
} BaseInfo;
ULONG ImageChecksum;
ULONG TimeDateStamp;
PVOID DefaultBase;
} RTL_PROCESS_MODULE_INFORMATION_EX, *PRTL_PROCESS_MODULE_INFORMATION_EX;
Virtual Secure Mode Enclave Host Controller
The broker has a helper class named EnclaveControllerVsm
in which SgrmEnclave_secure.dll
resides. SgrmEnclave_secure.dll
is the assertion engine, with the Lua engine embedded. Upon initialization, EnclaveControllerVsm
checks if the enclave ENCLAVE_TYPE_VBS
is supported by the processor. If supported, a enclave is then created, and the assertion engine is loaded into it. The details of the assertion engine can be found in the SgrmEnclave_secure.dll
section of this blog post.
The secure enclave is then initialized, and a pointer to EngHostAttest
is resolved from the global array of function names (g_engHostFunctionMappings
), and called.
The shim DLL (SgrmEnclave.dll) is also initialized within EnclaveControllerShim
, EnclaveControllerShim
. EngHostAttest
is then resolved and called from the same function table. This does not run in an VSM enclave.
Modes Of Operation
VTL-1 is not always avaliable on a machine, for example an older system may not support Virtual Secure Mode or have a TPM chip. Below are the modes in which the enclave can run in when verifying the result of an attestation.
# | Mode | Description |
---|---|---|
1 | Virtualization Based Security (VBS) + Dynamic Root of Trust for Measurement (DTRM) | Keys used to verify attestation placed in the TPM's PCR registers, allocated within $pcr17-23. |
2 | VBS | Keys used to verify attestation use host keys |
3 | None | No attestation, no keys used - not supported by the host |
Assists
The broker provides a number of assists which the assertion engine requires, as it is simply a Lua script. The assists are needed for the script to interact with the system to make the assertions based of certain system information. These assists are stored and initialized at runtime, and passed to the assertion engine upon startup. They are stored in a table named g_BrokerAssistCallbackTbl
of type BROKER_CALLBACK_TABLE
, and use the following format.
struct _BROKER_CALLBACK_TABLE g_BrokerAssistCallbackTbl
?g_BrokerAssistCallbackTbl@@3U_BROKER_CALLBACK_TABLE@@A dq offset ?
dq offset ?Broker_CancelTimer@Assists@@SAPEAXPEAX@Z ; Assists::Broker_CancelTimer(void *)
dq offset ?Broker_CloseHandle@Assists@@SAPEAXPEAX@Z ; Assists::Broker_CloseHandle(void *)
dq offset ?Broker_CreateEvent@Assists@@SAPEAXPEAX@Z ; Assists::Broker_CreateEvent(void *)
Some assists are simply user-mode calls, such as registry reads, and some require using the SgrmAgent.sys
driver for extended functionality.
Assists which require the usage of the agent driver are prefixed with Assists::Agent_
, an excerpt of these mapped to the internal agent functions are below.
Broker Assist Function | Driver Mailbox Function |
---|---|
Assists::Agent_ReadMsr |
OctpHandleGetMsrValue |
Assists::Agent_ThawThread |
OctpHandleThawThread |
Assists::Agent_UnmapFile |
OctpHandleUnmapFile |
Assists::Agent_MapVirtualAddress |
OctpHandleMapVirtualAddress |
As mentioned, some of the assists that can be done in user-mode, and don't require use of the driver. These are appended with Assists::Broker_
. For example, the assist for a Sleep
is as trivial as it sounds:
void Assists::Broker_Sleep(DWORD *dwSleepMs)
{
Sleep(*dwSleepMs);
return 0;
}
SgrmEnclave_secure.dll
The engine relies on the broker to load the Lua script from disk, meaning the DLL which is within the Secure Enclave does not perform any file I/O for security reasons - and is instead wrapped through an assist as previously covered. All of the assits within the function are called through a wrapper class, subsequently named AssistWrapper
.
The script which is loaded must have a "metadata" header with a specific format. Luckily for our reverse engineering efforts, the source code contains debug strings with the original struct field names. Below is the structure, along with the constraints that must be passed.
typedef struct _LUA_FILE_HEADER {
BYTE TLVEmpty[6]; // [1, 0, 0, 0, 0]
UINT16 Version; // header->Version == LUA_FILE_HEADER_FORMAT_VERSION
UINT32 SVN;
UINT16 MajorScriptVersion; // header->MajorScriptVersion > MAJOR_SCRIPT_VERSION
UINT16 MinorScriptVersion; // header->MinorScriptVersion > MINOR_SCRIPT_VERSION
UINT16 MajorEngineVersion; // header->MajorEngineVersion == MAJOR_ENGINE_VERSION
UINT16 MinorEngineVersion; // header->MinorEngineVersion == MINOR_ENGINE_VERSION
} *PLUA_FILE_HEADER, LUA_FILE_HEADER;
The size of the Lua script which is loaded is also checked, it must be greater than the size of the structure. If any of these checks fail, a EnforceLuaScriptMetadata
Etw event is triggered.
Followed by this header is compiled Lua code.
typedef struct _LUA_FILE {
LUA_FILE_HEADER lfhMetadata;
BYTE bCompiled[];
} *LUA_FILE, LUA_FILE;
The Lua engine lives inside the SgrmEnclave_secure.dll
DLL and is initialized within the EngInitialize
function. A global lock is issued to ensure the engine doesn't run twice, and a new heap is created for use by the runtime. For unsafe calls inside the engine. e.g. raw function pointers to NATIVE_*
functions, a special wrapper function EngpDispatchLuaPcallUnsafe
is used.
The state of the memory region used by the Lua runtime within the secure enclave is saved, any calls must pass a MemoryUtil::IsBufferOutsideEngine
check - which verifies the memory is within the bounds of MemoryUtil::EngineStart
and MemoryUtil::EngineEnd
. This utility is used to ensure mapped files, etc, aren't living outside the enclave region.
Lua Assertions
This is basically the core of SGRM, the assertion engine - where all of the magic is done. All of the assertions are within Lua, and contained within the compiled script which format is described in the previous section. When the script needs to use functionality, the functions are prefixed with NATIVE_*
, for example, NATIVE_GetProcessById
- which calls into the function within the assist wrapper in SgrmEnclave_secure.dll
.
The fully decompiled script can be found in my GitHub repository for this research here. For example, below we can see the reconstructed script reading the MSR C0010015
for AMD processors.
if not msrTable then
msrTable = {}
msrTable.CPUVendorId = "AuthenticAMD"
msrTable.CPUFamily = L14_2
msrTable.Msr_C0010015 = L18_2
msrTable = L1_2.rn_b151
msrTable(L18_2, L19_2, L20_2)
end
There are several global variables initialized within the script, some from assists when bootstrapping, such as GLOBAL_KERNEL_IMAGE_SIZE
:
GLOBAL_FRAMEWORK_ACCOUNTING |
GLOBAL_PROFILEENABLED |
GLOBAL_ASSERTION_TABLE |
GLOBAL_BOOTID |
GLOBAL_INITIALIZATION_FINISHED |
GLOBAL_PAGESIZE |
GLOBAL_KERNEL_IMAGE_BASE |
GLOBAL_KERNEL_IMAGE_SIZE |
GLOBAL_SCRIPT_ID |
The list of assertions can be read within the script; however this list is incomplete as the decompiled code is hard to read. The following list of drivers are checked to not be loaded, likely the top malicious drivers in their opinion as there are many more on their driver blocklist.
Driver | Description |
---|---|
\Device\mimidrv |
Mimikatz credential dumping utility driver |
\Device\HackSysExtremeVulnerableDriver |
An intentionally vulnerable open-source driver developed by HackSys |
\Device\Htsysm72FB |
A Capcom driver, which was commonly used by adversaries to achieve code execution in kernel mode |
The tampering of critical system processes is also tracked within the Lua script, the following tokens are verified against these processes:
Process | Token |
---|---|
SgrmBroker.exe |
PsProtectedSignerWinTcb |
MsMpEng.exe |
PsProtectedSignerAntimalware |
crss.exe |
PsProtectedSignerWinTcb |
MsMpEngCP.exe |
PsProtectedSignerAntimalware |
MsSense.exe |
PsProtectedSignerWindows |
For firmware integrity, the MSRs of these CPU manufacturers are checked:
Manufacturer | MSR |
---|---|
AMD | C0010015 |
Intel | C80 |
The integrity of several critical, and also Microsoft anti-malware, system drivers are checked:
Driver | Description |
---|---|
\Driver\mssecflt |
Microsoft Security Events Component Minifilter |
\Driver\WdFilter |
Microsoft Defender Antivirus Mini-Filter Driver |
\Driver\SgrmAgent |
System Guard Runtime Monitor Agent Driver, our favourite! |
By using the DRIVER_OBJECT
returned by assist functions, the following routines within the driver are checked. A state at first run is saved, and any changes trigger an assertion failure:
DriverObject->FastIoDispatch
DriverObject->MajorFunction
DriverObject->DriverStartIo
DriverObject->DriverUnload
DriverObject->DriverStart
DriverObject->DriverSize
DriverObject->DriverName
Length
andBuffer
are both checked in thisUNICODE_STRING
structure.
The list EPROCESS
structures is tracked, along with mitigations, the token, and protection level. I'm not currently sure if this is for all processes or just specific ones, as the decompiled output is very mangled. The following process mitigations are checked:
AuditDisableDynamicCode
AuditDisallowWin32kSystemCalls
AuditNonSystemFontLoading
AuditFilteredWin32kAPIs
AuditProhibitLowILImageMap
AuditBlockNonMicrosoftBinaries
AuditBlockNonMicrosoftBinariesAllowStore
AuditLoaderIntegrityContinuity
The following token attributes are checked:
Token->IntegrityLevelIndex
Token->UserAndGroups
Token->Lowbox
Token->IntegrityLevel
Token->TokenSource->SourceName
SID->SubAuthorityCount
SID->SubAuthority
The token of the current System.exe
process is checked and put into a struct within the script - L6_2.SystemToken
. The tokens on the system are then enumerated to make sure it's not been stolen.
SgrmLpac.exe
If the assertion engine detects a violation, the SgrmLapc RPC service is used to send an HTTP POST request to sgrm.microsoft.com/v1.0/Attestation
under the user-agent SgrmBroker
. A value atp_onboarded
is included in the requests, if the registry key SOFTWARE\Microsoft\Windows\CurrentVersion\Sgrm -> Region
is present, this value is sent to the server along with details of the system tampering violation. The region is then used to construct the specific SGRM host, e.g. europe.sgrm.microsoft.com
, or unitedkingdom.sgrm.microsoft.com
.
The RPC endpoint can be called by anyone under the IID a13a9961-953f-4157-8a29-e65e29be510d
, and makes use of the common Wininet!Http*
family of APIs for the requests. The endpoint has a prototype of (the unknown values don't matter when being called, they can be anything):
int s_HttpPost(
__int64 unk_0,
PWCHAR pswzServerName,
PWCHAR *pswcObjectName,
INTERNET_PORT ipInternetPort,
PWORD lpHeaders,
PVOID lpOptional,
DWORD dwOptionalLength,
_QWORD *unk_1,
_DWORD *unk_2
);
The RPC call functions end up calling an internal function within the RPC server named appropriately HttpClient::Post
. James Forshaw's WindowsRpcClients
here implements this nicely for us.
Conclusion
SGRM is a cool bit of kit, with an interesting architecture. It appears as though the project has gone unmaintaned since 2019, although still running on (just about) all modern devices using Windows Defender. Even though it may be unmaintained, or abandoned, there is still a wealth of potential for checks that could be implemented. The use of Lua allows (or would allow) Microsoft to quickly push new assertions to endpoints, hopefully we can see new updates soon.
Thanks to Alex Ionsecu for his initial research on SGRM, along with helping with queries I had. Along with friends who proof-read this blog (@Myrtus0x00, @herrcore, @smelly__vx, @fwosar, @su_charlie, @hela_luc, @d4n_tweets).
This research was done in personal research time at my own liberty, and does not reflect the opinion of my employer.