Το MIG δημιουργήθηκε για να απλοποιήσει τη διαδικασία δημιουργίας κώδικα Mach IPC. Βασικά δημιουργεί τον απαιτούμενο κώδικα για τον εξυπηρετητή και τον πελάτη να επικοινωνούν με μια δεδομένη ορισμένη. Ακόμη κι αν ο δημιουργημένος κώδικας είναι άσχημος, ένας προγραμματιστής θα χρειαστεί απλώς να τον εισάγει και ο κώδικάς του θα είναι πολύ απλούστερος από πριν.
Η ορισμένη είναι καθορισμένη στη Γλώσσα Ορισμού Διεπαφής (IDL) χρησιμοποιώντας την επέκταση .defs.
Αυτές οι ορισμοί έχουν 5 ενότητες:
Δήλωση υποσυστήματος: Η λέξη-κλειδί υποσύστημα χρησιμοποιείται για να υποδείξει το όνομα και το ID. Είναι επίσης δυνατό να το σημειώσετε ως KernelServer αν ο εξυπηρετητής πρέπει να εκτελείται στον πυρήνα.
Συμπερίληψη και εισαγωγές: Το MIG χρησιμοποιεί τον προεπεξεργαστή C, έτσι μπορεί να χρησιμοποιήσει εισαγωγές. Επιπλέον, είναι δυνατό να χρησιμοποιήσετε uimport και simport για κώδικα που δημιουργείται από χρήστη ή εξυπηρετητή.
Δηλώσεις τύπων: Είναι δυνατό να ορίσετε τύπους δεδομένων αν και συνήθως θα εισάγει τα mach_types.defs και std_types.defs. Για προσαρμοσμένους τύπους μπορεί να χρησιμοποιηθεί κάποια σύνταξη:
[in/out]tran: Συνάρτηση που πρέπει να μεταφραστεί από ένα εισερχόμενο ή προς ένα εξερχόμενο μήνυμα
c[user/server]type: Αντιστοίχιση σε άλλον τύπο C.
destructor: Καλέστε αυτήν τη συνάρτηση όταν ο τύπος απελευθερώνεται.
Λειτουργίες: Αυτές είναι οι ορισμοί των μεθόδων RPC. Υπάρχουν 5 διαφορετικοί τύποι:
routine: Αναμένει απάντηση
simpleroutine: Δεν αναμένει απάντηση
procedure: Αναμένει απάντηση
simpleprocedure: Δεν αναμένει απάντηση
function: Αναμένει απάντηση
Παράδειγμα
Δημιουργήστε ένα αρχείο ορισμού, σε αυτήν την περίπτωση με μια πολύ απλή λειτουργία:
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);
Σημειώστε ότι το πρώτο όρισμα είναι τη θύρα προς σύνδεση και το MIG θα χειριστεί αυτόματα τη θύρα απάντησης (εκτός αν καλείτε την mig_get_reply_port() στον κώδικα του πελάτη). Επιπλέον, το ID των λειτουργιών θα είναι συνεχόμενο ξεκινώντας από το ID του υποσυστήματος που υποδεικνύεται (έτσι αν μια λειτουργία είναι αποσυρμένη, διαγράφεται και χρησιμοποιείται το skip για να εξακολουθεί να χρησιμοποιείται το ID της).
Τώρα χρησιμοποιήστε το MIG για να δημιουργήσετε τον κώδικα εξυπηρετητή και πελάτη που θα είναι σε θέση να επικοινωνούν μεταξύ τους για να καλέσουν τη λειτουργία Αφαίρεση:
Θα δημιουργηθούν αρκετά νέα αρχεία στον τρέχοντα κατάλογο.
Μπορείτε να βρείτε ένα πιο πολύπλοκο παράδειγμα στο σύστημά σας με: mdfind mach_port.defs
Και μπορείτε να το μεταγλωττίσετε από τον ίδιο φάκελο με το αρχείο με: mig -DLIBSYSCALL_INTERFACE mach_ports.defs
Στα αρχεία myipcServer.c και myipcServer.h μπορείτε να βρείτε τη δήλωση και τον ορισμό της δομής SERVERPREFmyipc_subsystem, η οποία ουσιαστικά ορίζει τη λειτουργία που θα κληθεί με βάση το αναγνωριστικό μηνύματος που λαμβάνεται (καθορίσαμε έναν αριθμό εκκίνησης των 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)},}};
Ορισμός των συναρτήσεων που υλοποιούνται από τον server.
/* 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;
Βασισμένο στην προηγούμενη δομή, η συνάρτηση myipc_server_routine θα λάβει το ID μηνύματος και θα επιστρέψει την κατάλληλη συνάρτηση προς κλήση:
Σε αυτό το παράδειγμα έχουμε ορίσει μόνο 1 λειτουργία στις ορισμούς, αλλά αν είχαμε ορίσει περισσότερες λειτουργίες, θα βρίσκονταν μέσα στον πίνακα του SERVERPREFmyipc_subsystem και η πρώτη θα είχε ανατεθεί στο ID 500, η δεύτερη στο ID 501...
Αν η λειτουργία αναμενόταν να στείλει μια απάντηση, θα υπήρχε επίσης η λειτουργία mig_internal kern_return_t __MIG_check__Reply__<όνομα>.
Πράγματι, είναι δυνατό να αναγνωριστεί αυτή η σχέση στη δομή subsystem_to_name_map_myipc από το myipcServer.h (subsystem_to_name_map_*** σε άλλα αρχεία):
Τέλος, μια άλλη σημαντική λειτουργία για τη λειτουργία του διακομιστή θα είναι myipc_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);}
Ο πελάτης IPC είναι υπεύθυνος για τη δημιουργία ενός συνδεδεμένου socket και την αποστολή αιτημάτων στον εξυπηρετητή IPC. Αυτός ο κώδικας πελάτη δημιουργεί ένα αίτημα για την εκτέλεση της λειτουργίας do_work στον εξυπηρετητή IPC. Το αίτημα αυτό περιλαμβάνει έναν ακέραιο αριθμό, ο οποίος περνιέται ως παράμετρος στη λειτουργία do_work. Τέλος, ο πελάτης αναμένει την απάντηση από τον εξυπηρετητή IPC και εκτυπώνει το αποτέλεσμα. %}
// 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);}
Το NDR_record
Το NDR_record εξάγεται από το libsystem_kernel.dylib, και είναι ένας δομικός τύπος που επιτρέπει στο MIG να μετασχηματίζει τα δεδομένα ώστε να είναι ανεξάρτητα από το σύστημα στο οποίο χρησιμοποιείται, καθώς το MIG σχεδιάστηκε να χρησιμοποιείται μεταξύ διαφορετικών συστημάτων (και όχι μόνο στον ίδιο υπολογιστή).
Αυτό είναι ενδιαφέρον επειδή αν το _NDR_record βρεθεί σε ένα δυαδικό ως εξάρτηση (jtool2 -S <binary> | grep NDR ή nm), σημαίνει ότι το δυαδικό είναι ένας πελάτης ή Διακομιστής MIG.
Επιπλέον, οι Διακομιστές MIG έχουν τον πίνακα αποστολής στο __DATA.__const (ή στο __CONST.__constdata στον πυρήνα macOS και στο __DATA_CONST.__const σε άλλους πυρήνες *OS). Αυτό μπορεί να ανακτηθεί με το jtool2.
Και οι πελάτες MIG θα χρησιμοποιήσουν το __NDR_record για να στείλουν με το __mach_msg στους διακομιστές.
Ανάλυση Δυαδικού
jtool
Καθώς πολλά δυαδικά χρησιμοποιούν πλέον το MIG για να εκθέτουν θύρες mach, είναι ενδιαφέρον να γνωρίζουμε πώς να αναγνωρίσουμε ότι χρησιμοποιήθηκε το MIG και τις λειτουργίες που εκτελεί το MIG με κάθε αναγνωριστικό μηνύματος.
jtool2 μπορεί να αναλύσει πληροφορίες MIG από ένα δυαδικό Mach-O, ενδεικτικά του αναγνωριστικού μηνύματος και της αναγνώρισης της λειτουργίας προς εκτέλεση:
jtool2-d__DATA.__constmyipc_server|grepMIG
Επιπλέον, οι λειτουργίες MIG είναι απλά περιτύλιγμα της πραγματικής λειτουργίας που καλείται, πράγμα που σημαίνει ότι με τη διάσπασή της και την αναζήτηση για BL ενδέχεται να βρείτε την πραγματική λειτουργία που καλείται:
jtool2-d__DATA.__constmyipc_server|grepBL
Συναρτήσεις
Προηγουμένως αναφέρθηκε ότι η συνάρτηση που θα αναλάβει το να καλέσει τη σωστή συνάρτηση ανάλογα με το αναγνωριστικό μηνύματος που λαμβάνεται ήταν η myipc_server. Ωστόσο, συνήθως δεν θα έχετε τα σύμβολα του δυαδικού (καμία ονομασία συναρτήσεων), επομένως είναι ενδιαφέρον να ελέγξετε πώς μοιάζει αποσυναρμολογημένο καθώς θα είναι πάντα πολύ παρόμοιο (ο κώδικας αυτής της συνάρτησης είναι ανεξάρτητος από τις εκτεθειμένες συναρτήσεις):
int_myipc_server(int arg0,int arg1) {var_10 = arg0;var_18 = arg1;// Αρχικές οδηγίες για την εύρεση των κατάλληλων δεικτών συνάρτησης*(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);// Κλήση της sign_extend_64 που μπορεί να βοηθήσει στον εντοπισμό αυτής της συνάρτησης// Αποθηκεύει στο rax τον δείκτη προς την κλήση που πρέπει να γίνει// Ελέγξτε τη χρήση της διεύθυνσης 0x100004040 (πίνακας διευθύνσεων συναρτήσεων)// 0x1f4 = 500 (το αρχικό ID) rax =*(sign_extend_64(rax -0x1f4)*0x28+0x100004040); var_20 = rax;// Αν - αλλιώς, αν το if επιστρέφει false, ενώ το else καλεί τη σωστή συνάρτηση και επιστρέφει trueif (rax ==0x0) {*(var_18 +0x18) =**_NDR_record;*(int32_t*)(var_18 +0x20) =0xfffffffffffffed1;var_4 =0x0;}else {// Υπολογισμένη διεύθυνση που καλεί την κατάλληλη συνάρτηση με 2 ορίσματα (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;}
Αυτή είναι η ίδια συνάρτηση αποσυναρμολογημένη σε μια διαφορετική έκδοση του Hopper free:
int_myipc_server(int arg0,int arg1) {r31 = r31 -0x40;saved_fp = r29;stack[-8] = r30;var_10 = arg0;var_18 = arg1;// Αρχικές οδηγίες για την εύρεση των κατάλληλων δεικτών συνάρτησης*(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 (το αρχικό ID) 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;}}// Ίδιο if else με την προηγούμενη έκδοση// Ελέγξτε τη χρήση της διεύθυνσης 0x100004040 (πίνακας διευθύνσεων συναρτήσεων)if ((r8 &0x1) ==0x0) {*(var_18 +0x18) =**0x100004000;*(int32_t*)(var_18 +0x20) =0xfffffed1;var_4 =0x0;}else {// Κλήση στην υπολογισμένη διεύθυνση όπου πρέπει να γίνει η κλήση της συνάρτησης (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;}
Πράγματι, αν πάτε στη συνάρτηση 0x100004000 θα βρείτε τον πίνακα των δομών routine_descriptor. Το πρώτο στοιχείο της δομής είναι η διεύθυνση όπου η συνάρτηση υλοποιείται, και η δομή παίρνει 0x28 bytes, οπότε κάθε 0x28 bytes (ξεκινώντας από το byte 0) μπορείτε να πάρετε 8 bytes και αυτό θα είναι η διεύθυνση της συνάρτησης που θα κληθεί:
Ο κώδικας που δημιουργείται από το MIG καλεί επίσης το kernel_debug για τη δημιουργία καταγραφών σχετικά με λειτουργίες κατά την είσοδο και έξοδο. Είναι δυνατόν να τις ελέγξετε χρησιμοποιώντας trace ή kdv: kdv all | grep MIG