Κλήσεις συστήματος. Linux - syscalls. Κλήσεις συστήματος σε λειτουργική μονάδα πυρήνα Linux με δυνατότητα φόρτωσης

Πολλά - είπε ο Walrus - ήρθε η ώρα να μιλήσουμε.
L. Carroll (Απόσπασμα από το βιβλίο του B. Stroustrap)

Αντί εισαγωγής.

Για το θέμα της εσωτερικής δομής του πυρήνα Linux γενικά, των διαφόρων υποσυστημάτων και των κλήσεων του συστήματος ειδικότερα, έχει ήδη γραφτεί και ξαναγραφεί με τη σειρά. Πιθανώς, κάθε συγγραφέας που σέβεται τον εαυτό του πρέπει να γράφει για αυτό τουλάχιστον μία φορά, όπως και κάθε προγραμματιστής που σέβεται τον εαυτό του πρέπει να γράψει τον δικό του διαχειριστή αρχείων :) Αν και δεν είμαι επαγγελματίας συγγραφέας πληροφορικής και γενικά, σημειώνω μόνο για πρώτη φορά απ 'όλα, για να μην ξεχάσετε όσα μάθατε πολύ γρήγορα. Αλλά, αν οι ταξιδιωτικές σημειώσεις μου είναι πραγματικά χρήσιμες σε κάποιον, φυσικά, θα χαρώ μόνο. Λοιπόν, σε γενικές γραμμές, δεν μπορείτε να χαλάσετε το χυλό με βούτυρο, οπότε ίσως ακόμη και εγώ να μπορέσω να γράψω ή να περιγράψω κάτι που κανείς δεν μπήκε στον κόπο να αναφέρει.

Θεωρία. Τι είναι οι κλήσεις συστήματος;

Όταν εξηγούν στους μη μυημένους τι είναι λογισμικό (ή λειτουργικό σύστημα), συνήθως λένε τα εξής: ο ίδιος ο υπολογιστής είναι ένα κομμάτι υλικού, αλλά το λογισμικό είναι αυτό που καθιστά δυνατή την απόκτηση κάποιου οφέλους από αυτό το κομμάτι του υλικού. Τραχύ, φυσικά, αλλά συνολικά, κάπως αληθινό. Μάλλον θα έλεγα το ίδιο για το λειτουργικό σύστημα και τις κλήσεις συστήματος. Στην πραγματικότητα, σε διαφορετικά λειτουργικά συστήματα, οι κλήσεις συστήματος μπορούν να υλοποιηθούν με διαφορετικούς τρόπους, ο αριθμός αυτών των ίδιων κλήσεων μπορεί να διαφέρει, αλλά με τον ένα ή τον άλλο τρόπο, με τη μία ή την άλλη μορφή υπάρχει μηχανισμός κλήσης συστήματος σε οποιοδήποτε λειτουργικό σύστημα. Κάθε μέρα, ένας χρήστης εργάζεται ρητά ή σιωπηρά με αρχεία. Φυσικά, μπορεί να ανοίξει ρητά το αρχείο για επεξεργασία στο αγαπημένο του MS Word "e ή Σημειωματάριο" e, ή μπορεί απλά να ξεκινήσει ένα παιχνίδι, η εκτελέσιμη εικόνα του οποίου, παρεμπιπτόντως, είναι επίσης αποθηκευμένη σε ένα αρχείο, το οποίο, με τη σειρά του, πρέπει να ανοίξει και να διαβάσει τα εκτελέσιμα αρχεία του φορτωτή. Με τη σειρά του, το παιχνίδι μπορεί επίσης να ανοίξει και να διαβάσει δεκάδες αρχεία κατά τη διάρκεια της εργασίας του. Φυσικά, τα αρχεία δεν μπορούν μόνο να διαβαστούν, αλλά και να γραφτούν (όχι πάντα, ωστόσο, αλλά εδώ δεν μιλάμε για διαχωρισμό δικαιωμάτων και διακριτή πρόσβαση :)). Ο πυρήνας τα διαχειρίζεται όλα αυτά (στα λειτουργικά συστήματα μικροπυρήνα, η κατάσταση μπορεί να είναι διαφορετική, αλλά τώρα θα κλίνουμε διακριτικά προς το αντικείμενο της συζήτησής μας - Linux, οπότε θα αγνοήσουμε αυτό το σημείο). Η αναπαραγωγή μιας νέας διαδικασίας από μόνη της είναι επίσης μια υπηρεσία που παρέχεται από τον πυρήνα του λειτουργικού συστήματος. Όλα αυτά είναι υπέροχα, καθώς και το γεγονός ότι οι σύγχρονοι επεξεργαστές λειτουργούν σε συχνότητες gigahertz και αποτελούνται από πολλά εκατομμύρια τρανζίστορ, αλλά τι γίνεται στη συνέχεια; Ναι, τι θα γινόταν αν δεν υπήρχε μηχανισμός με τον οποίο οι εφαρμογές των χρηστών θα μπορούσαν να εκτελέσουν αρκετά κοσμικά και, ταυτόχρονα, απαραίτητα πράγματα ( Στην πραγματικότητα, σε κάθε περίπτωση, αυτές οι ασήμαντες ενέργειες εκτελούνται όχι από την εφαρμογή χρήστη, αλλά από τον πυρήνα του λειτουργικού συστήματος.), τότε το λειτουργικό σύστημα ήταν απλώς ένα πράγμα από μόνο του - απολύτως άχρηστο ή, αντίθετα, κάθε εφαρμογή χρήστη θα έπρεπε να γίνει λειτουργικό σύστημα για να εξυπηρετήσει ανεξάρτητα όλες τις ανάγκες του. Ωραία, έτσι δεν είναι;

Έτσι, καταλήξαμε στον ορισμό μιας κλήσης συστήματος κατά την πρώτη προσέγγιση: μια κλήση συστήματος είναι ένα είδος υπηρεσίας που παρέχει ο πυρήνας OS σε μια εφαρμογή χρήστη κατόπιν αιτήματος του τελευταίου. Μια τέτοια υπηρεσία μπορεί να είναι το ήδη αναφερόμενο άνοιγμα ενός αρχείου, η δημιουργία του, η ανάγνωση, η γραφή, η δημιουργία μιας νέας διαδικασίας, η λήψη του αναγνωριστικού διαδικασίας (pid), η τοποθέτηση του συστήματος αρχείων, ο τερματισμός λειτουργίας του συστήματος και τέλος. Στην πραγματική ζωή, υπάρχουν πολλές περισσότερες κλήσεις συστήματος από αυτές που αναφέρονται εδώ.

Πώς μοιάζει και τι είναι μια κλήση συστήματος; Λοιπόν, από όσα ειπώθηκαν παραπάνω, γίνεται σαφές ότι μια κλήση συστήματος είναι μια υπορουτίνα πυρήνα που έχει την αντίστοιχη εμφάνιση. Όσοι είχαν εμπειρία με προγραμματισμό Win9x / DOS θα θυμούνται πιθανώς την διακοπή int 0x21 με όλες (ή τουλάχιστον μερικές) από τις πολλές λειτουργίες της. Ωστόσο, υπάρχει μια μικρή ιδιαιτερότητα που αφορά όλες τις κλήσεις συστήματος Unix. Κατά συνθήκη, η συνάρτηση που υλοποιεί την κλήση συστήματος μπορεί να λάβει ορίσματα Ν ή καθόλου, αλλά με τον ένα ή τον άλλο τρόπο, η συνάρτηση πρέπει να επιστρέψει μια τιμή int. Οποιαδήποτε μη αρνητική τιμή ερμηνεύεται ως η επιτυχής εκτέλεση της συνάρτησης κλήσης συστήματος, και επομένως η ίδια η κλήση συστήματος. Μια τιμή μικρότερη από μηδέν είναι σημάδι σφάλματος και ταυτόχρονα περιέχει κωδικό σφάλματος (οι κωδικοί σφάλματος ορίζονται στις κεφαλίδες include / asm-generic / errno-base.h και περιλαμβάνουν / asm-generic / errno.h κεφαλίδες) Το Στο Linux, η πύλη για κλήσεις συστήματος μέχρι πρόσφατα ήταν η διακοπή int 0x80, ενώ στα Windows (έως XP Service Pack 2, αν δεν κάνω λάθος) μια τέτοια πύλη είναι η διακοπή 0x2e. Και πάλι, στον πυρήνα του Linux, μέχρι πρόσφατα, όλες οι κλήσεις συστήματος χειρίζονταν από τη λειτουργία system_call (). Ωστόσο, όπως αποδείχθηκε αργότερα, ο κλασικός μηχανισμός επεξεργασίας κλήσεων συστήματος μέσω της πύλης 0x80 οδηγεί σε σημαντική πτώση της απόδοσης στους επεξεργαστές Intel Pentium 4. Επομένως, ο κλασικός μηχανισμός αντικαταστάθηκε από τη μέθοδο των εικονικών δυναμικών κοινόχρηστων αντικειμένων (DSO - δυναμικό κοινόχρηστο αρχείο αντικειμένου. Δεν μπορώ να εγγυηθώ για τη σωστή μετάφραση, αλλά το DSO είναι αυτό που οι χρήστες των Windows γνωρίζουν ως DLL - δυναμικά φορτωμένη και συνδεδεμένη βιβλιοθήκη) - VDSO. Ποια είναι η διαφορά μεταξύ της νέας και της κλασικής μεθόδου; Αρχικά, ας δούμε την κλασική μέθοδο που λειτουργεί μέσω της πύλης 0x80.

Ο κλασικός μηχανισμός χειρισμού κλήσεων συστήματος στο Linux.

Διακόπτει την αρχιτεκτονική x86.

Όπως αναφέρθηκε παραπάνω, η πύλη 0x80 (int 0x80) χρησιμοποιήθηκε προηγουμένως για την εξυπηρέτηση αιτημάτων από προσαρμοσμένες εφαρμογές. Η λειτουργία ενός συστήματος που βασίζεται στην αρχιτεκτονική IA-32 γίνεται με διακοπή (αυστηρά μιλώντας, αυτό ισχύει για όλα τα συστήματα που βασίζονται σε x86 γενικά). Όταν συμβεί ένα συμβάν (ένα νέο τσιμπούρι χρονομέτρου, κάποια δραστηριότητα σε μια συσκευή, σφάλματα - διαίρεση με το μηδέν κ.λπ.), δημιουργείται μια διακοπή. Η διακοπή ονομάζεται έτσι επειδή συνήθως διακόπτει την κανονική ροή εκτέλεσης κώδικα. Οι διακοπές συνήθως υποδιαιρούνται σε διακοπές υλικού και λογισμικού. Οι διακοπές υλικού είναι διακοπές που δημιουργούνται από το σύστημα και τις περιφερειακές συσκευές. Όταν υπάρχει ανάγκη μιας συσκευής να προσελκύσει την προσοχή του πυρήνα του λειτουργικού συστήματος, αυτή (η συσκευή) παράγει ένα σήμα στη γραμμή αιτήματος διακοπής (IRQ - Interrupt ReQuest line). Αυτό οδηγεί στο γεγονός ότι σε συγκεκριμένες εισόδους επεξεργαστή δημιουργείται ένα αντίστοιχο σήμα, βάσει του οποίου ο επεξεργαστής αποφασίζει να διακόψει την εκτέλεση της ροής εντολών και να μεταφέρει τον έλεγχο στον χειριστή διακοπών, ο οποίος ανακαλύπτει ήδη τι συνέβη και τι χρειάζεται να γίνει. Οι διακοπές υλικού είναι ασύγχρονες στη φύση τους. Αυτό σημαίνει ότι μπορεί να προκύψει διακοπή ανά πάσα στιγμή. Εκτός από τις περιφερειακές συσκευές, ο ίδιος ο επεξεργαστής μπορεί να δημιουργήσει διακοπές (ή, πιο συγκεκριμένα, εξαιρέσεις υλικού - για παράδειγμα, η ήδη αναφερθείσα διαίρεση με το μηδέν). Αυτό γίνεται προκειμένου να ειδοποιηθεί το λειτουργικό σύστημα σχετικά με την εμφάνιση μιας μη φυσιολογικής κατάστασης, έτσι ώστε το λειτουργικό σύστημα να μπορεί να λάβει κάποια μέτρα ως απάντηση στην εμφάνιση μιας τέτοιας κατάστασης. Μετά την επεξεργασία της διακοπής, ο επεξεργαστής επιστρέφει στην εκτέλεση του προγράμματος που έχει διακοπεί. Μια διακοπή μπορεί να ξεκινήσει από μια προσαρμοσμένη εφαρμογή. Αυτή η διακοπή ονομάζεται διακοπή λογισμικού. Οι διακοπές λογισμικού, σε αντίθεση με τις διακοπές υλικού, είναι σύγχρονες. Δηλαδή, όταν καλείται μια διακοπή, ο κωδικός που την κάλεσε αναστέλλεται έως ότου εξυπηρετηθεί η διακοπή. Κατά την έξοδο από το χειριστή διακοπών, επανέρχεται η απομακρυσμένη διεύθυνση που αποθηκεύτηκε νωρίτερα (όταν καλείτε μια διακοπή) στη στοίβα, στην επόμενη εντολή μετά την εντολή που καλεί τη διακοπή (int). Ο χειριστής διακοπών είναι ένα κομμάτι κώδικα κάτοικος (που μένει στη μνήμη). Αυτό είναι συνήθως ένα μικρό πρόγραμμα. Αν και, αν μιλάμε για τον πυρήνα του Linux, τότε ο χειριστής διακοπών δεν είναι πάντα τόσο μικρός. Ο χειριστής διακοπών ορίζεται από ένα διάνυσμα. Ένα διάνυσμα δεν είναι παρά η διεύθυνση (τμήμα και μετατόπιση) της αρχής του κώδικα που θα πρέπει να χειρίζεται διακοπές με το δεδομένο ευρετήριο. Η εργασία με διακοπές διαφέρει σημαντικά στην πραγματική λειτουργία και την προστατευμένη λειτουργία του επεξεργαστή (επιτρέψτε μου να σας υπενθυμίσω ότι στο εξής εννοούμε τους επεξεργαστές Intel και αυτούς που είναι συμβατοί με αυτές). Στον πραγματικό (απροστάτευτο) τρόπο λειτουργίας του επεξεργαστή, οι χειριστές διακοπών ορίζονται από τα διανύσματά τους, τα οποία αποθηκεύονται πάντα στην αρχή της μνήμης, η επιθυμητή διεύθυνση παραλαμβάνεται από τον διανυσματικό πίνακα από το ευρετήριο, που είναι και ο αριθμός διακοπής. Ξαναγράφοντας το διάνυσμα με ένα συγκεκριμένο ευρετήριο, μπορείτε να αντιστοιχίσετε τον δικό σας χειριστή στη διακοπή.

Σε προστατευμένη λειτουργία, οι χειριστές διακοπών (πύλες, πύλες ή πύλες) δεν ορίζονται πλέον χρησιμοποιώντας έναν διανυσματικό πίνακα. Αντί αυτού του πίνακα, χρησιμοποιείται ένας πίνακας πύλης ή, πιο σωστά, ένας πίνακας διακοπών - IDT (Interrupt Descriptors Table). Αυτός ο πίνακας σχηματίζεται από τον πυρήνα και η διεύθυνσή του αποθηκεύεται στον καταχωρητή idtr του επεξεργαστή. Αυτό το μητρώο δεν είναι άμεσα προσβάσιμο. Μπορείτε να εργαστείτε μόνο με αυτό χρησιμοποιώντας τις οδηγίες lidt / sidt. Το πρώτο από αυτά (lidt) φορτώνει την τιμή που καθορίζεται στον τελεστή στον καταχωρητή idtr και είναι η βασική διεύθυνση του πίνακα περιγραφής διακοπής, το δεύτερο (sidt) αποθηκεύει τη διεύθυνση του πίνακα που βρίσκεται στο idtr στον καθορισμένο τελεστέο. Με τον ίδιο τρόπο που γίνεται η επιλογή πληροφοριών σχετικά με το τμήμα από τον πίνακα περιγραφής από τον επιλογέα, εμφανίζεται επίσης η επιλογή του περιγραφέα τμήματος που εξυπηρετεί τη διακοπή στην προστατευμένη λειτουργία. Η προστασία μνήμης υποστηρίζεται από επεξεργαστές Intel ξεκινώντας από τον επεξεργαστή i80286 (όχι με τον τρόπο που παρουσιάζεται τώρα, μόνο και μόνο επειδή ο 286 ήταν επεξεργαστής 16 -bit - επομένως το Linux δεν μπορεί να τρέξει σε αυτούς τους επεξεργαστές) και το i80386, και επομένως τον επεξεργαστή κάνει όλες τις απαραίτητες επιλογές και, ως εκ τούτου, δεν θα μπούμε σε όλες τις λεπτές λεπτομέρειες της προστατευμένης λειτουργίας (δηλαδή, το Linux λειτουργεί σε προστατευμένη λειτουργία). Δυστυχώς, ούτε ο χρόνος ούτε οι ευκαιρίες μας επιτρέπουν να σταθούμε στον μηχανισμό χειρισμού διακοπών σε προστατευμένη λειτουργία για μεγάλο χρονικό διάστημα. Και αυτός δεν ήταν ο στόχος κατά τη συγγραφή αυτού του άρθρου. Όλες οι πληροφορίες που δίνονται εδώ σχετικά με τη λειτουργία της οικογένειας των επεξεργαστών x86 είναι μάλλον επιφανειακές και παρέχονται μόνο για να κατανοήσουμε λίγο καλύτερα τον μηχανισμό των κλήσεων του συστήματος πυρήνα. Κάτι μπορεί να μάθει απευθείας από τον κώδικα του πυρήνα, αν και, για πλήρη κατανόηση του τι συμβαίνει, είναι ακόμα σκόπιμο να εξοικειωθείτε με τις αρχές της προστατευμένης λειτουργίας. Το κομμάτι κώδικα που συμπληρώνει τις αρχικές τιμές (αλλά δεν εγκαθιστά!) Το IDT βρίσκεται στο arch / i386 / kernel / head.S: / * * setup_idt * * ρυθμίζει ένα idt με 256 καταχωρήσεις που δείχνουν το * ignore_int, interrupt gates. Στην πραγματικότητα δεν φορτώνεται * idt - αυτό μπορεί να γίνει μόνο αφού έχει ενεργοποιηθεί η σελιδοποίηση * και ο πυρήνας μεταφερθεί στο PAGE_OFFSET. Οι διακοπές * είναι ενεργοποιημένες αλλού, όταν μπορούμε να είμαστε σχετικά * σίγουροι ότι όλα είναι εντάξει. * * Προειδοποίηση:% esi είναι ζωντανή σε αυτήν τη λειτουργία. * / 1.setup_idt: 2.lea ignore_int,% edx 3.movl $ (__ KERNEL_CS<< 16),%eax 4. movw %dx,%ax /* selector = 0x0010 = cs */ 5. movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ 6. lea idt_table,%edi 7. mov $256,%ecx 8.rp_sidt: 9. movl %eax,(%edi) 10. movl %edx,4(%edi) 11. addl $8,%edi 12. dec %ecx 13. jne rp_sidt 14..macro set_early_handler handler,trapno 15. lea \handler,%edx 16. movl $(__KERNEL_CS << 16),%eax 17. movw %dx,%ax 18. movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ 19. lea idt_table,%edi 20. movl %eax,8*\trapno(%edi) 21. movl %edx,8*\trapno+4(%edi) 22..endm 23. set_early_handler handler=early_divide_err,trapno=0 24. set_early_handler handler=early_illegal_opcode,trapno=6 25. set_early_handler handler=early_protection_fault,trapno=13 26. set_early_handler handler=early_page_fault,trapno=14 28. ret Λίγες σημειώσεις σχετικά με τον κώδικα: ο δεδομένος κώδικας είναι γραμμένος σε ένα είδος συναρμολογητή AT&T, οπότε η γνώση σας για τον συναρμολογητή στη συνήθη συμβολική του σημειογραφία Intel μπορεί να προκαλέσει σύγχυση. Η πιο βασική διαφορά είναι στη σειρά των τελεστών. Εάν η παραγγελία έχει οριστεί για σημειογραφία Intel - "συσσωρευτής"< "источник", то для ассемблера AT&T порядок прямой. Регистры процессора, как правило, должны иметь префикс "%", непосредственные значения (константы) префиксируются символом доллара "$". Синтаксис AT&T традиционно используется в Un*x-системах.

Στο παραπάνω παράδειγμα, οι γραμμές 2-4 ορίζουν τη διεύθυνση του προεπιλεγμένου χειριστή για όλες τις διακοπές. Ο προεπιλεγμένος χειριστής είναι ignore_int, το οποίο δεν κάνει τίποτα. Η παρουσία ενός τέτοιου στελέχους είναι απαραίτητη για τη σωστή επεξεργασία όλων των διακοπών σε αυτό το στάδιο, καθώς απλώς δεν υπάρχουν άλλες ακόμα (ωστόσο, οι παγίδες έχουν ρυθμιστεί λίγο χαμηλότερα στον κώδικα - ανατρέξτε στο Intel Architecture Manual Reference για παγίδες ή κάτι παρόμοιο , δεν θα είμαστε εδώ αγγίξτε τις παγίδες). Η γραμμή 5 ορίζει τον τύπο της βαλβίδας. Στη γραμμή 6, φορτώνουμε τη διεύθυνση του πίνακα IDT στον καταχωρητή ευρετηρίου. Ο πίνακας πρέπει να περιέχει 255 καταχωρήσεις, 8 bytes η κάθε μία. Στις γραμμές 8-13, γεμίζουμε ολόκληρο τον πίνακα με τις ίδιες τιμές που ορίστηκαν νωρίτερα στους καταχωρητές eax και edx - δηλαδή, πρόκειται για μια πύλη διακοπής που αναφέρεται στον χειριστή ignore_int. Παρακάτω ορίζουμε μια μακροεντολή για τη ρύθμιση παγίδων - γραμμές 14-22. Στις γραμμές 23-26, χρησιμοποιώντας την προαναφερθείσα μακροεντολή, θέσαμε παγίδες για τις ακόλουθες εξαιρέσεις: αποτυχία μετάφρασης σελίδας (14) ... Στην παρένθεση ο αριθμός των "διακοπών" δημιουργείται όταν προκύψει η αντίστοιχη μη φυσιολογική κατάσταση. Πριν ελέγξετε τον τύπο επεξεργαστή στο arch / i386 / kernel / head. S, το IDT ορίζεται καλώντας το setup_idt: / * * εκκίνηση συστήματος εγκατάστασης 32-bit. Πρέπει να επαναλάβουμε μερικά από τα πράγματα που έγιναν * σε λειτουργία 16-bit για τις "πραγματικές" λειτουργίες. * / 1.call setup_idt ... 2.call check_x87 3.lgdt early_gdt_descr 4.lidt idt_descrΑφού ανακαλύψουμε τον τύπο (συν) επεξεργαστή και πραγματοποιήσουμε όλα τα προπαρασκευαστικά βήματα στις γραμμές 3 και 4, φορτώνουμε τους πίνακες GDT και IDT, οι οποίοι θα χρησιμοποιηθούν στα αρχικά στάδια του πυρήνα.

Κλήσεις συστήματος και int 0x80.

Ας επιστρέψουμε από τις διακοπές στις κλήσεις συστήματος. Τι χρειάζεται λοιπόν για την εξυπηρέτηση μιας διαδικασίας που ζητά μια υπηρεσία; Πρώτον, πρέπει να μετακινηθείτε από το δακτύλιο 3 (επίπεδο προνομίων CPL = 3) στο πιο προνομιακό επίπεδο 0 (Δακτύλιος 0, CPL = 0), επειδή ο κώδικας πυρήνα βρίσκεται στο τμήμα με τα υψηλότερα προνόμια. Επιπλέον, απαιτείται κωδικός χειριστή για την εξυπηρέτηση της διαδικασίας. Για αυτό ακριβώς χρησιμοποιείται η πύλη 0x80. Αν και υπάρχουν αρκετές κλήσεις συστήματος, όλες χρησιμοποιούν ένα μόνο σημείο εισόδου - int 0x80. Ο ίδιος ο χειριστής εγκαθίσταται όταν καλείτε τη λειτουργία arch / i386 / kernel / traps.c :: trap_init (): void __init trap_init (void) (... set_system_gate (SYSCALL_VECTOR, & system_call); ...)Αυτή η γραμμή μας ενδιαφέρει περισσότερο στο trap_init (). Στο ίδιο αρχείο παραπάνω, μπορείτε να δείτε τον κώδικα της συνάρτησης set_system_gate (): στατικό κενό __init set_system_gate (χωρίς υπογραφή int n, void * addr) (_set_gate (n, DESCTYPE_TRAP | DESCTYPE_DPL3, addr, __KERNEL_CS);)Εδώ μπορείτε να δείτε ότι η πύλη για τη διακοπή 0x80 (δηλαδή, αυτή η τιμή ορίζεται από τη μακροεντολή SYSCALL_VECTOR - μπορείτε να πιστέψετε τη λέξη :)) έχει οριστεί ως παγίδα με το επίπεδο δικαιωμάτων DPL = 3 (Δακτύλιος 3), δηλ. αυτή η διακοπή θα εντοπιστεί όταν κληθεί από το χώρο χρήστη. Το πρόβλημα με τη μετάβαση από το δαχτυλίδι 3 στο δαχτυλίδι 0 έτσι. λύθηκε. Η συνάρτηση _set_gate () ορίζεται στο αρχείο κεφαλίδας περιλαμβάνει / asm-i386 / desc.h. Για όσους είναι ιδιαίτερα περίεργοι, παρακάτω είναι ο κώδικας, χωρίς μακροσκελείς εξηγήσεις, ωστόσο: στατική inline void _set_gate (int gate, unsigned int type, void * addr, unsigned short seg) (__u32 a, b; pack_gate (& a, & b, (unsigned long) addr, seg, type, 0); write_idt_entry (idt_table , πύλη, α, β);)Ας επιστρέψουμε στη συνάρτηση trap_init (). Καλείται από τη συνάρτηση start_kernel () στο init / main.c. Αν κοιτάξετε τον κώδικα trap_init (), μπορείτε να δείτε ότι αυτή η συνάρτηση ξαναγράφει μερικές από τις τιμές του πίνακα IDT - τους χειριστές που χρησιμοποιήθηκαν στα πρώτα στάδια της προετοιμασίας του πυρήνα (early_page_fault, early_divide_err, early_illegal_opcode, early_protection_fault) αντικαθίστανται με αυτά που θα χρησιμοποιηθούν ήδη στη διαδικασία διεργασίας του πυρήνα. Έτσι, σχεδόν φτάσαμε στο σημείο και γνωρίζουμε ήδη ότι όλες οι κλήσεις συστήματος υποβάλλονται σε επεξεργασία με τον ίδιο τρόπο - μέσω της πύλης int 0x80. Η λειτουργία system_call () έχει οριστεί ως χειριστής για int 0x80, όπως μπορείτε να δείτε από το παραπάνω κομμάτι του κώδικα arch / i386 / kernel / traps.c :: trap_init ().

system_call ().

Ο κωδικός συνάρτησης system_call () βρίσκεται στο arch / i386 / kernel / entry.S και μοιάζει με αυτό: # στέλεχος χειριστή κλήσεων συστήματος ENTRY (system_call) RING0_INT_FRAME # δεν μπορεί να χαλαρώσει στον χώρο του χρήστη ούτως ή άλλως pushl% eax # save orig_eax CFI_ADJUST_CFA_OFFSET 4 SAVE_ALL GET_THREAD_INFO (% ebp) # εντοπισμός κλήσεων συστήματος χρειάζεται testw * και όχι testb * / testw $ (_ TIF_SYSCALL_EMU | _TIF_SYSCALL_TRACE | _TIF_SECCOMP | _TIF_SYSCALL_AUDIT), TI_flags (% ebp) jnz syscall_trace_entry cmpl $%) ΤοΟ κωδικός δεν εμφανίζεται πλήρως. Όπως μπορείτε να δείτε, πρώτα, το system_call () ρυθμίζει τη στοίβα για να λειτουργεί στο Ring 0, αποθηκεύει την τιμή που της μεταβιβάζεται μέσω eax στη στοίβα, αποθηκεύει όλους τους καταχωρητές επίσης στη στοίβα, λαμβάνει δεδομένα σχετικά με το νήμα κλήσης και ελέγχει εάν η μεταβιβαζόμενη τιμή, ο αριθμός κλήσης συστήματος, υπερβαίνει τα όρια συστημάτων κλήσης και, στη συνέχεια, χρησιμοποιώντας την τιμή που μεταβιβάστηκε στο eax ως όρισμα, το system_call () μεταβαίνει στον πραγματικό χειριστή συστηματικής κλήσης με βάση την καταχώριση πίνακα που αναφέρεται από το ευρετήριο στο eax. Τώρα θυμηθείτε τον παλιό καλό διανυσματικό πίνακα διακοπής πραγματικής λειτουργίας. Δεν μοιάζει με τίποτα; Στην πραγματικότητα, φυσικά, όλα είναι κάπως πιο περίπλοκα. Συγκεκριμένα, η κλήση συστήματος πρέπει να αντιγράψει τα αποτελέσματα από τη στοίβα πυρήνα στη στοίβα χρήστη, να περάσει τον κωδικό επιστροφής και κάποια άλλα πράγματα. Σε περίπτωση που το όρισμα που καθορίζεται στο eax δεν αναφέρεται σε υπάρχουσα κλήση συστήματος (η τιμή είναι εκτός εμβέλειας), συμβαίνει ένα άλμα στην ετικέτα syscall_badsys. Εδώ, η τιμή -ENOSYS ωθείται στη στοίβα στην αντιστάθμιση στην οποία πρέπει να βρίσκεται η τιμή eax - η κλήση συστήματος δεν υλοποιείται. Αυτό ολοκληρώνει την εκτέλεση του system_call ().

Ο πίνακας κλήσεων συστήματος βρίσκεται στο αρχείο arch / i386 / kernel / syscall_table.S και έχει μια αρκετά απλή μορφή: ENTRY (sys_call_table) .long sys_restart_syscall / * 0 - παλιά κλήση συστήματος "setup ()", που χρησιμοποιείται για επανεκκίνηση * / .long sys_exit .long sys_fork .long sys_read .long sys_write .long sys_open / * long sys_creat ...Με άλλα λόγια, ολόκληρος ο πίνακας δεν είναι παρά μια σειρά διευθύνσεων συνάρτησης, διατεταγμένων με τη σειρά των αριθμών κλήσεων συστήματος που εξυπηρετούν αυτές οι συναρτήσεις. Ο πίνακας είναι ένας συνηθισμένος πίνακας διπλών λέξεων (ή λέξεων 32 -bit - όποια προτιμάτε). Ο κώδικας για ορισμένες από τις λειτουργίες που εξυπηρετούν κλήσεις συστήματος βρίσκεται στο τμήμα που εξαρτάται από την πλατφόρμα-arch / i386 / kernel / sys_i386.c και το τμήμα ανεξάρτητο από την πλατφόρμα βρίσκεται στον πυρήνα / sys.c.

Αυτό συμβαίνει με τις κλήσεις συστήματος και την πύλη 0x80.

Νέος μηχανισμός χειρισμού κλήσεων συστήματος στο Linux. sysenter / sysexit.

Όπως αναφέρθηκε, έγινε γρήγορα σαφές ότι ο παραδοσιακός τρόπος χειρισμού κλήσεων συστήματος που βασίζεται στην πύλη 0x80 οδηγεί σε απώλεια απόδοσης στους επεξεργαστές Intel Pentium 4. Επομένως, ο Linus Torvalds εφάρμοσε έναν νέο μηχανισμό στον πυρήνα που βασίζεται σε οδηγίες sysenter / sysexit για βελτίωση απόδοση πυρήνα σε μηχανές εξοπλισμένες με επεξεργαστή Pentium II ή υψηλότερο (με το Pentium II + οι επεξεργαστές Intel υποστηρίζουν τις προαναφερθείσες οδηγίες sysenter / sysexit). Ποια είναι η ουσία του νέου μηχανισμού; Παραδόξως, η ουσία παραμένει η ίδια. Η εκτέλεση έχει αλλάξει. Σύμφωνα με την τεκμηρίωση της Intel, η εντολή sysenter αποτελεί μέρος του μηχανισμού "γρήγορες κλήσεις συστήματος". Συγκεκριμένα, αυτή η οδηγία είναι βελτιστοποιημένη για γρήγορη μετάβαση από το ένα επίπεδο προνομίων στο άλλο. Πιο συγκεκριμένα, επιταχύνει τη μετάβαση στο δακτύλιο 0 (Δακτύλιος 0, CPL = 0). Με αυτόν τον τρόπο, το λειτουργικό σύστημα πρέπει να προετοιμάσει τον επεξεργαστή για να χρησιμοποιήσει τις οδηγίες του συστήματος. Αυτή η ρύθμιση πραγματοποιείται μία φορά κατά τη φόρτωση και την προετοιμασία του πυρήνα του λειτουργικού συστήματος. Όταν καλείται sysenter, ορίζει τους καταχωρητές επεξεργαστή σύμφωνα με τους μητρώους που εξαρτώνται από το μηχάνημα που είχαν οριστεί προηγουμένως από το λειτουργικό σύστημα. Συγκεκριμένα, έχει οριστεί ο καταχωρητής τμημάτων και ο καταχωρητής δείκτη εντολών - cs: eip, καθώς και το τμήμα στοίβας και το επάνω μέρος των δεικτών στοίβας, esp. Η μετάβαση σε ένα νέο τμήμα του κώδικα και η μετατόπιση πραγματοποιείται από το δακτύλιο 3 στο 0.

Το Sysexit κάνει το αντίθετο. Πραγματοποιεί μια γρήγορη μετάβαση από το επίπεδο προνομίων 0 στο επίπεδο 3 (CPL = 3). Σε αυτήν την περίπτωση, ο καταχωρητής του τμήματος κώδικα ορίζεται σε 16 + την τιμή του τμήματος cs, αποθηκευμένο στο μητρώο του επεξεργαστή που εξαρτάται από τη μηχανή. Ο καταχωρητής eip περιέχει το περιεχόμενο του καταχωρητή edx. Στο ss, εισάγεται το άθροισμα των 24 και οι τιμές των cs, τις οποίες το λειτουργικό σύστημα είχε εισαγάγει προηγουμένως στο μητρώο του επεξεργαστή που εξαρτάται από τη μηχανή κατά την προετοιμασία του πλαισίου για να λειτουργήσει η εντολή sysenter. Η Esp αποθηκεύει το περιεχόμενο του μητρώου ecx. Οι τιμές που απαιτούνται για να λειτουργούν οι οδηγίες sysenter / sysexit αποθηκεύονται στις ακόλουθες διευθύνσεις:

  1. SYSENTER_CS_MSR 0x174 - τμήμα κώδικα όπου γράφεται η τιμή του τμήματος στο οποίο βρίσκεται ο κωδικός χειριστή κλήσεων συστήματος.
  2. SYSENTER_ESP_MSR 0x175 - δείκτης στο επάνω μέρος της στοίβας για το χειριστή κλήσεων συστήματος.
  3. SYSENTER_EIP_MSR 0x176 - δείκτης μετατόπισης στο τμήμα κώδικα. Υποδεικνύει την έναρξη του κωδικού χειριστή κλήσεων συστήματος.
Αυτές οι διευθύνσεις αναφέρονται σε καταχωρητές που εξαρτώνται από το μοντέλο και δεν έχουν ονόματα. Οι τιμές γράφονται σε καταχωρητές που εξαρτώνται από το μοντέλο χρησιμοποιώντας την εντολή wrmsr, ενώ το edx: eax πρέπει να περιέχει το πάνω και το κάτω μέρος μιας λέξης μηχανής 64-bit, αντίστοιχα, και το ecx πρέπει να περιέχει τη διεύθυνση του καταχωρητή στον οποίο θα γραφτεί να γίνει. Στο Linux, οι διευθύνσεις των μητρώων που εξαρτώνται από το μοντέλο ορίζονται στο αρχείο κεφαλίδας include / asm-i368 / msr-index.h ως εξής (πριν από την έκδοση 2.6.22, τουλάχιστον ορίστηκαν στο include / asm-i386 / msr .h header file, επιτρέψτε μου να σας υπενθυμίσω ότι εξετάζουμε τον μηχανισμό κλήσεων συστήματος χρησιμοποιώντας το παράδειγμα του πυρήνα Linux 2.6.22): #define MSR_IA32_SYSENTER_CS 0x00000174 #define MSR_IA32_SYSENTER_ESP 0x00000175 #define MSR_IA32_SYSENTER_EIP 0x00000176Ο κώδικας πυρήνα που είναι υπεύθυνος για τη ρύθμιση των καταχωρητών που εξαρτώνται από το μοντέλο βρίσκεται στο arch / i386 / sysenter.c και μοιάζει με αυτό: 1.απόκλειση enable_sep_cpu (άκυρο) (2.int cpu = get_cpu (); 3.struct tss_struct * tss = & per_cpu (init_tss, cpu); 4.if (! Boot_cpu_has (X86_FEATURE_SEP)) (5.put_cpu (); . επιστροφή;) 7.tss-> x86_tss.ss1 = __KERNEL_CS; 8.tss-> x86_tss.esp1 = sizeof (struct tss_struct) + (χωρίς υπογραφή μακρύ) tss; 9.wrmsr (MSR_IA32_SYSENTER_CS, __KERNEL_ wCrms, MSR_IA32_SYSENTER_ESP, tss-> x86_tss.esp1, 0).Εδώ στη μεταβλητή tss λαμβάνουμε τη διεύθυνση της δομής που περιγράφει το τμήμα της κατάστασης εργασίας. Το TSS (Τμήμα κατάστασης εργασιών) χρησιμοποιείται για να περιγράψει το πλαίσιο μιας εργασίας και αποτελεί μέρος του μηχανισμού πολλαπλών εργασιών υλικού για την αρχιτεκτονική x86. Ωστόσο, το Linux πρακτικά δεν χρησιμοποιεί εναλλαγή περιβάλλοντος εργασίας υλικού. Σύμφωνα με την τεκμηρίωση της Intel, η μετάβαση σε άλλη εργασία γίνεται είτε με εκτέλεση μιας εντολής άλματος διατομής (jmp ή κλήση) που αναφέρεται στο τμήμα TSS, είτε με τον περιγραφέα πύλης εργασιών στο GDT (LDT). Ένας ειδικός καταχωρητής επεξεργαστή αόρατος στον προγραμματιστή - TR (Task Register) περιέχει έναν επιλογέα περιγραφής εργασιών. Η φόρτωση αυτού του μητρώου φορτώνει επίσης τους μη ορατούς από το λογισμικό μητρώους βάσης και ορίου που σχετίζονται με το TR.

Παρόλο που το Linux δεν χρησιμοποιεί εναλλαγή περιβάλλοντος εργασιών υλικού, ο πυρήνας αναγκάζεται να παραμερίσει μια καταχώριση TSS για κάθε επεξεργαστή εγκατεστημένο στο σύστημα. Αυτό συμβαίνει επειδή όταν ο επεξεργαστής μεταβαίνει από τη λειτουργία χρήστη στη λειτουργία πυρήνα, μεταφέρει τη διεύθυνση στοίβας πυρήνα από το TSS. Επιπλέον, απαιτείται TSS για τον έλεγχο της πρόσβασης στις θύρες εισόδου / εξόδου. Το TSS περιέχει έναν χάρτη των δικαιωμάτων πρόσβασης στη θύρα. Με βάση αυτόν τον χάρτη, καθίσταται δυνατός ο έλεγχος της πρόσβασης στη θύρα για κάθε διαδικασία χρησιμοποιώντας οδηγίες εισόδου / εξόδου. Εδώ tss-> x86_tss.esp1 δείχνει τη στοίβα πυρήνα. __KERNEL_CS δείχνει φυσικά ένα τμήμα κώδικα πυρήνα. Η διεύθυνση της συνάρτησης sysenter_entry () ορίζεται ως το offset-eip.

Η συνάρτηση sysenter_entry () ορίζεται στο arch / i386 / kernel / entry.S και μοιάζει με αυτό: / * SYSENTER_RETURN δείχνει μετά την οδηγία "sysenter" στη σελίδα vsyscall. Δείτε vsyscall-sysentry.S, το οποίο ορίζει το σύμβολο. * / # sysenter στέλεχος χειριστή κλήσης ENTRY (sysenter_entry) CFI_STARTPROC απλό CFI_SIGNAL_FRAME CFI_DEF_CFA esp, 0 CFI_REGISTER esp, ebp movl TSS_sysenter_esp0 (% esp),% esp esp * / ENABLE_INTERRUPTS (CLBR_NONE) pushl $ (__ USER_DS) CFI_ADJUST_CFA_OFFSET 4 / * CFI_REL_OFFSET ss, 0 * / pushl% ΕΒΡ CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET esp, 0 pushfl CFI_ADJUST_CFA_OFFSET 4 pushl $ (__ USER_CS) CFI_ADJUST_CFA_OFFSET 4 / * CFI_REL_OFFSET cs, 0 * / / * * Push current_thread_info () -> sysenter_return στη στοίβα. * Είναι απαραίτητη μια μικρή διόρθωση offset - 4 * 4 σημαίνει τις 4 λέξεις * που πιέστηκαν παραπάνω. Το +8 αντιστοιχεί στη ρύθμιση esp0 του copy_thread. * / Pushl (TI_sysenter_return-THREAD_SIZE + 8 + 4 * 4) (% esp) CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET eip, 0 / * * Φορτώστε το πιθανό έκτο επιχείρημα ασφαλείας . * / cmpl $ __ PAGE_OFFSET-3,% ebp jae syscall_fault 1: movl (% ebp),% ebp. τμήμα Το _TIF_SECCOMP είναι bit αριθμός 8, και επομένως χρειάζεται testw και όχι testb * / testw $ (_ TIF_SYSCALL_EMU | _TIF_SYSCALL_TRACE | _TIF_SECCOMP | _TIF_SYSCALL_AUDIT), TI_flags (% jyscalls_cmall_xalls,% esp) DISABLE_INTERRUPTS (CLBR_ANY) TRACE_IRQS_OFF movl TI_flags (% ebp),% ecx testw $ _TIF_ALLWORK_MASK,% cx_existers modysit s απενεργοποίηση sysexit * / movl PT_EP%% esp%, esp% ebp,% ebp TRACE_IRQS_ON 1: mov PT_FS (% esp),% fs ENABLE_INTERRUPTS_SYSEXIT CFI_ENDPROC .pushsection .fixup, "ax" 2: movl $ 0, PT_FS (% esp) jmp 1b .table t 2b .popsection ENDPROC (sysenter_entry)Όπως και με το system_call (), το μεγαλύτερο μέρος της εργασίας γίνεται στη γραμμή κλήσης * sys_call_table (,% eax, 4). Εδώ καλείται ο συγκεκριμένος χειριστής κλήσεων συστήματος. Έτσι, είναι σαφές ότι λίγα έχουν αλλάξει ριζικά. Το γεγονός ότι το διάνυσμα διακοπής είναι πλέον σφυρηλατημένο στο υλικό και ο επεξεργαστής μας βοηθά να μεταβούμε γρήγορα από ένα επίπεδο προνομίων σε άλλο αλλάζει μόνο μερικές από τις λεπτομέρειες εκτέλεσης με το ίδιο περιεχόμενο. Ωστόσο, οι αλλαγές δεν τελειώνουν εκεί. Θυμηθείτε από πού ξεκίνησε η ιστορία. Στην αρχή, ανέφερα ήδη για εικονικά κοινόχρηστα αντικείμενα. Έτσι, αν νωρίτερα η εφαρμογή μιας κλήσης συστήματος, ας πούμε, από τη βιβλιοθήκη συστήματος το libc έμοιαζε με κλήση διακοπής (παρά το γεγονός ότι η βιβλιοθήκη ανέλαβε ορισμένες λειτουργίες για τη μείωση του αριθμού των διακοπτών περιβάλλοντος), τώρα χάρη στο VDSO η κλήση συστήματος μπορεί να γίνει σχεδόν άμεσα, χωρίς libc. Θα μπορούσε προηγουμένως να εφαρμοστεί άμεσα, ξανά, ως διακοπή. Αλλά τώρα η κλήση μπορεί να ζητηθεί ως μια κανονική λειτουργία που εξάγεται από μια βιβλιοθήκη δυναμικά συνδεδεμένης (DSO). Κατά την εκκίνηση, ο πυρήνας καθορίζει ποιος μηχανισμός πρέπει και μπορεί να χρησιμοποιηθεί για μια δεδομένη πλατφόρμα. Ανάλογα με τις περιστάσεις, ο πυρήνας θέτει ένα σημείο εισόδου στη συνάρτηση που πραγματοποιεί την κλήση του συστήματος. Στη συνέχεια, η λειτουργία εξάγεται στο χώρο χρήστη ως βιβλιοθήκη linux-gate.so.1. Η βιβλιοθήκη linux-gate.so.1 δεν υπάρχει φυσικά στο δίσκο. Αυτό, για να το πούμε, μιμείται τον πυρήνα και υπάρχει ακριβώς όσο λειτουργεί το σύστημα. Εάν κλείσετε το σύστημα, τοποθετήσετε το ριζικό FS από άλλο σύστημα, τότε δεν θα βρείτε αυτό το αρχείο στη ρίζα FS του σταματημένου συστήματος. Στην πραγματικότητα, δεν θα μπορείτε να το βρείτε ακόμη και σε τρέχον σύστημα. Φυσικά, απλά δεν υπάρχει. Αυτός είναι ο λόγος για τον οποίο το linux -gate.so.1 είναι κάτι διαφορετικό από το VDSO - δηλ. Εικονικό δυναμικά κοινόχρηστο αντικείμενο. Ο πυρήνας χαρτογραφεί τη δυναμικά εξομοιωμένη δυναμική βιβλιοθήκη στο χώρο διευθύνσεων κάθε διαδικασίας. Είναι εύκολο να το επαληθεύσετε εάν εκτελέσετε την ακόλουθη εντολή: [προστασία ηλεκτρονικού ταχυδρομείου]: ~ $ cat / proc / self / maps 08048000-0804c000 r-xp 00000000 08:01 46 / bin / cat 0804c000-0804d000 rw-p 00003000 08:01 46 / bin / cat 0804d000-0806e000 rw-p 0804d000 00:00 0 ... b7fdf000-b7fe1000 rw-p 00019000 08:01 2066 /lib/ld-2.5.so bffd2000-bffe8000 rw-p bffd2000 00:00 0 ffffe000-fffff000 r-xp 00000000 00:00 0Εδώ η τελευταία γραμμή είναι το αντικείμενο που μας ενδιαφέρει: ffffe000-fffff000 r-xp 00000000 00:00 0Από το δοθέν παράδειγμα, φαίνεται ότι το αντικείμενο καταλαμβάνει ακριβώς μία σελίδα στη μνήμη - 4096 byte, πρακτικά στις αυλές του χώρου διευθύνσεων. Ας κάνουμε ένα ακόμη πείραμα: [προστασία ηλεκτρονικού ταχυδρομείου]: ~ $ ldd `which cat` linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e87000)/lib/ld-linux .so.2 (0xb7fdf000) [προστασία ηλεκτρονικού ταχυδρομείου]: ~ $ ldd `which gcc` linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e3c000)/lib/ld-linux .so.2 (0xb7f94000) [προστασία ηλεκτρονικού ταχυδρομείου]:~$ Εδώ πήραμε δύο εφαρμογές εκ των προτέρων. Μπορεί να φανεί ότι η βιβλιοθήκη είναι αντιστοιχισμένη στο χώρο διευθύνσεων διεργασίας στην ίδια σταθερή διεύθυνση - 0xffffe000. Τώρα ας προσπαθήσουμε να δούμε τι είναι πραγματικά αποθηκευμένο σε αυτήν τη σελίδα μνήμης ...

Μπορείτε να απορρίψετε τη σελίδα μνήμης όπου αποθηκεύεται ο κοινόχρηστος κωδικός VDSO χρησιμοποιώντας το ακόλουθο πρόγραμμα: #include #include #include int main () (char * vdso = 0xffffe000; char * buffer; FILE * f; buffer = malloc (4096); if (! buffer) exit (1); memcpy (buffer, vdso, 4096) εάν (! (f = fopen ("test.dump", "w + b"))) (δωρεάν (buffer); έξοδος (1);) fwrite (buffer, 4096, 1, f); fclose (f) ; δωρεάν (buffer); επιστροφή 0;)Αυστηρά μιλώντας, νωρίτερα αυτό θα μπορούσε να γίνει ευκολότερα χρησιμοποιώντας την εντολή dd if = / proc / self / mem of = test.dump bs = 4096 skip = 1048574 count = 1, αλλά οι πυρήνες από την έκδοση 2.6.22, ή ίσως και νωρίτερα, δεν χαρτογραφούν πλέον τη μνήμη διεργασίας στο / proc / `pid` / mem. Αυτό το αρχείο διατηρείται, προφανώς για λόγους συμβατότητας, αλλά δεν περιέχει περισσότερες πληροφορίες.

Ας μεταγλωττίσουμε και τρέξουμε το δεδομένο πρόγραμμα. Ας προσπαθήσουμε να αποσυναρμολογήσουμε τον κωδικό που προκύπτει: [προστασία ηλεκτρονικού ταχυδρομείου]: ~/tmp $ objdump --disassemble ./test.dump ./test.dump: μορφή αρχείου elf32-i386 Αποσυναρμολόγηση τμήματος .text: ffffe400<__kernel_vsyscall>: ffffe400: 51 push% ecx ffffe401: 52 push% edx ffffe402: 55 push% ebp ffffe403: 89 e5 mov% esp,% ebp ffffe405: 0f 34 sysenter ... ffffe40e: eb f3 jmp ffffe403<__kernel_vsyscall+0x3>ffffe410: 5d pop% ebp ffffe411: 5a pop% edx ffffe412: 59 pop% ecx ffffe413: c3 ret ... [προστασία ηλεκτρονικού ταχυδρομείου]: ~ / tmp $Εδώ είναι η πύλη μας για κλήσεις συστήματος, όλα με μια ματιά. Η διαδικασία (ή η βιβλιοθήκη συστήματος libc), που καλεί τη συνάρτηση __kernel_vsyscall, φτάνει στη διεύθυνση 0xffffe400 (στην περίπτωσή μας). Επιπλέον, το __kernel_vsyscall αποθηκεύει τα περιεχόμενα των καταχωρητών ecx, edx, ebp στη στοίβα της διαδικασίας χρήστη. Έχουμε ήδη μιλήσει για την εκχώρηση των μητρώων ecx και edx νωρίτερα, στο ebp χρησιμοποιείται αργότερα για την επαναφορά της στοίβας χρήστη. Η εντολή sysenter εκτελείται, "interrupt interception" και, κατά συνέπεια, η επόμενη μετάβαση στο sysenter_entry (βλ. Παραπάνω). Η εντολή jmp στο 0xffffe40e εισάγεται για επανεκκίνηση της κλήσης συστήματος με 6 ορίσματα (δείτε http://lkml.org/lkml/2002/12/18/). Ο κωδικός που τοποθετείται στη σελίδα βρίσκεται στο arch / i386 / kernel / vsyscall-enter.S (ή arch / i386 / kernel / vsyscall-int80.S για την παγίδα 0x80). Αν και διαπίστωσα ότι η διεύθυνση της συνάρτησης __kernel_vsyscall είναι σταθερή, πιστεύεται ότι αυτό δεν συμβαίνει. Συνήθως, η θέση του σημείου εισαγωγής στο __kernel_vsyscall () μπορεί να βρεθεί από το διάνυσμα ELF-auxv χρησιμοποιώντας την παράμετρο AT_SYSINFO. Το διάνυσμα ELF-auxv περιέχει πληροφορίες που μεταφέρονται στη διαδικασία μέσω της στοίβας κατά την εκκίνηση και περιέχει διάφορες πληροφορίες που απαιτούνται κατά την εκτέλεση του προγράμματος. Αυτό το διάνυσμα περιέχει, ειδικότερα, τις μεταβλητές περιβάλλοντος της διαδικασίας, ορίσματα κ.λπ.

Ακολουθεί ένα μικρό παράδειγμα C για το πώς μπορείτε να αποκτήσετε άμεση πρόσβαση στη συνάρτηση __kernel_vsyscall: #περιλαμβάνω int pid? int main () (__asm ​​("movl $ 20,% eax \ n" "call *% gs: 0x10 \ n" "movl% eax, pid \ n"); printf ("pid:% d \ n" , pid); επιστροφή 0;)Αυτό το παράδειγμα είναι παρμένο από τη σελίδα Manu Garg, http://www.manugarg.com. Έτσι, στο παραπάνω παράδειγμα, πραγματοποιούμε την κλήση συστήματος getpid () (αριθμός 20 ή αλλιώς __NR_getpid). Για να μην ανεβούμε στη στοίβα διεργασίας αναζητώντας τη μεταβλητή AT_SYSINFO, θα επωφεληθούμε από το γεγονός ότι η βιβλιοθήκη συστήματος libc.so κατά την εκκίνηση αντιγράφει την τιμή της μεταβλητής AT_SYSINFO στο μπλοκ ελέγχου νήματος (TCB - Block Control Block) Το Αυτό το μπλοκ πληροφοριών αναφέρεται συνήθως από έναν επιλογέα στο gs. Υποθέτουμε ότι η επιθυμητή παράμετρος βρίσκεται στην αντιστάθμιση 0x10 και πραγματοποιούμε μια κλήση στη διεύθυνση που είναι αποθηκευμένη σε% gs: $ 0x10.

Αποτελέσματα.

Στην πραγματικότητα, στην πράξη, δεν είναι πάντα δυνατό να επιτευχθεί ένα ειδικό κέρδος απόδοσης ακόμη και με την υποστήριξη του FSCF (Fast System Call Facility) σε αυτήν την πλατφόρμα. Το πρόβλημα είναι ότι με τον ένα ή τον άλλο τρόπο, μια διαδικασία σπάνια μιλά απευθείας στον πυρήνα. Και υπάρχουν καλοί λόγοι για αυτό. Η χρήση της βιβλιοθήκης libc σάς επιτρέπει να εγγυηθείτε τη φορητότητα του προγράμματος, ανεξάρτητα από την έκδοση του πυρήνα. Και μέσω της τυπικής βιβλιοθήκης συστήματος πηγαίνουν οι περισσότερες κλήσεις συστήματος. Ακόμα κι αν δημιουργήσετε και εγκαταστήσετε τον πιο πρόσφατο πυρήνα που δημιουργήθηκε για μια πλατφόρμα που υποστηρίζει FSCF, αυτό δεν αποτελεί εγγύηση κέρδους απόδοσης. Το θέμα είναι ότι η βιβλιοθήκη του συστήματος libc.so θα εξακολουθεί να χρησιμοποιεί int 0x80 και μπορεί να αντιμετωπιστεί μόνο με την ανακατασκευή του glibc. Αν η διεπαφή VDSO και το __kernel_vsysall υποστηρίζονται καθόλου σε glibc, ειλικρινά δυσκολεύομαι να απαντήσω αυτήν τη στιγμή.

Συνδέσεις.

Σελίδα του Manu Garg, http://www.manugarg.com
Scatter/Συλλέξτε σκέψεις από τον Johan Petersson, http://www.trilithium.com/johan/2005/08/linux-gate/
Παλιά καλή κατανόηση του πυρήνα του Linux Πού μπορούμε να πάμε χωρίς αυτόν :)
Και φυσικά, ο πηγαίος κώδικας Linux (2.6.22)

Αυτό το υλικό είναι μια τροποποίηση του ομώνυμου άρθρου του Vladimir Meshkov, που δημοσιεύτηκε στο περιοδικό "System Administrator"

Αυτό το υλικό είναι ένα αντίγραφο των άρθρων του Vladimir Meshkov από το περιοδικό "System Administrator". Αυτά τα άρθρα μπορείτε να τα βρείτε στους παρακάτω συνδέσμους. Ορισμένα παραδείγματα του πηγαίου κώδικα των προγραμμάτων έχουν επίσης αλλάξει - βελτιωθεί, βελτιωθεί. (Το παράδειγμα 4.2 τροποποιήθηκε σε μεγάλο βαθμό, αφού έπρεπε να υποκλέψω μια ελαφρώς διαφορετική κλήση συστήματος) URL: http://www.samag.ru/img/uploaded/p.pdf http://www.samag.ru/img/uploaded /a3. pdf

Έχετε ερωτήσεις; Τότε είστε εδώ: [προστασία ηλεκτρονικού ταχυδρομείου]

  • 2. Φορτώσιμη μονάδα πυρήνα
  • 4. Παραδείγματα υποκλοπής κλήσεων συστήματος με βάση το LKM
    • 4.1 Αποτροπή δημιουργίας καταλόγων

1. Γενική άποψη της αρχιτεκτονικής Linux

Η πιο κοινή εμφάνιση σας επιτρέπει να δείτε ένα μοντέλο δύο επιπέδων του συστήματος. πυρήνας<=>progs Στο κέντρο (αριστερά) βρίσκεται ο πυρήνας του συστήματος. Ο πυρήνας αλληλεπιδρά άμεσα με το υλικό του υπολογιστή, απομονώνοντας τα προγράμματα εφαρμογών από τα αρχιτεκτονικά χαρακτηριστικά. Ο πυρήνας έχει ένα σύνολο υπηρεσιών που παρέχονται σε προγράμματα εφαρμογών. Οι υπηρεσίες πυρήνα περιλαμβάνουν λειτουργίες εισόδου / εξόδου (άνοιγμα, ανάγνωση, εγγραφή και διαχείριση αρχείων), δημιουργία και διαχείριση διαδικασιών, συγχρονισμός και επικοινωνία μεταξύ των διαδικασιών. Όλες οι εφαρμογές ζητούν υπηρεσίες πυρήνα μέσω κλήσεων συστήματος.

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

Η αλληλεπίδραση με τον πυρήνα πραγματοποιείται μέσω της τυπικής διεπαφής κλήσης συστήματος. Η διεπαφή syscall είναι μια συλλογή υπηρεσιών πυρήνα και καθορίζει τη μορφή των αιτημάτων υπηρεσίας. Μια διαδικασία ζητά μια υπηρεσία μέσω μιας κλήσης συστήματος σε μια συγκεκριμένη διαδικασία πυρήνα, παρόμοια σε εμφάνιση με μια κανονική κλήση λειτουργίας βιβλιοθήκης. Ο πυρήνας, για λογαριασμό της διαδικασίας, εκτελεί το αίτημα και επιστρέφει τα απαραίτητα δεδομένα στη διαδικασία.

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

/ * Πηγή 1.0 * / #include main () (int fd; char buf; / * Ανοίξτε το αρχείο - λάβετε τον σύνδεσμο (περιγραφέας αρχείου) fd * / fd = open ("file1", O_RDONLY); / * Διαβάστε 80 χαρακτήρες στο buffer buf * / read ( fd, buf, sizeof (buf)). h αρχείο. Ας δούμε τώρα τον μηχανισμό πραγματοποίησης κλήσεων συστήματος χρησιμοποιώντας αυτό το παράδειγμα. Ο μεταγλωττιστής, έχοντας συναντήσει τη λειτουργία ανοίγματος () για να ανοίξει το αρχείο, το μετατρέπει σε κωδικό συναρμολόγησης, διασφαλίζοντας ότι ο αριθμός κλήσης συστήματος που αντιστοιχεί σε αυτήν τη λειτουργία και οι παράμετροί του φορτώνονται στα μητρώα επεξεργαστή και η επακόλουθη κλήση διακόπτεται 0x80. Οι ακόλουθες τιμές φορτώνονται σε καταχωρητές επεξεργαστών:

  • στο μητρώο EAX - τον αριθμό κλήσης συστήματος. Έτσι, για την περίπτωσή μας, ο αριθμός κλήσης συστήματος είναι 5 (δείτε __NR_open).
  • στο μητρώο EBX - η πρώτη παράμετρος της συνάρτησης (για ανοιχτό () είναι δείκτης μιας συμβολοσειράς που περιέχει το όνομα του αρχείου που ανοίγεται.
  • στο μητρώο ECX - δεύτερη παράμετρος (δικαιώματα αρχείου)
Η τρίτη παράμετρος φορτώνεται στον μητρώο EDX, σε αυτήν την περίπτωση δεν την έχουμε. Για την εκτέλεση κλήσης συστήματος στο λειτουργικό σύστημα Linux, χρησιμοποιείται η λειτουργία system_call, η οποία ορίζεται (ανάλογα με την αρχιτεκτονική, στην περίπτωση αυτή i386) στο αρχείο /usr/src/linux/arch/i386/kernel/entry.S. Αυτή η λειτουργία είναι το σημείο εισόδου για όλες τις κλήσεις συστήματος. Ο πυρήνας απαντά στη διακοπή 0x80 καλώντας τη συνάρτηση system_call, η οποία είναι, στην πραγματικότητα, ένας χειριστής για τη διακοπή 0x80.

Για να βεβαιωθούμε ότι είμαστε στο σωστό δρόμο, ας δούμε τον κώδικα για τη λειτουργία open () στο libc συστήματος:

# gdb -q /lib/libc.so.6 (gdb) ανοίγει disas Απόθεση κώδικα συναρμολογητή για λειτουργία ανοιχτή: 0x000c8080 : καλέστε 0x1082be< __i686.get_pc_thunk.cx >0x000c8085 : προσθέστε $ 0x6423b,% ecx 0x000c808b : cmpl $ 0x0,0x1a84 (% ecx) 0x000c8092 : jne 0xc80b1 0x000c8094 : πιέστε% ebx 0x000c8095 : mov 0x10 (% esp, 1),% edx 0x000c8099 : mov 0xc (% esp, 1),% ecx 0x000c809d : mov 0x8 (% esp, 1),% ebx 0x000c80a1 : mov $ 0x5,% eax 0x000c80a6 : int $ 0x80 ... Καθώς δεν είναι δύσκολο να φανεί στις τελευταίες γραμμές, οι παράμετροι μεταφέρονται στους καταχωρητές EDX, ECX, EBX και ο αριθμός κλήσης συστήματος μπαίνει στον τελευταίο μητρώο EAX, ίσος με 5 όπως ήδη ξέρω.

Τώρα ας επιστρέψουμε στην εξέταση του μηχανισμού κλήσεων συστήματος. Έτσι, ο πυρήνας καλεί τον χειριστή διακοπών 0x80 - τη λειτουργία system_call. Το System_call τοποθετεί αντίγραφα των καταχωρητών που περιέχουν τις παραμέτρους κλήσεων στη στοίβα χρησιμοποιώντας τη μακροεντολή SAVE_ALL και καλεί την απαιτούμενη λειτουργία συστήματος με την εντολή κλήσης. Ο πίνακας δεικτών προς συναρτήσεις πυρήνα που υλοποιούν κλήσεις συστήματος βρίσκεται στη συστοιχία sys_call_table (βλέπε αρχείο arch / i386 / kernel / entry.S). Ο αριθμός κλήσης συστήματος, ο οποίος βρίσκεται στον καταχωρητή EAX, είναι ένας δείκτης σε αυτόν τον πίνακα. Έτσι, εάν το EAX είναι 5, θα κληθεί η συνάρτηση πυρήνα sys_open (). Γιατί χρειάζεται η μακροεντολή SAVE_ALL; Η εξήγηση είναι πολύ απλή. Δεδομένου ότι σχεδόν όλες οι λειτουργίες του πυρήνα του συστήματος είναι γραμμένες σε C, αναζητούν τις παραμέτρους τους στη στοίβα. Και οι παράμετροι ωθούνται στη στοίβα χρησιμοποιώντας το SAVE_ALL! Η τιμή που επιστρέφεται από την κλήση συστήματος αποθηκεύεται στον καταχωρητή EAX.

Τώρα ας καταλάβουμε πώς να υποκλέψουμε την κλήση συστήματος. Ο μηχανισμός των φορτώσιμων μονάδων πυρήνα θα μας βοηθήσει σε αυτό.

2. Φορτώσιμη μονάδα πυρήνα

Το Loadable Kernel Module (συνήθως συντομογραφείται ως LKM - Loadable Kernel Module) είναι κώδικας προγράμματος που εκτελείται στον χώρο του πυρήνα. Το κύριο χαρακτηριστικό του LKM είναι η δυνατότητα δυναμικής φόρτωσης και εκφόρτωσης χωρίς να χρειάζεται επανεκκίνηση ολόκληρου του συστήματος ή επανασυγκρότηση του πυρήνα.

Κάθε LKM αποτελείται από δύο κύριες λειτουργίες (ελάχιστες):

  • συνάρτηση προετοιμασίας μονάδας. Καλείται όταν το LKM φορτώνεται στη μνήμη: int init_module (άκυρο) (...)
  • λειτουργία εκφόρτωσης ενότητας: void cleanup_module (void) (...)
Ακολουθεί ένα παράδειγμα της απλούστερης ενότητας: / * Source 2.0 * / #include int init_module (void) (printk ("Hello World \ n"); return 0;) void cleanup_module (void) (printk ("Bye \ n");) / * EOF * / Compile and load the module Η μονάδα φορτώνεται στη μνήμη με την εντολή insmod και οι φορτωμένες μονάδες εμφανίζονται με την εντολή lsmod: # gcc -c -DMODULE -I / usr / src / linux / include / src -2.0.c # insmod src -2.0. o Προειδοποίηση: φόρτωση src-2.0 .o θα αμαυρώσει τον πυρήνα: χωρίς άδεια Το module src-2.0 φορτώθηκε, με προειδοποιήσεις # dmesg | tail -n 1 Hello World # lsmod | grep src src-2.0 336 0 (αχρησιμοποίητο) # rmmod src-2.0 # dmesg | ουρά -ν 1 Αντίο

3. Αλγόριθμος για την υποκλοπή κλήσης συστήματος με βάση το LKM

Για να υλοποιήσετε μια ενότητα που υποκλέπτει μια κλήση συστήματος, είναι απαραίτητο να ορίσετε έναν αλγόριθμο υποκλοπής. Ο αλγόριθμος έχει ως εξής:
  • κρατήστε ένα δείκτη στην αρχική (αρχική) κλήση για να μπορέσετε να την επαναφέρετε
  • δημιουργήστε μια συνάρτηση που υλοποιεί τη νέα κλήση συστήματος
  • αντικατάσταση κλήσεων στον πίνακα κλήσεων συστήματος sys_call_table, δηλ. ρύθμιση του αντίστοιχου δείκτη σε νέα κλήση συστήματος
  • στο τέλος της εργασίας (κατά την εκφόρτωση της μονάδας) επαναφέρετε την αρχική κλήση συστήματος χρησιμοποιώντας τον προηγουμένως αποθηκευμένο δείκτη
Η παρακολούθηση σάς επιτρέπει να μάθετε ποιες κλήσεις συστήματος εμπλέκονται στη λειτουργία της εφαρμογής του χρήστη. Με την ανίχνευση, μπορείτε να καθορίσετε ποια κλήση συστήματος θα πρέπει να υποκλαπεί για να αναλάβει τον έλεγχο της εφαρμογής. # ltrace -S ./src-1.0 ... open ("file1", 0, 01 SYS_open ("αρχείο1", 0, 01) = 3<... open resumed>) = 3 ανάγνωση (3, SYS_read (3, "123 \ n", 80) = 4<... read resumed>"123 \ n", 80) = 4 κλείσιμο (3 SYS_close (3) = 0<... close resumed>) = 0 ... Τώρα έχουμε αρκετές πληροφορίες για να ξεκινήσουμε να μελετάμε παραδείγματα εφαρμογής ενοτήτων που υποκλέπτουν κλήσεις συστήματος.

4. Παραδείγματα υποκλοπής κλήσεων συστήματος με βάση το LKM

4.1 Αποτροπή δημιουργίας καταλόγων

Όταν δημιουργηθεί ο κατάλογος, καλείται η συνάρτηση πυρήνα sys_mkdir. Μια συμβολοσειρά που περιέχει το όνομα του δημιουργημένου καταλόγου ορίζεται ως παράμετρος. Εξετάστε τον κώδικα που υποκλέπτει την αντίστοιχη κλήση συστήματος. / * Πηγή 4.1 * / #include #περιλαμβάνω #περιλαμβάνω / * Εξαγωγή του πίνακα κλήσεων συστήματος * / extern void * sys_call_table; / * Ορισμός δείκτη για αποθήκευση της αρχικής κλήσης * / int ( * orig_mkdir) (const char * path); / * Ας δημιουργήσουμε τη δική μας κλήση συστήματος. Η κλήση μας δεν κάνει τίποτα, απλώς επιστρέφει μηδενική τιμή * / int own_mkdir (const char * path) (επιστροφή 0;) / * Κατά την προετοιμασία της μονάδας, αποθηκεύουμε τον δείκτη στην αρχική κλήση και αντικαθιστούμε την κλήση συστήματος * / int init_module (άκυρο ) (orig_mkdir = sys_call_table; sys_call_table = own_mkdir; printk ("sys_mkdir αντικαταστάθηκε \ n"); επιστροφή (0);) / * Κατά την εκφόρτωση, επαναφέρετε την αρχική κλήση * / void cleanup_module (void) (sys_call_table = orig_mkd sys_mkdir move_nmkdir ");) / * EOF * / Για να λάβετε τη μονάδα αντικειμένου, εκτελέστε την ακόλουθη εντολή και εκτελέστε μερικά πειράματα στο σύστημα: # gcc -c -DMODULE -I / usr / src / linux / include / src -3.1. c # dmesg | tail -n 1 sys_mkdir αντικαταστάθηκε # mkdir test # ls -ald test ls: test: Δεν υπάρχει τέτοιο αρχείο ή κατάλογος # rmmod src -3.1 # dmesg | tail -n 1 sys_mkdir μετακινήθηκε πίσω # mkdir test # ls -ald test drwxr-xr-x 2 root root 4096 2003-12-23 03:46 test Όπως μπορείτε να δείτε, η εντολή "mkdir" δεν λειτουργεί, ή μάλλον δεν συμβαίνει τίποτα. Για να επαναφέρετε τη λειτουργικότητα του συστήματος, αρκεί να ξεφορτώσετε τη μονάδα. Αυτό έγινε παραπάνω.

4.2 Απόκρυψη καταχώρησης αρχείου σε κατάλογο

Καθορίστε ποια κλήση συστήματος είναι υπεύθυνη για την ανάγνωση του περιεχομένου του καταλόγου. Για να γίνει αυτό, θα γράψουμε ένα άλλο δοκιμαστικό κομμάτι που θα διαβάζει τον τρέχοντα κατάλογο: / * Source 4.2.1 * / #include #περιλαμβάνω int main () (DIR * d; struct dirent * dp; d = opendir ("."); dp = readdir (d); return 0;) / * EOF * / Πάρτε το εκτελέσιμο και εντοπίστε το: # gcc -o src-3.2.1 src-3.2.1.c # ltrace -S ./src-3.2.1 ... opendir ("." SYS_open ( "", 100 352, 010 005 141 300) = 3 SYS_fstat64 (3, 0xbffff79c, 0x4014c2c0, 3, 0xbffff874) = 0 SYS_fcntl64 (3, 2, 1, 1, 0x4014c2c0) = 0 SYS6a5_brk (SYS_fcntl64 (3, 2, 1, 1, 0x4014c2c0) = 0 SYS6a5_brk (NS8014c2c0) = 0 SYS6a_brk (NULL = 0x0806a5f4 SYS_brk (NULL) = 0x0806a5f4 SYS_brk (0x0806b000) = 0x06<... opendir resumed>) = 0x08049648 readdir (0x08049648 SYS_getdents64 (3.0x08049678, 4096, 0x40014400, 0x4014c2c0) = 528<... readdir resumed>) = 0x08049678 ... Δώστε προσοχή στην τελευταία γραμμή. Τα περιεχόμενα του καταλόγου διαβάζονται από τη συνάρτηση getdents64 (το getdents είναι δυνατό σε άλλους πυρήνες). Το αποτέλεσμα αποθηκεύεται ως μια λίστα δομών τύπου strukt dirent και η ίδια η συνάρτηση επιστρέφει το μήκος όλων των καταχωρήσεων στον κατάλογο. Μας ενδιαφέρουν δύο πεδία αυτής της δομής:
  • d_reclen - μέγεθος εγγραφής
  • d_name - όνομα αρχείου
Για να αποκρύψετε την εγγραφή αρχείου σχετικά με το αρχείο (με άλλα λόγια, να το κάνετε αόρατο), είναι απαραίτητο να διακόψετε την κλήση συστήματος sys_getdents64, να βρείτε την αντίστοιχη εγγραφή στη λίστα των δομών που λαμβάνονται και να τη διαγράψετε. Εξετάστε τον κώδικα που εκτελεί αυτήν τη λειτουργία (ο συντάκτης του αρχικού κώδικα είναι ο Michal Zalewski): / * Πηγή 4.2.2 * / #include #περιλαμβάνω #περιλαμβάνω #περιλαμβάνω #περιλαμβάνω #περιλαμβάνω #περιλαμβάνω #περιλαμβάνω εξωτερικό κενό * sys_call_table; int ( * orig_getdents) (u_int fd, struct dirent * dirp, u_int count); / * Ορίστε τη δική μας κλήση συστήματος * / int own_getdents (u_int fd, struct dirent * dirp, u_int count) (χωρίς υπογραφή int tmp, n; int t; struct dirent64 (int d_ino1, d_ino2; int d_off1, d_off2; unsigned short d_reclen; unsigned char d_type; char d_name;) * dirp2, * dirp3; / * Το όνομα του αρχείου που θέλουμε να αποκρύψουμε * / char hide = "file1"; / * Καθορίστε το μήκος των καταχωρήσεων στον κατάλογο * / tmp = ( * orig_getdents) (fd, dirp, count); if (tmp> 0) ( / * Εκχωρήστε μνήμη για τη δομή στο χώρο του πυρήνα και αντιγράψτε τα περιεχόμενα του καταλόγου σε αυτό * / dirp2 = (struct dirent64 *) kmalloc (tmp, GFP_KERNEL); copy_from_user (dirp2, dirp, tmp); / * Ας χρησιμοποιήσουμε τη δεύτερη δομή και να αποθηκεύσουμε το μήκος των εγγραφών στον κατάλογο * / dirp3 = dirp2; t = tmp; / * Ας αρχίσουμε να ψάχνουμε το αρχείο μας * / ενώ (t> 0) ( / * Διαβάστε το μήκος της πρώτης εγγραφής και καθορίστε το υπόλοιπο μήκος των εγγραφών στον κατάλογο * / n = dirp3-> d_reclen; t - = n; / * Ελέγξτε αν το όνομα αρχείου από την τρέχουσα εγγραφή ταιριάζει με την αναζήτηση * / if (strstr ((char *) & (dirp3-> d_name), (char *) & απόκρυψη)! = NULL) ( / * Αν ναι, αντικαταστήστε την καταχώριση και υπολογίστε το νέο μήκος καταχωρήσεων στον κατάλογο * / memcpy (dirp3, (char *) dirp3 + dirp3-> d_reclen, t); tmp - = n; ) / * Τοποθετήστε τον δείκτη στην επόμενη εγγραφή και συνεχίστε την αναζήτηση * / dirp3 = (struct dirent64 *) ((char *) dirp3 + dirp3-> d_reclen); ) / * Επιστρέψτε το αποτέλεσμα και ελευθερώστε τη μνήμη * / copy_to_user (dirp, dirp2, tmp). kfree (dirp2); ) / * Επιστρέψτε το μήκος των καταχωρήσεων στον κατάλογο * / επιστρέψτε tmp; ) / * Οι συναρτήσεις για την εκκίνηση και την εκφόρτωση της μονάδας έχουν μια τυπική φόρμα * / int init_module (void) (orig_getdents = sys_call_table; sys_call_table = own_getdents; return 0;) void cleanup_module () (sys_call_table = orig_getdents;) / * EOF * Κατά τη σύνταξη αυτού του κώδικα, παρατηρήστε πώς εξαφανίζεται το "αρχείο1", όπως απαιτείται.

5. Μέθοδος άμεσης πρόσβασης στον χώρο διευθύνσεων του πυρήνα / dev / kmem

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

Η άμεση πρόσβαση στο χώρο διευθύνσεων του πυρήνα παρέχεται από το αρχείο συσκευής / dev / kmem. Αυτό το αρχείο εμφανίζει όλο τον διαθέσιμο χώρο εικονικής διεύθυνσης, συμπεριλαμβανομένου του διαμερίσματος ανταλλαγής (περιοχή ανταλλαγής). Για την εργασία με το αρχείο kmem, χρησιμοποιούνται οι τυπικές λειτουργίες συστήματος - άνοιγμα (), ανάγνωση (), εγγραφή (). Έχοντας ανοίξει / dev / kmem με τον τυπικό τρόπο, μπορούμε να αναφερθούμε σε οποιαδήποτε διεύθυνση στο σύστημα, ορίζοντάς την ως μετατόπιση σε αυτό το αρχείο. Αυτή η μέθοδος αναπτύχθηκε από τον Silvio Cesare.

Οι λειτουργίες του συστήματος είναι προσβάσιμες φορτώνοντας τις παραμέτρους της λειτουργίας σε καταχωρητές επεξεργαστών και κατόπιν καλώντας τη διακοπή λογισμικού 0x80. Ο χειριστής διακοπών, η λειτουργία system_call, σπρώχνει τις παραμέτρους κλήσεων στη στοίβα, ανακτά τη διεύθυνση της κληθείσας συνάρτησης συστήματος από τον πίνακα sys_call_ και μεταφέρει τον έλεγχο σε αυτήν τη διεύθυνση.

Με πλήρη πρόσβαση στο χώρο διευθύνσεων του πυρήνα, μπορούμε να λάβουμε ολόκληρο το περιεχόμενο του πίνακα κλήσεων συστήματος, δηλ. διευθύνσεις όλων των λειτουργιών του συστήματος. Αλλάζοντας τη διεύθυνση οποιασδήποτε κλήσης συστήματος, την υποκλέπουμε. Αλλά για αυτό πρέπει να γνωρίζετε τη διεύθυνση του πίνακα ή, με άλλα λόγια, την αντιστάθμιση στο αρχείο / dev / kmem όπου βρίσκεται αυτός ο πίνακας.

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

Σε πραγματική λειτουργία, ο επεξεργαστής, όταν καταγράφει μια διακοπή, αναφέρεται στον πίνακα διανυσμάτων διακοπής, ο οποίος βρίσκεται πάντα στην αρχή της μνήμης και περιέχει διευθύνσεις δύο συνθηκών των προγραμμάτων επεξεργασίας διακοπών. Σε προστατευμένη λειτουργία, ο πίνακας περιγραφής διακοπών (IDT) που βρίσκεται στο λειτουργικό σύστημα προστατευμένης λειτουργίας είναι ανάλογος με τον πίνακα διανυσματικών διακοπών. Για να έχει πρόσβαση ο επεξεργαστής σε αυτόν τον πίνακα, η διεύθυνση του πρέπει να φορτωθεί στο IDTR (Interrupt Descriptor Table Register). Το IDT περιέχει περιγραφείς για χειριστές διακοπών, οι οποίοι, ειδικότερα, περιλαμβάνουν τις διευθύνσεις τους. Αυτοί οι περιγραφείς ονομάζονται πύλες (πύλες). Ο επεξεργαστής, έχοντας καταγράψει μια διακοπή, με τον αριθμό του εξάγει την πύλη από το IDT, καθορίζει τη διεύθυνση του χειριστή και μεταφέρει τον έλεγχο σε αυτόν.

Για να υπολογίσετε τη διεύθυνση της συνάρτησης system_call από τον πίνακα IDT, είναι απαραίτητο να εξαγάγετε την πύλη διακοπής εντός $ 0x80 και από αυτήν - τη διεύθυνση του αντίστοιχου χειριστή, δηλ. διεύθυνση συνάρτησης system_call. Στη λειτουργία system_call, η πρόσβαση στον πίνακα system_call πραγματοποιείται με την εντολή κλήσης<адрес_таблицы>(,% eax, 4). Αφού βρούμε τον κωδικό πρόσβασης (υπογραφή) αυτής της εντολής στο αρχείο / dev / kmem, θα βρούμε επίσης τη διεύθυνση του πίνακα κλήσεων συστήματος.

Για να καθορίσουμε τον κωδικό πρόσβασης, θα χρησιμοποιήσουμε το πρόγραμμα εντοπισμού σφαλμάτων και θα αποσυναρμολογήσουμε τη λειτουργία system_call:

# gdb -q / usr / src / linux / vmlinux (gdb) disas system_call Απόρριψη κώδικα συναρμολογητή για λειτουργία system_call: 0xc0194cbc : push% eax 0xc0194cbd : cld 0xc0194cbe : push% es 0xc0194cbf : push% ds 0xc0194cc0 : πιέστε% eax 0xc0194cc1 : push% ebp 0xc0194cc2 : push% edi 0xc0194cc3 : push% esi 0xc0194cc4 : push% edx 0xc0194cc5 : push% ecx 0xc0194cc6 : push% ebx 0xc0194cc7 : mov $ 0x18,% edx 0xc0194ccc : mov% edx,% ds 0xc0194cce : mov% edx,% es 0xc0194cd0 : mov $ 0xffffe000,% ebx 0xc0194cd5 : και% esp,% ebx 0xc0194cd7 : testb $ 0x2,0x18 (% ebx) 0xc0194cdb : jne 0xc0194d3c 0xc0194cdd : cmp $ 0x10e,% eax 0xc0194ce2 : jae 0xc0194d69 0xc0194ce8 : καλέστε * 0xc02cbb0c (,% eax, 4) 0xc0194cef : mov% eax, 0x18 (% esp, 1) 0xc0194cf3 : nop Τέλος χωματερή συναρμολογητή. Η γραμμή "κλήση * 0xc02cbb0c (,% eax, 4)" είναι η κλήση στον πίνακα sys_call_table. Η τιμή 0xc02cbb0c είναι η διεύθυνση του πίνακα (πιθανότατα οι αριθμοί σας θα είναι διαφορετικοί). Παίρνουμε τον κωδικό πρόσβασης αυτής της εντολής: (gdb) x / xw system_call + 44 0xc0194ce8 : 0x0c8514ff Βρήκαμε τον κωδικό πρόσβασης της εντολής για πρόσβαση στον πίνακα sys_call_table. Είναι \ xff \ x14 \ x85. Τα επόμενα 4 byte είναι η διεύθυνση του πίνακα. Μπορείτε να το επαληθεύσετε εισάγοντας την εντολή: (gdb) x / xw system_call + 44 + 3 0xc0194ceb : 0xc02cbb0c Έτσι, βρίσκοντας την ακολουθία \ xff \ x14 \ x85 στο αρχείο / dev / kmem και διαβάζοντας τα ακόλουθα 4 byte, παίρνουμε τη διεύθυνση του πίνακα κλήσεων συστήματος sys_call_table. Γνωρίζοντας τη διεύθυνσή του, μπορούμε να πάρουμε τα περιεχόμενα αυτού του πίνακα (διευθύνσεις όλων των λειτουργιών του συστήματος) και να αλλάξουμε τη διεύθυνση οποιασδήποτε κλήσης συστήματος υποκλέπτοντάς την.

Εξετάστε τον ψευδοκώδικα που εκτελεί τη λειτουργία υποκλοπής:

Readaddr (old_syscall, scr + SYS_CALL * 4, 4); writeaddr (new_syscall, scr + SYS_CALL * 4, 4); Η συνάρτηση readaddr διαβάζει τη διεύθυνση κλήσης συστήματος από τον πίνακα κλήσεων συστήματος και την αποθηκεύει στην μεταβλητή old_syscall. Κάθε καταχώριση στον πίνακα sys_call_t έχει μήκος 4 byte. Η απαιτούμενη διεύθυνση βρίσκεται στο offset sct + SYS_CALL * 4 στο αρχείο / dev / kmem (εδώ sct είναι η διεύθυνση του πίνακα sys_call_table, SYS_CALL είναι ο αριθμός ακολουθίας της κλήσης συστήματος). Η συνάρτηση writeaddr αντικαθιστά τη διεύθυνση της κλήσης συστήματος SYS_CALL με τη διεύθυνση της συνάρτησης new_syscall και όλες οι κλήσεις στην κλήση συστήματος SYS_CALL θα εξυπηρετούνται από αυτήν τη λειτουργία.

Φαίνεται ότι όλα είναι απλά και ο στόχος έχει επιτευχθεί. Ωστόσο, ας θυμηθούμε ότι εργαζόμαστε στο χώρο διευθύνσεων του χρήστη. Εάν τοποθετήσουμε μια νέα λειτουργία συστήματος σε αυτόν τον χώρο διευθύνσεων, τότε όταν καλέσουμε αυτήν τη συνάρτηση θα λάβουμε ένα ωραίο μήνυμα σφάλματος. Εξ ου και το συμπέρασμα - μια νέα κλήση συστήματος πρέπει να τοποθετηθεί στο χώρο διευθύνσεων του πυρήνα. Για να το κάνετε αυτό, πρέπει: να λάβετε ένα μπλοκ μνήμης στο χώρο του πυρήνα, να πραγματοποιήσετε μια νέα κλήση συστήματος σε αυτό το μπλοκ.

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

  • γνωρίζοντας τη διεύθυνση του πίνακα sys_call_table, λαμβάνουμε τη διεύθυνση κάποιας κλήσης συστήματος (για παράδειγμα, sys_mkdir)
  • ορίστε μια συνάρτηση που καλεί τη συνάρτηση kmalloc. Αυτή η συνάρτηση επιστρέφει έναν δείκτη σε ένα μπλοκ μνήμης στο χώρο διευθύνσεων του πυρήνα. Ας καλέσουμε αυτήν τη συνάρτηση get_kmalloc
  • αποθηκεύστε τα πρώτα N byte της κλήσης συστήματος sys_mkdir, όπου N είναι το μέγεθος της συνάρτησης get_kmalloc
  • αντικαταστήστε τα πρώτα Ν byte της κλήσης sys_mkdir με τη συνάρτηση get_kmalloc
  • πραγματοποιούμε μια κλήση στην κλήση συστήματος sys_mkdir, ξεκινώντας έτσι τη συνάρτηση get_kmalloc για εκτέλεση
  • επαναφέρετε τα πρώτα N byte της κλήσης συστήματος sys_mkdir
Ως αποτέλεσμα, έχουμε στη διάθεσή μας ένα μπλοκ μνήμης που βρίσκεται στο χώρο του πυρήνα.

Αλλά για να εφαρμόσουμε αυτόν τον αλγόριθμο, χρειαζόμαστε τη διεύθυνση της συνάρτησης kmalloc. Υπάρχουν διάφοροι τρόποι για να το βρείτε. Το πιο εύκολο είναι να διαβάσετε αυτήν τη διεύθυνση από το αρχείο System.map ή να την προσδιορίσετε χρησιμοποιώντας το πρόγραμμα εντοπισμού σφαλμάτων gdb (print & kmalloc). Εάν τα modules είναι ενεργοποιημένα στον πυρήνα, η διεύθυνση kmalloc μπορεί να προσδιοριστεί χρησιμοποιώντας τη συνάρτηση get_kernel_syms (). Αυτή η επιλογή θα συζητηθεί παρακάτω. Εάν δεν υπάρχει υποστήριξη για μονάδες πυρήνα, τότε η διεύθυνση της συνάρτησης kmalloc θα πρέπει να αναζητηθεί από τον κωδικό πρόσβασης της εντολής κλήσης kmalloc - παρόμοια με τον τρόπο που έγινε για τον πίνακα sys_call_table.

Το Kmalloc λαμβάνει δύο παραμέτρους: το μέγεθος της ζητούμενης μνήμης και τον καθοριστή GFP. Για να βρούμε τον κωδικό πρόσβασης, θα χρησιμοποιήσουμε το πρόγραμμα εντοπισμού σφαλμάτων και θα αποσυναρμολογήσουμε οποιαδήποτε συνάρτηση πυρήνα που περιέχει κλήση στη συνάρτηση kmalloc.

# gdb -q / usr / src / linux / vmlinux (gdb) disas inter_module_register Απόθεση κώδικα συναρμολόγησης για τη λειτουργία inter_module_register: 0xc01a57b4 : push% ebp 0xc01a57b5 : push% edi 0xc01a57b6 : push% esi 0xc01a57b7 : push% ebx 0xc01a57b8 : sub $ 0x10,% esp 0xc01a57bb : mov 0x24 (% esp, 1),% ebx 0xc01a57bf : mov 0x28 (% esp, 1),% esi 0xc01a57c3 : mov 0x2c (% esp, 1),% ebp 0xc01a57c7 : movl $ 0x1f0,0x4 (% esp, 1) 0xc01a57cf : movl $ 0x14, (% esp, 1) 0xc01a57d6 : καλέστε 0xc01bea2a ... Δεν έχει σημασία τι κάνει η λειτουργία, το κύριο πράγμα σε αυτό είναι αυτό που χρειαζόμαστε - μια κλήση στη συνάρτηση kmalloc. Δώστε προσοχή στην τελευταία γραμμή. Αρχικά, οι παράμετροι φορτώνονται στη στοίβα (ο καταχωρητής esp δείχνει στο πάνω μέρος της στοίβας) και στη συνέχεια ακολουθεί η κλήση της συνάρτησης. Ο καθοριστής GFP ($ 0x1f0,0x4 (% esp, 1) φορτώνεται πρώτα στη στοίβα. Για τις εκδόσεις πυρήνα 2.4.9 και άνω, αυτή η τιμή είναι 0x1f0. Βρείτε τον κωδικό πρόσβασης αυτής της εντολής: (gdb) x / xw inter_module_register + 19 0xc01a57c7 : 0x042444c7 Αν βρούμε αυτόν τον κωδικό πρόσβασης, μπορούμε να υπολογίσουμε τη διεύθυνση της συνάρτησης kmalloc. Με την πρώτη ματιά, η διεύθυνση αυτής της συνάρτησης είναι ένα επιχείρημα για την εντολή κλήσης, αλλά αυτό δεν είναι απολύτως αληθές. Σε αντίθεση με τη λειτουργία system_call, εδώ πίσω από την εντολή δεν βρίσκεται η διεύθυνση kmalloc, αλλά η αντιστάθμισή της σε σχέση με την τρέχουσα διεύθυνση. Το επιβεβαιώνουμε καθορίζοντας τον κωδικό πρόσβασης της εντολής κλήσης 0xc01bea2a: (gdb) x / xw inter_module_register + 34 0xc01a57d6 : 0x01924fe8 Το πρώτο byte είναι e8, το οποίο είναι ο κωδικός πρόσβασης της εντολής κλήσης. Ας βρούμε την τιμή του ορίσματος αυτής της εντολής: (gdb) x / xw inter_module_register + 35 0xc01a57d7 : 0x0001924f Τώρα αν προσθέσουμε την τρέχουσα διεύθυνση 0xc01a57d6, offset 0x0001924f και 5 byte της εντολής, παίρνουμε την απαιτούμενη διεύθυνση της συνάρτησης kmalloc - 0xc01bea2a.

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

6. Παράδειγμα υποκλοπής μέσω / dev / kmem

/ * πηγή 6.0 * / #include #περιλαμβάνω #περιλαμβάνω #περιλαμβάνω #περιλαμβάνω #περιλαμβάνω #περιλαμβάνω #περιλαμβάνω / * Αριθμός κλήσης συστήματος για υποκλοπή * / #define _SYS_MKDIR_ 39 #define KMEM_FILE " / dev / kmem" #define MAX_SYMS 4096 / * IDTR register format format * / struct (unsigned short limit; unsigned int base;) __attribute__ ((packed) ) idtr? / * Μορφοποίηση περιγραφής της πύλης διακοπής IDT * / struct (χωρίς υπογραφή short off1; χωρίς υπογραφή short sel; unsigned char none, flags; unsigned short off2;) __attribute__ ((packed)) idt; / * Περιγραφή της δομής για τη συνάρτηση get_kmalloc * / struct kma_struc (ulong ( * kmalloc) (uint, int); // - διεύθυνση της συνάρτησης kmalloc int μέγεθος. // - μέγεθος μνήμης για κατανομή int σημαίες, // - σημαία, για πυρήνες> 2.4.9 = 0x1f0 (GFP) ulong mem;) __attribute__ ((συσκευασμένο)) kmalloc; / * Μια συνάρτηση που κατανέμει μόνο ένα μπλοκ μνήμης στο χώρο διευθύνσεων του πυρήνα * / int get_kmalloc (struct kma_struc * k) (k-> mem = k-> kmalloc (k-> μέγεθος, k-> σημαίες) · επιστροφή 0 ) MAX_SYMS || αριθμ< 0) return 0; get_kernel_syms(tab); for (i = 0; i < numsyms; i++) { if (!strncmp(n, tab[i].name, strlen(n))) return tab[i].value; } return 0; } /* Наша новая системная функция, ничего не делает;) */ int new_mkdir(const char *path) { return 0; } /* Читает из /dev/kmem с offset size данных в buf */ static inline int rkm(int fd, uint offset, void *buf, uint size) { if (lseek(fd, offset, 0) != offset){ printf("lseek err\n"); return 0; } if (read(fd, buf, size) != size) return 0; return size; } /* Аналогично, но только пишет в /dev/kmem */ static inline int wkm(int fd, uint offset, void *buf, uint size) { if (lseek(fd, offset, 0) != offset) return 0; if (write(fd, buf, size) != size) return 0; return size; } /* Читает из /dev/kmem данные размером 4 байта */ static inline int rkml(int fd, uint offset, ulong *buf) { return rkm(fd, offset, buf, sizeof(ulong)); } /* Аналогично, но только пишет */ static inline int wkml(int fd, uint offset, ulong buf) { return wkm(fd, offset, &buf, sizeof(ulong)); } /* Функция для получения адреса sys_call_table */ ulong get_sct(int kmem) { ulong sys_call_off; // - адрес обработчика // прерывания int $0x80 (функция system_call) char *p; char sc_asm; asm("sidt %0" : "=m" (idtr)); if (!rkm(kmem, idtr.base+(8*0x80), &idt, sizeof(idt))) return 0; sys_call_off = (idt.off2 << 16) | idt.off1; if (!rkm(kmem, sys_call_off, &sc_asm, 128)) return 0; p = (char *)memmem(sc_asm, 128, "\xff\x14\x85", 3) + 3; printf("call for sys_call_table at %08x\n",p); if (p) return *(ulong *)p; return 0; } /* Функция для определения адреса функции kmalloc */ ulong get_kma(ulong pgoff) { uint i; unsigned char buf, *p, *p1; int kmemz; ulong ret; ret = get_sym("kmalloc"); if (ret) { printf("\nZer gut!\n"); return ret; } kmemz = open("/dev/kmem", O_RDONLY); if (kmemz < 0) return 0; for (i = pgoff+0x100000; i < (pgoff + 0x1000000); i += 0x10000){ if (!rkm(kmemz, i, buf, sizeof(buf))) return 0; p1=(char *)memmem(buf,sizeof(buf),"\x68\xf0\x01\x00",4); if(p1) { p=(char *)memmem(p1+4,sizeof(buf),"\xe8",1)+1; if (p) { close(kmemz); return *(unsigned long *)p+i+(p-buf)+4; } } } close(kmemz); return 0; } int main() { int kmem; // !! - пустые, нужно подставить ulong get_kmalloc_size; // - размер функции get_kmalloc !! ulong get_kmalloc_addr; // - адрес функции get_kmalloc !! ulong new_mkdir_size; // - размер функции-перехватчика!! ulong new_mkdir_addr; // - адрес функции-перехватчика!! ulong sys_mkdir_addr; // - адрес системного вызова sys_mkdir ulong page_offset; // - нижняя граница адресного // пространства ядра ulong sct; // - адрес таблицы sys_call_table ulong kma; // - адрес функции kmalloc unsigned char tmp; kmem = open(KMEM_FILE, O_RDWR, 0); if (kmem < 0) return 0; sct = get_sct(kmem); page_offset = sct & 0xF0000000; kma = get_kma(page_offset); printf("OK\n" "page_offset\t\t:\t0x%08x\n" "sys_call_table\t:\t0x%08x\n" "kmalloc()\t\t:\t0x%08x\n", page_offset,sct,kma); /* Найдем адрес sys_mkdir */ if (!rkml(kmem, sct+(_SYS_MKDIR_*4), &sys_mkdir_addr)) { printf("Cannot get addr of %d syscall\n", _SYS_MKDIR_); perror("er: "); return 1; } /* Сохраним первые N байт вызова sys_mkdir */ if (!rkm(kmem, sys_mkdir_addr, tmp, get_kmalloc_size)) { printf("Cannot save old %d syscall!\n", _SYS_MKDIR_); return 1; } /* Перепишем первые N байт, функцией get_kmalloc */ if (!wkm(kmem, sys_mkdir_addr,(void *)get_kmalloc_addr, get_kmalloc_size)) { printf("Can"t overwrite our syscall %d!\n",_SYS_MKDIR_); return 1; } kmalloc.kmalloc = (void *) kma; //- адрес функции kmalloc kmalloc.size = new_mkdir_size; //- размер запращевоемой // памяти (размер функции-перехватчика new_mkdir) kmalloc.flags = 0x1f0; //- спецификатор GFP /* Выполним сис. вызов sys_mkdir, тем самым выполним нашу функцию get_kmalloc */ mkdir((char *)&kmalloc,0); /* Востановим оригинальный вызов sys_mkdir */ if (!wkm(kmem, sys_mkdir_addr, tmp, get_kmalloc_size)) { printf("Can"t restore syscall %d !\n",_SYS_MKDIR_); return 1; } if (kmalloc.mem < page_offset) { printf("Allocated memory is too low (%08x < %08x)\n", kmalloc.mem, page_offset); return 1; } /* Оторбразим результаты */ printf("sys_mkdir_addr\t\t:\t0x%08x\n" "get_kmalloc_size\t:\t0x%08x (%d bytes)\n\n" "our kmem region\t\t:\t0x%08x\n" "size of our kmem\t:\t0x%08x (%d bytes)\n\n", sys_mkdir_addr, get_kmalloc_size, get_kmalloc_size, kmalloc.mem, kmalloc.size, kmalloc.size); /* Разместим в пространстве ядра наш новый сис. вызво */ if(!wkm(kmem, kmalloc.mem, (void *)new_mkdir_addr, new_mkdir_size)) { printf("Unable to locate new system call !\n"); return 1; } /* Перепишем таблицу sys_call_table на наш новый вызов */ if(!wkml(kmem, sct+(_SYS_MKDIR_*4), kmalloc.mem)) { printf("Eh ..."); return 1; } return 1; } /* EOF */ Скомпилируем полученый код и определим адреса и размеры функций get_kmalloc и new_mkdir. Запускать полученое творение рано! Для вычисления адресов и размеров воспользуемся утилитой objdump: # gcc -o src-6.0 src-6.0.c # objdump -x ./src-6.0 >dump Ας ανοίξουμε το αρχείο χωματερή και βρούμε τα δεδομένα που μας ενδιαφέρουν: 080485a4 g F .text 00000032 get_kmalloc 080486b1 g F .text 0000000a new_mkdir Τώρα θα εισάγουμε αυτές τις τιμές στο πρόγραμμά μας: ulong get_kmalloc_size = 0x32 ulong get_kmalloc_addr = 0x080485a4; ulong new_mkdir_size = 0x0a; ulong new_mkdir_addr = 0x080486b1; Τώρα ας επανασυσκευάσουμε το πρόγραμμα. Ξεκινώντας το για εκτέλεση, θα υποκλέψουμε την κλήση συστήματος sys_mkdir. Όλες οι κλήσεις στην κλήση sys_mkdir θα εξυπηρετούνται πλέον από τη συνάρτηση new_mkdir.

Τέλος χαρτιού / EOP

Η απόδοση του κώδικα από όλες τις ενότητες δοκιμάστηκε στον πυρήνα 2.4.22. Κατά την προετοιμασία της έκθεσης, χρησιμοποιήθηκαν υλικά από τον ιστότοπο

Τις περισσότερες φορές, ο κωδικός κλήσης συστήματος με τον αριθμό __NR_xxx, ορίζεται στο /usr/include/asm/unistd.h, μπορεί να βρεθεί στην πηγή του πυρήνα Linux κάτω από τη συνάρτηση sys_xxx(). (Μπορείτε να βρείτε τον πίνακα κλήσεων για το i386 στο /usr/src/linux/arch/i386/kernel/entry.S.) Υπάρχουν πολλές εξαιρέσεις σε αυτόν τον κανόνα, κυρίως λόγω του γεγονότος ότι οι περισσότερες παλιές κλήσεις συστήματος αντικαθίστανται με νέες χωρίς κανένα σύστημα. Σε πλατφόρμες που μιμούνται ιδιόκτητα λειτουργικά συστήματα, όπως parisc, sparc, sparc64 και alpha, υπάρχουν πολλές επιπλέον κλήσεις συστήματος. υπάρχει επίσης ένα πλήρες σύνολο κλήσεων συστήματος 32-bit για mips64.

Με την πάροδο του χρόνου, εάν είναι απαραίτητο, υπήρξαν αλλαγές στη διεπαφή ορισμένων κλήσεων συστήματος. Ένας από τους λόγους αυτών των αλλαγών ήταν η ανάγκη αύξησης του μεγέθους των δομών ή των κλιμάκων που μεταφέρθηκαν στην κλήση συστήματος. Λόγω αυτών των αλλαγών, σε ορισμένες αρχιτεκτονικές (συγκεκριμένα, στο παλιό i386 32-bit), εμφανίστηκαν διάφορες ομάδες παρόμοιων κλήσεων συστήματος (για παράδειγμα, κολοβός (2) και περικοπή 64 (2)) που εκτελούν τις ίδιες εργασίες αλλά διαφέρουν ως προς το μέγεθος των επιχειρημάτων τους. (Όπως σημειώθηκε, αυτό δεν επηρεάζει τις εφαρμογές: τα περιτυλίγματα glibc κάνουν μέρος της εργασίας για την εκκίνηση της σωστής κλήσης συστήματος και αυτό παρέχει συμβατότητα ABI για παλαιότερα δυαδικά αρχεία.) Παραδείγματα κλήσεων συστήματος που έχουν πολλαπλές εκδόσεις:

* Αυτή τη στιγμή υπάρχουν τρεις διαφορετικές εκδόσεις στατ (2): sys_stat() (ένα μέρος __NR_oldstat), sys_newstat() (ένα μέρος __NR_stat) και sys_stat64() (ένα μέρος __NR_stat64), το τελευταίο χρησιμοποιείται αυτήν τη στιγμή. Παρόμοια κατάσταση με lstat (2) και fstat (2). * Παρομοίως ορίζεται __NR_oldolduname, __NR_oldunameκαι __NR_unameγια κλήσεις sys_olduname(), sys_uname() και sys_newuname(). * Το Linux 2.0 διαθέτει νέα έκδοση vm86 (2) ονομάζονται νέες και παλιές εκδόσεις πυρηνικών διαδικασιών sys_vm86old() και sys_vm86(). * Το Linux 2.4 έχει νέα έκδοση getrlimit (2) ονομάζονται νέες και παλιές εκδόσεις πυρηνικών διαδικασιών sys_old_getrlimit() (ένα μέρος __NR_getrlimit) και sys_getrlimit() (ένα μέρος __NR_ugetrlimit). * Στο Linux 2.4, το μέγεθος του πεδίου αναγνωριστή χρήστη και ομάδας έχει αυξηθεί από 16 σε 32 bit. Προστέθηκαν αρκετές κλήσεις συστήματος για να υποστηρίξουν αυτήν την αλλαγή (π. chown32 (2), getuid32 (2), getgroups32 (2), setresuid32 (2)), καταργώντας προηγούμενες κλήσεις με τα ίδια ονόματα, αλλά χωρίς το επίθημα "32". * Το Linux 2.4 προσθέτει υποστήριξη για πρόσβαση σε μεγάλα αρχεία (τα μεγέθη και οι μετατοπίσεις που δεν χωρούν σε 32 bits) σε εφαρμογές σε αρχιτεκτονικές 32-bit. Αυτό απαιτούσε αλλαγές στις κλήσεις συστήματος που αφορούσαν τα μεγέθη και τις αντισταθμίσεις αρχείων. Προστέθηκαν οι ακόλουθες κλήσεις συστήματος: fcntl64 (2), getdents64 (2), stat64 (2), statfs64 (2), περικοπή 64 (2) και τα αντίστοιχά τους, τα οποία χειρίζονται περιγραφείς αρχείων ή συμβολικούς συνδέσμους. Αυτές οι κλήσεις συστήματος είναι παρωχημένες από τις παλιές κλήσεις συστήματος, οι οποίες, με εξαίρεση τις κλήσεις "stat", ονομάζονται επίσης αλλά δεν έχουν το επίθημα "64".

Σε νεότερες πλατφόρμες με μόνο πρόσβαση αρχείων 64-bit και UID / GID 32-bit (π.χ. alpha, ia64, s390x, x86-64), υπάρχει μόνο μία έκδοση κλήσεων συστήματος για UID / GID και πρόσβαση σε αρχεία. Σε πλατφόρμες (συνήθως πλατφόρμες 32-bit) όπου υπάρχουν κλήσεις * 64 και * 32, άλλες εκδόσεις καταργούνται.

* Κλήσεις rt_sig *προστέθηκε στον πυρήνα 2.2 για υποστήριξη πρόσθετων σημάτων σε πραγματικό χρόνο (βλ. σήμα (7)). Αυτές οι κλήσεις συστήματος αντικαθιστούν τις παλιές κλήσεις συστήματος με τα ίδια ονόματα αλλά χωρίς το πρόθεμα "rt_". * Στις κλήσεις συστήματος επιλέγω (2) και mmap (2) χρησιμοποιούνται πέντε ή περισσότερα επιχειρήματα, προκαλώντας προβλήματα στον προσδιορισμό του τρόπου με τον οποίο τα ορίσματα μεταβιβάζονται στο i386. Κατά συνέπεια, ενώ σε άλλες αρχιτεκτονικές, καλεί sys_select() και sys_mmap() αγώνας __NR_selectκαι __NR_mmap, στο i386 αντιστοιχούν old_select() και old_mmap() (διαδικασίες που χρησιμοποιούν δείκτη σε μπλοκ επιχειρημάτων). Επί του παρόντος, δεν υπάρχει πλέον πρόβλημα με τη διέλευση περισσότερων από πέντε επιχειρημάτων και υπάρχει __NR__επιλογήπου ταιριάζει ακριβώς sys_select(), και την ίδια κατάσταση με __NR_mmap2.

ΒΛΑΔΙΜΙΡ ΜΕΣΧΚΟΒ

Υποκλοπή κλήσεων συστήματος στο Linux

Τα τελευταία χρόνια, το λειτουργικό σύστημα Linux εδραιώθηκε σταθερά ως η κορυφαία πλατφόρμα διακομιστή, μπροστά από πολλές εμπορικές εξελίξεις. Παρ 'όλα αυτά, τα ζητήματα προστασίας συστημάτων πληροφοριών που βασίζονται σε αυτό το λειτουργικό σύστημα δεν παύουν να είναι επίκαιρα. Υπάρχει μεγάλος αριθμός τεχνικών μέσων, τόσο λογισμικού όσο και υλικού, που σας επιτρέπουν να διασφαλίσετε την ασφάλεια του συστήματος. Πρόκειται για εργαλεία κρυπτογράφησης δεδομένων και κίνησης δικτύου, διαφοροποίησης δικαιωμάτων πρόσβασης σε πηγές πληροφοριών, προστασίας e-mail, διακομιστών ιστού, προστασίας από ιούς κ.λπ. Η λίστα, όπως καταλαβαίνετε, είναι αρκετά μεγάλη. Σε αυτό το άρθρο, σας προτείνουμε να εξετάσετε έναν μηχανισμό προστασίας που βασίζεται στην υποκλοπή κλήσεων συστήματος του λειτουργικού συστήματος Linux. Αυτός ο μηχανισμός σας επιτρέπει να αναλάβετε τον έλεγχο της εργασίας οποιασδήποτε εφαρμογής και έτσι να αποτρέψετε πιθανές καταστροφικές ενέργειες που μπορεί να εκτελέσει.

Κλήσεις συστήματος

Ας ξεκινήσουμε με έναν ορισμό. Οι κλήσεις συστήματος είναι μια συλλογή λειτουργιών που υλοποιούνται στον πυρήνα του λειτουργικού συστήματος. Κάθε αίτημα από την εφαρμογή του χρήστη τελικά μετατρέπεται σε κλήση συστήματος που εκτελεί την απαιτούμενη ενέργεια. Μπορείτε να βρείτε μια πλήρη λίστα κλήσεων συστήματος Linux στο αρχείο /usr/include/asm/unistd.h. Ας ρίξουμε μια ματιά στον γενικό μηχανισμό για την πραγματοποίηση κλήσεων συστήματος με ένα παράδειγμα. Αφήστε τη λειτουργία creat () να κληθεί στον πηγαίο κώδικα της εφαρμογής για να δημιουργήσετε ένα νέο αρχείο. Ο μεταγλωττιστής, όταν συναντήσει μια κλήση σε αυτήν τη λειτουργία, τον μετατρέπει σε κωδικό συγκέντρωσης, διασφαλίζοντας ότι ο αριθμός κλήσης συστήματος που αντιστοιχεί σε αυτήν τη λειτουργία και οι παράμετροί του φορτώνονται στα μητρώα επεξεργαστή και η επακόλουθη κλήση διακόπτεται 0x80. Οι ακόλουθες τιμές φορτώνονται σε καταχωρητές επεξεργαστών:

  • για εγγραφή EAX- αριθμός κλήσης συστήματος. Έτσι, για την περίπτωσή μας, ο αριθμός κλήσης συστήματος θα είναι 8 (βλ. __NR_creat).
  • στο μητρώο EBX- η πρώτη παράμετρος της συνάρτησης (για creat, αυτός είναι ένας δείκτης σε μια συμβολοσειρά που περιέχει το όνομα του αρχείου που πρόκειται να δημιουργηθεί).
  • στο μητρώο ECX- η δεύτερη παράμετρος (δικαιώματα πρόσβασης αρχείων).

Η τρίτη παράμετρος φορτώνεται στον μητρώο EDX, σε αυτήν την περίπτωση δεν την έχουμε. Για την εκτέλεση κλήσης συστήματος στο Linux, χρησιμοποιείται η λειτουργία system_call, η οποία ορίζεται στο αρχείο /usr/src/liux/arch/i386/kernel/entry.S. Αυτή η λειτουργία είναι το σημείο εισόδου για όλες τις κλήσεις συστήματος. Ο πυρήνας απαντά στη διακοπή 0x80 καλώντας τη συνάρτηση system_call, η οποία είναι, στην πραγματικότητα, ένας χειριστής για τη διακοπή 0x80.

Για να βεβαιωθούμε ότι είμαστε στο σωστό δρόμο, ας γράψουμε ένα μικρό δοκιμαστικό απόσπασμα στο assembler. Εδώ θα δούμε τι γίνεται η συνάρτηση creat () μετά τη μεταγλώττιση. Ας ονομάσουμε το αρχείο test.S. Ιδού το περιεχόμενό του:

Globl _start

Κείμενο

Αρχή:

Φορτώστε τον αριθμό κλήσης συστήματος στον καταχωρητή EAX:

movl $ 8,% eax

Ο καταχωρητής EBX είναι η πρώτη παράμετρος, ένας δείκτης σε μια συμβολοσειρά με το όνομα αρχείου:

movl $ filename,% ebx

Στο μητρώο ECX - η δεύτερη παράμετρος, δικαιώματα πρόσβασης:

movl $ 0,% ecx

Κλήση της διακοπής:

int $ 0x80

Βγαίνουμε από το πρόγραμμα. Για να το κάνετε αυτό, καλέστε τη συνάρτηση εξόδου (0):

movl $ 1,% eax movl $ 0,% ebx int $ 0x80

Στο τμήμα δεδομένων, καθορίστε το όνομα του αρχείου που θα δημιουργηθεί:

Δεδομένα

όνομα αρχείου: .string "file.txt"

Συντάσσω:

gcc -c δοκιμή. S.

δοκιμή δοκιμής ld -s -o.o

Η δοκιμή εκτελέσιμου αρχείου θα εμφανιστεί στον τρέχοντα κατάλογο. Με την εκτέλεσή του, θα δημιουργήσουμε ένα νέο αρχείο που ονομάζεται file.txt.

Τώρα ας επιστρέψουμε στην εξέταση του μηχανισμού κλήσεων συστήματος. Έτσι, ο πυρήνας καλεί τον χειριστή διακοπών 0x80 - τη λειτουργία system_call. Το System_call ωθεί αντίγραφα των καταχωρητών που περιέχουν τις παραμέτρους κλήσεων στη στοίβα χρησιμοποιώντας τη μακροεντολή SAVE_ALL και καλεί την απαιτούμενη λειτουργία συστήματος με την εντολή κλήσης. Ο πίνακας δεικτών προς συναρτήσεις πυρήνα που υλοποιούν κλήσεις συστήματος βρίσκεται στη συστοιχία sys_call_table (βλέπε αρχείο arch / i386 / kernel / entry.S). Ο αριθμός κλήσης συστήματος, ο οποίος βρίσκεται στον καταχωρητή EAX, είναι ένας δείκτης σε αυτόν τον πίνακα. Έτσι, εάν το EAX περιέχει τιμή 8, θα κληθεί η συνάρτηση πυρήνα sys_creat (). Γιατί χρειάζεται η μακροεντολή SAVE_ALL; Η εξήγηση είναι πολύ απλή. Δεδομένου ότι σχεδόν όλες οι λειτουργίες συστήματος του πυρήνα είναι γραμμένες σε C, αναζητούν τις παραμέτρους τους στη στοίβα. Και οι παράμετροι ωθούνται στη στοίβα χρησιμοποιώντας τη μακροεντολή SAVE_ALL! Η τιμή που επιστρέφεται από την κλήση συστήματος αποθηκεύεται στον καταχωρητή EAX.

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

Φόρτωση μονάδας πυρήνα

Μια μονάδα πυρήνα με δυνατότητα φόρτωσης (ας την ονομάσουμε LKM - Loadable Kernel Module) είναι κώδικας προγράμματος που εκτελείται στο χώρο του πυρήνα. Το κύριο χαρακτηριστικό του LKM είναι η δυνατότητα δυναμικής φόρτωσης και εκφόρτωσης χωρίς να χρειάζεται επανεκκίνηση ολόκληρου του συστήματος ή επανασυγκρότηση του πυρήνα.

Κάθε LKM αποτελείται από δύο κύριες λειτουργίες (ελάχιστες):

  • συνάρτηση προετοιμασίας μονάδας. Καλείται όταν φορτώνεται το LKM στη μνήμη:

int init_module (άκυρο) (...)

  • λειτουργία εκφόρτωσης ενότητας:

void cleanup_module (void) (...)

Ας δώσουμε ένα παράδειγμα της απλούστερης ενότητας:

#define MODULE

#περιλαμβάνω

int init_module (άκυρο)

printk ("Hello World");

επιστροφή 0?

void cleanup_module (άκυρο)

printk ("Αντίο");

Μεταγλωττίστε και φορτώστε τη μονάδα. Η μονάδα φορτώνεται στη μνήμη με την εντολή insmod:

gcc -c -O3 helloworld.c

insmod helloworld.o

Οι πληροφορίες για όλες τις ενότητες που είναι φορτωμένες επί του παρόντος στο σύστημα βρίσκονται στο αρχείο / proc / modules. Για να βεβαιωθείτε ότι η μονάδα είναι φορτωμένη, πληκτρολογήστε cat / proc / modules ή lsmod. Η εντολή rmmod ξεφορτώνει τη μονάδα:

rmmod helloworld

Αλγόριθμος υποκλοπής κλήσεων συστήματος

Για να υλοποιήσετε μια ενότητα που υποκλέπτει μια κλήση συστήματος, είναι απαραίτητο να ορίσετε έναν αλγόριθμο υποκλοπής. Ο αλγόριθμος έχει ως εξής:

  • αποθηκεύστε έναν δείκτη στην αρχική (αρχική) κλήση για να μπορέσετε να τον επαναφέρετε.
  • Δημιουργήστε μια συνάρτηση που υλοποιεί μια νέα κλήση συστήματος.
  • αντικαταστήστε τις κλήσεις στον πίνακα κλήσεων συστήματος sys_call_table, δηλ. ρυθμίστε τον κατάλληλο δείκτη σε μια νέα κλήση συστήματος ·
  • στο τέλος της εργασίας (κατά την εκφόρτωση της μονάδας), επαναφέρετε την αρχική κλήση συστήματος χρησιμοποιώντας τον προηγουμένως αποθηκευμένο δείκτη.

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

Τώρα έχουμε αρκετές πληροφορίες για να αρχίσουμε να εξετάζουμε παραδείγματα εφαρμογής ενοτήτων που υποκλέπτουν κλήσεις συστήματος.

Παραδείγματα υποκλοπής κλήσεων συστήματος

Αποτροπή δημιουργίας καταλόγων

Κατά τη δημιουργία ενός καταλόγου, καλείται η συνάρτηση πυρήνα sys_mkdir. Η παράμετρος είναι μια συμβολοσειρά που περιέχει το όνομα του καταλόγου που πρόκειται να δημιουργηθεί. Εξετάστε τον κώδικα που υποκλέπτει την αντίστοιχη κλήση συστήματος.

#περιλαμβάνω

#περιλαμβάνω

#περιλαμβάνω

Εξάγουμε τον πίνακα κλήσεων συστήματος:

εξωτερικό κενό * sys_call_table;

Ας ορίσουμε έναν δείκτη για την αποθήκευση της αρχικής κλήσης συστήματος:

int ( * orig_mkdir) (const char * path);

Ας δημιουργήσουμε τη δική μας κλήση συστήματος. Η κλήση μας δεν κάνει τίποτα, απλώς επιστρέφει μια μηδενική τιμή:

int own_mkdir (const char * path)

επιστροφή 0?

Κατά την προετοιμασία της μονάδας, αποθηκεύουμε τον δείκτη στην αρχική κλήση και αντικαθιστούμε την κλήση συστήματος:

int init_module ()

orig_mkdir = sys_call_table;

sys_call_table = own_mkdir; επιστροφή 0?

Κατά την εκφόρτωση, επαναφέρουμε την αρχική κλήση:

void cleanup_module ()

Sys_call_table = orig_mkdir;

Αποθηκεύστε τον κώδικα στο αρχείο sys_mkdir_call.c. Για να λάβετε τη μονάδα αντικειμένου, δημιουργήστε ένα Makefile με το ακόλουθο περιεχόμενο:

CC = gcc

CFLAGS = -O3 -Wall -fomit -frame -pointer

sys_mkdir_call.o: sys_mkdir_call.c

$ (CC) -c $ (CFLAGS) $ (MODFLAGS) sys_mkdir_call.c

Χρησιμοποιήστε την εντολή make για να δημιουργήσετε μια μονάδα πυρήνα. Αφού το κατεβάσουμε, ας προσπαθήσουμε να δημιουργήσουμε έναν κατάλογο με την εντολή mkdir. Όπως μπορείτε να δείτε, τίποτα δεν συμβαίνει. Η εντολή δεν λειτουργεί. Για να επαναφέρετε τη λειτουργικότητά του, αρκεί να ξεφορτώσετε τη μονάδα.

Αποτρέψτε την ανάγνωση του αρχείου

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

#περιλαμβάνω

#περιλαμβάνω

#περιλαμβάνω

#περιλαμβάνω

#περιλαμβάνω

#περιλαμβάνω

#περιλαμβάνω

εξωτερικό κενό * sys_call_table;

Δείκτης για τη διατήρηση της αρχικής κλήσης συστήματος:

int ( * orig_open) (const char * pathname, int flag, int mode);

Η πρώτη παράμετρος για τη λειτουργία ανοίγματος είναι το όνομα του αρχείου που θα ανοίξει. Μια νέα κλήση συστήματος θα πρέπει να συγκρίνει αυτήν την παράμετρο με το όνομα του αρχείου που θέλουμε να προστατεύσουμε. Εάν τα ονόματα ταιριάζουν, θα προσομοιωθεί ένα σφάλμα ανοίγματος αρχείου. Η νέα μας κλήση συστήματος μοιάζει με αυτό:

int own_open (const char * pathname, int flag, int mode)

Βάλτε εδώ το όνομα του αρχείου που θα ανοίξει:

char * kernel_path;

Το όνομα του αρχείου που θέλουμε να προστατεύσουμε:

char hide = "test.txt"

Κατανομή μνήμης και αντιγραφή του ονόματος του αρχείου για να ανοίξει εκεί:

kernel_path = (char *) kmalloc (255, GFP_KERNEL);

copy_from_user (kernel_path, όνομα διαδρομής, 255);

Συγκρίνω:

if (strstr (kernel_path, (char *) & hide)! = NULL) (

Δωρεάν μνήμη και επιστρέψτε έναν κωδικό σφάλματος εάν τα ονόματα ταιριάζουν:

kfree (kernel_path);

επιστροφή -ΕΝΕΥΡΕΣΗ?

αλλιώς (

Εάν τα ονόματα δεν ταιριάζουν, καλούμε την αρχική κλήση συστήματος για να εκτελέσουμε την τυπική διαδικασία ανοίγματος αρχείου:

kfree (kernel_path);

επιστροφή orig_open (όνομα διαδρομής, σημαία, λειτουργία).

int init_module ()

orig_open = sys_call_table;

sys_call_table = own_open;

επιστροφή 0?

void cleanup_module ()

sys_call_table = orig_open;

Ας αποθηκεύσουμε τον κώδικα στο αρχείο sys_open_call.c και δημιουργήστε ένα Makefile για να λάβετε τη μονάδα αντικειμένου:

CC = gcc

CFLAGS = -O2 -Wall -fomit -frame -pointer

MODFLAGS = -D__KERNEL__ -DMODULE -I / usr / src / linux / περιλαμβάνουν

sys_open_call.o: sys_open_call.c

$ (CC) -c $ (CFLAGS) $ (MODFLAGS) sys_open_call.c

Στον τρέχοντα κατάλογο, δημιουργήστε ένα αρχείο που ονομάζεται test.txt, φορτώστε τη μονάδα και εισαγάγετε την εντολή cat test.txt. Το σύστημα θα ενημερώσει για την απουσία ενός αρχείου με αυτό το όνομα.

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

Απόκρυψη καταχώρησης αρχείου σε έναν κατάλογο

Καθορίστε ποια κλήση συστήματος είναι υπεύθυνη για την ανάγνωση του περιεχομένου του καταλόγου. Για να γίνει αυτό, θα γράψουμε ένα άλλο δοκιμαστικό κομμάτι που διαβάζει τον τρέχοντα κατάλογο:

/ * Αρχείο Dir.c * /

#περιλαμβάνω

#περιλαμβάνω

int main ()

DIR * d;

strukt dirent * dp;

d = opendir (".");

dp = readdir (d);

Επιστροφή 0;

Ας πάρουμε την εκτελέσιμη ενότητα:

gcc -o dir dir.c

και εντοπίστε το:

strace ./dir

Ας δώσουμε προσοχή στην προτελευταία γραμμή:

getdents (6, / * 4 εγγραφές * /, 3933) = 72;

Τα περιεχόμενα του καταλόγου διαβάζονται από τη συνάρτηση getdents. Το αποτέλεσμα αποθηκεύεται ως μια λίστα δομών τύπου strukt dirent. Η δεύτερη παράμετρος αυτής της συνάρτησης είναι ένας δείκτης σε αυτήν τη λίστα. Η συνάρτηση επιστρέφει το μήκος όλων των καταχωρήσεων στον κατάλογο. Στο παράδειγμά μας, η συνάρτηση getdents καθόρισε την παρουσία τεσσάρων καταχωρήσεων στον τρέχοντα κατάλογο - ".", ".." και τα δύο αρχεία μας, την εκτελέσιμη ενότητα και τον πηγαίο κώδικα. Όλες οι καταχωρήσεις καταλόγου έχουν μήκος 72 byte. Οι πληροφορίες για κάθε εγγραφή αποθηκεύονται, όπως είπαμε, στη δομή δομής διαφορετικών. Μας ενδιαφέρουν δύο πεδία αυτής της δομής:

  • d_reclen- το μέγεθος της εγγραφής ·
  • d_name- Ονομα αρχείου.

Για να αποκρύψετε μια εγγραφή αρχείου (με άλλα λόγια, για να την κάνετε αόρατη), πρέπει να υποκλέψετε την κλήση συστήματος sys_getdents, να βρείτε την αντίστοιχη εγγραφή στη λίστα των δομών που έχετε λάβει και να τη διαγράψετε. Εξετάστε τον κώδικα που εκτελεί αυτήν τη λειτουργία (ο συντάκτης του αρχικού κώδικα είναι ο Michal Zalewski):

εξωτερικό κενό * sys_call_table;

int ( * orig_getdents) (u_int, struct dirent *, u_int);

Ας ορίσουμε την κλήση συστήματος.

int own_getdents (u_int fd, struct dirent * dirp, u_int count)

ανυπόγραφο int tmp, n;

int t?

Η εκχώρηση μεταβλητών θα εμφανιστεί παρακάτω. Επιπλέον, χρειαζόμαστε δομές:

strukt dirent * dirp2, * dirp3;

Το όνομα του αρχείου που θέλουμε να αποκρύψουμε:

char hide = "our.file";

Ας καθορίσουμε το μήκος των καταχωρήσεων στον κατάλογο:

tmp = (* orig_getdents) (fd, dirp, count);

εάν (tmp> 0) (

Κατανομή μνήμης για τη δομή στο χώρο του πυρήνα και αντιγραφή των περιεχομένων του καταλόγου σε αυτήν:

dirp2 = (struct dirent *) kmalloc (tmp, GFP_KERNEL);

copy_from_user (dirp2, dirp, tmp);

Ας χρησιμοποιήσουμε τη δεύτερη δομή και αποθηκεύουμε το μήκος των καταχωρήσεων στον κατάλογο:

dirp3 = dirp2;

t = tmp;

Ας ξεκινήσουμε να ψάχνουμε για το αρχείο μας:

ενώ (t> 0) (

Διαβάζουμε το μήκος της πρώτης εγγραφής και καθορίζουμε το υπόλοιπο μήκος των εγγραφών στον κατάλογο:

n = dirp3-> d_reclen;

t- = n;

Ελέγχουμε εάν το όνομα αρχείου από την τρέχουσα εγγραφή δεν ταιριάζει με το αναζητημένο:

if (strstr ((char *) & (dirp3-> d_name), (char *) & hide)! = NULL) (

Αν ναι, αντικαθιστούμε την καταχώριση και υπολογίζουμε τη νέα τιμή για το μήκος των καταχωρήσεων στον κατάλογο:

memcpy (dirp3, (char *) dirp3 + dirp3-> d_reclen, t);

tmp- = n;

Τοποθετούμε τον δείκτη στην επόμενη εγγραφή και συνεχίζουμε την αναζήτηση:

dirp3 = (struct dirent *) ((char *) dirp3 + dirp3-> d_reclen);

Επιστρέφουμε το αποτέλεσμα και ελευθερώνουμε τη μνήμη:

copy_to_user (dirp, dirp2, tmp);

kfree (dirp2);

Επιστρέφοντας το μήκος των καταχωρήσεων καταλόγου:

επιστροφή tmp?

Οι λειτουργίες προετοιμασίας και εκφόρτωσης της ενότητας έχουν μια τυπική μορφή:

int init_module (άκυρο)

orig_getdents = sys_call_table;

sys_call_table = own_getdents;

επιστροφή 0?

void cleanup_module ()

sys_call_table = orig_getdents;

Ας αποθηκεύσουμε την πηγή στο αρχείο sys_call_getd.c και δημιουργήστε ένα Makefile με το ακόλουθο περιεχόμενο:

CC = gcc

module = sys_call_getd.o

CFLAGS = -O3 -Τοίχιση

LINUX = / usr / src / linux

MODFLAGS = -D__KERNEL__ -DMODULE -I $ (LINUX) / περιλαμβάνουν

sys_call_getd.o: sys_call_getd.c $ (CC) -c

$ (CFLAGS) $ (MODFLAGS) sys_call_getd.c

Δημιουργήστε το αρχείο μας. Στον τρέχοντα κατάλογο και φορτώστε τη μονάδα. Το αρχείο εξαφανίζεται, όπως απαιτείται.

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

Εκεί μπορείτε να βρείτε πιο περίπλοκα και ενδιαφέροντα παραδείγματα υποκλοπής κλήσεων συστήματος. Γράψτε για όλα τα σχόλια και τις προτάσεις σας στο φόρουμ του περιοδικού.

Κατά την προετοιμασία του άρθρου, χρησιμοποιήθηκαν υλικά από τον ιστότοπο