Archive for mai, 2008

Hypervisor Abyss, part 3

Troisième et dernière partie de notre périple à travers les hyperviseurs, la dernière fois j’ai montré comment créer la structure de contrôle de notre VM Monitor, la VMCS. Cette fois, on va en enfin finaliser notre petit joujou, il ne reste plus qu’à définir le handler des VM Exits, la routine qui lancera l’hyperviseur et celle qui le lancera. Ce n’est pas le plus difficile, cependant il faut être prudent car un bug dans la routine qui gère les VM Exits peut avoir des répercussions beaucoup plus loin dans le Guest et je peux vous dire que pour debugger ça, c’est la gastro totale :=) (même que le pouvoir de constipation des chocapicz n’est pas assez puissant pour le contrer …) Donc on reste attentif et on lache rien.

La routine de gestion des VM Exits, a un rôle très important, c’est le code du VM Monitor qui fait le lien entre le Guest et l’hyperviseur. Elle a pour but de traiter les demandes du Guest qui ne peuvent pas être directement envoyées au hardware sans vérification afin d’éviter les conflits et erreurs comme par exemple ceux sur l’allocation des pages de mémoire, vous imaginez bien qu’il est nécessaire de faire la différence entre les pages du Guest et celles du Host. Elle sert aussi à transmettre au Guest les différents événements extérieurs qui surviennent, typiquement les interruptions.

Dans notre cas, la plupart de ces problèmes sont simplifiés, on n’a pas à ce prendre la tête avec des problèmes comme la gestion (ou plutôt virtualisation) de la mémoire ou bien la virtualisation des interruptions avec des Local APIC et I/O APIC, bref de devoir lire des schémas de ce genre qui piquent les yeux :
Virtual IDT

Pour nous, quand on ne sait pas comment gérer un évènement on l’envoie au Guest, vu que c’est notre OS hôte il sera se débrouiller avec même si il existe des actions que nous devons émuler obligatoirement. En fait, il y en en gros 2 causes de VM Exit, les premières dues à des instructions qui sont forcément virtualisées, ces instructions sont : CPUID, GETSEC, INVD, MOV from/to CR3 et les instructions introduite par VMX : VMCALL, VMCLEAR, VMLAUNCH, VMPTRLD, VMPTRST, VMREAD, VMRESUME, VMWRITE, VMXOFF, VMXON, and XSETBV. Juste pour préciser, GETSEC est une instruction qui provient du Safer Mode Extensions (SMX), INVD sert à flusher les caches du CPU sans les écrire en mémoire, XSETBV permet de jouer avec le Extended Control Register XCR0, la future extension des registres de contrôles. Pour toutes ces instructions nous devons les exécuter nous même dans notre VM Exit handler. Le problème est maintenant de connaître la cause du VM Exit et les informations relatives au différentes causes comme par exemple quel registre général (GPR) a servit lors du MOV FROM CR3.

Pour cela, le CPU va mettre à jour plusieurs champs de la VMCS, ceux qui nous intéressent sont VM_EXIT_REASON et EXIT_QUALIFICATION. Le premier est de la forme suivante :
Exit Reason

Le second dépend de la cause du VM Exit, par exemple lors d’un exit causé par un accès à un des control register on retrouve dans le EXIT_QUALIFICATION une structure du type :
Exit qualification structure

Avec ça on a tous les éléments pour émuler un mov eax, cr3 sans problème. Voici la liste des différentes causes de Vm Exit :

//
// VMX Exit Reasons
//

#define VMX_EXIT_REASONS_FAILED_VMENTRY 0x80000000

#define EXIT_REASON_EXCEPTION_NMI       0
#define EXIT_REASON_EXTERNAL_INTERRUPT  1
#define EXIT_REASON_TRIPLE_FAULT        2
#define EXIT_REASON_INIT                3
#define EXIT_REASON_SIPI                4
#define EXIT_REASON_IO_SMI              5
#define EXIT_REASON_OTHER_SMI           6
#define EXIT_REASON_PENDING_INTERRUPT   7

#define EXIT_REASON_TASK_SWITCH         9
#define EXIT_REASON_CPUID               10
#define EXIT_REASON_HLT                 12
#define EXIT_REASON_INVD                13
#define EXIT_REASON_INVLPG              14
#define EXIT_REASON_RDPMC               15
#define EXIT_REASON_RDTSC               16
#define EXIT_REASON_RSM                 17
#define EXIT_REASON_VMCALL              18
#define EXIT_REASON_VMCLEAR             19
#define EXIT_REASON_VMLAUNCH            20
#define EXIT_REASON_VMPTRLD             21
#define EXIT_REASON_VMPTRST             22
#define EXIT_REASON_VMREAD              23
#define EXIT_REASON_VMRESUME            24
#define EXIT_REASON_VMWRITE             25
#define EXIT_REASON_VMXOFF              26
#define EXIT_REASON_VMXON               27
#define EXIT_REASON_CR_ACCESS           28
#define EXIT_REASON_DR_ACCESS           29
#define EXIT_REASON_IO_INSTRUCTION      30
#define EXIT_REASON_MSR_READ            31
#define EXIT_REASON_MSR_WRITE           32

#define EXIT_REASON_INVALID_GUEST_STATE 33
#define EXIT_REASON_MSR_LOADING         34

#define EXIT_REASON_MWAIT_INSTRUCTION   36
#define EXIT_REASON_MONITOR_INSTRUCTION 39
#define EXIT_REASON_PAUSE_INSTRUCTION   40

#define EXIT_REASON_MACHINE_CHECK       41

#define EXIT_REASON_TPR_BELOW_THRESHOLD 43

#define VMX_MAX_GUEST_VMEXIT	EXIT_REASON_TPR_BELOW_THRESHOLD

Continuons, nous sommes capables de connaître la cause du Vm Exit et d’émuler l’instruction, pour relancer le Guest, il faut bien garder à l’esprit que nous devons lui redonner les mêmes GPR (sauf pour esp, eip et eflags) que ceux lors du Vm Exit, avec bien évidement la mise à jour nécessaire du à l’émulation d’une instruction les modifiants comme CPUI ou RDMSR. Pour faire simple, on sauvegarde un contexte avec pushad et on le modifie dans notre Vm Exit handler. Il faut aussi penser à mettre à jour l’eip, en effet, comme nous avons émulé le comportement de l’instruction nous devons faire pointer l’eip du Guest vers l’instruction suivante et pour cela la VMCS contient un champ nommé VM_EXIT_INSTRUCTION_LEN qui donne la taille de l’instruction qui a causé le VM Exit, même pas besoin d’un LDE (Length Disassembly Engine) \o/. Enfin on n’oublie pas d’exécuter l’instruction VMRESUME pour rendre la main au Guest. Au final, on a un gros switch sur les causes de VM Exit avec des « case » de la forme :

case EXIT_REASON_CPUID :
{
	//
	//
	//
	MyKdPrint("EXIT_REASON_CPUID occurs in process [PID : %lu TID : %lu]\n", PsGetCurrentProcessId(), PsGetCurrentThreadId());

	__asm
	{
		MOV EAX, LocalExitContext.GuestEAX
	
		CPUID
		
		MOV LocalExitContext.GuestEAX, EAX
		MOV LocalExitContext.GuestEBX, EBX
		MOV LocalExitContext.GuestECX, ECX
		MOV LocalExitContext.GuestEDX, EDX
	}
	WriteVMCS(GUEST_EIP, GuestEip+ExitInstructionLen);
	break;
}

Ok, on avance traquillement, on code ce qu’il faut pour émuler le reste des instructions qui sont obligatoirement virtualisées et on a notre VM Exit handler minimaliste. Maintenant On va pouvoir coder la routine qui lance l’hyperviseur mais avant il reste à régler un petit problème.

Lors du lancement de la VM par l’instruction VMLAUNCH, qui sera le premier VM Entry, le CPU va placer l’eip et l’esp du Guest en fonction des valeurs de la VMCS, GUEST_EIP et GUEST_ESP. De la même façon, lors d’un VM Exit, le VMX va mettre à jour les valeurs d’esp et d’eip en fonction des champs HOST_EIP et HOST_ESP. Commençons par résoudre le cas des VM Exit, on sait que le HOST_EIP doit pointer sur la routine qui gère les VM Exit, par contre quelle valeur de esp prendre pour le retour dans l’Host ? Sachant qu’un VM Exit peut intervenir dans le contexte de n’importe quel thread on risque d’abimer la stack ou pire de tomber dans une zone mémoire non valide. Du fait que lors de l’initialisation de la VMCS on a définit les segments du Host comme des segments ring0, on va se retrouver dans un contexte ring0, heureusement d’ailleurs car des instructions comme rdmsr ou invd sont privilégiées (elles génèrent un #GP(0) en ring3). Le plus simple est donc de faire pointer l’esp du Host vers une zone mémoire alloué en NonPaged Pool qui servira de stack pour la routine qui gère les VM Exit.

Concernant le premier VM Entry, j’avoue que j’ai fait simple, j’ai prit la valeur de esp avant VMLAUNCH et je l’ai écrite dans la VMCS et pour eip j’ai prit l’adresse de l’instruction suivant le VMLAUNCH ce qui donne le code suivante (attention ça fait de la peur) :

VOID StartVMX()
{
	EFLAGS EFlags;
	ULONG GuestStack;
	ULONG Error;

	__asm MOV GuestStack, ESP
	
	//FireBp();
	
	//
	//	Set ESP for the Guest right before calling VMLAUNCH
	//
	WriteVMCS(GUEST_ESP, GuestStack);

	//
	// Execute VMLAUNCH to launch the guest VM. If VMLAUNCH fails due to any 
	// consistency checks before guest-state loading, RFLAGS.CF or RFLAsiGS.ZF will 
	// set and the VM-instruction error field will contain the error
	// code.
	//   
	__asm
	{
		PUSH barp
		MOV EAX, GUEST_EIP

		_emit	0x0F // 	
		_emit	0x79 // 
		_emit	0x04 // VMWRITE EAX, [ESP]
		_emit	0x24 //

		ADD ESP, 4

		_emit	0x0F //	
		_emit	0x01 // VMLAUNCH 
		_emit	0xC2 //
barp:
	}
	EFlags=GetEFlags();
	if((EFlags.CF==1) || (EFlags.ZF==1))
	{
		//
		//	Get the ERROR number using VMCS field VM_INSTRUCTION_ERROR
		//
		ReadVMCS(VM_INSTRUCTION_ERROR, &Error);
		KdPrint(("VMLAUNCH failed, VM Instruction Error : %lu\n", Error));
		FireBp();
	}
	KdPrint(("VMLAUNCH OK!\n"));
}

Remarquez le bel asm inline avec notamment les instructions VMWRITE et VMLAUNCH hardcodées en _emit, le compilo C ne connaissant pas ces instructions il faut les écrire en hard … Par contre dans des fichiers .asm le compilo asm du WDK ne pose pas de soucis … va falloir que je recode tout cela de manière plus propre quand même :p

Whaou, on peut lancer notre hyperviseur, c’est le pied ! Maintenant si on veut arrêter la virtualisation il faut appeler l’instruction VMXOFF mais c’est à l’hyperviseur de la faire. Arrive donc le problème de la communication du guest avec le HVM et pour cela il existe l’instruction VMCALL, cette instruction qui ne peut être appelée qu’en ring0 oblige le Guest à faire un VM Exit, après à vous de traiter comme vous voulez cet exit dans votre VM Exit Handler. Perso j’effectue un VMCALL dans la routine DriverUnload de mon hyperviseur avec une valeur précise dans eax, une fois dans ma routine qui gère les VM Exit je lance VMXOFF, je restaure le contexte intervenu lors du VM Exit et je saute sur l’instruction située après VMLAUNCH. C’est ma façon de faire, à vous de voir si vous trouvez plus simple.

Alors, j’ai dit plus haut qu’il existait 2 types de VM Exit, les premiers, ceux dus à la virtualisation obligatoire de certaines instructions, maintenant nous allons voir ceux que l’utilisateur peut choisir. Le mieux étant un exemple concret nous allons effectuer un VM Exit sur les I/O, plus précisément sur l’ensemble des instructions IN, INS/INSB/INSW/INSD, OUT, OUTS/OUTSB/OUTSW/OUTSD. Le jeu d’instruction VMX nous offre 2 choix, le premier consiste à faire un VM Exit tout le temps dès qu’il y a un I/O, le second nous offre la possibilité de choisir les I/O sur lesquels le Guest doit effectuer un VM Exit.

J’ai choisit d’utiliser la second feature, pour cela, il faut mettre à 1 le bit « Use I/O bitmaps » (25) du PRIMARY_CPU_BASED_VM_EXEC_CONTROL. En faisait cela nous demandons au VMX de consulter lors d’un I/O les bitmaps dont les adresses physiques sont stockées dans les champs IO_BITMAP_A, IO_BITMAP_HIGH, IO_BITMAP_B et IO_BITMAP_B_HIGH de la VMCS. Ces bitmaps représentent tout simplement des bits qui indiquent s’ils sont à 1 de faire un VM Exit sur l’I/O indiqué par leur indice, par exemple si le 1337 ème bit est à 1 alors chaque I/O de 1 byte sur le port 1337 créera un VM Exit. Chaque IO_BITMAP est d’une taille de 4ko, c’est-à-dire que l’IO_BITMAP_A concerne les ports 0×0000 à 0x7FFF et que l’IO_BITMAP_B gère les ports 0×8000 jusqu’à 0xFFFF.

On sait que le champ EXIT_QUALIFICATION de la VMCS lors d’un I/O est de la forme :
Exit qualification I/O

A partir de là on peut savoir quel type d’I/O désire effectuer le Guest, reste le problème de l’émulation de l’instruction. Si on décide de l’émuler dans l’hyperviseur on va devoir prendre en compte chaque cas possible de forme d’instructions, c’est long, trop long … Il existe une façon plus élégante pour gérer cela, laisser le Guest faire l’I/O en mettant à 0 les bits du ou des ports concernés et de reprendre l’exécution là ou elle avait laissée au moment du VM Exit. Attention si vous faites par exemple un « in ax, 0×10 » comme l’I/O se fait sur 16 bits l’instruction in va lire les 8 bits de poids faible sur le port 0×10 et les 8 bits de poids forts sur le port 0×11. Il faut donc bien prendre en compte la taille de l’I/O pour savoir combien de bits on va mettre à 0.

Cette méthode est pratique mais ajoute 2 nouveaux problèmes, on ne sait pas quand on va remettre les bits du port à 1 dans le bitmap et on ne peut connaître le résultat d’un out sur un port. Solution, tracer l’instruction à l’aide du TrapFlag de l’eflags, en effet si on set ce bit dans l’eflags du Guest une exception sera levée après l’exécution d’une instruction et comme de par hasard notre hyperviseur nous permet d’effectuer des VM Exit sur certaines exceptions, pour monitorer les « int 1″ il suffit de mettre le bit 1 de l’EXCEPTION_BITMAP du VMCS à 1. Dans le cas ou le TrapFlag est à 1, on obtient une « int 1 » après l’exécution de chaque instruction donc notre VM Exit Handler n’aura plus qu’a gérer ça en contrôlant si l’eip d’où provient l’I/O est un eip tracé, si oui alors on restaure l’I/O bitmap et le TrapFlag sans réinjecter l’exception au Guest et ca repart :]

Reste le cas ou l’on tombe dans un VM Exit avec un « int 1 » provenant d’une instruction d’I/O non tracée par notre HVM, il faut la réinjecter au Guest. Lors d’une exception on retrouve dans le VM_EXIT_INTR_INFO de la VMCS la structure suivante :
VM Exit interruption info

Il est possible d’injecter un event dans l’IDT du Guest lors d’un VM Entry en utilisant le champ VM_ENTRY_INTR_INFO_FIELD qui est de la même forme que la structure VM_EXIT_INTR_INFO puis de définir les champs VM_ENTRY_INTR_INFO_FIELD et VM_ENTRY_INSTRUCTION_LEN comme dit dans la doc (oui j’ai la flemme de décrire) :
VM Entry interruption info

Avec cette application il nous est possible de contrôler tous les I/O sur les ports de notre machine, on pourrait très bien s’amuser à regarder les valeurs des in et out sur les ports clavier par exemple :p. Evidemement cela est une des possibilité offerte par la virtualisation mais rien qu’avec cette application on peut réaliser de nombreuses choses, c’est simple vous conaissez un tool qui vous permet de monitorer et de faire des stats sur les I/O de votre PC ?

Ici ce termine la série sur la création d’un hyperviseur de type bluepill, j’ai essayé d’être le plus clair possible même si je reconnais qu’il est loin d’être trivial d’implémenter une telle chose, c’est long, c’est difficile (surtout le debuggage), il faut lire et relire la doc dans tous les sens mais c’est très enrichissant. On apprend à connaître encore plus les fondements de notre OS et on se rapproche de plus en plus du hardware. Si vous avez des questions n’hésitez pas en tout cas. Concernant la realease du code de Abyss, je préfère attendre un peu et la remettre à plus tard, pourquoi ? Tout simplement parce que j’aimerais faire quelque chose de plus intéressant avec et que certaines parties du code font de la peur tellement qu’on dirait que c’est un québécois de l’underground qui les a faites (admirez la pub !).

En attendant vous pouvez regarder les codes mis en référence en bas du post que j’ai trouvé sur le net et qui m’ont servit dans mon approche de la virtualisation.
Voilà, je n’en ai pas finit avec la virtualisation, je vous promets encore de nombreuses applications à venir et j’espère sincèrement que vous aussi :]

https://www.rootkit.com/vault/mobydefrag/vmxcpu.rar
http://deroko.phearless.org/cpuid_break.rar
http://bluepillproject.org/stuff/nbp-0.32-public.zip

6 comments mai 26th, 2008

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


Calendar

mai 2008
L Ma Me J V S D
« avr   juin »
 1234
567891011
12131415161718
19202122232425
262728293031  

Posts by Month

Posts by Category