Even before the idea of this blog even arose, I’ve been asked about reversing Among Us and examining its information as more and more people started to get interested in it. I decided this is a great idea since I personally got interested in game hacking a whole lot recently and found this to be a great example to showcase some Unity game reversing.
Without any prior information on Among Us and the underlying engine in detail, let’s dive right in and see where this takes us and whether we can gain enough information to reimplement the network protocol and the online features of the game.
As a rule per thumb: Whenever you plan to reverse a game, make sure to actually play it! This probably sounds obvious, but seriously think about the in-game structures you encounter, what could be possible handled client-side, what could be taken care of by the server, and how the game is generally put together.
Even Among Us, while being a rather simplistic example, is complex enough to give you a real struggle in understanding its inner relations without knowing any gameplay. Take your time to play and think about these things from the perspective of a developer or hacker rather than a player.
An initial look into the installation directory of the game (in my case, a copy for messing around) reveals that the game was built in the popular game engine Unity. Unity provides rich scripting for games in C# through Mono, so my first thought was “this is going to be a piece of cake”.
But turns out, the Among Us developers had other plans for us! They made use of a Unity feature called IL2CPP which compiles the C# Assembly logic of the game’s scripting into native C++ code. The
il2cpp_data directory in
Among Us_Data gives hints about this. Of course, we are not intimidated by the fact that we’re going to reverse machine code generated from C++ and start right away to explore this binary.
With no prior experience with IL2CPP, I started to inspect
Among Us.exe in Ghidra which is just an entrypoint to the Unity runtime by calling
UnityMain from the
UnityPlayer.dll library. So I did some more research about IL2CPP on the interwebs and learned that
GameAssembly.dll is what contains the actual game logic.
At the same time, I came across Il2CppInspector which is capable of restoring class, function and type information for the
GameAssembly.dll C++ logic, which is a MASSIVE time saver over trying to do this by hand.
We can feed it the
GameAssembly.dll along with the
Among Us_Data/il2cpp_data/Metadata/global-metadata.dat file and it exports the resources we need to get proper analysis of
GameAssembly.dll inside Ghidra.
After loading it in, we click “No” when being prompted to auto-analyze the code. Instead, we parse the previously exported header file and execute the Python script to get the analysis we desire. For me, this took a very long time to complete, so be patient, get yourself some tea and relax while Ghidra labels everything.
When the process is done, the script produced all the relevant symbols for us. However, they are all mangled into output like
[email protected]@[email protected]@[email protected]. Fortunately, Ghidra ships with a certain
DemangleAllScript.java which makes our lives a lot easier. It demangles all the signatures into readable output and from there we can start to examine the list of functions of this clusterfuck for useful stuff related to the network protocol.
After some digging around and inspecting cross-references to functions with interesting names, we learn that most of the networking is done by a class called
InnerNetClient. With a closer look at all the methods that seem like they could do some networking, e.g.
InnerNetClient#KickUser, we’ll notice the usage of a
MessageWriter instance to outline a message definition.
Something that immediately hits the eye is the encapsulation of
MessageWriter#StartMessage, followed by packet data serialization, terminated by
MessageWriter#EndMessage. After that, the
MessageWriter object is being passed to
InnerNetClient#FLAJOMCGOMK, which presumably sends the message to the server and ultimately
MessageWriter#Recycle is used to free the memory allocated for the message buffer.
In unrelated news, I have several theories about how the name of the send method originated: Crack consumption, the cat running over the keyboard, a rampage, or all of these.
Examining these functions reveals that
MessageWriter#StartMessage pushes the start position of the internal message buffer onto a stack structure stored by the instance. This is necessary because
MessageWriter#EndMessage will need the start position to calculate the total length of the message which gets encoded in the first two bytes of the packet. Accordingly,
StartMessage reserves those bytes in position so no packet data get written to it. Following up the packet length, a single byte known as the
typeFlag gets encoded. This identifies the type of packet that we’re looking at (and thus gives hints about how to parse the follow-up data).
This can be further visualized through a Wireshark scan, which reveals further details (such as the use of UDP for communication).
We see the sequence
22 00 which turns into
0x22 bytes analogously to
Data (34 bytes). This is correct! The following type flag is
…which matches the implementation of
InnerNetClient#HostGame!! This serves us as instructions on how to find and learn every game packet we don’t know about yet.
…Again such funky names…
Now we need to get our hands dirty and actually look at how
MessageWriter’s API serializes supplied value types into packets. Its design comes very close to a Rust
Cursor, in case you’re familiar with the language. And if you aren’t, read the link anyway because it’s a good explanation.
We have different methods for serializing 8-bit, 16-bit and 32-bit signed and unsigned integers. All of the formats (except byte, obviously) are written in little-endian byte order to the internal buffer maintained by the
Floating-point numbers are represented through the
Single structure on the C# side. In contrast to the integer approach, these are written as bytes in big-endian order.
In case of a string, the serialization is two-fold. For one part, a string gets lowered down into a byte array through
UTF8Encoding.UTF8.GetBytes and the length of said array gets written to the packet using the packing algorithm. And for the other part, the byte array representing the string gets written.
For raw byte arrays, not related to strings, one can choose whether to pack the length of it too or just write the raw data.
For certain purposes in Among Us, e.g. packing Player IDs, a packing algorithm is applied to 32-bit integers.
It works by doing a
while (true) loop that ends when the value to write is zero. In every round, the low 8 bits of the current value get OR’d with
0x80. If the result of that is smaller than
0x80, the unmodified low 8 bits are used. This is then being written to the buffer like a single byte. And finally, the value to write gets shifted to right by 7 bits and the algorithm repeats. The purpose of all this is to store a number within a 32-bit boundary in as few bytes as possible.
MessageWriter and doing a rudimentary Wireshark scan, we learned the structure of packets, the serialization of types, and the communication protocol, UDP. By reversing
InnerNetClient, we learn the concrete layout of different types of packets and their purpose. And so we got a precise overview of the game client’s workings and a previously fully unknown packet protocol with no knowledge of the game in advance and minimal knowledge of Unity internals. This provides enough information to implement the client protocol. By analyzing the callbacks for server packets, one can learn about server-side messages and how a client is supposed to handle them.
Parallel to that, there’s a
InnerNetServer class which contains networking logic for a server. Diving into this will reveal the information an open source game server reimplementation would need. A funny and interesting project idea, left as an exercise to the reader. 😉