ファイルのバージョンは VS_FIXEDFILEINFO
にあります 構造体ですが、実行可能データ内で見つける必要があります。やりたいことを行うには 2 つの方法があります:
VS_FIXEDFILEINFO
を読み取ります 直接構造化します。.rsrc
を見つける セクション、リソース ツリーを解析し、RT_VERSION
を見つけます リソース、それを解析して VS_FIXEDFILEINFO
を抽出します データ。最初のものは簡単ですが、間違った場所で偶然に署名を見つける可能性があります。さらに、あなたが求めるその他のデータ (製品名、説明など) はこの構造にはありません。そのため、データを取得する方法を苦労して説明しようと思います.
PE 形式は少し複雑なので、コードを 1 つずつ貼り付け、コメントを付け、最小限のエラー チェックを行います。データを標準出力にダンプする単純な関数を作成します。適切な関数としてそれを書くことは、読者への演習として残されています:)
構造体フィールドのアラインメントまたはパディングに関連する移植性の問題を回避するために、構造体を直接マッピングする代わりに、バッファー内のオフセットを使用することに注意してください。とにかく、使用される構造体の型に注釈を付けました (詳細については、インクルード ファイル winnt.h を参照してください)。
最初にいくつかの有用な宣言がありますが、それらは一目瞭然です:
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)
次に、実行可能イメージでバージョン リソースを見つける関数 (サイズ チェックなし)。
const char *FindVersion(const char *buf)
{
EXE の最初の構造は MZ ヘッダーです (MS-DOS との互換性のため)。
//buf is a IMAGE_DOS_HEADER
if (READ_WORD(buf) != 0x5A4D) //MZ signature
return NULL;
MZ ヘッダーで重要な唯一のフィールドは、PE ヘッダーのオフセットです。 PE ヘッダーは本物です。
//pe is a IMAGE_NT_HEADERS32
const char *pe = buf + READ_DWORD(buf + 0x3C);
if (READ_WORD(pe) != 0x4550) //PE signature
return NULL;
実際には、PE ヘッダーは非常につまらないものです。すべてのシンボリック データを含む COFF ヘッダーが必要です。
//coff is a IMAGE_FILE_HEADER
const char *coff = pe + 4;
このフィールドから次のフィールドが必要です。
WORD numSections = READ_WORD(coff + 2);
WORD optHeaderSize = READ_WORD(coff + 16);
if (numSections == 0 || optHeaderSize == 0)
return NULL;
オプションのヘッダーは、EXE では実際には必須であり、COFF の直後にあります。魔法は、32 ビットと 64 ビットの Windows では異なります。ここからは 32 ビットを想定しています。
//optHeader is a IMAGE_OPTIONAL_HEADER32
const char *optHeader = coff + 20;
if (READ_WORD(optHeader) != 0x10b) //Optional header magic (32 bits)
return NULL;
ここで興味深いのは、リソース セクションを見つけたいということです。これには 2 つの部分があります:1. セクション データ、2. セクション メタデータ。
データの場所は、オプションのヘッダーの末尾にあるテーブルにあり、各セクションにはこのテーブルのよく知られたインデックスがあります。リソース セクションはインデックス 2 にあるため、次の方法でリソース セクションの仮想アドレス (VA) を取得します。
//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;
セクション メタデータを取得するには、セクション テーブルを反復して .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;
section 構造体には 2 つの関連するメンバーがあります:セクションの VA とファイルへのセクションのオフセット (セクションのサイズもチェックしていません!):
DWORD vaSec = READ_DWORD(sec + 12);
const char *raw = buf + READ_DWORD(sec + 20);
vaRes
に対応するファイル内のオフセット 以前取得した VA は簡単です。
const char *resSec = raw + (vaRes - vaSec);
これは、リソース データへのポインタです。個々のリソースはすべて、1) リソースのタイプ、2) リソースの識別子、3) リソースの言語の 3 つのレベルを持つツリー形式で設定されます。バージョンについては、正しいタイプの最初のものを取得します。
最初に、(リソースのタイプの) リソース ディレクトリがあり、名前付きと名前なしの両方のディレクトリ内のエントリの数を取得し、反復します:
WORD numNamed = READ_WORD(resSec + 12);
WORD numId = READ_WORD(resSec + 14);
int j;
for (j = 0; j < numNamed + numId; ++j)
{
リソース エントリごとにリソースのタイプを取得し、それが 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;
RT_VERSION の場合、ツリー内の次のリソース ディレクトリに移動します。
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);
次のディレクトリ レベルに進みます。ID は気にしません。これの:
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;
3 番目のレベルには、リソースの言語があります。どちらも気にしないので、最初のものを取得してください:
//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;
そして、実際のリソースに到達します。実際には、実際のリソースの場所とサイズを含む構造体ですが、サイズは気にしません。
DWORD verVa = READ_DWORD(verDir);
これはバージョン リソースの VA であり、簡単にポインターに変換されます。
const char *verPtr = raw + (verVa - vaSec);
return verPtr;
そして完了!見つからない場合は NULL
を返します .
}
return NULL;
}
return NULL;
}
バージョン リソースが見つかったので、それを解析する必要があります。実際には、「名前」/「値」のペアのツリー (その他) です。いくつかの値はよく知られており、それが探しているものです。いくつかのテストを行うだけで、どの値かがわかります。
注意 :すべての文字列は UNICODE (UTF-16) で保存されますが、私のサンプル コードでは ASCII への愚かな変換が行われます。また、オーバーフローのチェックもありません。
この関数は、バージョン リソースへのポインタとこのメモリへのオフセット (最初は 0) を受け取り、分析されたバイト数を返します。
int PrintVersion(const char *version, int offs)
{
まず、オフセットは 4 の倍数でなければなりません。
offs = PAD(offs);
次に、バージョン ツリー ノードのプロパティを取得します。
WORD len = READ_WORD(version + offs);
offs += 2;
WORD valLen = READ_WORD(version + offs);
offs += 2;
WORD type = READ_WORD(version + offs);
offs += 2;
ノードの名前は Unicode ゼロ終了文字列です。
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;
}
必要に応じて追加のパディング:
offs = PAD(offs);
type
の場合 が 0 でない場合は、文字列バージョンのデータです。
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);
}
または、名前が VS_VERSION_INFO
の場合 それなら VS_FIXEDFILEINFO
です 構造体。それ以外はバイナリ データです。
else
{
if (strcmp(info, "VS_VERSION_INFO") == 0)
{
ファイルと製品のバージョンを出力しているだけですが、この構造体の他のフィールドは簡単に見つけることができます。 混合エンディアンに注意してください
//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;
}
ここで再帰呼び出しを実行して、完全なツリーを出力します。
while (offs < len)
offs = PrintVersion(version, offs);
戻る前にさらにパディングを行います。
return PAD(offs);
}
最後に、おまけとして main
関数。
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;
}
いくつかのランダムな EXE でテストしましたが、問題なく動作するようです。
私は pev
を知っています は、他の多くの PE ヘッダー情報とともに、この情報を表示できる Ubuntu のツールです。私はそれが C で書かれていることも知っています。ドキュメントの履歴セクションから少し:
pev は、単純な必要性から 2010 年に誕生しました。PE32 ファイルのバージョン (ファイル バージョン) を調べ、Linux で実行できるプログラムです。このバージョン番号は、リソース (.rsrc) セクションに保存されていますが、当時は最適化せずに、wholebinary 内の文字列を単純に検索することにしました。
後で、.rsrcsection に到達するまで PE32 ファイルを解析し、File Version フィールドを取得することにしました。これを行うには、ファイル全体を解析する必要があり、すべてのフィールドと値も出力できるかどうかを考えました...
バージョン 0.40 まで、pev は PE ヘッダーとセクションを解析するための独自のプログラムでした (現在は readpe がこれを担当しています)。バージョン 0.50 では、マルウェア分析に焦点を当て、libpe と呼ばれるライブラリを超えて pev をさまざまなプログラムに分割しました。現在、すべての pev プログラムは libpe を使用しています。