Статья ASM. Внутреннее устройство механизма LPC, ALPC

Marylin

Mod.Assembler
Red Team
05.06.2019
340
1 501
BIT
976
Logo2.webp

В статье речь пойдёт о недокументированной части Windows – это внутренние порты обмена сообщениями ALPC, что подразумевает «Advanced Local Procedure Call», или расширенный вызов локальных процедур. Механизм пришёл на смену устаревшему LPC, который имел два основных недостатка: во-первых он был синхронным (программным потокам абонентов приходилось останавливаться в ожидании ответа), а во-вторых был плохо защищён, что отрицательно влияло на подсистему безопасности в целом. Поэтому начиная с Vista-NT6 инженеры избавились от проприетарного LPC, в пользу асинхронного режима ALPC. По понятным причинам Microsoft не раскрывает всех тайн его реализации, закрыв на амбарный замок функции поддержки в нативную либу Ntdll.dll. Только они не учли, что неизведанная область наоборот подогреет интерес исследователей во всём мире, а что из этого получилось – предлагаю ознакомиться ниже.

Оглавление:
  1. Введение в ALPC
  2. Архитектура портов коммуникации
  3. Формат передаваемых сообщений
  4. Практика – клиент/серверное приложение
  5. Постфикс


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-сервера внутри операционной системы:

SubSystem.webp

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.

ProcHacker.webp

Механизм поддерживается объектом ядра «ALPC PORT». В силу своей асинхронной природы, порты могут обслуживать сразу с несколько клиентов одновременно, что является несомненным их плюсом. Обратите внимание, что объект «PORT» нельзя открыть привычной нам функцией CreateFile(), как например объект «DEVICE» – к серверным портам можно только подключаться.

Внутри операционной системы порты носят массовый характер, а потому представляют цель для атак. Чтобы убедиться в их значимости, достаточно запустить утилиту «WinObj» М.Руссиновича, или «ObjectExplorer» от Four-F, и отобразить в ней сначала корневую папку, а затем и дир «RPC Control». Если-же стоит задача программно найти и перечислить порты ALPC, можно позвать на помощь пару функций NtOpenDirectoryObject() + NtQueryDirectoryObject():

WinObj.webp

К сожалению эти утилиты не дают нам исчерпывающей информации о портах, и приходится кряхтя слазить с печки, чтобы подключить отладчик 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.

AlpcTech.webp

В качестве разминки можно ещё раз собрать в отладчике инфу о произвольном порте, и думаю на этот раз всё станет намного прозрачней.

Код:
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:

PortMsg.webp

По приходу сообщения получателю, имеет смысл проверить его тип в младшем байте поля «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'

Если приложение не упало, значит всё гуд, и в системном пространстве имён должен появиться объект нашего порта – упс.. и точно имеется таковой:

CodebyPort.webp

Далее таким-же макаром пишем клиента:

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 ответить отрицательно, после чего сервер сам должен оборвать соединение, не забыв нам в след помахать рукой. Так это выглядит у меня:

Result.webp

Обратите внимание на поле «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» уже не хотят делиться с нами секретами, отмазываясь для галочки бесполезными дескрипторами.

pHacker.webp pMonitor.webp


5. Заключение

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

1.
2.
3.


 

Вложения

Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!