[ << ALL_FEED ]

How to fix CFG. Part two

More in Reverse engineering

How to Fix CFG. Part Two 🛠

Earlier we described how to restore a Control Flow Graph (CFG) when it has been obfuscated. However, often during analysis, even of non-obfuscated malware, one can encounter cases where the CFG of certain functions is generated with errors. One such example is malware written in Delphi, where due to the specifics of exception handling, one can often see a picture like in screenshot 1.

🤔 If you look more closely (screenshot 2), you can notice that in block (1), the address of one of the subsequent blocks (3) is saved onto the stack, after which some logic is executed (often — releasing resources or objects) and a jump occurs to the previously saved address (2).

Because block 2 contains an additional reference, IDA cannot unambiguously determine the address to which the jump in jmp eax will be made. To fix this problem, we will write a small hook that will check and automatically patch such places in the code.

Let’s create a hook class that will wait for the ev_ana_insn event. First, we need to make sure that this is indeed the sequence we are interested in, after which we go “upward” and try to find the address saved onto the stack. Then patch jmp eax to jmp short address.

class DelphiJmpEaxFixer(idaapi.IDP_Hooks):
    def lookup_push_insn(self, start: int, limit: int = 30) -> int | None:
        ...
 
    def ev_ana_insn(self, insn: idaapi.insn_t) -> bool:
        #
        b = bytes(idaapi.get_bytes(insn.ea - 1, 3))
        if idaapi.is_tail(idaapi.get_flags(insn.ea)):
            return True
        # pop eax | 58
        # jmp eax | ff e0
        # ensure all pop & jmp seq
        if b[0] != 0x58 or b[1] != 0xFF or b[2] != 0xE0:
            return False
 
        print(f"Got jmp short eax at {insn.ea:x}")
        pushed_address = self.lookup_push_insn(insn.ea)
        if pushed_address is None:
            return False
        delta = pushed_address - insn.ea
        if delta < 0 or delta > 128:
            return False
        print(f"{delta=}")
 
        asm_call = f"jmp short {delta}"
        assembled = idaapi.AssembleLine(insn.ea, 0, 0, True, asm_call)
        if assembled is None:
            return False
        return idaapi.patch_bytes(insn.ea, assembled)
Code language: Intel x86 Assembly (x86asm)


Let’s write the function for finding the address saved onto the stack, lookup_push_insn. In it, we will find the presumed upper boundary of block 2 and check that the block has a single reference. Additionally, we will limit the search range for optimization purposes.

def lookup_push_insn(self, start: int, limit: int = 30) -> int | None:
    ptr: int = start
    insn = idaapi.insn_t()
    jmp_ref = idaapi.BADADDR
    for _ in range(limit, 0, -1):
        prev_addr = idaapi.decode_prev_insn(insn, ptr)
        if prev_addr == idaapi.BADADDR:
            break
        ptr = prev_addr
        _refs = [xref for xref in idautils.CodeRefsTo(ptr, False)]
        # If we found refs it's likely an upper basic block address
        # it must be a single jmp ref
        if _refs:
            if len(_refs) != 1:
                return None
            jmp_ref = next(iter(_refs))
            break
 
    if jmp_ref == idaapi.BADADDR:
        return None
 
    ref_insn_sz = idaapi.decode_insn(insn, jmp_ref)
    if ref_insn_sz == 0 or insn.itype != idaapi.NN_jmp:
        return None
 
    addr = idaapi.decode_prev_insn(insn, ptr)
    if addr == idaapi.BADADDR or insn.itype != idaapi.NN_push:
        return None
 
    return insn.Op1.value
Code language: Python (python)


Let’s add the hook initialization when the script starts and load it into IDA. Let’s run the binary file analysis again. As a result, we get the fixed graph (screenshot 3).

The remaining single blocks are exception handler calls; in this case, they do not affect the program’s execution flow. It is worth remembering that while the hook is active, it will be automatically invoked even when marking up new code that was not previously marked up.

hook_instance = DelphiJmpEaxFixer()
hook_instance.hook()
Code language: plaintext (plaintext)


Happy reversing! 💫


#tip #reverse #idapython
@ptescalator

More from global_author

More from global_author

More in Reverse engineering