Reverse engineering

Reverse engineering a dead MMORPG protocol with nothing but the client

How I went from an unreadable, protected game binary to a working server emulator, with no servers, no documentation and no prior experience.

A discontinued MMORPG client can contain enough information to reconstruct much of its protocol: logging strings point toward dispatch code, runtime type information reveals packet classes, and deserializers expose how messages are read. That is how I went from a protected executable I could barely understand to a working server emulator, without the original servers, documentation or packet captures. It took years.

Quick answer

  • The client used an old version of Themida. Unpacking it and rebuilding its imports made the application code accessible to analysis in IDA.
  • Log strings such as RECV name=%s size=%d led into the networking and packet dispatch code.
  • The engine's reflection metadata exposed class names and relationships; vtables helped locate their methods.
  • Packet serializers and deserializers revealed field order, but the stream helpers still had to be understood to establish the actual encoding.
  • Encryption and compression were the missing transport pieces. In 2022, I finally reached the world selection screen.
  • In 2025, AI helped document packets from my analysis and IDA database. The emulator is roughly 80% functional, with results checked against the client.

Why this game

I started in 2019, when I was 15. It was my first real reverse engineering project, and the reason was simple: I had played the game, I missed it, and its official servers were gone. I wanted to find out whether the client on my disk was enough to bring a server back.

I had no documentation, no captures of the original service and, honestly, no idea what I was doing. Everything had to come from the client and my attempts to make it respond.

I am keeping the game unnamed and the emulator private because of my concerns about sharing it. This is the story of the investigation.

Step zero: a binary IDA could not make sense of

I dropped the executable into IDA and got what looked like garbage. The analysis did not give me useful application code, and the decompiler had little to work with. The client was protected with Themida, which can obscure code and run protected sections through a virtualization layer. Oreans' documentation describes that virtualization machinery.

This client used an old protection version. Public tools for that generation helped recover an unpacked executable and rebuild its import table. I am leaving out the tools and procedure; the interesting work here began once I could inspect the application itself.

The rebuilt executable still started and behaved like the original in the paths I checked. That gave me a usable baseline, although startup alone could not prove that every function was unchanged.

First contact with real code

An unpacked binary does not suddenly become readable source. Functions still had names like sub_401020, and I did not know which ones mattered. I started around WinMain and followed calls that mostly looked meaningless to me.

Eventually, something clicked: the names were missing, but the purpose was still there. A logging helper still logged. A function I could not explain was not useless; I had not found its context yet.

Logging helpers referenced strings written by the original developers. Those gave me a better starting point than instructions alone.

Strings first: finding the network layer

I searched for words I expected around networking: send, recv, packet and socket. One useful format string looked like this:

RECV name=%s size=%d

The message suggested a received packet with a printable name and a size. Following its cross-references led into the receive logging code and then the dispatch code around it. IDA's basic usage guide covers the string, cross-reference and renaming tools behind this kind of navigation.

A string is a lead. Whether the logged size described a frame, an encrypted buffer or a decoded packet still required reading the surrounding calls. But I finally had a specific question to investigate.

RTTI: finding the engine's type system

Following those references, I started seeing repeated object and type-information patterns. The engine used runtime type information as part of a reflection system, allowing code to identify types by name. Names and relationships that were absent from ordinary function labels were still present in that metadata.

There is a useful distinction here. C++ RTTI supports identifying types during execution; it does not automatically provide a complete reflection system or a list of every field. An engine can build its own facilities around type metadata. What helped me was this engine's particular combination of names, relationships and virtual methods.

I could now group related classes and connect them to their vtables. It was not the original source tree, but I could start recognizing the design.

Packets are classes

The hierarchy became clear: packet types derived from a base Packet, and Packet derived from Object, the root of the engine's type system. Concrete packet classes had virtual methods, including serialization and deserialization routines.

Those methods read or wrote fields through a binary stream object. The following C++-style pseudocode illustrates the pattern. The class, field names and helper names are invented for this explanation; this is not code or a packet definition from the game.

// Illustrative pseudocode: declarations and error handling omitted.
void CharacterSelectPacket::Deserialize(BinaryStream& stream)
{
    characterId = stream.ReadU32();
    slot        = stream.ReadU16();
    flags       = stream.ReadU8();
    name        = stream.ReadString();
}

Assuming the integer helpers consume fixed-width values as named, this suggests four bytes, two bytes, one byte, then a string representation. It establishes a reading order. It does not yet establish all the bytes needed to construct a valid message.

What the reader showsWhat still needs checking
A 32-bit integer readByte order, signedness and the field's meaning
A 16-bit integer readWhether it is an identifier, count, flags or something else
A string helperLength representation, encoding, termination and limits
A sequence of readsConditional branches, repeated fields and inherited reads
A packet classIts message identifier, framing and valid conversation state

The helper implementations are part of the specification. A name like ReadString is not enough to decide whether the wire uses a length prefix or a terminator. Likewise, a vtable pointer in an object is an implementation detail, not a field to copy into a network message.

I renamed types, methods and fields as I worked through them. The IDA database gradually became readable. Each established name made the next class easier to understand, and the repeated serialization pattern made it possible to document packets without historical traffic captures.

The missing piece: encryption and compression

Knowing packet fields did not mean I could send something the client would accept. I had made progress on parsing while still missing the encryption and compression around the messages. A correct payload inside the wrong transport representation was still unusable.

The distinction is between the bytes a packet deserializer consumes and the bytes that arrive from the connection. Framing, compression and encryption can transform or wrap those bytes. Their order and boundaries have to come from evidence in the client, not from what seems conventional.

This stalled me for a long time. I made numerous attempts over the following years. In 2022, I finally got the client to start and show the world selection screen. Seeing a game with no official servers respond to something I had built was one of the best moments of the project.

That screen was a concrete milestone. It showed that enough of the conversation worked to reach that state. It did not mean every packet or gameplay system was finished.

The slow years

Progress slowed after that. I could no longer spend much time on the project, and it sat for a while. Work like this benefits from uninterrupted time, which I did not have.

The names and notes in the database helped me recover my own understanding when I returned, without starting every investigation from scratch.

2025: bringing AI into the loop

In 2025, I started exploring AI automation for this work. I gave an AI agent my analysis results and the IDA database, or IDB. It helped considerably with development and packet documentation, especially the repetitive task of walking through many similar classes and describing their fields.

The agent worked from type names, renamed methods and stream operations that had taken years to establish. It helped process that existing material at a larger scale.

I still verify the results against the client. A confident field description is not enough if the reader has a branch the explanation missed. The useful distinction is between documenting an observed operation and guessing its meaning. AI can help write either one fluently; that does not make them equally reliable.

Where it stands

My rough estimate is that the server emulator is about 80% functional. That is a personal assessment of progress, not a measured coverage figure or proof of complete compatibility. Without the original server implementation, I cannot claim to have reproduced its internals exactly.

What I have is a working reimplementation that behaves sufficiently like the service the client expects for me to play again. It is my own small relic of nostalgia. The motivation is still the same as when I started: I wanted to return to a game I missed.

What I learned

  1. Unreadable is not meaningless. Recognizable behavior survives missing names.
  2. Strings narrow the search. A log message gives you a few callers to investigate.
  3. Type metadata gives structure. Reflection connected names, inheritance and methods.
  4. Rename constantly. The database is also a record for your future self.
  5. Check the transport. Field order alone does not make a valid exchange.
  6. Tools help; verification decides. The client remains the reference.

Where this is not enough

This is one client, one engine and one protection version. Another binary might have no useful strings, less type metadata or very different serialization. Tools that helped with an old Themida version are not a general solution for other versions or configurations.

The client cannot reveal every server-side decision. Matching what it sends and expects does not reconstruct all the original game's rules. Completing one action only verifies that path.

Where this fits in my work

I came to reverse engineering through a game I wanted back. It changed how I read code, including my own, and explains why undocumented protocols still interest me. That curiosity runs through my selected work and yumeko, my Yahoo Messenger service emulator.

For the broader method behind this account, see Reverse engineering starts with a notebook, not a guess. Here, the personal result was simpler: after years of trying to understand a client, I could finally use it again.

Frequently Asked Questions

Can you share the game's name or the server?

No. I am keeping the game unnamed and the emulator private because of my concerns about sharing it. This article describes the investigation without publishing the client's code or its protocol definitions.

Did you need to be able to run the game?

Yes. Static analysis provided clues, but a running client let me check how it responded to my implementation. Confirming that the rebuilt executable still ran was an initial baseline, not proof that every later inference was correct.

Why start from strings?

Strings can preserve the developers' own vocabulary after ordinary symbols are gone. A logging message gave me a route into networking code, although its surrounding calls still had to be inspected to understand what it described.

How much did AI actually help?

It helped document many packet classes from an existing analysis and assisted development. I have no measured speedup to claim. Understanding the engine and checking the client still mattered.

About the author

Alexandru Talnaci

Full-stack and systems engineer in Bucharest. Co-founder and technical lead at Legacies, working across web applications, integrations and C++ systems.

Explore the projects behind the notes
Back to all notesSubscribe via RSS