-
-
Save marcussacana/c0b108f4192e7193a1be62cfc0b8d4a3 to your computer and use it in GitHub Desktop.
REALbasic (Xojo) Reverse-Engineering Helper Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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. | |
# marcussacam, 2024-07-26 (Port for newer IDA) | |
# 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 = get_first_seg() | |
while ea != BADADDR: | |
if name == get_segm_name(ea): | |
return (ea, get_next_seg(ea)) | |
ea = get_next_seg(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(get_wide_byte(addr + 8 + i) == 0 for i in range(8)): | |
return False | |
if not rdata_end > get_wide_dword(addr) >= rdata_start: | |
return False | |
if not text_end > get_wide_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 += [( | |
get_strlit_contents(get_wide_dword(data_start), -1, STRTYPE_C), | |
get_wide_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(get_wide_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: | |
Makeget_wide_dword(pointer) | |
MakeNameEx(pointer, '_%s_length' % sect[1:], SN_NOWARN) | |
size = get_wide_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) | |
Makeget_wide_byte(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 = get_wide_byte(import_begin) | |
import_begin += 1 | |
import_size -= 1 | |
adjustment = 0 | |
# Framework runtime funtion refrence relocation | |
if ityp == 1: | |
reloc_type = get_wide_byte(import_begin) | |
adjustment += 1 | |
runtime_reloc_offset = get_wide_dword(import_begin + adjustment) | |
adjustment += 4 | |
runtime_name_len = get_wide_byte(import_begin + adjustment) | |
adjustment += 1 | |
runtime_name = get_strlit_contents( | |
import_begin + adjustment, runtime_name_len + 1, STRTYPE_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 = get_wide_dword(patch_addr) | |
if reloc_type == 1: | |
# Relative reloc | |
target_addr = (addr - (patch_addr + 4)) & 0xffffffff | |
else: | |
target_addr = addr | |
Patchget_wide_dword(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 = get_wide_dword(import_begin) | |
patch_addr = sections['.rb_text'][0] + data_reloc_offset | |
target_addr = (sections['.rb_data'][0] + get_wide_dword(patch_addr)) & 0xffffffff | |
Makeget_wide_dword(target_addr) | |
MakeNameEx(target_addr, 'var_%04d' % var_idx, SN_NOWARN) | |
Patchget_wide_dword(patch_addr, target_addr) | |
adjustment += 4 | |
var_idx += 1 | |
# Code relocation (user functions) | |
elif ityp == 3: | |
code_reloc_offset = get_wide_dword(import_begin) | |
patch_addr = sections['.rb_text'][0] + code_reloc_offset | |
Patchget_wide_dword(patch_addr, (sections['.rb_text'][0] + get_wide_dword(patch_addr)) & 0xffffffff) | |
adjustment += 4 | |
# External library relocations | |
elif ityp == 4: | |
proc_reloc_offset = get_wide_dword(import_begin) | |
adjustment += 4 | |
proc_name_len = get_wide_byte(import_begin + adjustment) | |
adjustment += 1 | |
proc_name = get_strlit_contents( | |
import_begin + adjustment, proc_name_len + 1, STRTYPE_C) | |
adjustment += proc_name_len + 1 | |
mod_name_len = get_wide_byte(import_begin + adjustment) | |
adjustment += 1 | |
mod_name = get_strlit_contents( | |
import_begin + adjustment, mod_name_len + 1, STRTYPE_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 = get_wide_dword(import_begin) | |
patch_addr = sections['.rb_text'][0] + const_reloc_offset | |
target_addr = (sections['.rb_data'][0] + get_wide_dword(patch_addr)) & 0xffffffff | |
Patchget_wide_dword(patch_addr, target_addr) | |
target_str_addr = target_addr + 20 | |
if get_wide_dword(target_addr + 4) == 0: | |
Patchget_wide_dword(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, get_wide_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