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…
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.
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
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 Details
API hashing essentially operates in the following:
- hash values for a windows APIs is precomputed leveraging some type of hashing algorithm such as “CRC32”, “DJB2” and “FNV-1a”.
- 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.
- For each exported API name, it compares the computed hash to the target hash.
- 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
The DLLbase (address of the module) for NTDLL can be found here in a closer lookup.

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.
Circling back
The malware can now call the function pointer directly. In this example, shows virtualalloc.

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