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

INFO

Автор статьи выражает благодарность hasherezade, автору crackme (пароль crackme).

По правилам нам нужно найти ключи и секретный флаг, а не просто поломать механизмы валидации, чтобы crackme думал, что он решен. Из инструментария нам понадобится: IDA, DiE и PuTTY. Начнем с DiE.

Crackme в DiE
Crackme в DiE

Энтропия в норме, файл ничем не упакован. Это вполне ожидаемо: накрывать кракми известными навесными упаковщиками — признак плохого тона. Поставить такую защиту может кто угодно, а ее снятие — это рутина, а не решение загадки. Давай запустим кракми и посмотрим, как он работает, что выводит на экран, как реагирует на ввод неправильного пароля.

Crackme
Crackme

При загрузке мы видим цветные строчки и забавного кролика. На ввод неправильного пароля программа пишет строчку «Nope!» и закрывается. Загрузим крякмис в дизассемблер IDA и посмотрим на начало программы, строки и структуру кода.

White Rabbit в IDA
White Rabbit в 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.

Ресурсы в DiE
Ресурсы в DiE

Открываем его в hex-представлении.

Ресурс 101
Ресурс 101

Данные ресурса зашифрованы, но обрати внимание на символическое представление 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
Дешифровка XOR

Разумеется, это функция дешифровки ресурса методом 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, чтобы иметь перед глазами пример.

Файл BMP в hex-редакторе
Файл BMP в hex-редакторе

Обратимся к документации по этому формату. Нас интересуют первые 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
Результат операции XOR

Применяем XOR и получаем фразу-пароль: follow_da_rabbitz. Вводим ее в кракми, и он сообщает нам: So far, so good! Обои рабочего стола меняются на черный фон с изображением кролика и предложением сохранять спокойствие и следовать за кроликом. Кракми тем временем просит нас ввести второй пароль.

Новые обои рабочего стола
Новые обои рабочего стола

Возвращаемся в IDA и исследуем следующий уровень. По структуре он очень похож на предыдущий. Поскольку мы уже дали функциям более понятные названия, все становится еще проще. Кроме того, мы понимаем, что в этот раз после ввода корректного пароля также создается файл с ресурсом, который имеет расширение exe. Это интересно! Давай взглянем на этот ресурс в DiE — может, предыдущий трюк нам поможет и здесь? 🙂

Второй ресурс в DiE
Второй ресурс в 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
Нашли WSAStartup

Мы нашли 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 решен! 🙂

3 комментария

  1. Аватар

    robotobor

    28.09.2018 в 10:26

  2. Аватар

    keeper-volok

    28.09.2018 в 12:57

    Занимательный крякми, спасибо )

  3. Аватар

    Sancio

    17.05.2020 в 15:02

Оставить мнение