Το προφίλ μας στο Google Plus
0

Code obfuscation στη γλώσσα C [μέρος 1ο]

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

Οι τεχνικές συσκότισης κώδικα (code obfuscation techniques) είναι ιδιαίτερα διαδεδομένες στη γλώσσα C και χρησιμοποιούνται κατά κόρο στην ανάπτυξη κακόβουλου λογισμικού. Οι δημιουργοί αυτών των προγραμμάτων θέλουν να δυσκολέψουν την ανάλυση του κώδικά τους, πάση θυσία. Το ίδιο επιδιώκουν και οι εταιρείες λογισμικού, που θέλουν να δυσκολέψουν το reversing και, τελικά, το παράνομο ξεκλείδωμα των προγραμμάτων τους. Σε αυτή τη σειρά άρθρων θα μελετήσουμε ορισμένες από τις τεχνικές συσκότισης, καθώς και τη γενικότερη διαδικασία ανάλυσης ενός προγράμματος. Όπως αντιλαμβάνεστε, ένα τόσο σύνθετο θέμα δεν θέλει βιασύνες. Αντιθέτως, απαιτείται μελέτη και υπομονή. Σ’ αυτό το τεύχος εξοικειωνόμαστε με την έννοια του disassembly κι εξετάζουμε δύο διαφορετικές προσεγγίσεις για την επίτευξή του. Επιπρόσθετα, θα κατασκευάσουμε ένα μικρό πρόγραμμα το οποίο θα χρησιμοποιήσουμε ως “πειραματόζωο” για την επίδειξη όσων θα παρουσιάσουμε θεωρητικά. Αν θέλετε να ακολουθήσετε τα βήματά μας, είναι απαραίτητη προϋπόθεση να έχετε κάποιο μηχάνημα με λειτουργικό σύστημα τύπου Unix (Σ.τ.Ε. Ένα Linuxάκι αρκεί :P). Καθίστε αναπαυτικά μπροστά στο laptop ή στον υπολογιστή κι ας ξεκινήσουμε.

Από binary σε Assembly
Αρχικά πρέπει να εξηγήσουμε τι περιλαμβάνει η διαδικασία που ονομάζεται disassembly, τι μας προσφέρει και γιατί είναι χρήσιμη κατά την ανάλυση του malware. Ως γνωστόν, για να φτάσουμε από τον πηγαίο κώδικα (source code) σε ένα εκτελέσιμο αρχείο διαμεσολαβεί ο compiler. Κι επειδή εμείς θα ασχοληθούμε με τη γλώσσα C, η δημοφιλέστερη επιλογή compiler για όσους χρησιμοποιούν Linux είναι ο GCC. (Για όσους προτιμούν το BSD, είναι ο clang.) Η διαδικασία για την παραγωγή ενός εκτελέσιμου αρχείου (elf32 ή elf64) περιλαμβάνει τέσσερα στάδια: preprocessing, compilation, Assembly και linking. Σημειώστε μάλιστα ότι ο GCC επιτρέπει να διακόψουμε τη διαδικασία της μεταγλώττισης σε οποιαδήποτε φάση.

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

Το disassembly, για να επιστρέψουμε στο θέμα μας, αποτελεί την αντίστροφη διαδικασία. Μη νομίζετε όμως ότι ξεκινάει από την τελική μορφή ενός εκτελέσιμου και καταφέρνει να μας δώσει τον πηγαίο κώδικα. Κάτι τέτοιο είναι απλά αδύνατο! Με το disassembly, όπως μαρτυρά και το όνομα της διαδικασίας, παίρνουμε τον κώδικα του εκάστοτε εκτελέσιμου σε μορφή Assembly (Assembly code). Αυτό είναι εφικτό γιατί σε κάθε εντολή Assembly αντιστοιχεί ένας συγκεκριμένος συνδυασμός bytes, που ονομάζεται opcode. Επομένως, για τη μετατροπή ενός εκτελέσιμου αρχείου σε κώδικα Assembly απαιτείται μια αντικατάσταση. Τα πράγματα όμως δεν είναι τόσο απλά όσο ακούγονται. Μέσα σε κάθε εκτελέσιμο, βλέπετε, υπάρχουν αρκετά τμήματα (sections) που δεν περιλαμβάνουν όλα κώδικα.

Για να δείτε πόσα διαφορετικά τμήματα περιλαμβάνει ένα εκτελέσιμο, ανοίξτε τον αγαπημένο σας text editor και πληκτρολογήστε το παρακάτω προγραμματάκι:

#include <stdio.h>
int main()
{ 
	printf("Hello, World!\n");
	return 0;
}

Αποθηκεύστε το ως “hello.c”, ανοίξτε ένα τερματικό (αν δεν δουλεύατε ήδη στη γραμμή εντολών) και μεταβείτε στον κατάλογο όπου αποθηκεύσατε τον κώδικα. Για τη μετατροπή σε εκτελέσιμο δώστε το ακόλουθο:

$ gcc -m32 hello.c -o hello

Η παράμετρος -m32 εξασφαλίζει ότι το τελικό εκτελέσιμο θα είναι τύπου elf-32bit και η χρήση της είναι αναγκαία για την εργασία μας, καθώς τα περισσότερα σημερινά συστήματα είναι 64μπιτα. Η μεταγλώττιση θα ολοκληρωθεί γρήγορα και θα προκύψει το τελικό εκτελέσιμο με το όνομα “hello”. Για να τσεκάρετε τη φύση του αρχείου χωρίς να εμφανιστεί ένα κατεβατό άσχετων πληροφοριών, μπορείτε να χρησιμοποιήσετε το εργαλείο file:

$ file hello

Στο μήνυμα που θα εμφανιστεί θα δείτε να γίνεται λόγος για ένα εκτελέσιμο τύπου Elf 32-bit, ευθύς εξαρχής. Για να δείτε τα sections που απαρτίζουν το εκτελέσιμο αρχείο, μπορείτε να χρησιμοποιήσετε το εργαλείο objdump:

$ objdump -h hello

Όπως θα διαπιστώσετε, αυτό το μικροσκοπικό και απλούστατο πρόγραμμα περιλαμβάνει 26 sections! Το objdump εμφανίζει διάφορες πληροφορίες για κάθε “περιοχή”, όπως την έκταση (size), τη διεύθυνση της εικονικής μνήμης στην οποία θα βρεθεί κατά την εκτέλεση του προγράμματος (VMA από το Virtual Memory Address) κ.λπ. Σημειώστε ότι ο κώδικας βρίσκεται συνήθως στο section με το όνομα “.text”.

Το μικρό και απλούστατο προγραμματάκι μας μεταγλωττίστηκε σε ένα εκτελέσιμο αρχείο που περιλαμβάνει 26 sections!

Πριν προχωρήσουμε, πρέπει να αναφέρουμε ότι υπάρχουν δύο διαφορετικά συντακτικά για τη γλώσσα Assembly. Το ένα προέρχεται από την εταιρία AT&T και το άλλο από την Intel. Θα λέγαμε ότι τα δύο συντακτικά είναι ισοδύναμα, καθώς δεν παρουσιάζουν πλεονεκτήματα ή μειονεκτήματα σε σχέση με την απόδοση του κώδικα. Αν ωστόσο κάποιος προγραμματιστής είναι εξοικειωμένος με το ένα μόνο συντακτικό, τότε αν από τη διαδικασία του disassembly πάρει κώδικα του άλλου συντακτικού ενδέχεται να δυσκολευτεί λίγο περισσότερο. Ας μην κολλάμε σε λεπτομέρειες όμως. Πρέπει να δούμε επιτέλους πώς ακριβώς επιτυγχάνεται το disassembly.

Μέθοδοι μετατροπής
Για τη μετατροπή ενός εκτελέσιμου αρχείου σε κώδικα Assembly υπάρχουν δύο αλγόριθμοι: o linear sweep και ο recursive traversal. Ο πρώτος αλγόριθμος (linear sweep) ξεκινά από την αρχή κάθε section και “αποκωδικοποιεί” τα bytes που συναντά γραμμικά, θεωρώντας ότι πρόκειται για opcodes. Με τον όρο γραμμικά εννοούμε ότι όταν εντοπιστεί μια εντολή κλήσης συναρτήσεων (call), μετάβασης (jmp) ή διακλάδωσης (jz, je, jnc, jpo κ.ά.), ο αλγόριθμος συνεχίζει τη δουλειά του με την αμέσως επόμενη εντολή και αδιαφορεί πλήρως για την όποια μεταβολή επιχειρήθηκε στη ροή του προγράμματος. Με άλλα λόγια, το section σαρώνεται byte προς byte και κάθε opcode μεταφράζεται στην αντίστοιχη εντολή Assembly. Οι πιθανές διακλαδώσεις αγνοούνται. Το πλεονέκτημα αυτής της προσέγγισης είναι ότι αποκωδικοποιούνται όλα τα bytes του εκάστοτε section, ενώ το μειονέκτημα είναι ότι αποκωδικοποιούνται ακόμα και bytes που αποτελούν δεδομένα (data) κι όχι εντολές. Μερικά προγράμματα που χρησιμοποιούν αυτόν τον αλγόριθμο είναι τα gdb, WinDbg και objdump.

Το αποτέλεσμα του disassembly με τον objdump μοιάζει με μια ατελείωτη λίστα. Το πρόγραμμα μετέτρεψε σε Assembly ακόμα και κάποια sections που περιλαμβάνουν δεδομένα.

Ο δεύτερος αλγόριθμος (recursive traversal) ομοιάζει με τον πρώτο, με την έννοια ότι μεταφράζει τα opcodes που συναντά διαδοχικά. Ωστόσο, όταν εντοπίζει εντολές που επηρεάζουν τη ροή εκτέλεσης του προγράμματος, συμπεριφέρεται λίγο πιο πονηρά. Πριν προχωρήσει στο αμέσως επόμενο opcode, αποθηκεύει τη διεύθυνση της διακλάδωσης σε μια λίστα. Αργότερα, όταν φτάσει στο τέλος του κώδικα του τρέχοντος section, επαναλαμβάνει την ίδια διαδικασία σε καθεμία από τις θέσεις που περιλαμβάνει η λίστα. Ένα πλεονέκτημα αυτού του αλγόριθμου αποτελεί το γεγονός ότι αποκωδικοποιεί μόνο τα bytes που θα εκτελούσε το σύστημα κι όχι όλα ανεξαιρέτως, όπως ο linear sweep. Αυτός ο αλγόριθμος, όμως, ενδέχεται να προσπεράσει κάποια τμήματα του προγράμματος χωρίς να τα μεταφράσει. Αυτό συμβαίνει όταν η διεύθυνση των συγκεκριμένων τμημάτων δεν αναφέρεται ρητά μέσα στον κώδικα. Όπως θα δούμε αργότερα, υπάρχουν διάφοροι τρόποι για να εκτελέσουμε τον κώδικα μιας περιοχής, χωρίς να εμφανίζεται η σχετική διεύθυνση μέσα στο πρόγραμμα. Ουσιαστικά, πρόκειται για μία τεχνική συσκότισης και, όπως αντιλαμβάνεστε, στην περίπτωση του recursive traversal λειτουργεί με επιτυχία. Ένα σχετικά γνωστό πρόγραμμα που χρησιμοποιεί τον εν λόγω αλγόριθμο είναι το IDA Pro.

Οι αλγόριθμοι σε δράση
Εφόσον βρισκόσαστε σε κάποιο σύστημα Unix, το μόνο πρόγραμμα που πρέπει να εγκαταστήσετε είναι το IDA Pro Demo. Η πιο πρόσφατη έκδοση διατίθεται από τη διεύθυνση www.hex-rays.com/products/ida/support/download_demo.shtml. Εμείς κατεβάσαμε το σχετικό πακέτο πληκτρολογώντας:

$ wget https://out7.hex-rays.com/files/idademo68_linux.tgz

Κατόπιν, χρησιμοποιήσαμε το tar για να αποσυμπιέσουμε:

$ tar –xzf idademo68_linux.tgz

Με την αποσυμπίεση προέκυψε ένας κατάλογος που περιλαμβάνει το πρόγραμμα, έτοιμο προς εκτέλεση.

Ας δούμε τώρα πώς “μεταφράζουν” οι αλγόριθμοι linear sweep και recursive traversal το προγραμματάκι hello. Προκειμένου να κατανοήσετε τη φιλοσοφία του linear sweep, μεταβείτε στον κατάλογο που περιλαμβάνει το hello κι εκτελέστε το ακόλουθο:

$ objdump -D hello

Όπως θα διαπιστώσετε, το objdump αποκωδικοποιεί σχεδόν όλα τα sections του εκτελέσιμου αρχείου, ανεξάρτητα από το αν περιέχουν κώδικα ή δεδομένα. Στα sections που περιλαμβάνουν δεδομένα θα παρατηρήσετε ότι τόσο οι εντολές, όσο και οι παράμετροί τους, παρουσιάζουν μια ύποπτη ομοιομορφία. Για παράδειγμα, μπορείτε να αντιπαραθέσετε τα αποτελέσματα του disassembly από τα sections “.text” και “.gnu.version”.

Για να δείτε τη διαφορά μεταξύ των δύο αλγορίθμων, μεταβείτε στον κατάλογο με το IDA Pro demo και ξεκινήστε το πρόγραμμα:

$ ./idaq

Έχετε υπόψη ότι την πρώτη φορά που θα το τρέξετε κι αμέσως μετά την οθόνη καλωσορίσματος, θα εμφανιστεί η άδεια χρήσης του προγράμματος. Αφού απαντήσετε θετικά στο αν την αποδέχεστε, εμφανίζεται ένα νέο παράθυρο με δύο βασικές επιλογές. Επειδή η εκμάθηση του IDA Pro ξεφεύγει από το σκοπό του παρόντος, ακολουθήστε την εύκολη οδό και πατήστε στο κουμπί “New”. Έτσι, θα εμφανιστεί ένα παράθυρο για την επιλογή του προγράμματος που θέλετε να αναλύσετε. Αν για κάποιο λόγο δεν εμφανίζεται το hello, μπορείτε να δώσετε τη θέση και το όνομα χειροκίνητα. Εμείς, για παράδειγμα, δώσαμε “/home/mag/hello”, καθώς είχαμε γράψει και μεταγλωττίσει το hello.c μέσα στο home directory. Όταν ανοίξετε το hello το IDA θα εμφανίσει ένα είδος γραφήματος που μοιάζει με δέντρο, σε κάθε κόμβο του οποίου υπάρχει ο κώδικας κάποιας υπορουτίνας. Κάνοντας δεξί κλικ σε κάποιο κόμβο κι επιλέγοντας το “Text view”, το πρόγραμμα εμφανίζει ολόκληρο τον κώδικα της σχετικής υπορουτίνας.

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

Όπως είδατε, οι δύο προσεγγίσεις στο disassembly οδηγούν σε εντελώς διαφορετικά αποτελέσματα. Μη βιαστείτε να επιλέξετε, όμως, ούτε να αποφανθείτε για το ποιος αλγόριθμος είναι καλύτερος. Βρισκόμαστε ακόμα στην αρχή και για την ώρα δεν έχουμε δει ούτε μία μέθοδο συσκότισης του κώδικα. Όταν προχωρήσουμε στη μελέτη αυτών των τεχνικών, θα μπορείτε να εκτιμήσετε πιο ολοκληρωμένα τα προτερήματα και τις αδυναμίες κάθε προσέγγισης στο disassembly. Μέχρι τότε, θα σας προτείναμε να παίξετε με το IDA Pro demo και να εξοικειωθείτε με το περιβάλλον του. Πρόκειται για ένα εργαλείο που χρησιμοποιείται από όλους όσοι ασχολούνται με το malware analysis και ο χρόνος που θα επενδύσετε αξίζει με το παραπάνω.

Επιλέγοντας το 'text view' για κάποιον από τους κόμβους, το IDA Pro εμφανίζει τον κώδικα της αντίστοιχης υπορουτίνας. Παρατηρήστε ότι στο πάνω μέρος του παραθύρου εμφανίζεται μια μπάρα που αναπαριστά τη γενική διάρθρωση του εκτελέσιμου αρχείου. Σε αυτή τη μπάρα χρησιμοποιούνται διαφορετικά χρώματα για τον κώδικα, για τα δεδομένα και για τις περιοχές που δεν έχουν μεταφραστεί καθόλου.

Ο Μαγειρίας Αναστάσιος ασχολείται με ό,τι αφορά στο UNIX και με στο security. Είναι συντάκτης στο Junkbytes, μαζί με τον Κώστα Βεντούρα.

Leave a Reply

You must be logged in to post a comment.

Σύνδεση

Αρχείο δημοσιεύσεων