Verze souboru je v VS_FIXEDFILEINFO
struct, ale musíte jej najít ve spustitelných datech. Existují dva způsoby, jak dělat, co chcete:
- Vyhledejte v souboru podpis VERSION_INFO a přečtěte si
VS_FIXEDFILEINFO
přímo strukturovat. - Najděte
.rsrc
analyzujte strom zdrojů a najděteRT_VERSION
zdroj, analyzujte jej a extrahujteVS_FIXEDFILEINFO
údaje.
První z nich je jednodušší, ale náchylnější k nalezení podpisu náhodou na nesprávném místě. Kromě toho ostatní údaje, které požadujete (název produktu, popis atd.), v této struktuře nejsou, takže se pokusím vysvětlit, jak získat data složitějším způsobem.
Formát PE je trochu spletitý, takže kód vkládám kousek po kousku, s komentáři a s minimální kontrolou chyb. Napíšu jednoduchou funkci, která vypíše data na standardní výstup. Napsat to jako správnou funkci je ponecháno jako cvičení na čtenáři :)
Všimněte si, že budu používat offsety ve vyrovnávací paměti místo přímého mapování struktur, abych se vyhnul problémům s přenositelností souvisejícími se zarovnáním nebo vyplněním polí struct. Každopádně jsem poznamenal typ použitých struktur (podrobnosti najdete v souboru winnt.h).
Nejprve několik užitečných deklarací, které by měly být samozřejmé:
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)
Poté funkce, která najde prostředek verze ve spustitelném obrazu (bez kontroly velikosti).
const char *FindVersion(const char *buf)
{
První strukturou v EXE je hlavička MZ (kvůli kompatibilitě s MS-DOS).
//buf is a IMAGE_DOS_HEADER
if (READ_WORD(buf) != 0x5A4D) //MZ signature
return NULL;
Jediné zajímavé pole v hlavičce MZ je offset hlavičky PE. Záhlaví PE je to pravé.
//pe is a IMAGE_NT_HEADERS32
const char *pe = buf + READ_DWORD(buf + 0x3C);
if (READ_WORD(pe) != 0x4550) //PE signature
return NULL;
Ve skutečnosti je PE hlavička docela nudná, chceme hlavičku COFF, která má všechna symbolická data.
//coff is a IMAGE_FILE_HEADER
const char *coff = pe + 4;
Z tohoto potřebujeme pouze následující pole.
WORD numSections = READ_WORD(coff + 2);
WORD optHeaderSize = READ_WORD(coff + 16);
if (numSections == 0 || optHeaderSize == 0)
return NULL;
Volitelná hlavička je ve skutečnosti v EXE povinná a je hned za COFF. Kouzlo je jiné pro 32 a 64 bitové Windows. Předpokládám 32 bitů odtud.
//optHeader is a IMAGE_OPTIONAL_HEADER32
const char *optHeader = coff + 20;
if (READ_WORD(optHeader) != 0x10b) //Optional header magic (32 bits)
return NULL;
Zde přichází ta zajímavá část:chceme najít sekci zdrojů. Má dvě části:1. data sekce, 2. metadata sekce.
Umístění dat je v tabulce na konci volitelného záhlaví a každá sekce má v této tabulce dobře známý index. Sekce prostředků je v indexu 2, takže virtuální adresu (VA) sekce prostředků získáváme pomocí:
//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;
Abychom získali metadata sekce, musíme iterovat tabulku sekce a hledat sekci s názvem .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;
Struktura sekce má dva relevantní členy:VA sekce a offset sekce do souboru (také velikost sekce, ale nekontroluji to!):
DWORD vaSec = READ_DWORD(sec + 12);
const char *raw = buf + READ_DWORD(sec + 20);
Nyní offset v souboru, který odpovídá vaRes
VA, kterou jsme dostali dříve, je snadné.
const char *resSec = raw + (vaRes - vaSec);
Toto je ukazatel na zdrojová data. Všechny jednotlivé zdroje jsou nastaveny ve formě stromu se 3 úrovněmi:1) typ zdroje, 2) identifikátor zdroje, 3) jazyk zdroje. Pro verzi získáme úplně první správného typu.
Nejprve máme adresář zdrojů (pro typ zdroje), získáme počet položek v adresáři, pojmenovaných i nepojmenovaných a iterujeme:
WORD numNamed = READ_WORD(resSec + 12);
WORD numId = READ_WORD(resSec + 14);
int j;
for (j = 0; j < numNamed + numId; ++j)
{
Pro každý záznam zdroje získáme typ zdroje a zahodíme jej, pokud to není konstanta 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;
Pokud se jedná o RT_VERSION, dostaneme se do dalšího adresáře prostředků ve stromu:
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);
A přejděte na další úroveň adresáře, id nás nezajímá. tohoto:
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;
Třetí úroveň má jazyk zdroje. Nám je to taky jedno, takže si vezměte první:
//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;
A dostáváme se ke skutečnému zdroji, no, vlastně struktuře, která obsahuje umístění a velikost skutečného zdroje, ale na velikosti nám nezáleží.
DWORD verVa = READ_DWORD(verDir);
To je VA zdroje verze, která se snadno převede na ukazatel.
const char *verPtr = raw + (verVa - vaSec);
return verPtr;
A hotovo! Pokud není nalezen, vraťte NULL
.
}
return NULL;
}
return NULL;
}
Nyní, když je zdroj verze nalezen, musíme jej analyzovat. Je to vlastně strom (co jiného) párů „jméno“ / „hodnota“. Některé hodnoty jsou dobře známé a to je to, co hledáte, stačí udělat test a zjistíte jaké.
POZNÁMKA :Všechny řetězce jsou uloženy v UNICODE (UTF-16), ale můj ukázkový kód provádí hloupou konverzi do ASCII. Také žádné kontroly přetečení.
Funkce vezme ukazatel na zdroj verze a offset do této paměti (0 pro začátečníky) a vrátí počet analyzovaných bajtů.
int PrintVersion(const char *version, int offs)
{
Nejprve musí být offset násobkem 4.
offs = PAD(offs);
Poté získáme vlastnosti uzlu stromu verzí.
WORD len = READ_WORD(version + offs);
offs += 2;
WORD valLen = READ_WORD(version + offs);
offs += 2;
WORD type = READ_WORD(version + offs);
offs += 2;
Název uzlu je řetězec Unicode zakončený nulou.
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;
}
Další odsazení, je-li to nutné:
offs = PAD(offs);
Pokud type
není 0, pak se jedná o údaje o verzi řetězce.
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);
}
Jinak, pokud je název VS_VERSION_INFO
pak je to VS_FIXEDFILEINFO
strukturovat. Jinak jde o binární data.
else
{
if (strcmp(info, "VS_VERSION_INFO") == 0)
{
Právě tisknu verzi souboru a produktu, ale ostatní pole této struktury najdete snadno. Dejte si pozor na smíšený endian objednávka.
//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;
}
Nyní proveďte rekurzivní volání pro tisk celého stromu.
while (offs < len)
offs = PrintVersion(version, offs);
A ještě nějaké vycpávky před návratem.
return PAD(offs);
}
Nakonec jako bonus main
funkce.
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;
}
Testoval jsem to s několika náhodnými EXE a zdá se, že to funguje dobře.
Znám pev
je nástroj na Ubuntu, který vám umožňuje zobrazit tyto informace spolu se spoustou dalších informací z hlavičky PE. Také vím, že je to napsané v C. Možná se na to budete chtít podívat. Něco ze sekce historie v dokumentech:
pev se zrodil v roce 2010 z jednoduché potřeby:programu pro zjištění verze (verze souboru) souboru PE32, který lze spustit v Linuxu. Toto číslo verze je uloženo v sekci Zdroje (.rsrc), ale v současné době rozhodl se jednoduše vyhledat řetězec v celém binárním souboru, bez jakékoli optimalizace.
Později jsme se rozhodli analyzovat soubor PE32, dokud nedosáhnete .rsrcsection a získáte pole Verze souboru. Abychom to udělali, musíme provést analýzu celého souboru a napadlo nás, že bychom mohli vytisknout také všechna pole a hodnoty...
Do verze 0.40 byl pev jedinečným programem pro analýzu PE headersand sekcí (nyní je za to zodpovědný readpe). Ve verzi 0.50 jsme se zaměřili na analýzu malwaru a rozdělili pev do různých programů mimo knihovnu, nazvanou libpe. V současné době všechny programy pev používají libpe.