鷲ノ巣

C# とか PowerShell とか。当ブログの記事は(特に公開直後は)頻繁に改定される場合があることをご了承ください。

PE ファイルについて (6.5) - 相対仮想アドレス

年末年始は更新をさぼってて申し訳ありません。
まだまだシリーズは続きます。

前回、相対仮想アドレスについてやりました。
が、重大な抜けがあったので、さらに追記。
なんでこれ書かなくていいと思ったんだ、俺よ。

相対仮想アドレスとは、DLL のベースアドレス(LoadLibrary の戻り値でも可)に対する相対アドレス、差分値です。
ベースアドレスに相対仮想アドレスを足すことで、実際のアドレスが求まります。

既に何度も言っていますが、実行のためにロードした場合と、CreateFileMappingReadFile 等の関数を使って、ファイルとして読み込んだ場合とでは、セクションの配置が異なります。
そのため、ファイルとして読み込んだ場合、(相対)仮想アドレスが指す場所には目的のデータはありません。

面倒なので、以降、「仮想アドレス」と言えば相対仮想アドレスを含むことにしますね。

第 4 回でセクションについてやりました。
セクションの情報を持つ IMAGE_SECTION_HEADER 構造体には VirtualAddress と PointerToRawData というメンバーがあります。
VirtualAddress は仮想アドレス、つまり LoadLibrary でロードされた場合のアドレスです。
PointerToRawData はファイル上のアドレスで、CreateFileMapping 等で読み込んだ場合のアドレスです。
このように、仮想アドレスとは別にファイル上でのアドレスも持っていれば、それを使ってアクセスすればいいのですが、PE ファイル中では仮想アドレスしかない場合がほとんどです。
そのため、ファイルとして読み込んだ場合、仮想アドレスからファイル上のアドレスを計算しなければなりません。

この計算には、先の 2 つのメンバーを使います。
つまり、(VirtualAddress - PointerToRawData) の値が、実行時にこのセクションが移動される距離を示しているわけです。
そのため、仮想アドレスからこの値を引いてやれば、ファイル上でのアドレスが求まります。

まず、目的の仮想アドレスがどのセクションに含まれているかを特定する必要がありますね。
これは、IMAGE_SECTION_HEADER の VirtualAddress と SizeOfRawData を使います。

言葉で長々説明するより、コードを載せてしまいましょう。

LPVOID RvaToVa(LPVOID pvBase, DWORD dwRva)
{
	return static_cast<LPBYTE>(pvBase) + dwRva;
}

LPVOID RvaToVa(LPVOID pvBase, DWORD dwRva, BOOL bLoaded, IMAGE_SECTION_HEADER ** ppSectionHeader = nullptr)
{
	auto va = static_cast<LPBYTE>(RvaToVa(pvBase, dwRva));

	if (!bLoaded)
	{
		auto pNtHeaders = ImageNtHeader(pvBase);
		auto pSectionHeader = IMAGE_FIRST_SECTION(pNtHeaders);

		for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; ++i, ++pSectionHeader)
		{
			auto pSection = static_cast<LPBYTE>(RvaToVa(pvBase, pSectionHeader->VirtualAddress));
			if (pSection <= va && va < (pSection + pSectionHeader->SizeOfRawData))
			{
				auto diff = pSectionHeader->VirtualAddress - pSectionHeader->PointerToRawData;
				va -= diff;

				if (ppSectionHeader != nullptr)
				{
					*ppSectionHeader = pSectionHeader;
				}

				break;
			}
		}
	}

	return va;
}

2 つのオーバーロードがあります。
最初の方は、単純に相対アドレスを足しているだけです。セクションの移動を考慮していません。
2 つ目の方が、セクションの移動を考慮したバージョンです。

前回チラッと言いましたが、ImageRvaToVa という関数があります。
これは、名前からもわかるように、相対仮想アドレス(RVA)から仮想アドレス(VA)を求める関数です。
つまり、ファイルとして読み込んだ場合には使えません。
上記の関数は、これをファイルとして読み込んだ場合にも使えるように拡張したものです。
第 2 引数 bLoaded は、ImageDirectoryToDataEx 関数と同じように、pvBase が指すイメージが実行のためにロードされているなら TRUE、ファイルとして読み込まれているなら FALSE を返します。

bLoaded が TRUE の場合、絶対仮想アドレスは単純にベースアドレスに相対仮想アドレスを足して求められます。
bLoaded が FALSE の場合、つまりファイルとして読み込んでいる場合は、目的とする仮想アドレスがどのセクションに含まれているのかを特定し、そのセクションの移動距離の分だけアドレスを減算しています。

なお、アドレスによっては、どのセクションにも含まれない場所を指すこともあります。
ロードした時の移動はセクションごとに行われますから、セクションに含まれないデータは移動の影響を受けません。
このため、ロードした時でもファイルとして読み込んだ時でも、同じ位置にあるということになります。

次回以降、相対アドレスから実際のアドレスを求める場合にはこの関数を使っていくことにします。