Skip to content

Instantly share code, notes, and snippets.

@iscgar
Created September 26, 2018 19:32
Show Gist options
  • Save iscgar/b326a77821ead2ce05a195a88e18264d to your computer and use it in GitHub Desktop.
Save iscgar/b326a77821ead2ce05a195a88e18264d to your computer and use it in GitHub Desktop.
REALbasic (Xojo) Reverse-Engineering Helper Script
# IDA REALbasic reverse engineering helper script
# It seems that every version of REALbasic (Xojo) the loader
# has a different behaviour (especially with regard to parsing
# the imports table), so I'm documenting here that this script
# was created based on a 2009r5 executable.
# iscgar, 2018-09-26
#
# Based on the REALbasic OVERLAY resolver
# XpoZed @ http://nullsecurity.org, 2017-07-16
# http://www.nullsecurity.org/article/reverse_engineering_realbasic_applications
import idaapi, idc
def get_segment_info(name):
ea = FirstSeg()
while ea != BADADDR:
if name == SegName(ea):
return (ea, NextSeg(ea))
ea = NextSeg(ea)
return None
def get_runtime_table():
text_start, text_end = get_segment_info(".text")
rdata_start, rdata_end = get_segment_info(".rdata")
data_start, data_end = get_segment_info(".data")
# A reloc entry is 16 bytes long:
# - 4 bytes framework runtime funtion name (C string)
# - 4 bytes framework runtime funtion address
# - 8 bytes padding (support for 64-bit reloc entries?)
def is_reloc_entry(addr):
if not all(Byte(addr + 8 + i) == 0 for i in range(8)):
return False
if not rdata_end > Dword(addr) >= rdata_start:
return False
if not text_end > Dword(addr + 4) >= text_start:
return False
return True
# Iterate the data section to find the beginning of the
# framework reloc table
while data_start < data_end:
if is_reloc_entry(data_start):
break
data_start += 16
table = []
# Parse reloc entries until we hit a row that is not
# a valid reloc entry
while data_start < data_end:
if not is_reloc_entry(data_start):
break
table += [(
GetString(Dword(data_start), -1, ASCSTR_C),
Dword(data_start + 4)
)]
data_start += 16
return table
def parse_overlay(table):
# Load overlay information
try:
overlay_start, overlay_end = get_segment_info("OVERLAY")
except TypeError:
print("Failed to locate the OVERLAY segment. Did you load it?")
return
# Make sure we have space for the overlay magic
if overlay_end - overlay_start < 6:
print('OVERLAY segment too small')
return
# Check for overlay magic (the loader also makes sure that the overlay
# is at the end of the file. We don't really need this).
if b''.join(chr(Byte(overlay_start + i)) for i in range(6)) != b'112358':
print('OVERLAY must begin with `112358`!')
return
overlay_start += 6
# These are the section that are held in the overlay.
# symbols, rsrc and options are optional and can be empty.
sections_info = [
(".rb_text", True),
(".rb_data", True),
(".rb_import", True),
(".rb_symbols", False),
(".rb_rsrc", False),
(".rb_options", False)
]
sections = {}
pointer = overlay_start
# Iterate existing sections
for sect, required in sections_info:
MakeDword(pointer)
MakeNameEx(pointer, '_%s_length' % sect[1:], SN_NOWARN)
size = Dword(pointer)
pointer += 4
print("Section `%s` @ 0x%08x: 0x%08x bytes" % (
sect, pointer - overlay_start, size))
if required and size <= 0:
print("Required section has no data")
elif pointer + size > overlay_end:
print("Section size overflows the OVERLAY section")
else:
sections[sect] = (pointer, size)
if size > 0:
MakeUnknown(pointer, size, DOUNK_SIMPLE)
MakeByte(pointer)
MakeArray(pointer, size)
MakeNameEx(pointer, '_%s' % sect[1:], SN_NOWARN)
pointer += size
continue
return
# Create the REALstring (REALtext?) type
rbstring_rec = AddStrucEx(-1, "REALstring", 0)
AddStrucMember(rbstring_rec, "refcount", 0x00, 0x20000400, -1, 4)
AddStrucMember(rbstring_rec, "data", 0x04, 0x20500400, 0, 4)
AddStrucMember(rbstring_rec, "alloc_size", 0x08, 0x20000400, -1, 4)
AddStrucMember(rbstring_rec, "data_len", 0x0C, 0x20000400, -1, 4)
AddStrucMember(rbstring_rec, "encoding", 0x10, 0x20000400, -1, 4)
AddStrucMember(rbstring_rec, "raw", 0x14, 0x20000400, -1, 1)
str_idx = var_idx = 0
import_begin, import_size = sections['.rb_import']
while import_size > 0:
ityp = Byte(import_begin)
import_begin += 1
import_size -= 1
adjustment = 0
# Framework runtime funtion refrence relocation
if ityp == 1:
reloc_type = Byte(import_begin)
adjustment += 1
runtime_reloc_offset = Dword(import_begin + adjustment)
adjustment += 4
runtime_name_len = Byte(import_begin + adjustment)
adjustment += 1
runtime_name = GetString(
import_begin + adjustment, runtime_name_len + 1, ASCSTR_C)
adjustment += runtime_name_len + 1
for name, addr in table:
if name == runtime_name:
# For some reason the loader trampolines here until
# it hits a relocation with no offset
while True:
patch_addr = sections['.rb_text'][0] + runtime_reloc_offset
reloc_next = Dword(patch_addr)
if reloc_type == 1:
# Relative reloc
target_addr = (addr - (patch_addr + 4)) & 0xffffffff
else:
target_addr = addr
PatchDword(patch_addr, target_addr)
if reloc_next == 0:
break
runtime_reloc_offset = reloc_next
break
else:
# There is runtime registration of framework funtions, so
# we might fail to resolve the relocation. Nothing we can
# do about it :(
print('Runtime relocation of `%s` failed' % runtime_name)
# Data relocation (global variables)
elif ityp == 2:
data_reloc_offset = Dword(import_begin)
patch_addr = sections['.rb_text'][0] + data_reloc_offset
target_addr = (sections['.rb_data'][0] + Dword(patch_addr)) & 0xffffffff
MakeDword(target_addr)
MakeNameEx(target_addr, 'var_%04d' % var_idx, SN_NOWARN)
PatchDword(patch_addr, target_addr)
adjustment += 4
var_idx += 1
# Code relocation (user functions)
elif ityp == 3:
code_reloc_offset = Dword(import_begin)
patch_addr = sections['.rb_text'][0] + code_reloc_offset
PatchDword(patch_addr, (sections['.rb_text'][0] + Dword(patch_addr)) & 0xffffffff)
adjustment += 4
# External library relocations
elif ityp == 4:
proc_reloc_offset = Dword(import_begin)
adjustment += 4
proc_name_len = Byte(import_begin + adjustment)
adjustment += 1
proc_name = GetString(
import_begin + adjustment, proc_name_len + 1, ASCSTR_C)
adjustment += proc_name_len + 1
mod_name_len = Byte(import_begin + adjustment)
adjustment += 1
mod_name = GetString(
import_begin + adjustment, mod_name_len + 1, ASCSTR_C)
adjustment += mod_name_len + 1
# Even if added an external import to IDB it wouldn't be enough
# because the loader does a relative fixup and trampolines here
# here too until it hits a relocation with no offset
print('%08x: External import %s:%s' % (
proc_reloc_offset, mod_name, proc_name))
# Constant import (strings)
elif ityp == 5:
const_reloc_offset = Dword(import_begin)
patch_addr = sections['.rb_text'][0] + const_reloc_offset
target_addr = (sections['.rb_data'][0] + Dword(patch_addr)) & 0xffffffff
PatchDword(patch_addr, target_addr)
target_str_addr = target_addr + 20
if Dword(target_addr + 4) == 0:
PatchDword(target_addr + 4, target_str_addr)
MakeStructEx(target_addr, -1, 'REALstring')
MakeNameEx(target_addr, 'rbstr_%04d' % str_idx, SN_NOWARN)
idaapi.make_ascii_string(
target_str_addr, Byte(target_str_addr) + 1, ASCSTR_PASCAL)
MakeNameEx(target_str_addr, 'str_%04d' % str_idx, SN_NOWARN)
adjustment += 4
str_idx += 1
# Shouldn't happen
else:
print('Unknown import type %d' % ityp)
return
import_begin += adjustment
import_size -= adjustment
MakeUnknown(sections['.rb_text'][0], sections['.rb_text'][1], DOUNK_SIMPLE)
MakeCode(sections['.rb_text'][0])
# The user entry funtion is located at the very beginning of the
# overlay code section
MakeFunction(sections['.rb_text'][0], BADADDR)
MakeNameEx(sections['.rb_text'][0], "_main_overlay", SN_NOWARN)
def main():
# Locate the framework runtime reloc table
runtime_table = get_runtime_table()
if not runtime_table:
print("Unable to locate Framework API table")
return
# Resolve Framework procedures
for name, addr in runtime_table:
MakeNameEx(addr, name, SN_NOWARN)
print("Procedures resolved: %d" % len(runtime_table))
# Parse the OVERLAY
parse_overlay(runtime_table)
print("Done.")
if __name__ == "__main__":
main()
@karpiyon
Copy link

Hi,
Any chnage to make this compatible with IDA 8/9 and python 3?

@iscgar
Copy link
Author

iscgar commented Oct 24, 2024

Sorry, this was a one-off, and I don't have access anymore to the 2009r5-based executable that I used to create this script. Also, as the header comment notes, it seems that almost every new release of REALbasic changes things, so the code in this script can only be used as a reference anyway and should not be expected to work with any other version.

You should try to make the conversion yourself, and fix any incompatibilities based on the info in the excellent blog post by XpoZed (referenced in the header comment). Then post the result and specify the exact version that your script works with, for the benefit of others who might need it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment