Злая труба. Используем Named Pipes при атаке на Windows

Содержание статьи

  • Что такое Pipe
  • Пример клиента и сервера
  • Изучение доступных пайпов
  • Process Hacker
  • C++
  • PowerShell
  • IO Ninja
  • PipeViewer
  • Имперсонация клиентов
  • Чейн с SeImpersonate
  • Скрытое чтение данных
  • Гонка пайпов
  • Выводы

В 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. Короче, если при соз­дании пай­па ты не ука­зал дес­крип­тор безопас­ности, то дан­ные из это­го пай­па смо­жет читать кто угод­но.

Для работы с пай­пом при­меня­ются еще некото­рые фун­кции (ссыл­ки на докумен­тацию):

  • ConnectNamedPipe();
  • WaitNamedPIpe();
  • DisconnectNamedPipe().

Пер­вая фун­кция дает воз­можность сер­веру ждать под­клю­чения кли­ента (кли­ент под­клю­чает­ся «проз­рачно» — ему дос­таточ­но ука­зать пайп в вызове 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 

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 \.pipeeventlog | ForEach-Object -ErrorAction SilentlyContinue GetAccessControl

При­мер вывода дес­крип­тора 

IO Ninja

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

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

PipeViewer

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

Ин­терфейс PipeViewer 

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

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

Источник: xakep.ru

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *