В данной статье будет рассмотрена еще одна известная уязвимость в Power Dependency Coordinator (pdc.sys) - CVE-2024-38107
По информации производителя, она эксплуатировалась in-the-wild, исправление для нее было выпущено в августе 2024
https://nvd.nist.gov/vuln/detail/cve-2024-38107
https://msrc.microsoft.com/update-guide/en-US/advisory/CVE-2024-38107
Тип уязвимости - Use-After-Free (UAF)
В предыдущей статье был рассмотрен интерфейс pdc.sys, его взаимодействие с клиентами, а также проанализирована уязвимость CVE-2025-27736. Многие упомянутые там особенности работы драйвера будут важны для понимания этой статьи, поэтому рекомендуется ознакомится с ее вводной частью (которая относится к ALPC и регистрационным сообщениям).
В любом случае, напомню, что для взаимодействия со своими клиентами pdc.sys использует ALPC, а на этапе соединения (NtAlpcConnectPort) производится регистрация клиента.
Типы ALPC сообщений
Для дальнейшего понимания уязвимости, надо более подробно рассмотреть взаимодействие клиента и сервера по ALPC. Для целей этой статьи будут важны типы ALPC сообщений, которые отправляются на разных этапах коммуникации между ALPC клиентом и сервером.
ALPC сообщения должны начинаться со структуры _PORT_MESSAGE
kd> dt nt!_PORT_MESSAGE -r2
+0x000 u1 :
+0x000 s1 :
+0x000 DataLength : Int2B
+0x002 TotalLength : Int2B
+0x000 Length : Uint4B
+0x004 u2 :
+0x000 s2 :
+0x000 Type : Int2B
+0x002 DataInfoOffset : Int2B
+0x000 ZeroInit : Uint4B
+0x008 ClientId : _CLIENT_ID
+0x000 UniqueProcess : Ptr64 Void
+0x008 UniqueThread : Ptr64 Void
+0x008 DoNotUseThisField : Float
+0x018 MessageId : Uint4B
+0x020 ClientViewSize : Uint8B
+0x020 CallbackId : Uint4B
Тип ALPC сообщения содержится в поле Type структуры _PORT_MESSAGE и может принимать следующие значения
#define LPC_REQUEST 1
#define LPC_REPLY 2
#define LPC_DATAGRAM 3
#define LPC_LOST_REPLY 4
#define LPC_PORT_CLOSED 5
#define LPC_CLIENT_DIED 6
#define LPC_EXCEPTION 7
#define LPC_DEBUG_EVENT 8
#define LPC_ERROR_EVENT 9
#define LPC_CONNECTION_REQUEST 10
Предшественником ALPC был LPC, и константы остались с тех времен. Некоторые из LPC типов сообщений потеряли актуальность при переходе на ALPC, другие же по-прежнему используются, рассмотрим некоторые из них. LPC_REQUEST — обычное ALPC сообщение (отправляемое через функцию NtAlpcSendWaitReceivePort), LPC_CLIENT_DIED — сообщение, отправляемое при завершении потока. LPC_CONNECTION_REQUEST – используется при установке ALPC соединения. Этот тип сообщения формируется в подвызове функции nt!AlpcpProcessConnectionRequest.
Пример стека при установке соединения с pdc
kd> kb
# RetAddr Call Site
00 fffff803124187f2 pdc!PdcProcessMessage+0x22a
01 fffff80312418619 pdc!PdcpAlpcProcessMessages+0x6a
02 fffff8031093bdcf pdc!PdcMessageCallback+0x9
03 fffff8031093bcfc nt!ExNotifyWithProcessing+0xc7
04 fffff80310be4b8c nt!ExNotifyCallback+0xc
05 fffff80310bdd3d7 nt!AlpcpCompleteDispatchMessage+0x8bc
06 fffff80310bdcfb6 nt!AlpcpDispatchConnectionRequest+0x13f
07 fffff80310bdd9b1 nt!AlpcpProcessConnectionRequest+0x1be
08 fffff80310bdc73e nt!AlpcpConnectPort+0x2c5
09 fffff80310a10ef5 nt!NtAlpcConnectPort+0x6e
0a 00007ff84c9edef4 nt!KiSystemServiceCopyEnd+0x25
0b 00007ff849e72fc7 ntdll!NtAlpcConnectPort+0x14
nt!AlpcpSendCloseMessage отправляет сообщение LPC_PORT_CLOSED. Это сообщение отправляется при закрытии дескриптора порта или при завершение процесса. Ниже пример стека для закрытия порта
kd> kb
# RetAddr Call Site
00 fffff803124187f2 pdc!PdcProcessMessage
01 fffff80312418619 pdc!PdcpAlpcProcessMessages+0x6a
02 fffff8031093bdcf pdc!PdcMessageCallback+0x9
03 fffff8031093bcfc nt!ExNotifyWithProcessing+0xc7
04 fffff80310be4b8c nt!ExNotifyCallback+0xc
05 fffff80310bdd519 nt!AlpcpCompleteDispatchMessage+0x8bc
06 fffff80310be08c1 nt!AlpcpDispatchCloseMessage+0x119
07 fffff80310be150e nt!AlpcpSendCloseMessage+0xe9
08 fffff80310c0718f nt!AlpcpClosePort+0x5e
09 fffff80310c0207c nt!ObCloseHandleTableEntry+0x51f
0a fffff80310a10ef5 nt!NtClose+0xec
0b 00007ff84c9ed1c4 nt!KiSystemServiceCopyEnd+0x25
0c 00007ff84a486785 ntdll!NtClose+0x14
Для завершения процесса
kd> kb
# RetAddr Call Site
00 fffff803124187f2 pdc!PdcProcessMessage
01 fffff80312418619 pdc!PdcpAlpcProcessMessages+0x6a
02 fffff8031093bdcf pdc!PdcMessageCallback+0x9
03 fffff8031093bcfc nt!ExNotifyWithProcessing+0xc7
04 fffff80310be4b8c nt!ExNotifyCallback+0xc
05 fffff80310bdd519 nt!AlpcpCompleteDispatchMessage+0x8bc
06 fffff80310be08c1 nt!AlpcpDispatchCloseMessage+0x119
07 fffff80310be150e nt!AlpcpSendCloseMessage+0xe9
08 fffff80310c0718f nt!AlpcpClosePort+0x5e
09 fffff80310c9b445 nt!ObCloseHandleTableEntry+0x51f
0a fffff80310c9cdd9 nt!ExSweepHandleTable+0xd5
0b fffff80310c9b170 nt!ObKillProcess+0x35
0c fffff80310caf30e nt!PspRundownSingleProcess+0x204
0d fffff80310ce7ff8 nt!PspExitThread+0x5f6
0e fffff803108f8ead nt!KiSchedulerApcTerminate+0x38
0f fffff80310a02a90 nt!KiDeliverApc+0x60d
Для целей статьи будут важны два сообщения - LPC_PORT_CLOSED и LPC_CLIENT_DIED.
Анализ патча
Если сравнить две версии драйвера pdc.sys до августа 2024 и после, то видно, что изменения находятся в двух функциях — PdcProcessMessage и PdcpAlpcProcessMessages.

Наиболее интересное изменение (в контексте анализа уязвимости) находится в функции PdcProcessMessage. Эта функция отвечает за обработку ALPC сообщений от клиентов.
Ниже приведен фрагмент функции PdcProcessMessage до изменений

После изменений тот же фрагмент имеет уже другой вид
Следует обратить внимание, что изменение касается как раз обработки различных типов ALPC сообщений – в старой версии драйвера сообщения LPC_PORT_CLOSED и LPC_CLIENT_DIED обрабатывались одинаково, а в новой версии их обработка происходит по-разному. Рассмотрим изменения более подробно.
В старой версии драйвера при получении сообщений данных типов происходила очистка ресурсов ALPC клиента с помощью функции PdcFreeClient.

В новой версии драйвера при получении LPC_PORT_CLOSED по-прежнему вызывается код очистки ресурсов, а LPC_CLIENT_DIED — не обрабатывается.
Для того, чтобы понять, в чем причина такого разделения и как это связано с уязвимостью, необходимо рассмотреть подробнее сообщение LPC_CLIENT_DIED.
LPC_CLIENT_DIED
Сообщение LPC_CLIENT_DIED отправляется функцией PspExitThread при завершении потока, получателем сообщения является так называемый termination port, который установлен завершаемому потоку. Участок кода PspExitThread, отвечающий за отправку сообщения приведен ниже
В обычном случае у потока нет termination port, но он может быть установлен с помощью функции NtRegisterThreadTerminatePort. Ее код приведен ниже
Надо сказать, что termination порты в системе все же используются, например в csrss.
Если проанализировать работу с termination портами в системе, то становится очевидно несколько вещей:
termination port – это ALPC port
-
termination port может быть не один, это список. Порт описывается структурой _TERMINATION_PORT
kd> dt nt!_TERMINATION_PORT
+0x000 Next : Ptr64 _TERMINATION_PORT
+0x008 Port : Ptr64 Void функция NtRegisterThreadTerminatePort не проверяет, что порт уже был установлен в качестве termination port и, в результате, один и тот же порт можно устанавливать потоку сколько угодно раз.
Как следствие предыдущего пункта, функция PspExitThread также будет отправлять сообщение LPC_CLIENT_DIED всем привязанным портам, и, если порт был привязан n раз, ему будет отправлено сообщение LPC_CLIENT_DIED также n раз.
NtRegisterThreadTerminatePort работает с текущим потоком, а не с произвольным.
Теперь у нас достаточно информации для того, чтобы перейти к срабатыванию уязвимости
Воспроизведение
Таким образом, если pdc port будет установлен потоку как termination port, то при завершении этого потока pdc получит сообщение LPC_CLIENT_DIED. При этом, если pdc port будет установлен потоку несколько раз, то сообщение LPC_CLIENT_DIED также будет получено несколько раз. Как было выяснено ранее при анализе функции PdcProcessMessage, до патча при обработке сообщения LPC_CLIENT_DIED происходила очистка ресурсов и освобождение памяти. Возможность повторного вызова кода освобождения ресурсов звучит как условие для UAF, но стоит проверить на практике, действительно ли отправка повторного LPC_CLIENT_DIED pdc port приведет к срабатыванию уязвимости.
Если выполнить последовательность шагов ниже
Создать поток (Thread1)
Присоединиться к порту \PdcPort функцией NtAlpcConnectPort с корректным регистрационным сообщением (см. предыдущую статью)
Из потока Thread1 дважды установить \PdcPort как Termination Port.
Завершить поток Thread1
В этом случае PspExitThread отправит сообщение LPC_CLIENT_DIED pdc порту дважды. На втором сообщении произойдет обращение к освобожденной памяти и далее BSOD, таким образом, после выполнения шагов выше получаем срабатывание уязвимости.