💾 Archived View for mirrors.apple2.org.za › active › 4am › images › games › adventure › batman › Bat… captured on 2024-08-19 at 03:43:08.
View Raw
More Information
-=-=-=-=-=-=-
-----------------Batman----------------
A 4am crack 2015-08-02
---------------------------------------
Name: Batman
Genre: arcade
Year: 1988
Publisher: Data East
Media: double-sided 5.25-inch floppy
OS: Quick-DOS
Previous cracks: Asimov has an
uncredited crack
Side B just says "Batman S2!!" and
halts. So I'll start with side A.
~
Chapter 0
In Which Various Automated Tools Fail
In Interesting Ways
COPYA
immediate disk read error
Locksmith Fast Disk Backup
unable to read any track
EDD 4 bit copy (no sync, no count)
no read errors, but copy just reboots
Copy ][+ nibble editor
all tracks use standard prologues
(address: D5 AA 96, data: D5 AA AD)
but modified epilogues
(address: FF FF EB, data: FF FF EB)
Disk Fixer
["O" -> "Input/Output Control"]
set Address Epilogue to "FF FF EB"
set Data Epilogue to "FF FF EB"
Success! All tracks readable!
T00 -> custom bootloader
T11 -> DOS 3.3 disk catalog
Why didn't COPYA work?
modified epilogue bytes (every track)
Why didn't Locksmith FDB work?
modified epilogue bytes (every track)
Why didn't my EDD copy work?
probably a nibble check during boot
Next steps:
1. Super Demuffin
2. Patch RWTS (if necessary)
3. Find nibble check and bypass it
~
Chapter 1
In Which We Choose
The Right Tool For The Job
I'm going to use Super Demuffin here
(instead of my usual go-to conversion
tool, Advanced Demuffin). The disk is
uses a custom bootloader called "Quick-
DOS". It's different enough from the
standard DOS 3.3 bootloader that my
automated tools can't capture the RWTS.
But luckily, the RWTS modifications are
minor -- custom epilogue bytes, same on
every track -- so Super Demuffin will
work just fine.
When you first run Super Demuffin, it
asks for the parameters of the original
disk. In this case, the prologue bytes
are the same, but the epilogues are "FF
FF EB" instead of "DE AA EB".
--v--
SUPER-DEMUFFIN AND FAST COPY
Modified by: The Saltine/Coast to Coast
Address prologue: D5 AA 96
Address epilogue: FF FF EB DISK
^^^^^ ORIGINAL
change from DE AA ---+++++
Data prologue: D5 AA AD
Data epilogue: FF FF EB
^^^^^
change from DE AA ---+++++
Ignore write errors while demuffining!
D - Edit parameters
<SPACE> - Advance to next parm
<RETURN> - Exit edit mode
R - Restore DOS 3.3 parameters
O - Edit Original disk's parameters
C - Edit Copy disk's parameters
G - Begin demuffin process
--^--
Pressing "G" switches to the Locksmith
Fast Disk Copy UI. It assumes that both
disks are in slot 6, and that drive 1
is the original and drive 2 is the
copy.
[S6,D1=original disk]
[S6,D2=blank disk]
--v--
LOCKSMITH 7.0 FAST DISK BACKUP
R...................................
W***********************************
HEX 00000000000000001111111111111111222
TRK 0123456789ABCDEF0123456789ABCDEF012
0...................................
1...................................
2...................................
3...................................
4...................................
5...................................
6...................................
7...................................
8...................................
9...................................
A...................................
B...................................
C...................................
D...................................
12 E...................................
F...................................
[ ] PRESS [RESET] TO EXIT
--^--
]PR#6
...reboots endlessly...
Let's go find that nibble check.
~
Chapter 2
In Which Fake Is The New Real
[S6,D1=original disk]
[S5,D1=my work disk]
]PR#5
CAPTURING BOOT0
...reboots slot 6...
...reboots slot 5...
SAVING BOOT0
]BLOAD BOOT0,A$800
]CALL -151
; check if this is the first run (DOS
; 3.3 does the same thing)
0801- A5 27 LDA $27
0803- C9 09 CMP #$09
; branch if it's not
0805- D0 27 BNE $082E
; first-time initialization path
0807- 78 SEI
; read/write language card RAM bank 2
0808- AD 83 C0 LDA $C083
080B- AD 83 C0 LDA $C083
; set up jump to $Cx5C so we can re-use
; the disk controller ROM routine to
; read more sectors
080E- A5 2B LDA $2B
0810- 4A LSR
0811- 4A LSR
0812- 4A LSR
0813- 4A LSR
0814- 09 C0 ORA #$C0
0816- 85 3F STA $3F
; also set low-level reset vector
0818- 8D FD FF STA $FFFD
081B- A9 5C LDA #$5C
081D- 85 3E STA $3E
081F- A9 00 LDA #$00
0821- 8D FC FF STA $FFFC
0824- 18 CLC
; first page is in $08BF
0825- AD BF 08 LDA $08BF
; sector count is in $08C0
0828- 6D C0 08 ADC $08C0
082B- 8D BF 08 STA $08BF
082E- AE C0 08 LDX $08C0
; branch when done
0831- F0 15 BEQ $0848
; logicla-to-physical sector mapping
0833- BD C1 08 LDA $08C1,X
0836- 85 3D STA $3D
0838- CE C0 08 DEC $08C0
083B- AD BF 08 LDA $08BF
083E- 85 27 STA $27
0840- CE BF 08 DEC $08BF
0843- A6 2B LDX $2B
; read a sector (exits through $0801)
0845- 6C 3E 00 JMP ($003E)
; execution continues here (from $0831)
0848- 2C 51 C0 BIT $C051
084B- 2C 54 C0 BIT $C054
084E- 2C 52 C0 BIT $C052
0851- 8D 0C C0 STA $C00C
0854- 8D 00 C0 STA $C000
; hmm
0857- 20 00 D3 JSR $D300
This is where I need to interrupt the
boot.
; set up callback at call to $D300
96F8- A9 4C LDA #$4C
96FA- 8D 57 08 STA $0857
96FD- A9 0A LDA #$0A
96FF- 8D 58 08 STA $0858
9702- A9 97 LDA #$97
9704- 8D 59 08 STA $0859
; start the boot
9707- 4C 01 08 JMP $0801
; copy code from language card to main
; memory so it survives a reboot (my
; work disk uses the language card to
; relocate DOS) -- no switches required
; here because we're still in R/W mode
: (set at $0808)
970A- A2 08 LDX #$08
970C- A0 00 LDY #$00
970E- B9 00 D2 LDA $D200,Y
9711- 99 00 22 STA $2200,Y
9714- C8 INY
9715- D0 F7 BNE $970E
9717- EE 10 97 INC $9710
971A- EE 13 97 INC $9713
971D- CA DEX
971E- D0 EE BNE $970E
; turn off RAM bank, back to ROM
; (required, DOS does not do this and
; will crash if I don't switch now)
9720- AD 82 C0 LDA $C082
; turn off slot 6 drive motor
9723- AD E8 C0 LDA $C0E8
; reboot to my work disk
9726- 4C 00 C5 JMP $C500
- BSAVE TRACE,A$9600,L$129
- 9600G
...reboots slot 6...
...reboots slot 5...
]BSAVE BOOT1 D300-D9FF,A$2300,L$700
]CALL -151
2300- A0 00 LDY #$00
2302- B9 0E D3 LDA $D30E,Y
2305- 99 00 60 STA $6000,Y
2308- C8 INY
2309- D0 F7 BNE $2302
230B- 4C 00 60 JMP $6000
; set up slot number in the form of $C6
6000- A6 2B LDX $2B
6002- 8A TXA
6003- 4A LSR
6004- 4A LSR
6005- 4A LSR
6006- 4A LSR
6007- 09 C0 ORA #$C0
6009- 8D 52 60 STA $6052
; will explore this in a minute
600C- 20 30 60 JSR $6030
; set up zero page to load a sector
; into $D300
600F- A9 08 LDA #$08
6011- 85 3D STA $3D
6013- A9 D3 LDA #$D3
6015- 85 27 STA $27
6017- A9 00 LDA #$00
6019- 85 26 STA $26
; put an "RTS" at $0801
601B- A9 60 LDA #$60
601D- 8D 01 08 STA $0801
; jumps to disk controller ROM routine
; to read sector (overwriting this code
; which is now executing at $6000 but
; started at $D300)
6020- 20 4E 60 JSR $604E
; wipe part of memory
6023- A2 60 LDX #$60
6025- A9 00 LDA #$00
6027- 9D 80 D8 STA $D880,X
602A- CA DEX
602B- 10 FA BPL $6027
; this will jump to the real $D300
; (which was just read)
602D- 4C 2A D3 JMP $D32A
So this entire sector is, in a sense,
fake. Its only purpose is to overwrite
itself in memory with the real sector
and continue execution there. Oh, and
to call the subroutine at $6030, which
I'm guessing is the copy protection.
~
Chapter 3
In Which Out-Of-Sync
Is The New In-Sync
; initialize 8 bytes in zero page
6030- A2 07 LDX #$07
6032- 8A TXA
6033- 95 F0 STA $F0,X
6035- CA DEX
6036- 10 FA BPL $6032
; turn on drive motor manually
6038- A6 2B LDX $2B
603A- BD 89 C0 LDA $C089,X
603D- BD 8E C0 LDA $C08E,X
; set up Death Counter
6040- A9 0B LDA #$0B
6042- 85 F0 STA $F0
6044- C6 F0 DEC $F0
6046- D0 0B BNE $6053
; if Death Counter hits zero, reboot
; (which is precisely what I saw on
; my non-working copy)
6048- EE F4 03 INC $03F4
604B- 6C FC FF JMP ($FFFC)
604E- A6 2B LDX $2B
6050- 4C 5C FF JMP $FF5C
; execution continues here (from $6046)
6053- A9 80 LDA #$80
6055- 85 F1 STA $F1
6057- C6 F1 DEC $F1
6059- F0 E9 BEQ $6044 ; fail
; get next address field
605B- 20 80 D8 JSR $D880
605E- B0 E4 BCS $6044 ; fail
; loop until we find sector $0A (in
; zero page $2D after routine at $D880)
6060- A5 2D LDA $2D
6062- C9 0A CMP #$0A
6064- D0 F1 BNE $6057
; here we go
6066- A0 00 LDY #$00
6068- BD 8C C0 LDA $C08C,X
606B- 10 FB BPL $6068
606D- 88 DEY
606E- F0 D4 BEQ $6044 ; fail
; find $D5
6070- C9 D5 CMP #$D5
6072- D0 F4 BNE $6068
; find $E7 $E7 $E7
6074- A0 00 LDY #$00
6076- BD 8C C0 LDA $C08C,X
6079- 10 FB BPL $6076
607B- 88 DEY
; fail if we don't find it in time
607C- F0 C6 BEQ $6044
607E- C9 E7 CMP #$E7
6080- D0 F4 BNE $6076
6082- BD 8C C0 LDA $C08C,X
6085- 10 FB BPL $6082
6087- C9 E7 CMP #$E7
6089- D0 B9 BNE $6044
608B- BD 8C C0 LDA $C08C,X
608E- 10 FB BPL $608B
6090- C9 E7 CMP #$E7
6092- D0 B0 BNE $6044
; kill some time to get out of sync
; with the "proper" start of nibbles
; (see below)
6094- BD 8D C0 LDA $C08D,X
6097- A0 10 LDY #$10
6099- 24 80 BIT $80
; now start looking for nibbles that
; don't really exist (except they do,
; because we're out of sync and reading
; timing bits as data)
609B- BD 8C C0 LDA $C08C,X
609E- 10 FB BPL $609B
60A0- 88 DEY
60A1- F0 A1 BEQ $6044
60A3- C9 EE CMP #$EE
60A5- D0 F4 BNE $609B
60A7- EA NOP
60A8- EA NOP
; now take the next (desynced) nibbles
; and store them in zero page $F0..$F7
60A9- A0 07 LDY #$07
60AB- BD 8C C0 LDA $C08C,X
60AE- 10 FB BPL $60AB
60B0- 99 F0 00 STA $00F0,Y
60B3- EA NOP
60B4- 88 DEY
60B5- 10 F4 BPL $60AB
A short digression here into some super
low-level disk stuff, because this
wasn't low-level enough already...
$E7 $E7 $E7 $E7. What would that nibble
sequence look like on disk? The answer
is, "It depends." $E7 in hexadecimal is
11100111 in binary, so here is the
simplest possible answer:
|--E7--||--E7--||--E7--||--E7--|
11100111111001111110011111100111
But wait. Every nibble read from disk
must have its high bit set. In theory,
you could insert one or two "0" bits
after any of those nibbles. (Two is the
maximum, due to hardware limitations.)
These extra "0" bits would be swallowed
by the standard "wait for data latch to
have its high bit set" loop, which you
see over and over in any RWTS code:
:1 LDA $C08C,X
BPL :1
Now consider the following bitstream:
|--E7--| |--E7--| |--E7--||--E7--|
11100111011100111001110011111100111
^ ^^
(extra) (extra)
The first $E7 has one extra "0" bit
after it, and the second $E7 has two
extra "0" bits after it. Totally legal,
works on any Apple II computer and any
floppy drive. A "LDA $C08C,X; BPL" loop
would still interpret this bitstream as
a sequence of four $E7 nibbles. Each of
the extra "0" bits appear after we've
just read a nibble and we're waiting
for the high bit to be set again.
Now, what if we miss the first few bits
of this bitstream, then start looking?
The disk is always spinning, whether
we're reading from it or not. If we
waste too much time doing something
other than reading, we'll literally
miss some bits as the disk spins by.
This is why the timing of low-level
RWTS code is so critical.
Let's say we waste 12 CPU cycles before
we start reading this bitstream. Each
bit takes 4 CPU cycles to go by, so
after 12 cycles, we would have missed
the first 3 bits (marked with an X).
(normal start)
|--E7--| |--E7--| |--E7--||--E7--|
11100111011100111001110011111100111
XXX |--EE--| |--E7--| |--FC--|
(delayed start)
Ah! It's interpreted as a completely
different nibble sequence if you delay
just a few CPU cycles before you start
reading. Also note that some of those
"extra" bits are no longer being
ignored; now they're being interpreted
as data, as part of the nibbles that
are being returned to the higher level
code. Meanwhile, other bits that were
part of the $E7 nibbles are now being
swallowed.
Now, let's go back to the first stream,
which had no extra bits between the
nibbles, and see what happens when we
waste those same 12 CPU cycles.
(normal start)
|--E7--||--E7--||--E7--||--E7--|
11100111111001111110011111100111
XXX |--FC--||--FC--||--FC--|
(delayed start)
After skipping the first three bits,
the stream is interpreted as a series
of $FC $FC $FC repeating endlessly --
not $EE $E7 $FC like the other stream.
Here's the kicker: generic bit copiers
didn't preserve these extra "0" bits
between nibbles. By "desynchronizing"
(wasting just the right number of CPU
cycles at just the right time), then
interpreting the bits on the disk in
mid-stream, developers could determine
at runtime whether you had an original
disk. Which is precisely the code we
just saw.
Here is the complete "E7 bitstream,"
annotated to show both the synchronized
and desynchronized nibble sequences.
($0265 wastes the right amount of time;
$0274 checks for $EE; $027F checks for
the rest of the nibbles, stored in
reverse order at $02C7.)
|--E7--| |--E7--| |--E7--||--E7--|
111001110111001110011100111111001110
XXX |--EE--| |--E7--| |--FC--||--E
|--E7--| |--E7--||--E7--| |--E7--|
111001110011100111111001110111001110
E--| |--E7--| |--FC--||--EE--| |--E
|--E7--||--E7--|
1110011111100111
E--| |--FC--|
We now return you to the actual code...
~
Chapter 4
In Which We Use The Original Disk
As A Weapon Against Itself,
And Everything Turns Out
Better Than Expected
60B7- A2 04 LDX #$04
60B9- A0 00 LDY #$00
60BB- 84 F8 STY $F8
60BD- BD DB 60 LDA $60DB,X
60C0- 86 FA STX $FA
60C2- 85 F9 STA $F9
We're getting addresses from an array
at $60DB and storing them in $F8/$F9.
60DB- D4 D5 D6 D7 D9
That's the rest of the bootloader.
60C4- 98 TYA
60C5- 29 07 AND #$07
60C7- AA TAX
; take a desynchronized nibble (stored
; earlier in zero page, at $60B0)
60C8- B5 F0 LDA $F0,X
; use it as a decryption key for this
; page of the bootloader
60CA- 51 F8 EOR ($F8),Y
60CC- 91 F8 STA ($F8),Y
60CE- C8 INY
60CF- D0 F3 BNE $60C4
; increment next bootloader page to be
; decrypted
60D1- E6 F9 INC $F9
; decrement index into page array
60D3- A6 FA LDX $FA
60D5- CA DEX
60D6- 10 E5 BPL $60BD
; restore X to the slot number (x16)
; on exit
60D8- A6 2B LDX $2B
60DA- 60 RTS
Well, this is inconvenient. But not
insurmountable. I can interrupt the
boot after the nibble check has
decrypted the rest of the bootloader
and the calling routine has loaded the
real $D300 sector from disk (at $6020).
$6000 was originally at $D30E, so $6023
was originally $D331. And that's where
I need to patch the boot.
; set up callback #1
96F8- A9 4C LDA #$4C
96FA- 8D 57 08 STA $0857
96FD- A9 0A LDA #$0A
96FF- 8D 58 08 STA $0858
9702- A9 97 LDA #$97
9704- 8D 59 08 STA $0859
; start the boot
9707- 4C 01 08 JMP $0801
; callback #1 is here
; set up callback #2
970A- A9 4C LDA #$4C
970C- 8D 31 D3 STA $D331
970F- A9 1C LDA #$1C
9711- 8D 32 D3 STA $D332
9714- A9 97 LDA #$97
9716- 8D 33 D3 STA $D333
; continue the boot
9719- 4C 00 D3 JMP $D300
; callback #2 is here
; copy the decrypted bootloader to the
; graphics page so it survives a reboot
971C- A2 07 LDX #$07
971E- A0 00 LDY #$00
9720- B9 00 D3 LDA $D300,Y
9723- 99 00 23 STA $2300,Y
9726- C8 INY
9727- D0 F7 BNE $9720
9729- EE 22 97 INC $9722
972C- EE 25 97 INC $9725
972F- CA DEX
9730- D0 EE BNE $9720
; switch off language card
9732- AD 82 C0 LDA $C082
; turn off slot 6 drive motor
9735- AD E8 C0 LDA $C0E8
; reboot to my work disk
9738- 4C 00 C5 JMP $C500
- BSAVE TRACE2,A$9600,L$13B
- 9600G
...reboots slot 6...
...reboots slot 5...
]BSAVE BOOT1 DECRYPTED D300-D9FF,
A$2300,L$700
]CALL -151
Now to write the decrypted bootloader
back to disk...
Here is a little helper program that
writes to the disk in slot 6, drive 1.
There is no error checking or
confirmation, because life is short and
uncertain.
08C0- A9 08 LDA #$08
08C2- A0 E8 LDY #$E8
08C4- 20 D9 03 JSR $03D9
08C7- AC ED 08 LDY $08ED
08CA- 88 DEY
08CB- 10 05 BPL $08D2
08CD- A0 0F LDY #$0F
08CF- CE EC 08 DEC $08EC
08D2- 8C ED 08 STY $08ED
08D5- CE F1 08 DEC $08F1
08D8- CE E1 08 DEC $08E1
08DB- D0 E3 BNE $08C0
08DD- 60 RTS
+-- sector count
v
08E0- 00 07 00 00 00 00 00 00
08E8- 01 60 01 00 00 07 FB 08
^ ^
track --+ +-- sector
(both of these count down)
08F0- 00 29 00 00 02 00 FE 60
^
+-- starting address
(also counts down)
08F8- 01 00 00 00 01 EF D8 00
- BSAVE WRITE BOOT1,A$8C0,L$40
The decrypted bootloader is already in
memory at $2300, so let's go.
...write write write...
...works...
The RWTS is flexible enough to read
disks in a standard format. It accepts
accept any nibble between $DE and $FF
as the first epilogue, and any nibble
between $AA and $FF as the second.
Side B uses the same modified epilogue
sequence as side A, and it converts to
a standard format with Super Demuffin
in the same way.
Quod erat liberandum.
---------------------------------------
A 4am crack No. 384
------------------EOF------------------