VB.NETは解析されやすいのか?(その1)
はじめに
C#やVBなどの.NET言語はリバースエンジニアリングに弱いと言われています。 ちょっとした用事でソースコードを紛失したライブラリを解析してその事を実感したので今回はその事です。
イントロ
それではまず、VB*1のコンパイルの過程をおさらいしましょう。
- VBでコードを書く
- VBコンパイラでVBのコードからCIL(Common Intermediate Language)に変換される
- コンパイル結果がファイルに格納される(ここまでがコンパイル時)
- プログラムがメモリにロードされる
- メソッド単位で初めて呼び出されるタイミングでCPU命令に変換される
という流れになっています。
また、自己反映計算《†リフレクション†》に対応するため3の時点で生成されるファイルにはクラス名はメソッド名などの情報が埋め込まれています。 そのため、メソッド名をそのまま読み取ることが可能であり、読みやすいコードを書いている場合はクラス名やメソッド名から役割や機能を類推する事も出来ます。
このブログは取り合えずやってみようぜの精神なので、とりあえずテキトーなアプリケーションを解析してみましょう。
ここで想定するアプリケーションは以下の条件を満たすものとします。
- すべてマネージで構成されている
- 難読化されていない
機能をP/Invokeに委譲してたりみんな大好き異形言語C++/CLIで実装されてたりとネイティブが含まれていると難易度が一気に跳ね上がります。 また、難読化されていると文字列が暗号化されて格納されたり、メソッドのオーバーロードをこれでもかと言わんばかりに突っ込まれたり、識別子を紛らわしい文字に置き換えらえたり、フローを書き換えて逆コンパイラを殺しにかかったりとこちらも鬼畜な感じになるのでやるなら気合を入れてとりかかってくだしあ。
世の中にはC#向け(一部VBにも対応)している逆コンパイラがあるので、弊社の知っている範囲ですが一応紹介します。今回は使いませんが
- ILSpy
- dotPeek
- .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コードの詳細は皆さんの脳内評価スタックを信じて詳しくは解説しませんが、以下の流れでパスワードを検証していることが分かります。
パスワードがプログラム中に直接埋め込まれていた場合、この時点でパスワードが判明します。まぁ、普通はそんなことはあり得ませんが。 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 確認しました。
問題なさそうですね。それでは起動してみましょう。
ようこそ >
期待通りに検証ロジックを迂回できています。
終わりに
今回は割と長くなってしまったので、いったんここで区切っておきます。 気が向いたらこの続きについて書こうと思います。
余談ですが、サンプルプログラムで使用しているCryptography API: Next GenerationはWindows Vista以降でしか動きません。 XP? なにそれ? おいしいの?
つづく