macOS IPC - Inter Process Communication
Last updated
Last updated
Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE) Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE)
Mach utiliza tareas como la unidad más pequeña para compartir recursos, y cada tarea puede contener múltiples hilos. Estas tareas y hilos se mapean en una relación 1:1 con procesos y hilos POSIX.
La comunicación entre tareas ocurre a través de la Comunicación entre Procesos de Mach (IPC), utilizando canales de comunicación unidireccionales. Los mensajes se transfieren entre puertos, que actúan como colas de mensajes gestionadas por el kernel.
Un puerto es el elemento básico del IPC de Mach. Puede ser utilizado para enviar mensajes y recibirlos.
Cada proceso tiene una tabla de IPC, donde es posible encontrar los puertos de Mach del proceso. El nombre de un puerto de Mach es en realidad un número (un puntero al objeto del kernel).
Un proceso también puede enviar un nombre de puerto con algunos derechos a una tarea diferente y el kernel creará esta entrada en la tabla de IPC de la otra tarea.
Los derechos de puerto, que definen qué operaciones puede realizar una tarea, son clave en esta comunicación. Los posibles derechos de puerto son (definiciones desde aquí):
Derecho de Recepción, que permite recibir mensajes enviados al puerto. Los puertos de Mach son colas MPSC (múltiples productores, un solo consumidor), lo que significa que solo puede haber un derecho de recepción para cada puerto en todo el sistema (a diferencia de las tuberías, donde varios procesos pueden tener descriptores de archivo al extremo de lectura de una tubería).
Una tarea con el Derecho de Recepción puede recibir mensajes y crear Derechos de Envío, lo que le permite enviar mensajes. Originalmente, solo la propia tarea tiene el Derecho de Recepción sobre su puerto.
Si el propietario del Derecho de Recepción muere o lo elimina, el derecho de envío se vuelve inútil (nombre muerto).
Derecho de Envío, que permite enviar mensajes al puerto.
El Derecho de Envío puede ser clonado para que una tarea que posea un Derecho de Envío pueda clonar el derecho y concedérselo a una tercera tarea.
Ten en cuenta que los derechos de puerto también pueden ser pasados a través de mensajes de Mac.
Derecho de Envío-una-vez, que permite enviar un mensaje al puerto y luego desaparece.
Este derecho no puede ser clonado, pero puede ser movido.
Derecho de conjunto de puertos, que denota un conjunto de puertos en lugar de un solo puerto. Desencolar un mensaje de un conjunto de puertos desencola un mensaje de uno de los puertos que contiene. Los conjuntos de puertos se pueden utilizar para escuchar varios puertos simultáneamente, de manera similar a select
/poll
/epoll
/kqueue
en Unix.
Nombre muerto, que no es un derecho de puerto real, sino simplemente un marcador de posición. Cuando se destruye un puerto, todos los derechos de puerto existentes para el puerto se convierten en nombres muertos.
Las tareas pueden transferir DERECHOS DE ENVÍO a otros, lo que les permite enviar mensajes de vuelta. Los DERECHOS DE ENVÍO también pueden ser clonados, por lo que una tarea puede duplicar y dar el derecho a una tercera tarea. Esto, combinado con un proceso intermedio conocido como el servidor de arranque, permite una comunicación efectiva entre tareas.
Los puertos de archivo permiten encapsular descriptores de archivo en puertos de Mac (usando derechos de puerto de Mach). Es posible crear un fileport
a partir de un FD dado usando fileport_makeport
y crear un FD a partir de un fileport usando fileport_makefd
.
Como se mencionó anteriormente, es posible enviar derechos usando mensajes de Mach, sin embargo, no puedes enviar un derecho sin tener ya un derecho para enviar un mensaje de Mach. Entonces, ¿cómo se establece la primera comunicación?
Para esto, el servidor de arranque (launchd en Mac) está involucrado, ya que cualquiera puede obtener un DERECHO DE ENVÍO al servidor de arranque, es posible pedirle un derecho para enviar un mensaje a otro proceso:
La tarea A crea un nuevo puerto, obteniendo el derecho de RECEPCIÓN sobre él.
La tarea A, siendo la titular del derecho de RECEPCIÓN, genera un DERECHO DE ENVÍO para el puerto.
La tarea A establece una conexión con el servidor de arranque, y le envía el DERECHO DE ENVÍO para el puerto que generó al principio.
Recuerda que cualquiera puede obtener un DERECHO DE ENVÍO al servidor de arranque.
La tarea A envía un mensaje bootstrap_register
al servidor de arranque para asociar el puerto dado con un nombre como com.apple.taska
.
La tarea B interactúa con el servidor de arranque para ejecutar una búsqueda de arranque para el nombre del servicio (bootstrap_lookup
). Para que el servidor de arranque pueda responder, la tarea B enviará un DERECHO DE ENVÍO a un puerto que creó previamente dentro del mensaje de búsqueda. Si la búsqueda tiene éxito, el servidor duplicará el DERECHO DE ENVÍO recibido de la tarea A y lo transmitirá a la tarea B.
Recuerda que cualquiera puede obtener un DERECHO DE ENVÍO al servidor de arranque.
Con este DERECHO DE ENVÍO, la tarea B es capaz de enviar un mensaje a la tarea A.
Para una comunicación bidireccional, por lo general la tarea B genera un nuevo puerto con un derecho de RECEPCIÓN y un derecho de ENVÍO, y le da el derecho de ENVÍO a la tarea A para que pueda enviar mensajes a la tarea B (comunicación bidireccional).
El servidor de arranque no puede autenticar el nombre de servicio reclamado por una tarea. Esto significa que una tarea podría potencialmente hacerse pasar por cualquier tarea del sistema, como reclamar falsamente un nombre de servicio de autorización y luego aprobar cada solicitud.
Luego, Apple almacena los nombres de los servicios proporcionados por el sistema en archivos de configuración seguros, ubicados en directorios protegidos por SIP: /System/Library/LaunchDaemons
y /System/Library/LaunchAgents
. Junto a cada nombre de servicio, también se almacena el binario asociado. El servidor de arranque, creará y mantendrá un derecho de RECEPCIÓN para cada uno de estos nombres de servicio.
Para estos servicios predefinidos, el proceso de búsqueda difiere ligeramente. Cuando se busca un nombre de servicio, launchd inicia el servicio dinámicamente. El nuevo flujo de trabajo es el siguiente:
La tarea B inicia una búsqueda de arranque para un nombre de servicio.
launchd verifica si la tarea está en ejecución y si no lo está, la inicia.
La tarea A (el servicio) realiza un registro de arranque (bootstrap_check_in()
). Aquí, el servidor de arranque crea un DERECHO DE ENVÍO, lo retiene y transfiere el DERECHO DE RECEPCIÓN a la tarea A.
launchd duplica el DERECHO DE ENVÍO y lo envía a la tarea B.
La tarea B genera un nuevo puerto con un derecho de RECEPCIÓN y un derecho de ENVÍO, y le da el derecho de ENVÍO a la tarea A (el servicio) para que pueda enviar mensajes a la tarea B (comunicación bidireccional).
Sin embargo, este proceso solo se aplica a tareas del sistema predefinidas. Las tareas no del sistema aún operan como se describió originalmente, lo que podría permitir potencialmente la suplantación.
Por lo tanto, launchd nunca debería fallar o todo el sistema se bloqueará.
Encuentra más información aquí
La función mach_msg
, esencialmente una llamada al sistema, se utiliza para enviar y recibir mensajes Mach. La función requiere que el mensaje se envíe como argumento inicial. Este mensaje debe comenzar con una estructura mach_msg_header_t
, seguida por el contenido real del mensaje. La estructura se define de la siguiente manera:
Los procesos que poseen un derecho de recepción pueden recibir mensajes en un puerto Mach. Por otro lado, los emisores tienen un derecho de envío o un derecho de envío único. El derecho de envío único es exclusivamente para enviar un único mensaje, después de lo cual se vuelve inválido.
El campo inicial msgh_bits
es un mapa de bits:
El primer bit (más significativo) se utiliza para indicar que un mensaje es complejo (más sobre esto a continuación)
El 3er y 4to bit son utilizados por el kernel
Los 5 bits menos significativos del 2do byte pueden ser utilizados para voucher: otro tipo de puerto para enviar combinaciones de clave/valor.
Los 5 bits menos significativos del 3er byte pueden ser utilizados para puerto local
Los 5 bits menos significativos del 4to byte pueden ser utilizados para puerto remoto
Los tipos que se pueden especificar en el voucher, puertos locales y remotos son (de mach/message.h):
Por ejemplo, MACH_MSG_TYPE_MAKE_SEND_ONCE
se puede usar para indicar que un derecho de envío único debe ser derivado y transferido para este puerto. También se puede especificar MACH_PORT_NULL
para evitar que el destinatario pueda responder.
Para lograr una comunicación bidireccional fácil, un proceso puede especificar un puerto mach en el encabezado del mensaje mach llamado el puerto de respuesta (msgh_local_port
) donde el receptor del mensaje puede enviar una respuesta a este mensaje.
Tenga en cuenta que este tipo de comunicación bidireccional se utiliza en mensajes XPC que esperan una respuesta (xpc_connection_send_message_with_reply
y xpc_connection_send_message_with_reply_sync
). Pero generalmente se crean puertos diferentes como se explicó anteriormente para crear la comunicación bidireccional.
Los otros campos del encabezado del mensaje son:
msgh_size
: el tamaño de todo el paquete.
msgh_remote_port
: el puerto por el cual se envía este mensaje.
msgh_voucher_port
: vales mach.
msgh_id
: el ID de este mensaje, que es interpretado por el receptor.
Tenga en cuenta que los mensajes mach se envían a través de un puerto mach
, que es un canal de comunicación de un solo receptor, múltiples emisores integrado en el núcleo mach. Múltiples procesos pueden enviar mensajes a un puerto mach, pero en cualquier momento solo un proceso puede leer de él.
Los mensajes se forman con el encabezado mach_msg_header_t
seguido del cuerpo y del trailer (si lo hay) y puede otorgar permiso para responder. En estos casos, el kernel solo necesita pasar el mensaje de una tarea a la otra.
Un trailer es información añadida al mensaje por el kernel (no puede ser establecida por el usuario) que puede ser solicitada en la recepción del mensaje con las banderas MACH_RCV_TRAILER_<trailer_opt>
(hay diferentes informaciones que se pueden solicitar).
Sin embargo, hay otros mensajes más complejos, como los que pasan derechos de puerto adicionales o comparten memoria, donde el kernel también necesita enviar estos objetos al destinatario. En estos casos, se establece el bit más significativo del encabezado msgh_bits
.
Los descriptores posibles para pasar están definidos en mach/message.h
:
En 32 bits, todos los descriptores son de 12B y el tipo de descriptor está en el 11º. En 64 bits, los tamaños varían.
El kernel copiará los descriptores de una tarea a la otra, pero primero creando una copia en la memoria del kernel. Esta técnica, conocida como "Feng Shui", ha sido abusada en varios exploits para hacer que el kernel copie datos en su memoria haciendo que un proceso envíe descriptores a sí mismo. Luego, el proceso puede recibir los mensajes (el kernel los liberará).
También es posible enviar derechos de puerto a un proceso vulnerable, y los derechos de puerto simplemente aparecerán en el proceso (incluso si no los está manejando).
Tenga en cuenta que los puertos están asociados al espacio de nombres de la tarea, por lo que para crear o buscar un puerto, también se consulta el espacio de nombres de la tarea (más en mach/mach_port.h
):
mach_port_allocate
| mach_port_construct
: Crear un puerto.
mach_port_allocate
también puede crear un conjunto de puertos: derecho de recepción sobre un grupo de puertos. Siempre que se reciba un mensaje, se indica el puerto desde donde se envió.
mach_port_allocate_name
: Cambiar el nombre del puerto (por defecto un entero de 32 bits)
mach_port_names
: Obtener nombres de puerto de un objetivo
mach_port_type
: Obtener derechos de una tarea sobre un nombre
mach_port_rename
: Renombrar un puerto (como dup2 para FDs)
mach_port_allocate
: Asignar un nuevo RECEIVE, PORT_SET o DEAD_NAME
mach_port_insert_right
: Crear un nuevo derecho en un puerto donde tienes RECEIVE
mach_port_...
mach_msg
| mach_msg_overwrite
: Funciones utilizadas para enviar y recibir mensajes mach. La versión de sobrescritura permite especificar un búfer diferente para la recepción del mensaje (la otra versión simplemente lo reutilizará).
Dado que las funciones mach_msg
y mach_msg_overwrite
son las que se utilizan para enviar y recibir mensajes, establecer un punto de interrupción en ellas permitiría inspeccionar los mensajes enviados y recibidos.
Por ejemplo, comience a depurar cualquier aplicación que pueda depurar ya que cargará libSystem.B
que utilizará esta función.
Para obtener los argumentos de mach_msg
verifica los registros. Estos son los argumentos (de mach/message.h):
Obtener los valores de los registros:
Inspecciona el encabezado del mensaje verificando el primer argumento:
Ese tipo de mach_msg_bits_t
es muy común para permitir una respuesta.
El nombre es el nombre predeterminado dado al puerto (ver cómo está aumentando en los primeros 3 bytes). El ipc-object
es el identificador único obfuscado del puerto.
También observe cómo los puertos con solo el derecho de enviar
están identificando al propietario de este (nombre del puerto + pid).
También observe el uso de +
para indicar otras tareas conectadas al mismo puerto.
También es posible usar procesxp para ver también los nombres de servicio registrados (con SIP deshabilitado debido a la necesidad de com.apple.system-task-port
):
Puedes instalar esta herramienta en iOS descargándola desde http://newosxbook.com/tools/binpack64-256.tar.gz
Observa cómo el emisor asigna un puerto, crea un derecho de envío para el nombre org.darlinghq.example
y lo envía al servidor de arranque mientras que el emisor solicitó el derecho de envío de ese nombre y lo utilizó para enviar un mensaje.
El archivo sender.c
contiene el código fuente de un programa de ejemplo que envía un mensaje a un puerto específico utilizando IPC en macOS.
Este programa ilustra cómo se puede abusar de la comunicación entre procesos para realizar ataques de escalada de privilegios en sistemas macOS.
Para compilar el programa, se puede utilizar el siguiente comando:
Una vez compilado, el programa puede ejecutarse para enviar mensajes a un puerto específico y explorar posibles vulnerabilidades en el sistema.
Es importante comprender y analizar este tipo de código para fortalecer la seguridad de los sistemas macOS contra posibles abusos de IPC.
Existen algunos puertos especiales que permiten realizar ciertas acciones sensibles o acceder a ciertos datos sensibles en caso de que una tarea tenga permisos de ENVÍO sobre ellos. Esto hace que estos puertos sean muy interesantes desde la perspectiva de un atacante no solo por las capacidades, sino porque es posible compartir permisos de ENVÍO entre tareas.
Estos puertos están representados por un número.
Los derechos de ENVÍO se pueden obtener llamando a host_get_special_port
y los derechos de RECIBIR llamando a host_set_special_port
. Sin embargo, ambas llamadas requieren el puerto host_priv
al que solo puede acceder el usuario root. Además, en el pasado, el usuario root podía llamar a host_set_special_port
y secuestrar arbitrariamente lo que permitía, por ejemplo, eludir las firmas de código secuestrando HOST_KEXTD_PORT
(SIP ahora lo previene).
Estos se dividen en 2 grupos: Los primeros 7 puertos son propiedad del kernel siendo el 1 HOST_PORT
, el 2 HOST_PRIV_PORT
, el 3 HOST_IO_MASTER_PORT
y el 7 es HOST_MAX_SPECIAL_KERNEL_PORT
.
Los que comienzan desde el número 8 son propiedad de los demonios del sistema y se pueden encontrar declarados en host_special_ports.h
.
Puerto del host: Si un proceso tiene privilegio de ENVÍO sobre este puerto, puede obtener información sobre el sistema llamando a sus rutinas como:
host_processor_info
: Obtener información del procesador
host_info
: Obtener información del host
host_virtual_physical_table_info
: Tabla de páginas virtuales/físicas (requiere MACH_VMDEBUG)
host_statistics
: Obtener estadísticas del host
mach_memory_info
: Obtener diseño de memoria del kernel
Puerto de privilegio del host: Un proceso con el derecho de ENVÍO sobre este puerto puede realizar acciones privilegiadas como mostrar datos de arranque o intentar cargar una extensión de kernel. El proceso necesita ser root para obtener este permiso.
Además, para llamar a la API kext_request
se necesitan otros permisos com.apple.private.kext*
que solo se otorgan a binarios de Apple.
Otras rutinas que se pueden llamar son:
host_get_boot_info
: Obtener machine_boot_info()
host_priv_statistics
: Obtener estadísticas privilegiadas
vm_allocate_cpm
: Asignar memoria física contigua
host_processors
: Derecho de envío a procesadores del host
mach_vm_wire
: Hacer residente la memoria
Como el usuario root puede acceder a este permiso, podría llamar a host_set_[special/exception]_port[s]
para secuestrar puertos especiales o de excepción del host.
Es posible ver todos los puertos especiales del host ejecutando:
Estos son puertos reservados para servicios conocidos. Es posible obtener/establecerlos llamando a task_[get/set]_special_port
. Se pueden encontrar en task_special_ports.h
:
Desde aquí:
TASK_KERNEL_PORT[derecho de envío de tarea-propia]: El puerto utilizado para controlar esta tarea. Se utiliza para enviar mensajes que afectan a la tarea. Este es el puerto devuelto por mach_task_self (ver Puertos de Tarea abajo).
TASK_BOOTSTRAP_PORT[derecho de envío de arranque]: El puerto de arranque de la tarea. Se utiliza para enviar mensajes solicitando el retorno de otros puertos de servicios del sistema.
TASK_HOST_NAME_PORT[derecho de envío de host-propio]: El puerto utilizado para solicitar información del host que contiene. Este es el puerto devuelto por mach_host_self.
TASK_WIRED_LEDGER_PORT[derecho de envío de libro mayor]: El puerto que nombra la fuente de la cual esta tarea extrae su memoria cableada del kernel.
TASK_PAGED_LEDGER_PORT[derecho de envío de libro mayor]: El puerto que nombra la fuente de la cual esta tarea extrae su memoria gestionada por defecto.
Originalmente, Mach no tenía "procesos", tenía "tareas" que se consideraban más como contenedores de hilos. Cuando Mach se fusionó con BSD, cada tarea se correlacionó con un proceso BSD. Por lo tanto, cada proceso BSD tiene los detalles que necesita para ser un proceso y cada tarea Mach también tiene sus funcionamientos internos (excepto por el pid inexistente 0 que es el kernel_task
).
Hay dos funciones muy interesantes relacionadas con esto:
task_for_pid(target_task_port, pid, &task_port_of_pid)
: Obtener un derecho de ENVÍO para el puerto de tarea de la tarea relacionada con el especificado por el pid
y dárselo al target_task_port
indicado (que suele ser la tarea del llamante que ha utilizado mach_task_self()
, pero podría ser un puerto de ENVÍO sobre una tarea diferente).
pid_for_task(task, &pid)
: Dado un derecho de ENVÍO a una tarea, encontrar a qué PID está relacionada esta tarea.
Para realizar acciones dentro de la tarea, la tarea necesitaba un derecho de ENVÍO
a sí misma llamando a mach_task_self()
(que utiliza la task_self_trap
(28)). Con este permiso, una tarea puede realizar varias acciones como:
task_threads
: Obtener derecho de ENVÍO sobre todos los puertos de tarea de los hilos de la tarea
task_info
: Obtener información sobre una tarea
task_suspend/resume
: Suspender o reanudar una tarea
task_[get/set]_special_port
thread_create
: Crear un hilo
task_[get/set]_state
: Controlar el estado de la tarea
y más se puede encontrar en mach/task.h
Ten en cuenta que con un derecho de ENVÍO sobre un puerto de tarea de una tarea diferente, es posible realizar tales acciones sobre una tarea diferente.
Además, el puerto de la tarea es también el puerto vm_map
que permite leer y manipular memoria dentro de una tarea con funciones como vm_read()
y vm_write()
. Básicamente, esto significa que una tarea con derechos de ENVÍO sobre el puerto de tarea de una tarea diferente podrá inyectar código en esa tarea.
Recuerda que debido a que el kernel también es una tarea, si alguien logra obtener permisos de ENVÍO sobre el kernel_task
, podrá hacer que el kernel ejecute cualquier cosa (jailbreaks).
Llama a mach_task_self()
para obtener el nombre de este puerto para la tarea del llamante. Este puerto solo se hereda a través de exec()
; una nueva tarea creada con fork()
obtiene un nuevo puerto de tarea (como caso especial, una tarea también obtiene un nuevo puerto de tarea después de exec()
en un binario suid). La única forma de generar una tarea y obtener su puerto es realizar la "danza de intercambio de puertos" mientras se hace un fork()
.
Estas son las restricciones para acceder al puerto (de macos_task_policy
del binario AppleMobileFileIntegrity
):
Si la aplicación tiene el permiso com.apple.security.get-task-allow, los procesos del mismo usuario pueden acceder al puerto de la tarea (comúnmente añadido por Xcode para depuración). El proceso de notarización no permitirá que se incluya en lanzamientos de producción.
Las aplicaciones con el permiso com.apple.system-task-ports
pueden obtener el puerto de tarea de cualquier proceso, excepto el del kernel. En versiones anteriores se llamaba task_for_pid-allow
. Esto solo se concede a aplicaciones de Apple.
Root puede acceder a los puertos de tarea de aplicaciones no compiladas con un tiempo de ejecución fortificado (y no de Apple).
El puerto de nombre de tarea: Una versión no privilegiada del puerto de tarea. Hace referencia a la tarea, pero no permite controlarla. Lo único que parece estar disponible a través de él es task_info()
.
Los hilos también tienen puertos asociados, que son visibles desde la tarea que llama a task_threads
y desde el procesador con processor_set_threads
. Un derecho de ENVÍO al puerto del hilo permite utilizar la función del subsistema thread_act
, como:
thread_terminate
thread_[get/set]_state
act_[get/set]_state
thread_[suspend/resume]
thread_info
...
Cualquier hilo puede obtener este puerto llamando a mach_thread_sef
.
Puedes obtener un shellcode desde:
Introduction to ARM64v8entitlements.plist
El archivo entitlements.plist
contiene información sobre los permisos y capacidades especiales que una aplicación tiene en macOS. Estos permisos pueden ser abusados para realizar escaladas de privilegios y llevar a cabo ataques de intercomunicación entre procesos (IPC) en el sistema. Es importante revisar y limitar cuidadosamente los permisos otorgados en este archivo para garantizar la seguridad del sistema.
Para más información sobre cómo asegurar y restringir los permisos en entitlements.plist
, consulta la documentación oficial de Apple sobre el tema.
Compila el programa anterior y agrega los permisos necesarios para poder inyectar código con el mismo usuario (de lo contrario, necesitarás usar sudo).
Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE) Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)