Хакер - Липосакция для fat binary. Ломаем программу для macOS с поддержкой нескольких архитектур
hacker_frei
МВК
Содержание статьи
- Немного теории
- Intel
- ARM
- Патчим плагин
Мы много раз писали о взломе программ для Windows. Для нее создано множество отладчиков, дизассемблеров и других полезных инструментов. Сегодня же мы обратим взор на мультипроцессорную программу для macOS, вернее, на плагин для маковского Adobe Illustrator CC 2021, который (в целях обучения!) будет превращен из пробной версии в полноценную. Причем понадобятся нам исключительно инструменты для Windows: IDA версии 7.2 и Hiew.
WARNING
Вся информация предоставлена исключительно в ознакомительных и обучающих целях. Ни автор, ни редакция не несут ответственности за любой возможный вред, причиненный материалами данной статьи. Нарушение лицензии при использовании ПО может преследоваться по закону.
НЕМНОГО ТЕОРИИ
Для начала коротко попытаемся получить представление, что именно нам предстоит ломать. Мы уже привыкли, что все исполняемые файлы и библиотеки под актуальные версии Windows именуются EXE/DLL и имеют структуру MZ-PE. Под macOS используется формат Mach-O (сокращение от Mach object), являющийся потомком формата a.out, который макось унаследовала от Unix.
Как известно, Apple любит периодически переходить с одного семейства процессоров на другое, из‑за чего меняется и архитектура приложений. Начав с PowerPC, Apple в середине нулевых переметнулась в стан Intel, после чего в недавнем прошлом корпорация решила перейти на платформу ARM. Дабы пользователи поменьше страдали от подобных метаний, был взят на вооружение мультипроцессорный формат Fat binary («жирный бинарник»), который может содержать код одновременно под несколько процессоров. Такой модуль может работать как под Intel, так и под ARM.
Что же такое модуль Mach-O? Обычно он состоит из трех областей. Заголовок содержит общую информацию о двоичном файле: порядок байтов (магическое число), тип процессора, количество команд загрузки и т. д. Затем следуют команды загрузки — это своего рода оглавление, которое описывает положение сегментов, динамическую таблицу символов и прочие полезные вещи. Каждая команда загрузки содержит метаданные, такие как тип команды, ее имя, позиция в двоичном файле. Наконец, третья область — это данные, обычно самая большая часть объектного файла. Она содержит код и различную дополнительную информацию.
Мультипроцессорный «жирный» модуль может включать в себя несколько обычных модулей Mach-O, заточенных под разные процессоры (обычно это i386 и x86_64, ARM или ARM64). Структура его предельно проста — сразу за Fat header, в котором описываются входящие в модуль блоки Mach-O, следует код этих блоков, расположенный подряд. Я не буду подробно останавливаться на описании всех секций и полей данного формата, желающие могут легко нагуглить спецификацию. Остановимся лишь на формате заголовков, поскольку они понадобятся нам в дальнейших действиях.
Структура классического заголовка Mach-O выглядит так.
struct mach_header {
// Сигнатура, обычно CF FA ED FE или CE FA ED FE, но для варианта с обратным порядком байтов, возможна и обратная сигнатура FE ED FA CF
uint32_t magic;
// Тип процессора, для intel это 7, для ARM — С
cpu_type_t cputype;
// Подтип процессора, 1 означает 64-разрядность, например, 07000001h — x86_64
cpu_subtype_t cpusubtype;
// Тип файла
uint32_t filetype;
// Количество команд, следующих за хидером
uint32_t ncmds;
// Размер команд, следующих за хидером
uint32_t sizeofcmds;
// Набор битовых флагов, которые указывают состояние некоторых дополнительных функций формата файла Mach-O
uint32_t flags;
};
«Жирный заголовок» представляет собой типичный заголовок Universal binary и выглядит вот так.
struct fat_header {
uint32_t magic; // 0BEBAFECAh
// Количество последующих блоков fat_arch, соответствующих поддерживаемым процессорам
uint32_t nfat_arch;
};
struct fat_arch {
cpu_type_t cputype;
cpu_subtype_t cpusubtype;
// Смещение до блока кода относительно начала файла
uint32_t offset;
// Длина соответствующего блока кода
uint32_t size;
// Выравнивание
uint32_t align;
};
struct fat_arch {
...
}

Ну а теперь, когда мы в достаточной степени вооружились теорией, рассмотрим практический пример. У нас есть некий инсталлированный иллюстраторовский плагин, который требуется отучить от суицида по прошествии триального периода. Предположим также, что доступа к маку, на котором он установлен, у нас нет, как и другого мака под рукой — только возможность переписывать файлы. Ищем в папке нужного плагина подпапку Contents\MacOS, а в ней — исполняемый модуль плагина. В данном случае это динамическая библиотека Fat Mach-O file, о чем нам говорит сигнатура CA FE BA BE.
INTEL
Загружаем наш файл в IDA Pro: нам будет предложено на выбор два (точнее три) способа загрузки данного файла: Fat Mach-O file, 1.X86_64 и Fat Mach-O file, 2.ARM64. Третий вариант, бинарный файл, нам неинтересен. Начнем с самого простого и знакомого всем пользователям Windows варианта: выбираем Intel X86_64. Бегло пробежавшись по списку названий функций, обнаруживаем имя checkPersonalize2_tryout. Так как у нас триал, данная функция вполне может оказаться проверкой на его валидность. Смотрим, откуда она вызывается — ага, из функции с еще более подозрительным названием _checkUser:
__text:0000000000006A62 mov edi, esi ; SPPlugin *
__text:0000000000006A64 mov rsi, rbx ; _UserData_NSD_ *
__text:0000000000006A67 call __Z24checkPersonalize2_tryoutP8SPPluginP14_UserData_NSD_Ph ; checkPersonalize2_tryout(SPPlugin *,_UserData_NSD_ *,uchar *)
__text:0000000000006A6C test eax, eax
__text:0000000000006A6E jnz short loc_6A95
__text:0000000000006A70 cmp [rbp+var_21], 0
__text:0000000000006A74 jz short loc_6A95
__text:0000000000006A76
__text:0000000000006A76 loc_6A76: ; CODE XREF: _checkUser:loc_6A5D↑j
__text:0000000000006A76 mov rax, [r12]
__text:0000000000006A7A mov qword ptr [rax], 0
__text:0000000000006A81 mov byte ptr [rax+63Dh], 1
__text:0000000000006A88 mov qword ptr [rax+648h], 0
__text:0000000000006A93 jmp short loc_6A99
__text:0000000000006A95 ; ----------------
__text:0000000000006A95
__text:0000000000006A95 loc_6A95: ; CODE XREF: _checkUser+198↑j
__text:0000000000006A95 ; _checkUser+19E↑j ...
__text:0000000000006A95 mov [rbp+var_21], 0
Поскольку загрузить программу в отладчик и дойти до этого места мы не можем, пробуем догадаться, какой вариант возвращаемого значения eax нас устраивает больше. Выражения в квадратных скобках byte ptr [rax+63Dh] и qword ptr [rax+648h] похожи на установку полей некоей структуры или свойств объекта. Поискав по коду чуть выше, мы увидим такую конструкцию:
__text:00000000000069E4 cmp byte ptr [rbx+63Dh], 0
__text:00000000000069EB jnz loc_6A99
__text:00000000000069F1
__text:00000000000069F1 loc_69F1: ; CODE XREF: _checkUser+124↑j
__text:00000000000069F1 mov [rbp+var_21], 0
__text:00000000000069F5 cmp byte ptr [rbx+653h], 0
__text:00000000000069FC jz short loc_6A35
__text:00000000000069FE cmp byte ptr [rbx+650h], 0
__text:0000000000006A05 jz short loc_6A5D
__text:0000000000006A07 mov edi, 8 ; unsigned __int64
__text:0000000000006A0C call __Znwm ; operator new(ulong)
__text:0000000000006A11 mov r14, rax
__text:0000000000006A14 mov ecx, 22h ; '"'
__text:0000000000006A19 mov rdi, rsp
__text:0000000000006A1C mov rsi, rbx
__text:0000000000006A1F rep movsq
__text:0000000000006A22 mov rdi, rax ; this
__text:0000000000006A25 call __ZN11AboutDialogC1E14_UserData_NSD_ ; AboutDialog::AboutDialog(_UserData_NSD_)
__text:0000000000006A2A mov rax, [r14]
__text:0000000000006A2D mov rdi, r14
__text:0000000000006A30 call qword ptr [rax+8]
__text:0000000000006A33 jmp short loc_6A99
По поведению программы мы помним, что о просрочке триала сигнализирует диалог About, внезапно выскакивающий при загрузке программы, ненулевое же значение байта по адресу [rbx+63Dh] инициирует обход данной ветки. Выходит, что правильной является ветка, в которой этому байту присваивается значение 1 начиная со смещения __text:0000000000006A76 (изначально этот байт инициализируется в 0). Не мудрствуя лукаво, просто закорачиваем весь кусок кода вызова процедуры checkPersonalize2_tryout, установив перед ним безусловный переход на loc_6A76:
__text:0000000000006A5D jmp loc_6A76
__text:0000000000006A62 ; ----------------
__text:0000000000006A62 mov edi, esi ; SPPlugin *
__text:0000000000006A64 mov rsi, rbx ; _UserData_NSD_ *
__text:0000000000006A67 call __Z24checkPersonalize2_tryoutP8SPPluginP14_UserData_NSD_Ph ; checkPersonalize2_tryout(SPPlugin *,_UserData_NSD_ *,uchar *)
__text:0000000000006A6C test eax, eax
__text:0000000000006A6E jnz short loc_6A95
__text:0000000000006A70 cmp [rbp+var_21], 0
__text:0000000000006A74 jz short loc_6A95
__text:0000000000006A76
__text:0000000000006A76 loc_6A76: ; CODE XREF: _checkUser:loc_6A5D↑j
__text:0000000000006A76 mov rax, [r12]
ARM
Итак, с частью кода, ответственной за х86, мы вроде разобрались, попробуем сделать то же самое с армовской частью. Снова загружаем этот модуль в IDA, на сей раз выбрав при загрузке вариант Fat Mach-O file, 2.ARM64. Мы видим, что функции _checkUser и checkPersonalize2_tryout присутствуют и в этой части кода, вышеописанное место вызова в переводе на армовский ассемблер выглядит вот так:
__text:000000000000716C MOV X2, SP
__text:0000000000007170 MOV X0, X19
__text:0000000000007174 MOV X1, X20
__text:0000000000007178 BL __Z24checkPersonalize2_tryoutP8SPPluginP14_UserData_NSD_Ph ; checkPersonalize2_tryout(SPPlugin *,_UserData_NSD_ *,uchar *)
__text:000000000000717C LDRB W8, [SP,#0x150+var_150]
__text:0000000000007180 CMP W0, #0
__text:0000000000007184 CCMP W8, #0, #4, EQ
__text:0000000000007188 B.NE loc_7194
__text:000000000000718C
__text:000000000000718C loc_718C ; CODE XREF: _checkUser+1A0↑j
__text:000000000000718C STRB WZR, [SP,#0x150+var_150]
__text:0000000000007190 B loc_71A4
__text:0000000000007194 ; ----------------
__text:0000000000007194
__text:0000000000007194 loc_7194 ; CODE XREF: _checkUser+1D0↑j
__text:0000000000007194 LDR X8, [X22]
__text:0000000000007198 MOV W9, #1
__text:000000000000719C STRB W9, [X8,#0x3A0]
__text:00000000000071A0 STR XZR, [X8,#0x3A8]
__text:00000000000071A4
__text:00000000000071A4 loc_71A4 ; CODE XREF: _checkUser+118↑j
__text:00000000000071A4 MOV W0, #0
Рассмотрев этот код повнимательнее, мы видим, что в армовском коде аналогом поля [rax+63Dh] служит байт по адресу [X8,#0x3A0], ибо именно ему присваивается единичка при удачном вызове checkPersonalize2_tryout. Поэтому, дабы не изобретать велосипед, действуем тем же способом, что и ранее — закорачиваем кусок кода, вставляя перед ним безусловный переход на loc_7194:
__text:000000000000716C B loc_7194
__text:0000000000007170 MOV X0, X19
__text:0000000000007174 MOV X1, X20
__text:0000000000007178 BL __Z24checkPersonalize2_tryoutP8SPPluginP14_UserData_NSD_Ph ; checkPersonalize2_tryout(SPPlugin *,_UserData_NSD_ *,uchar *)
__text:000000000000717C LDRB W8, [SP,#0x150+var_150]
__text:0000000000007180 CMP W0, #0
__text:0000000000007184 CCMP W8, #0, #4, EQ
__text:0000000000007188 B.NE loc_7194
__text:000000000000718C
__text:000000000000718C loc_718C ; CODE XREF: _checkUser+1A0↑j
__text:000000000000718C STRB WZR, [SP,#0x150+var_150]
__text:0000000000007190 B loc_71A4
__text:0000000000007194 ; ----------------
__text:0000000000007194
__text:0000000000007194 loc_7194 ; CODE XREF: _checkUser+1D0↑j
__text:0000000000007194 LDR X8, [X22]
ПАТЧИМ ПЛАГИН
Теперь, когда мы разобрались, что и где следует менять, нужно внести эти самые изменения. Самое простое, что у нас есть под рукой — маленький DOS-овский шестнадцатеричный редактор Hiew, который, помимо простого байтового редактирования, умеет дизассемблировать и ассемблировать код для Intel и даже для ARM. К сожалению, про ARM64, который нам нужен, и Fat Mach-O он ничего не знает, поэтому придется немного поработать руками, используя на практике описанную выше теорию.
Открыв заголовок модуля, мы видим в нем две секции Mach-O с абсолютными смещениями 8000h и 17С000h. Так и есть, по первому смещению сидит сигнатура секции CF FA ED FE и код процессора 07 00 00 01 — это интеловская часть. По второму смещению сигнатура та же, но код процессора другой 0C 00 00 01 — это ARM.

Прибавляем к 8000h смещение из IDA — 6A5Dh, и получаем EA5Dh — смещение до первого патча в интеловской части. Переключаемся через Ctrl-F1 в 64-битный режим и правим искомый jmp. Теперь внесем изменения в армовскую часть. Тут есть небольшая сложность. Смещение до патча 17С000h+716Ch=18316Ch мы нашли, однако при переключении в режим ARM дизассемблера через Shift-F1 код совсем другой, Hiew не понимает актуальный ARM64. Попробуем вычислить и поправить искомый опкод руками. Открываем спецификацию (если очень лениво искать, то можно просто посмотреть в IDA по соседним командам) — опкод команды безусловного перехода 14h (последний байт команды). Первыми байтами идет смещение до адреса перехода в 32-битных командах. Считаем: 7194h-716Ch=28h делим на 4 байта и получаем 0Ah — искомое смещение для перехода. В результате код исправленной команды выглядит так:
__text:000000000000716C 0A 00 00 14 B loc_7194
Итак, мы пропатчили обе части модуля, однако радоваться рано. При переписывании измененного модуля на место старого программа выдает ошибку. Оно и понятно: macOS делали параноики, каждый модуль должен быть подписан, а при изменении любого байта подпись, разумеется, слетает. По счастью, параноики оставили нам возможность заново подписать модуль на маке из терминала. Для этого после замены модуля нужно зайти в терминал и набрать следующую команду:
sudo codesign --force --deep -sign - <полный путь к пропатченному модулю>
По идее, можно вообще убрать подпись через stripcodesig или даже до копирования на мак, но это получается не всегда. Например, начиная с macOS Catalina, может потребоваться удалить приложение из карантина, для этого в терминале придется набрать следующую команду:
sudo xattr -rd com.apple.quarantine <полный путь к пропатченному модулю>
К сожалению, совсем без доступа к телу маку не обойтись — как минимум придется переписывать и подписывать патченные модули. Конечно, можно было бы перепаковать установочный образ плагина или попробовать натянуть виртуалку с macOS под Windows, но эти способы сильно сложнее. Мы же справились с поставленной задачей успешно, а главное — с минимальными усилиями.
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei