mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-21 14:12:29 -05:00
119 lines
4.0 KiB
Python
119 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Verify that generated ShellCore hook prologues are safe to copy."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
from capstone import CS_ARCH_X86, CS_GRP_CALL, CS_GRP_JUMP, CS_MODE_64, Cs
|
|
from capstone.x86 import X86_OP_IMM, X86_OP_MEM, X86_REG_RIP
|
|
|
|
from generate_shellcore_offsets import (
|
|
ShellCore,
|
|
firmware_key,
|
|
)
|
|
from generate_shellcore_offsets_from_files import firmware_files, generate
|
|
|
|
|
|
def verify_firmware(path: Path, firmware: int) -> None:
|
|
shellcore = ShellCore(path)
|
|
targets = shellcore.locate_targets()
|
|
decoder = Cs(CS_ARCH_X86, CS_MODE_64)
|
|
decoder.detail = True
|
|
|
|
names = ["launch_app"]
|
|
# AppInstallAll is hooked after the public TitleDir RPC disappeared.
|
|
if firmware >= 0x1200:
|
|
names.append("install_all")
|
|
|
|
for name in names:
|
|
address = targets[name]
|
|
patch_size = shellcore.patch_size(address)
|
|
offset = shellcore.virtual_to_file(address)
|
|
code = shellcore.data[offset : offset + patch_size]
|
|
instructions = list(decoder.disasm(code, address))
|
|
if sum(instruction.size for instruction in instructions) != patch_size:
|
|
raise ValueError(f"{path}: {name}: split instruction")
|
|
for instruction in instructions:
|
|
if instruction.group(CS_GRP_CALL) or instruction.group(CS_GRP_JUMP):
|
|
raise ValueError(
|
|
f"{path}: {name}: control flow in copied prologue at "
|
|
f"0x{instruction.address:x}"
|
|
)
|
|
for operand in instruction.operands:
|
|
if operand.type == X86_OP_MEM and operand.mem.base == X86_REG_RIP:
|
|
raise ValueError(
|
|
f"{path}: {name}: RIP-relative operand at "
|
|
f"0x{instruction.address:x}"
|
|
)
|
|
|
|
sandbox_address = targets["sandbox_ready"]
|
|
sandbox_offset = shellcore.virtual_to_file(sandbox_address)
|
|
sandbox_instruction = next(
|
|
decoder.disasm(shellcore.data[sandbox_offset : sandbox_offset + 5],
|
|
sandbox_address),
|
|
None,
|
|
)
|
|
if (
|
|
sandbox_instruction is None
|
|
or sandbox_instruction.mnemonic != "call"
|
|
or sandbox_instruction.size != 5
|
|
or not sandbox_instruction.operands
|
|
or sandbox_instruction.operands[0].type != X86_OP_IMM
|
|
or sandbox_instruction.operands[0].imm
|
|
!= targets["sandbox_ready_target"]
|
|
):
|
|
raise ValueError(
|
|
f"{path}: invalid sceApplicationSpawn2 call at "
|
|
f"0x{sandbox_address:x}"
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("root", type=Path)
|
|
args = parser.parse_args()
|
|
firmware_inputs: list[tuple[int, Path]] = []
|
|
firmware_dirs = sorted(
|
|
path
|
|
for path in args.root.iterdir()
|
|
if (path / "system/vsh/SceShellCore.elf").is_file()
|
|
)
|
|
for firmware_dir in firmware_dirs:
|
|
firmware_inputs.append(
|
|
(
|
|
firmware_key(firmware_dir.name),
|
|
firmware_dir / "system/vsh/SceShellCore.elf",
|
|
)
|
|
)
|
|
for major, minor, path in firmware_files(args.root):
|
|
firmware_inputs.append((firmware_key(f"{major}.{minor:02d}"), path))
|
|
if not firmware_inputs:
|
|
raise ValueError(f"no SceShellCore firmware files under {args.root}")
|
|
|
|
for firmware, path in firmware_inputs:
|
|
verify_firmware(path, firmware)
|
|
|
|
checked_in_offsets = (
|
|
Path(__file__).resolve().parents[1] / "src/sm_shellcore_offsets.inc"
|
|
)
|
|
if not firmware_dirs:
|
|
expected = generate(args.root)
|
|
actual = checked_in_offsets.read_text(encoding="utf-8")
|
|
if actual != expected:
|
|
raise ValueError(
|
|
f"{checked_in_offsets}: generated offsets are stale for "
|
|
f"{args.root}"
|
|
)
|
|
|
|
print(
|
|
f"verified {len(firmware_inputs)} firmware trampoline layouts"
|
|
+ (" and checked-in offset coverage" if not firmware_dirs else "")
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|