Welcome!

Join our community of MMO enthusiasts and game developers! By registering, you'll gain access to discussions on the latest developments in MMO server files and collaborate with like-minded individuals. Join us today and unlock the potential of MMO server development!

Join Today!

v193+ Encryption

Newbie Spellweaver
Joined
Sep 11, 2016
Messages
59
Reaction score
81
This is going to be my last release as I already quit maplestory development but I still see people struggling with this. I released a v188 idb that wasnt virtualized and gave all the information necessary but people still couldn't figure it out so here's leech-able code (for the most part).

Credits go to myself and Eric as we worked together to figure this out.

There are 2 parts to this.
Part 1

Multiple Encryption Types: Neckson introduced a secondary encryption type (Value 2) in their MakeBufferList which replaces AES (Value 1). They only use this for their (Server -> Client) packet data as (Client -> Server) still uses AES. It should also be noted they use this on every server except their login server.

The code to decrypt / encrypt this is below.

Code:
private static void EncInit(int uSeqSnd, byte[] aSrc, boolean bDecrypt) {
        for (int i = 0; i < aSrc.length; i++) {
            if (bDecrypt) {
                aSrc[i] -= (byte) uSeqSnd;
            } else {
                aSrc[i] += (byte) uSeqSnd;
            }
        }
    }

Bonus MapleShark Code:

Code:
public void EncInit(byte[] aBuffer, bool bLoopback) {
    uint uIV = BitConverter.ToUInt32(aIV, 0);
    for (int i = 0; i < aBuffer.Length; i++) {
        if (!bLoopback) {
            aBuffer[i] -= (byte)(uIV);
        } else {
            aBuffer[i] += (byte)(uIV);
        }
    }
}

Part 2
Opcode Encryption is something they introduced for their (Client -> Server) packets. The packet data itself still uses AES as its encryption but the opcodes are shuffled afterwards using DES-EDE2. Note that they only do this on every server except their login server. You will know if they do this or not by receiving a packet that is a length of 32,776 after MigrateIn. As of v194, the header to this packet is "40". Note that the block size is 4 because every opcode is 4 bytes encrypted however with 3DES, each encryption is 8 bytes as they combined 2 opcodes into a string for an 8 byte encryption.

The dissection of this packet is like this:

Code:
[XX XX XX XX] - Block Size: The amount of bytes to read for each encrypted opcode.
[XX XX XX XX] - Buffer Size: The amount of bytes to read for the buffer
[XX x Above Length] - Buffer: This includes all the encrypted opcodes plus padding if amount of opcodes does not reach full buffer length.

Here is the Java class to support 3DES.

Code:
/**
 *
 * @author PacketBakery
 */
public class TripleDESCipher {
 
    public byte[] aKey = new byte[24];
    public Key pKey;
 
    public TripleDESCipher(byte[] aKey) {
        System.arraycopy(aKey, 0, this.aKey, 0, aKey.length);
        this.pKey = new SecretKeySpec(aKey, "DESede");
    }
 
    public byte[] Encrypt(byte[] aData) throws Exception {
        Cipher pCipher = Cipher.getInstance("DESede");
        pCipher.init(Cipher.ENCRYPT_MODE, this.pKey);
        return pCipher.doFinal(aData);
    }
 
    public byte[] Decrypt(byte[] aData) throws Exception {
        Cipher pCipher = Cipher.getInstance("DESede");
        pCipher.init(Cipher.DECRYPT_MODE, this.pKey);
        return pCipher.doFinal(aData);
    }
}

Bonus MapleShark Class:

Code:
public class TripleDESCipher {
    public static String Decrypt(byte[] aData, byte[] aKey) {
        TripleDESCryptoServiceProvider pCipher = new TripleDESCryptoServiceProvider();
        pCipher.BlockSize = 64;
        pCipher.Mode = CipherMode.ECB;
        pCipher.Padding = PaddingMode.None;
        pCipher.FeedbackSize = 64;
        pCipher.GenerateIV();
        MethodInfo mi = pCipher.GetType().GetMethod("_NewEncryptor", BindingFlags.NonPublic | BindingFlags.Instance);
        object[] aObj = { aKey, pCipher.Mode, pCipher.IV, pCipher.FeedbackSize, 1};
        ICryptoTransform pTransform = mi.Invoke(pCipher, aObj) as ICryptoTransform;
        byte[] aResult = pTransform.TransformFinalBlock(aData, 0, aData.Length);
        pCipher.Clear();
        return UTF8Encoding.UTF8.GetString(aResult);
    }
}

Now there's a few things to note. First is the key.
The key consists of 2 parts;
Part 1: Character ID converted into ASCII
Part 2: Machine ID

The key length is a total of 16 bytes, it can not be less and it can not be more. Note that since we are using TripleDES which uses a 24 byte key, this means the first 8 bytes of your key need to be appended as the last 8 bytes as well.

So we need to initialize our key:

Code:
byte[] aKey = new byte[24];

Next we need to convert our characterid into ascii and append it into our key array.

Code:
String sCharacterID = String.valueOf(this.dwCharacterID);
for (int i = 0; i < sCharacterID.length(); i++) {
    aKey[i] = (byte) sCharacterID.charAt(i);
}

After this, part 1 is completed. Next is onto part 2.
We need to get the length of our current key so we dont go over 16 bytes and then we need to append our machineid into our key.

Code:
int nLength = sCharacterID.length();
for (int i = nLength; i < 16; i++) {
    aKey[i] = pMachineID.aMachineID[i - nLength];
}

Now our key is 16 bytes but since 3DES requires a 24 byte key, we append the 1st 8 bytes as our last 8 bytes as well.

Code:
System.arraycopy(aKey, 0, aKey, 16, 8);

Now our key is complete and we can create our buffer with our encrypted opcodes.
The way I do this is using a map to maintain the real opcode and encrypted opcode for when I decrypt it later in my OnPacket functions.

This map has to be local to the User (or their Socket) as each user does not get the same opcodes.

Code:
public Map<Integer, Integer> mEncryptedOpcode = new LinkedHashMap<>();

Next we have to populate the map with the real & encrypted opcodes.

Code:
List<Integer> aUsed = new ArrayList<>();
String sOpcode = "";
for (int i = ClientPacket.BeginUser.Get(); i < ClientPacket.Count.Get(); i++) {
    int nNum = Utilities.GetRandom(ClientPacket.BeginUser.Get(), 9999);
    while (aUsed.contains(nNum)) {
        nNum = Utilities.GetRandom(ClientPacket.BeginUser.Get(), 9999);
    }
    String sNum = String.format("%04d", nNum);
    if (!aUsed.contains(nNum)) {
        this.mEncryptedOpcode.put(nNum, i);
        aUsed.add(nNum);
        sOpcode += sNum;
    }
}
aUsed.clear();

After we populated our map, we have to create the buffer and send it to our user.
Note: The buffer does not have to be a size of 32,776 however I add random bytes to pad it since that's also what neckson does. The required length only needs to be that of the amount of opcodes.

Code:
TripleDESCipher pCipher = new TripleDESCipher(aKey);
try {
    byte[] aBuffer = new byte[Short.MAX_VALUE + 1];
    byte[] aEncrypt = pCipher.Encrypt(sOpcode.getBytes());
    System.arraycopy(aEncrypt, 0, aBuffer, 0, aEncrypt.length);
    for (int i = aEncrypt.length; i < aBuffer.length; i++) {
        aBuffer[i] = (byte) Math.random();
    }
    SendPacket(ClientSocketPacket.OnOpcodeEncryption(4, aBuffer), false);
} catch (Exception e) {
    e.printStackTrace();
}

As explained above, our packet will look like this:

Code:
public static OutPacket OnOpcodeEncryption(int nBlockSize, byte[] aBuffer) {
    OutPacket oPacket = new OutPacket(LoopbackPacket.OpcodeEncryption, false);
    oPacket.EncodeInt(nBlockSize);
    oPacket.EncodeInt(aBuffer.length);
    oPacket.EncodeBuffer(aBuffer);
    return oPacket;
}

Now the last part is decrypting it to the real opcodes when the client sends the server your encrypted header that you created. Wherever you read in the packet and decode your opcode, you will have to modify it to read from your encrypted opcode map:

Code:
int nType = iPacket.DecodeShort();
if(pSocket.mEncryptedOpcode.containsKey(nType)) {
    nType = pSocket.mEncryptedOpcode.get(nType);
}

...and that's all you have to do. Below is the mapleshark bonus to decrypt and sniff properly but it'll also be one code block.

Code:
if (nOpcode == OpcodeEncryption) {
    int nSize = BitConverter.ToInt32(packetBuffer, 4);
    byte[] aData = new byte[nSize];
    Buffer.BlockCopy(packetBuffer, 8, aData, 0, nSize);
    String sOpcode = TripleDESCipher.Decrypt(aData, pMain.aKey);
    try {
        int x = 0;
        for (int i = ClientPacket.BeginUser; i < ClientPacket.Count; i++) {
            pMain.mEncryptedOpcode.Add(Int32.Parse(sOpcode.Substring(x, 4)), i);
            x += 4;
        }
    } catch (Exception e) {
        MessageBox.Show(e.Message);
    }
} else if (nPort != LoginServer.nPort && bOutbound) {
    if (pMain.mEncryptedOpcode.ContainsKey(nOpcode)) {
        nOpcode = (ushort) pMain.mEncryptedOpcode[nOpcode];
    }
}

Now you should be able to handle the new encryptions both for your server and sniffing.
Again, credit to myself and Eric as he was a big help when we were working on this together.
This could of been released long ago by a few people who were aware of it and the only reason I'm releasing it now is because the 1st person who figured it out decided to keep it private and not offer much help to people who asked (or claimed they knew it and were full of poop but who knows). But thats it, I'm gone from this scene and wish everyone who still develops a maplestory server best of luck.
 
Last edited by a moderator:
Newbie Spellweaver
Joined
Mar 25, 2016
Messages
86
Reaction score
26
Cool stuff ill never use but cool
 
Last edited:

lai

Newbie Spellweaver
Joined
Jan 27, 2018
Messages
14
Reaction score
1
Cool stuff ill never use but cool
 
Custom Title Activated
Loyal Member
Joined
Jan 18, 2010
Messages
3,109
Reaction score
1,139
Very cool stuff.

All functions of the new version of idb cannot be decompiled. Isn't all functions handled by vm?

If you're referring to opcode crypto in new IDBs, all of the functions are VM'd, yes. Hence why the only way you'd be able to debug and implement this is with the unvirtualized v188 client/idb that he released previously. They're both the same though, the crypto hasn't changed since; only difference is that as of v193 you can't get in-game properly without it
 
Skilled Illusionist
Joined
Jul 17, 2010
Messages
333
Reaction score
165
It really doesn't matter but... isn't it since v186 (Override/Beyond) that they've started to use the part 1 crypto?
(and IIRC, the part 2 crypto has been used since v183 (Kerning Tower))
 
Newbie Spellweaver
Joined
Sep 11, 2016
Messages
59
Reaction score
81
It really doesn't matter but... isn't it since v186 (Override/Beyond) that they've started to use the part 1 crypto?
(and IIRC, the part 2 crypto has been used since v183 (Kerning Tower))

Yes these were added around the time NXL was introduced and I forgot what versions they were actually added however part 2 was never forced until v193 so I just went with that version in title.
 
Banned
Banned
Joined
Aug 31, 2016
Messages
193
Reaction score
1
Cool use ill never stuff but cool
Cool use ill never stuff but cool
 
Custom Title Activated
Loyal Member
Joined
Jan 18, 2010
Messages
3,109
Reaction score
1,139
What is ClientPacket.BeginUser?

Since somebody else had asked me about this, I'll just quote what I said previously.

Eric said:
ClientPacket.BeginUser is what comes right before UserTransferFieldRequest (in Odin this is ChangeMapHandler) and is the start of the USER section up until END_USER of course. ClientPacket.Count is the very last opcode in the client + 1 (aka the total count of ops from start to finish). I'm not sure what version you're on because that opcode would change every version almost. The 16 byte buffer I mentioned previously was in MigrateIn (in Odin this is InterServerHandler).
 
Back
Top