何個か方法がある。とりあえずは雑多にメモ書きしておく
一行一行全て手動で出力する
System.Refrection、System.Refrection.Emitを使ってilを直接書き出していく方法。
参考: c# - Reflection.Emit: AssemblyBuilder.SetEntryPoint does not set entry point - Stack Overflowゆーてもこれそのままやっても.netは知らないけどmonoだと動かなかったりするので、修正が必要。動くコードはこちら
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;
using System.IO;
using System.Diagnostics;
namespace iltest
{
class Program
{
static void Main(string[] args)
{
const string ASSEMBLY_NAME = "IL_Test";
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( new AssemblyName(ASSEMBLY_NAME), AssemblyBuilderAccess.Save);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(ASSEMBLY_NAME, "test.exe");
TypeBuilder typeBuilder = moduleBuilder.DefineType("Program", TypeAttributes.Class | TypeAttributes.Public);
MethodBuilder methodBuilder = typeBuilder.DefineMethod( "Main", MethodAttributes.HideBySig|MethodAttributes.Public | MethodAttributes.Static, typeof(void), new Type[] { typeof(string[]) });
ILGenerator gen = methodBuilder.GetILGenerator();
gen.Emit(OpCodes.Ldstr, "Hello, World!");
gen.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));
gen.Emit(OpCodes.Ldc_I4_1);
gen.Emit(OpCodes.Call, typeof(Console).GetMethod("ReadKey", new Type[] { typeof(bool) }));
gen.Emit (OpCodes.Pop);
gen.Emit (OpCodes.Ret);
typeBuilder.CreateType();
assemblyBuilder.SetEntryPoint(methodBuilder, PEFileKinds.ConsoleApplication);
File.Delete("test.exe");
assemblyBuilder.Save("test.exe");
Process.Start("test.exe");
}
}
}
手順は大雑把に
- 出力したいファイルの情報を作る。(Mainの1行目〜3行目)
- class、メソッドを作る(4, 5行目)
- ilを書き出す(6 〜 13行目)
- エントリーポイントを作る(14行目)
- ファイルを出力する(16行目)
- 実行する(17行目)
OpCOdesに関してはここをチェックしたOpCodes Fields (System.Reflection.Emit)
実際にどういうilコードを書けば良いかは、一回C#でビルドしたものを
$ monodis hoge.exe
みたいにしてilコードを表示させると良い
一旦ビルドしたものを手動で修正する
何個か選択肢があるみたいだが、Mono.Cecilを使ったので、Mono.Cecilについてのみ書く。
色々情報はあるんだけど、どうもバージョンの違いによって作法が若干変わるみたい。なので、これは2015/11/29時点の情報(バージョン0.9.6)になる。
test.exeにConsole.WriteLine("injected!")を追加するにはこんな感じで
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;
using Mono.Cecil;
using Mono.Cecil.Cil;
namespace ceciltest{
class Program {
public static void Main (string[] args)
{
asm = AssemblyDefinition.ReadAssembly(@"test.exe");
mainDef = asm.MainModule.Types.OfType<TypeDefinition>().Single(t => t.Name == "MainClass");
MethodDefinition mainMethod = mainDef.Methods.OfType<MethodDefinition>().Single(m => m.Name == "Main");
Instruction ret = mainMethod.Body.Instructions.Last(i => i.OpCode == OpCodes.Ret);
mainMethod.Body.Instructions.Remove (ret);
mainMethod.Body.Instructions.Add (Instruction.Create(Mono.Cecil.Cil.OpCodes.Ldstr,"injected!"));
var writeLineMethod = typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) });
var writeLineRef = asm.MainModule.ImportReference(writeLineMethod);
mainMethod.Body.Instructions.Add (Instruction.Create(OpCodes.Call, writeLineRef));
mainMethod.Body.Instructions.Add (Instruction.Create(OpCodes.Ret));
asm.Write("test.modified.exe");
}
}
}
大雑把にやってることはこんな感じ
- ビルドしたもの(dllとかexeとか)をアセンブリとして読み込む(Main関数の1行目)
- 変更したいクラス、メソッドを取り出す(2, 3行目)
- メソッド内のilを修正(追加とか削除とか)
- Main内に元々あるretを削除(4, 5行目)
- 出力したい文字列を設定する(6行目)
- Console.WriteLineを定義(7行目)
- Callする為の参照を作る(8行目)。
- Console.WriteLineをCallする(9行目)
- 消したretを再度追加
- 修正したものを書き出し(11行目)
雑多
- 他の情報でImportとあるのはImportReferenceになっている
- 型やメソッドなど参照で登録するのでImportReferenceを使う。アセンブリ内で独自に定義している型の場合は別の作法が必要。
参考: + Mono - Cecil - TypeReference of bool + .Net injection with Mono.Cecil(全てのメソッドを修正する例)
他参考資料