Структура Mach-O файлов и реверс-инжиниринг

Курс подробно разбирает внутреннее устройство бинарного формата Mach-O, применяемого в операционных системах macOS и iOS [xn--h1ajim.xn--p1ai](https://xn--h1ajim.xn--p1ai/Mach-O). Вы освоите методы статического и динамического анализа, работу с загрузчиком dyld, а также техники внедрения собственного кода и перехвата функций [habr.com](https://habr.com/ru/articles/893664/).

1. Введение в формат Mach-O: заголовки, команды загрузки и сегменты

Введение в формат Mach-O: заголовки, команды загрузки и сегменты

Формат Mach-O (Mach object) представляет собой базовый стандарт для исполняемых файлов, объектного кода, динамических библиотек и дампов памяти в экосистеме Apple. Понимание его внутреннего устройства — это первый и самый важный шаг в реверс-инжиниринге программного обеспечения под операционные системы macOS и iOS. В отличие от форматов Portable Executable в Windows или Executable and Linkable Format в Linux, архитектура Mach object изначально проектировалась с прицелом на поддержку нескольких процессорных архитектур внутри одного файла и тесную интеграцию с динамическим загрузчиком.

> Mach-O используется в большинстве систем, основанных на ядре Mach, например NeXTSTEP, iOS и Mac OS X. Был введён вместо формата a.out и предоставляет большие возможности для расширяемости и более быстрый доступ к информации в таблице символов. > > Энциклопедия Руниверсалис

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

  • Заголовок (Header) — содержит базовую информацию об архитектуре и типе файла.
  • Команды загрузки (Load Commands) — инструкции для операционной системы о том, как именно нужно размещать файл в оперативной памяти.
  • Данные (Data) — фактическое содержимое программы, разделенное на сегменты и секции (исполняемый код, строки, константы).
  • Заголовок файла: идентификация и архитектура

    Анализ любого неизвестного бинарного файла начинается с его первых байтов. В самом начале структуры располагается магическое число (magic number) — уникальная последовательность байтов, которая позволяет операционной системе мгновенно определить формат файла и его битность, не читая расширение.

    За магическим числом следуют поля, определяющие тип центрального процессора (CPU type), подтип процессора (CPU subtype), тип самого файла (исполняемый, библиотека или объектный файл) и количество команд загрузки.

    | Магическое число (Hex) | Битность | Описание | |---|---|---| | 0xfeedface | 32-bit | Классический 32-битный бинарный файл. В современных системах Apple практически не встречается. | | 0xfeedfacf | 64-bit | Стандартный 64-битный бинарный файл. Основной формат для современных macOS и iOS. | | 0xcafebabe | Universal | Универсальный бинарный файл (Fat binary). Является контейнером, внутри которого лежат несколько независимых файлов Mach-O для разных архитектур. |

    Концепция Fat binary заслуживает отдельного внимания. Когда компания Apple переходила с процессоров PowerPC на Intel, а затем с Intel на собственные чипы Apple Silicon (ARM), разработчикам требовался способ распространять одно приложение, которое работало бы на обеих архитектурах. Универсальный бинарный файл решает эту задачу: он начинается с магического числа 0xcafebabe, после которого идет таблица с указанием смещений к полноценным файлам Mach-O для каждой поддерживаемой архитектуры.

    Например, если размер универсального файла составляет 100 мегабайт, он может содержать внутри себя версию для Intel x86_64 размером 45 мегабайт и версию для ARM64 размером 55 мегабайт. Операционная система при запуске читает заголовок Fat binary, находит смещение для текущей архитектуры процессора и загружает в память только нужную часть, игнорируя остальной объем.

    Команды загрузки: инструкции для ядра и dyld

    Сразу после заголовка располагаются команды загрузки. Это своеобразная маршрутная карта для ядра операционной системы и динамического компоновщика (dyld). Каждая команда начинается с двух полей: типа команды и её размера в байтах. Благодаря указанию размера, парсеры могут легко пропускать неизвестные им команды, что обеспечивает высокую обратную совместимость формата.

    Среди десятков возможных команд загрузки в контексте реверс-инжиниринга наиболее важны следующие:

    * LC_SEGMENT или LC_SEGMENT_64 — указывает загрузчику выделить участок оперативной памяти и скопировать туда определенную часть файла. * LC_LOAD_DYLIB — сообщает о необходимости загрузить внешнюю динамическую библиотеку, от которой зависит программа (например, системные фреймворки). * LC_MAIN — определяет точку входа в программу, то есть адрес первой инструкции, с которой начнется выполнение кода. * LC_SYMTAB — содержит информацию о таблице символов, которая критически важна для отладки и дизассемблирования.

    Для просмотра команд загрузки в системах macOS традиционно используется консольная утилита otool.

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

    Сегменты и секции: организация данных в памяти

    Команды LC_SEGMENT_64 делят оставшуюся часть файла на логические блоки — сегменты. Имена сегментов традиционно пишутся в верхнем регистре с двумя подчеркиваниями в начале. Каждый сегмент имеет свои права доступа на уровне страниц виртуальной памяти: чтение (Read), запись (Write) и исполнение (Execute).

    Внутри сегментов данные дополнительно дробятся на секции. Имена секций пишутся в нижнем регистре, также с двумя подчеркиваниями.

  • Сегмент __TEXT — содержит исполняемый код программы и константы (данные, которые не должны изменяться). Этот сегмент всегда имеет права на чтение и исполнение, но защищен от записи. Это базовая мера безопасности: программа не может случайно или намеренно изменить собственный код во время работы.
  • * Секция __text — непосредственно скомпилированные машинные инструкции. * Секция __cstring — неизменяемые строковые литералы (например, текст, который выводится в консоль).
  • Сегмент __DATA — содержит глобальные и статические переменные, которые могут изменяться в процессе работы программы. Имеет права на чтение и запись, но не на исполнение.
  • * Секция __data — инициализированные переменные. * Секция __bss — неинициализированные переменные (заполняются нулями при загрузке).
  • Сегмент __LINKEDIT — содержит метаданные для динамического компоновщика: таблицы символов, информацию о строках и цифровые подписи.
  • При загрузке файла в память операционная система транслирует физические смещения внутри файла в виртуальные адреса оперативной памяти. Для понимания этого процесса используется базовая формула вычисления виртуального адреса:

    Где: * — итоговый виртуальный адрес в оперативной памяти, по которому будет доступна инструкция или переменная. * — базовый адрес загрузки сегмента, который назначается операционной системой (часто с использованием рандомизации адресного пространства — ASLR). * — физическое смещение конкретных данных относительно начала сегмента внутри самого файла.

    Допустим, при анализе бинарного файла мы видим, что сегмент __TEXT загружен ядром по базовому адресу 4096. Если интересующая нас текстовая строка находится на смещении 128 байт от начала этого сегмента в файле, то в оперативной памяти запущенного процесса мы сможем прочитать эту строку по виртуальному адресу 4224.

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