{
	"id": "8ad5b81b-e482-4686-8077-5bceca889140",
	"created_at": "2026-04-06T00:11:01.168189Z",
	"updated_at": "2026-04-10T13:12:18.578335Z",
	"deleted_at": null,
	"sha1_hash": "07654032a3b5d46f138a54471472927fde6fdd13",
	"title": "How to Deobfuscate Maze Ransomware | CrowdStrike",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 1643235,
	"plain_text": "How to Deobfuscate Maze Ransomware | CrowdStrike\r\nBy Shaun Hurley\r\nPublished: 2020-05-01 · Archived: 2026-04-05 19:34:30 UTC\r\nMaze ransomware is a recent addition to the ever-growing list of ransomware families. It stands out from the\r\nothers by leveraging a technique called control flow obfuscation to make static and dynamic analysis difficult for\r\nanyone attempting to reverse engineer the binary. Maze lives up to its name — anyone doing analysis will get lost\r\nthrough the endless amount of paths that can be taken through the code. But automated deobfuscation of Maze is\r\nnot a terribly difficult task. \r\nCode obfuscations are legitimate techniques used to protect code from analysis by reverse engineers. The most\r\ncommon form of code obfuscation that a malware analyst will run into daily is a packed binary. Various forms of\r\nanti-disassembly and control flow obfuscations are next in line. Deobfuscation of these obfuscations range from\r\ntrivial to bashing your head on the keyboard and hoping for a miracle. For more information on code obfuscation,\r\nread Surreptitious Software by Christian Collberg and Jasvir Nagra and Practical Binary Analysis by Dennis\r\nAndriesse.\r\nThe primary purpose of this blog post is to present an approach to attack and deobfuscate the various obfuscations\r\nleveraged by Maze’s author. The primary tools leveraged are the IDA Pro disassembler and Python. We cover\r\nsome foundation material, the obfuscations, the deobfuscated obfuscations, and how to accomplish deobfuscation\r\nusing IDA’s Python API.\r\nFigure 1. IDA’s Navigation Bar Depicting Maze Ransomware\r\nOnce any binary is opened up in IDA, the analyst should take a look at the navigation bar. This bar is a quick\r\nmethod to determine how difficult a binary may be to reverse engineer. The bar depicted in Figure 1 is of Maze.\r\nThe vast majority of the gray and brown areas are legitimate code, junk code and undefined functions. The fact\r\nthat there is so little blue (regular function code) lets an analyst know that they are about to have a rough day. \r\nThe approach taken in this blog post is blunt, effective and approachable by inexperienced reverse engineers. The\r\nprimary method for identifying the obfuscations is to search for byte patterns and then deobfuscate all located\r\npatterns. The following section, “Understanding Program Control Flow,” covers the basic technical details to pave\r\nthe way for the rest of the blog. \r\nUnderstanding Program Control Flow\r\nA program’s control flow is a path created out of the instructions that can be executed by the program.\r\nDisassemblers, like IDA, visualize control flow as a graph by creating a series of connected blocks (called “basic\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 1 of 23\n\nblocks”). Each basic block has a single implicit entry point and a single implicit exit point. The entry points for a\r\nbasic block are reached by jump instructions, by call instructions or through the binary’s code entry point\r\nspecified by the format (PE, ELF, etc). This section can be skipped if the reader already understands program\r\ncontrol flow. \r\nFigure 2. Standard Control Flow Example\r\nFigure 2 is an example of visualizing a program’s control flow. To start, take the highlighted TEST instruction at\r\naddress 004 . A TEST instruction will perform a bitwise AND operation on the two operands (EAX, EAX). If the\r\nresult of the bitwise AND is zero, then the CPU’s Zero Flag (ZF) is set to one; otherwise, the ZF is set to zero. The\r\nZF is used by the JZ (“jump if zero”) instruction at address 006 to determine which path (left or right) to take.\r\nThis is one example of something called a “conditional jump.” If the ZF is set to one, then the jump is taken (right\r\npath), and instruction 020 will be executed next. Otherwise, the fall-through side (left path) of the conditional\r\nbranch is taken, and the instruction at address 00C  is executed. \r\nThere are two more instructions that need to be discussed: CALL and JMP. The CALL instruction at 02A will\r\njump to the address of Function001 , execute the body of the function and return the address following the call\r\ninstruction ( 02F ). This is explained in more detail in the section on function calls. A JMP instruction ( 01F ,\r\n040 ) is called an “absolute jump” — the program will always jump to the specified target address. \r\nAbsolute Jumps, Conditional Jumps and Opcodes\r\nThis section is a quick overview of opcodes, jumps and conditional jumps. Before diving into the different types\r\nof jumps, know that all assembly instructions are human-readable representations of binary data. This binary data\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 2 of 23\n\nis put together into strings of bytes called “opcode bytes.” When patching code for binaries is discussed, what’s\r\nbeing patched are the opcode bytes and not an operation on the readable assembly instruction.\r\nFigure 3. JZ Breakdown\r\nLet’s take the JZ from Figure 2, break it down and explain each component at address 006 . This is done in\r\nFigure 3. The first component is the instruction address, which indicates the location in process memory where an\r\ninstruction exists. The opcode bytes are the machine code that represent the mnemonic and the operand. The\r\nmnemonic is the human readable representation of the operation going to be performed on the operand. \r\nIn this example, the kind of conditional jump being performed here is a relative jump if zero. This can be\r\ndetermined based on the opcode bytes, 0F 84 (Figure 3). To calculate the jump target address for a relative jump,\r\nthe last four bytes of the opcode are added to the address of the next instruction ( 00C ). In this case, the value of\r\nthe JZ operand is the four bytes following 0F 84 which are: 14 00 00 00 . The operand value is little endian\r\nand will be reversed when the instruction is executed, that is 14 00 00 00 is actually 00 00 00 14 or 14h . So,\r\nthe calculation to get a jump target address of 020 is 14h + 0Ch , which adds up to 020h . \r\nOpcode (in hexadecimal) Mnemonic Operand Value Size Description\r\nEB XX JMP 1 Byte Short relative jump\r\nE9 XX XX XX XX JMP  2 or 4 Bytes Near relative  jump\r\n75 XX JNZ  1 Byte Short relative jump if ZF = 0\r\n0F 85 XX XX XX XX JNZ  2 or 4 Bytes Near relative  jump if ZF = 0\r\n74 XX JZ  1 Byte Short relative jump if ZF = 1\r\n0F 84 XX XX XX XX JZ  2 or 4 Bytes Near relative jump if ZF = 1\r\nTable 1. Absolute and Conditional Jump Instructions\r\nA core part of deobfuscating Maze involves modifying the various jump instructions to remove control flow\r\nobfuscation. Table 1 describes the opcode, mnemonic and operand value size for each type of jump instruction that\r\nwill be discussed.\r\nHow Function Calls Work\r\nThere are many different types of calling conventions for functions, but this section provides a general explanation\r\ninto how function calls work. The primary takeaway from this section should be that a prologue sets up a function,\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 3 of 23\n\nan epilogue tears down a function, and a return address is where execution resumes when a RETN instruction is\r\nexecuted.\r\nEvery function starts by executing a series of instructions designed to allocate space that will contain the memory\r\nnecessary for the function to complete its purpose. This is called a “function prologue.” The allocated space is\r\ncalled a “stack frame,” and a function needs it to store and reference arguments, variables, and the return address.\r\nTwo registers are used when referencing the stack: a stack pointer and a base pointer. On an x86 system, the stack\r\nand base pointers are called ESP and EBP, respectively.\r\nThe stack pointer (ESP) always points to the top of the stack. That means the value of ESP will change when\r\nvalues are added or removed from the stack. This differs from EBP (the base pointer), which remains the same\r\nthroughout the lifetime of the function. By not being modified, EBP can be used as a reference by the program’s\r\ncode to know where local variables (EBP-4), arguments (EBP+8) and the return address (EBP+4) are located on\r\nthe stack. \r\nFigure 4. Function Prologue for Function_100\r\nFor example, in Figure 4, the Caller function (on the left) executes the CALL instruction at address 002 . The\r\nCALL pushes the return address, 008 , to the stack and shifts ESP so that it points to the top of the stack. On the\r\nright is the prologue for Function_100 that will set up the function’s stack frame. Starting at address 100 , the\r\nbase pointer for Caller (current value of EBP) is saved to the stack. The base pointer for Function_100 is set to\r\nESP.  At address 102 , 13Ch bytes are reserved for use by Function_100 ’s local variables. Finally, before\r\nanything else is done, the value stored in ECX is saved to the stack. This value will be restored in the function’s\r\nepilogue.\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 4 of 23\n\nFigure 5. Function Epilogue for Function_100\r\nThe epilogue in Figure 5 undoes everything that was done by the prologue. The original value of ECX is restored,\r\nlocal variable stack space is removed, both ESP and EBP are restored to their original values, and the return\r\ninstruction is executed. After the pop ebp instruction (09B), ESP points to the stack space where the return address\r\n008 is stored. The RETN instruction at 09C will return to whatever value is pointed to by ESP, in this case 008.\r\nAfter the RETN instruction is executed, control flow returns to 008, and ESP will point to the top of the stack for\r\nCaller.\r\nThe information covered in this section can be a bit tricky to follow at first. However, a complete understanding is\r\nnot necessary to follow along.\r\nAbsolute Jump Obfuscation\r\nA simple obfuscation is to insert extraneous control flow, and Maze does this quite a bit. On the left side of Figure\r\n6 is an absolute JMP instruction located at 004 . This will jump to location 020 . However, on the right side, a\r\nseries of conditional jumps is used to ultimately end up at the same destination, loc_020. Walking through it, if the\r\nJZ instruction is followed, loc_020 is reached. If it is not followed, however, the JNZ instruction will be\r\nfollowed, and the program jumps t0 loc_010 . At 010 is another JNZ that will jump to loc_020 . And, if the\r\nJNZ at 008 was followed because the zero flag is set to zero, then the JNZ instruction at 010 is also going to be\r\nfollowed because the ZF is going to be the same. Therefore, the program will always reach loc_020 .\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 5 of 23\n\nFigure 6: Absolute Jump Obfuscation\r\nThe fall-through branch for the JNZ instruction at 010 is the address 015 . This JZ instruction will never be\r\nreached because the JNZ will always jump. Conditional branches that will never be executed are called “opaque\r\npredicates.” As IDA starts to identify what is and isn’t code, it reaches these opaque predicates and displays them\r\nin the GUI. IDA expects the JZ instruction at 010 to have two branches: the code following 015 and the code\r\nlocated at address 019 . Although none of this will be reached, all of it is displayed as code in IDA, creating a\r\ncluttered mess. The analyst has to sift through all of this and decide which instructions are pertinent. \r\nThe following subsections describe the types of conditional jump patterns and how they can be deobfuscated.\r\nJMP Type One: Absolute, Conditional Jump\r\nThis obfuscation takes an absolute jump and transforms it into a conditional jump that will always end up at one\r\nof two jump target addresses. As can be seen in Figures 7, 8 and 9, this obfuscation places the jump JZ and JNZ\r\ninstructions in sequence. As mentioned previously, each conditional jump has a fall-through branch and a jump\r\ntarget. In the case of Figure 7, if the fall-through branch of the JZ instruction is executed (address 000 ), that\r\nmeans ZF is set to zero. The next instruction, JNZ, is executed and the jump target will always be taken\r\n( loc_004+4 ). This means that the address at 004 will never be reached. \r\nFigure 7. Short JMP Type One Obfuscation, Incorrect JNZ Target Label in IDA\r\nIDA, however, expects the JNZ instruction at 002 to fall through if ZF is set to one. That means the bytes located\r\nat 004 should be marked and displayed as valid x86 code. The consequence of displaying the binary data at 004\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 6 of 23\n\nas code is that IDA ends up incorrectly displaying both the operand ( loc_004+4 ) for 002 , and the code at jump\r\ntarget address 008 . It is jumping to the last byte of the instruction at 004 . Confusing the disassembler in this\r\nmanner is called “anti-disassembly.” In IDA, this can be resolved by undefining the instruction at address 004\r\nand marking the instruction at 008 as code (Figure 8). Throughout the rest of this post, each  example will\r\ndisplay the corrected code.\r\nFigure 8. Short JMP Type One Obfuscation, Correct JNZ Target Label in IDA\r\nFigure 9 is the same thing as Figure 7, but the instruction at 000 is a near (rather than a short) JZ. As expected,\r\nthe bytecode starts with 0F 84 and the entire instruction length goes from two to six. \r\nFigure 9. Near JMP Type One Obfuscation\r\nDeobfuscation\r\nIf the instruction at 000 is not taken, then the instruction at 002 will always be taken. That means that the JNZ\r\ninstruction at 002 is actually a short JMP. The transformation here is simple — the JNZ instruction at address 002\r\n(Figure 8) can be changed to a short JMP instruction by modifying the first byte from 75 to EB. \r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 7 of 23\n\nFigure 10. Short JMP Type One Obfuscation, Correct JNZ Target Label in IDA\r\nJMP Type Two: Absolute, Multiple Conditional Short Jump\r\nThis obfuscation is similar to the JMP Type One obfuscation but with added JZ/JNZ blocks to further confuse\r\nIDA’s ability to visualize control flow. In Figure 11, either the JZ instruction at 000 will be taken, or the JNZ\r\ninstruction at 002 will be executed. The JNZ instruction at 002 jumps to the JNZ target at 008. A JNZ jumping to\r\nanother JNZ is extraneous because the second JNZ jump will always be taken. In this example, one of two jump\r\ntargets will always be reached: loc_6E456049 or loc_6E456049 .\r\nFigure 11. Short JMP Type One Obfuscation, Correct JNZ Target Label in IDA\r\nThe instructions at 004 , 00A , 00C and loc_6E456002 are all unreachable. This technique multiplies the\r\nnumber of paths through Maze that IDA believes are accessible. Each one is a dead end. \r\nLike the JMP Type One obfuscation, the Type Two obfuscation has a short and a near version.\r\nDeobfuscation\r\nThe deobfuscation here is similar to the JMP Type One obfuscation. The JNZ instruction at 002 (Figure 12) is\r\nchanged to a short JMP by changing the first opcode byte 75 to EB . The JNZ instruction at 000 is not\r\ntouched. The JNZ/JZ block at 008 is zeroed out. \r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 8 of 23\n\nFigure 12. Near JMP Type Two Obfuscation, Correct JNZ Target Label in IDA\r\nCall Instruction Obfuscation\r\nSimilar to the previous obfuscation types, Maze transforms CALL instructions into conditional jumps. Not only\r\nwill this confuse control flow, it combines multiple functions into a single function. This process is called\r\n“function inlining,” and it makes it difficult for IDA to properly identify where functions begin and end. Figure 13\r\nillustrates the obfuscation process.\r\nFigure 13. Obfuscated Function Call\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 9 of 23\n\nEach function follows a similar template. At instruction 000 in Figure 13, the return address, loc_020 , for the\r\nfunction call is pushed to the stack. The next instruction jumps to the address of the function being called. The\r\nfunction prologue instructions preserve the state of registers by pushing them on the stack and to allocate memory\r\nfor local variables. After the prologue, the function body executes, and then the epilogue restores the state of the\r\nstack prior to the function being called. Once the state of the stack has been restored, control flow resumes to the\r\naddress that was pushed at instruction 000 .\r\nThere are multiple minor variations to the template, but the core part of the control flow remains intact. The\r\nfollowing series of sections breaks down each variation of the control flow obfuscation into numbered types. \r\nCall Type One: Indirect Absolute Jump\r\nThis obfuscation pushes the return address to the stack and then executes an indirect jump. An indirect jump is a\r\njump instruction where the operand of the JMP instruction is a register rather than the target address or the offset\r\nto a target address. The jump target address is stored in the register. In Figure 14, at address 005 , the JMP\r\ninstruction operand is the EDI register. To reach the destination of the JMP, the CPU needs to get the address from\r\nEDI. This obfuscation breaks control flow, because IDA does not know where to jump and where to return. \r\nFigure 14. Call Type One Obfuscation\r\nIndirect jumps are a common instruction, and that on its own does not constitute control flow obfuscation. It’s the\r\nPUSH instruction followed by jmp edi that makes this a Call Type One obfuscation. In Maze, these\r\nobfuscations are often used to execute a Windows procedure ( VariantClear , in this example). When a Windows\r\nprocedure is called, it is more or less always going to return to the instruction proceeding the call instruction. In\r\nthis case, the return address for the function is pushed to the stack, and the JMP instruction is executed. \r\nDeobfuscation\r\nTo deobfuscate, the indirect jump at 005 (Figure 14) gets changed to a call by subtracting 10h from the second\r\nopcode byte ( E7 ), the PUSH instruction at 000 gets changed to an absolute near jump, and the instructions are\r\nreordered so that the call instruction comes before the jump (Figure 15).   \r\nFigure 15. Call Type One Deobfuscation\r\nCall Type Two: Absolute, Conditional Jump\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 10 of 23\n\nThe primary difference between this obfuscation type and the JMP Type One obfuscation is the addition of\r\npushing the return address at 001 (Figure 16). The first instruction at 000 pushes whatever value is stored to\r\nESI as an argument to the function. \r\nFigure 16. Call Type Two Obfuscation\r\nDeobfuscation\r\nThis deobfuscation is a case where the deobfuscation instructions don’t require as many opcode bytes as the\r\noriginal obfuscation instructions. There are a few different approaches to resolve this issue — in this case, no\r\noperation (NOP) instructions are used to overwrite the original bytes. After overwriting the unnecessary bytes, the\r\nJZ/JNZ instructions are replaced with a relative CALL instruction, and the return address pushed to the stack is\r\nused as the target for the absolute near jump at 00E .\r\nFigure 17. Call Type Two Obfuscation\r\nA cleaner approach may be to move the CALL/JMP instruction sequence to address 001 , just after the PUSH\r\ninstruction, and then zero out the unused bytes. This method would remove the extraneous NOP instructions from\r\nthe basic block.\r\nCall Type Three: Absolute, Multiple Conditional Jump\r\nOnce again, the primary difference between this obfuscation type and the JMP Type Two obfuscation is the\r\naddition of pushing the return address at 001 (Figure 16). The first instruction at 000 pushes whatever value is\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 11 of 23\n\nstored to EDI as an argument to the function. \r\nFigure 18. Call Type Three Obfuscation, Correct JNZ Target Label in IDA\r\nThe example in Figure 18 is only a double jump, but there are variations where three of these linked JNZ\r\ninstructions ultimately end up in the same place. Deobfuscation has to be able to handle these cases.\r\nDeobfuscation\r\nThe deobfuscation for this type is similar to the method used in Call Type Two. \r\nFigure 19. Call Type Three Deobfuscation\r\nDeobfuscation with IDA Python\r\nIDA Python is built on the IDA SDK and this section will discuss how it can be leveraged to deobfuscate Maze.\r\nDeobfuscating the various control flow obfuscations discussed above gets redundant, so the only case covered is\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 12 of 23\n\nthe Call Type One obfuscation. This example will be enough to follow along in both the source code and in the\r\nlater sections. \r\nLocating Call Type One Obfuscations\r\nPrior to patching the IDA database (IDB) to remove the obfuscations, the locations for each obfuscation have to be\r\nidentified. The simplest method for locating the obfuscations is by searching the IDB for all instances of a specific\r\nopcode pattern. \r\nFigure 20. Call Type One Obfuscation\r\nIn Figure 20, the opcode pattern for address 000 is 68 E8 52 44 6E . This instruction is pushing the four-byte\r\naddress 6E4452E8 (little endian byte order) onto the stack. Because the four bytes following the 68 opcode byte\r\nare going to change based on which address is being pushed, these will have to be excluded. This same issue pops\r\nup with the second opcode byte E7 for the jump instruction at 005 . The search string ends up looking like: 68\r\n? ? ? ? FF . The ? is a wildcard match that will match any byte.\r\nFigure 21. Opcode Searching for Call Type One Obfuscation\r\nThe loop in Figure 21 will iterate over each address where the opcode pattern was found. The body of the loop\r\n(the snipped out code) will contain all of the logic used to verify that the found byte pattern is a Type One\r\nObfuscation. \r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 13 of 23\n\nFigure 22. Verify PUSH Instruction for Call Type One Obfuscation\r\nOnce an opcode pattern has been found, the instruction at that address needs to be interpreted. IDA may or may\r\nnot have the correct instruction displayed here, but there is no way of knowing if that is the case. This blog post\r\ntakes the approach of not trusting the current state of the IDB. So, an ida_ua.insn_t object is created and\r\npopulated using the ida_ua.decode_insn ( insn_t , ea ) method. The reference for this instruction can be found\r\nin the online documentation. The key to success is using the online documentation along with searching the IDA\r\nPython source located in IDAs installation directory. In short, the inst_t object gives us access to the following\r\ninformation about the instruction:\r\nInstruction size\r\nOperand type\r\nOperand value\r\nOperand address\r\nThe two highlighted blue functions in Figure 22 are helper functions. The first retrieves the target address for the\r\ninstruction (in this case, what is being pushed). The CheckValidTargettingInstr() function validates that the\r\nPUSH instructions operand is an o_imm type, o_far type or o_near type. After the type has been validated,\r\nthe code address being pushed — 6E4452E8 (Figure 20) — needs to be verified that the address points to\r\nexecutable code in the Maze binary.\r\nOnce this has been confirmed, the JMP instruction located at 005 can be validated. The steps are mostly the\r\nsame, but instead of validating the address, the operand type will be o_reg , o_phrase or o_displ .\r\nPatching Call Type One Obfuscations\r\nOnce these instructions have been validated, the IDB can be patched to transform the obfuscation into the code in\r\nFigure 23. This is where things can get a bit frustrating. The goal is to get IDA to disassemble the code and present\r\na cleaned-up version to whomever is analyzing the Maze binary. To ensure that this occurs, the deobfuscation\r\nscript takes the steps discussed in this section.    \r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 14 of 23\n\nFigure 23. Call Type One Deobfuscation\r\nIn order to accurately patch the program, the deobfuscation script needs to know the address for each of the\r\nobfuscated instructions, the address of the deobfuscated instructions, and the address that is going to be the jump\r\ntarget. The first item is the address of the byte pattern match from the previous section. \r\nFigure 24. Getting the Deobfuscation Instruction Addresses\r\nFor all cases, the call edi instruction is going to overwrite the push offset loc_6E4452E8 (Figure 20)\r\ninstruction. In Figure 24, this is accomplished by the following code: deobf_patch_call_addr =\r\nobf_push_instr_ea.\r\nNow that the location of call edi has been determined ( 000 ), the address for the jmp loc_6E4452E8\r\ninstruction needs to be calculated. Figure 23 shows that the JMP instruction will come after the two-byte CALL\r\ninstruction, so the address is 002 . The question is, will that always be the case?\r\nFigure 25. Call Type One Obfuscation\r\nFigure 25 shows that the length of the CALL will not always be two. However, calculating the correct address is\r\nnot difficult. Remember that the length of the deobfuscated CALL instruction ( call edi ) will always be the\r\nsame length as the obfuscated jump ( jmp edi ). The address of the obfuscated jump is already known, so the\r\ninstruction can be decoded into an ida_ua.isn_t object. Once the instruction has been decoded, the address of\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 15 of 23\n\nthe deobfuscated jump ( jmp loc_6E4452E8 ) can be calculated: deobf_patch_jmp_addr = obf_push_instr_addr +\r\nobf_jmp_insn.size .\r\nNow that the address of the deobfuscated jmp instruction is known, the JMP target address can be calculated. Take\r\na look at the JMP instruction at 003 in Figure 23. The E9 opcode indicates that this is going to be a relative\r\nnear jump. The four opcode bytes after the E9 byte are going to be the offset between the address of the next\r\ninstruction and the return address pushed by the PUSH instruction.\r\nFigure 26. Getting the JMP Target Address\r\nThe address of the next instruction is always going to be the address of the deobfuscated JMP instruction + five.\r\nThis is known because a relative near JMP instruction has a size of five bytes, and that is what will always be used\r\nin the Call Type One deobfuscation code. The highlighted sections in Figure 26 show how the offset is calculated.\r\nThe IDB can now be patched.\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 16 of 23\n\nFigure 27. Writing the Deobfuscated CALL Instruction\r\nWe start with the deobfuscated CALL instruction (Figure 27). First, the original PUSH instruction is deleted using\r\nthe IDA Python del_items ( insn_addr, FLAG )procedure. Next, the “for” loop walks over each byte of the\r\nobfuscated indirect JMP instruction ( jmp edi ). It writes the first byte to the address of the CALL instruction,\r\nsubtracts 16 from the second byte to convert the JMP to a CALL, then writes the rest of the instruction. After the\r\nCALL instruction has been written, it is created using the create_insn ( insn_addr ) procedure.\r\nFigure 28. Writing the Deobfuscated Relative Near JMP Instruction\r\nIn Figure 28, the instruction that exists at this location is undefined using the CleanupPatchArea ( addr, size )\r\nfunction (see source code). The first opcode byte (E9) is written to the location using patch_byte ( addr, byte ).\r\nNext, the offset address for the relative near jump is written using patch_dword(addr , dword) . The instruction\r\nis created, and plan_and_wait ( addr_start , addr_end ) is called to force IDA to reanalyze the instruction.\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 17 of 23\n\nFigure 29. Ensure that the Code at the Jump Target is Recognized as Code\r\nThe instruction located at the jump target address may not be recognized as code by IDA. To ensure that this will\r\nbe interpreted not only as a valid instruction but the correct instruction, the steps in Figure 29 are followed. Once\r\nthis has completed, the Call Type One obfuscation is now deobfuscated.\r\nWindows Procedure Call Obfuscation\r\nSome of the Windows API calls are obfuscated using the method outlined in this section. This obfuscation\r\nconnects all of the obfuscations covered in the previous sections and adds a few twists. The purpose of this\r\nobfuscation is to call the Winapi_LookupWindowsProcedure function. This function looks up the address of a\r\nmodule name by hash and then executes the procedure. The method used to retrieve a Microsoft Windows\r\nprocedure address will not be covered in this blog. \r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 18 of 23\n\nFigure 30. Windows Procedure Call Obfuscation, Incorrect IDA Rendering\r\nThe CALL instruction at address 010 in Figure 30 is doing something shifty. Recall that a CALL instruction\r\npushes the address of the next instruction to the stack — in this case, the address is going to be 015 . Following\r\nthe CALL instruction, the Winapi_LookupWindowsProcedure is immediately called using a Call Type Three\r\nobfuscation (highlighted green). So the CALL pushes 015 , and the Call Type Three pushes the return address\r\nloc_050 . This is somewhat new, so let’s take a look at address 015 .\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 19 of 23\n\nFigure 31. Windows Procedure Call Obfuscation, Correct IDA Rendering\r\nFigure 31 shows what is actually located at address 015 . It is the name of a Microsoft Windows DLL\r\n( gdi32.dll , in this case). This DLL is going to be the first argument to the Winapi_LookupWindowsProcedure\r\nfunction. The CALL instruction is used to push the first argument. IDA interprets the bytes at 015 as code, and\r\nthat makes it difficult to follow.\r\nDeobfuscation\r\nIn Figure 30, instruction 01F is a Call Type Three obfuscation, and instruction 050 is a Call Type One\r\nobfuscation. Each obfuscation is deobfuscated using previously mentioned methods.\r\nFigure 32. Windows Procedure Call Deobfuscation\r\nIn order to pass the first argument (module name) to Winapi_LookupWindowsProcedure , the CALL instruction at\r\n010 (Figure 31) will be replaced with a PUSH instruction. To accomplish this, the module name is shifted from\r\n015 to address 01F (Figure 32). Now, the PUSH instruction can be added at 010 to be used as the first\r\nargument for the CALL to Winapi_LookupWindowsProcedure ( 015 ).\r\nFunction by the Slice\r\nAll of the obfuscations that have been discussed turn the code into spaghetti. This confuses disassemblers, like\r\nIDA, and makes it difficult to automatically identify and create functions. After deobfuscation has occurred, it\r\nmay not be the case that IDA can automatically identify the functions. This can be resolved by using IDA Python\r\nto identify and create functions based on byte search patterns.The approach identifies an artifact from the prologue\r\nand an artifact from the epilogue, and then connects the two using a recursive descent parser.\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 20 of 23\n\nFigure 33. Function Epilogue\r\nThe function identification algorithm starts by locating a set of potential function epilogues. The algorithm for\r\nepilogue identification:\r\nSearch for all add esp , value opcodes ( 000 ):  83 C4 or 81 C4\r\nTrack the amount being added to ESP (38h)\r\nStarting at 000 , walk over the instructions until the return instruction is reached at 00A\r\nTrack the registers being popped off the stack (ESI, EDI, EBP)\r\nThis algorithm can be repeated until all of the epilogues have been identified.\r\nFigure 34. Function Prologue\r\nTo locate a set of all of the potential prologues, the start address for each prologue ( 000 , in this case) needs to be\r\nidentified. This is difficult because none of the functions that need to be identified have a standard prologue\r\n(Figure 34). Since each prologue will have an equivalent epilogue and the epilogues are known, this can be used\r\nto identify the start address for each prologue: \r\nSearch for all sub esp , value opcodes ( 004): 83 EC or 81 EC\r\nStarting with the first SUB instruction located:\r\nCompare the number of bytes subtracted from ESP ( 38h ) to what was added to ESP for each of the\r\nfunction epilogues that were found.\r\nThe expectation is that the same amount subtracted in the prologue will be added in the\r\nepilogue.\r\nWalk backward, from 004 to 000 , and verify that each register being pushed to the stack is in the\r\nsame order as what was popped from the stack in the set of potential epilogues for this function.\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 21 of 23\n\nIf the ESP manipulation value ( 38h ) and the register value PUSH/POP instructions match, then it is safe to say\r\nthat a function prologue has been identified and the start address has been located. \r\nControl Flow Graph Recovery\r\nBoth the start address and the end address for the function have been identified, and now the pieces in between,\r\nthe basic blocks, need to be walked to confirm that the prologue and epilogue actually connect. A recursive\r\ndescent parser is used to walk the control flow graph. The implementation used in the source code is based on the\r\nexample from the book Practical Binary Analysis by Dennis Andriesse. \r\nFunction Creation\r\nThe only thing left to do is create the functions that were identified and cleaned up using the algorithm outlined in\r\nthe CFG Recovery section. \r\n1. Create the identified functions during the CFG Recovery process using IDA’s add_func ( start_address,\r\nend_address ) method.\r\n2. Redefine functions that were undefined during the CFG Recovery process.\r\nConclusion\r\nAs demonstrated throughout this discussion, Maze’s obfuscated control flow can cause quite a headache for an\r\nanalyst, but with a little bit of planning (and Python), these obfuscations can be removed. In the opening\r\nparagraphs of this blog post an IDA navigation bar of the obfuscated Maze binary was shown (Figure 1). The\r\nexpectation is that this will be significantly different after deobfuscation. \r\nFigure 35. IDA Navigation Bar for Deobfuscated Maze Binary\r\nFigure 35 shows a significant improvement. Still, there is plenty of brown and gray space. It’s to be expected. Not\r\neverything is going to be part of a regular function. The presented solution is not going to deobfuscate 100% of\r\nthe code. It will provide an analyst with enough deobfuscated code that the control flow is both easier to follow\r\nand easier to manually correct. \r\nFor further reading on binary analysis and software obfuscation, both “Surreptitious Software” by Christian\r\nCollberg and Jasvir Nagra and “Practical Binary Analysis” are excellent resources. \r\nThe source code has been published on GitHub. \r\nAdditional Resources\r\nDownload the CrowdStrike® 2020 Global Threat Report.\r\nSee the CrowdStrike Falcon® platform in action, and learn how true next-gen AV performs against today’s\r\nmost sophisticated threats — get a full-featured free trial of CrowdStrike Falcon Prevent™ today.\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 22 of 23\n\nTo learn more about how to incorporate intelligence on threat actors and their tactics techniques and\r\nprocedures (TTPs) into your security strategy, please visit the Falcon X™ Threat Intelligence page.\r\nFor more information on the cellular automation depicted in the blog header image, read about  John\r\nConway’s Game of Life.\r\nSource: https://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nhttps://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/\r\nPage 23 of 23\n\nFigure 7. Short IDA, however, JMP Type One expects the JNZ Obfuscation, Incorrect instruction at JNZ Target 002 to fall through Label in if ZF is IDA set to one. That means the bytes located\nat 004 should be marked and displayed as valid x86 code. The consequence of displaying the binary data at 004\n   Page 6 of 23   \n\nCall Type Three: Once again, the Absolute, primary difference Multiple Conditional between Jump this obfuscation type and the JMP Type Two obfuscation is the\naddition of pushing the return address at 001 (Figure 16). The first instruction at 000 pushes whatever value is\n   Page 11 of 23",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://web.archive.org/web/20200522065530/https://www.crowdstrike.com/blog/maze-ransomware-deobfuscation/"
	],
	"report_names": [
		"maze-ransomware-deobfuscation"
	],
	"threat_actors": [
		{
			"id": "2864e40a-f233-4618-ac61-b03760a41cbb",
			"created_at": "2023-12-01T02:02:34.272108Z",
			"updated_at": "2026-04-10T02:00:04.97558Z",
			"deleted_at": null,
			"main_name": "WildCard",
			"aliases": [],
			"source_name": "ETDA:WildCard",
			"tools": [
				"RustDown",
				"SysJoker"
			],
			"source_id": "ETDA",
			"reports": null
		},
		{
			"id": "256a6a2d-e8a2-4497-b399-628a7fad4b3e",
			"created_at": "2023-11-30T02:00:07.299845Z",
			"updated_at": "2026-04-10T02:00:03.484788Z",
			"deleted_at": null,
			"main_name": "WildCard",
			"aliases": [],
			"source_name": "MISPGALAXY:WildCard",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		}
	],
	"ts_created_at": 1775434261,
	"ts_updated_at": 1775826738,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/07654032a3b5d46f138a54471472927fde6fdd13.pdf",
		"text": "https://archive.orkl.eu/07654032a3b5d46f138a54471472927fde6fdd13.txt",
		"img": "https://archive.orkl.eu/07654032a3b5d46f138a54471472927fde6fdd13.jpg"
	}
}