Skip to content

Instantly share code, notes, and snippets.

@Andoryuuta
Last active February 11, 2025 02:27
Show Gist options
  • Save Andoryuuta/2b60053ffa15d50312821a46b93598ee to your computer and use it in GitHub Desktop.
Save Andoryuuta/2b60053ffa15d50312821a46b93598ee to your computer and use it in GitHub Desktop.

Getting RE Engine file extension/versions

There is a single function in RE engine that is called with the file extension (UTF16 string), and version (int): image

Finding the function

  1. Dump game with x64dbg+scylla, open with IDA Pro/Ghidra/Binja/Radare/whatever and let the auto analysis finish.

  2. Search for a common file type string (e.g. motlist) as UTF16:

    image

  3. Xref the string (it should only have one reference)

    image

  4. Grab the address of the registration function: image

Dumping with emulation

We use emulation here to avoid manually parsing all of the different ways the data is loaded into the registers before the call.

It works by finding all the code xrefs to the registration function, walking back a few instructions from the call (to handle different ordering by the compiler), emulating, then reading the resulting register values.

The following script is IDA-specific, but you can easily change the Disassembler class to work with Ghidrathon, r2pipe, binja api, etc, for your specific disassembler.

from typing import List
from unicorn import *
from unicorn.x86_const import *

class Disassembler:
    import idc
    import ida_bytes
    import ida_xref
    
    @staticmethod
    def get_code_xrefs(address: int) -> List[int]:
        xrefs = []
        xref = Disassembler.ida_xref.get_first_cref_to(address)
        while xref != Disassembler.idc.BADADDR:
            xrefs.append(xref)
            xref = Disassembler.ida_xref.get_next_cref_to(address, xref)
        return xrefs

    @staticmethod
    def prev_head(address: int) -> int:
        return Disassembler.idc.prev_head(address)

    @staticmethod
    def get_bytes(address: int, size: int) -> bytes:
        return Disassembler.ida_bytes.get_bytes(address, size)

def hook_mem_invalid(uc, access, address, size, value, user_data):
    PAGE_SIZE=4096

    try:
        base = address & ~(PAGE_SIZE - 1)
        uc.mem_map(base, PAGE_SIZE)
        
        # Try to read unmapped code from disassembler, otherwise zero
        data = Disassembler.get_bytes(base, PAGE_SIZE) or b'\x00' * PAGE_SIZE
        uc.mem_write(base, data)
        return True
    except Exception as e:
        print(f'Failed to map {base:X}: {e}')
        return False

if __name__ == '__main__':
    register_file_extension_version_ea = 0x140208C90

    emu = Uc(UC_ARCH_X86, UC_MODE_64)
    emu.hook_add(UC_HOOK_MEM_UNMAPPED, hook_mem_invalid)
    
    for xref in Disassembler.get_code_xrefs(register_file_extension_version_ea):
        # Start a few instructions prior to the call
        end_ea = xref
        start_ea = end_ea
        for i in range(5):
            start_ea = Disassembler.prev_head(start_ea)	
        
        try:    
            emu.emu_start(start_ea, end_ea)

            # read the call arguments from the registers
            file_version = emu.reg_read(UC_X86_REG_RDX)
            ext_string_ptr = emu.reg_read(UC_X86_REG_RCX)
            
            # read the string data (statically, from the disassembler)
            ext_string = Disassembler.get_bytes(ext_string_ptr, 32).decode('utf16').split('\0')[0]
            
            print(f'{ext_string}.{file_version}')
        except UcError as e:
            print('Error during emulation:', e)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment