Ещё во-времена старушки дос стало очевидно, что некоторые программные модули лучше хранить не внутри исполняемого файла, а вынести их наружу и подгружать по мере необходимости. В то время такие "прицепы" называли оверлеями и с точки зрения экономии 1М-байтного адресного пространства это было разумно – одну большую программу кромсали на мелкие части, и эти части отрабатывали в памяти по очереди. Подобная техника докатилась и до наших дней, только теперь это динамически подгружаемые DLL.
По сути, нет смысла копать данную тему в очередной раз – всё давно расписано, поэтому добавим экшена и сделаем ставку на нестандартное их применение. Как показывает практика, фишка с защитой программ на основе статически прилинкованных DLL пользуется спросом среди коммерческого софта, значит пора сорвать с неё вуаль и познакомиться по-ближе.
---------------------------------------
Записки дилетанта
"Dynamic Link Library" или DLL – это часть исполняемого РЕ-файла в виде внешнего модуля. Он оформлен как ларчик с N-нным количеством уникальных для наших программ функций, которых нет в составе системных Win32-API. Программный экзо-скелет динамических библиотек идентичен исполняемым файлам экзе, однако есть и некоторые нюансы:
Система предоставляет нам два способа подключения DLL к своим проектам – статический и динамический. В первом случае мы подключаем библиотеку и указываем импортируемые из неё функции на этапе компиляции РЕ-файла, и эти функции сразу загружаются в наше адресное пространство, вместе с приложением. Во-втором (динамическом) случае, можно загрузить функцию из DLL в произвольный момент времени, сыграв аккордом LoadLibrary(), GetProcAddress() и FreeLibrary().
Скомпоновать библиотеку довольно просто – пишем обычный ЕХЕ, только в шапке указываем директиву "format PE DLL". В результате, из выхлопной трубы fasm'a получим файл в формате *.dll. Однако при программировании пользовательских библиотек нужно учитывать ряд их особенностей, в частности релокацию образа в памяти.
Чтобы DLL не загрузилась поверх исполняемого приложения (конфликт базовых адресов), её ImageBase обязательно должна быть перемещаемой – достаточно добавить секцию .reloc в коде, об остальном компилятор позаботится сам. В этой секции будут собраны т.н. фиксапы (fixups) – адреса, к которым загрузчик должен будет внести поправки. Фиксапы применяются исключительно к инструкциям, которые обращаются по абсолютным адресам в памяти. Если адрес относительный (в пределах 127 байт), то он не требует модификации:
Такие отладчики как OllyDbg подчёркивают адреса, которые требуют коррекции после перемещения образа в памяти – на рис.выше их всего 4, и непосредственно опкод инструкции не учитывается (здесь push\call, хотя могут быть и условные\безусловные переходы). Размер самих фиксапов равен 12-бит (выделены красным), а это 2^12=4096 или одна страница виртуальной памяти. Соответственно фиксап не может адресовать блок памяти свыше 4 Кбайт. Другими словами, каждая страница (блок) имеет свой набор фиксов.
Точка входа в DLL-библиотеку
Теперь о насущном..
Подобно исполняемым экзе-приложениям, библиотеки тоже имеют свою точку входа – в доках MSDN эта функция известна как DllEntryPoint() (или DllMain в терминологии си). Здесь и кроется всё самое интересное, чему мы посвятим весь последующий разговор.
Любое обращение EXE-модуля к функциям из DLL происходит через системного посредника LdrLoadDLL(). В системном ансамбле, эта кошерная функция из Ntdll.dll играет огромную роль. Она не только загружает библиотеки в пространство юзера на этапе проецирования образа в память, но и обслуживает функции динамического вызова процедур типа LoadLibrary(), GetModuleHandle() и прочии, от которых мы ожидаем получить дескрипторы модулей. Вот её прототип:
Диалог этого загрузчика с вызываемой библиотекой происходит по схеме "запрос-ответ". Любая библиотека должна иметь упомянутую функцию взаимодействия с загрузчиком DllEntryPoint(), от которой в регистре EAX лоадер ждёт или TRUE (библиотека способна обработать запрос), или FALSE (что-то пошло не так). Не соблюдение этих правил приводит к краху приложения.
LdrLoadDLL() может извещать библиотеку о четырёх событиях, которые происходят во-внешнем (по отношению к библиотеке) мире. Эта информация передаётся точке-входа в DLL в параметре fdwReason. Кроме события, загрузчик сразу передаёт либе её базу в памяти, и способ подключения к исполняемому файлу. Прототип описывается так:
Из всей этой братии, нам интересен лишь аргумент DLL_PROCESS_ATTACH = 1, благодаря которому статически присобачив библиотеку к нашему процессу, мы можем например, предварительно расшифровать основной код программы, обнаружить отладчик в фоне и т.д. Дело в том, что загрузчик проецирует DLL в пространство процесса задолго до точки-входа в программу, с которой начинают анализ все отладчики, а значит Оля пропустит этот этап между ног. Здесь уместно вспомнить про функции TLS-Callback, ..но поскольку загрузчик парсит импорт из библиотек вторым (а TLS аж десятым), то выигрыш тут на лицо.
DLL – промышленная реализация
Чтобы сформировать образ, приведём код типичной библиотеки. Пока она будет выводить на экран только сообщения об удачном подсоединению к родительскому процессу, а после его закрытия – покажет ещё одну мессагу, что мол родительский процесс отработал и отправляется на покой. Когда приложение вызывает библиотеку, в её стеке оказывается фрейм из трёх (не считая адреса-возврата) аргументов функции DllEntryPoint(), которые загрузчик кладёт туда на всеобщее обозрение – так это выглядит в отладчике:
Соответственно, мы можем снимать эти аргументы прямо со-стека, и сразу проверять их – код ниже придерживается именно такой политики:
Теперь у нас есть либа, и нужно написать родительское приложение, которое будет статически привязывать к себе эту библиотеку. Во-первых, обратим внимание на имя новоиспечённой DLL – здесь, в секции-экспорта я определил его как "about.dll", это важно! Теперь просто импортируем эту библиотеку по имени, и вызываем из неё функцию примерно так:
Тёмная сторона луны
Пробежавшись по макушкам кода библиотек, посмотрим на них из другой проекции..
Алгоритм работы загрузчика образов LDR плохо освещён в документации и это не удивительно – весь ядерный код мастдая, коммерческая тайна (будь она не ладна). Как это принято у Microsoft, она советует нам ознакомиться с третьей поправкой, восьмого исправления, четвёртой редакции от 32 февраля где сказано, что "..в военное время не только прямой угол может достигть 100 градусов, но и функция инициализации DllEntryPoint() может использоваться не по назначению". Самое главное: кто, где и когда объявляет это положение неизвестно, а значит мы вольны назначать его сами.
Мощь (и беспомощность) точки-входа в библиотеку в том, что некоторая часть театра действий происходит под управлением системных механизмов, отследить которые из прикладного уровня довольно сложно. В документации на РЕ-файл можно найти формат каталога секций "Data-Directories". В этом дире рассчитавшись на первый-второй выстроены в ряд все секции, которые обходит загрузчик образов LdrLoadDll() при инициализации приложения. Причём последовательность секций строго регламентируется. Вот как выглядит эта структура в представлении редактора PE-Explorer:
Таким образом, импорт анализируется загрузчиком на самом начальном этапе, и большинство служебных структур прикладного уровня в этот момент даже не инициализированы ещё до конца – в частности, это относится к структуре PEB, не говоря уже о дочерней к ней структуре ТЕВ. Например, если мы внутри DllEntryPoint() захотим из РЕВ получить флаг-отладки нашего приложения "BeingDebugger", то потерпим фиаско (проверено на практике). На скамейку запасных сразу отправляется и функция IsDebuggerPresent(), которая читает этот-же флаг из РЕВ. Значит нужно спускаться на уровень ниже, а для защитных механизмов это только гуд.
Если развивать мысль дальше, то наша библиотека не единственная у приложения. Кроме неё, в память каждого процесса система загружает и свои либы Ntdll.dll (собственно в ней и живёт загрузчик LDR), а так-же библиотеку kernel32.dll. С очерёдностью загрузки в память системных библиотек можно ознакомится в отладчике WinDbg, озадачив его командой !peb – поле InMemoryOrderModuleList как-раз отрапортует нам об этом:
Здесь видно, что первым десантируется в память мой исполняемый файл "DLL_attach.exe", следом за ним системные библиотеки, и только потом моя пользовательская либа "about.dll". Повторюсь, что система выстраивает структуру РЕВ только когда окончательно покончит с окружением процесса, скидывая в неё результаты проделанной работы. А лог на рисунке выше, WinDbg парсит уже из рабочего процесса, поэтому РЕВ как-бы готова к употреблению.
DllEntryPoint() на страже приложения
Теперь будем мыслить так.. Если точка-входа в библиотеку с аргументом DLL_PROCESS_ATTACH отрабатывает на низком уровне, значит на её основе можно соорудить защитный механизм. Система вызывает DllEntryPoint() с аргументом ATTACH сразу после того-как DLL спроецирована на адресное пространство процесса – такая ситуация возможна всего один раз, и на протяжении всего "сеанса" больше не повторяется! В следующий раз, когда тред вызовет LoadLibrary() для уже спроецированной на память DLL, система просто увеличит счётчик обращения к ней и всё.
В предоставленном на суд примере, статически (явно) прилинкованная библиотека внутри DllEntryPoint() будет искать отладчик, и если обнаружит таковой, то вернёт ошибку. Как упоминалось выше, флаги-отладки из структуры РЕВ для этих целей уже не подходят, поэтому придётся искать обходные пути.
Одним из вариантов обнаружения факта отладки является проверка своего статуса в системе. Дело в том, что в дефолте, запущенный на исполнение процесс не имеет привилегии SeDebugPrivilege, зато ею обладает отладчик. Когда он загружает нас в своё тело, то автоматом передаёт и свою привилегию, чекнув которую мы можем определить этот факт. Есть куча способов узнать привилегию своего процесса, и мы воспользуемся самым простым – попытаемся открыть системный процесс csrss.exe.
CSRSS.EXE – это часть пользовательской подсистемы Win32, и при обычных обстоятельствах он не доступен прикладным задачам. Однако привилегия Debug снимает этот запрет, и мы можем открыть его функцией OpenProcess() со-всеми вытекающими последствиями. CSRSS (client\server run-time subsystem) отвечает за консоль, работу с потоками Thread, и за 16-битную среду MS-DOS (на х64 её кастрировали). Это процесс пользовательского режима, который перехватывает обращения к ядру и решает простые вопросы на уровне прикладных задач.
Проблема в том, что функции OpenProcess() требуется идентификатор PID открываемого процесса, т.е. нам нужно будет просканировать всю память и найти нужный процесс по его имени – тривиальная задача по обнаружению отладчика превращается в ад. В сети можно встретить разные варианты перечисления процессов – это CreateToolhelp32Snapshot(), обход в цикле через Process32First\Next(), EnumProcess() и тяжеловес NtQuerySystemInformation().
Однако получить PID именно процесса CSRSS.EXE можно специально предназначенной для этого функцией из Ntdll.dll под названием CsrGetProcessId() – у неё нет аргументов и в EAX она сразу возвращает столь необходимый нам PID. С использованием этой функции, проверка на отладчик укладывается в пару строк ассемблерного кода. Мы поместим её внутрь DllEntryPoint() и будем проверять запрос на DLL_PROCESS_ATTACH.
В общем случае, программа будет следовать такому алго..
Мы пишем приложение, которое запрашивает пароль. Если юзер введёт валидный пасс, то управление примет зашифрованная функция, которую расшифрует декриптор из внешней библиотеки, с непримечальным именем "about.dll". Алгоритм декриптора – самый примитивный ксор 1-байтныйм ключом, однако тут есть подвох! Пароль на валидность мы вообще не будем проверять, а декриптор сняв с него хэш-сумму сразу расшифрует ей критический блок в основном приложении. Теперь уже взломщик не сможет просто обратить условие проверки, и ему придётся осуществлять только брут, перебором всех возможных ключей.
Если юзер подсунет левый пароль и его хэш не совпадёт с тем, которым мы зашифровали блок, то рано или поздно процессор нарвётся на исключение, поскольку пойдёт пахать зашифрованный код. Чтобы защитить честь его мундира, для таких случаев мы устанавливаем SEH-обработчик, который и будет отлавливать эти исключения. То-есть, если SEH примет управление, значит пасс невалидный и мы подкорректировав значение регистра EIP в контексте, выводим мессагу Wrong и на выход..
Основная проблема тут – правильно зашифровать критический блок кода в основном приложении. Я уже приводил пример шифрования в hex-редакторе HIEW, поэтому повторяться не буду. Если возникнут вопросы, их всегда можно задать в комментах этой темы. Ключ – это сумма всех символов любой строки. Например, в данном случае я использовал пароль "codeby.net" и получил его 1-байтную хэш сумму = 0xEB. Для этого можно воспользоваться услугами редактора HxD и виндовым калькулятором:
После того-как получим хэш валидного пассворда, можно приступать к шифрованию всего блока этим ключом. Если на запрос юзер введёт валидный пасс, то функция декриптора в библиотеке рассчитает его хэш на автомате, и опять проксорит этим-же клюсом – в результате получим расшифрованный блок, и процессор не споткнётся уже об него. Ниже приведён готовый к употреблению код, который остаётся только скомпилировать, и в основном приложении зашифровать указанный блок:
Код основного приложения:
Теперь код библиотеки, которую обязательно нужно назвать "about.dll".
Под занавес..
Для разминки мозгов, в скрепке предлагаю построенный на этом алгоритме крэкми. Здесь нужен брутом найти пароль, и чтобы он не занимал много времени, длина ключа такая-же 1-байт. Без проблем можно было увеличить его разрядность хоть до 4-х байт, однако сути это не меняет.. просто дольше нужно будет подбирать. Всех с наступающим 0х07E4 !!!
По сути, нет смысла копать данную тему в очередной раз – всё давно расписано, поэтому добавим экшена и сделаем ставку на нестандартное их применение. Как показывает практика, фишка с защитой программ на основе статически прилинкованных DLL пользуется спросом среди коммерческого софта, значит пора сорвать с неё вуаль и познакомиться по-ближе.
---------------------------------------
Записки дилетанта
"Dynamic Link Library" или DLL – это часть исполняемого РЕ-файла в виде внешнего модуля. Он оформлен как ларчик с N-нным количеством уникальных для наших программ функций, которых нет в составе системных Win32-API. Программный экзо-скелет динамических библиотек идентичен исполняемым файлам экзе, однако есть и некоторые нюансы:
1. Наличие секции-экспорта в коде, которой могут похвастаться исключительно библиотеки. Приложение может вызывать (импортировать) только те функции из DLL, которые оглашены в её секции-экспорта ".edata". Однако бывают и внутренние функции, которые DLL не экспортирует во-внешний мир, используя только внутри своей тушки для производственных нужд – к их числу можно отнести, например функцию точки-входа в библиотеку DllEntryPoint(). По сути, в данной статье мы сделаем акцент только на этой процедуре инициализации.
2. Второе отличие DLL от EXE – это не способность ими исполнять свой код самостоятельно, поскольку каждый участок кода обёрнут в отдельную функцию. Все эти функции тупо ждут своего часа, пока их услуги не станут восстребованы экзе-файлу и он не затребует их явно. Исключение здесь составляет только упомянутая точка-входа, с которой без нашего ведома активно общается загрузчик LDR, на протяжении всего времени работы приложения.
Система предоставляет нам два способа подключения DLL к своим проектам – статический и динамический. В первом случае мы подключаем библиотеку и указываем импортируемые из неё функции на этапе компиляции РЕ-файла, и эти функции сразу загружаются в наше адресное пространство, вместе с приложением. Во-втором (динамическом) случае, можно загрузить функцию из DLL в произвольный момент времени, сыграв аккордом LoadLibrary(), GetProcAddress() и FreeLibrary().
Скомпоновать библиотеку довольно просто – пишем обычный ЕХЕ, только в шапке указываем директиву "format PE DLL". В результате, из выхлопной трубы fasm'a получим файл в формате *.dll. Однако при программировании пользовательских библиотек нужно учитывать ряд их особенностей, в частности релокацию образа в памяти.
Чтобы DLL не загрузилась поверх исполняемого приложения (конфликт базовых адресов), её ImageBase обязательно должна быть перемещаемой – достаточно добавить секцию .reloc в коде, об остальном компилятор позаботится сам. В этой секции будут собраны т.н. фиксапы (fixups) – адреса, к которым загрузчик должен будет внести поправки. Фиксапы применяются исключительно к инструкциям, которые обращаются по абсолютным адресам в памяти. Если адрес относительный (в пределах 127 байт), то он не требует модификации:
Такие отладчики как OllyDbg подчёркивают адреса, которые требуют коррекции после перемещения образа в памяти – на рис.выше их всего 4, и непосредственно опкод инструкции не учитывается (здесь push\call, хотя могут быть и условные\безусловные переходы). Размер самих фиксапов равен 12-бит (выделены красным), а это 2^12=4096 или одна страница виртуальной памяти. Соответственно фиксап не может адресовать блок памяти свыше 4 Кбайт. Другими словами, каждая страница (блок) имеет свой набор фиксов.
Точка входа в DLL-библиотеку
Теперь о насущном..
Подобно исполняемым экзе-приложениям, библиотеки тоже имеют свою точку входа – в доках MSDN эта функция известна как DllEntryPoint() (или DllMain в терминологии си). Здесь и кроется всё самое интересное, чему мы посвятим весь последующий разговор.
Любое обращение EXE-модуля к функциям из DLL происходит через системного посредника LdrLoadDLL(). В системном ансамбле, эта кошерная функция из Ntdll.dll играет огромную роль. Она не только загружает библиотеки в пространство юзера на этапе проецирования образа в память, но и обслуживает функции динамического вызова процедур типа LoadLibrary(), GetModuleHandle() и прочии, от которых мы ожидаем получить дескрипторы модулей. Вот её прототип:
C-подобный:
LdrLoadDll(
PathToFile ;// путь (опционально);
Flags ;// флаги (опционально);
ModuleFileName ;// имя библиотеки;
ModuleHandle ) ;// возвращаемый дескриптор.
Диалог этого загрузчика с вызываемой библиотекой происходит по схеме "запрос-ответ". Любая библиотека должна иметь упомянутую функцию взаимодействия с загрузчиком DllEntryPoint(), от которой в регистре EAX лоадер ждёт или TRUE (библиотека способна обработать запрос), или FALSE (что-то пошло не так). Не соблюдение этих правил приводит к краху приложения.
LdrLoadDLL() может извещать библиотеку о четырёх событиях, которые происходят во-внешнем (по отношению к библиотеке) мире. Эта информация передаётся точке-входа в DLL в параметре fdwReason. Кроме события, загрузчик сразу передаёт либе её базу в памяти, и способ подключения к исполняемому файлу. Прототип описывается так:
C-подобный:
BOOL DllEntryPoint (
hinstDLL ;// виртуальная база данной библиотеки в памяти;
fdwReason ;// причина вызова;
lpvReserved ); ;// способ подключения: статический(0), или динамический(1).
;// ..(статический = явно, динамический = не явно)
;// Возможные значения "fdwReason"
;//-------------------------------
• DLL_PROCESS_DETACH (0) – FreeLibrary() или DLL отключается от процесса,
• DLL_PROCESS_ATTACH (1) – первое подключении DLL к процессу, при его запуске,
• DLL_THREAD_ATTACH (2) - процесс создаёт новый поток,
• DLL_THREAD_DETACH (3) - процесс завершает поток.
Из всей этой братии, нам интересен лишь аргумент DLL_PROCESS_ATTACH = 1, благодаря которому статически присобачив библиотеку к нашему процессу, мы можем например, предварительно расшифровать основной код программы, обнаружить отладчик в фоне и т.д. Дело в том, что загрузчик проецирует DLL в пространство процесса задолго до точки-входа в программу, с которой начинают анализ все отладчики, а значит Оля пропустит этот этап между ног. Здесь уместно вспомнить про функции TLS-Callback, ..но поскольку загрузчик парсит импорт из библиотек вторым (а TLS аж десятым), то выигрыш тут на лицо.
DLL – промышленная реализация
Чтобы сформировать образ, приведём код типичной библиотеки. Пока она будет выводить на экран только сообщения об удачном подсоединению к родительскому процессу, а после его закрытия – покажет ещё одну мессагу, что мол родительский процесс отработал и отправляется на покой. Когда приложение вызывает библиотеку, в её стеке оказывается фрейм из трёх (не считая адреса-возврата) аргументов функции DllEntryPoint(), которые загрузчик кладёт туда на всеобщее обозрение – так это выглядит в отладчике:
Соответственно, мы можем снимать эти аргументы прямо со-стека, и сразу проверять их – код ниже придерживается именно такой политики:
C-подобный:
format pe console dll ;// собираем DLL-модуль
include 'win32ax.inc'
entry DllEntryPoint ;// на точку-входа
;//-----
.data
attach db 13,10
db ' Dll load done!...' ,13,10 ;// флаг приложению, что DLL загрузилась
db ' Base: 0x%08X ' ,13,10,0 ;// ..сразу покажем свою базу
detach db ' Process shuts down!...',13,10,0 ;// флаг, что DLL выгружается из памяти
mess db " Hi, I'm a message from the library!",0 ;// строка для вызова из приложения!
frmt db '%s',0
;//-----
.code
start:
;//=== Точка входа в библиотеку! ==============
;// Проверяет на вызов своей тушки приложением
;// [esp+0] = адрес возврата в загрузчик LDR
;// [esp+4] = аргумент "hinstDLL"
;// [esp+8] = аргумент "fdwReason"
;// [esp+12] = аргумент "lpvReserved"
;//============================================
proc DllEntryPoint ;//====== Регистр букв имеет значение!!!
mov eax,[esp+8] ;// EAX = fdwReason (причина вызова)
cmp eax,DLL_PROCESS_ATTACH ;// тест на "атаку" либы!
jnz @fuck ;// если нет..
mov eax,[esp+4] ;// иначе: EAX = hinstDLL (своя база в памяти)
cinvoke printf,attach,eax ;// выводим мессагу.
jmp @thread ;// на выход..
@fuck: cmp eax,DLL_PROCESS_DETACH ;// тест на "отступление"!
jnz @thread ;// если нет..
cinvoke printf,detach ;// иначе: выводим мессагу.
@thread: mov eax,1 ;// если ни_то_ни_другое, значит это работа с потоками.
ret ;// return TRUE загрузчику LDR !!!
endp ;//======
;==============================================
;//=== Библиотека должна иметь как-минимум одну функцию (процедуру)
;// Здесь просто мессага, которую запросит основное приложение.
proc Hello
cinvoke printf,mess ;// мессага "Hi.."
ret ;// выход по адресу-возврата.
endp
;//=== Секция-импорта из системных DLL ========
section '.idata' import data readable
library msvcrt,'msvcrt.dll'
import msvcrt, printf,'printf'
;//=== Секция-экспорта своих функций ==========
section '.edata' export data readable
export 'about.dll', Hello,'Hello' ;// перечисляем их..
;//=== Секция-перемещения образа (релоков) ====
;// Если секция имеет флаг "Discardable",
;// она становится первым кандидатом на выгрузку из памяти,
;// когда система обнаружит её нехватку.
section '.reloc' fixups data discardable ;// клондайк фиксапов
Теперь у нас есть либа, и нужно написать родительское приложение, которое будет статически привязывать к себе эту библиотеку. Во-первых, обратим внимание на имя новоиспечённой DLL – здесь, в секции-экспорта я определил его как "about.dll", это важно! Теперь просто импортируем эту библиотеку по имени, и вызываем из неё функцию примерно так:
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//-----
.data
frmt db '%s',0
;//-----
.code
start:
cinvoke Hello ;// зовём функцию из about.dll !!!
cinvoke scanf,frmt,frmt+2 ;// ждём клаву..
cinvoke exit,0 ;// Game Over!
;//-----
section '.idata' import data readable
library msv,'msvcrt.dll', about,'about.dll' ;// указать точное имя DLL!
import msv, scanf,'scanf',exit,'exit'
import about, Hello,'Hello'
Тёмная сторона луны
Пробежавшись по макушкам кода библиотек, посмотрим на них из другой проекции..
Алгоритм работы загрузчика образов LDR плохо освещён в документации и это не удивительно – весь ядерный код мастдая, коммерческая тайна (будь она не ладна). Как это принято у Microsoft, она советует нам ознакомиться с третьей поправкой, восьмого исправления, четвёртой редакции от 32 февраля где сказано, что "..в военное время не только прямой угол может достигть 100 градусов, но и функция инициализации DllEntryPoint() может использоваться не по назначению". Самое главное: кто, где и когда объявляет это положение неизвестно, а значит мы вольны назначать его сами.
Мощь (и беспомощность) точки-входа в библиотеку в том, что некоторая часть театра действий происходит под управлением системных механизмов, отследить которые из прикладного уровня довольно сложно. В документации на РЕ-файл можно найти формат каталога секций "Data-Directories". В этом дире рассчитавшись на первый-второй выстроены в ряд все секции, которые обходит загрузчик образов LdrLoadDll() при инициализации приложения. Причём последовательность секций строго регламентируется. Вот как выглядит эта структура в представлении редактора PE-Explorer:
Таким образом, импорт анализируется загрузчиком на самом начальном этапе, и большинство служебных структур прикладного уровня в этот момент даже не инициализированы ещё до конца – в частности, это относится к структуре PEB, не говоря уже о дочерней к ней структуре ТЕВ. Например, если мы внутри DllEntryPoint() захотим из РЕВ получить флаг-отладки нашего приложения "BeingDebugger", то потерпим фиаско (проверено на практике). На скамейку запасных сразу отправляется и функция IsDebuggerPresent(), которая читает этот-же флаг из РЕВ. Значит нужно спускаться на уровень ниже, а для защитных механизмов это только гуд.
Если развивать мысль дальше, то наша библиотека не единственная у приложения. Кроме неё, в память каждого процесса система загружает и свои либы Ntdll.dll (собственно в ней и живёт загрузчик LDR), а так-же библиотеку kernel32.dll. С очерёдностью загрузки в память системных библиотек можно ознакомится в отладчике WinDbg, озадачив его командой !peb – поле InMemoryOrderModuleList как-раз отрапортует нам об этом:
Здесь видно, что первым десантируется в память мой исполняемый файл "DLL_attach.exe", следом за ним системные библиотеки, и только потом моя пользовательская либа "about.dll". Повторюсь, что система выстраивает структуру РЕВ только когда окончательно покончит с окружением процесса, скидывая в неё результаты проделанной работы. А лог на рисунке выше, WinDbg парсит уже из рабочего процесса, поэтому РЕВ как-бы готова к употреблению.
DllEntryPoint() на страже приложения
Теперь будем мыслить так.. Если точка-входа в библиотеку с аргументом DLL_PROCESS_ATTACH отрабатывает на низком уровне, значит на её основе можно соорудить защитный механизм. Система вызывает DllEntryPoint() с аргументом ATTACH сразу после того-как DLL спроецирована на адресное пространство процесса – такая ситуация возможна всего один раз, и на протяжении всего "сеанса" больше не повторяется! В следующий раз, когда тред вызовет LoadLibrary() для уже спроецированной на память DLL, система просто увеличит счётчик обращения к ней и всё.
В предоставленном на суд примере, статически (явно) прилинкованная библиотека внутри DllEntryPoint() будет искать отладчик, и если обнаружит таковой, то вернёт ошибку. Как упоминалось выше, флаги-отладки из структуры РЕВ для этих целей уже не подходят, поэтому придётся искать обходные пути.
Одним из вариантов обнаружения факта отладки является проверка своего статуса в системе. Дело в том, что в дефолте, запущенный на исполнение процесс не имеет привилегии SeDebugPrivilege, зато ею обладает отладчик. Когда он загружает нас в своё тело, то автоматом передаёт и свою привилегию, чекнув которую мы можем определить этот факт. Есть куча способов узнать привилегию своего процесса, и мы воспользуемся самым простым – попытаемся открыть системный процесс csrss.exe.
CSRSS.EXE – это часть пользовательской подсистемы Win32, и при обычных обстоятельствах он не доступен прикладным задачам. Однако привилегия Debug снимает этот запрет, и мы можем открыть его функцией OpenProcess() со-всеми вытекающими последствиями. CSRSS (client\server run-time subsystem) отвечает за консоль, работу с потоками Thread, и за 16-битную среду MS-DOS (на х64 её кастрировали). Это процесс пользовательского режима, который перехватывает обращения к ядру и решает простые вопросы на уровне прикладных задач.
Проблема в том, что функции OpenProcess() требуется идентификатор PID открываемого процесса, т.е. нам нужно будет просканировать всю память и найти нужный процесс по его имени – тривиальная задача по обнаружению отладчика превращается в ад. В сети можно встретить разные варианты перечисления процессов – это CreateToolhelp32Snapshot(), обход в цикле через Process32First\Next(), EnumProcess() и тяжеловес NtQuerySystemInformation().
Однако получить PID именно процесса CSRSS.EXE можно специально предназначенной для этого функцией из Ntdll.dll под названием CsrGetProcessId() – у неё нет аргументов и в EAX она сразу возвращает столь необходимый нам PID. С использованием этой функции, проверка на отладчик укладывается в пару строк ассемблерного кода. Мы поместим её внутрь DllEntryPoint() и будем проверять запрос на DLL_PROCESS_ATTACH.
В общем случае, программа будет следовать такому алго..
Мы пишем приложение, которое запрашивает пароль. Если юзер введёт валидный пасс, то управление примет зашифрованная функция, которую расшифрует декриптор из внешней библиотеки, с непримечальным именем "about.dll". Алгоритм декриптора – самый примитивный ксор 1-байтныйм ключом, однако тут есть подвох! Пароль на валидность мы вообще не будем проверять, а декриптор сняв с него хэш-сумму сразу расшифрует ей критический блок в основном приложении. Теперь уже взломщик не сможет просто обратить условие проверки, и ему придётся осуществлять только брут, перебором всех возможных ключей.
Если юзер подсунет левый пароль и его хэш не совпадёт с тем, которым мы зашифровали блок, то рано или поздно процессор нарвётся на исключение, поскольку пойдёт пахать зашифрованный код. Чтобы защитить честь его мундира, для таких случаев мы устанавливаем SEH-обработчик, который и будет отлавливать эти исключения. То-есть, если SEH примет управление, значит пасс невалидный и мы подкорректировав значение регистра EIP в контексте, выводим мессагу Wrong и на выход..
Основная проблема тут – правильно зашифровать критический блок кода в основном приложении. Я уже приводил пример шифрования в hex-редакторе HIEW, поэтому повторяться не буду. Если возникнут вопросы, их всегда можно задать в комментах этой темы. Ключ – это сумма всех символов любой строки. Например, в данном случае я использовал пароль "codeby.net" и получил его 1-байтную хэш сумму = 0xEB. Для этого можно воспользоваться услугами редактора HxD и виндовым калькулятором:
После того-как получим хэш валидного пассворда, можно приступать к шифрованию всего блока этим ключом. Если на запрос юзер введёт валидный пасс, то функция декриптора в библиотеке рассчитает его хэш на автомате, и опять проксорит этим-же клюсом – в результате получим расшифрованный блок, и процессор не споткнётся уже об него. Ниже приведён готовый к употреблению код, который остаётся только скомпилировать, и в основном приложении зашифровать указанный блок:
Код основного приложения:
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//-----
.data
capt db 13,10,' Attach-crackme v0.1'
db 13,10,' *********************'
db 13,10,' Type pass: ',0
wrong db 13,10,' Wrong Password!!!..',0
len = @endCrypt - @crypt
frmt db '%s',0
buff db ?
;//-----
section '.code' code readable writable executable ;// доступ на запись!
start:
;// Устанавливаем SEH-обработчик исключений
;// выводит сообщение "WrongPass" и выходит из программы
xor ebx,ebx ;//
push @trap ;// указатель на функцию
push dword[fs:ebx] ;//
mov [fs:ebx],esp ;//
cinvoke printf,capt ;// запрос на ввод пароля
cinvoke scanf,frmt,buff ;// ..........^^^
invoke Decrypt,len,@crypt,buff ;// (!)зовём функцию проверки-пароля из DLL
;//==== Начало шифруемого блока ============
@crypt: nop ;// 0x90 = маркер начала
call @delta ;// кладём в стек адрес возврата
db ' Pass.....: OK!',13 ;// <-- (он указывает сюда).
db 10,10,' Hard-Rock for ever..'
db 13,10,' --------------------'
db 13,10,' Avalanch'
db 13,10,' Blind Guardian'
db 13,10,' Def Con Dos'
db 13,10,' Fear Factory'
db 13,10,' Hamlet'
db 13,10,' HammerFall'
db 13,10,' Iced Earth'
db 13,10,' Iron Maiden'
db 13,10,' Korn'
db 13,10,' Mago De Oz'
db 13,10,' Metallica'
db 13,10,' Rammstein'
db 13,10,' Rhapsody'
db 13,10,' Stratovarius',0
@delta: pop esi ;// ESI = указатель на сообщение
cinvoke printf,esi ;// вывод его на консоль
nop ;// маркер окончания
@endCrypt:
;//==== Конец шифруемого блока =============
@exit: cinvoke scanf,frmt,frmt+2 ;// конец программы!
cinvoke exit,0 ;//
;//==== SEH обработчик =====================
;// в контексте регистров меняет EIP на выход,
;// и выводит сообщение "WRONG Pass!"
@trap: mov esi,[esp+12] ;// ESI = указатель на CONTEXT
mov eax,@exit ;// EAX = адрес метки EXIT
mov [esi+0xb8],eax ;// 0xb8 = смещение EIP в контексте
cinvoke printf,wrong ;// мессага
xor eax,eax ;// команда "ребут" диспетчеру-исключений
ret ;// return..
;//xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
section '.idata' import data readable
library msv,'msvcrt.dll', about,'about.dll'
import msv, printf,'printf',scanf,'scanf',exit,'exit'
import about, Decrypt,'Decrypt'
Теперь код библиотеки, которую обязательно нужно назвать "about.dll".
C-подобный:
format pe console dll ;// собираем DLL-модуль
include 'win32ax.inc'
entry DllEntryPoint
;//-----
.data
mes0 db ' Dll loaded done!...',13,10,0 ;// мессага-флаг, что DLL робит
frmt db ' Debug %04X',0
;//-----
.code
start:
;//=== Точка входа в библиотеку! =========================
;// Проверяет на ATTACH и отладчик.
;//=======================================================
proc DllEntryPoint hinst,Reason,Reserved ;// три аргумента..
mov eax,[Reason] ;// EAX = причина вызова DLL
cmp eax,DLL_PROCESS_ATTACH ;// тест на первое подключение к процессу
jnz @noAttach ;// выход, если не равно..
invoke CsrGetProcessId ;// иначе: EAX = PID процесса CSRSS.EXE
invoke OpenProcess,PROCESS_VM_READ,0,eax ;// пробуем его открыть..
push eax ;// запомнить хэндл для закрытия
or eax,eax ;// проверить на нуль выхлоп функции
je @ok ;// если нуль, значит ошибка и нет отладчика!
mov eax,0xDEAD ;// иначе: EAX слово "DEAD" хексами
cinvoke printf,frmt,eax ;// мессага "Debug DEAD",
jmp $ ;// ..и виснем внутри EPoint (придумай своё)
@ok: pop eax ;// если отладчика нет, EAX = хэндл csrss.exe
invoke CloseHandle,eax ;// закроем его (контроль счётчика обращений)
cinvoke printf,mes0 ;// мессага "DLL удачно загружена!"
@noAttach: ;//
mov eax,1 ;// Return TRUE загрузчику LDR
ret ;// возвращаем управление..
endp
;//=== Функция проверки пароля =======================
;// снимает с юзерского ввода хэш-сумму,
;// и использует её как ключ для расшифровки блока
;// Аргументы: len = длина шифруемого блока,
;// addr = адрес его начала,
;// pass = указатель на пароль юзера.
;=====================================================
proc Decrypt len,addr,pass ;// функция с агрументами
mov esi,[pass] ;// ESI = указатель на пароль
xor eax,eax ;// EAX,ЕВХ = 0
xor ebx,ebx ;//
@01: lodsb ;// AL = очередной байт из ESI
add bx,ax ;// ВХ = хэш сумма
or al,al ;// проверить AL на нуль (конец)
jnz @01 ;// повторить, если нет..
;// иначе: BX = хэш сумма юзера
mov esi,[addr] ;// ESI = указатель на шифруемый блок
mov edi,esi ;// EDI = он-же, для перезаписи
mov ecx,[len] ;// ECX = длина блока в байтах
@02: lodsb ;// AL = очередной байт из ESI
xor al,bl ;// ксорим его 1-байтным хэш-ключом юзера
stosb ;// записать проксоренный байт на место!
loop @02 ;// повторить по длинне ЕСХ..
ret ;// возврат управления приложению!!!
endp ;// ...
;//=== Импорт =======
section '.idata' import data readable
library msvcrt,'msvcrt.dll',nt,'ntdll.dll',krnl,'kernel32.dll'
import msvcrt, printf,'printf'
import nt, CsrGetProcessId,'CsrGetProcessId'
import krnl, OpenProcess,'OpenProcess',CloseHandle,'CloseHandle'
;//=== Экспорт ======
section '.edata' export data readable
export 'about.dll', Decrypt,'Decrypt'
;//=== Релоки =======
section '.reloc' fixups data discardable ;// перемещаемая база DLL
Под занавес..
Для разминки мозгов, в скрепке предлагаю построенный на этом алгоритме крэкми. Здесь нужен брутом найти пароль, и чтобы он не занимал много времени, длина ключа такая-же 1-байт. Без проблем можно было увеличить его разрядность хоть до 4-х байт, однако сути это не меняет.. просто дольше нужно будет подбирать. Всех с наступающим 0х07E4 !!!