- 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
Crypter
TableCrypter
RearrangeCrypter
XORCrypter
Rand32
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:
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:
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:
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!
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: