Внедрение локального shellcode в среде Windows

Внедрение локального shellcode в среде Windows

В этой статье описана новая методика создания 100% надежных локальных эксплойтов для операционных систем семейства Windows. Методика основана на недостатках архитектуры операционных систем Windows, позволяющих низко привилегированному процессу внедрять данные в почти любой другой процесс, даже если он запущен с более высокими правами.

Cesar Cerrudo, перевод Владимир Куксенок

В этой статье описана новая методика создания 100% надежных локальных эксплойтов для операционных систем семейства Windows. Методика основана на недостатках архитектуры операционных систем Windows, позволяющих низко привилегированному процессу внедрять данные в почти любой другой процесс, даже если он запущен с более высокими правами. Мы знаем, что реализация локальной атаки более проста, чем удаленной, однако и в этом процессе также есть некоторые трудности. После краткого введения и описания методики будет показано несколько примеров, понимание которых позволит читателю использовать описанные приемы при создании своих собственных эксплойтов.

Введение

При создании локального эксплойта под Windows вы можете столкнуться со следующими проблемами.
  • Разные адреса возврата
    • из-за разных версий Windows
    • из-за различных установленных пакетов обновлений
    • из-за разных локализаций Windows
  • Ограниченное пространство для shellcode
  • Ограничение на использование нулевых байтов
  • Ограничения, накладываемые кодировкой символов
  • Защиты от переполнения буфера / эксплойтов
  • И т.д.

Для обхода этих ограничений можно использовать различные адреса возврата и/или техники. После прочтения этой статьи вы сможете перестать волноваться обо всем этом, так как написать 100% надежный эксплойт, работающий на любой версии Windows, при любом пакете обновлений, языке, обходящий защиту от переполнения буфера будет очень просто. Происходить это будет вследствие того, что ваш код не будет выполняться из стека или кучи и не будет использовать фиксированный адрес возврата.

Предлагаемая методика использует Windows LPC (Local/Lightweight Procedure Call) – механизм межпроцессорного взаимодействия, используемый RPC для локальной связи. LPC позволяет процессам, используя LPC порты, взаимодействовать между собой посредством сообщений. LPC плохо документирован и не будет здесь детально описываться, однако более подробную информацию вы можете получить из источников, указанных в разделе “Ссылки”. LPC порты – это объекты Windows. Процессы могут создавать именованные LPC порты, к которым могут подключаться другие процессы, используя имя созданного LPC порта. Вы можете увидеть LPC порты процессов, воспользовавшись утилитой Process Explorer с сайта www.sysinternals.com. Для этого нужно выбрать процесс и посмотреть на строку Port в столбце Type (нижняя панель), в которой будет имя порта. Дополнительную информацию, например права доступа и т.д., можно получить, дважды кликнув на имени порта.

LPC активно используется внутренними механизмами операционной системы, такими как OLE/COM и т.д., что означает, что почти каждый процесс пользуется LPC портом. LPC порты могут быть защищены с помощью ACL, поэтому иногда, если клиентский процесс не имеет необходимых привилегий, соединение не может быть установлено.

Для применения методики мы будем использовать несколько функций API, которые будут описаны ниже.

Подключение к LPC порту

Для подключения к LPC порту используется функция Native API – NtConnectPort из Ntdll.dll.

NtConnectPort(
OUT PHANDLE ClientPortHandle,
IN PUNICODE_STRING ServerPortName,
IN PSECURITY_QUALITY_OF_SERVICE SecurityQos,
IN OUT PLPCSECTIONINFO ClientSharedMemory OPTIONAL,
OUT PLPCSECTIONMAPINFO ServerSharedMemory OPTIONAL,
OUT PULONG MaximumMessageLength OPTIONAL,
IN OUT PVOID ConnectionInfo OPTIONAL,
IN OUT PULONG ConnectionInfoLength OPTIONAL );

ClientPortHandle: указатель на дескриптор порта, возвращаемый функцией.
ServerPortName: указатель на структуру UNICODE_STRING, содержащую имя порта, к которому нужно подключиться.
SecurityQos: указатель на структуру SECURITY_QUALITY_OF_SERVICE.
ClientSharedMemory: указатель на структуру LPCSECTIONINFO, содержащую информацию о разделяемой памяти.
ServerSharedMemory: указатель на структуру содержащую информацию о разделяемой памяти.
MaximumMessageLength: указатель на максимальный размер сообщения, возвращаемый функцией.
ConnectionInfo: указатель на массив, в котором содержится сообщение (отправленные на LPC сервер и полученные от LPC сервера данные).
ConnectionInfoLength: указатель на размер сообщения.

Другие функции для взаимодействия с LPC не будут рассматриваться, так как в описываемой методике они не используются. Если вы хотите узнать о них больше, воспользуйтесь информацией из раздела “Ссылки”.

Самый важный параметр, которой мы передаем в вышеописанную функцию, это имя LPC порта, находящееся в структуре UNICODE_STRING:

typedef struct _UNICODE_STRING {
USHORT Length; //длинна строки
USHORT MaximumLength; //максимальная длинна строки + 2
PWSTR Buffer; //указатель на строку
} UNICODE_STRING;

Поля структуры LPCSECTIONINFO:

typedef struct LpcSectionInfo {
DWORD Length; //размер структуры
HANDLE SectionHandle; //дескриптор разделяемой секции
DWORD Param1; //не используется
DWORD SectionSize; //размер разделяемой секции
DWORD ClientBaseAddress; //возвращается функцией
DWORD ServerBaseAddress; // возвращается функцией
} LPCSECTIONINFO;

Перед заполнением этой структуры нужно создать разделяемую секцию (см. [1] для большей информации о разделяемых секциях) и спроецировать ее на оба процесса (процесс из которого происходит подключение и процесс, к которому мы подключаемся).

Для структуры LPCSECTIONMAPINFO мы должны установить только размер структуры:

typedef struct LpcSectionMapInfo{
DWORD Length; //размер структуры
DWORD SectionSize;
DWORD ServerBaseAddress;
} LPCSECTIONMAPINFO;

Структура SECURITY_QUALITY_OF_SERVICE может принимать любые значения, поэтому о ней мы можем не волноваться:

typedef struct _SECURITY_QUALITY_OF_SERVICE {
DWORD Length;
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel;
DWORD ContextTrackingMode;
DWORD EffectiveOnly;
} SECURITY_QUALITY_OF_SERVICE;

Для переменной ConnectionInfo мы можем использовать буфер со 100 нулевыми элементами; значение переменной ConnectionInfoLength должно быть равно размеру буфера.

Создание разделяемой секции

Для создания разделяемой секции используется следующая функция Native API из Ntdll.dll.

NtCreateSection(
OUT PHANDLE SectionHandle,
IN ULONG DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN PLARGE_INTEGER MaximumSize OPTIONAL,
IN ULONG PageAttributess,
IN ULONG SectionAttributes,
IN HANDLE FileHandle OPTIONAL );

SectionHandle: указатель на идентификатор секции, возвращаемый функцией.
DesiredAccess: определяет нужные виды доступа: чтение, запись, исполнение и т.д.
ObjectAttributes: указатель на структуру OBJECT_ATTRIBUTES.
MaximumSize: указатель на размер создаваемой разделяемой секции.
PageAttributes: атрибуты страницы памяти: чтение, запись, исполнение и т.д.
SectionAttributes: атрибуты секции в зависимости от типа создаваемой секции.
FileHandle: дескриптор файла, для которого создается проекция в память.

Ниже перечисляются интересующие нас параметры.

Для параметра DesiredAccess мы должны указать нужные нам виды доступа к секции (нам нужен доступ на чтение, запись и исполнение). MaximumSize определяет нужный нам размер секции; переменная может принимать любое значение, но оно должно быть достаточное для хранения данных, которые будут помещены нами в секцию позже. В значение переменной PageAttributes мы также должны установить права на запись и чтение, а в SectionAttributes – тип секции “committed memory”.

Методика

Теперь, когда мы ознакомились с функциями для установления LPC соединения, рассмотрим методику атаки. Как я уже сказал, большинство процессов в Windows имеют LPC порты, к которым мы можем подключиться (если обладаем соответствующими правами). Как вы могли заметить, в одной из структур, передаваемых в функцию функции NtConnectPort, есть поле – разделяемая секция, которая проецируется на оба процесса, осуществляющих обмен данными, что является очень хорошей новостью. Почему? - спросите вы. Потому что все, что мы поместим в разделяемую секцию нашего процесса, будет спроецировано на другие процессы. Другими словами мы можем внедрить любые данные (и конечно shellcode) в любой процесс, какой мы захотим, даже если он запущен с большими правами, чем наш процесс! Что еще более удивительно, адрес, на который будет спроецирована разделяемая секция, будет возвращен функцией! Если вы все же не понимаете что в этом хорошего, перед продолжением чтения вам нужно ознакомиться с принципами создания эксплойтов. В большинстве случаев при атаке на уязвимое приложение с использование LPC, мы будем иметь возможность поместить shellcode в атакуемый процесс, а также будем знать, где это код расположен, поэтому все, что нужно будет сделать, это заставить процесс сделать переход на этот адрес.

Например, если вы хотите внедрить код в процесс smss.exe, вам нужно создать разделяемую секцию, подключиться к LPC порту \DbgSsApiPort, затем поместить код в разделяемую секцию, что незамедлительно приведет к проецированию этого кода на адресное пространство smss.exe. Если вы захотите внедрить код в процесс services.exe, вам нужно будет сделать то же самое, только подключаться к LPC порту \RPC Control\DNSResolver.

Данная методика обладает следующими преимуществами:
  • Независимость от локализации Windows.
  • Независимость от установленных пакетов обновлений.
  • Независимость от версии Windows.
  • Нет ограничений на размер shellcode.
  • Нет ограничения на использования нулевых байтов, не нужно кодировать код.
  • Нет ограничений связанных с кодировками символов.
  • Обход некоторых защит от переполнения буфера.
  • Быстрая разработка эксплойта.

Однако есть и некоторые недостатки:

  • У большинства процессов в Windows есть LPC порт, но все же не у всех.
  • Методика может не сработать, если используется переполнение буфера ASCII строкой, так как адрес разделяемой секция сервера в процессе сервера иногда принимает значение 0x00XX0000. Нужно заметить, что это маловероятно, потому что в большинстве случаев (если не во всех) переполнение буфера в Windows происходит Unicode-строкой; кроме того, проблема может быть решена переподключением к LPC порту до тех пор, пока не будет возвращен “хороший” адрес.

Создание эксплойта

Эксплойт, работающий по описанной методике, должен выполнить следующие действия:

  • Создать разделяемую секцию, которая будет проецироваться при LPC соединении.
  • Подключаться к LPC порту уязвимого процесса. После успешного подключения будут получены два указателя на разделяемую секцию, один для клиентского процесса, другой для серверного процесса.
  • Скопировать shellcode в разделяемую секцию, спроецированную на клиентский процесс, в результате чего этот shellcode сразу же будет спроецирован на атакуемый процесс.
  • Используя уязвимость, заставить атакуемый процесс сделать переход на разделяемую секцию, где расположен shellcode.

Давайте рассмотрим простой эксплойт для фиктивной уязвимости в сервисе XYZ, в котором функция VulnerableFunction() получает и обрабатывает Unicode-строку без проверки размера буфера-приемника. В этом примере используется переполнение буфера, но описываемая методика не ограничивается этим типом уязвимостей и может применяться для уязвимостей любого типа, в чем вы сможете убедиться, посмотрев на эксплойты доступные с этой статьей (см. Примеры эксплойтов).

Следующий код создает разделяемую секцию, размером 0x10000 байт с полными правами доступа (чтение, запись, исполнение и т.д.) и с атрибутами страницы – запись и чтение:

HANDLE hSection=0;
LARGE_INTEGER SecSize;

SecSize.LowPart=0x10000;
SecSize.HighPart=0x0;

if(NtCreateSection(&hSection,SECTION_ALL_ACCESS,NULL,&SecSize,
PAGE_READWRITE,SEC_COMMIT ,NULL))
printf(“Could not create shared section. \n”);

Далее идет подключение к LPC порту с именем LPCPortName:

HANDLE hPort;
LPCSECTIONINFO sectionInfo;
LPCSECTIONMAPINFO mapInfo;
DWORD Size = sizeof(ConnectDataBuffer);
UNICODE_STRING uStr;
WCHAR * uString=L"\\LPCPortName";
DWORD maxSize;
SECURITY_QUALITY_OF_SERVICE qos;
byte ConnectDataBuffer[0x100];

for (i=0;i<0x100;i++)
ConnectDataBuffer[i]=0x0;

memset(&sectionInfo, 0, sizeof(sectionInfo));
memset(&mapInfo, 0, sizeof(mapInfo));

sectionInfo.Length = 0x18;
sectionInfo.SectionHandle =hSection;
sectionInfo.SectionSize = 0x10000;

mapInfo.Length = 0x0C;

uStr.Length = wcslen(uString)*2;
uStr.MaximumLength = wcslen(uString)*2+2;
uStr.Buffer =uString;

if (NtConnectPort(&hPort,&uStr,&qos,(DWORD *)&sectionInfo,(DWORD *)&mapInfo,
&maxSize,(DWORD*)ConnectDataBuffer,&Size))
printf(“Could not connect to LPC port.\n”);

После удачного соединения указатели на начало проецируемой разделяемой секции в клиентском и серверном процессах возвращаются в sectionInfo.ClientBaseAddress и sectionInfo.ServerBaseAddress соответственно.

Следующих код копирует shellcode в разделяемую секцию, спроецированную на адресное пространство клиентского приложения:

_asm {
pushad

lea esi, Shellcode
mov edi, sectionInfo.ClientBaseAddress
add edi, 0x10 //опасайтесь 0000
lea ecx, End
sub ecx, esi
cld

rep movsb
jmp Done
Shellcode:
//поместите сюда ваш shellcode
End:
Done:
popad
}

Следующий код, используя уязвимость, заставляет атакуемый процесс сделать переход на разделяемую секцию:

_asm {
pushad
lea ebx, [buffer+0xabc]
mov eax, sectionInfo.ServerBaseAddress
add eax, 0x10 //опасайтесь 0000
mov [ebx], eax //устанавливаем указатель на разделяемую секцию
//в серверном процессе для перезаписи адреса возврата
popad
}

VulnerableFunction(buffer); //используем уязвимую функцию для выполнения shellcode

Проблемы с использованием LPC портов

При работе с LPC портами могут возникнуть некоторые проблемы:

  1. Некоторые LPC порты имеют динамические имена (например, порты, используемые OLE/COM). Это означает, что имя порта, при создании его процессом, всегда новое.
  2. Некоторые LPC порты имеют строгие ACL и не позволяют нам подключиться, если у нас нет соответствующих прав.
  3. Для успешного подключения к некоторым портам нужно передать определенные данные через параметр ConnectionInfo.

Первую проблему можно решить двумя способами. Первый, достаточно трудоемкий способ состоит в дизассемблировании приложения и изучении алгоритма определения имен LPC портов. Второй способ заключается в перехвате некоторой функции для получения имени порта. При работе с Automation (OLE/COM) перед подключением к порту, клиентский процесс определяет имя LPC порта сервера некоторым магическим образом, реализованным внутри COM/OLE. Дизассемблирование всего этого весьма непростое занятие, но мы можем, перехватив вызов функции NtConnectPort, определить имя LPC порта, когда функция попытается подключиться к порту. Этот прием можно увидеть в одном из эксплойтов, прилагающихся к этой статье (см. Примеры эксплойтов).

Вторая проблема выглядит неразрешимой... Ой, я сказал “неразрешимой”? Извините, ведь такого слова нет в словаре хакера. На данный момент эта проблема кажется неразрешимой, но LPC настолько не изучен, и мне приходилось наблюдать некоторые сверхъестественные вещи, касающиеся LPC, поэтому я не уверен на 100%. Можно подключиться к LPC порту не напрямую, обойдя ограничения, но при этом будут проблемы с созданием разделяемой секции. Я углублюсь в эту тему, когда появится свободное время.

Третья проблема может быть легко решена дизассемблированием и установлением того, как происходит подключение к проблематичному порту. Попробуйте поставить точку останова на функцию NtConnectPort, посмотрите на значения параметров, передаваемых в функцию, и используйте их в эксплойте.

Примеры эксплойтов

Чтобы увидеть описанную методику в действии, взгляните на прилагающиеся эксплойты.

  • SSExploit2 (MS05-012 - COM Structured Storage Vulnerability - CAN-2005-0047)
  • TapiExploit (MS05-040 - Telephony Service Vulnerability – CAN-2005-0058)

Заключение

Как вы могли убедиться, используя описанную методику, очень просто за 5-10 минут создать почти 100% надежный (я говорю “почти”, потому что не все уязвимости просты в использовании, а иногда слишком сложны для создания надежного эксплойта) эксплойт, использующий простое переполнение стека, независимый от локализации системы и пакета обновлений. По крайнее мере мне потребовалось именно столько времени для создания локального TAPI эксплойта (MS05-040).

Ссылки

  1. Hacking Windows Internals:
    http://www.argeniss.com/research/hackwininter.zip
  2. Undocumented Windows Functions:
    http://undocumented.ntinternals.net
  3. Windows NT/2000 Native API reference:
    http://www.amazon.com/exec/obidos/tg/detail/-/1578701996/102-0709802-0324157
  4. Local Procedure Call:
    http://www.windowsitlibrary.com/Content/356/08/1.html
  5. Various security vulnerabilities with LPC ports:
    http://www.bindview.com/Services/razor/Advisories/2000/LPCAdvisory.cfm
  6. Обход аппаратной реализации DEP в Windows:
    http://www.securitylab.ru/analytics/263899.php

Тени в интернете всегда следят за вами

Станьте невидимкой – подключайтесь к нашему каналу.