← Back to list

Analysis of API Hashing and Import Lookup Techniques for Obfuscated API Resolution in Malware

In my last malops challenge, RokRAT loader was observed to use API hashing, a technique to use a hash to resolve into Windows API and then…

YUCA · 2025-11-11 15:14 · 0 claps · 7.1 min read
#cybersecurity #malware-analysis #malware #reversing #cybercrime
Open on Medium ↗
Wiki topics: 🔒 · Cybersecurity 📋 · Product Management

Analysis of API Hashing and Import Lookup Techniques for Obfuscated API Resolution in Malware

In my last malops challenge, RokRAT loader was observed to use API hashing, a technique to use a hash to resolve into Windows API and then proceeding to call it. This blog will detail how the hash resolves into an API.

Resolving API hashing subroutine being called.

Resolving API hashing subroutine being called.

Normal Window API Resolution

In normal Windows development, applications call Windows APIs directly after including the appropriate header files. When triaging malware , investigators may statically examine an executable’s import address table (IAT) to identify potentially malicious API usage. To hinder the efforts of analysis and detections, malware authors commonly try to evade detection by hiding or changing how they resolve APIs.

#include <windows.h>
#include <iostream>
#include <cwchar>

int main() {

    HANDLE hFile = CreateFileW(L"file.txt", GENERIC_WRITE, 0, NULL,
                               CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

    const wchar_t buffer[] = L"hello world!";

    DWORD bytesToWrite = (DWORD)(wcslen(buffer) * sizeof(wchar_t));

    BOOL status = WriteFile(hFile, buffer, bytesToWrite, NULL, NULL);

    if (!status) {
        std::cout << "Write File Failed " << GetLastError() << "\n";
        CloseHandle(hFile);
        return 0;
    }

    std::wcout << L"File was written with: " << buffer << L"\n";

    CloseHandle(hFile);
    return 0;
}

CreateFileW and WriteFile instructions detected in IAT with PEStudio

CreateFileW and WriteFile instructions detected in IAT with PEStudio

Malware authors may use techniques such as API hashing to reduce the amount of APIs in Import address table being flagged.

API hashing hides the imports

API hashing hides the imports

API Hashing Details

API hashing essentially operates in the following:

  1. hash values for a windows APIs is precomputed leveraging some type of hashing algorithm such as “CRC32”, “DJB2” and “FNV-1a”.
  2. The hash value is passed into function hash resolvers which in which the malware enumerates loaded modules at runtime. For each module (DLL) , malware walks the Export Address Table (EAT) to retrieve exported API names.
  3. For each exported API name, it compares the computed hash to the target hash.
  4. If match, function stores or returns that pointer allowing it to be called.

Precomputated Hashing

In the following example, CreateFileA and WriteFile will have a precomputated hash leveraging the FNV-1a (Fowler–Noll–Vo) cryptographic hash function.

#include <cstdint>
#include <cstring>
#include <iostream>
#include <iomanip>

// 64-bit FNV-1a
uint64_t fnv1a_64(const void* key, std::size_t len) {
    const unsigned char* data = static_cast<const unsigned char*>(key);
    const uint64_t FNV_OFFSET = 0xcbf29ce484222325ULL;
    const uint64_t FNV_PRIME  = 0x100000001b3ULL;

    uint64_t hash = FNV_OFFSET;
    for (std::size_t i = 0; i < len; ++i) {
        hash ^= static_cast<uint64_t>(data[i]);
        hash *= FNV_PRIME;
    }
    return hash;
}

int main() {
    const char* createfile_api = "CreateFileW";
    uint64_t createfile_hash = fnv1a_64(createfile_api, std::strlen(createfile_api));

    const char* writefile_api = "WriteFile";
    uint64_t writefile_hash = fnv1a_64(writefile_api, std::strlen(writefile_api));

    // print fixed-width 16-hex-digit values (leading zeros)
    std::cout << "CreateFileW Hash Generated: 0x"
              << std::hex << std::setw(16) << std::setfill('0')
              << createfile_hash << std::endl;

    std::cout << "WriteFile Hash Generated:  0x"
              << std::hex << std::setw(16) << std::setfill('0')
              << writefile_hash << std::endl;

    // restore to decimal (optional)
    std::cout << std::dec << std::setfill(' ');
    return 0;
}

API Function resolver

In order to resolve the hash, The function will read the running process for crucial data structures which consequently resolve into the pointer for the function. The function first searches for the PEB (Process Environment Block) which is a process-level info structure that the OS and programs use to share runtime details — loaded modules (DLLs), environment variables, execution context, and more. On 32-bit Windows (x86), the PEB pointer is located at offset 0x30 inside the TEB, which is accessed through the FS segment. On 64-bit Windows (x64), the PEB pointer is located at offset 0x60 inside the TEB, accessed through the GS segment.

After locating the PEB, the function attempts to access the PEB structure to specifically look for the PPEB_LDR_DATA structure to locate InInitializationOrderModuleList which contains the list of DLLs.

typedef struct _PEB {
  BYTE                          Reserved1[2];
  BYTE                          BeingDebugged;
  BYTE                          Reserved2[1];
  PVOID                         Reserved3[2];
  PPEB_LDR_DATA                 Ldr;   <--------- WE WANT THIS!!!
  PRTL_USER_PROCESS_PARAMETERS  ProcessParameters;
  PVOID                         Reserved4[3];
  PVOID                         AtlThunkSListPtr;
  PVOID                         Reserved5;
  ULONG                         Reserved6;
  PVOID                         Reserved7;
  ULONG                         Reserved8;
  ULONG                         AtlThunkSListPtr32;
  PVOID                         Reserved9[45];
  BYTE                          Reserved10[96];
  PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
  BYTE                          Reserved11[128];
  PVOID                         Reserved12[1];
  ULONG                         SessionId;
} PEB, *PPEB;
typedef struct _PEB_LDR_DATA {
  BYTE       Reserved1[8];
  PVOID      Reserved2[3];
  LIST_ENTRY InMemoryOrderModuleList; <--------- WE WANT THIS!!!
} PEB_LDR_DATA, *PPEB_LDR_DATA;
typedef struct _LIST_ENTRY {
  struct _LIST_ENTRY *Flink; <------ WE WANT THIS!!!
  struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, PRLIST_ENTRY;

Once the code obtains a Flink from the InMemoryOrderModuleList, it can use that Flink (which points into the InMemoryOrderLinks list entry) to obtain the containing LDR_DATA_TABLE_ENTRY structure. From that LDR_DATA_TABLE_ENTRY the code can read the module’s DllBase (load address), SizeOfImage, and the FullDllName / BaseDllName Unicode strings to get the module’s name and addresses.

//0xa8 bytes (sizeof)
struct _LDR_DATA_TABLE_ENTRY
{
    struct _LIST_ENTRY InLoadOrderLinks;                                     //0x0
    struct _LIST_ENTRY InMemoryOrderLinks;                                  //0x8
    struct _LIST_ENTRY InInitializationOrderLinks;                          //0x10
    VOID* DllBase;                                                          //0x18
    VOID* EntryPoint;                                                       //0x1c
    ULONG SizeOfImage;                                                      //0x20
    struct _UNICODE_STRING FullDllName;                                     //0x24
    struct _UNICODE_STRING BaseDllName;                                     //0x2c
    union
    {
        UCHAR FlagGroup[4];                                                 //0x34
        ULONG Flags;                                                        //0x34
        struct
        {
            ULONG PackagedBinary:1;                                         //0x34
            ULONG MarkedForRemoval:1;                                       //0x34
            ULONG ImageDll:1;                                               //0x34
            ULONG LoadNotificationsSent:1;                                  //0x34
            ULONG TelemetryEntryProcessed:1;                                //0x34
            ULONG ProcessStaticImport:1;                                    //0x34
            ULONG InLegacyLists:1;                                          //0x34
            ULONG InIndexes:1;                                              //0x34
            ULONG ShimDll:1;                                                //0x34
            ULONG InExceptionTable:1;                                       //0x34
            ULONG ReservedFlags1:2;                                         //0x34
            ULONG LoadInProgress:1;                                         //0x34
            ULONG LoadConfigProcessed:1;                                    //0x34
            ULONG EntryProcessed:1;                                         //0x34
            ULONG ProtectDelayLoad:1;                                       //0x34
            ULONG ReservedFlags3:2;                                         //0x34
            ULONG DontCallForThreads:1;                                     //0x34
            ULONG ProcessAttachCalled:1;                                    //0x34
            ULONG ProcessAttachFailed:1;                                    //0x34
            ULONG CorDeferredValidate:1;                                    //0x34
            ULONG CorImage:1;                                               //0x34
            ULONG DontRelocate:1;                                           //0x34
            ULONG CorILOnly:1;                                              //0x34
            ULONG ChpeImage:1;                                              //0x34
            ULONG ReservedFlags5:2;                                         //0x34
            ULONG Redirected:1;                                             //0x34
            ULONG ReservedFlags6:2;                                         //0x34
            ULONG CompatDatabaseProcessed:1;                                //0x34
        };
    };
    USHORT ObsoleteLoadCount;                                               //0x38
    USHORT TlsIndex;                                                        //0x3a
    struct _LIST_ENTRY HashLinks;                                           //0x3c
    ULONG TimeDateStamp;                                                    //0x44
    struct _ACTIVATION_CONTEXT* EntryPointActivationContext;                //0x48
    VOID* Lock;                                                             //0x4c
    struct _LDR_DDAG_NODE* DdagNode;                                        //0x50
    struct _LIST_ENTRY NodeModuleLink;                                      //0x54
    struct _LDRP_LOAD_CONTEXT* LoadContext;                                 //0x5c
    VOID* ParentDllBase;                                                    //0x60
    VOID* SwitchBackContext;                                                //0x64
    struct _RTL_BALANCED_NODE BaseAddressIndexNode;                         //0x68
    struct _RTL_BALANCED_NODE MappingInfoIndexNode;                         //0x74
    ULONG OriginalBase;                                                     //0x80
    union _LARGE_INTEGER LoadTime;                                          //0x88
    ULONG BaseNameHashValue;                                                //0x90
    enum _LDR_DLL_LOAD_REASON LoadReason;                                   //0x94
    ULONG ImplicitPathOptions;                                              //0x98
    ULONG ReferenceCount;                                                   //0x9c
    ULONG DependentLoadFlags;                                               //0xa0
    UCHAR SigningLevel;                                                     //0xa4
}; 

Below is the represenation of accessing the BaseDLLName in code:

//https://malwaretech.com/wiki/locating-modules-via-the-peb-x64
#include <Windows.h>
#include <Stdio.h>

/* Insert PEB, PEB_LDR_DATA, and LDR_DATA_TABLE_ENTRY definition here */

int main()
{
    PEB *peb = (PEB *)__readgsqword(0x60);
    PEB_LDR_DATA* ldr = (PEB_LDR_DATA*)peb->Ldr;

    LDR_DATA_TABLE_ENTRY *main_module = (LDR_DATA_TABLE_ENTRY * )ldr->InLoadOrderModuleList.Flink;
    LDR_DATA_TABLE_ENTRY *ntdll = (LDR_DATA_TABLE_ENTRY * )main_module->InLoadOrderLinks.Flink;
    LDR_DATA_TABLE_ENTRY *kernel32 = (LDR_DATA_TABLE_ENTRY * )ntdll->InLoadOrderLinks.Flink;

    printf("Module name: %S, Base address: 0x%p, Entrypoint: 0x%p\n", 
           main_module->BaseDllName.Buffer, main_module->DllBase, main_module->EntryPoint);

    printf("Module name: %S, Base address: 0x%p, Entrypoint: 0x%p\n",
           ntdll->BaseDllName.Buffer, ntdll->DllBase, ntdll->EntryPoint);

In debugger view (x32dbg) Action Finding :

Since 0x4D3908 is Flink pointing to the next module. If we follow dump by clicking the 0x4D3908 we are directed to next Flink pointer. we will likely iterate next InloadOrderLinks. If we follow dump on 0x777B8420. It will result in ntdll. BaseDLL points to ntdll. It then continues to kernel32.dll etc.

in debugger view after finding flink

in debugger view after finding flink

The DLLbase (address of the module) for NTDLL can be found here in a closer lookup.

NTDLL Image base address is 0x4D3818

NTDLL Image base address is 0x4D3818

With both module name and BaseDLL found, the function can now walk into the DLL and retrieve the the offset of the DataDirectory for the export address table which contains key elements such as the exported functions, function names and ordinals.

///struct of the Export Adddress table
typedef struct _IMAGE_EXPORT_DIRECTORY {
    uint32_t Characteristics;          // 0x00 (0)
    uint32_t TimeDateStamp;            // 0x04 (4)
    uint16_t MajorVersion;             // 0x08 (8)
    uint16_t MinorVersion;             // 0x0A (10)
    uint32_t Name;                     // 0x0C (12)  -> RVA to DLL name (ASCII)
    uint32_t Base;                     // 0x10 (16)  -> ordinal base
    uint32_t NumberOfFunctions;        // 0x14 (20)  -> total entries in AddressOfFunctions
    uint32_t NumberOfNames;            // 0x18 (24)  -> number of name pointers (AddressOfNames length)
    uint32_t AddressOfFunctions;       // 0x1C (28)  -> RVA to DWORD array of function RVAs (EAT)
    uint32_t AddressOfNames;           // 0x20 (32)  -> RVA to DWORD array of name RVAs
    uint32_t AddressOfNameOrdinals;    // 0x24 (36)  -> RVA to WORD array of ordinals (indexes into AddressOfFunctions)
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
// e_lfanew: 0x3c
//add offset 0x78 = Export Table
//How to access the Export Address Table
int32_t Export Table=
    * (int32_t*) (
        (uint8_t*)Dl1Base +
        *(uint32_t*)((uint8_t*)Dl1Base + 0x3C) +
        0x78
    );

// DlBase = module image base (void* or uint8_t*)
// e_lfanew: 0x3C
// OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT] is at +0x78 from NT headers
uint32_t export_rva = *(uint32_t*)(
    (uint8_t*)DlBase +
    *(uint32_t*)((uint8_t*)DlBase + 0x3C) +   // e_lfanew
    0x78                                      // Export DataDirectory RVA (within OptionalHeader)
);

// export_dir = (IMAGE_EXPORT_DIRECTORY*)(DlBase + export_rva)
IMAGE_EXPORT_DIRECTORY* exp = (IMAGE_EXPORT_DIRECTORY*)((uint8_t*)DlBase + export_rva);

// AddressOfNames is at offset 0x20 inside IMAGE_EXPORT_DIRECTORY
uint32_t addressOfNamesRVA = exp->AddressOfNames; // 0x20

// Convert RVA -> VA: pointer to an array of DWORD RVAs (each points to an ASCII name)
uint32_t* namesRVAArray = (uint32_t*)((uint8_t*)DlBase + addressOfNamesRVA);

// Example: get first exported name
char* firstName = (char*)DlBase + namesRVAArray[0];

// Example: iterate all names
for (uint32_t i = 0; i < exp->NumberOfNames; ++i) {
    char* name = (char*)DlBase + namesRVAArray[i];
    printf("%s\n", name);
}

After getting the relevant details such as the AddressofFunctions, Number of Functions, and NumberofNames. The function has a sufficient entries to implement a hashing algorithm in a for loop and store it somewhere. A comparison can now begin with the function input argument, if the hash matches, it returns the following which is a callable function pointer.


//cleaned version of return
return = DLLBase + AddressOfFunctions[ NameOrdinals[index] ]

raw return value for RokRAT loader’s API hash resolving function.

raw return value for RokRAT loader’s API hash resolving function.

Circling back

The malware can now call the function pointer directly. In this example, shows virtualalloc.

Resolving API hashing subroutine being called.

Resolving API hashing subroutine being called.

Key notes when reversing and identifying potential API hashing:

  • A hash is passed as an argument.
  • The retrurn value is then directly turned into a callable function pointer.
  • Identify walking of the Process Environment block (PEB) (FS:[0x30h] or GS:[0x60h]
  • Identify walking of the _PEB_LDR_DATA,_LDR_DATA_TABLE_ENTRY, and _IMAGE_EXPORT_DIRECTORY structures through looking at the offsets mentioned in the blog.
  • Or just run it against hashDB if the reversers lazy lol (unless it’s a custom algorithmn.

메타데이터
post_id
ffe711c616d2
slug
analysis-of-api-hashing-and-import-lookup-techniques-for-obfuscated-api-resolution-in-malware-ffe711c616d2
url
https://medium.com/@callyso0414/analysis-of-api-hashing-and-import-lookup-techniques-for-obfuscated-api-resolution-in-malware-ffe711c616d2
canonical_url
https://medium.com/@callyso0414/analysis-of-api-hashing-and-import-lookup-techniques-for-obfuscated-api-resolution-in-malware-ffe711c616d2
author_url
https://medium.com/@callyso0414
status
ok
fetched_at
2026-06-09 15:37:30