#!/usr/bin/env python3 """unwrap_self -- turn a decrypted SELF or fSELF container into a clean ELF. The relinker reads a plain ELF executable and skips SELF containers with a warning, so an eboot.bin or a *.prx taken from a title has to be unwrapped first. Two things differ inside a container: - The inner ELF program headers carry stale offsets. A SELF inserts its own metadata blocks between the segments, so the segment data sits at the offsets listed in the SELF segment-record table at file offset 0x40, records of 0x20 bytes holding flags, data offset, size and the size again. Segments the table does not cover are placed by the vaddr to file mapping of the PT_LOAD that contains them; segments without a vaddr carry the shift of the last mapped one. - The segments are packed without honouring p_align, while ELF requires p_offset % p_align == p_vaddr % p_align. Every region is copied to an offset that satisfies it, which the relinker enforces on guest modules. Only decrypted containers are handled. A container whose load segments all look encrypted is refused instead of copied: the entropy of each load segment is measured and a run of segments near eight bits per byte is reported. usage: unwrap_self.py [--verbose] """ import argparse import math import struct import sys from collections import Counter from pathlib import Path SELF_MAGIC = 0x1D3D154F SELF_MAGIC_KERNEL = 0xEEF51454 RECORD_OFFSET = 0x40 RECORD_SIZE = 0x20 FIRST_REGION = 0x400 PT_LOAD = 1 ENTROPY_LIMIT = 7.5 ENTROPY_SAMPLE = 0x10000 PHDR_NAMES = { PT_LOAD: "PT_LOAD", 2: "PT_DYNAMIC", 3: "PT_INTERP", 4: "PT_NOTE", 6: "PT_PHDR", 7: "PT_TLS", 0x6474E550: "PT_GNU_EH_FRAME", 0x6474E551: "PT_GNU_STACK", 0x6474E552: "PT_GNU_RELRO", 0x61000000: "PT_SCE_DYNLIBDATA", 0x61000001: "PT_OS_PROCPARAM", 0x61000002: "PT_OS_0x61000002", 0x6FFFFF00: "PT_OS_0x6fffff00", 0x6FFFFF01: "PT_OS_0x6fffff01", } class Refused(Exception): pass def entropy(data): if not data: return 0.0 counts = Counter(data) length = len(data) return -sum((count / length) * math.log2(count / length) for count in counts.values()) def read_records(data): records = [] offset = RECORD_OFFSET while offset + RECORD_SIZE <= FIRST_REGION: flags, segment, size, size_again = struct.unpack_from("= len(data) or segment < 0x350 or segment % 0x10 != 0: break if size > len(data): break records.append((flags, segment, size)) offset += RECORD_SIZE return records def align_up(value, alignment): if alignment <= 1: return value return (value + alignment - 1) // alignment * alignment def parse_elf(data): inner = data.find(b"\x7fELF") if inner < 0: raise Refused("no inner ELF header") if inner + 0x40 > len(data): raise Refused("inner ELF header is truncated") header = bytearray(data[inner:inner + 0x40]) program_offset = struct.unpack_from(" len(data): raise Refused("ELF program header table does not fit the file") headers = [] for index in range(count): offset = inner + program_offset + index * 56 kind, flags, file_offset, vaddr, _, file_size, memory_size, alignment = struct.unpack_from( " tail[0]: tail = (header["offset"], header["source"] - header["offset"]) elif header["size"]: header["source"] = header["offset"] + (tail[1] if tail else 0) if header["source"] > len(data): header["source"] = len(data) keep = max(0, len(data) - header["source"]) header["size"] = min(header["size"], keep) header["memory"] = min(header["memory"], keep) for header in headers: if header["source"] > len(data) or header["size"] > len(data) - header["source"]: raise Refused("segment %d does not fit the file" % header["index"]) def check_decrypted(data, headers): encrypted = [] for header in headers: if header["type"] != PT_LOAD or header["size"] == 0: continue sample = data[header["source"]:header["source"] + min(header["size"], ENTROPY_SAMPLE)] value = entropy(sample) if value >= ENTROPY_LIMIT: encrypted.append((header["index"], header["source"], value)) if encrypted and len(encrypted) == len([h for h in headers if h["type"] == PT_LOAD and h["size"]]): listed = ", ".join("segment %d at %#x (%.2f bits per byte)" % item for item in encrypted) raise Refused("every load segment looks encrypted: " + listed) def unwrap(path, output, verbose=False): data = bytes(Path(path).read_bytes()) if len(data) < FIRST_REGION: raise Refused("file is smaller than the SELF header area") magic = struct.unpack_from(" len(result): raise Refused("segment %d escaped the output" % header["index"]) result[0:0x40] = elf_header struct.pack_into("