80's & Games
Javanoïd | Pang
Marilyn | Ban
Amiga Oldies
modules | Goodies
Gallery | Goodies
Sci-Fi | Fantasy
Thriller Polar
Divers | Doc

..= Cracking Tuto : MFM+CheckSums =..
by Heavy aka VrS!-Cybfree

Toki/Ocean (1991)

 Amiga Tutos

I used original disk IPF #40 (1 disk)

- blank disk for AR dump
- blank disk for final Toki release
- assembler/resourcer

get the 1st release. It's a non-dos disk. no password.
first, check the disk with X-Copy.

ouch! there are non standard track...MFM protection

let's take a look at the boot : rt 0 1 50000
we can see the text:
"TOKI bootloader! --Disk routine and protection written by Pierre ADANE" ;)

Understand the Boot

yep, it's a full trackloader.
commented source :

move.l #$7FFF7FFF,d0 ; stop operating system
move.l d0,$DFF09A ; all interruptions off
move.w d0,$DFF096 ; all dma off
lea TrackLoader(pc),a0 ; copy main prog to $60000
lea $60000,a1 ; and put addr $60000 in exception vector $10
move.l a1,$10.w ; if a Illegal instruction occurs, jump to vector $10
move.w #(end-TrackLoader)/4-1,d0
move.l (a0)+,(a1)+
dbf d0,_copy
illegal ; start in Supervisor mode

the program start in Trace mode (TVD : Trace Vector Detection). we can't use breakpoint. but we can use for example a "btst #6,$bfe001" before jump.(08 39 00 06 00 bf e0 01 66 f6)
you can skip the trace mode : jump directly to $60000. several ways : change "move.l a1,$10.w" with pea $60000, and "illegal" with "rts", and nop "move #$2000,sr".
now we access to the hardware custom trackloader, the only one able to read this disk.

lea $400.w,sp ; Supervisor Stack at 400
move.l Datas(pc),-(sp) ; put Load buffer in stack to jump at the end.
move #$2000,sr

lea Datas(pc),a0
lea DelaySoft(pc),a5
lea $BFD100,a4 ; CIAB-PRB in A4
lea $DFF024,a6 ; DSKLEN custom base in A6
move.l #$55555555,d6 ; odd bits mask MFM uncode
move.l (a0)+,d5 ; Load address
move.l (a0)+,starttrk-vars(a5) ; start track*tracksize
move.l (a0)+,d4 ; len track*tracksize
move.l #$18B8,d3 ; custom track size
; (standard $1600)
move.b #$7F,(a4) ; Motor on
jsr (a5) ; cpu delay
bclr #3,(a4) ; DSKSEL0 select DF0
jsr (a5) ; cpu delay
bsr DiskReady

here, it's the loader init. we recognize the MFM masks $55555555, DSKLEN and CIA register who manage the disk drivers, and start DF0
take a look at Datas :
dc.l $40000
dc.l $f28d8
dc.l $4a28
it's seems to be the file information : certainly load buffer, start track, length.
let's continue to the load loop

clr.w try-vars(a5)
move.l starttrk-vars(a5),d7
bsr SeekTrack
move.w #$8210,$96-$24(a6) ; DMACON enable DMA Disk
move.w #$7F00,$9E-$24(a6) ; erase ADKCON
move.w #$9500,$9E-$24(a6) ; good value in ADKCON
move.w #$4124,$7E-$24(a6) ; DSKSYNC custom sync word (standard 4489)
move.w #2,$9C-$24(a6) ; INTREQ DSKBLK interrupt
bsr DiskReady
move.l #$70000,$20-$24(a6) ; DSKPTH mfm buffer address
move.w #$4000,(a6) ; erase DSKLEN
move.w #$8000+$18C4,(a6) ; DSKLEN raw read len (words) standard $8000+$1900
move.w #$8000+$18C4,(a6) ; 2 times to start DMA transfert
move.l #$A0000,d7 ; loop wait
btst #1,$1F-$24(a6) ; wait for disk drive to be ready
bne.s .continue
subq.l #1,d7
bne.s .wait
bra.s .loopload

the syncro word and the track len are customs : $4124 and $18C4. the standard values are $4489 and $1900.
let's see first the SeekTrack routine:

movem.l d0-d3,-(sp)
move.l d7,d0 ; start track
move.l d3,d2 ; track size 18B8
divu d2,d0 ; F28D8/18B8 = 157 ($9d)
move.w d0,d1 ; num track 157
swap d0
move.w d0,rest-vars(a5) ; rest of the division
ext.l d0
sub.l d0,starttrk-vars(a5) ; f28d8-rest
cmpi.w #80,d1 ; >80 ?
bge.s _greater
st head-vars(a5) ; side 0
bra.s _less

sf head-vars(a5)
subi.w #80,d1 ; track on side 1
move.b d1,d7
move.b head-vars(a5),d0
beq.s Side1
bset #2,(a4) ; DSKSIDE side 0
bra.s seekthetrack
bclr #2,(a4) ; DSKSIDE side 1
jsr (a5) ; cpu delay
tst.b track-vars(a5) ; track >=0
bge.s seekForw
bsr.s Seek0
cmp.b track-vars(a5),d7 ; current track = seek track
beq.s seekOk
blt.s seekBack
bsr.s seekForward
bra.s seekIt
bsr.s seekBackward
bsr.s DiskReady ; CIAA-PRA - DSKRDY
bra.s seekForw
movem.l (sp)+,d0-d3

bsr.s seekBackward

btst #4,$F01(a4) ; $BFE001 CIAA-PRA - DSKTRK0
bne.s noTrack0
sf track-vars(a5) ; clr

bclr #1,(a4) ; DSKDIR goto cyl 79
addq.b #1,track-vars(a5) ; incr current track
bra.s MoveHeads ; move +/-1 track

bset #1,(a4) ; DSKDIR goto cyl 0
subq.b #1,track-vars(a5) ; decr current track

bclr #0,(a4) ; DSKSTEP step low
bset #0,(a4) ; step high

the beginning of routine show that the value in d7 (start track) is divided by the len of a track to get the track num. If not integer division, the rest is sector in track *512.
after, he compare the track number with 80. If geater, then sub 80 and set flag side to 1.
This loader seems to use tracks 1-80 on lower side first and tracks 81-160 on upper side. and not lower-upper-lower-upper...like standard dos.
let's see now the decode routine:

bsr.s MFMUncodeCheck
beq.s .ok
addq.w #1,try-vars(a5) ; nb try
cmpi.w #8,try-vars(a5)
bls.s Load
moveq #0,d7
pea retry-vars(a5) ; retry
bra.w SeekTrack
movea.l buffer-vars(a5),a0 ; mfmbuffer (70000)
addq.w #8,a0 ; skip header sync
movea.l d5,a1 ; load address (40000)
move.w rest-vars(a5),d0
move.l d3,d7 ; track size
sub.w d0,d7 ; - real size read
lsr.w #2,d7 ; /2
subq.w #1,d7 ; -1
add.w d0,d0 ; rest*2 (words->bytes)
adda.w d0,a0 ; skipped in mfmbuffer
move.l (a0)+,d0 ; even long word
move.l (a0)+,d1 ; odd long word
and.l d6,d0 ; mask bits
and.l d6,d1
add.l d1,d1 ; rotate odd bits to left
or.l d1,d0 ; merge odd and even bits
move.l d0,(a1)+ ; store decoded long word
subq.l #4,d4 ; len -4 bytes
beq.s StopDrive ; finished
dbf d7,.decode

add.l d3,starttrk-vars(a5) ; next track
move.l a1,d5 ; load address for next track
bra Load

movem.l d1-a6,-(sp)
movea.l buffer-vars(a5),a0 ; mfmbuffer = 70000
bsr.s MFMchecksumHeader
move.l d0,d1 ; checksum
swap d1
cmpi.w #$5041,d1 ; checksum='PA' = "Pierre Adane"
bne.s .error ; not a PA track
cmp.w track-vars(a5),d0 ; check track number is
bne.s .error ; what we expected ? no
subq.l #8,a0 ; yep. start of data
move.w #($18C4-2)/4-1,d7 ; decode 1583.L (18B8+12-2 bytes)
moveq #0,d3 ; checksum
move.l (a0)+,d0 ; even long word
move.l (a0)+,d2 ; odd long word
and.l d6,d0 ; mask bits
and.l d6,d2
add.l d0,d3 ; rotate odd bits to the left
add.l d2,d3 ; add to checksum
dbf d7,.decode

bsr.s MFMchecksumHeader
cmp.l d0,d3 ; good checksum
bne.s .error ; no
movem.l (sp)+,d1-a6
moveq #0,d7 ; ok

.error movem.l (sp)+,d1-a6
moveq #-1,d7 ; error

move.l (a0)+,d0 ; even MFM long word : $5249
move.l (a0)+,d1 ; odd MFM long word
and.l d6,d0 ; MFM mask
and.l d6,d1
add.l d1,d1 ; rotate odd bits left
or.l d1,d0 ; merge odd and even bits

some others routs:

bset #7,(a4) ; DSKMOTOR off
bset #3,(a4) ; DSKSEL0 deselect df0
jsr (a5) ; delay
bclr #3,(a4)
jsr (a5) ; delay
bset #3,(a4)
rts ; exit then start @ 40000 (load buffer)

btst #5,$F01(a4) ; $BFE001 CIAA-PRA - bit DSKRDY
bne.s DiskReady

vars ; start of variables
move.w d7,-(sp)
move.w #10000,d7 ; ~7 ms
dbf d7,_wait
move.w (sp)+,d7
buffer dc.l $70000 ; mfm track buffer
try dc.w 0
rest dc.w 0
starttrk dc.l 0
track dc.b -1 ; track counter
head dc.b 0 ; side

the delay is software : to fix!
this trackloader is a raw sector mfm loader (sector precision).
now, we know how it works. I think/hope the game use the same.

Rip the Files

hmm...let's continue loading of the first file at 40000 (3 tracks from track 157).

disassemble at 40000:
it seems to be a decrypt/copy routine. crypted data at $400e8 copied to 60000.

Then jump at 60000. First "protection". I skip the "decrypt" routine here : not really necessary with AR.
continue and disassemble 60000 (when loading restart):
yet another crypted program. we can read "Anti L bootblock loader by Pierre ADANE". decrypted to $488. Second "protection".
disassemble $488:
hmm...strange, but we can recognize the same routine than in boot. it's the trackloader. other things seems crypted. Third "protection".
search for file info...we have chance : at $52e, the delay sub prog, and at $540, the variables :
$70000 (mfm buffer)
$60000 load address
$EF768 start track
$2710 len

let's continue loading 2 tracks from track 155...

the credits screen appear and loading continue.
stop and disassemble at 60000:
after VBI init, at $6009E he jump to trackloader $604d0. it's the same.
File infos at 606cc:
$23800 load address
$D0908 start track 135
$1EE60 len (~20 tracks)

at 600a2, he put load address in stack (jump at the end at $23800)
at 600b0, we can see the string "Ice!" : the file is ice-packed.
he depack the file. ending at 60104, and jump to depacked program at $23800.

after loading and depacking,...loading restart.
the Intro appear.

take a look at $23800. It's seems to be the main program (I hope).
at 2a132, after Interrupt settings, he jump to 2B72E : it's the trackloader.
hmm...not really the same. continue disassembling.
file info is always in A0. track size $18B8...
diffult to follow. at 2b2b4, the sync word is different : $4488

restart load at 2a142 and stop just at the end.
take a look at 60000: "Ice!"
so, at 2a14e, he depack the file. UnICE is at 29F6A.
search occurences of trackloader ($2B72E).
called 6 times : 2A148, 2A20A, 2A234, 2A26E, 2A28A and 2A2B2.
take a look at each.
at 2a284/2A2B2:
seems to select infos in a files table with the file number.
the table start at 29ee6. then jump to D300 with pointers to trackloader, unIce and file info as parameters. at D300 It's certainly the level loader.
when we'll patch the trackloader at $2B72E, no need to patch the loader.
get the files list (11 files).

at this moment, I think we can start ripping these files by using the trackloader directly for example in loader part

at D4BE, put a "btst #6,$bfe001 bne D4BE" (08 39 00 06 00 bf e0 01 66 f6) and change the file info pointer by address of the file you want to download at D4D0. like that, you get the original files unpacked at $50858.
you have the size of the file in the table infos. Save them.

the file 10 seems to be the intro, the file 8 loaded at D300 is the level loader. the files 1 to 7 are certainly the 7 levels.
after ripping, we can take a look at each file.
If file is packed with Ice, you can depack it with XFD. or with the builtin UnIce.
The level 1 is packed but levels are only datas.

to rip the first 2 files (The Credits and the Main program) use the credits loader... Both are packed.

It's also possible to rip the trackloader (from the Credits part) and make your own file grabber.

First Try

making of a first version :
copy the files ripped to a new blank disk. Depending on the trackloader, copy sector by sector or track by track. (we have chance this time, there is enought space on the disk)

For the trackloader, I chose the option to replace it with a new trackloader able to read by sector. (I found one on Aminet or dosloader from Golden Axe).

As the first files are packed, you either depack and repack with another cruncher or Ice, or rip original routine in the credits (unice is from 600a8 to 602b6).

To keep things simple, and use the original depacker, we will patch code after the depacking and before executing (as WHDload Slaves).

It's time to write our starter code and a boot.

the starter program will load and unpack the credits part and main program, replace trackloader, do the patchs and start.
the whole things is more than 1kb, so we need to load it from boot.
copy it just after boot, at sector 2. (Nothing here)

The Boot: only use trackdisk device for loading our little proggy.

dc.b 'DOS',0
dc.l 0
dc.l $370

move.w #2,$1c(a1)
move.l #$20000,$28(a1)
move.l #2*512,$2c(a1) ; offset
move.l #4*512,$24(a1) ;len
movea.l 4.w,a6
jsr -$1c8(a6)
jmp $20000

The starter:
first we load credits:

; load packed credits
move.w #2,$1c(a1)
move.l #$60000,$28(a1)
move.l #1733*512,$2c(a1) ; offset
move.l #20*512,$24(a1)
movea.l 4.w,a6
jsr -$1c8(a6)

move.w #$7fff,$dff09a
move.w #$7fff,$dff09c

movea.l #$60000,a0
moveq #0,d0
bsr Decrunch ; the unice routine

; patch credits - trackload
lea $6009e,a0 ; skip trackloader
move.w #$4ef9,(a0)+ ; jmp
pea loadmain(pc)
move.l (sp)+,(a0)+
; start credits -> download main prog at 23800 / return
jmp $60000

sub rout "loadmain":

lea FileMain(pc),a0
bsr.s Load ; our trackloader
bra continue
dc.l $23800
dc.l 1485*512 ; main program
dc.l $1EE60 ;keep original size or 248*512

the main program is loaded.

movea.l #$23800,a0
moveq #0,d0
bsr Decrunch
; patch main
; patch trackloader
lea $2b72e,a0
lea Load(pc),a1
move.w #$4ef9,(a0)+
move.l a1,(a0)+

now, patch the files infos with their new positions:

; patch files infos
lea $29ee6+4,a0
lea Files(pc),a1
moveq #11-1,d7 ; 11 files
move.l (a1)+,(a0)+ ; offset
lea 8(a0),a0 ; skip addr and len if original len used
dbf d7,.loop

Files ; make your own
dc.l 11*512
dc.l 11*512+sector size

the last thing, copy the trackloader in memory and start:

; do a copy of trackloader
lea Load(pc),a0
lea $23800-sizetk,a1
move.l #sizetk-1,d7
move.b (a0)+,(a1)+
dbf d7,cp

jmp $23800 ; start

... insert "loadmain"

include "trackloader.asm"

; ICE Depacker
; a0=source
include "tokiunice.asm"


(I found the address where I copy trackloader after some tests)
copy the boot and starter, and boot your new release

all work fine

we can play ! let you die. there is the "continue"...lets finish...he load scores.

and reload intro.
restart. play. if you die or if you finished level, the game should load score or level 2. but...Crash! ?!? break with AR.
dump memory at 23000 : our trackloader is no more here!

impossible to replace original trackloader because he is puzzled into the code.
we need to stock the trackloader at a safe place...
fill the space from $300 (sp) to $23800 (start of main program) with a "mark" and test game levels, scores...
take a look at $23000 to see where your mark is erased : from 400 to 236a0
hmm...it's too short for a trackloader, even a tiny one.
we need to make a copy somewhere and test if erased. address $100 is perhaps safer. try.
at $100, it's erase from $24e.
the download buffer for files start at 50858 for levels and 60000 for intro/scores.
the intro file loaded at 60000 (128kb) go until 7FD54 ! level 1 loaded at 50858 go to 7E738.
we have a safe space between 7FD54 and 7FFFE (7FFFF is used) = 682 bytes maximum.
The game use only 512 Kb Chip but almost the whole memory space!

phew, what a problem!
we need a tiny trackloader.
very difficult if we want to keep the 512K max memory. else we can test if extra mem (fast/chip) exists for copying the trackloader.

the smallest trackloader is from Alpha One (404 bytes) : it's only a Trackloader, but it's possible to adapt it for sector reading. (we have 280 bytes left).
or modify original one with standard values. or write your own ;)

change the starter code:

movea.l #$23800,a0
moveq #0,d0
bsr Decrunch
; patch main
; patch trackloader
lea $2b72e,a0
move.w #$4ef9,(a0)+
move.l #$7fd54,(a0) ; we have more space here.

lea load(pc),a0
lea $7fd54,a1
move.l #sizepl-1,d7
move.b (a0)+,(a1)+
dbf d7,.cppl

compile, copy the new starter and restart. It seems better. no more crash.


we have the numlevel at 239C8, we can test each level.

you can also access to the outro : at the start, he checks for level number 8 but it's where he put 1 in levelnum: change it.

at level2 ... screen stay black and "flash" before crash...another bug? a protection ?

levels 3,4,5,6 and 7 works.


so, now we have to find if there are any protections:
restart at level 2 and let's crash and stop. why crash ?
see registers

PC=??? it's not a good address for a jump!
dump stack at 2f8 (register A7):
next line to execute : 3C9EE.
it's from the sub rout at 3C9D8

this rout use registers d0 and d2 to get jump address in A0
err...d0=239c8 (level number)=$A57B x 4=(2)95ec.w ! it's not a good level number
and d2=1917d4 -> A0=xxxxxxxxxx ! bad address

where is modified the levelnum ? certainly not with its real address.

we have to understand what happen before crash : restart level 2.
stop and disassemble before crash. we are at 3c396.

cross backward until you reach the start of the routine : 3c26a
called from 2a40c <- from 2a342 (sub prog 2a2ee) <- from 2a2ce (2a226)

first sub called is 3ca66.

using num level to jump to a routine.
d 3d7c2 (level 1)
d 3da1e (level 2)

check differences between 2 routs

we can see a strange address in A1 : 1EB53 ? not in the range of program.
d1 take its value from a line of code: rts = $4e75
he put d0 in 1EB53+d1=239C8...the level number address !
at 3db06 : a loop where "d0" is computed with values from table 24482-258e6.

D0 need to be Null : NOP the line 3DB06.
and skip the test of D0. change BEQ at 3DBB8 with BRA (=$60) (both is better than only one)


that works !

add the crack of the protection in Starter:

...patch main...
; crack
; skip checksum
lea $3db06,a0
move.w #$4e71,(a0) ; nop
lea $3dbb8,a0
move.b #$60,(a0) ; bra

and test your new release.
If there is a checksum somewhere, why not more ?

test the game.
If you play the first level quickly and continue, no problems.
But if you loose and continue, or wait until the timer reach ~1:00, the Toki Gfx become corrupted.

when you continue, the whole screen is corrupted.

argl! another protection!

I tested level 2 and 3, waiting the gfx bug, sometime more than 6 minutes. nothing seems happen.

The bug appear during the game, not at the start like the level 2.
not simple. hmm...I'm searching for the use of numlevel to jump to the level manager (like level 2).
called always like that:
move.w $239c8,d0 ; 3039000239C8
add.w d0,d0 ; D040
add.w d0,d0
search the bytes: 30 39 00 02 39 C8 D0 40

the last is already known (3CA66 init level with first checksum).
check others:
1 and 4 are not used for a jump.
2,3 and 5 yes.
- nothing at 5 : RTS for level 1
only 2 left!
test: put a RTS at the second (2BC64) and try
oops! we got invulnerability against shot for enemies! not good at all. but gfx bugs are still there.

redo same thing for the third (2EB56). eh! no more enemies nor others moving objects... wait end of timer...no gfx bug !
disassemble the rout at 2EB72 (compare with level 2 for example):
search something odd...

a test and below a modified address.
what is these variables ? already called somewhere?
23A82 (counter) and 23C56 are used at 2C8B2.
the long word at 23AAE already set only and table 3BEB4 are used here. 2C8B2 is called from 2AFBC and 2C842. all these routs seems to be a checksum.
and :

an hidden call to 2C8B2 !

skip the rout : replace bne after tst or beq after sub by bra at 2ef1a or 2ef28)
the game seems ok. and no gfx bug

you can add the patch:

; checksum 2 (in level 1 routine)
lea $2ef1a,a0 ; or $2ef28
move.b #$60,(a0) ; bra

pheww. That's work now.
seems ok... difficult to see this kind of protections.

...enjoy...play... ... plop... a bug ?... after ~8 minutes :

hum... It seems to be a third checkum !
from here, we can see the bitplan address of the game panel at $12AA1518 !
another gfx pointer modified.
after searching, we can see the same kind of checkum test in the music player (called in VBI at 2BFEC).

at 2C25C, test a counter : if not 0, then decrement counter and compute checksum.
if you search the address 26594 (relative address!), you can see it's a bitplan pointer for the game screen!
if counter=0, change bitplan address with checksum value.

desactive it :
put a BRA $2C274 to 2C25C or 2C25E ($6016 or $6014)

update our starter proggy:

; skip check checksum 3 (modify a screen pointer in puma replayer!)
lea $2c25e,a0
move.w #$6014,(a0) ; bra ok1

Play : Seems ok now !

This protection is also used in Liquid Kids and Snow Bros. Take a look at the sources of WHDLoad patch by CFOU! The Checksum+Counter routine is very well explained.


make you own trainer for testing the whole game.
at start of main program, near 2a17e, he set some variables :
we already know the level number at 239c8 (select/skip level).
lives counter (start with 6) at 23CD9 (.b) : try to change it.
search for live counter decrement.
at 2C9F0: 6x nop or replace instruction. a TST for example ($4A)

we can see in each routine, the variable 27E8C is initialized with a longword like "40A0301" for level 1, "50A0301" for level 2, "60A0301" for level 3...
first level timer start at 4:30, level 2 at 6:30, level 3 at 5:30 !!
we have found the timer (unlimited time reached ;) see trainer)
try to change the first byte : it's the minutes value.
search this address: f 02 7e 8c,23800 50858

check each address.
at the first one, he test when minute=0, and decrement at 370CC :
replace the subq.b by TST ($4A).
at the second one, he write the timer on info panel (we can see 4E230 address).
at the third and fourth, he test the timer when all parts are null.
others addresses are the initialization of the timer.

seems to be ok. I hope (not yet fully tested ;) )

2008 (c) Heavy/VrS!/Cyb

Download :
- Cracked disk
- sources of the original boot, patched boot
- Toki The End