math314のブログ

主に競技プログラミング,CTFの結果を載せます

Windowsで、実行ファイルを書き換えずに既存の.Netアプリケーションのメソッドを置き換える話

動機

昔、箱庭XSSという問題がSECCONで出題されました。 どのような問題だったかは他の方のブログを見て頂ければ分かるかと思います。

nash.hatenablog.com

問題作者のスライドはこちらです。

【XSS Bonsai】 受賞のご挨拶 by @ymzkei5 【SECCON 2014】 - Dec 08, 2014

このアプリケーションは.Net製で、

  • 難読化
  • 既存のdecompilerでは C#, VB.Netのコードに変換出来ない(大体落ちたり例外が発生する)
  • デバッガでattachすると挙動が変わったり、答えが変わる
  • バイナリを書き換えると、checksum一致処理に引っかかる

という特性があります。

webの問題として解いてほしいらしく、作者さんのスライド曰く

f:id:math314:20170122002615p:plain

とのことなので、チートで解こうと考えました。

今回要求される要件は

  • debugger検知に引っかからない
  • 実行ファイルを書き換えない
    • checksumが変わるため

の二つです。

disassembleしてみると、「正規表現で文字列を置換している処理さえ潰せればフラグを取れるのでは?」と思ったので、「Regex.Replaceの挙動を変更する」ことを目標にしました。

しかし箱庭XSSのみに特化したプログラムを書いても面白くないので、もうちょっと大きく出て「.Netアプリケーションのメソッドを、実行ファイルを書き換えずに挙動を変える」ということにしましょう。

(なおこれ以降箱庭XSSは出てきません、手段と目的は往々にして入れ替わります)

実装

github.com

に置いてあるのでご自由にどうぞ。(英語でコメント書こうとして挫折した後がありますね) とりあえず試してみたい人は https://github.com/math314/DotNetInjection/releases/tag/v1.0 から Release.zip をダウンロードして試してみてください。動かし方はreadmeに書いてあります。

前置きが長くなりましたがやっていきましょう。

ネイティブアプリケーションなら、DLL injectionをするだけで簡単にメソッドを置き換えられます。 http://inaz2.hatenablog.com/entry/2015/08/08/223643 がわかりやすいかと思います。

.NetでもDLL injectionが出来れば…、と考えたんですが、どうも一筋縄ではいかないようです。 ネイティブアプリケーションであれば、DLL injection -> LoadLibraryExあたりをHookして、書き換えたいメソッドのあるDLLを発見した段階でDLLをメモリ上で書き換える…となるんですが、 そもそも.NetのRuntimeはManaged DLLを読み込む際にLoadLibraryを使っていないんじゃないかと思います(が確信はありません)。 それに、DLLを上手に読み込ませたとしてその後どのように挙動を変えればいいのでしょうか?ILを書き換えればよい…?困難が多そうです。

しかし.NetのRuntimeはこんなニッチな要求に答える機能を持っています。それは Profiler API です。

Profiler API(ICorProfilerCallback interface)

Profiler APIはどういうものか見ていきましょう。

MIDL_INTERFACE("176FBED1-A55C-4796-98CA-A9DA0EF883E7")
ICorProfilerCallback : public IUnknown
{
public:
    virtual HRESULT STDMETHODCALLTYPE Initialize(
        /* [in] */ IUnknown *pICorProfilerInfoUnk) = 0;
    ...
}

これはICorProfilerCallbackというInterfaceです。IUnknownを見て気付いた方もいると思いますが、COM Objectですね。大人しくC++でCOMとたわむれましょう。

.NetのRuntimeが、プロファイラを認識すると、ICorProfilerCallback::Initializeを呼び出します。

    ICorProfilerInfo2* iInfo2;
    HRESULT hr = pICorProfilerInfoUnk->QueryInterface(__uuidof(iInfo2), (LPVOID*)&iInfo2);
    if (FAILED(hr)) {
        DebugPrintf(L"Error: Failed to get ICorProfiler2\n"); //OutputDebugStringWを使って出力するformatメソッド
        exit(-1);
    }

    DWORD eventMask = 0;
    eventMask |= COR_PRF_MONITOR_JIT_COMPILATION;
    eventMask |= COR_PRF_DISABLE_OPTIMIZATIONS;
    eventMask |= COR_PRF_USE_PROFILE_IMAGES;

    return mCorProfilerInfo2->SetEventMask(eventMask);

その際 IUnknown *pICorProfilerInfoUnkが引数として渡されるので、使いたいinterfaceがあれば事前に取得します。 今回はICorProfilerInfo2が欲しいので、上記のように取得しておきます。

また、初期化時に ICorProfilerInfo2::SetEventMask を呼び出しておきましょう。

  • COR_PRF_MONITOR_JIT_COMPILATION: 後述するJITCompilationStartedイベントを発生させるフラグ
  • COR_PRF_DISABLE_OPTIMIZATIONS: ランタイムによるあらゆる最適化を無効化する。 入れておくのが無難?
  • COR_PRF_USE_PROFILE_IMAGES: ngenであらかじめJIT compileされたライブラリについても、profileが可能な、つまりJIT compileされていないイメージを使用する
    • これをしないとSystem.*** 系統のライブラリをHook出来なくなります。後述するJITCompilationStartedが走らなくなるためです。

これで、メソッドのJITコンパイルが開始するイベントを取得できるようになりました。

JITCompilationStarted

ICorProfilerCallbackに宣言されているメソッドの一つに JITCompilationStarted というものがあります。

    virtual HRESULT STDMETHODCALLTYPE JITCompilationStarted(
        /* [in] */ FunctionID functionId,
        /* [in] */ BOOL fIsSafeToBlock) = 0;

FunctionIDは(ClassID, ModuleID, mdToken) と 1:1 で紐づいているユニークなIDです。Windows programmingでよく出てくるHANDLEみたいなやつです。 ClassID,ModuleIDも似たようなやつで、これらのIDからメソッド名やクラス名、モジュール名やアセンブリ名、メソッドの返り値や引数を取得することができます。

このFunctionIDから必要な情報を取り出して、 自作のFunctionInfo classに詰め込みます。 なおこのクラスは

Really Easy Logging using IL Rewriting and the .NET Profiling API - CodeProject

のコードを元に適宜変更を加えてあります。

class FunctionInfo {
public:
    static FunctionInfo *CreateFunctionInfo(ICorProfilerInfo *profilerInfo, FunctionID functionID);
    ~FunctionInfo() {};

    FunctionID get_FunctionID() const { return mFunctionID; }
    ClassID get_ClassID() const { return mClassID; }
    mdTypeDef get_ClassTypeDef() const { return mClassTypeDef; }
    ModuleID get_ModuleID() const { return mModuleID; }
    mdToken get_FunctionToken() const { return mFunctionToken; }
    const std::wstring& get_ClassName() const { return mClassName; }
    const std::wstring& get_FunctionName() const { return mFunctionName; }
    const std::wstring& get_AssemblyName() const { return mAssemblyName; }
    const std::wstring& get_SignatureText() const { return mSignatureText; }
    const std::vector<BYTE>& get_SignatureBlob() const { return mSignatureBlob; }
    DWORD get_MethodAttributes() const { return mMethodAttributes; }
    ULONG get_ArgumentCount() const { return mArguments.size(); }
    const std::wstring& get_RetType() const { return mRetType; }
    const std::vector<std::wstring>& get_Arguments() const { return mArguments; }

    static PCCOR_SIGNATURE ParseSignature(IMetaDataImport *pMDImport, PCCOR_SIGNATURE signature, WCHAR* szBuffer);
private:
    FunctionInfo() {};

    FunctionID mFunctionID;
    ClassID mClassID;
    mdTypeDef mClassTypeDef;
    ModuleID mModuleID;
    mdToken mFunctionToken;
    std::wstring mClassName;
    std::wstring mFunctionName;
    std::wstring mAssemblyName;
    std::wstring mSignatureText;

    DWORD mMethodAttributes;
    std::vector<BYTE> mSignatureBlob;
    std::wstring mRetType;
    std::vector<std::wstring> mArguments;
};

const int MAX_LENGTH = 2048;

FunctionInfo *FunctionInfo::CreateFunctionInfo(ICorProfilerInfo *profilerInfo, FunctionID functionID)
{
    ClassID classID = 0;
    ModuleID moduleID = 0;
    mdToken tkMethod = 0;
    hrCheck(profilerInfo->GetFunctionInfo(functionID, &classID, &moduleID, &tkMethod));

    WCHAR moduleName[MAX_LENGTH];
    AssemblyID assemblyID;
    hrCheck(profilerInfo->GetModuleInfo(moduleID, NULL, MAX_LENGTH, 0, moduleName, &assemblyID));

    WCHAR assemblyName[MAX_LENGTH];
    hrCheck(profilerInfo->GetAssemblyInfo(assemblyID, MAX_LENGTH, 0, assemblyName, NULL, NULL));

    ComPtr<IMetaDataImport> metaDataImport;
    mdToken token = 0;
    hrCheck(profilerInfo->GetTokenAndMetaDataFromFunction(functionID, IID_IMetaDataImport, (LPUNKNOWN *)&metaDataImport, &token));

    mdTypeDef classTypeDef;
    WCHAR functionName[MAX_LENGTH];
    WCHAR className[MAX_LENGTH];
    PCCOR_SIGNATURE signatureBlob;
    ULONG signatureBlobLength;
    DWORD methodAttributes = 0;
    hrCheck(metaDataImport->GetMethodProps(token, &classTypeDef, functionName, MAX_LENGTH, 0, &methodAttributes, &signatureBlob, &signatureBlobLength, NULL, NULL));
    hrCheck(metaDataImport->GetTypeDefProps(classTypeDef, className, MAX_LENGTH, 0, NULL, NULL));

    PCCOR_SIGNATURE signatureBlobOrigin = signatureBlob;

    ULONG callConvension = IMAGE_CEE_CS_CALLCONV_MAX;
    signatureBlob += CorSigUncompressData(signatureBlob, &callConvension);

    ULONG argumentCount;
    signatureBlob += CorSigUncompressData(signatureBlob, &argumentCount);

    WCHAR returnType[MAX_LENGTH];
    returnType[0] = '\0';
    signatureBlob = ParseSignature(metaDataImport.Get(), signatureBlob, returnType);

    WCHAR signatureText[MAX_LENGTH] = L"";
    wsprintf(signatureText, L"fid=%08X|%s %s %s::%s",
        functionID,
        (methodAttributes & mdStatic) == 0 ? L"(nonstatic)" : L"static", returnType, className, functionName
        );

    std::vector<std::wstring> arguments;
    for (ULONG i = 0; (signatureBlob != NULL) && (i < argumentCount); ++i) {
        WCHAR parameters[MAX_LENGTH];
        parameters[0] = '\0';
        signatureBlob = ParseSignature(metaDataImport.Get(), signatureBlob, parameters);
        arguments.push_back(parameters);
    }

    lstrcatW(signatureText, L"(");
    for (ULONG i = 0; i < arguments.size(); i++) {
        if(i != 0) lstrcatW(signatureText, L",");
        lstrcatW(signatureText, arguments[i].c_str());
    }
    lstrcatW(signatureText, L")");

    FunctionInfo* result = new FunctionInfo();

    result->mFunctionID = functionID;
    result->mClassID = classID;
    result->mClassTypeDef = classTypeDef;
    result->mModuleID = moduleID;
    result->mFunctionToken = tkMethod;
    result->mFunctionName = functionName;
    result->mClassName = className;
    result->mAssemblyName = assemblyName;
    result->mSignatureText = signatureText;
    result->mSignatureBlob = std::vector<BYTE>(signatureBlobOrigin, signatureBlob);
    result->mMethodAttributes = methodAttributes;
    result->mRetType = returnType;
    result->mArguments = arguments;

    return result;
}

一部サボっててbuffer over flowが発生する可能性があるので気を付けてください。(すいません)

CorSigUncompressDataはcor.hに定義されているメソッドです。 ParseSignatureはFunctionInfoに定義されているメソッドであり、ここではあまり大事ではないため説明は省略します。

.NetのメソッドはheaderとILの2つで構成されています。header部を後で流用するために signatureBlobOriginsignatureBlob を計算しています。 ここでは、signatureBlobOriginからsignatureBlobまでがheader, signatureBlob以降がIL部を指します。

signatureTextにはメソッドの情報が入ります。例えば DateTime.get_Now() であれば fid=0547B110|static System.DateTime System.DateTime::get_Now(), Regex.Replace(string input,string replacement) であれば fid=054958AC|(nonstatic) string System.Text.RegularExpressions.Regex::Replace(string,string) となります。 fidはfunctionIDなので、参照しているモジュールが変わればfidも変わるかと思います。

必要な情報は集まったので、メソッドを置き換えます。例として DateTime.get_Now() を置き換えてみます。

今回は 2000/01/01 を常に返すように変えてみます。

STDMETHODIMP HakoniwaProfilerImpl::JITCompilationStarted(FunctionID functionID, BOOL fIsSafeToBlock) {
    std::shared_ptr<FunctionInfo> fi(FunctionInfo::CreateFunctionInfo(mCorProfilerInfo2.Get(), functionID));

    //クラス名,メソッド名が一致する物を置き換える
    if (fi->get_ClassName() == L"System.DateTime" && fi->get_FunctionName() == L"get_Now") {
        DebugPrintf(L"%s", fi->get_SignatureText().c_str());
        Tranpoline tranpoline(mCorProfilerInfo2, fi);
        tranpoline.Update(L"HakoniwaProfiler.MethodHook.MethodHook", L"get_Now");
    }
}
namespace HakoniwaProfiler.MethodHook {
    public class MethodHook {
        static public DateTime get_Now()
        {
            Console.WriteLine("[!] HakoniwaProfiler.MethodHook.MethodHook.get_Now");
            return new DateTime(2000,1,1);
        }
    }
}

Tranpoline::Updateを見てみましょう。

class Tranpoline {
public:
    Tranpoline(Microsoft::WRL::ComPtr<ICorProfilerInfo2>& info, std::shared_ptr<FunctionInfo>& fi)
        : info(info), fi(fi) {}
private:
    std::vector<BYTE> Tranpoline::GetFunctionSignatureBlob();
    mdMemberRef Tranpoline::DefineHakoniwaMethodIntoThisAssembly(const wchar_t* fullyQualifiedClassName, const wchar_t* methodName);
    ULONG calcNewMethodArgCount();
    void* AllocateFuctionBody(DWORD size);
    std::vector<BYTE> ConstructTranpolineMethodIL(mdMemberRef mdCallFunctionRef);
    std::vector<BYTE> ConstructTranpolineMethodHeader(DWORD codeSize);

    Microsoft::WRL::ComPtr<ICorProfilerInfo2> info;
    std::shared_ptr<FunctionInfo> fi;
};

void Tranpoline::Update(const wchar_t* className, const wchar_t* methodName) {
    // HakoniwaProfiler.MethodHook に定義されているメソッドの mdMemberRef を、対象のアセンブリ内に定義する
    mdMemberRef newMemberRef = DefineHakoniwaMethodIntoThisAssembly(className, methodName);

    // 新しいメソッドのheader部とIL部を作成
    std::vector<BYTE> newHeader = ConstructTranpolineMethodHeader(newILs.size());
    std::vector<BYTE> newILs = ConstructTranpolineMethodIL(newMemberRef);

    // メソッド用のメモリ領域を確保して書き込み
    ULONG newMethodSize = newHeader.size() + newILs.size();
    void *allocated = AllocateFuctionBody(newMethodSize);
    memcpy(allocated, &newHeader[0], newHeader.size());
    memcpy((BYTE*)allocated + newHeader.size(), &newILs[0], newILs.size());

    // メソッドを入れ替える
    hrCheck(info->SetILFunctionBody(fi->get_ModuleID(), fi->get_FunctionToken(), (LPCBYTE)allocated));
}

今回の肝である ConstructTranpolineMethodIL を見てみましょう。

std::vector<BYTE> Tranpoline::ConstructTranpolineMethodIL(mdMemberRef mdCallFunctionRef) {
    std::vector<BYTE> newILs;
    ULONG newArguments = calcNewMethodArgCount();

    // ldarg.0, ldarg.1 ... ldarg. newArguments - 1
    for (ULONG i = 0; i < newArguments; i++) {
        if (i < 4) {
            newILs.push_back(0x02 + (BYTE)i); //ldarg.0 ~ ldarg.3
        } else {
            newILs.push_back(0x0E);
            newILs.push_back((BYTE)i); //ldarg.s <index>
        }
    }

    // method call
    newILs.push_back(0x28);
    newILs.push_back((mdCallFunctionRef >> 0) & 0xFF);
    newILs.push_back((mdCallFunctionRef >> 8) & 0xFF);
    newILs.push_back((mdCallFunctionRef >> 16) & 0xFF);
    newILs.push_back((mdCallFunctionRef >> 24) & 0xFF);

    // ret
    newILs.push_back(0x2a);

    return newILs;
}

ここでは

ldarg.0
ldarg.1
ldarg.2
...
call    <置き換え先のメソッド>
ret

というILを組み立てています。

ここで、普通のアセンブラのようにjmpを使えばいいのでは?と思う方もいると思いますが 別モジュールに含まれるメソッドを呼び出そうとすると System.Security.VerificationExceptionが発生することがあるので使えません。 発生する条件は以下が参考になるかと思います。

OpCodes.Jmp Field (System.Reflection.Emit)

c# - How to use .net cil jmp opcode - Stack Overflow

余談ですが、C#では引数を216-1個まで受け付けることが出来るようです。今回は255個までしか受け付けませんが…

サンプル実行

ついにメソッドの置き換えが実装できました! 色々な種類のメソッドを置き換えてみましょう。

置き換えられるメソッド群です。全てConsoleAppTest.Program内に定義されています。

using System;
using System.Text.RegularExpressions;

namespace ConsoleAppTest {
    class Program {

        static string getStr1()
        {
            Console.WriteLine("ConsoleAppTest.Program.getStr1");
            return "getStr1";
        }

        static string haveArguments(string arg1, string arg2)
        {
            return string.Format("{0} + {1}", arg1, arg2);
        }

        static string haveManyArguments(string arg1, string arg2, string arg3, double arg4, int arg5, int arg6)
        {
            return string.Format("{0} + {1} + {2} + {3} + {4} + {5}", arg1, arg2, arg3, arg4, arg5, arg6);
        }

        static int intarg2(int x,int b)
        {
            return x + b;
        }

        static void hoge() {
            Console.WriteLine(DateTime.Now);
            string a = Regex.Replace("poyohugapoyopiyo", "piyo", "xxxx");
            Console.WriteLine(a);
            string b = new Regex("piyo").Replace("poyohugapoyopiyo", "xxxx");
            Console.WriteLine(b);
            Console.WriteLine(getStr1());

            Console.WriteLine(haveArguments("aa", "bb"));
            Console.WriteLine(intarg2(1,2));
            Console.WriteLine(haveManyArguments("a", "b", "c", 2.0, 3, 4));

        }

        static void Main(string[] args) {
            hoge();
            var x = TestClass.test1(1, "aaa");
            Console.WriteLine(x);
            var y = new TestClass(1).test2("aaa");
            Console.WriteLine(y);
        }
    }

    public class TestClass {

        int _a;

        public TestClass(int a)
        {
            _a = a;
        }

        public string test2(string b)
        {
            Console.WriteLine("TestClass.test2 : {0}", b);
            return b;
        }

    public static string test1(int a,string b)
        {
            Console.WriteLine("TestClass.test2 : {0} , {1}",a, b);
            return b;
        }
    }
}

普通にMain関数を実行してみましょう。

> ConsoleAppTest.exe
2017/01/21 23:34:05
poyohugapoyoxxxx
poyohugapoyoxxxx
ConsoleAppTest.Program.getStr1
getStr1
aa + bb
3
a + b + c + 2 + 3 + 4
TestClass.test2 : 1 , aaa
aaa
TestClass.test2 : aaa
aaa

普通の実行結果ですね。

次に、それぞれMain関数以外を以下の関数でHookしてみます。

using System;
using System.Reflection;
using System.Text.RegularExpressions;

namespace HakoniwaProfiler.MethodHook {
    public class MethodHook {

        static public string getStr1()
        {
            Console.WriteLine("[!] HakoniwaProfiler.MethodHook.MethodHook.getStr1");
            return "HHHH";
        }

        static public DateTime get_Now()
        {
            Console.WriteLine("[!] HakoniwaProfiler.MethodHook.MethodHook.get_Now");
            return new DateTime(2000,1,1);
        }

        static public string haveArguments(string arg1, string arg2)
        {
            Console.WriteLine("[!] HakoniwaProfiler.MethodHook.MethodHook.haveArguments");
            return string.Format("{0} + {1}", arg1, arg2);
        }

        static public string haveManyArguments(string arg1, string arg2, string arg3, double arg4, int arg5, int arg6)
        {
            Console.WriteLine("[!] HakoniwaProfiler.MethodHook.MethodHook.haveManyArguments");
            return string.Format("{0} + {1} + {2} + {3} + {4} + {5}", arg1, arg2, arg3, arg4, arg5, arg6);
        }

        static public int intarg2(int x, int b)
        {
            Console.WriteLine("[!] HakoniwaProfiler.MethodHook.MethodHook.intarg2");
            return x + b;
        }


        static public string Replace(string input, string pattern, string replacement)
        {
            return Replace(new Regex(pattern), input, replacement);
        }

        static public string Replace(Regex regex, string input, string replacement)
        {
            Console.WriteLine("[!] HakoniwaProfiler.MethodHook.MethodHook.Replace");

            var pattern_info = typeof(Regex).GetField("pattern", BindingFlags.NonPublic | BindingFlags.Instance);
            string pattern = (string)pattern_info.GetValue(regex);
            Console.WriteLine("------------------------------");
            Console.WriteLine(string.Format("input = {0}\npattern = {1},pattern_length = {2}", input, pattern, pattern.Length));
            Console.WriteLine("------------------------------");

            return input;
        }

        static public string test1(int a, string b)
        {
            Console.WriteLine("[!] HakoniwaProfiler.MethodHook.MethodHook.test1");
            return "";
        }

        static public string test2(ConsoleAppTest.TestClass x, string b)
        {
            Console.WriteLine("[!] HakoniwaProfiler.MethodHook.MethodHook.test2");
            return "";
        }
    }
}

出力は

> Injector.exe ConsoleAppTest.exe
ConsoleAppTest started.
[!] HakoniwaProfiler.MethodHook.MethodHook.get_Now
2000/01/01 0:00:00
[!] HakoniwaProfiler.MethodHook.MethodHook.Replace
------------------------------
input = poyohugapoyopiyo
pattern = piyo,pattern_length = 4
------------------------------
poyohugapoyopiyo
[!] HakoniwaProfiler.MethodHook.MethodHook.Replace
------------------------------
input = poyohugapoyopiyo
pattern = piyo,pattern_length = 4
------------------------------
poyohugapoyopiyo
[!] HakoniwaProfiler.MethodHook.MethodHook.getStr1
HHHH
[!] HakoniwaProfiler.MethodHook.MethodHook.haveArguments
aa + bb
3
[!] HakoniwaProfiler.MethodHook.MethodHook.haveManyArguments
a + b + c + 2 + 3 + 4
[!] HakoniwaProfiler.MethodHook.MethodHook.test1

[!] HakoniwaProfiler.MethodHook.MethodHook.test2

となりました。きちんと置き換えられています!

Injector

ここになって急に新しいアプリケーションが出てきました、Injector.exe です。

using System;
using System.Diagnostics;
using System.IO;

namespace Injector {
    class Program {

        const string PROFILER_UUID = "{9992F2A6-DF35-472B-AD3E-317F85D958D7}";
        const string PROFILER_NAME = "HakoniwaProfiler.dll";

        void StartTarget(string targetPath)
        {
            string current_dir = Directory.GetCurrentDirectory();
            string profiler_path = Path.Combine(current_dir, PROFILER_NAME);

            ProcessStartInfo psi = new ProcessStartInfo();
            psi.FileName = targetPath;
            psi.UseShellExecute = false;

            psi.EnvironmentVariables.Add("COR_ENABLE_PROFILING", "1");
            psi.EnvironmentVariables.Add("COR_PROFILER", PROFILER_UUID);
            psi.EnvironmentVariables.Add("COR_PROFILER_PATH", profiler_path);
            psi.EnvironmentVariables.Add("COMPLUS_Version", "v4.0.30319"); //.netのバージョンを強制的に4.0に固定する
            using (var proc = Process.Start(psi)) {
                Console.WriteLine("{0} started.",proc.ProcessName);
                proc.WaitForExit();
            }
        }

        static void Main(string[] args)
        {
            if (args.Length != 1) {
                Console.WriteLine("usage : Injector.exe <target app>");
                return;
            }
            new Program().StartTarget(args[0]);
        }
    }
}

環境変数を追加してプログラムを起動しているだけです。ここにあるように COR_PROFILERCOR_PROFILER_PATHを適切に設定することで、プロファイラ有効になります。 .Netのバージョンを4.0に固定しているので、 バージョンが4.0以下のアプリケーションであればちゃんと動くと思います。

このようにすることで、.NetのRuntimeはCOMのお作法にのっとりHakoniwaProfiler.dll内のDllGetClassObjectを呼び出して ICorProfilerCallback3を実装している UUID = {9992F2A6-DF35-472B-AD3E-317F85D958D7} のオブジェクトを取得しています。

Profilerについての公式記事: Profiling in the .NET Framework 4

ILのリスト: https://en.wikipedia.org/wiki/List_of_CIL_instructions

注意点

ここまでさらっと流してきましたが、きちんと動くようにするまでに多数の落とし穴があったので共有します。

  • 対象アプリケーションのプラットフォームターゲットがx86じゃないと動かない

    • 何故ならHakoniwaProfiler.dllがx86なので
    • x64にも対応したいなら,それ用のdllを作る必要がある
    • デフォルトでは"AnyCPU" だが,これでは64bitのOSで32bitアプリケーションとして実行は"出来ない"
      • 余談だが,.net4.5以降では "AnyCPU + 32bit優先" というオプションを付けることで,32bitでも動くようになる
    • CorFlags.exe というコマンドで,"AnyCPU"を"x86"に変更できる(exeを直接書き換える).
    • アウトプロセスサーバでプロファイラを作成すれば苦労が少ない?
    • 実はアウトプロセスサーバ+.netの場合,プロファイラのプラットフォームターゲットをAnyCPUにしてはいけない模様
      • ハングアップするらしい
    • 参考: http://qiita.com/mima_ita/items/57d7c1101543e214b1d6
  • mCorProfilerInfo2->SetEventMask で COR_PRF_USE_PROFILE_IMAGES を指定する必要がある

  • System.MethodAccessExceptionが発生する

    • 今回発生した理由は2つ
    • publicでない関数をcallしようとしたことにより発生した例外
      • あるクラスから,別クラスのprivateな関数の呼び出しをするように書き換えていた
      • publicな関数を呼び出すコードに書き変える事で対処
    • .net4.0から導入されたセキュリティモデルにより発生する例外
      • mscorlibのassemblyにはSystem.Security.SecurityTransparent属性が宣言されている
      • これが宣言されたアセンブリからは,SecurityTransparent,SecuritySafeCriticalな関数しか呼べない
      • デフォルトではSystem.Security.AllowPartiallyTrustedCallersが暗に宣言されているが,これではダメらしい
    • 参考http://www.atmarkit.co.jp/fdotnet/special/dotnet4security_01/dotnet4security_01_01.html
  • System.Security.SecurityTransparentを指定したアセンブリから,System.Security.AllowPartiallyTrustedCallersのアセンブリを呼び出せない

    • AllowPartiallyTrustedCallersのアセンブリのメソッドを置き換えるにはAllowPartiallyTrustedCallers, SecurityTransparentなアセンブリのメソッドを置き換えるにはSecurityTransparentのアセンブリを使う必要がある?
    • メソッドにSecuritySafeCritical属性をつけて試してみたが,セキュリティの例外が発生してしまった.
    • 置き換え先のアセンブリを2つ用意すれば解決するが,本質的ではないため,ConsoleAppTestはSecurityTransparent属性をつけている.
  • System.BadImageFormatException: バイナリ シグネチャが不適切です。(HRESULT = 0x80131192)

    • metaDataEmit->DefineMemberRef で設定したsignatureBlobにバグがあった
  • 署名をしていないアセンブリを,署名済みアセンブリに読み込ませることが出来なかった

    • 置き換え先のメソッドが定義されているアセンブリに署名をした
    • これにより,HakoniwaProfiler.MethodHook から, ConsoleAppTest を参照できなくなった.
    • 解決方法は,ConsoleAppTestに署名をするか,ConsoleAppTestで定義されている型を一切参照しないか,のどちらかである
    • 今回は簡単のため,前者の署名をした.

それでは、良いDLL Injection ライフをお楽しみください。