macOS IPC - Inter Process Communication
Повідомлення Mach через порти
Основна інформація
Mach використовує задачі як найменшу одиницю для обміну ресурсами, і кожна задача може містити кілька потоків. Ці задачі та потоки відображаються відношенням 1:1 до процесів та потоків POSIX.
Комунікація між задачами відбувається через міжпроцесорну комунікацію Mach (IPC), використовуючи односторонні канали зв'язку. Повідомлення передаються між портами, які діють як черги повідомлень, керовані ядром.
Порт є основним елементом IPC Mach. Його можна використовувати для відправлення та отримання повідомлень.
У кожному процесі є таблиця IPC, де можна знайти порти mach процесу. Назва порту Mach фактично є числом (вказівником на об'єкт ядра).
Процес також може відправити ім'я порту з деякими правами іншій задачі, і ядро зробить цей запис у таблиці IPC іншої задачі.
Права порту
Права порту, які визначають операції, які може виконувати задача, є ключовими для цієї комунікації. Можливі права порту (визначення тут):
Право отримання, яке дозволяє отримувати повідомлення, відправлені на порт. Порти Mach є чергами MPSC (багатопродуктові, одноконсумерні), що означає, що може бути лише одне право отримання для кожного порту в усій системі (на відміну від каналів, де кілька процесів можуть утримувати дескриптори файлів для читання з одного каналу).
Задача з правом отримання може отримувати повідомлення та створювати права відправлення, що дозволяє відправляти повідомлення. Спочатку лише власна задача має право отримання над своїм портом.
Якщо власник права отримання помер або вбив його, право відправлення стає некорисним (мертве ім'я).
Право відправлення, яке дозволяє відправляти повідомлення на порт.
Право відправлення можна клонувати, тому задача, яка володіє правом відправлення, може склонувати право та надати його третій задачі.
Зауважте, що права порту також можуть бути передані через повідомлення Mac.
Право відправлення один раз, яке дозволяє відправити одне повідомлення на порт, після чого воно зникає.
Це право не можна клонувати, але його можна перемістити.
Право на набір портів, яке вказує на набір портів замість одного порту. Вибір повідомлення з набору портів вибирає повідомлення з одного з його портів. Набори портів можуть використовуватися для прослуховування кількох портів одночасно, схоже на
select
/poll
/epoll
/kqueue
в Unix.Мертве ім'я, яке не є фактичним правом порту, а лише заповнювачем. Коли порт знищується, всі існуючі права порту на порт перетворюються на мертві імена.
Задачі можуть передавати ПРАВА ВІДПРАВЛЕННЯ іншим, дозволяючи їм відправляти повідомлення назад. ПРАВА ВІДПРАВЛЕННЯ також можна клонувати, тому задача може дублювати та давати право третій задачі. Це, разом із проміжним процесом, відомим як запусковий сервер, дозволяє ефективно спілкуватися між задачами.
Порти файлів
Порти файлів дозволяють інкапсулювати дескриптори файлів у портах Mac (з використанням прав порту Mach). Можливо створити fileport
з заданим FD за допомогою fileport_makeport
та створити FD з fileport за допомогою fileport_makefd
.
Встановлення зв'язку
Як вже зазначалося, можливо відправляти права за допомогою повідомлень Mach, однак ви не можете відправити право без наявності права відправлення повідомлення Mach. Тоді як встановлюється перший зв'язок?
Для цього включений запусковий сервер (launchd в Mac), оскільки кожен може отримати ПРАВО ВІДПРАВЛЕННЯ до запускового сервера, можливо попросити його про право відправити повідомлення іншому процесу:
Задача A створює новий порт, отримуючи ПРАВО ОТРИМАННЯ над ним.
Задача A, яка є власником ПРАВА ОТРИМАННЯ, генерує ПРАВО ВІДПРАВЛЕННЯ для порту.
Задача A встановлює з'єднання з запусковим сервером та відправляє йому ПРАВО ВІДПРАВЛЕННЯ для порту, яке вона згенерувала на початку.
Пам'ятайте, що кожен може отримати ПРАВО ВІДПРАВЛЕННЯ до запускового сервера.
Задача A відправляє повідомлення
bootstrap_register
запусковому серверу, щоб асоціювати заданий порт з ім'ям, наприклад,com.apple.taska
.Задача B взаємодіє з запусковим сервером для виконання пошуку запуску для імені служби (
bootstrap_lookup
). Щоб запусковий сервер міг відповісти, задача B відправить йому ПРАВО ВІДПРАВЛЕННЯ до порту, яке вона попередньо створила всередині повідомлення пошуку. Якщо пошук успішний, сервер скопіює ПРАВО ВІДПРАВЛЕННЯ, отримане від Задачі A, та передасть його Задачі B.
Пам'ятайте, що кожен може отримати ПРАВО ВІДПРАВЛЕННЯ до запускового сервера.
З цим ПРАВОМ ВІДПРАВЛЕННЯ Задача B може відправити повідомлення Задачі A.
Для двонапрямленого зв'язку зазвичай задача B генерує новий порт з ПРАВОМ ОТРИМАННЯ та ПРАВОМ ВІДПРАВЛЕННЯ, і дає ПРАВО ВІДПРАВЛЕННЯ Задачі A, щоб вона могла відправляти повідомлення ЗАДАЧІ B (двонапрямлене спілкування).
Запусковий сервер не може аутентифікувати ім'я служби, вказане задаче. Це означає, що задача може потенційно підробити будь-яку системну задачу, наприклад, фальшиво вказавши ім'я служби авторизації, а потім схвалюючи кожен запит.
Потім Apple зберігає імена служб, наданих системою, у захищених конфігураційних файлах, розташованих у каталогах, захищених SIP: /System/Library/LaunchDaemons
та /System/Library/LaunchAgents
. Поряд з кожним ім'ям служби також зберігається пов'язаний бінарний файл. Запусковий сервер створить та утримує ПРАВО ОТРИМАННЯ для кожного з цих імен служб.
Для цих попередньо визначених служб процес пошуку відрізняється трохи. При пошуку імені служби launchd запускає службу динамічно. Новий робочий процес виглядає наступним чином:
Задача B ініціює пошук запуску для імені служби.
launchd перевіряє, чи працює задача, і якщо ні, запускає її.
Задача A (служба) виконує перевірку реєстрації запуску (
bootstrap_check_in()
). Тут запусковий сервер створює ПРАВО ВІДПРАВЛЕННЯ, утримує його та передає ПРАВО ОТРИМАННЯ Задачі A.launchd копіює ПРАВО ВІДПРАВЛЕННЯ та відправляє його Задачі B.
Задача B генерує новий порт з ПРАВОМ ОТРИМАННЯ та ПРАВОМ ВІДПРАВЛЕННЯ, і дає ПРАВО ВІДПРАВЛЕННЯ Задачі A (службі), щоб вона могла відправляти повідомлення ЗАДАЧІ B (двонапрямлене спілкування).
Однак цей процес застосовується лише до попередньо визначених системних задач. Несистемні задачі все ще працюють, як описано раніше, що потенційно може дозволити підробку.
Отже, launchd не повинен ніколи аварійно завершувати роботу, інакше весь система аварійно завершить робот
Повідомлення Mach
Знайдіть більше інформації тут
Функція mach_msg
, яка в суті є системним викликом, використовується для надсилання та отримання повідомлень Mach. Функція вимагає, щоб повідомлення було надіслано як початковий аргумент. Це повідомлення повинно починатися зі структури mach_msg_header_t
, за якою йде вміст самого повідомлення. Структура визначається наступним чином:
Процеси, які мають право на отримання, можуть отримувати повідомлення на порті Mach. Натомість відправники мають право на відправку або право на відправку одного разу. Право на відправку одного разу призначене виключно для відправлення одного повідомлення, після чого воно стає недійсним.
Початкове поле msgh_bits
є бітовою маскою:
Перший біт (найбільш значущий) використовується для позначення того, що повідомлення складне (докладніше див. нижче)
3-й та 4-й біти використовуються ядром
5 менш значущих бітів 2-го байту можуть бути використані для ваучера: іншого типу порту для відправки комбінацій ключ/значення.
5 менш значущих бітів 3-го байту можуть бути використані для локального порту
5 менш значущих бітів 4-го байту можуть бути використані для віддаленого порту
Типи, які можуть бути вказані у ваучері, локальних та віддалених портах, визначені у файлі mach/message.h:
Наприклад, MACH_MSG_TYPE_MAKE_SEND_ONCE
може бути використаний для вказівки того, що право на відправку один раз повинно бути похідне та передане для цього порту. Також може бути вказано MACH_PORT_NULL
, щоб запобігти отримувачу мати можливість відповісти.
Для досягнення простої двосторонньої комунікації процес може вказати mach порт у заголовку mach повідомлення, який називається портом відповіді (msgh_local_port
), куди отримувач повідомлення може відправити відповідь на це повідомлення.
Зверніть увагу, що цей вид двосторонньої комунікації використовується в повідомленнях XPC, які очікують відповіді (xpc_connection_send_message_with_reply
та xpc_connection_send_message_with_reply_sync
). Проте, зазвичай створюються різні порти, як пояснено раніше, для створення двосторонньої комунікації.
Інші поля заголовка повідомлення:
msgh_size
: розмір всього пакета.msgh_remote_port
: порт, на який відправлено це повідомлення.msgh_voucher_port
: mach ваучери.msgh_id
: ідентифікатор цього повідомлення, який інтерпретується отримувачем.
Зверніть увагу, що mach повідомлення відправляються через mach порт
, який є каналом зв'язку один до одного, будується в ядрі mach. Декілька процесів можуть відправляти повідомлення на mach порт, але в будь-який момент тільки один процес може читати з нього.
Повідомлення потім формуються заголовком mach_msg_header_t
, за яким слідує тіло та трейлер (якщо є) і може надавати дозвіл на відповідь на нього. У цих випадках ядро просто повинно передати повідомлення від одного завдання до іншого.
Трейлер - це інформація, додана до повідомлення ядром (не може бути встановлена користувачем), яку можна запросити при отриманні повідомлення з прапорцями MACH_RCV_TRAILER_<trailer_opt>
(існує різна інформація, яку можна запросити).
Складні повідомлення
Проте, є інші більш складні повідомлення, наприклад, ті, які передають додаткові права порту або спільний доступ до пам'яті, де ядро також повинно надіслати ці об'єкти отримувачеві. У цих випадках найбільш значущий біт заголовка msgh_bits
встановлюється.
Можливі дескриптори для передачі визначені в mach/message.h
:
У 32 бітах всі дескриптори мають розмір 12 байтів, а тип дескриптора знаходиться в 11-му. У 64 бітах розміри відрізняються.
Ядро скопіює дескриптори з одного завдання в інше, але спочатку створює копію в пам'яті ядра. Цю техніку, відому як "Feng Shui", використовували в кількох експлойтах для того, щоб змусити ядро копіювати дані в своїй пам'яті, змушуючи процес надсилати дескриптори самому собі. Потім процес може отримати повідомлення (ядро їх звільнить).
Також можливо надіслати права порту вразливому процесу, і права порту просто з'являться в процесі (навіть якщо він не обробляє їх).
API портів Mac
Зверніть увагу, що порти пов'язані з простором імен завдання, тому для створення або пошуку порту також запитується простір імен завдання (докладніше в mach/mach_port.h
):
mach_port_allocate
|mach_port_construct
: Створити порт.mach_port_allocate
також може створити набір портів: право на отримання над групою портів. Кожного разу, коли отримується повідомлення, вказується порт, звідки воно було отримано.mach_port_allocate_name
: Змінити ім'я порту (за замовчуванням це 32-бітне ціле число)mach_port_names
: Отримати імена портів з цільового об'єктаmach_port_type
: Отримати права завдання на ім'яmach_port_rename
: Перейменувати порт (як dup2 для FDs)mach_port_allocate
: Виділити новий ОТРИМАТИ, ПОРТ_НАБІР або DEAD_NAMEmach_port_insert_right
: Створити нове право в порту, де у вас є ОТРИМАТИmach_port_...
mach_msg
|mach_msg_overwrite
: Функції, що використовуються для надсилання та отримання mach-повідомлень. Версія з перезаписом дозволяє вказати інший буфер для отримання повідомлення (інша версія просто повторно використовує його).
Налагодження mach_msg
Оскільки функції mach_msg
та mach_msg_overwrite
використовуються для надсилання та отримання повідомлень, встановлення точки зупинки на них дозволить перевірити надіслані та отримані повідомлення.
Наприклад, почніть налагодження будь-якої програми, яку можна налагоджувати, оскільки вона завантажить libSystem.B
, яка використовуватиме цю функцію.
Отримайте значення з реєстрів:
Перевірте заголовок повідомлення, перевіривши перший аргумент:
Такий тип mach_msg_bits_t
дуже поширений для дозволу відповіді.
Перелік портів
Ім'я - це типове ім'я, яке надається порту (перевірте, як воно збільшується у перших 3 байтах). ipc-object
- це затемнений унікальний ідентифікатор порту.
Також зверніть увагу на те, як порти лише з правом send
ідентифікують власника (ім'я порту + pid).
Також зверніть увагу на використання +
для позначення інших завдань, підключених до того ж порту.
Також можна використовувати procesxp, щоб побачити також зареєстровані назви служб (з вимкненим SIP через необхідність com.apple.system-task-port
).
Ви можете встановити цей інструмент в iOS, завантаживши його з http://newosxbook.com/tools/binpack64-256.tar.gz
Приклад коду
Зверніть увагу, як відправник виділяє порт, створює право на відправку для імені org.darlinghq.example
та надсилає його на сервер завантаження, тоді як відправник запросив право на відправку цього імені та використовував його для надсилання повідомлення.
Відправник
Цей приклад демонструє відправлення повідомлення через міжпроцесовий канал (IPC) на macOS. Він створює чергу повідомлень, відправляє повідомлення та очікує відповідь від отримувача.
Привілейовані порти
Існують деякі спеціальні порти, які дозволяють виконувати певні чутливі дії або отримувати доступ до певних чутливих даних, якщо завдання мають дозволи SEND на них. Це робить ці порти дуже цікавими з точки зору атаки не лише через можливості, але й через можливість поділу дозволів SEND між завданнями.
Спеціальні порти хоста
Ці порти представлені числами.
Права SEND можна отримати, викликавши host_get_special_port
, а права RECEIVE - викликавши host_set_special_port
. Однак обидва виклики потребують порту host_priv
, до якого може отримати доступ лише root. Більше того, у минулому root міг викликати host_set_special_port
та захоплювати довільний, що дозволяло, наприклад, обхід підписів коду, захоплюючи HOST_KEXTD_PORT
(SIP зараз запобігає цьому).
Ці порти поділені на 2 групи: перші 7 портів належать ядру, де 1 - HOST_PORT
, 2 - HOST_PRIV_PORT
, 3 - HOST_IO_MASTER_PORT
, а 7 - HOST_MAX_SPECIAL_KERNEL_PORT
.
Ті, що починають з номера 8, належать системним службам і можуть бути знайдені в оголошенні host_special_ports.h
.
Порт хоста: Якщо процес має привілей SEND на цей порт, він може отримати інформацію про систему, викликаючи його рутина, наприклад:
host_processor_info
: Отримати інформацію про процесорhost_info
: Отримати інформацію про хостhost_virtual_physical_table_info
: Віртуальна/фізична таблиця сторінок (потрібен MACH_VMDEBUG)host_statistics
: Отримати статистику хостаmach_memory_info
: Отримати розташування пам'яті ядраПривілейований порт хоста: Процес з правом SEND на цей порт може виконувати привілейовані дії, наприклад, показувати дані завантаження або спробу завантажити розширення ядра. Процес повинен бути root, щоб отримати цей дозвіл.
Більше того, для виклику API
kext_request
потрібно мати інші дозволиcom.apple.private.kext*
, які надаються лише бінарним файлам Apple.Інші рутина, які можна викликати:
host_get_boot_info
: Отриматиmachine_boot_info()
host_priv_statistics
: Отримати привілейовану статистикуvm_allocate_cpm
: Виділити послідовну фізичну пам'ятьhost_processors
: Послати право на процесори хостаmach_vm_wire
: Зробити пам'ять постійноюОскільки root може отримати доступ до цього дозволу, він може викликати
host_set_[special/exception]_port[s]
, щоб захопити спеціальні або виняткові порти хоста.
Можливо переглянути всі спеціальні порти хоста, запустивши:
Спеціальні порти завдань
Це порти, зарезервовані для відомих служб. Їх можна отримати/встановити, викликавши task_[get/set]_special_port
. Їх можна знайти в task_special_ports.h
:
З тут:
TASK_KERNEL_PORT[право на відправку task-self]: Порт, який використовується для управління цим завданням. Використовується для відправлення повідомлень, які впливають на завдання. Це порт, який повертає mach_task_self (див. порти завдань нижче).
TASK_BOOTSTRAP_PORT[право на відправку bootstrap]: Запусковий порт завдання. Використовується для відправлення повідомлень із запитом на повернення інших портів служб системи.
TASK_HOST_NAME_PORT[право на відправку host-self]: Порт, який використовується для запиту інформації про вміст хоста. Це порт, який повертає mach_host_self.
TASK_WIRED_LEDGER_PORT[право на відправку ledger]: Порт, який називає джерело, з якого це завдання витягує свою керовану ядром пам'ять.
TASK_PAGED_LEDGER_PORT[право на відправку ledger]: Порт, який називає джерело, з якого це завдання витягує свою пам'ять, керовану за замовчуванням.
Порти завдань
Спочатку у Mach не було "процесів", були "завдання", які вважалися більш як контейнери для потоків. Коли Mach був об'єднаний з BSD, кожне завдання було корелювано з процесом BSD. Тому кожен процес BSD має необхідні деталі для того, щоб бути процесом, і кожне завдання Mach також має свою внутрішню структуру (крім неіснуючого pid 0, який є kernel_task
).
Є дві дуже цікаві функції, пов'язані з цим:
task_for_pid(target_task_port, pid, &task_port_of_pid)
: Отримати право на відправку для порту завдання завдання, пов'язаного із вказанимpid
, і передати його вказаномуtarget_task_port
(який зазвичай є викликаючим завданням, яке використовувалоmach_task_self()
, але може бути портом відправки через інше завдання).pid_for_task(task, &pid)
: Заданий право на відправку до завдання, знайти, до якого PID це завдання відноситься.
Для виконання дій у межах завдання, завданню потрібно право на відправку до себе, викликаючи mach_task_self()
(яке використовує task_self_trap
(28)). З цим дозволом завдання може виконати кілька дій, таких як:
task_threads
: Отримати право на відправку над усіма портами завдань потоків завданняtask_info
: Отримати інформацію про завданняtask_suspend/resume
: Призупинити або відновити завданняtask_[get/set]_special_port
thread_create
: Створити потікtask_[get/set]_state
: Керувати станом завданняі більше можна знайти в mach/task.h
Зверніть увагу, що з правом на відправку порту завдання іншого завдання можливо виконати такі дії над іншим завданням.
Крім того, порт завдання також є портом vm_map
, який дозволяє читати та змінювати пам'ять всередині завдання за допомогою функцій, таких як vm_read()
та vm_write()
. Це в основному означає, що завдання з правами на відправку порту завдання іншого завдання зможе впровадити код у це завдання.
Пам'ятайте, що оскільки ядро також є завданням, якщо хтось зможе отримати права на відправку над kernel_task
, він зможе змусити ядро виконати будь-що (втечі з в'язниці).
Викликайте
mach_task_self()
для отримання імені для цього порту для викликаючого завдання. Цей порт спадковується лише під часexec()
; нове завдання, створене за допомогоюfork()
, отримує новий порт завдання (як особливий випадок, завдання також отримує новий порт завдання післяexec()
у suid-бінарних файлах). Єдиний спосіб створити завдання та отримати його порт - виконати "танець обміну портами" під час виконанняfork()
.Це обмеження доступу до порту (з
macos_task_policy
з бінарного файлуAppleMobileFileIntegrity
):Якщо додаток має дозвіл com.apple.security.get-task-allow, процеси від того ж користувача можуть отримати доступ до порту завдання (зазвичай додано Xcode для налагодження). Процес нотаризації не дозволить це для виробничих версій.
Додатки з дозволом
com.apple.system-task-ports
можуть отримати порт завдання для будь-якого процесу, крім ядра. У старих версіях це називалосяtask_for_pid-allow
. Це надається лише додаткам Apple.Root може отримати доступ до портів завдань додатків, не скомпільованих з захищеним режимом виконання (і не від Apple).
Порт імені завдання: Непривілейована версія порту завдання. Він посилається на завдання, але не дозволяє його керувати. Єдине, що, здається, доступне через нього, це task_info()
.
Порти потоків
У потоків також є пов'язані порти, які видно з завдання, що викликає task_threads
, та від процесора за допомогою processor_set_threads
. Право на відправку до порту потоку дозволяє використовувати функції підсистеми thread_act
, такі як:
thread_terminate
thread_[get/set]_state
act_[get/set]_state
thread_[suspend/resume]
thread_info
...
Будь-який потік може отримати цей порт, викликавши mach_thread_sef
.
Впровадження шелл-коду в потік через порт завдання
Ви можете отримати шелл-код з:
Introduction to ARM64v8