Théorie▲
La table des sections est en fait un tableau de structures suivant immédiatement l'entête PE. Le nombre de membres dans
ce tableau est donné par le champ NumberOfSections dans l'entête fichier (TImageFileHeader). La structure en question
est appelée TImageSectionHeader.
En voici la déclaration dans Windows.pas :
_IMAGE_SECTION_HEADER = packed
record
Name: packed
array
[0
..IMAGE_SIZEOF_SHORT_NAME-1
] of
Byte
;
Misc: TISHMisc;
VirtualAddress: DWORD;
SizeOfRawData: DWORD;
PointerToRawData: DWORD;
PointerToRelocations: DWORD;
PointerToLinenumbers: DWORD;
NumberOfRelocations: Word
;
NumberOfLinenumbers: Word
;
Characteristics: DWORD;
end
;
TImageSectionHeader = _IMAGE_SECTION_HEADER;
Encore une fois, tous les champs ne sont pas utiles. Voici la description de ceux qui sont vraiment intéressants :
Champ | Contenu |
---|---|
Name | Ce champ contient le nom de la section. Notez que la taille maximale est de 8 octets (IMAGE_SIZEOF_SHORT_NAME = 8). Le nom n'est qu'une "étiquette", rien de plus. Vous pouvez utiliser n'importe quel nom, voire laisser ce champ vide. Notez également qu'il n'y a aucune mention de caractère terminal nul. Le nom n'est pas une chaîne à zéro terminal, donc ne vous attendez pas à trouver un 0 à sa fin. |
VirtualAddress | La RVA de la section. Le PE Loader vérifie et utilise la valeur de ce champ lorsqu'il place une section en mémoire. Ainsi si la valeur de ce champ est $1000 et que le fichier PE est chargé à l'adresse $400000, la section sera chargée à l'adresse $401000. |
SizeOfRawData | La taille des données de la section, arrondie au prochain multiple de FileAlignment (voir l'entête fichier). Le PE Loader vérifie ce champ pour savoir combien d'octets de la section il va devoir placer en mémoire. |
PointerToRawData | L'offset fichier du début de la section. Le PE Loader utilise la valeur de ce champ pour trouver les données de la section dans le fichier. |
Characteristics | Contient des drapeaux indiquant par exemple si la section contient du code exécutable, des données non initialisées, si elle dispose d'un accès en écriture ou en lecture. |
Maintenant que nous connaissons la structure TImageSectionHeader, voyons comment on pourrait émuler le travail du PE Loader :
- Lire NumberOfSections dans l'entête fichier pour connaître le nombre de sections dans le fichier.
- Utiliser la valeur SizeOfHeaders comme l'offset fichier de la table des sections et y déplacer le pointeur de fichier.
- Parcourir le tableau de structures en examinant chaque membre.
- Pour chaque membre, récupérer la valeur de PointerToRawData et déplacer le pointeur de fichier à cet offset. Ensuite, lire la valeur de SizeOfRawData pour connaître le nombre d'octets à placer en mémoire. Lire la valeur VirtualAddress et y ajouter la valeur ImageBase pour obtenir l'adresse virtuelle où la section est censée débuter. À partir de là, on peut placer la section en mémoire et lui assigner les attributs spécifiés dans Characteristics.
- Parcourir le tableau jusqu'à avoir examiné toutes les sections.
Notez que nous n'avons pas utilisé le champ Name : il n'est pas vraiment nécessaire.
Exemple▲
L'exemple suivant permet d'ouvrir un fichier PE et d'afficher les informations des sections qu'il contient. Le programme
est basé sur l'utilisation d'une classe TSectionTable qui permet de lire toutes les sections d'un fichier exécutable et d'y donner accès
au moyen d'une propriété de type tableau. Nous ne détaillerons pas toute la classe mais donnerons seulement sa déclaration et la méthode
qui nous intéresse le plus : LireSections.
type
TSection = record
Header : TImageSectionHeader;
Donnees: pChar;
end
;
TSectionArray = Array
of
TSection;
TSectionTable = class
private
FichierPE: TFileStream;
fSections: TSectionArray;
procedure
LireSections(const
Fichier: String
);
function
GetSection(Index
: Integer
): TSection;
procedure
SetSection(Index
: Integer
; const
Value: TSection);
function
GetName(Index
: Integer
): String
;
function
GetCount: Integer
;
function
GetCharacteristics(Index
: Integer
): String
;
public
constructor
Create(Fichier: String
);
destructor
Destroy; override
;
function
SectionHasCharacteristic(Index
: Integer
; Charac: Cardinal
): Boolean
;
property
Sections[Index
: Integer
] : TSection read
GetSection write
SetSection; default
;
property
Name [Index
: Integer
] : String
read
GetName;
property
Count : Integer
read
GetCount;
property
StrCharacs[Index
: Integer
]: String
read
GetCharacteristics;
end
;
On remarque en premier lieu la déclaration du type TSection. Il associe simplement aux headers d'une section les données
qui lui correspondent, sous forme d'un pChar pointant vers une zone mémoire qui sera allouée au besoin.
Les propriétés publiques de cette classe sont les suivantes :
- Sections : Le tableau des sections du fichier exécutable ouvert par la classe.
- Name : Le tableau des noms de chacune des sections de l'exécutable.
- Count : Le nombre de sections dans l'exécutable.
- StrCharacs : Un tableau de chaînes de caractères donnant une interprétation lisible du champ characteristics de chaque section.
La méthode LireSections est appelée dans le constructeur, auquel on passe le fichier à ouvrir. Le constructeur se charge
de vérifier que le fichier fourni est bien un fichier PE en utilisant la fonction vue en deuxième partie
puis exécute LireSections :
procedure
TSectionTable.LireSections(const
Fichier: String
);
var
EnteteDOS : TImageDosHeader; //Structure pour l'entête DOS MZ
EntetePE : TImageNtHeaders; //Structure pour l'entête PE
SectionNbr: Integer
;
i : Integer
;
begin
FichierPE := TFileStream.Create(Fichier, fmOpenRead);
Try
//Lecture de l'entête PE
FichierPE.Read
(EnteteDOS, SizeOf(EnteteDOS));
FichierPE.Seek(EnteteDOS._lfanew, soFromBeginning);
FichierPE.Read
(EntetePE, SizeOf(EntetePE));
//On récupère le nombre de sections dans le fichier
SectionNbr := EntetePE.FileHeader.NumberOfSections;
If
SectionNbr <> 0
then
begin
SetLength(fSections, SectionNbr);
//Les sections sont à la suite dans le fichier, on les lit une à une
for
i := 0
to
SectionNbr - 1
do
FichierPE.Read
(fSections[i].Header, SizeOf(fSections[i].Header));
//Lecture et stockage des données de chaque section
for
i := 0
to
SectionNbr - 1
do
begin
FichierPE.Seek(fSections[i].Header.PointerToRawData, soFromBeginning);
GetMem(fSections[i].Donnees, fSections[i].Header.SizeOfRawData);
FichierPE.Read
(fSections[i].Donnees^, fSections[i].Header.SizeOfRawData);
end
;
end
;
Finally
FreeAndNil(FichierPE);
end
;
end
;
Après s'être rendu à la table des sections grâce aux informations contenues dans les différents entêtes, la méthode
ajoute chaque section au tableau fSections (qui sera accessible par la propriété Sections) puis parcourt
le fichier pour retrouver les données de chacune et les enregistrer en mémoire.
Une fois la lecture terminée, le fichier est fermé et toutes les informations sont accessibles sans relecture ultérieure.
Il suffit alors d'accéder aux éléments de Sections pour obtenir toutes les informations voulues.
Téléchargements▲
Vous pouvez télécharger le projet utilisant cette classe ici :
miroir 1
miroir 2 (si le miroir 1 ne fonctionne pas)
Vous aurez également besoin du fichier PE.pas :
miroir 1
miroir 2 (si le miroir 1 ne fonctionne pas)
Placez PE.pas dans un répertoire de votre choix et dézippez l'application dans un sous-répertoire de celui-ci pour pouvoir compiler.
Liens▲
Tutoriel suivant : La table d'importation