MIG è stato creato per semplificare il processo di creazione del codice Mach IPC. Fondamentalmente genera il codice necessario affinché il server e il client comunichino con una definizione data. Anche se il codice generato è brutto, uno sviluppatore dovrà solo importarlo e il suo codice sarà molto più semplice di prima.
La definizione è specificata in Interface Definition Language (IDL) utilizzando l'estensione .defs.
Queste definizioni hanno 5 sezioni:
Dichiarazione del sottosistema: La parola chiave sottosistema è usata per indicare il nome e l'id. È anche possibile contrassegnarlo come KernelServer se il server deve essere eseguito nel kernel.
Inclusioni e importazioni: MIG utilizza il preprocessore C, quindi è in grado di utilizzare importazioni. Inoltre, è possibile utilizzare uimport e simport per il codice generato dall'utente o dal server.
Dichiarazioni di tipo: È possibile definire tipi di dati anche se di solito importerà mach_types.defs e std_types.defs. Per quelli personalizzati si può utilizzare una certa sintassi:
[in/out]tran: Funzione che deve essere tradotta da un messaggio in arrivo o a un messaggio in uscita
c[user/server]type: Mappatura a un altro tipo C.
destructor: Chiama questa funzione quando il tipo viene rilasciato.
Operazioni: Queste sono le definizioni dei metodi RPC. Ci sono 5 tipi diversi:
routine: Si aspetta una risposta
simpleroutine: Non si aspetta una risposta
procedure: Si aspetta una risposta
simpleprocedure: Non si aspetta una risposta
function: Si aspetta una risposta
Esempio
Crea un file di definizione, in questo caso con una funzione molto semplice:
myipc.defs
subsystem myipc 500; // Arbitrary name and iduserprefix USERPREF; // Prefix for created functions in the clientserverprefix SERVERPREF; // Prefix for created functions in the server#include<mach/mach_types.defs>#include<mach/std_types.defs>simpleroutineSubtract(server_port : mach_port_t;n1 : uint32_t;n2 : uint32_t);
Nota che il primo argomento è la porta da associare e MIG gestirà automaticamente la porta di risposta (a meno che non venga chiamato mig_get_reply_port() nel codice del client). Inoltre, l'ID delle operazioni sarà sequenziale a partire dall'ID del sottosistema indicato (quindi se un'operazione è deprecata viene eliminata e skip viene utilizzato per continuare a usare il suo ID).
Ora usa MIG per generare il codice del server e del client che sarà in grado di comunicare tra loro per chiamare la funzione Subtract:
Diversi nuovi file verranno creati nella directory corrente.
Puoi trovare un esempio più complesso nel tuo sistema con: mdfind mach_port.defs
E puoi compilarlo dalla stessa cartella del file con: mig -DLIBSYSCALL_INTERFACE mach_ports.defs
Nei file myipcServer.c e myipcServer.h puoi trovare la dichiarazione e la definizione della struct SERVERPREFmyipc_subsystem, che definisce fondamentalmente la funzione da chiamare in base all'ID del messaggio ricevuto (abbiamo indicato un numero iniziale di 500):
/* Description of this subsystem, for use in direct RPC */conststruct SERVERPREFmyipc_subsystem SERVERPREFmyipc_subsystem = {myipc_server_routine,500, // start ID501, // 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)},}};
/* Description of this subsystem, for use in direct RPC */externconststruct 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 */unsignedint maxsize; /* Max msg size */vm_address_t reserved; /* Reserved */struct routine_descriptor /* Array of routine descriptors */routine[1];} SERVERPREFmyipc_subsystem;
Basato sulla struttura precedente, la funzione myipc_server_routine otterrà il ID del messaggio e restituirà la funzione appropriata da chiamare:
In questo esempio abbiamo definito solo 1 funzione nelle definizioni, ma se avessimo definito più funzioni, sarebbero state all'interno dell'array di SERVERPREFmyipc_subsystem e la prima sarebbe stata assegnata all'ID 500, la seconda all'ID 501...
Se ci si aspettava che la funzione inviasse una reply, la funzione mig_internal kern_return_t __MIG_check__Reply__<name> esisterebbe anche.
In realtà è possibile identificare questa relazione nella struct subsystem_to_name_map_myipc da myipcServer.h (subsystem_to_name_map_*** in altri file):
Finalmente, un'altra funzione importante per far funzionare il server sarà myipc_server, che è quella che effettivamente chiama la funzione relativa all'id ricevuto:
Controlla le righe precedentemente evidenziate che accedono alla funzione da chiamare per ID.
Il seguente è il codice per creare un semplice server e client dove il client può chiamare le funzioni Sottrai dal server:
// 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_tSERVERPREFSubtract(mach_port_t server_port,uint32_t n1,uint32_t n2){printf("Received: %d - %d = %d\n", n1, n2, n1 - n2);return KERN_SUCCESS;}intmain() {mach_port_t port;kern_return_t kr;// Register the mach servicekr =bootstrap_check_in(bootstrap_port,"xyz.hacktricks.mig",&port);if (kr != KERN_SUCCESS) {printf("bootstrap_check_in() failed with code 0x%x\n", kr);return1;}// 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);}
// 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"intmain() {// 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);return1;}printf("Port right name %d\n", port);USERPREFSubtract(port,40,2);}
The NDR_record
Il NDR_record è esportato da libsystem_kernel.dylib, ed è una struct che consente a MIG di trasformare i dati in modo che siano agnostici rispetto al sistema in cui viene utilizzato, poiché MIG è stato pensato per essere utilizzato tra diversi sistemi (e non solo nella stessa macchina).
Questo è interessante perché se _NDR_record viene trovato in un binario come dipendenza (jtool2 -S <binary> | grep NDR o nm), significa che il binario è un client o server MIG.
Inoltre, i server MIG hanno la tabella di dispatch in __DATA.__const (o in __CONST.__constdata nel kernel macOS e __DATA_CONST.__const in altri kernel *OS). Questo può essere estratto con jtool2.
E i client MIG utilizzeranno il __NDR_record per inviare con __mach_msg ai server.
Binary Analysis
jtool
Poiché molti binari ora utilizzano MIG per esporre porte mach, è interessante sapere come identificare che è stato utilizzato MIG e le funzioni che MIG esegue con ciascun ID messaggio.
jtool2 può analizzare le informazioni MIG da un binario Mach-O indicando l'ID messaggio e identificando la funzione da eseguire:
jtool2-d__DATA.__constmyipc_server|grepMIG
Inoltre, le funzioni MIG sono semplicemente wrapper della funzione reale che viene chiamata, il che significa che ottenendo la sua disassemblaggio e cercando BL potresti essere in grado di trovare la funzione effettiva che viene chiamata:
jtool2-d__DATA.__constmyipc_server|grepBL
Assembly
È stato precedentemente menzionato che la funzione che si occuperà di chiamare la funzione corretta a seconda dell'ID del messaggio ricevuto era myipc_server. Tuttavia, di solito non avrai i simboli del binario (nessun nome di funzione), quindi è interessante controllare come appare decompilato poiché sarà sempre molto simile (il codice di questa funzione è indipendente dalle funzioni esposte):
int_myipc_server(int arg0,int arg1) {var_10 = arg0;var_18 = arg1;// Istruzioni iniziali per trovare i puntatori delle funzioni appropriati*(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);// Chiamata a sign_extend_64 che può aiutare a identificare questa funzione// Questo memorizza in rax il puntatore alla chiamata che deve essere chiamata// Controlla l'uso dell'indirizzo 0x100004040 (array degli indirizzi delle funzioni)// 0x1f4 = 500 (l'ID di partenza) rax =*(sign_extend_64(rax -0x1f4)*0x28+0x100004040); var_20 = rax;// Se - else, l'if restituisce falso, mentre l'else chiama la funzione corretta e restituisce veroif (rax ==0x0) {*(var_18 +0x18) =**_NDR_record;*(int32_t*)(var_18 +0x20) =0xfffffffffffffed1;var_4 =0x0;}else {// Indirizzo calcolato che chiama la funzione appropriata con 2 argomenti (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;}
Questa è la stessa funzione decompilata in una versione diversa di Hopper free:
int_myipc_server(int arg0,int arg1) {r31 = r31 -0x40;saved_fp = r29;stack[-8] = r30;var_10 = arg0;var_18 = arg1;// Istruzioni iniziali per trovare i puntatori delle funzioni appropriati*(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 (l'ID di partenza) 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;}}// Stessa logica if else della versione precedente// Controlla l'uso dell'indirizzo 0x100004040 (array degli indirizzi delle funzioni)if ((r8 &0x1) ==0x0) {*(var_18 +0x18) =**0x100004000;*(int32_t*)(var_18 +0x20) =0xfffffed1;var_4 =0x0;}else {// Chiamata all'indirizzo calcolato dove dovrebbe essere la funzione (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;}
In realtà, se vai alla funzione 0x100004000 troverai l'array di routine_descriptor struct. Il primo elemento della struct è l'indirizzo dove la funzione è implementata, e la struct occupa 0x28 byte, quindi ogni 0x28 byte (a partire dal byte 0) puoi ottenere 8 byte e quello sarà l'indirizzo della funzione che verrà chiamata:
Il codice generato da MIG chiama anche kernel_debug per generare log sulle operazioni di ingresso e uscita. È possibile controllarli utilizzando trace o kdv: kdv all | grep MIG