Содержание статьи
В системе крутится огромное количество процессов: системные вроде explorer.
, RunTimeBroker.
, а также твои любимые браузер, Steam и МалварьПисатьБыстроСтудия.ехе. Большинство из них хранят молчание и не делятся никакой информацией с внешним миром — считай, такие процессы‑интроверты вроде нас с тобой. Однако бывает и иначе. Некоторые процессы должны передавать данные своим сородичам: информацию о состоянии CPU, разрешении экрана, нажимаемых символах на клавиатуре.
Простейший способ взаимодействия между двумя общими процессами — создание файла. Один процесс пишет, другой читает. Впрочем, это не самый удобный способ общения, правда? Здесь возникают проблемы с синхронизацией, атомарным доступом, настройкой дескрипторов безопасности...
Поэтому разработчики Windows придумали чуть более удобный способ передачи данных и изобрели огромное количество сущностей, позволяющих передавать данные между процессами. Одна из этих сущностей — именованный канал (Named Pipe).
Что такое 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(
для проверки того, нет ли в пайпе новых данных.
Изучение доступных пайпов
В системе одновременно работает множество именованных каналов. В следующих разделах будем их активно эксплуатировать, поэтому логично будет научиться находить работающие пайпы!
Process Hacker
Самый простой способ обнаружения пайпов — воспользоваться красивым GUI в Process Hacker.

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(
и в другом контексте: например, перехватив чужой хендл, как я описывал в статье «Ломаем дескрипторы! Как злоупотреблять хендлами в Windows».
Еще есть чуть более сложный вариант — сначала получить все хендлы, а потом среди них находить пайпы. Я бы гордо игнорировал этот метод, однако у меня в заметках сохранен такой код, значит, кому‑то когда‑то это понадобилось.
PowerShell
Согласись, что автоматизация и C++ — не самые близкие вещи. Исследовать пайпы можно и через PowerShell, можно даже сделать красивый вывод дескрипторов.
# Ко всем пайпам
Get-ChildItem \\.\pipe\ | ForEach-Object -ErrorAction SilentlyContinue GetAccessControl
# К конкретному пайпу
Get-ChildItem \\.\pipe\eventlog | ForEach-Object -ErrorAction SilentlyContinue GetAccessControl

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

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

Имперсонация клиентов
Предлагаю начать с базы. Серверы именованных каналов имеют право олицетворять подключенные клиенты. Причем если клиент не переопределял уровень имперсонации, то ему будет назначен стандартный — SecurityImpersonation. Такого уровня достаточно для запуска cmd.
от лица пользователя.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»