Post

StaticAPI-Hashing

StaticAPI-Hashing

What is it?

Resolves Windows API functions entirely by hash — no import table entries, no plaintext string references. Two custom functions replace GetModuleHandle and GetProcAddress by walking the PEB’s module list and a DLL’s export table, comparing Jenkins one-at-a-time hashes instead of string names.

How it works

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
Hash function — Jenkins One-at-a-Time (32-bit):
  HashingJenkins32BitA(String):
    Hash = 0
    for each char c in String:
      Hash += c
      Hash += Hash << 7   (SEED=7)
      Hash += Hash >> 6
    return Hash


GetModuleHandleHash(dwModuleHash):

  __readgsqword(0x60) → PEB
  PEB→Ldr→InMemoryOrderModuleList (circular doubly-linked list)

  Uses custom MY_LDR_DATA_TABLE_ENTRY struct
  (Microsoft's winternl.h hides DllBase/BaseDllName behind Reserved fields)

  Walk list until back at head:
    pDte = CONTAINING_RECORD(pEntry, MY_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks)
    
    pName = &pDte->BaseDllName  (e.g. L"USER32.DLL")

    Convert to uppercase ANSI (WCHAR → CHAR + toupper):
      for i in 0..pName->Length/sizeof(WCHAR):
        UpperCaseDLL[i] = toupper(pName->Buffer[i])

    HashA(UpperCaseDLL) == dwModuleHash?
      → return (HMODULE)pDte->DllBase


GetProcessAddrHash(hModule, dwApiHash):

  pBase = (PBYTE)hModule
  Parse DOS header → NT headers → Optional header
  → ExportDir VA → PIMAGE_EXPORT_DIRECTORY

  NameArray     = pBase + pExportDir->AddressOfNames
  AddressArray  = pBase + pExportDir->AddressOfFunctions
  OrdinalArray  = pBase + pExportDir->AddressOfNameOrdinals

  for i in 0..pExportDir->NumberOfNames:
    pFuncName = (CHAR*)(pBase + NameArray[i])
    
    dwApiHash == HashA(pFuncName)?
      funcRVA = AddressArray[OrdinalArray[i]]
      return (FARPROC)(pBase + funcRVA)


Usage:
  Pre-computed hashes (calculated once, hardcoded):
    USER32DLL_HASH   = HashA("USER32.DLL")  = 0xA48F48AC
    MESSAGEBOXA_HASH = HashA("MessageBoxA") = 0xD1CC12B7

  LoadLibraryA("USER32.DLL")  ← needed to ensure it's in the module list
  
  hUser32 = GetModuleHandleHash(0xA48F48AC)
  pMessageBoxA = GetProcessAddrHash(hUser32, 0xD1CC12B7)
  pMessageBoxA(NULL, "...", "N0xshell", MB_OK)


What static analysis sees:
  Imports:  HeapAlloc, LoadLibraryA, printf (no MessageBoxA, no GetProcAddress)
  Strings:  "USER32.DLL", "MessageBoxA" → ABSENT
            0xA48F48AC, 0xD1CC12B7 → meaningless numbers
  Code:     hash loop + PE parsing → recognizable pattern if analyst knows it

The custom MY_LDR_DATA_TABLE_ENTRY is necessary because Microsoft’s winternl.h definition of LDR_DATA_TABLE_ENTRY uses PVOID Reserved[N] arrays that obscure the actual field layout. The custom struct names the fields explicitly (DllBase, BaseDllName) matching their real offsets.

Module names are uppercased before hashing — USER32.DLL not user32.dll — so the pre-computed hashes only match if you uppercase consistently. This is a choice to make, and you just have to be consistent with it across both the hash computation at build time and the comparison at runtime.

apihashing.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
#include <Windows.h>        // Core Windows API definitions (HMODULE, FARPROC, etc.)
#include <stdio.h>          // For printf() and getchar()
#include <winternl.h>       // Contains PEB, PPEB, UNICODE_STRING, LIST_ENTRY structures

// =============================================
// Custom LDR_DATA_TABLE_ENTRY Structure
// Microsoft hides some fields in winternl.h, so we define our own
// to access BaseDllName reliably.
// =============================================
typedef struct _MY_LDR_DATA_TABLE_ENTRY
{
    PVOID Reserved1[2];                     
    LIST_ENTRY InMemoryOrderLinks;          // Linked list pointer for walking modules
    PVOID Reserved2[2];                     
    PVOID DllBase;                          // Base address of the DLL in memory (what we need)
    PVOID Reserved3[2];                     
    UNICODE_STRING FullDllName;             // Full path of DLL (e.g. C:\Windows\System32\user32.dll)
    UNICODE_STRING BaseDllName;             // Just the filename (e.g. USER32.DLL) - best for hashing
    BYTE Reserved4[8];                      
    PVOID Reserved5[3];                    
    union
    {
        ULONG CheckSum;
        PVOID Reserved6;
    };
    ULONG TimeDateStamp;
} MY_LDR_DATA_TABLE_ENTRY, * PMY_LDR_DATA_TABLE_ENTRY;


// =============================================
// Hashing Function: Jenkins One-at-a-Time (32-bit)
// =============================================
#define SEED 7
#define HashA(API) HashingJenkins32BitA((PCHAR)API)

// This function computes a hash for a given string.
// We use it for both DLL names and function names.
UINT32 HashingJenkins32BitA(_In_ PCHAR String)
{
    if (!String) return 0;                  // Safety check: prevent crash on NULL

    SIZE_T Index = 0;                       // Current position in string
    UINT32 Hash = 0;                        // Accumulated hash value
    SIZE_T strLength = lstrlenA(String);    // Get string length using Windows API

    while (Index != strLength)              // Loop through each character
    {
        Hash += String[Index++];            // Add current character to hash
        Hash += Hash << SEED;               
        Hash += Hash >> 6;                  
    }

    return Hash;                            // Return hash
}


// =============================================
// Resolve Function Address by Hash (Manual Export Table Parsing)
// =============================================
FARPROC GetProcessAddrHash(_In_ HMODULE hModule, _In_ DWORD dwApiHash)
{
    if (!hModule || !dwApiHash)             // Basic validation
        return NULL;

    PBYTE pBase = (PBYTE)hModule;           // Treat DLL base as byte pointer for RVA calculations

    // --- Parse PE Headers ---
    PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)pBase;
    if (pDosHdr->e_magic != IMAGE_DOS_SIGNATURE)  // Check 'MZ' signature
        return NULL;

    PIMAGE_NT_HEADERS pNtHdr = (PIMAGE_NT_HEADERS)(pBase + pDosHdr->e_lfanew);  // Jump to NT header

    // Get Export Directory RVA
    IMAGE_DATA_DIRECTORY ExportDir = pNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
    if (ExportDir.VirtualAddress == 0)
        return NULL;

    PIMAGE_EXPORT_DIRECTORY pExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ExportDir.VirtualAddress);

    // Three important arrays in export directory:
    PDWORD NameArray = (PDWORD)(pBase + pExportDir->AddressOfNames);        // Function names
    PDWORD AddressArray = (PDWORD)(pBase + pExportDir->AddressOfFunctions);    // Function RVAs
    PWORD  OrdinalArray = (PWORD)(pBase + pExportDir->AddressOfNameOrdinals);  // Name-to-Ordinal mapping

    // Loop through all named exported functions
    for (DWORD i = 0; i < pExportDir->NumberOfNames; i++)
    {
        CHAR* pFuncName = (CHAR*)(pBase + NameArray[i]);   // Get function name string

        if (dwApiHash == HashA(pFuncName))                 // Compare hash
        {
            DWORD funcRVA = AddressArray[OrdinalArray[i]]; // Get relative virtual address
            return (FARPROC)(pBase + funcRVA);             // Return absolute address
        }
    }

    return NULL;                            // Function not found
}


// =============================================
// Resolve Module (DLL) by Hash from PEB
// =============================================
HMODULE GetModuleHandleHash(_In_ DWORD dwModuleHash)
{
    if (!dwModuleHash)
        return NULL;

    // Get Process Environment Block (PEB) using GS register (x64 only)
    PPEB pPeb = (PPEB)__readgsqword(0x60);
    if (!pPeb || !pPeb->Ldr)
        return NULL;

    // Start walking the InMemoryOrderModuleList
    PLIST_ENTRY pHead = &pPeb->Ldr->InMemoryOrderModuleList;
    PLIST_ENTRY pEntry = pHead->Flink;

    while (pEntry != pHead)                 // Loop until we return to head (circular list)
    {
        // Get our custom structure using CONTAINING_RECORD macro
        PMY_LDR_DATA_TABLE_ENTRY pDte = CONTAINING_RECORD(pEntry, MY_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);

        if (pDte->DllBase)                  // Skip entries without base address
        {
            UNICODE_STRING* pName = &pDte->BaseDllName;   // Use BaseDllName (clean filename)

            if (pName->Length > 0 && pName->Buffer)
            {
                CHAR UpperCaseDLL[MAX_PATH] = { 0 };      // Buffer for converted name
                SIZE_T i = 0;

                // Convert UNICODE (WCHAR) to ANSI (CHAR) + uppercase
                for (i = 0; i < (pName->Length / sizeof(WCHAR)) && i < MAX_PATH - 1; i++)
                {
                    UpperCaseDLL[i] = (CHAR)toupper((UCHAR)pName->Buffer[i]);
                }
                UpperCaseDLL[i] = '\0';


                if (HashA(UpperCaseDLL) == dwModuleHash)  // Hash match
                {
                    return (HMODULE)pDte->DllBase;         // Return DLL base address
                }
            }
        }

        pEntry = pEntry->Flink;             // Move to next module in list
    }

    return NULL;                            // Module not found
}


// =============================================
// Function Typedef + Precomputed Hashes
// =============================================
typedef int (WINAPI* fnMessageBoxA)(
    HWND   hWnd,
    LPCSTR lpText,
    LPCSTR lpCaption,
    UINT   uType
    );

// These hashes were pre-calculated from uppercase strings
#define USER32DLL_HASH      0xA48F48AC      // Hash("USER32.DLL")
#define MESSAGEBOXA_HASH    0xD1CC12B7      // Hash("MessageBoxA")


int main()
{
    // Print hashes for verification
    printf("[i] Hash Of \"USER32.DLL\"  Is : 0x%08X\n", HashA("USER32.DLL"));
    printf("[i] Hash Of \"MessageBoxA\" Is : 0x%08X\n\n", HashA("MessageBoxA"));

    // Force load user32.dll into the process
    LoadLibraryA("USER32.DLL");

    // Resolve module handle using hash only
    HMODULE hUser32 = GetModuleHandleHash(USER32DLL_HASH);
    if (!hUser32)
    {
        printf("[!] GetModuleHandleHash failed - Could not find user32.dll\n");
        return -1;
    }

    printf("[+] Found user32.dll at: 0x%p\n", hUser32);

    // Resolve function address using hash only
    fnMessageBoxA pMessageBoxA = (fnMessageBoxA)GetProcessAddrHash(hUser32, MESSAGEBOXA_HASH);
    if (!pMessageBoxA)
    {
        printf("[!] Failed to resolve MessageBoxA by hash\n");
        return -1;
    }

    printf("[+] Resolved MessageBoxA at: 0x%p\n\n", pMessageBoxA);

    // Call the function
    pMessageBoxA(NULL, "Malware API Hashing Test", "N0xshell", MB_OK | MB_ICONEXCLAMATION);

    printf("[+] Success! Press <Enter> to exit...\n");
    getchar();

    return 0;
}
This post is licensed under CC BY 4.0 by the author.
Source code: StaticAPI-Hashing