{
	"id": "eff72489-77e9-404b-a6e3-a5ebda70b9ed",
	"created_at": "2026-04-06T00:09:14.876091Z",
	"updated_at": "2026-04-10T03:28:47.292515Z",
	"deleted_at": null,
	"sha1_hash": "a263b97f21e1a2602208995c6ed79be50fba33a9",
	"title": "Malcat scripting tutorial: deobfuscating Latrodectus",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 3673619,
	"plain_text": "Malcat scripting tutorial: deobfuscating Latrodectus\r\nBy Malcat EI\r\nArchived: 2026-04-05 14:57:43 UTC\r\nSample:\r\n013a92ea6df2995a8cdef11527dc4bda0b4a2e8dc642f7461c1cedb42297cadb (Bazaar)\r\nTools used:\r\nMalcat\r\nDifficulty:\r\nIntermediate\r\nWhile many of you use Malcat for its main purpose, that is malware analysis, Malcat's scripting capabilities are often\r\noverlooked. And that's too bad, because Malcat features a powerful yet simple API which is rather easy to learn. Now, why\r\nwould you want to learn Malcat's API when there are more wide-spread alternatives around, lika IDA/ghidra/binja scripts?\r\nFor a couple of reasons:\r\n1. In python we trust. Although Malcat's analysis core is coded in C++, a lot of python is used internally (parsers,\r\nanomalies, etc.). We have thus put a lot of effort into the python API, it's not something that was just put on top.\r\n2. It is available to free users. If you write useful scripts, your code can be used by everyone. Regarding scripting, the\r\nonly benefit of paid users is being able to run scripts from the command line, while free users have to run them\r\nthrough Malcat's script editor.\r\n3. You can extend Malcat. Several parts of Malcat are written in python and use the API. You can for instance program a\r\nnew parser, write your own transforms or improve the anomaly scanner.\r\nAnd if you're not yet convinced, let us discover the API through a small use case: writing a Latrodectus desobfucator.\r\nKey concepts\r\nBefore diving into Latrodectus, let us cover a couple of core concepts that we will need later. First and foremost, in Malcat\r\nscripts everything starts with the analysis object. This object can have two origins:\r\nIf you run your script through Malcat's script editor, you can access directly a globally defined analysis variable\r\nthat contains the analysis result of the file currently open in the UI.\r\nScripts using Malcat's headless library need to call the malcat.analyse() function, which will also return a\r\nmalcat.Analysis instance:\r\nanalysis = malcat.analyse(\"/samples/latrodectus\")\r\nprint(analysis.architecture)\r\n\u003e\u003e\u003e Architecture.X64\r\nEvery bit of information that you can see in Malcat's UI stems from this malcat.Analysis instance: you can read/edit raw\r\nbytes, query high-level analysis, disassemble, decompile, etc. No 100s of global functions, just a simple, well-documented\r\npython object. Now let us see what kind of information you have access to.\r\nRaw file access\r\nhttps://malcat.fr/blog/malcat-scripting-tutorial-deobfuscating-latrodectus/\r\nPage 1 of 11\n\nThe lowest-level access that the analysis object provides is access to the raw file through the analysis.file object. This object\r\nsimply represents the file as stored on-disk. Using this object you can access individual bytes:\r\nprint(analysis.file[0x40]) # read byte at offset #40\r\nbut also read a bytes range:\r\nprint(analysis.file[0x40:0x60]) # read bytes [#40-#60[\r\nor read ascii, utf-8 or utf-16 strings. You can even search things using PCRE2 regular expressions:\r\nmatch_off, match_len = analysis.file.search(r\"PE\\x00\\x00\", start=0, size=len(analysis.file))\r\nif match_len:\r\n print(f\"Pattern found! {match_len} bytes at #{match_off:x}\")\r\nAnd since Malcat's API tries to be pythonic, editing the file is also rather simple:\r\nanalysis.file[2] = 56\r\nanalysis.file[2:4] = b\"ab\"\r\nNB: if you run your script in Malcat's script editor, all edit operations are automatically registered to the UI's\r\nundo/redo list. If you don't want this, you can always configure the undo/redo behavior via the analysis.history\r\nobject. You can also group multiple edit operations together.\r\nAddressing\r\nNow before going further, a quick note regarding Malcat's addressing space. When you consider a runnable PE, ELF or\r\nMach-O program and you talk about the address of a program data, it can be usually expressed as either:\r\na physical address, i.e. an offset into the program as stored on disk\r\na virtual address, i.e. a memory pointer, once the program is loaded into memory by the O.S\r\na relative virtual address, a virtual address relative from the program imagebase\r\nTo make things worse, some program bytes can exist only in the physical space (e.g. a PE certificate), only in the virtual\r\nspace (e.g. the .bss section) or in both spaces (e.g. the .text section). This can be confusing. Adds to this the possibility\r\nfor the virtual space to have gaps, and it starts to be really hard.\r\nIn order to simplify addressing, Malcat works in a single, contiguous address space named the effective address space which\r\ncontains both physical and virtual address spaces and get rid of any gap. This makes things a lot easier.\r\nThe only exception is the object seen previously: analysis.file. This object only manipulates physical addresses /\r\nfile offsets for obvious reasons.\r\nYou get of course functions to handle different types of addresses:\r\nanalysis.ppa(ea) to pretty-print an effective address\r\nanalysis.a2p(ea) to convert an effective address into a physical one / file offset\r\nanalysis.p2a(offset) to convert a physical address / file offset into an effective address\r\nanalysis.a2v(ea) to convert an effective address into a virtual address\r\nanalysis.v2a(va) to convert a virtual address into an effective address\r\nanalysis.a2r(ea) to convert an effective address into an RVA\r\nanalysis.r2a(rva) to convert a RVA into an effective address\r\nFrom now on, every time that we will manipulate addresses, these will be addresses in the effective address space.\r\nAccessing annotations\r\nNow that addressing has been covered, let us dive into the core of Malcat's API: the annotations. Malcat has performed\r\nseveral kinds of analysis for you, which can be accessed using different member variables of the analysis object (see the\r\nwhole list here). These analyses are called annotations, since all they do is attach python objects to the effective address\r\nspace of the analysed file. Annotations are accessed also in a pythonic way, with a dictionary-like syntax and iterators.\r\nLet's take an example: the function analysis, accessed through the analysis.fns object. It annotates the whole effective\r\naddress space with malcat.Function objects. Say you want to access a function defined at the virtual address 0x401000 , you\r\nwould write:\r\nhttps://malcat.fr/blog/malcat-scripting-tutorial-deobfuscating-latrodectus/\r\nPage 2 of 11\n\nfn = analysis.fns[analysis.v2a(0x401000)]\r\nprint(fn.name)\r\n\u003e\u003e\u003e sub_401000\r\nNow say you want to iterate through all functions defined in an interval:\r\nstart = analysis.v2a(0x401000)\r\nend = analysis.v2a(0x402000)\r\nfor fn in analysis.fns[start:end]: # to go through all functions -\u003e for fn in analysis.fns:\r\n print(f\"{fn.name} found at {analysis.ppa(fn.address)}\")\r\n\u003e\u003e\u003e sub_401000 found at 0x00401000 (sub_401000)\r\n sub_401010 found at 0x00401010 (sub_401010)\r\n sub_401130 found at 0x00401130 (sub_401130)\r\n ...\r\nNow say you don't know whether a function is defined at a given address or not. You can check it using the following\r\nsyntax:\r\naddress = analysis.v2a(0x401000)\r\nis_function_present = address in analysis.fns\r\n# equivalent\r\nfn = analysis.fns.find(address)\r\nis_function_present = fn is not None\r\nYou can also query the next/previously defined function:\r\naddress = analysis.v2a(0x401000)\r\nfn = analysis.fns.find_forward(address) # returns the function defined over VA 0x401000 or the next function starting afte\r\nif fn is None:\r\n fn = analysis.fns.find_backward(address) # is there a function defined before 0x401000?\r\n if fn is None:\r\n raise ValueError(\"no function defined\")\r\nAnd ... that's mostly it! Of course more method are available, but using this simple paradigm you can access most of\r\nMalcat's annotations. Say you want to list all strings found in the .data section? Use the strings annotation:\r\ndata_section = analysis.map[\".data\"]\r\nfor s in analysis.strings[data_section.start:data_section.end]:\r\n print(f\"* string {repr(s.text)} found at offset #{analysis.a2p(s.address):x}: encoding={s.encoding} score={s.score}\")\r\nStraightforward, right? Now you want to look for embedded files? Again, same syntax, but use the annotation named\r\ncarved:\r\nrsrc_section = analysis.map[\".rsrc\"]\r\nfor o in analysis.carved[rsrc_section.start:rsrc_section.end]:\r\n print(f\"* object {o.type} of {len(o)} bytes found at offset #{analysis.a2p(o.address):x}\")\r\nA few annotations will feature more complex access primitives. For instance the analysis.struct annotation, which lets you\r\ndive through structures and sub-structures identified by the file parser, needs to offer more accessors. But even for the more\r\ncomplex cases, we have tried to make everything pythonic and intuitive.\r\nEditing\r\nEditing also follows the same guideline. You want to add a comment? Use the comments annotation:\r\nep_rva = analysis.struct[\"OptionalHeader\"][\"AddressOfEntryPoint\"]\r\nep_address = analysis.r2a(ep_rva)\r\nanalysis.comments[ep_address] = \"This is the entry point!\"\r\nAdd a symbol? we got you:\r\nep_rva = analysis.struct[\"OptionalHeader\"][\"AddressOfEntryPoint\"]\r\nep_address = analysis.r2a(ep_rva)\r\nanalysis.syms[ep_address] = \"TheEntryPoint\"\r\nhttps://malcat.fr/blog/malcat-scripting-tutorial-deobfuscating-latrodectus/\r\nPage 3 of 11\n\nSet the PE timestamp to the current date? Also easy:\r\nimport datetime\r\nanalysis.struct[\"PE\"][\"TimeDateStamp\"] = datetime.datetime.now() # cool right?\r\nForce a function start? Not much more complicated:\r\nanalysis.fns.force(analysis.v2a(0x401000))\r\nEtc. etc.\r\nGoing through code\r\nDisassembling also follows the same syntax as seen previously. First you want to get a function:\r\nep_rva = analysis.struct[\"OptionalHeader\"][\"AddressOfEntryPoint\"]\r\nep_address = analysis.r2a(ep_rva)\r\nep_function = analysis.fns[ep_address]\r\nThen you want to iterate through its basic blocks via the analysis.cfg object. Note that Malcat has code and data basic\r\nblocks, so every byte of the file is inside a basic block:\r\nfor bb in analysis.cfg[ep_function.start:ep_function.end]:\r\n if bb.code:\r\n print(f\"Found code basic block at {analysis.ppa(bb.start)}\")\r\nAnd finally, you can iterate through instructions using the analysis.asm annotation:\r\nfor bb in analysis.cfg[ep_function.start:ep_function.end]:\r\n if bb.code:\r\n for instr in analysis.asm[bb.start:bb.end]:\r\n print(f\"{analysis.a2v(instr.start):x}: {instr}\")\r\n for operand in instr:\r\n print(f\" {operand.type} -\u003e {operand.value:x}\")\r\nBut since analysing code is such a repetitive and common task, there are also a couple of shortcuts you can take. You can\r\ndirectly iterate through functions (yields basic blocks) and basic blocks (yield instructions). So the code above can be\r\nwritten as:\r\nfor bb in ep_function:\r\n if bb.code:\r\n for instr in bb:\r\n print(f\"{analysis.a2v(instr.start):x}: {instr}\")\r\nThis should be short enough even for the laziest among us. And if you want to navigate through the code more efficiently,\r\nthe three types of code-related objects we have seen (functions, basic blocks and instructions) offer more access primitives:\r\nYou can list incoming references using Function.inrefs, BasicBlock.inrefs or Instruction.inrefs\r\nYou can list outgoing references using Function.outrefs, BasicBlock.outrefs or Instruction.outrefs\r\nYou can get a textual disassembly listing using Function.disasm(), BasicBlock.disasm() or Instruction.disasm()\r\nYou can get a textual C code representation of a function using Function.decompile()\r\nYou can navigate through callers/callees via Function.callers and Function.callees\r\nYou can navigate through CFG predecessors/successors via BasicBlock.incoming and BasicBlock.outgoing\r\nThat's all we will need for our Latrodectus deobfuscator!\r\nUse case: desobfuscating Latrodectus\r\nThe Latrodectus malware, also known as BlackWidow, is a backdoor developed in C which has been first identified in\r\nOctober 2023. Latrodectus is thought to be developed by the threat actor Lunar Spider. It is sometimes seen as the successor\r\nof IcedId, developed by the very same threat actor. The backdoor features some light obfuscation, mainly string encryption\r\nand an hash-based dynamic API call resolving, in order to make detection and analysis a bit more difficult.\r\nWe won't analyse the malware capabilities, which have been already covered in detail. Instead, we will focus on the\r\nobfuscation in place inside the malware and see how to leverage Malcat scripting capabilities in order to quickly decrypt the\r\nhttps://malcat.fr/blog/malcat-scripting-tutorial-deobfuscating-latrodectus/\r\nPage 4 of 11\n\nstrings and resolve the API calls. Since this malware is relatively simple, it should make a gentle introduction to Malcat's\r\nAPI.\r\nResolving API calls\r\nWhat is dynamic API lookup?\r\nMalware need to use Windows APIs like any other software in order to interact with the Windows operating system. But\r\nvery rarely you will see malware import their APIs the normal way, i.e. by letting the linker list them inside the PE import\r\ntable. Instead, what a lot of malicious programs do is to resolve the API addresses they use at run time. This helps them hide\r\ntheir true intent against static analysis tools (e,g, capa).\r\nThe APIs they need can be searched either by their name, or by a hash of their name. By using hashes, malware can avoid\r\ndetection by static analysis tools that look for known API names. This technique complicates the reverse engineering\r\nprocess too, as analysts must first identify the hash and then determine which API it corresponds to.\r\nAPI lookup in Latrodectus\r\nLatrodectus is no exception and performs dynamic API lookup by hash. Identifying this behavior is rather easy in Malcat:\r\nLatrodectus uses a well-known hashing function (CRC32) to hash the API name, and almost all the API hashes are identified\r\nby Malcat's constant scanner, as we can see below:\r\nFigure 2: Identifying dynamic API lookup in Latrodectus\r\nIn the sample that we have analysed, API lookup is scattered across different functions. But the lookup process always\r\nfollow the same pattern in each of these functions:\r\nAn array is allocated on the stack\r\nEach API that needs to be looked up is setup using a triplet on the stack:\r\nFirst element of the triplet is the (hardcoded) CRC32 hash of the API name\r\nSecond element seems to be the previously computed CRC32 hash of the DLL\r\nThe third element is the address where the resolved API pointer will be stored\r\nYou can see below an extract of one of the functions doing this kind of API lookup:\r\nFigure 3: API call lookup in one of the functions\r\nAs we can see on the bottom left part if the screenshot above, API calls are hard to read for a reverser in this configuration.\r\nWhat would be nice, is having a symbol with the API's name on the third triplet's member, i.e. the API pointer actually used\r\nfor calls.\r\nThe script\r\nNow since Malcat was kind enough to identify the API hashes for you, you could add the symbols manually (Right click \u003e\r\nAdd/edit label). But we're talking about 163 hashes here, so it would be rather nice to automate this. Our plan will be as\r\nfollow:\r\nhttps://malcat.fr/blog/malcat-scripting-tutorial-deobfuscating-latrodectus/\r\nPage 5 of 11\n\n1. Identify all API hashes usage. For this we could:\r\nGo through all constants via the analysis.constants annotation\r\nCheck each constant category and look for \"apihash\"\r\nGet the constant's use address\r\n2. Starting from the constant's use location, go to the second lea opcode following the instruction:\r\nThe API hash constant beeing located in the middle of an instruction, we need first to go the the start of the\r\ninstruction using analysis.cfg.align()\r\nNext we need to iterate over instructions until we see two lea opcodes\r\nNext we need to get the second lea 's memory target: that's where the API address will be stored. We can\r\niterate through instruction operands easily.\r\n3. Finally, we need to add a symbol (the API name) to the second lea target, using the analysis.syms annotation.\r\nSo using everything we have learnt, putting a script together is relatively easy at the end:\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\nimport malcat\r\nwith analysis.history.group(): # \u003c-- group all edit operations into ONE undo/redo group\r\n for cst in analysis.constants:\r\n if cst.category == \"apihash\":\r\n apiname = cst.name[5:-1] # remove the \"hash(\" and \")\"\r\n instr_start_address = analysis.cfg.align(cst.address) # the constant is in the middle of an instruction, find the start of t\r\n count_lea = 0\r\n for instr in analysis.asm[instr_start_address: instr_start_address+40]: # iterate over the next instructions\r\n if instr.opcode == \"lea\" and instr[1].type == malcat.InstructionOperand.Type.GLOBAL and len(instr.outrefs) == 1: # lea\r\n count_lea += 1\r\n if count_lea == 2: # first one is dll, second one stores the resolved API address\r\n lea_target = instr.outrefs[0]\r\n print(f\"Adding symbol \\\"{apiname}\\\" to address {analysis.ppa(lea_target.address)}\")\r\n analysis.syms[lea_target.address] = apiname # add user symbol\r\n break\r\n else:\r\n print(f\"Could not find second lea instruction for API hash {apiname} at address {analysis.ppa(instr_start_address)} :/\")\r\nAnd if you want to see it in action:\r\nFigure 4: Automatically adding API name symbols\r\nThat was pretty easy right? We can see that 4 API hash usages did not respect the \"double-lea\" pattern, but that's because\r\nthey are used in a different context, which is beyond the scope of this tutorial. So let's count this as a 100% win!\r\nDecrypting the strings\r\nString encryption format in Latrodectus\r\nNow that API calls are visible to us, let us focus on the encrypted strings. If you look at the .data section, you will find high-entropy data. And even more interesting, this high-entropy data has incoming references scattered every 10-30 bytes (you\r\nhttps://malcat.fr/blog/malcat-scripting-tutorial-deobfuscating-latrodectus/\r\nPage 6 of 11\n\ncan spot the incoming references via the small green highlighting):\r\nFigure 5: High-entropy, referenced data in the .data section\r\nAll these incoming references stem from code, which looks always the same:\r\n.9:\r\n lea rdx, [rsp+0x220]\r\n lea rcx, [0x1a68871f000] ; \u003c-- our high-entropy referenced data\r\n call sub_1a68871bc60 ; \u003c-- always the same function\r\nAnd if it was not enough, the function called right after the reference ( sub_1a68871bc60 ) displays some interesting\r\nproperties:\r\nFigure 6: Start of sub_1a68871bc60\r\nBy now, your spider sense should definitely be tingling, or more appropriately hurling \"string decryption routine\" in your\r\near. But what kind of encryption? The 32 bytes constant points toward some block-cypher, especially since a couple of AES\r\nsubstitution blocks have been identified by Malcat. According to VMRay's blog, the latest version of Latrodectus uses AES-256 (CTR mode). The encrypted string format is as follow:\r\nA 2-bytes prefix tells us how big is the string\r\nThe next 16 bytes are the initialisation vector (IV) used for encryption with AES CTR\r\nFinally, the encrypted string bytes\r\nSo at the end it looks like:\r\nFigure 7: Latrodectus string format\r\nLet us put it to the test using Malcat's AES transform. The 32 bytes AES key can be obtained either from the decryption\r\nfunction's disassembly or from the string view (Malcat can extract stack strings, it's actually the one with the biggest score).\r\nSince other values have been put on the stack in sub_1a68871bc60 , the recovered stack string is bigger than 32\r\nbytes. You have to select the last 32 bytes:\r\nD623B8EF6226CEC3E24C55127DE873E7839C776BB1A93B57B25FDBEA0DB68EA2\r\nhttps://malcat.fr/blog/malcat-scripting-tutorial-deobfuscating-latrodectus/\r\nPage 7 of 11\n\nAnd good news ... it works! Now we just have to figure out how to automate this for all encrypted strings in the .data\r\nsection.\r\nFigure 8: Manually decrypting the first string\r\nLocating the decryption function\r\nThe first item on our agenda is locating the decryption function, and if possible in a manner applicable to other Latrodectus\r\nsample. So instead of searching by address or by pattern, we will focus on what makes this function unique:\r\nIt is called more than 100 times (i.e. it has more than 100 incoming references). It is unlikely that the number of\r\nstring will decrease in the future.\r\nIt constructs a dynamic string of at least 32 bytes on the stack\r\n... and that's enough!\r\nSo let us convert that into code. We will use annotations and methods that we have already described: the analysis.fns\r\nannotation, the Function.inrefs attribute and the analysis.strings annotation:\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\nimport malcat\r\ndef find_decryption_function_and_key(a:malcat.Analysis):\r\n \"\"\"\r\n find a function with high number of incoming refs and a large stack string\r\n \"\"\"\r\n for fn in a.fns: # go through all functions\r\n if len(fn.inrefs) \u003e 100: # has more than 100 incoming referennces\r\n for s in a.strings[fn.start:fn.end]:\r\n if s.type == malcat.FoundString.Type.DYNAMIC and s.size \u003e= 32: # function contains a stack string of at least 32 bytes\r\n return fn, s.bytes[-32:] # the key is the last 32 bytes of the stack\r\n return None, b\"\"\r\nfn, key = find_decryption_function_and_key(analysis)\r\nif fn is not None:\r\n print(f\"Found decryption function at {analysis.ppa(fn.address)} and decryption key {key.hex()}\")\r\n\u003e\u003e\u003e Found decryption function 0x1a68871bc60 (sub_1a68871bc60) and decryption key d623b8ef6226cec3e24c55127de873e7839c776bb1a93b57b25fdbea0\r\nListing encrypted strings\r\nSecond step in our journey: we have to locate all encrypted strings. This task is made considerable easier if we know the\r\ndecryption function. The idea is to locate all these code patterns:\r\n.9: ; \u003c-- this a the start of a basic block\r\n lea rdx, [rsp+0x220]\r\n lea rcx, [0x1a68871f000] ; \u003c-- the encrypted string\r\n call sub_1a68871bc60 ; \u003c-- the decryption function\r\nTo achieve this goal, we will follow all incoming references to the decryption function, then locate the start of the calling\r\nbasic block: the first lea target of the basic block should be the address of an encrypted string. But enough words, let us\r\nimplement this in python:\r\n 1\r\n 2\r\nimport struct\r\nimport malcat\r\nhttps://malcat.fr/blog/malcat-scripting-tutorial-deobfuscating-latrodectus/\r\nPage 8 of 11\n\n3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\n27\r\ndef iter_encrypted_strings(a:malcat.Analysis, decrypt_fn:malcat.Function):\r\n \"\"\"\r\n Returns a tuple of (offset, IV, ciphertext) for every encrypted string found\r\n \"\"\"\r\n for inref in decrypt_fn.inrefs: # iterate through all incoming refs to the decryption function\r\n bb = a.cfg[inref.address] # basic block containing the instruction doing the ref (in this case: the call sub_1a68871bc60)\r\n for instr in bb: # iterate through the instructions of the basic block\r\n if len(instr) == 2 and instr[0].type == malcat.InstructionOperand.Type.REGISTER and len(instr.outrefs) == 1: # has 2 operan\r\n ea = instr.outrefs[0].address # get outref\r\n offset = a.a2p(ea) # map to a physical offset\r\n if offset:\r\n size, = struct.unpack(\"\u003cH\", a.file[offset:offset+2]) # first 2 bytes: size of the string\r\n yield offset, a.file[offset+2: offset+2+16], a.file[offset+2+16:offset+2+16+size] # rest is IV + ciphertext\r\n break\r\ndecryption_function, key = find_decryption_function_and_key(analysis) # result of previous step\r\nfor offset, iv, ciphertext in iter_encrypted_strings(analysis, decryption_function): # iter through all encrypted strings\r\n print(f\"Found encrypted string at offset #{offset:x}: {iv.hex()}::{ciphertext.hex()}\")\r\n\u003e\u003e\u003e Found encrypted string at offset #edf8: 392e8360ce498a115986338b9f73101f::d43f\r\n Found encrypted string at offset #ee10: a9d634b4b40fa19b396f26fa1377bb33::04c1cbdb34d20014\r\n ...\r\nPutting everything together\r\nNow that we have the decryption function and all the encrypted strings, the only work left is actually decrypting the string.\r\nFor this, we will call directly Malcat's AES transform. You can find its source code in data/transforms/block.py . Beside\r\nthe usage of the transform, you won't find any notion in the script.\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\n27\r\n28\r\n29\r\n30\r\n31\r\n32\r\n33\r\n34\r\n35\r\n36\r\nimport json\r\nimport struct\r\nimport malcat\r\nmalcat.setup() # Add Malcat's data/ and bindings/ directories to sys.path when called in headless mode.\r\nfrom transforms.block import AesDecrypt\r\ndef latrodectus_decrypt_strings(a:malcat.Analysis, in_place:bool=False):\r\n \"\"\"\r\n Decrypts Latrodectus string, returning the decrypted strings in a list.\r\n If in_place is True, the encrypted string will be patched in place with their plaintext counterpart\r\n \"\"\"\r\n res = []\r\n decrypt_fn, key = find_decryption_function_and_key(a) # the first script function we've written\r\n if not decrypt_fn:\r\n raise ValueError(\"Could not locate decryption function\")\r\n print(f\"Found decryption function: {a.ppa(decrypt_fn)}, key: {key.hex()}\")\r\n decryptor = AesDecrypt() # Malcat's transform\r\n for offset, iv, ciphertext in iter_encrypted_strings(a, decrypt_fn): # the second script function we've written\r\n decrypted = decryptor.run(ciphertext, mode=\"ctr\", iv=iv, key=key) # decrypt the string\r\n if in_place:\r\n totalsz = len(ciphertext) + 2 + 16\r\n a.file[offset:offset+len(decrypted)] = decrypted # patch the decrypted string directly\r\n if len(decrypted) \u003e 2 and decrypted[1] == 0: # Latrodectus has both ascii and utf16-le strings,\r\n try: # make a best-effort guess\r\n decrypted = decrypted.decode(\"utf-16le\")\r\n except: pass\r\n else:\r\n try:\r\n decrypted = decrypted.decode(\"ascii\")\r\n except: pass\r\n if type(decrypted) == str and decrypted.endswith(\"\\x00\"):\r\n # remove null byte terminator\r\nhttps://malcat.fr/blog/malcat-scripting-tutorial-deobfuscating-latrodectus/\r\nPage 9 of 11\n\n37\r\n38\r\n39\r\n40\r\n41\r\n42\r\n43\r\n decrypted = decrypted[:-1]\r\n res.append(decrypted)\r\n return res\r\n# decrypt all strings in place\r\nwith analysis.history.group():\r\n print(json.dumps(latrodectus_decrypt_strings(analysis, in_place=True), indent=4))\r\nIf you want the complete script, you will be able to find inside your Malcat installation, in\r\ndata/scripts/config/latrodectus.py . But let us see the script in action:\r\nFigure 9: The whole script in action\r\nConclusion\r\nI hope that you've enjoyed this little introduction to Malcat API. If you want to go further (and you should!), just browse the\r\nonline documentation for each available annotation:\r\nanalysis.struct:\r\nnavigate through all the structures and fields identified by the file parser\r\nanalysis.entropy:\r\naccess the file's pre-computed entropy\r\nanalysis.strings:\r\nstrings identified by Malcat's different string extraction algorithms\r\nanalysis.asm:\r\ndisassembled instructions\r\nanalysis.cfg:\r\nbasic blocks identified through Malcat's CFG recovery algorithm\r\nanalysis.fns:\r\nfunctions identified through Malcat's CFG recovery algorithm\r\nanalysis.loops:\r\nStrongly Connected Components analysis\r\nanalysis.syms:\r\nSymbols (imports, exports, debug symbols, user symbols, etc.)\r\nanalysis.xrefs:\r\nCross reference analysis, i.e. lists everything that points toward a given address\r\nanalysis.carved:\r\nAll embedded files recovered by Malcat's carving algorithm\r\nanalysis.vfiles:\r\nVirtual files, i.e. all files listed by the current file parser. For instance, the members of a ZIP archive.\r\nanalysis.constants:\r\nAll known constants identified by Malcat's constant scanner\r\nanalysis.sigs:\r\nAll matching matching and non-matching Yara signatures\r\nanalysis.anomalies:\r\nAll anomalies identified by Malcat and their location\r\nanalysis.highlights:\r\nAll user highlighted regions\r\nanalysis.kesakode:\r\nhttps://malcat.fr/blog/malcat-scripting-tutorial-deobfuscating-latrodectus/\r\nPage 10 of 11\n\nAll kesakode hits and their location\r\nAnd don't hesitate to share your scripts. Remember, free users can use them too!\r\nSource: https://malcat.fr/blog/malcat-scripting-tutorial-deobfuscating-latrodectus/\r\nhttps://malcat.fr/blog/malcat-scripting-tutorial-deobfuscating-latrodectus/\r\nPage 11 of 11",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://malcat.fr/blog/malcat-scripting-tutorial-deobfuscating-latrodectus/"
	],
	"report_names": [
		"malcat-scripting-tutorial-deobfuscating-latrodectus"
	],
	"threat_actors": [
		{
			"id": "c2385aea-d30b-4dbc-844d-fef465cf3ea9",
			"created_at": "2023-01-06T13:46:38.916521Z",
			"updated_at": "2026-04-10T02:00:03.144667Z",
			"deleted_at": null,
			"main_name": "LUNAR SPIDER",
			"aliases": [
				"GOLD SWATHMORE"
			],
			"source_name": "MISPGALAXY:LUNAR SPIDER",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		},
		{
			"id": "7cfe3bc9-7a6c-4ee1-a635-5ea7b947147f",
			"created_at": "2024-06-19T02:03:08.122318Z",
			"updated_at": "2026-04-10T02:00:03.652418Z",
			"deleted_at": null,
			"main_name": "GOLD SWATHMORE",
			"aliases": [
				"Lunar Spider "
			],
			"source_name": "Secureworks:GOLD SWATHMORE",
			"tools": [
				"Cobalt Strike",
				"GlobeImposter",
				"Gozi",
				"Gozi Trojan",
				"IcedID",
				"Latrodectus",
				"TrickBot"
			],
			"source_id": "Secureworks",
			"reports": null
		},
		{
			"id": "475ea823-9e47-4098-b235-0900bc1a5362",
			"created_at": "2022-10-25T16:07:24.506596Z",
			"updated_at": "2026-04-10T02:00:05.015497Z",
			"deleted_at": null,
			"main_name": "Lunar Spider",
			"aliases": [
				"Gold SwathMore"
			],
			"source_name": "ETDA:Lunar Spider",
			"tools": [
				"BokBot",
				"IceID",
				"IcedID",
				"NeverQuest",
				"Vawtrak",
				"grabnew"
			],
			"source_id": "ETDA",
			"reports": null
		}
	],
	"ts_created_at": 1775434154,
	"ts_updated_at": 1775791727,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/a263b97f21e1a2602208995c6ed79be50fba33a9.pdf",
		"text": "https://archive.orkl.eu/a263b97f21e1a2602208995c6ed79be50fba33a9.txt",
		"img": "https://archive.orkl.eu/a263b97f21e1a2602208995c6ed79be50fba33a9.jpg"
	}
}