Reverse Engineering the Past : Copy Protection in School Fighter

Reverse Engineering the Past : Copy Protection in School Fighter

bgb00052

Here’s a summary of a quick and fun weekend project. For tl:dr people : Download the IPS file and run it together with your School Fighter ROM dump.

bgb00006  bgb00003 bgb00004

@inobscurity wanted to try the unlicensed HK game School Fighter for the GBC, but the ROM dump would hang before the first level. I had just set up my new workspace, so I decided to a peek into the situation.

Upon pressing start / passing the attract mode, the screen is stuck on a white screen, but it doesn’t show any symptom of crashing (no warnings, etc). A quick trip to our GBC debugger of choice shows the culprit:

ROM3:4045 21 62 41 ld hl,4162  // load HL with 4162
(...)
// below this is the part we are interested
ROM3:4058 21 0A C6 ld hl,C60A  // load HL with C60A
ROM3:405B CB 34    swap h      // turn it into 6C0A
ROM3:405D 7C       ld a,h      // A = 6C
ROM3:405E E6 F0    and a,F0    // A = 60
ROM3:4060 67       ld h,a      // HL = 600A
ROM3:4061 CB 35    swap l      // turn HL into 60A0 
ROM3:4063 7D ld    a,l         // A = A0
ROM3:4064 E6 F0    and a,F0    // A = A0
ROM3:4066 D6 20    sub a,20    // A - 20 = 80
ROM3:4068 6F ld    l,a         // HL = 6080
ROM3:4069 3E 65    ld a,65     // A = 65
ROM3:406B 22       ldi (hl),a  // "store" 65 in 6080 (HL increases by 1)
ROM3:406C 2D       dec l       // HL - 1= 6080
ROM3:406D C6 3B    add a,3B    // A = 65 + 3B = A0
ROM3:406F 67       ld h,a      // HL = A080
ROM3:4070 2A       ldi a,(hl)  // load A from A080
ROM3:4071 E6 0F    and a,0F    // A AND 0F
ROM3:4073 FE 05    cp a,05     // if A != 05...
ROM3:4075 20 CE    jr nz,4045  // ...loop to 4045

You can see that the code (after some obfuscation) attempts to write to the memory position 0x6080, and then to read data from 0xA080. These values are uncommon for two reasons:

  1. 0x6080 is a location in ROM, so writing there “does nothing”
  2. 0xA080 is a SRAM location, but this cart does not declare SRAM or has a save feature. As such, reading from it is undetermined (but let’s assume it’s 0xFF)

What the code does seem to point is that it’s attempting to write a value that then reading at another location (apparently the same value or at least the last 4 bits of it). Leaving that for now, this results in a infinite loop because A != 0x05. We assume this loop can safely be removed as there is no reason to call the “game related” part of the loop more than once. Sure enough, if we skip that jump instruction we get into the actual game. Or maybe not. Once you move the character forward the game then engages in a bit of voluntary resetting. Let’s have look at what is happening before the reset:

ROM0:09F4 21 8E 6F ld hl,6F8E // load HL with 6F8E
ROM0:09F7 7D       ld a,l     // A = 8E
ROM0:09F8 E6 F0    and a,F0   // A AND F0 = 80
ROM0:09FA 6F       ld l,a     // HL= 4180
ROM0:09FB 7C       ld a,h     // A = 41
ROM0:09FC 3E 60    ld a,60    // A = 60
ROM0:09FE 67       ld h,a     // HL = 6080
ROM0:09FF 3E 62    ld a,62    // A = 62
ROM0:0A01 77       ld (hl),a  // "store" 65 in 6080
ROM0:0A02 26 A0    ld h,A0    // HL = A080
ROM0:0A04 2A       ldi a,(hl) // load A from A080 (increase HL)
ROM0:0A05 E6 0F    and a,0F   // A AND 0F
ROM0:0A07 C6 12    add a,12   // A = A + 12
ROM0:0A09 00       nop 
ROM0:0A0A 00       nop 
ROM0:0A0B CD CC 00 call 00CC  // subroutine call

Our old friend 0x6080 is back, but no loop here. This is where things move into copy protection antics. Let’s check that call 00cc – it’s rather simple:

ROM0:00CC F5       push af
ROM0:00CD F6 00    or a,00
ROM0:00CF EA 80 20 ld (2080),a
ROM0:00D2 F1       pop af
ROM0:00D3 C9       ret

It’s a pretty humdrum code: an OR opcode to clear flags, and then a write to 0x2080. Writing to the address 0x20XX is special to the GameBoy hardware – it signals what part of the ROM (split in “pages”) is visible on the 0x4000 – 0x7FFF memory portion. So this will set the page to whatever the value is returned from.

Here’s what is interesting: going back to the code, the value of A is read from 0xA080. Then that value suffers some operations and is used to set the page. As stated before, reading from that memory position is undefined (let’s say 0xFF). So the page is set to 0x0F + 0x12 = 0x21 (this is actually page 0x01 because of gameboy hardware minutia). A bit ahead, there is a read from a 0x4000 memory position:

(BC = 0x04 but it's a very long sequence so it's skipped)
ROM0:0A3A 21 00 40 ld hl,4000 // HL = 4000
ROM0:0A3D 09       add hl,bc  // HL = HL + BC = 4004
ROM0:0A3E 2A       ldi a,(hl) // load A from 4004 (HL increases)
ROM0:0A3F 66       ld h,(hl)  // load H from 4005
ROM0:0A40 6F       ld l,a     // L = A 
ROM0:0A41 11 46 0A ld de,0A46 // DE = 0A46
ROM0:0A44 D5       push de    // store DE in the stack
ROM0:0A45 E9       jp hl      // jump code to whatever value we loaded from 4004
ROM0:0A46 E1       pop hl     // HL = 0A46

Without any modification the jump here goes to 0xFFFF, an invalid address that triggers a reset of the cart. The possibility of skipping the jump like before seems like a good one, but by having the wrong page we also misplaced half of the game code, and the game will crash for some other reason.

To solve this we have to look again to the first loop. The intention is to check if 0xA080 holds a value ended in 5 if you write 0x65. So let’s say that the cart has some “memory” that stores that value. We alter the code so that the game stores and retrieves the value in Game Boy memory – the WRAM value 0xCB80 was not in use, so let’s go with it:

ROM3:4058 21 0A CA ld hl,BC0A  // load HL with BC0A
ROM3:405B CB 34    swap h      // turn it into CB0A
ROM3:405D 7C       ld a,h      // A = CB
ROM3:405E E6 FF    and a,FF    // A = CB
ROM3:4060 67       ld h,a      // HL = CB0A
ROM3:4061 CB 35    swap l      // turn HL into CBA0 
ROM3:4063 7D ld    a,l         // A = A0
ROM3:4064 E6 F0    and a,F0    // A = A0
ROM3:4066 D6 20    sub a,20    // A - 20 = 80
ROM3:4068 6F ld    l,a         // HL = 6080
ROM3:4069 3E 65    ld a,65     // A = 65
ROM3:406B 22       ldi (hl),a  // "store" 65 in 6080 (HL increases by 1)
ROM3:406C 2D       dec l       // HL - 1 = 6080
ROM3:406D C6 3B    add a,66    // A = 65 + 66 = CB
ROM3:406F 67       ld h,a      // HL = CB80
ROM3:4070 2A       ldi a,(hl)  // load A from CB80
ROM3:4071 E6 0F    and a,0F    // A AND 0F
ROM3:4073 FE 05    cp a,05     // if A != 05...
ROM3:4075 20 CE    jr nz,4045  // ...loop to 4045

Since the value is now written in RAM, we can read the value 0x65 and hence no loop happens. So, let’s do the same with the later code:

ROM0:09F4 21 8E 6F ld hl,6F8E // load HL with 6F8E
ROM0:09F7 7D       ld a,l     // A = 8E
ROM0:09F8 E6 F0    and a,F0   // A AND F0 = 80
ROM0:09FA 6F       ld l,a     // HL= 6F80
ROM0:09FB 7C       ld a,h     // A = 6F
ROM0:09FC 3E CB    ld a,CB    // A = CB
ROM0:09FE 67       ld h,a     // HL = CB80
ROM0:09FF 3E 62    ld a,62    // A = 62
ROM0:0A01 77       ld (hl),a  // "store" 62 in CB80
ROM0:0A02 26 A0    ld h,CB    // HL = CB80
ROM0:0A04 2A       ldi a,(hl) // load A from CB80 (increase HL)
ROM0:0A05 E6 0F    and a,0F   // A AND 0F
ROM0:0A07 C6 12    add a,12   // A = A + 12
ROM0:0A09 00       nop 
ROM0:0A0A 00       nop 
ROM0:0A0B CD CC 00 call 00CC  // subroutine call

This way, the value 0x62 is saved in that RAM byte, and then loaded back, and 0x02 + 0x12 means we would access page 14. But surprise, the jump is again to the FFFF address! This isn’t the right page at all!

So, we have a non-standard “memory” write that would lock up a Game Boy, a lot of attempts to obscure the actual values written to, and now the value we thought it should retrieve isn’t! Yep, this is a copy protection method. Luckily the way it was made allows us to reverse engineer it too.

As I mentioned, the key part is that the “page” in memory is correct. The code grabs the last 4 bits of a value (so a maximum of 0x0F), then adds 0x12 to it, and this means that there are only 16 possible options (0x12+0x00 to 0x21). The reality is that we can ignore 0x20 and 0x21 (because of reasons mentioned above), so 14 values we can manually check. By changing the page until the game advanced, it was found that the proper call is to page 0x13, and hence the value retrieved should be 0x01. As we already went to the work of writing the value 0x62 to RAM, we can fix it by writing 0x61 instead!

ROM0:09FF 3E 61    ld a,61    // A = 61

Phew! This lets us move ahead to the fighting and hitting people part, but there is yet another loop hidden (triggered once you are knocked out). By this point, the author was much less inventive:

ROM0:3ABF 26 CB    ld h,60
ROM0:3AC1 2E 80    ld l,80    // HL = 6080
ROM0:3AC3 3E 4A    ld a,4A
ROM0:3AC5 77       ld (hl),a  // write 4A to 6080
ROM0:3AC6 26 CB    ld h,A0    // HL = A080
ROM0:3AC8 2A       ldi a,(hl) // read from A080
ROM0:3AC9 E6 0F    and a,0F
ROM0:3ACB FE 0A    cp a,0A    // if A != 0
ROM0:3ACD 20 E3    jr nz,3AB2 // loop to 3AB2

Short and to the point. Those are easily fixed by changing the h register writes to CB. This is then repeated in a few more other locations, with some minor changes to the results.

So, first let’s remember this is a unlicensed game, made at low cost and dating back to the start of the XXIst century. It’s unlikely the company building the carts wanted to use a lot of resources doing their carts, but wanted to have _some_ custom hardware to prevent copying. So the copy protection actually adds 5-6 logic gates and a buffer to create a “key” that returns different values to what it’s written for. Given some patience (and if I ever get a machine that runs GBC games), I’ll get back to it and fully reverse engineer it – might be possible that other “bad” dumps from the same developer are use the same sort of protection.

bgb00048