209 lines
6.8 KiB
Python
Executable File
209 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
"""
|
|
patchpiracysums - calculates the expected checksums that are used in piracy
|
|
checks and replaces the expected values in the ROM.
|
|
|
|
Usage:
|
|
patchpiracysums <rom> <ldmap>
|
|
|
|
To avoid piracy, the game calculates checksums of functions in memory and
|
|
compares them with expected values. This script is armed with a list of
|
|
locations where these piracy checks happen, as well as the algorithms used in
|
|
each, and calculates the expected checksums.
|
|
|
|
Locations are referenced by function name and resolved to addresses using a
|
|
linker map so it works with shifted ROMs. To find the location of the checksum
|
|
within a function, it expects the function to use CHECKSUM_PLACEHOLDER. It
|
|
searches for lui and ori instructions that load the placeholder value and
|
|
replaces the value with the calculated one.
|
|
"""
|
|
|
|
CHECKSUM_PLACEHOLDER = 0x99aabbcc
|
|
|
|
def algo01(checksum, word):
|
|
return checksum ^ word
|
|
|
|
def algo02(checksum, word):
|
|
return checksum ^ ~word
|
|
|
|
def algo03(checksum, word):
|
|
return ((checksum + word) & 0xffffffff) * 2
|
|
|
|
def algo04(checksum, word):
|
|
return checksum + ~word
|
|
|
|
def algo05(checksum, word):
|
|
return checksum * 2 + word
|
|
|
|
def algo06(checksum, word):
|
|
return checksum + word
|
|
|
|
def algo07(checksum, word):
|
|
checksum = (checksum << 1) & 0xffffffff
|
|
return checksum ^ word
|
|
|
|
def algo08(checksum, word):
|
|
checksum = (checksum + word) & 0xffffffff
|
|
return checksum + (word >> 1)
|
|
|
|
def algo09(checksum, word):
|
|
return checksum - ~word
|
|
|
|
def algo10(checksum, word):
|
|
return (checksum ^ word) << 1
|
|
|
|
def algo11(checksum, word):
|
|
return (checksum ^ ~word) << 1
|
|
|
|
def algo12(checksum, word):
|
|
checksum ^= ~word
|
|
checksum ^= (word << 5) & 0xffffffff
|
|
checksum ^= word >> 15
|
|
return checksum
|
|
|
|
class Tool:
|
|
def load_map(self):
|
|
fd = open(sys.argv[2], 'r')
|
|
ldmap = fd.read()
|
|
fd.close()
|
|
|
|
self.symbols = re.findall(r'^\s*0x([0-9a-f]+)\s+(\S+)$', ldmap, re.MULTILINE)
|
|
|
|
# Matching the following line:
|
|
# .boot 0x0000000070001000 0x2050 load address 0x0000000000001000
|
|
self.segpositions = re.findall(r'^\.(\S+)\s+0x([0-9a-f]+)\s+0x([0-9a-f]+)\s+load address\s+0x([0-9a-f]+)', ldmap, re.MULTILINE)
|
|
|
|
def ramtorom(self, ramaddr):
|
|
for pos in self.segpositions:
|
|
segname = pos[0]
|
|
rampos = int(pos[1], 16)
|
|
length = int(pos[2], 16)
|
|
rompos = int(pos[3], 16)
|
|
|
|
if ramaddr >= rampos and ramaddr < rampos + length:
|
|
return rompos + (ramaddr - rampos)
|
|
|
|
print('Couldn\'t translate RAM address 0x%08x to ROM' & romaddr)
|
|
exit(1)
|
|
|
|
def get_function_address(self, funcname):
|
|
startram = None
|
|
endram = None
|
|
|
|
for (index, symbol) in enumerate(list(self.symbols)):
|
|
if symbol[1] == funcname:
|
|
startram = int(symbol[0], 16)
|
|
endram = int(self.symbols[index + 1][0], 16)
|
|
break
|
|
|
|
if startram is None:
|
|
raise ValueError('Unable to find %s in map' % funcname)
|
|
|
|
startrom = self.ramtorom(startram)
|
|
endrom = self.ramtorom(endram)
|
|
return (startrom, endrom)
|
|
|
|
def is_branch_likely(self, word):
|
|
if word & 0xfc000000 == 0x50000000: # beql
|
|
return True
|
|
if word & 0xfc000000 == 0x54000000: # bnel
|
|
return True
|
|
if word & 0xfc000000 == 0x58000000: # blezl
|
|
return True
|
|
if word & 0xfc000000 == 0x5c000000: # bgtzl
|
|
return True
|
|
if word & 0xfc000000 == 0x01000000 and word & 0x001f0000 == 0x00020000: # bltzl
|
|
return True
|
|
if word & 0xfc000000 == 0x01000000 and word & 0x001f0000 == 0x00030000: # bgezl
|
|
return True
|
|
return False
|
|
|
|
def calc_checksum(self, sumfunc, algo):
|
|
(pos, end) = self.get_function_address(sumfunc)
|
|
self.fd.seek(pos)
|
|
checksum = 0
|
|
|
|
while pos < end:
|
|
word = int.from_bytes(self.fd.read(4), 'big')
|
|
checksum = algo(checksum, word) & 0xffffffff
|
|
pos += 4
|
|
|
|
return checksum
|
|
|
|
# Checksums are always written into $at with lui and ori
|
|
# 3c0199aa lui $at,0x99aa
|
|
# 3421bbcc ori $at,$at,0xbbcc
|
|
def write_checksum(self, patchfunc, checksum):
|
|
(pos, end) = self.get_function_address(patchfunc)
|
|
self.fd.seek(pos)
|
|
in_branchlikely = False
|
|
upperpos = None
|
|
lowerpos = None
|
|
|
|
while pos < end:
|
|
word = int.from_bytes(self.fd.read(4), 'big')
|
|
|
|
if in_branchlikely:
|
|
in_branchlikely = False
|
|
else:
|
|
if self.is_branch_likely(word):
|
|
in_branchlikely = True
|
|
elif word == 0x3c010000 | (CHECKSUM_PLACEHOLDER >> 16):
|
|
upperpos = pos
|
|
elif upperpos and word == 0x34210000 | (CHECKSUM_PLACEHOLDER & 0xffff):
|
|
lowerpos = pos
|
|
|
|
pos += 4
|
|
|
|
if upperpos is None or lowerpos is None:
|
|
print('Unable to find placeholder checksum in %s.' % patchfunc)
|
|
print('This can happen if you\'ve turned PIRACYCHECKS off, built the files, then turned it on without rebuilding.')
|
|
print('To fix, try running the following:')
|
|
print('')
|
|
print(' touch $(grep -lr PIRACYCHECKS src)')
|
|
print(' make')
|
|
print('')
|
|
exit(1)
|
|
|
|
self.fd.seek(upperpos)
|
|
self.fd.write((0x3c010000 | (checksum >> 16)).to_bytes(4, 'big'))
|
|
|
|
self.fd.seek(lowerpos)
|
|
self.fd.write((0x34210000 | (checksum & 0xffff)).to_bytes(4, 'big'))
|
|
|
|
def patch(self, algo, patchfunc, sumfunc):
|
|
checksum = self.calc_checksum(sumfunc, algo)
|
|
self.write_checksum(patchfunc, checksum)
|
|
|
|
def run(self):
|
|
self.load_map()
|
|
|
|
self.fd = open(sys.argv[1], 'rb+')
|
|
|
|
self.patch(algo01, '__scHandleTasks', 'bootPhase1')
|
|
self.patch(algo02, 'cheatMenuHandleDialog', '__scHandleTasks')
|
|
self.patch(algo03, 'propobjHandlePickupByAibot', 'func0f08e2ac')
|
|
self.patch(algo04, 'chrUncloak', 'propobjHandlePickupByAibot')
|
|
self.patch(algo05, 'chrsCheckForNoise', '__scHandleRetrace')
|
|
self.patch(algo06, 'lvInit', 'lvGetSlowMotionType')
|
|
self.patch(algo07, 'propAllocateEyespy', 'lvInit')
|
|
self.patch(algo08, 'chrConsiderGrenadeThrow', 'bgInit')
|
|
self.patch(algo09, 'bgun0f09e144', 'tagsAllocatePtrs')
|
|
self.patch(algo10, 'explosionAlertChrs', 'glassDestroy')
|
|
self.patch(algo11, 'func0f0069dc', 'mtxGetObfuscatedRomBase')
|
|
self.patch(algo12, 'func0f15c920', 'func0f0069dc')
|
|
|
|
self.fd.close()
|
|
|
|
# Piracy checks disabled for ntsc-beta for now...
|
|
# it's possible they don't exist in that version.
|
|
if os.environ['PIRACYCHECKS'] == '1' and os.environ['ROMID'] != 'ntsc-beta':
|
|
tool = Tool()
|
|
tool.run()
|
|
|