import contextlib import os import io import re import shutil import struct import sys import tempfile import subprocess import unittest from pathlib import Path from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import annotate_module import nid_names IMPORT_NAME = "sceKernelGetProcessTime" EXPORT_NAME = "sceKernelUsleep" SECOND_IMPORT_NAME = "sceKernelGetTscFrequency" TLS_NAME = "sceKernelGetCurrentCpu" GNU_HASH = 0x6FFFFEF5 CODE_ADDRESS = 0x1000 DATA_ADDRESS = 0x2000 SLOT = 0x2800 CALL_ADDRESS = 0x1006 EXPORT_ADDRESS = 0x100B def nid(name): return nid_names.compute_nid(name) def symbol_name(name): return nid(name) + "#A#A" def code_bytes(prefix=b""): stub = prefix + b"\xff\x25" + struct.pack(" " + IMPORT_NAME)) header = listing.index("; ===== export " + EXPORT_NAME) self.assertEqual(instruction(listing, "ret"), listing[header + 1]) self.assertEqual([], [path.name for path in self.root.iterdir() if path.suffix == ".bin"]) def test_unknown_nid_prints_the_nid(self): empty = self.root / "empty.csv" empty.write_text("", encoding="utf-8") self.run_tool(build_module(), "--db", str(empty)) self.assertEqual("%#x %s %s\n" % (SLOT, symbol_name(IMPORT_NAME), nid(IMPORT_NAME)), self.read(".imports.txt")) self.assertIn("; -> " + nid(IMPORT_NAME), self.read(".asm")) def test_no_names_skips_the_database(self): self.run_tool(build_module(), "--no-names") self.assertIn("; -> " + nid(IMPORT_NAME), self.read(".asm")) def test_mismatching_database_name_is_not_used(self): wrong = self.root / "wrong.csv" wrong.write_text("%s sceKernelOther\n" % nid(IMPORT_NAME), encoding="utf-8") self.run_tool(build_module(), "--db", str(wrong)) self.assertNotIn("sceKernelOther", self.read(".asm")) def test_gnu_hash_gives_the_symbol_count(self): symbols = [(IMPORT_NAME, 0, 0x12, 0), (SECOND_IMPORT_NAME, 0, 0x12, 0), (EXPORT_NAME, 1, 0x12, EXPORT_ADDRESS)] self.annotate(build_module(tags=(GNU_HASH, 5, 6, 10, 11, 23, 2, 20), symbols=symbols, kind="gnu")) self.assertIn(EXPORT_NAME, self.read(".exports.txt")) def test_glob_dat_slot_referenced_by_an_indirect_call(self): code = b"\xff\x15" + struct.pack(" " + IMPORT_NAME)) def test_eip_relative_stubs_and_calls_use_wrapped_got_addresses(self): for address in (CODE_ADDRESS, 0xFFFFF000, 0x100001000): for prefix in (b"\x67", b"\xf2\x67", b"\x3e\x67", b"\xf2\x3e\x67"): with self.subTest(address=address, prefix=prefix): length = len(prefix) + 6 code = prefix + b"\xff\x25" + struct.pack(" " + IMPORT_NAME)) def test_eip_references_do_not_name_the_unwrapped_address(self): address = 0x100001000 for opcode in (b"\xff\x15", b"\x48\x8b\x05"): with self.subTest(opcode=opcode): length = 1 + len(opcode) + 4 code = b"\x67" + opcode + struct.pack(" ", self.read(".asm")) def test_none_relocation_does_not_annotate_a_stub_or_call(self): module = with_relocation(build_module(), 0x2280, 0, SLOT, 1, 0) summary = self.annotate(module) self.assertIn("0 imports, 1 exports, 0 tls, 0 stubs", summary) self.assertEqual(self.read(".imports.txt"), "") listing = self.read(".asm") self.assertNotIn("; GOT ", listing) self.assertNotIn("; -> ", listing) self.assertIn("; ===== export " + EXPORT_NAME, listing) def test_none_relocation_keeps_the_real_import_annotation(self): symbols = DEFAULT_SYMBOLS + [(SECOND_IMPORT_NAME, 0, 0x12, 0)] module = build_module(symbols=symbols, relocations=((SLOT, 1), (SLOT, 3))) module = with_relocation(module, 0x2280, 1, SLOT, 3, 0) self.annotate(module) self.assertEqual(self.read(".imports.txt"), "%#x %s %s\n" % (SLOT, symbol_name(IMPORT_NAME), IMPORT_NAME)) listing = self.read(".asm").splitlines() self.assertTrue(instruction(listing, "jmp").endswith("; GOT " + IMPORT_NAME)) self.assertTrue(instruction(listing, "call").endswith("; -> " + IMPORT_NAME)) def test_tls_symbol_is_listed_but_not_an_address(self): symbols = DEFAULT_SYMBOLS + [(TLS_NAME, 1, 0x16, EXPORT_ADDRESS)] summary = self.annotate(build_module(symbols=symbols)) self.assertIn("1 exports, 1 tls", summary) self.assertIn("tls+%#x %s %s\n" % (EXPORT_ADDRESS, symbol_name(TLS_NAME), TLS_NAME), self.read(".exports.txt")) self.assertNotIn("export " + TLS_NAME, self.read(".asm")) def test_private_symbols_are_not_reported_as_exports(self): for binding in (0, 1, 2, 10): for visibility in (0, 1, 2, 3): for kind in (2, 6): with self.subTest(binding=binding, visibility=visibility, kind=kind): symbols = [(EXPORT_NAME, 1, binding << 4 | kind, EXPORT_ADDRESS)] module = bytearray(build_module(symbols=symbols, relocations=())) module[0x2040 + 24 + 5] = visibility summary = self.annotate(bytes(module)) visible = binding != 0 and visibility in (0, 3) expected = [] if visible: prefix = "tls+" if kind == 6 else "" expected.append("%s%#x %s %s" % (prefix, EXPORT_ADDRESS, symbol_name(EXPORT_NAME), EXPORT_NAME)) self.assertEqual(self.read(".exports.txt").splitlines(), expected) self.assertIn("%d exports, %d tls" % (visible and kind == 2, visible and kind == 6), summary) self.assertEqual("; ===== export " + EXPORT_NAME in self.read(".asm"), visible and kind == 2) def test_private_alias_does_not_hide_a_public_export(self): for hash_kind in ("sysv", "gnu"): for visibility in (1, 2): with self.subTest(hash_kind=hash_kind, visibility=visibility): symbols = DEFAULT_SYMBOLS + [(SECOND_IMPORT_NAME, 1, 0x12, EXPORT_ADDRESS)] tags = DEFAULT_TAGS if hash_kind == "sysv" else (GNU_HASH,) + DEFAULT_TAGS[1:] module = bytearray(build_module(symbols=symbols, tags=tags, kind=hash_kind)) module[0x2040 + 3 * 24 + 5] = visibility summary = self.annotate(bytes(module)) self.assertIn("1 imports, 1 exports, 0 tls, 1 stubs", summary) self.assertEqual(self.read(".exports.txt").splitlines(), ["%#x %s %s" % (EXPORT_ADDRESS, symbol_name(EXPORT_NAME), EXPORT_NAME)]) self.assertIn("; ===== export " + EXPORT_NAME, self.read(".asm")) self.assertNotIn("; ===== export " + SECOND_IMPORT_NAME, self.read(".asm")) self.assertIn("; -> " + IMPORT_NAME, self.read(".asm")) def test_export_at_address_zero_is_kept(self): symbols = [(IMPORT_NAME, 0, 0x12, 0), (EXPORT_NAME, 1, 0x12, 0)] self.annotate(build_module(symbols=symbols)) self.assertEqual("0x0 %s %s\n" % (symbol_name(EXPORT_NAME), EXPORT_NAME), self.read(".exports.txt")) def test_export_aliases_are_named_and_counted(self): symbols = DEFAULT_SYMBOLS + [(SECOND_IMPORT_NAME, 1, 0x12, EXPORT_ADDRESS)] for kind in ("sysv", "gnu"): with self.subTest(kind=kind): tags = DEFAULT_TAGS if kind == "sysv" else (GNU_HASH,) + DEFAULT_TAGS[1:] summary = self.annotate(build_module(symbols=symbols, tags=tags, kind=kind)) self.assertIn("1 imports, 2 exports, 0 tls, 1 stubs", summary) expected = ["%#x %s %s" % (EXPORT_ADDRESS, symbol_name(name), name) for name in (EXPORT_NAME, SECOND_IMPORT_NAME)] self.assertEqual(self.read(".exports.txt").splitlines(), expected) listing = self.read(".asm").splitlines() header = listing.index("; ===== export " + EXPORT_NAME) self.assertEqual(listing[header + 1], "; ===== export " + SECOND_IMPORT_NAME) self.assertEqual(listing[header + 2], instruction(listing, "ret")) def test_export_aliases_without_names_keep_each_nid(self): symbols = DEFAULT_SYMBOLS + [(SECOND_IMPORT_NAME, 1, 0x12, EXPORT_ADDRESS)] self.run_tool(build_module(symbols=symbols), "--no-names") for name in (EXPORT_NAME, SECOND_IMPORT_NAME): self.assertIn("%#x %s %s\n" % (EXPORT_ADDRESS, symbol_name(name), nid(name)), self.read(".exports.txt")) self.assertIn("; ===== export " + nid(name), self.read(".asm")) def test_tls_aliases_share_an_offset_without_becoming_code_labels(self): symbols = DEFAULT_SYMBOLS + [(TLS_NAME, 1, 0x16, EXPORT_ADDRESS), (SECOND_IMPORT_NAME, 1, 0x16, EXPORT_ADDRESS)] summary = self.annotate(build_module(symbols=symbols)) self.assertIn("1 exports, 2 tls", summary) expected = ["%#x %s %s" % (EXPORT_ADDRESS, symbol_name(EXPORT_NAME), EXPORT_NAME)] expected += ["tls+%#x %s %s" % (EXPORT_ADDRESS, symbol_name(name), name) for name in (TLS_NAME, SECOND_IMPORT_NAME)] self.assertEqual(self.read(".exports.txt").splitlines(), expected) for name in (TLS_NAME, SECOND_IMPORT_NAME): self.assertNotIn("export " + name, self.read(".asm")) def test_export_aliases_at_zero_are_sorted_before_other_addresses(self): symbols = DEFAULT_SYMBOLS + [(SECOND_IMPORT_NAME, 1, 0x12, 0), (TLS_NAME, 1, 0x12, 0)] summary = self.annotate(build_module(symbols=symbols)) self.assertIn("1 imports, 3 exports, 0 tls", summary) expected = ["0x0 %s %s" % (symbol_name(name), name) for name in (SECOND_IMPORT_NAME, TLS_NAME)] expected.append("%#x %s %s" % (EXPORT_ADDRESS, symbol_name(EXPORT_NAME), EXPORT_NAME)) self.assertEqual(self.read(".exports.txt").splitlines(), expected) def test_every_executable_segment_is_disassembled(self): self.annotate(build_module(extra_code=b"\xc3")) listing = self.read(".asm").splitlines() first, second = listing.index("; ===== segment 0x1000"), listing.index("; ===== segment 0x3000") self.assertLess(first, second) self.assertTrue(any(re.match(r"^\s*3000:\s+retq?\b", line) for line in listing[second:])) def test_empty_executable_segments_do_not_abort_other_disassembly(self): for memory_size in (0, 0x1000): for address in (0, 0x3000): with self.subTest(memory_size=memory_size, address=address): module = bytearray(build_module(extra_code=b"")) struct.pack_into(" " + IMPORT_NAME, listing) self.assertIn("; ===== export " + EXPORT_NAME, listing) def test_only_empty_executable_segments_fail_before_writing_reports(self): self.assert_fails(build_module(code=b"", extra_code=b"", symbols=[DEFAULT_SYMBOLS[0]]), "no executable PT_LOAD segment with file-backed data") for suffix in (".asm", ".imports.txt", ".exports.txt"): self.assertFalse((self.root / ("out" + suffix)).exists()) def test_nonempty_executable_segment_past_end_of_file_is_still_rejected(self): module = bytearray(build_module(extra_code=b"\xc3")) struct.pack_into("