Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.