La version du fichier est dans le VS_FIXEDFILEINFO
struct, mais vous devez le trouver dans les données exécutables. Il y a deux façons de faire ce que vous voulez :
- Recherchez la signature VERSION_INFO dans le fichier et lisez le
VS_FIXEDFILEINFO
structure directement. - Trouvez le
.rsrc
section, analysez l'arborescence des ressources, trouvez leRT_VERSION
ressource, parsez-la et extrayez leVS_FIXEDFILEINFO
données.
La première est plus facile, mais susceptible de retrouver la signature par hasard au mauvais endroit. De plus, les autres données que vous demandez (nom du produit, description, etc.) ne sont pas dans cette structure, je vais donc essayer d'expliquer comment obtenir les données à la dure.
Le format PE est un peu alambiqué, donc je colle le code morceau par morceau, avec des commentaires et avec un minimum de vérification des erreurs. Je vais écrire une fonction simple qui envoie les données sur la sortie standard. L'écrire comme une fonction propre est laissé comme exercice au lecteur :)
Notez que j'utiliserai des décalages dans le tampon au lieu de mapper directement les structures pour éviter les problèmes de portabilité liés à l'alignement ou au remplissage des champs de structure. Quoi qu'il en soit, j'ai annoté le type des structures utilisées (voir le fichier d'inclusion winnt.h pour plus de détails).
D'abord quelques déclarations utiles, elles doivent être explicites :
typedef uint32_t DWORD;
typedef uint16_t WORD;
typedef uint8_t BYTE;
#define READ_BYTE(p) (((unsigned char*)(p))[0])
#define READ_WORD(p) ((((unsigned char*)(p))[0]) | ((((unsigned char*)(p))[1]) << 8))
#define READ_DWORD(p) ((((unsigned char*)(p))[0]) | ((((unsigned char*)(p))[1]) << 8) | \
((((unsigned char*)(p))[2]) << 16) | ((((unsigned char*)(p))[3]) << 24))
#define PAD(x) (((x) + 3) & 0xFFFFFFFC)
Puis une fonction qui trouve la ressource Version dans l'image exécutable (pas de contrôle de taille).
const char *FindVersion(const char *buf)
{
La première structure de l'EXE est l'en-tête MZ (pour la compatibilité avec MS-DOS).
//buf is a IMAGE_DOS_HEADER
if (READ_WORD(buf) != 0x5A4D) //MZ signature
return NULL;
Le seul champ intéressant dans l'en-tête MZ est le décalage de l'en-tête PE. L'en-tête PE est la vraie chose.
//pe is a IMAGE_NT_HEADERS32
const char *pe = buf + READ_DWORD(buf + 0x3C);
if (READ_WORD(pe) != 0x4550) //PE signature
return NULL;
En fait, l'en-tête PE est assez ennuyeux, nous voulons l'en-tête COFF, qui contient toutes les données symboliques.
//coff is a IMAGE_FILE_HEADER
const char *coff = pe + 4;
Nous avons juste besoin des champs suivants de celui-ci.
WORD numSections = READ_WORD(coff + 2);
WORD optHeaderSize = READ_WORD(coff + 16);
if (numSections == 0 || optHeaderSize == 0)
return NULL;
L'en-tête facultatif est en fait obligatoire dans un EXE et se trouve juste après le COFF. La magie est différente pour Windows 32 et 64 bits. Je suppose 32 bits à partir de maintenant.
//optHeader is a IMAGE_OPTIONAL_HEADER32
const char *optHeader = coff + 20;
if (READ_WORD(optHeader) != 0x10b) //Optional header magic (32 bits)
return NULL;
Voici la partie intéressante :nous voulons trouver la section des ressources. Il comporte deux parties :1. les données de la section, 2. les métadonnées de la section.
L'emplacement des données se trouve dans un tableau à la fin de l'en-tête facultatif, et chaque section a un index bien connu dans ce tableau. La section ressource est en index 2, on obtient donc l'adresse virtuelle (VA) de la section ressource avec :
//dataDir is an array of IMAGE_DATA_DIRECTORY
const char *dataDir = optHeader + 96;
DWORD vaRes = READ_DWORD(dataDir + 8*2);
//secTable is an array of IMAGE_SECTION_HEADER
const char *secTable = optHeader + optHeaderSize;
Pour obtenir les métadonnées de la section, nous devons parcourir la table des sections à la recherche d'une section nommée .rsrc
.
int i;
for (i = 0; i < numSections; ++i)
{
//sec is a IMAGE_SECTION_HEADER*
const char *sec = secTable + 40*i;
char secName[9];
memcpy(secName, sec, 8);
secName[8] = 0;
if (strcmp(secName, ".rsrc") != 0)
continue;
La structure de la section a deux membres pertinents :la VA de la section et le décalage de la section dans le fichier (également la taille de la section, mais je ne la vérifie pas !) :
DWORD vaSec = READ_DWORD(sec + 12);
const char *raw = buf + READ_DWORD(sec + 20);
Maintenant le décalage dans le fichier qui correspond au vaRes
VA que nous avons avant est facile.
const char *resSec = raw + (vaRes - vaSec);
Il s'agit d'un pointeur vers les données de ressource. Toutes les ressources individuelles sont mises en place sous forme d'arborescence, à 3 niveaux :1) type de ressource, 2) identifiant de ressource, 3) langue de ressource. Pour la version, nous obtiendrons le tout premier du bon type.
Tout d'abord, nous avons un répertoire de ressources (pour le type de ressource), nous obtenons le nombre d'entrées dans le répertoire, à la fois nommées et non nommées et itérons :
WORD numNamed = READ_WORD(resSec + 12);
WORD numId = READ_WORD(resSec + 14);
int j;
for (j = 0; j < numNamed + numId; ++j)
{
Pour chaque entrée de ressource, nous obtenons le type de ressource et le supprimons s'il ne s'agit pas de la constante RT_VERSION (16).
//resSec is a IMAGE_RESOURCE_DIRECTORY followed by an array
// of IMAGE_RESOURCE_DIRECTORY_ENTRY
const char *res = resSec + 16 + 8 * j;
DWORD name = READ_DWORD(res);
if (name != 16) //RT_VERSION
continue;
S'il s'agit d'une RT_VERSION, nous passons au répertoire de ressources suivant dans l'arborescence :
DWORD offs = READ_DWORD(res + 4);
if ((offs & 0x80000000) == 0) //is a dir resource?
return NULL;
//verDir is another IMAGE_RESOURCE_DIRECTORY and
// IMAGE_RESOURCE_DIRECTORY_ENTRY array
const char *verDir = resSec + (offs & 0x7FFFFFFF);
Et passez au niveau de répertoire suivant, nous ne nous soucions pas de l'identifiant. de celui-ci :
numNamed = READ_WORD(verDir + 12);
numId = READ_WORD(verDir + 14);
if (numNamed == 0 && numId == 0)
return NULL;
res = verDir + 16;
offs = READ_DWORD(res + 4);
if ((offs & 0x80000000) == 0) //is a dir resource?
return NULL;
Le troisième niveau a la langue de la ressource. Nous nous en fichons non plus, alors prenez simplement le premier :
//and yet another IMAGE_RESOURCE_DIRECTORY, etc.
verDir = resSec + (offs & 0x7FFFFFFF);
numNamed = READ_WORD(verDir + 12);
numId = READ_WORD(verDir + 14);
if (numNamed == 0 && numId == 0)
return NULL;
res = verDir + 16;
offs = READ_DWORD(res + 4);
if ((offs & 0x80000000) != 0) //is a dir resource?
return NULL;
verDir = resSec + offs;
Et nous arrivons à la vraie ressource, eh bien, en fait une structure qui contient l'emplacement et la taille de la vraie ressource, mais nous ne nous soucions pas de la taille.
DWORD verVa = READ_DWORD(verDir);
C'est la VA de la ressource de version, qui est facilement convertie en pointeur.
const char *verPtr = raw + (verVa - vaSec);
return verPtr;
Et.. Voila! Si non trouvé, retournez NULL
.
}
return NULL;
}
return NULL;
}
Maintenant que la ressource de version est trouvée, nous devons l'analyser. Il s'agit en fait d'un arbre (what else) de paires "nom" / "valeur". Certaines valeurs sont bien connues et c'est ce que vous recherchez, faites juste un test et vous saurez lesquelles.
REMARQUE :Toutes les chaînes sont stockées en UNICODE (UTF-16) mais mon exemple de code effectue la conversion stupide en ASCII. De plus, aucune vérification de débordement.
La fonction prend le pointeur vers la ressource de version et le décalage dans cette mémoire (0 pour commencer) et renvoie le nombre d'octets analysés.
int PrintVersion(const char *version, int offs)
{
Tout d'abord, le décalage doit être un multiple de 4.
offs = PAD(offs);
Ensuite, nous obtenons les propriétés du nœud de l'arborescence des versions.
WORD len = READ_WORD(version + offs);
offs += 2;
WORD valLen = READ_WORD(version + offs);
offs += 2;
WORD type = READ_WORD(version + offs);
offs += 2;
Le nom du nœud est une chaîne Unicode terminée par zéro.
char info[200];
int i;
for (i=0; i < 200; ++i)
{
WORD c = READ_WORD(version + offs);
offs += 2;
info[i] = c;
if (!c)
break;
}
Plus de rembourrage, si nécessaire :
offs = PAD(offs);
Si type
n'est pas 0, alors il s'agit d'une donnée de version de chaîne.
if (type != 0) //TEXT
{
char value[200];
for (i=0; i < valLen; ++i)
{
WORD c = READ_WORD(version + offs);
offs += 2;
value[i] = c;
}
value[i] = 0;
printf("info <%s>: <%s>\n", info, value);
}
Sinon, si le nom est VS_VERSION_INFO
alors c'est un VS_FIXEDFILEINFO
structure. Sinon, ce sont des données binaires.
else
{
if (strcmp(info, "VS_VERSION_INFO") == 0)
{
J'imprime simplement la version du fichier et du produit, mais vous pouvez facilement trouver les autres champs de cette structure. Méfiez-vous du boutien mixte commande.
//fixed is a VS_FIXEDFILEINFO
const char *fixed = version + offs;
WORD fileA = READ_WORD(fixed + 10);
WORD fileB = READ_WORD(fixed + 8);
WORD fileC = READ_WORD(fixed + 14);
WORD fileD = READ_WORD(fixed + 12);
WORD prodA = READ_WORD(fixed + 18);
WORD prodB = READ_WORD(fixed + 16);
WORD prodC = READ_WORD(fixed + 22);
WORD prodD = READ_WORD(fixed + 20);
printf("\tFile: %d.%d.%d.%d\n", fileA, fileB, fileC, fileD);
printf("\tProd: %d.%d.%d.%d\n", prodA, prodB, prodC, prodD);
}
offs += valLen;
}
Effectuez maintenant l'appel récursif pour imprimer l'arbre complet.
while (offs < len)
offs = PrintVersion(version, offs);
Et un peu plus de rembourrage avant de revenir.
return PAD(offs);
}
Enfin, en prime, un main
fonction.
int main(int argc, char **argv)
{
struct stat st;
if (stat(argv[1], &st) < 0)
{
perror(argv[1]);
return 1;
}
char *buf = malloc(st.st_size);
FILE *f = fopen(argv[1], "r");
if (!f)
{
perror(argv[1]);
return 2;
}
fread(buf, 1, st.st_size, f);
fclose(f);
const char *version = FindVersion(buf);
if (!version)
printf("No version\n");
else
PrintVersion(version, 0);
return 0;
}
Je l'ai testé avec quelques EXE aléatoires et cela semble fonctionner très bien.
Je connais pev
est un outil sur Ubuntu qui vous permet de voir ces informations, ainsi que de nombreuses autres informations d'en-tête PE. Je sais aussi qu'il est écrit en C. Peut-être voudrez-vous y jeter un œil. Un peu de sa section historique dans la documentation :
pev est né en 2010 d'un simple besoin :un programme pour connaître la version (File Version) d'un fichier PE32 et qui pourrait être exécuté sous Linux. Ce numéro de version est stocké dans la section Resources (.rsrc) mais à l'époque nous avons a décidé de rechercher simplement la chaîne dans le binaire entier, sans aucune optimisation.
Plus tard, nous avons décidé d'analyser le fichier PE32 jusqu'à atteindre .rsrcsection et d'obtenir le champ Version du fichier. Pour ce faire, nous avons réalisé que nous devions analyser l'intégralité du fichier et nous avons pensé que si nous pouvions également imprimer tous les champs et valeurs...
Jusqu'à la version 0.40, pev était un programme unique pour analyser les en-têtes et les sections PE (c'est désormais readpe qui en est responsable). Dans la version 0.50, nous nous sommes concentrés sur l'analyse des logiciels malveillants et avons divisé pev en divers programmes au-delà d'une bibliothèque, appelée libpe. Actuellement, tous les programmes pev utilisent libpe.