mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-22 06:32:25 -05:00
297 lines
11 KiB
Python
297 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate SceShellCore bridge offsets from unpacked firmware files.
|
|
|
|
The generator identifies functions through semantic log strings, finds the
|
|
containing function prologue and emits a compact firmware offset table. It is
|
|
intentionally independent from IDA databases so the checked-in table can be
|
|
reproduced from unpacked firmware files.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import struct
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from capstone import CS_ARCH_X86, CS_MODE_64, Cs
|
|
from capstone.x86 import X86_OP_IMM
|
|
|
|
PROLOGUE = b"\x55\x48\x89\xe5"
|
|
TARGET_NAMES = (
|
|
"launch_app",
|
|
"sandbox_ready",
|
|
"install_title_dir",
|
|
"install_all",
|
|
)
|
|
PAGE_SIZE = 0x4000
|
|
RIP_REFERENCE = re.compile(
|
|
rb"(?=([\x48\x4c][\x8d\x8b][\x05\x0d\x15\x1d\x25\x2d\x35\x3d].{4}))",
|
|
re.DOTALL,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class LoadSegment:
|
|
file_offset: int
|
|
virtual_address: int
|
|
file_size: int
|
|
memory_size: int
|
|
flags: int
|
|
|
|
|
|
class ShellCore:
|
|
def __init__(self, path: Path):
|
|
self.path = path
|
|
self.data = path.read_bytes()
|
|
header = struct.unpack_from("<16sHHIQQQIHHHHHH", self.data, 0)
|
|
phoff, phentsize, phnum = header[5], header[9], header[10]
|
|
self.loads: list[LoadSegment] = []
|
|
self.executable: LoadSegment | None = None
|
|
for index in range(phnum):
|
|
values = struct.unpack_from(
|
|
"<IIQQQQQQ", self.data, phoff + index * phentsize
|
|
)
|
|
p_type, flags, offset, address, _, file_size, memory_size, _ = values
|
|
if p_type != 1:
|
|
continue
|
|
segment = LoadSegment(offset, address, file_size, memory_size, flags)
|
|
self.loads.append(segment)
|
|
if flags & 1:
|
|
self.executable = segment
|
|
if self.executable is None:
|
|
raise ValueError(f"{path}: executable PT_LOAD not found")
|
|
self._rip_references: list[tuple[int, int]] | None = None
|
|
|
|
def file_to_virtual(self, offset: int) -> int:
|
|
for segment in self.loads:
|
|
relative = offset - segment.file_offset
|
|
if 0 <= relative < segment.file_size:
|
|
return segment.virtual_address + relative
|
|
raise ValueError(f"file offset 0x{offset:x} is not mapped")
|
|
|
|
def virtual_to_file(self, address: int) -> int:
|
|
for segment in self.loads:
|
|
relative = address - segment.virtual_address
|
|
if 0 <= relative < segment.file_size:
|
|
return segment.file_offset + relative
|
|
raise ValueError(f"virtual address 0x{address:x} is not mapped")
|
|
|
|
def string_addresses(self, needle: bytes, use_string_start: bool) -> list[int]:
|
|
result: list[int] = []
|
|
offset = 0
|
|
while True:
|
|
offset = self.data.find(needle, offset)
|
|
if offset < 0:
|
|
return result
|
|
string_offset = offset
|
|
if use_string_start:
|
|
string_offset = self.data.rfind(
|
|
b"\0", max(0, offset - 256), offset
|
|
) + 1
|
|
result.append(self.file_to_virtual(string_offset))
|
|
offset += 1
|
|
|
|
def rip_xrefs(self, targets: list[int], slop: int = 0) -> list[int]:
|
|
if self._rip_references is None:
|
|
segment = self.executable
|
|
assert segment is not None
|
|
code = self.data[
|
|
segment.file_offset : segment.file_offset + segment.file_size
|
|
]
|
|
references: list[tuple[int, int]] = []
|
|
for match in RIP_REFERENCE.finditer(code):
|
|
index = match.start()
|
|
displacement = struct.unpack_from("<i", code, index + 3)[0]
|
|
instruction_address = segment.virtual_address + index
|
|
references.append(
|
|
(instruction_address, instruction_address + 7 + displacement)
|
|
)
|
|
self._rip_references = references
|
|
|
|
if slop == 0:
|
|
exact_targets = set(targets)
|
|
return [
|
|
address
|
|
for address, destination in self._rip_references
|
|
if destination in exact_targets
|
|
]
|
|
return [
|
|
address
|
|
for address, destination in self._rip_references
|
|
if any(
|
|
target - slop <= destination <= target + slop
|
|
for target in targets
|
|
)
|
|
]
|
|
|
|
def preceding_prologue(self, address: int, window: int = 0x40000) -> int:
|
|
end = self.virtual_to_file(address)
|
|
start = max(self.executable.file_offset, end - window) # type: ignore[union-attr]
|
|
found = self.data.rfind(PROLOGUE, start, end)
|
|
if found < 0:
|
|
raise ValueError(f"no prologue before 0x{address:x}")
|
|
return self.file_to_virtual(found)
|
|
|
|
def locate_targets(self) -> dict[str, int]:
|
|
launch_strings = self.string_addresses(b"launchApp(%s)", True)
|
|
launch_xrefs = self.rip_xrefs(launch_strings)
|
|
if len(launch_xrefs) != 1:
|
|
raise ValueError(f"launchApp xrefs: {launch_xrefs!r}")
|
|
launch = self.preceding_prologue(launch_xrefs[0])
|
|
|
|
sandbox_strings = self.string_addresses(
|
|
b"sceApplicationSpawn2(appId, outPid, path, root, argv", True
|
|
)
|
|
sandbox_xrefs = self.rip_xrefs(sandbox_strings)
|
|
if len(sandbox_xrefs) != 1:
|
|
raise ValueError(f"sceApplicationSpawn2 xrefs: {sandbox_xrefs!r}")
|
|
decoder = Cs(CS_ARCH_X86, CS_MODE_64)
|
|
decoder.detail = True
|
|
sandbox_xref = sandbox_xrefs[0]
|
|
sandbox_xref_file = self.virtual_to_file(sandbox_xref)
|
|
calls = [
|
|
(instruction.address, instruction.operands[0].imm, instruction.size)
|
|
for instruction in decoder.disasm(
|
|
self.data[sandbox_xref_file : sandbox_xref_file + 0xA0],
|
|
sandbox_xref,
|
|
)
|
|
if instruction.mnemonic == "call"
|
|
and instruction.operands
|
|
and instruction.operands[0].type == X86_OP_IMM
|
|
]
|
|
if len(calls) < 2:
|
|
raise ValueError(
|
|
f"sceApplicationSpawn2 call after 0x{sandbox_xref:x} not found"
|
|
)
|
|
# The first call writes the optional trace line. The next direct call
|
|
# is sceApplicationSpawn2, after ShellCore has finished the sandbox.
|
|
sandbox_ready, sandbox_ready_target, sandbox_call_size = calls[1]
|
|
if sandbox_call_size != 5:
|
|
raise ValueError(
|
|
f"sceApplicationSpawn2 call at 0x{sandbox_ready:x} "
|
|
f"has size {sandbox_call_size}"
|
|
)
|
|
|
|
install_strings = self.string_addresses(b"AppInstallTitleDirMain", False)
|
|
install_groups: dict[int, int] = {}
|
|
for xref in self.rip_xrefs(install_strings, slop=8):
|
|
prologue = self.preceding_prologue(xref)
|
|
install_groups[prologue] = install_groups.get(prologue, 0) + 1
|
|
if not install_groups:
|
|
raise ValueError("AppInstallTitleDirMain xrefs not found")
|
|
best_count = max(install_groups.values())
|
|
best = [address for address, count in install_groups.items() if count == best_count]
|
|
if len(best) != 1:
|
|
raise ValueError(f"ambiguous AppInstallTitleDirMain: {install_groups!r}")
|
|
|
|
app_install_all_strings = self.string_addresses(b"AppInstallAll()", True)
|
|
app_install_all_xrefs = self.rip_xrefs(app_install_all_strings)
|
|
call_targets: dict[int, int] = {}
|
|
for xref in app_install_all_xrefs:
|
|
xref_file = self.virtual_to_file(xref)
|
|
for call_file in range(xref_file - 32, xref_file):
|
|
if self.data[call_file] != 0xE8:
|
|
continue
|
|
call_address = self.file_to_virtual(call_file)
|
|
displacement = struct.unpack_from("<i", self.data, call_file + 1)[0]
|
|
target = call_address + 5 + displacement
|
|
call_targets[target] = call_targets.get(target, 0) + 1
|
|
if not call_targets:
|
|
raise ValueError("AppInstallAll call target not found")
|
|
app_install_all = max(call_targets, key=call_targets.get)
|
|
|
|
return {
|
|
"launch_app": launch,
|
|
"sandbox_ready": sandbox_ready,
|
|
"sandbox_ready_target": sandbox_ready_target,
|
|
"install_title_dir": best[0],
|
|
"install_all": app_install_all,
|
|
}
|
|
|
|
def bridge_cave(self) -> tuple[int, int]:
|
|
segment = self.executable
|
|
assert segment is not None
|
|
segment_end = segment.virtual_address + segment.memory_size
|
|
cave_start = (segment_end + 0xF) & ~0xF
|
|
cave_end = (segment_end + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1)
|
|
return cave_start, cave_end - cave_start
|
|
|
|
def patch_size(self, address: int, minimum: int = 12) -> int:
|
|
offset = self.virtual_to_file(address)
|
|
decoder = Cs(CS_ARCH_X86, CS_MODE_64)
|
|
size = 0
|
|
for instruction in decoder.disasm(
|
|
self.data[offset : offset + 64], address
|
|
):
|
|
size += instruction.size
|
|
if size >= minimum:
|
|
return size
|
|
raise ValueError(f"cannot determine patch size at 0x{address:x}")
|
|
|
|
def firmware_key(name: str) -> int:
|
|
major, minor = (int(part) for part in name.split(".", 1))
|
|
if major > 99 or minor > 99:
|
|
raise ValueError(f"firmware version is not BCD-compatible: {name}")
|
|
major_bcd = ((major // 10) << 4) | (major % 10)
|
|
minor_bcd = ((minor // 10) << 4) | (minor % 10)
|
|
return (major_bcd << 8) | minor_bcd
|
|
|
|
|
|
def generate(root: Path) -> str:
|
|
firmware_dirs = sorted(
|
|
(
|
|
path
|
|
for path in root.iterdir()
|
|
if (path / "system/vsh/SceShellCore.elf").is_file()
|
|
),
|
|
key=lambda path: tuple(int(part) for part in path.name.split(".")),
|
|
)
|
|
if not firmware_dirs:
|
|
raise ValueError(f"{root}: no firmware directories found")
|
|
records: list[str] = []
|
|
for firmware_dir in firmware_dirs:
|
|
firmware = firmware_key(firmware_dir.name)
|
|
shellcore = ShellCore(firmware_dir / "system/vsh/SceShellCore.elf")
|
|
targets = shellcore.locate_targets()
|
|
cave_offset, cave_size = shellcore.bridge_cave()
|
|
records.append(
|
|
" {\n"
|
|
f" .firmware = 0x{firmware:04x}u,\n"
|
|
f" .name = \"{firmware_dir.name}\",\n"
|
|
f" .bridge_cave_offset = 0x{cave_offset:x}u,\n"
|
|
f" .bridge_cave_size = 0x{cave_size:x}u,\n"
|
|
f" .sandbox_call_target_offset = "
|
|
f"0x{targets['sandbox_ready_target']:x}u,\n"
|
|
" .targets = {"
|
|
+ ", ".join(
|
|
"{.offset = "
|
|
f"0x{targets[name]:x}u, .patch_size = "
|
|
f"{5 if name == 'sandbox_ready' else shellcore.patch_size(targets[name])}u}}"
|
|
for name in TARGET_NAMES
|
|
)
|
|
+ "},\n }"
|
|
)
|
|
if not records:
|
|
raise ValueError(f"{root}: no SceShellCore firmware files found")
|
|
return "/* Generated by tools/generate_shellcore_offsets.py. */\n" + ",\n".join(records) + "\n"
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("root", type=Path)
|
|
parser.add_argument("--output", type=Path)
|
|
args = parser.parse_args()
|
|
output = generate(args.root)
|
|
if args.output:
|
|
with args.output.open("w", encoding="utf-8", newline="\n") as file:
|
|
file.write(output)
|
|
else:
|
|
print(output, end="")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|