Обучение волшебству: исследуем потайные места и темную магию macOS
Отправляемся на платформу 9¾, или Как настроить окружение
Прежде всего нам понадобится тестовое окружение для исследования ОС и анализа ВПО. Так сложилось, что, когда я начинал свой путь в роли исследователя безопасности приложений macOS, у меня был только компьютер с Windows и x86 процессором. К счастью, есть проверенное решение — средство виртуализации VMware Workstation.
Запускаем, начинаем создавать виртуалку и… сталкиваемся с тем, что в списке гостевых ОС нельзя выбрать macOS. Чтобы такая возможность появилась, можно воспользоваться сторонней утилитой Unlocker, которая легко находится в поиске. «Зря я это сказал…» :)
Теперь нужно где-то раздобыть установочный образ ОС. Можно, конечно, просто скачать один из множества представленных в интернете ISO-образ, но никогда не знаешь, кто в них покопался. Лучше возьмем оригинальный образ с серверов Apple.
Для этого существуют различные скрипты, к примеру, можно воспользоваться вот этим репозиторием. Забираем последний релиз, переходим в каталог \Utilities\macrecovery\, запускаем Python-скрипт macrecovery.py и следуем инструкциям. Пример запуска:
$ python3 macrecovery.py -b Mac-937A206F2EE63C01 -m 00000000000000000 download
После скачивания файла в системе появится каталог \com.apple.recovery.boot\ с образом BaseSystem.dmg. Его нужно сконвертировать в VMDK для VMware Workstation. В этом нам поможет утилита qemu-img и следующая команда:
$ qemu-img convert -O vmdk -o compat6 BaseSystem.dmg recovery.vmdk
Полученный после конвертации файл recovery.vmdk подключаем в качестве жесткого диска к виртуальной машине с macOS и завершаем установку.
Важная ремарка: в 2020 г. Apple перешла на собственные ARM-процессоры, соответственно, все свежие версии ОС разрабатываются под эту архитектуру. Эпоха Intel и x86 закончилась на macOS Catalina (10.15): начиная с macOS Big Sur (11) новые версии выходят только под ARM.
Это нужно иметь в виду, потому что виртуализация новых версий macOS на x86-процессоре будет работать медленно. Можно попробовать установить вторую виртуальную машину с macOS Catalina, но здесь поджидает другая проблема: не все современное ПО, в том числе вредоносное, имеет обратную совместимость, поэтому может попросту не запуститься.
Теперь переходим к компьютерам Apple. «Я почему раньше злой был? Потому что у меня макбука не было…» И правда, в этом случае окружение настраивается гораздо проще. Можно воспользоваться платным решением Parallels или же бесплатным UTM (на базе QEMU). Прелесть в том, что в них прямо из коробки есть функциональность для скачивания образов ОС в нужном формате.
Следующий шаг — необязательный, при необходимости к нему можно вернуться позже. Но раз уж заговорили о настройке окружения, сразу расскажу и о том, как получить больше контроля в гостевой среде, а именно об отключении SIP.
SIP (System Integrity Protection) — это один из встроенных в macOS механизмов безопасности, который контролирует доступ к критически важным компонентам системы и разрешает только чтение (в том числе для root-пользователя). В частности, речь идет о каталогах и подкаталогах /System, /usr, /bin, /sbin, var, а также предустановленных по умолчанию приложениях. Кроме того, SIP:
- контролирует доступ к подсистеме Endpoint Security;
- запрещает отладку и трассировку исполняемых системных файлов;
- проверяет подпись расширений ядра;
- контролирует целостность системного раздела (SSV).
Чтобы узнать, какие файлы защищает SIP, вводим в консоль следующую команду:
Для отключения SIP нужно загрузиться в режиме Recovery, открыть консоль и ввести команду csrutil disable, а затем перезагрузить ОС в обычном режиме.
Магия вне архитектуры процессора: как запускаются Intel-сборки ПО на Apple Silicon
В октябре 2020 г. появились компьютеры с процессорами Apple Silicon и архитектурой ARM. При переходе на новую архитектуру возникла проблема с совместимостью приложений: все, что было создано под Intel, перестало работать. Для решения этой задачи Apple предлагает использовать технологию Rosetta 2, которая позволяет транслировать код из x86 в arm. Возникает вопрос: что тогда такое Rosetta 1? Дело в том, что Apple уже меняла архитектуру процеcсора в 2006 году: Rosetta 1 транслировала код из PowerPC в x86.
Как все это работает? Основные сервисы расположены в каталоге /usr/libexec/rosetta.
При запуске программы, написанной под x86, в дело вступает oahd-демон. Он проверяет, не производилась ли трансляция кода ранее. Для этого демон сканирует каталог /var/db/oah/*/*/ на наличие файла с расширением .aot, в котором должны содержаться результаты трансляции кода. Если такого файла нет, начнется процесс трансляции и его создания. К слову, .aot — это аббревиатура Ahead-Of-Time, которая означает, что трансляция выполняется до фактического запуска потока выполнения.
Папки и файлы в каталоге /var/db/oah защищены SIP, поэтому без его отключения нельзя получить доступ к содержимому.
После того как файл .aot был найден или создан, он попадает в виртуальную память исходного процесса. Далее управление передается в эту секцию, и начинается выполнение кода программы.
Фантастические твари и где они обитают: форматы файлов и базовые инструменты
Предположим, у нас есть некий файл без расширения с неочевидным именем. Нужно его проанализировать и выдать вердикт: вредонос это или нет.
Для начала необходимо определить тип файла — от этого будет зависеть дальнейший выбор инструментов. Задачу можно решить с помощью встроенной утилиты командной строки file (к слову, этот способ подходит и для Linux):
$ file VT_applet/0107f1667048a08ca5fd79e7139f7df5ca9cfbc6085208582011421a1c91156aVT_applet/0107f1667048a08ca5fd79e7139f7df5ca9cfbc6085208582011421a1c91156a: Mach-O 64-bit executable x86_64
Итак, в macOS есть несколько основных типов файлов:
- образы дисков Apple Disk Images (.dmg);
- установочные пакеты (.pkg);
- различные скрипты: python (.py), shell (.sh), AppleScript (.scpt);
- application bundle, или пакет приложения (.app);
- исполняемый файл Mach-O.
Остановимся на каждом из них подробнее.
Образы дисков Apple Disk Images (.dmg)
Начнем с простого — образа macOS, который мы скачали ранее. Вводим в file следующую команду:
$ file /Utilities/macrecovery/com.apple.recovery.boot/BaseSystem.dmg
И получаем ответ:
Utilities/macrecovery/com.apple.recovery.boot/BaseSystem.dmg: zlib compressed data
Как видно из вывода утилиты, перед нами просто сжатые данные. На Windows содержимое образа можно посмотреть с помощью 7-zip (см. рис. 6).
В macOS есть встроенная утилита hdiutil, которая позволяет смонтировать образ в текущую файловую систему:
$ hdiutil attach /Utilities/macrecovery/com.apple.recovery.boot/BaseSystem.dmg
В конце вывода команды будет указан путь (/Volumes/macOS Base System), по которому можно найти смонтированный образ. Кстати, каталог /Volumes/ скрыт: чтобы отобразить его в Finder, нужно нажать Command + Shift +.
Установочные пакеты (.pkg)
Все установочные пакеты в macOS имеют расширение .pkg — в противном случае ОС не интерпретирует файл как установочный. В качестве примера рассмотрим fake.pkg.
$ file fake.pkg fake.pkg: xar archive compressed TOC: 760, SHA-1 checksum, contains zlib compressed data
Файл имеет формат XAR (Extensible Archive Format) и обладает следующей структурой:
- header,
- TOC (table of content),
- heap.
Структура заголовка header представлена ниже:
На рис. 8 представлен пример заголовка и TOC, сжатого с помощью zlib.
TOC (оглавление) — это XML-документ, который содержит информацию о файлах в архиве. Формат XAR позволяет независимо сжимать и кодировать отдельные файлы. При этом можно использовать разные варианты сжатия, например GZIP, BZIP2. Содержимое TOC выглядит следующим образом:
Наконец, Heap — это неструктурированный тип данных, в котором хранятся файлы и на который ссылается TOC.
Подсвечу интересные ноды в TOC:
- xar.toc.file.name == Scripts. Сжатый поток байтов, содержащий скрипты preinstall и postinstall в формате CPIO (Copy Input/Output).
- xar.toc.file.name == Distribution. Сжатый поток байтов, содержащий файл в формате XML. Может содержать скрипт для проверки окружения.
- xar.toc.file.name == Plugins. Вспомогательное ПО, которое вызывается в процессе установки.
- xar.toc.file.name == Payload. Сжатый поток байтов, содержащий основную нагрузку для установки в ОС.
На рис. 9 представлен пример легитимного скрипта в файле Distribution. Скрипт проверяет требования ОС для установки ПО.
А вот на рис. 10 представлен фрагмент подозрительного скрипта. В процессе его выполнения происходит закрепление в системе и скачивание некого файла через утилиту curl.
Аналогично служебные скрипты preinstall и postinstall отвечают за подготовку системы перед установкой и очистку временных файлов после нее, но часто применяются злоумышленниками для выполнения вредоносного кода.
Чтобы извлечь содержимое установочного пакета для дальнейшего анализа, можно воспользоваться встроенной утилитой командной строки pkgutil:
$ pkgutil --expand fake.pkg out
Кроме того, можно просмотреть содержимое установочного пакета с помощью Suspicious Package (отдельного ПО с GUI).
Скрипты: python (*.py), shell (*.sh), AppleScript (*.scpt)
Сегодня остановимся на AppleScript. Это разработанный Apple и встроенный в macOS язык сценариев, который используется на компьютерах корпорации с 1991 г. Одна из ключевых особенностей AppleScript заключается в том, что написанные на нем скрипты читаются как обычный английский текст (также они могут называться OSAScript — Open Scripting Architecture).
Скрипты позволяют управлять приложениями с поддержкой сценариев, а также компонентами самой macOS. Важное условие: приложения должны поддерживать межпрограммное взаимодействие, т. е. воспринимать и реагировать на AppleEvent-сообщения. Они отправляются, когда команда скрипта взаимодействует с приложением: например, когда сценарий tell application 'Finder' get disks запрашивает у файлового менеджера Finder имена примонтированных дисков.
Каждое приложение, понимающее AppleScript, публикует поддерживаемые команды в специальном словаре. Он представляет собой XML-файл с расширением .sdef (scripting difinition) и хранится в отдельной папке среди ресурсов пакета.
Каждая часть информации в AppleEvent связана с четырехзначным кодом, иногда даже с двумя. При этом файлы с определениями кодов разбросаны по всей системе. Приведу несколько примеров:
Сценарии можно компилировать и сохранять в файл для последующего запуска. Именно со скомпилированными скриптами и сталкивается вирусный аналитик. Однако стоит отметить, что такие скрипты можно «завернуть» еще и в Application Bundle — тогда они будут выглядеть как обычное приложение (но об этом чуть позже).
Скомпилированные скрипты имеют расширение *.scpt и две магических константы: строку FasdUAS в начале файла и байты \xFA\xDE\xDE\xAD в самом конце. Помимо этого, в них есть строка с версией скрипта (длиной 8 байт).
При сохранении сценария по умолчанию конечный файл будет содержать две формы его представления:
- Сериализованный формат — с сохранением связей и вложенности AppleEvent-кодов.
- Скомпилированный байт-код и список AppleEvent-кодов без сохранения взаимосвязей между ними (связь задается в байт-коде).
Такие файлы можно снова открыть в редакторе сценариев: в результате произойдет декомпиляция на основе п. 1 и вы увидите исходный текст сценария на английском языке. В то же время при сохранении в файл можно выставить флаг RunOnly: в этом случае итоговый файл будет содержать только часть п. 2.
Рассмотрим скрипт, который создает на рабочем столе папку с названием JackDaniels:
tell application "Finder"
make new folder at desktop with properties {name:"JackDaniels"}end tell
В терминах AppleEvent-кодов сценарий будет выглядеть следующим образом:
Если вы откроете файл в hex-редакторе, то увидите все эти core, crel, desk и JackDaniels (см. приложение).
Какой бы вариант сохранения сценария в файл вы ни выбрали, выполнение сценария будет происходить на основе байт-кода виртуальной машины, которая реализована в модуле /System/Library/PrivateFrameworks/AppleScript.framework/Versions/A/AppleScript в функции UASExecute1.
Для анализа подобных файлов можно использовать два инструмента:
В первом реализована часть функций из /System/Library/PrivateFrameworks/AppleScript.framework/Versions/A/AppleScript (а именно считывание скомпилированных файлов, парсинг и дизассемблирование байт-кода виртуальной машины). Во втором есть человекочитаемое описание четырехсимвольных кодов из некоторых словарей в системе (запускается только на macOS, потому что инструмент сначала проводит поиск словарей в системе).
Application bundle, или пакет приложения (.app)
Это каталог со стандартизированной структурой, который содержит исполняемый код и ресурсы, используемые этим кодом. Внутри application bundle есть подраздел Contents, в котором как минимум должны находиться следующие файлы:
- MacOS/<Executable>: исполняемый файл или скрипт.
- Info.plist: файл-манифест для приложения. В нем задается имя исполняемого файла, который запускается при старте приложения, а также различные требования (например, минимальная версия ОС или запрос на права доступа к микрофону и камере).
В качестве примера рассмотрим содержимое application bundle для Telegram (см. рис. 16).
Фрагмент Info.plist представлен ниже:
Application Bundle также может содержать необязательные файлы. Приведу несколько примеров:
- Resources/. Вспомогательные файлы: например, иконка, шрифты, локализации строк.
- Plugins/. Небольшие исполняемые файлы, расширяющие функционал основного.
- Frameworks/. Плагины и высокоуровневые фреймворки, необходимые приложению (модули зависимостей).
- _CodeSignature/CodeResources. XML-файл, содержащий сигнатуры для файлов в application bundle.
Вернемся к поднятому выше вопросу «завертывания» скомпилированных файлов в application bundle. Для этого в ОС хранится заготовка скомпилированного исполняемого файла:
/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/OpenScripting.framework/Versions/A/Resources/AppletStub.
При «заворачивании» AppletStub переименовывается в applet. Сама заготовка содержит вызовы нескольких API-функций, которые подгружает и запускает скомпилированный скрипт:
OpenDefaultComponent('aplt', 'scpt')CallComponentDispatch(…);Cкомпилированный скрипт должен располагаться в application bundle в папке Scripts и называться main.scpt.
Mach-O
В macOS есть свой формат исполняемых файлов — Mach-O. Такие файлы состоят из трех частей: заголовка (mach_header), команд загрузки (load_commands) и набора сегментов (segments).
- Заголовок содержит магические байты, информацию об архитектуре процессора, для которого скомпилирован файл, типе файла (динамическая библиотека, исполняемый и т. д.) и другие метаданные.
- Команды загрузки содержат информацию, которую динамический загрузчик использует для отображения двоичного файла в памяти. Здесь указаны адрес точки входа (LC_MAIN) для исполняемого файла, динамические зависимости (LC_LOAD_DYLIB) и др.
- За командами загрузки следует оставшаяся часть двоичного файла Mach O: в основном она состоит из байт-кода, переменных и т. д.
Первичную информацию об исполняемом файле можно получить прямо в консоли с помощью встроенных утилит:
- $ otool -L <input> — покажет, какие библиотеки находятся в зависимостях у исполняемого файла.
- $ otool -oV <input> — позволяет узнать, какие методы есть у исполняемого файла.
- $ otool -tV <input> — позволяет дизассемблировать код.
Исполняемый файл Mach-O содержит код только для одной архитектуры процессора. Поэтому приложения часто распространяются в виде бинарных файлов Universal Binary или Fat Binary, которые содержат несколько Mach-O для разных архитектур. В этом случае бинарный файл начинается с fat_header, а его тело содержит несколько склеенных Mach-O. Загрузчик Mach-O выбирает двоичный файл с подходящей архитектурой и загружает его в память для запуска программы.
Отмечу, что файл Fat Binary можно усечь и оставить только нужную архитектуру:
$ lipo <input> -thin x86_64 -output <output>
Важной частью исполняемого файла является подпись. Она позволяет установить авторство и убедиться в целостности файла, но не гарантирует, что это не вредонос. Чтобы посмотреть подпись, нужно ввести следующую команду (чем больше символов v, тем подробнее будет вывод):
Вместе с подписью хранятся Entitlements приложения — пары «ключ:значение», которые дают исполняемым файлам разрешения использовать тот или иной сервис либо технологию. Например, com.apple.security.get-task-allow разрешает подключаться к процессу отладчиком. Тем не менее приложение может запрашивать и другие права, не указанные в Entitlements.
Чтобы посмотреть набор прав, используйте команду: $ codesign -d --entitlements - <input>
Внимание к деталям: разбираемся с соглашениями о вызовах x86_64 и arm64
Говоря о реверсе, нельзя не упомянуть соглашения о вызовах. Они определяют, каким образом аргументы передаются в функцию, как возвращается результат и какая функция отвечает за очистку стека. Эти правила сформулированы в двоичном интерфейсе приложения — Application Binary Interface (ABI).
|
Аргумент |
Регистр |
|
1-й аргумент |
rdi |
|
2-ой аргумент |
rsi |
|
3-ий аргумент |
rdx |
|
4-ый аргумент |
rcx |
|
5-ый аргумент |
r8 |
|
6-ой аргумент |
r9 |
|
7-ой аргумент |
стек |
|
Возвращаемое значение |
rax |
Таблица 1. ABI для x86_64 в macOS
|
Аргумент |
Регистр |
|
C 1-го по 8-ой аргумент |
x0 – x7 |
|
9-ый аргумент и далее |
стек |
|
Возвращаемое значение |
x0 |
Таблица 2. ABI для arm64 в macOS
Скажите, почему когда что-то происходит, вы трое всегда оказываетесь рядом?
При обратной разработке бинарного файла чаще всего встречаются семплы, скомпилированные с помощью C/C++, Objective-C и Swift. Конечно, бывают и образцы на Go и Rust, но в их анализе нет ничего особенного (впрочем, как и в случае с C/C++): все как на Windows и Linux. А вот при встрече с сеэмплами, скомпилированным на Objective-C и Swift, сразу возникают непривычные языковые конструкции. Поговорим о них подробнее.
Objective-C
Этот язык часто используется для разработки приложений для macOS и iOS. Рассмотрим пример объявления и инициализации числа:
NSNumber *number = [[NSNumber alloc] initWithInt:1];
Если мы скомпилируем, а затем декомпилируем код, то получим следующий результат:
int v1; int v2;
v1 = objc_msgSend(_OBJC_CLASS_$_NSNUMBER, "alloc");
v2 = objc_msgSend(v1, "initWithInt", 1);
Этот пример демонстрирует основы выделения объектов и вызова методов. Каждый вызов метода выполняется путем вызова функции среды выполнения objc_msgSend:
id objc_msgSend(id self, SEL op, …);
Обязательные аргументы self и op — это указатели на объект класса и так называемый селектор; id — ключевое слово, обозначающее указатель на объект. В свою очередь, селектор представляет понятное для человека название метода: в рамках нашего примера это alloc и initWithInt.
Основная цель функции objc_msgSend — найти реализацию метода для данного объекта и селектора, а затем вызвать его и передать все указанные аргументы. Описанный подход привносит специфический нюанс в процесс реверса: человекочитаемый селектор не может быть обфусцирован для системного API, что помогает быстрее понять исполняемый код.
Названия селекторов содержатся в разделе __objc_methname сегмента __TEXT.
Swift
Язык Swift тоже активно применяется для разработки приложений для macOS и iOS. У него есть три интересные особенности…
Первая: помимо вызова привычных функций вида sub_* при реверсе можно встретить что-то невнятное, а-ля $s7SwiftUI3AppPAAE4mainyyFZ. Это вызов Swift-функции, которая представлена в виде «замангленного» имени (Swift mangled name).
В подобных именах закодирован класс, в котором содержится функция, ее имя, а также типы принимаемых аргументов и возвращаемого значения. Отмечу, что в пакете разработчика Swift есть специальная утилита для декодирования «замангленных» имен: swift-demangle.
Начиная с версии 7.7 (ниже не проверял) IDA умеет раскодировать такие имена прямо из коробки — функция включена по умолчанию. В качестве примера рассмотрим вывод декодированного имени Button.init (см. рис. 18).
Итак, функция принимает два аргумента, но что они означают? Мы помним, что в закодированном имени в том числе содержатся типы принимаемых аргументов. Пора испытать swift-demangle, причем в каждом из трех возможных режимов (см. рис. 19).
Выводы в первых двух режимах практически одинаковы: вероятнее всего, в IDA используется один из них. Из вывода в третьем режиме становится ясно, что функция Button.init первым аргументом принимает LocalizatedString с текстом кнопки, а вторым — action() — функцию, которая обрабатывает нажатие на кнопку. Резюмируем: IDA многое умеет из коробки, но за подробностями лучше обращаться в swift-demangle.
Теперь переходим ко второй особенности Swift — это реализация строк, которые здесь представлены в виде структур:
struct _StringObject
{ var _countAndFlagsBits;
var _object;
};
В зависимости от значений флагов в _countAndFlagsBits (60–63-й биты) в _objects может храниться либо непосредственное значение (для коротких строк), либо адрес.
Если посмотреть на биты с номерами 60–63 в 0xE600000000000000 и интерпретировать их согласно документации, то станет ясно, что перед нами короткая строка, а 'lecnaC' — не что иное, как непосредственное значение "Cancel".
Рассмотрим другой пример (см. рис. 21). Что такое 0xD0000000000000002A и 0x80000000010000AD80? Из документации узнаем, что это длинная строка, а младшие значащие биты первого числа 0x2A — это ее размер. Чтобы получить адрес строки, нужно к 0x10000AD80 прибавить 0x20 (nativeBias тоже из документации). В итоге получаем адрес 0x10000ADA0.
Наконец, третья особенность: в коде Swift можно вызывать методы из Objective-C. Для этого нужно сконвертировать данные из одного представления в другое. Например, Swift-строку из struct _StringObject в объект класса NSString в Objective-C. Для этого используются методы с названием class.bridgeToObjectiveC() и class.bridgeFromObjectiveC(). В контексте реверса достаточно понимать, что это обычная конвертация данных.
***
Сегодня мы разобрались, как настроить тестовую среду для исследования вредоносных программ, как использовать статический анализ для реверса двоичных файлов, а также рассмотрели несколько распространенных форматов файлов и инструменты их анализа. Продолжение — в будущих статьях ;)
Если это ваш первый опыт реверс-инжиниринга ВПО для macOS, также рекомендую две полезные ссылки:
Приложение
Hexdump-представление скомпилированного osascript
