Πώς να προστατέψετε διεργασίες και επεκτάσεις πυρήνα στο macOS

Γεια σου, Χαμπρ! Σήμερα θα ήθελα να μιλήσω για το πώς μπορείτε να προστατεύσετε τις διαδικασίες από επιθέσεις από εισβολείς στο macOS. Για παράδειγμα, αυτό είναι χρήσιμο για ένα σύστημα προστασίας από ιούς ή εφεδρικό σύστημα, ειδικά επειδή στο 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 στους εσωτερικούς μηχανισμούς του πυρήνα (αν και μπορεί να είναι μη τετριμμένοι, θα τις αφήσουμε για άλλο άρθρο 🙂 - επαλήθευση υπογραφής, σφάλματα μνήμης, χειρισμός εξόδου/τερματισμού, παραβιάσεις προστασίας αρχείων κ.λπ. .

Ας ξεκινήσουμε την ανασκόπηση με τη συνάρτηση και την αντίστοιχη κλήση συστήματος τερματισμός_με_ωφέλιμο φορτίο. Μπορεί να φανεί ότι εκτός από την κλασική κλήση kill, υπάρχει μια εναλλακτική προσέγγιση που είναι συγκεκριμένη για το λειτουργικό σύστημα macOS και δεν βρίσκεται στο BSD. Οι αρχές λειτουργίας και των δύο κλήσεων συστήματος είναι επίσης παρόμοιες. Είναι άμεσες κλήσεις στο psignal της συνάρτησης πυρήνα. Σημειώστε επίσης ότι πριν από την ολοκλήρωση μιας διεργασίας, εκτελείται ένας έλεγχος "cansignal" - εάν η διαδικασία μπορεί να στείλει ένα σήμα σε άλλη διεργασία· το σύστημα δεν επιτρέπει σε καμία εφαρμογή να σκοτώσει διεργασίες συστήματος, για παράδειγμα.

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 και των ορίων μνήμης, έτσι ώστε το σύστημα να μην επιβραδύνεται σημαντικά από τις δραστηριότητες προσωρινής αποθήκευσης αρχείων. Εάν μια εφαρμογή έχει φτάσει στο όριο πόρων της, όπως φαίνεται από τη συνάρτηση proc_apply_resource_actions, αποστέλλεται ένα σήμα SIGKILL στη διεργασία.

Αν και αυτή η κλήση συστήματος θα μπορούσε ενδεχομένως να σκοτώσει μια διεργασία, το σύστημα δεν έλεγξε επαρκώς τα δικαιώματα της διαδικασίας που καλεί την κλήση συστήματος. Πραγματικά έλεγχος υπήρχε, αλλά αρκεί να χρησιμοποιήσετε την εναλλακτική επισήμανση PROC_POLICY_ACTION_SET για να παρακάμψετε αυτήν τη συνθήκη.

Ως εκ τούτου, εάν «περιορίσετε» το όριο χρήσης της CPU της εφαρμογής (για παράδειγμα, επιτρέποντας μόνο 1 ns να τρέξει), τότε μπορείτε να σκοτώσετε οποιαδήποτε διεργασία στο σύστημα. Έτσι, το κακόβουλο λογισμικό μπορεί να σκοτώσει οποιαδήποτε διαδικασία στο σύστημα, συμπεριλαμβανομένης της διαδικασίας προστασίας από ιούς. Επίσης ενδιαφέρον είναι το αποτέλεσμα που εμφανίζεται όταν σκοτώνεται μια διεργασία με pid 1 (launchctl) - πανικός πυρήνα κατά την προσπάθεια επεξεργασίας του σήματος SIGKILL :)

Πώς να προστατέψετε διεργασίες και επεκτάσεις πυρήνα στο macOS

Πώς να λύσετε το πρόβλημα;

Ο πιο απλός τρόπος για να αποτρέψετε τη θανάτωση μιας διεργασίας είναι να αντικαταστήσετε τον δείκτη συνάρτησης στον πίνακα κλήσεων συστήματος. Δυστυχώς, αυτή η μέθοδος δεν είναι ασήμαντη για πολλούς λόγους.

Πρώτον, το σύμβολο που ελέγχει τη θέση μνήμης του sysent δεν είναι μόνο ιδιωτικό στο σύμβολο του πυρήνα XNU, αλλά δεν μπορεί να βρεθεί στα σύμβολα του πυρήνα. Θα πρέπει να χρησιμοποιήσετε ευρετικές μεθόδους αναζήτησης, όπως η δυναμική αποσυναρμολόγηση της συνάρτησης και η αναζήτηση ενός δείκτη σε αυτήν.

Δεύτερον, η δομή των καταχωρήσεων στον πίνακα εξαρτάται από τις σημαίες με τις οποίες συντάχθηκε ο πυρήνας. Εάν δηλωθεί η σημαία CONFIG_REQUIRES_U32_MUNGING, το μέγεθος της δομής θα αλλάξει - θα προστεθεί ένα επιπλέον πεδίο sy_arg_munge32. Είναι απαραίτητο να πραγματοποιηθεί ένας πρόσθετος έλεγχος για να προσδιοριστεί με ποια σημαία έχει μεταγλωττιστεί ο πυρήνας, ή εναλλακτικά, να ελέγξετε τους δείκτες συνάρτησης έναντι γνωστών.

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 για αυτήν.

Πώς να προστατέψετε διεργασίες και επεκτάσεις πυρήνα στο macOS

Το μειονέκτημα αυτής της προσέγγισης μπορεί να είναι η χαμηλή απόδοση καθώς αυξάνεται ο αριθμός των προθεμάτων. Για να διασφαλίσετε ότι η σύγκριση δεν είναι ίση με το O(πρόθεμα*μήκος), όπου το πρόθεμα είναι ο αριθμός των προθεμάτων, το μήκος είναι το μήκος της συμβολοσειράς, μπορείτε να χρησιμοποιήσετε ένα ντετερμινιστικό πεπερασμένο αυτόματο (DFA) που έχει κατασκευαστεί από προθέματα.

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

Στην πρώτη περίπτωση (όλοι οι χαρακτήρες κάτω από τους δρομείς είναι ίδιοι), παίρνουμε μια κατάσταση DFA που έχει μόνο μία μετάβαση στην ίδια γραμμή. Στη δεύτερη περίπτωση, παίρνουμε έναν πίνακα μεταβάσεων μεγέθους 256 (αριθμός χαρακτήρων και μέγιστος αριθμός ομάδων) σε επόμενες καταστάσεις που λαμβάνονται με αναδρομική κλήση της συνάρτησης.

Ας δούμε ένα παράδειγμα. Για ένα σύνολο προθεμάτων (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) μπορείτε να λάβετε τα εξής DFA. Το σχήμα δείχνει μόνο μεταβάσεις που οδηγούν σε άλλες καταστάσεις· άλλες μεταβάσεις δεν θα είναι τελικές.

Πώς να προστατέψετε διεργασίες και επεκτάσεις πυρήνα στο macOS

Όταν περνάτε από τις πολιτείες DKA, μπορεί να υπάρχουν 3 περιπτώσεις.

  1. Η τελική κατάσταση έχει επιτευχθεί - η διαδρομή είναι προστατευμένη, περιορίζουμε τις λειτουργίες KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA και KAUTH_VNODE_DELETE_CHILD
  2. Δεν επιτεύχθηκε η τελική κατάσταση, αλλά το μονοπάτι "τελείωσε" (έφθασε στο μηδενικό τερματικό) - η διαδρομή είναι γονική, είναι απαραίτητο να περιοριστεί το KAUTH_VNODE_DELETE. Σημειώστε ότι εάν το vnode είναι φάκελος, πρέπει να προσθέσετε ένα '/' στο τέλος, διαφορετικά μπορεί να το περιορίσει στο αρχείο "/foor/bar/t", το οποίο είναι λάθος.
  3. Η τελική κατάσταση δεν έφτασε, η διαδρομή δεν τελείωσε. Κανένα από τα προθέματα δεν ταιριάζει με αυτό, δεν εισάγουμε περιορισμούς.

Συμπέρασμα

Στόχος των λύσεων ασφαλείας που αναπτύσσονται είναι η αύξηση του επιπέδου ασφάλειας του χρήστη και των δεδομένων του. Από τη μία, αυτός ο στόχος επιτυγχάνεται με την ανάπτυξη του προϊόντος λογισμικού Acronis, το οποίο κλείνει εκείνα τα τρωτά σημεία όπου το ίδιο το λειτουργικό σύστημα είναι «αδύναμο». Από την άλλη πλευρά, δεν πρέπει να παραμελούμε την ενίσχυση εκείνων των πτυχών ασφαλείας που μπορούν να βελτιωθούν από την πλευρά του λειτουργικού συστήματος, ειδικά επειδή το κλείσιμο τέτοιων τρωτών σημείων αυξάνει τη σταθερότητά μας ως προϊόντος. Το θέμα ευπάθειας αναφέρθηκε στην Ομάδα Ασφάλειας Προϊόντων της Apple και έχει επιδιορθωθεί στο macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Πώς να προστατέψετε διεργασίες και επεκτάσεις πυρήνα στο macOS

Όλα αυτά μπορούν να γίνουν μόνο εάν το βοηθητικό πρόγραμμα σας έχει εγκατασταθεί επίσημα στον πυρήνα. Δηλαδή, δεν υπάρχουν τέτοια κενά για εξωτερικό και ανεπιθύμητο λογισμικό. Ωστόσο, όπως μπορείτε να δείτε, ακόμη και η προστασία των νόμιμων προγραμμάτων όπως τα συστήματα προστασίας από ιούς και τα συστήματα δημιουργίας αντιγράφων ασφαλείας απαιτεί δουλειά. Τώρα όμως τα νέα προϊόντα Acronis για macOS θα έχουν πρόσθετη προστασία από την εκφόρτωση από το σύστημα.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο