VB.NETでもMono.Cecilでアセンブリを書き換えたい

はじめに

某家電量販店でSurface Bookを予約したら全店舗で初予約者でした。どうも弊社です。 しばらくは霞を食べて生きていきます。

さて、シリーズになっているのかなっていないのかよくわからん.NET黒魔術(メタプログラミング)シリーズですが、今回はMono.Cecilを扱います。

Mono.Cecilとは何ぞやと言いますと

In simple English, with Cecil, you can load existing managed assemblies, browse all the contained types, modify them on the fly and save back to the disk the modified assembly.

だそうです。

それっぽく訳すと

Cecilを用いて既存のマネージドアセンブリを読み込み、包含されている全ての型を閲覧し、それらをその場で改変し改変済みアセンブリをディスクに保存することができます。

でしょうか。

まぁ、要するにコンパイル後のマネージドなexeとかdllを書き換えられるライブラリってことです。なにそれしゅごい

このライブラリを使えばコンパイル後であってもあーんなことやこーんなことが出来るわけですが、その分最高に面倒くさいです。 まぁ、正直なところ式木以外の動的コード生成は大概面倒なので諦めてください。むしろ式木が洗練されていて何これすげぇって感じなので。

目的

github.com

とりあえず今回はエントリポイントの冒頭に

If DateTime.Now > New DateTime("任意の日時") Then
    Environment.Exit(0)
End If

を放り込むようにしてみましょう。 要するにある日突然動かなくなるソフトウェアを生成するってことですね。

正直なところ嫌がらせ以外に使い道はありませんが、逆に考えると嫌がらせとしては非常に有用ですね。 まぁ一応言っておきますと、業務で用いられるソフトウェアでやると最悪『電子計算機損壊等業務妨害罪』あたりで起訴されるかもしれないんでやらないでください。

IL

弊社は人間を辞めているわけではないのでゼロベースからはILを組み立てることが出来ません。 そこで同じことを行うコードを一回コンパイルしてildasmで逆アセンブルして同じようなILをEmitするようにしましょう。

Sub Main()
    If DateTime.Now > New DateTime(2016, 2, 1) Then
        Environment.Exit(0)
    End If

    Console.WriteLine("Hello world")
End Sub
  .entrypoint
  .maxstack  4
  .locals init ([0] bool V_0)
  IL_0000:  nop
  IL_0001:  call       valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
  IL_0006:  ldc.i4     0x7e0
  IL_000b:  ldc.i4.2
  IL_000c:  ldc.i4.1
  IL_000d:  newobj     instance void [mscorlib]System.DateTime::.ctor(int32,
                                                                      int32,
                                                                      int32)
  IL_0012:  call       bool [mscorlib]System.DateTime::op_GreaterThan(valuetype [mscorlib]System.DateTime,
                                                                      valuetype [mscorlib]System.DateTime)
  IL_0017:  stloc.0
  IL_0018:  ldloc.0
  IL_0019:  brfalse.s  IL_0024
  IL_001b:  nop
  IL_001c:  ldc.i4.0
  IL_001d:  call       void [mscorlib]System.Environment::Exit(int32)
  IL_0022:  nop
  IL_0023:  nop
  IL_0024:  ldstr      "Hello world"
  IL_0029:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_002e:  nop
  IL_002f:  ret

厳密にはこれはC#コンパイラが吐き出したアセンブリを逆アセンブルした結果なのですが、やってる内容は等価なので命令数が少ない(≒実装がちょっと楽)なC#版を使用します。また、ローカル変数V_0IL_0017IL_0018は不要なのでばっさりカットします。Visual Studioのデバッガで評価式の結果を表示させるのに必要なのであってこの場合は不要ですからね。

余談ですが、Visual Studioのデバッガで評価スタックの中身を見るとかは可能なんでしょうか? わたし、気になります コンソールデバッガをペチペチすれば出来そうな気がしますが、コンソールデバッガとか面倒じゃないですか。

というわけで最終的な出力目標はこんな感じです。イメージなので命令のオフセットがずれているのは許してください。

  IL_0000:  nop
  IL_0001:  call       valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
  IL_0006:  ldc.i4     0x7e0 // 任意の値その1 年
  IL_000b:  ldc.i4.2         // 任意の値その2 月
  IL_000c:  ldc.i4.1         // 任意の値その3 日
  IL_000d:  newobj     instance void [mscorlib]System.DateTime::.ctor(int32,
                                                                      int32,
                                                                      int32)
  IL_0012:  call       bool [mscorlib]System.DateTime::op_GreaterThan(valuetype [mscorlib]System.DateTime,
                                                                      valuetype [mscorlib]System.DateTime)
  IL_0019:  brfalse.s  IL_0024
  IL_001b:  nop
  IL_001c:  ldc.i4.0
  IL_001d:  call       void [mscorlib]System.Environment::Exit(int32)
  IL_0022:  nop
  IL_0023:  nop
  IL_0024:  // 本来のエントリポイントの冒頭

そんな感じでポンと実装。

Imports Mono.Cecil
Imports Mono.Cecil.Cil

Module Program

    Sub Main(args As String())
        Dim assmPath = args(0)
        Dim limitDate = DateTime.Parse(args(1))

        Console.WriteLine($"Target:{assmPath}")
        Console.WriteLine($"Limit:{limitDate:yyyy/MM/dd}")

        Inject(assmPath, limitDate.Year, limitDate.Month, limitDate.Day)
    End Sub

    Sub Inject(path As String, year As Integer, month As Integer, day As Integer)
        Dim assm = AssemblyDefinition.ReadAssembly(path)
        Dim nowPropertyGetter = assm.MainModule.Import(GetType(DateTime).GetProperty("Now").GetGetMethod())
        Dim ctor = assm.MainModule.Import(GetType(DateTime).GetConstructor(New Type() {GetType(Integer), GetType(Integer), GetType(Integer)}))
        Dim graterThan = assm.MainModule.Import(GetType(DateTime).GetMethod("op_GreaterThan", New Type() {GetType(DateTime), GetType(DateTime)}))
        Dim envExit = assm.MainModule.Import(GetType(Environment).GetMethod("Exit", New Type() {GetType(Integer)}))

        Dim entryPoint = assm.EntryPoint
        Dim processor = entryPoint.Body.GetILProcessor()
        Dim first = entryPoint.Body.Instructions(0)

        processor.InsertBefore(first, processor.Create(OpCodes.Call, nowPropertyGetter))
        processor.InsertBefore(first, processor.Create(OpCodes.Ldc_I4, year))
        processor.InsertBefore(first, processor.Create(OpCodes.Ldc_I4, month))
        processor.InsertBefore(first, processor.Create(OpCodes.Ldc_I4, day))
        processor.InsertBefore(first, processor.Create(OpCodes.Newobj, ctor))
        processor.InsertBefore(first, processor.Create(OpCodes.Call, graterThan))
        processor.InsertBefore(first, processor.Create(OpCodes.Brfalse_S, first))
        processor.InsertBefore(first, processor.Create(OpCodes.Ldc_I4_0))
        processor.InsertBefore(first, processor.Create(OpCodes.Call, envExit))

        assm.Write(path)
    End Sub

End Module

糖質引数のチェックとかカットしているので変な値が放り込まれた瞬間に例外が飛びますが、まぁ気にしないでください。

試しにこんなコードに挿入してみましょう。

Module Module1

    Sub Main()
        FizzBuzz(100)
    End Sub

    Sub FizzBuzz([end] As Integer)
        For i = 1 To [end]
            If i Mod 15 = 0 Then
                Console.WriteLine("fizzbuzz")
            ElseIf i Mod 5 = 0 Then
                Console.WriteLine("buzz")
            ElseIf i Mod 3 = 0 Then
                Console.WriteLine("fizz")
            Else
                Console.WriteLine(i)
            End If
        Next
    End Sub

End Module

オリジナルのエントリポイントのILはこんな感じ。

.method public static void  Main() cil managed
{
  .entrypoint
  .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 ) 
  // コード サイズ       10 (0xa)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldc.i4.s   100
  IL_0003:  call       void TargetApp.Module1::FizzBuzz(int32)
  IL_0008:  nop
  IL_0009:  ret
} // end of method Module1::Main

んで、強制終了コードを挿入したILはこんな感じ。

.method public static void  Main() cil managed
{
  .entrypoint
  .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 ) 
  // コード サイズ       48 (0x30)
  .maxstack  8
  IL_0000:  call       valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
  IL_0005:  ldc.i4     0x7e0
  IL_000a:  ldc.i4     0x2
  IL_000f:  ldc.i4     0x1
  IL_0014:  newobj     instance void [mscorlib]System.DateTime::.ctor(int32,
                                                                      int32,
                                                                      int32)
  IL_0019:  call       bool [mscorlib]System.DateTime::op_GreaterThan(valuetype [mscorlib]System.DateTime,
                                                                      valuetype [mscorlib]System.DateTime)
  IL_001e:  brfalse.s  IL_0026
  IL_0020:  ldc.i4.0
  IL_0021:  call       void [mscorlib]System.Environment::Exit(int32)
  IL_0026:  nop
  IL_0027:  ldc.i4.s   100
  IL_0029:  call       void TargetApp.Module1::FizzBuzz(int32)
  IL_002e:  nop
  IL_002f:  ret
} // end of method Module1::Main

peverify*1もパスし、問題なく動いてる感じです。

おわりに

Mono.Cecilを用いてそれっぽくアセンブリを書き換えることが出来ました。

また、応用としてINotifyPropertyChangedを自動実装したり(既にある)、属性が付けられたクラスのToStringを自動実装したり(既にある)、属性の付けられたパラメータのnullチェックを自動実装したり(既にある)、全てのメソッドの開始と終了時にログを吐かせたり(既にある)、電子計算機損壊等業務妨害罪に問われたり(コレ)とかが出来ます。 なんだよ全部既にあんじゃねーか。

余談ってほど余談じゃないんですが、オリジナルのコードでのスタックサイズが4以下の場合は自動的に4に拡張されるので不意のスタックオーバーフローで死ぬってことが無いので大変お得となっております。

そんな感じで、コンパイル済みのものを書き換えるという割と暴力的なことも一応出来るってことで。

おわり

*1:ILコードを検査するツール Peverify.exe (PEVerify ツール)