.NET アプリケーションのパッキングとロード

サンプルプログラム(dotNetLoader.zip)のダウンロード

特殊な .NET アプリケーションのロード

 その昔、Wizard Bibleで、バイナリプロテクション という記事を書いたのですが、.NET アプリケーションのロードがうまくいかないという問い合わせを受けたので、 調べてみました。
 .NET アプリケーションをパックおよび実行すると「このアプリケーションを実行するランタイムのバージョン が見つかりません。」というエラーが表示されて正しく起動されません。


 .NET アプリケーションの場合は、ヘッダや MSIL などを PE フォーマットのオプショナルヘッダがもつデータ ディレクトリの15番目のエントリ(IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR)の先に持っています。先のパッカは それらに関する RVA を一切書き換えていないので当然の結果です。で、そこらへんの RVA を全て書き換えてや れば動作するようになるだろう、と思考錯誤を繰り返していたのですが、的外れでした。

 .NET アプリケーションの場合、ネィティブコードは _CorExeMain() を呼ぶだけのスタッブコードです。XP 以降の Windows では、この部分が実行されずに直接 _CorExeMain() が Windows ローダから呼ばれます。 で、XP 以降からは、「Windows ローダが起動に失敗したということは _CorExeMain() が実行されても意味がない」 ということで、_CorExeMain() の呼び出しは必ず失敗するみたいなのです。このことは以下の手順で確認できます。
  • .NET アプリケーションの EXE ファイルをヘックスエディタで開いて、データディレクトリの15番目のエントリの値をつぶします。 .text セクションヘッダのすぐ上に H の文字があるのですぐ分かります。
  • デバッガで修正した EXE を起動してエントリポイントで停止させます。
  • メモリ上のデータディレクトリの15番目のエントリの値を元の値で書き込みして実行再開させます。
  • _CorExeMain() には正しいアセンブリイメージが渡されたはずなのに「このアプリケーションを実行するランタイムのバージョンが見つかりません。」というエラーが表示されます。

.NET アプリケーションのロード方法

 上記のとおり、XP 以降の mscoree::_CorExeMain() はただのお飾りになってしまったようなので、 本来の動きを自分で実装してやらなければいけません。とは言っても、自インスタンスのヘッダを書き換えたり する必要はなく、 「メモリに読み込んだアセンブリイメージがあるので、それを読み込んでください」と .NET ランタイムに 命令するので Smart EXE のローダや _CorExeMain() とは若干異なります。
 今回は暗号化など一切せずに EXE そのままをメモリに読み込んで、それを実行する、という形をとります。 処理のほとんどは main.cpp の Main() 関数に書かれています。わざわざ main() や WinMain() ではなく Main() にしているのは main()/WinMain() の切り替えを簡単にするためとデバッグのためです。

main.cpp
 274 : //! 実際のメインルーチン。
 275 : static int Main()
 276 : {
 277 :   ICorRuntimeHost*        host_cp             = NULL;
 278 :   IUnknown*               unknown_domain_cp   = NULL;
 279 :   mscorlib::_AppDomain*   app_domain_cp       = NULL;
 280 :   mscorlib::_Assembly*    assembly_cp         = NULL;
 281 :   SAFEARRAY*              assembly_data_hp    = NULL;
 282 :   SAFEARRAY*              parameters_hp       = NULL;
 283 :   HRESULT                 hr                  = E_FAIL;
 284 : 
 285 :   do
 286 :   {
 287 :     // 共通言語ランタイムの初期化。
 288 :     hr = CorBindToRuntimeEx(
 289 :            NULL,
 290 :            NULL,
 291 :            0,
 292 :            CLSID_CorRuntimeHost,
 293 :            IID_ICorRuntimeHost,
 294 :            (void**)&host_cp
 295 :          );
 296 :     if( FAILED(hr) )
 297 :     {
 298 :       _RPTF1(_CRT_WARN, "* ERROR * : unable to bind to runtime : 0x%08X.\n", hr);
 299 :       break;
 300 :     }
 301 :     
 302 :     // 共通言語ランタイムを開始。
 303 :     hr = host_cp->Start();
 304 :     if( FAILED(hr) )
 305 :     {
 306 :       _RPTF1(_CRT_WARN, "* ERROR * : unable to start : 0x%08X.\n", hr);
 307 :       break;
 308 :     }
 309 :     
 310 :     // プロセスのデフォルトドメインを取得。
 311 :     hr = host_cp->GetDefaultDomain(&unknown_domain_cp);
 312 :     if( FAILED(hr) )
 313 :     {
 314 :       _RPTF1(_CRT_WARN,
 315 :              "* ERROR * : unable to get default domain : 0x%08X.\n", hr
 316 :             );
 317 :       break;
 318 :     }
 319 :     
 320 :     // QueryInterface() で _AppDomain* を取得。
 321 :     hr = unknown_domain_cp->QueryInterface(
 322 :            __uuidof(mscorlib::_AppDomain),
 323 :            (void**)&app_domain_cp
 324 :          );
 325 :     if( FAILED(hr) )
 326 :     {
 327 :       _RPTF1(_CRT_WARN, "* ERROR * : unable to query interface : 0x%08X.\n", hr);
 328 :       break;
 329 :     }
 330 :     
 331 :     // アセンブリファイルの読込。
 332 :     wchar_t file_path[MAX_PATH] = { 0 };
 333 :     GetAssemblyFilePath(file_path, numberof(file_path));
 334 :     assembly_data_hp = LoadAssemblyFile(file_path);
 335 :     if( assembly_data_hp == NULL )
 336 :     {
 337 :       _RPTF0(_CRT_WARN, "* ERROR * : unable to load assembly.\n");
 338 :       break;
 339 :     }
 340 :     
 341 :     // _AppDomain にロードさせる。
 342 :     assembly_cp = app_domain_cp->Load_3(assembly_data_hp);
 343 :     if( assembly_cp == NULL )
 344 :     {
 345 :       _RPTF0(_CRT_WARN, "* ERROR * : unable to load_3 : 0x%08X.\n");
 346 :       break;
 347 :     }
 348 :     
 349 :     // cElements は引数の数を表す。引数(string[])をとる場合は
 350 :     // パラメータを渡さなければいけない。
 351 :     if( assembly_cp->EntryPoint->GetParameters()->rgsabound[0].cElements )
 352 :       parameters_hp = CreateParameters();
 353 : 
 354 :     // エントリポイントを呼び出す。
 355 :     try
 356 :     {
 357 :       assembly_cp->EntryPoint->Invoke_3(_variant_t(), parameters_hp);
 358 :     }
 359 :     catch(...)
 360 :     {
 361 :       _RPTF0(_CRT_WARN, "* ERROR * : exception occurred.\n");
 362 :     }
 363 :   }
 364 :   while( 0 );
 365 :   
 366 :   // パラメータとして生成した SAFEARRAY を解放。
 367 :   DestroyParameters(parameters_hp);
 368 :   // アセンブリファイル用に生成した SAFEARRAY を解放。
 369 :   UnloadAssemblyFile(assembly_data_hp);
 370 :   // 各インターフェイスの Release()。_Assembly は解放しなくて良いみたい。
 371 :   // RELEASE(assembly_cp);
 372 :   RELEASE(unknown_domain_cp);
 373 :   RELEASE(app_domain_cp);
 374 :   if( host_cp )
 375 :   {
 376 :     host_cp->Stop();
 377 :     RELEASE(host_cp);
 378 :   }
 379 : 
 380 :   return 0;
 381 : }

 COM の VARIANT や SAFEARRAY のせいで煩雑に見えますが、ほとんどは定型処理です。重要なポイントは 332 行目から 342 行目のアセンブリファイルを .NET ランタイムに読み込ませている部分です。
 mscorlib::_Assembly::Load_3() に渡すアセンブリデータは BYTE の SAFEARRAY として渡さなければいけません。 LoadAssemblyFile() 関数がファイルから EXE を読み込んで SAFEARRAY に読み込んで返す役割を担っています。 LoadAssemblyFile() の定義は以下のようになっています。EXE の暗号化を行いたい場合は、ファイルを読み込んで 121 行目の SafeArrayUnaccessData() の直前に複合処理を追加すれば良いでしょう。
main.cpp
  53 : //! アセンブリファイルを読み込んで BYTE の SAFEARRAY* として返す。
  54 : static SAFEARRAY* LoadAssemblyFile(const wchar_t* i_file_path)
  55 : {
  56 :   SAFEARRAY*    retval        = NULL;
  57 :   SAFEARRAY*    safe_array_hp = NULL;
  58 :   HANDLE        file_handle   = INVALID_HANDLE_VALUE;
  59 :   LARGE_INTEGER file_size     = { 0 };
  60 :   HRESULT       hr            = E_FAIL;
  61 :   
  62 :   do
  63 :   {
  64 :     // ファイルを開く。
  65 :     file_handle = CreateFile(
  66 :                     i_file_path,
  67 :                     GENERIC_READ,
  68 :                     FILE_SHARE_READ,
  69 :                     NULL,
  70 :                     OPEN_EXISTING,
  71 :                     FILE_ATTRIBUTE_NORMAL,
  72 :                     NULL
  73 :                   );
  74 :     if( file_handle == INVALID_HANDLE_VALUE )
  75 :     {
  76 :       _RPTF2(_CRT_WARN,
  77 :              "* ERROR * : unable to open file[%S] : 0x%08X.\n",
  78 :              i_file_path, GetLastError()
  79 :             );
  80 :       break;
  81 :     }
  82 :     
  83 :     // ファイルサイズを取得する。
  84 :     if( GetFileSizeEx(file_handle, &file_size) == FALSE )
  85 :     {
  86 :       _RPTF1(_CRT_WARN,
  87 :              "* ERROR * : unable to get file size : 0x%08X.\n",
  88 :              GetLastError()
  89 :             );
  90 :       break;
  91 :     }
  92 :     
  93 :     // ファイルサイズをチェック。
  94 :     if( file_size.HighPart || file_size.LowPart > LONG_MAX
  95 :     ||  file_size.LowPart == 0 )
  96 :     {
  97 :       _RPTF0(_CRT_WARN, "* ERROR * : invalid file size.\n");
  98 :       break;
  99 :     }
 100 :     
 101 :     // メモリの確保
 102 :     BYTE* p = NULL;
 103 :     safe_array_hp = SafeArrayCreateVector(VT_UI1, 0, file_size.LowPart);
 104 :     
 105 :     // 配列へのアクセスを開始。
 106 :     hr = SafeArrayAccessData(safe_array_hp, (void**)&p);
 107 :     if( FAILED(hr) )
 108 :     {
 109 :       _RPTF1(_CRT_WARN, "* ERROR * : unable to access data : 0x%08X.\n", hr);
 110 :       break;
 111 :     }
 112 :     
 113 :     // ファイルの読込
 114 :     DWORD read_bytes = 0;
 115 :     if( ReadFile(file_handle, p, file_size.LowPart, &read_bytes, NULL) )
 116 :     { // 読込に成功した場合は retval に safe_array_hp をセット。
 117 :       retval = safe_array_hp;
 118 :     }
 119 :     
 120 :     // 配列へのアクセスを終了。
 121 :     SafeArrayUnaccessData(safe_array_hp);
 122 :     p = NULL;
 123 :   }
 124 :   while( 0 );
 125 :   
 126 :   // 失敗した状態で safe_array_hp が生成されている場合、破棄する。
 127 :   if( retval == NULL && safe_array_hp )
 128 :   {
 129 :     SafeArrayDestroy(safe_array_hp);
 130 :     safe_array_hp = NULL;
 131 :   }
 132 :   
 133 :   // ファイルを閉じる
 134 :   if( file_handle != INVALID_HANDLE_VALUE )
 135 :   {
 136 :     CloseHandle(file_handle);
 137 :     file_handle = INVALID_HANDLE_VALUE;
 138 :   }
 139 :   
 140 :   return retval;
 141 : }

 あと気をつけるところは 351 行目から 357 行目です。.NET アプリケーションの Main() 関数が string[] 引数 を受け取るかどうかで Invoke_3() の 2 番目のパラメータが代わります。string[] 引数を受け取らない場合は NULL、受け取る場合は VARIANT の SAFEARRAY* を渡さなければいけません。正しい値を渡してやらないと Invoke_3() 呼び出し時に例外が発生して起動してくれません。
main.cpp
 349 :     // cElements は引数の数を表す。引数(string[])をとる場合は
 350 :     // パラメータを渡さなければいけない。
 351 :     if( assembly_cp->EntryPoint->GetParameters()->rgsabound[0].cElements )
 352 :       parameters_hp = CreateParameters();
 353 : 
 354 :     // エントリポイントを呼び出す。
 355 :     try
 356 :     {
 357 :       assembly_cp->EntryPoint->Invoke_3(_variant_t(), parameters_hp);

まとめ

 面白くないのが、mscorlib::_Assembly::Load_3() に渡すアセンブリデータが EXE そのまんまという点です。 これだとここにブレークポイントを仕掛けてやれば簡単にアンパックできてしまいます。 「UnhandledExceptionFilter()を使ったアンチデバッギング手法」や「バイナリプロテクション3」と併用することで アンパックしにくくしておきましょう。というか、このローダ自体をパックすれば良いかな。