|
2 | 2 |
|
3 | 3 | Written by [ISSOtm](https://github.com/ISSOtm). |
4 | 4 |
|
| 5 | +::: tip TARGET AUDIENCE |
| 6 | + |
| 7 | +Unlike most resources here, this guide is not very useful to developers or even ROM hackers, but rather to glitch-hunters and exploit developers. |
| 8 | + |
| 9 | +::: |
| 10 | + |
5 | 11 | --- |
6 | 12 |
|
7 | | -## What is this ? |
8 | | -It's a simple technique that allows you to run custom code in most GB/SGB/CGB games, provided you have an ACE exploit. |
| 13 | +## What is it? |
| 14 | + |
| 15 | +*OAM DMA hijacking* is a simple technique that allows you to run custom code in most GB/SGB/CGB games, provided you have an ACE exploit. |
9 | 16 |
|
10 | | -What's the point, then? It's that code ran through DMA Hijacking will be run on every game frame (for most games, at least). |
| 17 | +One would be quick to point out that if you have an ACE exploit, you can already execute custom code. |
| 18 | +So then, what is the point? |
| 19 | +It's that code ran through DMA Hijacking will be run *on every game frame* (for most games, at least). |
11 | 20 |
|
12 | | -## How is it done ? |
13 | | -If you are familiar enough with OAM, you know about that feature called *OAM DMA* that requires a small routine to be ran in HRAM ? |
| 21 | +## How is it done? |
14 | 22 |
|
15 | | -Well, most games copy the routine when starting up and run it on every frame. I encountered some games which don't transfer OAM unless a specific flag is set ; I believe that it is always possible to override this limitation. more on that later. |
| 23 | +If you are familiar enough with [OAM](https://gbdev.io/pandocs/OAM), you may know about a feature called *OAM DMA*. |
16 | 24 |
|
17 | | -But if the routine is modified while the game is running - assuming you modify it fully in-between to VBlanks to prevent a crash, or you temporarily put a RET while modifying - then the game will happily run your custom routine. |
| 25 | +[OAM DMA](https://gbdev.io/pandocs/OAM_DMA_Transfer) is a convenient feature that allows quickly updating the on-screen ["objects"](https://gbdev.io/pandocs/Rendering#objects) (often known as "sprites") quickly—which is especially useful since it typically needs to occur on every frame. |
| 26 | +However, using OAM DMA requires a small routine to be copied to HRAM and then run from there. |
18 | 27 |
|
19 | | -Here is the standard routine, given by Nintendo in the GB programming manual : |
| 28 | +Interestingly, most games only copy the routine when starting up, and then execute it on every subsequent frame. |
| 29 | +But, *if we modified that routine while the game is running*, then the game will happily run the customized routine! |
| 30 | + |
| 31 | +### Patching the code |
| 32 | + |
| 33 | +Here is the standard routine, given by Nintendo in the GB programming manual (using [RGBASM syntax](https://rgbds.gbdev.io/docs/rgbasm.5) and a symbol from [`hardware.inc`](https://github.com/gbdev/hardware.inc)): |
20 | 34 |
|
21 | 35 | ```asm |
22 | | -ld a, OAMBuffer >> 8 |
23 | | -ldh [$FF46], a |
24 | | -ld a, $28 |
| 36 | + ld a, HIGH(OAMBuffer) |
| 37 | + ldh [rDMA], a ; $FF46 |
| 38 | + ld a, 40 |
25 | 39 | DMALoop: |
26 | | -dec a |
27 | | -jr nz, DMALoop |
28 | | -ret |
| 40 | + dec a |
| 41 | + jr nz, DMALoop |
| 42 | + ret |
29 | 43 | ``` |
30 | 44 |
|
31 | | -It's usually placed right at `$FF80`, but this isn't true for every game. |
32 | | -Now, overwriting the routine to place custom code would yield us 10 bytes to perform custom operations, at the cost of sprites. |
33 | | -But we can do better. |
| 45 | +The simplest way to get custom code (let's call it `DMAHook`) executed would be to overwrite the first few bytes with a jump to `DMAHook`: |
34 | 46 |
|
35 | | -```asm |
36 | | -call DMAHook |
37 | | -ldh [$FF00+c], a |
38 | | -ld a, $28 |
| 47 | +```asm{1} |
| 48 | + jp DMAHook |
| 49 | + db $46 ; Leftover operand byte of `ldh [rDMA], a` |
| 50 | + ld a, 40 ; None of this is executed |
39 | 51 | DMALoop: |
40 | | -dec a |
41 | | -jr nz, DMALoop |
42 | | -ret |
| 52 | + dec a |
| 53 | + jr nz, DMALoop |
| 54 | + ret |
43 | 55 | ``` |
44 | 56 |
|
45 | | -Allows us to make the perfect compromise ! |
| 57 | +Now, overwriting the routine like this works for our purposes, but comes with a large drawback: the routine isn't doing what it is intended to anymore, and so the game's objects won't update (unless you manually copied OAM, but beware of [the OAM corruption bug](https://gbdev.io/pandocs/OAM_Corruption_Bug)). |
| 58 | +Further, it's not possible to write to `rDMA` from `DMAHook`, as the write and subsequent wait loop **must** be executed from HRAM. |
| 59 | + |
| 60 | +But, there is a solution. |
| 61 | + |
| 62 | +```asm{1-2} |
| 63 | + call DMAHook |
| 64 | + ldh [c], a ; A write to `rDMA`, set up by DMAHook |
| 65 | + ld a, 40 |
| 66 | +DMALoop: |
| 67 | + dec a |
| 68 | + jr nz, DMALoop |
| 69 | + ret |
| 70 | +``` |
| 71 | + |
| 72 | +Provided that `DMAHook` returns with properly set registers, this allows writing to `rDMA` in the single HRAM byte left by the `call` instruction. |
46 | 73 | Here is a pattern for DMAHook : |
47 | 74 |
|
48 | 75 | ```asm |
49 | 76 | DMAHook: |
50 | | -[ custom code, do whatever you want, it's VBlank time ! ] |
51 | | -ld c, $46 |
52 | | -ld a, OAMBuffer >> 8 |
53 | | -ret |
| 77 | + ;; Custom code, do whatever you want, it's VBlank time! |
| 78 | + ; ... |
| 79 | + ld c, LOW(rDMA) ; $46 |
| 80 | + ld a, HIGH(OAMBuffer) |
| 81 | + ret |
54 | 82 | ``` |
55 | 83 |
|
56 | | -DMAHook can be anywhere (in WRAM, mostly). It will be executed in the context of the VBlank interrupt, so for most games interrupts will be disabled, etc. |
57 | | -An alert reader will notice the new DMA handler modifies C (whereas the original simply zeroes A). I don't know any game whose behavior is altered by this. |
| 84 | +`DMAHook` can live anywhere in memory, but typically it will be in WRAM. |
| 85 | +It will be executed in the context of the VBlank interrupt, so for most games interrupts will be disabled, etc. |
58 | 86 |
|
59 | | -DMA hijacking is also useful when combined with [cartswap](https://gist.github.com/ISSOtm/3008fd73ec66cb56f1caecfcc8b6fb6f) (swapping carts without shutting the console down, concept found by furrtek, developed by Cryo and me on the GCL forums), because it allows porting ACE to other games. |
| 87 | +## With Cartswap |
60 | 88 |
|
61 | | -General procedure : |
| 89 | +DMA Hijacking is also useful when combined with [cartswap](https://gist.github.com/ISSOtm/3008fd73ec66cb56f1caecfcc8b6fb6f) (swapping carts without shutting the console down, concept found by furrtek, developed by Cryo and me on the GCL forums), because it allows "transporting" ACE to other games. |
62 | 90 |
|
63 | | -- Acquire ACE in the donor game |
64 | | -- Perform cartswap, insert the recipient game |
65 | | -- Pseudo-initialize the recipient (clear enough memory to avoid crashing, while keeping our custom code in an unused region of memory we don't clear) |
66 | | -- Place the modified DMA handler in HRAM |
67 | | -- Transfer control back to the recipient's ROM |
68 | | -- ???? |
69 | | -- Profit. |
| 91 | +General procedure: |
70 | 92 |
|
71 | | -[Video demonstration, performed by Torchickens/ChickasaurusGL in BGB](http://youtu.be/BNyDmZlbsNI) |
| 93 | +1. Acquire ACE in the "source" game |
| 94 | +1. Perform cartswap, insert the "victim" game |
| 95 | +1. "Pseudo-initialize" the victim |
| 96 | +1. Place the modified DMA handler in HRAM |
| 97 | +1. Transfer control back to the victim's ROM |
| 98 | +1. ???? |
| 99 | +1. Profit! |
72 | 100 |
|
73 | 101 | Possible applications are checking for a button combo to trigger specific code (for example, credits warp), checking one or multiple memory addresses to detect a certain game state, etc. |
74 | 102 |
|
75 | | -Possible "attack vectors", ie ways of affecting the recipient game, are setting certain memory addresses (like GameShark), or even better : manipulating the stack. |
| 103 | +Possible "attack vectors", i.e. ways of affecting the victim game, are setting certain memory addresses (like a GameShark), or even better: manipulating the stack. |
76 | 104 |
|
77 | | -Manipulating the stack with this technique can not crash if the triggering game state is specific enough. I achieved text pointer manipulation in Pokémon Red this way. |
| 105 | +Here is a video demonstration: |
| 106 | +<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/BNyDmZlbsNI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> |
78 | 107 |
|
| 108 | +Manipulating the stack with this technique can not crash if the triggering game state is specific enough. |
| 109 | +I achieved text pointer manipulation in Pokémon Red this way. |
| 110 | +(This is not a ROM hack!) |
| 111 | +<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/yXy5sYZR9mk" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> |
79 | 112 |
|
80 | 113 | ### Details |
81 | | -Here are some details on how to combine DMA hijacking and cartswap to pwn any game. |
82 | 114 |
|
83 | | -First thing you will need is to find some RAM to store the DMA hook code. We'll call it "HookRAM". I recommend checking how much memory is allocated to the stack. |
| 115 | +This new technique hinges on breaking one of any game's core assumptions: its entry point. |
| 116 | +You see, normally, [the console transfers control to the game at address $0100](https://gbdev.io/pandocs/The_Cartridge_Header#0100-0103---entry-point), so any code placed there is designed to initialize all of the game's systems, in particular their memory. |
84 | 117 |
|
85 | | -Then : |
86 | | -- Clear as much RAM as needed for the game to run properly |
87 | | -- Copy the DMA hook code to HookRAM |
88 | | -- Copy the hijacked DMA routine to HRAM |
89 | | -- Emulate all game initialization up to right before DMA routine copy / HookRAM clearing |
90 | | -- Jump back to ROM |
| 118 | +However, since we have control of the CPU, we can jump to any location in the game's ROM, which allows bypassing some of said initialization. |
| 119 | +Doing so without any precautions is very likely to go haywire, though—it is important to initialize *enough* that the game runs, but not *too much* that it would end up overwriting the code we are trying to inject. |
| 120 | +This is what I call "**pseudo-initialization**". |
91 | 121 |
|
| 122 | +Another important part is finding some free space to store the hook code in. |
| 123 | +The stack area can work surprisingly well for this, as many games appear to over-allocate (e.g. 256 bytes when the typical usage doesn't go beyond 32). |
92 | 124 |
|
93 | | -## Trivia |
94 | | -DMA hijacking works similarly to the GameShark : it detected when the GB tried reading from the VBlank interrupt vector, and responded with instructions that applied the codes. |
| 125 | +None of this has a silver bullet: the game's init code must be analyzed, and its memory usage carefully scrutinized in order to dig up enough free space for your hook. |
95 | 126 |
|
96 | | -And yep, it is possible to use DMA hijacking to emulate GameShark codes. I have a PoC in Pokémon Red (a BGB save state), if anyone's interested. |
| 127 | +## Trivia |
97 | 128 |
|
98 | | -[Demo video](http://gbdev.gg8.se/forums/viewtopic.php?id=430). |
| 129 | +DMA hijacking works similarly to the GameShark: that device intercepts accesses to the ROM, and when it detects that the VBlank handler is being run, it "overlays" different instructions that apply the stored codes, and jump back to the actual handler. |
| 130 | + |
| 131 | +And, why yes, it is possible to use DMA hijacking to emulate GameShark codes! |
| 132 | +[Here is a proof-of-concept in Pokémon Red](http://gbdev.gg8.se/forums/viewtopic.php?id=430). |
| 133 | + |
| 134 | +## Notes |
| 135 | + |
| 136 | +- I encountered some games that don't transfer OAM unless a specific flag is set; I believe that it is always possible to override this limitation, by setting the flag back in the hook. |
| 137 | +- The OAM DMA routine is often placed at $FF80 in commercial games. |
| 138 | +- The patched OAM DMA routine with our hook may be modifying registers that the game expects to be preserved. |
| 139 | + This is all dependent on the target game, so no general advice can be given. |
| 140 | + |
| 141 | + Additionally, if the hook takes too long, it may cause code expecting to run in VBlank to break. |
| 142 | + This might be solved for example by manipulating the stack and injecting an additional return address; here is an example. |
| 143 | + ```asm |
| 144 | + jp DMAHook |
| 145 | + PostDMAHook: |
| 146 | + ldh [c], a |
| 147 | + ld a, 40 |
| 148 | + DMALoop: |
| 149 | + dec a |
| 150 | + jr nz, DMALoop |
| 151 | + jp hl |
| 152 | + ``` |
| 153 | + ```asm |
| 154 | + pop hl ; Get original return address |
| 155 | + ld bc, PostHandlerHook ; Address of code that will be executed once the VBlank handler finishes |
| 156 | + push bc ; Inject return address for VBlank handler |
| 157 | + ld c, LOW(rDMA) |
| 158 | + ld a, HIGH(OAMBuffer) |
| 159 | + jp PostDMAHook |
| 160 | + ``` |
| 161 | + (Since the handler almost certainly performs some `pop`s before returning, you will almost certainly need more complex stack manipulation, but that's the gist of it.) |
| 162 | +- Some games have a slightly more clever routine in HRAM, that omits the initial `ld a, HIGH(OAMBuffer)` saving 2 bytes of HRAM. |
| 163 | + ```asm |
| 164 | + ldh [rDMA], a |
| 165 | + ld a, 40 |
| 166 | + DMALoop: |
| 167 | + dec a |
| 168 | + jr nz, DMALoop |
| 169 | + ret |
| 170 | + ``` |
| 171 | + They can still be patched by overwriting the `ld a, 40` instead, and using e.g. the `b` register for the loop: |
| 172 | + ```asm{1-2,4} |
| 173 | + call DMAHook |
| 174 | + ldh [c], a ; Write to rDMA |
| 175 | + DMALoop: |
| 176 | + dec b |
| 177 | + jr nz, DMALoop |
| 178 | + ret |
| 179 | + ``` |
| 180 | + Then `DMAHook` needs to return with `b` additionally set to 40: |
| 181 | + ```asm{4-5} |
| 182 | + DMAHook: |
| 183 | + ;; Custom code, do whatever you want, it's VBlank time! |
| 184 | + ; ... |
| 185 | + ld bc, 40 << 8 | LOW(rDMA) ; 40 in B, $46 in C |
| 186 | + ld a, HIGH(OAMBuffer) |
| 187 | + ret |
| 188 | + ``` |
| 189 | + However, if the OAM buffer address passed to the function (in `a`) is not static, `push af` and `pop af` will have to be used instead of `ld a, HIGH(OAMBuffer)`. |
0 commit comments