Γεια σου, Χαμπρ! Σήμερα θα ήθελα να μιλήσω για το πώς μπορείτε να προστατεύσετε τις διαδικασίες από επιθέσεις από εισβολείς στο macOS. Για παράδειγμα, αυτό είναι χρήσιμο για ένα σύστημα προστασίας από ιούς ή εφεδρικό σύστημα, ειδικά επειδή στο macOS υπάρχουν διάφοροι τρόποι για να "σκοτώσετε" μια διαδικασία. Διαβάστε σχετικά με αυτό και τις μεθόδους προστασίας κάτω από το κόψιμο.
Ο κλασικός τρόπος να «σκοτώσεις» μια διαδικασία
Ένας πολύ γνωστός τρόπος για να "σκοτώσετε" μια διαδικασία είναι να στείλετε ένα σήμα SIGKILL στη διαδικασία. Μέσω του bash μπορείτε να καλέσετε το τυπικό "kill -SIGKILL PID" ή "pkill -9 NAME" για kill. Η εντολή "kill" είναι γνωστή από την εποχή του UNIX και είναι διαθέσιμη όχι μόνο στο macOS, αλλά και σε άλλα συστήματα παρόμοια με το UNIX.
Ακριβώς όπως στα συστήματα που μοιάζουν με UNIX, το macOS σάς επιτρέπει να παρεμποδίζετε τυχόν σήματα σε μια διεργασία εκτός από δύο - το SIGKILL και το SIGSTOP. Αυτό το άρθρο θα επικεντρωθεί κυρίως στο σήμα SIGKILL ως σήμα που προκαλεί τον τερματισμό μιας διεργασίας.
προδιαγραφές macOS
Στο macOS, η κλήση συστήματος kill στον πυρήνα XNU καλεί τη συνάρτηση psignal(SIGKILL,...). Ας προσπαθήσουμε να δούμε ποιες άλλες ενέργειες χρήστη στο userspace μπορούν να ονομαστούν από τη συνάρτηση psignal. Ας εξαλείψουμε τις κλήσεις στη συνάρτηση psignal στους εσωτερικούς μηχανισμούς του πυρήνα (αν και μπορεί να είναι μη τετριμμένοι, θα τις αφήσουμε για άλλο άρθρο 🙂 - επαλήθευση υπογραφής, σφάλματα μνήμης, χειρισμός εξόδου/τερματισμού, παραβιάσεις προστασίας αρχείων κ.λπ. .
Ας ξεκινήσουμε την ανασκόπηση με τη συνάρτηση και την αντίστοιχη κλήση συστήματος
static int
terminate_with_payload_internal(struct proc *cur_proc, int target_pid, uint32_t reason_namespace,
uint64_t reason_code, user_addr_t payload, uint32_t payload_size,
user_addr_t reason_string, uint64_t reason_flags)
{
...
target_proc = proc_find(target_pid);
...
if (!cansignal(cur_proc, cur_cred, target_proc, SIGKILL)) {
proc_rele(target_proc);
return EPERM;
}
...
if (target_pid == cur_proc->p_pid) {
/*
* psignal_thread_with_reason() will pend a SIGKILL on the specified thread or
* return if the thread and/or task are already terminating. Either way, the
* current thread won't return to userspace.
*/
psignal_thread_with_reason(target_proc, current_thread(), SIGKILL, signal_reason);
} else {
psignal_with_reason(target_proc, SIGKILL, signal_reason);
}
...
}
εκτόξευση
Παρουσιάζεται ο τυπικός τρόπος δημιουργίας δαιμόνων κατά την εκκίνηση του συστήματος και ελέγχου της διάρκειας ζωής τους. Λάβετε υπόψη ότι οι πηγές προορίζονται για την παλιά έκδοση του launchctl έως το macOS 10.10, παρέχονται παραδείγματα κώδικα για επεξηγηματικούς σκοπούς. Το σύγχρονο launchctl στέλνει σήματα εκκίνησης μέσω XPC, η λογική launchctl έχει μεταφερθεί σε αυτό.
Ας δούμε πώς ακριβώς σταματούν οι εφαρμογές. Πριν από την αποστολή του σήματος SIGTERM, επιχειρείται η διακοπή της εφαρμογής χρησιμοποιώντας την κλήση συστήματος «proc_terminate».
<launchctl src/core.c>
...
error = proc_terminate(j->p, &sig);
if (error) {
job_log(j, LOG_ERR | LOG_CONSOLE, "Could not terminate job: %d: %s", error, strerror(error));
job_log(j, LOG_NOTICE | LOG_CONSOLE, "Using fallback option to terminate job...");
error = kill2(j->p, SIGTERM);
if (error) {
job_log(j, LOG_ERR, "Could not signal job: %d: %s", error, strerror(error));
}
...
<>
Κάτω από την κουκούλα, το proc_terminate, παρά το όνομά του, μπορεί να στείλει όχι μόνο psignal με το SIGTERM, αλλά και το SIGKILL.
Έμμεση θανάτωση - Όριο πόρων
Μια πιο ενδιαφέρουσα περίπτωση μπορεί να δει κανείς σε μια άλλη κλήση συστήματος
Αν και αυτή η κλήση συστήματος θα μπορούσε ενδεχομένως να σκοτώσει μια διεργασία, το σύστημα δεν έλεγξε επαρκώς τα δικαιώματα της διαδικασίας που καλεί την κλήση συστήματος. Πραγματικά έλεγχος
Ως εκ τούτου, εάν «περιορίσετε» το όριο χρήσης της CPU της εφαρμογής (για παράδειγμα, επιτρέποντας μόνο 1 ns να τρέξει), τότε μπορείτε να σκοτώσετε οποιαδήποτε διεργασία στο σύστημα. Έτσι, το κακόβουλο λογισμικό μπορεί να σκοτώσει οποιαδήποτε διαδικασία στο σύστημα, συμπεριλαμβανομένης της διαδικασίας προστασίας από ιούς. Επίσης ενδιαφέρον είναι το αποτέλεσμα που εμφανίζεται όταν σκοτώνεται μια διεργασία με pid 1 (launchctl) - πανικός πυρήνα κατά την προσπάθεια επεξεργασίας του σήματος SIGKILL :)
Πώς να λύσετε το πρόβλημα;
Ο πιο απλός τρόπος για να αποτρέψετε τη θανάτωση μιας διεργασίας είναι να αντικαταστήσετε τον δείκτη συνάρτησης στον πίνακα κλήσεων συστήματος. Δυστυχώς, αυτή η μέθοδος δεν είναι ασήμαντη για πολλούς λόγους.
Πρώτον, το σύμβολο που ελέγχει τη θέση μνήμης του sysent δεν είναι μόνο ιδιωτικό στο σύμβολο του πυρήνα XNU, αλλά δεν μπορεί να βρεθεί στα σύμβολα του πυρήνα. Θα πρέπει να χρησιμοποιήσετε ευρετικές μεθόδους αναζήτησης, όπως η δυναμική αποσυναρμολόγηση της συνάρτησης και η αναζήτηση ενός δείκτη σε αυτήν.
Δεύτερον, η δομή των καταχωρήσεων στον πίνακα εξαρτάται από τις σημαίες με τις οποίες συντάχθηκε ο πυρήνας. Εάν δηλωθεί η σημαία CONFIG_REQUIRES_U32_MUNGING, το μέγεθος της δομής θα αλλάξει - θα προστεθεί ένα επιπλέον πεδίο
struct sysent { /* system call table */
sy_call_t *sy_call; /* implementing function */
#if CONFIG_REQUIRES_U32_MUNGING || (__arm__ && (__BIGGEST_ALIGNMENT__ > 4))
sy_munge_t *sy_arg_munge32; /* system call arguments munger for 32-bit process */
#endif
int32_t sy_return_type; /* system call return types */
int16_t sy_narg; /* number of args */
uint16_t sy_arg_bytes; /* Total size of arguments in bytes for
* 32-bit system calls
*/
};
Ευτυχώς, στις σύγχρονες εκδόσεις του macOS, η Apple παρέχει ένα νέο API για εργασία με διεργασίες. Το Endpoint Security API επιτρέπει στους πελάτες να εξουσιοδοτούν πολλά αιτήματα σε άλλες διεργασίες. Έτσι, μπορείτε να αποκλείσετε τυχόν σήματα σε διεργασίες, συμπεριλαμβανομένου του σήματος SIGKILL, χρησιμοποιώντας το παραπάνω API.
#include <bsm/libbsm.h>
#include <EndpointSecurity/EndpointSecurity.h>
#include <unistd.h>
int main(int argc, const char * argv[]) {
es_client_t* cli = nullptr;
{
auto res = es_new_client(&cli, ^(es_client_t * client, const es_message_t * message) {
switch (message->event_type) {
case ES_EVENT_TYPE_AUTH_SIGNAL:
{
auto& msg = message->event.signal;
auto target = msg.target;
auto& token = target->audit_token;
auto pid = audit_token_to_pid(token);
printf("signal '%d' sent to pid '%d'n", msg.sig, pid);
es_respond_auth_result(client, message, pid == getpid() ? ES_AUTH_RESULT_DENY : ES_AUTH_RESULT_ALLOW, false);
}
break;
default:
break;
}
});
}
{
es_event_type_t evs[] = { ES_EVENT_TYPE_AUTH_SIGNAL };
es_subscribe(cli, evs, sizeof(evs) / sizeof(*evs));
}
printf("%dn", getpid());
sleep(60); // could be replaced with other waiting primitive
es_unsubscribe_all(cli);
es_delete_client(cli);
return 0;
}
Ομοίως, μια Πολιτική MAC μπορεί να καταχωρηθεί στον πυρήνα, η οποία παρέχει μια μέθοδο προστασίας σήματος (policy proc_check_signal), αλλά το API δεν υποστηρίζεται επίσημα.
Προστασία επέκτασης πυρήνα
Εκτός από την προστασία των διεργασιών στο σύστημα, η προστασία της ίδιας της επέκτασης του πυρήνα (kext) είναι επίσης απαραίτητη. Το macOS παρέχει ένα πλαίσιο για τους προγραμματιστές να αναπτύσσουν εύκολα προγράμματα οδήγησης συσκευών IOKit. Εκτός από την παροχή εργαλείων για εργασία με συσκευές, το IOKit παρέχει μεθόδους για στοίβαξη προγραμμάτων οδήγησης χρησιμοποιώντας στιγμιότυπα κλάσεων C++. Μια εφαρμογή στον χώρο χρηστών θα μπορεί να "βρεί" ένα καταχωρημένο παράδειγμα της κλάσης για να δημιουργήσει μια σχέση πυρήνα-χρήστη.
Για τον εντοπισμό του αριθμού των παρουσιών κλάσης στο σύστημα, υπάρχει το βοηθητικό πρόγραμμα ioclasscount.
my_kext_ioservice = 1
my_kext_iouserclient = 1
Οποιαδήποτε επέκταση πυρήνα που επιθυμεί να εγγραφεί στη στοίβα προγράμματος οδήγησης πρέπει να δηλώσει μια κλάση που κληρονομεί από το IOService, για παράδειγμα my_kext_ioservice σε αυτήν την περίπτωση. Η σύνδεση εφαρμογών χρήστη προκαλεί τη δημιουργία μιας νέας παρουσίας της κλάσης που κληρονομεί από το IOUserClient, στο παράδειγμα my_kext_iouserclient.
Όταν προσπαθείτε να ξεφορτώσετε ένα πρόγραμμα οδήγησης από το σύστημα (εντολή kextunload), καλείται η εικονική συνάρτηση "bool terminate(IOOptionBits options)". Αρκεί να επιστρέψετε το false στην κλήση για τερματισμό όταν προσπαθείτε να ξεφορτώσετε για να απενεργοποιήσετε το kextunload.
bool Kext::terminate(IOOptionBits options)
{
if (!IsUnloadAllowed)
{
// Unload is not allowed, returning false
return false;
}
return super::terminate(options);
}
Η σημαία IsUnloadAllowed μπορεί να οριστεί από το IOUserClient κατά τη φόρτωση. Όταν υπάρχει όριο λήψης, η εντολή kextunload θα επιστρέψει την ακόλουθη έξοδο:
admin@admins-Mac drivermanager % sudo kextunload ./test.kext
Password:
(kernel) Can't remove kext my.kext.test; services failed to terminate - 0xe00002c7.
Failed to unload my.kext.test - (iokit/common) unsupported function.
Παρόμοια προστασία πρέπει να γίνει για το IOUserClient. Οι παρουσίες κλάσεων μπορούν να εκφορτωθούν χρησιμοποιώντας τη συνάρτηση χώρου χρηστών του IOKitLib "IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);". Μπορείτε να επιστρέψετε το false όταν καλείτε την εντολή «τερματισμός» μέχρι να «πεθάνει» η εφαρμογή userpace, δηλαδή να μην κληθεί η συνάρτηση «clientDied».
Προστασία αρχείων
Για την προστασία των αρχείων, αρκεί να χρησιμοποιήσετε το Kauth API, το οποίο σας επιτρέπει να περιορίσετε την πρόσβαση σε αρχεία. Η Apple παρέχει στους προγραμματιστές ειδοποιήσεις σχετικά με διάφορα συμβάντα στο πεδίο εφαρμογής· για εμάς, οι λειτουργίες KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA και KAUTH_VNODE_DELETE_CHILD είναι σημαντικές. Ο ευκολότερος τρόπος περιορισμού της πρόσβασης στα αρχεία είναι μέσω διαδρομής - χρησιμοποιούμε το API "vn_getpath" για να λάβουμε τη διαδρομή προς το αρχείο και να συγκρίνουμε το πρόθεμα διαδρομής. Σημειώστε ότι για τη βελτιστοποίηση της μετονομασίας των διαδρομών φακέλων αρχείων, το σύστημα δεν εξουσιοδοτεί την πρόσβαση σε κάθε αρχείο, αλλά μόνο στον ίδιο τον φάκελο που έχει μετονομαστεί. Είναι απαραίτητο να συγκρίνετε τη γονική διαδρομή και να περιορίσετε το KAUTH_VNODE_DELETE για αυτήν.
Το μειονέκτημα αυτής της προσέγγισης μπορεί να είναι η χαμηλή απόδοση καθώς αυξάνεται ο αριθμός των προθεμάτων. Για να διασφαλίσετε ότι η σύγκριση δεν είναι ίση με το O(πρόθεμα*μήκος), όπου το πρόθεμα είναι ο αριθμός των προθεμάτων, το μήκος είναι το μήκος της συμβολοσειράς, μπορείτε να χρησιμοποιήσετε ένα ντετερμινιστικό πεπερασμένο αυτόματο (DFA) που έχει κατασκευαστεί από προθέματα.
Ας εξετάσουμε μια μέθοδο για την κατασκευή ενός DFA για ένα δεδομένο σύνολο προθεμάτων. Αρχικοποιούμε τους δρομείς στην αρχή κάθε προθέματος. Εάν όλοι οι δρομείς δείχνουν τον ίδιο χαρακτήρα, τότε αυξήστε κάθε δρομέα κατά έναν χαρακτήρα και θυμηθείτε ότι το μήκος της ίδιας γραμμής είναι μεγαλύτερο κατά ένα. Εάν υπάρχουν δύο δρομείς με διαφορετικά σύμβολα, χωρίστε τους δρομείς σε ομάδες ανάλογα με το σύμβολο στο οποίο δείχνουν και επαναλάβετε τον αλγόριθμο για κάθε ομάδα.
Στην πρώτη περίπτωση (όλοι οι χαρακτήρες κάτω από τους δρομείς είναι ίδιοι), παίρνουμε μια κατάσταση DFA που έχει μόνο μία μετάβαση στην ίδια γραμμή. Στη δεύτερη περίπτωση, παίρνουμε έναν πίνακα μεταβάσεων μεγέθους 256 (αριθμός χαρακτήρων και μέγιστος αριθμός ομάδων) σε επόμενες καταστάσεις που λαμβάνονται με αναδρομική κλήση της συνάρτησης.
Ας δούμε ένα παράδειγμα. Για ένα σύνολο προθεμάτων (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) μπορείτε να λάβετε τα εξής DFA. Το σχήμα δείχνει μόνο μεταβάσεις που οδηγούν σε άλλες καταστάσεις· άλλες μεταβάσεις δεν θα είναι τελικές.
Όταν περνάτε από τις πολιτείες DKA, μπορεί να υπάρχουν 3 περιπτώσεις.
- Η τελική κατάσταση έχει επιτευχθεί - η διαδρομή είναι προστατευμένη, περιορίζουμε τις λειτουργίες KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA και KAUTH_VNODE_DELETE_CHILD
- Δεν επιτεύχθηκε η τελική κατάσταση, αλλά το μονοπάτι "τελείωσε" (έφθασε στο μηδενικό τερματικό) - η διαδρομή είναι γονική, είναι απαραίτητο να περιοριστεί το KAUTH_VNODE_DELETE. Σημειώστε ότι εάν το vnode είναι φάκελος, πρέπει να προσθέσετε ένα '/' στο τέλος, διαφορετικά μπορεί να το περιορίσει στο αρχείο "/foor/bar/t", το οποίο είναι λάθος.
- Η τελική κατάσταση δεν έφτασε, η διαδρομή δεν τελείωσε. Κανένα από τα προθέματα δεν ταιριάζει με αυτό, δεν εισάγουμε περιορισμούς.
Συμπέρασμα
Στόχος των λύσεων ασφαλείας που αναπτύσσονται είναι η αύξηση του επιπέδου ασφάλειας του χρήστη και των δεδομένων του. Από τη μία, αυτός ο στόχος επιτυγχάνεται με την ανάπτυξη του προϊόντος λογισμικού Acronis, το οποίο κλείνει εκείνα τα τρωτά σημεία όπου το ίδιο το λειτουργικό σύστημα είναι «αδύναμο». Από την άλλη πλευρά, δεν πρέπει να παραμελούμε την ενίσχυση εκείνων των πτυχών ασφαλείας που μπορούν να βελτιωθούν από την πλευρά του λειτουργικού συστήματος, ειδικά επειδή το κλείσιμο τέτοιων τρωτών σημείων αυξάνει τη σταθερότητά μας ως προϊόντος. Το θέμα ευπάθειας αναφέρθηκε στην Ομάδα Ασφάλειας Προϊόντων της Apple και έχει επιδιορθωθεί στο macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).
Όλα αυτά μπορούν να γίνουν μόνο εάν το βοηθητικό πρόγραμμα σας έχει εγκατασταθεί επίσημα στον πυρήνα. Δηλαδή, δεν υπάρχουν τέτοια κενά για εξωτερικό και ανεπιθύμητο λογισμικό. Ωστόσο, όπως μπορείτε να δείτε, ακόμη και η προστασία των νόμιμων προγραμμάτων όπως τα συστήματα προστασίας από ιούς και τα συστήματα δημιουργίας αντιγράφων ασφαλείας απαιτεί δουλειά. Τώρα όμως τα νέα προϊόντα Acronis για macOS θα έχουν πρόσθετη προστασία από την εκφόρτωση από το σύστημα.
Πηγή: www.habr.com