diff --git a/passport.py b/passport.py index 316c959..2b72e13 100755 --- a/passport.py +++ b/passport.py @@ -12,4 +12,4 @@ def opener(filename): return wozimage.EDDReader(filename) raise RuntimeError("unrecognized file type") -EDDToWoz(opener(sys.argv[1]), DefaultLogger) +Crack(opener(sys.argv[1]), DefaultLogger) diff --git a/passport/__init__.py b/passport/__init__.py index 45c0cdc..6745ac2 100755 --- a/passport/__init__.py +++ b/passport/__init__.py @@ -356,6 +356,7 @@ class BasePassportProcessor: # base class self.g = PassportGlobals() self.g.disk_image = disk_image self.logger = logger_class(self.g) + self.rwts = None self.output_tracks = {} self.patchers = [] self.patches_found = [] @@ -367,7 +368,7 @@ class BasePassportProcessor: # base class #JMPBECAPatcher, #JMPB660Patcher, #JMPB720Patcher, - #BadEmuPatcher, + bademu.BadEmuPatcher, #BadEmu2Patcher, rwts.RWTSPatcher, #RWTSLogPatcher, @@ -399,8 +400,8 @@ class BasePassportProcessor: # base class #T11DiskVolPatcher, #T02VolumeNamePatcher, universale7.UniversalE7Patcher, - #A6BC95Patcher, - #A5CountPatcher, + a6bc95.A6BC95Patcher, + a5count.A5CountPatcher, d5d5f7.D5D5F7Patcher, #ProDOSRWTSPatcher, #ProDOS6APatcher, @@ -421,7 +422,7 @@ class BasePassportProcessor: # base class #BootCounterPatcher, #JMPB412Patcher, #JMPB400Patcher, - #AdvIntPatcher, + advint.AdventureInternationalPatcher, #JSR8635Patcher, #JMPB4BBPatcher, #DOS32MUSEPatcher, @@ -436,13 +437,13 @@ class BasePassportProcessor: # base class if self.run(): self.postprocess() - def SkipTrack(self, rwts, track_num, track): + def SkipTrack(self, track_num, track): # don't look for whole-track protections on track 0, that's silly if track_num == 0: return False # Electronic Arts protection track? if track_num == 6: - if rwts.find_address_prologue(track): - address_field = rwts.address_field_at_point(track) + if self.rwts.find_address_prologue(track): + address_field = self.rwts.address_field_at_point(track) if address_field and address_field.track_num == 5: return True # Nibble count track? repeated_nibble_count = 0 @@ -464,9 +465,97 @@ class BasePassportProcessor: # base class def IDDiversi(self, t00s00): """returns True if T00S00 is Diversi-DOS bootloader, or False otherwise""" return find.at(0xF1, t00s00, - b'\xB3\xA3\xA0\xD2\xCF\xD2\xD2\xC5' - b'\x8D\x87\x8D') + b'\xB3\xA3\xA0\xD2\xCF\xD2\xD2\xC5' + b'\x8D\x87\x8D') + def IDProDOS(self, t00s00): + """returns True if T00S00 is ProDOS bootloader, or False otherwise""" + return find.at(0x00, t00s00, + b'\x01' + b'\x38' + b'\xB0\x03' + b'\x4C') + + def IDPascal(self, t00s00): + """returns True if T00S00 is Pascal bootloader, or False otherwise""" + if find.wild_at(0x00, t00s00, + b'\x01' + b'\xE0\x60' + b'\xF0\x03' + b'\x4C' + find.WILDCARD + b'\x08'): + return True + return find.at(0x00, t00s00, + b'\x01' + b'\xE0\x70' + b'\xB0\x04' + b'\xE0\x40' + b'\xB0') + + def IDDavidDOS(self, t00s00): + """returns True if T00S00 is David-DOS II bootloader, or False otherwise""" + if not find.at(0x01, t00s00, + b'\xA5\x27' + b'\xC9\x09' + b'\xD0\x17'): + return False + return find.wild_at(0x4A, t00s00, + b'\xA2' + find.WILDCARD + \ + b'\xBD' + find.WILDCARD + b'\x08' + \ + b'\x9D' + find.WILDCARD + b'\x04' + \ + b'\xCA' + b'\x10\xF7') + + def IDDatasoft(self, t00s00): + """returns True if T00S00 is encrypted Datasoft bootloader, or False otherwise""" + return find.at(0x00, t00s00, + b'\x01\x4C\x7E\x08\x04\x8A\x0C\xB8' + b'\x00\x56\x10\x7A\x00\x00\x1A\x16' + b'\x12\x0E\x0A\x06\x53\x18\x9A\x02' + b'\x10\x1B\x02\x10\x4D\x56\x15\x0B' + b'\xBF\x14\x14\x54\x54\x54\x92\x81' + b'\x1B\x10\x10\x41\x06\x73\x0A\x10' + b'\x33\x4E\x00\x73\x12\x10\x33\x7C' + b'\x00\x11\x20\xE3\x49\x50\x73\x1A' + b'\x10\x41\x00\x23\x80\x5B\x0A\x10' + b'\x0B\x4E\x9D\x0A\x10\x9D\x0C\x10' + b'\x60\x1E\x53\x10\x90\x53\xBC\x90' + b'\x53\x00\x90\xD8\x52\x00\xD8\x7C' + b'\x00\x53\x80\x0B\x06\x41\x00\x09' + b'\x04\x45\x0C\x63\x04\x90\x94\xD0' + b'\xD4\x23\x04\x91\xA1\xEB\xCD\x06' + b'\x95\xA1\xE1\x98\x97\x86') + + def IDMicrograms(self, t00s00): + """returns True if T00S00 is Micrograms bootloader, or False otherwise""" + if not find.at(0x01, t00s00, + b'\xA5\x27' + b'\xC9\x09' + b'\xD0\x12' + b'\xA9\xC6' + b'\x85\x3F'): + return False + return find.at(0x42, t00s00, b'\x4C\x00') + + def IDQuickDOS(self, t00s00): + """returns True if T00S00 is Quick-DOS bootloader, or False otherwise""" + return find.at(0x01, t00s00, + b'\xA5\x27' + b'\xC9\x09' + b'\xD0\x27' + b'\x78' + b'\xAD\x83\xC0') + + def IDRDOS(self, t00s00): + """returns True if T00S00 is Quick-DOS bootloader, or False otherwise""" + return find.at(0x00, t00s00, + b'\x01' + b'\xA9\x60' + b'\x8D\x01\x08' + b'\xA2\x00' + b'\xA0\x1F' + b'\xB9\x00\x08' + b'\x49') + def IDDOS33(self, t00s00): """returns True if T00S00 is DOS bootloader or some variation that can be safely boot traced, or False otherwise""" @@ -544,13 +633,13 @@ class BasePassportProcessor: # base class def IDBootloader(self, t00): """returns RWTS object that can (hopefully) read the rest of the disk""" - rwts = UniversalRWTSIgnoreEpilogues(self.logger) - physical_sectors = rwts.decode_track(t00) + temporary_rwts_for_t00 = UniversalRWTSIgnoreEpilogues(self.logger) + physical_sectors = temporary_rwts_for_t00.decode_track(t00) if 0 not in physical_sectors: self.logger.PrintByID("fatal0000") return None - t00s00 = physical_sectors[0] - + t00s00 = physical_sectors[0].decoded + if self.IDDOS33(t00s00): self.g.is_boot0 = True if self.IDDiversi(t00s00): @@ -559,15 +648,30 @@ class BasePassportProcessor: # base class self.logger.PrintByID("prontodos") else: self.logger.PrintByID("dos33boot0") - # TODO handle JSR08B3 here - rwts = self.TraceDOS33(rwts.reorder_to_logical_sectors(physical_sectors), rwts) - else: - self.logger.PrintByID("builtin") - self.g.tried_univ = True - rwts = UniversalRWTS(self.logger) - return rwts + logical_sectors = temporary_rwts_for_t00.reorder_to_logical_sectors(physical_sectors) + return self.TraceDOS33(logical_sectors) + # TODO JSR08B3 + # TODO MECC fastloader + # TODO DOS 3.3P + # TODO Laureate + # TODO Electronic Arts + # TODO DOS 3.2 + # TODO IDEncoded44 + # TODO IDEncoded53 + self.g.is_prodos = self.IDProDOS(t00s00) + if self.g.is_prodos: + # TODO IDVolumeName + # TODO IDDinkeyDOS + pass + self.g.is_pascal = self.IDPascal(t00s00) + self.g.is_daviddos = self.IDDavidDOS(t00s00) + self.g.is_datasoft = self.IDDatasoft(t00s00) + self.g.is_micrograms = self.IDMicrograms(t00s00) + self.g.is_quickdos = self.IDQuickDOS(t00s00) + self.g.is_rdos = self.IDRDOS(t00s00) + return self.StartWithUniv() - def TraceDOS33(self, logical_sectors, rwts): + def TraceDOS33(self, logical_sectors): """returns RWTS object""" use_builtin = False @@ -576,9 +680,7 @@ class BasePassportProcessor: # base class if i not in logical_sectors: use_builtin = True break - # TODO handle Protected.DOS here - if not use_builtin: # check for "STY $48;STA $49" at RWTS entry point ($BD00) use_builtin = not find.at(0x00, logical_sectors[7], b'\x84\x48\x85\x49') @@ -605,13 +707,19 @@ class BasePassportProcessor: # base class # TODO handle Infocom here if use_builtin: - self.logger.PrintByID("builtin") - return rwts + return self.StartWithUniv() self.logger.PrintByID("diskrwts") self.g.is_rwts = True return DOS33RWTS(logical_sectors, self.logger) + def StartWithUniv(self): + """return Universal RWTS object, log that we're using it, and set global flags appropriately""" + self.logger.PrintByID("builtin") + self.g.tried_univ = True + self.g.is_protdos = False + return UniversalRWTS(self.logger) + def preprocess(self): return True @@ -625,8 +733,8 @@ class BasePassportProcessor: # base class self.tracks[float(track_num)] = self.g.disk_image.seek(float(track_num)) # analyze track $00 to create an RWTS - rwts = self.IDBootloader(self.tracks[0]) - if not rwts: return False + self.rwts = self.IDBootloader(self.tracks[0]) + if not self.rwts: return False # initialize all patchers for P in self.patcher_classes: @@ -635,15 +743,15 @@ class BasePassportProcessor: # base class # main loop - loop through disk from track $22 down to track $00 for track_num in range(0x22, -1, -1): if track_num == 0 and self.g.tried_univ: - rwts = UniversalRWTSIgnoreEpilogues(self.logger) + self.rwts = UniversalRWTSIgnoreEpilogues(self.logger) should_run_patchers = False self.g.track = track_num - physical_sectors = rwts.decode_track(self.tracks[track_num], self.burn) + physical_sectors = self.rwts.decode_track(self.tracks[track_num], self.burn) if 0x0F not in physical_sectors: - if self.SkipTrack(rwts, track_num, self.tracks[track_num]): - self.save_track(rwts, track_num, None) + if self.SkipTrack(track_num, self.tracks[track_num]): + self.save_track(track_num, None) continue - if len(physical_sectors) < rwts.sectors_per_track: + if len(physical_sectors) < self.rwts.sectors_per_track: # TODO wrong in case where we switch mid-track. # Need to save the sectors that worked with the original RWTS # then append the ones that worked with the universal RWTS @@ -651,25 +759,25 @@ class BasePassportProcessor: # base class self.logger.PrintByID("fail") return False self.logger.PrintByID("switch", {"sector":0x0F}) # TODO find exact sector - rwts = UniversalRWTS(self.logger) + self.rwts = UniversalRWTS(self.logger) self.g.tried_univ = True - physical_sectors = rwts.decode_track(self.tracks[track_num], self.burn) - if len(physical_sectors) < rwts.sectors_per_track: + physical_sectors = self.rwts.decode_track(self.tracks[track_num], self.burn) + if len(physical_sectors) < self.rwts.sectors_per_track: self.logger.PrintByID("fail") # TODO find exact sector return False - self.save_track(rwts, track_num, physical_sectors) + self.save_track(track_num, physical_sectors) return True - def save_track(self, rwts, track_num, physical_sectors): + def save_track(self, track_num, physical_sectors): pass def apply_patches(self, logical_sectors, patches): pass class Verify(BasePassportProcessor): - def save_track(self, rwts, track_num, physical_sectors): + def save_track(self, track_num, physical_sectors): if not physical_sectors: return {} - logical_sectors = rwts.reorder_to_logical_sectors(physical_sectors) + logical_sectors = self.rwts.reorder_to_logical_sectors(physical_sectors) should_run_patchers = (len(physical_sectors) == 16) # TODO if should_run_patchers: for patcher in self.patchers: @@ -689,8 +797,8 @@ class Verify(BasePassportProcessor): self.logger.PrintByID("passver") class Crack(Verify): - def save_track(self, rwts, track_num, physical_sectors): - self.output_tracks[float(track_num)] = Verify.save_track(self, rwts, track_num, physical_sectors) + def save_track(self, track_num, physical_sectors): + self.output_tracks[float(track_num)] = Verify.save_track(self, track_num, physical_sectors) def apply_patches(self, logical_sectors, patches): for patch in patches: @@ -725,7 +833,7 @@ class EDDToWoz(BasePassportProcessor): self.burn = 2 return True - def save_track(self, rwts, track_num, physical_sectors): + def save_track(self, track_num, physical_sectors): track_num = float(track_num) track = self.tracks[track_num] if physical_sectors: @@ -733,6 +841,7 @@ class EDDToWoz(BasePassportProcessor): for s in physical_sectors.values(): b.extend(track.bits[s.start_bit_index:s.end_bit_index]) else: + # TODO this only works about half the time b = track.bits[:51021] self.output_tracks[track_num] = wozimage.Track(b, len(b)) diff --git a/passport/patchers/__init__.py b/passport/patchers/__init__.py index 93908e1..16bbd54 100644 --- a/passport/patchers/__init__.py +++ b/passport/patchers/__init__.py @@ -1,4 +1,8 @@ __all__ = [ + "a5count", + "a6bc95", + "advint", + "bademu", "d5d5f7", "microfun", "rwts", diff --git a/passport/patchers/a5count.py b/passport/patchers/a5count.py new file mode 100644 index 0000000..fd92c11 --- /dev/null +++ b/passport/patchers/a5count.py @@ -0,0 +1,22 @@ +from passport.patchers import Patch, Patcher +from passport.util import * + +class A5CountPatcher(Patcher): + """nibble count between $A5 and address prologue + +tested on +- Game Frame One +- Game Frame Two +""" + def should_run(self, track_num): + return self.g.is_pascal + + def run(self, logical_sectors, track_num): + offset = find.wild(concat_track(logical_sectors), + b'\x07' + b'\xE6\x02' + b'\xD0\x03' + b'\x4C\xA5\x00' + b'\xC9\xA5') + if offset == -1: return [] + return [Patch(track_num, offset // 256, 8 + (offset % 256), b'\xD0\x7B', "a5count")] diff --git a/passport/patchers/a6bc95.py b/passport/patchers/a6bc95.py new file mode 100644 index 0000000..e34e089 --- /dev/null +++ b/passport/patchers/a6bc95.py @@ -0,0 +1,36 @@ +from passport.patchers import Patch, Patcher +from passport.util import * + +class A6BC95Patcher(Patcher): + """nibble count after $A6 $BC $95 prologue + +tested on +- The Secrets of Science Island +""" + def should_run(self, track_num): + return self.g.is_pascal + + def run(self, logical_sectors, track_num): + buffy = concat_track(logical_sectors) + if -1 == find.wild(buffy, + b'\xBD\x8C\xC0' + b'\x10\xFB' + b'\xC9\xA6' + b'\xD0\xED'): + return False + if -1 == find.wild(buffy, + b'\xBD\x8C\xC0' + b'\x10\xFB' + b'\xC9\xBC'): + return False + if -1 == find.wild(buffy, + b'\xBD\x8C\xC0' + b'\x10\xFB' + b'\xC9\x95'): + return False + offset = find.wild(buffy, + b'\xAE\xF8\x01' + b'\xA9\x0A' + b'\x8D\xFE\x01') + if offset == -1: return [] + return [Patch(track_num, offset // 256, offset % 256, b'\x60', "a6bc95")] diff --git a/passport/patchers/advint.py b/passport/patchers/advint.py new file mode 100644 index 0000000..6768640 --- /dev/null +++ b/passport/patchers/advint.py @@ -0,0 +1,24 @@ +from passport.patchers import Patch, Patcher +from passport.util import * + +class AdventureInternationalPatcher(Patcher): + """encrypted protection check on Adventure International disks + +tested on +- SAGA1 - Adventureland v2.1-416 +- SAGA2 - Pirate Adventure v2.1-408 +- SAGA5 - The Count v2.1-115 +- SAGA6 - Strange Odyssey v2.1-119 +""" + def should_run(self, track_num): + return True # TODO self.g.is_adventure_international + + def run(self, logical_sectors, track_num): + buffy = concat_track(logical_sectors) + offset = find.wild(buffy, + b'\x85' + find.WILDCARD + find.WILDCARD + \ + b'\x74\x45\x09' + b'\xD9\x32' + b'\x0C\x30') + if offset == -1: return [] + return [Patch(track_num, offset // 256, offset % 256, b'\xD1\x59\xA7', "advint")] diff --git a/passport/patchers/bademu.py b/passport/patchers/bademu.py new file mode 100644 index 0000000..5702efe --- /dev/null +++ b/passport/patchers/bademu.py @@ -0,0 +1,25 @@ +from passport.patchers import Patch, Patcher +from passport.util import * + +class BadEmuPatcher(Patcher): + """RWTS checks for timing bit by checking if data latch is still $D5 after waiting "too long" but this confuses legacy emulators (AppleWin, older versions of MAME) so we patch it for compatibility + +tested on +- Dino Dig +- Make A Face +""" + def should_run(self, track_num): + return self.g.is_rwts and (track_num == 0) + + def run(self, logical_sectors, track_num): + if not find.at(0x4F, logical_sectors[3], + b'\xBD\x8C\xC0' + b'\x10\xFB' + b'\xC9\xD5' + b'\xD0\xF0' + b'\xEA' + b'\xBD\x8C\xC0' + b'\xC9\xD5' + b'\xF0\x12'): return [] + return [Patch(0, 3, 0x58, b'\xF0\x06')] + return patches diff --git a/passport/patchers/d5d5f7.py b/passport/patchers/d5d5f7.py index 4f01d1a..795caf6 100644 --- a/passport/patchers/d5d5f7.py +++ b/passport/patchers/d5d5f7.py @@ -2,6 +2,19 @@ from passport.patchers import Patch, Patcher from passport.util import * class D5D5F7Patcher(Patcher): + """nibble count with weird bitstream involving $D5 and $F7 as delimiters + +tested on +- Ace Detective +- Cat 'n Mouse +- Cotton Tales +- Dyno-Quest +- Easy Street +- Fraction-oids +- Math Magic +- RoboMath +- NoteCard Maker +""" def should_run(self, track_num): # TODO return True diff --git a/passport/patchers/microfun.py b/passport/patchers/microfun.py index 9b92bb8..65de319 100644 --- a/passport/patchers/microfun.py +++ b/passport/patchers/microfun.py @@ -2,6 +2,15 @@ from passport.patchers import Patch, Patcher from passport.util import * class MicrofunPatcher(Patcher): + """RWTS jumps to nibble check after reading certain sectors + +tested on +- Station 5 +- The Heist +- Miner 2049er (re-release) +- Miner 2049er II +- Short Circuit +""" def should_run(self, track_num): return self.g.is_rwts and (track_num == 0) diff --git a/passport/patchers/rwts.py b/passport/patchers/rwts.py index 57a3eff..7c2440e 100644 --- a/passport/patchers/rwts.py +++ b/passport/patchers/rwts.py @@ -2,6 +2,7 @@ from passport.patchers import Patch, Patcher from passport.util import * class RWTSPatcher(Patcher): + """RWTS fixups for DOS 3.3-shaped RWTSen""" def should_run(self, track_num): return self.g.is_rwts and (track_num == 0) diff --git a/passport/patchers/universale7.py b/passport/patchers/universale7.py index c3d4ddb..91a4372 100644 --- a/passport/patchers/universale7.py +++ b/passport/patchers/universale7.py @@ -2,6 +2,10 @@ from passport.patchers import Patch, Patcher from passport.util import * class UniversalE7Patcher(Patcher): + """replace remnants of E7 bitstream with a compatible BYTEstream that fools most E7 protection checks + +(invented by qkumba, see PoC||GTFO 0x11 and 4am crack no. 655 Rocky's Boots 4.0 for explanation) +""" e7sector = b'\x00'*0xA0 + b'\xAC\x00'*0x30 def should_run(self, track_num): diff --git a/passport/util/find.py b/passport/util/find.py index 3b2404a..56e9676 100644 --- a/passport/util/find.py +++ b/passport/util/find.py @@ -1,21 +1,19 @@ +import re + WILDCARD = b'\x97' def wild(source_bytes, search_bytes): - """Search source_bytes (bytes object) for the first instance of search_bytes (bytes_object). search_bytes may contain a wildcard that matches any byte, like '.' in a regular expression. Returns index of first match or -1, like string find() method.""" - ranges = search_bytes.split(WILDCARD) - first_index = last_index = source_bytes.find(ranges[0]) - if first_index == -1: return -1 - last_index += len(ranges[0]) - for search_range in ranges[1:]: - last_index += 1 - if not search_range: continue - if source_bytes[last_index:last_index + len(search_range)] != search_range: return -1 - last_index += len(search_range) - return first_index + """Search source_bytes (bytes object) for the first instance of search_bytes (bytes_object). search_bytes may contain WILDCARD, which matches any single byte (like "." in a regular expression). Returns index of first match, or -1 if no matches.""" + search_bytes = re.escape(search_bytes).replace(b'\\'+WILDCARD, b'.') + match = re.search(search_bytes, source_bytes) + if match: + return match.start() + return -1 def wild_at(offset, source_bytes, search_bytes): """returns True if the search_bytes was found in source_bytes at offset (search_bytes may include wildcards), otherwise False""" - return wild(source_bytes[offset:], search_bytes) == 0 + offset = wild(source_bytes[offset:], search_bytes) + return offset == 0 def at(offset, source_bytes, search_bytes): """returns True if the exact bytes search_bytes was found in source_bytes at offset (no wildcards), otherwise False"""