Вступление
У меня нет опыта в реверсе, есть некие базовые знания ассемблера, позволяющие сделать битхак условных переходов, чтобы данное приложение всегда выходило на success-path независимо от введенного значения. Но так как это слишком банально для того, чтобы быть опубликованным здесь, а до полного воспроизведения алгоритма проверки введенного ключа я еще не дорос, я решил попробовать метод брута, т.е. последовательного подбора ключей. Конечно же, я не могу сказать, что и этот способ отличается своей крутизной, но по крайней мере подробное описание этого способа не так часто встречается среди статей по реверсу.
Источник
Ссылка на оригинальный пост crackme#03.
В ветке, где опубликован этот крякми, пользователь ARCHANGEL опубликовал метод брута на C++, который действительно выдает правильный пароль для этого крякми. Он мог позволить себе написать брут на языке высокого уровня, так как смог воспроизвести алгоритм и нашел значение 0x23a06032, с которым сравнивается полученное crc.
Я же пока не научился так глубоко анализировать алгоритмы на asm-е, поэтому буду работать не отходя от кассы, используя алгоритм проверки в самом крякми как черный ящик.
Анализ условных переходов
Запускаем крякми и выключаем звук на компьютере (видимо именно этого добивался автор, добавив звуковое сопровождение).
Запускаю OllyDbg 2.01 c плагином OllyExt, в котором выставлены приведенные ниже настройки:
Этот плагин поможет избавиться от некоторых потенциальных приемов анти-отладки.
Аттачимся из Ольги к выполняемому процессу crackme#03.exe: File > Attach…
Далее пробуем ввести любое значение в поле ввода и без труда находим код, в котором оно обрабатывается. Сделать это можно выставив breakpoint-ы на потенциальные функции получения текста (в частности GetDlgItemTextA) или пролистать код исходного модуля — благо здесь он небольшой. Нажимаем кнопку Check.
Срабатывает breakpoint, жмем один раз F8 и смотрим листинг. Видим, что введенная нами строка хранится по адресу 004095BC. Также смотрим на инструкцию по адресу 0040101F. Она сравнивает длину введенной строки со значением 12 и в случае неравенства выбрасывает нас на 004010AF.
Далее, обратите внимание на инструкцию по адресу 00401028, которая сравнивает 12-ый байт введенного значения со значением 72, а это буква r в ASCII-кодировке. В случае неравенства снова выбрасывает нас на 004010AF. Что же это за адрес такой? Об этом чуть позже.
Теперь обратим внимание на инструкции в диапазоне 00401031 — 0040104C. Покурив справочник команд ассемблера по командам repne и scas, а также потрассировав выделенную область кода, приходим к выводу, что aehnprwy — это допустимый алфавит требуемого ключа.
Теперь посмотрим, что же у нас расположено по адресу 004010AF. Там вывод сообщения о неудачном вводе пароля.
Итак, подведем итоги первичного анализа:
1) Ключ должен состоять из 12 символов;
2) Последний символ ключа должен быть r;
3) Символы ключа должны принадлежать алфавиту aehnprwy.
Брутфорс
Приступим к реализации брут-метода. Реализуем его сначала на концептуальном уровне. Можно на блок-схеме, можно на языке высокого уровня. Я реализовал его прототип на C++. Для понимания дальнейшего материала необходимо переварить код под спойлером.
Скрытый текст
#include <stdio.h>
#include <string.h>
const char *Alphabet = "aeyhpnwr";
int Password_len = 12;
int CheckPassword(char pass[]) // функция проверки введенного пароля.
{
if (strcmp(pass, "aaaaaaahaahr") == 0) // проверка на заглушку
return 0;
return 1;
}
int main()
{
int Alphabet_len = strlen(Alphabet);
char* CodeArray = new char[Password_len + 1];
char* Password = new char[Password_len + 1];
int n;
// Инициализируем пароль, чтобы все символы были равны первому (т.е. нулевому) символу алфавита
Password[Password_len] = 0;
for (int i = 0; i < Password_len; i++)
{
CodeArray[i] = 0;
Password[i] = Alphabet[CodeArray[i]];
}
// последний символ всегда r
Password[Password_len - 1] = 'r';
while (true)
{
//printf("\nCurrent pass = %s", Password);
if (CheckPassword(Password) == 0)
{
printf("\nRight Password = %s\n", Password);
break;
}
n = Password_len - 2;
while (n >= 0)
{
CodeArray[n]++;
if (CodeArray[n] >= Alphabet_len)
{
CodeArray[n] = 0;
Password[n] = Alphabet[0];
n--;
continue;
}
Password[n] = Alphabet[CodeArray[n]];
break;
}
}
delete[] CodeArray;
delete[] Password;
}
Основные этапы:
1) Инициализация ключа (пароля) начальным значением.
2) Проверка ключа.
3) Если ключ неверен, то генерим следующий ключ и переходим к шагу 2. Если ключ верный, переходим к шагу 4. Если мы перебрали все ключи и ни один из них не подошел, переходим к шагу 5.
4) Crackme взломан.
5) Crackme не взломан. Где-то мы ошиблись.
Инициализация ключа
Программа хранит и обращается к ключу по адресу 004095BC — там и будем его инициализировать. Алфавит расположен по адресу 00409071.
004095CC — это адрес памяти, где мы будем хранить наш массив CodeArray.
Для инициализации ключа находим неиспользуемую область секции кода и вставляем туда наши инструкции. Я выбрал адрес 00404122.
Чтобы собственно эта инициализация выполнилась, идем к тому месту, где вызывается GetDlgItemTextA, аккуратно заменяем ее вызов на безусловный переход к нашему коду по адресу 00404122, а предшествующие ей push-команды заменяем nop-ами. Все делаем аккуратно, чтобы адрес команды CMP EAX, 0C остался прежним: 0040101F.
На рисунке ниже представлен сам код инициализации. По ее окончанию переходим к алгоритму валидации ключа в результате безусловного перехода на 0040101F.
Скрытый текст
Обнулить массив CodeArray, состоящего из 12 однобайтовых значений, можно в 3 итерации по 4 байта за итерацию.
Далее выполняем проставление первого (т.е. нулевого) символа алфавита для каждого байта строки по адресу 004095BC
00404122 B9 03000000 MOV ECX,3
00404127 BA CC954000 MOV EDX,004095CC ; адрес массива CodeArray
0040412C C7448A FC 000 MOV DWORD PTR DS:[ECX*4+EDX-4],0
00404134 E2 F6 LOOP SHORT 0040412C
Далее выполняем проставление первого (т.е. нулевого) символа алфавита для каждого байта строки по адресу 004095BC
for (int i = 0; i < Password_len; i++)
{
Password[i] = Alphabet[CodeArray[i]];
}
00404136 BB BC954000 MOV EBX,004095BC ; Адрес введенного ключа, к которому обращается crackme
0040413B B9 0C000000 MOV ECX,0C
00404140 31C0 XOR EAX,EAX
00404142 8A4411 FF MOV AL,BYTE PTR DS:[EDX+ECX-1]
00404146 8A80 71904000 MOV AL,BYTE PTR DS:[EAX+409071] ; 409071 - адрес алфавита
0040414C 884419 FF MOV BYTE PTR DS:[EBX+ECX-1],AL
00404150 E2 F0 LOOP SHORT 00404142
00404152 C643 0B 72 MOV BYTE PTR DS:[EBX+0B],72 ; Последний символ всегда r
00404156 C643 0C 00 MOV BYTE PTR DS:[EBX+0C],0 ; Закрываем строку нулевым символом
0040415A B8 0C000000 MOV EAX,0C ; Обозначаем длину ключа как 12
0040415F ^ E9 BFCEFFFF JMP 0040101F ; Возврат к коду валидации ключа
Получение следующего значения ключа
Теперь нам нужно реализовать код получения следующего значения ключа. Выбираем для этого адрес 00404165 (сразу после кода инициализации) и делаем переход на него с того места, где программа обзывает нас ламерами.
Модифицируем инструкции по адресам 004010AF и 004010B1.
Ну и по адресу 00404165 реализуем код получения нового ключа.
Скрытый текст
00404165 B9 0B000000 MOV ECX,0B ; Количество символов ключа, которые подлежат изменению
0040416A BA CC954000 MOV EDX,004095CC ; Адрес массива CodeArray
0040416F BB BC954000 MOV EBX,004095BC ; Адрес введенного ключа, к которому обращается crackme
00404174 31C0 XOR EAX,EAX
00404176 FE4411 FF INC BYTE PTR DS:[EDX+ECX-1] ; Получаем следующий порядковый номер символа в алфавите
0040417A 807C11 FF 08 CMP BYTE PTR DS:[EDX+ECX-1],8 ; Проверяем, не вышли ли за пределы алфавита
0040417F 7D 15 JGE SHORT 00404196
00404181 8A4411 FF MOV AL,BYTE PTR DS:[EDX+ECX-1] ; Если не вышли, грузим номер в AL
00404185 8A80 71904000 MOV AL,BYTE PTR DS:[EAX+409071] ; Получаем символ из алфавита по порядковому номеру. 409071 - адрес алфавита
0040418B 884419 FF MOV BYTE PTR DS:[EBX+ECX-1],AL ; Записываем букву в соотв-ий символ ключа
0040418F B8 0C000000 MOV EAX,0C ; Обозначаем длину ключа как 12
00404194 ^ EB C9 JMP SHORT 0040415F ; Прыгаем на код валидации ключа
00404196 83F9 01 CMP ECX,1 ; Если вышли за пределы алфавита, смотрим, не первый ли это символ ключа
00404199 7F 02 JG SHORT 0040419D ;
0040419B CD 03 INT 3 ; Перебор закончен, правильный ключ не найден
0040419D C64411 FF 00 MOV BYTE PTR DS:[EDX+ECX-1],0 ; Если ecx (i) > 1, CodeArray[i] = 0;
004041A2 A0 71904000 MOV AL,BYTE PTR DS:[409071] ; В AL записываем первый (нулевой) символ алфавита. 409071 - адрес алфавита
004041A7 884419 FF MOV BYTE PTR DS:[EBX+ECX-1],AL ; Записываем букву в соотв-ий символ ключа
004041AB 49 DEC ECX ; Переходим к след. элементу
004041AC ^ EB C6 JMP SHORT 00404174 ; Повторяем итерацию для следующего элемента
Запуск
Жмем кнопку Check и идем пить чай. Черт, и чай-то давно уж вскипел, придется кипятить заново.
Через некоторое время получаем:
Смотрим, что же у нас по адресу 004095BC
happynewyear — это и есть искомый ключ.
Спасибо за внимание!
Поделиться с друзьями
Комментарии (7)
Find_the_truth
27.03.2017 13:09Может, до попытки использования в качестве пароля слов из окна я бы и не додумался, но, получив список символов допустимого алфавита, первым делом попробовал бы составить слово/фразу.
P.S. Спасибо за ссыль на крякми.akk0rd87
27.03.2017 13:15Я даже если честно не обратил внимания на саму картинку, и то, что на ней написан сам key, заметил уже в момент написания статьи. И с интуицией, видимо, у меня не очень.
maniacscientist
27.03.2017 15:18+1Реверс в массы — это просто прекрасно. Столько достойных-отстойных целей — денува, aacs 2, e-licenser…
sbh
ключ был написан в окне программы! рука лицо ))))
akk0rd87
Да, поэтому среди решивших эту задачу были и те, кто сделал это без отладчика и дизассемблера.
fareloz
Сразу напомнило как взломали какой-то важный ресурс из-за интервью на канале, где в кадре были стикеры с паролями на мониторе.