Хабрахабр

Взлом ядерного Crackme

Привет, Хабралюди!

Сам процесс решения задачек на взломы особенно приятен, а когда есть решение – приятно вдвойне. Сегодня мы решили разобрать крякми, который попался нам на конференции ZeroNights в ноябре, где наша команда из школы кибербеза и ИТ HackerU дебютировала и сразу выдебютировала заняла первое место в hardware challenge. Решение crackme «SHADOW» пригодится тем, кто увлекается реверс-инжинирингом.

Для крякми этого уровня достаточно знать ассемблер и иметь базовое представление об устройстве драйверов под Windows.

Беглый анализ

У нас есть файл CrackmeZN17.exe. Для начала проведём его поверхностный осмотр в HIEW. Это даст нам общую информацию об образце. Открыв файл в hiew, мы видим стандартный заголовок исполняемого файла Windows, который начинается с букв «MZ». Также видно, что файл не упакован (из-за наличия большого количества пустых мест) и написан на C++. А если файл упакован, то это значит, что упаковщик постарается минимизировать все повторяющиеся байты. Таким образом, у упакованных файлов наблюдается повышенная энтропия.

Теперь жмем ALT+F6 и переходим в режим строк. Так мы видим только те байты, которые относятся к печатаемым символам. Но наша задача не рассматривать все строчки, а окинуть их взглядом и найти какую-нибудь зацепку. Это, может, и очевидно, но кучу полезной нформации можно достать, всего лишь просматривая строки: начиная с автора программы и заканчивая признанием файла вредоносным с вынесением точного вердикта (torjan-psw, trojan-ransom и т.д.)! Листаем файл в HIEW чуть ниже – и сразу видим кое-что интересное – строчки

«error: Can not extract driver files!», «error: Can not extract driver files! Password:
Serial is Valid!» и «error: Can not extract driver files!
Password:
Serial is Valid!
Serial is not Valid»:

Дальше – больше: по смещениям 0x5B8D и 0x63E4 мы видим заголовки ещё двух исполняемых файлов:

Это видно по тем же буквам «MZ».

Если пролистать в конец, выясняется, что программа требует права администратора при запуске, так как содержит в себе манифест:

На этом можно закончить визуальный анализ. Мы уже смогли понять следующее:

·         исполняемый файл CrackmeZN17.exe не упакован;
·         он написан на C++;
·         при запуске файл будет требовать права администратора;
·         и он содержит в себе ещё два исполняемых файла.

Небеглый анализ

Что ж, запустим этот crackme и попробуем поиграться с ним:

После запуска crackme в его директории появилось ещё два файла: CrackmeZN17.sys и CrackmeZN17_.sys. Теперь становится ясно, зачем нужны права администратора: они нужны для подгрузки драйвера, который в противном случае попросту не загрузится и почему мы в HIEW увидели заголовки ещё двух исполняемых файлов, начинающихся с байтов «MZ». Это были те самые драйвера, которые извлеклись при запуске crackme.

Ну ок, с этим всё понятно. Давайте дальше найдём место, где происходит проверка серийника. Откроем CrackmeZN17.exe в IDA. Жмём Shift+F12 и переходим в режим просмотра строк. Да-да, подобное уже было сделано в HIEW, но там мы производили лишь беглый анализ, а для более глубокого IDA подойдёт больше. И вот мы видим уже знакомые нам строки:

Теперь неплохо бы определить, в какой именно функции используются строки. Это выдаст нам функцию, где реализована проверка правильности ввода. Для этого переходим по кросс-ссылке строки «Serial is Valid» (жмём «Ctrl+X») и понимаем, что самой логики проверки серийника в файле CrackmeZN17.exe нет! Почему так?

А всё потому, что пара считается валидной только тогда, когда функция WinApi вернёт нам True. Что теперь? Копаем дальше. Мы видим, что посылается IRP запрос с IOCTL-кодом 22200Ch.

Используя функцию DeviceIoControl, можно добиться того, что диспетчер ввода вывода сформирует и заполнит IRP-пакет нужными нам данными и отправит его устройству. Устройство обычно создаётся самим драйвером при его загрузке в функции DriverEntry. А чтобы с ним можно было бы обращаться, как с обычным файлом (например, читать и писать),  создаётся символьная ссылка на это устройство. Символьная ссылка обычно создаётся также при загрузке драйвера в тойже DriverEntry функции. На самом деле, тут довольно много теории, и для решения этого задания необходимо базовое понимание принципов работы драйверов режима ядра. В этом разборе осветить это подробнее не получится, оставим как тему для отдельного обсуждения.

В итоге логика получается следующей: crackme дропает на диск два драйвера и затем загружает их. Один из драйверов создаёт некое устройство, которое затем принимает введённую пару логин-пароль. Последнюю устройство получает из IRP пакета, который формируется диспетчером ввода-вывода по запросу функции DeviceIoControl. Далее IRP-запрос обрабатывается функцией диспетчеризации, которая задаётся в DeviceIoControl.Эта функция будет ловить IRP-пакеты, отправленные устройству и обрабатывать только те, которые имеют интересующие её IOCTL-коды. Чем-то это напоминает процедуру обработки оконных сообщений.

В нашем случае интересным IOCTL-кодом будет – 0x22200C. Если запрос ввода-вывода завершается успешно, то DeviceIoControl вернёт нам True. Поэтому для решения crackme нам нужно найти функцию диспетчеризации.

Теперь нам нужно понять, какому именно устройству отправляется введённая пара. Давайте поставим точку останова на вызов функции CreateFileA по адресу 0x402591 и посмотрим, какому устройству планируется отправление IRP-пакета. После остановки мы видим в esi-регистре указатель на такую строку: «\\.\CrackmeZN17». И вот эта строка как раз и является символьной ссылкой на устройство, которое обслуживает один из двух наших драйверов. Какой именно – CrackmeZN17.sys  или CrackmeZN17_.sys – можно  понять, быстренько посмотрев эти файлы в HIEW. Для начала откроем CrackmeZN17.sys. Переходим в режим просмотра строк – ALT+F6 и видим вот это:  

Следовательно, за обслуживание устройства «CrackmeZN17» отвечает драйвер CrackmeZN17.sys. Ему и посылается IRP-пакет. Поэтому следующим шагом будет реверс именно этого драйвера.

Реверс CrackmeZN17.sys

Открываем файлик в IDA. Находим в нём функцию диспетчеризации. У нас это sub_104F8. Эта функция очень простая:  

Посмотрим функцию, которая выполняется в случае, если sub_10F60 возвращает 0.

Теперь посмотрим функцию, которая вызывается в противном случае:

Теперь все более менее понятно: функцию sub_10F60 можно переименовать в check. В случае правильного ввода она должна вернуть 1. Теперь нужно разобраться в том, какие именно параметры передаются в эту функцию. Для этого нам нужно подробное описание структуры IRP пакета. Но прежде стоит определить тип метода ввода-вывода – от этого будут зависеть нужные нам смещения внутри структуры. Определить метод ввода-вывода можно по IOCTL-коду (вы-то уже догадались, что определить тип ввода-вывода можно было и по юзермодному приложению? ). Мы для этого использовали плагин decoder, который можно взять тут. Вот что получилось:

Осталось только сопоставить смещения внутри структуры IRP. Детальное описание структуры можно получить, воспользовавшись отладчиком ядра WinDbg. В данной функции первым делом извлекается из IRP-пакета указатель на структуру _IO_STACK_LOCATION. Она нужна для того, чтобы прочитать IOCTL-код. Если он равен 22200Ch, значит, пакет наш и его можно обработать. Если пакет наш, то из него следует получить данные, которые нам передаются из режима пользователя. С учётом того, что метод передачи — METHOD_BUFFERED, данные нам могут быть переданы как во входном, так и в выходном буферах. При записи, диспетчер ввода-вывода выделяет кусок памяти в неподкачиваемом системном пуле, а затем копирует туда пользовательские данные. Адрес выделенной памяти хранится в поле SystemBuffer. Таким образом, с учётом того, в каком порядке были переданы логин и пароль в функции DeviceIoControl в CrackmeZN17.exe, получается вот что:

Дело осталось за малым – разревёрсить функцию check (sub_10F60). Интерес для нас представляет функция sub_10EE2, подфункция которой выглядит так:

Сразу же можно предположить, что функция sub_10EE2 с большой долей вероятности производит расчёт MD5-хеша. Это видно по константам. Забегая вперёд, скажу, что так оно и окажется. Так что давайте её переименуем в «GetMd5». После подсчёта хеша, полученное значение передаётся в sub_10EA2. Функция выглядит вот так:

На первый взгляд тут непонятно, что происходит, но на самом деле, всё просто. Ко всем символам, кроме «’.’,’@’» применяется логическое ИЛИ с 0x20. Так реализуется быстрый перевод латинской буквы в нижний регистр. Вот так:

Соответственно, противоположная операция – логическое И с 0x5F.

То есть функция sub_10EA2 понижает регистр латинских букв, поэтому переименуем её в «toLow». Но такой способ не будет работать для кириллических букв. Почему тут нет проверки на язык ввода, станет понятно дальше. В итоге, функция check становится похожей на нечто ниже:

После того, как выполнилась функция toLow, если в хеше первый символ – буква, то она переводится в верхний регистр. От полученного результата снова считается MD5-хеш, и указатель на результат помещается в массив P. Количество элементов в массиве P – 32 (это видно из условия окончания цикла – 31 строка). После этого, MD5 в последней итерации сравнивается с введёнными данными. Если они совпадали, то – вуаля! – пара логин-пароль – валидная!

Итак, подытожим алгоритм генерации серийника:

1)      считаем MD5-хеш от логина и переводим его в символьный вид;
2)      все большие буквы в хеше становятся маленькими;
3)      если хеш начинается с буквы, то она становится большой;
4)      MD5- хеш от полученной строки считается 32 раза. Последний раз выдаст нам правильный пароль.

Реверс драйвера CrackmeZN17_.sys

Но не торопитесь радоваться! Если вы реализуете этот алгоритм и отправите валидную пару с логином и паролем, то получите ответ, что серийник неверный. Почему так? Всё дело в том, что мы совсем забыли, что у нас два драйвера. Зачем же тогда используется второй? Давайте откроем его в IDA и посмотрим, что он делает.

Важно: тут драйвер не создаёт символьную ссылку на своё устройство. А судя по вызову функции IoAttachDeviceToDeviceStack, можно смело утверждать, что наш драйвер – драйвер-фильтр!

Этот драйвер будет первый получать все IRP-пакеты, отправляемые устройству CrackmeZN17. Поэтому не исключено, что по пути он их будет модифицировать. Нас интересует функция диспетчеризации запросов – sub_10462. Открываем ее и наблюдаем интересную картину:

Если кто-то инициировал передачу IRP-пакета с IOCTL-кодом 22200Ch устройству  CrackmeZN17, то мы его тут и отловим. Из пакета достаются присланные данные и подаются на вход функции sub_105B2. А эта функция как раз и занимается проверкой допустимого ввода. Посмотрим вот на эту подфункцию и сразу убедимся в этом:

Если строка с логином или паролем содержит в себе какие-нибудь другие символы, то вызывается sub_10438, которая завершит обработку IRP-пакета с ошибкой — STATUS_INVALID_PARAMETER.

Таким образом, драйвер-фильтр пропускает только те IRP-пакеты, которые содержат корректные данные. Вот почему в предыдущем драйвере не было никаких проверок, например, на язык алфавита. Если все условия выполнены, то для  логина вызывается ключевая функция sub_105F8, а для пароля — sub_10640. Функцию sub_10640 мы уже видели в предыдущем драйвере. Назовём её также «toLow».

Рассмотрим пока sub_105F8.

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

Только после этого изменённый IRP-пакет передаётся на дальнейшее обслуживание следующему устройству вызовом IoCallDriver. Учитывая это, можно написать keyGen и полностью решить этот крякми. В нашем случае кейген будет таким:

import sys
import hashlib def is_hex_number(str):
try: arr1 = int("".join(str), 16) return True
except ValueError: return False def getLogin(login): result = "" j = 0 for i in login: if j & 1 == 0: result = result + i.lower() else: result = result + i.upper() j = j+1
return result def getPass(login):
m = hashlib.md5()
m.update(login)
tmp = m.hexdigest()
login = tmp
result = ""
i = 0 while i < 32: login = login.lower() if ord(login[i]) <= ord('z') and ord(login[i]) >= ord('a'): login = login[:i] + chr(ord(login[i]) & 0xDF) + login[i+1:] m = hashlib.md5() m.update(login) tmp = m.hexdigest() #print tmp result = result + tmp[i] i = i+1 return result def keyGen(argv):
email = argv[1] #filter changed
login = getLogin(email) flag = getPass(login) return flag def main(argv):
try:
print keyGen(argv)
except: print('Usage: keygen <login>') if __name__ == "__main__": main(sys.argv)

Мишн аккомплишд! На решение этой задачки у нас ушло 3 часа, но это не точно.

Кроме пробы своих сил в реверсе есть много других интересных челленджей. К примеру, поиск и эксплуатация уязвимостей в веб-приложениях. Решение crackme позволяет прокачать навыки реверса, без которых не обойтись ни одному вирусному аналитику или security-ресечеру. Также крякми даёт понимание об устройстве исследуемой программы или операционной системы на самом низком уровне, а это часто необходимо в системном программировании.


Что ж, мы дольно быстро расправились с этим крякми, но, признаться, до этого мы много тренировались. Крякми – это добротный тренажер для пентестера, поэтому будущим уйатхетам придется решить десятки задачек, чтобы получить специализацию. У нас как раз идет набор на очный 9-месячный курс нашей московской школы HackerU «Профессиональный пентестер». Лучшие ученики вводного курса смогут продолжить обучение, получить профессию пентестера, и тогда заниматься крякми не только ради фана, но и заработка для.

Теги
Показать больше

Похожие статьи

Кнопка «Наверх»
Закрыть