This writeup is just a PoC on getting the handlers list in win10.
This PoC was done in Win10 build 19041.
VEH is used to catch exceptions happening in the application, when the exceptions are caught, you have a chance to resolve the exceptions to avoid application crash.
Credits
Almost this whole writeup is written by Dimitri Fourny and not my original writeup but some parts of it are modified as per my Win10 build version. Please kindly visit his blog to see the original writeup.
VEH usage example
LONG NTAPI MyVEHHandler(PEXCEPTION_POINTERS ExceptionInfo) {
printf("MyVEHHandler (0x%x)\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO) {
printf(" Divide by zero at 0x%p\n", ExceptionInfo->ExceptionRecord->ExceptionAddress);
ExceptionInfo->ContextRecord->Eip += 2;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
int main() {
AddVectoredExceptionHandler(1, MyVEHHandler);
int except = 5;
except /= 0;
return 0;
}
There are also applications that uses this method to other matters, such as Cheat Engine to bypass basic debugger checks.
Exception Path
When a CPU exception occurs, the kernel will call the function KiDispatchException
(ring0) which will follow this exception to the ntdll method KiUserExceptionDispatcher
(ring3). This function will call RtlDispatchException
which will try to handle it via the VEH. To do it, it will read the VEH chained list via RtlCallVectoredHandlers
and calling each handlers until one return EXCEPTION_CONTINUE_EXECUTION
. If a handler returned EXCEPTION_CONTINUE_EXECUTION
, the function RtlCallVectoredContinueHandlers
is called and it will call all the continue exception handlers.
The VEH handlers are important because the SEH handlers are called only if no VEH handler has caught the exception, so it could be the best method to catch all exceptions if you don’t want to hook KiUserExceptionDispatcher
. If you want more information about the exceptions dispatcher, 0vercl0ck has made a good paper about it.
The chained list
The VEH list is a circular linked list with the handlers functions pointers encoded:
The exception handlers are encoded with a process cookie but you can decode them easily. If you are dumping the VEH which is inside your own process, you can just use DecodePointer
and you don’t have to care about the process cookie. If it’s a remote process you can use DecodeRemotePointer
but you will need to create your own function pointer with GetModuleHandle("kernel32.dll")
and GetProcAddress("DecodeRemotePointer")
.
The solution that I have chosen is to imitate DecodePointer
by getting the process cookie with ZwQueryProcessInformation
and applying the same algorithm:
DWORD Process::GetProcessCookie() const {
DWORD cookie = 0;
DWORD return_length = 0;
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
_NtQueryInformationProcess NtQueryInformationProcess =
reinterpret_cast<_NtQueryInformationProcess>(
GetProcAddress(ntdll, "NtQueryInformationProcess"));
NTSTATUS success = NtQueryInformationProcess(
process_handle_, ProcessCookie, &cookie, sizeof(cookie), &return_length);
if (success < 0) {
return 0;
}
return cookie;
}
#define ROR(x, y) ((unsigned)(x) >> (y) | (unsigned)(x) << 32 - (y))
DWORD Process::DecodePointer(DWORD pointer) {
if (!process_cookie_) {
process_cookie_ = GetProcessCookie();
if (!process_cookie_) {
return 0;
}
}
unsigned char shift_size = 0x20 - (process_cookie_ & 0x1f);
return ROR(pointer, shift_size) ^ process_cookie_;
}
Finding the VEH list offset
Even if you can find the symbol LdrpVectorHandlerList
in the ntdll pdb, there is no official API to get it easily. My solution is to begin by getting a pointer to RtlpAddVectoredHandler
:
You can disassemble the method RtlAddVectoredExceptionHandler
until you find the instruction call
or you can just pretend that its address is always at 0x16
bytes after it:
BYTE* add_exception_handler = reinterpret_cast<BYTE*>(
GetProcAddress(ntdll, "RtlAddVectoredExceptionHandler"));
BYTE* add_exception_handler_sub =
add_exception_handler + 0x16; // RtlpAddVectoredHandler
And from here the same byte offset method could work, but a simple signature system could prevent us to be broken after a small Windows update:
const BYTE pattern_list[] = {
0x89, 0x46, 0x10, // mov [esi+10h], eax
0x81, 0xc3, 0, 0, 0, 0 // add ebx, offset LdrpVectorHandlerList
};
const char mask_list[] = "xxxxx????";
BYTE* match_list =
SearchPattern(add_exception_handler_sub, 0x100, pattern_list, mask_list);
BYTE* veh_list = *reinterpret_cast<BYTE**>(match_list + 5);
size_t veh_list_offset = veh_list - reinterpret_cast<BYTE*>(ntdll);
printf("LdrpVectorHandlerList: 0x%p (ntdll+0x%x)\n", veh_list, veh_list_offset);
Final code
#define ROR(x, y) ((unsigned)(x) >> (y) | (unsigned)(x) << 32 - (y))
DWORD Process::GetProcessCookie() const {
DWORD cookie = 0;
DWORD return_length = 0;
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
_NtQueryInformationProcess NtQueryInformationProcess =
reinterpret_cast<_NtQueryInformationProcess>(
GetProcAddress(ntdll, "NtQueryInformationProcess"));
NTSTATUS success = NtQueryInformationProcess(
process_handle_, ProcessCookie, &cookie, sizeof(cookie), &return_length);
if (success < 0) {
return 0;
}
return cookie;
}
DWORD Process::DecodePointer(DWORD pointer) {
if (!process_cookie_) {
process_cookie_ = GetProcessCookie();
if (!process_cookie_) {
return 0;
}
}
unsigned char shift_size = 0x20 - (process_cookie_ & 0x1f);
return ROR(pointer, shift_size) ^ process_cookie_;
}
typedef struct _VECTORED_HANDLER_ENTRY {
_VECTORED_HANDLER_ENTRY* next;
_VECTORED_HANDLER_ENTRY* previous;
ULONG refs;
PVECTORED_EXCEPTION_HANDLER handler;
} VECTORED_HANDLER_ENTRY;
typedef struct _VECTORED_HANDLER_LIST {
void* mutex_exception;
VECTORED_HANDLER_ENTRY* first_exception_handler;
VECTORED_HANDLER_ENTRY* last_exception_handler;
void* mutex_continue;
VECTORED_HANDLER_ENTRY* first_continue_handler;
VECTORED_HANDLER_ENTRY* last_continue_handler;
} VECTORED_HANDLER_LIST;
DWORD GetVEHOffset() {
HMODULE ntdll = LoadLibraryA("ntdll.dll");
printf("ntdll: 0x%p\n", ntdll);
perror_if_invalid(ntdll, "LoadLibrary");
BYTE* add_exception_handler = reinterpret_cast<BYTE*>(
GetProcAddress(ntdll, "RtlAddVectoredExceptionHandler"));
printf("RtlAddVectoredExceptionHandler: 0x%p\n", add_exception_handler);
perror_if_invalid(add_exception_handler, "GetProcAddress");
BYTE* add_exception_handler_sub = add_exception_handler + 0x16;
printf("RtlpAddVectoredExceptionHandler: 0x%p\n", add_exception_handler_sub);
const BYTE pattern_list[] = {
0x89, 0x46, 0x10, // mov [esi+10h], eax
0x81, 0xc3, 0, 0, 0, 0 // add ebx, offset LdrpVectorHandlerList
};
const char mask_list[] = "xxxxx????";
BYTE* match_list =
SearchPattern(add_exception_handler_sub, 0x100, pattern_list, mask_list);
perror_if_invalid(match_list, "SearchPattern");
BYTE* veh_list = *reinterpret_cast<BYTE**>(match_list + 5);
size_t veh_list_offset = veh_list - reinterpret_cast<BYTE*>(ntdll);
printf("LdrpVectorHandlerList: 0x%p (ntdll+0x%x)\n", veh_list,
veh_list_offset);
return veh_list_offset;
}
int main() {
auto process = Process::GetProcessByName(L"veh_dumper.exe");
perror_if_invalid(process.get(), "GetProcessByName");
printf("Process cookie: 0x%0x\n", process->GetProcessCookie());
DWORD ntdll = process->GetModuleBase(L"ntdll.dll");
VECTORED_HANDLER_LIST handler_list;
DWORD veh_addr = ntdll + GetVEHOffset();
printf("VEH: 0x%08x\n", veh_addr);
process->ReadProcMem(veh_addr, &handler_list, sizeof(handler_list));
printf("First entry: 0x%p\n", handler_list.first_exception_handler);
printf("Last entry: 0x%p\n", handler_list.last_exception_handler);
if (reinterpret_cast<DWORD>(handler_list.first_exception_handler) ==
veh_addr + sizeof(DWORD)) {
printf("VEH list is empty\n");
return 0;
}
printf("Dumping the entries:\n");
VECTORED_HANDLER_ENTRY entry;
process->ReadProcMem(
reinterpret_cast<DWORD>(handler_list.first_exception_handler), &entry,
sizeof(entry));
while (true) {
DWORD handler = reinterpret_cast<DWORD>(entry.handler);
printf(" handler = 0x%p => 0x%p\n", handler,
process->DecodePointer(handler));
if (reinterpret_cast<DWORD>(entry.next) == veh_addr + sizeof(DWORD)) {
break;
}
process->ReadProcMem(reinterpret_cast<DWORD>(entry.next), &entry,
sizeof(entry));
}
}
POC
With this, I can now walk through VEH and reverse what does the handlers do.
Again, this is not my original writeup, all credits goes to Dimitri Fourny.
Thank you for reading! I hope you’ve enjoyed 🙂