Cet article fait suite au premier publié dans le numéro 72. La première partie présentait l'acquisition de la mémoire volatile d'un système GNU/Linux ainsi que les « internals » de Volatility. Cet article présente les commandes et les possibilités d'analyses associées avec un rappel sur les structures noyaux utilisées.
1. Introduction
Avant d'entamer l'analyse, je vais revenir rapidement sur l'acquisition. Dans la partie 2.2 de l'article précédent, un teaser présentait la phrase suivante : « Sachez que certaines personnes travaillent actuellement sur une méthode de chargement d'un module pré-compilé et ce quelle que soit la version du noyau ». Ce travail a finalement abouti et a été présenté à Amsterdam à la première édition européenne du DFRWS. Le but de l'article n'est pas de présenter le fonctionnement complet du projet. Brièvement, la première étape repose sur la possibilité de parasiter un module déjà présent sur le système grâce au fait qu'un module noyau soit un objet ELF et puisse donc être lié à d'autres objets. Le lecteur intéressé pourra se délecter du papier intitulé « Robust Linux memory acquisition with minimal target impact », coécrit par Johannes Stüttgen et Michael Cohen ou bien regarder la présentation, les deux étant disponibles sur le site de la conférence [DFRWS14]. Pour les plus impatients, le code est accessible sur Google code [LMAP].
Petit bémol cependant, car Volatility ne supporte pas actuellement le format de fichier résultant de cette acquisition qui n'est autre qu'un fichier de type ELF64. Je ne doute pas que les développeurs de Volatility fourniront prochainement le module gérant cet addrspace mais en attendant, l'acquisition est exploitable au travers de rekall, le fork de Volatility. Le projet ressemble beaucoup à Volatility, mais il existe de subtiles différences internes.
Revenons à nos moutons : l'analyse d'une acquisition mémoire GNU/Linux avec Volatility. L'objectif est de présenter les différentes commandes implémentées dans le projet et permettant de mener à bien une analyse, que ce soit la récupération des différents processus, l'obtention des connexions réseaux créées ou, de la potentielle présence de rootkits. Afin de mettre en contexte les différentes commandes, l'article sera basé sur plusieurs codes permettant d'exploiter un système comme le ferait une personne malveillante.
2. Généralités
2.1 Les informations présentées
L'article précédent présentait les méthodes d'acquisition de la mémoire volatile d'un système GNU/Linux, la création d'un profil, son exploitation à l'aide de Volatility et la manière dont il était utilisé, pour présenter enfin le fonctionnement des Address Space. La suite de l'article va se concentrer sur les commandes utilisables sur une acquisition d'un système GNU/Linux en suivant la même catégorisation que la documentation en ligne sur le wiki du projet [VOLINUX] tout en expliquant sur quelles informations Volatility se base pour afficher ses résultats. Ainsi les informations suivantes seront présentées :
- les informations relatives au système ;
- l'analyse des processus ;
- les objets du noyau ;
- les informations relatives au réseau ;
- les détections de codes malicieux.
2.2 L'utilisation du profil et l'interprétation des données
La majorité des informations extraites par Volatility résulte de l'utilisation des symboles du noyau présents dans le fichier System.map. Si vous lisez le code source des différentes commandes, vous verrez que la majorité d'entre elles utilise la fonction get_symbol(), implémentée dans le fichier volatility/plugins/overlays/linux/linux.py, qui retourne l'adresse du symbole souhaité. Ce symbole est ensuite interprété par l'instanciation d'un objet Object utilisant la description de la structure au format dwarf comme présenté dans l'article précédent. Dans la suite de l'article, le symbole associé à une commande, et donc à l'information extraite, sera détaillé.
2.3 L'invocation des commandes
L'invocation des commandes relatives à l'analyse d'un système GNU/Linux est similaire à celle utilisée pour Windows. La seule différence est l'utilisation du préfixe linux. De manière générale, l'invocation d'une commande Volatility se fait comme suit lorsque l'on se trouve à la racine des sources du projet :
$ python vol.py –profile=ProfileDuSystème -f /chemin/vers/le/dump.bin <commande>
- --profile correspond au profil qui doit être utilisé par Volatility pour être en mesure d'interpréter les données. Il est possible de consulter la liste des profils en exécutant la commande python vol.py –info | less et de se rendre à la section Profiles.
- -f permet de fournir le chemin vers l'acquisition mémoire à analyser. Il peut être relatif ou absolu.
- commande correspond à la commande à utiliser. Il est possible de les lister en exécutant python vol.py --info | grep linux_
Des options supplémentaires peuvent être fournies pour, par exemple, activer le debug, utiliser un cache pour certaines informations récurrentes, paramétrer la timezone, etc. Le lecteur curieux se référera à la documentation générale [VOLUSAGE].
2.4 Practice yourself
Après ce rappel, il est temps de présenter les différentes commandes proposées. Pour vous exercer, de l'acquisition à l'analyse, une machine virtuelle, créée avec VirtualBox, est mise à votre disposition [VBOX]. Celle-ci est basée sur un système Debian Wheezy (7.5) en 64 bits. Le mot de passe du compte root est toor. À la racine du compte utilisateur misc (mot de passe misc), se trouvent les répertoires et fichiers suivants :
- suterusu : qui correspond au code open source d'un rootkit permettant de cacher des processus ou des connexions réseaux ainsi que de poser des hooks syscall ou de faire du keylogging. Au début de la rédaction de l'article, le code source accessible sur GitHub [SUTERUSU] a été modifié dans la machine virtuelle afin de présenter les mécanismes de détection de Volatility version 2.3.1. En effet, les techniques de hooks employées par ce rootkit n'étaient pas détectées par la version 2.3.1. Depuis la finalisation de l'article, la version 2.4 est disponible et détecte désormais les mécanismes utilisés par suterusu. Le lecteur est donc invité à compiler lui même la dernière version pour le constater.
- profile : contient le code permettant de générer le profil de manière automatisée. Il suffit d'exécuter la commande make dans ce répertoire.
- lime-forensics-1.1-r17 : contient le code du module noyau pour faire l'acquisition comme présentée dans l'article précédent.
Pour accéder simplement à l'acquisition, le script mount_sf.sh permet de monter un répertoire partagé avec le système hôte. Avant de lancer le script, il faut ajouter le répertoire en question à travers VirtualBox et associer au chemin en question le nom vbox comme sur la figure 1. Le point de montage dans la machine virtuelle se situe dans le répertoire share.
Figure 1 : Ajout d'un répertoire partagé à une machine virtuelle dans VirtualBox.
3. Informations du système
La première étape consiste à obtenir les informations de base sur le système. Cela permet de s'assurer que c'est la bonne machine qui est ciblée. La première commande à lancer affiche la version du système par le biais de la commande linux_banner qui récupère l'adresse du symbole éponyme et affiche la chaîne de caractères qui s'y trouve :
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_banner
Volatility Foundation Volatility Framework 2.3.1
Linux version 3.2.0-4-amd64 (debian-kernel@lists.debian.org) (gcc version 4.6.3 (Debian 4.6.3-14) ) #1 SMP Debian 3.2.57-3+deb7u1
L'obtention des informations relatives au CPU résulte de l'exécution de la commande linux_cpuinfo. Cette commande récupère le symbole cpu_info, puis selon le type d'architecture (mono ou multi cpu) va récupérer différentes structures pour extraire les informations :
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_cpuinfo
Volatility Foundation Volatility Framework 2.3.1
Processor Vendor Model
------------ ---------------- -----
0 AuthenticAMD AMD Phenom(tm) II X6 1090T Processor
Avant de travailler sur les processus et les différentes zones mémoire, il peut être intéressant de regarder du côté des logs du noyau grâce à la commande linux_dmesg. Encore une fois, le symbole éponyme est utilisé. Cependant, il existe une différence depuis l'apparition des noyaux 3.0. En effet, ce n'est plus un tableau de type char, mais des structures consécutives de type log qui sont utilisées. L'exécution de cette commande permet, par exemple, d'obtenir les paramètres fournis au noyau ou encore de voir l'activité des points de montage physique en recherchant la chaîne sda dans le log grâce à grep comme dans le listing suivant :
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_dmesg | grep BOOT_IMAGE
Volatility Foundation Volatility Framework 2.3.1
<6>[ 0.000000] Command line: BOOT_IMAGE=/boot/vmlinuz-3.2.0-4-amd64 root=UUID=2638027f-de2b-4f86-a668-5523f6ec9e37 ro quiet
<5>[ 0.000000] Kernel command line: BOOT_IMAGE=/boot/vmlinuz-3.2.0-4-amd64 root=UUID=2638027f-de2b-4f86-a668-5523f6ec9e37 ro quiet
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran1.lime linux_dmesg | grep sd
Volatility Foundation Volatility Framework 2.3.1
<5>[ 1.101424] sd 0:0:0:0: [sda] 16777216 512-byte logical blocks: (8.58 GB/8.00 GiB)
[...]
<6>[ 1.792592] EXT4-fs (sda1): mounted filesystem with ordered data mode. Opts: (null)
<6>[ 4.799232] Adding 392188k swap on /dev/sda5. Priority:-1 extents:1 across:392188k
4. Analyse des processus
La majorité des commandes relatives aux processus repose sur l'utilisation de la structure task_struct, définie dans include/linux/sched.h et présentée en figure 5. Elle est équivalente à la structure Eprocess pour Windows. Cette structure de données représente un processus dans le noyau Linux et contient des informations simples comme son nom, son état actuel, son uid, gid et des informations plus complexes telles que les allocations mémoires ou les descripteurs de fichiers ouverts.
Chaque structure task_struct contient un champ tasks de type list_head. Ce dernier type est au cœur du mécanisme officiel de gestion des listes chaînées dans le noyau Linux. Cette structure de listes chaînées contient les champs prev et next qui, dans le contexte d'une task_struct, pointeront respectivement sur la task_struct précédente et suivante. La gestion de ces listes chaînées est directement prise en compte par Volatility avec une possibilité d'itération par boucle for sur les structures contenant une list_head.
4.1 Lister les processus
Comment récupérer la liste des processus à partir d'une acquisition mémoire ? Pour comprendre le cheminement, un rappel sur l'initialisation d'un système GNU/Linux s'impose. Au cours de l'initialisation du noyau, l'ancêtre de tous les processus, le processus 0 ou idle process aussi appelé swapper process, est créé. Contrairement à tous les autres processus, ses structures sont allouées de manière statique. Parmi ces structures, celle qui nous intéresse est accessible à travers la variable init_task qui correspond, vous l'aurez deviné, à une task_struct.
Volatility récupère l'adresse du symbole init_task pour ensuite accéder à la liste des processus. C'est précisément le mécanisme implémenté par la commande linux_pslist. L'extrait de code suivant est implémenté dans la méthode calculate() du fichier volatility/plugins/linux/pslist.py.
init_task_addr = self.addr_space.profile.get_symbol("init_task")
init_task = obj.Object("task_struct", vm = self.addr_space, offset = init_task_addr)
for task in init_task.tasks:
yield task
Le résultat de cette commande est le suivant :
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_pslist
Volatility Foundation Volatility Framework 2.3.1
Offset Name Pid Uid Gid DTB Start Time
------------------ -------------------- --------------- --------------- ------ ------------------ ----------
0xffff88000f89e740 init 1 0 0 0x000000000fa54000 2014-08-04 15:21:45 UTC+0000
0xffff88000f89e040 kthreadd 2 0 0 ------------------ 2014-08-04 15:21:45 UTC+0000
[...]
0xffff88000d64d140 exim4 2334 101 104 0x000000000c7bb000 2014-08-04 15:21:52 UTC+0000
0xffff88000db3c040 nginx 2359 0 0 0x000000000c7f2000 2014-08-04 15:21:52 UTC+0000
[...]
0xffff88000c5b3140 sshd 2425 1000 1000 0x000000000c5ca000 2014-08-04 15:22:14 UTC+0000
0xffff88000f222740 bash 2426 1000 1000 0x000000000c802000 2014-08-04 15:22:14 UTC+0000
[...]
0xffff88000f1290c0 wget 2705 1000 1000 0x000000000f251000 2014-08-04 15:28:54 UTC+0000
0xffff88000f916040 bash 2706 1000 1000 0x000000000f0d1000 2014-08-04 15:28:59 UTC+0000
0xffff88000f0d7740 sudo 2707 0 1000 0x000000000cd95000 2014-08-04 15:28:59 UTC+0000
0xffff88000c5e2880 insmod 2708 0 0 0x000000000ec2f000 2014-08-04 15:28:59 UTC+0000
Vous noterez l'absence du processus swapper (processus 0) qui est un choix de l'équipe du projet. La première colonne correspond à l'adresse logique de l'objet task_struct, suivie du nom, du pid, uid et gid du processus par l'utilisation des membres comm, pid, uid et gid respectivement. La colonne DTB, pour Directory Table Base, correspond à l'adresse physique de la base de la table de répertoire des pages utilisée par le mécanisme de pagination pour traduire une adresse linéaire vers une adresse physique. La traduction d'adresse logique à linéaire repose sur le mécanisme de segmentation. C'est cette valeur qui est mise dans le registre de contrôle CR3 de la CPU lors d'un changement de contexte de processus. Cette adresse est associée au membre task_struct.mm.pgd. Cette structure sera détaillée dans une autre section de l'article ayant trait à l'accès des zones mémoire.
Dans l'exemple précédent, vous aurez remarqué que la colonne DTB pour le processus kthreadd est vide. Cela vient du fait que c'est un thread noyau qui utilise donc l'espace d'adressage du noyau. Ce qu'il se passe en réalité, c'est qu'un thread noyau se voit réattribuer l'espace d'adressage du dernier processus actif étant donné que l'espace d'adressage noyau est commun à tous les processus.
La dernière colonne fournit l'information temporelle de démarrage du processus grâce au membre start_time.
Il est possible d'enrichir les résultats de la commande précédente en affichant les arguments fournis au processus grâce à la commande linux_psaux qui reproduit le comportement de la commande ps aux. Les arguments fournis à un processus sont stockés dans un tampon accessible à l'adresse linéaire task_struct.mm.arg_start, d'une taille de task_struct.mm.arg_end.
Pour finir, il est possible de présenter la relation des processus sous forme d'arborescence. La méthode consiste à utiliser les membres children pour traverser les fils et sibling pour itérer sur les processus de même niveau grâce à la commande linux_pstree :
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_pstree
Volatility Foundation Volatility Framework 2.3.1
Name Pid Uid
init 1 0
.udevd 291 0
[...]
.exim4 2334 101
.nginx 2359 0
..nginx 2362 33
..nginx 2363 33
..nginx 2364 33
..nginx 2365 33
.sshd 2377 0
..sshd 2423 0
...sshd 2425 1000
....bash 2426 1000
.....nc 2570 1000
.....bash 2706 1000
......sudo 2707 0
.......insmod 2708 0
..sshd 2586 0
...sshd 2588 1000
....bash 2589 1000
.....wget 2705 1000
[...]
4.2 Espace d'adressage
Les sections suivantes présentent les commandes et structures utilisées pour récupérer l'espace mémoire d'un processus utilisateur. Le noyau représente celles-ci avec une structure appelée Memory Descriptor contenant toutes les informations relatives à l'espace d'adressage. Cette structure est représentée par mm_struct déclarée dans linux/sched.h. Elle est directement accessible depuis la task_struct par le membre mm. Le lecteur attentif aura remarqué que cette structure est aussi utilisée par la commande linux_psaux.
4.2.1 Répertoire des tables de page
À partir du membre mm, il est possible d'accéder à l'adresse physique pointant à la base de la page globale de répertoire par le biais du membre pgd présenté en section 4.1. L'article précédent expliquait que Volatility implémente les différents systèmes de translation d'adresse et notamment celui de la pagination, utilisé par la commande memmap, qui permet d'obtenir les pages physiques propres au processus. La sortie de la commande présentée ci-après affiche pour chaque processus les adresses virtuelles, physiques et la taille de chaque page allouée. Il est possible de filtrer un processus donné en renseignant l'argument -p suivi du PID en question.
$ pyhon vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_memmap -p 2725
Volatility Foundation Volatility Framework 2.3.1
Task Pid Virtual Physical Size
---------------- -------- ------------------ ------------------ ------------------
wget 2725 0x0000000000400000 0x0000000006401000 0x1000
wget 2725 0x0000000000401000 0x0000000006400000 0x1000
[...]
wget 2725 0x00007f0d21600000 0x000277b1cfe00000 0x200000
wget 2725 0x00007f0d21800000 0x00032ad0d6a00000 0x200000
[...]
wget 2725 0x00007f77c0000000 0x000fffffc0000000 0x40000000
wget 2725 0x00007f7800000000 0x000fffffc0000000 0x40000000
[...]
wget 2725 0x0000ffffff5ff000 0x0000000001695000 0x1000
wget 2725 0x0000ffffff600000 0x0000000001602000 0x1000
Il est possible d'obtenir des informations plus précises concernant les différentes allocations mémoire d'un processus en utilisant le mécanisme d'allocation des ressources mémoire, lesVirtual Memory Area (VMA).
4.2.2 Virtual Memory Area
Une autre façon d'accéder aux différentes zones de l'espace d'adressage d'un processus consiste à utiliser le membre mmap qui pointe sur la tête de la liste chaînée d'une structure VMA et représentée par une structure vm_area_struct définie dans linux/mm_types.h. Celle-ci représente une zone de mémoire unique dans un espace d'adressage donné (un processus). Une VMA peut représenter différents types de zones mémoire tels qu'un fichier mappé en mémoire ou la pile d'un processus utilisateur. Une zone est définie par l'intervalle représenté par les membres vm_start (inclusif) et vm_end (exclusif). C'est à partir de cette structure qu'il est également possible de connaître le chemin vers les fichiers mappés en mémoire en utilisant le membre vm_file de type file. Si ce pointeur n'est pas nul, il est possible d'accéder aux informations du fichier par l'intermédiaire du champ f_path.
Cette structure est utilisée par la commande linux_proc_maps qui affiche les informations pour chaque VMA d'un processus. Le résultat de la commande affiche le PID, l'adresse linéaire de début et de fin de la VMA, les différents flags qui lui sont associés (lecture, écriture, exécution). Si la région correspond à un fichier mappé en mémoire, le champ vm_pgoff correspond à l'offset dans le fichier en question. Sont ensuite affichés le numéro de major et minor du pilote, le numéro d'inode associé au fichier et enfin son nom. Lorsque la zone fait partie du tas ou de la pile, le nom associé sera respectivement [heap] et [stack]. Il est possible d'afficher uniquement les VMA pour un processus donné en passant le PID à l'argument -p.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_proc_maps -p 2359
Volatility Foundation Volatility Framework 2.3.1
Pid Start End Flags Pgoff Major Minor Inode File Path
-------- ------------------ ------------------ ------ ------------------ ------ ------ ---------- ------------------------------------------
2359 0x0000000000400000 0x00000000004ba000 r-x 0x0 8 1 152299 /usr/sbin/nginx
2359 0x00000000006b9000 0x00000000006ba000 r-- 0xb9000 8 1 152299 /usr/sbin/nginx
2359 0x00000000006ba000 0x00000000006ce000 rw- 0xba000 8 1 152299 /usr/sbin/nginx
2359 0x00000000006ce000 0x00000000006dd000 rw- 0x0 0 0 0
2359 0x0000000001140000 0x000000000119e000 rw- 0x0 0 0 0 [heap]
2359 0x00007f0e5ff34000 0x00007f0e5ff3f000 r-x 0x0 8 1 56 /lib/x86_64-linux-gnu/libnss_files-2.13.so
2359 0x00007f0e5ff3f000 0x00007f0e6013e000 --- 0xb000 8 1 56 /lib/x86_64-linux-gnu/libnss_files-2.13.so
[...]
2359 0x00007f0e61b1c000 0x00007f0e61b50000 r-x 0x0 8 1 148735 /usr/lib/libGeoIP.so.1.4.8
2359 0x00007f0e61b50000 0x00007f0e61d4f000 --- 0x34000 8 1 148735 /usr/lib/libGeoIP.so.1.4.8
[...]
2359 0x00007f0e62f5e000 0x00007f0e62fb4000 r-x 0x0 8 1 131514 /usr/lib/x86_64-linux-gnu/libssl.so.1.0.0
2359 0x00007f0e62fb4000 0x00007f0e631b4000 --- 0x56000 8 1 131514 /usr/lib/x86_64-linux-gnu/libssl.so.1.0.0
[...]
2359 0x00007fff959b9000 0x00007fff959db000 rw- 0x0 0 0 0 [stack]
2359 0x00007fff959ff000 0x00007fff95a00000 r-x 0x0 0 0 0
Si vous souhaitez accéder au contenu d'une VMA, il faut utiliser la commande linux_dump_map qui prend en paramètre le numéro de processus avec l'argument -p, l'adresse de la VMA à récupérer, préalablement fournie par linux_proc_map, avec l'argument -s et enfin le nom du répertoire où le fichier sera extrait avec -D. Le fichier créé a pour nomenclature task.<pid>.<vma_addr>.vma.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_dump_map -p 2359 -s 0x0000000000400000 -D /tmp
Volatility Foundation Volatility Framework 2.3.1
Task VM Start VM End Length Path
---------- ------------------ ------------------ ------------------ ----
2359 0x0000000000400000 0x00000000004ba000 0xba000 /tmp/task.2359.0x400000.vma
$ file /tmp/task.2359.0x400000.vma
/tmp/task.2359.0x400000.vma: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), stripped
4.3 Lister les fichiers ouverts
La liste des fichiers ouverts par tous les processus s'obtient par la commande linux_lsof. Elle utilise le membre files de la structure task_struct, de type files_struct permettant de pointer sur un tableau de file descriptors (struct fdtable *fdt). Chaque entrée de ce tableau pointe sur une structure de type file. La boucle est bouclée, il est maintenant possible de savoir à quel fichier correspond un fd ouvert ! Pour travailler uniquement sur un processus donné, l'argument -p suivi du PID peut être utilisé.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_lsof -p 2364
Volatility Foundation Volatility Framework 2.3.1
Pid FD Path
-------- -------- ----
2364 0 /dev/null
2364 1 /dev/null
2364 2 /var/log/nginx/error.log
2364 3 /
2364 4 /var/log/nginx/access.log
2364 5 /var/log/nginx/error.log
2364 6 /
2364 7 /
2364 8 /
2364 11 /
2364 12 /[eventpoll]
5. Modules noyau
Un module noyau est représenté par la structure module définie dans le fichier linux/module.h. Les deux commandes suivantes utilisent des champs de cette structure pour extraire les informations voulues.
5.1 Lister les modules
La liste des modules actuellement chargés dans l'espace mémoire noyau s'obtient en exécutant la commande linux_lsmod. Celle-ci récupère l'adresse du symbole module, donnant accès au premier module puis parcourt la liste chaînée des modules représentée par le membre list de la structure module. Cette commande peut recevoir deux arguments optionnels :
- -S pour afficher les différentes sections du module par l'utilisation de la structure détaillant les attributs de chacune d'entre elles : module_sect_attrs définie dans kernel/module.c. Celle-ci contient un membre nsections représentant la taille d'un tableau de structures module_sect_attr nommé attrs.
- -P pour afficher les paramètres fournis au module.
$ python vol.py --profile=LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_lsmod -P -S
lime 17991
.note.gnu.build-id 0xffffffffa0262000
.text 0xffffffffa0261000
.rodata 0xffffffffa0262024
.rodata.str1.1 0xffffffffa0262034
__param 0xffffffffa0262058
.data 0xffffffffa0263000
.gnu.linkonce.this_module 0xffffffffa0263010
.bss 0xffffffffa0263260
.symtab 0xffffffffa0036000
.strtab 0xffffffffa00363c0
format=lime
dio=Y
path=aldebaran.lime
[...]
En omettant les paramètres -P et -S, le chiffre après le nom du module représente l'addition des champs init_size et core_size. init_size représente la taille de la section .init qui contient le code d'initialisation. core_size représente quant à elle la taille de la section .text contenant le code du module à proprement parler.
5.2 Extraire les modules
Dans le cadre d'une étude d'un module noyau, il est possible d'extraire son code grâce à la commande linux_moddump. Celle-ci repose sur l'utilisation du membre module_core qui représente l'adresse de début du module et le membre core_size représentant la taille de la section core. Il est nécessaire de fournir l'argument -D à la commande pour spécifier le chemin où sera extrait le module. Enfin, il est possible d'extraire un seul module en fournissant une expression régulière avec l'argument -r.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_moddump -r lime -D /tmp/
Volatility Foundation Volatility Framework 2.3.1
$ strings /tmp/lime.0xffffffffa02af000.lkm
[...]
format
path
lime
padded
tcp:%d
System RAM
lime
setup_tcp
setup_disk
cleanup_module
init_module
write_vaddr_disk
write_vaddr_tcp
cleanup_tcp
cleanup_disk
6. Réseau
Dans le cas d'une analyse de système corrompu, il est plus que probable que des connexions réseaux illégitimes aient été effectuées vers l’extérieur. Dans la version simple, ces connexions ne sont pas cachées. Les connexions dissimulées seront présentées en section 7.
6.1 Connexions
La première commande utile va être de lister le cache ARP en utilisant linux_arp. Cette commande récupère le symbole neigh_tables de type neigh_table, défini dans le fichier net/neighbour.h, puis parcourt les éléments de la liste chaînée (membre next). Chaque élément de cette liste contient un tableau de type neighbour permettant de pointer sur les informations finales telles que l'adresse IP (v4 ou v6), le nom du périphérique réseau par lequel transiter et la MAC.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_arp
Volatility Foundation Volatility Framework 2.3.1
[:: ] at 00:00:00:00:00:00 on lo
[0.0.0.0 ] at 00:00:00:00:00:00 on lo
[10.0.2.2 ] at 52:54:00:12:35:02 on eth0
Pour obtenir les informations relatives aux interfaces réseau actives, l’équivalent de la commande ifconfig est implémenté par linux_ifconfig. Sur les noyaux récents, le symbole net_namespace_list est utilisé, sur les versions plus anciennes, ce sera le symbole dev_base. Pour les deux types, les informations de chaque carte sont accessibles en parcourant la liste chaînée des structures net_device. Cette dernière fournit l'adresse MAC du périphérique réseau et indique si la carte est en mode promiscious ou non. La liste des interfaces réseaux associées aux périphériques (gestion des alias) est obtenue en parcourant la liste ifa_list du membre ip_ptr.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_ifconfig
Volatility Foundation Volatility Framework 2.3.1
Interface IP Address MAC Address Promiscous Mode
---------------- -------------------- ------------------ ---------------
lo 127.0.0.1 00:00:00:00:00:00 False
eth0 10.0.2.15 08:00:27:e5:6b:0a False
Pour les versions du noyau antérieures à 3.6, il est possible d'obtenir les informations contenues dans le cache de la table de routage. Pour les versions plus récentes, ce cache n'existe plus car cela pouvait être une source de déni de service [ROUTECACHE]. Le symbole rt_hash_table est obtenu puis, pour chaque élément du tableau pointé, une nouvelle liste (chain) est parcourue pour fournir l'interface, la destination et la passerelle.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_route_cache
Volatility Foundation Volatility Framework 2.3.1
Interface Destination Gateway
---------------- -------------------- -------
eth0 10.1.0.2 10.0.2.2
eth0 10.1.0.2 10.0.2.2
lo 10.0.2.15 10.0.2.15
lo 127.0.0.1 127.0.0.1
eth0 10.0.2.2 10.0.2.2
eth0 10.0.2.2 10.0.2.2
lo 127.0.0.1 127.0.0.1
lo 10.0.2.15 10.0.2.15
eth0 152.19.134.47 10.0.2.2
lo 10.0.2.15 10.0.2.15
eth0 152.19.134.47 10.0.2.2
Pour terminer sur les connexions réseaux, l’équivalent de la sortie de la commande netstat peut être obtenu en exécutant linux_netstat. Cette commande récupère la liste des fichiers ouverts pour chaque processus en utilisant les résultats de la commande linux_lsof. Puis, pour chaque fichier, la commande va tester si le membre f_op pointe sur une structure socket_file_ops ou si le membre dentry.d_op pointe sur une structure sockfs_dentry_operations. Si tel est le cas, le membre dentry.d_inode est traduit en une structure socket permettant d’accéder au membre sk représentant la structure inet_sock. Cette dernière permet d’accéder aux informations telles que le protocole : TCP ou UDP, dans le cas de socket de type IPv4 ou IPv6 : les adresses, ports sources et destinations ainsi que l’état de la connexion (fermée, établie, en écoute). Dans le cas d'une socket locale, le chemin vers celle-ci sera affiché. Finalement, le nom et le PID du processus sont affichés.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_netstat
Volatility Foundation Volatility Framework 2.3.1
UNIX /run/udev/control
[...]
UDP 0.0.0.0:68 0.0.0.0:0 dhclient/1734
UDP 0.0.0.0:46020 0.0.0.0:0 dhclient/1734
UDP :::61435 :::0 dhclient/1734
UNIX /dev/log
UNIX /var/run/acpid.socket
TCP 127.0.0.1:25 0.0.0.0:0 LISTEN exim4/2334
TCP ::1:25 :::0 LISTEN exim4/2334
TCP 0.0.0.0:80 0.0.0.0:0 LISTEN nginx/2359
[…]
TCP 0.0.0.0:22 0.0.0.0:0 LISTEN sshd/2377
TCP :::22 :::0 LISTEN sshd/2377
[...]
TCP 10.0.2.15:22 10.0.2.2:33518 ESTABLISHED sshd/2586
TCP 10.0.2.15:22 10.0.2.2:33518 ESTABLISHED sshd/2588
TCP 10.0.2.15:54675 152.19.134.47:80 ESTABLISHED wget/2705
6.2 Contenu des paquets
À partir d'une acquisition mémoire, il est possible d'obtenir dans certains cas le contenu des paquets réseaux.
La première technique repose sur la mise en queue des paquets, ce qui arrive dans deux cas de figure :
- lorsque le réseau n'est pas dimensionné pour tenir la cadence d'envoi d'une socket ;
- lorsque les paquets destinés à un processus utilisateur n'ont pas été traités.
La commande linux_pkt_queues extrait les paquets de cette queue avec la nomenclature suivante : [receive|send].pid.fd. Il est donc nécessaire de croiser les informations obtenues précédemment avec pslist et lsof.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_pkt_queues -D /tmp/
Volatility Foundation Volatility Framework 2.3.1
Wrote 1460 bytes to write.2362.11
La dernière commande relative à l'extraction des paquets réseaux, linux_sk_buff_cache, utilise le cache du noyau Linux, le SLAB, pour récupérer les structures sk_buff dans les caches skbuff_head_cache et skbuff_fclone_cache. Le système de cache du noyau sera décrit plus tard dans l'article. Les tampons utilisés par le noyau pour gérer les paquets réseaux sont représentés par cette structure sk_buff. Elle contient une multitude d'informations comme les périphériques utilisés, les informations des couches 1 à 3 du modèle OSI (membres h, nh et mac). Dans le cas présent, le membre len est utilisé pour connaître la taille du paquet contenant les données brutes pointées par le membre data.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_sk_buff_cache -D /tmp/
Volatility Foundation Volatility Framework 2.3.1
Wrote 28 bytes to ffff88000c607180
Wrote 20 bytes to ffff88000c607280
[...]
Wrote 1420 bytes to ffff88000f10e180
Wrote 3594 bytes to ffff88000f10e580
Wrote 1420 bytes to ffff88000f10e980
Wrote 28 bytes to ffff88000c5e50c0
[...]
Wrote 56 bytes to ffff88000cdf3dc0
Wrote 28 bytes to ffff88000cde11c0
Sachez que la structure sk_buff n'est pas pleinement exploitée par Volatility et qu'il pourrait être intéressant de faire un module tirant parti des différentes informations contenues dans celle-ci, notamment pour croiser les données...
7. Stratégie d'allocation mémoire
Afin d'optimiser les allocations et libérations de structure noyau (processus avec task_struct ou paquet réseau avec sk_buff), le noyau fournit un mécanisme de cache fourni par l’intermédiaire du mécanisme SLAB. Cela consiste à allouer au démarrage du noyau un pool de structures définies. Au moment d'une demande de création de structure donnée, le noyau a uniquement besoin de retourner le pointeur vers une entrée libre du pool, évitant ainsi le coût d'une allocation et la fragmentation de la mémoire physique.
Ces fonctionnalités sont proposées par le noyau au travers d'une API permettant d'accéder à des fonctions génériques d'allocation mémoire (kmalloc, kmalloc_node, …) ou à des caches spécifiques (kmem_cache_alloc, kmem_cache_alloc_node). Derrière ce mécanisme, il existe trois stratégies d'allocation différentes : le SLAB, le SLUB et le SLOB. Le SLAB est la première version développée et utilisée par le noyau, le SLUB étant sa version améliorée avec une gestion différente des allocations comme l'utilisation d'une liste au lieu d'un tableau et une meilleure gouvernance du cache. Le SLOB est principalement utilisé pour les systèmes embarqués disposant de peu de mémoire.
Il est possible d'obtenir le statut actuel du cache de l'alloueur SLAB sous GNU/Linux en exécutant la commande slabtop. C'est une commande équivalente à top en termes d'affichage et qui permet de connaître la taille de chaque cache et son taux d'occupation. L’équivalent sous Volatility est possible en exécutant la commande linux_slabinfo comme on peut le constater dans le listing suivant :
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_slabinfo
Volatility Foundation Volatility Framework 2.3.1
<name> <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> <active_slabs> <num_slabs>
------------------------------ ------------- ---------- ---------- ------------ --------------- -------------- -----------
[...]
arp_cache 2 8 448 8 1 1 1
PING 0 0 832 9 2 0 0
RAW 2 9 832 9 2 1 1
UDP 6 9 832 9 2 1 1
TCP 12 12 1728 4 2 3 3
[...]
skbuff_fclone_cache 21 21 512 7 1 3 3
skbuff_head_cache 2577573693 315 256 15 1 21 21
file_lock_cache 11 20 192 20 1 1 1
[...]
sysfs_dir_cache 8262 8262 144 27 1 306 306
mnt_cache 25 30 256 15 1 2 2
filp 570 570 256 15 1 38 38
inode_cache 1981 1981 552 7 1 283 283
dentry 4460 4460 192 20 1 223 223
names_cache 2 2 4096 1 1 2 2
[...]
vm_area_struct 2662 2662 176 22 1 121 121
mm_struct 48 48 960 4 1 12 12
fs_cache 37 59 64 59 1 1 1
files_cache 63 77 704 11 2 7 7
signal_cache 82 98 1088 7 2 14 14
sighand_cache 75 75 2112 3 2 25 25
task_xstate 73 80 512 8 1 10 10
task_struct 92 92 1792 2 1 46 46
[...]
La majorité des commandes ayant comme suffixe _cache utilise les informations du cache grâce aux fonctions définies dans slab_info. Il est également nécessaire de préciser que Volatility ne gère pas l'alloueur d'objets de type SLUB.
8. Montrez ce que je ne saurais voir
Les sections précédentes ont présenté les commandes utiles pour travailler sur la partie visible d'un système. Cette section est consacrée aux commandes permettant de mettre en évidence ce qu'un attaquant souhaite dissimuler. De la même manière que les sections précédentes, l'envers du décor des commandes est présenté afin de comprendre les mécanismes de détection utilisés.
8.1 Historique des commandes exécutées
Lorsqu'une machine compromise est analysée, une des premières sources d'information est la liste des commandes exécutées par l'attaquant. Cependant, ce dernier prend généralement soin d'effacer le fichier .bash_history à la racine du compte utilisateur. Même si ce fichier a été effacé de manière sécurisée, tout n'est pas perdu. Il est en effet possible de récupérer l'historique des différentes commandes exécutées en analysant la mémoire allouée dynamiquement par le binaire bash. Ce dernier maintient une liste des commandes exécutées qu'il est possible de récupérer par l'utilisation du module linux_bash.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran3.lime linux_bash
Volatility Foundation Volatility Framework 2.3.1
Pid Name Command Time Command
-------- -------------------- ------------------------------ -------
[...]
2589 bash 2014-08-04 15:27:25 UTC+0000 sudo nano /etc/nginx/sites-available/
2589 bash 2014-08-04 15:27:25 UTC+0000 sudo /etc/init.d/nginx restart
2589 bash 2014-08-04 15:27:25 UTC+0000 ./sock 1 3505
2589 bash 2014-08-04 15:27:25 UTC+0000 sudo insmod suterusu.ko
2589 bash 2014-08-04 15:27:25 UTC+0000 sudo pgrep -l nc
2589 bash 2014-08-04 15:27:25 UTC+0000 ./sock 5 22
2589 bash 2014-08-04 15:27:25 UTC+0000 sudo wget http://www.gutenberg.org/ebooks/41622.txt.utf-8
[...]
2589 bash 2014-08-04 15:27:25 UTC+0000 netstat -ltpn
2589 bash 2014-08-04 15:27:25 UTC+0000 sudo pgrep -l nc
2589 bash 2014-08-04 15:27:25 UTC+0000 sudo netstat -ltpn
2589 bash 2014-08-04 15:27:25 UTC+0000 make linux-x86_64 KDIR=/lib/modules/$(uname -r)/build
2589 bash 2014-08-04 15:27:25 UTC+0000 nc -l -p 4242&
2589 bash 2014-08-04 15:27:25 UTC+0000 cd /tmp/
2589 bash 2014-08-04 15:27:25 UTC+0000 less sock.c
[...]
8.2 Hooking
Afin de rester sous le radar, la plupart des codes malveillants utilisent des techniques de dissimulation, dont celle du hooking. Elle consiste à intercepter les appels aux fonctions permettant par exemple de lister des fichiers, des processus, des connexions réseau, etc. L'objectif du code malveillant est de faire en sorte que ses fonctions soient appelées au lieu de celles du système. De manière simpliste, cela consiste à modifier l'adresse de la fonction interceptée en la remplaçant par l'adresse de la fonction de hook. Lors d'un appel à la fonction en question, celle du code malveillant sera donc exécutée. Si le résultat attendu de cet appel ne contient pas d'informations à dissimuler, la fonction originale est appelée sinon la fonction faisant office de hook retournera un résultat maquillé. Le principe de détection repose donc sur la validation des adresses des fonctions du système. Les sections suivantes présentent les hooks habituellement utilisés par du code malveillant et qui sont détectables par Volatility.
8.2.1 Modification des structures operations
Une technique couramment utilisée est d'intercepter les appels aux fonctions contenues dans une structure ayant pour suffixe _operations. Par exemple, celle relative aux fichiers se nomme file_operations et permet de lister le contenu d'un répertoire ou de lire le contenu d'un fichier. Cette structure est accessible par exemple depuis le membre f_op d'une structure de type file. Pourquoi modifier précisément ces informations ? Tout simplement parce que les outils habituels tels que ps ou top pour les processus ou netstat pour les connexions réseaux reposent sur l'utilisation du système de fichiers virtuel procfs.
Ce système de fichiers virtuel fournit les différentes informations relatives aux processus et au système de manière plus générale. Les répertoires nommés avec un nombre correspondent aux différents processus, le nombre représentant leur PID. Chaque répertoire contient les informations relatives au processus telles que ses arguments, ses fichiers mappés ou ses descripteurs de fichiers. Un code malveillant souhaitant dissimuler un processus à un utilisateur va donc hooker les routines de lecture du système de fichiers virtuel procfs.
La commande linux_check_fop permet de détecter aisément ce type d'usurpation en vérifiant que les pointeurs sur fonction pointent sur les adresses des symboles usuels définis dans System.map. La commande exécute cette vérification pour tous les fichiers ouverts fournis par la commande linux_lsof, sur les opérations associées à la structure proc_root accessible par l'obtention de l'adresse du symbole éponyme. Enfin le point de montage de procfs est également vérifié en récupérant le symbole proc_mnt représenté par la structure vfsmount (linux/mount.h).
Cette technique est également applicable pour dissimuler des connexions réseaux à une commande comme netstat. Ainsi, à l'instar de linux_check_fop, la commande linux_check_afinfo permet de rechercher ces modifications de pointeurs sur fonctions pour toutes les structures comportant des opérations d'entrées/sorties utilisées par les protocoles TCP et UDP. En plus de files_operations, cette commande effectue aussi la vérification sur la structure sequence_operations pour la fonction show par exemple.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_check_afinfo
Volatility Foundation Volatility Framework 2.3.1
Symbol Name Member Address
------------------------------------------ ------------------------------ ------------------
tcp6_seq_afinfo show 0xffffffffa036d1ee
tcp4_seq_afinfo show 0xffffffffa036d293
udp6_seq_afinfo show 0xffffffffa036d0a4
udp4_seq_afinfo show 0xffffffffa036d149
8.2.2 Syscall
Une autre technique consiste à modifier la table des appels systèmes. Cette table contient les fonctions appelées par un processus demandant un service au noyau. Ainsi lorsqu'un programme ouvre un fichier et lit dans celui-ci, les appels systèmes open() et read() sont utilisés. Un rootkit comme Kbeast va ainsi durant son initialisation poser différents hooks d'appels système en remplaçant le pointeur de la fonction du système par la sienne. Afin de détecter ce comportement, la commande linux_check_syscall vérifie que chaque adresse contenue dans la table des appels système correspond à une adresse connue dans les symboles du profile. Si tel est le cas, le nom du symbole est affiché et HOOKED dans le cas contraire.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_check_syscall
Volatility Foundation Volatility Framework 2.3.1
Table Name Index Address Symbol
---------- ------------------ ------------------ ------------------------------
64bit 0x0 0xffffffffa02fc7cc HOOKED
64bit 0x1 0xffffffffa02fc8ae HOOKED
64bit 0x2 0xffffffff810fa30c sys_open
64bit 0x3 0xffffffff810f9788 sys_close
64bit 0x4 0xffffffff810fe3ca sys_newstat
64bit 0x5 0xffffffff810fe44b sys_newfstat
64bit 0x6 0xffffffff810fe3f5 sys_newlstat
64bit 0x7 0xffffffff81109787 sys_poll
64bit 0x8 0xffffffff810fa860 sys_lseek
[...]
8.2.3 IDT
Une autre technique de hooking consiste à modifier l'Interrupt Descriptor Table. Cette table linéaire de 256 entrées contient un pointeur vers le gestionnaire d'interruptions. Brièvement, une interruption est un événement nécessitant une attention soudaine et qui va altérer la séquence d'instruction qui était en cours d'exécution par le processeur. Les sources d'une interruption peuvent provenir du matériel (Entrées/Sorties) ou peuvent être logicielles (par exemple avec l'instruction int3 utilisée par les debuggers pour poser un breakpoint). Le lecteur intéressé par les techniques de hooking IDT peut se reporter à l'article Handling Interrupt Descriptor Table for fun and profit paru dans Phrack [PHRACK].
La commande permettant de vérifier l’intégrité de l'IDT se nomme linux_check_idt et fonctionne sur le même principe de comparaison d'adresse que la commande linux_check_syscall.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_check_idt
Volatility Foundation Volatility Framework 2.3.1
Index Address Symbol
------------------ ------------------ ------------------------------
0x0 0xffffffff81356ab0 divide_error
0x1 0xffffffff8134ff80 debug
0x2 0xffffffff81350390 nmi
0x3 0xffffffff8134ffc0 int3
0x4 0xffffffff81356ad0 overflow
0x5 0xffffffff81356af0 bounds
0x6 0xffffffff81356b10 invalid_op
0x7 0xffffffff81356b30 device_not_available
0x8 0xffffffff81356b50 double_fault
0x9 0xffffffff81356b80 coprocessor_segment_overrun
0xa 0xffffffff81356ba0 invalid_TSS
0xb 0xffffffff81356bd0 segment_not_present
0xc 0xffffffff81350000 stack_segment
0xd 0xffffffff813500a0 general_protection
0xe 0xffffffff813500d0 page_fault
0xf 0xffffffff81356c00 spurious_interrupt_bug
0x10 0xffffffff81356c20 coprocessor_error
0x11 0xffffffff81356c40 alignment_check
0x12 0xffffffff81350130 machine_check
0x13 0xffffffff81356c70 simd_coprocessor_error
0x80 0xffffffff813573f0 ia32_syscall
8.3 Le jeu des différences
Une autre technique utilisée pour la détection d'anomalie repose sur la corrélation d'informations entre différentes sources. Selon les informations concernées, différents types de structures auront un accès à celles-ci. Si le code malveillant ne modifie pas tous ces pointeurs, il devient possible de détecter ce qui est dissimulé par ce biais. De plus, l'objectif d'un code malveillant, comme tout processus, est de se voir allouer du temps processeur. Même si son objectif est de rester le plus furtif possible, il aura toujours besoin d’être accessible d'une manière ou d'une autre.
8.3.1 Processus
Il est possible de dissimuler un processus en modifiant les pointeurs next et prev des task_struct précédentes et suivantes respectivement. Bien que le processus ne soit plus accessible depuis la liste doublement chaînée des task_struct, cela ne l'empêche pas d'obtenir du temps CPU pour continuer son exécution. En effet, afin de gérer l'exécution des processus, le noyau utilise un gestionnaire de tâches ou scheduler. Sa structure essentielle se nomme runqueue. Sur une architecture multiprocesseur, chaque CPU se voit attribuer une structure runqueue. Le champ le plus intéressant de cette structure est arrays qui est un tableau contenant la liste des processus actifs ou endormis. Ces deux éléments du tableau représentent une structure de type prio_array_t qui inclut 140 listes doublement chaînées représentant toutes les priorités existantes pouvant être attribuées à un processus. Le scheduler travaille directement à partir de ces listes pour gérer l'exécution des processus. Il faut savoir qu'au moment de la rédaction de l'article, il n'existait pas encore de module Volatility capable d'extraire les informations relatives aux runqueues.
Cependant d'autres structures du noyau permettent actuellement de mettre en avant des comportements potentiellement malicieux.
Comme vu précédemment, le noyau maintient un cache des objets alloués. Àpartir de celui-ci, il est donc possible d'obtenir des informations relatives aux allocations de la structure task_struct définissant un processus. C'est précisément la fonctionnalité fournie par la commande linux_pslist_cache qui va lister toutes les task_struct présentes dans le cache du mécanisme d'allocation SLAB. Par défaut, la commande va uniquement travailler sur les structures allouées, mais il est possible d'obtenir également une liste des éléments non alloués en fournissant l'argument -u à la commande. Il est ainsi possible de récupérer des informations relatives aux processus ayant pu être alloués précédemment.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran2.lime linux_pslist_cache
Volatility Foundation Volatility Framework 2.3.1
Offset Name Pid Uid Gid DTB Start Time
------------------ -------------------- --------------- --------------- ------ ------------------ ----------
0xffff88000f0d7040 rs:main Q:Reg 1953 0 0 0x000000000c6db000 2014-08-04 15:21:51 UTC+0000
0xffff88000f0d7740 sudo 2723 0 1000 0x000000000c7f0000 2014-08-04 15:29:29 UTC+0000
0xffff88000c5e2180 acpid 1992 0 0 0x000000000c6fd000 2014-08-04 15:21:52 UTC+0000
0xffff88000c5e2880 bash 2722 1000 1000 0x000000000f318000 2014-08-04 15:29:29 UTC+0000
0xffff88000c62b0c0 rpc.statd 1593 102 65534 0x000000000fadf000 2014-08-04 15:21:51 UTC+0000
0xffff88000c62b7c0 insmod 2724 0 0 0x000000000c7e1000 2014-08-04 15:29:29 UTC+0000
0xffff88000c6d0140 wget 2725 1000 1000 0x000000000c7c9000 2014-08-04 15:29:31 UTC+0000
[...]
0xffff88000f0ab1c0 nc 2570 1000 1000 0x000000000db69000 2014-08-04 15:23:47 UTC+0000
[...]
0xffff88000c59a100 nginx 2362 33 33 0x000000000e0ef000 2014-08-04 15:21:52 UTC+0000
0xffff88000c59a800 rsyslogd 1955 0 0 0x000000000c6db000 2014-08-04 15:21:51 UTC+0000
0xffff88000f089180 vminfo 2407 0 0 0x000000000c847000 2014-08-04 15:21:53 UTC+0000
0xffff88000f089880 cron 2061 0 0 0x000000000e08d000 2014-08-04 15:21:52 UTC+0000
0xffff88000d62d100 dhclient 1734 0 0 0x000000000d2eb000 2014-08-04 15:21:51 UTC+0000
0xffff88000d62d800 getty 2419 0 0 0x000000000e003000 2014-08-04 15:21:53 UTC+0000
0xffff88000fae0180 nginx 2364 33 33 0x000000000e296000 2014-08-04 15:21:52 UTC+0000
Une autre technique possible repose sur l'analyse des tables de hash de PID obtenues au travers du symbole pid_hash. Il existe quatre tables de hash différentes dans le noyau Linux permettant de récupérer un descripteur de processus à partir de son PID. Un de ces mécanismes de traduction est par exemple utilisé lors de l'appel système kill. La commande linux_pidhashtable est en charge d'analyser ces informations.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran2.lime linux_pidhashtable
Volatility Foundation Volatility Framework 2.3.1
Offset Name Pid Uid Gid DTB Start Time
------------------ -------------------- --------------- --------------- ------ ------------------ ----------
0xffff88000f1dc140 nginx 2365 33 33 0x000000000c485000 2014-08-04 15:21:52 UTC+0000
0xffff88000d62d100 dhclient 1734 0 0 0x000000000d2eb000 2014-08-04 15:21:51 UTC+0000
[...]
0xffff88000f0ab1c0 nc 2570 1000 1000 0x000000000db69000 2014-08-04 15:23:47 UTC+0000
0xffff88000f222740 bash 2426 1000 1000 0x000000000c802000 2014-08-04 15:22:14 UTC+0000
0xffff88000c5b3840 rpcbind 1562 0 0 0x000000000f1ae000 2014-08-04 15:21:50 UTC+0000
[...]
Bien qu'il soit possible d'utiliser les deux commandes précédentes de manière séparée, il est plutôt recommandé de recourir à la commande linux_psxview qui croise les informations de linux_pslist, linux_pslist_cache et linux_pidhashtable et qui, comme présenté dans la figure suivante, affiche le nom de chaque processus et leur présence dans chacune des trois informations obtenues.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran2.lime linux_psxview
Volatility Foundation Volatility Framework 2.3.1
Offset(V) Name PID pslist pid_hash kmem_cache
------------------ -------------------- ------ ------ -------- ----------
0xffff88000f89e740 init 1 True True True
0xffff88000f89e040 kthreadd 2 True True True
[...]
0xffff88000c5b3140 sshd 2425 True True True
0xffff88000f222740 bash 2426 True True True
0xffff88000d975800 kthread 2568 True True True
0xffff88000c5b0780 dlexec 2569 True True True
0xffff88000d64d840 sshd 2586 True True True
0xffff88000c58d740 sshd 2588 True True True
0xffff88000c7f37c0 bash 2589 True True True
0xffff88000c5e2880 bash 2722 True True True
0xffff88000f0d7740 sudo 2723 True True True
0xffff88000c62b7c0 insmod 2724 True True True
0xffff88000c6d0140 wget 2725 True True True
[...]
0xffff88000f0ab1c0 nc 2570 False True True
[...]
8.3.2 Modules noyau
De la même manière qu'un processus peut être caché en étant retiré de la liste task_struct, il est possible d'en faire autant avec la liste des modules noyaux chargés telle que présentée par la commande habituellement utilisée : lsmod. À titre informatif, le fichier /proc/modules est basé sur l'export de cette liste chaînée en question et c'est sur ce fichier que repose l'implémentation de la commande habituelle lsmod. Ce que fait Volatility pour détecter cette dissimulation est de procéder à une comparaison entre cette liste chaînée et le contenu du répertoire /sys/module qui représente chaque module par un répertoire portant son nom. Ce dernier contient à son tour les informations du module en question. Cette représentation « système de fichiers » repose en fait sur l'utilisation d'une nouvelle structure. Pour y accéder, il faut que le noyau ait été compilé avec le support de sysfs ce qui permet d'obtenir le symbole module_kset de type kset contenant un champ kobj. Ce champ donne ensuite accès à la liste chaînée de type kobject. La commande linux_check_modules fait par la suite une différence entre ces deux listes chaînées pour mettre en avant les modules suspicieux.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran2.lime linux_check_modules
Volatility Foundation Volatility Framework 2.3.1
Module Name
-----------
suterusu
8.4 Keylogging
Comme mentionné sur le wiki du projet, Volatility permet de mettre en avant deux techniques utilisées pour keylogger et présentées dans le papier [KEYLOGGERS].
8.4.1 TTY
La première technique repose sur la pose d'un contournement de la fonction receive_buff du pilote tty. La commande Volatility en charge de détecter ce hook se nomme linux_check_tty. Elle va dans un premier temps récupérer et itérer sur la liste des pilotes tty obtenue par le symbole tty_drivers. À chaque pilote est associé un tableau contenant les périphériques tty et qui sont représentés par la structure tty_struct. Pour chacune d'entre elles, la commande accède ensuite au champ relatif à la structure définissant les opérations propres aux tty line discipline présentée dans le papier précédemment cité : ldisc.ops. Cet accès permet finalement de pointer sur la fonction receive_buf. L'adresse de cette fonction obtenue, il suffit de vérifier si celle-ci est associée à un symbole du noyau (get_symbol_by_address("kernel", recv_buf)). Si tel n'est pas le cas, la commande affichera HOOKED dans la colonne Symbol.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_check_tty
Volatility Foundation Volatility Framework 2.3.1
Name Address Symbol
---------------- ------------------ ------------------------------
tty1 0xffffffff8122b296 n_tty_receive_buf
tty2 0xffffffff8122b296 n_tty_receive_buf
tty3 0xffffffff8122b296 n_tty_receive_buf
tty4 0xffffffff8122b296 n_tty_receive_buf
tty5 0xffffffff8122b296 n_tty_receive_buf
tty6 0xffffffff8122b296 n_tty_receive_buf
8.4.2 Keyboard notifier chains
Le côté intéressant de cette technique réside dans le fait qu'aucune modification de structure en mémoire n'est faite, mais qu'elle repose sur l'abus d'une API du noyau Linux existante depuis la version 1.1 : les notifiers chains. De manière simple, cette API permet d'associer à un événement système une liste chaînée de fonctions de rappel (callbacks). Lorsque l'événement en question se produit, la liste chaînée est parcourue et chaque fonction appelée. D'un point de vue programmation, ce mécanisme est équivalent au design pattern Observer. Vous l'aurez deviné, pour être informé d'un événement, il suffit d'ajouter sa callback à cette liste chaînée. Et depuis la version 2.6.24, il existe une chaîne de notification relative aux frappes clavier.
De la même manière que la commande linux_check_tty, la commande linux_keyboard_notifier va donc vérifier si les adresses de fonction de rappel enregistrées dans la liste chaînée keyboard_notifier_list correspondent à l'adresse d'un symbole du noyau ou non.
$ python vol.py --profile LinuxDebianWheezy7_5-3_2_0-4-amd64x64 -f aldebaran.lime linux_keyboard_notifier
Volatility Foundation Volatility Framework 2.3.1
Address Symbol
------------------ ------------------------------
0xffffffffa035f664 HOOKED
Conclusion
Cet article a présenté la majorité des différentes commandes utilisables par Volatility sur un système GNU/Linux. Résumer toutes les informations utilisées en un article n'est pas chose aisée notamment quand des livres sont dédiés à un seul sous-système du noyau. J'en profite pour attirer l'attention sur la sortie en juillet dernier d'une bible de 900 pages rédigée par l'équipe de développement de Volatility intitulé The Art of Memory Forensics traitant à la fois les systèmes Windows, Linux et Mac. Enfin, certaines informations ne sont pas encore utilisées et des pans entiers de structures n'ont pas été présentés, donnant la possibilité de développer de nouveaux modules à qui le veut.
Remerciements
Merci à Cédric et Jean-François pour leur relecture, Samir pour ses remarques et conseils pertinents ainsi qu'à Guillaume pour son extrême patience...
Bibliographie
[DFRWS14] http://www.dfrws.org/2014eu/program.shtml
[LMAP] https://code.google.com/p/rekall/source/browse/#git%2Ftools%2Flinux%2Flmap
[VOLINUX] http://code.google.com/p/volatility/wiki/LinuxCommandReference23
[VOLUSAGE] http://code.google.com/p/volatility/wiki/VolatilityUsage23
[VBOX] http://www.digital-forensic.org/media/vms/misc.zip
[SUTERUSU] https://github.com/mncoppola/suterusu
[ROUTECACHE] https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=89aef8921bfbac22f00e04f8450f6e447db13e42
[PHRACK] http://www.phrack.org/issues/59/4.html#article
[KEYLOGGERS] http://www.ieee-security.org/TC/SPW2012/proceedings/4740a097.pdf