Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Day I Reverse Engineered a Gameboy Advance ...

The Day I Reverse Engineered a Gameboy Advance Game (Coding Portugal meetup)

Gameboy Advance was one of the most popular video game platforms of its time and, because of it, many people worked together as a community to study and to document its architecture and develop ROM hacking and other interesting tools.

It turns out that this video game is a fantastic way to start studying reverse engineering: its architecture is very well documented and simpler if compared to the current game console generation – and, of course, it is very fun to work in a game-related project.

So... what do you think of learning reverse engineering through this challenge: developing a level editor for a GBA game called "Klonoa: Empire of Dreams"?
We need to understand the architecture behind ARM hardware, apply reverse engineering in order to discover how the logic of the game works, and then use our front-end knowledge (JS + React.js) to build a level editor.

Bruno Macabeus

October 30, 2019
Tweet

More Decks by Bruno Macabeus

Other Decks in Programming

Transcript

  1. This is the tile ID. In this example, all tiles

    with the ID 9B are exactly the bridge part, which is the left corner. 9A is the ID of the continuous part of the bridge.
  2. This tells us where the tile is stored in the

    memory. In this example, it is at 0600F1AD.
  3. VRAM pulls the tilemap
 containing exactly what the player has

    to see from somewhere else that has the entire thing.
  4. https://www.coranac.com/tonc/text/regbg.htm It is a feature of computer systems that allows

    certain hardware subsystems to access main system memory (random- access memory), independent of the CPU
  5. DMA3: 03000900 0600E000 80000400 DMA3: 03001100 0600E800 80000400 DMA3: 03004DB0

    0600F000 80000200 DMA3: 03004800 07000000 84000048 The closest to the bytes of our bridge (0600F1AD).
  6. DMA3: 03000900 0600E000 80000400 DMA3: 03001100 0600E800 80000400 DMA3: 03004DB0

    0600F000 80000200 DMA3: 03004800 07000000 84000048 This address is the VRAM data source. It’s located in a region called Fast WRAM.
  7. OAM

  8. DMA3: 03000900 0600E000 80000400 DMA3: 03001100 0600E800 80000400 DMA3: 03004DB0

    0600F000 80000200 DMA3: 03004800 07000000 8400003E We can see exactly the byte that writes on Klonoa’s object (03004800).
  9. DMA3: 03000900 0600E000 80000400 DMA3: 03001100 0600E800 80000400 DMA3: 03004DB0

    0600F000 80000200 DMA3: 03004800 07000000 8400003E We can see exactly the byte that writes on Klonoa’s OAM (03004800).
  10. 1 2

  11. Here is where Y fixes Klonoa’s position Here is where

    which decides if should fix or not Klonoa’s position
  12. Let’s find the tilemap on rom THE DAY I REVERSE

    ENGINEERED A GAMEBOY ADVANCE GAME
  13. Swi

  14. BIOS is a firmware that is intended to initialise the

    physical components of the system
  15. BIOS is a firmware that is intended to initialise the

    physical components of the system In GBA, its BIOS exposes many functions often used in games, including data compression and decompression
  16. BIOS is a firmware that is intended to initialise the

    physical components of the system In GBA, its BIOS exposes many functions often used in games, including data compression and decompression Each function has an associated numeric code, which must be used as a parameter in the SWI statement
  17. Input: How division works on ARM Assembly swi 0x06 R0


    numerator R1
 denominator Output:
  18. Input: How division works on ARM Assembly swi 0x06 R0


    numerator R1
 denominator R0
 numerator / denominator R1
 numerator % denominator R3
 abs(numerator / denominator) Output:
  19. 6E 6E 6E B0 B1 BD 77 AB B3 AB

    B3 7C 6E 6E 6E 6E 6E 6E 6E 6E 7C 6E 6E 6E 6E 7C 6E 6E
  20. 6E 6E 6E B0 B1 BD 77 AB B3 AB

    B3 7C 6E 6E 6E 6E 6E 6E 6E 6E 7C 6E 6E 6E 6E 7C 6E 6E
  21. 6E 6E 6E B0 B1 BD 77 AB B3 AB

    B3 7C 6E 6E 6E 6E 6E 6E 6E 6E 7C 6E 6E 6E 6E 7C 6E 6E 6E 6E 6E B0 B1 BD 77 AB B3 AB B3 7C 6E 6E 6E 6E 6E 6E 6E 6E 7C 6E 6E 6E 6E 7C 6E 6E
  22. 6E 6E 6E B0 B1 BD 77 AB B3 AB

    B3 7C 6E 6E 6E 6E 6E 6E 6E 6E 7C 6E 6E 6E 6E 7C 6E 6E 6E 6E 6E B0 B1 BD 77 AB B3 AB B3 7C 6E 6E 6E 6E 6E 6E 6E 6E 7C 6E 6E 6E 6E 7C 6E 6E
  23. 6E 6E 6E B0 B1 BD 77 AB B3 AB

    B3 7C 6E 6E 6E 6E 6E 6E 6E 6E 7C 6E 6E 6E 6E 7C 6E 6E 6E 6E 6E B0 B1 BD 77 AB B3 AB B3 7C 6E 6E 6E 6E 6E 6E 6E 6E 7C 6E 6E 6E 6E 7C 6E 6E =
  24. 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F 0x1A 0x1B 0x1C 0x1D

    0x1E 0x1F 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F
  25. 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F 0x1A 0x1B 0x1C 0x1D

    0x1E 0x1F 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F
  26. x x x x a x x x x x

    x x x b x x x x I’m on tile a
  27. x x x x a x x x x x

    x x x b x x x x I’m on tile a
  28. x x x x a x x x x x

    x x x b x x x x I’m on tile a x x x x a x x x x x x x x b x x x x From the tile a to b there are 9 bytes (= 9 tiles), so the length of the tilemap is exactly 9
  29. const Jimp = require('jimp') const { drop } = require('ramda')

    const fs = require('fs') const data = drop(3, fs.readFileSync('dump/level-2/tilemap'))
  30. const getPixelColor = hexTile "# { if (hexTile === 0)

    { return 0x00000000 } return 0xFFFFFFFF }
  31. new Jimp(300, 600, (err, image) "# { let x =

    0 let y = 0 for (let i = 0; i < data.length; i += 1) { const value = data[i] x += 1 if (x === 300) { y += 1 x = 0 } let color = getPixelColor(value) image.setPixelColor(color, x, y) } image.write('image.png') })
  32. export default { rom: { tilemap: [0x1B27FC, 0x1B36F3], customTilemap: [0x367700,

    0x3686AF], objects: [0xE2B90, 0xE2F59], portals: [0xD48C8, 0xD48EF], }, tilemap: { totalStages: 3, height: 60, width: 420, scheme: [ { name: 'grass', ids: [0x7D, 0x80, 0x81, 0x82, 0x8E, 0x8F, 0x90],
  33. export default { rom: { tilemap: [0x1B27FC, 0x1B36F3], customTilemap: [0x367700,

    0x3686AF], objects: [0xE2B90, 0xE2F59], portals: [0xD48C8, 0xD48EF], }, tilemap: { totalStages: 3, height: 60, width: 420, scheme: [ { name: 'grass', ids: [0x7D, 0x80, 0x81, 0x82, 0x8E, 0x8F, 0x90],
  34. export default { rom: { tilemap: [0x1B27FC, 0x1B36F3], customTilemap: [0x367700,

    0x3686AF], objects: [0xE2B90, 0xE2F59], portals: [0xD48C8, 0xD48EF], }, tilemap: { totalStages: 3, height: 60, width: 420, scheme: [ { name: 'grass', ids: [0x7D, 0x80, 0x81, 0x82, 0x8E, 0x8F, 0x90],
  35. { name: 'rock', ids: [0x54, 0x53, 0x55, 0x56, 0x57, 0x58,

    0x59, }, { name: 'darkRock', ids: [0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, }, { name: 'wood', ids: [0x52, 0x94, 0x98, 0x99, 0x9A, 0x9B, 0x9D, }, { name: 'bridgeRope', ids: [0x04, 0x05, 0x0B, 0x0C, 0x11, 0x13, 0x14,
  36. class HuffmanDecodeError extends Error { constructor (e) { super(`Error when

    tried to use huffman decoder: ${e}`) this.name = 'HuffmanDecodeError' } } huffmanModule.onRuntimeInitialized = () "# {} const huffmanDecode = (buffer) "# { FS.writeFile('file', buffer)
  37. class HuffmanDecodeError extends Error { constructor (e) { super(`Error when

    tried to use huffman decoder: ${e}`) this.name = 'HuffmanDecodeError' } } huffmanModule.onRuntimeInitialized = () "# {} const huffmanDecode = (buffer) "# { FS.writeFile('file', buffer)
  38. const huffmanDecode = (buffer) "# { FS.writeFile('file', buffer) huffmanModule._HUF_Decode() try

    { return huffmanModule.FS.readFile('file', { encoding: 'binary' }) } catch (e) { throw new HuffmanDecodeError(e) } }
  39. function docker_run_emscripten { local filename="$1" echo "Compiling $filename$$." docker run

    \ --rm -it \ -v $(pwd)/scissors/src/wasm:/src \ trzeci/emscripten \ emcc -s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s EXTRA_EXPORTED_RUNTIME_METHODS=[\"FS\"] -s EXPORT_NAME=\"$filename\" -o ./$filename.js $filename.c } docker_run_emscripten huffman docker_run_emscripten lzss
  40. function docker_run_emscripten { local filename="$1" echo "Compiling $filename$$." docker run

    \ --rm -it \ -v $(pwd)/scissors/src/wasm:/src \ trzeci/emscripten \ emcc -s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s EXTRA_EXPORTED_RUNTIME_METHODS=[\"FS\"] -s EXPORT_NAME=\"$filename\" -o ./$filename.js $filename.c } docker_run_emscripten huffman docker_run_emscripten lzss
  41. Second Level Third Level First Level Second Level Third Level

    > Customised First Level > Changes in the first level
  42. Second Level Third Level First Level Second Level Third Level

    > Customised First Level > Changes in the first level
  43. The switch between ARM and Thumb must be done by

    bx instruction. And this instruction only can read a register
  44. Instructions to load a level . . . . .

    . . . . Very big hole at the end of cartridge First Level__ Second Level__ Third Level__ 081B27FC 081B3E5C 081B50AC Instructions to load a level
  45. First Level__ Second Level__ Third Level__ . . . .

    . . . . . 081B27FC 081B3E5C 081B50AC Instructions to load a level Customised First Level__ Customised level loader Constant addresses map table Hole Customised Second Level__ Hole Customised Third Level__ Hole Hole 08367700 083686B0 08369380
  46. Instructions to load a level 08367620 FC 27 1B 08

    08367624 00 77 36 08 08367628 5C 3E 1B 08 0836762C B0 86 36 08 08367620 AC 50 1B 08 08367634 80 93 36 08 First Level__ Second Level__ Third Level__ . . . . . . . . . 081B27FC 081B3E5C 081B50AC Customised First Level__ Customised level loader Constant addresses map table Hole Customised Second Level__ Hole Customised Third Level__ Hole Hole 08367700 083686B0 08369380
  47. Instructions to load a level 08367620 FC 27 1B 08

    08367624 00 77 36 08 08367628 5C 3E 1B 08 0836762C B0 86 36 08 08367620 AC 50 1B 08 08367634 80 93 36 08 First Level__ Second Level__ Third Level__ . . . . . . . . . 081B27FC 081B3E5C 081B50AC Customised First Level__ Customised level loader Constant addresses map table Hole Customised Second Level__ Hole Customised Third Level__ Hole Hole 08367700 083686B0 08369380
  48. Instructions to load a level 08367620 FC 27 1B 08

    08367624 00 77 36 08 08367628 5C 3E 1B 08 0836762C B0 86 36 08 08367620 AC 50 1B 08 08367634 80 93 36 08 First Level__ Second Level__ Third Level__ . . . . . . . . . 081B27FC 081B3E5C 081B50AC Customised First Level__ Customised level loader Constant addresses map table Hole Customised Second Level__ Hole Customised Third Level__ Hole Hole 08367700 083686B0 08369380
  49. Instructions to load a level 08367620 FC 27 1B 08

    08367624 00 77 36 08 08367628 5C 3E 1B 08 0836762C B0 86 36 08 08367620 AC 50 1B 08 08367634 80 93 36 08 First Level__ Second Level__ Third Level__ . . . . . . . . . 081B27FC 081B3E5C 081B50AC Customised First Level__ Customised level loader Constant addresses map table Hole Customised Second Level__ Hole Customised Third Level__ Hole Hole 08367700 083686B0 08369380
  50. Instructions to load a level (patched) First Level__ Second Level__

    Third Level__ . . . . . . . . . 081B27FC 081B3E5C 081B50AC Customised First Level__ Customised level loader Constant addresses map table Hole Customised Second Level__ Hole Customised Third Level__ Hole Hole 08367700 083686B0 08369380 Original loader Patched loader 08043B0A mov r4,r0 08043B0C bl 08367610h 08043B10 bl 0805143Ch 08043B0A mov r4,r0 08043B0C add r0,r5,4 08043B0E mov r1,r4 08043B10 bl 0805143Ch
  51. Instructions to load a level (patched) First Level__ Second Level__

    Third Level__ . . . . . . . . . 081B27FC 081B3E5C 081B50AC Customised First Level__ Customised level loader Constant addresses map table Hole Customised Second Level__ Hole Customised Third Level__ Hole Hole 08367700 083686B0 08369380 R0 is the pointer to where is the tilemap that will load Original loader Patched loader 08043B0A mov r4,r0 08043B0C bl 08367610h 08043B10 bl 0805143Ch 08043B0A mov r4,r0 08043B0C add r0,r5,4 08043B0E mov r1,r4 08043B10 bl 0805143Ch
  52. Instructions to load a level (patched) First Level__ Second Level__

    Third Level__ . . . . . . . . . 081B27FC 081B3E5C 081B50AC Customised First Level__ Customised level loader Constant addresses map table Hole Customised Second Level__ Hole Customised Third Level__ Hole Hole 08367700 083686B0 08369380 Original loader Patched loader 08043B0A mov r4,r0 08043B0C bl 08367610h 08043B10 bl 0805143Ch 08043B0A mov r4,r0 08043B0C add r0,r5,4 08043B0E mov r1,r4 08043B10 bl 0805143Ch
  53. Instructions to load a level (patched) First Level__ Second Level__

    Third Level__ . . . . . . . . . 081B27FC 081B3E5C 081B50AC Customised First Level__ Customised level loader Constant addresses map table Hole Customised Second Level__ Hole Customised Third Level__ Hole Hole 08367700 083686B0 08369380 Original loader Patched loader 08043B0A mov r4,r0 08043B0C bl 08367610h 08043B10 bl 0805143Ch 08043B0A mov r4,r0 08043B0C add r0,r5,4 08043B0E mov r1,r4 08043B10 bl 0805143Ch
  54. Instructions to load a level (patched) First Level__ Second Level__

    Third Level__ . . . . . . . . . 081B27FC 081B3E5C 081B50AC Customised First Level__ Customised level loader Constant addresses map table Hole Customised Second Level__ Hole Customised Third Level__ Hole Hole 08367700 083686B0 08369380 08367610 mov r0,r15 08367612 add r0,3Ch 08367614 bx r0
  55. Instructions to load a level (patched) First Level__ Second Level__

    Third Level__ . . . . . . . . . 081B27FC 081B3E5C 081B50AC Customised First Level__ Customised level loader Constant addresses map table Hole Customised Second Level__ Hole Customised Third Level__ Hole Hole 08367700 083686B0 08369380 08367650 add r0,r4,4h 08367654 ldr r4,[r15,#-3Ch] 08367658 cmp r0,r4 0836765C ldreq r0,[r15,#-40h] 08367654 ldr r4,[r15,#-40h] 08367658 cmp r0,r4 0836765C ldreq r0,[r15,#-44h] 08367654 ldr r4,[r15,#-44h] 08367658 cmp r0,r4 0836765C ldreq r0,[r15,#-48h] 08367660 mov r4,r1 08367664 bx r14
  56. Instructions to load a level (patched) First Level__ Second Level__

    Third Level__ . . . . . . . . . 081B27FC 081B3E5C 081B50AC Customised First Level__ Customised level loader Constant addresses map table Hole Customised Second Level__ Hole Customised Third Level__ Hole Hole 08367700 083686B0 08369380 08367650 add r0,r4,4h 08367654 ldr r4,[r15,#-3Ch] 08367658 cmp r0,r4 0836765C ldreq r0,[r15,#-40h] 08367654 ldr r4,[r15,#-40h] 08367658 cmp r0,r4 0836765C ldreq r0,[r15,#-44h] 08367654 ldr r4,[r15,#-44h] 08367658 cmp r0,r4 0836765C ldreq r0,[r15,#-48h] 08367660 mov r4,r1 08367664 bx r14
  57. Instructions to load a level (patched) First Level__ Second Level__

    Third Level__ . . . . . . . . . 081B27FC 081B3E5C 081B50AC Customised First Level__ Customised level loader Constant addresses map table Hole Customised Second Level__ Hole Customised Third Level__ Hole Hole 08367700 083686B0 08369380 08367650 add r0,r4,4h 08367654 ldr r4,[r15,#-3Ch] 08367658 cmp r0,r4 0836765C ldreq r0,[r15,#-40h] 08367654 ldr r4,[r15,#-40h] 08367658 cmp r0,r4 0836765C ldreq r0,[r15,#-44h] 08367654 ldr r4,[r15,#-44h] 08367658 cmp r0,r4 0836765C ldreq r0,[r15,#-48h] 08367660 mov r4,r1 08367664 bx r14
  58. Instructions to load a level (patched) First Level__ Second Level__

    Third Level__ . . . . . . . . . 081B27FC 081B3E5C 081B50AC Customised First Level__ Customised level loader Constant addresses map table Hole Customised Second Level__ Hole Customised Third Level__ Hole Hole 08367700 083686B0 08369380 08367650 add r0,r4,4h 08367654 ldr r4,[r15,#-3Ch] 08367658 cmp r0,r4 0836765C ldreq r0,[r15,#-40h] 08367654 ldr r4,[r15,#-40h] 08367658 cmp r0,r4 0836765C ldreq r0,[r15,#-44h] 08367654 ldr r4,[r15,#-44h] 08367658 cmp r0,r4 0836765C ldreq r0,[r15,#-48h] 08367660 mov r4,r1 08367664 bx r14
  59. Instructions to load a level (patched) First Level__ Second Level__

    Third Level__ . . . . . . . . . 081B27FC 081B3E5C 081B50AC Customised First Level__ Customised level loader Constant addresses map table Hole Customised Second Level__ Hole Customised Third Level__ Hole Hole 08367700 083686B0 08369380 08367650 add r0,r4,4h 08367654 ldr r4,[r15,#-3Ch] 08367658 cmp r0,r4 0836765C ldreq r0,[r15,#-40h] 08367654 ldr r4,[r15,#-40h] 08367658 cmp r0,r4 0836765C ldreq r0,[r15,#-44h] 08367654 ldr r4,[r15,#-44h] 08367658 cmp r0,r4 0836765C ldreq r0,[r15,#-48h] 08367660 mov r4,r1 08367664 bx r14
  60. 1. Redirect to the patched code 2. Check if the

    R0 is on the table 3. If yes, load the associated value 4. Return to the original loader
  61. 1. Redirect to the patched code 2. Check if the

    R0 is on the table 3. If yes, load the associated value 4. Return to the original loader
  62. 1. Redirect to the patched code 2. Check if the

    R0 is on the table 3. If yes, load the associated value 4. Return to the original loader
  63. 1. Redirect to the patched code 2. Check if the

    R0 is on the table 3. If yes, load the associated value 4. Return to the original loader
  64. 1. Redirect to the patched code 2. Check if the

    R0 is on the table 3. If yes, load the associated value 4. Return to the original loader
  65. 1st load: ˜3 1.8 seconds! 2nd load: ˜13 3.6 seconds!

    Perf recap: Removing wasted renders and other React optimisation stuff.
  66. 1st load: ˜3 1.8 1.6 seconds! 2nd load: ˜13 3.6

    2.8 seconds! Perf recap: Canvas → WebGL
  67. 1st load: ˜3 1.8 1.6 0.625 seconds! 2nd load: ˜13

    3.6 2.8 0.536 seconds! Perf recap: React → WebGL
  68. DMA3: 03000900 0600E000 80000400 DMA3: 03001100 0600E800 80000400 DMA3: 03004DB0

    0600F000 80000200 DMA3: 03004800 07000000 8400003E Update the OAM
  69. DMA3: 03000900 0600E000 80000400 DMA3: 03001100 0600E800 80000400 DMA3: 03004DB0

    0600F000 80000200 DMA3: 03004800 07000000 8400003E Update the OAM
  70. DMA3: 03000900 0600E000 80000400 DMA3: 03001100 0600E800 80000400 DMA3: 03004DB0

    0600F000 80000200 DMA3: 03004800 07000000 8400003E Update the OAM 0300480A:0300480B ✘
  71. C

  72. ROM OAM of the first level 08367700 083686B0 08369380 ROM

    OAM of the third level ROM OAM of the second level Hole Hole . . .
  73. const [ xFirstPosition, yFirstPosition, xSecondPosition, ySecondPosition, xThirdPosition, yThirdPosition, xFourthPosition, yFourthPosition,

    xFifthPosition, yFifthPosition, kind, ] = qunpack.unpack('v2 x4 v2 x4 v2 x4 v2 x4 v2 x5 c', bytes);
  74. const [ xFirstPosition, yFirstPosition, xSecondPosition, ySecondPosition, xThirdPosition, yThirdPosition, xFourthPosition, yFourthPosition,

    xFifthPosition, yFifthPosition, kind, ] = qunpack.unpack('v2 x4 v2 x4 v2 x4 v2 x4 v2 x5 c', bytes); ✘
  75. const { xStage1, yStage1, xStage2, yStage2, xStage3, yStage3, xStage4, yStage4,

    xStage5, yStage5, kind, } = binary.parse(memory) .word16lu('xStage1').word16lu('yStage1') .skip(4) .word16lu('xStage2').word16lu('yStage2') .skip(4) .word16lu('xStage3').word16lu('yStage3') .skip(4) .word16lu('xStage4').word16lu('yStage4') .skip(4) .word16lu('xStage5').word16lu('yStage5') .skip(5) .word8lu('kind') .vars