{
	"id": "a36a5b5c-c45b-473b-8f3c-0cac22d6af04",
	"created_at": "2026-04-06T00:22:04.008925Z",
	"updated_at": "2026-04-10T13:11:30.212661Z",
	"deleted_at": null,
	"sha1_hash": "7b9df81904e947fae835b7aa8bced771a0ee368e",
	"title": "API Hashing in the Zloader malware – nullteilerfrei",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 130722,
	"plain_text": "API Hashing in the Zloader malware – nullteilerfrei\r\nBy born\r\nPublished: 2021-01-04 · Archived: 2026-04-05 12:58:03 UTC\r\nDirecting your attention as a reverse engineer is key for not wasting your life looking at irrelevant code. This blag post will\r\nuse an anti-analysis technique used in the Zloader malware as an example to practice this art. We will also take a short\r\ndetour into code-level obfuscation and are going to re-implement the API hashing function from Zloader in Python.\r\nThis post is aimed towards reverse engineering beginners that have already heard about API hashing. If you don't know,\r\nwhat Ghidra is or how to use it, you will need to brush over some parts of this post.\r\nWhat is API Hashing\r\nIn case, you don't follow this blag closely, I'll quickly summarize, what I mean with API hashing: if a malware author\r\ndoesn't want to include API function names in the malware - neither in the import address table nor as strings somehow\r\npassed to GetProcAddress for example - they can use API hashing. This involves calculating some sort of hash for each\r\ncombination of DLL file name and API function name (often only the latter) and inclusion of those hashes in the malware\r\ninstead.\r\nGiven an API hash, the malware can enumerate all loaded DLLs and their exported functions to calculate hashes with the\r\nsame custom algorithm and compare the result to the given hash, ultimately enabeling resolution of the corresponding API\r\nfunction. The topic is covered more thoroughly in the post about API hashing in the REvil ransomware.\r\nIdentifying the API Resolution Function\r\nWe will be looking at the Zloader sample with SHA256 hash\r\n4029f9fcba1c53d86f2c59f07d5657930bd5ee64cca4c5929cbd3142484e815a\r\nIn another blag post about string obfuscation, we stumbled upon the API hashing function of Zloader: The function\r\nFUN_030a3170 is called in 190 places and each time, it receives some small integral number and a larger value fitting into a\r\nDWORD . This alone slightly smells like API hashing but a dead give-away is the fact that the returned value of the function is\r\nalways CALL ed shortly after:\r\npcVar1 = (code *)FUN_030a3170(0,0x6aa0e84);\r\niVar2 = (*pcVar1)(2,0);\r\nSo let us rename FUN_030a3170 to ev_ResolveApi and take note of a few argument combinations:\r\nFirst\r\nArgument\r\nSecond\r\nArgument\r\nCall\r\n0 0x6aa0e84 f(2,0)\r\n1 0xf3c7b77 f(0,puVar5,puVar4,0xcf0000,0x80000000,0x80000000,0x80000000,0x80000000,0,0,uVar3,0);\r\nhttps://blag.nullteilerfrei.de/2020/06/11/api-hashing-in-the-zloader-malware/\r\nPage 1 of 8\n\nFirst\r\nArgument\r\nSecond\r\nArgument\r\nCall\r\n9 0xabc78f7 f(puVar1,1,\u0026local_14,0)\r\nA quick look at the decompiled code of the function should instantly make you loose interested in reverse engineering it top\r\nto bottom: It looks very convoluted and long. But let us not give up but leap our way to the goal.\r\nThe First Argument\r\nSo let's skip over everything and only realize that the first argument is used to index the array named PTR_DAT_030bc2ec .\r\nThe data PTR_DAT_030bc2ec[param_1] is then passed to the string deobfuscation function, analyzed in a previous blag post.\r\nDouble clicking the array will show the following in the assembly listing view:\r\n PTR_DAT_030bc2ec XREF[1]: FUN_030a3170:030a3224(R)\r\n030bc2ec e8 c3 0b 03 addr DAT_030bc3e8 = 32h\r\n030bc2f0 f5 c3 0b 03 addr DAT_030bc3f5 = 2Ch\r\n030bc2f4 00 c4 0b 03 addr DAT_030bc400 = 37h\r\n...\r\n030bc348 e8 c3 0b 03 addr DAT_030bc3e8 = 32h\r\n030bc34c db c4 0b 03 addr DAT_030bc4db = 3Bh\r\nGhidra identified each of the array entries as a pointer. So let's interpret each entry of this array as an obfuscated string and\r\ndecrypt it:\r\nIndex DLL Name\r\n0 kernel32.dll\r\n1 user32.dll\r\n2 ntdll.dll\r\n3 shlwapi.dll\r\n4 iphlpapi.dll\r\n5 urlmon.dll\r\n6 ws2_32.dll\r\n7 crypt32.dll\r\n8 shell32.dll\r\n9 advapi32.dll\r\n10 gdiplus.dll\r\n11 gdi32.dll\r\n12 ole32.dll\r\nhttps://blag.nullteilerfrei.de/2020/06/11/api-hashing-in-the-zloader-malware/\r\nPage 2 of 8\n\nIndex DLL Name\r\n13 psapi.dll\r\n14 cabinet.dll\r\n15 imagehlp.dll\r\n16 netapi32.dll\r\n17 wtsapi32.dll\r\n18 mpr.dll\r\n19 wininet.dll\r\n20 userenv.dll\r\n21 bcrypt.dll\r\nHence the first argument to the function is an index into the above listed array of DLL names and hence almost certainly\r\nused to specify the DLL to use when resolving an API function.\r\nNote the choice of words here: I did not say that I am sure that the argument is used to specify the DLL which is used to\r\nresolve a function; but only, that I am almost certain. When reverse engineering like this, you should keep the amount of\r\ncertainty for every statement in the back of your head. So if something doesn't make sense anymore, you can track back and\r\nmore easily assess, where to dig deeper. In this case, I don't see a lot of other possibilities, what this DLL name will be used\r\nfor otherwise.\r\nThe second argument\r\nLet us do a similar trick with the second argument: don't reverse engineer the whole function but just look at the four places,\r\nwhere the second argument appears while keeping in mind that we believe it to specify an API hash of the function to be\r\nresolved:\r\nuVar1 = param_2 % uVar1;\r\n...\r\nuVar2 = FUN_030b9a70(uVar2,param_2,0,0);\r\n...\r\npcVar5 = FUN_030a3620(iVar4,param_2);\r\n...\r\n*local_14 = param_2;\r\nThe alleged API hash is passed into the two functions FUN_030b9a70 and FUN_030a3620 . We will now take a look at the\r\ntwo, keeping an eye out for code that calculates an API hash to then compare it to the passed argument.\r\nOn first glance, the first of the two functions looks promising: it contains some arithmetic operations and calls a few other\r\nfunctions. But looking at the return value, one can instantly see that it either returns 0xa1 or 0 . So these are probably not\r\nthe droids we are looking for. The second function - FUN_030a3620 - looks at least as promising as the first one: it contains\r\nthe two constants 0x60 and 0x18 at the very top and also uses a few (nested) loops.\r\nhttps://blag.nullteilerfrei.de/2020/06/11/api-hashing-in-the-zloader-malware/\r\nPage 3 of 8\n\nSo if someone would point a gun to my head and ask me for an opinion, which of the two you should investigate further, I'd\r\ndefinitely choose the second. And you should always imagine that someone is pointing a gun to your head while reverse\r\nengineering. We don't have no time for anything else.\r\nI nearly forgot to repeat a life hack from the very same blag post already referenced a few times, which should clear up, why\r\nI got so excited about the two constants 0x60 and 0x18 : 0x18 is the offset of the Optional Header within the PE header\r\nand 0x60 is the offset of the Data Directories within that Optional Header. We don't need to understand everything here\r\nbut can simply assume that there is some sort of PE parsing going on (that is parsing of the Windows Portable Executable\r\nfile format). And you need PE parsing to list exports from loaded DLLs, hence you need PE parsing to calculate API hashes\r\nof loaded functions.\r\nLucky for us, the API hash passed in as an argument is only used in one single line:\r\nif (uVar3 == param_2) {\r\nAnd since it does not make much sense to compare an API hash with anything else but another API hash, it is reasonable to\r\nassume that uVar3 also contains an API hash. It is also plausible that it contains the API hash calculated by the malware\r\nbased on loaded DLL names and their exported functions. Since the value of uVar3 comes out of FUN_030a3140 let's\r\nrename that function to pr_ApiHash . It receives local_90 and -1 as arguments. So let's just assume for now that\r\nlocal_90 is somehow derived from DLL and function names and dive into pr_ApiHash .\r\nThe API Hashing Function\r\nLazy time is over now. We finally need to understand some code and what exactly, pr_ApiHash does with its arguments to\r\narrive at an API hash. Since we already assumed that the first argument contains some data derived from DLL and function\r\nnames, let us focus on the second argument for now: It is first compared with -1 - which makes sense because we already\r\nobserved this value as an argument - and another function, FUN_030a2fe0 , is called with the alleged DLL and function\r\nnames as arguments. Let's look into FUN_030a2fe0 and retype its argument to BYTE * :\r\nint __cdecl FUN_030a2fe0(BYTE *param_1) {\r\n int iVar1;\r\n int iVar2;\r\n if (param_1 != (BYTE *)0x0) {\r\n iVar2 = -1;\r\n do {\r\n iVar1 = iVar2 + 1;\r\n iVar2 = iVar2 + 1;\r\n } while (param_1[iVar1] != '\\0');\r\n return iVar2;\r\n }\r\n return 0;\r\n}\r\nIf the passed data is the NULL pointer, the function will return 0 . Otherwise, it will initialize the variable iVar2 with\r\n-1 and increase value passed into the function until it is the NULL terminator. During each iteration, the return variable\r\niVar2 is incremented by one. Since this is a do-while loop, this incrementation happens at least one time. Staring at this\r\ncode a bit more, you can see that this function will interpret the passed argument as a string and return its length. This is\r\nhttps://blag.nullteilerfrei.de/2020/06/11/api-hashing-in-the-zloader-malware/\r\nPage 4 of 8\n\nhuge because we can now guess the type of the argument and also the type of the variable passed into this function: it\r\nprobably is just char * as opposed to some complex data structure derived from DLL and function names.\r\nSo let us rename FUN_030a2fe0 to strlen and retype the two arguments to pr_ApiHash according to what we just\r\nlearned. While we are at it, realize that uVar4 is the value returned from pr_ApiHash and rename that variable to\r\nApiHash .\r\nuint __cdecl pr_ApiHash(char *SomeString,int StrLen) {\r\n byte bVar1;\r\n uint uVar2;\r\n uint uVar3;\r\n uint ApiHash;\r\n if (StrLen == -1) {\r\n StrLen = strlen(SomeString);\r\n }\r\n ApiHash = 0;\r\n if ( (SomeString != (char *)0x0) \u0026\u0026 (0 \u003c StrLen) ) {\r\n ApiHash = 0;\r\n do {\r\n bVar1 = FUN_030a5260();\r\n ApiHash = (uint)(byte)*SomeString + (ApiHash \u003c\u003c (bVar1 \u0026 0x1f));\r\n if ( (ApiHash \u0026 0xf0000000) != 0 ) {\r\n uVar3 = (ApiHash \u0026 0xf0000000) \u003e\u003e 0x18;\r\n uVar2 = FUN_030a9b90(0xfffffff,0xffffffff,0);\r\n ApiHash = FUN_030aeef0(~(uVar2 | ~ApiHash | uVar3),(uVar2 | ~ApiHash) \u0026 uVar3,(HINSTANCE)0x0);\r\n }\r\n SomeString = (char *)( (byte *)SomeString + 1);\r\n StrLen = StrLen + -1;\r\n } while (StrLen != 0);\r\n }\r\n return ApiHash;\r\n}\r\nCode-Level Obfuscation\r\nNow we need to get really un-lazy. There are three functions used during calculation of the API hash with names\r\nFUN_030a5260 , FUN_030a9b90 and, FUN_030aeef0 . Each of these functions needs special attention.\r\nEven though the return value of FUN_030a5260 is used, Ghidra did not correctly guess the function signature and\r\nsomehow determine that it is a void function. Change the signature (Hotkey F ), check \"Use Custom Storage\" and\r\nchange the returned data type to int and the storage location to EAX . Choosing EAX is often correct and I suggest\r\nto just try it and justify later if the resulting decompiled code makes sense. Again, purely for time-efficiency reasons.\r\nThe result will be a convoluted function that ends with return _DAT_030be374 ^ 0xa2df808b . Follow\r\n_DAT_030be374 and change the type to ddw (Hotkey D three times). This reveals that this global variable contains\r\nthe value 0xA2DF808F . Xor-ing with 0xa2df808b results in 4 . Hence, we can rename FUN_030a5260 to\r\nReturn4 .\r\nSimilarly, FUN_030a9b90 is identified to be a void function. Performing the same procedure as above (adapt the\r\nsignature to return an int in EAX ) will lead to a very simple decompiled function that only Xors the first two\r\narguments. Hence you can rename it to Xor (and also remove the last parameter if you feel tidy).\r\nhttps://blag.nullteilerfrei.de/2020/06/11/api-hashing-in-the-zloader-malware/\r\nPage 5 of 8\n\nFinally, FUN_030aeef0 only seems to calculate the binary or of the two parameters, hence rename it to Bor (and,\r\nagain, remove the third parameter if you like).\r\nThe above three functions are probably caused by anti-analysis techniques employed by the malware author. The technique\r\nused in the first function is called \"constant unfolding\" because it is the opposite of the compiler optimization technique\r\ncalled constant folding. Constant folding evaluates constant expressions during compile time to avoid unnecessary\r\ncalculations during run time. Constant unfolding does the reverse: it identifies constants - 4 in this case - and replaces\r\nthem with some sort of calculation - _DAT_030be374 ^ 0xa2df808b in this case - during compile/build time.\r\nSimilarly, the other two function employ the opposite of the compiler optimization technique called inlining: Instead of\r\nperforming the arithmetic operation in-line (here, a simple Xor / Binary Or), a function is called that performs this operation.\r\nIn addition to that, unnecessary instructions where inserted into this un-in-lined function that make the code harder to read.\r\nSpecifically the condition of a branch like\r\nif ( ( ( (param_2 == 0xb4c6d61) \u0026\u0026 (fuLoad != param_1)) \u0026\u0026\r\n (in_stack_0000000c != (HINSTANCE)0xb4c6d61)) \u0026\u0026\r\n ( ( (int)in_stack_0000000c \u003c\u003c 7 | (uint)in_stack_0000000c) == 0)) {\r\nthat is never taken is called opaque predicate. In addition to that, both function contain some jung instructions without any\r\nside effects. A lot of them have been removed by the Ghidra decompiler, but since identifying those is hard - even\r\nheuristically - some still remain.\r\nRe-Implementing the Hashing Function\r\nOffentimes you want to emulate API hashing in a different language because it enables you to annotate API resolution calls\r\nduring static analysis. Re-implementing an algorithm will often also get rid of any implementational details that may have\r\neven been introduces by a compiler or obfuscator during built. This in turns eases identification of overlaps in the hashing\r\nmethod between different malware families, which in turn may indicate a link between the families.\r\nAfter performing the above-described steps and some minor adjustments to variable names in the pr_ApiHash function, we\r\nend up with the following:\r\nuint __cdecl pr_ApiHash(char *SomeString,int StrLen) {\r\n byte Four;\r\n uint Mask;\r\n uint HighNibble;\r\n uint ApiHash;\r\n if (StrLen == -1) {\r\n StrLen = strlen(SomeString);\r\n }\r\n ApiHash = 0;\r\n if ( ( SomeString != (char *)0x0 ) \u0026\u0026 (0 \u003c StrLen) ) {\r\n ApiHash = 0;\r\n do {\r\n _Four = Return4();\r\n ApiHash = (uint)(byte)*SomeString + (ApiHash \u003c\u003c ( (byte)_Four \u0026 0x1f ));\r\n if ( (ApiHash \u0026 0xf0000000) != 0 ) {\r\n HighNibble = (ApiHash \u0026 0xf0000000) \u003e\u003e 0x18;\r\n Mask = Xor(0xfffffff,0xffffffff);\r\nhttps://blag.nullteilerfrei.de/2020/06/11/api-hashing-in-the-zloader-malware/\r\nPage 6 of 8\n\nApiHash = Bor(~(Mask | ~ApiHash | HighNibble),(Mask | ~ApiHash) \u0026 HighNibble);\r\n }\r\n SomeString = (char *)( (byte *)SomeString + 1 );\r\n StrLen = StrLen + -1;\r\n } while (StrLen != 0);\r\n }\r\n return ApiHash;\r\n}\r\nYou can just copy this into a text editor and change the syntax a bit until it is valid code of your language of choice. If your\r\nlanguage natively supports bigints (like Python), better make sure to sprinkle it with enough \u0026 0xffffffff . I decided to\r\nuse Python for now and since I very much enjoy totally unnecessary optimizations, I ended up with the following:\r\ndef calc_hash(function_name):\r\n mask = 0xf0000000\r\n ret = 0\r\n for c in function_name:\r\n ret = ord(c) + (ret \u003c\u003c 0x4)\r\n if ret \u0026 mask:\r\n ret = (~ret | mask) ^ (~ret | ~mask) \u003e\u003e 0x18\r\n return ret \u0026 0xffffffff\r\nAPI Hash Lookup\r\nWe already know that the API hash 0x6aa0e84 from the kernel32.dll should resolve to a function that accepts two\r\narguments like so f(2,0) . So let us plug all exports from the kernel32.dll into the hashing function and check the\r\nresult:\r\nimport pefile\r\npe = pefile.PE(data=open('C:\\\\Windows\\\\SysWOW64\\\\kernel32.dll', 'rb').read())\r\nexport = pe.DIRECTORY_ENTRY_EXPORT\r\ndll_name = pe.get_string_at_rva(export.struct.Name)\r\nfor pe_export in export.symbols:\r\n export_name = pe_export.name.decode('utf-8')\r\n if calc_hash(export_name) == 0x6aa0e84:\r\n print(export_name)\r\nWhich ... fails by giving no result. Since I was pretty sure about everything but the data actually passed into pr_ApiHash , I\r\ndecided to do some reversing around that next: Ghidra determined the type of variable local_90 to be undefined2\r\nlocal_90 [50] . This is Ghidra's way of telling you that it thinks it is an array with 50 entries where each entry has a length\r\nof 2 bytes. Since we already established that the array is actual a string, I decided to retype it to char[100] :\r\n...\r\nFUN_0309ea50(local_90, uVar3);\r\ncVar1 = *(char *)(iVar5 + param_1);\r\nif (cVar1 != '\\0') {\r\n i = 0;\r\n do {\r\nhttps://blag.nullteilerfrei.de/2020/06/11/api-hashing-in-the-zloader-malware/\r\nPage 7 of 8\n\nlocal_90[i] = FUN_0309a690(cVar1);;\r\n cVar1 = *(char *)(iVar5 + param_1 + 1 + i);\r\n i = i + 1;\r\n } while (cVar1 != '\\0');\r\n}\r\nuVar4 = pr_ApiHash(local_90,-1);\r\n...\r\nSo the values of that array come out of the function FUN_0309a690 . Let's take a close look at it: the function receives a\r\nvalue and either returns it or adds 0x20 and returns the result. Because the condition looks complicated and was hit pretty\r\nhard by the obfuscator the author probably uses, I was just lucky to know what adding the number 0x20 in the context of\r\nstrings may mean: converting upper-case characters to lower-case characters. So my leap of faith was to assume that\r\nFUN_0309a690 actually is pr_toLower . And heureca! Running the above Python code with lower-cased export_name\r\nresults in a single hit, namely CreateToolhelp32Snapshot which accepts two DWORD arguments according to the\r\ndocumentation. This is in-line with our observation from the table at the start of this post.\r\nSummary\r\nWhat can you take away from this post? Maybe it is that I'm just as lazy as a sloth and don't even reverse engineer. Maybe,\r\nthat adding or subtracting 0x20 means converting between upper and lower case strings. Maybe, that offsets 0x18 and\r\n0x60 indicate PE parsing. Or maybe, that it is sometimes possible to understand a lot of about a malware without going\r\ninto every single line of code and understanding everything.\r\nSource: https://blag.nullteilerfrei.de/2020/06/11/api-hashing-in-the-zloader-malware/\r\nhttps://blag.nullteilerfrei.de/2020/06/11/api-hashing-in-the-zloader-malware/\r\nPage 8 of 8",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://blag.nullteilerfrei.de/2020/06/11/api-hashing-in-the-zloader-malware/"
	],
	"report_names": [
		"api-hashing-in-the-zloader-malware"
	],
	"threat_actors": [],
	"ts_created_at": 1775434924,
	"ts_updated_at": 1775826690,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/7b9df81904e947fae835b7aa8bced771a0ee368e.pdf",
		"text": "https://archive.orkl.eu/7b9df81904e947fae835b7aa8bced771a0ee368e.txt",
		"img": "https://archive.orkl.eu/7b9df81904e947fae835b7aa8bced771a0ee368e.jpg"
	}
}