VB.NETは解析されやすいのか?(その2)
はじめに
前回はildasm
を用いた逆アセンブルとプログラムに出現する文字列から当該処理を行う箇所を特定し、特定の処理の迂回をやってみました。
今回はデバッガを用いて実際の動作を覗いてみましょう。
動的解析
ところで、前回アセンブラを用いてILコードから実行ファイルを生成したときのコマンドをもう一度確認してみましょう。
>ilasm SuperUsefulApplication.il /exe /debug=impl /resource=SuperUsefulApplication.res /output=SuperUsefulApplication2.exe
ここで重要なのが/debug=impl
の部分です。
このオプションでデバッグシンボルを一緒に生成しています。
プログラムを起動し、デバッガをアタッチしてみましょう。 なお、ここではVisual Studio 2015 community update 3を使用しています。
デバッグ->プロセスにアタッチ
で一覧から目的のプロセスを見つけ、アタッチします。
ところで、このアプリケーションの処理内容について説明していませんでしたね。 文字列を入力するとその文字が格納され、なにも入力しないと入力した逆順に文字列が返されるようです。
なにそれただのスタックじゃん。なんだろな~。どうやって実装しているんだろうな~。気になるなぁ~。
前回の逆アセンブルコードから、SuperUsefulApplication.SuperUsefulClass`1<string>::Pop()
とSuperUsefulApplication.SuperUsefulClass`1<string>::Push()
が中核的な処理をつかさどっているようです。
ですので、この二つのメソッドにブレークポイントを設定してみましょう。
デバッグ->ブレークポイントの作成->関数のブレークポイント
からPop
とPush
にブレークポイントを設定します。
文字列を4つ程突っ込んでデバッガで内部状態を確認してみましょう。
ローカル変数を確認すると、線形リストでスタックを実装しているようです。
このアプリケーションは単純なのでここで終わりですが、もっと複雑なアプリケーション/ライブラリの場合でも普段と同じようにデバッグができるでしょう。
おわりに
このように、マネージドアプリケーションはリバースエンジニアリングに対して非常に脆弱であることが分かります。
リバースエンジニアリングに対する防御策としては、
などが挙げられます。
ロジックの重要度と実施するコストとを勘案していい感じにアレすればいいと思います。
おわり
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? なにそれ? おいしいの?
つづく
VB13以前でもNull条件演算子を使いたい
はじめに
ちょっと前にVB14の新機能についての話をしたじゃないですか。 その中にNull条件演算子なんてものがあったと思います。
弊社はEntity FrameworkみたいなライブラリでFirstOrDefault
みたいなクエリを割合ぽいぽい投げ返ってきた値がNothing
かそうでないかで処理を分岐させることが多いのですが、
Dim hoge = db.Hoges.Where(Function(it) it.UniqueKey = "HOGE").FirstOrDefault() Dim hogeName As String If Not hoge Is Nothing Then hogeName = hoge.Name Else hogeName = Nothing End If
みたいなコードをほぼ毎回のように書かなくてはならず非常にげんなりします。 まぁ、設計がクソなのも一理ありますが。
そこでNull条件演算子を使いたいのですが、現在使用している環境では使えないという最高にクソな感じに仕上がっており*1日夜冗長でクソなコードを生成している次第でございます。うおォン 俺はまるでクソコードジェネレータだ
あああああ
かくなる上はメタプログラミングを用いてごり押しで解決したいところなのですが、さすがに構文に介入することはできません。 コンパイル時に介入してASTを弄れれば話は別かもしれませんが。
Roslyn? なにそれ? おいしいの?
というわけで拡張メソッドでそれっぽく仕上げてお茶を濁すほかありません。
というわけでほい。
Module NullableExtension <Extension> Public Function N(Of TModel, TResult As Class)(value As TModel, expr As Func(Of TModel, TResult)) As TResult If value Is Nothing Then Return Nothing Else Return expr(value) End If End Function <Extension> Public Function Ns(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, TResult)) As Nullable(Of TResult) If value Is Nothing Then Return Nothing Else Return expr(value) End If End Function <Extension> Public Function Ns(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, Nullable(Of TResult))) As Nullable(Of TResult) If value Is Nothing Then Return Nothing Else Return expr(value) End If End Function End Module
とくに解説することも無いのですが、
N(Of TModel, TResult As Class)(value As TModel, expr As Func(Of TModel, TResult))
と
N(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, TResult))
がオーバーロード不可なのはなんとなくわかりますが、
Ns(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, TResult))
と
Ns(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, Nullable(Of TResult)))
はオーバーロードできるんですね。驚きです。
んで、こんな感じに使います。
Dim a = New Hoge() With {.Id = 1, .Name = "Hoge", .ParentId = 0} Dim b = New Hoge() With {.Id = 1, .Name = "Hoge", .ParentId = Nothing} Dim c As Hoge Console.WriteLine(a.Ns(Function(it) it.Id)) Console.WriteLine(a.N(Function(it) it.Name)) Console.WriteLine(a.Ns(Function(it) it.ParentId)) Console.WriteLine(b.Ns(Function(it) it.Id)) Console.WriteLine(b.N(Function(it) it.Name)) Console.WriteLine(b.Ns(Function(it) it.ParentId)) Console.WriteLine(c.Ns(Function(it) it.Id)) Console.WriteLine(c.N(Function(it) it.Name)) Console.WriteLine(c.Ns(Function(it) it.ParentId))
おわりに
なんというか、拡張メソッドの乱用ですよね。
Optional
みたいなアプローチではなくnull
だろうがなんだろうがゴリ押しで値を取得するみたいなアプローチ、私は結構好きです。
ただ、実際に他人のコードでこんなのを見たら『うわぁ・・・』は必至ですね。
取り合えず設計をもう少しどうにかしろよ的な。
余談ですが、VBのラムダってFunction(it) it...
みたいに冗長なのが嫌いなんですよね。
同じ意味を持つC#のそれと比較してもit => it.Name
とFunction(it) it.Name
の差の大きいこと。
まぁ、そもそもC#/VBののラムダの内部に可変ステートを抱えられる仕様はちょっとどうかな~と思います。 シングルスレッドならともかく、マルチスレッドでうっかり可変ステートを外部に出してしまうとスレッドセーフの要件の一つである『不変なステートは常にスレッドセーフ』が途端に崩壊してしまうのでマルチスレッドで使うときはハラハラします。
とにかく、Decimalの内部構造を取り出して桁数を解析する以来のバッドノウハウの再来ということで。
おわり
*1:そもそもYield Returnが使えない。複数行ラムダはぎりぎり使える。あっ(察し)