По правилам нам нужно найти ключи и секретный флаг, а не просто поломать механизмы валидации, чтобы crackme думал, что он решен. Из инструментария нам понадобится: IDA, DiE и PuTTY. Начнем с DiE.
Энтропия в норме, файл ничем не упакован. Это вполне ожидаемо: накрывать кракми известными навесными упаковщиками — признак плохого тона. Поставить такую защиту может кто угодно, а ее снятие — это рутина, а не решение загадки. Давай запустим кракми и посмотрим, как он работает, что выводит на экран, как реагирует на ввод неправильного пароля.
При загрузке мы видим цветные строчки и забавного кролика. На ввод неправильного пароля программа пишет строчку «Nope!» и закрывается. Загрузим крякмис в дизассемблер IDA и посмотрим на начало программы, строки и структуру кода.
Начальный код очень прост, и у нас есть два пути его анализа: поискать перекрестные ссылки на сообщения о вводе пароля (по-крякерски!) или начать его исследовать с самого начала. Давай пойдем по второму пути — в этом конкретном случае так будет интереснее. Кроме того, обратим внимание на структуру кода: распознанных исполняемых инструкций не так много. И нельзя не заметить размер файла — больше шести мегабайт!
Запомним это все и начинаем исследовать код. Видим несколько вызовов и подпрограмму loc_403CDE
.
loc_403CDE:
push offset aNope ; "Nope!\n"
push 0C0h ; wAttributes
call sub_403990
add esp, 8
push 3E8h ; dwMilliseconds
call ds:Sleep
or eax, 0FFFFFFFFh
retn
Судя по всему, этот код выполняется при неверном вводе пароля: выводится сообщение «Nope!» и программа завершается. Давай переименуем эту подпрограмму в exit_err
, чтобы было понятнее, что это завершение программы после некорректного ввода данных.
Итак, первый вызов — это call sub_402CC0
. Заходим в него и разбираемся.
Вызываются функции работы с атрибутами текста в консоли, кроме того, мы видим буфер текста «Wake up, Neo… I have a challenge for…». Очевидно, этот call отвечает за начальный баннер кракми. Переименуем call sub_402CC0
в call banner
, чтобы нам было легче читать код в дальнейшем.
Движемся дальше по коду. После вызова загрузочного баннера у нас идет call sub_4034D0
. Заходим в него — беглый осмотр показывает, что это один из уровней крякмиса. Об этом нам говорят следующие строки кода:
push offset aPassword1 ; "Password#1:\n"
...
push offset aSoFarSoGood ; "So far, so good!\n"
Это данные, которые передаются функции вывода на экран. С уверенностью переименовываем call sub_4034D0
в call level_1
и начинаем исследовать уровень более детально. После стандартного пролога и механизма защиты от переполнения буфера стека мы видим интересный код:
lea eax, [ebp+nNumberOfBytesToWrite]
push 65h ; hResData (101)
push eax ; int
mov [ebp+nNumberOfBytesToWrite], 0
call sub_403D90
Давай перейдем в call sub_403D90
и посмотрим, что там. А там находится несложный код работы с ресурсами. Наблюдается обилие вызовов FindResourceA
, LoadResource
, LockResource
, SizeofResource
. Дальше все это завершается кодом выделения памяти и копирования данных:
mov esi, [ebp+arg_0]
push 4 ; flProtect
push 3000h ; flAllocationType
push eax ; dwSize
push 0 ; lpAddress
mov [esi], eax
call ds:VirtualAlloc
push dword ptr [esi] ; size_t
mov edi, eax
push ebx ; void *
push edi ; void *
call _memmove_0
Становится понятно, что sub_403D90
читает ресурс нашего кракми и копирует его в выделенный буфер в памяти. Переименуем sub_403D90
в load_res_in_buf
и остановимся на этом подробнее.
Код push 65h
говорит нам о ресурсе 101
. Давай вернемся в DiE и посмотрим на ресурсы. Для этого жмем кнопочку Resource и в открывшемся окне раскроем список под названием RT_RCDATA
. Видим два ресурса, из которых нас интересует 101.
Открываем его в hex-представлении.
Данные ресурса зашифрованы, но обрати внимание на символическое представление hex-данных — четко виден повторяющийся паттерн. Это значит, что применен простой шифр, у которого отсутствует лавинный эффект в шифротексте, но присутствуют повторяющиеся паттерны в шифровании. Похоже, тут взяли какую-то фразу-пароль и поксорили ей данные блоками.
Можно сделать предположение, что после первых нескольких десятков байт идут однородные данные, зашифрованные этим шифром. Выделяем повторяющийся паттерн и считаем его размер в байтах. Получается 17 байт. Если мы верно сделали предположение насчет шифра, то 17 байт — длина нашего пароля.
Смотрим размер ресурса — 0x5eec36h
, то есть 6 220 854 байт, или 5 Мбайт. Результат выполнения функции сохраняется в регистр esi
.
Возвращаемся к изучению кракми. Видим код запроса пароля:
push offset aPassword1 ; "Password#1:\n"
push 0A0h ; wAttributes
call sub_403990
Очевидно, что в функцию sub_403990
передаются данные, которые будут выведены в консоль. Зайдя в нее, можно убедиться в этом: функция весьма похожа на функцию banner
, которую мы уже видели. Переименуем sub_403990
в print_str
и идем дальше.
Следующий вызов — call sub_401000
. Если зайти в него, то окажется, что он немного запутан из-за инструкций компилятора Microsoft Visual Studio по отлову исключений и механизмов от переполнения буфера, которые мы уже видели. А главное — что этой функции передается указатель на переменную в стеке var_438
. Если проследить манипуляции с этим указателем в самой функции, то оказывается, что тут происходит всего лишь чтение вводимых пользователем данных, которые записываются в эту переменную. Вероятно, это чтение пароля. Переименуем sub_401000
в read_data
, переменную var_438
— в pass
и продолжим исследовать код.
Упражнение
Чтобы попрактиковаться в анализе таких структур, попробуй исследовать простенькую программу с разными вариантами оптимизации в настройках компилятора.
int main(){
std::cout << "Hello World!\n";
std::string s;
std::cin >> s;
Sleep(5000);
return 0;
}
Далее следует интересный код:
call sub_404150
add esp, 1Ch
cmp eax, 57585384h
jnz loc_4036B1
Мы видим, что идет вызов call sub_404150
и далее сравнение cmp eax, 57585384h
, затем условный переход. Очевидно, мы подошли вплотную к логике проверки пароля. Прыгаем в call sub_404150
и смотрим, что там.
Видим цикл операций xor
с каким-то «магическим» числом 82F63B78h
. Если поискать информацию об этом числе, то мы наткнемся на статью Википедии о вычислении CRC32. Очевидно, цикл вычисляет CRC32 данных, далее идет сравнение cmp eax, 57585384h
, потом принимается решение о переходе. Разумеется, 57585384h
— это эталонный CRC32 пароля, который сравнивается с введенными данными. Переименуем sub_404150
в crc32
и читаем код дальше.
lea eax, [ebp+pass]
cmovnb eax, [ebp+pass]
push eax
push [ebp+nNumberOfBytesToWrite]
push esi
call sub_403C90
Очень интересно. В функцию sub_403C90
передается пароль, переменная nNumberOfBytesToWrite
(размер ресурса) и адрес ресурса. Заглянем внутрь.
Разумеется, это функция дешифровки ресурса методом XOR. Переименуем функцию sub_403C90
в xor_crypt
и просмотрим код далее.
Беглый просмотр кода выявляет вызов функции WinAPI для получения пути временной директории, число миллисекунд с момента запуска компьютера и вот такие интересные данные, которые кладутся в стек:
push offset aSWallpXTmp ; "%s\\wallp%x.tmp"
Далее — функция sub_403090
, которая создает и записывает данные в файл, вызывая CreateFileW
и WriteFile
, и аргументы которой выглядят таким образом:
; int __cdecl sub_403090(LPCWSTR lpFileName, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite)
sub_403090 proc near
NumberOfBytesWritten= dword ptr -4
lpFileName= dword ptr 8
lpBuffer= dword ptr 0Ch
nNumberOfBytesToWrite= dword ptr 10h
И далее — код установки обоев рабочего стола в функции call sub_403D20
, о чем говорит параметр SPI_SETDESKWALLPAPER(0x0014)
, передаваемый функции SystemParametersInfo
:
.text:00403D23 push 2 ; fWinIni
.text:00403D25 push [ebp+pvParam] ; pvParam
.text:00403D28 push 0 ; uiParam
.text:00403D2A push 14h ; uiAction
.text:00403D2C call ds:SystemParametersInfoW
Далее в дизасме идет сообщение об успешном вводе пароля.
Итак, что мы имеем? Мы знаем, что пароль будет 17 байт, знаем, что этим паролем зашифрован ресурс, который занимает 5 Мбайт и будет в дальнейшем установлен как обои рабочего стола. Можно предположить, что наш ресурс — это файл в формате BMP, который подходит под все эти признаки (включая размер).
Отталкиваясь от этого, будем рассуждать логически: файл BMP начинается с заголовка, формат его документирован, и, посмотрев в документацию, мы можем восстановить первые 17 байт заголовка. В итоге мы будем иметь 17 байт шифротекста и 17 байт открытого текста. Учитывая, что алгоритм шифрования — это XOR, мы можем поксорить эти последовательности между собой, чтобы таким образом получить пароль и расшифровать остальные данные.
Для начала нам нужен стандартный заголовок файла формата BMP. Открываем в hex-редакторе любую картинку в BMP, чтобы иметь перед глазами пример.
Обратимся к документации по этому формату. Нас интересуют первые 17 байт.
BMP Header:
0h 2 42 4D
— идентификатор (буквы BM)2h 4 46 00 00 00
— размер файла (нам известен, заполняется в формате big-endian)6h 2 00 00
— зарезервировано8h 2 00 00
— зарезервированоAh 4 36 00 00 00
— смещение начала изображения
DIB Header:
Eh 4 28 00 00 00
— число байтов DIB header
Итак, восстановленный заголовок с известными данными:
42 4D 36 EC 5E 00 00 00 00 00 36 00 00 00 28 00 00
Шифротекст:
24 22 5A 80 31 77 5F 64 61 5F 44 61 62 62 41 74 7A
Применяем XOR и получаем фразу-пароль: follow_da_rabbitz
. Вводим ее в кракми, и он сообщает нам: So far, so good! Обои рабочего стола меняются на черный фон с изображением кролика и предложением сохранять спокойствие и следовать за кроликом. Кракми тем временем просит нас ввести второй пароль.
Возвращаемся в IDA и исследуем следующий уровень. По структуре он очень похож на предыдущий. Поскольку мы уже дали функциям более понятные названия, все становится еще проще. Кроме того, мы понимаем, что в этот раз после ввода корректного пароля также создается файл с ресурсом, который имеет расширение exe. Это интересно! Давай взглянем на этот ресурс в DiE — может, предыдущий трюк нам поможет и здесь? 🙂
Очевидно, что нет. Так просто здесь не будет. Давай смотреть функцию шифрования, то есть ту функцию, которая теперь на месте xor_crypt
. Теперь она называется sub_403E10
. Вот ее фрагменты, по которым ты можешь понять, как работает алгоритм шифрования.
...
mov esi, offset aMicrosoftEnhan ; "Microsoft Enhanced RSA and AES Cryptogr"...
lea edi, [ebp+szProvider]
push eax ; phProv
rep movsd
call ds:CryptAcquireContextW
...
lea eax, [ebp+phHash]
push eax ; phHash
push 0 ; dwFlags
push 0 ; hKey
push 800Ch ; Algid
push [ebp+phProv] ; hProv
call ds:CryptCreateHash
...
lea eax, [ebp+phKey]
push eax ; phKey
push 0 ; dwFlags
push [ebp+phHash] ; hBaseData
push 660Eh ; Algid
push [ebp+phProv] ; hProv
call ds:CryptDeriveKey
Становится понятно, что этот шифр так просто не взломать, — это AES. Давай вернемся и поищем подсказки, например в наших новых обоях рабочего стола.
INFO
На самом деле я сразу подумал, что раз уж есть какой-то графический файл, то нужно или готовить Stegdetect, или смотреть водяные знаки. Прятать всякое в картинках сейчас модно!
Найдем картинку в директории временных файлов пользователя. Расширение у нее — tmp, а название начинается с wallp. Нужно всего лишь поменять расширение на bmp, и можно открывать в Paint. Для поиска водяных знаков бывает удобно инвертировать цвета.
Бинго! Наш пароль — это водяной знак в изображении, IMdsSqFGLf6v_wxO
. Пробуем ввести его в кракми.
Крякмис принимает этот пароль и закрывается. Но мы-то помним, что он создает во временной директории и запускает файл good_rabbit****.exe
!
Загружаем его в IDA и смотрим импорт файла.
Интересно — тут есть функции работы с сетью. Следовательно, изучим сетевую активность приложения. Можешь сделать это хоть в любимом файрволе, хоть командой netstat -a
. Понимаем, что наш файл открыл и слушает порт 1337. Что ж, давай прямо в лоб и поищем функцию WSAStartup
или номер порта в IDA.
Мы нашли WSAStartup
, она в самом начале функции под названием sub_404480
. Поднимаемся на уровень выше и смотрим, какие параметры передаются в эту функцию. Открываем первую перекрестную ссылку на нее (да-да, мы находимся в TLS, но это не имеет никакого отношения к делу :)).
А вот и номер порта в качестве параметра! Скроллим чуть ниже и видим еще назначение портов и передачу в эту функцию. Порты — 1337, 1338 и 1339. Заходим в sub_404480
и смотрим, что делает эта функция. Перед нами стандартная инициализация работы с сетью, вызовы WSAStartup
, socket
, inet_addr
, bind
, listen
и accept
. Кто писал клиент-серверные приложения, знает эту структуру как свои пять пальцев, поэтому тут все банально и мы это пропускаем. А интересный вызов у нас уже между accept
и closesocket
.
Давай заглянем в него.
В общем-то, код совсем простой, и он умещается на один скриншот. Если с самого начала мы вводим 9, то на экране показывается Y. Переходим из подпрограммы дальше, порт меняется на другой, на единицу больше. Мы вводим 3 — показывается E, опять переходим на следующий порт и вводим 5, видим S.
Эти данные нужно посылать на порт, начав с 1337 и подключаясь всякий раз к порту на единицу больше. То есть 1337 → 9, 1338 → 3, 1339 → 5. Я посылал данные на порты при помощи программы PuTTY. После этого соединение с сервером закрывается и открывается браузер с коротким видеороликом, в котором нам демонстрируется секретный флаг.
Crackme решен! 🙂