def main() -> None: parser = argparse.ArgumentParser() parser.add_argument( "csv_path", help= "Path to a list of functions to identify (in the same format as the main function CSV)" ) parser.add_argument("candidates_path", help="Path to a list of candidates (names only)") args = parser.parse_args() csv_path = Path(args.csv_path) candidates_path = Path(args.candidates_path) candidates = read_candidates(candidates_path) new_matches: Dict[int, str] = dict() checker = util.checker.FunctionChecker() # Given a list L of functions to identify and a small list of candidates C, this tool will attempt to # automatically identify matches by checking each function in L against each function in C. # # This matching algorithm is quite naive (quadratic time complexity if both lists have about the same size) # but this should work well enough for short lists of candidates... for func in utils.get_functions(csv_path): if func.status != utils.FunctionStatus.NotDecompiled: continue match_name = "" for candidate_name, candidate in candidates.items(): if len(candidate.data) != func.size: continue if checker.check( util.elf.get_fn_from_base_elf(func.addr, func.size), candidate): match_name = candidate_name break if match_name: new_matches[func.addr] = match_name utils.print_note( f"found new match: {Fore.BLUE}{match_name}{Fore.RESET} ({func.addr | 0x71_00000000:#018x})" ) # This is no longer a candidate. del candidates[match_name] else: utils.warn( f"no match found for {Fore.BLUE}{func.name}{Fore.RESET} ({func.addr | 0x71_00000000:#018x})" ) # Output the modified function CSV. writer = csv.writer(sys.stdout, lineterminator="\n") for func in utils.get_functions(): if func.status == utils.FunctionStatus.NotDecompiled and func.addr in new_matches: func.raw_row[3] = new_matches[func.addr] writer.writerow(func.raw_row)
def find_function_info(name: str): for info in utils.get_functions(): if info.decomp_name == name or (find_wip and info.status == utils.FunctionStatus.Wip): return info for info in utils.get_functions(): if name in cxxfilt.demangle(info.decomp_name): return info return None
def load_data_for_project(self) -> None: self.decompiled_fns = { func.addr: func.decomp_name for func in utils.get_functions() if func.decomp_name } self.get_data_symtab().load_from_csv(utils.get_repo_root() / "data" / "data_symbols.csv")
def main() -> None: all_vtables = ai_common.get_vtables() names = ai_common.get_ai_vtable_names() not_decompiled = {func.addr for func in utils.get_functions() if func.status == utils.FunctionStatus.NotDecompiled} new_names: Dict[int, str] = dict() order = ai_common.topologically_sort_vtables(all_vtables, "AI") for vtable_addr in order: if vtable_addr in BaseClasses: continue class_name = names.get(vtable_addr) for i, fn_ea in enumerate(iterate_vtable(vtable_addr)): if idaapi.get_name(fn_ea) == "__cxa_pure_virtual": continue real_fn_ea = fn_ea & ~_ida_base if real_fn_ea not in new_names: if i < len(_vtable_fn_names): new_names[real_fn_ea] = format_fn_name(_vtable_fn_names[i], class_name) else: # Unknown member function. new_names[real_fn_ea] = f"uking::ai::{class_name}::m{i}" if real_fn_ea in not_decompiled: idaapi.set_name(fn_ea, new_names[real_fn_ea]) utils.add_decompiled_functions(dict(), new_names)
def main() -> None: parser = argparse.ArgumentParser( "Identifies matching functions by looking at function calls in matching functions" ) parser.add_argument("-f", "--fn", help="Functions to analyze", nargs="*") args = parser.parse_args() functions_to_analyze = set(args.fn) if args.fn else set() functions_by_addr: Dict[int, utils.FunctionInfo] = { fn.addr: fn for fn in utils.get_functions() } fn_checker = Checker() for fn in functions_by_addr.values(): if functions_to_analyze and fn.decomp_name not in functions_to_analyze: continue if fn.status != utils.FunctionStatus.Matching: continue base_fn = elf.get_fn_from_base_elf(fn.addr, fn.size) try: my_fn = elf.get_fn_from_my_elf(fn.decomp_name) except KeyError: utils.warn(f"could not find function {fn.decomp_name}") continue fn_checker.checking = fn.decomp_name fn_checker.check(base_fn, my_fn) if fn_checker.invalid_call_descriptions: for x in fn_checker.invalid_call_descriptions: utils.print_note(x) utils.fail("invalid calls detected") new_matches: Dict[int, str] = dict() calls = fn_checker.get_possible_calls().copy() for base_target, my_target in calls.items(): target_info = functions_by_addr.get(base_target) if target_info is None: continue if target_info.status != utils.FunctionStatus.NotDecompiled: continue base_fn = elf.get_fn_from_base_elf(target_info.addr, target_info.size) try: name = fn_checker.addr_to_symbol[my_target] my_fn = elf.get_fn_from_my_elf(name) except KeyError: continue if fn_checker.check(base_fn, my_fn): new_matches[base_target] = name utils.print_note( f"new match: {Fore.BLUE}{cxxfilt.demangle(name)}{Fore.RESET}") utils.add_decompiled_functions(new_matches)
def main() -> None: failed = False nonmatching_fns_with_dump = { p.stem: util.elf.Function(p.read_bytes(), 0) for p in (utils.get_repo_root() / "expected").glob("*.bin") } checker = util.checker.FunctionChecker(log_mismatch_cause=True) for func in utils.get_functions(): if not func.decomp_name: continue try: util.elf.get_fn_from_my_elf(func.decomp_name) except KeyError: utils.warn( f"couldn't find {utils.format_symbol_name_for_msg(func.decomp_name)}" ) continue if func.status == utils.FunctionStatus.Matching: if not check_function(checker, func.addr, func.size, func.decomp_name): utils.print_error( f"function {utils.format_symbol_name_for_msg(func.decomp_name)} is marked as matching but does not match" ) a1, a2, reason = checker.get_mismatch() if a1 != -1: sys.stderr.write( f" at {a1 | 0x7100000000:#x} : {Fore.CYAN}{reason}{Fore.RESET}\n" ) failed = True elif func.status == utils.FunctionStatus.Equivalent or func.status == utils.FunctionStatus.NonMatching: if check_function(checker, func.addr, func.size, func.decomp_name): utils.print_note( f"function {utils.format_symbol_name_for_msg(func.decomp_name)} is marked as non-matching but matches" ) fn_dump = nonmatching_fns_with_dump.get(func.decomp_name, None) if fn_dump is not None and not check_function( checker, func.addr, len(fn_dump), func.decomp_name, fn_dump): utils.print_error( f"function {utils.format_symbol_name_for_msg(func.decomp_name)} does not match expected output" ) failed = True if failed: sys.exit(1)
def dump_table(name: str) -> None: try: symbols = util.elf.build_addr_to_symbol_table(util.elf.my_symtab) decomp_symbols = { fn.decomp_name for fn in utils.get_functions() if fn.decomp_name } offset, size = util.elf.get_symbol_file_offset_and_size( util.elf.my_elf, util.elf.my_symtab, name) util.elf.my_elf.stream.seek(offset) vtable_bytes = util.elf.my_elf.stream.read(size) if not vtable_bytes: utils.fail( "empty vtable; has the key function been implemented? (https://lld.llvm.org/missingkeyfunction.html)" ) print( f"{Fore.WHITE}{Style.BRIGHT}{cxxfilt.demangle(name)}{Style.RESET_ALL}" ) print(f"{Fore.YELLOW}{Style.BRIGHT}vtable @ 0x0{Style.RESET_ALL}") assert size % 8 == 0 for i in range(size // 8): word: int = struct.unpack_from("<Q", vtable_bytes, 8 * i)[0] name = symbols.get(word, None) if word == 0: pass elif name is not None: demangled_name: str = cxxfilt.demangle(name) color = Fore.GREEN if name in decomp_symbols else Fore.BLUE print(f"{color}{bold(demangled_name)}{Style.RESET_ALL}") print(f" {name}") elif word & (1 << 63): offset = -struct.unpack_from("<q", vtable_bytes, 8 * i)[0] print() print( f"{Fore.YELLOW}{Style.BRIGHT}vtable @ {offset:#x}{Style.RESET_ALL}" ) else: print(f"{Fore.RED}unknown data: {word:016x}{Style.RESET_ALL}") except KeyError: utils.fail("could not find symbol")
from colorama import Fore, Style import diff_settings import subprocess from util import utils parser = argparse.ArgumentParser(description="Prints build/uking.elf symbols") parser.add_argument("--print-undefined", "-u", help="Print symbols that are undefined", action="store_true") parser.add_argument("--print-c2-d2", "-c", help="Print C2/D2 (base object constructor/destructor) symbols", action="store_true") parser.add_argument("--hide-unknown", "-H", help="Hide symbols that are not present in the original game", action="store_true") parser.add_argument("--all", "-a", action="store_true") args = parser.parse_args() listed_decomp_symbols = {info.decomp_name for info in utils.get_functions()} original_symbols = {info.name for info in utils.get_functions()} config: dict = dict() diff_settings.apply(config, {}) myimg: str = config["myimg"] entries = [x.strip().split() for x in subprocess.check_output(["nm", "-n", myimg], universal_newlines=True).split("\n")] for entry in entries: if len(entry) == 3: addr = int(entry[0], 16) symbol_type: str = entry[1] name = entry[2] if (symbol_type == "t" or symbol_type == "T" or symbol_type == "W") and (
code_size: tp.DefaultDict[FunctionStatus, int] = defaultdict(int) counts: tp.DefaultDict[FunctionStatus, int] = defaultdict(int) nonmatching_fns_with_dump = { p.stem for p in (Path(__file__).parent.parent / "expected").glob("*.bin") } def should_hide_nonmatching(name: str) -> bool: if not args.hide_nonmatchings_with_dumps: return False return name in nonmatching_fns_with_dump for info in utils.get_functions(): code_size_total += info.size num_total += 1 if not info.decomp_name: continue counts[info.status] += 1 code_size[info.status] += info.size if not args.csv: if info.status == FunctionStatus.NonMatching: if args.print_nm and not should_hide_nonmatching(info.decomp_name): print( f"{Fore.RED}NM{Fore.RESET} {utils.format_symbol_name(info.decomp_name)}" )
def main() -> None: parser = argparse.ArgumentParser(description="Identifies matching AI class functions.") parser.add_argument("aidef") parser.add_argument("--type", choices=["Action", "AI", "Behavior", "Query"], required=True) args = parser.parse_args() type_: str = args.type new_matches: Dict[int, str] = dict() checker = util.checker.FunctionChecker() functions: Dict[str, utils.FunctionInfo] = {fn.name: fn for fn in utils.get_functions()} aidef = oead.byml.from_text(Path(args.aidef).read_text(encoding="utf-8")) def get_query_pairs(orig_name, name): prefix = f"AI_Query_{orig_name}::" return [ (f"{prefix}ctor", f"_ZN5uking5query{len(name)}{name}C1ERKN4ksys3act2ai5Query7InitArgE"), (f"{prefix}dtor", f"_ZN5uking5query{len(name)}{name}D1Ev"), (f"{prefix}dtorDelete", f"_ZN5uking5query{len(name)}{name}D0Ev"), (f"{prefix}m10", f"_ZN5uking5query{len(name)}{name}10loadParamsERKN4evfl8QueryArgE"), (f"{prefix}loadParams", f"_ZN5uking5query{len(name)}{name}10loadParamsEv"), (f"{prefix}rtti1", f"_ZNK5uking5query{len(name)}{name}27checkDerivedRuntimeTypeInfoEPKN4sead15RuntimeTypeInfo9InterfaceE"), (f"{prefix}rtti2", f"_ZNK5uking5query{len(name)}{name}18getRuntimeTypeInfoEv"), (f"AI_F_Query_{orig_name}", f"_ZN4ksys3act2ai12QueryFactory4makeIN5uking5query{len(name)}{name}EEEPNS1_5QueryERKNS7_7InitArgEPN4sead4HeapE"), ] def get_action_pairs(orig_name, name): pairs = [] def add_pair(x): pairs.append((x, x)) pairs.append( (f"AI_Action_{orig_name}::ctor", f"_ZN5uking6action{len(name)}{name}C1ERKN4ksys3act2ai10ActionBase7InitArgE")) pairs.append( (f"AI_Action{orig_name}::ctor", f"_ZN5uking6action{len(name)}{name}C1ERKN4ksys3act2ai10ActionBase7InitArgE")) pairs.append((f"AI_F_Action_{orig_name}", f"_ZN4ksys3act2ai13ActionFactory4makeIN5uking6action{len(name)}{name}EEEPNS1_6ActionERKNS1_10ActionBase7InitArgEPN4sead4HeapE")) add_pair(f"_ZN5uking6action{len(name)}{name}D1Ev") add_pair(f"_ZN5uking6action{len(name)}{name}D0Ev") add_pair(f"_ZN5uking6action{len(name)}{name}11loadParams_Ev") add_pair(f"_ZN5uking6action{len(name)}{name}5init_EPN4sead4HeapE") add_pair(f"_ZN5uking6action{len(name)}{name}6enter_EPN4ksys3act2ai15InlineParamPackE") add_pair(f"_ZN5uking6action{len(name)}{name}6leave_Ev") add_pair(f"_ZN5uking6action{len(name)}{name}5calc_Ev") add_pair( f"_ZNK5uking6action{len(name)}{name}27checkDerivedRuntimeTypeInfoEPKN4sead15RuntimeTypeInfo9InterfaceE") add_pair(f"_ZNK5uking6action{len(name)}{name}18getRuntimeTypeInfoEv") return pairs def get_ai_pairs(orig_name, name): pairs = [] def add_pair(x): pairs.append((x, x)) pairs.append( (f"AI_AI_{orig_name}::ctor", f"_ZN5uking2ai{len(name)}{name}C1ERKN4ksys3act2ai10ActionBase7InitArgE")) pairs.append( (f"AI_AI{orig_name}::ctor", f"_ZN5uking2ai{len(name)}{name}C1ERKN4ksys3act2ai10ActionBase7InitArgE")) pairs.append((f"AI_F_AI_{orig_name}", f"_ZN4ksys3act2ai9AiFactory4makeIN5uking2ai{len(name)}{name}EEEPNS1_2AiERKNS1_10ActionBase7InitArgEPN4sead4HeapE")) add_pair(f"_ZN5uking2ai{len(name)}{name}D1Ev") add_pair(f"_ZN5uking2ai{len(name)}{name}D0Ev") add_pair(f"_ZN5uking2ai{len(name)}{name}11loadParams_Ev") add_pair(f"_ZN5uking2ai{len(name)}{name}5init_EPN4sead4HeapE") add_pair(f"_ZN5uking2ai{len(name)}{name}6enter_EPN4ksys3act2ai15InlineParamPackE") add_pair(f"_ZN5uking2ai{len(name)}{name}6leave_Ev") add_pair(f"_ZN5uking2ai{len(name)}{name}5calc_Ev") add_pair(f"_ZNK5uking2ai{len(name)}{name}27checkDerivedRuntimeTypeInfoEPKN4sead15RuntimeTypeInfo9InterfaceE") add_pair(f"_ZNK5uking2ai{len(name)}{name}18getRuntimeTypeInfoEv") return pairs if type_ == "Action": action_vtable_names = ai_common.get_action_vtable_names() identify(functions, checker, new_matches, action_vtable_names.values(), get_action_pairs) if type_ == "AI": ai_vtable_names = ai_common.get_ai_vtable_names() identify(functions, checker, new_matches, ai_vtable_names.values(), get_ai_pairs) elif type_ == "Query": identify(functions, checker, new_matches, aidef["Querys"].keys(), get_query_pairs) utils.add_decompiled_functions(new_matches)
def main() -> None: new_matches: Dict[int, str] = dict() functions_by_addr: Dict[int, utils.FunctionInfo] = { fn.addr: fn for fn in utils.get_functions() } md = cs.Cs(cs.CS_ARCH_ARM64, cs.CS_MODE_ARM) md.detail = True decomp_addr_to_symbol = elf.build_addr_to_symbol_table(elf.my_symtab) decomp_glob_data_table = elf.build_glob_data_table(elf.my_elf) processed: Set[int] = set() for fn in functions_by_addr.values(): if fn.status != utils.FunctionStatus.Matching: continue if fn.size != 0x5C or ( not fn.decomp_name.endswith("8getRuntimeTypeInfoEv") and not fn.name.endswith("rtti2")): continue base_fn = elf.get_fn_from_base_elf(fn.addr, fn.size) try: my_fn = elf.get_fn_from_my_elf(fn.decomp_name) except KeyError: utils.warn(f"could not find function {fn.decomp_name}") continue assert len(base_fn.data) == len(my_fn.data) vtable_ptr1 = 0 vtable_ptr2 = 0 for j, (i1, i2) in enumerate( zip(md.disasm(base_fn.data, base_fn.addr), md.disasm(my_fn.data, my_fn.addr))): assert i1.mnemonic == i2.mnemonic if j == 10: assert i1.mnemonic == "adrp" assert i1.operands[0].reg == i2.operands[0].reg vtable_ptr1 = i1.operands[1].imm vtable_ptr2 = i2.operands[1].imm elif j == 11: assert i1.mnemonic == "ldr" assert i1.operands[0].reg == i2.operands[0].reg assert i1.operands[1].value.mem.base == i2.operands[ 1].value.mem.base vtable_ptr1 += i1.operands[1].value.mem.disp vtable_ptr2 += i2.operands[1].value.mem.disp break assert vtable_ptr1 != 0 and vtable_ptr2 != 0 if vtable_ptr1 in processed: continue processed.add(vtable_ptr1) ptr1, = struct.unpack("<Q", elf.read_from_elf(elf.base_elf, vtable_ptr1, 8)) ptr2 = decomp_glob_data_table[vtable_ptr2] vtable1 = elf.get_vtable_fns_from_base_elf(ptr1 + 0x10, num_entries=1) vtable2 = elf.unpack_vtable_fns(elf.read_from_elf(elf.my_elf, addr=ptr2 + 0x10, size=8), num_entries=1) if functions_by_addr[ vtable1[0]].status == utils.FunctionStatus.Matching: continue decomp_derive_fn_addr = vtable2[0] if decomp_derive_fn_addr == 0: decomp_derive_fn_addr = decomp_glob_data_table.get(ptr2 + 0x10, 0) if decomp_derive_fn_addr == 0: raise RuntimeError( f"Derive virtual function pointer is null " f"(fn: {fn.decomp_name}, decomp vtable at {ptr2:#x})") name = decomp_addr_to_symbol[decomp_derive_fn_addr] new_matches[vtable1[0]] = name utils.print_note( f"new match: {Fore.BLUE}{cxxfilt.demangle(name)}{Fore.RESET} (from {fn.decomp_name})" ) # overwrite the original names because they are likely to be incorrect utils.add_decompiled_functions(new_matches, new_orig_names=new_matches)