hacktricks/macos-hardening/macos-security-and-privilege-escalation/macos-proces-abuse/macos-ipc-inter-process-communication/macos-mig-mach-interface-generator.md

16 KiB

macOS MIG - Generador de Interfaz Mach

Aprende a hackear AWS desde cero hasta convertirte en un experto con htARTE (Experto en Equipos Rojos de AWS de HackTricks)!

Otras formas de apoyar a HackTricks:

MIG fue creado para simplificar el proceso de creación de código de IPC de Mach. Básicamente genera el código necesario para que el servidor y el cliente se comuniquen con una definición dada. Incluso si el código generado es feo, un desarrollador solo necesitará importarlo y su código será mucho más simple que antes.

Ejemplo

Crea un archivo de definición, en este caso con una función muy simple:

{% code title="myipc.defs" %}

subsystem myipc 500; // Arbitrary name and id

userprefix USERPREF;        // Prefix for created functions in the client
serverprefix SERVERPREF;    // Prefix for created functions in the server

#include <mach/mach_types.defs>
#include <mach/std_types.defs>

simpleroutine Subtract(
server_port :  mach_port_t;
n1          :  uint32_t;
n2          :  uint32_t);

{% endcode %}

Ahora utiliza mig para generar el código del servidor y del cliente que serán capaces de comunicarse entre sí para llamar a la función Subtract:

mig -header myipcUser.h -sheader myipcServer.h myipc.defs

Se crearán varios archivos nuevos en el directorio actual.

En los archivos myipcServer.c y myipcServer.h se puede encontrar la declaración y definición de la estructura SERVERPREFmyipc_subsystem, que básicamente define la función a llamar en función del ID del mensaje recibido (indicamos un número de inicio de 500):

{% tabs %} {% tab title="myipcServer.c" %}

/* Description of this subsystem, for use in direct RPC */
const struct SERVERPREFmyipc_subsystem SERVERPREFmyipc_subsystem = {
myipc_server_routine,
500, // start ID
501, // end ID
(mach_msg_size_t)sizeof(union __ReplyUnion__SERVERPREFmyipc_subsystem),
(vm_address_t)0,
{
{ (mig_impl_routine_t) 0,
// Function to call
(mig_stub_routine_t) _XSubtract, 3, 0, (routine_arg_descriptor_t)0, (mach_msg_size_t)sizeof(__Reply__Subtract_t)},
}
};

{% endtab %}

{% tab title="myipcServer.h" %}

macOS MIG (Mach Interface Generator)

El Generador de Interfaz Mach (Mach Interface Generator, MIG) es una herramienta utilizada en macOS para generar código fuente en C a partir de definiciones de interfaz. Permite la comunicación entre procesos a través de llamadas a procedimientos remotos.

Creación de una definición de interfaz MIG

Para crear una definición de interfaz MIG, se debe definir un archivo .defs que contenga las declaraciones de los procedimientos remotos que se desean exponer. Luego, se utiliza la herramienta mig para generar el código fuente en C correspondiente.

Implementación de un servidor MIG

Una vez que se ha generado el código fuente en C a partir de la definición de interfaz MIG, se puede implementar un servidor MIG que maneje las llamadas a procedimientos remotos definidos en la interfaz.

#include <stdio.h>
#include <mach/mach.h>
#include "myipcServer.h"

kern_return_t my_remote_procedure_implementation(mach_port_t server, int data, int *result) {
    // Implementación del procedimiento remoto
    *result = data * 2;
    return KERN_SUCCESS;
}

int main() {
    mach_port_t server = mig_create_server();
    mig_server(server);
    return 0;
}

En el ejemplo anterior, se muestra la implementación de un servidor MIG simple que maneja una llamada a un procedimiento remoto para multiplicar un número por 2.

La comunicación entre procesos a través de MIG es fundamental en macOS y puede ser utilizada de manera maliciosa para abusar de los privilegios del sistema. Es importante asegurar que las implementaciones de servidores MIG estén correctamente protegidas para prevenir posibles vulnerabilidades de seguridad.

{% endtab %}

/* Description of this subsystem, for use in direct RPC */
extern const struct SERVERPREFmyipc_subsystem {
mig_server_routine_t	server;	/* Server routine */
mach_msg_id_t	start;	/* Min routine number */
mach_msg_id_t	end;	/* Max routine number + 1 */
unsigned int	maxsize;	/* Max msg size */
vm_address_t	reserved;	/* Reserved */
struct routine_descriptor	/* Array of routine descriptors */
routine[1];
} SERVERPREFmyipc_subsystem;

{% endtab %} {% endtabs %}

Basado en la estructura anterior, la función myipc_server_routine obtendrá el ID del mensaje y devolverá la función adecuada para llamar:

mig_external mig_routine_t myipc_server_routine
(mach_msg_header_t *InHeadP)
{
int msgh_id;

msgh_id = InHeadP->msgh_id - 500;

if ((msgh_id > 0) || (msgh_id < 0))
return 0;

return SERVERPREFmyipc_subsystem.routine[msgh_id].stub_routine;
}

En este ejemplo solo hemos definido 1 función en las definiciones, pero si hubiéramos definido más funciones, estarían dentro del array de SERVERPREFmyipc_subsystem y la primera se habría asignado al ID 500, la segunda al ID 501...

De hecho, es posible identificar esta relación en la estructura subsystem_to_name_map_myipc de myipcServer.h:

#ifndef subsystem_to_name_map_myipc
#define subsystem_to_name_map_myipc \
{ "Subtract", 500 }
#endif

Finalmente, otra función importante para que el servidor funcione será myipc_server, que es la que realmente llamará a la función relacionada con el ID recibido:

mig_external boolean_t myipc_server
(mach_msg_header_t *InHeadP, mach_msg_header_t *OutHeadP)
{
/*
* typedef struct {
* 	mach_msg_header_t Head;
* 	NDR_record_t NDR;
* 	kern_return_t RetCode;
* } mig_reply_error_t;
*/

mig_routine_t routine;

OutHeadP->msgh_bits = MACH_MSGH_BITS(MACH_MSGH_BITS_REPLY(InHeadP->msgh_bits), 0);
OutHeadP->msgh_remote_port = InHeadP->msgh_reply_port;
/* Tamaño mínimo: ¡routine() lo actualizará si es diferente! */
OutHeadP->msgh_size = (mach_msg_size_t)sizeof(mig_reply_error_t);
OutHeadP->msgh_local_port = MACH_PORT_NULL;
OutHeadP->msgh_id = InHeadP->msgh_id + 100;
OutHeadP->msgh_reserved = 0;

if ((InHeadP->msgh_id > 500) || (InHeadP->msgh_id < 500) ||
	    ((routine = SERVERPREFmyipc_subsystem.routine[InHeadP->msgh_id - 500].stub_routine) == 0)) {
		((mig_reply_error_t *)OutHeadP)->NDR = NDR_record;
((mig_reply_error_t *)OutHeadP)->RetCode = MIG_BAD_ID;
return FALSE;
}
	(*routine) (InHeadP, OutHeadP);
	return TRUE;
}

Verifique las líneas previamente resaltadas accediendo a la función a llamar por ID.

En el siguiente código se muestra cómo crear un servidor y un cliente simples donde el cliente puede llamar a las funciones Restar del servidor:

{% tabs %} {% tab title="myipc_server.c" %}

// gcc myipc_server.c myipcServer.c -o myipc_server

#include <stdio.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>
#include "myipcServer.h"

kern_return_t SERVERPREFSubtract(mach_port_t server_port, uint32_t n1, uint32_t n2)
{
printf("Received: %d - %d = %d\n", n1, n2, n1 - n2);
return KERN_SUCCESS;
}

int main() {

mach_port_t port;
kern_return_t kr;

// Register the mach service
kr = bootstrap_check_in(bootstrap_port, "xyz.hacktricks.mig", &port);
if (kr != KERN_SUCCESS) {
printf("bootstrap_check_in() failed with code 0x%x\n", kr);
return 1;
}

// myipc_server is the function that handles incoming messages (check previous exlpanation)
mach_msg_server(myipc_server, sizeof(union __RequestUnion__SERVERPREFmyipc_subsystem), port, MACH_MSG_TIMEOUT_NONE);
}

{% endtab %}

{% tab title="myipc_client.c" %}

macOS MIG (Mach Interface Generator)

El Generador de Interfaz Mach (MIG) es una herramienta utilizada en macOS para generar código fuente en C que facilita la comunicación entre procesos a través de llamadas a procedimientos remotos. Permite a los procesos enviar mensajes y datos entre sí de manera eficiente y segura.

En el contexto de la seguridad, los atacantes pueden abusar de las interfaces generadas por MIG para realizar ataques de escalada de privilegios o para comprometer la integridad del sistema. Es fundamental implementar medidas de seguridad adecuadas al utilizar MIG para prevenir posibles vulnerabilidades y proteger la comunicación entre procesos en macOS.

Para más información sobre cómo protegerse de posibles abusos de procesos en macOS, consulta la sección correspondiente en este libro.

#include <stdio.h>
#include <mach/mach.h>
#include <servers/bootstrap.h>
#include "myipc.h"

int main() {
    mach_port_t server_port;
    kern_return_t kr;

    kr = bootstrap_look_up(bootstrap_port, "com.example.myipc", &server_port);
    if (kr != KERN_SUCCESS) {
        printf("Failed to look up server port\n");
        return 1;
    }

    myipc_function(server_port);

    return 0;
}

{% endtab %}

// gcc myipc_client.c myipcUser.c -o myipc_client

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <mach/mach.h>
#include <servers/bootstrap.h>
#include "myipcUser.h"

int main() {

// Lookup the receiver port using the bootstrap server.
mach_port_t port;
kern_return_t kr = bootstrap_look_up(bootstrap_port, "xyz.hacktricks.mig", &port);
if (kr != KERN_SUCCESS) {
printf("bootstrap_look_up() failed with code 0x%x\n", kr);
return 1;
}
printf("Port right name %d\n", port);
USERPREFSubtract(port, 40, 2);
}

Análisis Binario

Dado que muchos binarios ahora utilizan MIG para exponer puertos mach, es interesante saber cómo identificar que se utilizó MIG y las funciones que MIG ejecuta con cada ID de mensaje.

jtool2 puede analizar la información de MIG de un binario Mach-O indicando el ID del mensaje e identificando la función a ejecutar:

jtool2 -d __DATA.__const myipc_server | grep MIG

Se mencionó anteriormente que la función que se encargará de llamar a la función correcta dependiendo del ID del mensaje recibido era myipc_server. Sin embargo, generalmente no se tendrán los símbolos del binario (nombres de funciones), por lo que es interesante ver cómo se ve decompilado ya que siempre será muy similar (el código de esta función es independiente de las funciones expuestas):

{% tabs %} {% tab title="myipc_server decompiled 1" %}

int _myipc_server(int arg0, int arg1) {
var_10 = arg0;
var_18 = arg1;
// Instrucciones iniciales para encontrar los punteros de función adecuados
*(int32_t *)var_18 = *(int32_t *)var_10 & 0x1f;
*(int32_t *)(var_18 + 0x8) = *(int32_t *)(var_10 + 0x8);
*(int32_t *)(var_18 + 0x4) = 0x24;
*(int32_t *)(var_18 + 0xc) = 0x0;
*(int32_t *)(var_18 + 0x14) = *(int32_t *)(var_10 + 0x14) + 0x64;
*(int32_t *)(var_18 + 0x10) = 0x0;
if (*(int32_t *)(var_10 + 0x14) <= 0x1f4 && *(int32_t *)(var_10 + 0x14) >= 0x1f4) {
rax = *(int32_t *)(var_10 + 0x14);
// Llamada a sign_extend_64 que puede ayudar a identificar esta función
// Esto almacena en rax el puntero a la llamada que debe realizarse
// Ver el uso de la dirección 0x100004040 (array de direcciones de funciones)
// 0x1f4 = 500 (el ID de inicio)
            rax = *(sign_extend_64(rax - 0x1f4) * 0x28 + 0x100004040);
            var_20 = rax;
// If - else, el if devuelve falso, mientras que el else llama a la función correcta y devuelve verdadero
            if (rax == 0x0) {
                    *(var_18 + 0x18) = **_NDR_record;
*(int32_t *)(var_18 + 0x20) = 0xfffffffffffffed1;
var_4 = 0x0;
}
else {
// Dirección calculada que llama a la función adecuada con 2 argumentos
                    (var_20)(var_10, var_18);
                    var_4 = 0x1;
}
}
else {
*(var_18 + 0x18) = **_NDR_record;
*(int32_t *)(var_18 + 0x20) = 0xfffffffffffffed1;
var_4 = 0x0;
}
rax = var_4;
return rax;
}

{% endtab %}

{% tab title="myipc_server decompiled 2" %} Esta es la misma función decompilada en una versión gratuita diferente de Hopper:

int _myipc_server(int arg0, int arg1) {
r31 = r31 - 0x40;
saved_fp = r29;
stack[-8] = r30;
var_10 = arg0;
var_18 = arg1;
// Instrucciones iniciales para encontrar los punteros de función adecuados
*(int32_t *)var_18 = *(int32_t *)var_10 & 0x1f | 0x0;
*(int32_t *)(var_18 + 0x8) = *(int32_t *)(var_10 + 0x8);
*(int32_t *)(var_18 + 0x4) = 0x24;
*(int32_t *)(var_18 + 0xc) = 0x0;
*(int32_t *)(var_18 + 0x14) = *(int32_t *)(var_10 + 0x14) + 0x64;
*(int32_t *)(var_18 + 0x10) = 0x0;
r8 = *(int32_t *)(var_10 + 0x14);
r8 = r8 - 0x1f4;
if (r8 > 0x0) {
if (CPU_FLAGS & G) {
r8 = 0x1;
}
}
if ((r8 & 0x1) == 0x0) {
r8 = *(int32_t *)(var_10 + 0x14);
r8 = r8 - 0x1f4;
if (r8 < 0x0) {
if (CPU_FLAGS & L) {
r8 = 0x1;
}
}
if ((r8 & 0x1) == 0x0) {
r8 = *(int32_t *)(var_10 + 0x14);
// 0x1f4 = 500 (el ID de inicio)
                    r8 = r8 - 0x1f4;
                    asm { smaddl     x8, w8, w9, x10 };
r8 = *(r8 + 0x8);
var_20 = r8;
r8 = r8 - 0x0;
if (r8 != 0x0) {
if (CPU_FLAGS & NE) {
r8 = 0x1;
}
}
// Mismo if else que en la versión anterior
// Ver el uso de la dirección 0x100004040 (array de direcciones de funciones)
                    if ((r8 & 0x1) == 0x0) {
                            *(var_18 + 0x18) = **0x100004000;
                            *(int32_t *)(var_18 + 0x20) = 0xfffffed1;
var_4 = 0x0;
}
else {
// Llamada a la dirección calculada donde debería estar la función
                            (var_20)(var_10, var_18);
                            var_4 = 0x1;
}
}
else {
*(var_18 + 0x18) = **0x100004000;
*(int32_t *)(var_18 + 0x20) = 0xfffffed1;
var_4 = 0x0;
}
}
else {
*(var_18 + 0x18) = **0x100004000;
*(int32_t *)(var_18 + 0x20) = 0xfffffed1;
var_4 = 0x0;
}
r0 = var_4;
return r0;
}

{% endtab %} {% endtabs %}

De hecho, si vas a la función 0x100004000 encontrarás el array de estructuras routine_descriptor. El primer elemento de la estructura es la dirección donde está implementada la función, y la estructura ocupa 0x28 bytes, por lo que cada 0x28 bytes (comenzando desde el byte 0) puedes obtener 8 bytes y esa será la dirección de la función que se llamará:

Estos datos se pueden extraer usando este script de Hopper.