{
	"id": "af90a799-d3ff-4a66-8002-f2ca7dce9578",
	"created_at": "2026-04-06T00:20:10.612066Z",
	"updated_at": "2026-04-10T03:20:19.849801Z",
	"deleted_at": null,
	"sha1_hash": "0b9bdade7c908aedaed2615921bcc99c257315b2",
	"title": "Reversing a recent IcedID Crypter",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 1165636,
	"plain_text": "Reversing a recent IcedID Crypter\r\nBy Leandro Fróes\r\nPublished: 2023-07-04 · Archived: 2026-04-05 17:54:37 UTC\r\nIntro\r\nLast week a friend shared a sample of a recent IcedID malware using an interesting Crypter and although there’s\r\nnothing new regarding the final payload (it’s just the classical IcedID Lite Loader) the analysis of the Crypter was\r\nfunny, so I decided to take some time to reverse it and share my analysis notes here.\r\nTo be honest I’m not that familiar with crypters in general so I ended up doing the analysis not knowing if the\r\ncrypter was known or not and after finishing the analysis I ended up noticing it’s actually a kind of variant of a\r\nCrypter named Snow . According to a very nice report from IBM, Snow is a new/active crypter that has been used\r\nby malwares like Pikabot, IcedID and Qakbot and that has some code overlap indicating this is a sucessor of the\r\nHexa crypter.\r\nBefore we start, it’s worth to mention that I cleaned the code in IDA (at least a good part of it) so if you open this\r\nfile in your IDA or whatever framework it might not match exactly what you’ll see here. I decided to use my clean\r\nversion in the screenshots to make it easier to the reader to understand the explanation. Also, since the crypter\r\nstages contains a lot of junk code splitted in multiple branches I decided to rely on the decompiler most part of the\r\ntime.\r\nGeneral execution flow\r\nThe analyzed malware is a 64 bits DLL file and it’s execution starts by calling an exported function named vcab\r\n(usually via the rundll32.exe binary). A parameter named /k is passed to the file as well as it’s value. In the\r\nanalyzed sample the parameter value passed is the string zefirka748 :\r\nrundll32.exe mw.dll,vcab /k zefirka748\r\nIf we take a quick look at the file statically we can notice that there’s a lot of exports available other than this\r\n“vcab”:\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 1 of 20\n\nMalware export table\r\nA simple Google search would tell us this seems to be some sort of Trojanized version of a library that implements\r\nthe Theora video compression format. If we search for the “vcab” export in the library’s export list we find zero\r\nresults and that kind of confirms to us that this export is in fact suspicious and that probably the whole malicious\r\nactions would start from there.\r\nOnce the export function is executed the malware executes multiple stages (basically shellcodes) and ends up\r\nloading and executing the final payload, which is the IcedID Lite Loader.\r\nStage 0 (vcab export)\r\nCmdline parameter checking\r\nThe first thing performed by this export function is check if the process cmdline has a parameter named /k and a\r\nvalue for it. At this point there’s no checks regarding the value passed and the content is just saved for further\r\nusage.\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 2 of 20\n\nFunction\r\nresponsible for getting the cmdline parameter.\r\nCrypter configuration\r\nThe crypter reads and manipulates a lot of fields from what seems to be it’s configuration. These fields are splitted\r\nin multiple sections such as .text and /81 and contains information like encrypted shellcodes, export function\r\nnames, shellcode sizes, and more.\r\nIn the analyzed sample, the configuration is present 0x16735 bytes after the base address of the malware DLL\r\nmodule. In order to read the configuration the malware gets the current module base address and adds the\r\nmentioned RVA to it.\r\nThe module base address is obtained by using the function responsible for getting the config offset as a base\r\naddress and then searching backwards until it finds both the PE Signature and the “MZ” Signature:\r\n Get config offset function.\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 3 of 20\n\nGet module base function.\r\nThe mentioned config has something similar to the following format:\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\nstruct MAIN_CONFIG_INFO\r\n{\r\n DWORD init_export_str_offset;\r\n DWORD stage2_offset;\r\n DWORD stage2_size;\r\n DWORD stage3_offset;\r\n DWORD stage3_size;\r\n DWORD stage1_offset;\r\n DWORD stage1_size;\r\n DWORD stage4_offset;\r\n DWORD stage4_size;\r\n DWORD main_payload_info_offset;\r\n DWORD main_payload_compressed_size;\r\n DWORD config_xor_key;\r\n};\r\nThe “offset” word here is actually an RVA since those are added to the main module base address.\r\nAPI function resolving\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 4 of 20\n\nOnce the necessary information is obtained 3 functions are resolved in runtime: VirtualAlloc , LoadLibraryA\r\nand VirtualProtect . Those functions are resolved via the classic API Hashing technique and the algorithm used\r\nis the well known Metasploit ROR13 algorithm. To make my life easier during static analysis I used the nice\r\nHashDB plugin from OALabs to recognize the function hashes used.\r\nThe API Hashing technique is basically the parsing of the Loaded Modules List from PEB as well as the Export\r\nTable from the target modules. Each export name entry would have it’s hash calculated using the hashing\r\nalgorithm and the result is compared against the hashes specified by the malware. Once the desired hash is found\r\nthe export address is returned.\r\nStage 2, 3 and 4 decryption\r\nWith the config in hands the next stages content (2, 3 and 4 specifically) is read and written to a memory location\r\nallocated using VirtualAlloc . A key is then read from the config (5c 3b 0c 00 in this case) and is used as a\r\nmultibyte XOR key to “decrypt” (well, it’s just XORed) the mentioned stages:\r\n XOR key\r\nlocated in the malware config.\r\nConfig parsing and next stages decryption.\r\nThe decrypted content will be saved and passed to the next stage further on.\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 5 of 20\n\nStage 1 decryption and call\r\nThe LoadLibraryA function is used to load a Windows DLL named dpx.dll . Once the base address of this\r\nDLL is obtained via the return value of LoadLibraryA it’s PE headers are parsed and it’s Export Directory\r\nobtained. It then gets the first exported function from the dpx.dll file ( DpxCheckJobExists in this case):\r\ndpx.dll loading and export table parsing.\r\nConsidering the DLL is loaded in the same address space of the malware module it’s content can be easialy\r\nreplaced and that’s exactly what the crypter does. The content of the Stage 1 that is present in the config is written\r\ninto the DpxCheckJobExists function. By default this stage is “encrypted” (XOR again!). After it’s written to the\r\nmentioned function it’s decryted using a multibyte XOR calculation using the provided cmdline key as the XOR\r\nkey.\r\nThe final step of the Stage 0 (vcab export) is call the DpxCheckJobExists function from the dpx.dll, passing 5\r\nparameters to it:\r\n1. The malware DLL base address\r\n2. The address of the “init” string (obtained from the malware config)\r\n3. A struct containing information regarding the next stages\r\n4. A struct containing information regarding the main payload\r\n5. The XOR key used to decrypt the Stage 2, 3 and 4\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 6 of 20\n\nStage 1 decryption and call.\r\nStage 1 (DpxCheckJobExists export)\r\nThis stage is the first “shellcode” involved in the chain. In order to analyze it (as well as the other shellcodes) I\r\ndumped it from the process memory using the crypter config fields as a reference (e.g. offset and size). Once it’s\r\ndumped we can pretty much load it in IDA and force the analysis. Since it’s a raw payload IDA will not load the\r\nWindows type libraries so we need to do it manually by going to View -\u003e Open subviews -\u003e Type Libraries (or\r\nsimply Shift + F11). In the opened window we Right Click -\u003e Load type library (or simply Ins) and add the library\r\nthat better fits our needs. In general I would go with the mssdk64_win10 one.\r\nThe beginning of this stage involves a lot of manipulation of the information received via parameter of the\r\nDpxCheckJobExists function. Other than that, a kind of new structure is created and receives some new\r\ninformation. We’ll refer to this new structure as “final structure”:\r\nExample of the final struct manipulation.\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 7 of 20\n\nThe format of this “final structure” is something similar to the following:\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\nstruct FINAL_STRUCT_INFO\r\n{\r\n char main_process_cmdline[2048];\r\n char init_export_str[56];\r\n LPVOID main_payload_addr;\r\n QWORD main_payload_size;\r\n QWORD config_xor_key;\r\n LPVOID stage4_shellcode_addr;\r\n QWORD stage4_shellcode_size;\r\n char main_payload_content[7853];\r\n char stage4_shellcode_content[3150];\r\n};\r\nFixing the next Stages\r\nConsidering the next stages are shellcodes and would use some functions from the Windows API there’s only 2\r\nways to make those adresses available: either via runtime linking performed by the shellcode itself (e.g. the API\r\nhashing technique mentioned previously) or those function addresses needs to be written in the correct place\r\ninside the shellcodes by an external payload. The Crypter approach is exactly the second one.\r\nIt uses the same API Hashing function to resolve 6 functions and then performs a byte pattern search inside both\r\nthe Stage 2 and 3 content in order to locate specific DWORDs to be replaced by the addresses of the resolved\r\nWindows functions. The list bellow shows each pattern searched and the API function used to replace it:\r\nStage 2:\r\n0xA1A2A3A4A5: ZwCreateThreadEx\r\nStage 3:\r\n0xA1A2A3A4A9: RtlAllocateHeap\r\n0xA1A2A3A4A7: ReadProcessMemory\r\n0xA1A2A3A4AA: NtClose\r\n0xA1A2A3A4A6: LoadLibraryA\r\n0xA1A2A3A4A8: VirtualProtect\r\n0xA1A2A3A4A5: CreateThread\r\nThe x64dbg view bellow shows an example of the Stage 3 content before and after the patch:\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 8 of 20\n\nStage 3 before the function patch\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 9 of 20\n\nStage 3 after the function patch.\r\nSyscall stubs usage\r\nAt this point (specially in the injection part) most part of the API calls performed would not rely on the regular\r\nWindows DLLs and will use a crafted syscall stub array instead.\r\nIt first parses the ntdll exports and creates a kind of list of structs containing the addresses of the real syscall stubs,\r\norganized in an ascending order based on it’s SSN (System Service Number), followed by the hash of the syscall\r\nname (same ROR13 algorithm) and then the bytes (opcodes) responsible for performing the syscall instruction\r\n(let’s say custom stub).\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 10 of 20\n\nSyscall\r\nstubs.\r\nWe can imagine that each entry in this list has the following fields:\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\nstruct SYSCALL_STUBS_INFO\r\n{\r\n QWORD syscall_stub_addr;\r\n DWORD syscall_hash;\r\n char stub_bytes[16];\r\n};\r\nThe “stub_bytes” field represents the following assembly instructions (custom stub):\r\n1\r\n2\r\n3\r\nmov r10, rcx\r\nmov, eax,\u003cid\u003e\r\nret\r\nOnce this list is created every time a function needs to be resolved it first sets the function arguments and then\r\ncalls a function responsible for getting the proper custom stub. This function receives the base of the created stub\r\nlist as well as the desired hash. The hash is then compared against each hash in the stub list and once it’s found the\r\nrespective custom stub is returned:\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 11 of 20\n\nSyscall stub resolving.\r\nSyscall stub example.\r\nThe usage of this approach usually is to avoid usermode hooks performed by AV/EDR engines as well as make the\r\nRE process a bit more complicated since breakpoints in the regular API functions for example wouldn’t work as\r\nexpected. I’ll not go into more details regarding this technique cause there’s a thousand of reports about it\r\navailable already.\r\nProcess injection\r\nAt this point the preparation to inject into a target process begins and the “svchost.exe” process is the target of this\r\ncrypter.\r\nFirst, the crypter obtains information from all the processes using the NtQuerySystemInformation function\r\npassing the SystemProcessInformation parameter to it. By using this parameter a struct of type\r\nSYSTEM_PROCESS_INFORMATION is returned for each available process. The field ImageName of this structure is\r\nobtained, the same hash algorithm used before is applied to it and then it’s then compared against the expected\r\n“svchost” hash. If there’s a match the process PID is obtained:\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 12 of 20\n\nGet list of process information.\r\nSince the next stages would be injected into svchost process the function responsible for the injection receives our\r\n“final structure” as a parameter. The injection function starts resolving multiple “custom syscall stubs” to be used:\r\nInjection stubs resolving.\r\nA call to NtOpenProcess is performed to get a handle to the svchost process using the collected PID. All svchost\r\nthreads are then enumerated and for each thread opened via NtOpenThread it creates an event using\r\nNtCreateEvent , duplicate it to the target process using NtDuplicateObject and then queues an user APC\r\npassing the NtSetEvent as the APC function and the created event handle as it’s parameter. Once all the threads\r\nhad an APC queued it calls NtWaitForMultipleObjects passing a list of all event handles to it.\r\nThe injection approach used by this crypter is via a basic APC injection. APCs are basically a way to execute code\r\nin the context of a thread and whenever the kernel receives a request to queue an APC it first checks the mode\r\n(user or kernel) and then inserts the APC into the proper thread queue. In order to execute an user APC a thread\r\nneeds to be in an alertable state and this is why the calls mentioned above are used.\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 13 of 20\n\nThese calls are a kind of preventive measure to make sure there’s a thread in svchost process in alertable state via\r\nthe duplicated events being triggered:\r\nQueue an APC for each remote thread.\r\n Wait until an\r\nobject is ready.\r\nOnce the proper thread is identified the function WinHelpW is overwritten with the Stage 2 content and the\r\nfunction WinHelpA with the Stage 3 content (both exported by user32.dll ). For performance reasons once a\r\nDLL is mapped to a process memory Windows tries to maintain the same address for all the other processes and\r\nthis is why use the addresses obtained from the main process (rundl32.exe) would match the addresses inside\r\nsvchost.exe process (considering the user32.dll is already loaded, of course).\r\nA new hex pattern (0xA1A2A3A4AB) is searched in the Stage 3 content and replaced by the main process handle\r\nand this handle is duplicated. This way the code injected in the target process would have access to the main\r\nprocess memory. The final step of Stage 1 is then call NtQueueApcThread function to queue the tampared\r\nWinHelpW function to the alertable thread, passing both the WinHelpA address and the “final struct” address in\r\nthe main process to it:\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 14 of 20\n\nWrite Stage 2 and 3 content and queue an APC.\r\nStage 2 (WinHelpW export)\r\nThis is the first function executed inside the “svchost.exe” process and it’s job is very straight forward: it creates a\r\nthread using ZwCreateThreadEx to call the tampered WinHelpA function (Stage 3) and passes the address of our\r\n“final structure” inside the main process (rundll32.exe) as the thread function parameter.\r\nWinHelpW call.\r\nStage 3 (WinHelpA export)\r\nThis stage is the one responsible for calling the final stage in this whole chain, which is the Snow Crypter loader\r\n(Stage 4). The first thing done here is get the content of the loader inside the “final structure”. It does so by using\r\nthe address passed as the thread parameter and calling the ReadProcessMemory function to read the content from\r\nthis address. The access to the main process is possible cause a handle to it was written to this stage by stage 1\r\nalready:\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 15 of 20\n\nRead the final structure from the main process memory.\r\nThe LoadLibraryA function is then called to load the dpx.dll module again, but now inside the “svchost.exe”\r\nprocess. The address of the DpxCheckJobExists function is resolved and replaced by the Stage 4 content (same\r\napproach applied by the Stage 0 payload). The screenshot bellow shows the DLL being loaded, the export being\r\nresolved and the Stage 4 content being written:\r\nDpxCheckJobExists export tampering.\r\nThe tampered function (Stage 4) is then called via a CreateThread call, passing the “final struct” (now accesible\r\nlocally) as the thread parameter:\r\nStage 4 call via a new thread.\r\nStage 4 (DpxCheckJobExists export, again)\r\nWe finally reached the final stage! With access to the “final structure” this payload can read and decrypt the final\r\npayload. The algorithm used to “decrypt” it is again a multibyte XOR operation using the key read from the initial\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 16 of 20\n\nconfig and then subtracting the byte next to the XORed byte in the array.\r\nThe result content is not exactly a valid PE file, it’s more of a struct containing a compressed binary as well as\r\nsome other information such as it’s size. This data is passed to a function in which seems to perform some sort of\r\ndecompression and then it returns both the fully “unpacked” PE file as well as it’s size.\r\nRegarding the decompression algorithm used, I’m assuming it’s QuickLZ due to what I saw in IBM’s report, but\r\nto be honest I know close to nothing about those type of algorithms so I’m just assuming it’s true:\r\nFinal payload decryption and decompression.\r\n Decompression result.\r\nDecompression result.\r\nThe final step here is the old manual mapping technique. A region of memory is allocated and then the clean\r\npayload is mapped to it: it’s dependencies resolved via LoadLibrary + GetProcAddress , realocation applied\r\nand so on. The final payload is a DLL and has it’s DllMain function executed, followed by the previously\r\nmentioned init export function:\r\nFinal payload map and execution.\r\nSome reversing shortcuts:\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 17 of 20\n\nIn case you’re only interested in the final payload I have some shortcuts for you!\r\nConsidering the fact dpx.dll will be loaded at svchost.exe process and the execution will be transfered to the final\r\nIcedID payload at some point we can use tools like Process Explorer, System Informer or Process Hacker and\r\nsearch for any process that has the dpx.dll loaded. If it’s svchost.exe there’s a high chance this is our target.\r\nAfter it we would just need to find an allocated region inside it that contains a PE file and dump it:\r\ndpx.dll search in Process Hacker.\r\nAllocated memory search in Process Hacker.\r\nThe downside of this approach is that the file would be already mapped in memory so it would be aligned to a\r\npage boundary and we would need to fix it. A better approach is to try to find the real final payload before it’s\r\nmapped by the loader. Since that would be the raw binary it’s aligment will be all good and it will be way easier to\r\nmanipulate.\r\nAs we saw the earlier, the decompression function receives the decrypted final payload and returns the\r\nuncompressed one as well as it’s size. If we perform a simple check in x64dbg hex dump we’ll see there’s 0x400\r\nbytes (the headers) from the first byte of the file until the first byte of the .text section. Considering 0x400 is\r\nusually the value of the File Aligment field in the IMAGE_OPTIONAL_HEADER we can assume this is the final\r\npayload, clean and ready to be dumped!\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 18 of 20\n\nAlignment of the decompressed payload.\r\nThe only thing we need to do to dump it using x64dbg is select the 0x3400 bytes (unpacked payload size) in the\r\nhex dump -\u003e Right Click -\u003e Binary -\u003e Save to File. And there we go! A clean payload to be analyzed. We can\r\ncheck it with DIE and see some of the known IcedID strings and names:\r\nGeneral information.\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 19 of 20\n\nPayload imports.\r\nSome famous IcedID strings.\r\nConclusion\r\nI hope you enjoyed the reading and if you have any feedback regarding this analysis I would love to know about\r\nit.\r\nHappy reversing!\r\nSource: https://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nhttps://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/\r\nPage 20 of 20\n\n3. A struct containing 4. A struct containing information information regarding regarding the next stages the main payload\n5. The XOR key used to decrypt the Stage 2, 3 and 4\n   Page 6 of 20",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://leandrofroes.github.io/posts/Reversing-a-recent-IcedID-Crypter/"
	],
	"report_names": [
		"Reversing-a-recent-IcedID-Crypter"
	],
	"threat_actors": [],
	"ts_created_at": 1775434810,
	"ts_updated_at": 1775791219,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/0b9bdade7c908aedaed2615921bcc99c257315b2.pdf",
		"text": "https://archive.orkl.eu/0b9bdade7c908aedaed2615921bcc99c257315b2.txt",
		"img": "https://archive.orkl.eu/0b9bdade7c908aedaed2615921bcc99c257315b2.jpg"
	}
}