573in1/tools/common/analysis.py

230 lines
6.7 KiB
Python

# -*- coding: utf-8 -*-
# 573in1 - Copyright (C) 2022-2024 spicyjpeg
#
# 573in1 is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# 573in1 is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# 573in1. If not, see <https://www.gnu.org/licenses/>.
import logging, re
from collections.abc import Sequence
from pathlib import Path
from .cart import *
from .decompile import AnalysisError, PSEXEAnalyzer
from .mips import ImmInstruction, Opcode, Register, encodeADDIU, encodeJR
from .util import InterleavedFile
## MAME NVRAM directory reader
class MAMENVRAMDump:
def __init__(self, nvramDir: Path):
try:
with InterleavedFile(
open(nvramDir / "29f016a.31m", "rb"),
open(nvramDir / "29f016a.27m", "rb")
) as file:
file.seek(FLASH_HEADER_OFFSET)
self.flashHeader: ROMHeaderDump | None = ROMHeaderDump(
DumpFlag.DUMP_PUBLIC_DATA_OK,
b"",
file.read(FLASH_HEADER_LENGTH)
)
# FIXME: the executable's CRC32 should probably be validated
file.seek(FLASH_EXECUTABLE_OFFSET)
try:
self.bootloader: PSEXEAnalyzer | None = PSEXEAnalyzer(file)
except AnalysisError:
self.bootloader: PSEXEAnalyzer | None = None
except FileNotFoundError:
self.flashHeader: ROMHeaderDump | None = None
self.bootloader: PSEXEAnalyzer | None = None
try:
with open(nvramDir / "m48t58", "rb") as file:
file.seek(RTC_HEADER_OFFSET)
self.rtcHeader: ROMHeaderDump | None = ROMHeaderDump(
DumpFlag.DUMP_PUBLIC_DATA_OK,
b"",
file.read(RTC_HEADER_LENGTH)
)
except FileNotFoundError:
self.rtcHeader: ROMHeaderDump | None = None
self.installCart: CartDump | None = \
self._loadCartDump(nvramDir / "cassette_install_eeprom")
self.gameCart: CartDump | None = \
self._loadCartDump(nvramDir / "cassette_game_eeprom")
def _loadCartDump(self, path: Path) -> CartDump | None:
try:
with open(path, "rb") as file:
return parseMAMECartDump(file.read())
except FileNotFoundError:
return None
## Bootloader executable analysis
_BOOT_VERSION_REGEX: re.Pattern = \
re.compile(rb"\0BOOT VER[. ]*(1\.[0-9A-Z]+)\0")
def getBootloaderVersion(exe: PSEXEAnalyzer) -> str:
for matched in _BOOT_VERSION_REGEX.finditer(exe.body):
version: bytes = matched.group(1)
argString: bytes = b"\0" + version + b"\0"
# A copy of the version string with no "BOOT VER" prefix is always
# present in the launcher and passed to the game's command line.
if argString not in exe.body:
logging.warning("found version string with no prefix-less copy")
return version.decode("ascii")
raise AnalysisError("could not find version string")
## Game executable analysis
# In order to support chips from multiple manufacturers, Konami's flash and
# security cartridge drivers use vtable arrays to dispatch API calls to the
# appropriate driver. The following arrays are present in the binary:
#
# struct {
# int (*eraseChip)(const uint8_t *dataKey);
# int (*setDataKey)(
# uint8_t type, const uint8_t *oldKey, const uint8_t *newKey
# );
# int (*readData)(
# const uint8_t *dataKey, uint32_t offset, void *output, size_t length
# );
# int (*writeData)(
# const uint8_t *dataKey, uint32_t offset, const void *data, size_t length
# );
# int (*readConfig)(const uint8_t *dataKey, void *output);
# int (*writeConfig)(const uint8_t *dataKey, const void *config);
# int (*readDS2401)(void *output);
# int chipType, capacity;
# } CART_DRIVERS[4];
#
# struct {
# int (*eraseSector)(void *ptr);
# int (*flushErase)(void);
# int (*flushEraseLower)(void);
# int (*flushEraseUpper)(void);
# int (*writeHalfword)(void *ptr, uint16_t value);
# int (*writeHalfwordAsync)(void *ptr, uint16_t value);
# int (*flushWrite)(void *ptr, uint16_t value);
# int (*flushWriteLower)(void *ptr, uint16_t value);
# int (*flushWriteUpper)(void *ptr, uint16_t value);
# int (*resetChip)(void *ptr);
# } FLASH_DRIVERS[4];
def _findDriverTableCalls(
exe: PSEXEAnalyzer,
dummyErrorCode: int,
functionNames: Sequence[str] = (),
valueNames: Sequence[str] = ()
) -> dict[str, int]:
# The first entry of each array is always a dummy driver containing pointers
# to a function that returns an error code. The table can thus be found by
# locating the dummy function and all contiguous references to it.
table: int = 0
for dummy in exe.findBytes(
encodeJR(Register.RA) +
encodeADDIU(Register.V0, Register.ZERO, dummyErrorCode)
):
try:
table = exe.findSingleMatch(
(dummy.to_bytes(4, "little") * len(functionNames)) +
bytes(4 * len(valueNames))
)
break
except StopIteration:
continue
if not table:
raise AnalysisError(
"could not locate any valid table referenced by a dummy function"
)
logging.debug(f"table found at {table:#010x}")
# Search the binary for functions that are wrappers around the driver table.
memberNames: Sequence[str] = functionNames + valueNames
functions: dict[str, int] = {}
for offset in exe.findFunctionReturns():
match (
exe.disassembleAt(offset + 4),
exe.disassembleAt(offset + 16),
exe.disassembleAt(offset + 40)
):
case (
ImmInstruction(
opcode = Opcode.LUI, rt = Register.V1, value = msb
), ImmInstruction(
opcode = Opcode.ADDIU, rt = Register.V1, value = lsb
), ImmInstruction(
opcode = Opcode.LW, rt = Register.V0, value = index
)
) if ((msb << 16) + lsb) == table:
index //= 4
if (index < 0) or (index >= len(memberNames)):
logging.debug(
f"ignoring candidate at {offset:#010x} due to "
f"out-of-bounds index {index}"
)
continue
name: str = memberNames[index]
functions[name] = offset
logging.debug(f"found {name} at {offset:#010x}")
return functions
def findCartFunctions(exe: PSEXEAnalyzer) -> dict[str, int]:
return _findDriverTableCalls(
exe, -2, (
"eraseChip",
"setDataKey",
"readSector",
"writeSector",
"readConfig",
"writeConfig",
"readCartID",
), (
"chipType",
"capacity"
)
)
def findFlashFunctions(exe: PSEXEAnalyzer) -> dict[str, int]:
return _findDriverTableCalls(
exe, -1, (
"eraseSector",
"flushErase",
"flushEraseLower",
"flushEraseUpper",
"writeHalfword",
"writeHalfwordAsync",
"flushWrite",
"flushWriteLower",
"flushWriteUpper",
"resetChip"
)
)