If you have been reversing literally anything on the Windows platform in the past decade, you have heard about Scylla, CHimpREC or other import address table (IAT) rebuilders.

Reversing a program when you can’t see any of its imports makes it much more complicated especially if its code is mutated or virtualized (for example with VMProtect). This is why some programs try to hide or obfuscate their IAT in some way.

Import rebuilders know it and that is why they don’t simply read the PE header from memory and then try to parse the import table. If they did it this way, you could just zero out the pointer in IMAGE_NT_HEADERS->IMAGE_OPTIONAL_HEADER->DataDirectory and they would not be able to figure out anything.

Take Scylla for example. To figure out where the import table is and how to reconstruct it, it uses disassembler to find jumps/calls to possible imports and then goes from there to do the actual reconstruction. Zeroing anything would not help.

The question arises, how can you hide the imports without using syscalls directly or rewriting the entire program?

It’s quite simple actually. There are many ways to do it, but this is what I have seen some cheat providers using. Use an absolute jump while offsetting the address so the static disassembler can’t find the target. How does it look in practise?

First, we need to get a pointer to the DOS header of the module that we want to hide imports of. If we are using MSVC and we want to hide imports of the main module, we can just use __ImageBase as usual:

extern "C" IMAGE_DOS_HEADER __ImageBase;
PIMAGE_DOS_HEADER dosHeader = & __ImageBase;

Now we need to get to NT headers and optional header:

PIMAGE_NT_HEADERS64 ntHeaders = (PIMAGE_NT_HEADERS64)((DWORD64)dosHeader + dosHeader->e_lfanew);
PIMAGE_OPTIONAL_HEADER64 optionalHeader = &ntHeaders->OptionalHeader;

Loop through the imports as usual:

PIMAGE_IMPORT_DESCRIPTOR currentImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD64)(dosHeader)+optionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
while (currentImportDescriptor->FirstThunk)
{
    PIMAGE_THUNK_DATA64 currentFirstThunk = (PIMAGE_THUNK_DATA64)((DWORD64)(dosHeader)+currentImportDescriptor->FirstThunk);
    PIMAGE_THUNK_DATA64 currentOriginalFirstThunk = (PIMAGE_THUNK_DATA64)((DWORD64)(dosHeader)+currentImportDescriptor->OriginalFirstThunk);

    while (currentOriginalFirstThunk->u1.Function)
    {                   
        // currentFirstThunk->u1.Function contains pointer to imported function

        currentOriginalFirstThunk++;
        currentFirstThunk++;
    }

    currentImportDescriptor++;
}

Now that we can loop through the imports, we need to make the shellcode for the jump itself. Since I am bad with writing assembly, let’s just write it in C and then get the shellcode out of it:

typedef void(_stdcall* dummy_t)();
void Code()
{
    volatile DWORD64 address = 0xDEADFEEDDEADFEED;
    address = address - 0x1234;
    dummy_t dummy = (dummy_t)address;
    dummy();
}

This code will firstly subtract some number from the address and then jump to it.

After compiling this code with MSVC with optimization enabled and protections such as security cookie disabled, we get this shellcode…

mov     rax, 0DEADFEEDDEADFEEDh
mov     [rsp+arg_0], rax
mov     rax, [rsp+arg_0]
sub     rax, 1234h
mov     [rsp+arg_0], rax
mov     rax, [rsp+arg_0]
jmp     rax

…which we can save as char array…

\x48\xB8\xED\xFE\xAD\xDE\xED\xFE\xAD\xDE\x48\x89\x44\x24\x08\x48\x8B\x44\x24\x08\x48\x2D\x34\x12\x00\x00\x48\x89\x44\x24\x08\x48\x8B\x44\x24\x08\x48\xFF\xE0

…and now that we have it, we just need to create a function that we can use to allocate memory for it and change the target address with the correct offset:

PVOID GetShellCode(DWORD64 address)
{      
    address += 0x1234;
    
    const char* shellcode = "\x48\xB8\xED\xFE\xAD\xDE\xED\xFE\xAD\xDE\x48\x89\x44\x24\x08\x48\x8B\x44\x24\x08\x48\x2D\x34\x12\x00\x00\x48\x89\x44\x24\x08\x48\x8B\x44\x24\x08\x48\xFF\xE0";

    PVOID allocated = VirtualAlloc(nullptr, 39, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (!allocated)
        return 0;

    memcpy(allocated, shellcode, 39);

    *(DWORD64*)((DWORD64)allocated + 2) = address; // mov rax, 0DEADFEEDDEADFEEDh

    return allocated;
}

Now we just add it to our import loop:

void CleanImports()
{
    PIMAGE_DOS_HEADER dosHeader = &__ImageBase;
    
    PIMAGE_NT_HEADERS64 ntHeaders = (PIMAGE_NT_HEADERS64)((DWORD64)dosHeader + dosHeader->e_lfanew);
    PIMAGE_OPTIONAL_HEADER64 optionalHeader = &ntHeaders->OptionalHeader;

    PIMAGE_IMPORT_DESCRIPTOR currentImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD64)(dosHeader)+optionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
    while (currentImportDescriptor->FirstThunk)
    {
        PIMAGE_THUNK_DATA64 currentFirstThunk = (PIMAGE_THUNK_DATA64)((DWORD64)(dosHeader)+currentImportDescriptor->FirstThunk);
        PIMAGE_THUNK_DATA64 currentOriginalFirstThunk = (PIMAGE_THUNK_DATA64)((DWORD64)(dosHeader)+currentImportDescriptor->OriginalFirstThunk);

        DWORD oldProtect;
        if (!VirtualProtect(currentOriginalFirstThunk, sizeof(IMAGE_THUNK_DATA64), PAGE_READWRITE, &oldProtect))
            return;

        while (currentOriginalFirstThunk->u1.Function)
        {             
            if (!VirtualProtect(currentFirstThunk, sizeof(IMAGE_THUNK_DATA64), PAGE_READWRITE, &oldProtect))
                continue;
            
            PIMAGE_IMPORT_BY_NAME thunkData = (PIMAGE_IMPORT_BY_NAME)((DWORD64)(dosHeader)+currentOriginalFirstThunk->u1.AddressOfData);

            PVOID jumpcode = GetShellCode(currentFirstThunk->u1.Function);
            currentFirstThunk->u1.Function = (ULONGLONG)jumpcode;

            currentOriginalFirstThunk++;
            currentFirstThunk++;
        }

        currentImportDescriptor++;
    }
}

Please note that the import descriptor section is not writable by default and so you need to change its protection.

Now if we try to dump our program, the imports will be hidden:

Obviously, this just by itself will not really help you with anything but combined with some code virtualization and other clever tricks this can waste some time of people trying to reverse engineer your program.