Quelle surprise la première fois que l'on
"titille" l'IDT sur un système multi-processeurs.
Le crash que l'on se prend dans les dents est assez
violent. Soit... mais ça a au moins le mérite
d'exposer le problème assez rapidement. ;p
En effet, chaque micro-processeur (cpu) travaille
avec son propre jeu de registres et son propre environnement.
Dans un environnement multi-processeurs, il est bien
sur possible de jouer avec les interruptions sans
être embêté. Il faudra juste prendre
la peine de sonder ou modifier tous les environnements
des cpu présents.
Le but de ce texte est simplement d'exposer 4 techniques
qui permettent d'obtenir les pointeurs et/ou les tailles
des tables IDT et GDT propres à chaque processeur
présent.
Le principe des deux premiers exemples consiste simplement
à soutirer nos informations à l'aide
des instructions assembleurs sidt et sgdt. Dans une
boucle, basée sur le nombre de cpu présent,
nos instructions sont exécutées dans
un thread que l'on force à fonctionner dans
chaque cpu. Ceci permet de récupérer
les valeurs des registres idtr et gdtr de chaque cpu
et donc nos tables. :)
Le premier exemple expose
une routine fonctionnant en mode utilisateur. Alors
que le deuxième exemple
expose une routine basée sur le même
principe mais fonctionnant en mode kernel. L'inconvénient
majeur de cette technique est le temps d'exécution.
Le passage d'un thread vers un autre cpu demande du
temps.
En mode kernel, le niveau de priorité du code
exécuté (irql) ne permet pas tout le
temps d'utiliser les API Native. Le
troisième exemple essaye de palier à
ce problème en proposant une méthode
basée sur la recherche et le scan de la table
HalpProcessorPCR présente dans hal.dll. Cette
table contient les pointeurs des objets PCR (Processor
Control Region) propre à chaque cpu. Ces objets
définissent les environnements courants des
cpu. Cette méthode, utilisé par Softice,
exécute une recherche mémoire par signature
afin de trouver en premier lieu la routine HalInitializeProcessor.
Malheureusement, depuis Windows Server 2003 SP1, les
signatures changent. Cette méthode a donc ses
limites et il devient de plus en plus difficile de
développer quelque chose de générique.
Pour palier à ce problème, j'ai développer
une quatrième méthode
permmettant de ne pas tenir compte du système
hôte. Elle est donc (à ce jour) générique.
Cette dernière méthode recherche directement
les différents KPCR présents en mémoire.
La recherche est cette fois basée sur des régles
simples découlant directement de l'architecture
des systèmes Windows.
User
Mode
méthode 1
Cette technique utilise des API documentés
et fonctionne en mode utilisateur. Ces caractéristiques
la rendent sûre et fiable. Cependant, il est
à noter qu'elle ne fonctionne pas dans un environnement
de type Machine Virtuelle.
Pour obtenir ce que l'on désire, il nous
faut seulement 2 ingrédients.
Kernel
Mode
méthode 2
PASSIVE_LEVEL (IRQL=0)
Une technique similaire à celle du mode utilisateur
est aussi réalisable en mode kernel. Il ne
faut pour cela pas avoir peur d'utiliser des API heuuu...
plus ou moins documentées. ;)
Toutes les API et data system proposés dans
cette technique sont directement exportés par
ntoskrnl.exe.
Pour connaître le nombre de cpu présents,
il est possible d'utiliser le data KeNumberProcessors.
Ce data est global au système. Il est aussi
possible d'utiliser les API NtQuerySystemInformation
ou ZWQuerySystemInformation (plus ou moins documentés).
Le nombre de cpu se trouve dans la structure SYSTEM_BASIC_INFORMATION.
Cette structure s'obtient en donnant System_Basic_Information
(valeur:0) au paramètre SYSTEM_INFORMATION_CLASS.
Etant non documentée, il est difficile de
dire à quel irql la routine KeSetAffinityThread
peut être exécutée. Il est donc
raisonnable de s'imposer l'irql le plus bas (PASSIVE_LEVEL)
pour assurer son exécution. Ceci dit, il est
sûrement possible de l'exécuter 1 ou
2 crans plus haut. c'est à vous de voir...
;p
Quoi qu'il en soit, cette API ne peut pas être
exécuté à tous les niveaux de
priorité. Cela pose donc le problème
de la récupération des tables idt et
gdt à un niveau de priorité élevé.
La dernière technique proposera une solution
à ce problème.
En regardant la routine KeSetAffinityThread de plus
près, nous pouvons voir qu'elle mène
tout droit au crash dans 2 cas :
Il faudra donc faire correctement ces vérifications
avant d'utiliser KeSetAffinityThread.
Cette procédure fonctionne très bien
dans la routine d'initialisation d'un driver puisque
celle-ci est exécutée en PASSIVE_LEVEL.
Kernel
Mode
méthode 3
ALL_LEVEL
(IRQL=0 à 31)
Cette dernière technique permet
d'obtenir ce que l'on désire sans dépendre
de l'irql courant. Elle est directement inspirée
de celle utilisée par Softice pour soutirer
les informations dont il a besoin. C'est celle que
je préfère :)
Dans un système NT, la couche
la plus basse se nomme HAL. C'est la couche d'abstraction
matérielle. Cette couche communique directement
avec le matériel (hardware). Il n'y a donc
rien d'étonnant à trouver les routines
d'initialisation des cpu à cet endroit :)
Chaque CPU est définie par
un objet nommé PCR (Processor Control Region).
C'est dans cette région que l'on trouve tout
l'environnement courant du cpu. Et c'est donc dans
cette région que l'on trouve les tables IDT
et GDT propres au cpu. :)
lkd> dt _kpcr
nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x01c SelfPcr : Ptr32 _KPCR
+0x020 Prcb : Ptr32 _KPRCB
+0x024 Irql : UChar
+0x028 IRR : Uint4B
+0x02c IrrActive : Uint4B
+0x030 IDR : Uint4B
+0x034 KdVersionBlock : Ptr32 Void +0x038
IDT : Ptr32 _KIDTENTRY
+0x03c GDT : Ptr32 _KGDTENTRY
+0x040 TSS : Ptr32 _KTSS
+0x044 MajorVersion : Uint2B
+0x046 MinorVersion : Uint2B
+0x048 SetMember : Uint4B
+0x04c StallScaleFactor : Uint4B
+0x050 DebugActive : UChar
+0x051 Number : UChar
+0x052 Spare0 : UChar
+0x053 SecondLevelCacheAssociativity : UChar
+0x054 VdmAlert : Uint4B
+0x058 KernelReserved : [14] Uint4B
+0x090 SecondLevelCacheSize : Uint4B
+0x094 HalReserved : [16] Uint4B
+0x0d4 InterruptMode : Uint4B
+0x0d8 Spare1 : UChar
+0x0dc KernelReserved2 : [17] Uint4B
+0x120 PrcbData : _KPRCB |
|
Lorsque les objets PCR sont créés,
ntoskrnl appelle la routine HalInitializeProcessor
afin d'initialiser chaque cpu et permettre à
tout ce beau monde de fonctionner ensemble. Cette
routine se trouve dans hal.dll. C'est HalInitializeProcessor
qui a la charge de stocker les pointeurs de tous les
objets PCR. Pour cela, elle a à sa disposition
la table nommée HalpProcessorPCR. Sympa la
table ! :)
Si nous avons accès à cette table, nous
pouvons aisément faire notre récolte
:)
Malheureusement cette table n'est pas exportée.
Pour l'identifier clairement il nous faut donc les
symbols de hal.dll.
Sans les symbols, impossible d'identifier le pointeur
de la table HaLpPr0cESs0rPcR
! Hein ? Quoi ?
Oulaaaaa..... pas si vite ! Il faudrait peut être
voir aussi comment l'on peut interpréter "identifier
clairement". Personnellement, j'ai tendance à
être plutôt large... :p
Commençons déjà
par une petite recherche sur notre table HalpProcessorPCR
:
8001907C |
_HalpProcessorPCR
dd 20h dup(0) |
; DATA XREF:
HalInitializeProcessor(x,x)+1Dw |
|
|
; HalpResetAllProcessors():loc_80017F39r |
|
Cette table a une capacité
de 32 pointeurs d'objet PCR. Ceci illustre tout simplement
le fait que les systèmes NT supportent jusqu'à
32 CPU maximum (20h).
En regardant les infos de droite,
nous pouvons voir que seulement deux endroits font
référence à la table : dans les
routines HalInitializeProcessor
et HalpResetAllProcessors.
La routine HalpResetAllProcessors
n'est accessible qu'avec les symbols.... Bon, on est
pas plus avancé... Par contre HalInitializeProcessor
est bel et bien PUBLIC et exportée par hal.dll.
C'est mieux déjà :)
Maintenant regardons de plus prés
cette fameuse routine sous les principaux systèmes
NT :
|
|
; ¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦
S U B R O U T I N E ¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦
; __stdcall HalInitializeProcessor(x,x) |
|
|
public _HalInitializeProcessor@8
_HalInitializeProcessor@8 proc near
arg_0 = byte ptr 4
arg_4 = dword ptr 8 |
80011FC4
80011FCE
80011FD3
80011FD8
80011FDE
80011FE5
80011FEF
80011FF7
80011FF9
.
.
. |
C7 05 30 F0 DF FF FF FF+
0F B6 44 24 04
A2 94 F0 DF FF
8B 0D 1C F0 DF FF 89
0C 85 C8 7F
01 80
C7 05 4C F0 DF FF 64 00+
F0 0F AB 05 20 9D 01 80
8B D0
A1 58 8B 01 80
.
.
. |
mov dword ptr ds:0FFDFF030h, 0FFFFFFFFh
movzx eax, [esp+arg_0]
mov ds:0FFDFF094h, al
mov ecx, ds:0FFDFF01Ch mov
ds:_HalpProcessorPCR[eax*4],
ecx
mov dword ptr ds:0FFDFF04Ch, 64h
lock bts ds:_HalpActiveProcessors, eax
mov edx, eax
mov eax, ds:_HalpDefaultInterruptAffinity
.
.
. |
|
|
; ¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦
S U B R O U T I N E ¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦
; __stdcall HalInitializeProcessor(x,x) |
|
|
public _HalInitializeProcessor@8
_HalInitializeProcessor@8 proc near
arg_0 = byte ptr 4
arg_4 = dword ptr 8 |
80011760
8001176B
80011770
80011776
8001177D
80011784
8001178F
80011797
80011799
.
.
. |
64 C7 05 30 00 00 00 FF+
0F B6 44 24 04
64 A2 94 00 00 00
64 8B 0D 1C 00 00 00 89
0C 85 7C 90
01 80
64 C7 05 4C 00 00 00 64+
F0 0F AB 05 00 32 02 80
8B D0
A1 6C DA 01 80
.
.
. |
mov large dword ptr fs:30h, 0FFFFFFFFh
movzx eax, [esp+arg_0]
mov large fs:94h, al
mov ecx, large fs:1Ch mov
ds:_HalpProcessorPCR[eax*4],
ecx
mov large dword ptr fs:4Ch, 64h
lock bts ds:_HalpActiveProcessors, eax
mov edx, eax
mov eax, ds:_HalpDefaultInterruptAffinity
.
.
. |
Windows
Server 2003 Entreprise SP0
|
|
; ¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦
S U B R O U T I N E ¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦
; __stdcall HalInitializeProcessor(x,x) |
|
|
public _HalInitializeProcessor@8
_HalInitializeProcessor@8 proc near
arg_0= byte ptr 4
arg_4= dword ptr 8 |
80012F10
80012F1B
80012F20
80012F26
80012F2D
80012F34
80012F3F
80012F47
80012F49
.
.
. |
64 C7 05 30 00 00 00 FF+
0F B6 44 24 04
64 A2 94 00 00 00
64 8B 0D 1C 00 00 00 89
0C 85 80 A0
01 80
64 C7 05 4C 00 00 00 64+
F0 0F AB 05 20 4D 02 80
8B D0
A1 BC EC 01 80
.
.
. |
mov large dword ptr fs:30h, 0FFFFFFFFh
movzx eax, [esp+arg_0]
mov large fs:94h, al
mov ecx, large fs:1Ch mov
ds:_HalpProcessorPCR[eax*4],
ecx
mov large dword ptr fs:4Ch, 64h
lock bts ds:_HalpActiveProcessors, eax
mov edx, eax
mov eax, ds:_HalpDefaultInterruptAffinity
.
.
. |
|
Nous pouvons voir quelques différences
entre win2k et XP/Server 2003. Les adresses du PCR
courant sont en brut pour win2k. Alors que sous XP
et Server 2003, le PCR courant est manipulé
en passant par le segment fs.
Mais il faut surtout relever que
l'opcode utilisé pour remplir la table garde
la même signature pour les trois systèmes.
signature : 0x89,0x0C,0x85
+ p_HalpProcessorPCR
Voilà :) Tout est là
!
Il n'y a plus qu'à faire ses
courses tranquillement. Pas besoin d'API, aucune dépendance
à l'irql courant, rapide et ouverte...
Elle est bien sur limitée
aux systèmes étudiés pour ce
texte et demande donc quelques modifications pour
pouvoir fonctionner sur d'autres versions de windows.
Car depuis Windows Server 2003 SP1 les signatures
changent.
Kernel
Mode
méthode 4 (générique)
ALL_LEVEL (IRQL=0
à 31)
Cette méthode n'utilise pas d'API et pas de
structure en brut. Elle travaille sur une gestion
du scan de la mémoire qui évite les
fautes de pages. Elle n'est donc pas dépendante
de l'IRQL courant.
C'est pour l'instant la méthode la plus générique
que j'ai pu trouver. Le principe est simple : scanner
la mémoire du noyau à la recherche des
KPCR.
Pour cela, quelques règles simples sont utilisées
pour retrouver sans ambiguïté tous les
KPCR :