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!

MapleStory 2 Packet Encryption (Java)

Custom Title Activated
Loyal Member
Joined
Jan 18, 2010
Messages
3,109
Reaction score
1,139
This is Nexon's official MapleStory 2 Packet Encryption, written in Java. With this, and the client IDBs I've provided, should be a big help on getting started. I do plan on releasing my Orion2 emulator itself soon, but I want to fix up some things with it first. Until then, here is all you should need to be able to set up a working crypto for your packets. If you have any questions, feel free to ask away here and I'll get back to you.

Take note: OrionConfig.VERSION refers to the version of the client. The version has to be correct in order for the crypto to be in sync with the client.

BufferCryptManager
PHP:
/*
 *      This file is part of Orion2, a MapleStory2 Emulator Project.
 *      Copyright (C) 2017 Eric Smith <muffinman75013@yahoo.com>
 * 
 *      This program is free software: you can redistribute it and/or modify
 *      it under the terms of the GNU General Public License as published by
 *      the Free Software Foundation, either version 3 of the License, or
 *      (at your option) any later version.
 * 
 *      This program is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU General Public License for more details.
 * 
 *      You should have received a copy of the GNU General Public License
 */
package base.network.crypto;

import base.common.OrionConfig;

/**
 * Encryption Manager.
 * Handles the various routines used to encrypt/decrypt
 * MapleStory2 packet buffers.
 * 
 * @author Eric
 */
public class BufferCryptManager {
    private static final BufferCryptManager INSTANCE = new BufferCryptManager(OrionConfig.VERSION);
    public static final int
            ENCRYPT_NONE        = 0,    // No encryption
            ENCRYPT_REARRANGE   = 1,    // Table swapping encryption
            ENCRYPT_XOR         = 2,    // XOR encryption (Rand32 XOR mask)
            ENCRYPT_TABLE       = 3     // Table swapping with shuffle encryption (256-byte Rand32 shuffling)
    ;
    private final Crypter[] encrypt;
    
    public BufferCryptManager(int version) {
        this.encrypt = new Crypter[4];
        
        this.encrypt[(version + ENCRYPT_REARRANGE) % 3 + 1] = new RearrangeCrypter();
        this.encrypt[(version + ENCRYPT_XOR) % 3 + 1]       = new XORCrypter();
        this.encrypt[(version + ENCRYPT_TABLE) % 3 + 1]     = new TableCrypter();
        
        for (int i = 3; i > 0; i--) {
            this.encrypt[i].init();
        }
    }
    
    public static BufferCryptManager getInstance() {
        return INSTANCE;
    }
    
    public boolean decrypt(byte[] src, int offset, int seqBlock, int seqRcv) {
        if (seqBlock != 0) {
            int block = 0;
            
            while (seqBlock > 0) {
                block = seqBlock + 10 * (block - seqBlock / 10);
                
                seqBlock /= 10;
            }
            
            if (block != 0) {
                int dest;
                while (block > 0) {
                    dest = block / 10;
                    
                    Crypter crypt = encrypt[block % 10];
                    if (crypt != null) {
                        if (crypt.decrypt(src, offset, seqRcv) == 0)
                            return false;
                    }
                    
                    block = dest;
                }
                return true;
            }
        }
        return true;
    }
    
    public int encrypt(byte[] src, int offset, int seqBlock, int seqSnd) {
        int dest = 0;
        if (seqBlock != 0) {
            int block = seqBlock / 10;
            
            while (block != 0) {
                block = seqBlock / 10;
                dest = 10 * block;
                
                Crypter crypt = encrypt[seqBlock % 10];
                if (crypt != null) {
                    crypt.encrypt(src, offset, seqSnd);
                }

                seqBlock = block;
            }
        }
        return dest;
    }
}

Crypter
PHP:
/*
 *      This file is part of Orion2, a MapleStory2 Emulator Project.
 *      Copyright (C) 2017 Eric Smith <muffinman75013@yahoo.com>
 * 
 *      This program is free software: you can redistribute it and/or modify
 *      it under the terms of the GNU General Public License as published by
 *      the Free Software Foundation, either version 3 of the License, or
 *      (at your option) any later version.
 * 
 *      This program is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU General Public License for more details.
 * 
 *      You should have received a copy of the GNU General Public License
 */
package base.network.crypto;

/**
 * Crypter.
 * The interface to which each type of encryption implements.
 * 
 * @author Eric
 */
public interface Crypter {
    
    public void init();
    public int encrypt(byte[] src, int offset, int seqKey);
    public int decrypt(byte[] src, int offset, int seqKey);
    
}

TableCrypter
PHP:
/*
 *      This file is part of Orion2, a MapleStory2 Emulator Project.
 *      Copyright (C) 2017 Eric Smith <muffinman75013@yahoo.com>
 * 
 *      This program is free software: you can redistribute it and/or modify
 *      it under the terms of the GNU General Public License as published by
 *      the Free Software Foundation, either version 3 of the License, or
 *      (at your option) any later version.
 * 
 *      This program is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU General Public License for more details.
 * 
 *      You should have received a copy of the GNU General Public License
 */
package base.network.crypto;

import base.common.OrionConfig;
import base.util.Rand32;

/**
 * Extended Encryption (Table swapping with shuffled indexes)
 * 
 * @author Eric
 */
public class TableCrypter implements Crypter {
    private final byte[] decrypted;
    private final byte[] encrypted;
    
    public TableCrypter() {
        this.decrypted = new byte[256];
        this.encrypted = new byte[256];
    }
    
    @Override
    public void init() {
        int[] shuffle = new int[256];
        for (int i = 0; i < shuffle.length; i++) {
            shuffle[i] = i;
        }
        
        Rand32 rand32 = new Rand32((int) Math.pow(OrionConfig.VERSION, 2));
        shuffle(shuffle, rand32);
        
        // Shuffle the table of bytes
        for (int i = 0; i < shuffle.length; i++) {
            encrypted[i] = (byte) (shuffle[i] & 0xFF);
            decrypted[encrypted[i] & 0xFF] = (byte) (i & 0xFF);
        }
    }
    
    @Override
    public int encrypt(byte[] src, int offset, int seqKey) {
        int dest = 0;
        if (offset != 0) {
            while (dest < offset) {
                src[dest] = encrypted[src[dest] & 0xFF];
                
                dest++;
            }
        }
        return dest;
    }
    
    @Override
    public int decrypt(byte[] src, int offset, int seqKey) {
        if (offset != 0) {
            for (int i = 0; i < offset; i++) {
                src[i] = decrypted[src[i] & 0xFF];
            }
        }
        return 1;
    }
    
    private void shuffle(int[] data, Rand32 rand32) {
        int len = data.length - 1;
        
        while (len >= 1) {
            int rand = (int) (rand32.random() % (len + 1));
            
            if (len != rand) {
                if (rand >= data.length || len >= data.length) {
                    return;
                }
                int val = data[len];
                
                data[len] = data[rand];
                data[rand] = val;
            }
            
            --len;
        }
    }
}

RearrangeCrypter
PHP:
/*
 *      This file is part of Orion2, a MapleStory2 Emulator Project.
 *      Copyright (C) 2017 Eric Smith <muffinman75013@yahoo.com>
 * 
 *      This program is free software: you can redistribute it and/or modify
 *      it under the terms of the GNU General Public License as published by
 *      the Free Software Foundation, either version 3 of the License, or
 *      (at your option) any later version.
 * 
 *      This program is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU General Public License for more details.
 * 
 *      You should have received a copy of the GNU General Public License
 */
package base.network.crypto;

/**
 * Basic Encryption - Simple table swapping.
 * 
 * @author Eric
 */
public class RearrangeCrypter implements Crypter {
    
    public RearrangeCrypter() {
        
    }
    
    @Override
    public void init() {
        
    }
    
    @Override
    public int encrypt(byte[] src, int offset, int seqKey) {
        int len = offset >> 1;
        
        if (len != 0) {
            for (int i = 0; i < len; i++) {
                byte data = src[i];
                
                src[i] = src[i + len];
                src[i + len] = data;
            }
        }
        
        return 0;
    }
    
    @Override
    public int decrypt(byte[] src, int offset, int seqKey) {
        int len = offset >> 1;
        
        if (len != 0) {
            for (int i = 0; i < len; i++) {
                byte data = src[i];
                
                src[i] = src[i + len];
                src[i + len] = data;
            }
        }
        
        return 1;
    }
}

XORCrypter
PHP:
/*
 *      This file is part of Orion2, a MapleStory2 Emulator Project.
 *      Copyright (C) 2017 Eric Smith <muffinman75013@yahoo.com>
 * 
 *      This program is free software: you can redistribute it and/or modify
 *      it under the terms of the GNU General Public License as published by
 *      the Free Software Foundation, either version 3 of the License, or
 *      (at your option) any later version.
 * 
 *      This program is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU General Public License for more details.
 * 
 *      You should have received a copy of the GNU General Public License
 */
package base.network.crypto;

import base.common.OrionConfig;
import base.util.Rand32;

/**
 * Extended Encryption - XOR
 * 
 * @author Eric
 */
public class XORCrypter implements Crypter {
    private final byte[] shuffle;
    
    public XORCrypter() {
        this.shuffle = new byte[2];
    }
    
    @Override
    public void init() {
        Rand32 rand1 = new Rand32(OrionConfig.VERSION);
        Rand32 rand2 = new Rand32(2 * OrionConfig.VERSION);
        
        shuffle[0] = (byte) (rand1.randomFloat() * 255.0f);
        shuffle[1] = (byte) (rand2.randomFloat() * 255.0f);
    }
    
    @Override
    public int encrypt(byte[] src, int offset, int seqKey) {
        int dest = 0;
        if (offset != 0) {
            int flag = 0;
            while (dest < offset) {
                src[dest] ^= (shuffle[flag] & 0xFF);
                
                dest++;
                flag ^= 1;
            }
        }
        return dest;
    }
    
    @Override
    public int decrypt(byte[] src, int offset, int seqKey) {
        if (offset != 0) {
            int flag = 0;
            for (int i = 0; i < offset; i++) {
                src[i] ^= (shuffle[flag] & 0xFF);
                
                flag ^= 1;
            }
        }
        return 1;
    }
}

Rand32
PHP:
/*
 * This file is part of Orion2, a MapleStory2 Emulator Project.
 * Copyright (C) 2017 Eric Smith <muffinman75013@yahoo.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package base.util;

/**
 * MapleStory RNG
 * 
 * @author Eric
 */
public class Rand32 {
    private static Rand32 RAND;
    
    private int s1;
    private int s2;
    private int s3;
    
    /**
     * Construct a new Rand32 RNG.
     * 
     * Initializes the seeds of the RNG with the respective default values.
     * All initial seeds will be modified by the time(0) 
     */
    public Rand32() {
        this((int) (System.currentTimeMillis() / 1000));
    }
    
    public Rand32(int seed) {
        int rand = crtRand(seed);
        
        this.s1 = seed | 0x100000;
        this.s2 = rand | 0x1000;
        this.s3 = crtRand(rand) | 0x10;
    }
    
    /**
     * For use with all global/static Rand32 generated Randoms.
     * This is considered to be our global rand (g_rand).
     * 
     *     [USER=850422]return[/USER] The global Rand32 instance
     */
    public static Rand32 getInstance() {
        if (RAND == null) {
            RAND = new Rand32();
        }
        return RAND;
    }
    
    /**
     * Nexon's old RNG formula used to create a random.
     * 
     * This formula is used when generating a Random in
     * KMS Beta clients and other old versions. In addition,
     * Nexon uses this formula for their Center communication
     * sequences.
     * 
     *     [USER=2000183830]para[/USER]m seed The seed value to create the random.
     *     [USER=850422]return[/USER] The newly created Rand
     */
    public static int crtRand(int seed) {
        return 214013 * seed + 2531011;
    }
    
    /**
     * Generates a new random within a specified range (R) 
     * and beginning at a specified start (N).
     * 
     *     [USER=2000183830]para[/USER]m range The maximum range of the random
     *     [USER=2000183830]para[/USER]m start The minimum random
     *     [USER=850422]return[/USER] A new random
     */
    public static final Long getRand(int range, int start) {
        if (range != 0)
            return getInstance().random() % range + start;
        return getInstance().random();
    }
    
    /**
     * A shortcut to generating a random versus:
     * g_rand->Random()
     * or: GetInstance()->Random()
     * or: new Rand32().Random()
     * 
     *     [USER=850422]return[/USER] A new pseudorandom number
     */
    public static final Long genRandom() {
        return getInstance().random();
    }
    
    /**
     * Uses the available unsigned integer seeds,
     * bitshifts them around, updates the past seeds,
     * updates the current seeds, and returns an 
     * unsigned integer of the newly generated Random.
     * 
     * -->We do not need to unsigned shiftright because
     * we choose to use a standard int64 (long) as our
     * initialized v3~v6 variables. Only in the end do
     * we need to cast back our long to the bits of an
     * unsigned integer. 
     * 
     * -->In addition, we use a Long object for the simple
     * reason that we have the free ability to cast the Long
     * back into a intValue() returned by the object.
     * 
     *     [USER=850422]return[/USER] An unsigned integer Random
     */
    public Long random() {
        int v3;
        int v4;
        int v5;

        v3 = ((((s1 >> 6) & 0x3FFFFFF) ^ (s1 << 12)) & 0x1FFF) ^ ((s1 >> 19) & 0x1FFF) ^ (s1 << 12);
        v4 = ((((s2 >> 23) & 0x1FF) ^ (s2 << 4)) & 0x7F) ^ ((s2 >> 25) & 0x7F) ^ (s2 << 4);
        v5 = ((((s3 << 17) ^ ((s3 >> 8) & 0xFFFFFF)) & 0x1FFFFF) ^ (s3 << 17)) ^ ((s3 >> 11) & 0x1FFFFF);

        s3 = v5;
        s1 = v3;
        s2 = v4;
            
        return (s1 ^ s2 ^ s3) & 0xFFFFFFFFL;
    }
    
    public float randomFloat() {
        int bits = (int) ((random() & 0x007FFFFF) | 0x3F800000);
        
        return Float.intBitsToFloat(bits) - 1.0f;
    }
}

The above encryption is used to encrypt the body of packets, where as MapleStory used AES (and some Shanda). In MapleStory 2, however, the packet headers are still the same CInPacket and COutPacket from MapleStory. As such, here is what the RequestVersion (aka OnConnect in MapleStory) looks like:
PHP:
OutPacket packet = new OutPacket(LoopbackPacket.RequestVersion);
packet.encodeInt(OrionConfig.VERSION);
packet.encodeInt(sequence.getRecvSeq()); // Receive Sequence
packet.encodeInt(sequence.getSendSeq()); // Send Sequence
packet.encodeInt(sequence.getBlockSeq()); // Block (Body) Sequence
packet.encodeByte(0); // Unknown (maybe region? always 0 on GMS2), new v100
sendPacket(packet, false);

Similar to MapleStory, you encode a version (except unlike a short major and string minor, it is now an int), a receive sequence, and a send sequence. What's new here is a Block Sequence. This is the int sequence that controls the seed for encryption body packets, and unlike send and receive sequences, this is static. Once initialized to the client, it does not change.

I've made a SocketSequence class that allows me to access and update all three sequences:
PHP:
/*
 *      This file is part of Orion2, a MapleStory2 Emulator Project.
 *      Copyright (C) 2017 Eric Smith <muffinman75013@yahoo.com>
 * 
 *      This program is free software: you can redistribute it and/or modify
 *      it under the terms of the GNU General Public License as published by
 *      the Free Software Foundation, either version 3 of the License, or
 *      (at your option) any later version.
 * 
 *      This program is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU General Public License for more details.
 * 
 *      You should have received a copy of the GNU General Public License
 */
package base.network;

import base.util.Rand32;

/**
 *
 * @author Eric
 */
public class SocketSequence {
    private final int[] sequence;
    
    public SocketSequence() {
        this(0, 0, 0);
    }
    
    public SocketSequence(int recv, int send, int block) {
        this.sequence = new int[3];
        
        this.sequence[0] = recv;
        this.sequence[1] = send;
        this.sequence[2] = block;
    }
    
    public int getRecvSeq() {
        return sequence[0];
    }
    
    public int getSendSeq() {
        return sequence[1];
    }
    
    public int getBlockSeq() {
        return sequence[2];
    }
    
    public void updateRecvSeq() {
        updateSequence(0);
    }
    
    public void updateSendSeq() {
        updateSequence(1);
    }
    
    private void updateSequence(int type) {
        this.sequence[type] = Rand32.crtRand(this.sequence[type]);
    }
}

You'll want to save the initial sequence you've initialized (I store this in my ClientSocket class), and you're going to need to check if the current sequence is the initial sequence. This is because the first packet sent here is going to have an unencrypted body.

When sending a packet, you just need to encrypt the buffer with BufferCryptManager.getInstance().encrypt(buffer, offset, blockSequence, sendSequence). Once it's encrypted, you need to encrypt the sequence:
PHP:
/**
 * Using the raw <code>seqBase</code> and the crypter's <code>seqKey</code>,
 * this method will perform bitwise operation which will result in encoding
 * the final sequence base containing both the version of the game, and the
 * client's current sequence.
 * 
 * This method is designed to encode a sequence that determines if the packet 
 * is both correct, and that the server's version matches the client's version.
 * 
 *     [USER=2000183830]para[/USER]m seqBase The sequence base (or, version) of the game
 *     [USER=2000183830]para[/USER]m seqKey The sequencing key of the client's crypter
 * 
 *     [USER=850422]return[/USER] The encoded sequence base of the packet
 */
public int encodeSeqBase(int seqBase, int seqKey) {
    if (seqKey != 0) {
        // movzx   edi, word ptr [ebp+uSeqBase]
        // movzx   ecx, word ptr [ecx+2]
        // xor     ecx, edi
        seqKey = (seqBase ^ (seqKey >>> 16));
    } else {
        seqKey = seqBase;
    }
    return seqKey;
}

The returned sequence key is encoded as a short. This is the first 2 bytes of your 6-byte header. The next 4 bytes is an int, which is simply the length of the encrypted buffer. After encoding the short (rawSequence) and the int (dataLength), you encode the encrypted bytes. This is all you need to do to send packets to the client.

To decode MapleStory 2 packets is simply just the reverse. You decode a short which is the sequence (unlike MapleStory, sequences aren't encrypted, just raw), and then the int which is the data length. You'll want to validate the packet, which is just simply doing ((recvSequence >> 16) ^ rawSequence) == VERSION. If the version is correct, then read dataLen amount of bytes, and decrypt them using BufferCryptManager.getInstance().decrypt(data, length - offset, blockSequence, recvSequence). The reason we do (length - offset) is because the header is never encrypted, so we deduct that from the overall length of the packet.

Other than that, the rest should be fairly simple - just have to setup your networking and get this working. In the upcoming week or so after finals I'll release Orion2, so everyone can contribute to that project if they do not wish to start their own. In the meantime, let the MapleStory 2 development begin! :)
 
Last edited:
Custom Title Activated
Loyal Member
Joined
Jan 18, 2010
Messages
3,109
Reaction score
1,139
Just a little update! First, I've updated the thread to now handle the crypto's index formula. No more manually updating indexes :)

and second, I wanted to mention that as of v100/v101, Nexon has updated the RequestVersion (OnConnect) packet:
Code:
OutPacket packet = new OutPacket(LoopbackPacket.RequestVersion); // Opcode (always 1)
packet.encodeInt(OrionConfig.VERSION); // Client Version
packet.encodeInt(sequence.getRecvSeq()); // Receive Sequence
packet.encodeInt(sequence.getSendSeq()); // Send Sequence
packet.encodeInt(sequence.getBlockSeq()); // Block (Body) Sequence
[b]packet.encodeByte(0); // Unknown (maybe region? always 0 on GMS2), new v100[/b]
sendPacket(packet, false);

I'm not currently aware of what the byte is at the moment, so if anyone wants to shed some light on it, feel free. Just thought I'd mention that they've updated the handshake packet.
 
Newbie Spellweaver
Joined
Jan 28, 2015
Messages
5
Reaction score
1
I'm not currently aware of what the byte is at the moment, so if anyone wants to shed some light on it, feel free. Just thought I'd mention that they've updated the handshake packet.
Maybe it has something to do with new feature of seamless reconnects.
 
Newbie Spellweaver
Joined
Mar 13, 2016
Messages
28
Reaction score
9
You'll want to validate the packet, which is just simply doing ((recvSequence >> 16) ^ rawSequence) == VERSION.
I was playing around with the mapleshark source and it seems like this check doesn't actually mean anything. Is it actually necessary.

EDIT: Figured it out.

DecodeSeqBase is always using the rawSeq from the original packet (handshake). You actually need to read the new rawSeq from each packet.

Code:
mRawSeq = BitConverter.ToUInt16(mBuffer, 0);
if (DecodeSeqBase(iv) != Build) {
...
This is the check I am referring to:


EDIT 2: Well this works for outbound packets. Inbound is still wrong for whatever reason.


EDIT 3: Figured it out for outbound as well, you have to use the shifted IV in that case.
 
Last edited:
Custom Title Activated
Loyal Member
Joined
Jan 18, 2010
Messages
3,109
Reaction score
1,139

While this check was "half wrong" when I first made it, it is necessary because it ensures that the packet data you're about to decode is actually valid. I only knew the outbound packet check (server), but inbound was failing. That's why when it fails validation sometimes, I just console log it rather than throwing exceptions. I was supposed to fix this check but ended up never really working on MS2 much.

There was a PR made to fix this the other day (probably you), so I'll check it out and merge it later.

Oh, and also.. this is actually the wrong thread. You should've replied this regarding MapleShark2 specifically because this thread is for the server-end crypto which is actually correct.
 
Newbie Spellweaver
Joined
Mar 13, 2016
Messages
28
Reaction score
9
Sorry, I just posted here since I was reading this thread to figure out the problem.

Another question I have that's actually related to this thread is the "offset" parameter in the Encrypt/Decrypt methods. This seems to be packet length and not offset. Is there a reason it's called offset?
 
Back
Top