不自然な診断







エンドユーザーによるプログラムのクラッシュに対処することは重要ですが、かなり難しいです。 クライアントのマシンへのアクセスは通常そうではありません。 アクセスがある場合、デバッガはありません。 デバッガーがある場合、問題が再現されないなどのことがわかります。 アプリケーションの特別なバージョンを収集してクライアントにインストールする機会さえない場合はどうすればよいですか? それから、キャットへようこそ!



したがって、 TRIZの用語では、技術的な矛盾があります。ログを書き込み、クラッシュレポートを送信するようにプログラムを変更する必要がありますが、プログラムを変更する方法はありません。 明確にするために、自然に変更したり、必要な機能を追加したり、クライアントを再構築およびインストールしたりする方法はありません。 したがって、我々は、 熱直腸暗号解読の達人の教訓に従って、それを不自然な方法で変更します!



このような困難なケースの作成を含め、 クラッシュレポーターをプログラムに埋め込みます 。 もちろん、開発者が本来意図していなかった他のコードをプログラムに導入するために、次のアプローチを使用することを気にする人はいません。



そのため、必要なアセンブリをダウンロードして初期化コードを実行するには、 マネージアプリケーション自体が何らかの「魔法の方法」で必要です。



LogifyAlert client = LogifyAlert.Instance; client.ApiKey = "my-api-key"; client.StartExceptionsHandling();
      
      





まあ、彼らは運転しました。



必要な「マジック」テクノロジーが存在し、 DLL-injectionと呼ばれます。これは、アプリケーションを起動する(または既に起動されているものにアタッチする)ローダーとなり、必要なDLLをアプリケーションプロセスに導入します。



こんな感じ



相互運用パック
 [DllImport("kernel32.dll")] static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId); [DllImport("kernel32.dll", CharSet = CharSet.Auto)] static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, AllocationType flAllocationType, uint flProtect); [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, AllocationType dwFreeType); [DllImport("kernel32.dll", SetLastError = true)] static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out UIntPtr lpNumberOfBytesWritten); [DllImport("kernel32.dll")] static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId); [DllImport("kernel32.dll", SetLastError = true)] static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds); [DllImport("kernel32.dll", SetLastError = true)] [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] [SuppressUnmanagedCodeSecurity] [return: MarshalAs(UnmanagedType.Bool)] static extern bool CloseHandle(IntPtr hObject); [Flags] public enum AllocationType { ReadWrite = 0x0004, Commit = 0x1000, Reserve = 0x2000, Decommit = 0x4000, Release = 0x8000, Reset = 0x80000, Physical = 0x400000, TopDown = 0x100000, WriteWatch = 0x200000, LargePages = 0x20000000 } public const uint PAGE_READWRITE = 4; public const UInt32 INFINITE = 0xFFFFFFFF;
      
      







プロセス識別子(PID)によってアプリケーションプロセスにアクセスし、その中にDLLを実装します。



 int access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ; IntPtr procHandle = OpenProcess(access, false, dwProcessId); InjectDll(procHandle, BootstrapDllPath);
      
      





私たち自身が子プロセスを起動した場合、このために管理者権限さえ必要ありません。 添付する場合は、権利に注意する必要があります。



 static Process AttachToTargetProcess(RunnerParameters parameters) { if (!String.IsNullOrEmpty(parameters.TargetProcessCommandLine)) return StartTargetProcess(parameters.TargetProcessCommandLine, parameters.TargetProcessArgs); else if (parameters.Pid != 0) { Process.EnterDebugMode(); return Process.GetProcessById(parameters.Pid); } else return null; }
      
      





アプリケーションマニフェストでは:



 <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
      
      





次に、 LoadLibraryW関数のアドレスを見つけて、外部プロセスで呼び出して、ロードするDLLの名前を示します。 プロセスで関数のアドレスを取得し、他の誰かのアドレスを呼び出します。 これは、kernel32.dllライブラリがすべてのプロセスで同じベースアドレスを持っているため機能します。 これが一度変更されたとしても(これはほとんどありません)、異なるベースアドレスの場合に問題を解決する方法がさらに示されます。



InjectDllおよびMakeRemoteCallコード
 static bool InjectDll(IntPtr procHandle, string dllName) { const string libName = "kernel32.dll"; const string procName = "LoadLibraryW"; IntPtr loadLibraryAddr = GetProcAddress(GetModuleHandle(libName), procName); if (loadLibraryAddr == IntPtr.Zero) { return false; } return MakeRemoteCall(procHandle, loadLibraryAddr, dllName); } static bool MakeRemoteCall(IntPtr procHandle, IntPtr methodAddr, string argument) { uint textSize = (uint)Encoding.Unicode.GetByteCount(argument); uint allocSize = textSize + 2; IntPtr allocMemAddress; AllocationType allocType = AllocationType.Commit | AllocationType.Reserve; allocMemAddress = VirtualAllocEx(procHandle, IntPtr.Zero, allocSize, allocType, PAGE_READWRITE); if (allocMemAddress == IntPtr.Zero) return false; UIntPtr bytesWritten; WriteProcessMemory(procHandle, allocMemAddress, Encoding.Unicode.GetBytes(argument), textSize, out bytesWritten); bool isOk = false; IntPtr threadHandle; threadHandle = CreateRemoteThread(procHandle, IntPtr.Zero, 0, methodAddr, allocMemAddress, 0, IntPtr.Zero); if (threadHandle != IntPtr.Zero) { WaitForSingleObject(threadHandle, Win32.INFINITE); isOk = true; } VirtualFreeEx(procHandle, allocMemAddress, allocSize, AllocationType.Release); if (threadHandle != IntPtr.Zero) Win32.CloseHandle(threadHandle); return isOk; }
      
      





ここにはどんな錫が書かれていますか? 外部プロセスのLoadLibraryW呼び出しに文字列パラメーターを渡す必要があります。 これを行うには、別のプロセスのアドレス空間に行を書き込みます。これはVirtualAllocWriteProcessMemoryが行うことです 。 次に、外部プロセスでスレッドを作成します。これは、先ほど書いたパラメーターでLoadLibraryWを実行するアドレスです。 スレッドの完了を待っており、メモリを消去します。





ただし、残念ながら、この技術は通常のDLLにのみ適用可能であり、アセンブリを管理しています。 レーピンの絵「帆走」!



事実、マネージアセンブリにはDllMainのようなエントリポイントがないため、通常のDLLとしてプロセスに挿入しても、アセンブリは自動的に制御を取得できません。



制御を手動で転送することは可能ですか? 理論的には、 モジュール初期化子を使用するか、マネージアセンブリから関数をエクスポートして呼び出すという2つの方法があります。 標準のC#を使用すると、どちらも実行できないことを意味します。 モジュール初期化子は、たとえばModuleInit.Fodyを使用して固定できますが、問題はモジュール初期化子が単独で実行されないため、最初にアセンブリの特定の型を使用する必要があることです。 猫のマトロスキンがかつて言ったように:「不必要なものを売るには、まず不必要なものを買わなければなりませんが、お金はありません!」



エクスポートについては、理論的にはUnmanagedExportsがありますが、会議がありませんでした。 マネージアセンブリの2つのマネージバージョン(AnyCPUはサポートされていません)を構築する必要がありました。



この方向で私たちを照らすものは何もないようです。 そして、電気テープで包んだら? そして、アンマネージDLLをプロセスに導入し、それからマネージアセンブリを既に呼び出そうとしているのですか?



それはあなたができることが判明
 HRESULT InjectDotNetAssembly( /* [in] */ LPCWSTR pwzAssemblyPath, /* [in] */ LPCWSTR pwzTypeName, /* [in] */ LPCWSTR pwzMethodName, /* [in] */ LPCWSTR pwzArgument ) { HRESULT result; ICLRMetaHost *metaHost = NULL; ICLRRuntimeInfo *runtimeInfo = NULL; ICLRRuntimeHost *runtimeHost = NULL; // Load .NET result = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&metaHost)); result = metaHost->GetRuntime(L"v4.0.30319", IID_PPV_ARGS(&runtimeInfo)); result = runtimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&runtimeHost)); result = runtimeHost->Start(); // Execute managed assembly DWORD returnValue; result = runtimeHost->ExecuteInDefaultAppDomain( pwzAssemblyPath, pwzTypeName, pwzMethodName, pwzArgument, &returnValue); if (metaHost != NULL) metaHost->Release(); if (runtimeInfo != NULL) runtimeInfo->Release(); if (runtimeHost != NULL) runtimeHost->Release(); return result; }
      
      







それほど怖くはなく、デフォルトでAppDomainを呼び出すことさえ約束されているようです。 ただし、どのスレッドにあるのかは明確ではありませんが、ありがとうございます。



次に、ブートローダーからこのコードを呼び出す必要があります。



DLLがロードされるアドレスからの関数アドレスのオフセットは、どのプロセスでも一定であるという仮定を使用します。



LoadLibraryを使用して必要なDLLをプロセスにロードし、ベースアドレスを取得します。 GetProcAddressを通じて、呼び出された関数のアドレスを見つけます。



 static long GetMethodOffset(string dllPath, string methodName) { IntPtr hLib = Win32.LoadLibrary(dllPath); if (hLib == IntPtr.Zero) return 0; IntPtr call = Win32.GetProcAddress(hLib, methodName); if (call == IntPtr.Zero) return 0; long result = call.ToInt64() - hLib.ToInt64(); Win32.FreeLibrary(hLib); return result; }
      
      





パズルの最後のピースは残り、別のプロセスでDLLのベースアドレスを見つけます。



 static ulong GetRemoteModuleHandle(Process process, string moduleName) { int count = process.Modules.Count; for (int i = 0; i < count; i++) { ProcessModule module = process.Modules[i]; if (module.ModuleName == moduleName) return (ulong)module.BaseAddress; } return 0; }
      
      





最後に、別のプロセスで目的の関数のアドレスを取得します。



 long offset = GetMethodOffset(BootstrapDllPath, "InjectManagedAssembly"); InjectDll(procHandle, BootstrapDllPath); ulong baseAddr = GetRemoteModuleHandle(process, Path.GetFileName(BootstrapDllPath)); IntPtr remoteAddress = new IntPtr((long)(baseAddr + (ulong)offset));
      
      





MakeRemoteCall(上記参照)を介して、別のプロセスでLoadLibraryを呼び出したのと同じ方法で、受信したアドレスに呼び出しを行います。



不便なのは、1行しか転送できないことと、4つも必要なマネージアセンブリを呼び出すことです。ホイールを再発明しないために、コマンドラインとしてラインを形成し、アンマネージ側ではノイズやほこりのないCommandLineToArgvWシステム関数を使用します



 HRESULT InjectManagedAssemblyCore(_In_ LPCWSTR lpCommand) { LPWSTR *szArgList; int argCount; szArgList = CommandLineToArgvW(lpCommand, &argCount); if (szArgList == NULL || argCount < 3) return E_FAIL; LPCWSTR param; if (argCount >= 4) param = szArgList[3]; else param = L""; HRESULT result = InjectDotNetAssembly( szArgList[0], szArgList[1], szArgList[2], param ); LocalFree(szArgList); return result; }
      
      





また、関数のオフセットの再計算では、ブートローダーのビットとターゲットアプリケーションが厳密に同じであると暗黙的に仮定していることに注意してください。 すなわち ビットネスからは何も得られません。ブートローダーの2つのバリアント(32ビットと64ビット)とアンマネージDLLの2つのバリアントの両方を実行する必要があります( 正しいビット数のDLL のみがプロセスにロードできるためです)。



したがって、64ビットOSで作業する場合、プロセスのビット数の一致のチェックを追加します。 独自のプロセス:



 Environment.Is64BitProcess
      
      





エイリアンプロセス:



 [DllImport("kernel32.dll", CallingConvention = CallingConvention.Winapi)] [return: MarshalAs(UnmanagedType.Bool)] static extern bool IsWow64Process([In] IntPtr process, [Out] out bool wow64Process); public static bool Is64BitProcess(Process process) { bool isWow64; if (!IsWow64Process(process.Handle, out isWow64)) { return false; } return !isWow64; } static bool IsCompatibleProcess(Process process) { if (!Environment.Is64BitOperatingSystem) return true; bool is64bitProcess = Is64BitProcess(process); return Environment.Is64BitProcess == is64bitProcess; }
      
      





以下を示すMessageBoxを使用して、マネージアセンブリを作成します。



 public static int RunWinForms(string arg) { InitLogifyWinForms(); } static void InitLogifyWinForms() { MessageBox.Show("InitLogifyWinForms"); }
      
      





チェックし、すべてが呼び出され、MessageBoxが表示されます。 ほら!







MessageBoxをクラッシュレポーターのトライアル初期化に置き換えます。



 static void InitLogifyWinForms() { try { LogifyAlert client = LogifyAlert.Instance; client.ApiKey = "my-api-key"; client.StartExceptionsHandling(); } catch (Exception ex) { } }
      
      





ボタンがクリックされたときに例外をスローするテストWinFormsアプリケーションを作成しています。



 void button2_Click(object sender, EventArgs e) { object o = null; o.ToString(); }
      
      





すべてのようです。 立ち上げ、確認...そして沈黙。 そして道に沿って、三つ編みの死者が立っています。



クラッシュレポーターコードをテストアプリケーションに直接貼り付け、参照を追加します。



 static void Main() { InitLogifyWinForms(); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); }
      
      





私たちはチェックします-動作するので、重要なのは初期化コードではありません。 スレッドに何か問題があるのでしょうか? 変更:



 static void Main() { Thread thread = new Thread(InitLogifyWinForms); thread.Start(); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); }
      
      





確認してください、再び動作します。 何が問題なの?! この質問に対する答えはありません。 AppDomain.UnhandledExceptionイベントのこの動作の理由を他の誰かが明らかにすることができます。 それにもかかわらず、私は回避策を見つけました。 アプリケーションに少なくとも1つのウィンドウが表示されるのを待って、このウィンドウのメッセージキューを介してBeginInvokeを実行します。



回避策、18 +
 public static int RunWinForms(string arg) { bool isOk = false; try { const int totalTimeout = 5000; const int smallTimeout = 1000; int count = totalTimeout / smallTimeout; for (int i = 0; i < count; i++) { if (Application.OpenForms == null || Application.OpenForms.Count <= 0) Thread.Sleep(smallTimeout); else { Delegate call = new InvokeDelegate(InitLogifyWinForms); Application.OpenForms[0].BeginInvoke(call); isOk = true; break; } } if (!isOk) { InitLogifyWinForms(); } return 0; } catch { return 1; } }
      
      









そして、見よ、それは巻きついた。 深刻なマイナスに注意してください。コンソールアプリケーションでは動作しません。



磨きをかけ、クラッシュレポーターに独自の構成ファイルから構成するように教えます。 非常にトリッキーですが、それは本物であることがわかりました:



 ExeConfigurationFileMap map = new ExeConfigurationFileMap(); map.ExeConfigFilename = configFileName; Configuration config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);
      
      







設定を書いています
 <?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="logifyAlert" type="DevExpress.Logify.LogifyConfigSection, Logify.Alert.Win"/> </configSections> <logifyAlert> <collectBreadcrumbs value="1" /> <breadcrumbsMaxCount value="500" /> <apiKey value="my-api-key"/> <confirmSend value="false"/> <offlineReportsEnabled value="false"/> <offlineReportsDirectory value="offlineReports"/> <offlineReportsCount value="10"/> </logifyAlert> </configuration>
      
      









アプリケーションexe-shnikの隣に配置します。 実行、確認、おっと。







どっち? 必要なアセンブリは既にプロセスにロードされており、何らかの理由でランタイムは新しい方法でそれを探すことにしました。 アセンブリの完全な名前を使用しようとしても、同様に成功します。



正直なところ、私はこの(私見、まったく論理的ではない)動作の理由を調査し始めませんでした。 問題を回避するには2つの方法があります。AppDomain.AssemblyResolveにサブスクライブし、アセンブリが配置されているシステムを表示します。 または、必要なアセンブリをexe-shnikを使用してディレクトリにコピーするだけです。 AppDomain.UnhandledExceptionの奇妙な振る舞いに気をつけて、私はリスクを冒さず、アセンブリをコピーしました。



再構築してみてください。 クラッシュレポートを正常に構成して送信します。







次に、ルーチン、CLIインターフェースをブートローダーに接続し、一般的にプロジェクトをコーミングします。



CLI
LogifyRunner (C) 2017 DevExpress Inc.



Usage:



LogifyRunner.exe [--win] [--wpf] [--pid=value>] [--exec=value1, ...]



--win Target process is WinForms application

--wpf Target process is WPF application

--pid Target process ID. Runner will be attached to process with specified ID.

--exec Target process command line



NOTE: logify.config file should be placed to the same directory where the target process executable or LogifyRunner.exe is located.

Read more about config file format at: https://github.com/DevExpress/Logify.Alert.Clients/tree/develop/dotnet/Logify.Alert.Win#configuration









これで、テクニカルサポートスペシャリストの兵器庫に、アプリケーションのクラッシュレポート(およびクラッシュに先行するユーザーアクション )を取得できるシンプルでオークベースのツールがあります。このツールでは、クラッシュレポーターは元々組み込まれていませんでした。



PS:



githubのソース。

興味のある方はプロジェクトのウェブサイトドキュメントを ご覧ください 。 こちらのHabréのLogifyに関する紹介記事もご覧ください。



All Articles