mPhrack Inc.==
Volume 0x0c, Issue 0x41, Phile #0x04 of 0x0f
|=-----------------------------------------------------------------------=|
|==[ Hooking furtif : Une autre façon de compromètre le noyau Windows ]==|
|=-----------------------------------------------------------------------=|
|=--------------------=[ par mxatone et ivanlef0u ]=---------------------=|
|=---------------------=[v2 by taz pour arsouyes ]=----------------------=|
1 - Introduction on anti-rookits technologies and bypass
1.1 - Rookits and anti-rootkits techniques
1.2 - About kernel level protections
1.3 - Concept key: use kernel code against itself
2 - Introducing stealth hooking on IDT.
2.1 - How Windows manage hardware interrupts
2.1.1 - Hardware interrupts dispatching on Windows
2.1.2 - Hooking hardware IT like a ninja
2.1.3 - Application 1 : Kernel keylogger
2.1.4 - Application 2 : NDIS incoming packets sniffer
2.2 - Conclusion about stealth hooking on IDT
3 - Owning NonPaged pool using stealth hooking
3.1 - Kernel allocation layout review
3.1.1 - Difference between Paged and NonPaged pool
3.1.2 - NonPaged pool tables
3.1.3 - Allocation and free algorithms
3.2 - Getting code execution abusing allocation code
3.2.1 - Data corruption of MmNonPagedPoolFreeListHead
3.2.2 - Expend it for every size
3.3 - Exploit our position
3.3.1 - Generic stack redirection
3.3.2 - Userland process code injection
4 - Detection
5 - Conclusion
6 - References
---[ 1 - Introduction aux techniques anti-rootkits et à leur contournement
Les rootkits et anti-rootkits actuels prennent une place de plus en plus
prépondérante dans le monde de la sécurité des TIC. Aimés par certains,
détestés par d'autres, les rootkits peuvent être considérés comme le
saint-graal des backdoors : furtif, petit, proche du hard, ingénieux,
vicieux... Leurs capacités de contrôle sur un ordinateur, localement ou
à distance, les rendent incontournables pour un attaquant.
Les anti-rootkits tentent de détecter ces sales petits programmes.
Les techniques des Rk et leur complexité évoluent et, de nos jours, le
développement d'un rootkit ou d'un anti-rootkit est une tâche difficile.
Cet article traitera des rootkits sur plateforme Windows et plus
précisément des nouvelles techniques d'hijacking qui peuvent être appliquées
au noyau Windows.
On considèrera que les lecteurs sont au courant des techniques employées
par les rootkits sous Windows.
----[ 1.1 - Rootkits, anti-rootkits et techniques associées
Un rootkit détourne le comportement d'un OS. Pour ce faire, il peut simplement
modifier les binaires du système d'exploitation, mais ce n'est pas très furtif.
La plupart des rootkits utilisent des hooks sur des fonctions clef, et changent
leurs résultats. Un hook basique redirige le flot d'execution en altérant le
début de la fonction ou un pointeur de fonction, mais il n'y a pas qu'une seule
façon d'y parvenir. L'exemple le plus commun est l'utilisation de la SSDT
(System Service Descriptor Table : Table des descripteurs des services systèmes).
Cette table contient la liste des appels systèmes (NDT: syscall), qui est en
fait un ensemble de pointeurs sur fonctions. Si vous pouvez modifier un pointeur
dans cette table, vous êtes en mesure de contrôler le comportement d'une de
ces fonctions. C'est une illustration du fonctionnement d'un rootkit, evidement
il existe de nombreuses zones suceptibles d'etre visé par un attaquant.
Les Anti-rootkits tentent de contrôler ces zones, mais la tâche est ardue.
La plupart du temps, les logiciels anti-rootkits font une comparaison entre une
image de la mémoire du programme et son binaire sur le disque, ou vérifient
quelques tables de pointeurs sur fonctions pour voir si quelque chose a changé.
C'est ainsi que la guerre entre les créateurs de rk et les anti-rk-junkies a
commencé, par la recherche du meilleur moyen, de la meilleur zone, pour
hooker les fonctionnalités critiques du système d'exploitation. Sous Windows,
les zones suivantes sont fréquemment utilisées par les rootkits :
- la SSDT (table des appels de fonction noyau) et la shadow SSDT (table des
appels win32k) représentent le chemin le plus direct.
- les MSR (Model Specific Registers) peuvent être modifiés par un rootkit.
Sous windows, le MSR_SYSENTER_EIP est utilisé par l'instruction assembleur
'sysenter' pour entrer en mode ring0. Détourner ce MSR permet à un attaquant
de contrôler le système.
- Les MajorFunctions (NDT: les fonctions majeures ;) sont des fonctions utilisées
par les drivers pour traiter les entrées/sorties avec les autres périphériques,
hooker ces fonctions peut être utile pour un rootkit.
- L'IDT (Interrupt Descriptor Table : Table des descripteurs d'interruption)
est la table utilisée par le système pour intercepter et traiter les exceptions
et les interruptions.
Une autre famille de techniques est apparue. En accédant aux objets du noyau,
un rootkit peut aisément altérer les informations des processus, threads,
modules chargés et autres. Ces techniques sont désignées sous
le nom de DKOM (Direct Kernel Object Manipulation : manipulation directe
des objets du noyau). Par exemple, le noyau Windows maintient une liste
doublement chaînée nommée PsActiveProcessList (structures EPROCESS)
contenant les informations sur les processus courants. Détachez l'un
d'entre eux de la liste et votre processus disparaitra de la liste des
tâches du task manager, alors qu'il tourne toujours.
Pour parer à ce genre de modification des objets noyau, les anti-rk vérifient
d'autres sections. Pour les processus, ils lisaient la PspCidTable qui
contient une table des PID (Process IDentifier) et des TID (Thread
IDentifier). Une comparaison entre ces tables et PsActiveProcessList
montre les processus cachés. Contre ces attaques, les outils anti-rk
doivent trouver d'autres sections et des astuces pour détecter les
objets altérés.
L'un des premiers articles sur la furtivité sous Windows a été écrit par
Holy Father, "Invisibility on NT boxes"[1]. Avec cet article est apparu
la première implémentation publique d'un rootkit avec driver ring0,
Hacker Defender [2], codé par Holy Father et Ratter du fameux e-zine
consacré aux Virus: 29A [3]. Ce driver était capable d'élever les droits
d'un processus en manipulant un jeton (NDT : Token Manipulation).
Le reste du rootkit utilise des hooks en user-land pour le camouflage au
niveau des fichiers et du registre, et l'infection de processus avec
injection de dll. Le NT Rootkit de Greg Hoglund est un bon exemple de
rootkit purement ring0 [4], ce driver utilise des hooks dans la SSDT pour
rester furtif. Il référence un Filter Device Object au-dessus du système
de fichiers NTFS et au-dessus du périphérique clavier pour filtrer les
IRP (I/O Request Packets : paquet de demande d'Entrée/Sortie). Il fournit
aussi un driver de protocole NDIS pour cacher les communications sur le
réseau. Même si ce rootkit fut écrit pour NT4.0 et Win2k, c'est un
parfait exemple pour débutants.
Ensuite, des rootkits ring0 plus aboutis comme FU [5], écrit par Fuzen_op et
son amélioration FUto ont été publiés dans le fameux journal technique
Uninformed [6]. Les améliorations de Vista sur la vérification des drivers
introduit de nouveaux rootkits basé majoritairement sur des fonctionnalités
hardware, comme BootRoot [7] et Pixie [8] par Eeye, chargé avant toute
protection. Pour finir, Joanna Rutkowska avec sa Pilule bleue (Blue Pill)
[9] a utilisé les technologies de virtualisation pour créer une couche entre
le système d'exploitation et le matériel.
Dans la nature, les rootkits sont utilisés principalement pour une minable
production de spams ou la construction de botnets. Ils utilisent souvent
des techniques qui datent mais certains restent intéressants comme la
Série des Rustock [10] ou StormWorm[11] et le rootkit MBR [12]. Ils
implémentent un paquet d'astuces comme l'ADS (Alternate Data Stream),
l'obfuscation de code, de l'anti-debug, anti-VM ou du code polymorphique.
Le but n'est pas seulement de compromettre le noyau, mais aussi de ralentir
leur analyse et de les rendre plus durs à vaincre.
Même si la technologie employée par les rootkits est de plus en plus
sophistiquée, la communauté underground développe toujours des POCs pour
améliorer les techniques actuelles. Unreal [13] et AK992[14] en sont deux
superbes exemples. Le premier utilise un ADS et des hooks de MajorFunctions
NTFS pour se cacher, le second vérifie la complétion des IRP lorsqu'elles
sont envoyées au périphérique disque. Vous pouvez trouver plein d'exemples
de techniques de rootkits sur rootkit.com.
Finalement, cette partie ne serait pas complète sans parler d'anti-rk.
Le plus fameux est Rk Unhooker, par MP_ART et EP_X0FF et leur team UG North.
Les autres anti-rk sont DarkSpy [15] par CardMagic, IceSword [16] par pjf et
Gmer [17].
----[ 1.2 - À propos des protections au niveau noyau
Quand on parle de protections, il faut trouver où la protection agit dans
le système. Une protection a un avantage sur une attaque uniquement si elle
opère depuis un niveau supérieur. Les protections comme PaX ou Exec Shield
sont efficaces parce qu'elles protègent l'userland depuis le noyau.
Les protections comme PatchGuard et autres HIPS protègent aussi l'intégrité
du système, mais aussi longtemps qu'un attaquant trouvera un moyen
d'attaquer à leur propre niveau, elles seront inutiles. Une protection
est fiable seulement si elle ne peut être corrompue par un attaquant.
Sachant qu'un attaquant trouve un moyen d'injecter du code
dans la protection... PWNED COMME IVANLEF0U @#!!!.
C'est pourquoi PatchGuard n'est pas si efficace [18]. Mais nous savons
que désactiver ou détruire une protection n'est pas discret. Non, le
meilleur chemin consiste à voler sous les radars en travaillant avec
des objets spéciaux
et des events qui ne peuvent être vérifiés à cause de leur volatilité.
En juin 2006, Greg Hoglund a présenté le concept de KOH (Kernel Object
Hooking) [19]. C'est une nouvelle façon de dévier l'execution du code : pas
besoin de modifier des sections statiques de code, mais on travaille
plutôt sur des structures/codes alloués dynamiquement comme les DPC
(Deferred Procedure Call). Pour les protections, c'est difficile de trouver
et vérifier ces zones à cause de leur instabilité.
D'autres objets cools sont les IRP. Ils sont utilisés par le
Gestionaire d'Entrée/Sortie du noyau Windows pour communiquer avec les
périphériques. Chaque opération d'Entrée/Sortie sur le materiel génère
une IRP, les syscalls envoient un IRP à un driver à travers son
interface. En général, un driver possède plusieur interfaces : l'une
d'entre elles est utilisée pour communiquer avec l'userland en utilisant
des IOCTL et d'autres interfaces gèrent les IRP en les filtrant ou en
effectuant la tâche demandée. Les IRP sont envoyés à un driver en
utilisant sa table des MajorFunctions. Cette table inclut les différentes
fonctionnalités fournies par le driver. Vous pouvez vérifier le résultat
retourné par une MajorFunction en installant une routine de complétion
sur un IRP. Ce sont des objets très volatiles, les rendant très durs à
contrôler et à vérifier.
En fait, si vous vouliez vérifier tout, vous auriez besoin de re-concevoir
entièrement l'architecture du système d'exploitation. Donc gardez bien
en tête qu'une protection ne peut être partout à tout moment et nous
le démontrerons dans les parties suivantes.
----[ 1.3 - Concept clef : Retourner le code du noyau contre lui
L'idée derrière cet article c'est l'exploitation du code noyau.
l'Exploitation est possible parceque les entrées déterminent le
comportement du code. En soumettant une entrée forgée à un logiciel
vulnérable peut conduire à une execution de code. l'entrée dangereuse
est bien entendu définie par votre cible. l'espace noyau contient plus
de scenarios d'exploitation parceque vous pouvez changer son environement.
Un rootkit ne peut pas changer des entrés basiques en arguments. Mais il
peut changer l'environement autour d'un code. les techniques d'exploitation
par le tas (Heap exploitation) comme l'unlinking en est un parfait
exemple. En changeant la structure d'un bloc mémoire, vous pouvez re-écrire
4 bytes. Certaines techniques peuvent meme changer le prochain bloc
d'adresse alloué [20]. ça marche parcequ'un programme ne remet pas en
cause ces informations. Dans le noyau, vous avez un contrôle total sur
l'environement. De plus vérifier totalement le noyau dégraderais les
performances et serait totalement impossible.
L'altérationn de l'environement du code à été utilisé avec succès pour
la technique du rootkit phide2 [21]. Ce rootkit peut cacher des threads
sans hooker l'ordonanceur windows, ce qui est très impressionnant. Commme
il repose sur le comportement du code, il nécessite une connaissance solide
du reverse engineering. Il étend ce concept à des comportements de l'OS
méconus. Les protections génériques sont basé sur des prédicats
génériques. Tel que la vérification seule des images des drivers à la
recherche de hooks dans le code. Actuellement, le design des OS joue
contre ces protections et requière des techniques logicielles
avancés issue des rootkits.
---[ 2 - Introduction au hooking furtif sur IDT
Introduisont maintenant notre concept de hooking furtif avec un exemple
basé sur les IDT. Premièrement nous verrons que ce qu'est l'IDT et à quoi
il sert. Ensuite nous discuterons des interruptions hardware et de la
manière dont Windows les traite.
L'IDT (Interrupt Descriptor Table) est une table linéaire spécifique au CPU
et localisée dans le kernel-land. l'IDT peut être lue ave des privilèges
ring3 mais vous avez besoin des privilèges ring0 si vous voulez y écrire.
l'IDT est composé de 256 entrées vers des structures KIDTENTRY et vous
pouvez utiliser le debugger noyau (KD: Kernel Debugger)
inclue dans les outils de debug Windows [22] pour voir les définitions
d'une entrée IDT.
kd> dt nt!_KIDTENTRY
+0x000 Offset : Uint2B
+0x002 Selector : Uint2B
+0x004 Access : Uint2B
+0x006 ExtendedOffset : Uint2B
Nous ne voulons pas re-expliquer ici l'architecture de l'IDT, nous vous
conseillons donc de lire l'article de Kad publié dans le phrack 59 à propos
de l'IDT et de ses principes de fonctionnement.
Les 32 premières entrés de l'IDT sont réservés par le CPU pour les exceptions.
Les autres sont utilisés pour traiter les interruptions hardware et certains
évenement système spéciaux.
Voici un dump des 64 premières entrés de l'IDT Windows :
kd> !idt -a
Dumping IDT:
00: 804df350 nt!KiTrap00
01: 804df4cb nt!KiTrap01
02: Task Selector = 0x0058
03: 804df89d nt!KiTrap03
04: 804dfa20 nt!KiTrap04
05: 804dfb81 nt!KiTrap05
06: 804dfd02 nt!KiTrap06
07: 804e036a nt!KiTrap07
08: Task Selector = 0x0050
09: 804e078f nt!KiTrap09
0a: 804e08ac nt!KiTrap0A
0b: 804e09e9 nt!KiTrap0B
0c: 804e0c42 nt!KiTrap0C
0d: 804e0f38 nt!KiTrap0D
0e: 804e164f nt!KiTrap0E
0f: 804e197c nt!KiTrap0F
10: 804e1a99 nt!KiTrap10
11: 804e1bce nt!KiTrap11
12: 804e197c nt!KiTrap0F
13: 804e1d34 nt!KiTrap13
14: 804e197c nt!KiTrap0F
15: 804e197c nt!KiTrap0F
16: 804e197c nt!KiTrap0F
17: 804e197c nt!KiTrap0F
18: 804e197c nt!KiTrap0F
19: 804e197c nt!KiTrap0F
1a: 804e197c nt!KiTrap0F
1b: 804e197c nt!KiTrap0F
1c: 804e197c nt!KiTrap0F
1d: 804e197c nt!KiTrap0F
1e: 804e197c nt!KiTrap0F
1f: 804e197c nt!KiTrap0F
20: 00000000
21: 00000000
22: 00000000
23: 00000000
24: 00000000
25: 00000000
26: 00000000
27: 00000000
28: 00000000
29: 00000000
2a: 804deb92 nt!KiGetTickCount
2b: 804dec95 nt!KiCallbackReturn
2c: 804dee34 nt!KiSetLowWaitHighThread
2d: 804df77c nt!KiDebugService
2e: 804de631 nt!KiSystemService
2f: 804e197c nt!KiTrap0F
30: 806f3d48 hal!HalpClockInterrupt
31: 80dd816c i8042prt!I8042KeyboardInterruptService (KINTERRUPT 80dd8130)
32: 804ddd04 nt!KiUnexpectedInterrupt2
33: 80dd3224 serial!SerialCIsrSw (KINTERRUPT 80dd31e8)
34: 804ddd18 nt!KiUnexpectedInterrupt4
35: 804ddd22 nt!KiUnexpectedInterrupt5
36: 804ddd2c nt!KiUnexpectedInterrupt6
37: 804ddd36 nt!KiUnexpectedInterrupt7
38: 806edef0 hal!HalpProfileInterrupt
39: 80f0827c ACPI!ACPIInterruptServiceRoutine (KINTERRUPT 80f08240)
3a: 80dc67cc vmsrvc+0x1C16 (KINTERRUPT 80dc6790)
3b: 80df6414 NDIS!ndisMIsr (KINTERRUPT 80df63d8)
3c: 80de040c i8042prt!I8042MouseInterruptService (KINTERRUPT 80de03d0)
3d: 804ddd72 nt!KiUnexpectedInterrupt13
3e: 80ed78a4 atapi!IdePortInterrupt (KINTERRUPT 80ed7868)
3f: 80f01dd4 atapi!IdePortInterrupt (KINTERRUPT 80f01d98)
40: 804ddd90 nt!KiUnexpectedInterrupt16
[...]
Ce dump présente une IDT windows typique, vous pouvez voir l'index
des entrés de l'IDT suivie par l'adresse du handler et son nom.
les 32 premières entrés sont renseigné avec la fonction KiTrap* qui
traite les exceptions. Le reste de la table est laissé au système, vous
pouvez voir les interruptions système spéciales comme KiSystemService et
KiCallbackReturn et les handlers utilisé par les driver come
I8042KeyboardInterruptService ou I8042MouseInterruptService.
----[ 2.1 - Comment Windows gère les interruptions materielles
Quand on pare d'interruptions, on doit introduire le concept d'IRQL
(Interrupt ReQuest Level : Niveau d'une requete d'interruption).
La représentation interne des IRQLs se fait sous la forme d'un nombre
de 0 a 31 sur archi x86, les nombres les plus élevés représentant
les interruptions les plus prioritaires. Le noyau définit aussi un jeu
standard d'IRQLs pour les interruptions logicielles, le HAL (Hardware
Abstraction Layer) mappe les interruptions hardware avec les IRQLs
+----------------+
31 | Highests | \
to | IRQLs | | Clock, system failure.
27 | | /
+----------------+
26 | | \
to | DEVICE_IRQL | | Hardware interrupts.
3 | | /
+----------------+
2 | DISPATCH_LEVEL | Scheduler, DPC.
+----------------+
1 | APC_LEVEL | Used when dispatching APC.
+----------------+
0 | PASSIVE_LEVEL | Threads run at this IRQL.
+----------------+
Chaque processeur possède sa propre IRQL. vous pouvez avoir un coeur qui
tourne sous l'IRQL=DISPATCH_LEVEL alors qu'un autre tourne en PASSIVE_LEVEL.
En fait l'IRQL représente une "capacité de masquage" du code en cours. Les
interruptions en provenance d'une source avec un IRQL au dessus du niveau
courrant peuvent l'interrompre, les autres interruptions avec un IRQL égal
ou inferieur au niveau courrant sont masqué jusqu'a ce qu'un thread en
cour d'execution baisse le niveau d'IRQL.
Certains composants système ne sont pas accessibles quand le code tourne
avec un IRQL>=DISPATH_LEVEL. L'accès à de la mémoire paginé (mémoire pouvant
être swappé sur le disque) est impossible et un grand nombre de fonctions
du noyau ne peuvent être utilisés.
les interruptions hardware sont asynchrones et sont levés par les
périphériques externes. Par exemple quand vous pressez une touche, votre
clavier envoie un IRQ (Interrupt ReQuest: demande d'interruption) routé
par le southbridge [24] sur le controleur d'interruption au travers du
northbridge [25]. le southbridge est une puce qui peut être décrite comme
un hub de controle des Entrés/Sorties. cette puce reçoit toutes les
interruptions d'Entrés/Sorties externes et les envoie au northbridge.
le northbridge est directement connecté à votre mémoire et au bus
graphique à haute vitesse ainsi qu'a votre CPU. Cette puce est aussi
connue comme etant le hub de contrôle mémoire.
Sur la plupart des systèmes x86 on trouve un chipset appellé i82489,
Advanced Programmable INterrupt Controller (APIC : controleur d'interruptions
programmables avancé). L'APIC est composé de 2 partie maitresses, un APIC
d'Entré/Sortie, un par CPU, et un LAPIC (Local APIC) sur chaque coeur.
l'APIC d'Entré/Sortie utilise un algo de routage pour envoyer chaque
interruption sur le coeur le plus adapté. d'après le principe de localité,
l'APIC d'E/S va envoyer l'interruption du périphérique vers le coeur
qui l'a précédement traité [26].
Après ça, le LAPIC traduit l'IRQ en une valeur sur 8 bits, le vecteur
d'interruption. ce vecteur d'interruption représente l'index de l'IDT
associé avec le handler. quand le coeur est disposé à traiter l'interruption,
son flow d'execution est redirigé vers l'entré de l'IDT correspondante.
IDT IDT IDT IDT
1 2 3 4
+---+ +---+ +---+ +---+
| | | | | | | |
|---| |---| |---| |---|
| | | | | | | |
|---| |---| |---| |---|
| | | | | | | |
+---+ +---+ +---+ +---+
| | | |
+--------+ +--------+ +--------+ +--------+
| | | | | | | |
| core 1 | | core 2 | | core 3 | | core 4 |
| | | | | | | |
+--------+ +--------+ +--------+ +--------+
| LAPIC | | LAPIC | | LAPIC | | LAPIC |
+---+----+ +---+----+ +---+----+ +---+----+
| | | |
| | | |
<---+--------------+------+-------+-------------+----->
Interrupt | Processor system bus
Messages |
|
|
External +------+------+
Interrupts | |
---------------> I/O APIC |
| |
+-------------+
-----[ 2.3.1 Répartition des interruptions materielles sous Windows
Sous windows, le handler de l'interruption n'est pas executé immédiatement,
il y a un template de code qui le précède. Ce template est implémenté
dans la fonction KiInterruptTemplate et fait deux choses : Premièrement,
il sauve l'etat courrant du coeur dans la pile et dirige le flot du code
vers le bon "interrupt dispatcher" (NDT: répartiteur d'interuption).
Quand une interruption est levée, après la sauvegarde du status du coeur,
le flot d'execution est transféré vers le haldler d'interruption comme
définit dans l'IDT. En fait chaque gestionnaire d'interruption dans l'IDT
pointe vers une routine KiInterruptTemplate [27]. KiInterruptTemplate va
appeller KiInterruptDispatch, qui effectuera les opérations suivantes :
- Acceder a la routine fournissant le spinlock (NDT:verrou tournant)
- Elever l'IRQL jusqu'au DEVICE_IRQL, l'IRQL d'un vecteur d'interruption
donné est calculé en soustrayant le vecteur d'interruption à 27d.
- Appeller le handler de l'interruption, l'ISR (Interrupt Service Routine).
- Abaisser l'IRQL.
- Liberer la routine fournissant le spinlock.
Par exemple, l'ISR du périphérique clavier est I8042KeyboardInterruptService.
les ISR sont des routines pour traiter les interruption comme le top-halve
dans le noyau linux. D'après le WDK (Windows Driver Kit), les ISR doivent
tout faire sur le périphérique pour dégager l'interruption. Ensuite il doit
faire le strict minimum pour sauvegarder les informations utiles et rajouter
une DPC dans la queue. Cela signifie que la gestion de l'interruption
se fera à une IRQL plus basse que durant l'execution de l'ISR. le
traitement étant reporté dans le DPC.
Les DPC (Deferred Procedure Call) sont équivalente aux bottom-halves dans
linux. les DPC travaille à l'IRQL DISPATCH_LEVEL, en dessous de l'IRQL des
ISR. En fait, l'ISR va rajouter dans la file d'attente une DPC pour
traiter la totalité de l'interruption à un niveau d'IRQL plus bas, pour
éviter que la préemption du coeur prenne trop de temps. Pour le clavier
la DPC est I8042KeyboardIsrDpc. Voici un shéma pour résumer le processus
d'interruption :
+-------------------------+
Interruption materielle /----> Ici nous sommes à |
| | | l'IRQL=DEVICE_LEVEL |
| | | La routine |
| | | KiInterruptDispatch |
/---> IDT ---\ | | fait appel à l'ISR. |
| | | |
| | |l'ISR gère l'interruption|
+-----------------------+ | | et met un DPC dans la |
| KiInterruptTemplate ------/ | file pour un traitement |
+-----------------------+ | Ulterieur |
+-------------------------+
KiInterruptDispatch reçoit un argument principal en provenance de
KiInterruptTemplate, un pointeur vers un objet interruption stocké dans
le registre EDI. Les objets Interrupt sont définit par la structure
KINTERRUPT :
kd> dt nt!_KINTERRUPT
+0x000 Type : Int2B
+0x002 Size : Int2B
+0x004 InterruptListEntry : _LIST_ENTRY
+0x00c ServiceRoutine : Ptr32 unsigned char
+0x010 ServiceContext : Ptr32 Void
+0x014 SpinLock : Uint4B
+0x018 TickCount : Uint4B
+0x01c ActualLock : Ptr32 Uint4B
+0x020 DispatchAddress : Ptr32 void
+0x024 Vector : Uint4B
+0x028 Irql : UChar
+0x029 SynchronizeIrql : UChar
+0x02a FloatingSave : UChar
+0x02b Connected : UChar
+0x02c Number : Char
+0x02d ShareVector : UChar
+0x030 Mode : _KINTERRUPT_MODE
+0x034 ServiceCount : Uint4B
+0x038 DispatchCount : Uint4B
+0x03c DispatchCode : [106] Uint4B
On retrouve dans cette structure, le SpinLock et la ServiceRoutine.
on peut constater que SynchronizeIrql contien l'IRQL ou sera executé
l'ISR.
Pour chaque entrée dans l'IDT qui gère une interruption hardware,
le KiInterruptTemplate est contenu dans la table DispatchCode de
la structure KINTERRUPT.
Pour le périphérique clavier on a cette KINTERRUPT:
kd> dt nt!_KINTERRUPT 80dd8130
+0x000 Type : 22
+0x002 Size : 484
+0x004 InterruptListEntry : _LIST_ENTRY [ 0x80dd8134 - 0x80dd8134 ]
+0x00c ServiceRoutine : 0xfa815495 unsigned char
->i8042prt!I8042KeyboardInterruptService+0
+0x010 ServiceContext : 0x80e2ec88
+0x014 SpinLock : 0
+0x018 TickCount : 0xffffffff
+0x01c ActualLock : 0x80e2ed48 -> 0
+0x020 DispatchAddress : 0x804da8d8 void nt!KiInterruptDispatch+0
+0x024 Vector : 0x31
+0x028 Irql : 0x1a ''
+0x029 SynchronizeIrql : 0x1a ''
+0x02a FloatingSave : 0 ''
+0x02b Connected : 0x1 ''
+0x02c Number : 0 ''
+0x02d ShareVector : 0 ''
+0x030 Mode : 1 ( Latched )
+0x034 ServiceCount : 0
+0x038 DispatchCount : 0xffffffff
+0x03c DispatchCode : [106] 0x56535554
Jettons un oeil au début du KiInterruptTemplate :
nt!KiInterruptTemplate:
804da972 54 push esp
804da973 55 push ebp
804da974 53 push ebx
804da975 56 push esi
804da976 57 push edi
804da977 83ec54 sub esp,54h
804da97a 8bec mov ebp,esp
804da97c 89442444 mov dword ptr [esp+44h],eax
804da980 894c2440 mov dword ptr [esp+40h],ecx
804da984 8954243c mov dword ptr [esp+3Ch],edx
804da988 f744247000000200 test dword ptr [esp+70h],20000h
804da990 0f852a010000 jne nt!V86_kit_a (804daac0)
804da996 66837c246c08 cmp word ptr [esp+6Ch],8
804da99c 7423 je nt!KiInterruptTemplate+0x4f (804da9c1)
804da99e 8c642450 mov word ptr [esp+50h],fs
804da9a2 8c5c2438 mov word ptr [esp+38h],ds
804da9a6 8c442434 mov word ptr [esp+34h],es
804da9aa 8c6c2430 mov word ptr [esp+30h],gs
804da9ae bb30000000 mov ebx,30h
804da9b3 b823000000 mov eax,23h
804da9b8 668ee3 mov fs,bx
804da9bb 668ed8 mov ds,ax
804da9be 668ec0 mov es,ax
804da9c1 648b1d00000000 mov ebx,dword ptr fs:[0]
804da9c8 64c70500000000ffffffff mov dword ptr fs:[0],0FFFFFFFFh
804da9d3 895c244c mov dword ptr [esp+4Ch],ebx
804da9d7 81fc00000100 cmp esp,10000h
804da9dd 0f82b5000000 jb nt!Abios_kit_a (804daa98)
804da9e3 c744246400000000 mov dword ptr [esp+64h],0
804da9eb fc cld
804da9ec 8b5d60 mov ebx,dword ptr [ebp+60h]
804da9ef 8b7d68 mov edi,dword ptr [ebp+68h]
804da9f2 89550c mov dword ptr [ebp+0Ch],edx
804da9f5 c74508000ddbba mov dword ptr [ebp+8],0BADB0D00h
804da9fc 895d00 mov dword ptr [ebp],ebx
804da9ff 897d04 mov dword ptr [ebp+4],edi
804daa02 f60550f0dfffff test byte ptr ds:[0FFDFF050h],0FFh
804daa09 750d jne nt!Dr_kit_a (804daa18)
nt!KiInterruptTemplate2ndDispatch:
804daa0b bf00000000 mov edi,0
nt!KiInterruptTemplateObject:
804daa10 e9c3fcffff jmp nt!KeSynchronizeExecution+0x2 (804da6d8)
[...]
Faut pas oublier que ce code est unique pour chaque KINTERRUPT. On a
vu précédement que KiInterruptDispatch reçoit ses argument du registre
EDI (un pointer vers le KINTERRUPT de l'interruption). dans le
KiInterruptTemplate on peut trouver ce petit code :
[...]
nt!KiInterruptTemplate2ndDispatch:
804daa0b bf00000000 mov edi,0
nt!KiInterruptTemplateObject:
804daa10 e9c3fcffff jmp nt!KeSynchronizeExecution+0x2 (804da6d8)
[...]
Ici on a un mov "edi, 0" et un jmp, mais si nous regardons le
code KiInterruptTemplate contenu dans le KINTERRUPT du clavier on a:
ffb72525 bf5024b7ff mov edi,0FFB72450h ; Keyboard KINTERRUPT
ffb7252a e9a9839680 jmp nt!KiInterruptDispatch (804da8d8)
Wow, les instructions sont modifiés! Le noyau va dynamiquement changer
ces 2 instructions dans le code du KiInterruptTemplate. dans EDI on
trouve l'objet KINTERRUPT et le branchement du saut sur KiInterruptDispatch.
Pourquoi une telle implémentation ? Parcequ'on peut facilement changer le
handler de répartition (dispatch handler). même si la plupart du temps on
a le KiInterruptDispatch, on peut aussi trouver KiFloatingDispatch ou
KiChainDispatch. KiChainDispatch est fait pour les vecteurs partagé entre
plusieurs objets interrupt et KiFloatingDispatch est un peu comme
KiInterruptDispatch, mais il sauvegarde aussi l'etat des registres flotant.
Windows fournit des API pour connecter des interruptions à l'IDT.
IoConnectInterrupt et IoConnectInterruptEx, d'après le WDK :
NTSTATUS
IoConnectInterrupt(
OUT PKINTERRUPT *InterruptObject,
IN PKSERVICE_ROUTINE ServiceRoutine,
IN PVOID ServiceContext,
IN PKSPIN_LOCK SpinLock OPTIONAL,
IN ULONG Vector,
IN KIRQL Irql,
IN KIRQL SynchronizeIrql,
IN KINTERRUPT_MODE InterruptMode,
IN BOOLEAN ShareVector,
IN KAFFINITY ProcessorEnableMask,
IN BOOLEAN FloatingSave
);
Comme vous pouvez le constater, IoConnectInterrupt renvoie dans le paramètre
InterruptObject une structure KINTERRUPT, la même qu'on retrouve dans l'IDT.
précèdement on a vu dans le KiInterruptTemplate deux labels,
KiInterruptTemplateObject et KiInterruptTemplate2ndDispatch. ces deux labels
sont utilisé par les fonctions noyau pour trouver les deux instructions dans
la KiInterruptTemplateRoutine. KeInitializeInterrupt utilise le label
KiInterruptTemplateObject pour mettre à jour le "jmp Ki*Dispatch" et la
fonction KiConnectVectorAndInterruptObject utilise KiInterruptTemplate2ndDispatch
pour modifier le "mov edi, <&Kinterrupt>".
-----[ 2.3.2 Hooker les IDT genre ninja
Maintenant Imaginez. On veut hooker l'IDT de manière furtive, on sait
qu'altérer une entrée directement n'est pas la meilleur solution.
les Anti-rootkits ne vérifient pas les les routines KiInterruptTemplate
alloués dynamiquement. Donc on peut modifier cette routine comme on veut.
Ya trois façon possibles :
- Rediriger le "jmp Ki*Dispatch" vers notre routine de dispatch, on doit
coder notre propre routine, pas bien dur.
- Modifier l'adresse du kinterrupt passé dans EDI par l'instruction
"mov edi, <&Kinterrupt>". Le nouveau KINTERRUPT sera le même que le
précédent, seul la ServiceRoutine sera modifié par nos soins.
- Créer notre propre KiInterruptTemplate, chaud...
Dans cet article, on a choisit la simplicité. on remplace le
"mov edi, <&kinterrupt>" par un "mov edi, <&OurKinterrupt>" et on
implémente notre propre ServiceRoutine. On sait que cette instruction est
suivie d'un jmp, donc avec un moteur de déassemblage, on peut retrouver
l'instruction précédent le jmp nt!KiInterruptDispatch et la modifier.
Il faut garder à l'esprit que lorsque ServiceRoutine tourne, l'interruption
n'est pas encore gérée et qu'on tourne à l'IRQL DEVICE_IRQL. ce n'est pas
une situation très pratique, parcequ'un grand nombre de fonctions du noyau
Windows ne sont pas accessibles. On sait, que la plupart des ISR ont ajouté
une DPC dans la file, donc après l'execution de l'ISR, la dernière entrée
dans la file des DPC du coeur courrant doit contenir la routine DPC de notre
Interruption.
Si on veut accèder aux données générés par l'interruption, on doit procéder
de la même façon que l'ISR. Remplacer l'ISR originale par notre propre handler
d'ISR est très dur, parcequ'il dépend trop du périphérique materiel. Mais on
sait que le vrai traitement d'Entré/Sortie est fait par la DPC, donc quand
KiInterruptTemplate appellera notre ServiceRoutine, on appellera d'abord
la ServiceRoutine originale, puis nous modifierons la dernière entrée DPC
par la notre.
Les DPC sont représenté par les structures KDPC :
kd> dt nt!_KDPC
+0x000 Type : Int2B
+0x002 Number : UChar
+0x003 Importance : UChar
+0x004 DpcListEntry : _LIST_ENTRY
+0x00c DeferredRoutine : Ptr32 void
+0x010 DeferredContext : Ptr32 Void
+0x014 SystemArgument1 : Ptr32 Void
+0x018 SystemArgument2 : Ptr32 Void
+0x01c Lock : Ptr32 Uint4B
la liste des DPC se trouve dans la structure KPRCB (Kernel Processor
Control Region Block) du processeur courrant. KPRCB est précédé par une
structure KPCR (Kernel Processor Control Block) situé en FS:[0x1C] dans
le processeur courrant. KPRCB est a 0x120 bytes en partant du début de
la structure KPCR.
dt nt!_KPRCB
[...]
+0x860 DpcListHead : _LIST_ENTRY
+0x868 DpcStack : Ptr32 Void ; DPC arguments
+0x86c DpcCount : Uint4B ; DPC core counter
+0x870 DpcQueueDepth : Uint4B ; Numbers of DPC in the list
+0x874 DpcRoutineActive : Uint4B
+0x878 DpcInterruptRequested : Uint4B
+0x87c DpcLastCount : Uint4B
+0x880 DpcRequestRate : Uint4B
+0x884 MaximumDpcQueueDepth : Uint4B
+0x888 MinimumDpcRate : Uint4B
Maintenant on sait comment retrouver le DPC de notre interruption, on
peut facilement le modifier par le notre et gérer les données.
Pour le clavier, la DPC est rajouté dans la file par KeInsertQueueDpc
dans la routine I8xQueueCurrentKeyboardInput appellée par l'ISR du
clavier.
kd> dt nt!_KDPC 80e3461c
+0x000 Type : 19 ; 19=DpcObject
+0x002 Number : 0 ''
+0x003 Importance : 0x1 ''
+0x004 DpcListEntry : _LIST_ENTRY [ 0xffdff980 - 0x80559684 ]
+0x00c DeferredRoutine : 0xfa815650 void i8042prt!I8042KeyboardIsrDpc
+0x010 DeferredContext : 0x80e343b8
+0x014 SystemArgument1 : (null)
+0x018 SystemArgument2 : (null)
+0x01c Lock : 0xffdff9c0 -> 0
Voici un shéma de l'attaque :
structure MyKinterrupt
+---------------------+
Interruption Materielle /----> MyServiceRoutine |
| | | Appel l'ISR |
| | | d'origine ------\
\---> IDT ---\ | | et modife la file | |
| | | des DPC | |
| | +---------------------+ |
+---------------------+ | |
| KiInterruptTemplate -----/ Original Kinterrupt |
+---------------------+ +---------------------+ |
Coeur | | |
+------------+ | ServiceRoutine <-----/
| | | Ajoute la DPC de |
|DpcListHead |--\ | l'ISR dans la queue |
| | | +---------------------+
+------------+ |
| +-----+ +-----+ +-----+ +-----+
\-> DPC |---->| DPC |---->| DPC |---->| DPC |-->DpcListHead
DpcListHead<---| |<----| |<----| |<----| |
+-----+ +-----+ +-----+ +-----+
/\
||
Dernière entrée DPC
Modifié après l'appel
à la ServiceRoutine.
-----[ 2.3.3 - Application 1 : keylogger noyau
C'est le moment de concevoir un POC. dans cet extrait, on verra comment
sniffer les frappes du clavier. Comme vous l'avez vu précédement, on est
maintenant capable de controler la DPC générée par une interruption.
Pour le clavier on va détourner la routine I8042KeyboardIsrDpc qui est
mise en place dans la DPC de l'interruption clavier. Avec notre propre
handler DPC, nous allons reprodure le comportement de la routine d'origine,
helas ce genre de routine est dure à écrire, on a donc arraché quelque
bouts de code et fait du reverse (notez le style feignasse de hacker).
dans notre handler DPC on doit appeller la routine
KeyboardClassServiceCallback [28], cette routine est fournie par le driver
Kbdclass. Ce callback transfert le buffer des donnés d'entré d'un périph
vers la classe data queue. Une fonction keyboard driver doit appeller
ce class service callback dans sa procédure DPC.
Voila le prototype du KeyboardClassServiceCallback :
VOID
KeyboardClassServiceCallback (
IN PDEVICE_OBJECT DeviceObject,
IN PKEYBOARD_INPUT_DATA InputDataStart,
IN PKEYBOARD_INPUT_DATA InputDataEnd,
IN OUT PULONG InputDataConsumed
);
Paramètres :
DeviceObject : Pointer vers la classe device object.
InputDataStart : Pointer vers le premier paquet de donné d'entré clavier
dans le buffer de données d'entré du port device
InputDataEnd : Pointer vers le input data packet du clavier qui suit
immédiatement le dernier data packet dans le buffer de donné d'entré
du port device.
InputDataConsumed : Pointer vers le nombre de paquets de donné d'entré
clavier qui ont été transféré par la procédure.
KEYBOARD_INPUT_DATA is defined by :
typedef struct _KEYBOARD_INPUT_DATA {
USHORT UnitId;
USHORT MakeCode;
USHORT Flags;
USHORT Reserved;
ULONG ExtraInformation;
} KEYBOARD_INPUT_DATA, *PKEYBOARD_INPUT_DATA;
Donc dans notre handler de DPC on a juste à vérifier le membre MakeCode
de l'ensemble des structures KEYBOARD_INPUT_DATA. Le MakeCode (ou scancode)
représente les donnés envoyé par le clavier au système lorsque vous pressez
linked list using hardware locking instruction "lock".
ou relachez une touche, chaque touche à son propre scancode et d'habitude
le système traduit le scancode dans un caractère en fonction de votre
page de code. Par exemple le scancode 19d sur un clavier US classique est
traduit dans le code de touche e.
Pour savoir si CAPSLOCK est activé, on envoie un IOCTL au périphérique
clavier fonctionnel, mais on peut aussi envoyer un IOCTL a l'IRQL
PASSIVE_LEVEL. Pour ça on utilise un système de thread qui va envoyer
des IOCTL à travers l'api noyau IoBuildDeviceIoControlRequest. En fait
les scancodes sont placé dans une liste vérouillé par un spinlock et
les threads sont synchronisés par un sémaphore. Le thread est à l'écoute
des frappes au clavier et convertis ensuite les scancodes en keycodes.
Tout comme le keylogger Klog [29].
-----[ 2.3.4 - Application 2 : sniffer de paquets NDIS
De la même manière, une interruption est levée quand notre carte réseau
reçoit un paquet. Quand ce genre d'interruption est levé, la procédure
de gestion ISR pour NDIS (ndisMIsr) lance le handler d'interruption ISR
miniport. La procédure ndisMIsr est utilisé comme un wrapper pour l'ISR
miniport et les DPC. vous pouvez trouver les entrés suivantes dans l'IDT:
3b: 80df6414 NDIS!ndisMIsr (KINTERRUPT 80df63d8)
ça signifie que votre handler d'ISR n'est pas appellé directement quand
une interruption arrive mais la routine ndisMIsr. l'ISR du miniport est
appellé par ndisMIsr et la DPC du miniport est aussi mis dans la file par
cette procédure. La DPC mise dans la file est en fait la routine ndisMDpc
qui encapsule votre propre DPC miniport handler. Finalement NDIS encapsule
tout le processus d'interruption avec ndisMIsr et ndisMDpc sous winXP avec
NDIS 5.1 . On sait pas si cette implémentation est toujours présente dans
NDIS 6.0 .
Maintenant qu'on sait comment remplacer le handler ndisMDpc par le notre.
Avec NDIS nous allons procéder de la même façon mais sans hooker MiniportDpc
on va plutot hooker directement ndisMDpc. Pourquoi ? parcequ'on sait que
ndisMDpc encapsule la routine MiniportDpc et qu'en fait MiniportDpc dépend
beaucoup trop du matériel du périphérique miniport. chaque périphérique
miniport est représenté par une structure NDIS_MINIPORT_BLOCK [30], dans
cette structure on retrouve une référence vers la structure
NDIS_MINIPORT_INTERRUP, qui à la tête suivante :
kd> dt ndis!_NDIS_MINIPORT_INTERRUPT
+0x000 InterruptObject : Ptr32 _KINTERRUPT
+0x004 DpcCountLock : Uint4B
+0x008 Reserved : Ptr32 Void
+0x00c MiniportIsr : Ptr32 Void
+0x010 MiniportDpc : Ptr32 Void
+0x014 InterruptDpc : _KDPC
+0x034 Miniport : Ptr32 _NDIS_MINIPORT_BLOCK
+0x038 DpcCount : UChar
+0x039 Filler1 : UChar
+0x03c DpcsCompletedEvent : _KEVENT
+0x04c SharedInterrupt : UChar
+0x04d IsrRequested : UChar
si on jette un oeuil à la routine ndisMDpc, on constate que le premier
paramètre est le seul utilisé, ce paramètre fait référence à une structure
NDIS_MINIPORT_INTERRUPT. La fonction ndisMDpc va appeller le champ MiniportDpc
de cette structure. Nous allons simplement remplacer ce pointeur par notre
routine pour controler les paquets entrants dans le système.
La documentation NDIS spécifie qu'une routine DPC miniport doit notifier
le driver du bound protocol qu'un tableau de paquets reçus est disponible
en appellant la fonction NdisMIndicateReceivePacket [31].
VOID
NdisMIndicateReceivePacket(
IN NDIS_HANDLE MiniportAdapterHandle,
IN PPNDIS_PACKET ReceivePackets,
IN UINT NumberOfPackets
);
In the ndis.h header we have :
#define NdisMIndicateReceivePacket(_H, _P, _N) \
{ \
(*((PNDIS_MINIPORT_BLOCK)(_H))->PacketIndicateHandler)( \
_H, \
_P, \
_N); \
}
Donc dans notre routine MiniportDpc on va détourner le PacketIndicateHandler,
qui est souvent l'ethFilterDprIndicateReceivePacket dans la structure
NDIS_MINIPORT_BLOCK, pour filtrer les paquets entrant sur le miniport.
Après avoir hijacké ce pointeur, on appelle la routine MiniportDpc d'origine
qui va traiter le tout. après ça, on restaure le handler PacketIndicateHandler
dans le NDIS_MINIPORT_BLOCK pour rester discret.
En résumé on doit :
- Hijacker la rtouine dans la DPC mis en file d'attente par la routine ndisMIsr.
- Maintenant qu'on a détourné le ndisMDpc, on modifie le PacketIndicateHandler
dans le NDIS_MINIPORT_BLOCK du miniport.
- On appelle la routine ndisMDpc. elle va appeller le handler MiniportDpc
d'origine.
- La routine MiniportDpc appelle la macro NdisMIndicateReceivePacket. Notre
fonction de filtrage est appellé et on fait notre boulot.
- Quand le ndisMDpc a fini, on restaure le PacketIndicateHandler d'origine
dans le NDIS_MINIPORT_BLOCK du miniport.
Avec ce filtre, on peut monitorer ou modifier les paquets entrants. Par
Exemple, notre hook du PacketIndicateHandler peut rechercher dans les
paquets entrants un marqueur, quand il trouve ce marqueur, le rootkit
déclenche une fonction.
---[ 2.2 - Conclusion sur le hooking furtif sur IDT
Dans cette partie, nous avont vu comment windows gère ses interruptions
materielles en utilisant un fonction template globale dédié a toute les
interruptions. Le fait est que cette routine template est forgé pour
chaque interruption, c'est le principal point de cette attaque, avec ça
on peut créer une fausse routine template qui ne peut être détectée
directement. la furtivité de notre attaque repose sur deux points :
- Nous ne modifions que du code alloué dynamiquement, et du code forgé
- Nous détournons des structures alloué dynamiquement hautement temporaires
qui lorsqu'elles tournent, préemptent toujours le coeur.
Donc, même si la porté de notre attaque est réduite, pour un rootkit
controler le hardware est la meilleure méthode pour atteindre les composants
critiques. Finalement, nous avons juste trompé le système avec ses propres
fonctionnalités et c'est le but d'un rootkit furtif.
--[ 3 - Possèder les pool NonPaged en utilisant le hooking furtif
La sophisticité d'un rootkit dépend de la méthode utilisée pour tromper
le noyau. des techniques de plus en plus complexes apparaissent au fur
et a mesure que la compréhension du matériel et du noyau évolue.
Cependant il y a tellement de façon de compromettre le noyau, rendant
les protections plus dure à contourner. nous allons présenter un
autre moyen pour gagner le controle. les techniques suivantes utilisent
cette approche de l'allocateur mémoire du noyau.
Notre but est d'obtenir l'execution sur chaque allocation NonPaged sans
utiliser de hook. Il doit passer au travers de n'importe quelle vérification
de hooks, même celles basé sur la comparaison des pages de code ou le hashing.
Nous procèderons simplement en modifiant les donnés utilisés par l'allocateur.
Nous applicons le concept d'utilisation du code contre lui même. Nous croyons
sincèrement que ce concept peut être utilisé sur d'autres composants avec
succès, et ce de plein de manières différentes.
Nous ne cherchons pas a vous convaincre que cette technique est parfaite.
Elle échape aux protections et systèmes de détection actuels. Le plus
important c'est qu'ils leur faudra bien plus qu'une légère modification pour
prévenir et bloquer une attaque basé sur le comportement du code noyau.
---[ 3.1 - Allocation noyau : revue d'architecture
Comme tout OS, le noyau windows met en avant certaines fonctions pour
allouer ou liberer de la mémoire. La mémoire virtuelle est organisé en
blocs mémoire nommé pages. dans l'architecture Intel x86, la taille d'une
page est de 4096 octets et la plupart des allocations nécessitent moins
de mémoire que ça. De plus, les fonctions du noyau comme ExAllocatePoolWithTag
et ExFreePoolWithTag gardent les blocs mémoire non utilisé pour les
allocations suivantes. les fonctions internes interagissent directement
avec le materiel dès qu'une page est requise. Toute ces procédures sont
complexes et délicates, c'est pour ça que les drivers font confiance
à l'implémentation du noyau.
-----[ 3.1.1 - Différences entre le pool Paged et NonPaged
La mémoire système du noyau est divisé en deux sorte de groupe. Cette
séparation a pour but de distingué les blocs mémoire les plus utilisés.
Le système doit savoir quelle page doit etre résidente, et laquelle peut
être temporairement mise de coté. Le gestionnaire des défauts de page restaure
la mémoire paginable seulement quand l'IRQL est inferieur au niveau DPC
ou DISPATCH. le pool Paged peut etre paginé dedans ou en dehors du système.
un bloc mémoire dé-paginé (NDT: swappé, paged out en anglais) sera sauvé
dans le système de fichier et ainsi les parties inutilisés de la mémoire
paginé ne resterons pas résident en mémoire. le NonPaged pool est présent
à chaque niveau d'IRQL et est en suite exploité pour les taches importantes.
Le fichier pagefile.sys contien la mémoire swappé. Il a été attaqué pour
injecter du code non signé dans le noyau vista [32]. Certaines solutions
furent apporté tel que desactiver la pagination mémoire du noyau. Joanna
Rutkowska a défendu cette solution come etant plus sur que les autres mais
présentant une légère perte de mémoire physique. Microsoft a simplement
ignorée les accès direct au disque (NDT : raw access), Ce qui tend à
prouver que l'architecture Paged et NonPaged est une fonctionnalité
importante dans le noyau windows [33].
Cet article se concentre sur l'architecture du pool NonPaged vu que
la gestion du PagedPool est coplètement différent. le pool NonPaged peut
etre plus ou moins considéré comme l'implémentation typique d'un Tas
(NDT : Heap). L'information globale sur le pool système peut etre
trouvé dans Microsoft Windows Internals [34].
-----[ 3.1.2 - Les tables du NonPaged pool
l'algoritme d'allocation doit etre rapide dans l'allocation des tailles
les plus utilisés. c'est pourquoi trois tables différentes existent et
chacune est dédié à une fourchette de taille d'alloc. Nous avons trouvé
cette organisation dans la plupart des algo de gestion de la mémoire.
Récupérer des blocs mémoire depuis le matériel prend du temps. Windows
fait l'équilibre entre temps de réponse rapide et le gaspillage de la
mémoire. Le temps de réponse s'améliore si les blocs mémoire son stocké
pour la prochaine allocation. d'un autre coté, si vous gardez trop de
mémoire en réserve, ça peut pénaliser les demandes de mémoire.
Chaque table implémente une façon différente de stocker les blocs mémoire.
Nous vous présenterons chaque table et où vous pouvez la trouver.
La table lookaside NonPaged est dédié a chaque processeur, et couvre
une taille inferieure ou égale a 256 octets. Chaque processeur possède un
registre de controle processeur (PCR: processor control register) stockant
les informations concernant seulement un seul processeur, tel que le niveau
d'IRQL, la GDT, l'IDT. Son extension, la région de controle processeur
(PCRB: processor control region buffer) contien les tables lookaside.
le dump windbg suivant représente les tables lookaside NonPaged et
sa structure :
kd> !pcr
KPCR for Processor 0 at ffdff000:
Major 1 Minor 1
NtTib.ExceptionList: 805486b0
NtTib.StackBase: 80548ef0
NtTib.StackLimit: 80546100
NtTib.SubSystemTib: 00000000
NtTib.Version: 00000000
NtTib.UserPointer: 00000000
NtTib.SelfTib: 00000000
SelfPcr: ffdff000
Prcb: ffdff120
Irql: 00000000
IRR: 00000000
IDR: ffffffff
InterruptMode: 00000000
IDT: 8003f400
GDT: 8003f000
TSS: 80042000
CurrentThread: 80551920
NextThread: 00000000
IdleThread: 80551920
DpcQueue: 0x80551f80 0x804ff29c
kd> dt nt!_KPRCB ffdff120
[...]
+0x5a0 PPNPagedLookasideList : [32]
+0x000 P : 0x819c6000 _GENERAL_LOOKASIDE
+0x004 L : 0x8054dd00 _GENERAL_LOOKASIDE
[...]
kd> dt nt!_GENERAL_LOOKASIDE
+0x000 ListHead : _SLIST_HEADER
+0x008 Depth : Uint2B
+0x00a MaximumDepth : Uint2B
+0x00c TotalAllocates : Uint4B
+0x010 AllocateMisses : Uint4B
+0x010 AllocateHits : Uint4B
+0x014 TotalFrees : Uint4B
+0x018 FreeMisses : Uint4B
+0x018 FreeHits : Uint4B
+0x01c Type : _POOL_TYPE
+0x020 Tag : Uint4B
+0x024 Size : Uint4B
+0x028 Allocate : Ptr32 void*
+0x02c Free : Ptr32 void
+0x030 ListEntry : _LIST_ENTRY
+0x038 LastTotalAllocates : Uint4B
+0x03c LastAllocateMisses : Uint4B
+0x03c LastAllocateHits : Uint4B
+0x040 Future : [2] Uint4B
les tables lookaside permettent de retrouver plus rapidement les blocs
qu'avec une classique liste doublement chainé. Pour cette optimisation, le
temps de verouillage est vraiment crucial, et une liste simplement chainé
est un mécanisme plus rapide que le verouillage logiciel.
la fonction ExInterlockedPopEntrySList est utilisé pour dépiler une entrée
d'une liste chainé en utilisant l'instruction hardware de verouillage "lock".
la PPNPagedLookasideList est la table lookaside dont nous parlions.
Elle contien deux listes lookaside P et L. le champ Depth
de la structure GENERAL_LOOKASIDE définit combien d'entrés peuvent etre
listé dans une seule liste ListHead. le système met à jour régulierement
la profondeur en utilisant différents compteurs. L'algorithme de maj est
basé sur le nombre de processeurs et est différent pour P ET L. la
pronfondeur de la liste P est mise à jour plus fréquement que la liste L
vu que la liste P optimise les performances sur les très petits blocs.
La seconde table dépend du nombre de processeurs utilisés et de comment
le système les gère. le système d'allocation le parcours si sa taille est
inferieure ou égale à 4080 octets ou si une recherche dans la lookaside a
échoué. Meme si la table cible peut changer, elle a toujours la même
structure POOL_DESCRIPTOR. sur un seul processeur, une variable nommé
PoolVector est utilisé pour retrouver le pointeur NonPagedPoolDescriptor.
Sur un multi-processeur, la table ExpNonPagedPoolDescriptor possède 16
entrés contenant les pool descriptors. Chaque PRCB d'un processeur pointe
sur une structure KNODE. Un node peut être lié a plus d'un processeur
et contien un champ de coloration utilisé comme index dans le
ExpNonPagedPoolDescriptor. Le shema suivant illustre l'algoritme.
PoolVector
+------------+
| NonPaged | --------------> NonPagedPoolDescriptor
|------------+
| Paged |
+------------+
[ Figure 1 - Descripteur pour un seul processeur ]
Processor #1
+------------+
| | ExpNonPagedPoolDescriptor
| PRCB ------\ +-------------------+
| | | /---------> SLOT #01 |
+------------+ | | | SLOT #02 |
/---------/ | | SLOT #03 |
| KNODE | | SLOT #04 |
|---> +------------+ | | SLOT #05 |
| | Proc mask | | | SLOT #06 |
| | color (01) --/ | SLOT #07 |
| | ... | | SLOT #08 |
| +------------+ | SLOT #09 |
| | SLOT #10 |
\---------\ | SLOT #11 |
Processor #2 | | SLOT #12 |
+------------+ | | SLOT #13 |
| | | | SLOT #14 |
| PRCB ------/ | SLOT #15 |
| | | SLOT #16 |
+------------+ +-------------------+
[ Figure 2 - Descripteur de pool pour architecture multi-processeur ]
Une variable globale ExpNumberOfNonPagedPools définit si on est dans une
approche multi-proccesseur. Elle doit refléter le nombre de processeur, mais
elle peut changer d'une version d'OS a l'autre.
Le dump suivant montre la structure POOL_DESCRIPTOR depuis windbg.
kd> dt nt!_POOL_DESCRIPTOR
+0x000 PoolType : _POOL_TYPE
+0x004 PoolIndex : Uint4B
+0x008 RunningAllocs : Uint4B
+0x00c RunningDeAllocs : Uint4B
+0x010 TotalPages : Uint4B
+0x014 TotalBigPages : Uint4B
+0x018 Threshold : Uint4B
+0x01c LockAddress : Ptr32 Void
+0x020 PendingFrees : Ptr32 Void
+0x024 PendingFreeDepth : Int4B
+0x028 ListHeads : [512] _LIST_ENTRY
la synchronisation des spinlock en file d'attente, partie intégrande de la
librairie HAL, est utilisé pour limiter les accès concurent sur un
descripteur de pool. Elle assure qu'un seul thread et un seul processeur
pourra accèder et un-linker une entrée d'un descripteur de pool. la
librairie HAL change en fonction des architectures, et ce qui n'est qu'une
simple augmentation d'IRQL sur un seul processeur, deviens un système de
gestion de file bien plus complex sur une architecture multi-processeur.
Pour le descripteur de pool par défaut, le spinlock mis en file d'attente
globale pour les NonPaged est verouillé (LockQueueNonPagedPoolLock).
Sinon un spinlock mis en file d'attente spéciale est créé.
La troisième et dernière table est partagés par les processeurs pour les
tailles supèrieures à 4080 octets. MmNonPagedPoolFreeListHead est aussi
utilisé quand les autres tables sont en manque de mémoire. Elle est composé
de 4 LIST_ENTRY, chacune représentant un nombre de page, excepté pour la
dernière qui contiens toutes les pages suppérieures gardés par le système.
L'accès à cette table est gardé par un spinlock général mis en file pour les
pages non-paged aussi connu comme LockQueueNonPagedPoolLock. durant la
procédure de libération d'un bloc plus petit, ExFreePoolWithTag le fusionne
avec les blocs libres qui le précèdent et qui le suive. Elle peut créer
des blocs suppérieurs ou égaux à 1 page. dans ce cas, le nouveau bloc est
ajouté dans la table MmNonPagedPoolFreeListHead.
-----[ 3.1.3 - Algos d'allocation et de libération.
L'allocation noyau ne change pas des masses entre diverses versions
d'OS, mais cet algoritme est aussi compliqué que l'allocation mémoire
userland du tas. Dans cette partie, nous voulons illustrer le comportement
basique entre les tables pendant les procédures d'allocation ou de
libération. Biens des détails ont été écartés tels que les mécanismes de
synchronisation. Ces algoritmes vous aiderons pour comprendre l'explication
technique, mais aussi les éléments de bases de l'allocation noyau. Bien
que l'exploitation du noyau ne fait pas parti de l'article, le debordement
du pool est un sujet intéressant qui nécessite la compréhension de certaines
parties de cet algoritme.
algoritme d'allocation des NonPaged pool (ExAllocatePoolWithTag):
IF [ Size > 4080 bytes ]
[
- Appeller la fonction MiAllocatePoolPages.
- Parcourir la table de LIST_ENTRY MmNonPagedPoolFreeListHead.
- Récupérer la mémoire du materiel si nécessaire.
- Retourner la page mémoire alignée (sans header).
]
IF [ Size <= 256 bytes ]
[
- Dépiler la première entrée de la table PPNPagedLookasideList.
- Si on trouve quelquechose on retourne le bloc mémoire.
]
IF [ ExpNumberOfNonPagedPools > 1 ]
- le PoolDescriptor venant de ExpNumberOfNonPagedPools et l'index
utilisé viens de la couleur PRCB KNODE.
ELSE
- le PoolDescriptor vien de la première entrée du PoolVector,
et est désigné par le symbole comme NonPagedPoolDescriptor.
FOREACH [ >= Taille de l'entrée de PoolDescriptor.ListHeads ]
[
IF [ Entrée non vide ]
[
- dé-linker l'entré et la splitter si nécessaire.
- Retourner le bloc mémoire.
]
]
- Appeller la fonction MiAllocatePoolPages.
- Parcourir la table de LIST_ENTRY MmNonPagedPoolFreeListHead.
- la splitter correctement à la bonne taille.
- Retourner le bloc mémoire fraichement créé.
Algoritme de libération du pool NonPaged (ExFreePoolWithTag) :
IF [ Le bloc mémoire est alligné sur une frontière de page ]
[
- Appeller la fonction MiFreePoolPages
- Determiner le type de bloc (Paged ou NonPaged)
- En fonction du nombre de blocs contenus dans
MmNonPagedPoolFreeListHead, on les rends au materiel.
]
ELSE
[
- Fusionner le bloc précédent et suivant si possible
IF [ taille de NewMemoryBlock <= 256 bytes ]
[
- Regarder la profondeur de l'entrée PPNPagedLookasideList
et verifier si on doit la garder.
- si un bloc mémoire est poussé dans la liste lookaside
on fait un return.
]
IF [ taille de NewMemoryBlock <= 4080 bytes ]
[
- Utiliser la variable PoolIndex de type POOL_HEADER pour
- déterminer quel PoolDescriptor doit etre utilisé.
- l'insérer dans le bon tableau d'entré LIST_ENTRY.
- Si tout s'est bien passé on fait un return
]
- En fonction du nombre de bloc gardés dans
MmNonPagedPoolFreeListHead, on le rend au materiel.
]
l'algoritme du pool Paged (ndt: paginé :/ ) est vraiment différent
spécialement pour les bloc allignés sur une frontière de page.
La gestion des tailles plus petites ne doit pas être très éloingnée des
NonPaged mais en code assembleur, nous avons bien vu que les pools NonPaged
et Paged sont totalement dissociés. Maintenant que vous en savez plus sur
l'allocation des NonPaged et son fonctionnement, nous allons pouvoir parler
de l'exploitation.
---[ 3.2 - Obtenir l'execution de code en abusant le code d'allocation
Notre principal but est d'obtenir l'execution de code à chaque tentative
d'allocation pour les NonPaged pool seulement. Ce resultat doit etre fait
en changeant seulement les donnés utilisés par le code ciblé. Notre but
etant de prouver que le code du noyau peut servir nos interets en changeant
seulement les donnés typiques de l'environement. Notre travail est basé
sur un nouveau rootkit développé pour prendre la main sur l'allocation
NonPaged.
Nous commençons par obtenir l'execution du code pour une allocation
supérieure ou égale à 1 page. Comme nous l'avons vu précédement, ça
concerne la troisième et la dernière table.
-----[ 3.2.1 - Corruption des donnés de MmNonPagedPoolFreeListHead
MmNonPagedPoolFreeListHead conserve les blocs mémoire aligné sur une frontière
de page pour accèlérer l'allocation mémoire. Il lie la mémoire retenue en
utilisant une structure LIST_ENTRY. Cette structure est communément utilisé
dans la librairie du tas de windows.
kd> dt nt!_LIST_ENTRY
+0x000 Flink : Ptr32 _LIST_ENTRY
+0x004 Blink : Ptr32 _LIST_ENTRY
l'acces a MmNonPagedPoolFreeListHead est protégé par le spinlock NonPaged
mis en file d'attente LockQueueNonPagedPoolLock. Il assure qu'un seul thread
et un seul proesseur peut regarder et modifier cette structure.
Donc nous avons besoin de quelquechose pour prendre le controle sur
l'allocation, la procédure d'unlinking semble parfaite. Nous pouvons
pourrir cette linked list avec une fausse entrée, de la plus grande taille
possible, dont l'unlinking modifiera le code en cour d'execution.
Au niveau noyau, vous pouvez modifier le code comme des donnés sans soucis
de protections. l'Unlinking fut utilisé dans les debuts de l'exploitation
du tas (ndt : heap overflows ;) )[20] mais modifier le code n'etait pas
possible depuis l'userland. Commme le spinlock nous assure l'exclusivité,
il n'y a pas de risque de race condition. le hook ainsi créé sera dynamique
et le code sera restauré directement. le Reverse de la protection patch
guard [18] montre que les vérifications de code sont faites toute les 5 min.
Si jamais une modification est faite, le vrai code est simplement replacé.
Cette méthode à plein d'avantages, mais présente aussi un grand nombre
d'obstacles. commençons à les énumérer :
- dans une implémentation basique de l'unlinking, la liste devien impossible
à parcourir, ce qui la rend inutilisable la plupart du temps.
- passer au travers des methodes de nétoyage, et etre toujours le premier bloc
dans la liste sinon on risque de rater certains appels.
- On brise le flot d'execution et tot ou tard on doit retourner comme si
notre détournement n'avait jamais exité et que tout allait bien.
- Le pre-chargement des instructions fait par le processeur rend dangereux
les instructions s'auto-modifiant.
L'unlinking nous donne 4 octets a écraser pour construire un opcode et créer
une redirection. Dans notre cas, on a influencé le contect courrant et un
registre doit pointer vers l'entrée un-linké. Nous avions dit qu'il devait
pointer sans choisir un seul registre a cause des changements entre les version
des noyaux ou a cause des service pack. dès que nous parlerons de contexte,
nous parlerons seulement de cas généraux. Nous avons choisit de faire un jmp
[reg+XX] ce qui nous donne FF60XX en hexa.
L'efficacité de cette technique nécessite que la MmNonPagedPoolFreeListHead
reste parcourable. une liste doublement chainé, comme LIST_ENTRY, est
parcourable si Flink est correct. De plus nous pouvons choisir une adresse
pour Flink telle que 0xXXXX60FF et Blink pointera vers l'adresse du code.
L'architecture intel x86 utilise le little endian, rendant notre adresse
assez simple à trouver, on doit juste vérifier les offset des opcodes et
jarter les possibilités trop proches. la figure suivante illustre une
entrée empoisonée.
MmNonPagedPoolFreeListHead[i]
/------> +--------------------+
| | Flink | ---\
| |--------------------| |
| <---- | Blink | |
| +--------------------+ |
| | ... | |
| +--------------------+ |
| /-------------------------------/
| |
| | Entré enpoisonée
| | +--------------------+
| | | PreviousSize : - |
| | +--------------------+
| | | PoolIndex : - |
| | +--------------------+
| | | PoolType: NonPaged |
| | +--------------------+
| | | BlockSize : i |
| | +--------------------+
| | | PoolTag : - |
| \---> +--------------------+
| | Flink : 0xYYXX60FF | <--\
| |--------------------| |
| X--- | Blink : 0x80YYYYYY | |
| +--------------------+ |
| |
| /-------------------------------/
| | Fausse entrée (0xYYXX60FF)
| | +--------------------+
| | | PreviousSize : - |
| | +--------------------+
| | | PoolIndex : - |
| | +--------------------+
| | | PoolType: NonPaged |
| | +--------------------+
| | | BlockSize : < i |
| | +--------------------+
| | | PoolTag : - |
| |---> +--------------------+
| | | Flink : 0x80..... | ---\
| | |--------------------| |
| \---- | Blink : Poisoned | |
| +--------------------+ |
\--------------- [...] ------------/
Instruction d'Unlinking : mov [0x80YYYYYY], 0xYYXX60FF
Nouvel opcode après l'unlinking : jmp [reg+XX] (FF 60 XX)
[ Figure 3 - liste doublement chainé empoisonnée ]
Ce shéma montre la structure d'une entrée MmNonPagedPoolFreeListHead qui
assure un unlinking prédictif et la bonne execution du code. On doit
absolument maintenir cette structure en place ou on risque de perdre
notre position. les blocs NonPaged proviennent de deux plages de mémoire
virtuelle. Le second debut de région mémoire est stocké dans
MmNonPagedPoolExpansionStart. Une fonction de nétoyage est parfoit appellée
pour libérer les blocs provenant de l'expansion du pool NonPaged. Pour
éviter ce néttoyage, nous pouvons utiliser un bloc Paged pool locké. Vous
pouvez locker un bloc mémoire avec la fonction MmProbeAndLockPages. Ce
verrou rend résidente la zone mémoire décrite. Une façon plus discrette
consiste à remapper un bloc NonPaged avec la fonction
MmMapLockedPagesSpecifyCache. C'est encore plus discret car ce mapping
sera juste avant la plage mémoire du pool d'expension NonPaged. utiliser
un bloc du pool Paged (ndt paginé) vérouillé crée une adresse carrément
différement. Un coup d'oeuil rapide sur ces adresses entre les NonPaged
montre une nette différence. Comme la mémoire virtuelle est très grande,
ça prend pas trop de temps pour trouver une adresse genre 0xYYXX60FF.
Nous ne dévérouillerons pas ces pages tant que notre technique ne
tourne pas.
Pour battre le problème du flot d'execution, on distingue deux etats
différents. Le premier lorsque notre bloc est selectionné. Le second lorsque
notre bloc est Unlinké. Si nous sommes capables de retourner au premier etat
avec notre prochaine fausse entrée selectionnée, nous pouvons continuer le
parcours du code normalement. Nous arrivons à ce resultat en utilisant une
approche générique. Lorsque l'IRQL est égal au DISPATCH_LEVEL, nous corrompons
une entrée MmNonPagedPoolFreeListHead avec quelques pointeurs foireux. Avec
un hook sur le handler (ndt gestionnaire) de défaut de page, nous sommes
capables de voir la première et la seconde étape, et de restaurer le bon
contexte à chaque fois, ainsi que de sauvegardre la différence de contexte
entre ces deux etats.
Dump assembleur de MiAllocatePoolPages :
lea eax, [esi+8] ; Etape #1 esi le bloc selectionné et esi+8 sa taille
cmp [eax], ebx ; Comparaison avec la taille nécesaire.
mov ecx, esi
jnb loc_47014B
[...]
loc_47014B:
sub [esi+8], ebx
mov eax, [esi+8]
shl eax, 0Ch
add eax, esi
cmp _MmProtectFreedNonPagedPool, 0 ; Mode protégé, on s'en fout
mov [ebp+arg_4], eax
jnz short loc_47016E
mov eax, [esi] ; \ Etape #2
mov ecx, [esi+4] ; | procédure
mov [ecx], eax ; | d'Unlinking
mov [eax+4], ecx ; /
jmp short loc_470174
Maintenant voyons comment ça marche pendant notre technique de test avec
le gestionnaire d'interruption des défauts de page (int 0xE) hooké :
lea eax, [esi+8]
; Etape #1 - Comparaison avec la taille nécesaire.
cmp [eax], ebx ; ----> PAGE FAULT esi = 0xAAAAAAAA | eax = esi + 8
; - On garde l'EIP et tout les registres
; - Scanner tout les registres à la recherche de
; 0xAAAAAAAA +/- 8 et corriger le contexte
; courrant. Continuer.
mov ecx, esi
jnb loc_47014B
[...]
loc_47014B:
sub [esi+8], ebx
mov eax, [esi+8]
shl eax, 0Ch
add eax, esi
cmp _MmProtectFreedNonPagedPool, 0 ; mode de protection, on s'en ballance
mov [ebp+arg_4], eax
jnz short loc_47016E
mov eax, [esi] ; \ Etape #2 - Procédure d'unlinking
mov ecx, [esi+4] ; |
mov [ecx], eax ; | ------> PAGE FAULT ecx = 0xBBBBBBBB
; | eax = 0xCCCCCCCC
; | - Conserver l'EIP et soustraire ce contexte
; | du contexte sauvé à l'etape #1
; | - Modifier les registres de faute et
; | les pointeurs de structure. Continuer.
mov [eax+4], ecx ; /
jmp short loc_470174
Les adresses d'erreur 0xAAAAAAA, 0xBBBBBBBB et 0xCCCCCCCC doivent pointer
sur des adresses invalides pour forcer un défaut de page intercepté. Ce
test est fait une seule fois et quand nous avons encore l'exclu sur tout
les processeurs. le handler int 0xE (défaut de page) est restauré juste
après.
Cette technique générique nous permet de restaurer un contexte valide
juste avant la vérification de la taille du bloc selectionné. Dès que
nous avons l'execution du code, nous appliquons la différence de contexte,
changeons le registre du bloc courrant et retournons a l'adresse de la
première étape. ça fonctionne bien parceque nos deux étapes sont très
rapprochés, dès que la taille du bloc selectionné est vérifié, l'unlinking
est executé cash.
Les exemples étaient basé sur une seule LIST_ENTRY de la table
MmNonPagedPoolFreeListHead, mais vous devez empoisonner toutes les
entrés. Si une entrée donnée est vide (a part pour notre faux bloc),
l'algorithme essaye l'entrée suivante. Ce qui signifie qu'il sera appellé
plus d'une fois par allocation. Nous avons créé un mécanisme pour
gérer les appels multiples sur une seule allocation. Si la première
entrée est vide, la seconde entrée est utilisé, et ainsi de suite.
Donc nous serons appellé deux fois ou plus. En vérifiant la table en cour
nous pouvons prédire la prochaine execution de code sur la même allocation
et éviter d'executer la paylode plus d'une fois par demande d'allocation.
Le Prefetch (Préchargement) est une fonctionnalité qui récupère plus
d'une instruction en mémoire avant de les executer. Certains processeurs
utilisent un algo de prédiction de branchement complexe pour charger
un maximum d'instructions possible. Après quelques tests, nous avons
vu que les processeurs invalident le cache de code quand une modification
à lieu dans les adresses mémoire en cache. Notre driver supporte un cas
ou la modification du code peut être faite juste après l'instruction
en cours. Pour ce faire, nous avons créé une routine qui calcule
la taille du cache de préchargement et le prend en compte dans
les prochaines parties de notre technique. Nous pouvons aussi rechercher
les instructions spécifiques qui néttoient le cache de préchargement
comme un jump lointain, mais c'est optionnel.
Cette technique nous permet d'executer du code pour les allocation NonPaged
supperieures ou égales à 1 page. Elle y parviens avec un hook furtif, créé
par le code du noyau et nettoyé par notre routine juste après. C'est loin
d'etre parfait vu que ce genre d'allocations ne sont pas des masses utilisés.
La prohaine partie décrit comment cette technique peut etre étendue pour
obtenir un controle sur toutes les allocation du pool NonPaged.
-----[ 3.2.2 - L'étendre pour toute les tailles
Les autres listes ne peuvent pas etre détournés de la meme manière parceque
les mécanismes de synchro ne sont pas exclusifs. la modification de bouts
de code assembleur devien chaud s'il est potentiellement executé par plus
d'un thread a la fois. Notre méthode nous assure l'execution à notre
technique précédente lors de n'importe quelle allocation. Dès que nous
avons le contrlole, nous pouvons trouver un moyen de restaurer le cntexte
de ExAllocatePoolWithTag avec la bonne valeur de retour. On doit faire ça
sans écrire une seule ligne d'allocateur mémoire. Il est possible de créer
notre propre allocateur, mais celui de windows est très bien et fera
parfaitement l'affaire dans notre cas.
Pendant l'allocation, la liste de lookaside est vérifiée en premier.
Ce qui va remonter une entrée et si cette entrée est non-nulle, l'utiliser.
Cette entrée proviens du champ GENERAL_LOOKASIDE de type ListHeader. Ce champ
est struccturé comme SLIST_HEADER.
kd> dt nt!_SLIST_HEADER .
+0x000 Alignment : Uint8B
+0x000 Next :
+0x000 Next : Ptr32 _SINGLE_LIST_ENTRY
+0x004 Depth : Uint2B
+0x006 Sequence : Uint2B
La fonction ExInterlockedPopEntrySList pop une entrée de la structure
SLIST_HEADER. le champ suivant est un pointeur vers le prochain noeud SLIST
(liste simplement chainée). Le champ Depth représente combiend d'entrés sont
gardés dans la liste. ExFreePoolWithTag compare la profondeur optimale de
GENERAL_LOOKASIDE avec la profondeur courrante de SLIST_HEADER.
ExAllocatePoolWithTag ne vérifie pas seulement ce champ, et vérifie simplement
si quelques entrés peuvent etre dépilés dans le prochain champ. Pour assomer
l'allocation et les procédure de libération sur la table lookaside des
NonPaged, nous avons placé le champ suivant à NULL et la profondeur du champ
à 0xFFFF. Cet etat sera préservé et cette table ne sera plus jamais utiliée.
Notre technique repose entièrement sur le détournement de l'usage habituel de
la table ExpNonPagedPoolDescriptor. Dans la partie précédente, nous avons
expliiqué l'implication de la variable globale ExpNumberOfNonPagedPools dans
ce process. Il est possible d'etendre un grand nombre de pools NonPaged et de
jouer ensuite avec la couleur du KNODE courrant. Pendant l'allocation, la
couleur du KNODE définit quel descripteur de pool est utilisé. Ensuite durant
la procédure de libération, le champ PoolIndex du POOL_HEADER maintien la
couleur du descripteur de pool.
Donc nous pouvons utiliser cette fonctionnalité sympa à notre avantage.
la couleur par défaut du KNODE sur tous les processeurs devrais pointer
vers un descripteur de pool vide. Ce qui conduira à l'execution de notre
code utilisant nos techniques de base. Si l'adresse de retour de la fonction
MiAllocatePoolPages n'est pas celle utilisée pour l'allocation de pages
arondies classique, nous savons qu'une plus petite allocation à lieu. Tout
ce que nous avons à faire c'est de switcher le pointeur PRCB du KNODE vers
une copie avec une couleur maison, et rappeller ExAllocatePoolWithTag.
tout ce qui se rapporte à l'allocation et à la gestion des blocs sera
implémenté comme si c'etait la même chose d'une version d'os à l'autre.
les blocs PoolIndex retournés pointerons vers notre prporpe pool descriptor
et notre procédure de libération (ndt : free), ce qui fonctionnera parfaitement.
voyons la tête que ça aurais sur un seul processeur.
ExpNonPagedPoolDescriptor
+-------------------+
| PREVIOUS POOLDESC | <--- Gardé pour raison de compatibilité (0)
| EMPTY POOLDESC | <--- KNODE->color par défaut (1)
| -- |
| -- |
| -- |
| -- |
| -- |
| -- |
| -- |
| -- |
| -- |
| -- |
| -- |
| -- |
| -- |
| CUSTOM POOLDESC | <--- Uttilisé pour nos allocations (16)
+-------------------+
[ Figure 4 - ExpNonPagedPoolDescriptor corrompus]
[ sur un seul processeur ]
Cette configuration est un simple exemple, et vous pouvez gérer son
agencement comme vous le voulez. Nous pouvions transférer les blocs
précédents en provenance des vieux descripteurs de pool vers le notre et
en suite recevoir les blocs libres. Il est aussi possible d'utiliser
plusieurs descripteurs de pools, ainsi de suite. Attention au recyclage
des descripteurs de pool système, pouvant induire des comportements
zarbi, particulièrement sur les archis multi-processeurs.
Dès qu'on a notre bloc fraichement alloué, nous devons retourner sur
l'adresse de retour de ExAllocatePoolWithTag. MiAllocatePoolPages à été
appellé pour retrouver une nouvelle page et remplir le descripteur de pool
courant avec cette dernière. c'est clair que nous ne pouvons pas retourner
betement et laisser l'allocation de page se dérouler. Sur les archi Intel
x86, la pile est utilisée pour stocker les variables locales, les arguments
et les registres sauvegardés. Le compilateur windows commence par réserver
les variables locales et empile en suite chaque registre avant leurs
modifications. la figure suivante montre la configuration de notre pile
lorsque notre code est executé.
top/haut
+---------------------+
| nos elements de pile| Exemple d'asm de restauration
+---------------------+ <------ /---------------\
| | | pop ecx |
|Registres Sauvegardés| | pop ebx |
| | | pop esi |
+---------------------+ | leave |
| | | retn 0Ch |
| | \---------------/
| | |
| | |
| variables de pile | |
| | |
| | |
| | |
+---------------------+ [new stack level]
| EBP Sauvé | |
+---------------------+ |
| Adresse de retour | |
+---------------------+ |
| | |
|arguments de la | |
| Fonction | |
+---------------------+ <--------------/
bottom / bas
[ Figure 5 - Contexte de la pile après l'execution de notre code ]
[ ~ cas des piti blocs ~ ]
La partie asm de restauration montre exactement l'asm de la fonction
courrante qui restaure parfaitement le contexte. Ce qui ne correspond pas
a la première série d'instructions pop avant le return. et on cours le
risque de se retrouver avec un registre pas encore empilé. Il est possible
de déduire le nombre de registres poussés en regardant le prologue de la
fonction quand les variables de la pile sont réservés. Dans le compilo
Windows, c'est plutot simple et nous pouvons aisément calculer le nombre
de registres poussés. Une simple analyse du code désassemblé sur le nombre
de registres dépilé nécessaire fera l'affaire. Il faut le faire pour
MiAllocatePoolPages et ExAllocatePoolWithTag. Nous sauvegardons l'adresse
de retour sauvegardée sur la pile, et allons à l'adresse MiAllocatePoolPages
déduite. La dernière étape consiste à régler le registre eax à la valeur de
retour. Les deux fonctions retournent une valeur et présèrvent celle de
l'eax. Notre analyseur est dynamique et référence chaque pop et son
registre associé. Ainsi nous pouvons restaurer le bon contexte même si
il change d'une version à l'autre.
Le compilateur windows est vraiment très prédictible et ne génère pas
une structure d'asm trop tordue. Cette technique est théoriquement possible
sur n'importe quel code assembleur suivant la spécification stdcall.
l'approche peut varier sur les autres compilateurs. (ndt : optimisations
oblige)
---[ 3.3 Exploiter notre position
Cette article présente une methode pour corrompre le noyau windows en
modifiant seuelement les donnés. Pas de pointeur sur fonction, pas de
hook statiques ou autre techniques classiques. ça devrais suffire pour
éviter de plus amples explications. mais ça ne serais pas complet sans
quelques exemples concrets. Personellement, je pense que la seule limite
de cette technique, c'est l'imagination.
-----[ 3.3.1 redirection de pile générique
l'allocation à lieu à tellement d'endroits que vous pouvez compter
seulement sur le contexte de la fonction. Dès que tout est en place
et avant de rendre l'exclusivité, une base de données sur les redirection
de pile peut etre créée.
La première méthode consiste à appeller un handler si le retraçage de
la pile (ndt: stack backtracing) révèle une fonction spéifique. Retracer
la pile révèle seulement l'adresse de retour et non la fonction qui
l'a appellée. Les debuggeurs réslovent ces fonction par une analyse en
profondeur ou en vérifiant les symboles. l'implémentation de ces
fonctionnalités prendrai trop de temps. Il vaut donc mieux cibler une
adresse de retour spécifique sur le cadre de la pile de ExAllocatePoolWithTag
. Ce qui va grandement améliorer la vitesse de vérification. Pour ce faire,
nous indiquons à notre API de redirection de pile que nous ciblons une
fonction spécifique. Chaque allocation durant cette periode fera apparaitre
les retraçages de pile importants.
Disont qu'on cible un IRP et nous connaissont la fonction qui le gère en
regardant la table de répartition des IRP. Nous pouvons aussi le savoir
en reversant, vu qu'il va allouer un bloc NonPaged. en lançant une requête
d'Entré/Sortie, notre API peut référencer quelques appels NonPaged et
les reconnaitres plus tard.
Dans la nature, il appellera le bon handler avec les informations du
sous-contexxte. Parfois, obtenir un contexte ne suffit pas. La seconde
méthode repose sur les mêmes principes mais modifie la pile pour s'assurer
de l'appel de notre handler lorsque la fontion se termine. L'efficacité
de la méthode dépend de la cible et des modification faites sur cette
dernière.
-----[ 3.3.2 Injection de code dans un processus Userland
Cette technique peut aussi etre utilisée pour injecter du code en userland
pour corrompre les applications de confiance. l'allocation NonPaged à souvent
lieu en mode noyau, et ça arrive dans chaque processus. Certains drivers
noyau comme win32k.sys fait appel à l'userland souvent. Cet appel est
réalisé par la fonction KeUserModeCallback [35]. Il modifie la pile
userland pour switcher temporairement vers un appel en userland. Le nombre
de fonctions disponibles est limité par une table.
L'injection Userland depuis le noyau ne doit pas etre résidente et ne
doit concerner que des application connue pour avoir tout les droits comme
les navigateurs. l'Injection peut aussi avoir lieu sur explorer.exe pour
lancer une instance caché d'un programme de confiance. l'algorithme
KeUserModeCallback peut facilement etre recréé ou copié puis déplacé.
la table des redirections peut etre corrompue pour rediriger l'appel.
nous pensons aussi à l'exploitation des appels userland. ça n'a pas de
sens d'ajouter des vérifications sur ces fonctions disponibles.
--[ 4 - Detection
Cet article n'essaie pas de vous convaincre que la compromission de l'IDT ou
que les mécanismes du système d'allocation représentent le futur. La plupart
des outils de détections se contentent d'indiquer si un rootkit est peut etre
dans cet ordi ou pas. ils galèrent pas mal pour identifier quel module et
responsable de l'infection. Il détecte les antivirus ou les firewalls comme
des rootkits. les articles sur les rootkits montre suffisament de méthodes
pour contourner ces protections. mais on ne les retrouve pas des masses dans
la nature, tout simplement parcequ'ils n'en ont pas besoin.
Detecter les modifications comportementales d'un logiciel devrait faire
partie d'un système d'exploitation vérifiable [36]. Ce qui impliquerais
des vérificatoions basiques sur les structures mémoires connues. la
vérification de l'intégrité des structures LIST_ENTRY et leur correction
si nécessaire. nous pouvons blamer la détection de rootkits autant que nous
le voulons, mais la detection de rootkits dans un OS propriétaire fermé est
pratiquement impossible. donner de plus amples informations sur
les composant du noyau mènera certainement a des attaque bien plus
sophistiqués. Mais d'un autre coté, ça réduira grandement la surface
d'attaque. Ce qui est particulièrement vrai dans un OS orienté défense.
Les prochaines améliorations devrais venir de l'OS lui même.
Maintenant qu'il existe des améliorations hardware pour la virtualisation,
tel que les hyperviseurs, il y aura des extension pour détecter et se
protéger des rootkit au niveau hardware. Ce qui offre un vrai controle
sur le comportement de l'OS sans recherches poussés sur la structure
du noyau. Certaines techniques de protection qui étaient impossibles à
implémenter en environement windows tel que PAX, pourraient reposer
à l'avenir sur ces fonctionnalités hardwares. Nos techniques pourraient
etre détectés en enregistrant et monitorant certains évenements spécifiques
sur le processeur. C'est faisable à ce jour, mais pas sans un impact
important sur les performances.
Notre attaque pourrait être bloquée en utilisant des protections ciblés
comme les signatures. Une attaque se définie par le temps nécessaire
pour créer une protection générique. Dans ce domaine, Patchguard est
une amélioration significative.
--[ 5 - Conclusion
Cet article technique etait fait pour montrer qu'une technique de détournement
élégante peut encore échapper à la plupart des protections tout en évitant
d'impacter les performances ou d'induire des comportements instables.
Malgrès ça, ces techniques sont peut fiables et devrait etre prise en
compte uniquement comme une preuve de concept technique. Les nouvelles
protections ne sont pas suffisament efficaces ou présentes. Elles ne
représentent pas de menace sérieuse pour un rootkit ciblant des millions
d'ordinateurs. le Reverse est un outil clef dans l'amélioration
des techiques de rootkit logiciels. Détecter qu'un rootkit est présent
ne devrait pas etre suffisant. Une protection qui ne peut pas désinstaller
un rootkit ou prévenir l'infection est inutile. la signature des pilotes
etait une bonne idée vu qu'elle etait concue pour stoper les vecteurs
d'infection actuels. Mais la prévention des infections inclue aussi
l'exploitation en local du noyau. la detection générique de ces attaques
nécessiterais de grand progres dans les protections des anti-rootkits
et la conception des systèmes d'exploitations.
--[ 6 - References
[1] Holy Father, Invisibility on NT boxes, How to become unseen on Windows
NT (Version: 1.2)
http://vx.netlux.org/lib/vhf00.html
[2] Holy Father, Hacker Defender
https://www.rootkit.com/vault/hf/hxdef100r.zip
[3] 29A
http://vx.netlux.org/29a
[4] Greg Hoglund, NT Rootkit
https://www.rootkit.com/vault/hoglund/rk_044.zip
[5] fuzen_op, FU
http://www.rootkit.com/project.php?id=12
[6] Peter Silberman, C.H.A.O.S, FUto
http://uninformed.org/?v=3&a=7
[7] Eeye, Bootroot
http://research.eeye.com/html/tools/RT20060801-7.html
[8] Eeye, Pixie
http://research.eeye.com/html/papers/download/
eEyeDigitalSecurity_Pixie%20Presentation.pdf
[9] Joanna Rutkowska and Alexander Tereshkin, Blue Pill project
http://bluepillproject.org/
[10] Frank Boldewin, A Journey to the Center of the Rustock.B Rootkit
http://www.reconstructer.org/papers/
A%20Journey%20to%20the%20Center%20of%20the%20Rustock.B%20Rootkit.zip
[11] Frank Boldewin, Peacomm.C - Cracking the nutshell
http://www.reconstructer.org/papers/
Peacomm.C%20-%20Cracking%20the%20nutshell.zip
[12] Stealth MBR rootkit
http://www2.gmer.net/mbr/
[13] EP_X0FF and MP_ART, Unreal.A, bypassing modern Antirootkits
http://www.rootkit.com/newsread.php?newsid=647
[14] AK922 : Bypassing Disk Low Level Scanning to Hide File
http://rootkit.com/newsread.php?newsid=783
[15] CardMagic and wowocock, DarkSpy
http://www.fyyre.net/~cardmagic/index_en.html
[16] pjf, IceSword
http://pjf.blogone.net
[17] Gmer
http://www.gmer.net/index.php
[18] Pageguard papers (Uniformed) :
- Bypassing PatchGuard on Windows x64 by skape & Skywing
http://www.uninformed.org/?v=all&a=14&t=sumry
- Subverting PatchGuard Version 2 by Skywing
http://www.uninformed.org/?v=all&a=28&t=sumry
- PatchGuard Reloaded: A Brief Analysis of PatchGuard Version 3 by Skywing
http://www.uninformed.org/?v=all&a=38&t=sumry
[19] Greg Hoglund, Kernel Object Hooking Rootkits (KOH Rootkits)
http://www.rootkit.com/newsread.php?newsid=501
[20] Windows Heap Overflows - David Litchfield
http://www.blackhat.com/presentations/win-usa-04/bh-win-04-litchfield/
bh-win-04-litchfield.ppt
[21] Bypassing Klister 0.4 With No Hooks or Running a Controlled
Thread Scheduler by 90210 - 29A
http://vx.netlux.org/29a/magazines/29a-8.rar
[22] Microsoft, Debugging Tools for Windows
http://www.microsoft.com/whdc/devtools/debugging/default.mspx
[23] Kad, Phrack 59, Handling Interrupt Descriptor Table for fun and profit
[23] Kad, Phrack 59, Handling Interrupt Descriptor Table for fun and profit
http://phrack.org/issues.html?issue=59&id=4#article
[24] Wikipedia, Southbridge
http://en.wikipedia.org/wiki/Southbridge_(computing)
[25] Wikipedia, Northbridge
http://en.wikipedia.org/wiki/Northbridge_%28computing%29
[26] The NT Insider, Stop Interrupting Me -- Of PICs and APICs
http://www.osronline.com/article.cfm?article=211 (login required)
[27] Russinovich, Solomon, Microsoft Windows Internals, Fourth Edition
Chapter 3. System Mechanisms -> Trap Dispatching
[28] MSDN, KeyboardClassServiceCallback
http://msdn2.microsoft.com/en-us/library/ms793303.aspx
[29] Clandestiny, Klog
http://www.rootkit.com/vault/Clandestiny/Klog%201.0.zip
[30] Alexander Tereshkin, Rootkits: Attacking Personal Firewalls
www.blackhat.com/presentations/bh-usa-06/BH-US-06-Tereshkin.pdf
[31] MSDN, NdisMIndicateReceivePacket
http://msdn2.microsoft.com/en-us/library/aa448038.aspx
[32] Subverting VistaTM Kernel For Fun And Profit by Joanna Rutkowska
http://invisiblethings.org/papers/
joanna%20rutkowska%20-%20subverting%20vista%20kernel.ppt
[33] Vista RC2 vs. pagefile attack by Joanna Rutkowska
http://theinvisiblethings.blogspot.com/2006/10/
vista-rc2-vs-pagefile-attack-and-some.html
[34] Russinovich, Solomon, Microsoft Windows Internals, Fourth Edition
Chapter 7. Memory Management -> System Memory Pools
[35] KeUserModCallback ref - "Ring0 under WinNT/2k/XP" by Ratter - 29A
http://www.illmob.org/files/text/29a7/Articles/29A-7.003
[36] Joanna Rutkowska - Towards Verifiable Operating Systems
http://theinvisiblethings.blogspot.com/2007/01/
towards-verifiable-operating-systems.htm