Вернёмся к первой части и вспомним, о чём тут вообще идёт речь..
Статья рассматривает, как при запуске приложения передать бразды фиктивной точке-входа EntryPoint. При этом РЕ-заголовок дискового файла не модифицируется, что пускает по ложному следу софт реверс-инженеринга типа анализаторы, дизассемблеры и отладчики. Акцент сделан на то, чтобы TLS-callback процедурой вмешаться в работу загрузчика LDR и перехватить точку-входа именно в стеке, а не в РЕ-заголовке файла, как это делают многие.
Загрузчик использует стек программы в качестве пусковой установки. Координаты цели в виде точки-входа, он берёт из нашего заголовка по смещению PE.28h, и дальше функцией ZwContinue() передаёт на неё управление. Наша задача как диверсантов выбрать подходящий момент и пока функция ZwContinue() ждёт от загрузчика команды "пуск", подставить в стек левый EntryPoint, захватив таким образом цель в свою пользу. Что примечательно – мастдай не может этому противостоять, поскольку стек всегда доступен на запись.
Но в теории всё просто, зато на практике – это минное поле. От любого неверного шага сразу срабатывает ревун, и сопровождаемые прикладом автоматов системной охраны, мы отправляемся на допрос к диспетчеру исключений. Значит не нужно никому доверять, а принимать решения только по обстоятельствам, для чего осуществляем поиск точки-входа в стеке перебором, по её сигнатуре из поля РЕ.28h. На этом месте, в первой части и была поставлена жирная точка.
В этой части..
Будем считать, что изменили точку-входа в программу, что дальше?
Теперь нужно придумать, на какой адрес и зачем передавать управление. Здесь самое время подключить фантазию, которую ограничивает только наш опыт в программировании скилл. Например, если хотим защитить прожку от отладчиков типа OllyDbg, нужно передать управление на участок кода, который до оригинальной точки будет искать Олю и одним из способов признаваться ей в "любви". Если-же мы пытаемся скрыть свою тушку от дизассемблеров (что гораздо трудней), то выход один – шифровать всё, или только критически-важный участок тела.
Чтобы не разделять читателей на два этих лагеря, мы устроим файлу полный тюннинг, припарковав к нему защиту и от отладки, и от дизасма. Программа будет запрашивать пароль, и по результатам проверки выводить или "OK", или посылать нас.. нет не туда, а читать книжки по реверсу. Условимся, что точку-входа будем сбрасывать в нуль, в результате чего управление примет ImageBase. Тогда алгоритм программы может быть примерно таким:
Значит если мы обнаруживаем факт отладки, то прямо из коллбэка передаём управление на оригинальную точку-входа, пропуская процесс расшифровки критически-важного участка кода с проверкой пароля. В результате отладчик нарвётся на зашифрованный код и будет пыхтель над ним, вплоть до какого-нибудь исключения. Зато если отладчика нет, то по фиктивной точке-входа, управление примет декриптор в ImageBase, который расшифрует процедуру проверки пароля, и она сделает своё благое дело.
Скажу сразу, что данный алгоритм далёк от идеала и представлен чисто для демонстрации различных техник. В реальных программах, на этом этапе обычно запускают трилогию математических манипуляций с различными указателями, накрывают код протектором и прочее. Задачей максимум в них является как-можно эффективнее сбить взломщика с толку, играя без каких-либо правил.
ImageBase как EntryPoint
Откроем наш crackme в любом хек-редакторе (у меня HxD) и посмотрим на его облик..
Зайдя в будуар базы, сразу натыкаемся на 40h-байтный DOS-заголовок, за которым следует заглушка Stub, озадаченая выводом мессаги "Этот файл не для MS-DOS". Всё это досовское барахло можно смело затереть нулями и файл от этого никак не пострадает. Освободившееся пространство будем использовать для своей фиктивной точки-входа.
Правда нужно оставить два важных поля, без которых исполняемый файл просто не запустится – это сигнатура Марка Збиковски "MZ" по-смещению Base+00, и указатель на РЕ-заголовок по-смещению Base+3Ch. Рисунок ниже демонстрирует сказанное:
После кастрации дос-заголовка пробуем запустить файл на исполнение – нормально запускается и мы освободили пространство для своего декриптора. Анализаторы не суют свой нос в эту область файла, считая её пережитком прошлого – значит пока всё идёт по-плану и мы нигде не засветились.. едем дальше..
Если управление принимает ImageBase, то процессор сразу наткнётся на пару байт 4D5Ah (см.рис.выше), после которых ровным строем идёт болото нулей. Можно-ли трактовать значение 4D5Ah как инструкцию процессора, или эта пара вызовет исключение? Чтобы разобраться, загружаем файл-кастрат в отладчик и комбинацией Ctrl+G переходим на адрес ImageBase, который в данном случае равен 0х00400000 (дефолтная база пользовательских программ):
Так-так.. в верхнем окне видим вполне нормальные инструкции DEC_EBP и POP_EDX, которые на самом деле являются сигнатурой "MZ". По умолчанию это данные, но мы сделали из них код. Ещё один важный момент – это сбитый на входе стек. Чтобы выровнить его после POP_EDX, "запушим" этот EDX обратно с инкрементом EBP, и тогда наша совесть будет чиста.
Теперь, в диапазон от сигнатуры MZ и до указателя на РЕ-заголовок нужно повесить декриптор, который будет расшифровывать процедуру проверки пароля в секции-кода нашей программы. Значит нужно написать сначала саму процедуру пароля, и к её адресу потом цеплять декриптор. Здесь есть несколько вариантов и для наглядности я выбрал самый простой – вот пример:
Здесь видно, что правильный пасс и выводимые строки хранятся внутри зашифрованного участка кода, что позволило скрыть их от посторонних глаз. Адрес валидного пароля мы получаем через инструкцию CALL, которая перед тем как передать управление на свой операнд, сохраняет в стеке адрес-возврата для заканчивающей вызов, инструкции RET.
Так сложились звёзды, что адрес-возврата будет указывать как-раз на начало строки с валидным пассвордом, поэтому мы снимает его со-стека в регитр EDI и используем как неявный второй операнд для инструкции сравнения строк CMPSB (она сравнивает esi с edi по длинне ecx). Такую технику вычисления адресов назвали "получением дельта-смещения" и она широко используется малварью всех поколений:
Теперь у нас есть "процедура проверки пароля" и нужно прицепить к ней декриптор (саму проверку шифровать будем позже). Поскольку декриптор придётся переносить в область ImageBase, оформим его как shell. Для этого просто поместим код декриптора в секцию-данных нашей программы и компилятор ассемблера сам выстроит из него цепочку байт с опкодами инструкций. Дальше хек-редактором вырежем эту цепочку из .data-секции, и поместим её по адресу ImageBase.
Здесь нужно учитывать, что в секции-данных есть и полезные переменные со-строками, для которых трассер компилятора уже сформировал адреса. Поэтому код декриптора кидаем в самый конец секции, что позволит безболезненно для окружающих, потом изъять его от туда. Значит добавляем в секцию-данных предыдущего кода такие строки:
Разберём детали этого кода...
Значит в нижнем окне секции-данных, среди текстовых строк видим маркеры начала и конца шелла, в виде цепочки байт со-значением 0хFF – это чтобы визуально выделить полезную нагрузку Payload. Сам шелл в красном блоке занимает всего 1Fh байт и вполне влезет в пустую область ImageBase, где мы приготовили для него место. Можно было оптимизировать код, например убрав из четвёртой инструкции 0хВ960000000 три байта нулей в хвосте, но в демонстрационном примере это не важно – у нас в ImageBase места предостаточно.
Интересным моментом являются опкоды переходов, например той-же инструкции цикла LOOP (см.верхнее окно). На этапе компиляции, вычисляющий адреса трассировщик ассемблера подставляет в код не абсолютный, а относительный адрес – в данном случае это опкод 0xE2F6. Второй байт со-значением 0xF6 это и есть адрес относительно текущего – он кодируется одним знаковым байтом. Диапазон этого байта от нуля и до 0x7F означает переход "вперёд", а верхняя половина от 0xFF и до 0х80 – это переход "назад" (на указанное кол-во байт). Здесь мы видим значение 0xF6, значит это
Другими словами, если шелл-код использует переходы в пределах 127 байт (0х80), то компилятор вставит относительный адрес – значит после компиляции шелл можно вырезать и переместить в любое место. В противном случае (переход за пределы 127 байт), компилятор вставит уже абсолютный адрес, и двигать такой шелл со-своей позиции нельзя – вся конструкция рухнет. Если нужны дальние FAR-переходы, то можно использовать стек с последующим RET, что я и сделал в своём шелл-коде на выходе, указавав ему переход на метку "start" (оригинальная точка-входа). Это нужно учитывать при зачатии шелл-кодов.
Ну и в первых трёх инструкциях, шелл жонглирует регистрами – что это и зачем?
Выше упоминалось, что сигнатура "MZ" в виде слова 0х4D5A мутирует в инструкции DEC_EBP и POP_EDX. Поэтому для отвода глаз сначала я меняю местами EDX и EAX, после чего восстанавливаю EBP и отправляю EAX (в лице регистра edx) на своё место. На этом этапе стек держится на "честном слове", и чтобы не засветиться сторожам, необходимо соблюдать его баланс.
Инкапсуляция шелл-кода в ImageBase
У нас есть готовый шелл и теперь нужно переместить его из секции-данных в ImageBase, т.е. в заголовок DOS-Header. Для этого открываем "crackme " в хек-редакторе HxD и ищем в нём шелл-код по маркерам начала и конца с цепочкой байт 0хFF. Упс, есть такой – выделяем и копируем его в буфер, а оригинал забиваем нулями. Теперь переходим в самое начало файла, и выделив после сигнатуры "MZ" 1Fh-байт (размер в опкодах), вставляем шелл из буфера по Ctrl+V. Обязательно нужно выделить блок и потом вставить из буфа, иначе указатель на РЕ по смещению Base+0х3С съедет со-своей позиции. Должно получиться примерно так:
Как-видим, выделенный шелл не занял даже и четверти свободного пространства в дос-заголовке. Можно было напичкать его ничего не делающими мусорными инструкциями (обфускация), вставляя их между полезным кодом – это значительно затруднило-бы анализ приложения взломщиком, хотя в демо-примере окончательно сбило-бы нас с толку. Так-что будем считать этот crackme скелетом, который в последующем можно будет усовершенствовать. Тут главное понять суть, а детали реализации могут быть у каждого свои.
Основная Callback-процедура для перехвата EntryPoint
Если сейчас запустить файл, он отработает как положено, т.е. запросит пароль и вернёт ответ. Оригинальная точка-входа ещё не перехвачена, а декриптор хоть и есть, он не получает управления. Теперь наступает самый ответственный момент, от которого зависит весь последующий алгоритм программы. Callback исполняется внутри загрузчика, поэтому нужно быть предельно аккуратным, чтобы не нарушить его работу.
Во-первых, на входе в Callback нужно обязательно сохранять все регистры инструкцией PUSHA, и восстановив их на выходе через глупый POPA, возвратить загрузчику в регистре EAX значение 1. Только в этом случае наш план по перехвату оригинальной EnrtyPoint возымеет успех, и своей функцией NtContinue() загрузчик проглотит наживку. Ситуацию нагнетает и то, что на момент работы Callback-процедуры обработчик структурных исключений SEH ещё не оформлен, поэтому если мы сделаем что-то неправильно, то даже не узнаем об этом – наша прожка тупо провалится в чёрную дыру, без каких-либо намёков на исключение.
Смысл процедуры Callback в том, чтобы получив адрес оригинальной точки-входа, найти его на дне стека и изменить на ImageBase. Для этого берём из поля РЕ.28h относительный RVA адрес точки, и отнимаем его от найденного значения в стеке. В результате получим базу образа, которую и подхватит функия NtContinue(), передавая управление на наш декриптор. После того как декриптор расшифрует уязвимый участок кода с проверкой пароля, он сам передаст управление на Original EntryPoint (OEP) и программа продолжит уже работу по заданному алго.
Тут есть одна проблема – запрет на запись в секцию-кода! Дело в том, что в процессе расшифровки декриптор должен будет ксорить и перезаписывать каждое слово кода, значит нам нужно внутри процедуры Callback снять эту защиту функцией VirtualProtect(), с аргументом "PAGE_EXECUTE_READWRITE" равным 0х40. Кроме этого аргумента, функция требует ещё и адреса блока (в нашем случае секция-кода), её размер 0х1000 или одна страница, и указатель на переменную для текущих флагов. Эта функция BOOL, так-что возвращает нуль в случае ошибки.
В примере ниже, коллбэк ищет ещё и отладчик по системному флагу NtGlobalFlag, и если обнаружит его, то сразу выходит из Callback'a. В результате, точка-входа в стеке оказывается не перехваченной и декриптор управление не получает. Тогда в отладчике мы пароль-то введём, а вот его проверку осуществить не сможем, т.к. блок останется не расшифрованным. Вот сама процедура, как рождественский гусь напичканная комментариями:
Обратитте внимание, что Callback не передаёт управление на фиктивную точку по адресу ImageBase. Мы только поменяли ОЕР в стеке инструкцией
Шифрование процедуры проверки пароля
Теперь наш декриптор принимает управление и ксорит все слова уязвимого блока ключом 3e97h в надежде, что эта процедура зашифрована. Однако на данном этапе мы ещё не шифровали её, и декриптор вместо расшифровки наоборот шифрует блок. Ручное шифрование интересно, но требует особой внимательности. Так-что вынимаем мозги из штанов и приступаем к шифрованию критически-важного участка кода.
В качестве инструмента, выберем необычайно богатый на возможности хек-редактор HIEW. В его папке есть файл "hiew_ru", где можно найти вполне понятный хелп по крипту бинарного кода. На подготовительном этапе нужно собрать необходимую информацию, в виде начального адреса и длинны шифруемого блока. Посмотрим, чем занимается наш декриптор:
Здесь видно, что длинна блока в байтах заносится в регистр ECX, но поскольку мы собираемся ксорить словами по 2 байта, то операцией сдвига вправо, ECX делится на 2. Остаток от деления нас не интересует, в результате чего кол-во итераций цикла в любом случае будет чётным. Теперь загрузим crackme в отладчик, и посмотрим, что лежит в регистре ECX нашего шелл-кода:
Красным по белому мы видим здесь значение 0х30, что в десятичном будет 48. Адрес начала блока равен 0х402021 – запишем эти два значения на бумажке. Теперь вскармливаем crackme чудо-редактору Hiew и по энтеру выбрав режим дизассемблера, приступаем к шифрованию блока "проверки пароля".
Для начала жмём Goto (F5) и предварив точкой, вводим адрес начала блока .402021 – у меня это выглядит так:
Теперь жмём Edit (F3) и в меню появляется Crypt (F7), выбрав который видим окно, в где нам предлагают определиться с размером ксора на каждом шаге (1,2,4,8 байт) и задать алгоритм шифрования. Поскольку наш декриптор расшифровывает словами, то по F2 обязательно задаём размер Word:
В независимости от операнда инструкции XOR в нашем коде, для Hiew'a это всегда регистр EAX, причём переходы LOOP, JMP и прочии он привязывает не к адресу, а к номеру строки в своём окне. Двигать указатель на следующее слово не ;нужно – LOOP во второй строке сам прибавит два. Скрин ниже показывает детали:
Что интересно, тулза не может отменять предыдущую операцию и если вы сделали что-то не так, то придётся перезагрузить Hiew. Если ошибок нет, то жмём Esc и приступаем непосредственно к шифрованию, для чего тискаем всё ту-же клавишу Crypt (F7). Каждое зашифрованное слово выделяется жёлтым цветом, и нам нужно повторить операцию 48 раз (длинна блока в словах = 30h или 48).
В демо-версии Hiew'a есть ограничение (а может и не только в демке) – как только мы дойдём до конца окна, ксор встаёт как вкопанный. Нужно будет запомнить кол-во уже проксоренных слов, и сохранить текущее состояние клавишей Update (F9). Теперь прокручиваем окно вверх и продолжаем шифровать комбинацией F3->F7 опять до видимой части окна. Алгоритм каждый раз задавать не нужно – просто обновляем по F9->F3 и едем дальше по F7, пока не простучим по ней (в данном случае) 48 раз. Это напрягает, но другого выхода вроде нет. Пример представлен ниже:
Эпилог..
После всех манипуляций, наш фрегат может отправиться в свободное плавание, и никакие препятствия в виде отладчиков или дизассемблеров ему не страшны. Ручное шифрование требует терпения, которое с лихвой оправдывается результатом. Ясно, что это лишь учебный пример который ни на что не претендует. Но идея была в том, чтобы показать технику перехвата точки-входа в приложения так, чтобы никто не заметил подвоха.
У анализаторов исполняемых файлов есть такой параметр как "энтропия кода". Этот параметр по 10-бальной шкале определяет меру беспорядка в коде. Так, если код зашифрован весь, то его энтропия может достигать значения (7) – у накрытых протектором она ещё больше. Поскольку я зашифровал только малую часть своего кода, то этот показатель у меня 4.72, а у секции-данных вообще около двух. Таким способом удалось обвести вокруг пальца поле EntryPoint, в котором гордо красуется RVA 0x2000:
Готовый исходник и скомпилированный к употреблению файл цепляю в скрепке. После компиляции его в fasm'e, нужно будет только по указанной методике перенести декриптор в заголовок DOS-Header, и зашифровать уязвимый блок "проверки пароля" ключом, которым расшифровывает декриптор. Чтобы проверить свои силы в реверсе, можете вскормить отладчику готовый экхешник и посмотреть на его реакцию – уверяю, будет интересно. Из всех
В следующий раз попробуем написать код, который будет трассировать сам-себя. Это ещё один анти-дебаг, который отладчиками практически не ловится. Зоопарк антиотладочных приёмов поражает своим разнообразием, и желающий защитить свои программы кодер должен иметь в запазухе десяток из них.
Статья рассматривает, как при запуске приложения передать бразды фиктивной точке-входа EntryPoint. При этом РЕ-заголовок дискового файла не модифицируется, что пускает по ложному следу софт реверс-инженеринга типа анализаторы, дизассемблеры и отладчики. Акцент сделан на то, чтобы TLS-callback процедурой вмешаться в работу загрузчика LDR и перехватить точку-входа именно в стеке, а не в РЕ-заголовке файла, как это делают многие.
Загрузчик использует стек программы в качестве пусковой установки. Координаты цели в виде точки-входа, он берёт из нашего заголовка по смещению PE.28h, и дальше функцией ZwContinue() передаёт на неё управление. Наша задача как диверсантов выбрать подходящий момент и пока функция ZwContinue() ждёт от загрузчика команды "пуск", подставить в стек левый EntryPoint, захватив таким образом цель в свою пользу. Что примечательно – мастдай не может этому противостоять, поскольку стек всегда доступен на запись.
Но в теории всё просто, зато на практике – это минное поле. От любого неверного шага сразу срабатывает ревун, и сопровождаемые прикладом автоматов системной охраны, мы отправляемся на допрос к диспетчеру исключений. Значит не нужно никому доверять, а принимать решения только по обстоятельствам, для чего осуществляем поиск точки-входа в стеке перебором, по её сигнатуре из поля РЕ.28h. На этом месте, в первой части и была поставлена жирная точка.
В этой части..
Будем считать, что изменили точку-входа в программу, что дальше?
Теперь нужно придумать, на какой адрес и зачем передавать управление. Здесь самое время подключить фантазию, которую ограничивает только наш опыт в программировании скилл. Например, если хотим защитить прожку от отладчиков типа OllyDbg, нужно передать управление на участок кода, который до оригинальной точки будет искать Олю и одним из способов признаваться ей в "любви". Если-же мы пытаемся скрыть свою тушку от дизассемблеров (что гораздо трудней), то выход один – шифровать всё, или только критически-важный участок тела.
Чтобы не разделять читателей на два этих лагеря, мы устроим файлу полный тюннинг, припарковав к нему защиту и от отладки, и от дизасма. Программа будет запрашивать пароль, и по результатам проверки выводить или "OK", или посылать нас.. нет не туда, а читать книжки по реверсу. Условимся, что точку-входа будем сбрасывать в нуль, в результате чего управление примет ImageBase. Тогда алгоритм программы может быть примерно таким:
- Зашифровать процедуру проверки пароля
- Поместить декриптор процедуры (1) в ImageBase
- Создать TLS-callback функцию в коде
- Перехватить в стеке оригинальную точку-входа Callback'ом
- Фиктивной точкой обозначить ImageBase
- Проверить этим-же Callback'ом наличие отладчика
- Если обнаружим отладчик, передаём управление сразу на Original ePoint (OEP)
- Если отладчика нет, то расшифровываем процедуру (1) и передаём на неё управление.
Значит если мы обнаруживаем факт отладки, то прямо из коллбэка передаём управление на оригинальную точку-входа, пропуская процесс расшифровки критически-важного участка кода с проверкой пароля. В результате отладчик нарвётся на зашифрованный код и будет пыхтель над ним, вплоть до какого-нибудь исключения. Зато если отладчика нет, то по фиктивной точке-входа, управление примет декриптор в ImageBase, который расшифрует процедуру проверки пароля, и она сделает своё благое дело.
Скажу сразу, что данный алгоритм далёк от идеала и представлен чисто для демонстрации различных техник. В реальных программах, на этом этапе обычно запускают трилогию математических манипуляций с различными указателями, накрывают код протектором и прочее. Задачей максимум в них является как-можно эффективнее сбить взломщика с толку, играя без каких-либо правил.
ImageBase как EntryPoint
Откроем наш crackme в любом хек-редакторе (у меня HxD) и посмотрим на его облик..
Зайдя в будуар базы, сразу натыкаемся на 40h-байтный DOS-заголовок, за которым следует заглушка Stub, озадаченая выводом мессаги "Этот файл не для MS-DOS". Всё это досовское барахло можно смело затереть нулями и файл от этого никак не пострадает. Освободившееся пространство будем использовать для своей фиктивной точки-входа.
Правда нужно оставить два важных поля, без которых исполняемый файл просто не запустится – это сигнатура Марка Збиковски "MZ" по-смещению Base+00, и указатель на РЕ-заголовок по-смещению Base+3Ch. Рисунок ниже демонстрирует сказанное:
После кастрации дос-заголовка пробуем запустить файл на исполнение – нормально запускается и мы освободили пространство для своего декриптора. Анализаторы не суют свой нос в эту область файла, считая её пережитком прошлого – значит пока всё идёт по-плану и мы нигде не засветились.. едем дальше..
Если управление принимает ImageBase, то процессор сразу наткнётся на пару байт 4D5Ah (см.рис.выше), после которых ровным строем идёт болото нулей. Можно-ли трактовать значение 4D5Ah как инструкцию процессора, или эта пара вызовет исключение? Чтобы разобраться, загружаем файл-кастрат в отладчик и комбинацией Ctrl+G переходим на адрес ImageBase, который в данном случае равен 0х00400000 (дефолтная база пользовательских программ):
Так-так.. в верхнем окне видим вполне нормальные инструкции DEC_EBP и POP_EDX, которые на самом деле являются сигнатурой "MZ". По умолчанию это данные, но мы сделали из них код. Ещё один важный момент – это сбитый на входе стек. Чтобы выровнить его после POP_EDX, "запушим" этот EDX обратно с инкрементом EBP, и тогда наша совесть будет чиста.
Теперь, в диапазон от сигнатуры MZ и до указателя на РЕ-заголовок нужно повесить декриптор, который будет расшифровывать процедуру проверки пароля в секции-кода нашей программы. Значит нужно написать сначала саму процедуру пароля, и к её адресу потом цеплять декриптор. Здесь есть несколько вариантов и для наглядности я выбрал самый простой – вот пример:
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//------
.data
capt db 13,10,' Demo crackme v0.1'
db 13,10,' *********************************'
db 13,10,' Type password..: ',0
frmt db '%s',0
pass rb 64 ;// резерв под юзерский пароль
;//------
.code
start:
cinvoke printf,capt ;// запрос на ввод..
cinvoke scanf,frmt,pass ;// принимаем пасс от юзера
;//*** Критический участок кода, который будем шифровать ********************
;//**************************************************************************
@crypt: call @check ;// пропускаем строку ниже *
db 'codeby.net' ;// валидный пароль *
mOk db ' PASS OK! thanks.',0 ;// строки валидации *
mWrong db ' WRONG password!' ,0 ;// ..^^^ *
@check: pop edi ;// EDI = адрес валидного пароля *
mov esi,pass ;// ESI = адрес пароля юзера *
mov ecx,10 ;// длина валидного пассворда *
repe cmpsb ;// сравнить их! *
jcxz @pass_OK ;// если ECX = 0 (jump cx zero) *
cinvoke printf,mWrong ;// иначе: выводим WRONG *
jmp @exit ;// ..и на выход. *
@pass_OK: ;// пароль совпал по всей длине ECX *
cinvoke printf,mOk ;// ОК! *
@end_crypt: ;//==== шифровать до этой метки *
;//*** Конец критичекого блока **********************************************
;//**************************************************************************
@exit: cinvoke scanf,frmt,capt ;// ждём клаву..
invoke exit,0 ;
;//=== Секция импорта =======================
data import
library msvcrt, 'msvcrt.dll'
import msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
end data
Здесь видно, что правильный пасс и выводимые строки хранятся внутри зашифрованного участка кода, что позволило скрыть их от посторонних глаз. Адрес валидного пароля мы получаем через инструкцию CALL, которая перед тем как передать управление на свой операнд, сохраняет в стеке адрес-возврата для заканчивающей вызов, инструкции RET.
Так сложились звёзды, что адрес-возврата будет указывать как-раз на начало строки с валидным пассвордом, поэтому мы снимает его со-стека в регитр EDI и используем как неявный второй операнд для инструкции сравнения строк CMPSB (она сравнивает esi с edi по длинне ecx). Такую технику вычисления адресов назвали "получением дельта-смещения" и она широко используется малварью всех поколений:
C-подобный:
@crypt: call @check ;// пропускаем строку ниже
db 'codeby.net' ;// валидный пароль
mOk db ' PASS OK! thanks.',0 ;// строки валидации
mWrong db ' WRONG password!' ,0 ;// ..^^^
@check: pop edi ;// EDI = адрес валидного пароля
mov esi,pass ;// ESI = адрес пароля юзера
mov ecx,10 ;// длина валидного пассворда
repe cmpsb ;// сравнить их!
Теперь у нас есть "процедура проверки пароля" и нужно прицепить к ней декриптор (саму проверку шифровать будем позже). Поскольку декриптор придётся переносить в область ImageBase, оформим его как shell. Для этого просто поместим код декриптора в секцию-данных нашей программы и компилятор ассемблера сам выстроит из него цепочку байт с опкодами инструкций. Дальше хек-редактором вырежем эту цепочку из .data-секции, и поместим её по адресу ImageBase.
Здесь нужно учитывать, что в секции-данных есть и полезные переменные со-строками, для которых трассер компилятора уже сформировал адреса. Поэтому код декриптора кидаем в самый конец секции, что позволит безболезненно для окружающих, потом изъять его от туда. Значит добавляем в секцию-данных предыдущего кода такие строки:
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//------
.data
capt db 13,10,' Demo crackme v0.1'
db 13,10,' *********************************'
db 13,10,' Type password..: ' ,0
frmt db '%s',0
pass rb 64
align 16 ;// выравнивание на 16-байт (параграф)
db 16 dup(0xff) ;// маркер начала блока
;//~~~ Шелл-код декриптора ~~~~~~~~~8<~~~~~~~~~~~~~~~8<~~~~~~~~~~~~~~~~~~~~~
;//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
xchg eax,edx ;// восстанавливаем стек,
inc ebp ;// ..и регистр EBP,
push eax ;// ..от глюков сигнатуры "MZ"
mov ecx,@end_crypt - @crypt ;// ECX = длина шифруемого блока
shr ecx,1 ;// разделить её на 2 (в словах)
mov esi,@crypt ;// ESI = адрес начала блока
decrypt: xor word[esi],3e97h ;// ксорим слово из ESI ключом
add esi,2 ;// следующее слово..
loop decrypt ;// промотать цикл ECX-раз!
push start ;// куда передавать управление
ret ;// перейти по адресу в стеке!!!
;//~~~ Вырезать будем до сюда ~~~~~~>8~~~~~~~~~~~~~~>8~~~~~~~~~~~~~~~~~~~~~~
db 16 dup(0xff) ;// маркер хвоста блока
;//------
.code
start:
cinvoke printf,capt ;// шапка
cinvoke scanf,frmt,pass ;// принимаем пасс от юзера
;//*** Критический участок кода, который будем шифровать ********************
;//**************************************************************************
@crypt: call @check ;// пропускаем строку ниже *
db 'codeby.net' ;// валидный пароль *
mOk db ' PASS OK! thanks.',0 ;// строки валидации *
mWrong db ' WRONG password!' ,0 ;// ..^^^ *
@check: pop edi ;// EDI = адрес валидного пароля *
mov esi,pass ;// ESI = адрес пароля юзера *
mov ecx,10 ;// длина валидного пассворда *
repe cmpsb ;// сравнить их! *
jcxz @pass_OK ;// если ECX = 0 (jump cx zero) *
cinvoke printf,mWrong ;// иначе: выводим WRONG *
jmp @exit ;// ..и на выход. *
@pass_OK: ;// пароль совпал по всей длине ECX *
cinvoke printf,mOk ;// ОК! *
@end_crypt: ;//==== шифровать до этой метки *
;//*** Конец критичекого блока **********************************************
;//**************************************************************************
@exit: cinvoke scanf,frmt,capt ;// ждём нажатие клавиши..
invoke exit,0 ;
;//=== Секция импорта =======================
data import
library msvcrt, 'msvcrt.dll'
import msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
end data
Разберём детали этого кода...
Значит в нижнем окне секции-данных, среди текстовых строк видим маркеры начала и конца шелла, в виде цепочки байт со-значением 0хFF – это чтобы визуально выделить полезную нагрузку Payload. Сам шелл в красном блоке занимает всего 1Fh байт и вполне влезет в пустую область ImageBase, где мы приготовили для него место. Можно было оптимизировать код, например убрав из четвёртой инструкции 0хВ960000000 три байта нулей в хвосте, но в демонстрационном примере это не важно – у нас в ImageBase места предостаточно.
Интересным моментом являются опкоды переходов, например той-же инструкции цикла LOOP (см.верхнее окно). На этапе компиляции, вычисляющий адреса трассировщик ассемблера подставляет в код не абсолютный, а относительный адрес – в данном случае это опкод 0xE2F6. Второй байт со-значением 0xF6 это и есть адрес относительно текущего – он кодируется одним знаковым байтом. Диапазон этого байта от нуля и до 0x7F означает переход "вперёд", а верхняя половина от 0xFF и до 0х80 – это переход "назад" (на указанное кол-во байт). Здесь мы видим значение 0xF6, значит это
FF-F6=9
байт назад от текущего адреса.Другими словами, если шелл-код использует переходы в пределах 127 байт (0х80), то компилятор вставит относительный адрес – значит после компиляции шелл можно вырезать и переместить в любое место. В противном случае (переход за пределы 127 байт), компилятор вставит уже абсолютный адрес, и двигать такой шелл со-своей позиции нельзя – вся конструкция рухнет. Если нужны дальние FAR-переходы, то можно использовать стек с последующим RET, что я и сделал в своём шелл-коде на выходе, указавав ему переход на метку "start" (оригинальная точка-входа). Это нужно учитывать при зачатии шелл-кодов.
Ну и в первых трёх инструкциях, шелл жонглирует регистрами – что это и зачем?
Выше упоминалось, что сигнатура "MZ" в виде слова 0х4D5A мутирует в инструкции DEC_EBP и POP_EDX. Поэтому для отвода глаз сначала я меняю местами EDX и EAX, после чего восстанавливаю EBP и отправляю EAX (в лице регистра edx) на своё место. На этом этапе стек держится на "честном слове", и чтобы не засветиться сторожам, необходимо соблюдать его баланс.
Инкапсуляция шелл-кода в ImageBase
У нас есть готовый шелл и теперь нужно переместить его из секции-данных в ImageBase, т.е. в заголовок DOS-Header. Для этого открываем "crackme " в хек-редакторе HxD и ищем в нём шелл-код по маркерам начала и конца с цепочкой байт 0хFF. Упс, есть такой – выделяем и копируем его в буфер, а оригинал забиваем нулями. Теперь переходим в самое начало файла, и выделив после сигнатуры "MZ" 1Fh-байт (размер в опкодах), вставляем шелл из буфера по Ctrl+V. Обязательно нужно выделить блок и потом вставить из буфа, иначе указатель на РЕ по смещению Base+0х3С съедет со-своей позиции. Должно получиться примерно так:
Как-видим, выделенный шелл не занял даже и четверти свободного пространства в дос-заголовке. Можно было напичкать его ничего не делающими мусорными инструкциями (обфускация), вставляя их между полезным кодом – это значительно затруднило-бы анализ приложения взломщиком, хотя в демо-примере окончательно сбило-бы нас с толку. Так-что будем считать этот crackme скелетом, который в последующем можно будет усовершенствовать. Тут главное понять суть, а детали реализации могут быть у каждого свои.
Основная Callback-процедура для перехвата EntryPoint
Если сейчас запустить файл, он отработает как положено, т.е. запросит пароль и вернёт ответ. Оригинальная точка-входа ещё не перехвачена, а декриптор хоть и есть, он не получает управления. Теперь наступает самый ответственный момент, от которого зависит весь последующий алгоритм программы. Callback исполняется внутри загрузчика, поэтому нужно быть предельно аккуратным, чтобы не нарушить его работу.
Во-первых, на входе в Callback нужно обязательно сохранять все регистры инструкцией PUSHA, и восстановив их на выходе через глупый POPA, возвратить загрузчику в регистре EAX значение 1. Только в этом случае наш план по перехвату оригинальной EnrtyPoint возымеет успех, и своей функцией NtContinue() загрузчик проглотит наживку. Ситуацию нагнетает и то, что на момент работы Callback-процедуры обработчик структурных исключений SEH ещё не оформлен, поэтому если мы сделаем что-то неправильно, то даже не узнаем об этом – наша прожка тупо провалится в чёрную дыру, без каких-либо намёков на исключение.
Смысл процедуры Callback в том, чтобы получив адрес оригинальной точки-входа, найти его на дне стека и изменить на ImageBase. Для этого берём из поля РЕ.28h относительный RVA адрес точки, и отнимаем его от найденного значения в стеке. В результате получим базу образа, которую и подхватит функия NtContinue(), передавая управление на наш декриптор. После того как декриптор расшифрует уязвимый участок кода с проверкой пароля, он сам передаст управление на Original EntryPoint (OEP) и программа продолжит уже работу по заданному алго.
Тут есть одна проблема – запрет на запись в секцию-кода! Дело в том, что в процессе расшифровки декриптор должен будет ксорить и перезаписывать каждое слово кода, значит нам нужно внутри процедуры Callback снять эту защиту функцией VirtualProtect(), с аргументом "PAGE_EXECUTE_READWRITE" равным 0х40. Кроме этого аргумента, функция требует ещё и адреса блока (в нашем случае секция-кода), её размер 0х1000 или одна страница, и указатель на переменную для текущих флагов. Эта функция BOOL, так-что возвращает нуль в случае ошибки.
В примере ниже, коллбэк ищет ещё и отладчик по системному флагу NtGlobalFlag, и если обнаружит его, то сразу выходит из Callback'a. В результате, точка-входа в стеке оказывается не перехваченной и декриптор управление не получает. Тогда в отладчике мы пароль-то введём, а вот его проверку осуществить не сможем, т.к. блок останется не расшифрованным. Вот сама процедура, как рождественский гусь напичканная комментариями:
C-подобный:
;//=================================================
;//=== CallBack процедура для подмены EP в стеке |
;//=================================================
proc fixStackEpoint hinst,reason,reserved ;// загрузчик передаёт в коллбэк три аргумента
pusha ;// сохранить все регистры!
mov esi,[ fs:0x30] ;// ESI = PEB address
mov eax,[esi+0x68] ;// EAX = PEB.68h NtGlobalFlag
cmp eax,0x70 ;// EAX = 70h - процесс отлаживается!
je @stopCallback ;// выйти из коллбэка..
;// иначе..
mov ebx,[esi+0х08] ;// EBX = PEB.ImageBase
mov ecx,[ebx+0х3C] ;// ECX = РЕ.Offset
add ecx,ebx ;// ECX = РЕ VirtualAddr
mov ecx,[ecx+0x28] ;// ECX = PE.EntryPoint_RVA
mov edx,ecx ;// EDX = EntryPoint RVA
add edx,ebx ;// EDX = оригинал eРoint - сигнатура!
;//=== Подмена EntryPoint в стеке ==================================
mov esi,[fs:4] ;// ESI = TEB.04 StackBase
std ;// DF=1 - обратный шаг для ESI
@find_Signature:
lodsd ;// EAX = dword из ESI
cmp eax,edx ;// сравнить его с оригинал ePoint
jne @find_Signature ;// повторить, если не равно..
cld ;// иначе: сбросить DF – прямой шаг
add esi,4 ;// коррекция указателя ESI в стеке
sub [esi],ecx ;// подмена точки-входа в стеке!!!
;//=== Открываем секции-кода доступ на запись ======================
invoke VirtualProtect,edx,0x1000,0x40,flags ;// PAGE_EXECUTE_READWRITE
@stopCallback:
popa ;// восстановить все регистры
mov eax,1 ;// вернуть загрузчику TRUE
ret ;// возвратить ему управление.
endp
Обратитте внимание, что Callback не передаёт управление на фиктивную точку по адресу ImageBase. Мы только поменяли ОЕР в стеке инструкцией
sub [esi],ecx
(отнять RVA), а дальше функция NtContinue() снимет наше значение со-стека, и сама вызовет декриптор.Шифрование процедуры проверки пароля
Теперь наш декриптор принимает управление и ксорит все слова уязвимого блока ключом 3e97h в надежде, что эта процедура зашифрована. Однако на данном этапе мы ещё не шифровали её, и декриптор вместо расшифровки наоборот шифрует блок. Ручное шифрование интересно, но требует особой внимательности. Так-что вынимаем мозги из штанов и приступаем к шифрованию критически-важного участка кода.
В качестве инструмента, выберем необычайно богатый на возможности хек-редактор HIEW. В его папке есть файл "hiew_ru", где можно найти вполне понятный хелп по крипту бинарного кода. На подготовительном этапе нужно собрать необходимую информацию, в виде начального адреса и длинны шифруемого блока. Посмотрим, чем занимается наш декриптор:
C-подобный:
;//~~~ Шелл-код декриптора ~~~~~~~~~
mov ecx,@end_crypt - @crypt ;// ECX = длина шифруемого блока
shr ecx,1 ;// разделить её на 2 (в словах)
mov esi,@crypt ;// ESI = адрес начала блока
decrypt: xor word[esi],3e97h ;// ксорим слово из ESI ключом
add esi,2 ;// следующее слово..
loop decrypt ;// промотать цикл ECX-раз!
push start ;// адрес оригинальной точки-входа
ret ;// отдать ей управление!!!
Здесь видно, что длинна блока в байтах заносится в регистр ECX, но поскольку мы собираемся ксорить словами по 2 байта, то операцией сдвига вправо, ECX делится на 2. Остаток от деления нас не интересует, в результате чего кол-во итераций цикла в любом случае будет чётным. Теперь загрузим crackme в отладчик, и посмотрим, что лежит в регистре ECX нашего шелл-кода:
Красным по белому мы видим здесь значение 0х30, что в десятичном будет 48. Адрес начала блока равен 0х402021 – запишем эти два значения на бумажке. Теперь вскармливаем crackme чудо-редактору Hiew и по энтеру выбрав режим дизассемблера, приступаем к шифрованию блока "проверки пароля".
Для начала жмём Goto (F5) и предварив точкой, вводим адрес начала блока .402021 – у меня это выглядит так:
Теперь жмём Edit (F3) и в меню появляется Crypt (F7), выбрав который видим окно, в где нам предлагают определиться с размером ксора на каждом шаге (1,2,4,8 байт) и задать алгоритм шифрования. Поскольку наш декриптор расшифровывает словами, то по F2 обязательно задаём размер Word:
В независимости от операнда инструкции XOR в нашем коде, для Hiew'a это всегда регистр EAX, причём переходы LOOP, JMP и прочии он привязывает не к адресу, а к номеру строки в своём окне. Двигать указатель на следующее слово не ;нужно – LOOP во второй строке сам прибавит два. Скрин ниже показывает детали:
Что интересно, тулза не может отменять предыдущую операцию и если вы сделали что-то не так, то придётся перезагрузить Hiew. Если ошибок нет, то жмём Esc и приступаем непосредственно к шифрованию, для чего тискаем всё ту-же клавишу Crypt (F7). Каждое зашифрованное слово выделяется жёлтым цветом, и нам нужно повторить операцию 48 раз (длинна блока в словах = 30h или 48).
В демо-версии Hiew'a есть ограничение (а может и не только в демке) – как только мы дойдём до конца окна, ксор встаёт как вкопанный. Нужно будет запомнить кол-во уже проксоренных слов, и сохранить текущее состояние клавишей Update (F9). Теперь прокручиваем окно вверх и продолжаем шифровать комбинацией F3->F7 опять до видимой части окна. Алгоритм каждый раз задавать не нужно – просто обновляем по F9->F3 и едем дальше по F7, пока не простучим по ней (в данном случае) 48 раз. Это напрягает, но другого выхода вроде нет. Пример представлен ниже:
Эпилог..
После всех манипуляций, наш фрегат может отправиться в свободное плавание, и никакие препятствия в виде отладчиков или дизассемблеров ему не страшны. Ручное шифрование требует терпения, которое с лихвой оправдывается результатом. Ясно, что это лишь учебный пример который ни на что не претендует. Но идея была в том, чтобы показать технику перехвата точки-входа в приложения так, чтобы никто не заметил подвоха.
У анализаторов исполняемых файлов есть такой параметр как "энтропия кода". Этот параметр по 10-бальной шкале определяет меру беспорядка в коде. Так, если код зашифрован весь, то его энтропия может достигать значения (7) – у накрытых протектором она ещё больше. Поскольку я зашифровал только малую часть своего кода, то этот показатель у меня 4.72, а у секции-данных вообще около двух. Таким способом удалось обвести вокруг пальца поле EntryPoint, в котором гордо красуется RVA 0x2000:
Готовый исходник и скомпилированный к употреблению файл цепляю в скрепке. После компиляции его в fasm'e, нужно будет только по указанной методике перенести декриптор в заголовок DOS-Header, и зашифровать уязвимый блок "проверки пароля" ключом, которым расшифровывает декриптор. Чтобы проверить свои силы в реверсе, можете вскормить отладчику готовый экхешник и посмотреть на его реакцию – уверяю, будет интересно. Из всех
Ссылка скрыта от гостей
, один только F-Secure что-то там бормочит, а остальные молчат как партизаны.В следующий раз попробуем написать код, который будет трассировать сам-себя. Это ещё один анти-дебаг, который отладчиками практически не ловится. Зоопарк антиотладочных приёмов поражает своим разнообразием, и желающий защитить свои программы кодер должен иметь в запазухе десяток из них.
Вложения
Последнее редактирование: