Pull to refresh

Исследование защиты игры Charm Solitaire

Reading time 13 min
Views 13K
Charm Solitaire
Лет 7-8 назад мне случайно попалась игра CharmSolitaire, скопированная вместе с другими играми с чужого винта в процессе обмена информацией. Это такой не совсем обычный карточный пасьянс. В незарегистрированной версии на игру отводится один час, и открыта только половина уровней. В той копии время уже почти закончилось. Денег на покупку у меня не было, поэтому скорее всего я бы ее удалил. Но в то время я немного увлекался взломом и решил попробовать найти регистрационный код. Опыт был довольно интересным. В статье рассказывается об основных особенностях защиты, а также о том, как security through obscurity может ее ослабить.



Ключа в открытом виде в статье нет, но кому интересно, сможет найти его самостоятельно.

Все действия вы выполняете на свой страх и риск.


Исследование защиты


Загружаем файл в IDA, запускаем игру и нажимаем кнопку «Ключ». Вводим что-нибудь, например 123321, и ищем через ArtMoney.

image

Ставим hardware breakpoint на этот адрес. Переключаемся на окно игры, пропускаем срабатывания брейкпойнта, пока окно игры не станет активным. Жмем OK, брейкпойнт срабатывает.

Жмем Ctrl + F7 (Run until return), пока не попадем из системных dll в пространство процесса. Это будет процедура обработки сообщений Controls::TWinControl::DefaultHandler(). Продолжаем выходить из функций, пока не попадем на вызов Controls::TControl::GetText(void):
Скрытый текст
004C2700:
                call    @Controls@TControl@GetText$qqrv ; Controls::TControl::GetText(void)
EIP->           mov     edx, [ebp+var_8]
                mov     eax, [ebp+var_4]
                call    sub_4C23A8
                test    al, al
                jnz     short loc_4C277C
                
                ...
                mov     edx, offset _str_WKeyError.Text
                call    sub_4AF2F8
                ...
                jmp     short loc_4C27D2

loc_4C277C:
                ...
                mov     edx, offset _str_WRegistrationTh.Text
                call    sub_4AF2F8


В переменной [ebp+var_8] находится указатель на строку с введенным кодом. Далее видим вызов функции sub_4C23A8() и условный переход. По строкам _str_WKeyError и _str_WRegistrationThanks можно догадаться, что sub_4C23A8() и есть функция проверки ключа.
Скрытый текст
check_key_4C23A8 proc near

                ...
                mov     [ebp+key_8], edx
                mov     [ebp+this_4], eax
                ...
                mov     [ebp+is_right_key_9], 0
                cmp     [ebp+key_8], 0
                jz      loc_4C257B
                
                lea     edx, [ebp+key_copy_28]
                mov     eax, [ebp+key_8]
                call    copy_digits_492734
                
                mov     edx, [ebp+key_copy_28]
                mov     eax, ds:pp_key_4F305C
                call    @System@@LStrAsg$qqrv ; System::__linkproc__ LStrAsg(void)
                ...
                
                mov     eax, ds:pp_dirname_4F2F54
                push    dword ptr [eax]
                push    offset _str_slash.Text
                push    offset _str_CharmSolitaire.Text
                push    offset _str__udf.Text
                lea     eax, [ebp+udf_filename_2C]
                mov     edx, 4
                call    str_cat_40522C
                
                mov     edx, [ebp+udf_filename_2C]          ; %GAME_DIR%\CharmSolitaire.udf
                mov     eax, [ebp+mem_stream_encrypted_10]
                call    @Classes@TMemoryStream@LoadFromFile$qqrx17System@AnsiString_0 ; Classes::TMemoryStream::LoadFromFile(System::AnsiString)
                
                mov     eax, ds:pp_key_4F305C
                cmp     dword ptr [eax], 0
                jz      short loc_4C2496
                
                mov     eax, ds:pp_key_4F305C
                mov     eax, [eax]
                call    strlen_40516C
                
004C2473:       cmp     eax, 18h
                jnz     short loc_4C2496
                
004C2478:       ...


loc_4C25A5:
                mov     al, [ebp+is_right_key_9]
                ...
                retn
check_key_4C23A8 endp


Из инструкции по адресу 004C2473 видно, что длина строки должна быть 18h, то есть 24 символа. ОК, ставим брейкпойнт, запускаем, вводим ключ 123456789012345678901234.
Скрытый текст
004C2478:       lea     edx, [ebp+var_30]
                mov     eax, ds:pp_key_4F305C
                mov     eax, [eax]
                call    sub_4924D0
                
                mov     edx, [ebp+var_30]
                mov     eax, ds:off_4F2C24
                call    @System@@LStrAsg$qqrv ; System::__linkproc__ LStrAsg(void)
                jmp     short loc_4C24A5


Процедура sub_4924D0 производит некоторые манипуляции со строкой и переводит результат в int64
Скрытый текст
key_to_hex_4924D0 proc near
                ...
                mov     [ebp+p_res_10], edx
                mov     [ebp+key_C], eax
                ...
                lea     edx, [ebp+key_copy_24]
                mov     eax, [ebp+key_C]
                call    copy_digits_492734
                
                mov     eax, [ebp+key_copy_24]
                lea     edx, [ebp+mixed_str_14]
                call    mix_symbols_492688
                
                lea     eax, [ebp+mixed_str_14]
                mov     ecx, 3
                mov     edx, 1
                call    delete_symbols_40540C
                jmp     short loc_492539

loc_492527:
                lea     eax, [ebp+mixed_str_14]
                mov     ecx, 1
                mov     edx, 1
                call    delete_symbols_40540C
loc_492539:
                mov     eax, [ebp+mixed_str_14]
                cmp     byte ptr [eax], 30h ; '0'
                jz      short loc_492527            ; delete leading zeros
                
                push    0       ; default value
                push    0       ; default value
                mov     eax, [ebp+mixed_str_14]
                call    @Sysutils@StrToInt64Def$qqrx17System@AnsiStringj ; Sysutils::StrToInt64Def(System::AnsiString,__int64)
                mov     [ebp+v64lo_8], eax
                mov     [ebp+v64hi_4], edx
                
                mov     eax, [ebp+p_res_10]
                call    @System@@LStrClr$qqrr17System@AnsiString ; System::__linkproc__ LStrClr(System::AnsiString &)
                mov     [ebp+i_18], 1

loc_492562:
                mov     eax, [ebp+i_18]
                test    byte ptr [ebp+eax-1+v64lo_8], 7Fh
                jbe     short loc_49258C
                
                lea     eax, [ebp+char_str_28]
                mov     edx, [ebp+i_18]
                mov     dl, byte ptr [ebp+edx-1+v64lo_8]
                and     dl, 7Fh
                call    str_from_pchar_405084 ; Borland Visual Component Library & Packages
                
                mov     edx, [ebp+char_str_28]
                mov     eax, [ebp+p_res_10]
                call    @System@@LStrCat$qqrv ; System::__linkproc__ LStrCat(void)
                mov     eax, [ebp+p_res_10]

loc_49258C:
                inc     [ebp+i_18]
                cmp     [ebp+i_18], 9
                jnz     short loc_492562

loc_4925A2:
                ...
                retn
key_to_hex_4924D0 endp


Пседокод:
mixed_str_14 = mix_symbols_492688(key);
v64_4 = StrToInt64Def(mixed_str_14, 0);
while (v64_4[i] & 0x7F > 0) (string)p_res_10 += (char)v64_4[i] & 0x7F, i++;

Функция mix_symbols_492688 работает так — значения из первой половины строки становятся на нечетные места (если считать от 0), из второй на четные. Наша строка превращается в 314253647586970819203142.

В функции StrToInt64Def вызывается другая системная функция ValInt64. У нее есть одна особенность — если после обработки в конце строки еще остались символы, то в выходную переменную (code) возвращается текущая позиция, иначе 0. Обработка заканчивается, если текущее значение превышает 0x0CCCCCCCCCCCCCCC (потому что 0x0CCCCCCCCCCCCCCC = 0x7FFFFFFFFFFFFFFF / 0x0A; 0x0A — основание системы счисления). В StrToInt64Def есть проверка на это, и если в code вернулось не 0, то вместо результата возвращается значение по умолчанию (в данном случае 0).

314253647586970819203142 явно превышает это значение. Возьмем в качестве кода к примеру то же 0x0CCCCCCCCCCCCCCC. Переведем в десятичную систему и совершим действия, обратные действиям функции mix_symbols_492688 — нечетные символы запишем в первую половину, четные во вторую:
0x0CCCCCCCCCCCCCCC = 922337203685477580

000000922337203685477580
 0 0 0 2 3 7 0 6 5 7 5 0
0 0 0 9 2 3 2 3 8 4 7 8 
000237065750000923238478


Запускаем снова, вводим новый код, возвращаемся к функции check_key_4C23A8.
Скрытый текст
004C2478:       lea     edx, [ebp+p_key64_30]
                mov     eax, ds:pp_key_4F305C
                mov     eax, [eax]
                call    key_to_hex_4924D0
                
                mov     edx, [ebp+p_key64_30]
                mov     eax, ds:p_key_bytes_4F2C24
                call    @System@@LStrAsg$qqrv ; System::__linkproc__ LStrAsg(void)
                jmp     short loc_4C24A5
                ...
loc_4C24A5:
                mov     ecx, ds:p_key_bytes_4F2C24
                mov     ecx, [ecx]
                mov     edx, [ebp+mem_stream_decrypted_14]
                mov     eax, [ebp+mem_stream_encrypted_10]
                call    sub_492B48
                
                mov     eax, [ebp+mem_stream_decrypted_14]
                call    sub_492C94
                
                test    al, al
                jz      loc_4C255F
                ...
loc_4C255F:
                mov     [ebp+is_right_key_9], 0
                ...
loc_4C25A5:
                mov     al, [ebp+is_right_key_9]
                ...
                ret
check_key_4C23A8 endp


Сначала взглянем на функцию sub_492C94. Константы 20h, 09h, 0Dh, 0Ah, в инструкциях сравнения говорят о том, что здесь есть какая-то работа с текстом. Более подробное изучение показывает, что эта функция проверяет, является ли содержимое mem_stream_decrypted_14 текстом. Назовем ее is_text_492C94.

Основная работа происходит в функции sub_492B48. Там с помощью ключа расшифровываются данные из mem_stream_encrypted_10, в который до этого было загружено содержимое файла CharmSolitaire.udf. Можно посмотреть на него в HEX-редакторе. В начале есть блок, где каждый 8 байт равен 0x33. Интересно, но не очень понятно, как это использовать. Идем дальше.

Скрытый текст
decrypt_492B48  proc near
                ...
                mov     [ebp+key_bytes_28], ecx
                mov     [ebp+mem_stream_decrypted_24], edx
                mov     [ebp+mem_stream_encrypted_20], eax
                ...
                mov     eax, [ebp+key_bytes_28]
                call    strlen_40516C
                test    eax, eax
                jnz     short loc_492B87
                ...
loc_492B87:
                ...     ; key_bytes_28 может быть меньше 8 символов
                ...     ; key_bytes8_34 заполняется до 8 символов повторением key_bytes_28
                ...     ; в принципе можно считать, что они равны

loc_492BD4:
                lea     edx, [ebp+buf_14]
                mov     ecx, 8
                mov     eax, [ebp+mem_stream_encrypted_20]
                mov     ebx, [eax]
                call    dword ptr [ebx+0Ch]         ; read bytes
                mov     [ebp+bytes_read_C], eax
                
                xor     eax, eax
                mov     [ebp+i_2C], eax

loc_492BEC:
                mov     eax, [ebp+i_2C]
                mov     al, [ebp+eax+key_bytes8_34]
                mov     edx, [ebp+i_2C]
                xor     byte ptr [ebp+edx+buf_14], al
                inc     [ebp+i_2C]
                cmp     [ebp+i_2C], 8
                jnz     short loc_492BEC
                
                cmp     [ebp+bytes_read_C], 8
                jnz     short loc_492C3C
                
                push    ebp
                call    sub_492A6C
                pop     ecx
                xor     eax, eax
                mov     [ebp+i_2C], eax
loc_492C15:
                mov     eax, [ebp+i_2C]
                mov     al, [ebp+eax+key_bytes8_34]
                mov     edx, [ebp+i_2C]
                xor     byte ptr [ebp+edx+decrypted_buf_1C], al
                inc     [ebp+i_2C]
                cmp     [ebp+i_2C], 8
                jnz     short loc_492C15
                
                lea     edx, [ebp+decrypted_buf_1C]
                mov     ecx, [ebp+bytes_read_C]
                mov     eax, [ebp+mem_stream_decrypted_24]
                mov     ebx, [eax]
                call    dword ptr [ebx+10h]     ; write bytes
                jmp     short loc_492C4A

loc_492C3C:
                lea     edx, [ebp+buf_14]
                mov     ecx, [ebp+bytes_read_C]
                mov     eax, [ebp+mem_stream_decrypted_24]
                mov     ebx, [eax]
                call    dword ptr [ebx+10h]     ; write bytes

loc_492C4A:
                cmp     [ebp+bytes_read_C], 8
                jz      short loc_492BD4
                
                ...
                retn
decrypt_492B48  endp


Псевдокод:
bytes_read = mem_stream_encrypted->read(buf, 8);
for (i = 0; i < 8; i++) buf ^= key[i];

if (bytes_read == 8)
{
    sub_492A6C(buf, out decrypted_buf);
    for (i = 0; i < 8; i++) decrypted_buf ^= key[i];
    mem_stream_decrypted->write(decrypted_buf, bytes_read);
}
else
{
    mem_stream_decrypted->write(buf, bytes_read);
}

Сначала делается XOR с ключом.
Затем, если число прочитанных байт равно 8, вызывается процедура sub_492A6C, которая некоторым образом перемешивает биты результата.
Затем еще раз делается XOR с ключом.
Результат записывается в выходной буфер.
Если не равно (последние байты зашифрованного буфера), то ничего не вызывается, и второй XOR не делается.

sub_492A6C это вложенная функция, туда передается ebp родительской функции. Псевдокод довольно большой, проще описать словами. Она принимает на вход 8 байт, из первых битов составляет первый байт результата, из вторых бит второй и т.д.

(младший бит числа слева)

11111111      10000000
00000000      10000000
00000000      10000000
00000000  ->  10000000
00000000      10000000
00000000      10000000
00000000      10000000
00000000      10000000

Мне представляется примерно такой диалог:
— Давай через XOR с ключом зашифруем.
— Не, давай XOR, потом вот так вот перевернем, и еще раз XOR. Чтобы совсем было непонятно.
— Точно, давай.

Другими словами, матрица 8x8 бит обращается вокруг главной диагонали (транспонируется), после чего делается повторный XOR с ключом. В этом и заключается ошибка.


Взлом


Что мы делаем? Мы ксорим блок 8x8 бит с байтами ключа, обращаем эту матрицу, и ксорим еще раз с этим же ключом. Получается, биты на главной диагонали не шифруются вообще. Остальные биты имеют зависимость:
C[x][y] ^ K[x][y] ^ K[y][x] = D[y][x]
C[y][x] ^ K[y][x] ^ K[x][y] = D[x][y]

где C - зашифрованный блок, D - расшифрованный блок, K - ключ, x и y - произвольные координаты в матрице.

Элементы K[x][y] ^ K[y][x] образуют симметричную матрицу T[x][y]:
T[x][y] = T[y][x]

C[x][y] ^ T[x][y] = D[y][x]
C[y][x] ^ T[x][y] = D[x][y]

Это значит, что нам не надо искать все исходные биты ключа. Можно предположить, что верхний треугольник матрицы ключа составляют нули. Тогда верхний и нижний треугольники матрицы T будут равны нижнему треугольнику ключа.

Это позволяет уменьшить количество вариантов для перебора более чем в 2 раза — половина матрицы + диагональ. Количество неизвестных бит: (7 + 6 + 5 + 4 + 3 + 2 + 1) = 28. Итого 2^28 вариантов, причем после расшифровки матрицы должен получиться текст.

Метод перебора:
— читаем зашифрованный блок 8 байт
— размещаем текущее значение счетчика в нижнем треугольнике ключа
— делаем XOR с зашифрованным блоком
— обращаем полученную матрицу
— делаем XOR еще раз
— повторяем
— после расшифровки проверяем на текст; если текст не получился, то ключ неправильный
— для ускорения поиска можно проверять каждый расшифрованный блок

Типы битов в матрице (младший бит слева):
#define FIXED_0 0
#define FIXED_1 1
#define UNKNOWN 2

const unsigned char bitTypes[8][8] = {
	{0, 0, 0, 0, 0, 0, 0, 0},
	{2, 0, 0, 0, 0, 0, 0, 0},
	{2, 2, 0, 0, 0, 0, 0, 0},
	{2, 2, 2, 0, 0, 0, 0, 0},
	{2, 2, 2, 2, 0, 0, 0, 0},
	{2, 2, 2, 2, 2, 0, 0, 0},
	{2, 2, 2, 2, 2, 2, 0, 0},
	{2, 2, 2, 2, 2, 2, 2, 0}
};
Текущее значение счетчика перебора распределяется по битам UNKNOWN (в конце статьи есть код).

Также есть любопытная особенность. Сделав XOR частей, которые находятся по одинаковые стороны от знака ' = ', получаем:
C[x][y] ^ K[x][y] ^ K[y][x] ^ C[y][x] ^ K[y][x] ^ K[x][y] = D[y][x] ^ D[x][y]
C[x][y] ^ C[y][x] ^ (K[x][y] ^ K[x][y]) ^ (K[y][x] ^ K[y][x]) = D[y][x] ^ D[x][y]
C[x][y] ^ C[y][x] = D[y][x] ^ D[x][y]

XOR двух симметричных битов зашифрованного текста равен XOR этих битов расшифрованного текста. Возможно, это могло бы пригодиться в каких-то случаях, но здесь это не важно.


Не все так просто


1. После завершения перебора получится 65 вариантов текста.

На моей системе это заняло минут 15-20. Можно посмотреть их все вручную и выбрать подходящий. Но у нас есть подсказка — каждый 8-й байт в начале файла равен 0x33. Теперь мы знаем, как он появляется — это (старшие биты блока из 8 символов) XOR (8-й байт матрицы T). Если предположить, что текст на латинице, то старшие биты везде 0, и 0x33 — это 8-й байт матрицы в открытом виде.

Тогда можно перебирать в 256 раз меньше вариантов, то есть 2^20:

#define FIXED_0 0
#define FIXED_1 1
#define UNKNOWN 2

const unsigned char bitTypes[8][8] = {
	{0, 0, 0, 0, 0, 0, 0, 0},
	{2, 0, 0, 0, 0, 0, 0, 0},
	{2, 2, 0, 0, 0, 0, 0, 0},
	{2, 2, 2, 0, 0, 0, 0, 0},
	{2, 2, 2, 2, 0, 0, 0, 0},
	{2, 2, 2, 2, 2, 0, 0, 0},
	{2, 2, 2, 2, 2, 2, 0, 0},
	{1, 1, 0, 0, 1, 1, 0, 0}
};

После запуска перебора найдем единственный вариант. Это и будет предполагаемый ключ. Но в таком виде он не подходит.

2. Если количество байт в файле не кратно 8, то остаток получается зашифрованным обычным побайтовым XOR с ключом.

Здесь нужно проверять только вручную — смотреть расшифрованный текст, предполагать, какими могут быть последние байты, и на основе их восстанавливать матрицу.
Скрытый текст
Например, длина файла CharmSolitaire.udf равна 0x823, то есть последние 3 байта зашифрованы обычным XOR с ключом.
Сначала нужно найти предполагаемый ключ и расшифровать 0x820 байт.
На основе текста предположить, какими должны быть 3 оставшиеся байта.
Затем найти XOR между 3 зашифрованными и расшифрованными вручную байтами, это будут настоящие байты ключа.

Записываем побитово байты предполагаемого ключа.
Поверх записываем биты настоящих байтов. При этом, если какие-то биты не совпадают, нужно инвертировать симметричный бит в другой половине матрицы.
Если бит на главной диагонали, просто перезаписываем.

Подсказка: расшифрованный текст заканчивается на «ять.» :) (видеопамять)

Уровни после 30-го тоже зашифрованы. Уровень представляет собой XML-файл. Если ключ неточный, то могут быть разные проблемы — от артефактов в графике до исключения с сообщением о неправильной разметке XML. На основе файла CharmSolitaire.udf можно найти 3 младшие байта настоящего ключа и с таким ключом доиграть до 39 уровня. Его длина равна 0x246F, то есть можно найти все 7 неизвестных байт.
Скрытый текст
— перевести игру в оконный режим.
— поставить брейкпойнт на loc_492C3C в процедуре decrypt_492B48 (это код записи последних расшифрованных байт)
— взять байты, которые находятся в переменной buf_14
— сделать XOR с текущим ключом
— сделать XOR с текстом, который должен быть

Подсказка: уровень заканчивается тегом
</Level>0x0D,0x0A



Результат


8 байт ключа:
Bre6Vqd3

Текст на латинице в начале файла:
Some years ago the small pussy cat came to his house and ask the bug lived there about dinner.

Код программы:
Скрытый текст
#include <vcl.h>
#pragma hdrstop

#include "MainUnit.h"
#include <vector>
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TMainForm *MainForm;
//---------------------------------------------------------------------------
__fastcall TMainForm::TMainForm(TComponent* Owner)
	: TForm(Owner)
{
}
//---------------------------------------------------------------------------

inline unsigned int getBit(unsigned char byte, unsigned int bitNumber)
{
	return (byte & ((unsigned char)1 << bitNumber) ? 1 : 0);
}

inline void setBit(unsigned char &byte, unsigned int bitNumber, unsigned int bitValue)
{
	if (bitValue)
		byte |= ((unsigned char)1 << bitNumber);
	else
		byte &= ~((unsigned char)1 << bitNumber);
}

int isText(unsigned char *pData, int streamSize)
{
	int isText = 1;
	if (streamSize < 1) return isText;

	char prevChar = 0;
	do
	{
		if (*pData < 0x20 && *pData != 9)
		{
			if ((*pData == 0x0D || *pData == 0x0A))
			{
				// для ускорения поиска можно проверять каждый расшифрованный блок
				// так как он может начинаться с 0x0A, то эта часть кода не нужна
				  
				//if (*pData == 0x0A && prevChar != 0x0D)
				//{
				//	isText = 0;
				//	break;
				//}
			}
			else
			{
				isText = 0;
				break;
			}
		}
		
		prevChar = *pData;
		pData++;
	}
	while (--streamSize);

	return isText;
}




#define FIXED_0 0
#define FIXED_1 1
#define UNKNOWN 2

const unsigned char bitTypes[8][8] = {
	// младший бит слева
	{0, 0, 0, 0, 0, 0, 0, 0},
	{2, 0, 0, 0, 0, 0, 0, 0},
	{2, 2, 0, 0, 0, 0, 0, 0},
	{2, 2, 2, 0, 0, 0, 0, 0},
	{2, 2, 2, 2, 0, 0, 0, 0},
	{2, 2, 2, 2, 2, 0, 0, 0},
	{2, 2, 2, 2, 2, 2, 0, 0},
	{1, 1, 0, 0, 1, 1, 0, 0}
};

void getKeyMatrix(unsigned int keyBits, unsigned char matrix[8])
{
	int x, y;
	unsigned int bitValue = 0, bitNumber = 0;

	memset(matrix, 8, 0);
	for(y = 0; y < 8; y++)
	{
		for(x = 0; x < 8; x++)
		{
			// для массива координаты идут в обратном порядке
			if (bitTypes[y][x] == UNKNOWN)
			{
				bitValue = getBit(((unsigned char*)&keyBits)[bitNumber / 8], bitNumber % 8);
				bitNumber++;
			}
			else if (bitTypes[y][x] == FIXED_1)
				bitValue = 1;
			else
				bitValue = 0;

			setBit(matrix[y], x, bitValue);
		}
	}
}

void reverseMatrix(unsigned char block[8])
{
	unsigned int x, y, bitValue;
	unsigned char tmpBlock[8];
	for (y = 0; y < 8; y++)
	{
		for (x = 0; x < 8; x++)
		{
			bitValue = getBit(block[x], y);
			setBit(tmpBlock[y], x, bitValue);
		}
	}

	memcpy(block, tmpBlock, 8);
}

void decryptBlock(unsigned int keyBits, unsigned char *encryptedBlock, unsigned char *decryptedBlock, unsigned int blockSize)
{
	unsigned char key[8];
	unsigned int i;
	getKeyMatrix(keyBits, key);

	for(i = 0; i < blockSize; i++)
		decryptedBlock[i] = encryptedBlock[i] ^ key[i];

	if (blockSize == 8)
	{
		reverseMatrix(decryptedBlock);

		for(i = 0; i < 8; i++)
			decryptedBlock[i] = decryptedBlock[i] ^ key[i];
	}
}

void decryptText(unsigned char *encryptedText, unsigned char *decryptedText, unsigned int textSize, unsigned int keyBits)
{
	unsigned int position = 0, blockSize = 8, bytesToRead = 0;
	unsigned int i, j;

	while(position < textSize)
	{
		if (position + blockSize <= textSize)
			bytesToRead = blockSize;
		else
			bytesToRead = textSize - position;

		decryptBlock(keyBits, encryptedText + position, decryptedText + position, bytesToRead);
		// для ускорения поиска проверяем каждый блок
		if (bytesToRead == 8 && !isText(decryptedText + position, 8))
			break;

		position += bytesToRead;
	}
}

void getKeyVariants(unsigned char *encryptedText, unsigned int textSize, std::vector<unsigned int> &keyList)
{
	unsigned int variantsCount = 0;
	unsigned int possibleBits = 0;

	unsigned char *decryptedText = new unsigned char[textSize];
	//for (possibleBits = 0; possibleBits < (1 << 20); possibleBits++)
	for (possibleBits = 0; possibleBits < (1 << 6); possibleBits++)
	{
		decryptText(encryptedText, decryptedText, textSize, possibleBits);

		if (isText(decryptedText, textSize - textSize % 8))
		{
			keyList.push_back(possibleBits);
			variantsCount++;
		}
	}
	variantsCount = variantsCount;	// для брейкпойнта
	

	delete []decryptedText;
}

AnsiString getKeyText(unsigned char keyMatrix[8])
{
	AnsiString str = "000000000000000000000000" + IntToStr(*(__int64*)keyMatrix);
	str = str.SubString(str.Length() - 24 + 1, 24);  // нумерация с 1 

	AnsiString keyText = "";
	for(int i = 0; i < 24; i += 2)
		keyText.cat_printf("%c", str.c_str()[i + 1]);
	for(int i = 0; i < 24; i += 2)
		keyText.cat_printf("%c", str.c_str()[i]);
	
	return keyText;
}

//---------------------------------------------------------------------------

std::vector<unsigned int> keyList;
void __fastcall TMainForm::btnStartClick(TObject *Sender)
{
	AnsiString filename = "F:\\Games\\Charm Solitaire\\CharmSolitaire.udf";
	TMemoryStream *encryptedStream = new TMemoryStream();
	encryptedStream->LoadFromFile(filename);

	getKeyVariants((unsigned char *)encryptedStream->Memory, encryptedStream->Size, keyList);

	unsigned char keyMatrix[8];
	getKeyMatrix(keyList[0], keyMatrix);
	getKeyText(keyMatrix);
}


void __fastcall TMainForm::btnDecryptClick(TObject *Sender)
{
	AnsiString filename = "F:\\Games\\Charm Solitaire\\CharmSolitaire.udf";
	TMemoryStream *encryptedStream = new TMemoryStream();
	encryptedStream->LoadFromFile(filename);

	unsigned int keyBits = keyList[0];
	unsigned int textSize = encryptedStream->Size;
	unsigned char *decryptedText = new unsigned char[textSize + 1];
	decryptedText[textSize] = 0;

	decryptText((unsigned char *)encryptedStream->Memory, decryptedText, textSize, keyBits);
	mDecryptedText->Text = AnsiString((char*)decryptedText);
}
//---------------------------------------------------------------------------



Tags:
Hubs:
+14
Comments 4
Comments Comments 4

Articles