macOS IPC - Inter Process Communication
Last updated
Last updated
Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE) Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Mach використовує tasks як найменшу одиницю для обміну ресурсами, і кожен task може містити кілька потоків. Ці tasks і потоки відображаються 1:1 на POSIX процеси і потоки.
Зв'язок між tasks відбувається через Mach Inter-Process Communication (IPC), використовуючи односторонні канали зв'язку. Повідомлення передаються між портами, які діють як черги повідомлень, що управляються ядром.
Порт є базовим елементом Mach IPC. Його можна використовувати для відправки повідомлень і їх отримання.
Кожен процес має IPC таблицю, в якій можна знайти mach порти процесу. Ім'я mach порту насправді є числом (вказівником на об'єкт ядра).
Процес також може надіслати ім'я порту з певними правами іншому task і ядро зробить цей запис у IPC таблиці іншого task.
Права порту, які визначають, які операції може виконувати task, є ключовими для цього зв'язку. Можливі права порту (визначення звідси):
Право отримання, яке дозволяє отримувати повідомлення, надіслані на порт. Mach порти є MPSC (багато-виробник, один-споживач) чергами, що означає, що може бути лише одне право отримання для кожного порту в усій системі (на відміну від труб, де кілька процесів можуть утримувати дескриптори файлів для читання з однієї труби).
Task з правом отримання може отримувати повідомлення і створювати права відправки, що дозволяє йому надсилати повідомлення. Спочатку лише власний task має право отримання над своїм портом.
Якщо власник права отримання гине або його вбиває, право відправки стає марним (мертве ім'я).
Право відправки, яке дозволяє надсилати повідомлення на порт.
Право відправки може бути клоновано, тому task, що володіє правом відправки, може клонувати право і надати його третьому task.
Зверніть увагу, що права порту також можуть бути передані через Mac повідомлення.
Право одноразової відправки, яке дозволяє надіслати одне повідомлення на порт і потім зникає.
Це право не може бути клоновано, але його можна перемістити.
Право набору портів, яке позначає набір портів, а не один порт. Витягування повідомлення з набору портів витягує повідомлення з одного з портів, які він містить. Набори портів можуть використовуватися для прослуховування кількох портів одночасно, подібно до select
/poll
/epoll
/kqueue
в Unix.
Мертве ім'я, яке не є фактичним правом порту, а лише заповнювачем. Коли порт знищується, всі існуючі права порту на порт перетворюються на мертві імена.
Tasks можуть передавати права ВІДПРАВКИ іншим, дозволяючи їм надсилати повідомлення назад. Права ВІДПРАВКИ також можуть бути клоновані, тому task може дублювати і надавати право третьому task. Це, в поєднанні з проміжним процесом, відомим як bootstrap server, дозволяє ефективно спілкуватися між tasks.
File ports дозволяють інкапсулювати дескриптори файлів у Mac портах (використовуючи права Mach порту). Можна створити fileport
з даного FD, використовуючи fileport_makeport
, і створити FD з fileport, використовуючи fileport_makefd
.
Як вже згадувалося раніше, можливо надсилати права, використовуючи Mach повідомлення, однак ви не можете надіслати право, не маючи вже права на відправку Mach повідомлення. Отже, як встановлюється перше спілкування?
Для цього залучається bootstrap server (launchd в mac), оскільки кожен може отримати право ВІДПРАВКИ до bootstrap server, можливо попросити його про право на відправку повідомлення іншому процесу:
Task A створює новий порт, отримуючи ПРАВО ОТРИМАННЯ на нього.
Task A, будучи власником ПРАВА ОТРИМАННЯ, генерує ПРАВО ВІДПРАВКИ для порту.
Task A встановлює з'єднання з bootstrap server і надсилає йому ПРАВО ВІДПРАВКИ для порту, який він згенерував на початку.
Пам'ятайте, що будь-хто може отримати ПРАВО ВІДПРАВКИ до bootstrap server.
Task A надсилає повідомлення bootstrap_register
до bootstrap server, щоб асоціювати даний порт з ім'ям на кшталт com.apple.taska
Task B взаємодіє з bootstrap server, щоб виконати bootstrap lookup для імені сервісу (bootstrap_lookup
). Щоб bootstrap server міг відповісти, task B надішле йому ПРАВО ВІДПРАВКИ до порту, який він раніше створив всередині повідомлення lookup. Якщо lookup успішний, сервер дублює ПРАВО ВІДПРАВКИ, отримане від Task A, і передає його Task B.
Пам'ятайте, що будь-хто може отримати ПРАВО ВІДПРАВКИ до bootstrap server.
З цим ПРАВОМ ВІДПРАВКИ, Task B здатний надсилати повідомлення Task A.
Для двостороннього спілкування зазвичай task B генерує новий порт з ПРАВОМ ОТРИМАННЯ і ПРАВОМ ВІДПРАВКИ, і надає ПРАВО ВІДПРАВКИ Task A, щоб він міг надсилати повідомлення до TASK B (двостороннє спілкування).
Bootstrap server не може аутентифікувати ім'я сервісу, яке заявляє task. Це означає, що task може потенційно вдаватись під будь-який системний task, наприклад, неправильно заявляючи ім'я сервісу авторизації і потім схвалюючи кожен запит.
Тоді Apple зберігає імена сервісів, наданих системою, у захищених конфігураційних файлах, розташованих у SIP-захищених каталогах: /System/Library/LaunchDaemons
і /System/Library/LaunchAgents
. Поряд з кожним ім'ям сервісу, також зберігається асоційований бінарний файл. Bootstrap server створить і утримає ПРАВО ОТРИМАННЯ для кожного з цих імен сервісів.
Для цих попередньо визначених сервісів, процес lookup трохи відрізняється. Коли ім'я сервісу шукається, launchd динамічно запускає сервіс. Новий робочий процес виглядає так:
Task B ініціює bootstrap lookup для імені сервісу.
launchd перевіряє, чи працює task, і якщо ні, запускає його.
Task A (сервіс) виконує bootstrap check-in (bootstrap_check_in()
). Тут bootstrap сервер створює ПРАВО ВІДПРАВКИ, утримує його і передає ПРАВО ОТРИМАННЯ Task A.
launchd дублює ПРАВО ВІДПРАВКИ і надсилає його Task B.
Task B генерує новий порт з ПРАВОМ ОТРИМАННЯ і ПРАВОМ ВІДПРАВКИ, і надає ПРАВО ВІДПРАВКИ Task A (сервісу), щоб він міг надсилати повідомлення до TASK B (двостороннє спілкування).
Однак цей процес застосовується лише до попередньо визначених системних tasks. Несистемні tasks все ще працюють, як було описано спочатку, що може потенційно дозволити вдавання.
Отже, launchd ніколи не повинен аварійно завершуватися, інакше вся система зупиниться.
Функція mach_msg
, по суті, є системним викликом, що використовується для надсилання та отримання Mach повідомлень. Функція вимагає, щоб повідомлення, яке потрібно надіслати, було першим аргументом. Це повідомлення повинно починатися з структури mach_msg_header_t
, за якою слідує фактичний вміст повідомлення. Структура визначається наступним чином:
Процеси, що мають receive right, можуть отримувати повідомлення на Mach порту. Навпаки, senders отримують send або send-once right. Право send-once призначене виключно для відправки одного повідомлення, після чого воно стає недійсним.
Початкове поле msgh_bits
є бітовою картою:
Перший біт (найбільш значущий) використовується для вказівки на те, що повідомлення є складним (більше про це нижче)
3-й та 4-й використовуються ядром
5 найменш значущих бітів 2-го байта можуть бути використані для voucher: ще один тип порту для відправки пар ключ/значення.
5 найменш значущих бітів 3-го байта можуть бути використані для local port
5 найменш значущих бітів 4-го байта можуть бути використані для remote port
Типи, які можуть бути вказані в voucher, local та remote портах, є (з 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
: ID цього повідомлення, який інтерпретується отримувачем.
Зверніть увагу, що mach повідомлення надсилаються через mach порт
, який є одиничним отримувачем, багаторазовим відправником комунікаційним каналом, вбудованим у ядро mach. Багато процесів можуть надсилати повідомлення до mach порту, але в будь-який момент лише один процес може читати з нього.
Повідомлення формуються заголовком mach_msg_header_t
, за яким слідує тіло та трейлер (якщо є), і це може надати дозвіл на відповідь. У цих випадках ядру просто потрібно передати повідомлення від одного завдання до іншого.
Трейлер - це інформація, додана до повідомлення ядром (не може бути встановлена користувачем), яка може бути запитана під час отримання повідомлення з прапорами MACH_RCV_TRAILER_<trailer_opt>
(можна запитати різну інформацію).
Однак є й інші, більш складні повідомлення, такі як ті, що передають додаткові права на порти або ділять пам'ять, де ядру також потрібно надіслати ці об'єкти отримувачу. У цих випадках найзначніший біт заголовка msgh_bits
встановлюється.
Можливі дескриптори для передачі визначені в mach/message.h
:
В 32-бітних системах всі дескриптори мають розмір 12B, а тип дескриптора знаходиться в 11-му. У 64-бітних системах розміри варіюються.
Ядро скопіює дескриптори з одного завдання в інше, але спочатку створюючи копію в пам'яті ядра. Цю техніку, відому як "Feng Shui", зловживали в кількох експлойтах, щоб змусити ядро копіювати дані в своїй пам'яті, змушуючи процес надсилати дескриптори самому собі. Тоді процес може отримувати повідомлення (ядро їх звільнить).
Також можливо надіслати права порту в уразливий процес, і права порту просто з'являться в процесі (навіть якщо він їх не обробляє).
Зверніть увагу, що порти асоційовані з простором імен завдання, тому для створення або пошуку порту також запитується простір імен завдання (більше в 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 для FD)
mach_port_allocate
: Виділити новий RECEIVE, PORT_SET або DEAD_NAME
mach_port_insert_right
: Створити нове право в порту, де у вас є RECEIVE
mach_port_...
mach_msg
| mach_msg_overwrite
: Функції, які використовуються для надсилання та отримання mach-повідомлень. Версія з перезаписом дозволяє вказати інший буфер для отримання повідомлень (інша версія просто повторно використовує його).
Оскільки функції mach_msg
та mach_msg_overwrite
використовуються для надсилання та отримання повідомлень, встановлення точки зупинки на них дозволить перевірити надіслані та отримані повідомлення.
Наприклад, почніть налагодження будь-якого додатку, який ви можете налагоджувати, оскільки він завантажить libSystem.B
, яка використовуватиме цю функцію.
Щоб отримати аргументи mach_msg
, перевірте регістри. Це аргументи (з mach/message.h):
Отримайте значення з реєстрів:
Перевірте заголовок повідомлення, перевіряючи перший аргумент:
Цей тип 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
і надсилає його на bootstrap server, в той час як відправник запитує право на відправку цього імені і використовує його для надсилання повідомлення.
Існують деякі спеціальні порти, які дозволяють виконувати певні чутливі дії або отримувати доступ до певних чутливих даних, якщо завдання має 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
.
Host port: Якщо процес має SEND привілей на цьому порту, він може отримати інформацію про систему, викликаючи його рутинні функції, такі як:
host_processor_info
: Отримати інформацію про процесор
host_info
: Отримати інформацію про хост
host_virtual_physical_table_info
: Віртуальна/фізична таблиця сторінок (вимагає MACH_VMDEBUG)
host_statistics
: Отримати статистику хоста
mach_memory_info
: Отримати макет пам'яті ядра
Host Priv port: Процес з SEND правом на цьому порту може виконувати привілейовані дії, такі як показ даних завантаження або спроба завантажити розширення ядра. Процес повинен бути root, щоб отримати цей дозвіл.
Більше того, для виклику kext_request
API потрібно мати інші права 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 send right]: Порт, що використовується для контролю цього завдання. Використовується для надсилання повідомлень, які впливають на завдання. Це порт, що повертається функцією mach_task_self (див. Task Ports нижче).
TASK_BOOTSTRAP_PORT[bootstrap send right]: Бутстрап-порт завдання. Використовується для надсилання повідомлень з проханням повернути інші порти системних служб.
TASK_HOST_NAME_PORT[host-self send right]: Порт, що використовується для запиту інформації про місто, що містить. Це порт, що повертається функцією mach_host_self.
TASK_WIRED_LEDGER_PORT[ledger send right]: Порт, що вказує на джерело, з якого це завдання отримує свою фіксовану пам'ять ядра.
TASK_PAGED_LEDGER_PORT[ledger send right]: Порт, що вказує на джерело, з якого це завдання отримує свою пам'ять за замовчуванням.
Спочатку Mach не мав "процесів", він мав "завдання", які вважалися більше контейнерами потоків. Коли Mach був об'єднаний з BSD, кожне завдання було пов'язане з процесом BSD. Тому кожен процес BSD має деталі, необхідні для того, щоб бути процесом, а кожне завдання Mach також має свої внутрішні механізми (за винятком неіснуючого pid 0, який є kernel_task
).
Є дві дуже цікаві функції, пов'язані з цим:
task_for_pid(target_task_port, pid, &task_port_of_pid)
: Отримати право SEND для порту завдання, пов'язаного з вказаним pid
, і надати його вказаному target_task_port
(який зазвичай є завданням виклику, що використовувало mach_task_self()
, але може бути портом SEND для іншого завдання).
pid_for_task(task, &pid)
: Задане право SEND для завдання, знайти, до якого PID це завдання пов'язане.
Щоб виконати дії в межах завдання, завдання потрібно право SEND
на себе, викликавши mach_task_self()
(який використовує task_self_trap
(28)). З цим дозволом завдання може виконувати кілька дій, таких як:
task_threads
: Отримати право SEND на всі порти завдання потоків завдання
task_info
: Отримати інформацію про завдання
task_suspend/resume
: Призупинити або відновити завдання
task_[get/set]_special_port
thread_create
: Створити потік
task_[get/set]_state
: Контролювати стан завдання
і більше можна знайти в mach/task.h
Зверніть увагу, що з правом SEND на порт завдання іншого завдання можливо виконувати такі дії над іншим завданням.
Більше того, порт task_port також є vm_map
портом, який дозволяє читати та маніпулювати пам'яттю всередині завдання за допомогою функцій, таких як vm_read()
і vm_write()
. Це в основному означає, що завдання з правами SEND на порт task_port іншого завдання зможе впроваджувати код у це завдання.
Пам'ятайте, що оскільки ядро також є завданням, якщо хтось зможе отримати дозволи SEND на kernel_task
, він зможе змусити ядро виконувати що завгодно (jailbreaks).
Викликайте 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
. Право SEND на порт потоку дозволяє використовувати функції з підсистеми thread_act
, такі як:
thread_terminate
thread_[get/set]_state
act_[get/set]_state
thread_[suspend/resume]
thread_info
...
Будь-який потік може отримати цей порт, викликавши mach_thread_sef
.
Ви можете отримати shellcode з:
Introduction to ARM64v8Скомпілюйте попередню програму та додайте entitlements, щоб мати можливість інжектувати код з тим самим користувачем (якщо ні, вам потрібно буде використовувати sudo).