Welcome to my really really verbose writeup of TheZZAZZGlitch’s April Fools 2022 event! As alyways, my english is really bad. Please, don’t judge too much :(
Last year I was like one week late in writing my writeup. Better play safe this year! I’m actually ending this writeup the day two days later the event was done. RIIIIP
⚠️ THIS IS STILL A WIP! I still have to re-read everything and hire someone who can review my shitty english
Full list of achievments/hacking challs description:
Preeamble
It’s incredible the precision I have in fill myself with work and university exams EXACTLY in the Fools period. This year was kinda crazy. (Notice the “5 others”)
Btw let’s start with our story. Vcvtne2ps2bf16, an italian folk who helped me in Fools2021, contacted me one week before the event sharing with me his hype. He told me that this year the event would have been on gen3. I asked him why he was saying so, he pointed me to this image zzazzdzz uploaded on the fools2022 repo. He told me that if you download that image, import it on GIMP (Image using Photoshop in 2022, kek cringe lmfaoo) as a raw image, export it as png and then open it as MP4 video, you’ll get a sneaky peek into a WIP of Fools2022. Here’s the video btw:
Finally my nightmare of having a Fool event on the GBA (Architecture I don’t know AT ALL) became true. I spent the week before the event between travels, work, university study/projects and setting up an enviroment to play, debug and decompile a Gen3 pokémon game. I went throught a fuckton of problems, because I tried first installing the DevkitPro on an oldass Linux version. Dynamic library linking broke me so hard. Eventually I ended up creating a new VM with Ubuntu 21. My setup was complete: mGBA running on the Windows Host, a Ubuntu21 VM running arm-none-eabi-gdb connected to mGBA, putty connected to the VM thru ssh to use gdb, the fools client and IDA running natively on the windows host. For none-eabi-gdb I got from vcvtne2ps2bf16 a modified version of the gef plugin, that was handling better the Game Boy Advance CPU. Mentioning vcvtne2ps2bf16, I have to thanks him really a lot! He helped me with setting up almost everything, and also it was a nice guy to talk with during the exploration and hacking part of Fools2022! 💖 Before the competition, I had only one problem remaining: for reasons single stepping inside gdb was taking AGES, literally 2/3 seconds per instruction. I was unable to fix this problem completely, I only mitigated by running both mGBA and gdb on the VM down to 0.5/1secs per single instruction. Weird
Exploration/Normal achievments
31th night I took my train from Rome to Milan, since I had some friends to meet and 3 different works to do there. The 1st of Aprill was going to be a chill day at my friend’s house, so my plans were to complete at least the exploration part on the first day and work slowly on the hacking challs between work times in the following days. The event started shortly before my launch, and I already had problems: The local client was unable to communicate with the server in any way.
I felt that it was some kind of cert errors, situations like this happened to me many times in the past and it was always dns ssl. To be sure I tried getting the stdout/stderr from the client to check what was really happening, failing miserably, so I contacted TheZZAZZGlitch himself asking for some help. He relased two updated client versions, the last one with all the SSL verifications disabled. It worked and, a little bit late, I was ingame.
For the exploration part there’s nothing much to say. For going around and knowing what to do I used a lot the Radixan’s Fools2018 map viewer. Like I did back in 2018 I used the savestates, even if ZZAZZ warned about us to not do so. Unlike what I did back in 2018 I didn’t hacked any item or stuff to get exploration achievments done faster (Like the pokéballs). An important thing I must say about this first part of Fools2022 is that I noticed only when I had only one Pokéball remaining to finish ALL of the exploration achievments, that pressing B I makes you use your running shoes and move faster ingame… I’m an idiot for not discovering this before. This is the main reason (The second one of course is because I needed to interact with my friends who were hosting me irl. I love you Nara and Hypewolf <3) why it took so long for me to complete the exploration part. Btw at like 4 am I was done with the achievments and I went to sleep, ready to do some hacking pro gamerz stuff
Cracker Cavern Reborn
I slept about 3 hours (321 rule ftw) before being woke up in the early morning by my friends. After I became aware of myself, I immediately started working on CCR1.
CCR1
“Before we get into the real deal, let’s begin with an easy task. Use whatever methods you desire to go through the rock barrier below and reach the next floor. Note: basic protections against cheating are implemented. The person on the right can help you achieve your goal, but it’s not necessary to use their help. Good luck!”
So, basically find a way to get thru obstacles. The guy on the right was asking for a luxury ball, however I completely ignored him. Fuck him. He doesn’t deserve a luxury ball. What he has done? Why he should get a luxury ball from me for free? Fuck off.
I decided to solve this challenge exactly like I resolved CC1 in fools2018 and the first hacking challenge in fools2019 (And I think also a challenge in fools2017): by simply changing the player’s x/y coordinates to be next to the warp, than take a step and be warped in the following map.
What was trivia for me is actually finding the map addresses of my x/y coordinates, due to my lack knowledge of the gen3 games and due the completely absence of a ram map of this game like the ones for gen1. So I opened the rom’s elf file on IDA, the disassebly on github and the symbol files on vscode. I searched on vs for “xy” and found an interesting function name, ScrCmd_getplayerxy. Searched that function on github and this popped out:
So, my coordinates are saved inside the gSaveBlock1Ptr struct. I searched it inside the symbol files, and got the address 0x0000000003005d8c. I opened that address inside the mGBA ram viewer (And I started crying, because I miss the BGB ram viewer and debugger :c), gone to this address, and I started moving around in game while observing the memory after gSaveBlock1Ptr. By scrolling a little bit inside the memory viewer, I finally found out that the player’s X/Y coordinates are saved starting from the address 0x3005e5e.
So, I aligned vertically to the warp point, and positioned myself two “blocks” before the barrier (Check the following screen). Incremented by two my X coordinate and…
…and the X coordinate was back at the starting value, without being incremented. WTF. I first thought it was me being dumb in using the mGBA memory viewer, so I paused the game, changed the value and restarted the game: Only when the game was running the values was being overritten, meaning the “real” X/Y coordinates are somewhere else and they get copied over the ones we already have.
I decided to go with the “search in ram” tactic: I first searched my X coordinate in ram using the mGBA’s search tool, then moved one step, filtered for the new X, and so on, till I had only 3 addresses left: 0x3005e5e (which I already had), 0x203760 and 0x203764. I went full monkey, positioned myself like before, changed ALL of the addreses, did one step right, and I was warped into CCR2 successfully.
CCR2
“As you may have noticed, this entire world is made of many different small maps, loaded on demand. Use wathever tools you desire to reach the map with identifier 0x1337. Once this is done, return here and take the bottom exit to continue. Note: bypassing the barrier with client-side hacks won’t work anymore. Checks are done server-side. Next challenge will not load until this one is completed. Good luck!”.
Also this challenge is pretty similar to one from fools2018. That time I solved it my changing the ingame warp ids for the map boundaries. This time, however, I was on a gen3 game. I had no idea about how all of this stuff works here. So I decided to work on something I know. I opened burp suite, setted up the proper intercept rules, and I immediately starting seeing the communication between the local client and the server. Without any kind of SSL errors! This was because for making the client working for me, ZZAZZ disabled all SSL cert checks. It was working like a charm!
So I took one request the client made when I changed maps ingame and I noticed it contained only raw bytes encoded in base64. No clue however what those bytes meant! This is when I had a good intuition: Fools2022 is really really similar to Fools2018. Maybe zzazz mantained also the map ids from the old event? So I searched the map I was loading using the Radixan’s Fools2018 map viewer, took the map ID, and voilat: it was encoded in little endian inside the request.
I used again burp suite to intercept a load-new-map request, changed the map id, forwarded the request, and loaded in game the map 0x1337. It worked, buuuuuuuuuuuuuuuuuuuut I had a problem:
I was in the middle of blocks, no way I could reach the land in the upperleft corner. The first thing I tried was searching for a WTW cheat code, but I was unable to get it working. Idk if it was me being dumb using the cheat tools of mgba or what. So I instead tried loading that map using another warp, maybe with some luck I would got a good alignment. But nope, it didn’t worked neither. Vcvtne2ps2bf16 suggested me to patch the collision function inside the rom, but I had problems in recompiling the game, so I had to “dynamically” patch it: I searched for “collision” on the pokeemerald github, and found CheckForPlayerAvatarCollision. Hooked up gdb to mGBA, putted a breakpoint and… And then I didn’t know how the ARM calling convention works. However, in the call tree I noticed that this function was called by PlayerNotOnBikeMoving at 0x0808AF0C which is something like:
staticvoidPlayerNotOnBikeMoving(u8 direction, u16 heldKeys){
u8 collision =CheckForPlayerAvatarCollision(direction);if(collision){//return from the function}//walk()}
That after being compiled was
Now, I have no fuckin idea what those LSLS and LSRS instruction are. But for me it was enough seeing the CMP collision ($R4), 0; BEQ <code blocks where there's no collision> (For non asm ppl, this means compare the value of the register $R4 (aka, the collision variable) with 0, and if it's equals (to 0), jump) to just put a breakpoint in gdb here (Address: 0x808af14) and set the register $R4 to 0 each time I took a step. Fun fact I learn: if in gdb you run set $R4=0, it won't give you any error but it won't work. You have to write set $r4=0 (Check the lowercase) to properly set the register.
I eventually reached the land, and talked to a OOOOOOOOORB “You did pretty well. Please reset your game” Fool you, I made a savestat before warping to this map. Reloaded CC2, and walked to the now opened access to CC3
CCR3
“You’re halfway there. Here’s next challenge. The passage to the next map is guarded by a password. The objective is simple. Find the correct password. Note: bypassing the barrier with client-side hacks won’t work. Note: there may exist multiple correct passwords. Any of them will be accepted. Good luck!”
Sooo… Finally we got here, to this year’s password cracking/bruteforcing challenge
If you talked with the NPC that was there, he was, in order, prompting for a password, checking it (locally) and telling me if it was correct or not.
The first thing I did was searching for the underneath algorithm which was responsible of checking the password. I tried CTRL+Cing from GDB immediately after inputting the password, however I was unable from looking at the stack trace of determining where all of this stuff was called from. I knew there weren’t map scripts (like in gen1/2) involved, some NPC in the map “WORLD 4-4” told me that, however I had no idea how custom code was handled. Sadly, my free time ended, and I had to quit and go work.
At work I was still thinking at the chall and while talking with Vcvtne2ps2bf16 he leaked me that I had to search up for ScriptContext. So I did in the first moment of pause I had!
scriptPtr looked really promising 👀 As I did in CC2, I searched the two variables holding the ScriptContext structure, and got their addresses: 0x03000E40 for sScriptContext1 and 0x03000EB8 for sScriptContext2. Analized them in gdb (Btw it really looked like that the compiler rearranged the struct for optimization), to find out that only the script1 was used, and the script code’s address was 0x0x020182c2. Runned the good ol’ x/32i in gdb and got this: Well, I don’t know pretty much of arm asm, but this really looks like garbage to me
By looking furhter inside the pokeemerald git, I found this function: u8 SetupBytecodeScript(struct ScriptContext *ctx, const u8 *ptr). Bytecode. Oh. So, the “map scripts” were not plain assembly, like in gen1/gen2 pokémons game, but a script with custom istructions “emulated” by the game itself. I read again the ScriptContext struct, and noticed the ScrCmdFunc *cmdTable;. Not only it was running custom instructions, but each script could run its own set of custom instructions. I thought: It’s unlickely that ZZAZZ sends to the gameboy both the script, a translation table for the instructions, and ALL the instructions’ code to run the script, it’s way too much data, so he’s probably using the default cmdTable. This intuition was later confirmed also by Vcvtne2ps2bf16. Now it’s time to write a disassembler!
The first thing we need is the full cmdTable. By searching again for ScriptContext in the repo, I found in src/scrcmd.c the code implemented by each default script-instruction. This will be useful later, but was not the table I was searching for (it’s not a table at all hahaha). By searching for one of the instruction name in that file (EG: ScrCmd_nop), I found exactly what I wanted From data/script_cmd_table.inc
Now I (hopefully) have the scriptTable and I know where the script is, it’s time to dump the bytecode and translate (disassemble) it into human redable instructions! Dumped 4kB of instructions thanks to GDB, copied it into vscode and with some search and replace I trasformed them into a huge-ass Python array. Before writing the disassembler script, I first wrote a script to generate the list of instructions by their index and the parameters as well as their size in byte/half word/word (Each instruction took a custom amount of parameters, you could see it clearly by the instructions implemented in src/scrcmd.c, making the script engine CISC-ish). The script was taking in input a stripped version of the script_cmd_table.inc file and the script.c file but without the extra extra functions not used in the cmdTable, and thanks to some regex magic it was yeeting out a cool array with everything:
import re
TYPES = ["ScriptReadByte", "ScriptReadHalfword", "ScriptReadWord"]
symbols = []
withopen("function_symbols.txt", "r") as f:
symbols = f.readlines()
out = [0] * len(symbols)
withopen("scripts.c", "r") as f:
data = f.read()
names = re.findall(r"[a-zA-Z0-9]+\sScrCmd_[a-zA-Z0-9_]+\(struct ScriptContext \*ctx\)\n{", data)
sp = re.split(r"[a-zA-Z0-9]+\sScrCmd_[a-zA-Z0-9_]+\(struct ScriptContext \*ctx\)\n{", data)
sp = sp[1:]
for i inrange(len(sp)):
name = "ScrCmd_" + names[i].split("ScrCmd_")[1].split("(")[0].strip()
params = []
for l in sp[i].split("\n"):
n = 1fortypein TYPES:
if(typein l):
break
n = n << 1if(n < 8): #max size are words
params.append(n)
idx = len(symbols) + 1for i inrange(len(symbols)):
if(symbols[i].strip() == name):
idx = i
breakif(idx < len(symbols)):
out[idx] = {"name": name, "params": params}
print(out)
With that script, I was able to create the disassembler:
# !!!! NOTE !!!!# COMMANDS and DATA arrays are omitted, because they're *huge*. You can still find them inside my google drive fools22 folder, in cc3/decode.py: https://drive.google.com/drive/folders/1YHG-E_-jy6cLS-teHRol88Cyu9NJr0Gq?usp=sharing
COMMANDS = [{'name': 'ScrCmd_nop1', 'params': []}, {'name': 'ScrCmd_call_if', 'params': [1, 4]}, {'name': 'ScrCmd_setobjectsubpriority', 'params': [2, 1, 1, 1]}] #example commands
idx = 0defread(len):
global idx
res = 0
idx += lenfor i inrange(len):
res = res << 8
res |= DATA[idx - i - 1]
return res
BYTE = 1
HALF = 2
WORD = 4
NAMES = {BYTE: "b", HALF: "h", WORD: "w"}
usedIstr = set()
out = ""try:
while idx < len(DATA):
cmd = read(BYTE)
ptr = idx + (0x20182ad - 1)
cmd = COMMANDS[cmd]
pars = []
for p in cmd['params']:
pars.append((f"0x%0{p * 2}lx" % read(p)).ljust(12))
usedIstr.add(str(cmd))
outLine = f"{'0x%08lx' % ptr}{cmd['name'][len('ScrCmd_'):].ljust(32)}{', '.join(pars)}"print(outLine)
#out += outLineexcept:
passfor c in usedIstr:
print(c)
print(len(usedIstr))
#with("scritp.txt", "w") as f:# f.write(out)
…and got some code that finally made sense! You can find it complete inside the google drive folder!
At that point, however, I ended again my free time and I had to be back at work again, always thinking about the chall. I knew it was something fast to bruteforce, since Buurazu said he bruteforced and got the password directly, without needing of simplifying math like we did back in fools2018 (or 2019? The one where we had a decryption algorithm who was taking literally ages to complete, and we had to simplify xor operations to run it in minutes). I knew also there were many solutions to that chall, and this made me question myself if reversing the script would have really been faster than reimplementing the bytecode interpreter, feeding it with the original script data and then bruteforcing the input. When, in the late night, I was done with my work and I had time to play again on the chall, I decided to properly reverse it, since I was working on a potato laptop (Reminder I was not in my home in Rome!) where bruteforcing would have been slow
The first thing I did on the script was splitting the code into basic blocks (Which are, for non IT people, blocks of code without any kind of branch/jump), so by just doing that I could had a great overview of how the code is “composed”. Luckily the script was really simple, and I didn’t need to create a control flow diagram of it :)
It all starts with a main method that resets a 16 bytes long buffer (as long as our input… Pretty sus), then calls a long function (presumibly, the password checking algorithm) and checks its result: if it’s 0 it exits, if it’s 1 it does a bunch of stuff before.
A cool thing that I want to point out, before reversing the check password function, is that dumping 4kB of data, was really a lucky decision: the full script is 4080 bytes long, it fits really closely my dump
Btw, as I said, the password checking algorithm was really simple. Already when I was tracing the basic blocks I noticed that the code was just the same thing over and over again: some movs and calcs, then a small loop, and then again another block of code like that. In the middle of the function there were just some extra movs, and at the end the compares that checks if the “processed” password is correct. Let’s see it in detail, starting by the block of code copypasted many times:
//Comments without a tab before are the high level description of what's happening0x020184df addvar 0x8001,0x12df//out += CONSTANT0x020184e4 loadbytefromptr 0x00,0x02021dc9// $R0 = str[i]0x020184ea setptrbyte 0x00,0x020184f3// 0x020184f3 (in the middle of the next instruction) = $R00x020184f0 addvar 0x8001,0x00ff//out += str[i]0x020184f5 loadbytefromptr 0x00,0x020375da// $R0 = [0x020375da]0x020184fb setptrbyte 0x00,0x0201851a//addb1_low = [$R0]0x02018501 loadbytefromptr 0x00,0x020375db// $R0 = [0x020375db]0x02018507 setptrbyte 0x00,0x0201851b//addb1_high = [$R0]0x0201850d setvar 0x8001,0x0000//out = 00x02018512 setvar 0x8002,0x0065//i = N.loop
0x02018517 addvar 0x8001,0x1084//out += (!addb1_high << 8) | !!addb1_low 0x0201851c subvar 0x8002,0x0001//i--;0x02018521 compare_var_to_value 0x8002,0x0000//loop if i > 00x02018526 goto_if NEQ (5),0x02018517//.loop
I already spoiled in the comments what’s happening here, however let’s recap it verbally: It first adds to our output variable (contained in 0x8001) a costant, than takes the i byte of the input (We know that the address of the input is like 0x02021dcX from the reset buffer function) and sums it to the output variable (by modifying the code itself! This is why ppl in the discord channel were talking about self modifying scripts. Luckily they’re just editing some variables, not modifying the execution flow. This would have been HELL). Then it loads an half word starting from 0x020375da, and changes the addvar inside the loop to add that value. It resets the out variable to 0 and then it adds the content from previous half word N times in a loop, effectively making a multiply function. The only thing to look into is what’s stored in 0x020375da, since our code doesn’t write at all in that memory location. Vcvtne2ps2bf16 spoiled it again: it was the memory address where the out variable (0x8001) is stored. The only three things that were changing between one of these blocks and the others: the inital constant, the address of the input buffer (of course) and the number of loops (The multiplier). By rewriting in high level code the whole block, we have something like this:
for(int i =0; i < input.length; i++){
var += COSTANTS[i];
var += input[i];
var = var * MULTIPLIERS[i];}
There were 20 of these blocks inside the check password function. All of them are the same, except:
The first block have a setvar 0x8001 instead of a addvar 0x8001 since it was initializing the 0x8001 var
The middle (10th) iteration have an extra copyvar 0x8003, 0x8001; setvar 0x8001, 0x0539 before
From the middle (10th) iteration on, the algorithm restarts checking the password from the beginning, making the last 6 bytes of the password useless
After the last check (20th) we had this small block of code:
Which was basically checking if the calculated values are right.
Since the script was really really simple to reimplement and it was internally only some adds and muls, I decided to go full monke and use Z3 to get the password. I’m really sure that this was the easiest way to complete that chall in resonable computing time btw. But what is Z3? Z3 is a SAT solver. In a really really simple explaination, let’s say that normally you have an input, you feed that input to an algorithm, and you get the output which was NOT known at the beginning. Z3 helps you solving the opposite situation, when you have a known output (the final values that the variables 0x8001 and 0x8003 must have to be accepted as a valid input) a known algorithm (what we reversed so far), and you need to obtain the original UNKNOWN input (our password). In that instances, the input becomes a set of symbolic variables (That are like normal variables, except they have no value) that are processed inside the algorithm and the output becomes a set of constraints to be applied to the variables after (or even before!) they’re used in the algo. This has some limitations tho: Z3 is painfull slow with algorithms who uses symbolic variables as an index for arrays, and you cannot use a symbolic variable inside any kind of branch condition: If you don’t know the value of a variable yet, how do you know if you can take the branch or not? Luckily this was absolutely not the case with our script, and it was really easy (and fast) writing (and running!) a solution for that challenge
from z3 import *
#Extracted from the script
CONSTANTS_1 = [0x1b39, 0x18df, 0x13eb, 0x11ef, 0x1145, 0x12df, 0x0dfd, 0x13af, 0x149f, 0x0fef]
MULTIPLIERS_1 = [0x0049, 0x0061, 0x000d, 0x0029, 0x0043, 0x0065, 0x0059, 0x008b, 0x0047, 0x0053]
CONSTANTS_2 = [0x0539, 0x0e75, 0x11fb, 0x1237, 0x125f, 0x107b, 0x1951, 0x1b47, 0x151f, 0x14b1]
MULTIPLIERS_2 = [0x003b, 0x00b5, 0x007f, 0x00a3, 0x0067, 0x00a3, 0x0095, 0x00c1, 0x00d3, 0x0097]
s = Solver()
pwd = []
# Thanks Buurazu for giving me the translation of the pokeemerald characters!for i inrange(10):
c = BitVec(f"pwd_{i}", 16)
pwd.append(c)
#s.add(And(c >= 0, c <= 0xff)) #injectable chars
s.add(Or(And(c >= 0xD5, c <= 0xEE))) #only lower letters#s.add(Or(And(c >= 0xBB, c <= 0xEE))) #only letters#s.add(Or(And(c >= 0xA1, c <= 0xAE), And(c >= 0xBB, c <= 0xEE), c == 0, And(c >= 0xB0, c <= 0xB8))) #full input#s.add(Or(And(c >= 0xA1, c <= 0xAA), And(c >= 0xBB, c <= 0xD4), And(c >= 0xD5, c <= 0xEE), c == 0, c == 0xff, And(c >= 0xAB, c <= 0xAE), And(c >= 0xB0, c <= 0xB8)))
var1 = 0for i inrange(10):
var1 += CONSTANTS_1[i] + pwd[i]
var1 = var1 * MULTIPLIERS_1[i]
var1 += 0x0fb5
var2 = var1
var1 = 0for i inrange(10):
var1 += CONSTANTS_2[i] + pwd[i]
var1 = var1 * MULTIPLIERS_2[i]
var1 += 0x13eb
s.add(And(var2 == 0xb0ef, var1 == 0xd4b9))
if(s.check() == sat):
m = s.model()
print("PW FOUND!!")
for i inrange(10):
n = m[pwd[i]].as_long()
print(hex(n))
#print(hex(n), chr(n - 0xd5 + 0x61))else:
print("ripp")
It was really longer to extract the constants and the multipliers from the script than writing the solver code (And I actually also managed to fail copying some constants, who made me found invalid passwords at first) In order, you can see:
A for loop declaring the symbolic variables and adding constraints to have them inside the lowercase chars range
Our reversed algorithm, made by
The first for loop who calcs the value of var2 (0x8003 in the original script)
The second for loop who calcs the value of var1 (0x8001 in the original script)
The addition of constraints to have var1 and var2 to be equal to the correct out value
A check if Z3 was managed to solve the password, and if yes, a loop who prints the password
Pretty easy, isn’t it? And the best part is that running this script on my potato laptop only takes 20secs to get a result! Here’s the first password it found: sswlwsjxop. Note that with minor changes, you can get without problems the list of ALL correct passwords. Check thewhile(s.check() == sat): inside my fools 2021 writeup if you’re curious. Btw, little note: Z3 is not pretty good at handling overflows between differently sized vars… Even if we’re working with 8bit chars in input, I had to declare them 16bit long and add constraints to have them inside the [0x00, 0xff] range, or it was giving me bad results, loosing overall performances
When I solved CC3 it was really late in night, but this hasn’t stopped me from writing some code to search for meme passwords. So I added the following function to my code:
defsetTextConstraint(text, pwd):
i = 0
text = text.lower()
for c in text:
n = ord(c) - 0x61
s.add(Or(pwd[i] == n + 0xBB, pwd[i] == n + 0xD5))
i += 1# Example usage:
setTextConstraint("bepis", pwd)
It’s basically adding new constraints to the input password, so the first N chars are the same of the text variable, both in lower and upper case. It could be better, it could also support l33t chars so you may have more chances to find a correct password starting by a given input, but it was made a 3am with the alarm set at 6am. I just wanted to sleep. The first password I tried searching was “sus”+random letters. Since “sus” it’s only 3 chars long I wasn’t surprised about finding valid results: suShBcpZjr. After sus, I tried “amogus”. Sadly I was unable to find a working password with printable chars starting like that, even using if I also included symbols and numbers. However “amogus”+random_bytes was actually a real password: AmOgUSk\x12\x83\x2a. I posted this inside the GCRI discord, and IssoTM immediately told me to kill this meme :(. Poor little sus guy… Btw, the next and the last thing I searched for was “bepis”+printable chars. Since bepis is one char shorter than amogus I was pretty much confident that I would have had results, and I was right! BEPIs8qoTz is a valid CCR3 password.
Btw thanks also to IssoTM who provided me the full list of the characters translations! By using it, I made this simple script that takes in input hex bytes and outputs the encoded char:
#As before, you can find the full list of characters inside the google drive folder, in the file cc3/hex2password.py: https://drive.google.com/drive/folders/1YHG-E_-jy6cLS-teHRol88Cyu9NJr0Gq?usp=sharing
s = ""whileTrue:
inp = input(">")
if(inp == "!QUIT"):
break
n = int(inp, 16)
try:
print(CHAR_MAP[n])
s += CHAR_MAP[n]
except:
passprint(s)
CCR4
“You have come so far. Here’s your final mission! The person on your right gives away Silver Certificates of Commendation. They’re cool and all, but I really want to see a Gold Certificate. I heard the certificates are protected with military-grade AES encryption. Can you forge a Gold Certificate? Maybe there’s a weakness somewhere? That’s for you to find out. Good luck!”
I woke up early to take a 8-hours-long economy train, to lead back home. My plans were to complete the chall before my arrival, but the bad internet connection was not by my side.
Btw, speaking about the chall: In the ccr4 map there was an NPC. I talked to him, he connected to the servers and did stuff, then talked again to him, he connected again to the servers and showed me my certificate info (Cert issuer name, cert type (WE WANT TO CHANGE THAT!), cert serial number). I reopened burp suite to start analyzing requests like I did in CCR2, and requested again a certificate. Opened the intercepted request and noticed it was a huge base64, that decoded gave me another base64 appended with raw data. By decoding also the inner base64, I got again raw data, different from the first one, meaning that the whole certificate is crypted in AES, and we don’t have something like cert+crypted hash.
🔴 REQ:
🔴 RESP:
At that point, while talking with Buurazu that was on this chall from more time than me, he proposed to work together and I accepted. He gave me the notes he made so far and an interesting thing immediately popped to my sight: These really looks like the buffers used for communication. A good start for reversing what’s happening under the hoods! You can clearly see at the start the unencrypted certificate, and later what I sent in the previous request! I hooked again GDB and, as I did before, I got the address of the scripts that were handling this: Map script at 0x0203e332; Talk script at 0x020182b5. Aaaaaand mgba crashed. Thinking about this crash, now, I can only say that I was really lucky, since I was going into an extremely wrong direction.
I reopened everything, loaded the old CCR2 savestate, made my way again into CCR4, talked to the NPC and this time he didn’t gave me a certificate. What? Talked to him again, and this time I got a certificate. WHAT??. Looked at burp suite and I noticed only now that the game was making two entirely different requests. I played with it a little bit and finally managed to understand what’s happening. The client first asks for a new certificate when you first talk to the NPC and only then validates it. I played lazy, and instead of reversing the code I decided to just look at the buffers discovered before while the requests are made to find out where the AES was being done, so I could reverse just that specific part. To my surprise, I found out that:
The unencrypted certificate is created when you first load the CCR4 map. Actually it really looks like it’s sent together with map data while downloading CCR4
The encrypted certificate comes DIRECTLY from the first request, and it’s coming already encrypted from the servers. No processing at all is done client side
The raw data you send in the second request, after the base64 certificate, is actually junk data. If you fill it with any byte even before talking for the first time with the NPC, all your edits will remain there and they will be trasmitted to the server together with the second request
After these considerations I made a sequence diagram of the generation/validation protocol:
Ok. We know how the protocol works. We know that client side there’s nothing happening, the encryption and the validation are entirely done server side. What do we do now? This is where I started struggling. We don’t really have too much to play with. I thought that trying to work on the already encrypted cert was worthless, since it is encrypted, so I tried doing random stuff to the unencrypted one. As expected, changing the cert type from silver to gold, even if using the correct padding and terminator, gave me a fantastic FoolsDescriptiveProcessingError('you may only generate silver certificates'); I knew from the GCRI discord that CCR4 was easy, but not so much hahaha. I first, thanks to the help of IssoTM, made a client to do request easier to the server, instead of encoding and decoding by hand on cyberchef the base64s:
So I tried doing random stuff, from appending other cert-types-tags to try something like the X-Forwarded-For http header attack, to trying incredible long names… Nothing worked. I only noticed that you could only generates certs with a length mutiple of 4, that you cannot include a = in the value of the cert (Or it would return a nice Directive with more than 1 value error), and that you can actually have invalid keys (they will be ignored). I poked IssoTM (who was the 3rd getting this chall done) and asked him if it was more a crypto challenge or more something else. He told that it was purely crypto, and suggested me to look deeper into the encrypted packet.
The first thing I did was checking if it was AES ECB Plim plom, big actually moment: When you encrypt and decrypt stuff in AES, you need to first split your data in blocks, and then work on each single block. The way you let every block interact with each other (as well as the key, the IV and the CipherText/PlainText) define the AES mode. ECB is a mode where each single block is indipendent from all the other ones. This introduces some vulnerabilities, for example, you can extract easily the context of the plain text starting by the ciphertext Another vulnerability of ECB is that you can swap encrypted blocks without problems, to create a “custom” plain text. In CC4, this could be abused by, for example, creating a cert with the name aligned to have at the start of one block the string gold and at the end of another block the string type=. By sending to the server the two packets swapped in order, you could had a cert that, when decrypted, it had the full /type=gold/ in it. Example scheme:
How do you check if you’re working with AES ECB? Just change one single byte at the start of the PT and see if it you have only one block changing in the CT.
Sadly for us, it was not ECB. Just by changing the first letter of the cert’s name, most of our PT has changed:
Aelita: 21 32 27 a7 e8 5c 4b 07 cf d8 2e 6b bd 30 32 ca 03 1e 25 1c 94 37 1d 43 cd bf eb a1 fe 18 63 bf f5 4e eb 03 ab b9 34 8a d0 de 62 fe fa e3 bf 56 4f 38 0d 59 7e d2 29 22 67 43 6c 85 43 d0 b0 39
Celita: 21 32 27 a7 e8 5c 4b 07 cf d8 2e 6b bd 30 32 ca 03 f2 a2 55 81 a5 19 13 01 3c be dc dc 9c 0f c9 dc 5c aa c8 75 fe 70 27 90 d7 62 01 a8 dd f8 bd 1e 7d 6f e3 e6 16 e9 3a 4d a7 4a e2 cc ef a9 26
I noticed tho that the first block remined the same tho… Prolly it was containing the stuff that’s authority key/value prependend in the cert decryption
Btw, at this point I was really lost. I had really nothing much to do than literally trying to corrupt random bytes. I tried setting at 0 a byte in the middle of the encrypted cert, and got this as an answer: b'N\x00\x00\x00error=1/desc=Directive with more than 1 value/authority=CrackerFour' WHAT? This is NOT an usual error! The exception is coded INSIDE the cert response, not like before! Is that progress? So I continued poking around and received yet another error: b'N\x00\x00\x00error=1/desc=Invalid key i@%\x92b{\xc7\xab,\x83\xdf\x8f6\xae\x1e\xc5rFoursserial/authority=CrackerFour'. I AM CHANGING THE DECRYPTED PLAIN TEXT! WHAAAAAAAAAAT??? I tried changing by a little bit the byte I was corrupting, and I eventually got Wrong cert type smlver/authority=CrackerFour. OH. I asked confirmation to Buurazu if I was in the right direction and HE CONFIRMED. I was SOOOOOO hyped! I modified my request script to bruteforce a single byte, till I got in the error the correct byte, so I could manually move to the next one. This is the final version (the common functions are omitted).
defsubstituteByte(certificate, sub_index, incr):
return certificate[:sub_index - 1] + struct.pack("<B", (certificate[sub_index - 1] + incr) & 0xff) + certificate[sub_index:]
i = 0whileTrue:
print(i)
try:
# Using always the same cert, because the serial is random each time you generate a new one, and it messes with the bruteforcer
txt = "SVRJbnAraGNTd2ZQMkM1cnZUQXl5cXVHQjJMWGtqWDFnM08vTHV2ZlQrODRQUU56Zjl6Qm9IUkxhKzdRUlRyVVQrdnQ1dUtXRXN3UGRXaWljSkxWNkY0ZG02clU3QTZRVDduOVRJakJISDk0OGM5NEpGNXl4WEwvRGp2YTRtMVVBem1JczQrb3dkUEcwR3MrcXdUWHdUdjBiL2I1NFQ5SWFZb0dRSWR0dFpMOHlXS0FNRTlDbk1mYnRiWXJBS1M4/w=="
certificate = b64decode(b64decode(txt))
certificate = substituteByte(certificate, 121, 20)
certificate = substituteByte(certificate, 122, 2)
certificate = substituteByte(certificate, 124, 18)
certificate = substituteByte(certificate, 125, i) #150. String terminator
validateReq = (VAL_PRE + b64encode(certificate)).ljust(0x103, b"\x00") + b"\x00"
r = makepkt(validateReq)
print(f"{i}\tRESP DECODED:", b64decode(r.text))
if(b"gold\xff"in b64decode(r.text)):
print("I == ", i)
breakexcept Exception as e:
print(traceback.format_exc())
i += 1
I let that script run for a while and boom, it worked! :D I’ve made the CCR4 achievment and by refreshing the map I was able to get into the last CCR room “Congratulations! You have completed the Cracker Cavern Reborn! People of Glitchland are hugely amazed by your hacking skill. With such an achievement, I think a proper reward is in order. $You have unlocked the coveted Exploitation Expert title!$ This leaderboard flair will let everyone know of your greatness. Now go forth and continue exploring Glitchland. Hopefully, do so without hacking. Don’t ruin the fun for yourself!”
I’m crying ngl. But now the question is: Why this actually worked? *My
My wrong assumption was that if you change one byte of PT in any AES except ECB, you get full corruption of the CT, at least starting from the blocks where you’re doing the changes. The challenge was using AES CBC; this is the decryption scheme: What’s wrong in that? If you change one byte in the CT, where does this change propagates? Obviously to the PT’s block immediately decrypted from that block, Pboxes and Sboxes are a thing tho, but CBC is not ECB, so the change propagates also to the following blocks. In what way? Just with a xor. This is why by changing one byte in the CT, it yes corrupts the relative block in the PT, but it also changes only one byte in the following block’s PT: the one xorred with the one we’re changing. This is why my solution worked
Conclusion
This marks the end of my fools2022 writeup. The first person I want to thanks, is TheZZAZZglitch himself. Exactly 5 years ago I played Fools2017, the first cybersecurity competition I ever been part of. I never ever had any kind of experience before Fools2017, except some random pokémon hacking. Fools2017 made me love this kind of competitions and made me know what a CTF is. From there I started playing local, italian, CTFs and now I’m part of the mHACKeroni team, with which I had the honour to play Defcon CTF in the past years. Thanks to Fools2017 I also entered the good ol’ GCL discord, where I met a lot of friends during the years (and I also improved my completely broken english to something at least I can use to communicate decently). You all have no idea how much TheZZAZZGlitch’s fools events impacted my life in a positive way in this 5 years. I will be always grateful to him.
Btw, let’s leave behind my deep thoughts. I ofc want to thanks my fellow pasta friend Vcvtne2ps2bf16, then Buurazu and IssoTM whom helped a lot with CCR3 and CCR4! I also want to thank the entire GCRI discord, it’s an amazing community and it’s always fun having a lot of people talking together while solving the fools challanges :) And, thanks also to you, dear reader, that managed to get 'till here!
So this is the end. Even if shorter than the previous years, this event was still hella fun. I must admit that I liked that the challenges were not so hard. Since nowdays I’m more and more busy with study and work, I was glad I was able to get a perfect score with less work than the past years!
Here you can find random stuff used during the challs :)
ncat connection forwarding
As I said in the introduction, I had problems with gdb and mGBA. Loading the context was taking ages, a single instruction could cost up to 3secs. One thing I did for mitigate this problem was running mgba and gdb inside the same VM. However, I was not able to proxy the client thru burp suite if I runned it under wine. And the client was only listening on localhost. Solution? Use ncat to forward the traffic from the vm to the windows host! Those are the commands I runned in windows to forward both of the ports used by the mGBA’s dolphin protocol
It really slowed down the communication between the client and mgba, but it worked! :D
420 maps visited
From the GCRI discord I knew a bunch of folks that managed to get up to 65k maps visited while dumping them. This made me think that even invalid maps are counted in that total. So I took the packet from CCR2 and made a script that made me visit exactly 420 maps. memes.
import time
import struct
import requests
import traceback
from base64 import b64encode, b64decode
requests.packages.urllib3.disable_warnings()
USERNAME = "Stranck"
PASSWORD = "youtu.be/dQw4w9WgXcQ"
PRE = bytes.fromhex("01668e5a")
POST = bytes.fromhex("0503")
s = requests.session()
uid = None
headers = {
"User-Agent": "Stranck-mapScraper",
"Content-Type": "application/x-www-form-urlencoded",
"Accept-Encoding": "identity"
}
#thanks isso for the code! <3defmakereq(endpoint, data=None, json=None):
global headers
req = s.post("https://fools2022.online/" + endpoint, data=data, json=json, headers=headers, verify=False)
time.sleep(2)
return req
defmakepkt(pkt, encode=True):
global uid
if(encode):
pkt = b64encode(pkt)
return makereq("packet/%s" % uid, data=pkt)
deflogin(user, pwd):
global uid
global headers
r = makereq("login", json={"u": user, "p": pwd})
headers["X-Foolssessiontoken"] = r.json()["data"]["session"]
uid = r.json()["data"]["uid"]
login(USERNAME, PASSWORD)
VISIT_LIMIT = 420
i = 620#starting map indexwhileTrue:
try:
payload = PRE + struct.pack("<H", i) + POST
r = makepkt(payload, encode=True)
print(f"[{i}]\t Got response: [{r.status_code}]. Len = ", len(b64decode(r.text)))
time.sleep(1.5)
visitedNo = s.get(f"https://fools2022.online/profile/{uid}", headers=headers, verify=False).json()["data"]["visited"]
print(f"[{i}]\t Visited {visitedNo} / {VISIT_LIMIT} maps")
time.sleep(1.5)
if(visitedNo >= VISIT_LIMIT):
breakif("You are making an excessive amount of requests. Please try again later"in r.text):
time.sleep(30)
except:
print(traceback.format_exc())
time.sleep(60)
i += 1
Lottery match bot
Next fools event I swear I’m gonna call myself Stranck_abcdefghijklmnopqrstuvwxyz if there are again lottery match achievments. Btw here’s a bot that tries to get something more than a triple match (Spoiler: it is failing)
import time
import requests
import traceback
from base64 import b64encode
requests.packages.urllib3.disable_warnings()
USERNAME = "Stranck"
PASSWORD = "Do you really think that rickroll is my real password?"
s = requests.session()
uid = None
headers = {
"User-Agent": "Stranck-lotteryMatchRequestGenerator",
"Content-Type": "application/x-www-form-urlencoded",
"Accept-Encoding": "identity"
}
defmakereq(endpoint, data=None, json=None):
global headers
req = s.post("https://fools2022.online/" + endpoint, data=data, json=json, headers=headers, verify=False)
time.sleep(1.1)
return req
defmakepkt(pkt, encode=True):
global uid
if(encode):
pkt = b64encode(pkt)
return makereq("packet/%s" % uid, data=pkt)
deflogin(user, pwd):
global uid
global headers
r = makereq("login", json={"u": user, "p": pwd})
headers["X-Foolssessiontoken"] = r.json()["data"]["session"]
uid = r.json()["data"]["uid"]
whileTrue:
try:
login(USERNAME, PASSWORD)
r = makepkt(b"BbLeWXMaAgg=", encode=False)
print(f"Got response: [{r.status_code}]", r.text)
time.sleep(1 * 60 * 20) #sleep 20 minsexcept:
print(traceback.format_exc())
time.sleep(60)
Oh, the packet I’m using was dumped thanks to burp suite directly when you ask ingame for a lottery match. Luckily for us the request is always the same and we don’t have to do offline computation: the checks for any match are done server side
Femboy
There were a lot of femboys mentions inside the whole event. So much of them that I’ve been infected
It’s saying “finally you will become femboy”. It was right
Stranck’s Fools2022 writeup
Welcome to my really really verbose writeup of TheZZAZZGlitch’s April Fools 2022 event!
As alyways, my english is really bad. Please, don’t judge too much :(
Last year I was like one week late in writing my writeup. Better play safe this year!I’m actually ending this writeup
the daytwo days later the event was done. RIIIIP⚠️ THIS IS STILL A WIP! I still have to re-read everything and hire someone who can review my shitty english
🔴 Play this and the old Fools editions here! https://stranck.ovh/foolsRehosting
🔴 This year notes! https://stranck.ovh/writeups/data/fools2022/notes
🔴 Google drive folder containing everything I used during the event: https://drive.google.com/drive/folders/1YHG-E_-jy6cLS-teHRol88Cyu9NJr0Gq?usp=sharing
Index
FemboyAchievmnet list
Full list of achievments/hacking challs description:
Preeamble
It’s incredible the precision I have in fill myself with work and university exams EXACTLY in the Fools period. This year was kinda crazy.

(Notice the “5 others”)
Btw let’s start with our story. Vcvtne2ps2bf16, an italian folk who helped me in Fools2021, contacted me one week before the event sharing with me his hype. He told me that this year the event would have been on gen3. I asked him why he was saying so, he pointed me to this image zzazzdzz uploaded on the fools2022 repo. He told me that if you download that image, import it on GIMP (Image using Photoshop in 2022, kek cringe lmfaoo) as a raw image, export it as png and then open it as MP4 video, you’ll get a sneaky peek into a WIP of Fools2022.
Here’s the video btw:
Finally my nightmare of having a Fool event on the GBA (Architecture I don’t know AT ALL) became true. I spent the week before the event between travels, work, university study/projects and setting up an enviroment to play, debug and decompile a Gen3 pokémon game. I went throught a fuckton of problems, because I tried first installing the DevkitPro on an oldass Linux version. Dynamic library linking broke me so hard. Eventually I ended up creating a new VM with Ubuntu 21. My setup was complete:
mGBA running on the Windows Host, a Ubuntu21 VM running arm-none-eabi-gdb connected to mGBA, putty connected to the VM thru ssh to use gdb, the fools client and IDA running natively on the windows host. For none-eabi-gdb I got from vcvtne2ps2bf16 a modified version of the gef plugin, that was handling better the Game Boy Advance CPU. Mentioning vcvtne2ps2bf16, I have to thanks him really a lot! He helped me with setting up almost everything, and also it was a nice guy to talk with during the exploration and hacking part of Fools2022! 💖
Before the competition, I had only one problem remaining: for reasons single stepping inside gdb was taking AGES, literally 2/3 seconds per instruction. I was unable to fix this problem completely, I only mitigated by running both mGBA and gdb on the VM down to 0.5/1secs per single instruction. Weird
Exploration/Normal achievments
31th night I took my train from Rome to Milan, since I had some friends to meet and 3 different works to do there. The 1st of Aprill was going to be a chill day at my friend’s house, so my plans were to complete at least the exploration part on the first day and work slowly on the hacking challs between work times in the following days.

The event started shortly before my launch, and I already had problems: The local client was unable to communicate with the server in any way.
I felt that it was some kind of cert errors, situations like this happened to me many times in the past and it was always

dnsssl. To be sure I tried getting the stdout/stderr from the client to check what was really happening, failing miserably, so I contacted TheZZAZZGlitch himself asking for some help. He relased two updated client versions, the last one with all the SSL verifications disabled. It worked and, a little bit late, I was ingame.For the exploration part there’s nothing much to say. For going around and knowing what to do I used a lot the Radixan’s Fools2018 map viewer. Like I did back in 2018 I used the savestates, even if ZZAZZ warned about us to not do so. Unlike what I did back in 2018 I didn’t hacked any item or stuff to get exploration achievments done faster (Like the pokéballs). An important thing I must say about this first part of Fools2022 is that I noticed only when I had only one Pokéball remaining to finish ALL of the exploration achievments, that pressing B I makes you use your running shoes and move faster ingame… I’m an idiot for not discovering this before. This is the main reason (The second one of course is because I needed to interact with my friends who were hosting me irl. I love you Nara and Hypewolf <3) why it took so long for me to complete the exploration part. Btw at like 4 am I was done with the achievments and I went to sleep, ready to do some hacking pro gamerz stuff
Cracker Cavern Reborn
I slept about 3 hours (321 rule ftw) before being woke up in the early morning by my friends. After I became aware of myself, I immediately started working on CCR1.
CCR1
“Before we get into the real deal, let’s begin with an easy task. Use whatever methods you desire to go through the rock barrier below and reach the next floor. Note: basic protections against cheating are implemented. The person on the right can help you achieve your goal, but it’s not necessary to use their help. Good luck!”
So, basically find a way to get thru obstacles. The guy on the right was asking for a luxury ball, however I completely ignored him. Fuck him. He doesn’t deserve a luxury ball. What he has done? Why he should get a luxury ball from me for free? Fuck off.
I decided to solve this challenge exactly like I resolved CC1 in fools2018 and the first hacking challenge in fools2019 (And I think also a challenge in fools2017): by simply changing the player’s x/y coordinates to be next to the warp, than take a step and be warped in the following map.
What was trivia for me is actually finding the map addresses of my x/y coordinates, due to my lack knowledge of the gen3 games and due the completely absence of a ram map of this game like the ones for gen1. So I opened the rom’s elf file on IDA, the disassebly on github and the symbol files on vscode. I searched on vs for “xy” and found an interesting function name,
ScrCmd_getplayerxy. Searched that function on github and this popped out:So, my coordinates are saved inside the
gSaveBlock1Ptrstruct. I searched it inside the symbol files, and got the address0x0000000003005d8c. I opened that address inside the mGBA ram viewer(And I started crying, because I miss the BGB ram viewer and debugger :c), gone to this address, and I started moving around in game while observing the memory after gSaveBlock1Ptr. By scrolling a little bit inside the memory viewer, I finally found out that the player’s X/Y coordinates are saved starting from the address 0x3005e5e.So, I aligned vertically to the warp point, and positioned myself two “blocks” before the barrier (Check the following screen). Incremented by two my X coordinate and…

…and the X coordinate was back at the starting value, without being incremented. WTF. I first thought it was me being dumb in using the mGBA memory viewer, so I paused the game, changed the value and restarted the game: Only when the game was running the values was being overritten, meaning the “real” X/Y coordinates are somewhere else and they get copied over the ones we already have.
I decided to go with the “search in ram” tactic: I first searched my X coordinate in ram using the mGBA’s search tool, then moved one step, filtered for the new X, and so on, till I had only 3 addresses left: 0x3005e5e (which I already had), 0x203760 and 0x203764. I went full monkey, positioned myself like before, changed ALL of the addreses, did one step right, and I was warped into CCR2 successfully.
CCR2
“As you may have noticed, this entire world is made of many different small maps, loaded on demand. Use wathever tools you desire to reach the map with identifier 0x1337. Once this is done, return here and take the bottom exit to continue. Note: bypassing the barrier with client-side hacks won’t work anymore. Checks are done server-side. Next challenge will not load until this one is completed. Good luck!”.
Also this challenge is pretty similar to one from fools2018. That time I solved it my changing the ingame warp ids for the map boundaries. This time, however, I was on a gen3 game. I had no idea about how all of this stuff works here. So I decided to work on something I know. I opened burp suite, setted up the proper intercept rules, and I immediately starting seeing the communication between the local client and the server. Without any kind of SSL errors! This was because for making the client working for me, ZZAZZ disabled all SSL cert checks. It was working like a charm!
So I took one request the client made when I changed maps ingame and I noticed it contained only raw bytes encoded in base64. No clue however what those bytes meant!
This is when I had a good intuition: Fools2022 is really really similar to Fools2018. Maybe zzazz mantained also the map ids from the old event? So I searched the map I was loading using the Radixan’s Fools2018 map viewer, took the map ID, and voilat: it was encoded in little endian inside the request.
I used again burp suite to intercept a load-new-map request, changed the map id, forwarded the request, and loaded in game the map 0x1337. It worked, buuuuuuuuuuuuuuuuuuuut I had a problem:

I was in the middle of blocks, no way I could reach the land in the upperleft corner. The first thing I tried was searching for a WTW cheat code, but I was unable to get it working. Idk if it was me being dumb using the cheat tools of mgba or what. So I instead tried loading that map using another warp, maybe with some luck I would got a good alignment. But nope, it didn’t worked neither. Vcvtne2ps2bf16 suggested me to patch the collision function inside the rom, but I had problems in recompiling the game, so I had to “dynamically” patch it:
I searched for “collision” on the pokeemerald github, and found
CheckForPlayerAvatarCollision. Hooked up gdb to mGBA, putted a breakpoint and… And then I didn’t know how the ARM calling convention works. However, in the call tree I noticed that this function was called byPlayerNotOnBikeMovingat 0x0808AF0C which is something like:That after being compiled was

Now, I have no fuckin idea what those LSLS and LSRS instruction are. But for me it was enough seeing the
CMP collision ($R4), 0; BEQ <code blocks where there's no collision>(For non asm ppl, this meanscompare the value of the register $R4 (aka, the collision variable) with 0, and if it's equals (to 0), jump) to just put a breakpoint in gdb here (Address: 0x808af14) and set the register $R4 to 0 each time I took a step. Fun fact I learn: if in gdb you runset $R4=0, it won't give you any error but it won't work. You have to writeset $r4=0(Check the lowercase) to properly set the register.I eventually reached the land, and talked to a OOOOOOOOORB

“You did pretty well. Please reset your game”
Fool you, I made a savestat before warping to this map. Reloaded CC2, and walked to the now opened access to CC3
CCR3
“You’re halfway there. Here’s next challenge. The passage to the next map is guarded by a password. The objective is simple. Find the correct password. Note: bypassing the barrier with client-side hacks won’t work. Note: there may exist multiple correct passwords. Any of them will be accepted. Good luck!”
Sooo… Finally we got here, to this year’s password cracking/bruteforcing challenge
If you talked with the NPC that was there, he was, in order, prompting for a password, checking it (locally) and telling me if it was correct or not.
The first thing I did was searching for the underneath algorithm which was responsible of checking the password.
I tried CTRL+Cing from GDB immediately after inputting the password, however I was unable from looking at the stack trace of determining where all of this stuff was called from. I knew there weren’t map scripts (like in gen1/2) involved, some NPC in the map “WORLD 4-4” told me that, however I had no idea how custom code was handled. Sadly, my free time ended, and I had to quit and go work.
At work I was still thinking at the chall and while talking with Vcvtne2ps2bf16 he leaked me that I had to search up for
ScriptContext. So I did in the first moment of pause I had!scriptPtr looked really promising 👀

As I did in CC2, I searched the two variables holding the ScriptContext structure, and got their addresses:
0x03000E40for sScriptContext1 and0x03000EB8for sScriptContext2. Analized them in gdb (Btw it really looked like that the compiler rearranged the struct for optimization), to find out that only the script1 was used, and the script code’s address was0x0x020182c2. Runned the good ol’x/32iin gdb and got this:Well, I don’t know pretty much of arm asm, but this really looks like garbage to me
By looking furhter inside the pokeemerald git, I found this function:
u8 SetupBytecodeScript(struct ScriptContext *ctx, const u8 *ptr). Bytecode. Oh. So, the “map scripts” were not plain assembly, like in gen1/gen2 pokémons game, but a script with custom istructions “emulated” by the game itself. I read again the ScriptContext struct, and noticed theScrCmdFunc *cmdTable;. Not only it was running custom instructions, but each script could run its own set of custom instructions. I thought: It’s unlickely that ZZAZZ sends to the gameboy both the script, a translation table for the instructions, and ALL the instructions’ code to run the script, it’s way too much data, so he’s probably using the default cmdTable. This intuition was later confirmed also by Vcvtne2ps2bf16. Now it’s time to write a disassembler!The first thing we need is the full cmdTable. By searching again for ScriptContext in the repo, I found in src/scrcmd.c the code implemented by each default script-instruction. This will be useful later, but was not the table I was searching for (it’s not a table at all hahaha). By searching for one of the instruction name in that file (EG:

ScrCmd_nop), I found exactly what I wantedFrom data/script_cmd_table.inc
Now I (hopefully) have the scriptTable and I know where the script is, it’s time to dump the bytecode and translate (disassemble) it into human redable instructions!
Dumped 4kB of instructions thanks to GDB, copied it into vscode and with some search and replace I trasformed them into a huge-ass Python array. Before writing the disassembler script, I first wrote a script to generate the list of instructions by their index and the parameters as well as their size in byte/half word/word (Each instruction took a custom amount of parameters, you could see it clearly by the instructions implemented in src/scrcmd.c, making the script engine CISC-ish). The script was taking in input a stripped version of the script_cmd_table.inc file and the script.c file but without the extra extra functions not used in the cmdTable, and thanks to some regex magic it was yeeting out a cool array with everything:
With that script, I was able to create the disassembler:
…and got some code that finally made sense!

You can find it complete inside the google drive folder!
At that point, however, I ended again my free time and I had to be back at work again, always thinking about the chall. I knew
it was something fast to bruteforce, since Buurazu said he bruteforced and got the password directly, without needing of simplifying math like we did back in fools2018 (or 2019? The one where we had a decryption algorithm who was taking literally ages to complete, and we had to simplify xor operations to run it in minutes). I knew also there were many solutions to that chall, and this made me question myself if reversing the script would have really been faster than reimplementing the bytecode interpreter, feeding it with the original script data and then bruteforcing the input. When, in the late night, I was done with my work and I had time to play again on the chall, I decided to properly reverse it, since I was working on a potato laptop (Reminder I was not in my home in Rome!) where bruteforcing would have been slow
The first thing I did on the script was splitting the code into basic blocks (Which are, for non IT people, blocks of code without any kind of branch/jump), so by just doing that I could had a great overview of how the code is “composed”.
Luckily the script was really simple, and I didn’t need to create a control flow diagram of it :)
It all starts with a main method that resets a 16 bytes long buffer (as long as our input… Pretty sus), then calls a long function (presumibly, the password checking algorithm) and checks its result: if it’s 0 it exits, if it’s 1 it does a bunch of stuff before.
A cool thing that I want to point out, before reversing the check password function, is that dumping 4kB of data, was really a lucky decision: the full script is 4080 bytes long, it fits really closely my dump
Btw, as I said, the password checking algorithm was really simple. Already when I was tracing the basic blocks I noticed that the code was just the same thing over and over again: some movs and calcs, then a small loop, and then again another block of code like that. In the middle of the function there were just some extra movs, and at the end the compares that checks if the “processed” password is correct. Let’s see it in detail, starting by the block of code copypasted many times:
I already spoiled in the comments what’s happening here, however let’s recap it verbally: It first adds to our output variable (contained in 0x8001) a costant, than takes the i byte of the input (We know that the address of the input is like 0x02021dcX from the reset buffer function) and sums it to the output variable (by modifying the code itself! This is why ppl in the discord channel were talking about self modifying scripts. Luckily they’re just editing some variables, not modifying the execution flow. This would have been HELL). Then it loads an half word starting from 0x020375da, and changes the addvar inside the loop to add that value. It resets the out variable to 0 and then it adds the content from previous half word N times in a loop, effectively making a multiply function. The only thing to look into is what’s stored in 0x020375da, since our code doesn’t write at all in that memory location. Vcvtne2ps2bf16 spoiled it again: it was the memory address where the out variable (0x8001) is stored. The only three things that were changing between one of these blocks and the others: the inital constant, the address of the input buffer (of course) and the number of loops (The multiplier).
By rewriting in high level code the whole block, we have something like this:
There were 20 of these blocks inside the check password function. All of them are the same, except:
setvar 0x8001instead of aaddvar 0x8001since it was initializing the 0x8001 varcopyvar 0x8003, 0x8001; setvar 0x8001, 0x0539beforeWhich was basically checking if the calculated values are right.
Since the script was really really simple to reimplement and it was internally only some adds and muls, I decided to go full monke and use Z3 to get the password. I’m really sure that this was the easiest way to complete that chall in resonable computing time btw. But what is Z3? Z3 is a SAT solver. In a really really simple explaination, let’s say that normally you have an input, you feed that input to an algorithm, and you get the output which was NOT known at the beginning. Z3 helps you solving the opposite situation, when you have a known output (the final values that the variables 0x8001 and 0x8003 must have to be accepted as a valid input) a known algorithm (what we reversed so far), and you need to obtain the original UNKNOWN input (our password). In that instances, the input becomes a set of symbolic variables (That are like normal variables, except they have no value) that are processed inside the algorithm and the output becomes a set of constraints to be applied to the variables after (or even before!) they’re used in the algo. This has some limitations tho: Z3 is painfull slow with algorithms who uses symbolic variables as an index for arrays, and you cannot use a symbolic variable inside any kind of branch condition: If you don’t know the value of a variable yet, how do you know if you can take the branch or not? Luckily this was absolutely not the case with our script, and it was really easy (and fast) writing (and running!) a solution for that challenge
It was really longer to extract the constants and the multipliers from the script than writing the solver code (And I actually also managed to fail copying some constants, who made me found invalid passwords at first)
In order, you can see:
Pretty easy, isn’t it?
And the best part is that running this script on my potato laptop only takes 20secs to get a result! Here’s the first password it found:
sswlwsjxop. Note that with minor changes, you can get without problems the list of ALL correct passwords. Check thewhile(s.check() == sat):inside my fools 2021 writeup if you’re curious.Btw, little note: Z3 is not pretty good at handling overflows between differently sized vars… Even if we’re working with 8bit chars in input, I had to declare them 16bit long and add constraints to have them inside the [0x00, 0xff] range, or it was giving me bad results, loosing overall performances
When I solved CC3 it was really late in night, but this hasn’t stopped me from writing some code to search for meme passwords. So I added the following function to my code:
It’s basically adding new constraints to the input password, so the first N chars are the same of the text variable, both in lower and upper case. It could be better, it could also support l33t chars so you may have more chances to find a correct password starting by a given input, but it was made a 3am with the alarm set at 6am. I just wanted to sleep.
The first password I tried searching was “sus”+random letters. Since “sus” it’s only 3 chars long I wasn’t surprised about finding valid results:
suShBcpZjr. After sus, I tried “amogus”. Sadly I was unable to find a working password with printable chars starting like that, even using if I also included symbols and numbers. However “amogus”+random_bytes was actually a real password:AmOgUSk\x12\x83\x2a. I posted this inside the GCRI discord, and IssoTM immediately told me to kill this meme :(. Poor little sus guy… Btw, the next and the last thing I searched for was “bepis”+printable chars. Since bepis is one char shorter than amogus I was pretty much confident that I would have had results, and I was right!BEPIs8qoTzis a valid CCR3 password.Btw thanks also to IssoTM who provided me the full list of the characters translations! By using it, I made this simple script that takes in input hex bytes and outputs the encoded char:
CCR4
“You have come so far. Here’s your final mission! The person on your right gives away Silver Certificates of Commendation. They’re cool and all, but I really want to see a Gold Certificate. I heard the certificates are protected with military-grade AES encryption. Can you forge a Gold Certificate? Maybe there’s a weakness somewhere? That’s for you to find out. Good luck!”
I woke up early to take a 8-hours-long
economytrain, to lead back home. My plans were to complete the chall before my arrival, but the bad internet connection was not by my side.Btw, speaking about the chall:
In the ccr4 map there was an NPC. I talked to him, he connected to the servers and did stuff, then talked again to him, he connected again to the servers and showed me my certificate info (Cert issuer name, cert type (WE WANT TO CHANGE THAT!), cert serial number).
I reopened burp suite to start analyzing requests like I did in CCR2, and requested again a certificate. Opened the intercepted request and noticed it was a huge base64, that decoded gave me another base64 appended with raw data. By decoding also the inner base64, I got again raw data, different from the first one, meaning that the whole certificate is crypted in AES, and we don’t have something like cert+crypted hash.
🔴 REQ:

🔴 RESP:

At that point, while talking with Buurazu that was on this chall from more time than me, he proposed to work together and I accepted. He gave me the notes he made so far and an interesting thing immediately popped to my sight:

These really looks like the buffers used for communication. A good start for reversing what’s happening under the hoods! You can clearly see at the start the unencrypted certificate, and later what I sent in the previous request! I hooked again GDB and, as I did before, I got the address of the scripts that were handling this: Map script at 0x0203e332; Talk script at 0x020182b5. Aaaaaand mgba crashed.
Thinking about this crash, now, I can only say that I was really lucky, since I was going into an extremely wrong direction.
I reopened everything, loaded the old CCR2 savestate, made my way again into CCR4, talked to the NPC and this time he didn’t gave me a certificate. What? Talked to him again, and this time I got a certificate. WHAT??. Looked at burp suite and I noticed only now that the game was making two entirely different requests. I played with it a little bit and finally managed to understand what’s happening. The client first asks for a new certificate when you first talk to the NPC and only then validates it. I played lazy, and instead of reversing the code I decided to just look at the buffers discovered before while the requests are made to find out where the AES was being done, so I could reverse just that specific part. To my surprise, I found out that:
After these considerations I made a sequence diagram of the generation/validation protocol:
Ok. We know how the protocol works. We know that client side there’s nothing happening, the encryption and the validation are entirely done server side. What do we do now?
This is where I started struggling. We don’t really have too much to play with. I thought that trying to work on the already encrypted cert was worthless, since it is encrypted, so I tried doing random stuff to the unencrypted one. As expected, changing the cert type from silver to gold, even if using the correct padding and terminator, gave me a fantastic
FoolsDescriptiveProcessingError('you may only generate silver certificates'); I knew from the GCRI discord that CCR4 was easy, but not so much hahaha. I first, thanks to the help of IssoTM, made a client to do request easier to the server, instead of encoding and decoding by hand on cyberchef the base64s:So I tried doing random stuff, from appending other cert-types-tags to try something like the X-Forwarded-For http header attack, to trying incredible long names… Nothing worked. I only noticed that you could only generates certs with a length mutiple of 4, that you cannot include a = in the value of the cert (Or it would return a nice
Directive with more than 1 valueerror), and that you can actually have invalid keys (they will be ignored). I poked IssoTM (who was the 3rd getting this chall done) and asked him if it was more a crypto challenge or more something else. He told that it was purely crypto, and suggested me to look deeper into the encrypted packet.The first thing I did was checking if it was AES ECB


Plim plom, big actually moment:
When you encrypt and decrypt stuff in AES, you need to first split your data in blocks, and then work on each single block. The way you let every block interact with each other (as well as the key, the IV and the CipherText/PlainText) define the AES mode. ECB is a mode where each single block is indipendent from all the other ones. This introduces some vulnerabilities, for example, you can extract easily the context of the plain text starting by the ciphertext
Another vulnerability of ECB is that you can swap encrypted blocks without problems, to create a “custom” plain text. In CC4, this could be abused by, for example, creating a cert with the name aligned to have at the start of one block the string
goldand at the end of another block the stringtype=. By sending to the server the two packets swapped in order, you could had a cert that, when decrypted, it had the full/type=gold/in it. Example scheme:How do you check if you’re working with AES ECB? Just change one single byte at the start of the PT and see if it you have only one block changing in the CT.
Sadly for us, it was not ECB. Just by changing the first letter of the cert’s name, most of our PT has changed:
I noticed tho that the first block remined the same tho… Prolly it was containing the stuff that’s authority key/value prependend in the cert decryption
Btw, at this point I was really lost. I had really nothing much to do than literally trying to corrupt random bytes. I tried setting at 0 a byte in the middle of the encrypted cert, and got this as an answer:
b'N\x00\x00\x00error=1/desc=Directive with more than 1 value/authority=CrackerFour'WHAT? This is NOT an usual error! The exception is coded INSIDE the cert response, not like before! Is that progress? So I continued poking around and received yet another error:
b'N\x00\x00\x00error=1/desc=Invalid key i@%\x92b{\xc7\xab,\x83\xdf\x8f6\xae\x1e\xc5rFoursserial/authority=CrackerFour'. I AM CHANGING THE DECRYPTED PLAIN TEXT! WHAAAAAAAAAAT???I tried changing by a little bit the byte I was corrupting, and I eventually got
Wrong cert type smlver/authority=CrackerFour. OH. I asked confirmation to Buurazu if I was in the right direction and HE CONFIRMED. I was SOOOOOO hyped! I modified my request script to bruteforce a single byte, till I got in the error the correct byte, so I could manually move to the next one. This is the final version (the common functions are omitted).I let that script run for a while and boom, it worked! :D I’ve made the CCR4 achievment and by refreshing the map I was able to get into the last CCR room

“Congratulations! You have completed the Cracker Cavern Reborn! People of Glitchland are hugely amazed by your hacking skill. With such an achievement, I think a proper reward is in order. $You have unlocked the coveted Exploitation Expert title!$ This leaderboard flair will let everyone know of your greatness. Now go forth and continue exploring Glitchland. Hopefully, do so without hacking. Don’t ruin the fun for yourself!”
I’m crying ngl. But now the question is:

Why this actually worked?
*My
My wrong assumption was that if you change one byte of PT in any AES except ECB, you get full corruption of the CT, at least starting from the blocks where you’re doing the changes.


The challenge was using AES CBC; this is the decryption scheme:
What’s wrong in that? If you change one byte in the CT, where does this change propagates? Obviously to the PT’s block immediately decrypted from that block, Pboxes and Sboxes are a thing tho, but CBC is not ECB, so the change propagates also to the following blocks. In what way? Just with a xor. This is why by changing one byte in the CT, it yes corrupts the relative block in the PT, but it also changes only one byte in the following block’s PT: the one xorred with the one we’re changing. This is why my solution worked
Conclusion
This marks the end of my fools2022 writeup. The first person I want to thanks, is TheZZAZZglitch himself. Exactly 5 years ago I played Fools2017, the first cybersecurity competition I ever been part of. I never ever had any kind of experience before Fools2017, except some random pokémon hacking. Fools2017 made me love this kind of competitions and made me know what a CTF is. From there I started playing local, italian, CTFs and now I’m part of the mHACKeroni team, with which I had the honour to play Defcon CTF in the past years. Thanks to Fools2017 I also entered the good ol’ GCL discord, where I met a lot of friends during the years (and I also improved my completely broken english to something at least I can use to communicate decently). You all have no idea how much TheZZAZZGlitch’s fools events impacted my life in a positive way in this 5 years. I will be always grateful to him.
Btw, let’s leave behind my deep thoughts. I ofc want to thanks my fellow pasta friend Vcvtne2ps2bf16, then Buurazu and IssoTM whom helped a lot with CCR3 and CCR4! I also want to thank the entire GCRI discord, it’s an amazing community and it’s always fun having a lot of people talking together while solving the fools challanges :)
And, thanks also to you, dear reader, that managed to get 'till here!
So this is the end. Even if shorter than the previous years, this event was still hella fun. I must admit that I liked that the challenges were not so hard. Since nowdays I’m more and more busy with study and work, I was glad I was able to get a perfect score with less work than the past years!
See you next year guys!
(Copy pasted from before, but it’s important)
🔴 Play this and the old Fools editions here! https://stranck.ovh/foolsRehosting
🔴 This year notes! https://stranck.ovh/writeups/data/fools2022/notes
🔴 Google drive folder containing everything I used during the event: https://drive.google.com/drive/folders/1YHG-E_-jy6cLS-teHRol88Cyu9NJr0Gq?usp=sharing
Extra
Here you can find random stuff used during the challs :)
ncat connection forwarding
As I said in the introduction, I had problems with gdb and mGBA. Loading the context was taking ages, a single instruction could cost up to 3secs. One thing I did for mitigate this problem was running mgba and gdb inside the same VM. However, I was not able to proxy the client thru burp suite if I runned it under wine. And the client was only listening on localhost. Solution? Use ncat to forward the traffic from the vm to the windows host! Those are the commands I runned in windows to forward both of the ports used by the mGBA’s dolphin protocol
It really slowed down the communication between the client and mgba, but it worked! :D
420 maps visited
From the GCRI discord I knew a bunch of folks that managed to get up to 65k maps visited while dumping them. This made me think that even invalid maps are counted in that total. So I took the packet from CCR2 and made a script that made me visit exactly 420 maps. memes.
Lottery match bot
Next fools event I swear I’m gonna call myself Stranck_abcdefghijklmnopqrstuvwxyz if there are again lottery match achievments. Btw here’s a bot that tries to get something more than a triple match (Spoiler: it is failing)
Oh, the packet I’m using was dumped thanks to burp suite directly when you ask ingame for a lottery match. Luckily for us the request is always the same and we don’t have to do offline computation: the checks for any match are done server side
FemboyThere were a lot of femboys mentions inside the whole event. So much of them that I’ve been infected
It’s saying “finally you will become femboy”. It was right
Solving CCR3 but ✨ gay ✨
mfs getting ready to play fools2022