MFM + checksums
MFM + checksums
- 1st release IPF #40
- blank disk for AR dump
- blank disk for final Toki release
get the 1st release. It's a non-dos disk. no password.
first, check the disk :
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 Bootyep, 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 _copy: move.l (a0)+,(a1)+ dbf d0,_copy illegal ; start in Supervisor modethe program start in Trace mode. 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)
now we access to the hardware custom trackloader, the only one able to read this disk.
TrackLoader: 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 DiskReadyhere, 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 :
it's seems to be the file information : certainly load buffer, start track, length.
let's continue to the load loop
retry clr.w try-vars(a5) Load: move.l starttrk-vars(a5),d7 bsr SeekTrack move.w #$8210,$96-$24(a6) ; DMACON enable DMA Disk .loopload: 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 .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 .looploadthe 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:
SeekTrack: 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 _greater: sf head-vars(a5) subi.w #80,d1 ; track on side 1 _less: move.b d1,d7 move.b head-vars(a5),d0 beq.s Side1 bset #2,(a4) ; DSKSIDE side 0 bra.s seekthetrack Side1: bclr #2,(a4) ; DSKSIDE side 1 seekthetrack: jsr (a5) ; cpu delay tst.b track-vars(a5) ; track >=0 bge.s seekForw bsr.s Seek0 seekForw: cmp.b track-vars(a5),d7 ; current track = seek track beq.s seekOk blt.s seekBack bsr.s seekForward bra.s seekIt seekBack: bsr.s seekBackward seekIt: bsr.s DiskReady ; CIAA-PRA - DSKRDY bra.s seekForw seekOk: movem.l (sp)+,d0-d3 rts noTrack0: bsr.s seekBackward Seek0: btst #4,$F01(a4) ; $BFE001 CIAA-PRA - DSKTRK0 bne.s noTrack0 sf track-vars(a5) ; clr rts seekForward: bclr #1,(a4) ; DSKDIR goto cyl 79 addq.b #1,track-vars(a5) ; incr current track bra.s MoveHeads ; move +/-1 track seekBackward: bset #1,(a4) ; DSKDIR goto cyl 0 subq.b #1,track-vars(a5) ; decr current track MoveHeads: bclr #0,(a4) ; DSKSTEP step low bset #0,(a4) ; step highthe 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:
.continue: 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 .ok: 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 .decode: 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 MFMUncodeCheck: 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 .decode: 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 rts .error movem.l (sp)+,d1-a6 moveq #-1,d7 ; error rts MFMchecksumHeader: 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 rts
some others routs:
StopDrive: 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) DiskReady: btst #5,$F01(a4) ; $BFE001 CIAA-PRA - bit DSKRDY bne.s DiskReady rts vars ; start of variables DelaySoft: move.w d7,-(sp) move.w #10000,d7 ; ~7 ms _wait: dbf d7,_wait move.w (sp)+,d7 rts 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 ; sidethe 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 fileshmm...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".
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".
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
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.
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 trymaking 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
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 continue:
sub rout "loadmain":
loadmain: lea FileMain(pc),a0 bsr.s Load ; our trackloader bra continue FileMain dc.l $23800 dc.l 1485*512 ; main program dc.l $1EE60 ;keep original size or 248*512
the main program is loaded.
continue: 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 .loop 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 cp move.b (a0)+,(a1)+ dbf d7,cp jmp $23800 ; start ... insert "loadmain" Load: include "trackloader.asm" sizetk=*-Load ; ICE Depacker ; a0=source Decrunch: include "tokiunice.asm" sizet=*-starter(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 on Flashtro 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:
continue: movea.l #$23800,a0 moveq #0,d0 bsr Decrunch ; patch main ; patch trackloader lea $2b72e,a0 move.w #$4ef9,(a0)+ move.l #$2b2b4,(a0) ; we have more space here. lea patchtrackload(pc),a0 lea $2b2b4,a1 move.l #sizepl-1,d7 .cppl move.b (a0)+,(a1)+ dbf d7,.cppl
sub rout "patchtrackload":
patchtrackload: movem.l d7/a0/a1,-(sp) lea $23800-sizetk,a1 cmpi.w #$48e7,(a1) ; test if trackloader always here! beq.s .ok ; recopy lea $7FD54,a0 ; safe move.l #sizetk-2-1,d7 ; don't copy "current track" variable .cpy move.b (a0)+,(a1)+ dbf d7,.cpy .ok movem.l (sp)+,d7/a0/a1 jmp $23800-sizetk sizepl=*-patchtrackload
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.
CRACKINGso, now we have to find if there are any protections:
restart at level 2 and let's crash and stop. why crash ?
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
search the bytes: 30 39 00 02 39 C8 D0 40
the last is already known (3CA66 init level with first checksum).
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 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.
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.
by curiosity, I know than the modules from Toki are in Puma format. I checked the replay routine from Exotica.
something is different with original one (called from VBI at 2BFEC).
at 2C25C there is a BRA and not the TST.L (A6) !?!
seems to be another checksum!
and if you search the address 26594 (relative address!), you can see it's a bitplan pointer for the game screen! what do a gfx pointer here ?!
I haven't seen any other gfx bugs, but perhaps it's in other part of game.
desactive it like in the puma replayer source - certainly got from the cracked version of Toki! congratulations to the crackers. I don't know how they found this one !
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
I think it's ok now!
feel free to explain how to find the Puma player hidden checksum! and where he is activated.
trainermake 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 ;) )
Filesize: 5KB, downloaded 21 times
Filesize: 5KB, downloaded 21 times