Week 7

This week I emerge from the lab and put on a different hat.

1. Switching hats

With Interceptor being successful in blinding ESET Internet Security sufficiently to run a meterpreter reverse shell, it is time to put on the red team hat and get out of the perfect lab environment. To do just that, I had to revert some settings I turned off at the beginning of this series.

First, I enabled Secure Boot and disabled test signing mode on the target VM. Secure Boot will enable Microsoft’s Driver Signature Enforcement (DSE) policy, which blocks non-WHQL-signed drivers from being loaded, which includes my Interceptor driver. It’s important to note I left HyperGuard (HVCI) turned off, because I currently have no way of defeating Virtualization-based protection.

With the target configured, I then setup a Cobalt Strike Teamserver using a Gmail Malleable C2 profile and configured my EarlyBird shellcode injector to deliver a HTTPS Beacon. My idea was to simulate a scenario where an attacker (me) had managed to gain a foothold on the target and obtained an implant with elevated privileges. The attacker would then use the implant to disable DSE on the compromised system and load the Interceptor driver, all directly in memory to keep a low footprint. Once Interceptor has been loaded on the target system, it would cripple the EDR/AV product and allow the attacker to run Mimikatz undetected.

Naturally, nothing ever goes as planned.

2. Outspoofing myself

The first issue I ran into was executing my shellcode injector with elevated privileges. No matter what I tried, I couldn’t seem to get a Beacon callback with elevated privileges, so I took my issue to infosec Twitter and unmasked the culprit with the help of @trickster012.

The code that is responsible for spawning a new spoofed process which is then used to inject the Beacon payload into looks like this:

PROCESS_INFORMATION Spawn(LPSTR procPath, HANDLE parentHandle)
{
    //do dynamic imports
    hK32 = GetModuleHandleA("kernel32");
    FARPROC fpInitializeProcThreadAttributeList = GetProcAddress(hK32, "InitializeProcThreadAttributeList");
    _InitializeProcThreadAttributeList InitializeProcThreadAttributeList = (_InitializeProcThreadAttributeList)fpInitializeProcThreadAttributeList;
    FARPROC fpUpdateProcThreadAttribute = GetProcAddress(hK32, "UpdateProcThreadAttribute");
    _UpdateProcThreadAttribute UpdateProcThreadAttribute = (_UpdateProcThreadAttribute)fpUpdateProcThreadAttribute;
    FARPROC fpDeleteProcThreadAttributeList = GetProcAddress(hK32, "DeleteProcThreadAttributeList");
    _DeleteProcThreadAttributeList DeleteProcThreadAttributeList = (_DeleteProcThreadAttributeList)fpDeleteProcThreadAttributeList;

    STARTUPINFOEXA si;
    PROCESS_INFORMATION pi;
    SIZE_T attributeSize;

    memset(&si, 0, sizeof(si));
    memset(&pi, 0, sizeof(pi));

    InitializeProcThreadAttributeList(NULL, 2, 0, &attributeSize);
    si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
    InitializeProcThreadAttributeList(si.lpAttributeList, 2, 0, &attributeSize);

    DWORD64 policy = PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON;
    //enable CIG
    UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, &policy, sizeof(DWORD64), NULL, NULL);
    //PPID spoof: set parentHandle as parent process
    UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parentHandle, sizeof(HANDLE), NULL, NULL);

    si.StartupInfo.cb = sizeof(si);
    si.StartupInfo.dwFlags = EXTENDED_STARTUPINFO_PRESENT;

    if (!CreateProcessA(NULL, procPath, NULL, NULL, TRUE, CREATE_SUSPENDED | CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi))
    {
        throw "";
    }

    std::cout << "Process created!" << " PID: " << pi.dwProcessId << "\n";

    DeleteProcThreadAttributeList(si.lpAttributeList);
    NtClose(parentHandle);

    return pi;
}

The Spawn() function takes a parameter HANDLE parentHandle, which is used to set the parent process of the newly created process. The handle would in this case point to explorer.exeas this is the process I was spoofing. @CaptMeelo recently posted a great blogpost titled Picky PPID Spoofing which covers the topic of PPID spoofing quite well.

To make a long story short, as stated in the Microsoft documentation, the to-be-created process inherits certain attributes from its parent process (the one we’re spoofing), this also happens to include the process token. One of the many things contained in a token are the privileges held by the user or the user’s group that are associated with the process.

Parent process attributes

If we take a look at explorer.exe in Process Hacker we can see the associated user and token. We can also see that the process is not running in elevated context. Taking into consideration the attribute inheritance, it makes sense that I couldn’t manage to spawn an elevated process with explorer.exe set as parent.

Explorer.exe process hacker

With this issue identified and remediated, I ran head first into the next one: concealing Beacon from EDR/AV. My shellcode injector is still confiured to use embedded shellcode, instead of pulling a payload from somewhere else. So far this has worked quite well, using staged payloads. I replaced the meterpreter payload with one of Cobalt Strike’s stagers, which would then pull a full HTTPS Beacon payload. I have not (yet) modified Beacon, so once the stager pulls the payload, EDR/AV detects a Cobalt Strike artifact in memory and takes action. Uh oh, not good. As of writing this blogpost, I have not yet figured out the answer to this problem, if there are any reader suggestions, you’re more than welcome to share them with me on Twitter.

3. Disabling Driver Signature Enforcement (DSE)

Instead, I decided to move on to the task at hand: disabling driver signature enforcement (DSE) on the target and loading Interceptor. Over the course of my research I stumbled across Kernel Driver Utility (KDU) a tool developed by @hfiref0x. One of the many wonderous things this tool can do is disable Driver Signature Enforcement (DSE). It does this by loading a WHQL-signed driver with an arbitrary kernel memory read/write vulnerability to change the state of ntoskrnl.exe g_CiEnabled or CI.dll g_CiOptions depending on the build version of Windows.

I tested KDU and it worked well, except it didn’t tick all the boxes required for the scenario:

  1. It got flagged by EDR/AV
  2. It cannot be executed in memory from a Beacon

What I need is a custom Beacon Object File (BOF) which only purpose is to disable DSE and load Interceptor, or any other malicious driver for that matter. Windows provides API’s like NtLoadDriver() and NtUnloadDriver() to handle loading drivers programmatically, there’s just one catch: drivers cannot be loaded from memory, they need to touch disk, which is not good for OPSEC. This statement is not 100% correct, because there are ways to manually map drivers into memory, however they come with a lot of drawbacks like:

  • Invalid DeviceObject and RegistryPath objects
  • No Structured Exception Handling (SEH)
  • Cannot be unloaded, so they persist until reboot
  • Only ntoskrnl.exe imports are resolved
  • Cannot use certain kernel primitives like callbacks because of PatchGuard

I won’t go into much details here, but manually mapping comes with so much overhead and instability it is out of the equation (until I get bored). So instead, I’ll have to sacrifice some OPSEC and touch disk for a safer and more stable result. I’m currently developing a BOF to disable DSE using CVE-2015-2291 which will also be integrated in my CobaltWhispers framework for Cobalt Strike, which I just updated to use SysWhispers2 and InlineWhispers2 to dynamically resolve direct syscalls.

Disable DSE

4. Conclusion

With the release of this blogpost, the kernel driver Interceptor is nearly complete in functionality and is able to fullfill its purpose. Writing tools wouldn’t be very useful if they don’t work outside of a lab environment and not all of us have magical access to code signing certificates and administrator privileges in a target environment. I spent a good amount of time uncovering new and different hurdles that come with the scenario I presented, and subsequently tried to find solutions to them. I guess it goes to show, most challenges to remain undetected and bypass EDR/AV are still presented in user space and have to be addressed as such.

Besides the challenges in user space, there are still several kernel space aspects I want to look at in upcoming blogposts if the time permits. These include:

  • disabling Sysmon and Event Tracing for Windows (ETW)
  • hooking minifilters
  • inspecting and filtering IRPs

But as with everything, time flies by when one’s having fun ;)