Exemple & Application d’une génération de code à partir de fichiers .PDB et de l’api DIA_SDK

Premier post sur le Blog SX ! Le but de cet article est de présenter une méthode pour faciliter la portabilité d’un code anti-rootkit, situé en ring0, qui irait directement lire les structures kernel, par exemple les champs ‘VadRoot’ et ‘ObjectTable’ de la structure ‘_EPROCESS’. Le problème qui se pose avec cette méthode est l’instabilité du code : un accès erroné à une zone mémoire dans un code kernel et c’est le BSOD assuré !

I) Introduction au problème

Illustrons le problème que nous tentons de résoudre :

PHANDLE_TABLE		pCurTable = NULL;

pCurTable = *(PHANDLE_TABLE *)((UINT_PTR)IoGetCurrentProcess() + 0x0f4/*ObjectTable*/);

Ce code marche très bien sous mon Windows 7 SP0 32 bits. Mais marche-t-il sous un XP 32 bits ? Sous un Win 7 64 bits ? La réponse est non car le champ ‘ObjectTable’ de la structure _EPROCESS ne se situe pas à l’offset 0x0f4 sous XP 32 bits et Win7 64 bits :

  • – Sous XP 32 bits SP0 : le champ ‘ObjectTable’ est à l’offset 0x0c4
  • – Sous Win 7 64 bits SP0 : le champ ‘ObjectTable’ est à l’offset 0x200

Si on veut pouvoir utiliser notre code pour récupérer le pointeur vers la table des handles d’un processus sur plusieurs versions différentes de Windows, il va falloir trouver une façon de spécifier l’offset du champ ‘ObjectTable’ en fonction de la version de l’OS sur lequel ce code s’exécute. Une première solution consisterait à faire ainsi :

DWORD		ObjectTable_Offset = 0x0;

if (OS == WINDOWS_XP_SP0_32BITS) {
	ObjectTable_Offset = 0x0c4;
} else if (OS == WINDOWS_7_SP0_32BITS) {
	ObjectTable_Offset = 0x0f4;
} else if (OS == WINDOWS_7_SP0_64BITS) {
	ObjectTable_Offset = 0x200;
} else {
	// Erreur ! Cette version de Windows n'est pas supportée !
}

Bon très bien, c’est une solution qui va marcher. Arrêtons-nous maintenant quelques minutes et listons ensemble tous les défauts d’une telle méthode :

  1. Coding style pourri ! En jargon de programmeur, on appelle ça une forêt de if/elseif/else et c’est le mal ! Bon en soit si c’est juste du coding style, après tout on s’en fou, non ? Les points suivants vont expliquer pourquoi cette pratique de programmation est rarement justifiée.
  2. Si je veux ajouter une nouvelle version, je dois éditer tous les endroits de mon code où se trouve une telle construction. En effet, le même problème va se poser pour d’autres champs, comme le champ ‘VadRoot’.
  3. Je dois installer chaque version de chaque OS que je veux supporter et récupérer l’offset dans un WINDBG… Je vous préviens vous n’êtes pas sorti de l’auberge et vous en aurez pour un long moment. De plus, comme c’est un travail répétitif, vous risquez de faire des erreurs et donc d’avoir tout à reprendre plusieurs fois. Bref de quoi devenir fou.

Cette liste n’est pas exhaustive. Bref, vous l’aurez compris, ce n’est pas une solution viable. Je ne discuterai pas ici d’ailleurs de la méthode qui consiste à aller lire directement les structures kernel plutôt que d’utiliser des APIs documentées (ou pas) car c’est un autre débat.

Dans la seconde partie, nous présentons rapidement les fichiers .PDB, à quoi ils servent et surtout à quoi ils vont nous servir.

II) Les fichier .PDB

Ces fichiers sont créés lors d’une compilation d’un projet en mode DEBUG. Ils permettent de retrouver facilement toute sorte d’information concernant, par exemple une fonction ou un type de donnée, afin de faciliter le debugging du programme.

En effet, si vous avez déjà utilisé WINDBG, le debugger de Microsoft, vous avez pu remarquer qu’il y avait une histoire de symboles et qu’il fallait configurer WINDBG de telle sorte qu’il utilise les bons symboles. Par exemple, si vous faites du local kernel debugging et que vous n’avez pas installé les symboles, vous ne pourrez obtenir aucune information concernant les types de données manipulés par le noyau Windows. Un type de donnée très connu est celui qui représente un processus en mémoire, _EPROCESS. Lorsque vous tapez la commande ‘dt nt!_EPROCESS’ dans l’invite de commande de LKD (le debugger local de Microsoft), vous obtenez :

lkd> dt nt!_EPROCESS
+0x000 Pcb              : _KPROCESS
+0x098 ProcessLock      : _EX_PUSH_LOCK
+0x0a0 CreateTime       : _LARGE_INTEGER
+0x0a8 ExitTime         : _LARGE_INTEGER
+0x0b0 RundownProtect   : _EX_RUNDOWN_REF
+0x0b4 UniqueProcessId  : Ptr32 Void
[...]
+0x0f4 ObjectTable      : Ptr32 _HANDLE_TABLE
[...]
+0x1a8 Peb              : Ptr32 _PEB
[...]
+0x278 VadRoot          : _MM_AVL_TABLE
[...]
+0x2bc TimerResolutionStackRecord : Ptr32 _PO_DIAG_STACK_RECORD

En l’absence des symboles, vous n’obtenez rien à l’exception d’un message d’erreur qui vous dit que les symboles n’ont pas pu être trouvés. A titre d’information, sous Linux, un format équivalent est le format DWARF

Il y a seulement quelques années, Microsoft a eu la bonne idée de fournir une API pour manipuler ces fichiers .PDB : jusque là, le format PDB n’était pas documenté et il fallait faire de la rétro-ingéniérie pour en extraire les informations voulues, rendant la tâche extrêmement longue et complexe. Nous allons donc utiliser l’API de Microsoft pour nous balader dans ce format de fichier afin d’en extraire les informations qui nous intéressent : les symboles !

Le but de ce post n’est pas de vous expliquer comment utiliser cette API, donc je laisse le soin aux lecteurs intéressés d’aller explorer la MSDN et/ou les sources de mon code, jointes à cet article. Vous verrez, ce n’est pas bien dur et c’est plutôt bien documenté !

III) La solution sur papier

L’idée principale est de ne plus avoir à éditer le code pour ajouter une version d’un OS ou un champ d’une structure et donc bannir la forêt de if/elseif/else présentée dans la première partie. Idéalement, nous voudrions avoir le code suivant qui marcherait correctement, peu importe la version de l’OS !

PHANDLE_TABLE			pCurTable = NULL;
pCurTable = *(PHANDLE_TABLE *)((UINT_PTR)IoGetCurrentProcess() + OBJECT_TABLE_OFFSET);

Et bien c’est possible. Un exemple valant mieux qu’un long discours, je vais vous montrer l’idée générale appliquée à cet exemple.

typedef struct _FIELD_DEFINITION {
	const char *		Name;
	DWORD			Offset;
}FIELD_DEFINITION, *PFIELD_DEFINITION;

typedef struct _SYMBOLE_DEFINITION {
	const char *		Name;
	PFIELD_DEFINITION	Fields;
}SYMBOLE_DEFINITION, *PSYMBOLE_DEFINITION;

Nous avons une structure SYMBOLE_DEFINITION pour chaque symbole dont nous allons avoir besoin dans notre programme. La structure SYMBOLE_DEFINITION contient le nom du symbole auquel il se rapporte, par exemple ici dans notre exemple _EPROCESS et un tableau de structures de type FIELD_DEFINITION. Il y a autant de structures de type FIELD_DEFINITION qu’il y a de champs dont nous voulons récupérer les offsets concernant le symbole _EPROCESS.

Dans notre exemple, nous voulons récupérer les champs ‘ObjectTable’ et ‘VadRoot’ du symbole _EPROCESS pour un Windows 7 SP0 32 bits. Nous aurons donc le tableau SYMBOLE_DEFINITION suivant pour notre symbole _EPROCESS :

SYMBOLE_DEFINITION	Win7_32bits_RTM_Internals_glTab[] = {
	{"_EPROCESS",	{
				{"ObjectTable", 0xf4},
				{"VadRoot", 0x278}
			}
	}
};

Pour Windows 7 SP0 64 bits :

SYMBOLE_DEFINITION	Win7_64bits_RTM_Internals_glTab[] = {
	{"_EPROCESS",	{							
				{"ObjectTable", 0x200},
				{"VadRoot", 0x440}
			}
	}
};

Et donc un autre tableau similaire pour Windows XP SP0 32 bits, vous l’aurez compris ! Et si je veux aussi avoir des champs d’un autre symbole pour mon Win7 32bits et mon Win7 64bits, je fais comment ? Et bien c’est tout bête, il suffit de rajouter une entrée pour le symbole que vous voulez ajouter, mettons _FILE_OBJECT avec les champs ‘DeviceObject’ et ‘FileName’ :

SYMBOLE_DEFINITION	Win7_32bits_RTM_Internals_glTab[] = {
	{"_EPROCESS",	{
				{"ObjectTable", 0xf4},
				{"VadRoot", 0x278}
			}
	},
	{"_FILE_OBJECT",{
				{"DeviceObject", 0x4},
				{"FileName", 0x30}
			}
	}
};

Pour Windows 7 SP0 64 bits :

SYMBOLE_DEFINITION	Win7_64bits_RTM_Internals_glTab[] = {
	{"_EPROCESS",	{
				{"ObjectTable", 0x200},
				{"VadRoot", 0x440}
			}
	},
	{"_FILE_OBJECT",{
				{"DeviceObject", 0x8},
				{"FileName", 0x58}
			}
	}
};

Et voilà ! Donc si nous résumons où nous en sommes jusqu’à présent, on a un tableau de structures SYMBOLE_DEFINITION par version de Windows que nous voulons supporter. Le code ci-dessous donne la deuxième partie de la solution :

SYMBOLE_DEFINITION *MySymboleTab = NULL;

MySymboleTab = GetInternalsGltabForMyRunningOs();
if (!MySymboleTab) {
	fprintf(stderr, "This OS version is not currently supported!\n");
} else {
	printf("ObjectTable offset on my OS = 0x%x\n",
		MySymboleTab[_EPROCESS].Fields[_eprocess__ObjectTable].Offset);
}

La fonction GetInternalsGltabForMyRunningOs() récupère le tableau de structures SYMBOLE_DEFINITION qui correspond à la version courante de l’OS. Bingo ! Quelques déclarations supplémentaires pour que vous puissiez comprendre le bout de code ci-dessus :

enum _SYMBOLE_INDEX {
	_EPROCESS = 0,
	_FILE_OBJECT
};

enum _FIELD_EPROCESS_INDEX {
	_eprocess__ObjectTable = 0,
	_eprocess__VadRoot
};

enum _FIELD_FILE_OBJECT_INDEX {
	_file_object__DeviceObject = 0,
	_file_object__FileName
};

IV) L’automatisation du processus et la génération de code

Faisons le point de ce que nous avons accompli jusqu’à présent : on a trouvé une façon plutôt élégante de ne plus utiliser une forêt de if/elseif/else et tout le code chargé de récupérer les offset des champs des structures qui nous intéressent est centralisé, ce qui augmente la clarté du code et réduit les risques d’erreurs, tout en facilitant la maintenance du code.

Seulement, nous n’avons toujours pas résolu le point ci-dessous (rappel) :

3- Je dois installer chaque version de chaque OS que je veux supporter et récupérer l’offset dans un WINDBG… Je vous préviens vous n’êtes pas sorti de l’auberge et vous en aurez pour un long moment. De plus, comme c’est un travail répétitif, vous risquez de faire des erreurs et donc d’avoir tout à reprendre plusieurs fois. Bref de quoi devenir fou.

Et puis… Vous avez vraiment envie de constituer vous-même tous ces tableaux de structures ? Bien sûr que non, ça prend un temps fou et tout serait à refaire dans le cas où les définitions des symboles changeraient où que vous voudriez en ajouter d’autres. On va donc automatiser tout cela grâce à un petit programme. Le programme va générer automatiquement pour nous tous le code permettant de créer les tableaux de type SYMBOLE_DEFINITION de même que toutes les énumérations.

Nous on veut juste faire ça :

SYMBOLE_DEFINITION *MySymboleTab = NULL;

MySymboleTab = GetInternalsGltabForMyRunningOs();
if (!MySymboleTab) {
	fprintf(stderr, "This OS version is not currently supported!\n");
} else {
	printf("ObjectTable offset on my OS = 0x%x\n",
		MySymboleTab[_EPROCESS].Fields[_eprocess__ObjectTable].Offset);
}

De plus, le programme doit pouvoir être assez flexible et donc paramétrable, c’est pourquoi il prendra en paramètre un fichier au format TXT qui sera structuré ainsi :

<module_name1>
NomDuSymbole1{Champ1,Champ2,…,ChampN}
NomDuSymbole2{Champ1,Champ2,…,ChampN}
.
.
.
NomDuSymboleN{Champ1,Champ2,…,ChampN}
<module_name2>
NomDuSymbole1{Champ1,Champ2,…,ChampN}
NomDuSymbole2{Champ1,Champ2,…,ChampN}
.
.
.
NomDuSymboleN{Champ1,Champ2,…,ChampN}
<module_nameN>
NomDuSymbole1{Champ1,Champ2,…,ChampN}
NomDuSymbole2{Champ1,Champ2,…,ChampN}
.
.
.
NomDuSymboleN{Champ1,Champ2,…,ChampN}

Fichier TXT - Exemple

Une remarque importante concernant le fonctionnement du programme que j’ai réalisé. Avant de le lancer, il faut, dans le répertoire où vous comptez le lancer, créer un sous-dossier par version de Windows supportée, avec des noms précis et définis que vous trouverez dans la capture d’écran ci-dessous. Et chaque dossier contiendra les fichiers .PDB de la version en question.

Organisation des dossiers

Vous trouverez joint à cet article deux archives :

  1. Une archive nommée Example.rar qui contient un exemple de fichier .TXT et les fichiers .h/.c générés. Le code de la fonction ‘GetInternalsGltabForMyRunningOs()’ est également fourni.
  2. Une archive nommée Generator_srcs.rar qui contient le code source du générateur. Le code est loin d’être parfait, j’ai codé ça rapidement, il faudrait reprendre quelques parties 😉 mais ça marche bien. Je n’ai pas fourni les fichiers de projet de Visual, ni de CMake, mais si vous voulez compiler le programme qui génère les .h/.c vous ne devriez pas avoir de problèmes. Si vraiment vous y tenez je peux pusher une version du code clé en main sur un compte GitHub.

Si vous avez des remarques, des questions, des erreurs à me signaler ou encore des suggestions, bref, n’hésitez pas à vous inscrire sur le forum et à le faire dans ce sujet. Vous pouvez également me contacter par mail à l’adresse suivante : egwene@security-x.fr

Egwene.