Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
packer-tutorial — Windows向けパッカーの書き方チュートリアル! | Kitploit
ツール/GitHubGitHub/frank2/packer-tutorial
リバースエンジニアリングマルウェア分析バイナリ解析学習と教育
GitHubfrank2/packer-tutorial

packer-tutorial

Windows向けパッカーの書き方チュートリアル!

リポジトリを見る
319322年前Kitploit レビュー済み

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

PACKERS

目次

  1. パッカーとは何か?: パッカーの目的の紹介と、パッカーに必要な開発要素についてのガイド。
  2. 前提条件: このチュートリアルで使用するために必要なツール。
  3. 実際に試す: このチュートリアルで構築する内容のデモ。
  4. CMakeプロジェクトの設計: パッカーのやや複雑なニーズに合わせたビルドシステムを構築するためのミニCMakeチュートリアル。
  5. バイナリをスタブにパックする: パッカー/スタブの組み合わせのうちパッカー部分を書く方法のチュートリアル、およびWindows実行可能ファイル形式の入門。
    1. リソースの管理
    2. PEファイルの解析
    3. PEファイルの操作
  6. ローダーのシミュレーション: 対象の実行可能ファイルを展開してロードするための最小限のスタブ実行可能ファイルを構築する方法のチュートリアル、およびより高度なWindows実行可能ファイル操作の入門。
    1. メモリからPEを読み込む
    2. 実行のためにPEをロードする
    3. APIインポートの解決
    4. アドレスの解決
    5. 実行の移行
  7. さらなる演習: パッカー開発の知識をさらに深めるためのいくつかの演習。

難しそうに思えますか? このREADMEを要約したプレゼンテーション版を試してみてください。YouTube動画も近日公開予定です!

パッカーとは何か?

パッカーは、自身のアドレス空間(あるいは別プロセスのアドレス空間)内で別のプログラムを展開し起動するプログラムです。デバッガや仮想サンドボックスなどの解析環境を攻撃するベクターとして知られることもあります。主に以下のような用途で使われます:

  • 圧縮: パッカーは、特定のバイナリのコードを圧縮するために一般的に使われます。これは数少ない合法的な用途の1つです。圧縮型パッカーの例についてはUPXを参照してください。
  • 難読化: パッカーは、プログラムを難読化したり、リバースエンジニアリングから防御する目的でも使われます。リバースエンジニアリング対策パッカーの例については、Riot Gamesのpackmanパッカーを参照してください。
  • 回避: マルウェアは、アンチウイルスやEDRさえも回避するために、さまざまなパッカーを使用することがよくあります。回避型パッカーの例については、SmokeLoaderパッカーのこの分析を参照してください。

基本的に、パッカーにはほんのいくつかの基本ステップしかありません:

  1. 圧縮段階: 元の実行可能ファイルを圧縮、難読化、またはその両方を行い、新しいバイナリに変換する段階です。
  2. 展開段階: パックされた実行可能ファイルが、ロードのために元の実行可能ファイルを展開または難読化解除する段階です。
  3. ロード段階: パッカーが、ホストプラットフォームの実行可能ローダーと同様のさまざまな手順を模倣する段階です。実行可能バイナリの複雑さのため、どの程度の深さまでエミュレートするかにもよりますが、これが最も複雑なステップです。
  4. 実行段階: ホストのスタブ実行可能ファイルから、新たにロードされた(つまり展開された)コードへ制御が移る段階です。

同様にシンプルに、パッカーはいくつかの構成要素だけで成り立っています:

  • パッカー: この構成要素は、スタブ実行可能ファイル と呼ばれるものを準備し、指定されたバイナリを展開するために必要なデータを提供する役割を担います。これは最終的に圧縮段階です。
  • スタブ: このバイナリは、元のバイナリの展開、ロード、実行を担当するコード部分です。ご覧のとおり、スタブ実行可能ファイルがパッカーの大部分の重労働を行います。

パッカーの構築は厄介な場合があります。なぜなら、スタブをビルドし、何らかの方法でパッカー実行可能ファイルに組み込む必要があるからです。Windowsでは、単純なコンパイルを超えたVisual Studioのビルドシステムを学ぶのは骨の折れる作業になり得ます。幸い、CMakeはVisual Studioをサポートするシンプルでクロスプラットフォームなビルドシステムを提供し、高度にカスタマイズ可能です!

このチュートリアルでは、以下を教えることを目的としています:

  • パッカーの2つの主要部分を、まとまりのあるテスト可能なビルド環境に分割し統合する方法。
  • Windows実行可能ファイルを操作して、追加のロード可能なデータを追加する方法。
  • Windows実行可能ファイル内をナビゲートして、任意のデータを取得・ロードする方法。
  • Windowsローダーの一部をエミュレートして、Windows実行可能ファイルをロード・実行する方法。

すでにC++とCMakeに精通している方は、パッキングのセクションに直接進んでください。そうでない場合は、読み進めてください!

前提条件

  • C++の知識: このチュートリアルではポインタ演算を多用するため、C++を知らなければあまり役に立ちません。
  • Visual Studio: Visual Studioには、フル機能のWindows C++コンパイラが含まれています。このプロジェクトはVisual Studio 2019でテストされていますが、新しいバージョンでも問題ないはずです。
  • CMake: CMakeは、Visual Studioコンパイラ向けにビルドを構成するために使用するビルドシステムです。

実際に試す

まず、このビルドシステムが機能し、適切にパックされた実行可能ファイルを作成することを何らかの方法で確認しましょう。CMakeとVisual Studioをインストールしたら、お好みのターミナルでこのリポジトリのルートディレクトリに移動し、次のコマンドを実行してください:``` $ mkdir build $ cd build $ cmake ../

root@kitploit:~
これにより、packerチュートリアルコードをビルドするために必要なプロジェクトファイルが作成されます。次に実行します:```
$ cmake --build ./ --config Release

This will build the packer project in Release mode. Then, you can run the following test:

これを実行すると、Release モードで packer プロジェクトがビルドされます。次に、次のテストを実行できます:``` $ ctest -C Release ./

root@kitploit:~
すべてがうまくいけば、test\_pack と test_unpack は成功するはずです。パッキングの結果を自分で確認したい場合は、次のように表示されるはずです:```
$ ./packed.exe
I'm just a little guy!

これを実現したすべての構成要素について説明しましょう。

CMakeプロジェクトの設計

パッカーの主要コンポーネントが何であるかはわかりましたが、開発サイクルの一環としてパッカーをすぐにテストするにはどうすればよいでしょうか? バイナリのパックおよびアンパック処理を適切にテストするには、プロジェクト全体に3つ目のバイナリを追加する必要があります。つまり、全体として次の3つのプロジェクトが必要です:

  • パッカー
  • スタブ
  • パックするためのダミー実行ファイル

また、バイナリがスタブ実行ファイルに圧縮されるようにするための圧縮ライブラリも必要です。zlib がこれに最適です。

最初の段階でのプロジェクト階層は、次のようになるはずです:``` packer/ +---+ CMakeLists.txt + dummy/ | | | +---+ CMakeLists.txt | + src/ | | | +---+ main.cpp | + stub/ | | | +---+ CMakeLists.txt | + src/ | | | +---+ main.cpp | + src/ | | | +---+ main.cpp | + zlib-1.2.13/ | +---+ CMakeLists.txt + ...

root@kitploit:~
各フォルダのmain.cppは、今のところ次のようにシンプルにできます:```cpp
#include <iostream>

int main(int argc, char *argv[]) {
    std::cout << "I'm just a little guy!" << std::endl;
    
    return 0;
}

すでに、対処すべき単純な依存関係チェーンがあることを確認しておきましょう。これは、CMake がいくつかの設定を追加することでうまく解決してくれます。

  • packer と stub は zlib に依存する
  • packer は stub に依存する
  • dummy は packer に依存する(packer によってパックされる必要があるため)

では、CMake の設定として、ルートプロジェクト、つまり packer から始めましょう。

CMake を扱うには、通常、最低バージョンの指定が必要です。CMake は長い間存在しており、以前のバージョンの長期的な使用をサポートしているためです。その後、packer プロジェクトを C++ プロジェクトとして宣言できます。```cmake

target a cmake version, you can target a lower version if you like

cmake_minimum_required(VERSION 3.24)

declare our packer as a C++ project (since zlib is a C project and the compilation

detection might get confused)

project(packer CXX)

root@kitploit:~
また、パッカーを「MultiThreaded」(すなわち /MT)として宣言し、「MultiThreadedDLL」(すなわち /MD)の代わりにすることで、ランタイムDLLの依存関係を気にする必要がなくなります。```cmake
# this line will mark our packer as MultiThreaded, instead of MultiThreadedDLL
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")

変数設定文の山かっこ内の文はジェネレータ式と呼ばれ、必要な場所で構成時のデータを解決するのに役立ちます。このファイル全体で頻繁に目にすることになるでしょう。このジェネレータ式は、構成がDebugであることを検出すると文字列Debugを出力し、それ以外の場合は何も出力しません。これにより、Debugコンパイルプロファイルが選択された場合はMultiThreadedDebugのランタイムライブラリが提供され、Releaseなどの非デバッグコンパイルプロファイルが選択された場合はMultiThreadedが提供されます。そこで何が起こっているのかをよく理解するには、条件付きジェネレータ式を参照してください。CMAKE_MSVC_RUNTIME_LIBRARY変数の詳細については、CMakeドキュメントを参照してください。

CMakeでは、Visual StudioがUIでプロジェクトを作成するときに自動的に行うように、ソースコードを階層に整理できます。また、フォルダー内を再帰的に検索して、一致するファイル名を見つけます。設定ファイルでは、ヘッダー(.hpp)、コード(.cpp)、リソーススクリプト(.rc)にグローバル再帰を設定しています。この例では、main.cppだけが必要ですが、これは大規模なプロジェクトにとって知っておくと便利です。```cmake

this will collect header, source and resource files into convenient variables

file(GLOB_RECURSE SRC_FILES ${PROJECT_SOURCE_DIR}/src/.cpp) file(GLOB_RECURSE HDR_FILES ${PROJECT_SOURCE_DIR}/src/.hpp) file(GLOB_RECURSE RC_FILES ${PROJECT_SOURCE_DIR}/src/*.rc)

this will give you source groups in the resulting Visual Studio project

source_group(TREE "${PROJECT_SOURCE_DIR}" PREFIX "Header Files" FILES ${HDR_FILES}) source_group(TREE "${PROJECT_SOURCE_DIR}" PREFIX "Source Files" FILES ${SRC_FILES}) source_group(TREE "${PROJECT_SOURCE_DIR}" PREFIX "Resource Files" FILES ${RC_FILES})

root@kitploit:~
ここで、CMakeはより正確には**メイクシステム**と表現すべきであることを説明しておきます。CMakeは、指定されたコンパイラに基づいて、与えられた環境に適した正しい「メイクシステム」を作成します。Linuxでは、これは検出された(または指定された)コンパイラ用のmakefileになります。Windows上の私たちにとっては、これは私たちのVisual Studioのバージョンと互換性のあるVisual Studioプロジェクトを生成します。つまり、MSVCメイクシステムを作成したら、必要に応じてVisual Studioだけをすべてに使用できるということです! 結局のところ、あなたはCMakeを使ってVisual Studioを構成しているのです。つまり、ここで行っていることは、プロジェクトをGUIで作成するときにVisual Studioが自動的に行う、プロジェクト内のファイルのツリーをVisual Studioで作成することに他なりません。

次に、依存プロジェクトを追加する必要があります。```cmake
# this will add zlib as a build target
add_subdirectory(${PROJECT_SOURCE_DIR}/zlib-1.2.13)

# this will add our stub project
add_subdirectory(${PROJECT_SOURCE_DIR}/stub)

# this will add our test dummy project
add_subdirectory(${PROJECT_SOURCE_DIR}/dummy)

先ほど触れたように、packerプロジェクトはstubプロジェクトに依存しています。packerは何らかの方法でstubを保持し、それを操作して最終的にパックされた実行ファイルを得る必要があります。Windowsリソースファイルを使用して、ビルド構成に関係なく、stub実行ファイルをpackerバイナリに埋め込むことができます!さらに、CMakeを使用してこれらのファイルを生成させることで、CMakeプロジェクト内でビルド済み実行ファイルへの参照が確実になります。今のうちにリソースファイルを生成して、プロジェクトに含められるようにしましょう:```cmake

this will make sure our stub data will be included in the resources of our packer

despite where it may reside in cmake's build system

file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/$/stub.hpp" CONTENT "#pragma once\n#define IDB_STUB 1000\n") file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/$/stub.rc" CONTENT "#include <winresrc.h>\n#include "stub.hpp"\nIDB_STUB STUB "$<TARGET_FILE:stub>"\n")

root@kitploit:~
`CMAKE_CURRENT_BINARY_DIR` は、現在のビルドディレクトリを含む文字列です。Visual Studio は構成に基づいてバイナリをフォルダに出力するため、`$<CONFIG>` ジェネレータステートメントを使用して、ビルドの現在の構成を取得します。また、`$<TARGET_FILE:stub>` ジェネレータステートメントを使用して、stub バイナリがコンパイルされた後の実行可能ファイル名を出力します。これらのファイル(生成された RC ファイルと生成されたヘッダー)をプロジェクトに含めると、stub バイナリをパッカー プロジェクトに正常に統合できます。

次に、先ほど収集したファイルを使用して、パッカー プロジェクトの実行可能ファイルを宣言します。```cmake
# this will create our packer executable
add_executable(packer ${HDR_FILES} ${SRC_FILES})
target_sources(packer PRIVATE ${RC_FILES} "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/stub.rc")

次に、インポートしたzlibライブラリをリンカーにリンクします。```cmake

this will link zlib to our packer

target_link_libraries(packer zlibstatic)

root@kitploit:~
代わりに、本当に zlib の DLL が必要ならば、`zlib` だけをリンクすることもできます。

生成されたファイルがあるため(zlib もビルドステップの一部として生成します)、プロジェクトのインクルードヘッダーに動的ディレクトリを含める必要があります。CMake ではプロジェクトがプロジェクトのルートにあることに依存しているため、スタブバイナリにも zlib のインクルードディレクトリを追加します:```cmake
# zlib, as part of its build step, drops a config header in the build directory.
# we do this too, so make sure to include everything for the build!
target_include_directories(packer PUBLIC
  "${PROJECT_SOURCE_DIR}/src"
  "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>"
  "${PROJECT_SOURCE_DIR}/zlib-1.2.13"
  "${CMAKE_CURRENT_BINARY_DIR}/zlib-1.2.13"
)

# also set the includes for the stub from here.
# we can't set this in the stub CMake file because CMake requires includes to be in the same
# directory as the build target. for this file, our build target is packer, so this sets
# up includes relative to the packer executable.
target_include_directories(stub PUBLIC
  "${PROJECT_SOURCE_DIR}/zlib-1.2.13"
  "${CMAKE_CURRENT_BINARY_DIR}/zlib-1.2.13"
)

最後に、依存関係チェーンを整理するため、CMakeに依存関係の状況を伝えます。スタブをパッカーの依存関係として、パッカーをダミーの依存関係としてマークします。```cmake

this will add our stub as a dependency and our dummy as being dependent on the packer.

add_dependencies(packer stub) add_dependencies(dummy packer)

root@kitploit:~
[スタブ](https://github.com/frank2/packer-tutorial/blob/main/stub/CMakeLists.txt) と [ダミー](https://github.com/frank2/packer-tutorial/blob/main/dummy/CMakeLists.txt) の CMake ファイルは、もうかなり簡単に理解できるはずです!

素晴らしいのは、CMake がテストを管理してくれることです! この時点で、パッカーのテスト生成はスムーズなプロセスです。単にコマンドを発行し、終了コードが 0 ならテストは成功とみなせます。簡単のため、実行可能ファイルの最初の引数としてバイナリを渡してパックすることにしましょう。次のようなものを想定します:```
$ packer.exe dummy.exe

CMakeでそれを行うのはとても簡単です:```cmake

enable testing to verify our packer works

enable_testing() add_test(NAME test_pack COMMAND "$<TARGET_FILE:packer>" "$<TARGET_FILE:dummy>")

root@kitploit:~
最後に、プログラムがアンパックされるかどうかのテストはさらに簡単です。出力を実行するだけです!ローダーをエミュレートしようとする場合、実行するだけでプログラムをクラッシュさせるエラーにどれだけ遭遇するか、驚くことではないでしょう。ここでも簡単のため、パックされたバイナリは "packed.exe" に出力されるものとしましょう。その場合、次のようにするだけです:```cmake
add_test(NAME test_unpack
  COMMAND "packed.exe")

これは、パッカーがバイナリの出力に失敗した場合に、優雅に失敗します。残念ながら、Visual Studio用のCMakeはADDITIONAL_CLEAN_FILES変数を無視するため、生成されたstub.rsとstub.hppファイルを含む、ビルドシステム内のすべての生成ファイルを手動でクリーンアップする必要があります。

おめでとうございます!パッカーのビルドとテストを成功させるために、多くの作業が行われました。さて、野菜を食べ終えたところで、私たちのパッカーは以下のことができるようになりました:

  • すべての依存関係を正しい順序でコンパイルする
  • スタブバイナリをビルドし、それをパッカーバイナリに注入する
  • パッキング/アンパッキングプロセスを自動的にテストする

では、いよいよ本題に移りましょう!

バイナリをスタブにパッキングする

コンパイラをセットアップして、スタブ実行可能ファイルをリソースとしてパッカーバイナリにコンパイルすることには成功しました。しかし、どうやってバイナリをパックされた状態でスタブに取り込むのでしょうか?パッカーのエンドユーザーがコンパイラを使うことを期待できないため、リソースとして追加することはできません。パッカー実行ファイルはスタンドアロンソリューションであることを想定されています。

私がよく使ってきたテクニックの1つ(分析者には明らかかもしれませんが)は、スタブバイナリに新しいセクションを追加し、最終的に実行時にロードするというものです。これは、PE形式のクラッシュコースを兼ねることになります。しかし、まず、リソースからデータを取り出すにはどうすればよいでしょうか?

リソースの管理

実行時にバイナリからリソースデータを取得するには、3つの基本的なステップがあります:

  • リソースを見つける
  • リソースをロードする
  • リソースをロックする(リソースのバイトを取得する)

次の関数は、特定のバイナリからリソースを検索して取得する方法を示しています:```cpp std::vectorstd::uint8_t load_resource(LPCSTR name, LPCSTR type) { auto resource = FindResourceA(nullptr, name, type);

if (resource == nullptr) { std::cerr << "Error: couldn't find resource." << std::endl; ExitProcess(6); }

auto rsrc_size = SizeofResource(GetModuleHandleA(nullptr), resource); auto handle = LoadResource(nullptr, resource);

if (handle == nullptr) { std::cerr << "Error: couldn't load resource." << std::endl; ExitProcess(7); }

auto byte_buffer = reinterpret_cast<std::uint8_t *>(LockResource(handle));

return std::vectorstd::uint8_t(&byte_buffer[0], &byte_buffer[rsrc_size]); }

root@kitploit:~
データを簡単に操作できるベクターに格納したので、スタブイメージを解析して新しいデータを追加できるようになりました。

### PEファイルの解析

Windows実行可能ファイルは、その最も基本的な構成要素として、*ヘッダー* と *セクションデータ* の2つの部分に分かれます。ヘッダーにはローディングプロセスに重要なメタデータが多数含まれており、セクションデータはその名の通りデータであり、実行可能コード(例:`.text` セクション)または任意のデータ(例:`.data` セクション)になります。すべてのWindows実行可能ファイルの先頭は、`IMAGE_DOS_HEADER` 構造体で始まります。```c
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

この構造は一見すると多くの要素があるように見えますが、名前からもわかるように、このヘッダーは過去のバージョンのWindowsおよびMicrosoft DOSの名残です。ここでは、このヘッダーの2つの値、e_magicとe_lfanewだけに注目します。e_magicは、イメージの先頭にあるマジックヘッダー値、つまりファイルの先頭の"MZ"のことです。e_lfanewは、ファイルの先頭からNTヘッダーまでのオフセットで、NTヘッダーは実行可能ファイルに関するより多くのメタデータ情報を含むPEヘッダーです。たとえば、次のようにしてパッカー用の簡単なPEバリデータを構築できます。```cpp void validate_target(const std::vectorstd::uint8_t &target) { auto dos_header = reinterpret_cast<const IMAGE_DOS_HEADER *>(target.data());

// IMAGE_DOS_SIGNATURE is 0x5A4D (for "MZ") if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) { std::cerr << "Error: target image has no valid DOS header." << std::endl; ExitProcess(3); }

auto nt_header = reinterpret_cast<const IMAGE_NT_HEADERS *>(target.data() + dos_header->e_lfanew);

// IMAGE_NT_SIGNATURE is 0x4550 (for "PE") if (nt_header->Signature != IMAGE_NT_SIGNATURE) { std::cerr << "Error: target image has no valid NT header." << std::endl; ExitProcess(4); }

// IMAGE_NT_OPTIONAL_HDR64_MAGIC is 0x020B if (nt_header->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) { std::cerr << "Error: only 64-bit executables are supported for this example!" << std::endl; ExitProcess(5); } }

root@kitploit:~
`IMAGE_NT_HEADERS` は全体としてはかなり大きな構造体なので、ここで完全に文書化することはしませんが、[Microsoft のヘッダーに関するドキュメント](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers64) に必要な情報がすべて記載されています。いずれにせよ、これらのヘッダーから必要なのはほんの一部の構造体メンバーだけです。ここで、スタブデータに追加するために、対象のバイナリを圧縮する必要があります。

zlib の `compress` 関数と `decompress` 関数の使用は非常に簡単です。zlib をもっと凝った使い方をしたいのであれば、圧縮ストリームをチャンク単位で処理できる `deflate`/`inflate` 関数を使うことをお勧めします。[zlib マニュアル](https://www.zlib.net/manual.html) の「Advanced Functions」のセクションを参照してください。ただし、この例では `compress` と `decompress` で十分です。

まず、zlib の `compressBound` 関数を使って、対象のバイナリのサイズからサイズ値を取得します。このサイズ値は、データのサイズが与えられたときに圧縮データストリームを保持するために必要な最大値に相当します。次に、この値を使って圧縮データを格納するベクターを割り当てます。`compress` 関数は最終的に圧縮バッファの実際のサイズを返すので、そのサイズに合わせてベクターをリサイズできます。```cpp
// get the maximum size of a compressed buffer of the target binary's size.
uLong packed_max = compressBound(target.size());
uLong packed_real = packed_max;

// allocate a vector with that size
std::vector<std::uint8_t> packed(packed_max);
   
if (compress(packed.data(), &packed_real, target.data(), target.size()) != Z_OK)
{
   std::cerr << "Error: zlib failed to compress the buffer." << std::endl;
   ExitProcess(8);
}

// resize the buffer to the real compressed size
packed.resize(packed_real);

PEファイルの操作

データアラインメントについて少し説明します。特定のデータストリームは、そのアドレスまたはサイズが特定のアライメント境界で割り切れる場合、アラインされていると見なされます。たとえば、PEファイルでは、ディスク上のデータセクションは通常0x400境界にアラインされ、メモリ内では0x1000境界にアラインされます。特定の値がアラインされているかどうかは、その値に対してアライメントの剰余を取ることで判定できます(すなわち、value % alignment == 0)。PEファイルは他の値に対しても任意にアラインでき、この値はPEローダー全体の中に存在し、重要です。特定の値と特定の境界をアラインするのは比較的単純な操作です。```cpp template T align(T value, T alignment) { auto result = value + ((value % alignment == 0) ? 0 : alignment - (value % alignment)); return result; }

root@kitploit:~
この関数は基本的に、アラインされていない可能性のある値を、指定された境界に正しくアラインするために必要な残りでパディングします。

PEファイルに任意のデータを適切に追加するには、特に*ファイルアライメント*に注意する必要があります——後でメモリ内のPEファイルの適切なアライン値を計算することはできますが、今はデータを追加する際に、ファイルをファイルアライメント境界にアラインする必要があります。次のコードでは、ヘッダーを取得し、次にファイルアライメントとセクションアライメントの境界を取得してから、スタブデータをファイル境界にアラインし、新しくパックしたセクションを追加します。```cpp
// next, load the stub and get some initial information
std::vector<std::uint8_t> stub_data = load_resource(MAKEINTRESOURCE(IDB_STUB), "STUB");
auto dos_header = reinterpret_cast<IMAGE_DOS_HEADER *>(stub_data.data());
auto e_lfanew = dos_header->e_lfanew;

// get the nt header and get the alignment information
auto nt_header = reinterpret_cast<IMAGE_NT_HEADERS64 *>(stub_data.data() + e_lfanew);
auto file_alignment = nt_header->OptionalHeader.FileAlignment;
auto section_alignment = nt_header->OptionalHeader.SectionAlignment;

// align the buffer to the file boundary if it isn't already
if (stub_data.size() % file_alignment != 0)
   stub_data.resize(align<std::size_t>(stub_data.size(), file_alignment));
      
// save the offset to our new section for later for our new PE section
auto raw_offset = static_cast<std::uint32_t>(stub_data.size());

// encode the size of our unpacked data into the stub data
auto unpacked_size = target.size();
stub_data.insert(stub_data.end(),
                 reinterpret_cast<std::uint8_t *>(&unpacked_size),
                 reinterpret_cast<std::uint8_t *>(&unpacked_size)+sizeof(std::size_t));

// add our compressed data.
stub_data.insert(stub_data.end(), packed.begin(), packed.end());

さて、ファイルセクション境界に従ってセクションのデータを追加したかもしれませんが、スタブ実行可能ファイルはPEファイル内のこのセクションをまだ認識していません。PEファイルのセクションテーブルを解析するだけでなく、私たちのセクションを指す新しいエントリを追加する必要があります。これがraw_offset変数の存在理由です。

まず、NumberOfSectionsを更新することで、セクション数を簡単に増やすことができます。通常、最後のセクションテーブルの後ろのデータはゼロで埋められているため、そのゼロデータを私たちの新しいセクションで簡単に上書きできます。```cpp // increment the number of sections in the file header auto section_index = nt_header->FileHeader.NumberOfSections; ++nt_header->FileHeader.NumberOfSections;

root@kitploit:~
次に、セクションテーブル自体へのポインタを取得する必要があります。技術的にはNTヘッダーのオプショナルヘッダーの直後にあるものの、オプショナルヘッダーのサイズは実際にはNTファイルヘッダーの`SizeOfOptionalHeader`値によって決まります。したがって、そこに到達するには、`OptionalHeader`構造体の先頭から`SizeOfOptionalHeader`値が提供するオフセットまでのポインタを計算する必要があります。```cpp
// acquire a pointer to the section table
auto size_of_header = nt_header->FileHeader.SizeOfOptionalHeader;
auto section_table = reinterpret_cast<IMAGE_SECTION_HEADER *>(
   reinterpret_cast<std::uint8_t *>(&nt_header->OptionalHeader)+size_of_header
);

ついに、セクションメタデータの追加を開始する準備ができました。これがPEのセクションヘッダーです:```c typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // IMAGE_SIZEOF_SHORT_NAME is 8 union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

root@kitploit:~
私たちが新しいセクションヘッダーで注目する特定の変数は、`Name`、`VirtualSize`、`VirtualAddress`、`SizeOfRawData`、`PointerToRawData`、`Characteristics` です。この時点で、ファイルアラインメントとメモリアラインメントという2種類のアラインメントを認識する必要がある理由は、与えられたPE実行可能ファイルには、ロードプロセスの結果として、*ディスク上*での見え方と*メモリ内*での見え方という2つの異なるメモリ状態が存在するためです。特定のPEファイルを、ロードされるかどうかに関係なく同じメモリレイアウトを持つように構成することは可能ですが、それは一般的な構成ではありません。

`Name` 変数は、新しいセクションに付けることができる8バイトのラベルです。ここでは `.packed` を選びました。これは7バイトのASCII文字列であり、バッファに完全に収まります。

`VirtualAddress` は、メモリ内の特定のセクションのオフセットを指します。これは「相対仮想アドレス」、または RVA とも呼ばれます。`VirtualSize` は、メモリ内のセクションのサイズを指します。(ちなみに、MSVC はこの値をセクションの非整列サイズ値としてコンパイルするため、ここでもその慣例に従います。)`PointerToRawData` は、ディスク上の特定のセクションのオフセットを指し、`SizeOfRawData` は、ディスク上のセクションのサイズを指します。

`Characteristics` は複雑で、他の指標に加えて、セクションが読み取り可能、書き込み可能、または実行可能であることを示すことができます。[`IMAGE_SECTION_HEADER` のドキュメント](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_section_header) の characteristics セクションを参照してください。今のところ、必要なのはセクションが読み取り可能であり、初期化データを含むものとしてマークされていることだけです。

これらをすべて踏まえると、新しいセクションを作成できます。```cpp
// get a pointer to our new section and the previous section
auto section = &section_table[section_index];
auto prev_section = &section_table[section_index-1];

// calculate the memory offset, memory size and raw aligned size of our packed section
auto virtual_offset = align(prev_section->VirtualAddress + prev_section->Misc.VirtualSize, section_alignment);
auto virtual_size = section_size;
auto raw_size = align<DWORD>(section_size, file_alignment);

// assign the section metadata
std::memcpy(section->Name, ".packed", 8);
section->Misc.VirtualSize = virtual_size;
section->VirtualAddress = virtual_offset;
section->SizeOfRawData = raw_size;
section->PointerToRawData = raw_offset;

// mark our section as initialized, readable data.
section->Characteristics = IMAGE_SCN_MEM_READ | IMAGE_SCN_CNT_INITIALIZED_DATA;

しかし、これで小さな問題が生じる。イメージのサイズが変わったのだ。問題にならないと思うかもしれないが、NTヘッダーのオプショナルヘッダーにはSizeOfImageという変数があり、これはローダーが実行可能ファイルに割り当てる必要のあるスペースの量を決定する。ただし、これは簡単な修正で済む。イメージに必要なサイズは、最後のセクションをセクション整列境界に合わせたサイズだからだ。```cpp // calculate the new size of the image. nt_header->OptionalHeader.SizeOfImage = align(virtual_offset + virtual_size, section_alignment);

root@kitploit:~
これで完了です!圧縮バイナリを新しいセクションとしてスタブに正常に追加できました。スタブは後でこれを展開してロードできます。これで、変更したスタブイメージをディスクに保存するだけです。```cpp
std::ofstream fp("packed.exe", std::ios::binary);
   
if (!fp.is_open()) {
   std::cerr << "Error: couldn't open packed binary for writing." << std::endl;
   ExitProcess(9);
}
   
fp.write(reinterpret_cast<const char *>(stub_data.data()), stub_data.size());
fp.close();

おめでとうございます!これまでに以下のことを達成しました:

  • パッカーのリソースディレクトリからスタブバイナリをコンパイルし、注入し、実行時に取得しました
  • スタブバイナリとターゲットバイナリの両方の実行可能ヘッダーを解析して、重要な情報を取得しました
  • 実行ファイルを変更して、そのセクションデータを拡張し、パックした実行ファイルを含められるようにしました

これでパッカーの作成も半分まで完了しました!次は、おそらくプロセス全体で最も難しい部分、つまりスタブバイナリを作り込む作業に進みます。

ローダーのシミュレーション

アンパックスタブの作成は、内部の細部が複雑になりがちですが、基本的にはほんの数ステップに集約されます:

  • イメージデータの取得: 対象バイナリの表現を取得して解凍し(必要に応じて難読化も解除して)、後続の処理に備えます。
  • イメージのロード: これが断然最も複雑で、ローダーをシミュレートして対象イメージの実行準備を行います。
  • イメージのエントリポイントの呼び出し: 通常「オリジナルエントリポイント」または OEP と呼ばれ、パックされたバイナリのロード段階から実行段階へと移行するポイントです。

このプロセスは非常に単純なので、メインルーチンはほんの数個の関数だけです:```cpp int main(int argc, char *argv[]) { // first, decompress the image from our added section auto image = get_image();

// next, prepare the image to be a virtual image
auto loaded_image = load_image(image);

// resolve the imports from the executable load_imports(loaded_image);

// relocate the executable relocate(loaded_image);

// get the headers from our loaded image auto nt_headers = get_nt_headers(loaded_image);

// acquire and call the entrypoint auto entrypoint = loaded_image + nt_headers->OptionalHeader.AddressOfEntryPoint; reinterpret_cast<void(*)()>(entrypoint)();

return 0; }

root@kitploit:~
### メモリからPEを読み取る

まず、実行中のバイナリからパッカーが作成したセクションのデータを何とかして取得する必要があります。実行中のバイナリのヘッダーを実行時に取得することは可能でしょうか?はい、絶対に可能です!null引数を指定した [`GetModuleHandleA`](https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlea) は、最終的に実行中のPEヘッダーへのポインタを返します!つまり、実行時にはメモリ上に存在するイメージに簡単にアクセスできます。これこそが、バイナリに新しいセクションを追加することが非常に魅力的である理由です。バイナリから目的のセクションを非常に簡単に解析できるからです。

セクションテーブルの解析について学んだことを踏まえると、このコードの部分は理解しやすいはずです。```cpp
// find our packed section
auto base = reinterpret_cast<const std::uint8_t *>(GetModuleHandleA(NULL));
auto nt_header = get_nt_headers(base);
auto section_table = reinterpret_cast<const IMAGE_SECTION_HEADER *>(
   reinterpret_cast<const std::uint8_t *>(&nt_header->OptionalHeader)+nt_header->FileHeader.SizeOfOptionalHeader
);
const IMAGE_SECTION_HEADER *packed_section = nullptr;

for (std::uint16_t i=0; i<nt_header->FileHeader.NumberOfSections; ++i)
{
   if (std::memcmp(section_table[i].Name, ".packed", 8) == 0)
   {
      packed_section = &section_table[i];
      break;
   }
}

if (packed_section == nullptr) {
   std::cerr << "Error: couldn't find packed section in binary." << std::endl;
   ExitProcess(1);
}

次に、バイナリからスタブデータを展開する必要があります。zlibは、展開ルーチンが何らかの方法でアクセスできるように元の展開後ペイロードのサイズをエンコードすることを推奨しています。そのため、パックデータのヘッダーに展開後バイナリのサイズをエンコードしました。そこで、展開後データへのポインタを取得し、展開後データを保持できる新しいバッファを作成して、zlibの展開関数を呼び出します。```cpp // decompress our packed image auto section_start = base + packed_section->VirtualAddress; auto section_end = section_start + packed_section->Misc.VirtualSize; auto unpacked_size = *reinterpret_cast<const std::size_t *>(section_start); auto packed_data = section_start + sizeof(std::size_t); auto packed_size = packed_section->Misc.VirtualSize - sizeof(std::size_t);

auto decompressed = std::vectorstd::uint8_t(unpacked_size); uLong decompressed_size = static_cast(unpacked_size);

if (uncompress(decompressed.data(), &decompressed_size, packed_data, packed_size) != Z_OK) { std::cerr << "Error: couldn't decompress image data." << std::endl; ExitProcess(2); }

return decompressed;

root@kitploit:~
見てのとおり、`get_image` は結局のところ比較的単純な関数でした。追加されたセクションから対象のバイナリを抽出できたので、次にそれをロードする必要があります。

### 実行のためにPEをロードする

Windowsの実行可能ローダーは、内部で多くのさまざまな処理を行い、さまざまな実行可能構成をサポートしています。今日構築する構成は技術的に非常に最小限であるため、サンプルバイナリ以外を試すと、このチュートリアルが生成するパッカーでさまざまなエラーに遭遇する可能性があります。しかし、実行に必要な最低限の基盤を確立するために、最新のWindows実行可能ファイルをロードするには、次のことを行う必要があります。

* 実行可能イメージのメモリ表現を保持するイメージを割り当てる
* ヘッダーを含む実行可能イメージのセクションを、割り当てたイメージにマッピングする
* バイナリが必要とする他のライブラリへのランタイムインポートを解決する
* イメージ内のさまざまなアドレスが本来あるべき場所を指すように、バイナリイメージを再マッピングする

まず、`load_image` 関数から始めましょう。最初に、パックされたバイナリからセクションテーブルを取得する必要があります。これは最終的に、新しく割り当てたイメージにマッピングされます。適切な実行可能バッファの場合、割り当ては非常に簡単です-- セクションヘッダーから `SizeOfImage` の値を取得し、読み取り・書き込み・実行が可能なイメージを作成する [`VirtualAlloc`](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc) で新しいバッファを作成して、マッピング先とします。```cpp
// get the original image section table
auto nt_header = get_nt_headers(image.data());
auto section_table = reinterpret_cast<const IMAGE_SECTION_HEADER *>(
   reinterpret_cast<const std::uint8_t *>(&nt_header->OptionalHeader)+nt_header->FileHeader.SizeOfOptionalHeader
);

// create a new VirtualAlloc'd buffer with read, write and execute privileges
// that will fit our image
auto image_size = nt_header->OptionalHeader.SizeOfImage;
auto base = reinterpret_cast<std::uint8_t *>(VirtualAlloc(nullptr,
                                                          image_size,
                                                          MEM_COMMIT | MEM_RESERVE,
                                                          PAGE_EXECUTE_READWRITE));

if (base == nullptr) {
   std::cerr << "Error: VirtualAlloc failed: Windows error " << GetLastError() << std::endl;
   ExitProcess(3);
}

バッファを割り当てたら、次に行う必要があるのは、ヘッダーとセクションをそこへコピーすることです。これは、先ほどの SectionAlignment 変数に適合している必要があります。幸い、私たちのセクションの準備方法も、他のセクションの準備方法も、すでに SectionAlignment 境界に揃っています。つまり、最終結果は単純なポインタ演算です。ターゲットイメージを PointerToRawData オフセットから、読み込んだイメージの VirtualAddress オフセットへコピーします。

任意で、PEヘッダーをイメージの先頭にコピーすることもできます。元のヘッダーを保持しておくと開発が容易になりますが、それを削除したい場合は、解析に耐性のあるパッカーを構築するための良い一歩となります。ここでは使いやすさのためにヘッダーをコピーしています。```cpp // copy the headers to our new virtually allocated image std::memcpy(base, image.data(), nt_header->OptionalHeader.SizeOfHeaders);

// copy our sections to their given addresses in the virtual image for (std::uint16_t i=0; i<nt_header->FileHeader.NumberOfSections; ++i) if (section_table[i].SizeOfRawData > 0) std::memcpy(base+section_table[i].VirtualAddress, image.data()+section_table[i].PointerToRawData, section_table[i].SizeOfRawData);

return base;

root@kitploit:~
これでローディングプロセスの簡単な部分は終わりです。メモリからバイナリを取得し、アンパックし、そのセクションを実行可能なメモリ領域に再マッピングしました。イメージの準備ができたので、ローディングプロセスの細部に踏み込むことができます。

### APIインポートの解決

オプショナルヘッダー内には、*データディレクトリ*と呼ばれるものがあります。このディレクトリには、イメージがエクスポートするシンボルや、アイコンやビットマップなどのリソースといった、実行可能ファイルに関するさまざまな情報が含まれています。このチュートリアルでは、**インポートディレクトリ**と**リロケーションディレクトリ**という2つのデータディレクトリを解析します。各ディレクトリはハードコードされており、そのインデックスは[オプショナルヘッダーのドキュメント](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header32)(`DataDirectory`の説明までスクロールしてください)にあります。`VirtualAddress`の値が非nullの場合、データディレクトリが存在します。```c
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

提供されたRVAをキャストすることで、データディレクトリへのポインタを取得します。たとえば、このようにしてインポートデータディレクトリからインポートテーブルを最終的に取得します:```cpp // get the import table directory entry auto nt_header = get_nt_headers(image); auto directory_entry = nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];

// if there are no imports, that's fine-- return because there's nothing to do. if (directory_entry.VirtualAddress == 0) { return; }

// get a pointer to the import descriptor array auto import_table = reinterpret_cast<IMAGE_IMPORT_DESCRIPTOR *>(image + directory_entry.VirtualAddress);

root@kitploit:~
APIインポートを解決するために、ローダーはこのディレクトリを解析し、その後、必要なライブラリをロードして、それらのインポートされた関数を取得します。幸いなことに、このディレクトリは比較的解析が容易です。

それはインポート記述子構造から始まります:```c
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;

私たちが最も注目しているのは、紛らわしい名前を持つ2つの変数、OriginalFirstThunkとFirstThunkです。OriginalFirstThunkには、この実行可能ファイルがName RVAで指定されたDLLに関連して必要とするインポートに関する情報が含まれています。さらに紛らわしいことに、FirstThunkにも同様の情報が含まれています。この2つの違いは何でしょうか?FirstThunkには、解決後のインポートが含まれています。この解決は、悪名高いGetProcAddress関数によって実行されます。

私たちのサンクは、対処すべき追加のデータ構造です。```c typedef struct _IMAGE_THUNK_DATA64 { union { ULONGLONG ForwarderString; // PBYTE ULONGLONG Function; // PDWORD ULONGLONG Ordinal; ULONGLONG AddressOfData; // PIMAGE_IMPORT_BY_NAME } u1; } IMAGE_THUNK_DATA64;

root@kitploit:~
このデータ構造はインポートサンクとエクスポートサンクの両方をカバーするため、`ForwarderString` は無視できます。インポートサンクは、`Ordinal` または別の構造体 `IMAGE_IMPORT_BY_NAME` へのRVAのいずれかです。*序数* は、単に指定されたDLLのエクスポートテーブル内のオフセットです。`IMAGE_IMPORT_BY_NAME` 構造体は次のようになります:```c
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    CHAR   Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

これは可変長構造の例です。配列アクセスにおける境界チェックの欠如を利用して、サイズが可変の構造を作成します。この場合、Name はゼロ終端のC文字列であることが期待されます。

バイナリデータには型が含まれないため、オーディナルとインポートは、サンクエントリの最上位ビットによって区別されます。オーディナルは、32ビットと64ビットの両方の実装で、整数の下半分に含まれます。

インポートテーブルはC文字列のように機能します-- 最後のエントリは、潜在的なインポートの終わりを示すためにNULL終端されたOriginalFirstThunkです。サンクデータも同様に機能し、サンク配列内のNULLエントリによって終了します。

すべてをまとめると、インポートテーブルの解析は次のような擬似コードで行います:``` for every import descriptor: load the dll parse the original and first thunk

root@kitploit:~
for every thunk:
    if ordinal bit set:
        import by ordinal
    else:
        import by name
        
    store import in first thunk
root@kitploit:~
興味深いことに、`GetProcAddress` は関数の引数としてC文字列が型指定されているにもかかわらず、Windowsは序数値をC文字列としてキャストするだけで序数によるインポートを期待します。まったく不思議な話です。

これらの説明を踏まえると、このwhileループの意味が理解できるはずです:```cpp
// when we reach an OriginalFirstThunk value that is zero, that marks the end of our array.
// typically all values in the import descriptor are zero, but we do this
// to be shorter about it.
while (import_table->OriginalFirstThunk != 0)
{
   // get a string pointer to the DLL to load.
   auto dll_name = reinterpret_cast<char *>(image + import_table->Name);

   // load the DLL with our import.
   auto dll_import = LoadLibraryA(dll_name);

   if (dll_import == nullptr) {
      std::cerr << "Error: failed to load DLL from import table: " << dll_name << std::endl;
      ExitProcess(4);
   }

   // load the array which contains our import entries
   auto lookup_table = reinterpret_cast<IMAGE_THUNK_DATA64 *>(image + import_table->OriginalFirstThunk);

   // load the array which will contain our resolved imports
   auto address_table = reinterpret_cast<IMAGE_THUNK_DATA64 *>(image + import_table->FirstThunk);

   // an import can be one of two things: an "import by name," or an "import ordinal," which is
   // an index into the export table of a given DLL.
   while (lookup_table->u1.AddressOfData != 0)
   {
      FARPROC function = nullptr;
      auto lookup_address = lookup_table->u1.AddressOfData;

      // if the top-most bit is set, this is a function ordinal.
      // otherwise, it's an import by name.
      if (lookup_address & IMAGE_ORDINAL_FLAG64 != 0)
      {
         // get the function ordinal by masking the lower 32-bits of the lookup address.
         function = GetProcAddress(dll_import,
                                   reinterpret_cast<LPSTR>(lookup_address & 0xFFFFFFFF));

         if (function == nullptr) {
            std::cerr << "Error: failed ordinal lookup for " << dll_name << ": " << (lookup_address & 0xFFFFFFFF) << std::endl;
            ExitProcess(5);
         }
      }
      else {
         // in an import by name, the lookup address is an offset to
         // an IMAGE_IMPORT_BY_NAME structure, which contains our function name
         // to import
         auto import_name = reinterpret_cast<IMAGE_IMPORT_BY_NAME *>(image + lookup_address);
         function = GetProcAddress(dll_import, import_name->Name);

         if (function == nullptr) {
            std::cerr << "Error: failed named lookup: " << dll_name << "!" << import_name->Name << std::endl;
            ExitProcess(6);
         }
      }

      // store either the ordinal function or named function
      // in our address table.
      address_table->u1.Function = reinterpret_cast<std::uint64_t>(function);

      // advance to the next entries in the address table and lookup table
      ++lookup_table;
      ++address_table;
   }

   // advance to the next entry in our import table
   ++import_table;
}

インポートが解決されたので、いよいよ最後の部分である、リロケーション ディレクトリ(再配置ディレクトリ)の処理に移ることができます。

アドレスの解決

次に取り組むべきデータ ディレクトリは、リロケーション ディレクトリと呼ばれるものです。このディレクトリは、コード内の絶対アドレスを新しいベース値に変換する役割を担っています。この処理は、基本的には アドレス空間配置のランダム化(ASLR)と呼ばれるものを実装するものですが、DLL のアドレス空間同士が衝突しないようにする役割も担っています。

まず、対象のバイナリが実際にアドレス ベースを移動できることを確認する必要があります。特に古いバイナリでは、これが有効になっていないことがあります。特定の Windows 実行可能ファイルには、その特性を示す値がオプション ヘッダー内にあり、紛らわしくも DllCharacteristics と呼ばれています。ここで注目するのは "dynamic base" 特性です。非 ASLR バイナリをアンパックする特別な方法もありますが、それについては説明しないため、バイナリがこの特性をサポートしていない場合はエラーとします。(これは、スタブではなくパッカー側でエラーにする方が良いのですが、PE ヘッダーの学習の流れをスムーズにするため、ここに置いています。)```cpp // first, check if we can even relocate the image. if the dynamic base flag isn't set, // then this image probably isn't prepared for relocating. auto nt_header = get_nt_headers(image);

if (nt_header->OptionalHeader.DllCharacteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE == 0) { std::cerr << "Error: image cannot be relocated." << std::endl; ExitProcess(7); }

// once we know we can relocate the image, make sure a relocation directory is present auto directory_entry = nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];

if (directory_entry.VirtualAddress == 0) { std::cerr << "Error: image can be relocated, but contains no relocation directory." << std::endl; ExitProcess(8); }

root@kitploit:~
次に、*アドレスデルタ*を計算する必要があります。これは単に、イメージの`ImageBase`変数と仮想イメージのベースアドレスの差です。バイナリ内のハードコードされたアドレスの値をすばやく調整するために使用されます。```cpp
// calculate the difference between the image base in the compiled image
// and the current virtually allocated image. this will be added to our
// relocations later.
std::uintptr_t delta = reinterpret_cast<std::uintptr_t>(image) - nt_header->OptionalHeader.ImageBase;

さて、リロケーションテーブルに取り組む準備ができました。```c typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; // WORD TypeOffset[1]; } IMAGE_BASE_RELOCATION;

root@kitploit:~
`TypeOffset` 配列がコメントアウトされていることに注目してください。実はここで重要なんです!再配置テーブルは、調整するアドレスを含むオフセットのブロックで構成されており、`VirtualAddress` RVA によって識別されます。`TypeOffset` 配列には、再配置の種類と、調整対象の `VirtualAddress` からのオフセットの両方を含むエンコードされたワード値が格納されています。再配置の種類に関して言えば、64ビットバイナリでは、1つの再配置の種類だけを気にすればよいです。残念ながら、この構造体は実際には可変長構造体ではないため、`TypeOffset` 配列を取得するにはポインタ演算を行う必要があります。

前述のとおり、`TypeOffset` 配列にはエンコードされたワードが含まれています。上位4ビット(マスク `0xF000`)には再配置の種類が含まれており、これはPEフォーマットのドキュメントの["base relocation types" セクション](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format)に記載されています。下位12ビット(マスク `0x0FFF`)には、調整対象の `VirtualAddress` 引数からのオフセットが含まれています。

これを説明するのは手間がかかるうえ、結局は非常に紛らわしくなってしまいます。再配置するアドレスへのポインタを取得する方法は次のようになります。```cpp
auto ptr = reinterpret_cast<std::uintptr_t *>(image + relocation_table->VirtualAddress + offset);

そして、そのアドレスを調整すると、次のようになります:```cpp *ptr += delta;

root@kitploit:~
リロケーションディレクトリの説明は複雑ですが、実際の操作はコードで見ると非常に単純です。

`SizeOfBlock` 変数を使うと、リロケーションデータの次のブロックへの進行が簡単になります。このブロックには、ヘッダーのサイズ *および* `TypeOffset` 配列のサイズが含まれます。もし `TypeOffset` が静的にサイズが決まった配列だった場合、リロケーションヘッダーの `sizeof` 呼び出しを追加するだけで次のリロケーションエントリに進めるでしょう。

これらすべてを説明したので、次のリロケーションコードを理解できるはずです:```cpp
// get the relocation table.
auto relocation_table = reinterpret_cast<IMAGE_BASE_RELOCATION *>(image + directory_entry.VirtualAddress);

// when the virtual address for our relocation header is null,
// we've reached the end of the relocation table.
while (relocation_table->VirtualAddress != 0)
{
   // since the SizeOfBlock value also contains the size of the relocation table header,
   // we can calculate the size of the relocation array by subtracting the size of
   // the header from the SizeOfBlock value and dividing it by its base type: a 16-bit integer.
   std::size_t relocations = (relocation_table->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(std::uint16_t);

   // additionally, the relocation array for this table entry is directly after
   // the relocation header
   auto relocation_data = reinterpret_cast<std::uint16_t *>(&relocation_table[1]);

   for (std::size_t i=0; i<relocations; ++i)
   {
      // a relocation is an encoded 16-bit value:
      //   * the upper 4 bits are its relocation type
      //     (https://learn.microsoft.com/en-us/windows/win32/debug/pe-format see "base relocation types")
      //   * the lower 12 bits contain the offset into the relocation entry's address base into the image
      //
      auto relocation = relocation_data[i];
      std::uint16_t type = relocation >> 12;
      std::uint16_t offset = relocation & 0xFFF;
      auto ptr = reinterpret_cast<std::uintptr_t *>(image + relocation_table->VirtualAddress + offset);

      // there are typically only two types of relocations for a 64-bit binary:
      //   * IMAGE_REL_BASED_DIR64: a 64-bit delta calculation
      //   * IMAGE_REL_BASED_ABSOLUTE: a no-op
      //
      if (type == IMAGE_REL_BASED_DIR64)
         *ptr += delta;
   }

   // the next relocation entry is at SizeOfBlock bytes after the current entry
   relocation_table = reinterpret_cast<IMAGE_BASE_RELOCATION *>(
      reinterpret_cast<std::uint8_t *>(relocation_table) + relocation_table->SizeOfBlock
   );
}

Congratulations! Our image is now ready for execution! We've accomplished a lot so far:

  • we unpacked our binary from our section table at runtime
  • we mapped our binary onto an executable memory region
  • we resolved the imports needed for our binary to execute
  • we relocated the addresses in the image to point to our new image base

Now we're ready to run our unpacked binary!

Transferring execution

Let's go back to our main loop now:```cpp int main(int argc, char *argv[]) { // first, decompress the image from our added section auto image = get_image();

// next, prepare the image to be a virtual image
auto loaded_image = load_image(image);

// resolve the imports from the executable load_imports(loaded_image);

// relocate the executable relocate(loaded_image);

// get the headers from our loaded image auto nt_headers = get_nt_headers(loaded_image);

// acquire and call the entrypoint auto entrypoint = loaded_image + nt_headers->OptionalHeader.AddressOfEntryPoint; reinterpret_cast<void(*)()>(entrypoint)();

return 0; }

root@kitploit:~
実行の移譲は非常に簡単ですが、[関数ポインタ](https://en.wikipedia.org/wiki/Function_pointer)に関する知識が必要です。ここが該当する部分です。```cpp
// acquire and call the entrypoint
auto entrypoint = loaded_image + nt_headers->OptionalHeader.AddressOfEntryPoint;
reinterpret_cast<void(*)()>(entrypoint)();

AddressOfEntryPoint は、ご想像のとおり、ロードされたイメージのコードエントリ内の RVA です。main エントリポイントについてご存じかもしれないこととは別に、特定のバイナリの生のエントリポイントは型付けされておらず、C++ コンパイラが主に、main か WinMain かを問わず、期待される main 関数に引数を渡すためのコード環境を設定する役割を担っています。

基本的に、関数ポインタは次のように宣言されます:```c return_type (*variable_name)(int arg1, int arg2, ...)

root@kitploit:~
したがって、型が関連付けられていないエントリポイントを呼び出すための関数ポインタは、次のようになります。```c
void (*entrypoint)()

キャストとして、これは次のように簡約される:```c void(*)()

root@kitploit:~
すべてをまとめると、エントリポイントを関数ポインタとしてキャストし、同じ行で呼び出すだけで済みます。次のように:```cpp
reinterpret_cast<void(*)()>(entrypoint)();

すべてが正しく読み込まれると、パックされたプログラムが実行されるのを確認できるはずです。今回の場合、ダミーの実行ファイルをパックしたので、単にメッセージを出力するだけです。``` $ ./packed.exe I'm just a little guy!

root@kitploit:~
おめでとうございます!完了です!Windows パッカーを書きました!

## さらなる演習

* **アナリストを攻撃する**: [いくつかのアンチデバッグ技術](https://anti-reversing.com/Downloads/Anti-Reversing/The_Ultimate_Anti-Reversing_Reference.pdf) を実装することを学び、パッカーを強化しましょう。
* **サポートを拡張する**: 再配置不可能なバイナリを実装することを学ぶか、スレッドローカルストレージディレクトリ (`IMAGE_TLS_DIRECTORY`) やリソースディレクトリ (`IMAGE_RESOURCE_DIRECTORY`) などのさらなるディレクトリを実装することを学びましょう。このレベルではドキュメントが不足しているため、[exe-rs](https://github.com/exe-rs) で私の実装を見ることができます。
* **スタブを難読化する**: アナリストがどのようにしてあなたのバイナリをアンパックするかを考え、[ヘッダーの消去](https://github.com/frank2/packer-tutorial/blob/main/stub/src/main.cpp#L84) などによってアンパックを容易にできないようにしましょう。
ツールをダウンロード