Holiday Hack 2023: Gameboy Hunt and Hack
Overview
Three objectives show up in my badge with no location information on them:
Each just says to find the cartridge and beat the game, with difficulties of 1, 3, and 3 respectively. Each is located on a different island.
Elf The Dwarf’s, Gloriously, Unfinished, Adventure! - Vol1!
Getting To
The first cartridge is located at Tarnished Trove on the Island of Misfit Toys, which is located on the goose’s leg:
Location Layout
Tarnished Trover has a bunch of misfit toys, but nothing super interesting beyond a game cartridge:
Challenge
Dusty Giftwrap stands near the dock with a hint about buried treasure:
Dusty Giftwrap
Arrr, matey, shiver me timbers! There be buried treasure herrrrre.
Just kidding, I’m not really a pirate, I was just hoping it would make finding the treasure easier.
I guess you heard about the fabled buried treasure, too? I didn’t expect to see anyone else here. This uncharted islet was hard to find.
There are 3 buried treasures in total, each in its own uncharted area around Geese Islands.
I’ve been searching for hours now with no luck, and these strange toys are starting to give me the creeps.
Maybe you’ll be able to find it. Here, use my Gameboy Cartridge Detector. Go into your items and test it to make sure it’s still working.
When you get close to the treasure, it’ll start sounding off. The closer you get, the louder the sound.
No need to activate or fiddle with it. It just works!
I bet one of these creepy toys has the treasure, and I’m sure not going anywhere near them!
If you find the treasure, come back and show me, and I’ll tell you what I was able to research about it.
Good luck!
The detector is in my Badge Items:
For what it’s worth, whichever of the three elves at the three cartridge locations I talk to first will provide the detector.
Find Cartridge
When I approach the hat that looks like it may belong to Ed at the top left of the island, the Game Boy Cartridge Detector starts beaping.
Once I walk over it, I get the cartridge in the Items area of my badge:
The button opens the game.
Before playing, I’ll talk again to Dusty:
Dusty Giftwrap
Whoa, you found it!
It’s a… video game cartridge? Coooooollll… I mean, arrrrrr….
So, here’s what my research uncovered. Not sure what it all means, maybe you can make sense of it.
Dusty provides some hints:
- Giving things a little push never hurts.
- Out of sight but not out of ear-shot
- You think you fixed the QR code? Did you scan it and see where it leads?
Game
The game loads with a splash screen that includes the keyboard buttons that map to Game Boy keys:
After a bit of story, I’m sent out as Elf the Dwarf, eventually finding a big area with a QRcode, Kody the dog at the top.
There are 7 blocks out of place in the QRCode. I need to sing (B-button, which is “r” or “x” on the keyboard) which will shoot out music notes, and if one hits a block that needs moving, it will light up and show where it should be:
I’m able to push blocks into place:
It is possible to just play the game without cheating, and once I find and fix all seven, it zooms out and gives (a completely different) QRCode:
The decodes to “8bitelf.com”.
oxdf@hacky$ curl https://8bitelf.com/
<html>
<body>
<p>flag:santaconfusedgivingplanetsqrcode</p>
</body>
</html>
Entering that flag into the badge completes the challenge.
Cheats
Find Bindings
In looking at the game in the dev console, there’s a big block of code starting at line 478 in script.js
that handles binding keys to functions. At the very top, there are keys that aren’t in the help at the top of the screen:
bindKeys() {
this.keyFuncs = {
Backspace: this.keyRewind.bind(this),
" ": this.keyPause.bind(this),
"[": this.keyPrevPalette.bind(this),
"]": this.keyNextPalette.bind(this),
};
Space doesn’t seem to do anything, but the other three do.
Color Palette
”[” and “]” change the color palette. Most of the color palettes change things universally, but some of them have different colors for the blocks that need to be moved. For example, in this section, there are four blocks that need moving with a redish color as opposed to the others in black:
This makes walking through the blocks much easier.
Rewind
The backspace key is mapped to a “rewind” function. If I push a block by accident, rather than starting over, I can hold down backspace to rewind:
Elf The Dwarf’s, Gloriously, Unfinished, Adventure! - Vol2!
Getting To
The second cartridge is located in Driftbit Grotto, which is located on the north side of Pixel Island:
Location Layout
Much like Rainraster Cliffs, the topology presents a 2-d space with movement left and right, and up and down:
Despite the large appearance with lots of levels, the only place I can walk is on the level with the boat and Tinsel.
Challenge
Tinsel tells me about another cartridge located in this cavern:
Tinsel Upatree
I can’t believe I was actually able to find this underground cavern!
I discovered what looked liike an old pirate map in the attic of one of those huts in Rainraster Cliffs, and it actually led somewhere!
But now that I’ve seen where it leads, I think this might’ve been a bad idea. This place is scary! Maybe you want to take it from here?
I’m sure that cartridge is right nearby. Start walking around!
Once you run into it, check back with me and I’ll tell you what I know about winning.
Good luck!
Walking to the left-most point in the map provides the cartridge in the badge’s Items list:
Tinsel is impressed:
Tinsel Upatree
Whoa, you found it!
What version is it?
Did you know that many games had multiple versions released? Word is: volume 2 has 2 versions!
Game
The game starts with some dialog before I walk out into an open field where T-Wiz is waiting by the only gap in the trees:
If I try to walk up through the path, T-Wiz says:
And then my character walks back to where I came.
Versions
Identify
If I load the game a few more times in the browser, I’ll notice that sometimes the world is mirrored:
In this world, I’m trying to go down through a gap, and T-Wiz is blocking.
Source
In the dev console, there’s a script.js
that manages loading the emulator:
binjgb.js
and binjgb.wasm
seem like they are likely this Webassembly Game Boy emulator, which make them less interesting things to study at this point.
At line 140 of script.js
, there’s this function that loads the Game Boy ROM:
// Load a ROM.
(async function go() {
let ranNum = Math.round(Math.random()).toString()
let filename = ROM_FILENAME + ranNum + ".gb";
console.log(filename);
let response = await fetch(filename);
let romBuffer = await response.arrayBuffer();
const extRam = new Uint8Array(JSON.parse(localStorage.getItem("extram")));
Emulator.start(await binjgbPromise, romBuffer, extRam);
emulator.setBuiltinPalette(vm.palIdx);
})();
ROM_FILENAME
is set to “rom/game” at line 10. Math.random()
will get a number between 0 and 1, and then Math.round()
will round that to 0 or 1. If I watch in the Network tab and refresh a few times, I’ll see it download rom/game0.gb
and rom/game1.gb
.
Comparison
I’ll download both files to my VM and compare them. Both are (unsurprisingly) Game Boy ROM images:
oxdf@hacky$ file game?.gb
game0.gb: Game Boy ROM image: "VOL" (Rev.01) [MBC5+RAM+BATT], ROM: 1Mbit, RAM: 256Kbit
game1.gb: Game Boy ROM image: "VOL" (Rev.01) [MBC5+RAM+BATT], ROM: 1Mbit, RAM: 256Kbit
Both are exactly the same size:
oxdf@hacky$ stat -c %s game?.gb
131072
131072
A trick I love to do binary comparisons takes advantage of the process substitution operator in bash
, <()
. Whatever command is inside the ()
is run, and the results are handled as if they were in a file. So I’ll run diff <(xxd file1) <(xxd file 2)
. This will create hex dumps of each file, and then compare the results and show the lines that are different.
For these ROMs, it’s not very much:
oxdf@hacky$ diff <(xxd game0.gb) <(xxd game1.gb )
21c21
< 00000140: 0000 0000 3030 001b 0203 0033 0142 71b3 ....00.....3.Bq.
---
> 00000140: 0000 0000 3030 001b 0203 0033 0142 7186 ....00.....3.Bq.
90c90
< 00000590: 5405 050b 4b9a 2300 0000 0000 06ad 4210 T...K.#.......B.
---
> 00000590: 5405 05d2 ac3d 2d00 0000 0000 06ad 4210 T....=-.......B.
5801c5801
< 00016a80: 2080 0c80 0300 000f f807 0000 0000 0f10 ...............
---
> 00016a80: 2080 0c80 0b00 000f f807 0000 0000 0f10 ...............
5804c5804
< 00016ab0: 0000 0000 2000 0600 0900 000f f807 0000 .... ...........
---
> 00016ab0: 0000 0000 2000 0600 0600 000f f807 0000 .... ...........
6089c6089
< 00017c80: 0200 fe80 002a 0013 fffe fffb 13ff ffff .....*..........
---
> 00017c80: 0100 fe80 002a 0013 fffe fffb 13ff ffff .....*..........
6225,6226c6225,6226
< 00018500: 1204 2103 c60d 5701 1400 00ff fc14 0280 ..!...W.........
< 00018510: fffd 140b 80ff fe35 fffc 3200 fffc 2703 .......5..2...'.
---
> 00018500: 1204 2103 c60d 5701 1400 00ff fc14 0300 ..!...W.........
> 00018510: fffd 1404 00ff fe35 fffc 3200 fffc 2703 .......5..2...'.
Bypass Wizard
I’ll show two ways to hack this ROM and get past T-Wiz.
Swap Redirection
So little is different between the two ROMs, it must be that the differences above are what determine which map I get. It could be that both worlds exist, and something above is a flag that tells the game which to use. Or both maps exist, and the data that’s different defines starting positions for my Elf and T-Wiz.
I’ll create a copy of game0.gb
and try playing with it in a hex editor. The bytes at offset 0x17c80 is just a 02 vs a 01. I’ll change the 02 in my game0-mod.gb
to the value from game1.gb
, 01 and save it. I’ll load the modified ROM in an emulator (I’m using bgb, which is a Windows .exe
, but with wine
runs on Linux as well). When I load the ROM, it shows a warning:
The game loads fine, and when I get to T-Wiz, he still says the same thing, but then my character is forced up through the gap:
In fact, it won’t let me walk back to the bottom, instead pushing me up to the top.
Modify String
For some reason, just messing with the “You shall not pass!!!” string is enough to have T-Wiz not block the path. I’ll create a fresh copy of the game0.gb
(called game0-mod.gb
) and open it in a hex editor. I’ll find the string and remove the “not”, adding nulls to the end to make sure not to change the overall length of the binary:
Now T-Wiz doesn’t say anything and I walk right on through.
Decode Morse
On the other side there’s a portal:
Entering leads to a room with two items. On the left, ChatNPT says:
Interacting with the radio stops the music and starts a series of long and short beeps. After about 15 seconds, there’s a longer break, and then it loops.
I’ll record the beeps and upload it to a morse code decoder site:
The flag is “gl0ry”.
Elf The Dwarf’s, Gloriously, Unfinished, Adventure! - Vol3!
Getting To
The third cartridge if located on Steampunk Island, at Rust Quay on the goose’s backside under a wing:
Location Layout
The area is almost P shaped, with a rusty maze making up the top part:
Challenge
Angel Candysalt tells me about the Gameboy Cartridge treasure:
Angel Candysalt
The name’s Angel Candysalt, the great treasure hunter!
A euphemism? No, why do people always ask me that??
Anyways, I came here to nab the treasure hidden in this ship graveyard, only to discover it’s protected by this rusted maze.
That must be why all these old ships are here. Their crew came to find the treasure, only to get lost in the labyrinth.
At least it’s obvious where this one is. See that shiny spot over to the right? That’s gotta be where it is! If only I had a bird’s eye view.
But how to get there? Up? Down? Left? Right? Oh well, that’s your problem now!
Come back if you can find your way to it, and I’ll tell you some secrets I’ve heard about this one.
Get Cartridge
Locate Cartridge
The cartridge is relatively easy to find, as it’s visible in the screen while I’m talking to Angel:
Using the TamperMonkey script I wrote last year also shows it’s coordinates, and that it’s basically at the same row as Angel:
Navigate Maze
Navigating the maze is a bit trickier. I’ll open the browser dev tools and go to the network tab. There, I’ll select only “img”:
I’ll refresh the page, and in the body it’ll show all the HTTP requests for images:
spi-rustyquay_floor.png
seems like a good starting place. Double-clicking it opens https://2023.holidayhackchallenge.com/images/fabric/spi-rustyquay_floor.png
in a new tab where I get the raw image. I’ll open it in a image editor and draw out the path from the game cartridge back out:
Once I navigate the maze and get close, the beep noise from the detector is quite loud:
When I pick up the item, it shows up under Items in my badge:
More From Angel
Angel is impressed:
Angel Candysalt
The life of a treasure hunter isn’t easy, but it sure is exciting!
Oh it’s a video game, I love video games! But you’ve claimed this treasure, nicely done.
Now, about those secrets I’ve been told. They’re pretty cryptic, but they are. Hopefully that helps with something!
These challenges don’t have to be solved in order, but because I now have all three, Angel adds:
Angel Candysalt
You have all three? Wow, you must be the greatest treasure hunter that ever lived!
Talking to Angel unlocks three hints:
- This one is a bit long, it never hurts to save your progress!
- 8bit systems have much smaller registers than you’re used to.
- Isn’t this great?!? The coins are OVERFLOWing in their abundance.
Game
Overview
This time, Elf is in a 2-D left/right scroll game with coins and little monsters:
Jumping on the little monsters kills them, though running into them kills you. The coins are worth either 1, 10, or 100 coins, which updates the counter at the bottom left.
After three stretches of coins and monsters, I meet Jared:
At the end of this stretch, there’s just an empty void (and jumping into it just sends Elf back to the start):
999 Coins
It seems clear I need 999 coins. The challenge is that when I try to collect these honestly, when I get the coin that would put me at 999, an error message comes up:
Then my coins are set to 0:
Hacking
Find Coin Addresses
bgb, the Gameboy emulator I’m using, has a “cheat searcher” function that’s really neat (there’s a nice walkthrough on this page in the documentation). The idea is that you start with all the memory values, and then over time filter out ones that don’t match my expectation. Given that these are 8-bit registers, I’m going to look for three bytes, each controlling ones, tens, and hundreds.
I’ll start the game with 000 coins, and find “cheat searcher” in the right-click menu:
The window has a big open space with buttons at the bottom:
I’ll select 8-bit values and click start. All the bytes from memory and their values are populated into the space:
I’ll note there are tons of values here (see the size of the scrollbar). It doesn’t have to be true, but I’ll start by assuming that 0 coins means some byte has 0 value. So I’ll set the “keep values which are” to “equal to” and then “this value: 0”, and click search:
There’s still a lot, but less. I’ll play the game and grab a coin, giving me 001. I’ll update “this value:” to 1, and click search:
There are only six bytes in memory that have followed my requirements. I’ll avoid the 10 and 100 coins, and grab some more 1 coins, updating each time, until there are only two bytes left:
I can do the same thing with the 10- and the 100-coins, until I have six bytes, two sets of three, that seem to correspond to the coins:
ones | tens | hundreds |
---|---|---|
0xC0F8 | 0xC12C | 0xC160 |
0xCBA2 | 0xCB9C | 0xCB9E |
A lot of experimentation leads me to think that the first set is something associated with the display, whereas the second is the actual value of coins. Having just the first hacked is not enough to win the game, but just the second is.
Fix Coins
I’ll right-click on an address and select “go here in debugger”:
It’s in the dump window at the bottom of the debugger:
Right clicking on the byte offers “Freeze ram address”:
I’ll give it the value of 9, and now it shows up as yellow in the debugger memory:
Changing the first set of addresses above (that I think are related to the display) updates the display immediately, where as just setting the second only updates the next time a coin is collected. I’ll freeze the other two bytes from the second set of addresses as well:
Now when I grab a coin, the counter doesn’t change, but rather stays at 999 (though it may through the error again, but still stays at 999).
Success
I’ll play the game through to the end of the level where the gap was before. It’s very useful to use the memory snapshot features in the emulator. For example, in bgb, F2 will save the current state, and F4 will reload it. So I just save every 10 seconds or so, and if I die, F4 takes me back without much lost progress.
At the end, there’s now a platform where before there was none:
On the other side, there’s a door. Inside I’ll find Tom Listen, ChatNPT, and a heavy rock:
There’s a good bit of dialog with Liston, and then he gives a passphrase to give ChatNPT:
ChatNPT accepts it, and makes the rock movable:
Now I can push it aside and reveal stairs:
Tom joins down the stairs and the flag is shown on the screen: