# TheZZAZZGlitch's April Fools Event 2025 Once again a year has passed - and I am back at my second go at these hacking challenges. While I was a beginner the first time, by now I've become an amateur glitch scientist. At this point, I have to thank ZZAZZ for dumping me into this rabbit hole I no longer want to escape. Needless to say, this year was a blast as well! Unfortunately, I was feeling kind of sick and unable to take the challenge on the first day, so it was already Friday afternoon when I first sat down and went for it. ## The main game I could have gone for the hack immediately, but my goal was to beat the 22 challenges with as little cheating as possible. So I grabbed myself a [Kanto Map](https://blog.vjeux.com/wp-content/uploads/2023/12/pokemon_blue-1.png) and went off, to enter every room in the entire game. This was a good way to get to know Gen1 games better since I never really played them. I could talk about every NPC, but I'm gonna highlight a few key ones: ### #8 The train puzzle (Celadon) I decided to solve this one with a quick program since doing it with math or graphs seemed complex. In the end, it still only took 10 minutes to solve. [**Link to code**](https://gist.github.com/WernerderChamp/4d86c48b2d2f15fcc7fa44abe8964515) ### #18 Diglet Cave I knew that #18 was a fun-based one, but I had no clue which it was. Multiple options existed, especially the meme-NPCs in some cities or the "Post-Endpoint". Where the #10 was indicated by a gap in the trees that isn't normally there, #18 did not have such an indication - it could've just been an undertale joke. Especially since resetting a few times didn't do it for me. While I found the fun value in the code by now, it still took a hint for this one to find the correct room and set it accordingly. Further semi-interesting ones: * **#3 - Touch Grass (nugget bridge)**: I really liked the idea of this one! * **#4 - Sprite Quiz in Viridian**: This little fella almost messed up my run because I somehow managed to walk past this only accidentally found it later while messing with #18. * **#7 - Mr. Q (after victory road)**: I could have gone for the name rater. Unfortunately, programmers are lazy. * **#9 - HM guy in front of power plant**: I don't know if I would have figured that out if I hadn't grabbed the HM05 in Celadon before. * **#10 Pokemon on Route 2**: This one only appeared with a 1/5 chance. I got lucky on the second attempt thankfully. * **#16 Dancer in Pewter**: Thank god for emulator slowdown^^ * **#21 Name rater guy**: I never figured out the ball puzzle near Cycling Road. But I compensated for that by knowing by heart what address to modify to which value ### Goofs There were some oversights such as usable PCs in Cinnebar Lab, the Celedon mansion, and SilphCo (Floor 11). Swapping boxes would break the code. There were also some forgotten items such as the TM43 in Victory Road, which I thought I had to give to the scientist in Pellet town. After all, Sky Attack could indeed remind you of Missingno. Oh, and there was a TM10 in SilphCo. Who doesn't think of the ZZAZZ glitch when hearing this move? SilphCo also had an unbeaten trainer. I also swept the floor with the elite four ... and borked the save because entering the HOF removes the injection ZZAZZ set. Maybe block this off next time. Also, the Seafoam dungeon exit, which was overwritten by the level designer, triggered the anti-cheat and could've used a block. But overall, this was a high-effort event once again. I am hoping we will see the source code for this event - one day I might create a hacked save as well. ## The gem Judging the leaderboard, the gem was a really good beginner-level hacking challenge, hats off! While it obviously didn't take me that long to just modify my X position to move the map next to me, many others also got this one. I got the idea from a ZZAZZ video where he retrieved an OOB nugget with it. Unfortunately I was not able to find this video again. Oh, and I was somehow too stupid to enter the password (probably a typo) on the website and thought there was some sort of secondary challenge to this. Doh! By now it was Sunday 3 am already, and the following day, it was finally time to get going for the hardest challenge. ## The password At first, I took a look at the dialogue triggers, which are stored at $DE30 following. After all the entire thing runs in a textbox's ASM command. After accidentally solving the gem again by modifying hSpriteIndex, I stumbled upon a suspiscious function at $DAFD which triggers a jump. Turns out I was on the wrong path and just discovered the integrity check which passes, so I get routed to $A70E, which loads and jumps to the pointer to print the text now. Nothing interesting happens here, and after we choose yes, it fades out the audio and calls the actual password game, which sits at $B6CF. This function waits for inputs and modifies the onscreen password. The entered password is stored in the tileMap starting at $C441. The list of available characters sits at $B85A and we just take the next or previous one upon down or up press. The loop had an inaccuracy that kinda triggered me. Normally, when waiting for input (unless you want sub-frame accuracy), you use the halt instruction, which just puts the CPU to sleep until the next interrupt (which in most cases will be vBlank). By this, we save CPU cycles which would equal battery life if we ran on an actual GameBoy. This halt comes at least one instruction too late, so it is ineffective as we will only wait once AFTER a button has been pressed. Also the `ld a,[hJoyInput]` could have been an `ldh a,[hJoyInput]`, saving one byte. ![image](https://hackmd.io/_uploads/SyPOUVvC1l.png) But now we come to the actual challenge, what happens if we press A? This gets us to $B70B, where we play Sound $FF, delay 10 frames and then check if the function at $B7A1 returns zero, in which case we print the correct password message, else the incorrect password message. And this function is where we finally get on the challenge. ### passwordSetup ($B639) The first thing this function does is initialize $DB8A with the sequence 0xE334AD30. Then we copy 256 bytes from $B656 to $C800 and jump there. #### passwordSetup.sub ($C800) Until here we ran in SRAM bank 2, which we can't do anymore since the first thing this function does is set SRAM bank to 0 which makes the function we just jumped from inaccessible. We then write a pattern for the entire SRAM bank. ![image](https://hackmd.io/_uploads/ryEoKVPCJg.png) Something noticeable is, that a segment is only 8 bytes long. Out of these 8 bytes, 6 are constant. Ultimately, we do two more steps. For the first 20 segments, we clear the highest bit on the eighth byte. For the last 20 segments, we change the $20 byte into an $18 byte. But there was something very important I thankfully realized early. This function gives us a **constant output**. So there is no need to reimplement or give it a more in-depth look. Rather, I took a memory dump. I ran very successfully with this on [last year's Missingno challenge](https://hackmd.io/@WernerderChamp/Bk7lgul-R#Setting-up) as well! The function then switches back to SRAM bank 2, restoring access to the code. After returning we now load the address of the first password char into hl and call into passwordChk1. ### passwordChk1 ($B7E6) passwordChk1 makes use of a scramble function that ANDs, adds, and shifts registers around. I had ideas to reverse it as it seemed possible but gave up on that later. The function now reads a character 5 times, gets its index in the character list (giving us a value from 0 to 63) and then we scramble, with a noteworthy exception of the last character which just gets stored into h. In between, we transfer data between our registers. Again there is something noteworthy - the function reads all registers before it sets them. This means this function does not have any outside influence aside from the password. After returning, we check if the result in register l has the last 3 bits cleared. If not, we return "fail" early. ### passwordChk2 ($B6B5) Without further actions, we call into the second check function. The first step is checking for h (the last character) to be 32 or larger. If this is the case, we just return hl=$6969. Otherwise, we add $A000 to hl and set it at $DB55/DB56, before jumping to a subfunction #### passwordChk2_sub ($DB4A) The subfunction clears registers bc and hl, swaps to SRAM bank 0 again, and then **JUMPS INTO THE FRICKING PATTERN WE GENERATED EARLYER**! ![image](https://hackmd.io/_uploads/r1XwjEDC1e.png) This gives us a confusing chain of instruction, that caused several people to straight give up. I was getting worried about having to write a CPU emulator here, especially since there was not too much time left. ![image](https://hackmd.io/_uploads/ByBg-BPC1l.png) #### Welcome to A000 land This area became known as A000 land in the discord. After the initial shock, I figured out that the code was way simpler than it looked, following a simple pattern:: * Load <x> into C * add BC to HL * Since B is zero and never touched and C is not read again until it is written, these two commands are essentially just HL + *<x>* * decrement d and return if it isn't zero * decrement e and jump *<y>* if it isn't zero * As for the last 20 segments the $18 byte is set to $20 - if we are in this area we also jump if e is zero * I also remembered that JR instructions take a **signed** 8-bit value as a parameter. That's why the 8th bit was cleared for the first 20 entries - so we never jr backward into VRAM! So, *thankfully*, the opcodes are always the same (except for the last 20 segments), and it's only the parameters that vary. After the function has done d loops (with d=0 resulting in 256 loops) we reset to SRAM bank 2 again and return. After this, we load $ADDA into de and call a function to check if hl and de are equal. If not, we return "fail" again. ### Password Chunks As I continued, I noticed that the pattern repeats. Once again we call passwordCheck1 - this time with the 6th character. And again we check for l to have its 3 lower bits cleared before calling passwordCheck2 and comparing it with a value of de. Oh and since we return hl=$6969 if the last character has an index of 32 or higher, these passwords will never pass! Essentially, we now have three password chunks that are 100% identical, except for the value we check for at the end. As a result, my approach was to brute force all remaining 2^29 combinations for a password chunk and check if it matched any of the three. ### Putting it all together Now it was time to implement it. I started by reimplementing passwordChk1 and made sure its results equaled the one the program. The sla and swap instructions were implemented as separate functions. I continued with coding a reader to read the dump I made earlier and put it into two arrays. With that, I could now just look there to get me *<x>* and *<y>* for whatever segment I am in right now. With that done it was time to do passwordChk2 now. I reimplemented the pattern and pulled the parameters from the array I just set up. Once again I validated that the function behaved exactly as in the game. The next half hour was testing random values and fixing bugs. But after trying a million random passwords without crashes and having them match the original's behavior, it was time for an attempt. To be fair, I expected this to take quite some time to brute force as I have an older PC. I was greeted with a big surprise when it only took 25 seconds and I had my password. Challenge 25 had fallen. I later implemented proper multithreading, getting the solve time down to just 4,5 seconds. Go was a good choice here as it does not check for overflows, allowing me to do everything with uint8 variables. There are also several spots when we just don't care when it happens. In these cases, I'd have to put extra guards in place, costing runtime. [**Here is the source code!**](https://gist.github.com/WernerderChamp/53ec859d685b356e0a426e25420a5af0) ## The unused code Now, at last, it was time to find the hidden code. I started by observing the tilemap once again whilst talking to NPC #1 and figured out the 8 bytes where it is written, which is $C46E - $C475. So I set a watchpoint on each of these variables and checked where they were written to. The first write was within the NPlaceChar function of the game. This turned out to just be used to clear the screen. The other writes were all at A079 - which was in the middle of the hijacked code! Bullseye! So I took a look at the stack to find the function that sets the data. The next value was either $A068 or $A06C - which made sense since these had a call instruction beforehand. After this came a $AA02 - which was again right after a call. There was a push hl before this one, which corresponded to the 0x1F46 I saw there. And the push dc at $A9ED gave the 0x0014 on the stack. After this, there was a $24D7 which is the end of the text script. So I put a breakpoint at $A9FF and traced backward (praise the bgb devs for implementing this), finding that this function's entry is at $A9AD. So now we have a good point to start. I decided to just test what the parameters going into the functions did. c is set to 0x14 before the push bc, so I decided to increase this to 0x40. As a result, the code spinning ran much longer and gave a different code. So we were pretty close here. Above this, I noticed af getting popped. This value seemed to be pushed onto the stack from outside. And very suspiciously, the code set hl to 0x1F3C and added our value three times. a was 0x02 in this context, so I set it to 0x03. And it gave me the code for the guy in Pewter, locked behind the glitchy tiles. BINGO! At this point, it was easygoing. a=0x01 was the diglet cave password. But a=0x00 did not correspond to any entry! So I tried a=17 (dec 23) and this one was correct! Bepis obtained! This entire thing only took me about 35 minutes. I kinda cheesed it but hey, if it works, it works ¯\\\_(ツ)\_/¯ ## Final remarks First 1337 for me! While I only scored #17 because I lost the first three days and did not go into hacking the codes right away (which would have probably saved me two days), a 1337 is a 1337. Thank you once again ZZAZZ for hosting these events! I did not expect such a well-hacked save again, especially after you announced you wouldn't make a small event this year. Keep it going and I'm looking forward to the next video on your channel!