Алгоритм дешифровки Lineage2Ver41x

Mizuwokiru

Величайший
Проверенный
Сообщения
945
Розыгрыши
0
Решения
1
Репутация
1 038
Реакции
451
Баллы
1 553
В общем сабж: нужны некоторые разъяснения по поводу расшифровки файлов с данным хедером.
Как я вижу это сейчас:
1. Читаем 28 байт. Это заголовок в виде строки из wide char`ов.
2. Размер оставшегося файла округляем до нацело делимого на размер RSA ключа, т.е. на 128 байт.
3. Считываем следующий блок из 128 байт и приводим его к виду Big Integer Number.
4. Возводим блок в степень private exponent и находим остаток от деления на private key.
5. Приводим полученный остаток обратно к виду массива байт.
6. Судя по , часть этого остатка отсекается, а остальное считается за расшифрованный блок. Так вот тут мне чуток не понятно, что да как, из-за этой иерархии наследования и FilterInputStream`а (не совсем пойму, как оно работает).
7. Записываем расшифрованный блок в какой-то поток или еще куда-то.
8. Повторяем 3-6 до тех пор, пока не закончатся блоки.
9. Поток с расшифрованными данными распаковываем ZLib`ой.
10. Вуаля!

Собственно вопрос по 6-ому пункту. Ну и, естественно, принимаются замечания. :Mail11:
 

Вся соль в реализации BigInteger и Zlib в C#. В Java BigInteger - число без знака и имеет порядок байт Big Endian, в отличие от C#, где для беззнаковости нужно в конце добавить нулевой байт, а сам порядок байт для задания BigInteger - Little Endian. Данную проблему решили в .NET Core 2.1, скорее всего войдет в релиз .NET Standard 2.1.
Также в C# отсутствует поддержка Zlib, насколько я понял, поэтому нужно дополнительно выкачивать модуль Zlib (я брал из DotNetZip).
Что в итоге получилось:
C#:
        private const int HeaderSize = 28;
        private const int BlockSize = 128;

        private static int ReadHeader(Stream stream)
        {
            var buffer = new byte[HeaderSize];
            stream.Read(buffer, 0, buffer.Length);
            return int.Parse(Encoding.Unicode.GetString(buffer).Substring(buffer.Length / 2 - 3));
        }

        private static byte[] DecryptBlock(Stream stream)
        {
            var block = new byte[BlockSize];
            stream.Read(block, 0, BlockSize);
            Array.Reverse(block);
            var valueBytes = new byte[129];
            block.CopyTo(valueBytes, 0);
            var modPowBytes = BigInteger.ModPow(new BigInteger(valueBytes), Keys.PrivateExponent413, Keys.Modulus413).ToByteArray();
            Array.Clear(block, 0, block.Length);
            Array.Copy(modPowBytes, block, modPowBytes.Length);
            Array.Reverse(block);
            int size = block[3];
            if (size > 124)
                throw new InvalidOperationException("DecryptBlock");
            int position = 128 - size - ((124 - size) % 4);
            var result = new byte[size];
            Array.Copy(block, position, result, 0, size);
            return result;
        }

        public static MemoryStream DecryptData(string filename)
        {
            MemoryStream uncompressedData;
            using (var compressedData = new MemoryStream())
            {
                using (var stream = new FileStream(filename, FileMode.Open))
                {
                    int cryptoVersion = ReadHeader(stream);
                    switch (cryptoVersion)
                    {
                        case 413:
                            var blockCount = (stream.Length - stream.Position) / BlockSize;
                            var block = DecryptBlock(stream);
                            compressedData.Write(block, 4, block.Length - 4);
                            for (var i = 1; i < blockCount; i++)
                            {
                                block = DecryptBlock(stream);
                                compressedData.Write(block, 0, block.Length);
                            }
                            break;
                        // TODO
                    }
                }
                compressedData.Seek(0, SeekOrigin.Begin);
                using (var uncompressingStream = new ZlibStream(compressedData, CompressionMode.Decompress))
                {
                    uncompressedData = new MemoryStream();
                    var buffer = new byte[0x1000];
                    int r;
                    while ((r = uncompressingStream.Read(buffer, 0, buffer.Length)) != 0)
                    {
                        uncompressedData.Write(buffer, 0, r);
                    }
                }
            }
            uncompressedData.Seek(0, SeekOrigin.Begin);
            return uncompressedData;
        }
    }

По красоте, конечно, обернуть это все дело в классы-потоки, но пока и так сойдет :D
 
А вот как выглядят те самые BIgInteger`ы PrivateExponent413 и Modulus413:
C#:
    public static class Keys
    {
        public static BigInteger Modulus413 = new BigInteger(new byte[] {
            0x83, 0x2D, 0xD7, 0x8F, 0x66, 0xC1, 0x3D, 0x12,
            0x38, 0xDC, 0x89, 0x6A, 0xA8, 0xBA, 0x51, 0x45,
            0x74, 0xC5, 0x9B, 0xD0, 0x11, 0x05, 0xD4, 0x14,
            0x93, 0x74, 0xB6, 0x67, 0x78, 0x8F, 0xE6, 0x50,
            0xE4, 0x3C, 0x08, 0x29, 0xE3, 0x4B, 0x8D, 0xC0,
            0xB7, 0x5E, 0xB4, 0xF2, 0x9E, 0x7D, 0xA6, 0xC9,
            0x02, 0x3A, 0x79, 0x81, 0x38, 0x63, 0x55, 0x86,
            0x60, 0x03, 0x1D, 0xB7, 0xA0, 0xC0, 0x2C, 0xE7,
            0xBD, 0xD7, 0xB5, 0xE5, 0xDD, 0xD1, 0x3E, 0x4D,
            0x27, 0xEA, 0x6E, 0xBE, 0x03, 0x7D, 0x41, 0xB6,
            0x50, 0x88, 0xA1, 0x94, 0x73, 0x6E, 0xE4, 0x75,
            0x01, 0xF9, 0x9E, 0xDA, 0x2D, 0x29, 0x19, 0xFC,
            0x3C, 0x4C, 0x6E, 0x1C, 0xBC, 0x29, 0xE8, 0xD6,
            0xE1, 0x8A, 0x8A, 0xA3, 0x61, 0x16, 0xEF, 0x0F,
            0x2F, 0x17, 0x8D, 0x7E, 0xD1, 0x0C, 0x0A, 0xEF,
            0x37, 0xF7, 0xDD, 0x72, 0x84, 0x39, 0xDF, 0x97,
            0x00
        });

        public static BigInteger PrivateExponent413 = new BigInteger(0x35u);
    }
 
Ты юзаешь встроенный BigInteger. Хотя есть свои реализации.
Это да, но нужда в этих реализациях отпадает после .NET Core 2.1.

Собсна, теперь появилась возможность сделать редактор даток поудобней с помощью WPF или Avalonia UI (если вдруг каким-то образом понадобится кроссплатформенность). К примеру, была идея сделать не открытие определенной датки, а папки system в целом + отображение и выбор текстур в датках напрямую из utx-пакетов, автоопределение ревизии, языка и ключа, а там и изменение ключа в l2.exe. В общем идей море, лишь бы не забить на это все. :D
 
6. Судя по , часть этого остатка отсекается, а остальное считается за расшифрованный блок. Так вот тут мне чуток не понятно, что да как, из-за этой иерархии наследования и FilterInputStream`а (не совсем пойму, как оно работает).
Первые 4 байта расшифрованного массива - это размер данных в текущем блоке. Для всех блоков кроме последнего там записано число 124.
 
Первые 4 байта расшифрованного массива - это размер данных в текущем блоке. Для всех блоков кроме последнего там записано число 124.
Заметил это при отладке на примере ItemName. Чуть позже запущу для всех остальных и если будет так же само, тогда можно смело оптимизировать кусок кода из DecryptBlock.

Кстати, попробовал реализовать это же на С++ (gmp + zlib) - проблем, с какими столкнулся на С#, у меня не возникло.
 
Позновательно, практически аналогичный метод и я собрал годика два назад. Но дальше застрял в тупике - нужен енкодер под оф сервер.
Должно же быть решения за столько лет. Неужели никто не смог...
 
Позновательно, практически аналогичный метод и я собрал годика два назад. Но дальше застрял в тупике - нужен енкодер под оф сервер.
Должно же быть решения за столько лет. Неужели никто не смог...
Нужна ОЧЕНЬ мощная тачка или вообще набор тачек для нахождения множителей. Куда проще заменить ключ на свой.
 
Если я понял правильно очень многое в этом алгоритме упрощено за счет использование C# / Java (acmi).

На вскидку насколько сложно будет в сравнении с C# сделать подобное на C++. Мне нужно использовать это в UE4 как Third Party Library, поэтому я рассчитываю написать DLLку которую я потом использую в UE4.

Основные проблемы я вижу в использовании ByteBuffer'a, (такого в С++ вроде и нет) BigInteger'a (нет нативной поддержки, придется либу искать) и Zlib'ы ( тоже самое что и с BigInteger).
 
Если я понял правильно очень многое в этом алгоритме упрощено за счет использование C# / Java (acmi).

На вскидку насколько сложно будет в сравнении с C# сделать подобное на C++. Мне нужно использовать это в UE4 как Third Party Library, поэтому я рассчитываю написать DLLку которую я потом использую в UE4.

Основные проблемы я вижу в использовании ByteBuffer'a, (такого в С++ вроде и нет) BigInteger'a (нет нативной поддержки, придется либу искать) и Zlib'ы ( тоже самое что и с BigInteger).
gmp и zlib. На самом деле ничего сложного, мб чуть позже примерчик кину.
 
Если я понял правильно очень многое в этом алгоритме упрощено за счет использование C# / Java (acmi).

На вскидку насколько сложно будет в сравнении с C# сделать подобное на C++. Мне нужно использовать это в UE4 как Third Party Library, поэтому я рассчитываю написать DLLку которую я потом использую в UE4.

Основные проблемы я вижу в использовании ByteBuffer'a, (такого в С++ вроде и нет) BigInteger'a (нет нативной поддержки, придется либу искать) и Zlib'ы ( тоже самое что и с BigInteger).
Немного говнокодисто, но мне лень переделывать по красоте.
C++:
#include <fstream>
#include <sstream>
#include <zlib.h>
#include "mini-gmp.h"

const char modulusBytes[] = {
    0x97, 0xdf, 0x39, 0x84, 0x72, 0xdd, 0xf7, 0x37,
    0xef, 0x0a, 0x0c, 0xd1, 0x7e, 0x8d, 0x17, 0x2f,
    0x0f, 0xef, 0x16, 0x61, 0xa3, 0x8a, 0x8a, 0xe1,
    0xd6, 0xe8, 0x29, 0xbc, 0x1c, 0x6e, 0x4c, 0x3c,
    0xfc, 0x19, 0x29, 0x2d, 0xda, 0x9e, 0xf9, 0x01,
    0x75, 0xe4, 0x6e, 0x73, 0x94, 0xa1, 0x88, 0x50,
    0xb6, 0x41, 0x7d, 0x03, 0xbe, 0x6e, 0xea, 0x27,
    0x4d, 0x3e, 0xd1, 0xdd, 0xe5, 0xb5, 0xd7, 0xbd,
    0xe7, 0x2c, 0xc0, 0xa0, 0xb7, 0x1d, 0x03, 0x60,
    0x86, 0x55, 0x63, 0x38, 0x81, 0x79, 0x3a, 0x02,
    0xc9, 0xa6, 0x7d, 0x9e, 0xf2, 0xb4, 0x5e, 0xb7,
    0xc0, 0x8d, 0x4b, 0xe3, 0x29, 0x08, 0x3c, 0xe4,
    0x50, 0xe6, 0x8f, 0x78, 0x67, 0xb6, 0x74, 0x93,
    0x14, 0xd4, 0x05, 0x11, 0xd0, 0x9b, 0xc5, 0x74,
    0x45, 0x51, 0xba, 0xa8, 0x6a, 0x89, 0xdc, 0x38,
    0x12, 0x3d, 0xc1, 0x66, 0x8f, 0xd7, 0x2d, 0x83
};
constexpr unsigned long privateExponent = 0x35;

constexpr int ReverseBytes(int value)
{
    char* bytes = (char*)&value;
    return (int)bytes[3] | ((int)bytes[2] << 8) | ((int)bytes[1] << 16) | ((int)bytes[0] << 24);
}

int main()
{
    mpz_t modulus, readBlock, modPowedBlock;
    mpz_init(modulus);
    mpz_import(modulus, 128, 1, 1, 0, 0, modulusBytes);
    mpz_init(readBlock);
    mpz_init(modPowedBlock);

    std::ifstream input("Q:\\Lineage2_NA_175_152\\system\\ItemName-e.dat", std::ios::binary);
    char header[28] { 0 };
    input.read(header, 28); // читаем заголовок, тот самый Lineage2Ver413

    std::stringstream decryptedData;
    char encBuffer[128] { 0 };
    input.read(encBuffer, 128);
    mpz_import(readBlock, 128, 1, 1, 0, 0, encBuffer);
    mpz_powm_ui(modPowedBlock, readBlock, privateExponent, modulus);
    size_t count = 32;
    int decBuffer[32] { 0 };
    mpz_export(decBuffer, &count, 1, 4, 1, 0, modPowedBlock);
    int decryptedSize = ReverseBytes(decBuffer[0]);
    int startPosition = 128 - decryptedSize - ((124 - decryptedSize) % 4);
    int unpackedSize = ((unsigned int *)&((char *) decBuffer)[startPosition])[0];
    startPosition += 4;
    decryptedSize -= 4;
    decryptedData.write(&((const char *) decBuffer)[startPosition], decryptedSize);
    while (!input.eof())
    {
        input.read(encBuffer, 128);
        mpz_import(readBlock, 128, 1, 1, 0, 0, encBuffer);
        mpz_powm_ui(modPowedBlock, readBlock, privateExponent, modulus);
        count = 32;
        memset(decBuffer, 0, 128); // TODO: нужно ли?
        mpz_export(decBuffer, &count, 1, 4, 1, 0, modPowedBlock);
        decryptedSize = ReverseBytes(decBuffer[0]);
        startPosition = 128 - decryptedSize - ((124 - decryptedSize) % 4);
        decryptedData.write(&((const char *) decBuffer)[startPosition], decryptedSize);
    }
    input.close();

    char* unpackedData = new char[unpackedSize];
    z_stream unpackStream;
    unpackStream.zalloc = Z_NULL;
    unpackStream.zfree = Z_NULL;
    unpackStream.opaque = Z_NULL;
    unpackStream.avail_in = (uInt) decryptedData.str().size();
    unpackStream.next_in = (Bytef *) decryptedData.str().c_str();
    unpackStream.avail_out = unpackedSize;
    unpackStream.next_out = (Bytef *) unpackedData;

    inflateInit(&unpackStream);
    inflate(&unpackStream, Z_NO_FLUSH);
    inflateEnd(&unpackStream);

    std::ofstream output("Q:\\Lineage2_NA_175_152\\system\\ItemName-e-dec.dat", std::ios::binary);
    output.write(unpackedData, unpackedSize);
    mpz_clear(modulus);
    mpz_clear(readBlock);
    mpz_clear(modPowedBlock);
    delete[] unpackedData;
    return 0;
}

Использованные материалы:


 
У вас обширные знания, спасибо за предоставленный пример. Буду изучать.

Чисто для "будущих поколений".

Если у вас проект настроен чтобы интерпретировать warning'и как ошибки, то при написании

Код:
const char modulusBytes[] = { // any hex int representation }

Возникнет ошибка при компиляции

Код:
conversion from 'int' to 'const char' requires a narrowing conversion

что на самом деле имеет смысл. Ведь как такого типа byte в c++ нет, его непосредственной заменой является unsigned char, поэтому инициализировать массив char'ов интами не получиться)

поэтому просто добавив unsigned char все заработает)

Код:
const unsigned char modulusBytes[] = { // any }


Еще раз спасибо большое за ваш пример

Уххх вот это дичь))


В общем да, я тоже смог повторить это на с++ но, интересная вещь, вероятно у вас в коде ошибка (но это не точно возможно связанно с shared_pointers).

Ошибка вот в чём,

Код:
...
unpackStream.avail_in = (uInt) decryptedData.str().size();
unpackStream.next_in = (Bytef *) decryptedData.str().c_str();
...

Ну, суть очень интересная, я, доверяя этому коду даже не думал первое время взглянуть на то что возвращает метод .c_str() и получал различные еррор коды при инфелйте, (копал в других местах)

В общем он будет возвращать лишь 1 символ строки ( ибо это указатель до 1 нулл терминейтора)

Погуглив посмотрел это.


Код:
// Костыляга
const std::string& tmp = decrypted->str();
const char* cstr = tmp.c_str();


std::vector<char> decompressedData(decompressedSize);
z_stream unpackStream;
unpackStream.zalloc = Z_NULL;
unpackStream.zfree = Z_NULL;
unpackStream.opaque = Z_NULL;
unpackStream.avail_in = (uInt)decrypted->str().size();
unpackStream.next_in = (Bytef*)cstr;


Это так, на будущее.

В общем он будет возвращать лишь 1 символ строки ( ибо это указатель до 1 нулл терминейтора)

он будет указывать на инвалидный участок памяти, т.к объект созданный .str() временный.

П.С
Как вообще редактировать тут сообщения?
 
Последнее редактирование модератором:

    Mizuwokiru

    Баллов: 21
    За поправление :)
Вся соль в реализации BigInteger и Zlib в C#. В Java BigInteger - число без знака и имеет порядок байт Big Endian, в отличие от C#, где для беззнаковости нужно в конце добавить нулевой байт, а сам порядок байт для задания BigInteger - Little Endian. Данную проблему решили в .NET Core 2.1, скорее всего войдет в релиз .NET Standard 2.1.
Также в C# отсутствует поддержка Zlib, насколько я понял, поэтому нужно дополнительно выкачивать модуль Zlib (я брал из DotNetZip).
Привет, а можно тебя попросить выложить весь проект на C#? с Zlib и т.п буду очень благодарен!
 
Назад
Сверху Снизу