Лет 7-8 назад мне случайно попалась игра CharmSolitaire, скопированная вместе с другими играми с чужого винта в процессе обмена информацией. Это такой не совсем обычный карточный пасьянс. В незарегистрированной версии на игру отводится один час, и открыта только половина уровней. В той копии время уже почти закончилось. Денег на покупку у меня не было, поэтому скорее всего я бы ее удалил. Но в то время я немного увлекался взломом и решил попробовать найти регистрационный код. Опыт был довольно интересным. В статье рассказывается об основных особенностях защиты, а также о том, как security through obscurity может ее ослабить.
Ключа в открытом виде в статье нет, но кому интересно, сможет найти его самостоятельно.
Все действия вы выполняете на свой страх и риск.
Исследование защиты
Загружаем файл в IDA, запускаем игру и нажимаем кнопку «Ключ». Вводим что-нибудь, например 123321, и ищем через ArtMoney.
Ставим 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 зашифрованными и расшифрованными вручную байтами, это будут настоящие байты ключа.
Записываем побитово байты предполагаемого ключа.
Поверх записываем биты настоящих байтов. При этом, если какие-то биты не совпадают, нужно инвертировать симметричный бит в другой половине матрицы.
Если бит на главной диагонали, просто перезаписываем.
Подсказка: расшифрованный текст заканчивается на «ять.» :)
Сначала нужно найти предполагаемый ключ и расшифровать 0x820 байт.
На основе текста предположить, какими должны быть 3 оставшиеся байта.
Затем найти XOR между 3 зашифрованными и расшифрованными вручную байтами, это будут настоящие байты ключа.
Записываем побитово байты предполагаемого ключа.
Поверх записываем биты настоящих байтов. При этом, если какие-то биты не совпадают, нужно инвертировать симметричный бит в другой половине матрицы.
Если бит на главной диагонали, просто перезаписываем.
Подсказка: расшифрованный текст заканчивается на «ять.» :)
Уровни после 30-го тоже зашифрованы. Уровень представляет собой XML-файл. Если ключ неточный, то могут быть разные проблемы — от артефактов в графике до исключения с сообщением о неправильной разметке XML. На основе файла CharmSolitaire.udf можно найти 3 младшие байта настоящего ключа и с таким ключом доиграть до 39 уровня. Его длина равна 0x246F, то есть можно найти все 7 неизвестных байт.
Скрытый текст
— перевести игру в оконный режим.
— поставить брейкпойнт на loc_492C3C в процедуре decrypt_492B48 (это код записи последних расшифрованных байт)
— взять байты, которые находятся в переменной buf_14
— сделать XOR с текущим ключом
— сделать XOR с текстом, который должен быть
Подсказка: уровень заканчивается тегом
— поставить брейкпойнт на 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);
}
//---------------------------------------------------------------------------
Комментарии (3)
berez
11.06.2015 14:52Да-а… Я бы, наверное, обошелся простым битхаком: заменил бы проверку
call sub_4C23A8 inc al ; (+ nop, если надо). Было: test al, al jnz short loc_4C277C
Или еще проще: вставил бы mov al, 1; ret в самое начало check_key_4C23A8.
Это совсем не по-пацански, наверное?michael_vostrikov Автор
11.06.2015 15:37+7Здесь уровни после 30-го зашифрованы и расшифровываются ключом, так что просто обойти проверку не поможет)
datacompboy
Да, Known-Plaintext и Guessed-Plaintext это убийственные способы съесть иногда больше, чем хотели предложить :)