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 :