Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion tools/import_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,18 @@ def file_offset(address, size, what):
bucket_data = take(data, bucket_offset, buckets * 4, path, "GNU hash buckets")
chain_offset = bucket_offset + buckets * 4
maximum = first_symbol - 1
visited_chain_entries = set()
for index in struct.unpack(f"<{buckets}I", bucket_data) if buckets else ():
if index == 0:
continue
if index < first_symbol:
raise AuditError(f"{path}: GNU hash bucket index {index} precedes symbol offset {first_symbol}")
symbol_index = index
while True:
while symbol_index not in visited_chain_entries:
chain_address = dynamic[0x6ffffef5] + (chain_offset - hash_offset) + (symbol_index - first_symbol) * 4
chain_file_offset = file_offset(chain_address, 4, "GNU hash chain")
chain_value = unpack("<I", data, chain_file_offset, path, "GNU hash chain entry")[0]
visited_chain_entries.add(symbol_index)
maximum = max(maximum, symbol_index)
if chain_value & 1:
break
Expand Down
64 changes: 61 additions & 3 deletions tools/tests/test_import_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def build_elf(symbols, needed=()):
return bytes(image)


def build_sectionless_elf(symbols, needed=(), gnu_hash=False):
def build_sectionless_elf(symbols, needed=(), gnu_hash=False, gnu_buckets=(1,)):
strings = b"\0"
entries = [struct.pack("<IBBHQQ", 0, 0, 0, 0, 0, 0)]
for name, index, info in symbols:
Expand All @@ -73,8 +73,9 @@ def build_sectionless_elf(symbols, needed=(), gnu_hash=False):
symbol_table = b"".join(entries)
hash_offset = (symbols_offset + len(symbol_table) + 3) & ~3
if gnu_hash:
hash_table = struct.pack("<IIII", 1, 1, 1, 0) + bytes(8)
hash_table += struct.pack("<I", 1) + b"".join(struct.pack("<I", 1 if index == len(entries) - 1 else 0) for index in range(1, len(entries)))
hash_table = struct.pack("<IIII", len(gnu_buckets), 1, 1, 0) + bytes(8)
hash_table += b"".join(struct.pack("<I", index) for index in gnu_buckets)
hash_table += b"".join(struct.pack("<I", 1 if index == len(entries) - 1 else 0) for index in range(1, len(entries)))
hash_tag = 0x6ffffef5
else:
hash_table = struct.pack("<II", 1, len(entries)) + struct.pack("<I", 0) + bytes(4 * len(entries))
Expand All @@ -100,6 +101,21 @@ def build_sectionless_elf(symbols, needed=(), gnu_hash=False):
return bytes(image)


def gnu_hash_chain_offset(image):
header = struct.unpack_from("<16sHHIQQQIHHHHHH", image)
segments = [struct.unpack_from("<IIQQQQQQ", image, header[5] + index * header[9])
for index in range(header[10])]
dynamic = next(segment for segment in segments if segment[0] == 2)
tags = dict(struct.unpack_from("<qQ", image, position)
for position in range(dynamic[2], dynamic[2] + dynamic[5], 16))
address = tags[0x6ffffef5]
load = next(segment for segment in segments
if segment[0] == 1 and segment[3] <= address < segment[3] + segment[5])
hash_offset = address - load[3] + load[2]
bucket_count, _, bloom_count, _ = struct.unpack_from("<IIII", image, hash_offset)
return hash_offset + struct.calcsize("<IIII") + bloom_count * struct.calcsize("<Q") + bucket_count * struct.calcsize("<I")


def exporting(*names):
return build_elf([(name, 1, GLOBAL_FUNCTION) for name in names])

Expand Down Expand Up @@ -1040,6 +1056,48 @@ def test_sectionless_elf_gnu_hash(self):
self.assertEqual(import_audit.elf_program_dynamic_symbols(Path("module.prx"), image),
({"AAAAAAAAAAA": False}, {"BBBBBBBBBBB"}, {"libSceA.prx"}))

def test_sectionless_elf_gnu_hash_reads_each_chain_entry_once(self):
symbols = [(f"symbol{index}", int(index != 0), GLOBAL_FUNCTION) for index in range(32)]
expected = ({"symbol0": False}, {name for name, section, _ in symbols if section}, {"libSceA.prx"})
for buckets, terminator in (((1,) * 32, None), (tuple(range(32, 0, -1)), None), ((17, 1, 0, 17), 16)):
with self.subTest(buckets=buckets, terminator=terminator):
image = bytearray(build_sectionless_elf(symbols, ["libSceA.prx"], gnu_hash=True, gnu_buckets=buckets))
chain_offset = gnu_hash_chain_offset(image)
if terminator is not None:
struct.pack_into("<I", image, chain_offset + (terminator - 1) * 4, 1)
with patch.object(import_audit, "unpack", wraps=import_audit.unpack) as unpack:
self.assertEqual(import_audit.elf_program_dynamic_symbols(Path("module.prx"), image), expected)
chain_reads = [call.args[2] for call in unpack.call_args_list
if call.args[0] == "<I" and chain_offset <= call.args[2] < chain_offset + len(symbols) * 4]
self.assertEqual(len(chain_reads), len(symbols))
self.assertEqual(set(chain_reads), set(range(chain_offset, chain_offset + len(symbols) * 4, 4)))

def test_sectionless_elf_gnu_hash_empty_buckets(self):
for buckets in ((), (0,), (0, 0, 0)):
with self.subTest(buckets=buckets):
image = build_sectionless_elf([], gnu_hash=True, gnu_buckets=buckets)
chain_offset = gnu_hash_chain_offset(image)
with patch.object(import_audit, "unpack", wraps=import_audit.unpack) as unpack:
self.assertEqual(import_audit.elf_program_dynamic_symbols(Path("module.prx"), image), ({}, set(), set()))
self.assertFalse(any(call.args[0] == "<I" and chain_offset <= call.args[2] < len(image)
for call in unpack.call_args_list))

def test_sectionless_elf_gnu_hash_requires_file_backed_chains(self):
symbols = [(f"symbol{index}", 1, GLOBAL_FUNCTION) for index in range(4)]
for buckets in ((1, 1), (3, 1, 2)):
for terminated in (False, True):
image = bytearray(build_sectionless_elf(symbols, gnu_hash=True, gnu_buckets=buckets))
chain_offset = gnu_hash_chain_offset(image)
chain_end = chain_offset + len(symbols) * 4
if not terminated:
struct.pack_into("<I", image, chain_end - 4, 0)
file_size = chain_end - int(terminated)
struct.pack_into("<QQ", image, 64 + 32, file_size, file_size)
for reader in (import_audit.elf_exports, import_audit.elf_imports):
with self.subTest(buckets=buckets, terminated=terminated, reader=reader.__name__):
with self.assertRaisesRegex(import_audit.AuditError, "GNU hash chain.*outside file-backed"):
reader(Path("unmapped-chain.prx"), image)

def test_sectionless_hash_metadata_must_be_file_backed(self):
for gnu_hash in (False, True):
image = bytearray(build_sectionless_elf([("AAAAAAAAAAA", 1, GLOBAL_FUNCTION)], gnu_hash=gnu_hash))
Expand Down
Loading