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

В сис­теме кру­тит­ся огромное количес­тво про­цес­сов: сис­темные вро­де explorer.exe, RunTimeBroker.exe, а так­же твои любимые бра­узер, 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.

Достаточно ввести в поисковой строке pipe
Дос­таточ­но ввес­ти в поис­ковой стро­ке pipe
 

C++

Для более глу­боко­го кон­тро­ля и написа­ния собс­твен­ных инс­тру­мен­тов было бы неп­лохо соз­дать пол­ноцен­ную тул­зу для обна­руже­ния работа­ющих пай­пов. Здесь нам подой­дет осо­бен­ность име­нова­ния каналов — все они начина­ются с .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».

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

 

PowerShell

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

# Ко всем пайпам
Get-ChildItem \\.\pipe\ | ForEach-Object -ErrorAction SilentlyContinue GetAccessControl
# К конкретному пайпу
Get-ChildItem \\.\pipe\eventlog | ForEach-Object -ErrorAction SilentlyContinue GetAccessControl
Пример вывода дескриптора
При­мер вывода дес­крип­тора
 

IO Ninja

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

Пример использования IO Ninja
При­мер исполь­зования IO Ninja
 

PipeViewer

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

Интерфейс PipeViewer
Ин­терфейс PipeViewer
 

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

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

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии