UnhookMe: Dynamically unhooking imports resolver
UnhookMe – Dynamically unhooking imports resolver
In the era of intrusive AVs and EDRs that introduce hot-patches to the running processes for their enhanced optics requirements, modern adversaries must have a robust tool to slide through these watchguards. The proposed implementation of dynamic imports resolver that would be capable of unhooking used functions in-the-fly is yet another step towards strengthening adversary resilience efforts.
The solution I’m proposing here is to switch from using linker-resolved WinAPI imports, staying invisible in compiled executable’s PE headers (Import Address Table specifically) to favor a fully-dynamic approach insisting on resolving imports only in a dynamic fashion. Such dynamical resolver can be equipped with unhooking logic happening in the background, without any sort of guidance from the operator’s side.
Here’s how the UnhookMe example works:
- It presents us with the first MessageBoxW that is not subject for hooking
- Then we hook MessageBoxW prologue ourselves to make it always return 0 without displaying its message
- Finally, we resolve MessageBoxW dynamically using the UnhookingImportResolver resolver, which will detect applied prologue patches and restore original bytes, effectively unhooking MessageBoxW functionality.
In the meantime of popping message boxes, these are the loglines printed to console’s stdout:
How does it work?
The underlying resolver leverages a custom PE headers parser, that processes every referenced DLL module to map their exports and verify that module’s PE headers integrity as well as the integrity of referenced function’s stub bytes.
The idea is following:
- Firstly we issue LoadLibrary to load referenced by the user library (the one specified as the first parameter for RESOLVE macro) if it could not be reached through GetModuleHandle.
- Then we process loaded/referenced library’s PE headers, map its exports, retrieve an array of export addresses as well as compute these addresses ourselves for cross-verification.
- If the address of a routine defined in DLL’s Export Address Table doesn’t correspond to what we would expect, the export is considered EAT hooked. The same goes for if our Executable Import Address Table (IAT) entry for that function was altered and no longer points to the correct spot in DLL’s code section – then the function is considered to be IAT hooked.
- Assuming no hooks were found so far, we fetch the first N bytes of the function’s prologue and compare them to what’s in DLL’s file stored in the disk. If there is a discrepancy between bytes fetched from memory and from file – we consider the function was inline patched (hot-patched).
- If the function was considered hooked – we return the original export’s address (the one we computed ourselves) and/or unhook the entry. If there were patch bytes in place, we’ll restore them.
- Finally, in order to optimize the resolver’s performance impact – we cache all of the loaded modules’ imagebases and resolved functions addresses and return them from a cache (being std::map ) during subsequent hits.
Among the problems such dynamically-unhooking resolver faced are the issues with traversing forwarded APIs (a DLL may contain Export thunk saying that this function is not implemented in this module, but it is in another one) – which although this implementation has support for, sometimes it brokes its traversal logic.