VB.NETは解析されやすいのか?(その1)

はじめに

C#VBなどの.NET言語はリバースエンジニアリングに弱いと言われています。 ちょっとした用事でソースコードを紛失したライブラリを解析してその事を実感したので今回はその事です。

イントロ

それではまず、VB*1コンパイルの過程をおさらいしましょう。

  1. VBでコードを書く
  2. VBコンパイラVBのコードからCIL(Common Intermediate Language)に変換される
  3. コンパイル結果がファイルに格納される(ここまでがコンパイル時)
  4. プログラムがメモリにロードされる
  5. メソッド単位で初めて呼び出されるタイミングでCPU命令に変換される

という流れになっています。

また、自己反映計算《†リフレクション†》に対応するため3の時点で生成されるファイルにはクラス名はメソッド名などの情報が埋め込まれています。 そのため、メソッド名をそのまま読み取ることが可能であり、読みやすいコードを書いている場合はクラス名やメソッド名から役割や機能を類推する事も出来ます。

このブログは取り合えずやってみようぜの精神なので、とりあえずテキトーなアプリケーションを解析してみましょう。

ここで想定するアプリケーションは以下の条件を満たすものとします。

  • すべてマネージで構成されている
  • 難読化されていない

機能をP/Invokeに委譲してたりみんな大好き異形言語C++/CLIで実装されてたりとネイティブが含まれていると難易度が一気に跳ね上がります。 また、難読化されていると文字列が暗号化されて格納されたり、メソッドのオーバーロードをこれでもかと言わんばかりに突っ込まれたり、識別子を紛らわしい文字に置き換えらえたり、フローを書き換えて逆コンパイラを殺しにかかったりとこちらも鬼畜な感じになるのでやるなら気合を入れてとりかかってくだしあ。

世の中にはC#向け(一部VBにも対応)している逆コンパイラがあるので、弊社の知っている範囲ですが一応紹介します。今回は使いませんが

  • ILSpy
    • この手のツールとしては珍しくVBへの逆コンパイルに対応している。
    • オープンソースとして開発されておりタダで使える。弊社も割とお世話になってる。
  • dotPeek
    • ReSharperで有名なJetBrains社の逆コンパイラC#のみ)。というよりReSharperのツールチェーンの一部。
    • dotPeekだけならタダで使える。
    • Visual Studioっぽい外見でかっこいい。けどやたら重いので起動を待っている間にやる気が消滅することもある。
    • C#プロジェクトへのエクスポート機能があったりシンボルサーバとして動かしたりすることも出来る。
    • 見た目がかっこいい。
  • .NET Refactor
    • RedGate社の逆コンパイラ
    • 有償。
    • 使ったことがないので正直よくわからない。

解析

とりあえずテキトーなアプリケーションとしてSuperUsefulApplication.exeというコンソールアプリケーションを用意しました。

パスワードを入力してください
>password
パスワードが違います

まず、起動するとパスワードを求められます。初めにこれを突破してみましょう。

アセンブルしておきましょう。

>ildasm SuperUsefulApplication.exe /utf8 /out=SuperUsefulApplication.il

すると以下のファイルが生成されるはずです。

SuperUsefulApplication.il
SuperUsefulApplication.res
SuperUsefulApplication.Resources.resources

ここで興味があるのはSuperUsefulApplication.ilだけですので、他のはとりあえず気にしない方向で。

上記の動作例を見るとパスワードを入力させ判定するコードの周辺に「パスワードを入力してください」という文字があるはずです。 .NET Frameworkの内部コードはUTF-16 LEですので、

D1 30 B9 30 EF 30 FC 30 C9 30 92 30 65 51 9B 52 57 30 66 30 4F 30 60 30 55 30 44 30

というバイトアレイがリテラルとして近くにあればいいな~という願望を抱きながら検索します。

    .entrypoint
    .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 ) 
    // コード サイズ       188 (0xbc)
    .maxstack  4
    .locals init (class [System.Core]System.Security.Cryptography.SHA1Cng V_0,
             class SuperUsefulApplication.SuperUsefulClass`1<string> V_1,
             string V_2)
    IL_0000:  newobj     instance void [System.Core]System.Security.Cryptography.SHA1Cng::.ctor()
    IL_0005:  stloc.0
    // パスワードを入力してください
    IL_0006:  ldstr      bytearray (D1 30 B9 30 EF 30 FC 30 C9 30 92 30 65 51 9B 52
                                    57 30 66 30 4F 30 60 30 55 30 44 30 )
    IL_000b:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_0010:  ldstr      ">"
    IL_0015:  call       void [mscorlib]System.Console::Write(string)
    IL_001a:  ldstr      "Z454hURH5p7ckn+/aqZMFTQ7J/o="
    IL_001f:  ldloc.0
    IL_0020:  call       class [mscorlib]System.Text.Encoding [mscorlib]System.Text.Encoding::get_UTF8()
    IL_0025:  call       string [mscorlib]System.Console::ReadLine()
    IL_002a:  callvirt   instance uint8[] [mscorlib]System.Text.Encoding::GetBytes(string)
    IL_002f:  callvirt   instance uint8[] [mscorlib]System.Security.Cryptography.HashAlgorithm::ComputeHash(uint8[])
    IL_0034:  call       string [mscorlib]System.Convert::ToBase64String(uint8[])
    IL_0039:  ldc.i4.0
    IL_003a:  call       int32 [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Operators::CompareString(string,
                                                                                                                      string,
                                                                                                                      bool)
    IL_003f:  brfalse.s  IL_0051

    IL_0041:  ldstr      bytearray (D1 30 B9 30 EF 30 FC 30 C9 30 4C 30 55 90 44 30   // .0.0.0.0.0L0U.D0
                                    7E 30 59 30 )                                     // ~0Y0
    IL_0046:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_004b:  ldc.i4.0
    IL_004c:  call       void [mscorlib]System.Environment::Exit(int32)
    IL_0051:  ldstr      bytearray (88 30 46 30 53 30 5D 30 )                         // .0F0S0]0
    IL_0056:  call       void [mscorlib]System.Console::WriteLine(string)

ありました。今回はツイてます。

ILコードの詳細は皆さんの脳内評価スタックを信じて詳しくは解説しませんが、以下の流れでパスワードを検証していることが分かります。

  1. パスワードを入力させる
  2. パスワードをUTF-8としてバイト列に変換する
  3. 上のバイト列のSHA1ハッシュを計算する
  4. ハッシュをBase64に変換して文字列と比較する

パスワードがプログラム中に直接埋め込まれていた場合、この時点でパスワードが判明します。まぁ、普通はそんなことはあり得ませんが。 SHA1を突破するのはあきらめてこの検証ロジック自体を迂回する事とします。

IL_004cまでが検証ロジックのようですので、プログラムの先頭で直接IL_0051にジャンプすれば何とかなりそうです。

ということでほい。

    .entrypoint
    .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
    // コード サイズ       188 (0xbc)
    .maxstack  4
    .locals init (class [System.Core]System.Security.Cryptography.SHA1Cng V_0,
             class SuperUsefulApplication.SuperUsefulClass`1<string> V_1,
             string V_2)
              br.s       IL_0051 // ここを追加
    IL_0000:  newobj     instance void [System.Core]System.Security.Cryptography.SHA1Cng::.ctor()
    IL_0005:  stloc.0

アセンブルして確認してみましょう。

>ilasm SuperUsefulApplication.il /exe /debug=impl /resource=SuperUsefulApplication.res /output=SuperUsefulApplication2.exe

一応peverifyで生成されたプログラムを確認しておきましょう。

>peverify SuperUsefulApplication2.exe

Microsoft (R) .NET Framework PE Verifier バージョン  4.0.30319.0
Copyright (c) Microsoft Corporation. All rights reserved.

含まれるすべてのクラスおよびメソッドSuperUsefulApplication2.exe 確認しました。

問題なさそうですね。それでは起動してみましょう。

ようこそ
>

期待通りに検証ロジックを迂回できています。

終わりに

今回は割と長くなってしまったので、いったんここで区切っておきます。 気が向いたらこの続きについて書こうと思います。

github.com

余談ですが、サンプルプログラムで使用しているCryptography API: Next GenerationはWindows Vista以降でしか動きません。 XP? なにそれ? おいしいの?

つづく

*1:C#も同じですが、VB向けの記事ですので・・・