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

The Day I Reverse Engineered a Gameboy Advance Game

The Day I Reverse Engineered a Gameboy Advance Game

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.

Matheus Albuquerque

August 17, 2019
Tweet

More Decks by Matheus Albuquerque

Other Decks in Programming

Transcript

  1. """

  2. 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.
  3. This tells us where the tile is stored in the

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

    to see from somewhere else that has the entire thing.
  5. 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
  6. 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).
  7. 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.
  8. OAM

  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. 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).
  11. 1 2

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

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

    ENGINEERED A GAMEBOY ADVANCE GAME
  14. Swi

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

    physical components of the system
  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
  17. 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
  18. Input: How division works on ARM Assembly swi 0x06 R0


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


    numerator R1
 denominator R0
 numerator / denominator R1
 numerator % denominator R3
 abs(numerator / denominator) Output:
  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
  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. 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 =
  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. 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F 0x1A 0x1B 0x1C 0x1D

    0x1E 0x1F 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F
  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
  29. 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
  30. const Jimp = require('jimp') const { drop } = require('ramda')

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

    { return 0x00000000 } return 0xFFFFFFFF }
  32. 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') })
  33. export default { rom: { tilemap: [0x1B27FC, 0x1B36F3], oam: [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], oam: [0xE2B90,

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

    0xE2F59], portals: [0xD48C8, 0xD48EF], }, tilemap: { totalStages: 3, height: 60, width: 420, scheme: [ { name: 'grass', ids: [0x7D, 0x80, 0x81, 0x82, 0x8E, 0x8F, 0x90], },
  36. { 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,
  37. import React from 'react' const ROMContext = React.createContext() export default

    ROMContext import React from 'react' const VisionContext = React.createContext() export default VisionContext klo-gba.js/brush/src/context/ROMContext.js klo-gba.js/brush/src/context/VisionContext.js
  38. import React from 'react' const ROMContext = React.createContext() export default

    ROMContext import React from 'react' const VisionContext = React.createContext() export default VisionContext klo-gba.js/brush/src/context/ROMContext.js klo-gba.js/brush/src/context/VisionContext.js
  39. const ROMProvider = (props) "# { const [romBuffer, setROMBuffer] =

    useROMBuffer() return ( <ROMContext.Provider value={{ romBufferMemory: romBuffer.memory, romBufferStatus: romBuffer.status, setROMBuffer, }} > {props.children} </ROMContext.Provider> ) }
  40. const SelectVision = () "# { const { romBufferMemory }

    = useContext(ROMContext) const { updateVision, vision: { state: visionState }, visionIndex, visionWorld, } = useContext(VisionContext) const setNewVision = (newVision) "# { const [newVisionWorld, newVisionIndex] = newVision.split('-') updateVision(romBufferMemory, newVisionWorld, newVisionIndex) }
  41. const SelectVision = () "# { const { romBufferMemory }

    = useContext(ROMContext) const { updateVision, vision: { state: visionState }, visionIndex, visionWorld, } = useContext(VisionContext) const setNewVision = (newVision) "# { const [newVisionWorld, newVisionIndex] = newVision.split('-') updateVision(romBufferMemory, newVisionWorld, newVisionIndex) }
  42. import { useContext, useEffect } from 'react' import VisionContext from

    '../context/VisionContext' const useWhenVisionChanges = (callback) "# { const { visionIndex, visionWorld } = useContext(VisionContext) useEffect(callback, [visionIndex, visionWorld]) } export default useWhenVisionChanges
  43. const LoadedRom = () "# { const { vision }

    = useContext(VisionContext) const [highlightCoordinates, setHighlightCoordinates] = useState([-1, -1]) const [optShowGrid, setOptShowGrid] = useState(false) const [optShowOAM, setOptShowOAM] = useState(true) const [optShowPortals, setOptShowPortals] = useState(true) const [toolState, setToolState] = useTool() const [resolution, setResolution] = useState(1) useWhenVisionChanges(() "# { setHighlightCoordinates([-1, -1]) }) $( $$.
  44. const LoadedRom = () "# { const { vision }

    = useContext(VisionContext) const [highlightCoordinates, setHighlightCoordinates] = useState([-1, -1]) const [optShowGrid, setOptShowGrid] = useState(false) const [optShowOAM, setOptShowOAM] = useState(true) const [optShowPortals, setOptShowPortals] = useState(true) const [toolState, setToolState] = useTool() const [resolution, setResolution] = useState(1) useWhenVisionChanges(() "# { setHighlightCoordinates([-1, -1]) }) $( $$.
  45. import { useState } from 'react' const useTool = ()

    "# { const [{ currentTool, toolsValue }, setToolsStates] = useState({ currentTool: 'magnifyingGlass', toolsValue: { brush: null, magnifyingGlass: null, }, })
  46. const updateToolState = (tool, newToolState) "# { if (newToolState ))*

    undefined) { setToolsStates({ currentTool: tool, toolsValue: { $$.toolsValue, [tool]: newToolState, }, }) return } setToolsStates({ currentTool: tool, toolsValue, }) }
  47. const huffmanWasm = require('./wasm/huffman.wasm') const huffmanModule = require('./wasm/huffman.js')({ locateFile (path)

    { if (path.endsWith('.wasm')) { return `static/wasm/${huffmanWasm}` } return path }, })
  48. 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)
  49. 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)
  50. const huffmanDecode = (buffer) "# { FS.writeFile('file', buffer) huffmanModule._HUF_Decode() try

    { return huffmanModule.FS.readFile('file', { encoding: 'binary' }) } catch (e) { throw new HuffmanDecodeError(e) } }
  51. 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
  52. 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
  53. 1st load: ˜3 1.8 seconds! 2nd load: ˜13 3.6 seconds!

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

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

    3.6 2.8 0.536 seconds! Perf recap: React → WebGL