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

はじめに

jyuch.hatenablog.com

前回はildasmを用いた逆アセンブルとプログラムに出現する文字列から当該処理を行う箇所を特定し、特定の処理の迂回をやってみました。 今回はデバッガを用いて実際の動作を覗いてみましょう。

動的解析

ところで、前回アセンブラを用いてILコードから実行ファイルを生成したときのコマンドをもう一度確認してみましょう。

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

ここで重要なのが/debug=implの部分です。 このオプションでデバッグシンボルを一緒に生成しています。

プログラムを起動し、デバッガをアタッチしてみましょう。 なお、ここではVisual Studio 2015 community update 3を使用しています。

デバッグ->プロセスにアタッチで一覧から目的のプロセスを見つけ、アタッチします。

f:id:jyuch:20160709233050p:plain

ところで、このアプリケーションの処理内容について説明していませんでしたね。 文字列を入力するとその文字が格納され、なにも入力しないと入力した逆順に文字列が返されるようです。

なにそれただのスタックじゃん。なんだろな~。どうやって実装しているんだろうな~。気になるなぁ~。

前回の逆アセンブルコードから、SuperUsefulApplication.SuperUsefulClass`1<string>::Pop()SuperUsefulApplication.SuperUsefulClass`1<string>::Push()が中核的な処理をつかさどっているようです。 ですので、この二つのメソッドにブレークポイントを設定してみましょう。

デバッグ->ブレークポイントの作成->関数のブレークポイントからPopPushブレークポイントを設定します。

f:id:jyuch:20160709233059p:plain

文字列を4つ程突っ込んでデバッガで内部状態を確認してみましょう。

f:id:jyuch:20160709233045p:plain

f:id:jyuch:20160709233054p:plain

ローカル変数を確認すると、線形リストでスタックを実装しているようです。

このアプリケーションは単純なのでここで終わりですが、もっと複雑なアプリケーション/ライブラリの場合でも普段と同じようにデバッグができるでしょう。

おわりに

このように、マネージドアプリケーションはリバースエンジニアリングに対して非常に脆弱であることが分かります。

リバースエンジニアリングに対する防御策としては、

  • 一部もしくはすべてをアンマネージで実装する
  • サードパーティー製品を使用して難読化を施す
  • 重要なロジックはネットワーク越しにAPIを通じて利用するようなサービスとして展開する

などが挙げられます。

ロジックの重要度と実施するコストとを勘案していい感じにアレすればいいと思います。

おわり

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向けの記事ですので・・・

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.NameFunction(it) it.Nameの差の大きいこと。

まぁ、そもそもC#/VBののラムダの内部に可変ステートを抱えられる仕様はちょっとどうかな~と思います。 シングルスレッドならともかく、マルチスレッドでうっかり可変ステートを外部に出してしまうとスレッドセーフの要件の一つである『不変なステートは常にスレッドセーフ』が途端に崩壊してしまうのでマルチスレッドで使うときはハラハラします。

とにかく、Decimalの内部構造を取り出して桁数を解析する以来のバッドノウハウの再来ということで。

github.com

おわり

*1:そもそもYield Returnが使えない。複数行ラムダはぎりぎり使える。あっ(察し)