Содержание статьи
В системе крутится огромное количество процессов: системные вроде 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.
от лица пользователя.
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее