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 real
MMORPG networking. And it has been done right.
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.
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 ammends. 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.
It is a fact, posts that try to explain Reverse Engineering adventures often fail, and I will fail aswell! But I will atleast 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 togheter 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!
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:
private static readonly uint[] xor_tab_muerror = new uint[4] { 2676079996u, 1457689405u, 1053979434u, 3214246898u };
private static readonly byte[] xor_table_3byte = new byte[3] { 252, 207, 171 };
private static readonly uint[] xor_tab_datfile = new uint[4] { 1057531803u, 3797729927u, 2480044729u, 551462847u };
private static readonly byte[] xor_tab_C1C2 = new byte[32]
{
231, 109, 58, 137, 188, 178, 159, 115, 35, 168,
254, 182, 73, 93, 57, 93, 138, 203, 99, 141,
234, 125, 43, 95, 195, 177, 233, 131, 41, 81,
232, 86
};
private static uint[] enc1_keys = new uint[12];
private static uint[] enc2_keys = new uint[12];
private static uint[] dec1_keys = new uint[12];
private static uint[] dec2_keys = new uint[12];
These keys are stolen in runtime, use your favorite Disassembler and grab the unpacked keys from the game client. For 0.98Y you are looking for this:
uint[] enc1_keys = {
0x1F44F,
0x28386,
0x1125B,
0x1A192,
0x5BC1,
0x2E87,
0x4D68,
0x354F,
0xBD1D,
0xB455,
0x3B43,
0x9239
};
uint[] dec1_keys = {
0x11E6E,
0x1ADA5,
0x1821B,
0x29C32,
0x4673,
0x7684,
0x607D,
0x2B85,
0xF234,
0xFB99,
0x8A2E,
0xFC57
};
uint[] enc2_keys = {
0x11E6E,
0x1ADA5,
0x1821B,
0x29C32,
0x3371,
0x4A5C,
0x8A9A,
0x7393,
0xF234,
0xFB99,
0x8A2E,
0xFC57
};
uint[] dec2_keys = {
0x11E6E,
0x1ADA5,
0x1821B,
0x29C32,
0x4673,
0x7684,
0x607D,
0x2B85,
0xF234,
0xFB99,
0x8A2E,
0xFC57
};
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:
private static void Encode8BytesTo11Bytes(byte[] outbuffer, int outbuf, byte[] packet, int pktptr, uint num_bytes, uint[] dec_dat)
{
byte[] finale = new byte[2]
{
(byte)num_bytes,
0
};
finale[0] ^= 61;
finale[1] = 248;
for (int k = 0; k < num_bytes; k++)
{
finale[1] ^= packet[pktptr + k];
}
finale[0] ^= finale[1];
ShiftBytes(outbuffer, outbuf, 72u, finale, 0, 0u, 16u);
uint[] ring = new uint[4];
ushort[] cryptbuf = new ushort[4];
for (int j = 0; j < num_bytes; j += 2)
{
cryptbuf[j / 2] = packet[pktptr + j];
if (j + 1 < num_bytes)
{
cryptbuf[j / 2] += (ushort)(packet[pktptr + j + 1] * 256);
}
}
ring[0] = (dec_dat[8] ^ cryptbuf[0]) * dec_dat[4] % dec_dat[0];
ring[1] = (dec_dat[9] ^ (cryptbuf[1] ^ (ring[0] & 0xFFFF))) * dec_dat[5] % dec_dat[1];
ring[2] = (dec_dat[10] ^ (cryptbuf[2] ^ (ring[1] & 0xFFFF))) * dec_dat[6] % dec_dat[2];
ring[3] = (dec_dat[11] ^ (cryptbuf[3] ^ (ring[2] & 0xFFFF))) * dec_dat[7] % dec_dat[3];
uint[] ring_backup = ring.ToArray();
ring[2] = ring[2] ^ dec_dat[10] ^ (ring_backup[3] & 0xFFFFu);
ring[1] = ring[1] ^ dec_dat[9] ^ (ring_backup[2] & 0xFFFFu);
ring[0] = ring[0] ^ dec_dat[8] ^ (ring_backup[1] & 0xFFFFu);
byte[] subring = new byte[16];
for (int i = 0; i < 4; i++)
{
subring[i * 4] = (byte)(ring[i] % 256u);
subring[i * 4 + 1] = (byte)(ring[i] / 256u);
subring[i * 4 + 2] = (byte)(ring[i] / 65536u);
subring[i * 4 + 3] = (byte)(ring[i] / 16777216u);
}
ShiftBytes(outbuffer, outbuf, 0u, subring, 0, 0u, 16u);
ShiftBytes(outbuffer, outbuf, 16u, subring, 0, 22u, 2u);
ShiftBytes(outbuffer, outbuf, 18u, subring, 4, 0u, 16u);
ShiftBytes(outbuffer, outbuf, 34u, subring, 4, 22u, 2u);
ShiftBytes(outbuffer, outbuf, 36u, subring, 8, 0u, 16u);
ShiftBytes(outbuffer, outbuf, 52u, subring, 8, 22u, 2u);
ShiftBytes(outbuffer, outbuf, 54u, subring, 12, 0u, 16u);
ShiftBytes(outbuffer, outbuf, 70u, subring, 12, 22u, 2u);
}
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 1byte 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
.
public MuPacket(byte[] source, int index, bool tos = false, byte counter = 0)
{
Source = source;
Index = index;
ToServer = tos;
Counter = counter;
}
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 send thousands of packets, and if you miss a single packet, you're done.
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-ecrypted" 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 function 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 to this day: