morsisko / NosCrypto

A reverse engineered packet cryptography - encryption and decryption routines to emulate NosTale client or server in Python.
MIT License
9 stars 1 forks source link

Newline issue #1

Closed ApourtArtt closed 4 years ago

ApourtArtt commented 4 years ago

Hello, I have been trying to use your crypto you just released when I realized something was going wrong. I changed it to C++ so I can use it in a little project to try it out.

I found an issue when client is sending multiple packets : '\n' is causing, later on, a break on line 79 (https://github.com/morsisko/NosCrypto/blob/master/noscrypto/Utils.py#L79)

from noscrypto import Server
from noscrypto import Client

def main():
    stringt = b'\x0d\x67\x1c\x6a\x90\x11\x17\x21\x16\x1a\x25\x19\x21\x10\x21\x17\x0a\x9b\x89\x43\x42\x09\x9d\x04\x0d\x67\x1c\x7a\x97\x10\x1c\x1d\x17\x1d\x17\x23\x22\x19\x1b\x20\x21\x04'
    s = Server.WorldDecrypt(stringt, 5000, False).decode("ascii")
    print(s)

if __name__ == "__main__":
    main()

This sample is supposed to print 18532 usernametes GF 2 thisisgfmode You can verify it by breaking on line 79 after the second entry (instead of the first one)

but instead, we got 18532 usernametes GF 2

I tried for a long time to fix it, but no way, new line is causing a \x04 byte in packet string and then becomes a \xFF and break when line 79. But removing this break (or breaking after the second entry in the condition) is causing an imperfect packet.

A turnaround to this problem would be to split the buffer by \x04, but I guess that is not a clean idea ?

Btw1 : how are you supposed to get the sessionId if you already need it to decrypt it ? Btw2 : there is no issue on your crypto for the login encryption/decryption part. Did not tried client one yet

morsisko commented 4 years ago

As I understand - You try to make your own server emulator? (Because you call Server.WorldDecrypt and not Client.WorldDecrypt).

The problem is - each of function in this library outputs only single packet - this is done to avoid situations when you call the decryption on part of data, because in TCP stream there is no guarantee you receive whole packet in single recv() call. Imagine situation when server send encrypted packet "walk 12 20" and you received it like "walk 1" "2 20". When you call the decryption on the second part of the packet it won't work, because you can't call the decryption function in the middle of data. So you ends with two decrypted packets: incomplete walk 1 and bunch of garbage bytes.

Each packet send by world client/server ends with 0xFF before xoring. While it's easy to procedee as client, it is a bit harder if you try to make server, because firstly you need to properly xor the received byte. You can't just split by \x04 because the delimiter will work only for first packet, and will be different for each session id.

For the current state of this library I could recommend something like this (this isn't the optimal solution, I'm just adding it because of it's simplicity)

packet = b""
isFirstPacket = True
session = 1337
while True:
    recvByte = s.recv(1) #socket recv

    if len(recvByte) == 0:
        break

    packet += recvByte

    if Server._WorldXor(recvByte, session, isFirstPacket)[0] == 0xFF:
        completedPacket = Server.WorldDecrypt(packet, session, isFirstPacket).decode("ascii")
        print(completedPacket)
        isFirstPacket = False
        packet = b""

When you make client emulator, you don't need to call the _WorldXor, because the 0xFF byte is plain in packets you receive from server, you don't need to xor anything then.

Btw1 : how are you supposed to get the sessionId if you already need it to decrypt it ? You don't need it. You can call the first decrypt with whatever session you like, but you need to set the last parameter (is_first_packet) to True, because well, it is first packet that you process. So for example: Server.WorldDecrypt(packet, -1, True).decode("ascii") to decode the session.

BTW. I added the _WorldXor function right now, so you need to update the library like pip install noscrypto --upgrade. I might implement something like DecryptMultiplePackets, but for reasons listed above, it wouldn't be the best idea.

ApourtArtt commented 4 years ago

Yep, I am using the server code on purpose. Cryless' crypto works fine for me (even with special character, at least for french's one) even though his code is ugly.

I have made some modification on the decryption part and for now I am really satisfied. Maybe it could be usefull for you (do you even want a pull request, noting that my code is in C++ ?)

So, first, how do I use the decrypt function now :

std::string packet = "\x0d\x67\x1c\x6a\x90\x11\x17\x21\x16\x1a\x25\x19\x21\x10\x21\x17\x0a\x9b\x89\x43\x42\x09\x9d\x04\x0d\x67\x1c\x7a\x97\x10\x1c\x1d\x17\x1d\x17\x23\x22\x19\x1b\x20\x21\x04";
std::string tmp;
while (!packet.empty() && !(tmp = Cryptography::WorldDecrypt(&packet, 5000, false)).empty())
    std::cout << "[" << tmp << "]" << std::endl;
std::cout << "Packet : [" << packet << "]" << " size : " << packet.size();

Output :

[18532 usernametes GF 2] [18533 thisisgfmode] Packet : [] size : 0

Output after removing the last byte in packet (\x04) :

[18532 usernametes GF 2] gzù#" size : 17 (well, in readable bytes : \x0d\x67\x1c\x7a\x97\x10\x1c\x1d\x17\x1d\x17\x23\x22\x19\x1b\x20\x21)

std::string Cryptography::WorldDecrypt(std::string *packet, int session, bool isFirstPacket)
{
    if(!packet)
        return "";
    std::string output;
    int stype = (-1) * (isFirstPacket) + (stype = (session >> 6) & 3) * (!isFirstPacket);
    unsigned char key = session & 0xFF;
    unsigned char c = 0;
    bool found = false;
    for(auto&& i : *packet)
    {
        if (stype == 0)
            c = ((i - key - 0x40) & 0xFF);
        else if (stype == 1)
            c = ((i + key + 0x40) & 0xFF);
        else if (stype == 2)
            c = (((i - key - 0x40) ^ 0xC3) & 0xFF);
        else if (stype == 3)
            c = (((i + key + 0x40) ^ 0xC3) & 0xFF);
        else
            c = ((i - 0xF) & 0xFF);
        output.push_back(c);
        if (c == 0xFF)
        {
            found = true;
            break;
        }
    }
    if (found)
    {
        packet->erase(0, output.size());
        return unpack(output, DECRYPTION_TABLE);
    }
    return "";
}

std::string Cryptography::unpack(std::string packet, const char* charsToUnpack)
{
    std::string output;

    for (size_t pos = 0; packet.size() > pos && (unsigned char)packet[pos] != 0xFF;)
    {
        unsigned char currentChunkLength = (unsigned char)packet[pos] & 0x7F;
        bool isPacked = (unsigned char)packet[pos] & 0x80;
        pos++;
        if (isPacked)
        {
            for (size_t i = 0; i < ceil((double)currentChunkLength / 2) && pos < packet.size(); i++)
            {
                unsigned char twoChars = packet[pos];
                pos++;
                unsigned char leftChar = twoChars >> 4;
                output.push_back(charsToUnpack[leftChar]);
                unsigned char rightChar = twoChars & 0xF;
                if (rightChar == 0)
                    break;
                output.push_back(charsToUnpack[rightChar]);
            }
        }
        else
        {
            for (size_t i = 0; i < currentChunkLength && pos < packet.size(); i++, pos++)
                output.push_back(((unsigned char)packet[pos] ^ 0xFF));
        }
    }
    return output;
}

Edit : about the encryption key, you're right, I already tried and didn't succeed, just retried and succeed this time... Guess I did something wrong ! Thank you 👍

morsisko commented 4 years ago

I think it's about your modifications in that code you posted. This:

from noscrypto import Server

packet = b""
isFirstPacket = False
session = 5000
string = b"\x0d\x67\x1c\x6a\x90\x11\x17\x21\x16\x1a\x25\x19\x21\x10\x21\x17\x0a\x9b\x89\x43\x42\x09\x9d\x04\x0d\x67\x1c\x7a\x97\x10\x1c\x1d\x17\x1d\x17\x23\x22\x19\x1b\x20\x21\x04"
for recvByte in string:

    packet += bytes([recvByte])

    if Server._WorldXor(bytes([recvByte]), session, isFirstPacket)[0] == 0xFF:
        completedPacket = Server.WorldDecrypt(packet, session, isFirstPacket).decode("ascii")
        print(completedPacket)
        isFirstPacket = False
        packet = b""

Works fine for me, and decrypts exactly

18532 usernametes GF 2
18533 thisisgfmode

Any pull requests are open, but as this is python library I would rather keep it all in python. I used cryless crypto for testing and comparsion with this library, the problem is you can encrypt packet in two ways and server and this library will read them anyway (not packing the bytes, just xoring them by 0xFF). With his crypto the something called "mask generation" is not matching client one 1:1, thus it won't produce 1:1 output as client.

ApourtArtt commented 4 years ago

I guess there is a misunderstanding. If I use your string string, I will get the same output, but if I remove the final \x04, then I will have the second output, meaning that "finished" packet are printed while "unfinished" are saved in the same variable.

Finally, everything works fine now, thank you !