DISCLAIMER
This post is NOT SUPPOSED to be a writeup for GameHacking CTF, though it involved some usage of mod loading/game reverse engineering. I highly recommend you to try on GameHacking CTF before reading this writeup. This writeup do not provide a suggestion or method of game hacking that you can apply in real game hacking.
Table of contents
Open Table of contents
Introduction
This year, when I am an apprentice in SEFCOM @ Arizona State University, I have got my first DEF CON! During the event, most of the time I worked on GameHackingCTF, comes from GameHacking.gg. They have hosted GameHackingCTF as the main activity of Game Hacking Community, and it was so much fun! I have made new friends (who worked in gaming industry and made my mind blow with slot machines 🤯), and a lot of merch!!!

During the competition, our team was thinking of how to grab all the flags as fast as possible. However, Hacker Jeopardy was so much fun that I spent more 3 hours each day to watch it xD, so I didn’t finish it on time. But anyways, when I come back my lab, I did some game hacking and somehow I figured out what I have missed in my previous attempt to grab all the flag, and here you have this writeup :D
A little bit (that I know) about Unity Games
Flag protection mechanism
As we are thinking the game as a Reversing challenge, it must have some flag protection mechanism right? Else you can just use strings… So lame. As mentioned above, using dnSpy and the Module Assembly-Csharp.dll will help us know about the game logic.

There are two sus guys: Decryptor and Encryption? Is this the flag that we want to seek???
Decryptor
Firstly, the method B() seems to be invoked first.

This method does nothing much, it only checks if the item name contains Flag or not (but you may ask, which item?). If it is, it will go to method A(), bringing with the g.EventItem as a param.

Method A() just do some sanity check, like the tag “Inventory”, base.transform.localRotation, etc. Not much that we will look into. It also check if the description contains GHCTF{ or not, which is the flag format of the CTF. Then, it will invoke the Encryption.B() method, with two params is:
- The
e.Description, where.Descriptionis some field of theInventoryItem. From the game, we can know that the flag is one of the inventory, collected after we passed the level. base.transform.localRotation.ToString(), wherebase.transform.localRotationis thelocalRotationfield of theDecryptorobject, that is somewhere in the game asset. The.ToString()method is fromC#, which is kinda nasty (we will see later D:)
Encryption
Now we know that the flag will be “encrypted” in game assets and “decrypted” when we solved the challenge. Let’s check how this thing works.
Follow the previous Decryptor.A(), we can quickly navigate Encryption.B().

Oh, Encryption.B() is not so complicated I guess :D. Remind ourselves that the call of Encryption.B() in Decryptor.B():
Encryption.B(description, base.transform.localRotation.ToString())
So, as we can see in here, the function firstly decode the description (as we know that it is the description of some objects in game assets), from base64 -> bytestream. Next, the .ToString() string is going in to Encryption.C(), which is

The SHA256 hexdigest of the .ToString() string. Next, it will be decrypted by XOR One-time Pad (OTP). If the length of base64 encoded description is longer than the hexdigest, it will go cyclic.
Summing up
So, in general, the challenge author uses OTP as the flag protection. To break the mechanism, there are two only ways:
- YOU SOLVE THE GAME HACKING!!!! (Very fun, highly recommended. Thanks again
GameHacking.GGfor making my first DEF CON super interesting :D) - You must know:
- Where, and which object is holding the
.Description()? (for the Base64-encoded one) - Where, and which object is the “Decryptor” one, which we will grab the
.localTransform()field. - And data format of
.ToString(), as well.
- Where, and which object is holding the
Then the final part is just following the decryption :D
How I solved it
As mentioned above, I did think of this way could make us at least go into Top 10 (and Hall of Fame as well, yay!). But oh boy, Hacker Jeopardy was so interesting as well so I forgot what I need to do :X. When I comes to LVCC in the last day, I had only 2 hours to do all the jobs so… :D we are Ok with our 1337 achievement :D And a Game Hacking Fundamentals book ~~
First attempt - Full Static Analysis
From the information I provided above, you could know at least, everything you need is in front of your eyes, the game assets! So I look for some plugins/programs that can help me navigate through the game asset. I chose AssetRipper and it did a quite good job at the first time.
If you have a look into the game asset, you could see that there is a bunch of level{xxx} file in the folder. I have tried to open it and… TADA!



Great! We have found the rotation. But remember, the rotation is parsed via .ToString()… And I am too lazy to install a whole bunch of Unity just to run a line of code. So, I have check the docs from Unity, reverse the type of .localTransform(), and know that .localTransform() is actually a Quaternion object. This doc helped me to know what is the original C# type. Then, I just went to DotNetFiddle to write a prototype. Code is sponsored by Claude :)

Seems like the default one took 6 decimal places… I have written a test script to check this output. As the level is pretty easy, I could grab its flag: GHCTF{this_is_the_first_flag_woot}, and during the solve of this quest, my friends from team BitBodgers (Thanks a lot Jacob and Eric :D) have debugged the games and give me the Description of the first level:

With a little of Python, you can verify this quite quick:
from pwn import *
from base64 import b64decode
from hashlib import sha256
description = "rj4VUH9bck+ujJxE2HM1In1EDKHIh80e4i8/kIl4qeGdCw=="
flag = b"GHCTF{this_is_the_first_flag_woot}"
_candidate_to_String = b"{X: -0.600107, y: 0.156564, z: -0.618445, w: -0.482581}"
def get_pad_from_rotation(rotation_string: bytes):
return sha256(rotation_string).digest()
print(xor(b64decode(description), get_pad_from_rotation(_candidate_to_String)) == flag)
# not to spoil, but it's false :)
What I am WRONG????? I am not sure, let’s ask my friend Claude :)

After several trial-and-errors… I have figured out a little bit :))) The .ToString() method from Unity has the output as Claude said, but in the current version of the game, it is implemented as rounding up to 5 decimal places instead of 6 as Claude suggested. Also, I have figured out how to implement the rounding method of C# because the native way of Python, round() seems not work :#. Here is the link you need.
Phew… we have finished half of the puzzles. I am also looking for a quick way to grab all these values from some lib like unityparser or UnityPy but they’re so dumb, I gave up on it :)
Grab the description - Melon Loader come in the game!
When I look through the level{xxx} files, seems like it doesn’t have anything related to the descriptions, where we need to find. So, I decided to use MelonLoader and UnityExplorer to search inside the game. This way can help you to figure out the hidden flag so you should try :). Installation is quite easy so I will not mention here.
From the conditions that we found in the previous reverse engineering, I have put some filters on and…


Ehe :D Now we have all the things right? Actually, these descriptions can be found in file resources.assets in the game asset.

Let’s write solve script
So the final part is quite easy. You can, manually copy the description and the rotation, as well as write a parser on the data. I am looking for ways to automate this, so if you know some, tell me plsss :DDDDDD. The script is kinda long so here is the gist you look for.
Final words
Again, thanks GameHacking.gg for bringing this very interesting CTF, where I learned more on game hacking and reversing :D Hope the next DEF CON we will have… Game Hacking Village? (so I can hope that I can have a black badge… =))))