Решаем Kaspersky Crackme 2015
Всем привет! Вот недавно возникла необходимость раскурочить один из трёх crackme от zero_nights 2015, которые были подготовлены специально для этого события Лабораторией Касперского. Своей жертвой я выбрал второй хрякми :-), так как в первом слишком много букав надо было читать, а решение третьего уже кто-то выкладывал на хабре. Поэтому, мы будем сегодня решать второй crackme — «Кредитная карта». Зная задачки с 2014 года, я рассчитывал потратить минимум неделю, на решение данной таски, но, к счастью, этот crackme оказался довольно простым. Зато, он был интереснее, так как упор был сделан не только на реверс алгоритма, но и на некоторые приёмы, которые используются во вредоносных программах. Сам crackme можете взять тут. Итак, приступим.
Приступаем к решению
Первым делом, давайте запустим exeшник в ide и попробуем найти проверяющую функцию по строкам «Serial is invalid». Жмём shift+F12. Да уж, таких строк мы не находим! Значит, часть данных (а то и кода) будет получаться динамически, в процессе работы программы. Давайте тогда посмотрим на winmain функу.
Как мы видим, тут находится ресурс по идентификатору, далее, вызов LockResource вернёт нам указатель на найденный ранее и уже загруженный ресурс в память. После этого происходит выделение памяти вызовом VirtualAlloc и, копирование туда полученного ресурса. Удобнее ресурсы будет понаблюдать в CFF Explorer.
Кстати говоря, раз уж мы уже открыли бедолагу в CFF explorer, то давайте сразу снимем галочку в Optional Header/DllCharacteristics/Dll can move. Если она будет установлена, то тогда, у нас будет постоянно меняться базовый адрес загрузки модуля. Это сделано для безопасности, но сейчас, нам это никчему.
Смотрим дальше. После того, как все данные нужных ресурсов будет подгружены в память, мы видим, что происходит вызов функции CallWindowProcW. Это немного необычно, так как, обычно оконную процедуру вызывает сама система, а программист реализует цикл обработки оконных сообщений. Теперь, давайте поставим на этом месте бряк и запустим crackme в дебагере. Когда брякнемся, попробуем снова найти в памяти строки «Serial is invalid». У нас цикл повторится два раза, после чего программа завершится. Если мы прекратим отладку и закроем ida pro, то по идее у нас должна завершиться и отлаживаемая программа, но этого мы не наблюдаем! Давайте всё повторим снова, но только когда брякнемся, сразу заглянем в Process Hacker.
Как видим, у нас создался дочерний процесс! Поэтому при закрытии отладчика завершается родительский, а сам, рабочий остаётся. Теперь взглянем на оконную процедуру, которая передаётся в CallWindowProcW первым параметром. По этому указателю лежит вот такой код:
Взглянем на unk_3D0054.
Тут находится куча однотипных вызовов каких-то функций. Что там делается не так интересно, за исключением вот этой вот: sub_3D0420, которая сначала вызывает sub_3D0477, а затем sub_3D04AB. sub_3D0477 получит нам адрес загрузки kernel32.dll
Принцип получения всё тот же, как и у базонезависимых программ (шелкодов, например). Сначала получается PEB, затем из него вытаскивается список загруженных модулей, в общем, всё как обычно. Подробнее я описывал в статье Буткит, часть 3 — Эскалация привилегий. Далее, полученный адрес загрузки kernel32.dll передаётся в sub_3D04AB вместе с числом 0xEC0E4EA4, которое является хешем от слова «LoadLibraryW». Капитан очевидности подсказывает, что sub_3D04AB — получает адрес функции по хешу от её имени, но давайте проверим:
Как мы видим, сначала, по дос заголовку получается смещение до PE заголовка, затем получается адрес таблицы экспорта и так далее. Алгоритм хеширования всё тот же: сумма кода символа ror 0x0d. Все подробности реализации алгоритма смотрите также, в заключительной статье про буткиты. Давайте отмотаем в конец!
Последним делом у нас получаются адреса функций с хешами 0E8A7C7D3h и 9E4A3F88, что соответствует функциям SetThreadContext и ResumeThread. Мы пролистали место, где получается адрес функции CreateProcessInternal, которая создаёт дочерний процесс в замороженном виде:
Идея тут в следующем: нужно динамически получить адреса нужных функций, чтобы их не светить в экспортах, далее распаковать/расшифровать в память нужные данные и создать копию своего процесса. Как только все приготовления будут завершены, происходит переключение контекста из родительского процесса в дочерний на точку входа. Для этого изменяется EIP регистр в структуре CONTEXT, а после вызывается функция ResumeThread, которая будет выполняться уже в контексте дочернего процесса. Вот этот ключевой момент:
По адресу 003D0409 передаётся в стек указатель на структуру CONTEXT. Давайте посмотрим, нашу будущую точку входа. Для этого наложим структуру CONTEXT на этот указатель. Жмём Shift+F9, далее, если в списке нет данной структуры, то жмите insert и кнопку Add standard structure. В выпавшем списке выбирайте CONTEXT.
Теперь, нужно выделить 2сс байт (столько занимает данная структура). Чтобы не скролить столько, сделаем так: установим выделение за курсором, (жмем Alt+L) и переходим на конец структуры. Далее, наложим саму структуру: Alt+Q, выбираем CONTEXT и получаем вот что:
В таком виде, уже понятнее, что к чему! 777964D8 является нашей новой точкой входа! С родительским процессом всё ясно. Давайте теперь приаттатчимся к дочернему. Но прежде чем это сделать, для интереса, сдампим его. В дампе мы в последний раз попробуем найти наши строки и, находим!
Это означает, что процесс готов! Теперь, можно уже всё делать, как обычно. По этим строкам мы найдём проверяющую функцию:
Тут идёт получение текста с эдитов, проверка ввода на правильность. Находим саму функцию, от результата которой зависит, какое сообщение нам высветится:
Я её назвал Z3r0_N1ghts, по соответствующей важной константе, в ней найденной. В этой функции первым делом проверяется длина пароля. Которая должна соответствовать правилу: (pass_len^2 — 24) mod 1000 = 0. Так как мне было лень в уме решать, то я накидал скрипт на сях, который бы мне сам нашёл это магическое число. Вот какой получился скрипт:
#include <cstdlib> #include <iostream> #include <stdio.h> using namespace std; int main(int argc, char *argv[]) { for (int i=0; i<0xFFFFFFFF; i++) { if ( (i*i - 0x18) % 0x3e8 == 0 ) { cout<<"Fount!"<< i<<endl; break; }else { printf(" i= %d\r",i); } } cout<<"done!"<<endl; system("PAUSE"); return EXIT_SUCCESS; }
Результат программы:
Ага! Длина пароля может быть 32! Далее встречаем функцию подсчёта md5. Её узнаём по константам. Видим, что сначала считается хеш от введённого email, далее от константы Z3r0_N1ghts.
После, происходит обработка нашего серийника. С помощью функции sprintf строки переводятся в байты. Если в строках есть символы, которые не входят в диапазон [0-9,a-f,A-F], то функция вернёт 0 и проверка на этом прекратится. Иначе, хеш сконвертнётся из символьного вида в байтовый. Но, в процессе конвертации происходит следующее:
То есть результат записывается так: md5(mail)[i]+md5(Z3r0_N1ghts)[i]. Окей, смотрим далее:
Тут как раз видим, что полученный сложением байт хеш должен быть равен серийнику, который мы ввели! Ну всё, алгоритм понятен, пишем keygen.
Пишем keygen
#include <Windows.h> #include <iostream> #include <stdio.h> #include "..\..\hash-library\md5.h" using namespace std; char ZN[] = "Z3r0_N1ghts"; int main() { MD5 md5; string mail, result = ""; cout << "mail: " << endl; cin >> mail; string md5_zn = md5(ZN); string md5_mail = md5(mail); //a9ae1f9cdfe64dd33e6f209da14a61dd for (int i = 0; i < md5_zn.length(); i+=2) { BYTE byte1 = strtol(md5_mail.substr(i, 2).c_str(), NULL, 16); BYTE byte2 = strtol(md5_zn.substr(i, 2).c_str(), NULL, 16); BYTE byteRes; __asm { movzx eax, byte2 mov ebx, eax movzx eax, byte1 sub al, bl neg al mov [byteRes], al } char tmp[4]; itoa(byteRes, tmp, 16); result += tmp; } system("cls"); cout << mail << " : " << result << endl; system("pause"); return 0; }
Компилируем, запускаем:
If you found an error, highlight it and press Shift + Enter or click here to inform us.
А почему нельзя снять дамп с уже запущенного приложения?
Делаю дамп дочернего процесса, пытаюсь запустить — вылеает с ошибкой доступа к памяти. Пробовал так же дебажить путем аттача к дочернему процессу. Зависаю на ntdll.dll. Автор — расскажи более подробно как ты аттачился к дочернему процессу? И почему сдампленный образ из памяти не работает?