Softlock


As promised, this month I'll be concentrating on Softlock, so go dig out an old Firebird game (preferably Chimera because I'm doing that as an example) and then come back. (Scuttle scuttle.)
Got it? (Yep. Ed) Then off we go...


The Basic Bit
First up, *Load and *List as usual...

"chimera" LINE 0 LEN 355
0 BORDER 0: INK 0: PAPER 0: CLS: PRINT AT 21,11;"*LOADING*": POKE 20107,255:
RANDOMIZE USR (PEEK 23627+256*PEEK 23628)
1 SAVE "CHIMERA" LINE0
...so we see it runs from 23923, which is 5D73 hex.
5D73 LD   IYL,A
5D75 DEC  SP
5D76 DEC  SP
5D77 POP  BC
5D78 LD   HL, 0000
5D7B PUSH HL
5D7C POP  IX
5D7E LD   A,2E
5D80 LD   IXH,40
5D83 SLA  A
5D85 LD   D,(IX+0)
5D88 LD   E,(IX+1)
5D8B INC  IX
5B8D INC  IX
5D8F ADD  HL,DE
5D90 CP   IXH
5D92 JR   NZ.5D85
This checks the screen (IXH=40 hex, which is the start of the screen area), so put a breakpoint at 5D94, return to Basic the GOTO 0 (because the screen is set up by the Basic). When control returns to the disassembler, BC is 5D73 and DE is CA4E. These 2 values are used by the decrypter which follows...
5D94 EX   DE,HL
5D95 LD   HL,003D
5D98 ADD  HL,BC
5D99 LD   IXH,B
5D9B LD   IXL,C
5D9D LD   C,32
5D9F LD   A,(HL)
5DA0 XOR  E
5DA1 ADD  A,C
5DA2 LD   (HL),A
5DA3 LD   C,A
5DA4 INC  HL
5DA5 INC  DE
5DA6 LD   A,(HL)
5DA7 XOR  D
5DA8 ADD  A,C
5DA9 LD   (HL),A
5DAA LD   C,A
5DAB INC  HL
5DAC INC  DE
5DAD CP   48
5DAF LD   A,A
5DB0 JR   NZ,5D9F
This decrypts 2 bytes at a time, starting at 5DB0 (the JR NZ instruction). When it comes to cracking it in a routine, we'll move it to somewhere convenient, stick the JR NZ on the end and run it from there. As it is, firstly single-step through it, then move 5D9F-5DB1 to somewhere, stick a breakpoint on the end and run it from there.
When finished, you'll see the following code at 5DB2...
5DB2 LD   SP,0000
5DB5 LD   (5C3D),SP
5DB9 LD   HL,0556
5DBC LD   DE,FF00
5DBF LD   B,H
5DC0 LD   C,L
5DC1 LDIR
5DC3 LD   H,FF
5DC5 LD   DE,007E
5DC8 ADD  IX,DE
5DCA PUSH IX
5DCC POP  DE
5DCD LD   B,05
5DCF LD   A,(DE)
5DD0 LD   L,A
5DD1 LD   A,(HL)
5DD2 SRL  (HL)
5DD4 SRL  (HL)
5DD6 SUB  (HL)
5DD7 LD   (HL),A
5DD8 INC  DE
5DD9 DJNZ 5DCF
5DDB LD   B,17
5DDD LD   A,(DE)
5DDE INC  DE
5DDF LD   L,A
5DE0 LD   A,(DE)
5DE1 INC  DE
5DE2 LD   (HL),A
5DE3 DJNZ 5DDD
5DE5 XOR  A
5DE6 LD   L,A
5DE7 DEC  A
5DE8 LD   IX,4000
5DEC LD   DE,1C00
5DEF SCF
5DF0 JP   (HL)
Some of this code will be new to you, but what it does is to make a copy from the ROM loader (at 0556) at FF00, by the LDIR at the start. It then uses a table to change some of the timing constants so that it turboloads (which is what the rest of the code does). Finally, it sets IX and DE to load from 4000-5C00 (the screen and a bit of code) and off it goes.
FF00 INC  D
FF01 EX   AF,AF'
FF02 DEC  D
FF03 DI
FF04 LD   A,0F
FF06 OUT  (FE),A
FF08 LD   HL,5B00
FF0B PUSH HL
This is the start of the ROM loader, and how it works is unimportant. All you need to know is that the PUSH HL at FF0B PUSHes the return address for when loading has finished, which in this case is 5B00. To find the code at 5B00 (remember it hasn't been loaded yet), change the 5B00 at FF09 to somewhere convenient, where you have placed a breakpoint. Once loaded, the code at 5B00 looks a bit like this...
5B00 DEC  SP
5B01 DEC  SP
5B02 CALL FF70
5B05 LD   A,L
5B06 LD   IXL,A
5B08 CALL FF70
5B0B LD   A,L
5B0C LD   IXH,A
5B0E PUSH IX
5B10 CALL FF70
5B13 LD   A,L
5B14 LD   IXL,A
5B16 CALL FF70
5B19 LD   A,L
5B1A LD   IXH,A
5B1C LD   A,IXL
5B1E OR   IXH
5B20 RET  Z
5B21 POP  DE
5B22 JP   FF70
This code loads 4 bytes, and treats them as new values of IX and DE. These new values then get loaded as another headerless block (like Powerload). The DEC SP: DEC SP at the start ensures that this routine is always what control is returned to once the block was loaded. Unless one of the following happens... To find out which of these it is, we are going to write a simple routine which will load those values and store them elsewhere, and which will load code at 5B00 but nowhere else.
FE00 LD   IX,4000
FE03 LD   DE,1C00
FE07 SCF
FE08 LD   HL,FE11
FE0B LD   (FF09),HL
FE0E JP   FF00
FE11 LD   A,(5B24)
FE14 CP   FF
FE16 JR   Z,FE1B
FE18 {breakpoint}
FE1B LD   A,28
FE1D LD   (5B23),A
FE20 LD   A,FE
FE22 LD   (5B24),A
FE25 JP   5B00
FE28 LD   (FEF0),SP
FE2C LD   SP,(FEF2)
FE30 PUSH IX
FE32 PUSH DE
FE33 LD   (FEF2),SP
FE37 LD   SP,(FEF0)
FE3B LD   A,IXH
FE3D CP   5B
FE3F JR   NZ,FE4E
FE41 LD   A,DD
FE43 LD   (FF58),A
FE46 LD   A,75
FE48 LD   (FF59),A
FE4B JP   FF70
FE4E XOR  A
FE4F LD   (FF58),A
FE52 LD   (FF59),A
FE55 JP   FF70
Before using this routine, POKE 65266,254 so that you know where the stack is. To find out where the game loads to...
10 FOR F=65020 TO 0 STEP -4: IF PEEK F THEN PRINT PEEK
(F+2)+256*PEEK (F+3);",";PEEK F+256*PEEK (F+1): NEXT F
The program will give you this...
56320,6232
61000,2000
64900,400
23296,100
65455,48
23324,2
39936,16384
23324,5
23296,256
63000,800
64000,1000
23552,16384
23324,2
23296,92
As you can see, 23296 is loaded over a few times, but loading continues. We can therefore assume that these blocks do not alter the code there in any way, or at least if they do, not sufficiently enough to worry about. Loading finishes when the block of 92 bytes was loaded, so this must be different.
One disassembly later...
5B00 XOR  A
5B01 OUT  (FE),A
5B03 LD   HL,F870
5B06 LD   DE,F870
5B09 LD   BC,9470
5B0C LD   IX,5AFF
5B10 LD   A,FF
5B12 LD   R,A
5B14 LD   A,(HL)
5B15 SUB  (IX+0)
5B18 XOR  IYL
5B1A RLCA
5B1B XOR  IYH
5B1D LD   (DE),A
5B1E DEC  HL
5B1F DEC  DE
5B20 DEC  BC
5B21 DEC  IX
5B23 LD   A,(IXH)
5B25 OR   IXL
5B27 JR   NZ,5B2C
5B29 LD   IXH,5A
5B2C LD   A,B
5B2D OR   C
5B2E JR   NZ,5B14
5B30 LD   HL,F8D4
5B33 LD   DE,5B01
5B36 LD   BC,00FF
5B39 LD   SP,5FB4
5B3C PUSH HL
5B3D LD   HL,5B00
5B40 LD   A,C9
5B42 LD   (HL),A
5B43 LDIR
This routine firstly decrypts the game, then sets the stack pointer and PUSHes the return address for the game (the PUSH at 5B3C), then fills the printer buffer with RETs. To stick POKEs in, simply move them down into 5B3D, then stick a RET at the end to start the game.


The Chimera Hack
This routine loads the Basic, then moves the decrypter to a convenient address. Once there, the JR NZ at the end is put in manually, then the entry values are put in and it is CALLed. It then puts a RET at the end of the routine which creates the turboloader and CALLs it, abd once in memory the return address is patched and it starts loading. After each short headerless and leaderless block is loaded, it checks a value in the printer buffer to check whether or not the game decrypter is there - if it is then loading must have finished and the infy lives POKEs are stuck on the end of the decrypter. Otherwise, control is returned to 5B00 so that the next block can be loaded. The routine is ORGed (Oo-er. Ed) to 63801, because this is a safe place which never gets loaded over (as can be seen by the table of the load addresses). Note that before the game decrypter is run, the hacking routine is deleted, because the game is decrypted through it.
       ORG  63801
LOAD   LD   IX,#5CCB
       LD   DE,355
       LD   A,#FF
       SCF
       CALL #566               ;load basic with a standard headerless load
       JR   NC,LOAD            ;go back if load unsuccesful
       LD   HL,#5D99           ;start of decrypter
       LD   DE,#4600           ;bung it in the screen because it will be safe
       LD   BC,#17             ;length of decrypter
       LDIR                    ;copy it down
       EX   DE,HL              ;HL is now the end of the copy
       LD   (HL),#20           ;20 is code for jr nz
       INC  HL                 ;point to next address
       LD   (HL),#ED           ;offset for the jr nz
       INC  HL                 ;point ot next address
       LD   (HL),#C9           ;stick a ret on the end
       LD   HL,#5DB0           ;initial value of HL
       LD   BC,#5D73           ;initial value of BC
       LD   DE,#CA4E           ;initial value of DE
       CALL #4600              ;do the decrypter
       LD   A,#C9              ;C9 is code for ret
       LD   (#5DF0),A          ;stick a ret at the end of the turboload creator
       CALL #5DB9              ;create the turboload
       LD   HL,NEWRET          ;patch in a new return address
       LD   (#FF09),HL         ;the patch is at FF09
       LD   SP,0               ;initial value of SP
       JP   #FF00              ;start loading
NEWRET LD   A,(#5B32)          ;see if there's any code here
       CP   #F8                ;check if the byte at 5B32 is a F8
       JP   NZ,#5B00           ;if not, load another block
       LD   HL,POKES           ;otherwise copy the pokes down
       LD   DE,#5B3D
       LD   BC,END-POKES
       LDIR
       JP   #5B3D+DELETE-POKES ;need to delete this routine before decrypting
                               ;the game
POKES  XOR  A                  ;A=
       0   LD
       H,A   LD                L,A;HL=A=0
       LD   (#E6EE),A          ;infy time poke
       LD   (#EE20),HL         ;infy food poke
       LD   (#EDF1),A
       LD   (#EF9C),HL         ;infy water pokes
       RET                     ;to the game
DELETE LD   HL,63801           ;start of this routine
       LD   DE,63802           ;next byte
       LD   BC,END-63801       ;length of the routine
       LD   (HL),0             ;put a 0 in at the start
       LDIR                    ;and delete the rest of it
       JP   #5B00              ;you can now decrypt the game
END    EQU  $