Date Tags CSharp

何個か方法がある。とりあえずは雑多にメモ書きしておく

一行一行全て手動で出力する

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(全てのメソッドを修正する例)

他参考資料