В статье речь пойдёт о недокументированной части Windows – это внутренние порты обмена сообщениями ALPC, что подразумевает «Advanced Local Procedure Call», или расширенный вызов локальных процедур. Механизм пришёл на смену устаревшему LPC, который имел два основных недостатка: во-первых он был синхронным (программным потокам абонентов приходилось останавливаться в ожидании ответа), а во-вторых был плохо защищён, что отрицательно влияло на подсистему безопасности в целом. Поэтому начиная с Vista-NT6 инженеры избавились от проприетарного LPC, в пользу асинхронного режима ALPC. По понятным причинам Microsoft не раскрывает всех тайн его реализации, закрыв на амбарный замок функции поддержки в нативную либу Ntdll.dll. Только они не учли, что неизведанная область наоборот подогреет интерес исследователей во всём мире, а что из этого получилось – предлагаю ознакомиться ниже.
Оглавление:
- Введение в ALPC
- Архитектура портов коммуникации
- Формат передаваемых сообщений
- Практика – клиент/серверное приложение
- Постфикс
1. Введение в ALPC
Microsoft ввела механизм локального вызова процедур LPC для обеспечения связи с тем, что в NT называют подсистемой. Поскольку переход был от IBM-OS/2, то помимо основной подсистемы Win32, инженерам пришлось тащить за собой хвост в виде дополнительной POSIX (компонент SUA, subsystem for unix application), и замыкала цепочку старушка OS/2. Каждую из них поддерживал свой индивидуальный набор библиотек и функций программного интерфейса. Позже, по дороге к WinXP естественный отбор отправил к праотцам бесполезный аппендикс OS/2.
Но это только по словам самих Microsoft, а на деле - реализация той-же POSIX была кривой, чего не хотел признавать отдел маркетинга мягких. В общем случае, эволюция растасовала карты следующим образом: на Win98 были все/три подсистемы, от WinXP до Win7 нет OS/2 с пародией на POSIX (читай осталась только Win32), зато с выходом на сцену Win10, инженеры полностью переработали SUA и вернули её на место, тупо разрекламировав как «Windows Subsystem for Linux» WSL.
Если открыть раздел реестра
HKLM\System\CurrentControlSet\Control\SessionManager\SubSystem
, можно обнаружить в нём поддержку подсистем текущей ОС. Ключ «Required» задаёт порядок штатной загрузки, а «Optional» указывает на дополнительный вызов по требованию. Обратите внимание, что поддержка привычной нам подсистемы Win32 обеспечивается файлом «Client-Server-Runtime-SubSystem» сsrss.exe, который выступает в роли основного Internal-сервера внутри операционной системы:Win проектировалась как клиент-серверная ОС (не путать с серверами сети интернет). Локальные серверы подсистемы Win32 – это процессы пользовательского уровня, которые работают в контексте безопасности ОС. Они запускаются с правами пользователя System в закрытой сессии(0), и следовательно защищены от клиентского доступа из сессии(1). Помимо csrss.exe, в Win имеются ещё несколько локальных серверов – вот наиболее известные из них:
1. Session Manager (smss.exe) – отслеживает сеансы входа в систему.
2. Local Security Authority (lsass.exe) – управляет маркерами безопасности Token пользователей.
3. Service Host (svchost.exe) – основной хост-процесс для загрузки системных служб (как правило в нулевую сессию).
Связь клиентских процессов и серверами Win32 происходит строго по защищённым каналам – она активируется вызовом локальных процедур LPC, которые специально были разработаны для этих целей. Кроме LPC, в широких штанинах Win имеется и механизм удалённых вызовов процедур по сети «Remote Procedure Call» RPC (см.протокол RDP), но по прибытию на узел получателя после сетевого стека, в конечном итоге и он садится на каналы транспорта LPC. Таким образом порты LPC/ALPC являются основным механизмом связи клиентов с локальными серверами в современных ОС, и по факту относятся к одному из методов взаимодействия между процессами IPC (inter-process communication), таких как Pipe, или Socket.
Механизм поддерживается объектом ядра «ALPC PORT». В силу своей асинхронной природы, порты могут обслуживать сразу с несколько клиентов одновременно, что является несомненным их плюсом. Обратите внимание, что объект «PORT» нельзя открыть привычной нам функцией CreateFile(), как например объект «DEVICE» – к серверным портам можно только подключаться.
Внутри операционной системы порты носят массовый характер, а потому представляют цель для атак. Чтобы убедиться в их значимости, достаточно запустить утилиту «WinObj» М.Руссиновича, или «ObjectExplorer» от Four-F, и отобразить в ней сначала корневую папку, а затем и дир «RPC Control». Если-же стоит задача программно найти и перечислить порты ALPC, можно позвать на помощь пару функций NtOpenDirectoryObject() + NtQueryDirectoryObject():
К сожалению эти утилиты не дают нам исчерпывающей информации о портах, и приходится кряхтя слазить с печки, чтобы подключить отладчик WinDbg. Когда мы знаем имя порта (пусть будет SmApiPort из списка выше), то передав его командам
!object
и !alpc
можно вытянуть из ядра буквально всю описывающую порт структуру – так это выглядит на моём экспериментальном узле Win7:
Код:
0: kd> !object \SmApiPort
Object: fffffa8004bebe60 Type: (fffffa80039fc080) ALPC Port
Object Header : fffffa8004bebe30
Directory Object: fffff8a000004c70 Name: SmApiPort
0: kd> !alpc /p fffffa8004bebe60
Port @ fffffa8004bebe60
Type : ALPC_CONNECTION_PORT
CommunicationInfo : fffff8a000753430
ConnectionPort : fffffa8004bebe60
ClientCommunicationPort : 0000000000000000
ServerCommunicationPort : 0000000000000000
OwnerProcess : fffffa8004bd0300 (smss.exe) <----// Имя сервера Win32 (родитель порта)
SequenceNo : 0x00000005 (5)
CompletionPort : fffffa8004bd9bc0
CompletionList : 0000000000000000
MessageZone : 0000000000000000
ConnectionPending : No
Disconnected : No
Closed : No
FlushOnClose : Yes
ReturnExtendedInfo : No
Waitable : No
Security : Static
Wow64CompletionList : No
Main queue is empty.
Large queue is empty.
Pending queue is empty.
Canceled queue is empty.
2. Архитектура портов коммуникации
После вводной части рассмотрим внутреннее устройство ALPC-портов. Повторюсь, что интерфейс является внутренним, а потому недокументирован инженерами Microsoft от слова совсем. Из того-что есть можно порекомендовать бестселлер М.Руссиновича и Д.Соломона «Внутреннее устройство Windows» издание(6), часть(1), страница(254).
Значит, чтобы принимать запросы от клиентов, сервер должен создать себе именованный порт функцией NtAlpcCreatePort(), а клиент затем подключается к нему функцией NtAlpcConnectPort(). Прототипы этих функций описываются в сишном хидере ntalpcapi.h, электронная версия которого лежит
Ссылка скрыта от гостей
. При вызове первой функции, ядро Ntoskrnl.exe создаёт представление порта в лице структуры ALPC_PORT – в дальнейшем все обращения к порту будут перенаправляться только в эту структуру. Просмотреть её мемберов можно в отладчике WinDbg (я оставил только приоритетные для нас поля):
Код:
0: kd> dt _alpc_port
nt!_ALPC_PORT
+0x010 CommunicationInfo : Ptr64 _ALPC_COMMUNICATION_INFO
+0x018 OwnerProcess : Ptr64 _EPROCESS
+0x020 CompletionPort : Ptr64 Void
+0x038 PortContext : Ptr64 Void
+0x040 StaticSecurity : struct _SECURITY_CLIENT_CONTEXT
+0x088 MainQueue : struct _LIST_ENTRY
+0x098 PendingQueue : struct _LIST_ENTRY
+0x0a8 LargeMessageQueue : struct _LIST_ENTRY
+0x0b8 WaitQueue : struct _LIST_ENTRY
+0x0d0 PortAttributes : struct _ALPC_PORT_ATTRIBUTES
+0x168 SequenceNo : Int4B
Для удобства будем продвигаться траекторией пьяного гонщика снизу-вверх..
Поле SequenceNo играет роль счётчика обращений к порту, далее указываются его атрибуты (вернёмся позже), после которых видим связанные списки на 4 очереди сообщений Queue – мотаем их на ус. Во-вложенной структуре Security лежат атрибуты безопасности, которыми можно разграничить права доступа к данному объекту «PORT». Следущее поле PortContext так-же связано с безопасностью – это просто число по типу дескриптора (сервер назначает его каждому клиенту отдельно). Поле CompletionPort имеется смысл только в асинхронном режиме ALPC, что даёт возможность серверу просигналить об окончании обработки сообщения клиенту (см.ниже формат сообщений).
Так, мелкими перебежками мы подобрались к последним\первым двум полям структуры, которые раскрывают всю суть ALPC-портов.
Owner в переводе означает родитель и характеризует того, кто создал данный порт. Как видим в поле лежит указатель на структуру ядра
EPROCESS
, где найдём полный паспорт родительского процесса, включая его имя, идентификаторы Pid/Tid и прочее. В большинстве случаях это серверы подсистемы Win32: csrss-lsass-smss-svchost.exe.
Код:
0: kd> dt _eprocess
ntdll!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x168 CreateTime : Uint8B
+0x180 UniqueProcessId : Ptr64
+0x188 ActiveProcessLinks : _LIST_ENTRY
+0x1d8 VirtualSize : Uint8B
+0x200 ObjectTable : Ptr64 _HANDLE_TABLE
+0x208 Token : _EX_FAST_REF
+0x238 PhysicalVadRoot : Ptr64 _MM_AVL_TABLE
+0x270 SectionBaseAddress : Ptr64
+0x2d0 Session : Ptr64
+0x2d8 ImageFileName : [15] UChar
+0x2e7 PriorityClass : UChar
+0x300 ThreadListHead : _LIST_ENTRY
+0x310 SecurityPort : Ptr64
+0x320 ActiveThreads : Uint4B
+0x330 Peb : Ptr64 _PEB
+0x390 Vm : _MMSUPPORT
+0x440 VadRoot : _MM_AVL_TABLE
+0x480 AlpcContext : _ALPC_PROCESS_CONTEXT
+0x4c8 SequenceNumber : Uint8B
+0x4e8 HighestUserAddress : Ptr64
0: kd>
Ну и наконец первое поле с говорящим за себя названием CommunicationInfo.
В нём приписан линк на структуру с таким прототипом, которая требует некоторых объяснений:
Код:
0: kd> dt _alpc_communication_info
nt!_ALPC_COMMUNICATION_INFO
+0x000 ConnectionPort : Ptr64 _ALPC_PORT
+0x008 ServerCommunicationPort : Ptr64 _ALPC_PORT
+0x010 ClientCommunicationPort : Ptr64 _ALPC_PORT
+0x018 CommunicationList : struct _LIST_ENTRY
+0x028 HandleTable : struct _ALPC_HANDLE_TABLE
Здесь отчётливо видно, что один ALPC-порт фактически включает в себя ещё 2 порта коммуникации, и это создаёт путаницу. Всё дело в том, что порты ALPC бывают двух типов:
1. Connection Port – глобальный «Порт подключения» к серверу. К этому порту могут коннектиться несколько клиентов одновременно, просто указав его имя. Часто один сервер создаёт себе сразу несколько именованных портов, чтобы сортировать клиентов по их функциональным требованиям. Например тот-же сервер svchost.exe может как запускать\останавливать системные сервисы, так и быть посредником в вопросах электропитания.
2. Communication Port – внутренний безымянный «Порт связи» клиента с сервером. Инженеры построили схему так, что в момент подключения клиента, сервер создаёт для него связанную пару своих портов, которые обозначены в полях структуры "Server\ClientCommunicationPort". Для следующего клиента из выхлопной трубы сервера выйдет ещё одна пара портов-связи, и т.д. Такой расклад делает невозможным прямое общение двух клиентов между собой – сервер обязательно должен знать, что происходит в зоне его ответственности, пропуская через себя весь трафик. В поле "CommunicationList" лежит связанный список
LIST_ENTRY
, где в замкнутый круг паравозиком выстроены все клиенты данного порта.Теперь посмотрим на общую архитектуру механизма ALPC с высоты птичьего полёта..
Значит сервер создаёт себе порт-подключения через NtAlpcCreatePort(), а клиент пытается к нему пришвартоваться вызовом NtAlpcConnectPort(). Однако на входе его встречает функция сервера NtAlpcAcceptConnectPort(), которая может как принять клиента на борт, так и отказать, если он не пройдёт фейс-контроль охраны Security (см.структуру порта выше).
Если всё гуд, сервер возвращает клиенту дескриптор Handle своего порта "Connection", выделяет ему отдельный поток Thread для обслуживания, и сразу создаёт пару безымянных портов-связи "Communication". На данном этапе клиент с сервером могут уже «выпить за мировую», т.к. порт считается активным. Теперь на обоих концах связи абоненты вызывают свои функции NtAlpcSendWaitReceivePort(), которая (как и следует из её названия) выполняет сразу три связанные по смыслу задачи – отправка сообщений получателю Send, ожидание ответа Wait, и приём от отправителя Receive. Отметим, что во-времена устаревшего ныне LPC, для этого потребовались-бы три отдельные функции API.
В качестве разминки можно ещё раз собрать в отладчике инфу о произвольном порте, и думаю на этот раз всё станет намного прозрачней.
Код:
0: kd> !object \Sessions\1\Windows\ApiPort
Object: fffffa8005520a60 Type: fffffa80039fc080 ALPC Port
Object Header : fffffa8005520a30 <----------------: Object -0x30 = ObjectHeader
Directory Object: fffff8a000ef2b70 Name: ApiPort
0: kd> !alpc /p fffffa8005520a60
Port @ fffffa8005520a60
Type : ALPC_CONNECTION_PORT
CommunicationInfo : fffff8a000f1d990
ConnectionPort : fffffa8005520a60 ------+ Три порта сервера,
ClientCommunicationPort : 0000000000000000 +---> ... без активных клиентов
ServerCommunicationPort : 0000000000000000 ------+ ... на данный момент.
OwnerProcess : fffffa8004f9e700 (csrss.exe) <---- SERVER ----------
SequenceNo : 0x000053DC (21468) <---------------- Всего подключений
CompletionPort : 0000000000000000
MessageZone : 0000000000000000
ConnectionPending : No
Disconnected : No
Closed : No
FlushOnClose : Yes
ReturnExtendedInfo : No
Waitable : No
Security : Static <--------- Атрибуты безопасности = дефолт
Main queue is empty.
Large queue is empty.
Pending queue is empty.
Canceled queue is empty.
;//--------------------------------------
0: kd> dt _object_header fffffa8005520a30 <----- Адрес лежит в заголовке выше
nt!_OBJECT_HEADER
+0x000 PointerCount : 107
+0x008 HandleCount : 1
+0x010 Lock : _EX_PUSH_LOCK
+0x018 TypeIndex : 0x24
+0x019 TraceFlags : 0
+0x01a InfoMask : 0xE
+0x01b Flags : 0
+0x020 ObjectCreateInfo : 0xfffff800`02c01e40 _OBJECT_CREATE_INFORMATION
+0x028 SecurityDescriptor : 0xfffff8a0`097c2599 Void <------- Сбросить младшую тетраду!
+0x030 Body : _QUAD |
|
0: kd> !sd 0xfffff8a0`097c2590 <------- SecurityDescriptor -----------------+
->Revision: 0x1
->Control : 0x8004
SE_DACL_PRESENT
SE_SELF_RELATIVE
->Owner : S-1-5-32-544
->Group : S-1-5-18 <---------- Кому разрешён доступ
->Dacl : ->AclRevision: 0x2
->Dacl : ->AclSize : 0x30
->Dacl : ->AceCount : 0x2
->Dacl : ->Ace[0]: ->AceType : ACCESS_ALLOWED_ACE_TYPE
->Dacl : ->Ace[0]: ->AceSize : 0x14
->Dacl : ->Ace[0]: ->Mask : 0x00120001 <------ Разрешённые операции
->Dacl : ->Ace[0]: ->SID : S-1-1-0 ^^^^ см.свойства порта в ObjectExplorer
->Dacl : ->Ace[1]: ->AceType : ACCESS_ALLOWED_ACE_TYPE
->Dacl : ->Ace[1]: ->AceSize : 0x14
->Dacl : ->Ace[1]: ->Mask : 0x00120001
->Dacl : ->Ace[1]: ->SID : S-1-5-12
->Sacl : is NULL
0: kd>
2.1. Атрибуты портов ALPC
Ещё один важный аспект представляют атрибуты порта подключения «ALPC-Connection» (у безымянных портов их нет).
При создании порта мы можем отдельно задать характеристики как самому объекту в пространстве имён, так и непосредственно его ALPC-порту. Об этом явно свидетельствует прототип функции:
C++:
NTSTATUS
NtAlpcCreatePort( <--------------;// ОК = 0 (Status_Success)
Out PortHandle dq 0 ;// линк на переменную, куда вернётся дескриптор порта
In opt ObjectAttributes dq 0 ;// линк на структуру OBJECT_ATTRIBUTES
In opt PortAttributes dq 0 ;// линк на структуру ALPC_PORT_ATTRIBUTES
);
;//---------------------------
align 8
struct OBJECT_ATTRIBUTES
Length dq sizeof.OBJECT_ATTRIBUTES
RootDirectory dq 0 ;// если нуль, в поле ниже указать полный путь
ObjectName dq 0 ;// линк на структуру UNICODE_STRING с именем объекта/порта
Attributes dq 0 ;// 0, или OBJ_EXCLUSIVE(0x20) = монопольный доступ (другие процессы не смогут открыть)
SecurityDesc dq 0 ;// см. https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_security_descriptor
SecurityOfService dq 0 ;// 0 = дефолт
ends
struct ALPC_PORT_ATTRIBUTES
Flags dd 0 ;// см. перечисление ниже
SecurityQos dd 0,0,0 ;// 0, или структура SECURITY_QUALITY_OF_SERVICE
MaxMessageLength dq 0 ;// макс размер сообщений = 64 Кб (режим DualBuff)
MemoryBandwidth dq 0 ;// 0 = дефолт
MaxPoolUsage dq 0 ;// 0 = дефолт
MaxSectionSize dq 0 ;// 0 = дефолт
MaxViewSize dq 0 ;// 0, или размер общей памяти ALPC-Blob (режим SectionMap)
MaxTotalSectionSize dq 0 ;// 0 = дефолт
DupObjectTypes dd 0 ;// 0 = дефолт
Reserved dd 0 ;// выравнивание структуры на 8-байтную границу
ends
;// Flags в структуре ALPC_PORT_ATTRIBUTES
;//-------------------------------------------
ALPC_PORTFLG_NONE = 0 ;// дефолт
ALPC_PORTFLG_ALL = 0x01ef0000 ;// Все флаги скопом (сервер проигнорит не поддерживаемые)
ALPC_PORTFLG_ALLOWIMPERSONATION = 0x00010000 ;// Разрешить серверу выдавать себя за клиента
ALPC_PORTFLG_ALLOW_LPC_REQUESTS = 0x00020000 ;// Разрешить серверу принимать запросы от клиентов
ALPC_PORTFLG_WAITABLE_PORT = 0x00040000 ;// Порт с синхронизацией (семафоры\ивенты)
ALPC_PORTFLG_ALLOW_DUP_OBJECT = 0x00080000 ;// Разрешить дубликат объекта порта
ALPC_PORTFLG_LRPC_WAKE_POLICY1 = 0x00200000 ;// Win10+
ALPC_PORTFLG_LRPC_WAKE_POLICY2 = 0x00400000 ;// ^^^^
ALPC_PORTFLG_LRPC_WAKE_POLICY3 = 0x00800000 ;// ^^^^
ALPC_PORTFLG_DIRECT_MESSAGE = 0x01000000 ;// ^^^^ прямая очередь сообщений, вместо основной Main
Судя по данному описанию, инженеры предоставили нам неплохой выбор свойств порта-подключения, но на практике даже сами майки не используют их в полной мере, сбрасывая большую часть в дефолт. Из наиболее привлекательных имеем флаг
OBJ_EXLUSIVE = 20h
, после чего объект нашего порта никто не сможет уже открыть по имени, пока мы будем удерживать его. Другими словами, к такому порту с монопольным доступом в любой момент времени сможет подключаться лишь один из многочисленных клиентов, а остальные будут ждать, когда текущий освободит его функцией NtAlpcDisconnectPort().А вот прототип функции NtAlpcAcceptConnectPort(), которая играет роль амбала-секьюрити на входе в бар. Большинство её параметров опциональны, а значит при установке значений в нуль, ядро сбросит их в дефолт. Обычно для своего сервера мы просто указываем первые три аргумента, далее линк на структуру
PORT_MESSAGE
, и важно выставить в единицу последнее значение Accept, чтобы заставить сервак реагировать на наши запросы:
C-подобный:
NTSTATUS
NtAlpcAcceptConnectPort(
Out PortHandle dq 0 ;// линк, куда получим дескриптор внутр.порта-связи
In ConnectionPortHndl dq 0 ;// дескриптор от NtAlpcCreatePort() порта-подключения
In opt Flags dq 0 ;// -- ALPC_SYNC_CONNECTION(0x20000) = двухсторонний обмен
In opt ObjectAttributes dq 0 ;// линк на OBJECT_ATTRIBUTES
In opt PortAttributes dq 0 ;// линк на ALPC_PORT_ATTRIBUTES
In opt PortContext dq 0 ;// линк на переменную Void
In_read ConnectionRequest dq 0 ;// линк на PORT_MESSAGE
InOut opt ConnectionMsgAttr dq 0 ;// линк на ALPC_MESSAGE_ATTRIBUTES
In AcceptConnection dq 0 ;// BOOL: 1 = принять запрос на подключение, 0 = отказать
);
Глобальным так-же является Flags. Если не выставить в нём константу
ALPC_SYNC_CONNECTION
, то при Accept=1 сервер принимать-то наши сообщения будет, а вот отвечать на них по схеме DualBuffer уже нет. По сути Sync включает тумблером устаревший синхронный режим LPC, который изначально реализован как полный дуплекс с транспортом сообщений в обе стороны. Модель-же нового асинхронного обмена ALPC рассматривается ниже.2.2. Порт на стороне клиента
Будем считать, что с сервером разобрались – теперь пару слов о портах клиента.
Как упоминалось выше, клиент посылает запрос на подключение к серверу своей функцией NtAlpcConnectPort() с прототипом ниже. Здесь тоже инженеры упростили нам жизнь, поскольку 8 из 11-ти параметров опциональны и могут принимать значения нуль. Даже Flags можно сбросить, однако дефолтным режимом на сервере является именно асинхронный ALPC, а потому если не указать нём
ALPC_SYNC_CONNECTION=0x20000
, на ответы сервера можно не рассчитывать, т.к. связь станет односторонней.
C-подобный:
NTSTATUS
NtAlpcConnectPort(
Out PortHandle dq 0 ;// линк на переменную, куда получим дескриптор сервера
In PortName dq 0 ;// линк на UNICODE_STRING с именем порта сервера
In opt ObjectAttributes dq 0 ;// 0, или линк на OBJECT_ATTRIBUTES
In opt PortAttributes dq 0 ;// 0, или линк на ALPC_PORT_ATTRIBUTES
In Flags dq 0 ;// <--- константа ALPC_SYNC_CONNECTION
In opt RequiredServerSid dq 0 ;// 0, или линк на SID
InOut opt ConnectionMessage dq 0 ;// линк на PORT_MESSAGE
InOut opt BufferLength dq 0 ;// линк на переменную с размером буфера
InOut opt OutMessageAttr dq 0 ;// 0, или линк на ALPC_MESSAGE_ATTRIBUTES
InOut opt InMessageAttr dq 0 ;// 0, или линк на ALPC_MESSAGE_ATTRIBUTES
In opt Timeout dq 0 ;// 0, или линк на переменную с тайм-аутом
);
NTSTATUS
NtAlpcDisconnectPort( <-----------------;// Отключиться от порта
In PortHandle dq 0 ;// его хендл
In Flags dq 0 ;// 0
);
NTSTATUS
NtAlpcQueryInformation( <-----------------;// Некоторая инфа о портах
In opt PortHandle dq 0 ;// дескриптор порта
In PortInfoClass dq 0 ;// одна из констант ниже ALPC_PORT_INFORMATION_CLASS
InOut PortInfoBuff dq 0 ;// линк на приёмный буфер
In BuffLength dq 0 ;// размер приёмного буфа
Out opt ReturnLength dq 0 ;// 0, или линк на переменную, куда вернётся размер данных
);
;// PortInfoClass для NtAlpcQueryInformation()
;// https://ntdoc.m417z.com/alpc_port_information_class
;//----------------------------------------------------
AlpcBasicInfo = 0
AlpcPortInfo = 1
AlpcAssociateCompletionPortInfo = 2
AlpcConnectedSIDInfo = 3
AlpcServerInfo = 4
AlpcMessageZoneInfo = 5
AlpcRegisterCompletionListInfo = 6
AlpcCompletionListRundownInfo = 10
AlpcWaitForPortReferences = 11
3. Формат и атрибуты сообщений
Сообщения Message это то, ради чего вообще придумали коммуникацию ALPC. Однако отсутствие документации делает своё дело, и приходится искать состояние нирваны опытным путём (благо есть отладчик). ALPC поддерживает целых три режима обмена сообщениями, которые в корень отличаются друг от друга.
1. Режим «DualBuffer». Дефолтная схема механизма ALPC с двойной буферизацией – у сервера свой буф в памяти ядра, а на стороне клиента свой в пространстве юзера. Макс.размер этих буферов возвращает функция AlpcMaxAllowedMessageLength(). Для устаревшего LPC лимит был выставлен строго на 256-байт, зато в ALPC его увеличили сразу до 64К. В этой схеме ядро копирует данные из своего буфера в юзер или наоборот, в зависимостри от направления передачи.
2. Режим «SectionMap». Используется для данных больше 64К. NtAlpcCreatePortSection() создает секцию во-внутреннем механизме порта ALPC, а полученный на выходе дескриптор передаётся далее функции NtAlpcCreateSectionView(), которая и отображает данные в общей сервера с клиентом памяти. Детали обсуждаются ниже.
3. Режим «Direct», или сквозной обмен. Фишка работает только начиная с Win10, а потому не подходит для кросс-платформенных приложений. Найти что-то по теме мне так и не удалось, но известно, что здесь задействуется т.н. «зона сообщений» с таблицей описания MDL (Memory Descriptor List). Механизм работает на уровне физических фреймов памяти (а не вирт.страниц), которые отображаются в адресном пространстве абонентов связи.
В свою очередь, внутри каждого из перечисленных выше режимов можно передавать три различных типа сообщений:
1. Синхронный запрос LPC. Клиент блокируется в ожидании до тех пор, пока сервер не ответит ему. При вызове NtAlpcSendWaitReceivePort() необходимо указать флаг
ALPC_MSGFLG_SYNC_REQUEST
, а так-же адрес приёмного буфера сообщений.
2. Асинхронный запрос ALPC. Односторонний с флагом нуль. Абоненты не блокируется, а просто делают Send и продолжают заниматься своими делами (если они есть). Сервер подтверждает только факт приёма сообщений, но не отправляет никаких данных в ответ. В качестве шлюза подтверждения абонент может использовать или объекты событий типа семафора\ивента, или-же специально предназначенный для этих целей системный порт завершения операции «Completion Port». Здесь выбор уже возлагается на инициатора асинхронного обмена ALPC.
3. Запросы датаграмм. Вариант совсем пассивный, и похож на сетевые пакеты UDP. Ни сервер ни клиент не блокируются, поскольку ответ не приходит ни в какой форме. Просто выстрел на авось, без гарантированной доставки сообщений.
Если заглянуть теперь в ядерную структуру порта, можно обнаружить в ней уже до боли знакомых нам мемберов (фрагмент):
Код:
0: kd> dt _alpc_port
nt!_ALPC_PORT
+0x010 CommunicationInfo : Ptr64 _ALPC_COMMUNICATION_INFO
+0x018 OwnerProcess : Ptr64 _EPROCESS
+0x020 CompletionPort : Ptr64 Void
+0x0c8 Semaphore : Ptr64 _KSEMAPHORE
+0x0c8 DummyEvent : Ptr64 _KEVENT
+0x140 MessageZone : Ptr64 _ALPC_MESSAGE_ZONE
+0x168 SequenceNo : Int4B
Полезный трафик заворачивается в структуру
PORT_MESSAGE
, которая должна предварять сам буфер с сообщением в памяти. Фактически эта структура является заголовком мессаги, что представлено на схеме ниже. Затенённые серым поля заполняет ядро механизма ALPC, а нам остаётся лишь указать реальный размер пайлоада «DataLength», а так-же в следующем поле общий размер «Total» передаваемого сообщения, включая заголовок (вычисляется как sizeof, или константой 0x28). Обязательное требование к данному блоку – это выравнивание его в памяти на 8 байтную границу, иначе функция отправки\приёма сообщений NtAlpcSendWaitReceivePort() будет возвращать ошибку STATUS_INVALID_PARAMETERS = 0xC000000d
:По приходу сообщения получателю, имеет смысл проверить его тип в младшем байте поля «u2.Type» (назначение старшего байта мне неизвестно, т.к. в нём чёто лежит). Возможные варианты перечислены ниже. Интерес представляет и поле с идентификатором «MessageId», по которому можно будет в отладчике вытащить технические детали сообщения. Так-же в распоряжение получаем ID процесса-отправителя «ClientPid», а значит вызовом OpenProcess() сможем получить его имя, при слепых атаках на порты ALPC. Последнее поле «CallbackId» используется в ядре и нам не интересно:
C-подобный:
;// MessageType в поле PORT_MESSAGE -> u2.Type
;//-------------------------------------------
LPC_REQUEST = 1 ;// обычный запрос
LPC_REPLY = 2 ;// ответ
LPC_DATAGRAM = 3 ;// датаграмма
LPC_LOST_REPLY = 4 ;// потерянный ответ
LPC_PORT_CLOSED = 5 ;// оповещение
LPC_CLIENT_DIED = 6 ;// оповещение
LPC_EXCEPTION = 7 ;// исключение
LPC_DEBUG_EVENT = 8 ;// отладка
LPC_ERROR_EVENT = 9 ;// ошибка
LPC_CONNECTION_REQUEST = 10 ;// запрос на подключение
LPC_CONNECTION_REPLY = 11 ;// ответ ^^^
LPC_INTERNAL_REQUEST = 12 ;// внутренний (Pid\Tid = 0)
LPC_PORT_DISCONNECTED = 13 ;// потеря связи с сервером
3.1. Свойства и атрибуты сообщений
Свойства сообщений позволяют переключать режимы их передачи – как правило это «DualBuff» или «SectionMap». Причём первый проще в реализации, а второй выигрывает в производительности, т.к. не требует частых переходов в ядро ОС и обратно. Сообщений размером в 64 Кбайт обычно хватает за глаза, ведь ALPC разрабатывался прежде всего для передачи текста и закодированных команд на сервер, а не больших объёмов данных типа графики и мультимедиа. Поэтому далее мы рассмотрим именно метод двойной буферизации, а отображение секций отложим до лучших времён.
Выше упоминалось, что огромные надежды во-всём механизме ALPC инженеры возлагают на функцию NtAlpcSendWaitReceivePort(). Если дизассемблировать её в отладчике, то в реализации получим всего 133 инструкции процессора, однако под капотом они вызывают и неэкспортируемые наружу внутренние API с префиксом(р) от «Private», например AlpcpSendMessage() для отправки, и аналогичную для приёма сообщений – итого набегает нехилый набор инструкций. Дампит дизассм-листинг команда
uf
отладчика, а аргументы /i /c
отображают счётчик инструкций + только вызовы call соответственно:
Код:
0: kd> uf /i /c nt!NtAlpcSendWaitReceivePort
nt!NtAlpcSendWaitReceivePort (fffff800`02cf9970), 133 instructions
call to nt!ObReferenceObjectByHandleWithTag (fffff800`02dfc9b0)
call to nt!AlpcpSendMessage (fffff800`02cfb060)
call to nt!ObfDereferenceObject (fffff800`02a58900)
call to nt!AlpcpProcessSynchronousRequest (fffff800`02e428b0)
call to nt!ObfDereferenceObject (fffff800`02a58900)
call to nt!AlpcpReceiveMessage (fffff800`02cf10d0)
call to nt!ObfDereferenceObject (fffff800`02a58900)
call to nt!KiCheckForKernelApcDelivery (fffff800`02a33b38)
call to nt!ObfDereferenceObject (fffff800`02a58900)
0: kd>
А вот собственно и прототип функции отправки\приёма сообщений.
Значит если мы хотим не только отправлять, но и получать сообщения от сервера, то обязательно ставим флаг
ALPC_MSGFLG_SYNC_REQUEST=0x20000
. Это будет означать работу в синхронном режиме LPC. Поскольку коммуникация связи планируется в обе стороны, значит нужно передавать API-функции указатели (линк) на структуры PORT_MESSAGE
как для отправки Send, так и для приёма Receive. В противном случае клиент может указать только аргумент Send, а сервер противоположное Receive. В общем всё зависит от того, каким образом мы выстраиваем отношения с сервером.. поэтому и параметры функции являются опциональными. Для удобства восприятия, я разделил на блоки аргументы отправителя и получателя:
C-подобный:
NTSTATUS
NtAlpcSendWaitReceivePort( ;//<------ OK = 0 (Status Success)
In PortHandle, ;// дескриптор порта Connection сервера
In Flags ;// ALPC = 0, LPC = флаг ALPC_MSGFLG_SYNC_REQUEST
In opt SendMessage ;// линк на исходящее сообщение = PORT_MESSAGE + DataBuff
InOut opt SendMessageAttr ;// 0, или линк на его атрибуты = ALPC_MESSAGE_ATTRIBUTES
Out opt ReceiveMessage ;// линк на входящее сообщение = PORT_MESSAGE + DataBuff
InOut opt BufferLength ;// линк на размер приёмного буфа
InOut opt ReceiveMessageAttr ;// 0, или линк на атриб.входящего = ALPC_MESSAGE_ATTRIBUTES
In opt Timeout ;// 0 = нет тайм-аута
);
;// Flags для NtAlpcSendWaitReceivePort()
;//---------------------------------------
ALPC_MSGFLG_NONE = 0 ;// асинхронное сообщение (дефолт)
ALPC_MSGFLG_REPLY_MESSAGE = 1 ;// ответное сообщение
ALPC_MSGFLG_LPC_MODE = 2 ;// режим LPC
ALPC_MSGFLG_RELEASE_MESSAGE = 0x00010000 ;// исходящее сообщение
ALPC_MSGFLG_SYNC_REQUEST = 0x00020000 ;// синхронный запрос
ALPC_MSGFLG_WAIT_USER_MODE = 0x00100000 ;// ожидание сообщения от клиента
ALPC_MSGFLG_WAIT_ALERTABLE = 0x00200000 ;// ожидание предупреждения
ALPC_MSGFLG_WOW64_CALL = 0x80000000 ;// привязка только к WоW64
Такое обилие флагов позволяет динамически менять схему ответов. Например в зависимости от ситуации, для одного сообщения можно передать серверу Sync, для второго Async, для третьего Reply, и т.д. Проверив флаги в сообщении, ядро ALPC на стороне сервера будет точно знать, что конкретно хочет от него клиент. Но эта фишка чисто для гурманов, а в большинстве случаях достаточно одного флага
SYNC
.В свою очередь атрибуты сообщения предназначены для отправки\получения технических деталей механизма ALPC, в дополнение к полезному трафику «Data». Они передаются абонентам вместе с сообщением через опциональную структуру
ALPC_MESSAGE_ATTRIBUTES
. Тип может принимать одно из перечисленных ниже значений. Обратите внимание на порядок их следования от большего значения 0x80000000
к меньшему 0x02000000
– это важно, поскольку в родитеской структуре, вложенные распологаются аналогично:
C-подобный:
struct ALPC_MESSAGE_ATTRIBUTES
AllocatedAttributes dd 0 ;// (!) линк на поле Security
ValidAttributes dd 0 ;// маска с константами ALPC_MESSAGE_xx
Security ALPC_SECURITY_ATTR
View ALPC_DATA_VIEW_ATTR
Context ALPC_CONTEXT_ATTR ;//<--- Всегда Valid
Handle ALPC_HANDLE_ATTR
Token ALPC_TOKEN_ATTR ;//<--- Всегда Valid
;//----------- Win10+ --------------------------
DirectEvent ALPC_DIRECT_ATTR
WorkOnBehalf ALPC_WORK_ON_BEHALF_ATTR
Padding dq 0
ends
;// Флаги в поле ValidAttributes структуры ALPC_MESSAGE_ATTRIB
;//-----------------------------------------------------------
ALPC_MESSAGE_SECURITY_ATTRIBUTE = 0x80000000
ALPC_MESSAGE_VIEW_ATTRIBUTE = 0x40000000
ALPC_MESSAGE_CONTEXT_ATTRIBUTE = 0x20000000
ALPC_MESSAGE_HANDLE_ATTRIBUTE = 0x10000000
ALPC_MESSAGE_TOKEN_ATTRIBUTE = 0x08000000
ALPC_MESSAGE_DIRECT_ATTRIBUTE = 0x04000000
ALPC_MESSAGE_WORK_ON_BEHALF_ATTRIBUTE = 0x02000000
ALPC_MESSAGE_ATTRIBUTE_ALL = 0xFE000000 ;// ядро сбросит не поддерживаемые
В своём примере я не буду использовать атрибуты сообщения, но пару слов сказать всё-таки нужно.
Суть в том, что реально структура
ALPC_MESSAGE_ATTRIBUTES
содержит в себе только 2 первых поля, а вложенные структуры я уже добавил сам. Так вот в первом поле ориг.структуры нужно прописать указатель Allocated на голову всех остальных структур, а во-втором Valid флаг, какая из всех этих структур действительна для текущего сообщения. По факту, ALPC_MESSAGE_ATTRIBUTES
является заголовком всех атрибутов, как это было с описанной выше структурой PORT_MESSAGE
.Из всех атрибутов, интерес (на мой взгляд) представляет только структура
ALPC_DATA_VIEW_ATTRIB
. Она имеет смысл только в режиме передачи «SectionMap». В ней можно будет найти указатель и размер общей для сервера и клиента секции памяти. Для рассматриваемого нами режима «DualBuff» смысл её теряется.
C-подобный:
struct ALPC_DATA_VIEW_ATTR
Flags dq 0 ;// непонятно какой флаг
SectionHandle dq 0 ;// дескриптор секции (вернёт сервер)
ViewBase dq 0 ;// линк на область в памяти
ViewSize dq 0 ;// её размер
ends
3.2. Очереди сообщений
Ну и последний момент..
При запуске ОС, ALPC-клиенты начинают со-всех сторон вести перекрёстный огонь по портам своих серверов, после чего наступает относительная тишина. Чтобы отразить атаки подобного рода, серверам требуются очереди сообщений «MessageQueue». В ядре предусмотрено 5 типов таких очередей: основная Main (сообщение принято и обрабатывается сервером на текущий момент), большая Large для отображённого в память буфера, ожидающая ответов Pending, очередь аннулированных сообщений Canceled (например по истечении времени таймера), и наконец сквозная Direct. Поскольку управлением очередями заправляет ядро, мы в лице клиентов не можем повлиять на ход событий – нам достаточно просто знать, что таковые существуют. Кстати их состояние отображает WinDbg в своём логе команды
!alpc
:
Код:
0: kd> !alpc /p fffffa8005520a60
..........
CompletionPort : 0000000000000000
Waitable : No
Security : Static
Main queue is empty. <--- Win7 не поддерживает Direct,
Large queue is empty. ... поэтому видим всего 4 очереди,
Pending queue is empty. ... и все свободны Emply на текущий момент.
Canceled queue is empty.
4. Практика – пишем клиент и сервер
Чтобы не лезть глубоко в дебри, я максимально упростил код, который позже можно будет масштабировать.
Комменты есть, а потому повторяться не буду.
C-подобный:
format pe64 console 6.0 ;//<---- Vista +
include 'win64ax.inc'
include 'equates\alpc.inc'
entry start
;//-----------
.data
pmSend PORT_MESSAGE ;// заголовок сообщения
RequestBuff db 128 dup(0) ;// ...после которого сразу идёт буфер “Data”
pmReceive PORT_MESSAGE
ReceiveBuff db 128 dup(0)
align 8
basicInfo ALPC_BASIC_INFORMATION ;// структуры должны быть выровнены на 8-байт
objAttr OBJECT_ATTRIBUTES
portAttr ALPC_PORT_ATTRIBUTES
uStr UNICODE_STRING
usPort UNICODE_STRING
Name du '\CodebyNet',0 ;// имя ALPC-порта в корневом каталоге объектов
align 8
msgLen dd 128
pid dd 0
tid dd 0
hThread dq 0
hServPort dq 0
hPort dq 0
buff db 0
;//-----------
section '.text' code readable executable
;//*************************** *******************
proc AlpcPortCreateListen
invoke GetCurrentProcessId
mov [pid],eax
invoke GetCurrentThreadId
mov [tid],eax
;//----- Настраиваем и создаём основной порт "Connection" ------------
invoke RtlInitUnicodeString,usPort,Name
mov [objAttr.ObjectName],usPort
invoke AlpcMaxAllowedMessageLength
mov [portAttr.MaxMessageLength],rax
invoke NtAlpcCreatePort,hServPort,objAttr,portAttr
;//----- Покажем основную информацию о нём ---------------------------
cinvoke printf,<10,' Alpc SERVER Info ----> Pid=%04x Tid=%04x',\
10,' Port object name..: Root%ls [ Handle: 0x%x ]',0>,\
[pid],[tid],Name,[hServPort]
mov eax,[tid]
mov qword[buff],rax
invoke NtAlpcQueryInformation,[hServPort],0,basicInfo,32,0
or eax,eax ;// +--------------> InfoClass = Basic
jz @f
cinvoke printf,<10,' NtAlpcQueryInfo() ERROR: %x',0>,rax
jmp @exit
@@: invoke printf,<10,' Port flags........: 0x%08X',\
10,' Max message length: %d byte',10,\
10,' Wait for incomming connection... ',0>,\
[basicInfo.Flags],[portAttr.MaxMessageLength]
;//----- Ждать прихода пакета от клиента ---------------------------------
mov [pmReceive.u1.DataLength], 0
mov [pmReceive.u1.TotalLength],0 + sizeof.PORT_MESSAGE
invoke NtAlpcSendWaitReceivePort,[hServPort],\
0,\
0,\ ; send: PORT_MESSAGE
0,\ ; send: ALPC_MESSAGE_ATTRIBUTES
pmReceive,\ ; recv: PORT_MESSAGE
msgLen,\ ; recv: BuffLen
0,\ ; recv: ALPC_MESSAGE_ATTRIBUTES
0 ; TimeOut
;//----- Клиент послал сообщение! ----------------------------------------
;//----- Создать безымянный порт коммуникации с клиентом -----------------
mov rax,[pmReceive.MessageId]
mov [pmSend.MessageId], rax
mov [pmSend.u1.DataLength], 0
mov [pmSend.u1.TotalLength],0 + sizeof.PORT_MESSAGE
invoke NtAlpcAcceptConnectPort,hPort,[hServPort],\
0,\ ;// можно выставить флаг ALPC_SYNC_CONNECTION
0,\ ;// In ObjAttr = OBJECT_ATTRIBUTES
0,\ ;// In PortAttr = PORT_ATTRIBUTES
0,\ ;// In PortCntx = Void
pmSend,\ ;// InRd ConnectSend = PORT_MESSAGE
0,\ ;// InOut ConnectMsgAttr = MESSAGE_ATTRIBUTES
1 ;// In AcceptConnect = Bool (принять\отвергнуть)
or eax,eax
jz @ok
cinvoke printf,<10,' AcceptConnectPort ERROR: %x',0>,rax
jmp @exit
;//----- В цикле ждём сообщений от клиента -------------------------------
@ok: call GetReceiveInfo
@nextMessage:
mov rdi,ReceiveBuff ;// очистить приёмный буфер
mov ecx,128/8
xor rax,rax
rep stosq
mov [pmReceive.u1.DataLength], 0
mov [pmReceive.u1.TotalLength],0 + sizeof.PORT_MESSAGE
invoke NtAlpcSendWaitReceivePort,[hServPort],\
0,\ ;// ALPC_MSGFLG_SYNC_REQUEST
0,\ ;// send: PORT_MESSAGE
0,\ ;// send: ALPC_MESSAGE_ATTRIBUTES
pmReceive,\ ;// recv: PORT_MESSAGE
msgLen,\ ;// recv: BuffLen
0,\ ;// recv: ALPC_MESSAGE_ATTRIBUTES
0 ;// TimeOut
or eax,eax
jz @f
cinvoke printf,<10,' SendReceive ERROR: %x',10,0>,eax
jmp @stop
@@: call GetReceiveInfo
cmp word[pmReceive.u2.Type],5 ;// LPC_PORT_CLOSED ???
jnz @nextMessage
@stop: cinvoke printf,<10,10,' ***** Exit & Close Port Handle! ***************',0>
invoke NtAlpcDisconnectPort,[hServPort],0
invoke CloseHandle,[hServPort]
ret
endp
align 8
proc GetReceiveInfo
mov rax, [pmReceive.ClientPid]
mov rbx, [pmReceive.ClientTid]
cinvoke printf,<' OK!',10,10,\
' ALPC Client Info ----> Pid=%04x Tid=%04x',0>,rax,rbx
movzx eax,byte[pmReceive.u2.Type]
dec eax
shl eax,2
mov esi,TypeTable
add esi,eax
mov esi,[esi]
mov rax,[pmReceive.MessageId]
movzx ebx,[pmReceive.u1.DataLength]
cinvoke printf,<10,' Message Type......: %s',\
10,' Message Id........: 0x%08x',\
10,' Message Length....: %d byte',\
10,' Message Data......: %s',10,\
10,' Wait client request... ',0>,esi,rax,rbx,ReceiveBuff
ret
endp
align 8
TypeTable dd Request,Reply,Datagr,LostRep,PortClose,Client
dd Except,Debug,Error,Connect,ConRep,Internal,pDiscon
Request db 'Lpc_Simple_Request',0
Reply db 'Lpc_Reply',0
Datagr db 'Lpc_Datagram',0
LostRep db 'Lpc_Lost_Reply',0
PortClose db 'Lpc_Port_Closed',0
Client db 'Lpc_Client_Died',0
Except db 'Lpc_Exception',0
Debug db 'Lpc_Debug_Event',0
Error db 'Lpc_Error_Event',0
Connect db 'Lpc_Connection_Request',0
ConRep db 'Lpc_Connection_Reply',0
Internal db 'Lpc_Internal_Null_Request',0
pDiscon db 'Lpc_Port_Disconnected',0
;//************************* ТОЧКА ВХОДА В ПРОГРАММУ ********************************
align 16
start: sub rsp,8
invoke SetConsoleTitle,<'*** ALPC-SERVER example (x64 Win7+) ***',0>
invoke CreateThread,0,0,AlpcPortCreateListen,0,0,0
mov [hThread],rax
invoke WaitForSingleObject,[hThread],INFINITY
cinvoke printf,<10,' ***** The end communication session. Bye! *****',10,0>,eax
@exit: cinvoke _getch
cinvoke exit, 0
;//------------------------
section '.idata' import data readable writeable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',\
user32,'user32.dll',ntdll,'ntdll.dll'
include 'api\msvcrt.inc'
include 'api\kernel32.inc'
include 'api\user32.inc'
include 'api\ntdll.inc'
Если приложение не упало, значит всё гуд, и в системном пространстве имён должен появиться объект нашего порта – упс.. и точно имеется таковой:
Далее таким-же макаром пишем клиента:
C-подобный:
format pe64 console 6.0
include 'win64ax.inc'
include 'equates\alpc.inc'
entry start
;//-----------
.data
pmReceive PORT_MESSAGE
reseiveBuff db 128 dup(0)
pmSend PORT_MESSAGE
sendBuff db 128 dup(0)
basicInfo ALPC_BASIC_INFORMATION
usPortName UNICODE_STRING
Name du '\CodebyNet',0
align 8 ;<----------- требует NtAlpcQueryInformation()
msgLen dd 0
inHndl dd 0
hServerPort dq 0
resvBuffSz dq 128
buff db 0
;//-----------
section '.text' code readable executable
start: sub rsp,8
invoke SetConsoleTitle,<'*** ALPC-CLIENT example (x64 Win7+) ***',0>
invoke GetStdHandle,STD_INPUT_HANDLE
mov [inHndl],eax
;//----- Попытка подключения к серверу ---------------------------------
invoke RtlInitUnicodeString,usPortName,Name
invoke NtAlpcConnectPort,hServerPort,usPortName,0,0,0,0,0,0,0,0,0
or eax,eax
jz @f
cinvoke printf,<10,' NtAlpcConnectPort() ERROR: %x',0>,rax
jmp @exit
;//----- ОК! Покажем базовую информацию о себе -------------------------
@@: invoke GetCurrentProcessId
push rax
invoke GetCurrentThreadId
pop rbx
cinvoke printf,<10,' Alpc CLIENT info -----> Pid=%04x Tid=%04x ',\
10,' Connection port....: Root%ls [ Handle: 0x%x ]',0>,\
rbx,rax,Name,[hServerPort]
invoke NtAlpcQueryInformation,[hServerPort],0,basicInfo,32,0
or eax,eax ;// +--------------> InfoClass = Basic
jz @f
cinvoke printf,<10,' NtAlpcQueryInfo() ERROR: %x',0>,rax
jmp @exit
@@: invoke AlpcMaxAllowedMessageLength
xchg r10,rax
mov eax,[basicInfo.Flags]
mov ebx,[basicInfo.SequenceNo]
mov r11,[basicInfo.PortContext]
cinvoke printf,<10,' Port flags.........: 0x%08x',\
10,' Port context.......: 0x%08x',\
10,' Message max len....: %d byte',\
10,' Sequence number....: %d',10,0>,rax,r11,r10,rbx
;//----- Цикл отправки команд на сервер --------------------------------
@SendMessage:
mov rdi,sendBuff ;// очистить буфер "Data" сообщений
mov ecx,128/8
xor rax,rax
rep stosq
cinvoke printf,<10,10,' Message to Server: > ',0>
invoke ReadConsole,[inHndl],sendBuff,128,msgLen,0
mov eax,[msgLen]
dec eax
mov [pmSend.u1.DataLength],ax ;// заполняем структуру PORT_MESSAGE
mov [pmSend.u1.TotalLength],ax
add [pmSend.u1.TotalLength],sizeof.PORT_MESSAGE
invoke NtAlpcSendWaitReceivePort,[hServerPort],\
0,\ ;// можно установить ALPC_MSGFLG_SYNC_REQUEST
pmSend,\ ;// send: PORT_MESSAGE
0,\ ;// send: ALPC_MESSAGE_ATTRIBUTES
0,\ ;// recv: PORT_MESSAGE
0,\ ;// recv: BuffLen
0,\ ;// recv: ALPC_MESSAGE_ATTRIBUTES
0 ;// TimeOut
or eax,eax
jz @f
cinvoke printf,<' Send message ERROR: %x',0>,eax
jmp @exit
@@: cinvoke printf,<' Send new message? y/n...',0>
cinvoke _getch
cmp al,'y'
jz @SendMessage
@exit: invoke NtAlpcDisconnectPort,[hServerPort],0
invoke CloseHandle,[hServerPort]
cinvoke _getch
cinvoke exit, 0
;//------------------------
section '.idata' import data readable writeable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',\
user32,'user32.dll',ntdll,'ntdll.dll'
include 'api\msvcrt.inc'
include 'api\kernel32.inc'
include 'api\user32.inc'
include 'api\ntdll.inc'
Теперь у нас есть твикс, который сможет общаться между собой.
Пробуем из клиента послать серверу какую-нибудь мессагу, за ней ещё одну, и т.д. Чтобы прервать связь, достаточно на запрос
y/n
ответить отрицательно, после чего сервер сам должен оборвать соединение, не забыв нам в след помахать рукой. Так это выглядит у меня:Обратите внимание на поле «MessageType» логов. При первом запуске клиента, сервер сразу опознал его, а ядро механизма ALPC послало за нас серваку запрос типа
Lpc_Connection_Request
. Зато следующие 2 запроса посылал уже я от имени клиента, и соответственно тип у них изменился на дефолтный Lpc_Request
. Это видно и при разрыве связи Lpc_Port_Closed
.А что там с отладчиком WinDbg? Сможет-ли он обнаружить нашу коммуникацию, и если да, то в каком виде?
Пробуем открыть объект нашего порта по имени, а дальше будем действовать по обстоятельствам:
Код:
0: kd> !object \CodebyNet
Object: fffffa800475a8b0 Type: (fffffa80039fc080) ALPC Port
Object Header : fffffa800475a880
Directory Object: fffff8a000004c70 Name: CodebyNet <--------//
0: kd> !alpc /p fffffa800475a8b0
Port @ fffffa800475a8b0
Type : ALPC_CONNECTION_PORT
CommunicationInfo : fffff8a005253330
ConnectionPort : fffffa800475a8b0
ClientCommunicationPort : 0000000000000000
ServerCommunicationPort : 0000000000000000
OwnerProcess : fffffa800460e060 (ALPC_Server.EXЕ)
SequenceNo : 0x00000001 (1)
CompletionPort : 0000000000000000
MessageZone : 0000000000000000
ConnectionPending : No
Disconnected : No
Closed : No
FlushOnClose : Yes
ReturnExtendedInfo : No
Waitable : No
Security : Static
Main queue is empty.
Large message queue is empty.
Canceled queue is empty.
Pending queue has 2 message(s) <------- Пара наших сообщений попала в очередь ожиданий
fffff8a00a0ed6d0 0000061c 000f9c:000d80 0 fffffa8004a14b50 LPC_REQUEST
fffff8a01bb38d00 00000208 000f9c:000d80 0 fffffa8004a14b50 LPC_REQUEST
;//--------------------- Запросим инфу об одном из них --------------------
0: kd> !alpc /m 61c
Message @ fffff8a00a0ed6d0
MessageID : 0x061C (1564)
CallbackID : 0xEBAED (965357)
SequenceNumber : 0x00000001 (1)
Type : LPC_REQUEST
DataLength : 0x000B (11) <----- Длина определилась корректно
TotalLength : 0x0033 (51)
Canceled : No
Release : No
ReplyWaitReply : No
Continuation : Yes
OwnerPort : fffffa8003dc6070 [ALPC_CLIENT_COMMUNICATION_PORT]
WaitingThread : 0000000000000000
QueueType : ALPC_MSGQUEUE_PENDING
QueuePort : fffffa800475a8b0 [ALPC_CONNECTION_PORT]
QueuePortOwnerProcess : fffffa800460e060 (ALPC_Server.EXЕ) <------ Имя сервера тоже совпадает
ServerThread : fffffa8004a14b50
QuotaCharged : No
CancelQueuePort : 0000000000000000
CancelSequencePort : 0000000000000000
CancelSequenceNumber : 0x00000000 (0)
ClientContext : 0000000000000000
ServerContext : 0000000000000000
PortContext : 0000000000000028 <----- Маркер контекста безопасности
CancelPortContext : 0000000000000000
SecurityData : 0000000000000000
View : 0000000000000000 <----- Режим “DualBuff”
0: kd>
Наличие ALPC-портов у приложений можно отследить в мониторе, например П.Йосифовича «ProcMonX». Эта мощная утилита имеет свой драйвер режима ядра, а потому с лёгкостью отображает даже идентификаторы всех сообщений, которые потом можно вскормить отладчику WinDbg по представленной выше схеме. А вот «ProcessHacker\Explorer» уже не хотят делиться с нами секретами, отмазываясь для галочки бесполезными дескрипторами.


5. Заключение
Это была попытка представить фундаментальные основы механизма ALPC, но поскольку тема объёмная и недокументированная официальными лицами, то материал не охватил даже и половины из того, что планировалось изначально. Для самостоятельной работы оставляю парочку линков, где можно найти векторы атак на порты, и много другое. В скрепке найдёте готовые экзешники для тестов, а так-же инклуд для ассемблера FASM, с описанием необходимых для сборки исходника структур. Всем удачи, пока!
1.
Ссылка скрыта от гостей
2.
Ссылка скрыта от гостей
3.
Ссылка скрыта от гостей