Hypervisor Abyss, part 3

mai 26th, 2008 at 05:51 admin

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

Entry Filed under: RE

6 Comments

  • 1. texane  |  mai 27th, 2008 at 20:26

    pas mal, comme tu dis faut pousser encore un peu tout ca.
    En ce qui concerne le code + propre, tu peux utiliser les intrinsics du compilo ie. __vmx_write, include intrin.h (j ai eu qques soucis avec certains d entre eux non definis, je suppose que j utilisais une version batarde du ddk/cl).
    Pour ton application de monitoring des io ports c’est pas mal… comme t’es dans windows, et avec un peu de boulot si pas deja fait, tu peux pousser le bouza en enumerant les devices PCI et leur plage memoire, pour monitorer aussi les memory mapped io, ca te ferait un truc vraiment pas mal.
    D ailleurs tu arrives a appeler des fonctions de ntos depuis l’hyperviseur? Si oui, t as quoi comme limitation? Et avec PAE enable sur l’host, t as des problemes pour switch?
    En tous cas c’est cool, y a beau avoir des projets similaires existants, y a tres peu d applications au dessus de ca, comme si l hyperviseur c’etait la fin en soi… vivement le code.


  • 2. admin  |  mai 28th, 2008 at 14:09

    Yo texane,

    Merci pour l’astuce des intrinsics du compilo, je savais qu’ils en existait mais je ne savais pas ou les trouver, par contre je ne vois pas le intrin.h dans le dossier « inc » de mon WDK (version 6001.18001) par contre il apparaît bien dans les includes de mon Visual Studio C 2008 comme dit ici , la et la. Il existe bien les fichiers : emmintrin.h, mmintrin.h et xmmintrin.h dans mon WDK mais ils servent aux extensions MMX et SSE3. Bref de toute façon j’ai codé mes propres wrappers VMX dans des fichiers .asm qui vont aussi servir à vérifier que l’instruction VMX s’est bien déroulée en regardant le statut de l’eflags.

    Concernant le monitoring des memory mapped I/O, j’avoue que je ne me suis pas encore penché dessus, d’après ce que j’ai lu et compris, il faudrait retrouvé les PTE des memory mapped I/O, les marqués non-present puis trapper les exceptions lors des accès sur ceux ci avec l’hyperviseur, ca serait la seule solution veux que le VMX ne peut faire la différence entre un accès mémoire sur un adressage physique qui correspond à la de RAM et un adressage physique qui correspond à un memory mapped I/O … Je verrais ça après je pense :p

    Sinon quand je suis dans le contexte de l’hyperviseur je me retrouve dans le cas d’un driver normal et je peux donc avoir accès a toutes les fonctionnalité du kernel, donc c’est cool :) Le PAE ne pose pas de limitation, sauf si tu veux jouer avec la mémoire il faut faire attention à la forme de tes PDE/PTE


  • 3. texane  |  mai 28th, 2008 at 20:07

    ok ok, bien pour tout ca.
    Pour le memory mapped io je pensais a ca aussi. Je sais que c’est faisable pour un nombre limite de CR3, mais y aurait pas moyen de shadow une translation d’addresse? ca peut etre vraiment pratique pour analyser le comportement d un driver proprio, voir faire mumuse avec les ring buffers des cartes reseaux pci (encore plus bas que ndis, meme si t as pas besoin d un hyperviseur pour ca).
    Pour les intrin ca me revient, j avais surement cp le header dans mon projet, mais j avais de toute facon du recoder un wrapper sur certaines des instruction vmx (cpuid aussi je crois).


  • 4. admin  |  mai 29th, 2008 at 16:46

    Yop, effectivement tu peux implémenter des shadow CR3 avec l’hyperviseur, ca te permet de faire des VM Exit si ton cr3 est différents des N valeurs gérés par l’hvm, je vois pas ce que ca peut m’apporter dans le cas présent car il faudrait dire que les pages des mm I/O sont non-present et pour ca, ya pas le choix faut modif les PTEs à la main. D’ailleurs je me demandais si il fallait le faire pour tous les PTEs kernel de chaque process ….

    En tout cas, c’est loin d’être trivial …


  • 5. texane  |  mai 29th, 2008 at 20:14

    justement, ma question c’etait de savoir si y avait un moyen de shadow les translations de la mmu comme tu peux le faire avec les acces CR3. Apres les TLB mais avant la mmu donc.
    Dans l optique ou un process arriverait a mapper un device PCI avec une @ userland qui soit accessible dans tous les process (a part en passant par un driver, je vois vraiment pas), il faudrait en effet marquer les ptes correspondantes de chaque process comme invalid. Ca ne poserait pas de probleme puisque t as un vm exit a chaque mov CR3, XXXX, ce qui arrive dans KiSwapContext au moment de switcher de process.


  • 6. Malware :: Hiding Virtual&hellip  |  novembre 27th, 2012 at 00:40

    [...] Ivanlef0u’s Blog – Hypervisor Abyss (in French) :: Part 1 :: Part 2 :: Part 3 [...]


Trackback this post


Calendar

octobre 2019
L Ma Me J V S D
« fév    
 123456
78910111213
14151617181920
21222324252627
28293031  

Most Recent Posts