Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
В Windows есть много средств межпроцессного взаимодействия. Одно из них — именованные каналы, в народе — пайпы. Давай попробуем направить всю мощь ввода‑вывода на благо пентеста и научимся злоупотреблять этим механизмом сообщений. Пусть никто не уйдет без эскалации привилегий!
В системе крутится огромное количество процессов: системные вроде explorer.exe
, RunTimeBroker.exe
, а также твои любимые браузер, Steam и МалварьПисатьБыстроСтудия.ехе. Большинство из них хранят молчание и не делятся никакой информацией с внешним миром — считай, такие процессы‑интроверты вроде нас с тобой. Однако бывает и иначе. Некоторые процессы должны передавать данные своим сородичам: информацию о состоянии CPU, разрешении экрана, нажимаемых символах на клавиатуре.
Простейший способ взаимодействия между двумя общими процессами — создание файла. Один процесс пишет, другой читает. Впрочем, это не самый удобный способ общения, правда? Здесь возникают проблемы с синхронизацией, атомарным доступом, настройкой дескрипторов безопасности…
Поэтому разработчики Windows придумали чуть более удобный способ передачи данных и изобрели огромное количество сущностей, позволяющих передавать данные между процессами. Одна из этих сущностей — именованный канал (Named Pipe).
Пайп представляет собой объект типа FILE_OBJECT
, управляемый специальной файловой системой с именем NPFS — Named Pipe File System. Пайп позволяет писать и считывать из себя данные разным процессам, что и решает задачу их взаимодействия. На сетевом уровне передача данных происходит поверх протокола SMB.
Создание именованного канала происходит с помощью функции CreateNamedPipe().
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()
для проверки того, нет ли в пайпе новых данных.
В системе одновременно работает множество именованных каналов. В следующих разделах будем их активно эксплуатировать, поэтому логично будет научиться находить работающие пайпы!
Самый простой способ обнаружения пайпов — воспользоваться красивым GUI в Process Hacker.
Достаточно ввести в поисковой строке pipe
Для более глубокого контроля и написания собственных инструментов было бы неплохо создать полноценную тулзу для обнаружения работающих пайпов. Здесь нам подойдет особенность именования каналов — все они начинаются с .pipe
. В действительности это отдельное пространство имен. По нему можно пробегаться так же, как и при поиске обычных файлов.
#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()
и в другом контексте: например, перехватив чужой хендл, как я описывал в статье «Ломаем дескрипторы! Как злоупотреблять хендлами в Windows».
Еще есть чуть более сложный вариант — сначала получить все хендлы, а потом среди них находить пайпы. Я бы гордо игнорировал этот метод, однако у меня в заметках сохранен такой код, значит, кому‑то когда‑то это понадобилось.
Согласись, что автоматизация и C++ — не самые близкие вещи. Исследовать пайпы можно и через PowerShell, можно даже сделать красивый вывод дескрипторов.
# Ко всем пайпам Get-ChildItem \.pipe | ForEach-Object -ErrorAction SilentlyContinue GetAccessControl # К конкретному пайпу Get-ChildItem \.pipeeventlog | ForEach-Object -ErrorAction SilentlyContinue GetAccessControl
Пример вывода дескриптора
Но самый крутой вариант, особенно с целью найти уязвимость, — это IO Ninja. Эта утилита позволяет выводить максимально подробную информацию о пайпах и предоставляет все необходимые данные для ресерча.
Пример использования IO Ninja
С IO Ninja может смело посоревноваться PipeViewer. У инструмента приятный графический интерфейс, автоматический вывод дескрипторов и функция PipeChat, позволяющая установить быстрое соединение с каналом.
Интерфейс PipeViewer
Предлагаю начать с базы. Серверы именованных каналов имеют право олицетворять подключенные клиенты. Причем если клиент не переопределял уровень имперсонации, то ему будет назначен стандартный — SecurityImpersonation. Такого уровня достаточно для запуска cmd.exe
от лица пользователя.
Источник: xakep.ru