La versión del archivo está en el VS_FIXEDFILEINFO
struct, pero debe encontrarlo en los datos ejecutables. Hay dos formas de hacer lo que quieres:
- Busque la firma VERSION_INFO en el archivo y lea el
VS_FIXEDFILEINFO
estructura directamente. - Encuentra el
.rsrc
sección, analice el árbol de recursos, busque elRT_VERSION
recurso, analícelo y extraiga elVS_FIXEDFILEINFO
datos.
El primero es más fácil, pero susceptible de encontrar la firma por casualidad en el lugar equivocado. Además, los demás datos que solicita (nombre del producto, descripción, etc.) no están en esta estructura, por lo que intentaré explicar cómo obtener los datos de la manera difícil.
El formato PE es un poco complicado, así que estoy pegando el código pieza por pieza, con comentarios y con una verificación mínima de errores. Escribiré una función simple que descargue los datos en la salida estándar. Escribirlo como una función propia se deja como ejercicio para el lector :)
Tenga en cuenta que usaré compensaciones en el búfer en lugar de mapear las estructuras directamente para evitar problemas de portabilidad relacionados con la alineación o el relleno de los campos de estructura. De todos modos, he anotado el tipo de estructuras utilizadas (consulte el archivo de inclusión winnt.h para obtener más detalles).
Primero, algunas declaraciones útiles, deberían explicarse por sí mismas:
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)
Luego, una función que encuentra el recurso Versión en la imagen ejecutable (sin controles de tamaño).
const char *FindVersion(const char *buf)
{
La primera estructura en el EXE es el encabezado MZ (para compatibilidad con MS-DOS).
//buf is a IMAGE_DOS_HEADER
if (READ_WORD(buf) != 0x5A4D) //MZ signature
return NULL;
El único campo interesante en el encabezado MZ es el desplazamiento del encabezado PE. El encabezado PE es real.
//pe is a IMAGE_NT_HEADERS32
const char *pe = buf + READ_DWORD(buf + 0x3C);
if (READ_WORD(pe) != 0x4550) //PE signature
return NULL;
En realidad, el encabezado PE es bastante aburrido, queremos el encabezado COFF, que tiene todos los datos simbólicos.
//coff is a IMAGE_FILE_HEADER
const char *coff = pe + 4;
Solo necesitamos los siguientes campos de este.
WORD numSections = READ_WORD(coff + 2);
WORD optHeaderSize = READ_WORD(coff + 16);
if (numSections == 0 || optHeaderSize == 0)
return NULL;
El encabezado opcional es obligatorio en un EXE y está justo después de COFF. La magia es diferente para Windows de 32 y 64 bits. Asumo 32 bits de ahora en adelante.
//optHeader is a IMAGE_OPTIONAL_HEADER32
const char *optHeader = coff + 20;
if (READ_WORD(optHeader) != 0x10b) //Optional header magic (32 bits)
return NULL;
Aquí viene la parte interesante:queremos encontrar la sección de recursos. Tiene dos partes:1. los datos de la sección, 2. los metadatos de la sección.
La ubicación de los datos está en una tabla al final del encabezado opcional, y cada sección tiene un índice bien conocido en esta tabla. La sección de recursos está en el índice 2, por lo que obtenemos la dirección virtual (VA) de la sección de recursos con:
//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;
Para obtener los metadatos de la sección, necesitamos iterar la tabla de la sección buscando una sección llamada .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 estructura de la sección tiene dos miembros relevantes:el VA de la sección y el desplazamiento de la sección en el archivo (también el tamaño de la sección, ¡pero no lo estoy comprobando!):
DWORD vaSec = READ_DWORD(sec + 12);
const char *raw = buf + READ_DWORD(sec + 20);
Ahora el desplazamiento en el archivo que corresponde al vaRes
VA que obtuvimos antes es fácil.
const char *resSec = raw + (vaRes - vaSec);
Este es un puntero a los datos del recurso. Todos los recursos individuales se configuran en forma de árbol, con 3 niveles:1) tipo de recurso, 2) identificador de recurso, 3) idioma de recurso. Para la versión obtendremos la primera del tipo correcto.
Primero, tenemos un directorio de recursos (para el tipo de recurso), obtenemos el número de entradas en el directorio, tanto con nombre como sin nombre, e iteramos:
WORD numNamed = READ_WORD(resSec + 12);
WORD numId = READ_WORD(resSec + 14);
int j;
for (j = 0; j < numNamed + numId; ++j)
{
Para cada entrada de recurso obtenemos el tipo de recurso y lo descartamos si no es 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;
Si es una RT_VERSION, llegamos al siguiente directorio de recursos en el árbol:
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);
Y pase al siguiente nivel de directorio, no nos importa la identificación. de este:
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;
El tercer nivel tiene el idioma del recurso. A nosotros tampoco nos importa, así que toma el primero:
//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;
Y llegamos al recurso real, bueno, en realidad una estructura que contiene la ubicación y el tamaño del recurso real, pero no nos importa el tamaño.
DWORD verVa = READ_DWORD(verDir);
Ese es el VA del recurso de la versión, que se convierte fácilmente en un puntero.
const char *verPtr = raw + (verVa - vaSec);
return verPtr;
¡Y hecho! Si no se encuentra, devuelve NULL
.
}
return NULL;
}
return NULL;
}
Ahora que se encuentra el recurso de la versión, tenemos que analizarlo. En realidad es un árbol (qué más) de pares "nombre"/"valor". Algunos valores son bien conocidos y eso es lo que está buscando, solo haga una prueba y descubrirá cuáles.
NOTA :Todas las cadenas se almacenan en UNICODE (UTF-16), pero mi código de muestra hace la conversión tonta a ASCII. Además, no se comprueba el desbordamiento.
La función lleva el puntero al recurso de la versión y el desplazamiento a esta memoria (0 para empezar) y devuelve el número de bytes analizados.
int PrintVersion(const char *version, int offs)
{
En primer lugar, el desplazamiento debe ser un múltiplo de 4.
offs = PAD(offs);
Luego obtenemos las propiedades del nodo del árbol de versiones.
WORD len = READ_WORD(version + offs);
offs += 2;
WORD valLen = READ_WORD(version + offs);
offs += 2;
WORD type = READ_WORD(version + offs);
offs += 2;
El nombre del nodo es una cadena Unicode terminada en cero.
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;
}
Más relleno, si es necesario:
offs = PAD(offs);
Si type
no es 0, entonces es un dato de versión de cadena.
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);
}
De lo contrario, si el nombre es VS_VERSION_INFO
entonces es un VS_FIXEDFILEINFO
estructura De lo contrario, son datos binarios.
else
{
if (strcmp(info, "VS_VERSION_INFO") == 0)
{
Solo estoy imprimiendo la versión del archivo y el producto, pero puede encontrar fácilmente los otros campos de esta estructura. Cuidado con el endian mixto orden.
//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;
}
Ahora haz la llamada recursiva para imprimir el árbol completo.
while (offs < len)
offs = PrintVersion(version, offs);
Y algo más de relleno antes de volver.
return PAD(offs);
}
Finalmente, como bono, un main
función.
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;
}
Lo probé con algunos EXE aleatorios y parece funcionar bien.
Lo sé pev
es una herramienta en Ubuntu que le permite ver esta información, junto con mucha otra información de encabezado de PE. También sé que está escrito en C. Tal vez quieras echarle un vistazo. Un poco de su sección de historial en los documentos:
pev nació en 2010 de una simple necesidad:un programa para saber la versión (File Version) de un archivo PE32 y que pudiera ejecutarse en Linux. Este número de versión se almacena en la sección de Recursos (.rsrc) pero en ese momento hemos decidió simplemente buscar la cadena en el binario completo, sin ninguna optimización.
Más tarde, decidimos analizar el archivo PE32 hasta llegar a .rsrcsection y obtener el campo Versión del archivo. Para hacer eso, nos dimos cuenta de que debemos analizar todo el archivo y pensamos que si también podíamos imprimir todos los campos y valores...
Hasta la versión 0.40, pev era un programa único para analizar los encabezados y secciones de PE (ahora readpe es responsable de esto). En la versión 0.50, nos enfocamos en el análisis de malware y dividimos pev en varios programas más allá de una biblioteca, llamada libpe. Actualmente todos los programas pev usan libpe.