{
	"id": "f83afdf3-5443-4920-9626-ca319a89659b",
	"created_at": "2026-04-06T00:09:36.177985Z",
	"updated_at": "2026-04-10T13:12:07.602382Z",
	"deleted_at": null,
	"sha1_hash": "0215a91850f15065ad7ca1677f87ca4ad88f5327",
	"title": "FinSpy VM Unpacking Tutorial Part 3: Devirtualization. Phase #3: Fixing the Function-Related Issues — Möbius Strip Reverse Engineering",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 117049,
	"plain_text": "FinSpy VM Unpacking Tutorial Part 3: Devirtualization. Phase #3:\r\nFixing the Function-Related Issues — Möbius Strip Reverse\r\nEngineering\r\nBy Rolf Rolles\r\nPublished: 2018-02-21 · Archived: 2026-04-05 13:58:54 UTC\r\n[Note: if you've been linked here without context, the introduction to Part #3 describing its four phases can be\r\nfound here.]\r\n1. Introduction\r\nAt the end of Part #3, Phase #2, we noted that our first attempt at devirtualization is still subject to two remaining\r\nmajor issues.\r\nWe had deferred generating proper displacements when devirtualizing the FinSpy VM X86CALLOUT\r\ninstructions into x86 CALL instructions.\r\nWe discovered that the functions in the devirtualized code were missing their prologues, since the\r\nprologues of virtualized function execute in x86 before entering the VM.\r\nResolving these issues was the most difficult part of devirtualizing FinSpy, and took about half of the total time I\r\nspent on devirtualization. I dreamed up one \"nice\" solution, and implemented instead one \"hacky\" solution, but\r\nboth of them fall short of full automation.\r\nPhase #3, this entry, discusses how FinSpy virtualizes functions and function calls. We illustrate the problems that\r\nFinSpy's design decisions present us in properly devirtualizing functions and function calls. We state precisely\r\nwhat problems need to be solved. Then, we discuss several ideas for solving them, and eventually write an\r\nIDAPython script to collect the information we need. We finish by attending to some issues that arise in the course\r\nof writing and using this script.\r\nPart #3, Phase #4, the next entry, will incorporate this information into the devirtualization process. After fixing a\r\nfew more issues, our journey in devirtualizing FinSpy will be complete.\r\n2. How FinSpy Virtualizes Functions and Calls\r\nUltimately, our major remaining task in devirtualizing FinSpy VM bytecode programs is to devirtualize the\r\nX86CALLOUT instructions. To do that, we need to attend to the several issues described in Part #3, Phase #2 and\r\nin the introduction above. It turns out that these issues arise due to how the FinSpy VM virtualizes functions, and\r\ndue to the mechanics of the X86CALLOUT VM instruction. \r\nIn looking at the FinSpy VM bytecode, we can see that function calls have been virtualized in a way that is very\r\nnatural, identical to what we'd expect to see in an x86 binary compiled with ordinary compilation techniques. For\r\nhttps://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nPage 1 of 16\n\nexample:\r\n0x006ff0: X86 push ebx\r\n0x007020: X86 push dword ptr [ebp+0FFFFFFFCh]\r\n0x007098: X86 push eax\r\n0x0070c8: X86CALLOUT 0x40ae74\r\nThis sequence almost looks like an x86 function call sequence already. And for this particular example, if we\r\ninspect the code at the destination of the X86CALLOUT instruction -- 0x40ae74 -- we see a normal x86 function\r\nbody:\r\n.text:0040AE74  ; void *__cdecl memcpy(void *Dst, const void *Src, size_t Size)\r\n.text:0040AE74  memcpy proc near\r\n.text:0040AE74\r\n.text:0040AE74  Dst= dword ptr 4\r\n.text:0040AE74  Src= dword ptr 8\r\n.text:0040AE74  Size= dword ptr 0Ch\r\n.text:0040AE74\r\n.text:0040AE74 000 jmp  ds:__imp_memcpy\r\n.text:0040AE74\r\n.text:0040AE74  memcpy endp\r\nIf we were to devirtualize the \"X86CALLOUT\" VM instruction from above into an x86 CALL instruction to the\r\nsame location, this is indeed exactly what we would see in a normal binary. The obvious approach to devirtualize\r\nthe X86CALLOUT instruction in the snippet above would be to replace it with an ordinary x86 CALL instruction\r\ntargeting the function above, and there seem to be no drawbacks or serious complications with doing so in this\r\ncase.\r\nHowever, other X86CALLOUT instructions tell a different tale. Here's another, similar sequence of FinSpy VM\r\ninstructions:\r\n0x00f570: X86 push 40h\r\n0x00f5a0: X86 push 1000h\r\n0x00f5d0: X86 push eax\r\n0x00f600: X86 push esi\r\n0x00f630: X86CALLOUT 0x408810\r\nIn this example, at the location specified in the X86CALLOUT instruction -- 0x408810 -- we do not see an\r\nordinary x86 function. Rather, like we discussed in part one, we see that the original, pre-virtualized function has\r\nbeen overwritten by a sequence of code that ultimately enters the FinSpy VM. To re-state: virtualized functions\r\nstill reside at their original addresses in the binary, and can be executed by calling that location as usual; however,\r\nvirtualized functions have been modified to transfer control to the suitable location inside of the VM bytecode\r\nprogram. (I suspect the FinSpy authors chose to retain virtualized function addresses in order to preserve certain\r\nhttps://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nPage 2 of 16\n\nmetadata in the PE64 header surrounding exception records and function start addresses.) The x86 code at the\r\nlocation from above shows:\r\n.text:00408810  mov  edi, edi  ; Ordinary prologue #0\r\n.text:00408812  push  ebp  ; Ordinary prologue #0\r\n.text:00408813  mov  ebp, esp  ; Ordinary prologue #0\r\n.text:00408815  push  ecx  ; Ordinary prologue #0\r\n.text:00408816  push  ecx  ; Ordinary prologue #0\r\n.text:00408817  push  eax  ; Save obfuscation register #1.1\r\n.text:00408818  push  ebx  ; Save obfuscation register #2.1\r\n.text:00408819  mov  ebx, offset unk_41B164 ; Junk obfuscation\r\n.text:0040881E  mov  eax, 0B3BF98Dh  ; Junk obfuscation\r\n; ... more junk obfuscation ...\r\n.text:0040883E  bswap  eax  ; Junk obfuscation\r\n.text:00408840  stc  ; Junk obfuscation\r\n.text:00408841  pop  ebx  ; Restore obfuscation register #2.2\r\n.text:00408842  pop  eax  ; Restore obfuscation register #1.2\r\n.text:00408843  push  5A7314h  ; Push VM instruction entry key #3\r\n.text:00408848  push  eax  ; Obfuscated JMP\r\n.text:00408849  xor  eax, eax  ; Obfuscated JMP\r\n.text:0040884B  pop  eax  ; Obfuscated JMP\r\n.text:0040884C  jz  GLOBAL__Dispatcher  ; Enter FinSpy VM #4\r\nAs in the code above, virtualized functions have been overwritten by sequences of x86 instructions that do up to\r\nfour things.\r\n1. Execute the original prologue of the pre-virtualized function (on the lines labeled #0 above)\r\n2. After saving two registers on the lines labeled #1.1 and #2.1, perform junk obfuscation (meaningless\r\ncomputations), and then restore the values of those registers on the lines labeled #2.2 and #1.2.\r\n3. Push the VM instruction key for the virtualized function body onto the stack (on the line labeled #3 above)\r\n4. Jump to the FinSpy VM interpreter (on the line labeled #4 above)\r\nThe most important observations from the code above are:\r\n1. Whenever an X86CALLOUT instruction targets the x86 location 0x408810, the result is that the VM\r\nbegins executing VM instructions starting with the one whose key is 0x5A7314.\r\n2. For the virtualized function at x86 location 0x408810 (at the VM instruction keyed 0x5A7314), the\r\nvirtualized function's prologue is the first five x86 instructions from the sequence above (all of which are\r\nlabeled #0).\r\n2.1 Comments on Devirtualization Strategy\r\nIn discussing X86CALLOUT VM instructions to non-virtualized targets, we suggested that we could devirtualize\r\nthem into an x86 CALL instruction directly targeting the specified location. However, that strategy will not\r\nhttps://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nPage 3 of 16\n\nproduce good results when the targeted address is a virtualized function. If we were to devirtualize the\r\nX86CALLOUT instruction in the second example above with an x86 CALL instruction to the second targeted\r\nfunction, our resulting \"devirtualization\" would still contain obfuscation and references to the VM. I.e., upon\r\nencountering an x86 CALL instruction in the \"devirtualized\" code, we as analysts would have to analyze the x86\r\ncode shown above to determine which VM instruction ended up executing as a result, and to inspect the prologue\r\nfor the targeted function, which would not be present at the site of the devirtualized function body. This is hardly a\r\n\"devirtualization\", given that we would still need to analyze obfuscated code involving the FinSpy VM, and that\r\nIDA and Hex-Rays will balk at functions with no prologues.\r\nTherefore, to truly \"devirtualize\" FinSpy VM programs, we need to replicate what happens when a virtualized\r\nfunction is called: namely, we need to replicate the original function's prologue from before the VM entry\r\nsequence, and also determine the VM location at which the virtualized function's body resides, in order to emit an\r\nX86 CALL instruction from the site of the X86CALLOUT VM instruction to the devirtualized address of the\r\ndestination, thereby entirely eliminating reliance on the FinSpy VM.\r\nHence, it is clear that we need two different strategies for devirtualizing X86CALLOUT instructions. For non-virtualized targets, we could simply emit x86 CALL instructions to the destination. For virtualized targets, we\r\nwould need to determine the VM address of the VM bytecode translation for the function, locate the devirtualized\r\nversions of those VM instructions within our devirtualized array, and replace the X86CALLOUT VM instruction\r\nwith an x86 CALL instruction pointing to the proper location within the devirtualized code.\r\nAdditionally, for each virtualized function, we need to extract its function prologue from the .text section, and\r\ninsert those instructions before the devirtualized body of the corresponding function.\r\n3. Stating Our Task Precisely\r\nTo devirtualize X86CALLOUT instructions, we need the following information:\r\nA list of all x86 locations used as call targets, and whether or not the functions at those locations are\r\nvirtualized.\r\nFor each virtualized function:\r\nThe VM instruction key pushed before entering the VM\r\nThe raw machine code comprising the function's prologue\r\nWith this information, we could solve our remaining issues as follows:\r\nWhen devirtualizing any FinSpy VM instruction, look up its VM instruction key to see if it's the first VM\r\ninstruction implementing a virtualized function body. If so, insert the prologue bytes from the\r\ncorresponding x86 function in the .text section before devirtualizing the instruction as normal.\r\nWhen devirtualizing X86CALLOUT instructions, check to see whether the destination is a virtualized\r\nfunction or not. \r\nIf the destination is virtualized, emit an x86 CALL instruction whose displacement points to the x86\r\naddress of the devirtualized function body within the blob of devirtualized code. \r\nIf the destination is not virtualized, emit an X86 CALL instruction pointing to the referenced\r\nlocation in the .text section. This requires knowledge of the base address at which the devirtualized\r\nhttps://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nPage 4 of 16\n\ncode shall be stored in the binary containing the FinSpy VM.\r\n3.1 Challenges in Obtaining the Requisite Information\r\nUnfortunately for us, we've got a bit of work ahead of us, since the FinSpy VM does not readily provide the\r\ninformation we're interested in. \r\nThe FinSpy VM metadata does not contain a list of called function addresses (virtualized or not). We have\r\nto perform our own analysis to determine which x86 functions may be called through VM execution, and\r\nwhether or not the called functions are virtualized. \r\nThe FinSpy VM makes no distinction between the cases where the destination address is virtualized, and\r\nthe case in which it is not virtualized. In both cases, the target of an X86CALLOUT location is simply an\r\naddress in the .text section. If the target is virtualized, then there will be a FinSpy VM entry sequence at the\r\ndestination. If the target is not virtualized, there will not be a FinSpy VM entry sequence at the destination.\r\nThus, the onus is upon us to determine whether a particular call target is virtualized or not, and if it is, to\r\nextract its function prologue and determine the VM key of the VM instruction that implements the\r\nvirtualized function's body.\r\n4. Initial Approach to Discovering Virtualized Function Addresses\r\nNow that our tasks are laid out, let's set about solving them. For the first task -- obtaining the list of virtualized\r\nfunction addresses -- my first thought was to extract the targets from all of the X86CALLOUT instructions. That\r\nidea was easy enough to implement. I wrote a small Python script to dump all of the X86CALLOUT targets from\r\nthe FinSpy VM bytecode program:\r\n# Iterate through the VM instructions and extract direct call targets\r\n# Inputs:\r\n# * insns: a list of VM instruction objects\r\n# * vmEntrypoint: the address of WinMain() in the original binary\r\n# Output:\r\n# * A list of call targets\r\ndef ExtractCalloutTargets(insns, vmEntrypoint):\r\n # Create a set containing just the address of the VM entrypoint\r\n calloutTargets = set([vmEntrypoint])\r\n # Iterate through all VM instructions\r\n for i in insns:\r\n # Was the instruction an X86CALLOUT?\r\n if isinstance(i,RawX86Callout):\r\n # Add it to the set\r\n calloutTargets.add(i.X86Target)\r\n # Return a list instead of a set\r\n return list(calloutTargets)\r\nhttps://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nPage 5 of 16\n\nWe add the address of the VM entrypoint because it isn't explicitly called from anywhere within the VM program -\r\n- WinMain() contains a VM entry sequence that causes it to execute.\r\nThe code above was a good start toward obtaining the list of x86 functions that may be called by virtualized code,\r\nand as far as I knew at this point, this list contained all such function addresses. I later discovered that some\r\nvirtualized functions are not referenced via X86CALLOUT instructions (analogously to how WinMain()'s\r\nvirtualized entrypoint is not the target of an X86CALLOUT instruction), and hence are not included in the list of\r\nvirtualized functions generated above. Later it became clear that not including those virtualized function addresses\r\nnot referenced by X86CALLOUT instructions causes serious problems while collecting the function information\r\nin preparation for devirtualization. Still, all of those problems were yet to materialize; this was my initial,\r\nblissfully ignorant approach.\r\n5. A Nice Approach to Extracting Information from Virtualized Functions\r\nFor extracting the VM entry keys and function prologues, the first solution that came to mind was the best one I\r\ncame up with. It is not, however, what I ended up actually doing.\r\nSome modern reverse engineering tools (such as the Unicorn Engine) support custom emulation of assembly\r\nlanguage snippets (as opposed to full-system emulation). The user supplies a starting address and an initial state\r\nfor the registers and memory. From there, the emulation is controllable programmatically: you can emulate one\r\ninstruction at a time and inspect the state after each to determine when to stop. (My own program analysis\r\nframework, Pandemic, part of my SMT training class, also supports this functionality.)\r\nWith access to a customizable x86 emulator, and assuming that we knew the address of the VM interpreter, we\r\ncould extract the VM entry instruction keys for a virtualized function as follows:\r\nPut the emulator's registers and memory into some initial state.\r\nSet the initial x86 EIP to the beginning of the virtualized function's x86-to-VM entrypoint.\r\nEmulate instructions until EIP reaches the address of the VM interpreter. Save each emulated instruction\r\ninto a list.\r\nExtract the DWORD from the bottom of the stack. This is the VM key corresponding to the beginning of\r\nthe function.\r\nScan the list of emulated instructions in reverse order to determine which two registers are being used for\r\njunk obfuscation.\r\nScan the list of emulated instructions in forward order until the point at which those two registers are\r\npushed. The instructions before these two x86 PUSH instructions comprise the prologue for the function\r\nbeginning at the starting address.    \r\nThis is the best solution I have come up with so far, which mirrors the approaches I've taken for similar problems\r\narising from different obfuscators. The emulation-based solution is very generic. For the task of recovering the\r\nVM entry key for a given entrypoint, it uses a semantic pattern -- no matter how the code at the VM entrypoint is\r\nimplemented, the end result is that the VM entry key is pushed on the stack before the VM begins executing.\r\nSemantic pattern-matching is preferable in the extreme to syntactic pattern-matching. It would not matter if the\r\nFinSpy authors change the obfuscation at the virtualized functions, so long as the fundamental architecture of VM\r\nhttps://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nPage 6 of 16\n\nentry remains the same. I expect that this method would continue to work until and unless there was a major\r\noverhaul of the FinSpy VM entry schema.\r\nThe logic for extracting the function prologues is based on syntactic pattern-matching. Therefore it is more brittle,\r\nand would require tweaking if the FinSpy VM authors changed the VM entrypoint obfuscation.\r\nThe reason I did not pursue the emulation-based solution is not especially interesting: I had just gotten a new\r\nlaptop and had not installed my development environment at that time, and I wanted a quick solution. Anyway,\r\neven if I had, I'm not sure I would have released the code for it. While staring at build environment errors, I started\r\nto wonder if there was another way to obtain the x86 call target/VM key information. What I came up to replace\r\nthe VM entry key extraction did work, but in all fairness, it wasn't a very good solution and is not suitable for a\r\nfully-automated solution, since it does involve some manual work.\r\n6. Hacky Solution, Overview\r\nRemember, our overall goal is to discover, for each virtualized function: \r\n1. The key for the VM instruction implementing the virtualized function's body\r\n2. The non-virtualized prologue instructions for the virtualized function\r\nIn lieu of the nicer, emulation-based solution, the hacky solution I came up with uses the IDA API to extract\r\ninformation in several separate parts before eventually combining them. The entire script can be found here.\r\n1. First, we find all x86 locations that jump to the FinSpy VM interpreter.\r\n2. Second, for each such referencing address, we extract the key pushed shortly beforehand, and also extract\r\nthe names of the registers used by the junk obfuscation following the non-virtualized function prologue.\r\n3. For each virtualized function start address, match it up with the subsequent address that enters the VM (i.e.,\r\nmatch it with one of the addresses described in #1). From step #2, we know which VM key is pushed\r\nbefore entering the VM interpreter; thus, we now know which VM key corresponds to the function start\r\naddress. We also know which registers are used for the junk obfuscation for the virtualized function\r\nbeginning at that address.\r\n4. Now that we know the identities of the registers used in junk obfuscation for a given virtualized function,\r\nscan forwards from its starting address looking for the beginning of the junk obfuscation -- i.e., two\r\nconsecutive x86 PUSH instructions pushing those register values. Copy the raw bytes before the beginning\r\nof the junk obfuscation; these are the prologue bytes.\r\n5. Correlate the addresses of the virtualized functions with the prologue bytes and VM instruction key\r\nextracted in previous steps.\r\n6.1 Steps #1 and #2: Extract VM Entry Keys and Obfuscation Registers\r\nThe implementation for the first two steps just described is not very sophisticated or interesting. The first function,\r\nExtractKey, takes as input the address of an instruction that jumps to the VM entrypoint. I.e., its input is the\r\naddress of an instruction such as the one labeled #4 below:\r\nhttps://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nPage 7 of 16\n\n; ... prologue, junk obfuscation above this ...\r\n.text:004083FC pop eax ; Pop obfuscation register #2.2\r\n.text:004083FD  pop  ebx  ; Pop obfuscation register #1.2\r\n.text:004083FE  push  5A6C26h  ; Push VM instruction key -- #3\r\n.text:00408403  push  eax  ; Obfuscated JMP\r\n.text:00408404  xor  eax, eax  ; Obfuscated JMP\r\n.text:00408406  pop  eax  ; Obfuscated JMP\r\n.text:00408407  jz  GLOBAL__Dispatcher  ; Enter FinSpy VM #4\r\nFrom there, the script disassembles instructions backwards, up to some user-specifiable amount, and inspects them\r\none-by-one looking for the first x86 PUSH instruction (i.e., the one on the line labeled #3 above) that pushes the\r\nVM key of the first instruction in the virtualized function's body. \r\nOnce it finds the x86 PUSH instruction on line #3, it inspects the previous two x86 instructions -- those on lines\r\n#1.2 and #2.2 -- to see if they are x86 POP instructions, corresponding to the restoration of the registers the VM\r\nentry sequences use for obfuscation.\r\nIf all checks pass, the script returns a 4-tuple containing:\r\nThe address of the branch to the VM (i.e., that of line #4 above) \r\nThe VM key DWORD (from line #3, e.g. 0x5A6C26 in the above)\r\nThe two x86 register names from line #1.2 and #2.2. \r\nThat code is shown below (excerpted from the complete script):\r\n# Given an address that references the VM dispatcher, extract\r\n# the VM instruction key pushed beforehand, and the names of\r\n# the two registers used for junk obfuscation. Return a tuple\r\n# with all information just described.\r\ndef ExtractKey(vmXref):\r\n currEa = vmXref\r\n Key = None\r\n # Walk backwards from the VM cross-reference up to a\r\n # specified number of instructions.\r\n for _ in xrange(MAX_NUM_OBFUSCATED_JMP_INSTRUCTIONS):\r\n # Is the instruction \"PUSH DWORD\"?\r\n if idc.GetMnem(currEa) == \"push\" and idc.GetOpType(currEa, 0) == o_imm:\r\n # Extract the key, exit loop\r\n Key = idc.GetOperandValue(currEa, 0)\r\n break\r\n # Otherwise, move backwards by one instruction\r\n else:\r\nhttps://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nPage 8 of 16\n\ncurrEa = idc.PrevHead(currEa, 0)\r\n # After looping, did we find a key?\r\n if Key is not None:\r\n # Extract the first operands of the two subsequent instructions\r\n prevEa1 = idc.PrevHead(currEa, 0)\r\n prevEa2 = idc.PrevHead(prevEa1, 0)\r\n # Were they pop instructions?\r\n if idc.GetMnem(prevEa1) == \"pop\" and idc.GetMnem(prevEa2) == \"pop\":\r\n # Return the information we just collected\r\n return (vmXref,Key,idc.GetOpnd(prevEa1,0),idc.GetOpnd(prevEa2,0))\r\n # If not, be noisy about the error\r\n else:\r\n print \"%#lx: found key %#lx, but not POP reg32, inspect manually\" % (xref, Key)\r\n return (vmXref,Key,\"\",\"\")\r\n # Key not found, be noisy\r\n else:\r\n print \"%#lx: couldn't locate key within %d instructions, inspect manually\" % (xref,MAX_NUM_OB\r\n return None\r\nThere is a second function, ExtractKeys, which iterates over all cross-references to the VM entrypoint, and calls\r\nExtractKey for each. It is not interesting, and so the code is not reproduced in this document, though it is in the\r\nincluded source code.\r\nThe results of ExtractKeys -- the tuples from above -- are then saved in a Python variable called locKey. \r\nlocKey = ExtractKeys(0x00401950)\r\nThis script can go wrong in several ways. Some of the references to the VM entrypoint might not have been\r\nproperly disassembled, and so those references won't be in the set returned by IDA's cross-reference functionality.\r\nSecondly, if the FinSpy VM authors modified their virtualized entrypoint obfuscation strategy, it might necessitate\r\nchanges to the strategy of simply walking backwards. Also, I found out later that not every x86-to-VM entry\r\nsequence used junk obfuscation, so the pattern-matching looking for the two x86 POP instructions failed. But,\r\nwe're getting ahead of ourselves; I found and fixed those issues later on.\r\n6.2 Step #3: Correlating Virtualized Function Beginnings with VM Entrypoints\r\nNow that we have information about each location that enters the VM, we need to correlate this information with\r\nthe list of X86CALLOUT targets corresponding to virtualized functions. I.e., given an address that is targeted by\r\nan X86CALLOUT instruction, we want to determine which address will subsequently transfer control into the\r\nVM (i.e., one of the addresses inspected in the previous steps). To assist in explanation, an example of a VM entry\r\nsequence is shown.\r\nhttps://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nPage 9 of 16\n\n.text:00408360 mov edi, edi ; Original function prologue #0\r\n.text:00408362 push ebp ; Original function prologue #0\r\n.text:00408363  mov  ebp, esp  ; Original function prologue #0\r\n.text:00408365  sub  esp, 320h  ; Original function prologue #0\r\n.text:0040836B  push  ebx  ; Original function prologue #0\r\n.text:0040836C  push  edi  ; Original function prologue #0\r\n.text:0040836D  push  ebx  ; Push obfuscation register #1.1\r\n.text:0040836E  push  eax  ; Push obfuscation register #2.1\r\n; ... junk obfuscation not shown ...\r\n.text:004083FC  pop  eax  ; Pop obfuscation register #2.2\r\n.text:004083FD  pop  ebx  ; Pop obfuscation register #1.2\r\n.text:004083FE  push  5A6C26h  ; Push VM instruction key -- #3\r\n.text:00408403  push  eax  ; Obfuscated JMP\r\n.text:00408404  xor  eax, eax  ; Obfuscated JMP\r\n.text:00408406  pop  eax  ; Obfuscated JMP\r\n.text:00408407  jz  GLOBAL__Dispatcher  ; Enter FinSpy VM #4\r\nIn the previous section, we extracted information about each location -- such as the address labeled #4 above --\r\nthat enters the VM. Namely, we extract the VM instruction key for the virtualized function body from the line\r\nlabeled #3, and the names of the two registers used for obfuscation on the lines labeled #2.2 and #1.2.\r\nNow, given the beginning of a virtualized function's VM entrypoint -- such as the first address above labeled #0 --\r\nwe want to know the address of the instruction that ultimately transfers control into the VM, the one labeled #4 in\r\nthe example above. Once we know that, we can look up the address of that instruction within the information\r\nwe've just collected, which will then tell us which two registers are used for the junk obfuscation (the ones popped\r\non the lines labeled #2.2 and #1.2 -- namely, EAX and EBX). From there, we can scan forward in the prologue\r\n(the instructions labeled #0 above) until we find x86 PUSH instructions that save those two registers before the\r\njunk obfuscation begins (namely the x86 PUSH instructions on the lines labeled #1.1 and #2.1 above). Once we\r\nfind those two registers being pushed in that order, we know that we've reached the end of the prologue.\r\nTherefore, every instruction leading up to that point is the virtualized function's prologue. We also know the VM\r\ninstruction key for the first instruction in the virtualized function's body, the one labeled #3 in the sequence above.\r\nCorrelating virtualized function addresses with VM entry addresses is simple. Since there is no control flow in the\r\njunk obfuscation, given the address of a virtualized function's entrypoint, the address of the sequentially-next JZ\r\ninstruction that enters the VM is the corresponding VM entry address.\r\nTherefore, I chose to sort the VM entry information from the previous step -- called locKey -- by the address of\r\nthe JZ instruction, and stored the sorted list in a Python variable called sLocKey:\r\nsLocKey = sorted(locKey, key=lambda x: x[0])\r\nhttps://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nPage 10 of 16\n\nAnd then I wrote a small function that, given the start address for a virtualized function, iterates through sLocKey\r\nand finds the next greater address that enters the VM, and returns the corresponding tuple of information about the\r\nVM entry sequence.\r\n# Given:\r\n# * ea, the address of a virtualized function\r\n# Find the entry in sLocKey whose VM entry branch\r\n# location is the closest one greater than ea\r\n# Ouput:\r\n# The tuple of information for the next VM entry\r\n# instruction following ea\r\ndef LookupKey(ea):\r\n global sLocKey\r\n for i in xrange(len(sLocKey)):\r\n if sLocKey[i][0] \u003c ea:\r\n continue\r\n return sLocKey[i]\r\n return sLocKey[i-1]\r\nNow by passing any virtualized function start address into the LookupKey function above, we will obtain the VM\r\nkey and junk obfuscation register information from the next-subsequent address after the start address that enters\r\nthe VM.\r\nTwo functions collate the virtualized function addresses with the register data:\r\n# Given the address of a virtualized function, look up the VM key and\r\n# register information. Return a new tuple with the virtualized function\r\n# start address in place of the address that jumps to the VM interpreter.\r\ndef CollateCalloutTarget(tgt):\r\n assocData = LookupKey(tgt)\r\n return (tgt, assocData[1], assocData[2], assocData[3])\r\n# Apply the function above to all virtualized function addresses.\r\ndef CollateCalloutTargets(targets):\r\n return map(CollateCalloutTarget, targets)\r\nThe only thing that can go wrong here is if we haven't located all of the JZ instructions that enter the VM. If we\r\ndon't, then the information returned will correspond to some other virtualized function -- which is a real problem\r\nthat did happen in practice, and required some manual effort to fix. Those issues will be discussed later when they\r\narise.\r\n6.3 Step #4: Extract Prologues for Virtualized Functions\r\nAt this point, for every virtualized function's starting address, we have information about the registers used in the\r\njunk obfuscation, as well as the VM instruction key for the virtualized function body. All that's left to do is extract\r\nhttps://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nPage 11 of 16\n\nthe x86 function's prologue.\r\nThis is a very simple affair using standard pattern-matching tricks, very similar to the techniques we used to\r\nextract the VM instruction key and junk obfuscation register names. We simply iterate through the instructions at\r\nthe beginning of a function, looking for two x86 PUSH instructions in a row that push the two registers used in\r\njunk obfuscation. (I noticed that the first junk instruction after the two x86 PUSH instructions was \"mov reg2,\r\naddress\", where \"address\" was within the binary. I added this as a sanity check.) The very simple code is shown\r\nbelow (excerpted from the complete script).\r\n# Given:\r\n# * CallOut, the target of a function call\r\n# * Key, the VM Instruction key DWORD pushed\r\n# * Reg1, the name of the first obfuscation register\r\n# * Reg2, the name of the second obfuscation register\r\n# Extract the prologue bytes and return them.\r\ndef ExtractPrologue(CallOut, Key, Reg1, Reg2):\r\n # Ensure we have two register names.\r\n if Reg1 == \"\" or Reg2 == \"\":\r\n return (Key, CallOut, [])\r\n currEa = CallOut\r\n # Walk forwards from the call target.\r\n for i in xrange(MAX_NUM_PROLOG_INSTRUCTIONS):\r\n # Look for PUSH of first obfuscation register.\r\n if idc.GetMnem(currEa) == \"push\" and idc.GetOpnd(currEa, 0) == Reg1:\r\n nextEa = idc.NextHead(currEa, currEa+16)\r\n # Look for PUSH of second obfuscation register.\r\n if idc.GetMnem(nextEa) == \"push\" and idc.GetOpnd(nextEa, 0) == Reg2:\r\n thirdEa = idc.NextHead(nextEa, nextEa+16)\r\n # Sanity check: first junk instruction is \"mov reg2, address\"\r\n if idc.GetMnem(thirdEa) == \"mov\" and idc.GetOpnd(thirdEa, 0) == Reg2:\r\n destAddr = idc.GetOperandValue(thirdEa, 1)\r\n # Was \"address\" legal?\r\n if destAddr \u003c= PROG_END and destAddr \u003e= PROG_BEGIN:\r\n return (Key, CallOut, [ idc.Byte(CallOut + j) for j in xrange(currEa-CallOut\r\n # If not, be noisy\r\n else:\r\n print \"# 0x%08lx/0x%08lx: found push %s / push %s, not followed by mov %s, ad\r\n pass\r\nhttps://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nPage 12 of 16\n\n# Move forward by one instruction\r\ncurrEa = idc.NextHead(currEa, currEa+16)\r\n # If we didn't find the two PUSH instructions within some\r\n # specified number, be noisy.\r\n print \"# 0x%08lx: did not find push %s / push %s sequence within %d instructions\" % (CallOut, Reg\r\n return (Key, CallOut, [])\r\nNow we have all of the information we need to devirtualize X86CALLOUT instructions properly. One final\r\nfunction collates the information for virtualized function entrypoints with the information collected for jump\r\ninstructions into the VM:\r\n# Extract the prologues from all virtualized functions.\r\ndef GetPrologues(calloutTargets):\r\n return map(lambda data: ExtractPrologue(*data),CollateCalloutTargets(calloutTargets))\r\n6.4 All Together\r\nFirst, we run our script to extract the targets of the FinSpy VM X86CALLOUT instructions. This script uses the\r\nVM bytecode disassembler, which runs outside of IDA. Thus, we take the output of this script, and copy and paste\r\nit into the IDAPython script we developed above to extract function information.\r\nNext, inside of the IDA database for the FinSpy sample, we run our scripts above. They first extract the VM entry\r\ninstruction information, and then use the X86CALLOUT targets generated by the previous scripts to extract the\r\nfunction prologue and VM entry key for each virtualized function. We print that information out, and copy and\r\npaste it into the devirtualization program.\r\nA portion of the output is shown below; the full data set can be seen in the complete Python file:\r\n(0x5a4b3a, 0x406a02, [139, 255, 85, 139, 236, 81, 81, 83, 86, 87]),\r\n(0x5a19e6, 0x40171c, [85, 139, 236]),\r\n(0x5a7a1d, 0x408e07, [139, 255, 85, 139, 236, 81, 131, 101, 252, 0]),\r\n(0x5a7314, 0x408810, [139, 255, 85, 139, 236, 81, 81]),\r\n(0x5a8bf9, 0x409a11, [139, 255, 85, 139, 236, 83, 86, 87, 179, 254]),\r\nEach tuple contains:\r\n1. The VM instruction key for the first instruction of the virtualized function's body\r\n2. The x86 address of the virtualized function\r\n3. A list of x86 machine code bytes for the virtualized function's prologue\r\n6.5 Issues in Collecting Function Information\r\nhttps://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nPage 13 of 16\n\nThe goal of the script detailed in pieces above is to collect the VM instruction keys, junk obfuscation register\r\nnames, and function prologues for each virtualized function. Ultimately we will use this information to\r\ndevirtualize X86CALLOUT instructions and prepend the prologues to the devirtualized versions of the virtualized\r\nfunctions. The scripts above assist in substantially automating this process, but it still requires manual intervention\r\nif any of its assumptions are violated. We can detect these erroneous situations and manually edit the data. Below\r\nwe discuss the failure cases for the scripts above.\r\n(The scripts just created are the primary reason my approach is \"semi-automated\" and not \"fully-automated\".)\r\n6.5.1 Issue #1: Not All Referenced Callout Targets Were Defined as Code\r\nFirst, not every address extracted as the target of an X86CALLOUT instruction was defined as code in the IDA\r\ndatabase for the FinSpy VM sample. This lead to two issues. First, the script for extracting the prologue for that\r\nlocation would fail. Second, since the subsequent reference to the VM entrypoint was consequently also not\r\ndefined as code, that reference would not be included in the set of addresses generated by the script for extracting\r\nthe key and register sequence. \r\nThis situation was easy to identify. The script for extracting the prologue would give an error about not being able\r\nto find the PUSH instructions for the two obfuscation registers. I inspected the locations for which the prologue\r\nscript failed, and if that location was not defined as code, I manually defined the locations as code and ran the\r\nscripts again. Thereafter, the scripts would properly process these locations automatically.\r\n6.5.2 Issue #2: Not Every Virtualized Function Used the Same VM Entry Schema\r\nSecond, the FinSpy VM did not always generate identical schema for x86-to-VM entrypoints at the sites of\r\nvirtualized functions. Since the FinSpy virtualization obfuscator tool overwrites the original function with a VM\r\nentry sequence, small functions might not provide enough space for all parts of the obfuscation sequences. Either\r\nthere would be no junk obfuscation sequence after the prologue, or the JMP to the VM interpreter would not be\r\nobfuscated as an XOR REG, REG / JZ sequence. Also, small functions might not have prologues. \r\nThe script to extract virtualized function VM key / obfuscation register information would fail for these functions.\r\nTwo errors are shown, as well as the corresponding code at the erroneous locations.\r\n0x408412: found key 0x5a6d38, but not POP reg32, inspect manually\r\n0x408d17: found key 0x5a7952, but not POP reg32, inspect manually\r\n.text:0040840D  push  5A6D38h\r\n.text:00408412  jmp  GLOBAL__Dispatcher\r\n.text:00408D0C  mov  edi, edi\r\n.text:00408D0E  push  5A7952h\r\n.text:00408D13  push  ecx\r\n.text:00408D14  xor  ecx, ecx\r\n.text:00408D16  pop  ecx\r\n.text:00408D17  jz  GLOBAL__Dispatcher\r\nhttps://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nPage 14 of 16\n\nOur ultimate goal with this script is to collect key and prologue information for all virtualized functions.\r\nAutomation has failed us, because our assumption that each x86-to-VM entrypoint had the same format was\r\nwrong. Thus, for these functions, I manually collected the information for these functions. In particular, I added\r\neight manually-created entries to the list generated by the scripts; the first two shown below correspond to the\r\nexamples above. (For the second entry, the prologue bytes [0x8B, 0xFF] correspond to the \"mov edi, edi\"\r\ninstruction on line 0x408D0C above.)\r\n(0x5A6D38,0x40840D,[]),\r\n(0x5A7952,0x408D0C,[0x8B,0xFF]),\r\n(0x5A2A19,0x405022,[0x8B,0x65,0xE8,0xC7,0x45,0xC0,0x01,0x00,0x00,0x00,0x83,0x4D,0xFC,0xFF,0x33,0xF6]\r\n(0x5A3FA0,0x4060E6,[]),\r\n(0x5A6841,0x408053,[]),\r\n(0x5A6958,0x408107,[]),\r\n(0x5A6AE9,0x408290,[]),\r\n(0x5A6ED6,0x40851C,[0x8B,0xFF,0x55,0x8B,0xEC]),\r\n6.5.3 Issue #3: Not All Callout Targets Were Virtualized Functions\r\nThird, not all of the targets of X86CALLOUT instructions were virtualized -- ten addresses used as\r\nX86CALLOUT targets were not virtualized. This included a handful of C runtime functions for string\r\nmanipulation and exception handling support, but also a few important functions used by the virtualized program.\r\nThese functions were easy to identify since the prologue extraction script would fail for them. The script issued\r\nten errors, the first three of which are shown:\r\n# 0x0040a07c: did not find push ebp / push edx sequence within 20 instructions\r\n# 0x0040ae80: did not find push ebp / push edx sequence within 20 instructions\r\n# 0x0040709e: did not find push ecx / push edx sequence within 20 instructions\r\nI inspected these addresses manually, and upon determining that the functions weren't virtualized, I created a\r\nPython set called NOT_VIRTUALIZED containing the addresses of these functions. \r\nNOT_VIRTUALIZED = set([\r\n0x0040a07c,\r\n0x0040ae80,\r\n0x0040709e,\r\n0x0040ae74,\r\n0x0040aec7,\r\n0x004070d8,\r\n0x00407119,\r\n0x00401935,\r\n0x00408150,\r\n0x00407155,\r\n])\r\nhttps://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nPage 15 of 16\n\nThen, in my second devirtualization attempt in Part #3, Phase #4, I used this set to determine whether or not to\r\nemit an x86 CALL instruction directly to the referenced target.\r\n7. Conclusion\r\nIn Phase #3, we examined FinSpy's mechanism for virtualizing functions and function calls. We determined that\r\nwe would need several pieces of information to devirtualize these elements correctly, and then wrote scripts to\r\ncollect the information. We attended to issues that arose in running the scripts, and ended up with the data we\r\nneeded for devirtualization.\r\nIn the next and final phase, Part #3, Phase #4, we will incorporate this information into devirtualization. After\r\nfixing a few remaining issues, we will have successfully devirtualized our FinSpy VM sample.\r\nSource: https://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nhttps://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues\r\nPage 16 of 16",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://www.msreverseengineering.com/blog/2018/2/21/devirtualizing-finspy-phase-3-fixing-the-function-related-issues"
	],
	"report_names": [
		"devirtualizing-finspy-phase-3-fixing-the-function-related-issues"
	],
	"threat_actors": [],
	"ts_created_at": 1775434176,
	"ts_updated_at": 1775826727,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/0215a91850f15065ad7ca1677f87ca4ad88f5327.pdf",
		"text": "https://archive.orkl.eu/0215a91850f15065ad7ca1677f87ca4ad88f5327.txt",
		"img": "https://archive.orkl.eu/0215a91850f15065ad7ca1677f87ca4ad88f5327.jpg"
	}
}