VB.NETでもMono.Cecilでアセンブリを書き換えたい
はじめに
今回は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を書き換えられるライブラリってことです。なにそれしゅごい
このライブラリを使えばコンパイル後であってもあーんなことやこーんなことが出来るわけですが、その分最高に面倒くさいです。 まぁ、正直なところ式木以外の動的コード生成は大概面倒なので諦めてください。むしろ式木が洗練されていて何これすげぇって感じなので。
目的
とりあえず今回はエントリポイントの冒頭に
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_0
とIL_0017
とIL_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 ツール) | Microsoft Docs