~/forbannet/blog~reverse-engineering-mu-onlin…
now compiling: io_uring branch, --releaselatest push: e8af13c → main · 12 min agoreading: "What Every Programmer Should Know About Memory" — Drepper, 2007currently playing with: ftrace + perf for syscall latencyopen PRs: 3 · issues triaged today: 14now compiling: io_uring branch, --releaselatest push: e8af13c → main · 12 min agoreading: "What Every Programmer Should Know About Memory" — Drepper, 2007currently playing with: ftrace + perf for syscall latencyopen PRs: 3 · issues triaged today: 14
Techie#5782
Techie#5782guest
//.post.06.programmingpublished
DETOURS.DLL / 0x4C3F / ws2_32::connect()  ws2_32::send()  ws2_32::recv()
55                 PUSH    EBP
8B EC              MOV     EBP, ESP
83 EC 14           SUB     ESP, 14h
E8 25 00 00 00     CALL    DetourTransactionBegin
0F B6 45 08        MOVZX   EAX, BYTE [EBP+8]
KERNELRIOT / reader::post / markdown::render / prism::pending
55                 PUSH    EBP
8B EC              MOV     EBP, ESP
post.0x06programming.reader
programmingmmorpgreverse-engineering

Reverse Engineering MU Online Protocol

MU Online is old, ancient, but their protocol is something that I still enjoy to this day! And I've learned a lot from what could be called the first <cl>real</cl> MMORPG networking. And it has been done right.

/blog/reverse-engineering-mu-online-protocol

Before we start

If you read my previous blog post where I mentioned MU Online for the first time, it is clear that I've been tinkering with the Game Client and Server for a long time. It will be quite hard to summarize everything here, but I will do my best to show what I've learned, what I think is odd and what I think is great.

Some background and inspiration

I will stress how hard it is to write a blog post about reverse engineering, and I will pay respects to those who helped me pave my way here. I am not the best reverse engineer around, most of my peers are brilliant and they are above me in many ways, I've learned a lot from them. My main inspirations are:

Akaruz from Ragezone, which was not only who inspired me to become a programmer, but helped me a lot on my earlier days. I miss you a lot old friend.

Vlatix helped me a lot with C and Assembly.

Asxetos was very kind to me and because of "CheatHappens" and his help, I snowballed on MU Online - Maya.

I can't stress how Drew Benton inspired me to move forward with MMORPG protocols and bots, and he is a brilliant guy, extremely humble and an absolute gem.

WeeMan from the creator of PHBot, we used to hate each other, we were somehow competitors back in the day, now that we are older, we've made amends. His SRO Bot is great and he is just as stubborn as I am. Congratz man! I really enjoy your product and how you strive to make it better.

Last but not least, Ivo Ivanov, cheers man, I still remember when I bothered you for weeks (or was it months?) - Thank you for your attention and your gigantic patience.

Going back to the topic

It is a fact, posts that try to explain Reverse Engineering adventures often fail, and I will fail as well! But I will at least fail differently, what I am going to do is: I will show you some quirks, parts of the code, and the final product!

I think that the main reason we struggle when trying to explain how we develop bots, aimbots, wallhacks, is that, it is often a very complicated solution all glued together in a mess of API Hooking, Drivers and Networking Magic that we don't even understand how does it work, not even ChatGPT can help us. But it does work and I have proof!

Encryption

The core is Encryption, especially if you're aiming for a Clientless bot, which is ideal. The weak point is always the client, there you can try to guess how the packets are encrypted, this is where we spent 80% of our time and brainpower. This part of the code tends to be less straightforward.

For MU Online the key lies on a couple tricks:

Loading the Encryption Keys

/// <summary>
/// All static crypto tables and server-specific keys.
/// Nothing here is logic — pure data.
/// Swap keys for a different server, everything else stays the same.
///
/// ReadOnlySpan properties over constant data → compiler embeds in PE data section.
/// Zero heap allocation. Zero GC tracking. The bytes live in the binary itself.
/// </summary>
public static class MuKeys
{
    // ── Protocol Constants ──────────────────────────────────────────

    public const byte C1 = 0xC1;
    public const byte C2 = 0xC2;
    public const byte C3 = 0xC3;
    public const byte C4 = 0xC4;

    // ── XOR Tables (universal, same for all MU 0.97D servers) ───────

    /// <summary>3-byte rotating XOR for login/password fields.</summary>
    public static ReadOnlySpan<byte> Xor3 => [0xFC, 0xCF, 0xAB];

    /// <summary>32-byte XOR table for C1/C2 content encoding (client→server).</summary>
    public static ReadOnlySpan<byte> XorC1C2 =>
    [
        0xE7, 0x6D, 0x3A, 0x89, 0xBC, 0xB2, 0x9F, 0x73,
        0x23, 0xA8, 0xFE, 0xB6, 0x49, 0x5D, 0x39, 0x5D,
        0x8A, 0xCB, 0x63, 0x8D, 0xEA, 0x7D, 0x2B, 0x5F,
        0xC3, 0xB1, 0xE9, 0x83, 0x29, 0x51, 0xE8, 0x56
    ];

    /// <summary>XOR mask applied when reading .dat key files (not needed if keys are hardcoded).</summary>
    public static ReadOnlySpan<uint> XorDatFile => [0x3F08A79B, 0xE25CC287, 0x93D27AB9, 0x20DEA7BF];

    // ── Server Keys (hardcoded, pre-XOR'd from .dat files) ──────────
    //
    // Layout per key span [12]:
    //   [0..3]  = moduli
    //   [4..7]  = multipliers (enc) or inverse multipliers (dec)
    //   [8..11] = XOR seeds
    //
    // Enc/Dec pairs share moduli [0..3] and seeds [8..11],
    // differ only in multipliers [4..7] (multiplicative inverses).

    /// <summary>Client→Server encryption keys.</summary>
    public static ReadOnlySpan<uint> Enc1 =>
    [
        0x0001F44F, 0x00028386, 0x0001125B, 0x0001A192,
        0x00005BC1, 0x00002E87, 0x00004D68, 0x0000354F,
        0x0000BD1D, 0x0000B455, 0x00003B43, 0x00009239
    ];

    /// <summary>Server→Client encryption keys.</summary>
    public static ReadOnlySpan<uint> Enc2 =>
    [
        0x00011E6E, 0x0001ADA5, 0x0001821B, 0x00029C32,
        0x00003371, 0x00004A5C, 0x00008A9A, 0x00007793,
        0x0000F234, 0x0000FB99, 0x00008A2E, 0x0000FC57
    ];

    /// <summary>Client→Server decryption keys.</summary>
    public static ReadOnlySpan<uint> Dec1 =>
    [
        0x0001F44F, 0x00028386, 0x0001125B, 0x0001A192,
        0x00007B38, 0x000007FF, 0x0000DEB3, 0x000027C7,
        0x0000BD1D, 0x0000B455, 0x00003B43, 0x00009239
    ];

    /// <summary>Server→Client decryption keys.</summary>
    public static ReadOnlySpan<uint> Dec2 =>
    [
        0x00011E6E, 0x0001ADA5, 0x0001821B, 0x00029C32,
        0x00004673, 0x00007684, 0x0000607D, 0x00002B85,
        0x0000F234, 0x0000FB99, 0x00008A2E, 0x0000FC57
    ];
}

These keys are stolen in runtime, use your favorite Disassembler and grab the unpacked keys from the game client.

Encoding

If you've done your homework, you'll clearly remember that for MU Online packets that start with C1 and C2 are not Encrypted, they can be encoded, but not encrypted. Most C1/C2 packets are easy to debug and replay, but if you want to do something like walk or send a message you'll need to encode them.

Here is the encoding method:

static void Encode8To11(
    Span<byte> o, int oo,
    ReadOnlySpan<byte> p, int po,
    uint n, ReadOnlySpan<uint> k)
{
    Span<byte> fin = stackalloc byte[2];
    fin[0] = (byte)(n ^ 0x3D);
    fin[1] = 0xF8;
    for (int i = 0; i < n; i++) fin[1] ^= p[po + i];
    fin[0] ^= fin[1];
    BitCopy(o, oo, 0x48, fin, 0, 0x00, 0x10);

    Span<ushort> cb = stackalloc ushort[4];
    cb.Clear();
    for (int i = 0; i < n; i += 2)
    {
        cb[i / 2] = p[po + i];
        if (i + 1 < n) cb[i / 2] += (ushort)(p[po + i + 1] * 0x100);
    }

    Span<uint> r = stackalloc uint[4];
    r[0] = ((k[8] ^ cb[0]) * k[4]) % k[0];
    r[1] = ((k[9] ^ (cb[1] ^ (r[0] & 0xFFFF))) * k[5]) % k[1];
    r[2] = ((k[10] ^ (cb[2] ^ (r[1] & 0xFFFF))) * k[6]) % k[2];
    r[3] = ((k[11] ^ (cb[3] ^ (r[2] & 0xFFFF))) * k[7]) % k[3];

    uint s0 = r[0], s1 = r[1], s2 = r[2], s3 = r[3];
    r[2] ^= k[10] ^ (s3 & 0xFFFF);
    r[1] ^= k[9] ^ (s2 & 0xFFFF);
    r[0] ^= k[8] ^ (s1 & 0xFFFF);

    Span<byte> sr = stackalloc byte[16];
    sr.Clear();
    for (int i = 0; i < 4; i++)
        BinaryPrimitives.WriteUInt32LittleEndian(sr[(i * 4)..], r[i]);

    BitCopy(o, oo, 0x00, sr, 0, 0x00, 0x10); BitCopy(o, oo, 0x10, sr, 0, 0x16, 0x02);
    BitCopy(o, oo, 0x12, sr, 4, 0x00, 0x10); BitCopy(o, oo, 0x22, sr, 4, 0x16, 0x02);
    BitCopy(o, oo, 0x24, sr, 8, 0x00, 0x10); BitCopy(o, oo, 0x34, sr, 8, 0x16, 0x02);
    BitCopy(o, oo, 0x36, sr, 12, 0x00, 0x10); BitCopy(o, oo, 0x46, sr, 12, 0x16, 0x02);
}

Disconnects and details

Many of us banged our heads, because even though we managed to encrypt our packets accordingly and "replay" them. After a while it would crash. It took a genius to figure out that there was a counter, every packet you sent to the server, the server will increment +1 to a counter.

This counter is only 1 byte long, so 00/FF, the maximum value is 255. We often replayed the packets and couldn't notice this tiny detail, it worked for our first 255 packets because we were mimicking what we received, but after that, we were faced with a brutal Disconnect.

/// <summary>
/// Rolling C3/C4 packet counter per connection. Thread-safe. Wraps 0-255.
/// </summary>
public sealed class MuCounter
{
    byte _value;
    readonly Lock _lock = new();

    public MuCounter(byte initial = 0) => _value = initial;
    public byte Current => _value;

    public byte Next()
    {
        using (_lock.EnterScope())
        {
            byte v = _value;
            _value = (byte)((_value + 1) & 0xFF);
            return v;
        }
    }

    public void Reset(byte value = 0)
    {
        using (_lock.EnterScope()) _value = value;
    }
}

You have to keep a close eye on the counter, once it gets to 255, reset it to 0 and start again. You'll be sending thousands of packets, and if you miss a single packet, you're done.

Simple things become bloated very fast

For instance, our first and most important action, we need to log in to the game server. Ideally we're looking for a packet like this:

0xC344778C28171FAD5593A4DEEB48703B64569C01EEDCCAFFE40C6C048C37B28DA1FFCA0EFD4E9CE44CD108E9EDD8F97B09BCD825C0B6A4E1D4CDC97381D1BED18E74C4F1

The decrypted packet looks like

0xC131F1018CBDCA92A4D2FCCFABFCCDFF99CCFC9BFCCFABFCD6BCE30B313030313353444653343533343566676667676A6A

What information do we have there? Let's start with the "non-encrypted" Login packet.

C1 is the packet type, now it is decrypted, so, C1. The second byte 31 is the packet size. F1 is the OpCode and 01 is the sub OpCode. Then we have both Username/Password encoded. Then a random big number that took me months to realize that it was Environment.TickCount, after that we have the Server Serial.

Since we are developing a full clientless bot, we need to figure out every single step of the Client and Server communication.

So this is just an introduction on how I've achieved my fully functional MU Online Clientless Bot. Below is a video of the bot in action, and it works like a charm.

It took me around 2 years to have it fully figured out and working, but the end result is something that I am still proud of to this day:

End Result

/comments

comments (0)

Markdown supported, fenced code encouraged.

no comments yet — be the first.
// add to the thread
TE
posting as Techie#5782 guest
be excellent. ⇧⏎ for newline.