Originally Posted by BladeTiger12
Hello NosTale Community,
some days ago I reversed the new "cryption" of Vendetta.
Actually it is just a simple xor with a table.
How it works:
When Client sends a packet:
- Crypt the packet as always (standard online encryption login & game)
- Xor the encrypted packet
When Client receives a packet:
- Xor the received encrypted packet
- Uncrypt the packet as always (standard online encryption login & game)
They use two different tables for sent and received packets.
In the attachment is the xorTable file. (Every table has a size of 0x10000.)
I wrote two classes for C++ & C#:
You need to call the initialize function (Best in the main/startup function of your application):
Code:
CNTXor::Initialize()
NTXor.h (Header File)
Code:
#pragma once
#define TABLE_SIZE 0x10000
class CNTXor
{
public:
CNTXor(bool i_fIsEncrypt);
virtual ~CNTXor() = default;
public:
static void Initialize();
public:
void crypt(char* i_pBuffer, size_t i_nLength);
void reset() {
this->m_nTableIndex = 0;
}
private:
bool m_fIsEncrypt;
size_t m_nTableIndex;
static bool s_fIsInitialized;
static char s_aEncryptTable[];
static char s_aDecryptTable[];
};
NTXor.cpp
Code:
#include "NTXor.h"
#include <fstream>
#include <stdexcept>
bool CNTXor::s_fIsInitialized = false;
char CNTXor::s_aEncryptTable[TABLE_SIZE];
char CNTXor::s_aDecryptTable[TABLE_SIZE];
CNTXor::CNTXor(bool i_fIsEncrypt)
: m_fIsEncrypt(i_fIsEncrypt), m_nTableIndex(0)
{
}
void CNTXor::Initialize()
{
if (s_fIsInitialized) {
return;
}
std::ifstream oXorTable("xorTable", std::ios::binary);
if (!oXorTable.is_open()) {
throw std::runtime_error("Couldn't find file 'xorTable'. Make sure to place it in the same directory as the binaries.");
}
oXorTable.read(s_aEncryptTable, TABLE_SIZE);
oXorTable.read(s_aDecryptTable, TABLE_SIZE);
oXorTable.close();
s_fIsInitialized = true;
}
void CNTXor::crypt(char* i_pBuffer, size_t i_nLength)
{
if (!s_fIsInitialized) {
throw std::runtime_error("XorTable is not initialized!");
}
for (size_t i = 0; i < i_nLength; i++) {
i_pBuffer[i] ^= (this->m_fIsEncrypt ? s_aEncryptTable[this->m_nTableIndex % TABLE_SIZE] : s_aDecryptTable[this->m_nTableIndex % TABLE_SIZE]);
this->m_nTableIndex++;
}
}
NTXor.cs
Code:
public class NTXor
{
private static readonly byte[] TableEncrypt = null;
private static readonly byte[] TableDecrypt = null;
private int _tableIndex;
private readonly bool _isEncrypt;
static NTXor()
{
using (var binaryReader = new BinaryReader(File.Open("xorTable", FileMode.Open)))
{
var length = (int)binaryReader.BaseStream.Length;
TableEncrypt = binaryReader.ReadBytes(length / 2);
TableDecrypt = binaryReader.ReadBytes(length / 2);
}
}
public NTXor(bool isEncrypt)
{
this._isEncrypt = isEncrypt;
}
public void Crypt(ref byte[] packet, int size = -1)
{
if (size == -1)
{
size = packet.Length;
}
for (var i = 0; i < size; i++)
{
if (_isEncrypt)
{
packet[i] ^= TableEncrypt[_tableIndex % TableEncrypt.Length];
}
else
{
packet[i] ^= TableDecrypt[_tableIndex % TableEncrypt.Length];
}
_tableIndex++;
}
}
public void Reset()
{
_tableIndex = 0;
}
}
Important: You have to create the class two times. The constructor accepts a boolean value. So true/false.
true = Encrypt
false = Decrypt
And you have to call reset when you connect to a new server.
C++ Test if your setup is correct (You should see at the end "Same! Crypt was successful")
Code:
#include <iostream>
#include <string>
#include <iomanip>
#include "NTXor.h"
int main()
{
char szPacket[] = { 0x9C, 0xBB, 0x9F, 0x02, 0x05, 0x03, 0x05, 0xF2, 0x0A, 0x00, 0x01, 0x09, 0x02, 0x05, 0x02, 0xF2, 0xC6, 0xB5, 0xBF, 0xC6, 0xF2, 0xFF, 0x03, 0x05, 0x03, 0xFF, 0x06, 0x06, 0x04, 0xFF, 0x05, 0x01, 0x03, 0x05, 0xFF, 0xFF, 0x03, 0x06, 0x06, 0xF2, 0x02, 0x02, 0x02, 0x06, 0x00, 0x05, 0x95, 0xFF, 0xD7, 0x01, 0xFC, 0x02, 0xFC, 0x02, 0xFC, 0xFF, 0x00, 0xF2, 0x02, 0xF2, 0xFF, 0x8F, 0x8F, 0xFF, 0x00, 0x05, 0xFF, 0x04, 0x94, 0x96, 0x01, 0x05, 0x06, 0x04, 0x8F, 0x96, 0x03, 0x95, 0x02, 0x01, 0x09, 0x09, 0x96, 0x91, 0x95, 0x01, 0x09, 0xFF, 0x05, 0x05, 0x04, 0xFF, 0xD8 };
const char szCrypted[] = { 0x01, 0xCA, 0xAD, 0x68, 0x12, 0xE5, 0x41, 0x55, 0x6B, 0xDC, 0xEF, 0x54, 0x0D, 0x6C, 0x35, 0x95, 0x14, 0x9A, 0x41, 0xAD, 0x3C, 0xDF, 0x48, 0x80, 0xF3, 0x51, 0x2D, 0x8F, 0xFB, 0x86, 0xCB, 0xA4, 0x57, 0x0D, 0x30, 0x04, 0xA1, 0x0A, 0x18, 0x15, 0xDE, 0x87, 0xF5, 0x5B, 0xAC, 0x23, 0xC8, 0x6D, 0xCE, 0x48, 0x57, 0x61, 0x7F, 0xC6, 0x30, 0x18, 0x8F, 0x84, 0xBD, 0xD1, 0x36, 0xEB, 0x47, 0x28, 0x03, 0x50, 0x61, 0x9F, 0x9A, 0x31, 0xBB, 0xD0, 0x68, 0x5F, 0x80, 0x60, 0x61, 0x04, 0x03, 0x8F, 0x58, 0x36, 0x6A, 0x2E, 0x7F, 0xBF, 0x0B, 0xCD, 0x77, 0x1B, 0xDB, 0x3A, 0x0F };
CNTXor::Initialize();
CNTXor ntXor(true);
ntXor.crypt(szPacket, sizeof(szPacket));
bool fIsSame = true;
for (int i = 0; i < sizeof(szPacket); i++) {
if (szPacket[i] != szCrypted[i]) {
std::cout << std::dec << std::setw(2) << std::setfill(' ') << i << " is different! " << std::hex << std::setw(2) << std::setfill('0') << (short(szPacket[i]) & 0xFF) << ":" << std::setw(2) << std::setfill('0') << (short(szCrypted[i]) & 0xFF) << std::endl;
fIsSame = false;
}
}
std::cout << (fIsSame ? "Same! Crypt was successful" : "Not same! Something went wrong.");
std::cin.get();
}
Code:
global NTXor encrypt(true);
global NTXor decrypt(false);
startup() {
CNTXor::Initialize(); // This line you need only in C++, C# does this automatically.
string packet = "packet to server";
// When LoginServer
Cryptography::EncryptLoginPacket(packet);
// When GameServer
Cryptography::EncryptGamePacket(packet);
// Vendetta only
encrypt.Crypt(packet, packet.length);
}
// When you connected to a new endpoint
onConnected() {
encrypt.Reset();
decrypt.Reset();
}
// When new incoming packet
packetReceived(buffer, size) {
// Vendetta only
decrypt.Crypt(buffer, size);
// When LoginServer
Cryptography::DecryptLoginPacket(buffer, size);
// When GameServer
Cryptography::DecryptGamePacket(buffer, size);
}
That's actually it.
I Hope it's useful for someone.
Average reverse time: 3h. Was a lot of confusing bullshit asm code.
#Edit: Fixed C++ Class. (used "i" instead of "m_nTableIndex")
|