{
	"id": "3d9ab540-3b1d-4dfa-a05d-91eb23005820",
	"created_at": "2026-04-10T03:22:08.117561Z",
	"updated_at": "2026-04-10T03:22:16.556444Z",
	"deleted_at": null,
	"sha1_hash": "8863a15b9fcd585363d988fe65bcd04b6ae1183d",
	"title": "Deobfuscating APT32 Flow Graphs with Cutter and Radare2",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 170795,
	"plain_text": "Deobfuscating APT32 Flow Graphs with Cutter and Radare2\r\nBy deugenio\r\nPublished: 2019-04-24 · Archived: 2026-04-10 02:57:08 UTC\r\nResearch by: Itay Cohen\r\nThe Ocean Lotus group, also known as APT32, is a threat actor which has been known to target East Asian\r\ncountries such as Vietnam, Laos and the Philippines. The group strongly focuses on Vietnam, especially private\r\nsector companies that are investing in a wide variety of industrial sectors in the country. While private sector\r\ncompanies are the group’s main targets, APT32 has also been known to target foreign governments, dissidents,\r\nactivists, and journalists.\r\nAPT32’s toolset is wide and varied. It contains both advanced and simple components; it is a mixture of\r\nhandcrafted tools and commercial or open-source ones, such as Mimikatz and Cobalt Strike. It runs the gamut\r\nfrom droppers, shellcode snippets, through decoy documents and backdoors. Many of these tools are highly\r\nobfuscated and seasoned, augmented with different techniques to make them harder to reverse-engineer.\r\nIn this article, we get up and close with one of these obfuscation techniques. This specific technique was used in a\r\nbackdoor of Ocean Lotus’ tool collection. We’ll describe the technique and the difficulty it presents to analysts —\r\nand then show how bypassing this kind of technique is a matter of writing a simple script, as long as you know\r\nwhat you are doing.\r\nThe deobfuscation plugin requires Cutter, the official GUI of the open-source reverse engineering framework –\r\nradare2. Cutter is a cross-platform GUI that aims to expose radare2’s functionality as a user-friendly and modern\r\ninterface.  Last month, Cutter introduced a new Python plugin system, which figures into the tool we’ll be\r\nconstructing below. The plugin itself isn’t complicated, and neither is the solution we demonstrate below. If simple\r\nworks, then simple is best.\r\nDownloading and installing Cutter\r\nCutter is available for all platforms (Linux, OS X, Windows). You can download the latest release here. If you are\r\nusing Linux, the fastest way to get a working copy of Cutter is to use the AppImage file.\r\nIf you want to use the newest version available, with new features and bug fixes, you should build Cutter from\r\nsource. If you are up for that detour, follow this tutorial.\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 1 of 25\n\nFig 1: Cutter interface\r\nThe Backdoor\r\nFirst, let’s have a look at the backdoor itself. The relevant sample ( 486be6b1ec73d98fdd3999abe2fa04368933a2ec )\r\nis part of a multi-stage infection chain, which we have lately seen employed in the wild. All these stages are quite\r\ntypical for Ocean Lotus, especially the chain origin being a malicious document\r\n( 115f3cb5bdfb2ffe5168ecb36b9aed54 ). The document purports to originate from Chinese security vendor Qihoo\r\n360, and contains a malicious VBA Macro code that injects a malicious shellcode to rundll32.exe. The\r\nshellcode contains decryption routines to decrypt and reflectively load a DLL file to the memory. The DLL\r\ncontains the backdoor logic itself.\r\nFirst, the backdoor decrypts a configuration file which is pulled from the file resource. The configuration file\r\nstores information such as the Command and Control servers. The binary then tries to load an auxiliary DLL to the\r\nmemory using a custom-made PE loader. This DLL is called HTTPProv.dll and is capable of communicating\r\nwith the C2 servers. The backdoor can receive dozens of different commands from the Command and Control\r\nservers, including shellcode execution, creation of new processes, manipulation of files and directories, and more.\r\nMany obfuscation techniques are used by Ocean Lotus in order to make their tools harder to reverse engineer.\r\nMost noticeable, Ocean Lotus is using an enormous amount of junk code in their binaries. The junk code makes\r\nthe samples much bigger and more complicated, which distracts researchers trying to pry into the binary. Trying to\r\ndecompile some of these obfuscated functions is a lost cause; the assembly often plays around with the stack\r\npointer, and decompilers are not well-equipped to handle this kind of pathological code.\r\nThe Obfuscation\r\nUpon analysis of the backdoor, one obfuscation technique can be immediately noticed. It is the heavy use of\r\ncontrol flow obfuscation which is created by inserting junk blocks into the flow of the function. These junk blocks\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 2 of 25\n\nare just meaningless noise and make the flow of the function confusing.\r\nFig 2: An example of a junk block\r\nAs you can see in the image above, the block is full of junk code which has nothing to do with what the function\r\nactually does. It’s best to ignore these blocks, but that’s easier said than done. A closer look at these blocks will\r\nreveal something interesting. These junk blocks are always being fail-jumped to by a conditional jump from a\r\nprevious block. Furthermore, these junk blocks will almost always end with a conditional jump which is the\r\nopposite of the conditional jump of the previous block. For example, if the condition above the junk block was jo\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 3 of 25\n\n\u003csome_addr\u003e , the junk block will most likely end with jno \u003csome_addr\u003e . If the block above ended with jne\r\n\u003canother_addr\u003e , the junk block will then end with… you guessed right – je \u003canother_addr\u003e .\r\nFig 3: Opposite conditional jumps\r\nWith this in mind, we can begin structuring the characteristics of these junk blocks. The first characteristic of the\r\nobfuscation is the occurrence of two successive blocks which end with opposite conditional jumps to the\r\nsame target address. The other characteristic requires the second block to contain no meaningful instructions\r\nsuch as string references or calls.\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 4 of 25\n\nWhen these two characteristics are met, we can say with a high chance that the second block is a junk block. In\r\nsuch a case, we would want the first block to jump over the junk block so the junk block would be removed from\r\nthe graph. This can be done by patching the conditional jump with an unconditional jump, aka a simple JMP\r\ninstruction.\r\nFig 4: Modifying the conditional jump to a JMP instruction will ignore the junk block\r\nWriting the Plugin\r\nSo here is a heads up for you – the plugin we present below is written for Cutter, but was designed to be\r\ncompatible with radare2 scripts, for those of you who are CLI gurus. That means that we are going to use some\r\nnifty radare2 commands through r2pipe – a Python wrapper to interact with radare2. This is the most effective and\r\nflexible way for scripting radare2.\r\nIt’s not trivial to get the plugin to support both Cutter and radare2, since one is a GUI program and the other is a\r\nCLI. That means that GUI objects would be meaningless inside radare2. Luckily, Cutter supports r2pipe and is\r\nable to execute radare2 commands from inside its Python plugins.\r\nWriting the Core Class\r\nThe first thing we are going to do is to create a Python class which will be our core class. This class will contain\r\nour logic for finding and removing the junk blocks. Let’s start by defining its __init__ function. The function\r\nwill receive a pipe, which will be either an r2pipe (available from import r2pipe ) object from radare2 or a\r\ncutter (available from import cutter ) object from Cutter.\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 5 of 25\n\nclass GraphDeobfuscator:\r\n def __init__(self, pipe):\r\n \"\"\"an initialization function for the class\r\n \r\n Arguments:\r\n pipe {r2pipe} -- an instance of r2pipe or Cutter's wrapper\r\n \"\"\"\r\n self.pipe = pipe\r\nNow we can execute radare2 commands using this pipe. The pipe object contains two major ways to execute r2\r\ncommands. The first is pipe.cmd(\u003ccommand\u003e) which will return the results of the command as a string, and the\r\nsecond is pipe.cmdj(\u003ccommand\u003ej) which will return a parsed JSON object from the output of radare2’s\r\ncommand.\r\nNote: Almost every command of radare2 can be appended with a j to get the output as JSON.\r\nThe next thing we would want to do is to get all the blocks of the current function and then iterate over each one\r\nof them. We can do this by using the afbj command which stands for Analyze Function Blocks and will return a\r\nJson object with all the blocks of the function.\r\n def clean_junk_blocks(self):\r\n \"\"\"Search a given function for junk blocks, remove them and fix the flow.\r\n \"\"\"\r\n # Get all the basic blocks of the function\r\n blocks = self.pipe.cmdj(\"afbj @ $F\")\r\n if not blocks:\r\n print(\"[X] No blocks found. Is it a function?\")\r\n return\r\n modified = False\r\n # Iterate over all the basic blocks of the function\r\n for block in blocks:\r\n # do something\r\nFor each block, we want to know if there is a block which fails-to in a case where the conditional jump would not\r\ntake place. If a block has a block to which it fails, the second block is an initial candidate to be a junk block.\r\n def get_fail_block(self, block):\r\n \"\"\"Return the block to which a block branches if the condition is fails\r\n \r\n Arguments:\r\n block {block_context} -- A JSON representation of a block\r\n \r\n Returns:\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 6 of 25\n\nblock_context -- The block to which the branch fails. If not exists, returns None\r\n \"\"\"\r\n # Get the address of the \"fail\" branch\r\n fail_addr = self.get_fail(block)\r\n if not fail_addr:\r\n return None\r\n # Get a block context of the fail address\r\n fail_block = self.get_block(fail_addr)\r\n return fail_block if fail_block else None\r\nNote: Since our space is limited, we won’t explain every function that appears here. Functions as\r\nget_block (addr) or get_fail_addr (block) that are used in the snippet above are subroutines we\r\nwrote to make the code cleaner. The function implementations will be available in the final plugin that\r\nis shown and linked at the end of the article. Hopefully, you’ll find the function names self-explanatory.\r\nNext, we would like to check whether our junk block candidate comes immediately after the block. If no, this is\r\nmost likely not a junk block since from what we inspected, junk blocks are located in the code immediately after\r\nthe blocks with the conditional jump.\r\n def is_successive_fail(self, block_A, block_B):\r\n \"\"\"Check if the end address of block_A is the start of block_B\r\n Arguments:\r\n block_A {block_context} -- A JSON object to represent the first block\r\n block_B {block_context} -- A JSON object to represent the second block\r\n \r\n Returns:\r\n bool -- True if block_B comes immediately after block_A, False otherwise\r\n \"\"\"\r\n return ((block_A[\"addr\"] + block_A[\"size\"]) == block_B[\"addr\"])\r\nThen, we would want to check whether the block candidate contains no meaningful instructions. For example, it is\r\nunlikely that a junk block will contain CALL instructions or references for strings. To do this, we will use the\r\ncommand pdsb which stands for Print Disassembly Summary of a Block. This radare2 command prints the\r\ninteresting instructions that appear in a certain block. We assume that a junk block would not contain interesting\r\ninstructions.\r\n def contains_meaningful_instructions (self, block):\r\n '''Check if a block contains meaningful instructions (references, calls, strings,...)\r\n \r\n Arguments:\r\n block {block_context} -- A JSON object which represents a block\r\n \r\n Returns:\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 7 of 25\n\nbool -- True if the block contains meaningful instructions, False otherwise\r\n '''\r\n # Get summary of block - strings, calls, references\r\n summary = self.pipe.cmd(\"pdsb @ {addr}\".format(addr=block[\"addr\"]))\r\n return summary != \"\"\r\nLast, we would like to check whether the conditional jumps of both blocks are opposite. This will be the last piece\r\nof the puzzle to determine whether we are dealing with a junk block. For this, we would need to create a list of\r\nopposite conditional jumps. The list we’ll show is partial since the x86 architecture contains many conditional\r\njump instructions. That said, from our tests, it seems like the below list is enough to cover all the different pairs of\r\nopposite conditional jumps that are presented in APT32’s backdoor. If it doesn’t, adding additional instructions is\r\neasy.\r\n jmp_pairs = [\r\n ['jno', 'jo'],\r\n ['jnp', 'jp'],\r\n ['jb', 'jnb'],\r\n ['jl', 'jnl'],\r\n ['je', 'jne'],\r\n ['jns', 'js'],\r\n ['jnz', 'jz'],\r\n ['jc', 'jnc'],\r\n ['ja', 'jbe'],\r\n ['jae', 'jb'],\r\n ['je', 'jnz'],\r\n ['jg', 'jle'],\r\n ['jge', 'jl'],\r\n ['jpe', 'jpo'],\r\n ['jne', 'jz']]\r\n def is_opposite_conditional(self, cond_A, cond_B):\r\n \"\"\"Check if two operands are opposite conditional jump operands\r\n \r\n Arguments:\r\n cond_A {string} -- the conditional jump operand of the first block\r\n cond_B {string} -- the conditional jump operand of the second block\r\n \r\n Returns:\r\n bool -- True if the operands are opposite, False otherwise\r\n \"\"\"\r\n sorted_pair = sorted([cond_A, cond_B])\r\n for pair in self.jmp_pairs:\r\n if sorted_pair == pair:\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 8 of 25\n\nreturn True\r\n return False\r\nNow that we defined the validation functions, we can glue these parts together inside the clean_junk_blocks()\r\nfunction we created earlier.\r\n def clean_junk_blocks(self):\r\n \"\"\"Search a given function for junk blocks, remove them and fix the flow.\r\n \"\"\"\r\n # Get all the basic blocks of the function\r\n blocks = self.pipe.cmdj(\"afbj @ $F\")\r\n if not blocks:\r\n print(\"[X] No blocks found. Is it a function?\")\r\n return\r\n modified = False\r\n # Iterate over all the basic blocks of the function\r\n for block in blocks:\r\n fail_block = self.get_fail_block(block)\r\n if not fail_block or \\\r\n not self.is_successive_fail(block, fail_block) or \\\r\n self.contains_meaningful_instructions(fail_block) or \\\r\n not self.is_opposite_conditional(self.get_last_mnem_of_block(block), self.get_last_mnem_of\r\n continue\r\nIn case that all the checks are successfully passed, and we can say with a high chance that we found a junk block,\r\nwe would want to patch the first conditional jump to JMP over the junk block, hence removing the junk block\r\nfrom the graph and thus, from the function itself.\r\nTo do this, we use two radare2 commands. The first is aoj @ \u003caddr\u003e which stands for Analyze Opcode and will\r\ngive us information on the instruction in a given address. This command can be used to get the target address of\r\nthe conditional jump. The second command we use is wai \u003cinstruction\u003e @ \u003caddr\u003e which stands for Write\r\nAssembly Inside. Unlike the wa \u003cinstruction\u003e @ \u003caddr\u003e command to overwrite an instruction with another\r\ninstruction, the wai command will fill the remaining bytes with NOP instructions. Thus, in a case where the\r\nJMP \u003caddr\u003e instruction we want to use is shorter than the current conditional-jump instruction, the remaining\r\nbytes will be replaced with NOP s.\r\n def overwrite_instruction(self, addr):\r\n \"\"\"Overwrite a conditional jump to an address, with a JMP to it\r\n \r\n Arguments:\r\n addr {addr} -- address of an instruction to be overwritten\r\n \"\"\"\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 9 of 25\n\njump_destination = self.get_jump(self.pipe.cmdj(\"aoj @ {addr}\".format(addr=addr))[0])\r\n if (jump_destination):\r\n self.pipe.cmd(\"wai jmp 0x{dest:x} @ {addr}\".format(dest=jump_destination, addr=addr))\r\nAfter overwriting the conditional-jump instruction, we continue to loop over all the blocks of the function and\r\nrepeat the steps described above. Last, if changes were made in the function, we re-analyze the function so that the\r\nchanges we made appear in the function graph.\r\n def reanalize_function(self):\r\n \"\"\"Re-Analyze a function at a given address\r\n \r\n Arguments:\r\n addr {addr} -- an address of a function to be re-analyze\r\n \"\"\"\r\n # Seek to the function's start\r\n self.pipe.cmd(\"s $F\")\r\n # Undefine the function in this address\r\n self.pipe.cmd(\"af- $\")\r\n # Define and analyze a function in this address\r\n self.pipe.cmd(\"afr @ $\")\r\nAt last, the clean_junk_blocks() function is now ready to be used. We can now also create a function,\r\nclean_graph() , that cleans the obfuscated function of the backdoor.\r\n def clean_junk_blocks(self):\r\n \"\"\"Search a given function for junk blocks, remove them and fix the flow.\r\n \"\"\"\r\n # Get all the basic blocks of the function\r\n blocks = self.pipe.cmdj(\"afbj @ $F\")\r\n if not blocks:\r\n print(\"[X] No blocks found. Is it a function?\")\r\n return\r\n # Have we modified any instruction in the function?\r\n # If so, a reanalyze of the function is required\r\n modified = False\r\n # Iterate over all the basic blocks of the function\r\n for block in blocks:\r\n fail_block = self.get_fail_block(block)\r\n # Make validation checks\r\n if not fail_block or \\\r\n not self.is_successive_fail(block, fail_block) or \\\r\n self.contains_meaningful_instructions(fail_block) or \\\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 10 of 25\n\nnot self.is_opposite_conditional(self.get_last_mnem_of_block(block), self.get_last_mnem_of\r\n continue\r\n self.overwrite_instruction(self.get_block_end(block))\r\n modified = True\r\n if modified:\r\n self.reanalize_function()\r\n \r\n def clean_graph(self):\r\n \"\"\"the initial function of the class. Responsible to enable cache and start the cleaning\r\n \"\"\"\r\n # Enable cache writing mode. changes will only take place in the session and\r\n # will not override the binary\r\n self.pipe.cmd(\"e io.cache=true\")\r\n self.clean_junk_blocks()\r\nThis concludes the core class.\r\nCutter or Radare2?\r\nAs mentioned earlier, our code will be executed either as a plugin for Cutter, or straight from the radare2 CLI as a\r\nPython script. That means that we need to have a way to understand whether our code is being executed from\r\nCutter or from radare2. For this, we can use the following simple trick.\r\n# Check if we're running from cutter\r\ntry:\r\n import cutter\r\n from PySide2.QtWidgets import QAction\r\n pipe = cutter\r\n cutter_available = True\r\n# If no, assume running from radare2\r\nexcept:\r\n import r2pipe\r\n pipe = r2pipe.open()\r\n cutter_available = False\r\nThe code above checks whether the cutter library can be imported. If it can, we are running from inside Cutter\r\nand can feel safe to do some GUI magic. Otherwise, we’re running from inside radare2, and so we opt to import\r\nr2pipe . In both statements, we are assigning a variable named pipe which will be later passed to the\r\nGraphDeobfuscator class we created.\r\nRunning from Radare2\r\nThis is the simplest way to use this plugin. Checking if __name__ equals “__main__” is a common Python idiom\r\nthat checks if the script was run directly or imported. If this script was run directly, we simply execute the\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 11 of 25\n\nclean_graph() function.\r\nif __name__ == \"__main__\":\r\n graph_deobfuscator = GraphDeobfuscator(pipe)\r\n graph_deobfuscator.clean_graph()\r\nRunning from Cutter\r\nCutter’s documentation describes how to go about building and executing a Plugin for Cutter, and we follow its\r\nlead. First, we need to make sure that we are running from inside Cutter. We already created a boolean variable\r\nnamed cutter_variable . We simply need to check whether this variable is set to True . If it does, we proceed\r\nto define our plugin class.\r\nif cutter_available:\r\n # This part will be executed only if Cutter is available.\r\n # This will create the cutter plugin and UI objects for the plugin\r\n class GraphDeobfuscatorCutter(cutter.CutterPlugin):\r\n name = \"APT32 Graph Deobfuscator\"\r\n description = \"Graph Deobfuscator for APT32 Samples\"\r\n version = \"1.0\"\r\n author = \"Itay Cohen (@Megabeets_)\"\r\n def setupPlugin(self):\r\n pass\r\n def setupInterface(self, main):\r\n pass\r\n \r\n def create_cutter_plugin():\r\n return GraphDeobfuscatorCutter()\r\nThis is a skeleton of a Cutter plugin — it contains no proper functionality at all. The function\r\ncreate_cutter_plugin() is called by Cutter upon loading. At this point, if we will place our script in Cutter’s\r\nplugins directory, Cutter will recognize our file as a plugin.\r\nTo make the plugin execute our functionality, we need to add a menu entry that the user can press to trigger our\r\ndeobfuscator. We chose to add a menu entry, or an Action, to the “Windows -\u003e Plugins” menu.\r\nif cutter_available:\r\n # This part will be executed only if Cutter is available. This will\r\n # create the cutter plugin and UI objects for the plugin\r\n class GraphDeobfuscatorCutter(cutter.CutterPlugin):\r\n name = \"APT32 Graph Deobfuscator\"\r\n description = \"Graph Deobfuscator for APT32 Samples\"\r\n version = \"1.0\"\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 12 of 25\n\nauthor = \"Megabeets\"\r\n def setupPlugin(self):\r\n pass\r\n def setupInterface(self, main):\r\n # Create a new action (menu item)\r\n action = QAction(\"APT32 Graph Deobfuscator\", main)\r\n action.setCheckable(False)\r\n # Connect the action to a function - cleaner.\r\n # A click on this action will trigger the function\r\n action.triggered.connect(self.cleaner)\r\n # Add the action to the \"Windows -\u003e Plugins\" menu\r\n pluginsMenu = main.getMenuByType(main.MenuType.Plugins)\r\n pluginsMenu.addAction(action)\r\n def cleaner(self):\r\n graph_deobfuscator = GraphDeobfuscator(pipe)\r\n graph_deobfuscator.clean_graph()\r\n cutter.refresh()\r\n def create_cutter_plugin():\r\n return GraphDeobfuscatorCutter()\r\nThe script is now ready, and can be placed in the Python folder, under Cutter’s plugins directory. The path to the\r\ndirectory is shown in the Plugins Options, under “Edit -\u003e Preferences -\u003e Plugins“. For example, on our machine\r\nthe path is: “~/.local/share/RadareOrg/Cutter/Plugins/Python“.\r\nNow, when opening Cutter, we can see in “Plugins -\u003e Preferences” that the plugin was indeed loaded.\r\nFig 5: The plugin was successfully loaded\r\nWe can also check the “Windows -\u003e Plugins” menu to see if the menu item we created is there. And indeed, we\r\ncan see that the “APT32 Graph Deobfuscator” item now appears in the menu.\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 13 of 25\n\nFig 6: The menu item we created was successfully added\r\nWe can now choose some function which we suspect for having these junk blocks, and try to test our Plugin. In\r\nthis example, We chose the function fcn.00acc7e0 . Going to a function in Cutter can be done either by selecting\r\nit from the left menu, or simply pressing “g” and typing its name or address in the navigation bar.\r\nMake sure you are in the graph view. Feel free to wander around and try to spot the junk blocks. We highlighted\r\nthem in the image below which shows the Graph Overview (mini-graph)  window.\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 14 of 25\n\nFig 7: Junk block highlighted in fcn.00acc7e0\r\nSince we have a candidate suspicious function, we can trigger our plugin and see if it successfully removes them.\r\nTo do this, click on “Windows -\u003e Plugins -\u003e APT32 Graph Deobfuscator“. After a second, we can see that our\r\nplugin successfully removed the junk blocks.\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 15 of 25\n\nFig 8: The same function after removing the junk blocks\r\nIn the following images, you can see more pairs of function graphs before and after the removal of junk blocks.\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 16 of 25\n\nFig 9: Before and After of fcn.00aa07b0\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 17 of 25\n\nFig 10: Before and After of fcn.00a8a1a0\r\nFinal Words\r\nOcean Lotus’ obfuscation techniques are in no way the most complicated or hard to beat. In this article we went\r\nthrough understanding the problem, drafting a solution and finally implementing it using the python scripting\r\ncapabilities of Cutter and Radare2. The full script can be found in our Github repository, and also attached to the\r\nbottom of this article.\r\nIf you are interested in reading more about Ocean Lotus, we recommend this high-quality article published by\r\nESET’s Romain Dumont. It contains a thorough analysis of Ocean Lotus’ tools, as well as some exposition of the\r\nobfuscation techniques involved.\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 18 of 25\n\nAppendix\r\nSample SHA-256 values\r\nBe6d5973452248cb18949711645990b6a56e7442dc30cc48a607a2afe7d8ec66\r\n8d74d544396b57e6faa4f8fdf96a1a5e30b196d56c15f7cf05767a406708a6b2\r\nAPT32 Graph Deobfuscator – Full Code\r\n1. \"\"\" A plugin for Cutter and Radare2 to deobfuscate APT32 flow graphs\r\n2. This is a python plugin for Cutter that is compatible as an r2pipe script for\r\n3. radare2 as well. The plugin will help reverse engineers to deobfuscate and remove\r\n4. junk blocks from APT32 (Ocean Lotus) samples.\r\n5. \"\"\"\r\n6. \r\n7. __author__ = \"Itay Cohen, aka @megabeets_\"\r\n8. __company__ = \"Check Point Software Technologies Ltd\"\r\n9. \r\n10. # Check if we're running from cutter\r\n11. try:\r\n12. import cutter\r\n13. from PySide2.QtWidgets import QAction\r\n14. pipe = cutter\r\n15. cutter_available = True\r\n16. # If no, assume running from radare2\r\n17. except:\r\n18. import r2pipe\r\n19. pipe = r2pipe.open()\r\n20. cutter_available = False\r\n21. \r\n22. \r\n23. class GraphDeobfuscator:\r\n24. # A list of pairs of opposite conditional jumps\r\n25. jmp_pairs = [\r\n26. ['jno', 'jo'],\r\n27. ['jnp', 'jp'],\r\n28. ['jb', 'jnb'],\r\n29. ['jl', 'jnl'],\r\n30. ['je', 'jne'],\r\n31. ['jns', 'js'],\r\n32. ['jnz', 'jz'],\r\n33. ['jc', 'jnc'],\r\n34. ['ja', 'jbe'],\r\n35. ['jae', 'jb'],\r\n36. ['je', 'jnz'],\r\n37. ['jg', 'jle'],\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 19 of 25\n\n38. ['jge', 'jl'],\r\n39. ['jpe', 'jpo'],\r\n40. ['jne', 'jz']]\r\n41. \r\n42. def __init__(self, pipe, verbose=False):\r\n43. \"\"\"an initialization function for the class\r\n44.\r\n45. Arguments:\r\n46. pipe {r2pipe} -- an instance of r2pipe or Cutter's wrapper\r\n47.\r\n48. Keyword Arguments:\r\n49. verbose {bool} -- if True will print logs to the screen (default: {False})\r\n50. \"\"\"\r\n51. \r\n52. self.pipe = pipe\r\n53. \r\n54. self.verbose = verbose\r\n55. \r\n56. def is_successive_fail(self, block_A, block_B):\r\n57. \"\"\"Check if the end address of block_A is the start of block_B\r\n58. \r\n59. Arguments:\r\n60. block_A {block_context} -- A JSON object to represent the first block\r\n61. block_B {block_context} -- A JSON object to represent the second block\r\n62.\r\n63. Returns:\r\n64. bool -- True if block_B comes immediately after block_A, False otherwise\r\n65. \"\"\"\r\n66. \r\n67. return ((block_A[\"addr\"] + block_A[\"size\"]) == block_B[\"addr\"])\r\n68. \r\n69. def is_opposite_conditional(self, cond_A, cond_B):\r\n70. \"\"\"Check if two operands are opposite conditional jump operands\r\n71.\r\n72. Arguments:\r\n73. cond_A {string} -- the conditional jump operand of the first block\r\n74. cond_B {string} -- the conditional jump operand of the second block\r\n75. \r\n76. Returns:\r\n77. bool -- True if the operands are opposite, False otherwise\r\n78. \"\"\"\r\n79. \r\n80. sorted_pair = sorted([cond_A, cond_B])\r\n81. for pair in self.jmp_pairs:\r\n82. if sorted_pair == pair:\r\n83. return True\r\n84. return False\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 20 of 25\n\n85.\r\n86. def contains_meaningful_instructions (self, block):\r\n87. '''Check if a block contains meaningful instructions (references, calls, strings,...)\r\n88.\r\n89. Arguments:\r\n90. block {block_context} -- A JSON object which represents a block\r\n91.\r\n92. Returns:\r\n93. bool -- True if the block contains meaningful instructions, False otherwise\r\n94. '''\r\n95. \r\n96. # Get summary of block - strings, calls, references\r\n97. summary = self.pipe.cmd(\"pdsb @ {addr}\".format(addr=block[\"addr\"]))\r\n98. return summary != \"\"\r\n99. \r\n100. def get_block_end(self, block):\r\n101. \"\"\"Get the address of the last instruction in a given block\r\n102.\r\n103. Arguments:\r\n104. block {block_context} -- A JSON object which represents a block\r\n105.\r\n106. Returns:\r\n107. The address of the last instruction in the block\r\n108. \"\"\"\r\n109. \r\n110. # save current seek\r\n111. self.pipe.cmd(\"s {addr}\".format(addr=block['addr']))\r\n112. # This will return the address of a block's last instruction\r\n113. block_end = self.pipe.cmd(\"?v $ @B:-1\")\r\n114. return block_end\r\n115. \r\n116. def get_last_mnem_of_block(self, block):\r\n117. \"\"\"Get the mnemonic of the last instruction in a block\r\n118.\r\n119. Arguments:\r\n120. block {block_context} -- A JSON object which represents a block\r\n121.\r\n122. Returns:\r\n123. string -- the mnemonic of the last instruction in the given block\r\n124. \"\"\"\r\n125. \r\n126. inst_info = self.pipe.cmdj(\"aoj @ {addr}\".format(addr=self.get_block_end(block)))[0]\r\n127. return inst_info[\"mnemonic\"]\r\n128. \r\n129. def get_jump(self, block):\r\n130. \"\"\"Get the address to which a block jumps\r\n131.\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 21 of 25\n\n132. Arguments:\r\n133. block {block_context} -- A JSON object which represents a block\r\n134.\r\n135. Returns:\r\n136. addr -- the address to which the block jumps to. If such address doesn't exist, re\r\n137. \"\"\"\r\n138. \r\n139. return block[\"jump\"] if \"jump\" in block else None\r\n140. \r\n141. def get_fail_addr(self, block):\r\n142. \"\"\"Get the address to which a block fails\r\n143.\r\n144. Arguments:\r\n145. block {block_context} -- A JSON object which represents a block\r\n146.\r\n147. Returns:\r\n148. addr -- the address to which the block fail-branches to. If such address doesn't e\r\n149. \"\"\"\r\n150. return block[\"fail\"] if \"fail\" in block else None\r\n151. \r\n152. def get_block(self, addr):\r\n153. \"\"\"Get the block context in a given address\r\n154.\r\n155. Arguments:\r\n156. addr {addr} -- An address in a block\r\n157.\r\n158. Returns:\r\n159. block_context -- the block to which the address belongs\r\n160. \"\"\"\r\n161. \r\n162. block = self.pipe.cmdj(\"abj. @ {offset}\".format(offset=addr))\r\n163. return block[0] if block else None\r\n164. \r\n165. def get_fail_block(self, block):\r\n166. \"\"\"Return the block to which a block branches if the condition is fails\r\n167.\r\n168. Arguments:\r\n169. block {block_context} -- A JSON representation of a block\r\n170.\r\n171. Returns:\r\n172. block_context -- The block to which the branch fails. If not exists, returns None\r\n173. \"\"\"\r\n174. # Get the address of the \"fail\" branch\r\n175. fail_addr = self.get_fail_addr(block)\r\n176. if not fail_addr:\r\n177. return None\r\n178. # Get a block context of the fail address\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 22 of 25\n\n179. fail_block = self.get_block(fail_addr)\r\n180. return fail_block if fail_block else None\r\n181. \r\n182. def reanalize_function(self):\r\n183. \"\"\"Re-Analyze a function at a given address\r\n184.\r\n185. Arguments:\r\n186. addr {addr} -- an address of a function to be re-analyze\r\n187. \"\"\"\r\n188. # Seek to the function's start\r\n189. self.pipe.cmd(\"s $F\")\r\n190. # Undefine the function in this address\r\n191. self.pipe.cmd(\"af- $\")\r\n192. \r\n193. # Define and analyze a function in this address\r\n194. self.pipe.cmd(\"afr @ $\")\r\n195. \r\n196. def overwrite_instruction(self, addr):\r\n197. \"\"\"Overwrite a conditional jump to an address, with a JMP to it\r\n198.\r\n199. Arguments:\r\n200. addr {addr} -- address of an instruction to be overwritten\r\n201. \"\"\"\r\n202. \r\n203. jump_destination = self.get_jump(self.pipe.cmdj(\"aoj @ {addr}\".format(addr=addr))[0])\r\n204. if (jump_destination):\r\n205. self.pipe.cmd(\"wai jmp 0x{dest:x} @ {addr}\".format(dest=jump_destination, addr=add\r\n206. \r\n207. def get_current_function(self):\r\n208. \"\"\"Return the start address of the current function\r\n209. \r\n210. Return Value:\r\n211. The address of the current function. None if no function found.\r\n212. \"\"\"\r\n213. function_start = int(self.pipe.cmd(\"?vi $FB\"))\r\n214. return function_start if function_start != 0 else None\r\n215. \r\n216. def clean_junk_blocks(self):\r\n217. \"\"\"Search a given function for junk blocks, remove them and fix the flow.\r\n218. \"\"\"\r\n219. \r\n220. # Get all the basic blocks of the function\r\n221. blocks = self.pipe.cmdj(\"afbj @ $F\")\r\n222. if not blocks:\r\n223. print(\"[X] No blocks found. Is it a function?\")\r\n224. return\r\n225. # Have we modified any instruction in the function?\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 23 of 25\n\n226. # If so, a reanalyze of the function is required\r\n227. modified = False\r\n228. \r\n229. # Iterate over all the basic blocks of the function\r\n230. for block in blocks:\r\n231. fail_block = self.get_fail_block(block)\r\n232. # Make validation checks\r\n233. if not fail_block or \\\r\n234. not self.is_successive_fail(block, fail_block) or \\\r\n235. self.contains_meaningful_instructions(fail_block) or \\\r\n236. not self.is_opposite_conditional(self.get_last_mnem_of_block(block), self.get_last\r\n237. continue\r\n238. if self.verbose:\r\n239. print (\"Potential junk: 0x{junk_block:x} (0x{fix_block:x})\".format(junk_block=\r\n240. self.overwrite_instruction(self.get_block_end(block))\r\n241. modified = True\r\n242. if modified:\r\n243. self.reanalize_function()\r\n244.\r\n245. def clean_graph(self):\r\n246. \"\"\"the initial function of the class. Responsible to enable cache and start the cleani\r\n247. \"\"\"\r\n248. \r\n249. # Enable cache writing mode. changes will only take place in the session and\r\n250. # will not override the binary\r\n251. self.pipe.cmd(\"e io.cache=true\")\r\n252. self.clean_junk_blocks()\r\n253.\r\n254. \r\n255. if cutter_available:\r\n256. # This part will be executed only if Cutter is available. This will\r\n257. # create the cutter plugin and UI objects for the plugin\r\n258. class GraphDeobfuscatorCutter(cutter.CutterPlugin):\r\n259. name = \"APT32 Graph Deobfuscator\"\r\n260. description = \"Graph Deobfuscator for APT32 Samples\"\r\n261. version = \"1.0\"\r\n262. author = \"Itay Cohen (@Megabeets_)\"\r\n263. \r\n264. def setupPlugin(self):\r\n265. pass\r\n266. \r\n267. def setupInterface(self, main):\r\n268. # Create a new action (menu item)\r\n269. action = QAction(\"APT32 Graph Deobfuscator\", main)\r\n270. action.setCheckable(False)\r\n271. # Connect the action to a function - cleaner.\r\n272. # A click on this action will trigger the function\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 24 of 25\n\n273. action.triggered.connect(self.cleaner)\r\n274.\r\n275. # Add the action to the \"Windows -\u003e Plugins\" menu\r\n276. pluginsMenu = main.getMenuByType(main.MenuType.Plugins)\r\n277. pluginsMenu.addAction(action)\r\n278. \r\n279. def cleaner(self):\r\n280. graph_deobfuscator = GraphDeobfuscator(pipe)\r\n281. graph_deobfuscator.clean_graph()\r\n282. cutter.refresh()\r\n283. \r\n284. \r\n285. def create_cutter_plugin():\r\n286. return GraphDeobfuscatorCutter()\r\n287. \r\n288. \r\n289. if __name__ == \"__main__\":\r\n290. graph_deobfuscator = GraphDeobfuscator(pipe)\r\n291. graph_deobfuscator.clean_graph()\r\n292. \r\nSource: https://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nhttps://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/\r\nPage 25 of 25",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://research.checkpoint.com/deobfuscating-apt32-flow-graphs-with-cutter-and-radare2/"
	],
	"report_names": [
		"deobfuscating-apt32-flow-graphs-with-cutter-and-radare2"
	],
	"threat_actors": [],
	"ts_created_at": 1775791328,
	"ts_updated_at": 1775791336,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/8863a15b9fcd585363d988fe65bcd04b6ae1183d.pdf",
		"text": "https://archive.orkl.eu/8863a15b9fcd585363d988fe65bcd04b6ae1183d.txt",
		"img": "https://archive.orkl.eu/8863a15b9fcd585363d988fe65bcd04b6ae1183d.jpg"
	}
}