Для чего вообще нужно внедрять свои DLL-ки в чужие процессы и устанавливать там хуки? Для того, чтобы понять какие функции будет вызывать это приложение, с какими параметрами и что эти функции вернут. Таким образом мы можем понять внутреннюю логику работы этого приложения, узнать к каким файлам оно пытается получить доступ, какие данные пересылает по сети, мы можем добавить в него логирование, профилирование, отладить баг, получить из приложения некоторые данные или наоборот — добавить в его интерфейс что-нибудь нужное нам. Хуки использует известная утилита Spy++ для работы с окнами приложений, DirectX-отладчики вроде RenderDoc, некоторые утилиты от SysInternals, программы типа ApiMonitor и т.д.
Некоторое время назад я писал вводные статьи об использовании хуков на примерах библиотек Microsoft Detours и madCodeHook (если вы совсем не знакомы с хуками — это то, с чего можно начать). Описанных там техник достаточно для работы с обычными приложениями, но время не стоит на месте и вот сегодня у нас уже естьMetro Modern-приложения, входящие в комплект Windows 8 и Windows 10, а также распространяющиеся через Microsoft Store программы сторонних производителей. К примеру, новый браузер Spartan — это Modern-приложение. Внедрение DLL в эти процессы имеет свои особенности и на первый взгляд может даже показаться невозможным, но это не так. В этой статье я расскажу, как мы можем залезть внутрь Modern-приложения и установить в нём хуки в своих коварных целях.
32-битные и 64-битные приложения
Операционная система Windows бывает 32-разрядная и 64-разрядная. Соответственно, Modern-приложения тоже могут быть 32-битные либо 64-битные. При загрузке приложения в магазин Microsoft разработчик собирает обе версии (плюс еще, возможно, ARM), а Windows уже сам решает, какую загрузить пользователю. Отсюда следует очевидный вывод, что DLL-ка, которую мы будем внедрять в Modern-приложение, тоже должна быть в двух вариантах. Менее очевидный вывод в том, что приложение, которое будет «забрасывать» нашу DLL-ку в чужой процесс, тоже должно быть соответствующей битности. Нам ведь нужно запустить внутри чужого процесса поток, который загрузит нашу библиотеку и вызовет из неё какую-то функцию. Делается это через вызов CreateRemoteThread(Ex), а вот уже она требует одинаковой битности вызывающего и вызываемого процессов (а иначе как передавать адреса?).
Т.е. в итоге наш комплект будет состоять из двух «внедряемых» библиотек и двух «инжекторов»:
Доступ к внедряемой DLL
Как вы, возможно, знаете, Modern-приложения живут в своих песочницах, откуда они имеют доступ только к своей папке и некоторым другим доступным для них (ограниченным) ресурсам ОС. К примеру, Modern-приложение не может вот просто так взять и открыть любой файл с диска. Для нас это означает то, что попытавшись заставить чужое приложение подгрузить нашу библиотеку в своё адресное пространство — мы получим ошибку доступа. Это можно легко увидеть, воспользовавшись, к примеру, утилитой ProcMon.
Что же делать? Разрешить доступ к файлу внедряемой библиотеки для Modern-приложений. Для этого есть системная утилита icacls. Вот эта команда открывает доступ к файлу на диске для всех Modern-приложений:
Теперь ошибки доступа у нас не будет.
Линковка DLL-ки
Но не всё так просто. DLL-ка может иметь зависимости. И даже если вам кажется, что их нет — возможно, вы забываете о зависимости от VC++ Runtime Library. По умолчанию созданная в Visual Studio С++ библиотека предполагает динамическую линковку с рантайм-библиотекой С++. Это означает, что когда какое-то приложение захочет загрузить вашу библиотеку — функция LoadLibrary() первым делом увидит, что вам необходима, к примеру, библиотека msvcp90r.dll и попытается её найти, что вобщем-то, получится если речь идёт об обычном (не Modern) приложении и наличии соответствующего VC++ Redistribution Package. Что касается Modern-приложений, то порядок поиска DLL-ок совсем другой. Если коротко: будучи вызванной из Modern-приложения, LoadLibrary() не найдёт библиотеки VC++ Redistribution Package, даже если они до этого были успешно установлены.
Это немного неожиданно и слегка удивляет. Варианты выхода из ситуации:
Первые два способа крайне некрасивы (а без аккаунта администратора и невозможны), третий — сработает, но влияет на глобальную конфигурацию ОС, что тоже может быть чревато последствиями. Так что самый простой вариант — статическая линковка (ключи /MTd и /MT на вкладке Code Generation).
Цифровая подпись
Решив все вопросы с доступом к файлу DLL-ки может показаться, что у нас всё получилось — ProcMon не показывает никаких ошибок доступа. Но инъекция всё равно не работает — DLL-ка не появляется в списке загруженных модулей процесса. Что же пошло не так? На этот вопрос нам даёт ответ просмотр Windows Event Viewer.
Операционная система не хочет загружать нашу DLL-ку, поскольку она не подписана. В этом месте мне стало тревожно, поскольку я уж было подумал, что все модули, загружаемые в Modern-приложение, должны быть подписаны цифровой подписью одного издателя (разработчика). Но, к счастью, это не так. Windows требует от DLL-ки просто наличия валидной цифровой подписи, но не требует её соответствия подписи автора приложения. Т.е. мы можем взять подписанную DLL-ку от Хрома и забросить её в Spartan. Что же делать нам? Получить сертификат и подписывать нашу DLL-ку.
Да, это серьёзно усложняет работу. Но ведь в принципе, если мы разрабатываем нормальный продукт, а не какие-нибудь вирусы\трояны\малвари\кейлоггеры\локеры — у нас не будет проблем с получением сертификата и его использованием для подписи DLL-ки. Вообще, я считаю эту защиту одной из самых серьёзных шагов Microsoft по защите пользовательских данных и изоляции Modern-приложений в песочнице. Фактически от желающего пробросить мостик к приложению требуют предъявить документы и идентифицироваться, а это остановит 99% скрипт-киддисов.
Права доступа на канал связи
Ок, теперь наша DLL-ка внедрена в чужой процесс, она может вешать хуки на разные системные функции, делать что-то полезное, но… Сама она живёт в той же песочнице, что и родительский процесс, то есть ни передать данные «наружу», ни получать команды «извне» она не может, ведь для этого нужно создать канал связи, на который она по умолчанию не имеет прав. Как решать эту задачу я уже отдельно рассказывал вот в этой статье, а здесь просто упоминаю в качестве пункта, о котором важно не забыть.
Некоторое время назад я писал вводные статьи об использовании хуков на примерах библиотек Microsoft Detours и madCodeHook (если вы совсем не знакомы с хуками — это то, с чего можно начать). Описанных там техник достаточно для работы с обычными приложениями, но время не стоит на месте и вот сегодня у нас уже есть
32-битные и 64-битные приложения
Операционная система Windows бывает 32-разрядная и 64-разрядная. Соответственно, Modern-приложения тоже могут быть 32-битные либо 64-битные. При загрузке приложения в магазин Microsoft разработчик собирает обе версии (плюс еще, возможно, ARM), а Windows уже сам решает, какую загрузить пользователю. Отсюда следует очевидный вывод, что DLL-ка, которую мы будем внедрять в Modern-приложение, тоже должна быть в двух вариантах. Менее очевидный вывод в том, что приложение, которое будет «забрасывать» нашу DLL-ку в чужой процесс, тоже должно быть соответствующей битности. Нам ведь нужно запустить внутри чужого процесса поток, который загрузит нашу библиотеку и вызовет из неё какую-то функцию. Делается это через вызов CreateRemoteThread(Ex), а вот уже она требует одинаковой битности вызывающего и вызываемого процессов (а иначе как передавать адреса?).
Т.е. в итоге наш комплект будет состоять из двух «внедряемых» библиотек и двух «инжекторов»:
- InjectedLibrary32.dll
- InjectedLibrary64.dll
- Injector32.exe
- Injector64.exe
Доступ к внедряемой DLL
Как вы, возможно, знаете, Modern-приложения живут в своих песочницах, откуда они имеют доступ только к своей папке и некоторым другим доступным для них (ограниченным) ресурсам ОС. К примеру, Modern-приложение не может вот просто так взять и открыть любой файл с диска. Для нас это означает то, что попытавшись заставить чужое приложение подгрузить нашу библиотеку в своё адресное пространство — мы получим ошибку доступа. Это можно легко увидеть, воспользовавшись, к примеру, утилитой ProcMon.
Что же делать? Разрешить доступ к файлу внедряемой библиотеки для Modern-приложений. Для этого есть системная утилита icacls. Вот эта команда открывает доступ к файлу на диске для всех Modern-приложений:
icacls.exe some_file.ext /grant *S-1-15-2-1:(F)
Теперь ошибки доступа у нас не будет.
Линковка DLL-ки
Но не всё так просто. DLL-ка может иметь зависимости. И даже если вам кажется, что их нет — возможно, вы забываете о зависимости от VC++ Runtime Library. По умолчанию созданная в Visual Studio С++ библиотека предполагает динамическую линковку с рантайм-библиотекой С++. Это означает, что когда какое-то приложение захочет загрузить вашу библиотеку — функция LoadLibrary() первым делом увидит, что вам необходима, к примеру, библиотека msvcp90r.dll и попытается её найти, что вобщем-то, получится если речь идёт об обычном (не Modern) приложении и наличии соответствующего VC++ Redistribution Package. Что касается Modern-приложений, то порядок поиска DLL-ок совсем другой. Если коротко: будучи вызванной из Modern-приложения, LoadLibrary() не найдёт библиотеки VC++ Redistribution Package, даже если они до этого были успешно установлены.
Это немного неожиданно и слегка удивляет. Варианты выхода из ситуации:
- Копировать VC++ Runtime Library в папку Modern-приложения
- Копировать VC++ Runtime Library в %SystemRoot%\system32
- Прописать библиотеки в специальный раздел в реестре (HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs)
- Использовать статическую линковку (когда код VC++ Runtime Library включается в тело нашей DLL-ки)
Первые два способа крайне некрасивы (а без аккаунта администратора и невозможны), третий — сработает, но влияет на глобальную конфигурацию ОС, что тоже может быть чревато последствиями. Так что самый простой вариант — статическая линковка (ключи /MTd и /MT на вкладке Code Generation).
Цифровая подпись
Решив все вопросы с доступом к файлу DLL-ки может показаться, что у нас всё получилось — ProcMon не показывает никаких ошибок доступа. Но инъекция всё равно не работает — DLL-ка не появляется в списке загруженных модулей процесса. Что же пошло не так? На этот вопрос нам даёт ответ просмотр Windows Event Viewer.
Операционная система не хочет загружать нашу DLL-ку, поскольку она не подписана. В этом месте мне стало тревожно, поскольку я уж было подумал, что все модули, загружаемые в Modern-приложение, должны быть подписаны цифровой подписью одного издателя (разработчика). Но, к счастью, это не так. Windows требует от DLL-ки просто наличия валидной цифровой подписи, но не требует её соответствия подписи автора приложения. Т.е. мы можем взять подписанную DLL-ку от Хрома и забросить её в Spartan. Что же делать нам? Получить сертификат и подписывать нашу DLL-ку.
Да, это серьёзно усложняет работу. Но ведь в принципе, если мы разрабатываем нормальный продукт, а не какие-нибудь вирусы\трояны\малвари\кейлоггеры\локеры — у нас не будет проблем с получением сертификата и его использованием для подписи DLL-ки. Вообще, я считаю эту защиту одной из самых серьёзных шагов Microsoft по защите пользовательских данных и изоляции Modern-приложений в песочнице. Фактически от желающего пробросить мостик к приложению требуют предъявить документы и идентифицироваться, а это остановит 99% скрипт-киддисов.
Права доступа на канал связи
Ок, теперь наша DLL-ка внедрена в чужой процесс, она может вешать хуки на разные системные функции, делать что-то полезное, но… Сама она живёт в той же песочнице, что и родительский процесс, то есть ни передать данные «наружу», ни получать команды «извне» она не может, ведь для этого нужно создать канал связи, на который она по умолчанию не имеет прав. Как решать эту задачу я уже отдельно рассказывал вот в этой статье, а здесь просто упоминаю в качестве пункта, о котором важно не забыть.
afiskon
Очень интересно, спасибо!