Взлом ядерного 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 «Профессиональный пентестер». Лучшие ученики вводного курса смогут продолжить обучение, получить профессию пентестера, и тогда заниматься крякми не только ради фана, но и заработка для.

    ВышТех

    38,00

    Компания

    Поделиться публикацией
    Комментарии 22
      0
      Спасибо!
      Было интересно!
        0
        Интересно, почему авторы этого crackme задействовали всю эту тему с драйверами? Если суть задачи именно в анализе хэш-функции, то для чего все эти сложности гонять данные через драйверы?
          +3
          Особенностью этого crackme является то, что его решение тренирует не только навыки реверса, но и даёт понимание, как происходит общение между юзермодным приложением и драйвером режима ядра.
          +2
          мы видим стандартный заголовок… «MZ». Также видно, что файл… написан на C++.
          А какие бывают другие заголовки?
          А как определить язык? Там осталась debug-информация?
            0
            У исполняемых файлов Windows формат заголовка один и тот же, на то он и стандартный заголовок.
            Язык программирования выдаёт совокупность признаков. С++ особенно выдаёт библиотека crt. К примеру, на точке входа вызывается функция 0004072EB инициализации канареек. Далее идёт jmp, в котором в самом низу идёт вызов функции main. Это характерные признаки для исполняемых файлов, собранных в visual studio. А если посмотреть на строки, то можно увидеть что-то типа: " delete[], vector constructor iterator", чтобы убедиться, что это C++.
                0
                Строго говоря, MZ — Это досовский заголовок 16 битного исполняемого файла. В нынешних исполняемых файлах он для совместимости) вдруг вы в досе запустите, а оно там выведет красивое «This program can not be run in DOS mode.»
                Виндовый заголовок — PE.

                Язык на 100% не определить. Но если известно, что файл после компилятора не модифицировался, можно по косвенным признакам узнать компилятор. Например секции TEXT, DATA капсом, любят компиллеры Borland. А еще более детально компилятор узнают по сигнатурам стандартных функций. (В IDA — ромашка вроде.) При нахождении более 100 системных функций из одной библиотеки сигнатур, можно с большой вероятностью сказать что оно писалось на таком-то языке.
                  0
                  Все языки обладают спецификацией и как правило моделью памяти. Уже по расположению функций, классов, структур и стэка можно достаточно ясно определить язык. Подключаемые файлы и сборки тоже обычно стандартные и как следствие используют один и тот же код, а такие дизассемблеры как IDA уже давно детектируют их на лету.
                  Компилятор это множество (под)программ, включая линкер, оптимизатор и саму генерацию кода. На ассемблере можно по разному записать некоторые действия(кто бы мог подумать?),
                  к примеру:
                  mov ax, 0;поместить 0 в регистр 'ax'
                  mul ax, 0;умножить 'ax' на 0
                  xor ax, ax; обнулить совпадающие биты из ax(совпали все => все нули: ax = 0)

                  и т.д. так же есть внутренние соглашения о вызовах.
                  Анализируя всё это можно определить даже компилятор, его версию и версию его компонентов.
                    0
                    Может быть ZM
                      0
                      Поменял MZ на ZM — Win7 не запускает такую прогу.
                      Я думал, что заголовок MZ единственный для .exe, просто предложение в статье даёт усомниться в этом.
                        0
                        В досе работало, возможно для винды в какой-то момент подкрутили.
                        MZ это инициалы какого-то польского программиста, Марека Не-помню-фамилию, который в 80 лохматом году разрабатывал формат EXE. Тогда ещё писали на асме и экономили каждый байт, и там был хитрый трюк через XOR, экономивший пару байт на сравнении. Но он пропускал как MZ, так и ZM.
                    –1

                    А почему бы было просто не подменить драйвер, что бы он всегда возвращал true?
                    Как головоломка хорошо, как система защиты — плохо.

                      0
                      Смысл головоломки в том что бы сделать кейген. Так то можно заменить один условный переход на безусловный, что бы всегда выводилось «Serial is Valid». Даже без возни с драйверами.
                      0
                      Очень полезное занятие.
                        0
                        Спасибо за разбор. Было интересно читать.
                        Было бы здорово, если бы вы опубликовали аналогичные статьи с описанием процесса исследования простых вирусов, т.к. разборов различного рода crackme и keygenme в интернете хватает, а вот живой malware analysis встречается редко.
                          0
                          спасибо, возьмем на заметку.
                          0
                          а можно ссылку для скачивания этого crackme?
                            0
                            Да, конечно: goo.gl/9QQVCV
                              0
                              спасибо
                                0
                                все таки, не работает…
                                  0
                                  А что именно не работает? На момент подготовки статьи вроде все работало.
                                    0
                                    не рабочая ссылка

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое