{
	"id": "27ff5248-7347-4b18-a5ae-f3ce6e66a932",
	"created_at": "2026-04-06T00:19:53.924591Z",
	"updated_at": "2026-04-10T13:12:35.719481Z",
	"deleted_at": null,
	"sha1_hash": "2fed6f98eeedcd254c27c634b8a9d67ef423d6e4",
	"title": "Zloader String Obfuscation – nullteilerfrei",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 124981,
	"plain_text": "Zloader String Obfuscation – nullteilerfrei\r\nBy born\r\nArchived: 2026-04-05 17:21:28 UTC\r\nThis blag post describes my though-process during identification of the string deobfuscation method in a sample\r\nbelonging to the Zloader malware family. Specifically, I wanted to identify the function or functions responsible\r\nfor string deobfuscation only using static analysis and Ghidra, understand the algorithm, emulate it in Java and\r\nimplement a Ghidra script to deobfuscate all strings in a binary of this family.\r\nThe target audience of this post are people that have some experience with static reverse engineering and Ghidra\r\nbut who always asked themselves how the f those reversing wizards identify specific functionality within a binary\r\nwithout wasting hours, days and weeks.\r\nTarget Sample\r\nWe will be looking at the sample with SHA256 hash\r\n4029f9fcba1c53d86f2c59f07d5657930bd5ee64cca4c5929cbd3142484e815a , probably created on 2020-04-08\r\n18:19:58. According to people on the internet, this sample leverages string obfuscation, API hashing, a Domain\r\nGeneration Algorithm (DGA) and, code-level obfuscation techniques like constant unfolding, dead code insertion\r\nor arithmetic substitutions to hinder analysis. Right now, we only care about the string obfuscation and try to avoid\r\nlooking at anything else.\r\nThe malware family was first mentioned publicly by Fortinet in mid 2016: Their blog calls it DELoader based on\r\nsuspected targeting of Germany which in turn is based on geo-information of IPs in log files exposed by the\r\noperators in an open directory (\"DE\" is Germany's country code). The post also draws a connection to a handle\r\nAleksandr and usage of the banking Trojan Zeus, which seems to motivate the later name Zloader.\r\nIdentify the Deobfuscation function\r\nFollowing the list of heuristics from a previous blag post, we start at the entry point and while trying to avoid code\r\nthat's too complicated, find a function that is called in a lot of other places too and, which adheres to certain\r\nrequirements on data flowing into and out of it. Without even traversing into any of the functions called after the\r\nentry point, we click every function and list its references (use X if you have the best Ghidra Keybindings\r\navailable on the free market). Here is a table of called functions together with their number of references (that is,\r\nnot only calls at the entry point but in the whole binary):\r\nFunction Xrefs\r\nFUN_030a3170 190\r\nFUN_030ba440 55\r\nhttps://blag.nullteilerfrei.de/2020/05/24/zloader-string-obfuscation/\r\nPage 1 of 9\n\nFunction Xrefs\r\nFUN_030a3340 33\r\nFUN_030ba030 30\r\nFUN_030b8710 29\r\nFUN_030b1760 19\r\nFUN_030a3400 14\r\nFUN_030ba300 10\r\nFUN_030ba9d0 7\r\n... ...\r\nThe list is sorted by the number of references and we will work our way down from the highest number of cross\r\nreferences (is a handy script to generate such a list of functions together with their number of cross-references).\r\nFirst Things First\r\nThe first candidate is FUN_030a3170 . It seems to receive two arguments, both of which are used in conjunction\r\nwith arithmetic operators like % and \u003c . This makes it plausible that Ghidra correctly guessed their types to be\r\nnumbers. So if this is indeed a string deobfuscation function, it needs to access some sort of global variable\r\ncontaining the obfuscated variant of the string. In order for this function to be able to deobfuscate more than one\r\nstring, at least one of the two arguments should determine the concrete string within that global variable. But\r\nbefore we dive into that, let us double check that the data types of the arguments are correct by listing a few calls\r\nof the function:\r\npcVar1 = (code *)FUN_030a3170(0,0x6aa0e84);\r\niVar2 = (*pcVar1)(2,0);\r\n[...]\r\npcVar1 = (code *)FUN_030a3170(0,uVar3);\r\nuVar3 = (*pcVar1)(iVar2,local_23c);\r\n[...]\r\npcVar1 = (code *)FUN_030a3170(0,0xfed02a7);\r\niVar4 = (*pcVar1)(iVar2,local_23c);\r\nSo the first argument seems to be a small number and the second one a large one. What is more interesting though\r\nis, that the return value of this function is not used as a string but is called directly after. This suggests that the\r\nfunction we are looking at is not responsible for string deobfuscation but merely to resolve some API functions,\r\npotentially with the help of API hashing (see a post on API hashing if you want to learn about this technique in\r\ngeneral). Let's rename the function to pr_ResolveApi and not investigate it any further, we are here for string\r\ndeobfuscation!\r\nhttps://blag.nullteilerfrei.de/2020/05/24/zloader-string-obfuscation/\r\nPage 2 of 9\n\nFirst Try, Second Attempt\r\nFUN_030ba440 is a very short function that just calls FUN_03091c50 if it didn't receive the NULL pointer as an\r\nargument. This function in turn calls two other functions, one of which is our pr_ResolveApi . The other called\r\nfunction seems to be junk code but overall I'm confident that this aren't the droids, we are looking for.\r\nThird Time's a Charm\r\nLet's take a look at FUN_030a3340 : Ghidra determined that the function receives two pointer arguments. Looking\r\nat a few calls to this function, the first argument always seems to be a global variable while the second argument is\r\na local array variable. So if this is a string deobfuscation function, the first argument could be the obfuscated data\r\nwhile the second is a pointer where the result is written to. Following data flow within FUN_030a3340\r\ncorroborates the second part of this hypothesis: the content of param_2 is copied to a local variable which is later\r\nreturned.\r\nIf the hypothesis is correct and the malware sample uses an encryption scheme that needs a key, FUN_030a3340\r\nwould need to access some global variable to be used as a key - because there is no parameter left for the key:\r\naccording to our hypothesis, the first parameter is the obfuscated string while the second is an output parameter.\r\nThe only global variable (shown in purple in Ghidra) within the function is PTR_DAT_030be000 . This variable is\r\nused in two lines within FUN_030a3340 :\r\nuVar4 = (short)(char)*PTR_DAT_030be000 ^ *param_1;\r\n[...]\r\nuVar1 = FUN_030aba90((uint)*(ushort *)((int)param_1 + iVar5), (short)(char)PTR_DAT_030be000[uVar6 % 0x11]);\r\nHence this variable probably contains a pointer to an array. This array is indexed with uVar6 % 0x11 suggesting\r\na length of 17:\r\n00000000 59 49 2c 72 54 66 79 23 46 33 4d 61 71 31 33 69 |YI,rTfy#F3Maq13i|\r\n00000010 66 |f|\r\nAt this point, instead of reverse engineering the whole function in detail, let's take a leap: In the first of the two\r\ncode lines above, PTR_DAT_030be000 is Xor-ed with the first element of the param_1 array. Hence it may point\r\nto an Xor-key of length 17. Let's look up one of the passed arguments and Xor it with the above key: puVar1 =\r\nFUN_030a3340((ushort *)ARRAY_030bc900,local_52); references ARRAY_030bc900 which contains the following\r\ndata:\r\n00000000 0a 00 26 00 4a 00 06 00 23 00 07 00 0b 00 46 00 |..\u0026.J...#.....F.|\r\n00000010 1a 00 7e 00 24 00 02 00 03 00 5e 00 40 00 06 00 |..~.$.....^.@...|\r\n00000020 00 00 2d 00 49 00 |..-.I.|\r\nsadly, Xoring results in:\r\nhttps://blag.nullteilerfrei.de/2020/05/24/zloader-string-obfuscation/\r\nPage 3 of 9\n\n00000000 53 49 0a 72 1e 66 7f 23 65 33 4a 61 7a 31 75 69 |SI.r.f.#e3Jaz1ui|\r\n00000010 7c 59 37 2c 56 54 64 79 20 46 6d 4d 21 71 37 33 ||Y7,VTdy FmM!q73|\r\n00000020 69 66 74 49 65 72 |iftIer|\r\nBut you might have noticed that every second byte in the alleged obfuscated data is a zero-byte. This suggest that\r\nthe data is in fact a wide string with all upper bytes set to zero. After removing the zero-bytes, Xoring results in\r\nthe following:\r\n00000000 53 6f 66 74 77 61 72 65 5c 4d 69 63 72 6f 73 6f |Software\\Microso|\r\n00000010 66 74 00 |ft.|\r\nWe have found a string deobfuscation function! And we could also already determine that it uses Xor-encryption\r\nwith a hard-coded key of length 17. Don't forget to rename FUN_030a3340 to something like\r\nev_WideStringDeobfuscate .\r\nIt is also plausible, and can be confirmed by reversing ev_WideStringDeobfuscate a bit more, that the obfuscated\r\ndata is null-terminated. So instead of passing the length of the obfuscated data as an argument, the length is simply\r\ndetermined by the first occurrence of the \\0 -character.\r\nDue Diligence\r\nNow that we know that the global array pointed to by PTR_DAT_030be000 contains an Xor-Key, let's check for\r\nother references. As it turns out, the only other function referencing it, is FUN_030a3400 . This function is also on\r\nour list above (with 14 references) and we just do the same leap of faith, we did above: look up a reference to it\r\nand Xor the data passed as the argument\r\n00000000 32 2c 5e 1c 31 0a 4a 11 68 57 21 0d 71 |2,^.1.J.hW!.q|\r\nwith the hard-coded key:\r\n00000000 6b 65 72 6e 65 6c 33 32 2e 64 6c 6c 00 |kernel32.dll.|\r\nThere are no zero bytes in the obfuscated data, so maybe this is the non-wide-string variant of\r\nev_WideStringDeobfuscate . So let's rename FUN_030a3400 to ev_StringDeobfuscate .\r\nAutomation\r\nAs always, let us automate the process of finding all function references and deobfuscate the passed buffers.\r\nRoughly, we will follow this plan:\r\nask the user for name of deobfuscation function and parse its assembly to determine the Xor-Key\r\nfind all calls to the deobfuscation function and determine the first argument passed to the function\r\nhttps://blag.nullteilerfrei.de/2020/05/24/zloader-string-obfuscation/\r\nPage 4 of 9\n\ndeobfuscate the data and enrich the different Ghidra views (namely, assembly listing, decompiled code and\r\nthe bookmarks list)\r\nLet's first look at the only part that wasn't handled in other blag posts: parsing assembly and determining the Xor-Key. Both deobfuscation functions use special MOV instructions - namely, MOVZX (Move with Zero-Extend) and\r\nMOVSX (Move with Sign-Extension) - to read the Xor-Key from memory. Both instructions accept two operands,\r\na source and a destionation, and copy the contents of the source operand to the destination operand (while\r\nextending the value in some way that we don't care about). Below, are the two instructions in question from the\r\nev_StringDeobfuscate and ev_WideStringDeobfuscate functions respectively:\r\n030a3435 0F B6 30 ..0 movzx esi, byte ptr [eax]\r\n[...]\r\n030a3363 0F BE 19 ... movsx ebx, byte ptr [ecx]\r\nThe first one copies the value referenced by the register eax to esi while the second one does the same for\r\necx and ebx . Since there is only one of those move instruction in both functions, our goal is to search for it and\r\ntry to determine the value that was moved.\r\nThis situation also already demonstrates that compilers may use different registers in very similar situation. It also\r\nmeans, that we need to do some extra work if we want to automate discovery of the Xor key: We cannot simply\r\nuse the value from a fixed register but are merely going to iterate over all instructions within the function while\r\ntracking register values.\r\nFor tracking register values let us use the following simple Java helper class:\r\nprivate class InvalidRegisterNameException extends Exception {\r\n public InvalidRegisterNameException(String registerName) {\r\n super(String.format(\"Invalid register name: %s\", registerName));\r\n }\r\n}\r\nprivate class RegisterValues {\r\n private int[] values;\r\n public boolean debug;\r\n public RegisterValues() {\r\n values = new int[8];\r\n debug = false;\r\n }\r\n private int nameToIndex(String registerName) throws InvalidRegisterNameException {\r\n if (registerName.equals(\"EAX\") || registerName.equals(\"AL\") || registerName.equals(\"AH\")) {\r\n return 0;\r\n } else if (registerName.equals(\"EBX\") || registerName.equals(\"BL\") || registerName.equals(\"BH\")) {\r\n return 1;\r\nhttps://blag.nullteilerfrei.de/2020/05/24/zloader-string-obfuscation/\r\nPage 5 of 9\n\n} else if (registerName.equals(\"ECX\") || registerName.equals(\"CL\") || registerName.equals(\"CH\")) {\r\n return 2;\r\n } else if (registerName.equals(\"EDX\") || registerName.equals(\"DL\") || registerName.equals(\"DH\")) {\r\n return 3;\r\n } else if (registerName.equals(\"EBP\") || registerName.equals(\"BL\") || registerName.equals(\"BH\")) {\r\n return 4;\r\n } else if (registerName.equals(\"ESI\")) {\r\n return 5;\r\n } else if (registerName.equals(\"EDI\")) {\r\n return 6;\r\n } else if (registerName.equals(\"ESP\")) {\r\n return 7;\r\n } else {\r\n throw new InvalidRegisterNameException(registerName);\r\n }\r\n }\r\n public void set(String registerName, int value, Address address) throws InvalidRegisterNameException {\r\n if (debug) {\r\n println(String.format(\"0x%x writing 0x%x to %s\", address.getOffset(), value, registerName));\r\n }\r\n values[nameToIndex(registerName)] = value;\r\n }\r\n public int get(String registerName, Address address) throws InvalidRegisterNameException {\r\n int registerValue = values[nameToIndex(registerName)];\r\n if (debug) {\r\n println(String.format(\"0x%x reading %s as 0x%x\", address.getOffset(), registerName, registerValue));\r\n }\r\n return registerValue;\r\n }\r\n}\r\nAnd now, we can use this class to implement the actual algorithm to search for the Xor-Key:\r\npublic byte[] readXorKey(Function func, int searchDepth) throws MemoryAccessException {\r\n int i = 0;\r\n RegisterValues currentValues = new RegisterValues();\r\n for (Instruction instruction : currentProgram.getListing().getInstructions(func.getEntryPoint(), true)) {\r\n try {\r\n if (instruction.getMnemonicString().equals(\"MOVZX\")) {\r\n // MOVSX EBX,byte ptr [ECX]=\u003eBYTE_ARRAY_030bc4f0 =\r\n // Index 0: EBX\r\n // Index 1: ECX\r\n String registerName = instruction.getOpObjects(1)[0].toString();\r\n int registerValue = currentValues.get(registerName, instruction.getAddress());\r\nhttps://blag.nullteilerfrei.de/2020/05/24/zloader-string-obfuscation/\r\nPage 6 of 9\n\nbyte[] dataPtr = getOriginalBytes(toAddr(registerValue), 0x4);\r\n if (dataPtr != null) {\r\n byte[] data = getOriginalBytes(unpackAddressLE(dataPtr), 0x11);\r\n if (data != null \u0026\u0026 data.length == 0x11) {\r\n return data;\r\n }\r\n }\r\n } else if (instruction.getMnemonicString().equals(\"MOV\")) {\r\n // MOV ECX,dword ptr [030be000 == OBFU_PTR]\r\n // Index 0: ECX\r\n // Index 1: 030be000\r\n String registerName = instruction.getOpObjects(0)[0].toString();\r\n int copiedValue = instruction.getInt(1);\r\n currentValues.set(registerName, copiedValue, instruction.getAddress());\r\n } else if (instruction.getMnemonicString().equals(\"RET\")) {\r\n break;\r\n }\r\n } catch (InvalidRegisterNameException e) {\r\n println(String.format(\"Exception: %s\", e.toString()));\r\n }\r\n i++;\r\n if (i \u003e searchDepth)\r\n break;\r\n }\r\n byte[] defaultKey = { 0x59, 0x49, 0x2c, 0x72, 0x54, 0x66, 0x79, 0x23, 0x46, 0x33, 0x4d, 0x61, 0x71, 0x31, 0x\r\n 0x69, 0x66 };\r\n return defaultKey;\r\n}\r\nThe function just returns a default key if identifying the value is not successful.\r\nAsking the user for a function, finding all references as well as tracking argument values of those calls has been\r\ncovered thoroughly in previous blag posts. So the only thing left is the obfuscation itself, which is a simple Xor\r\nwith a multi-byte key:\r\nprivate byte[] cryptXor(byte[] data, byte[] key) {\r\n final byte[] ret = new byte[data.length];\r\n for (int k = 0; k \u003c data.length; k++)\r\n ret[k] = (byte) (data[k] ^ key[k % key.length]);\r\n return ret;\r\n}\r\nAs always, you can find the fully working script to deobfuscate all strings in Zloader on github.\r\nhttps://blag.nullteilerfrei.de/2020/05/24/zloader-string-obfuscation/\r\nPage 7 of 9\n\nSummary\r\nThe string obfuscation used in Zloader is quite generic: the analysed sample contains two different functions that\r\nuse the same global hard-coded Xor-key to decrypt zero-terminated obfuscated data. It was possible to identify\r\nthese functions without actual reverse engineering a lot of code in detail by starting at the entry point and looking\r\nat all calls sorted by number of other references of the called function.\r\nDeobfuscated Strings:\r\nFor google-ability:\r\nAddress Deobfuscated String\r\n0x0309111C kernel32.dll\r\n0x0309133F Software\\Microsoft (wide)\r\n0x03091AFF Software\\Microsoft (wide)\r\n0x03091CE0 BOT-INFO\r\n0x03091CF3 It's a debug version.\r\n0x03091D0F Proxifier.exe (wide)\r\n0x03091D4A BOT-INFO\r\n0x03091D60\r\nProxifier is a conflict program, form-grabber and web-injects will not works.\r\nTerminate proxifier for solve this problem.\r\n0x0309218F SeSecurityPrivilege (wide)\r\n0x030923E5\r\nMozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)\r\nChrome/79.0.3945.88 Safari/537.36\r\n0x030928F9 Software\\Microsoft (wide)\r\n0x0309559F /post.php\r\n0x030955F4 https://\r\n0x03095D99 C:\\Windows\\SystemApps\\* (wide)\r\n0x03095E1D Microsoft.MicrosoftEdge (wide)\r\n0x03095E57 6.3\r\n0x030962E4 HideClass (wide)\r\n0x03096332 HideWindow (wide)\r\nhttps://blag.nullteilerfrei.de/2020/05/24/zloader-string-obfuscation/\r\nPage 8 of 9\n\nAddress Deobfuscated String\r\n0x03096348 HideClass (wide)\r\n0x03096E37 .exe (wide)\r\n0x03096E5B .dll (wide)\r\n0x03096E7A .dll (wide)\r\n0x03096EAB .exe (wide)\r\n0x03096F5F .exe\r\n0x03096FC8 \u003e\u003e (wide)\r\n0x03097022 .dll\r\n0x030972E1 Software\\Microsoft\\ (wide)\r\n0x0309741D UNKNOWN (wide)\r\n0x03097492 Software\\Microsoft\\Windows NT\\CurrentVersion (wide)\r\n0x030974A5 InstallDate (wide)\r\n0x030974CB DigitalProductId (wide)\r\n0x030974F1 %s_%08X%08X (wide)\r\n0x0309754F INVALID_BOT_ID (wide)\r\n0x03097790 rundll32.exe %s,DllRegisterServer (wide)\r\n0x030977C5 Software\\Microsoft\\Windows\\CurrentVersion\\Run (wide)\r\n0x030977F9 Software\\Microsoft\\Windows\\CurrentVersion\\Run (wide)\r\n0x03097CF2 Software\\Microsoft\\ (wide)\r\n0x03098065 Software\\Microsoft\\Windows\\CurrentVersion\\Run (wide)\r\n0x0309809E .dll (wide)\r\n0x03098174 S:(ML;;NRNWNX;;;LW) (wide)\r\n0x030986F1 Software\\Microsoft\\ (wide)\r\n0x0309BD3F .com\r\nSource: https://blag.nullteilerfrei.de/2020/05/24/zloader-string-obfuscation/\r\nhttps://blag.nullteilerfrei.de/2020/05/24/zloader-string-obfuscation/\r\nPage 9 of 9",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://blag.nullteilerfrei.de/2020/05/24/zloader-string-obfuscation/"
	],
	"report_names": [
		"zloader-string-obfuscation"
	],
	"threat_actors": [],
	"ts_created_at": 1775434793,
	"ts_updated_at": 1775826755,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/2fed6f98eeedcd254c27c634b8a9d67ef423d6e4.pdf",
		"text": "https://archive.orkl.eu/2fed6f98eeedcd254c27c634b8a9d67ef423d6e4.txt",
		"img": "https://archive.orkl.eu/2fed6f98eeedcd254c27c634b8a9d67ef423d6e4.jpg"
	}
}