/// <summary>
        ///   Releases remote resources used by a previously injected module.
        /// </summary>
        /// <param name="state">Injection state struct.</param>
        internal static void Eject(InjectionState state)
        {
            // get the FreeLibrary function address
            IntPtr kernelModule   = Kernel32.LoadLibrary(nameof(Kernel32));
            IntPtr freeLibraryPtr = Kernel32.GetProcAddress(kernelModule, "FreeLibrary");

            Kernel32.FreeLibrary(kernelModule);

            // create remote thread for loading the library
            Log.Debug("ejecting remote library");
            IntPtr threadHandle = Kernel32.CreateRemoteThread(state.RemoteProcessHandle,
                                                              IntPtr.Zero,
                                                              0,
                                                              freeLibraryPtr,
                                                              state.RemoteModuleHandle,
                                                              0,
                                                              out _);

            if (threadHandle == IntPtr.Zero)
            {
                int lastError = Marshal.GetLastWin32Error();
                Kernel32.CloseHandle(state.RemoteProcessHandle);
                throw new Win32Exception(lastError);
            }

            if (Kernel32.GetExitCodeThread(threadHandle, out int exitCode))
            {
                if (exitCode == 1)
                {
                    Log.Info("library ejection succeeded!");
                }
                else
                {
                    Log.Warn("could not eject remote library!");
                }
            }
            else
            {
                Log.Warn("library ejection status unknown");
            }

            Log.Debug("closing remote process handle");
            Kernel32.CloseHandle(state.RemoteProcessHandle);
        }
        /// <summary>
        ///   Injects a library to the target process.
        /// </summary>
        /// <remarks>
        ///   TODO: perform injection in a secondary thread so we don't block the application message loop
        /// </remarks>
        /// <param name="pid">Process ID</param>
        /// <param name="path32">32-bit library path</param>
        /// <param name="path64">64-bit library path</param>
        /// <returns>An struct containing the injection state.</returns>
        internal static InjectionState Inject(int pid, string path32, string path64)
        {
            // TODO: only set actually needed permissions and retry with more extensive ones if access gets denied
            //       in one of the following calls
            const Kernel32.ProcessAccessRights accessRights = Kernel32.ProcessAccessRights.PROCESS_CREATE_THREAD |
                                                              Kernel32.ProcessAccessRights.PROCESS_QUERY_INFORMATION |
                                                              Kernel32.ProcessAccessRights.PROCESS_VM_OPERATION |
                                                              Kernel32.ProcessAccessRights.PROCESS_VM_WRITE |
                                                              Kernel32.ProcessAccessRights.PROCESS_VM_READ;

            // create injection state struct
            var state = new InjectionState();

            // open process handle
            IntPtr processHandle = Kernel32.OpenProcess(accessRights, false, pid);

            if (processHandle == IntPtr.Zero)
            {
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }

            state.RemoteProcessHandle = processHandle;
            Log.Debug($"handle for process {pid} opened successfully");

            // try to guess if this is a 32- or 64-bit process
            if (!Kernel32.IsWow64Process(processHandle, out bool isWowProcess))
            {
                Log.Warn("could not guess remote process architecture - trying with 32-bit library");
            }

            // get path for the target process architecture
            string path             = isWowProcess ? path32 : Environment.Is64BitOperatingSystem ? path64 : path32;
            int    pathBufferLength = 2 * path.Length + 1; // LoadLibraryW is Unicode

            Log.Debug($"library to be injected: {path}");

            // allocate buffer for library path, in the remote virtual address space
            IntPtr remotePathBuffer = Kernel32.VirtualAllocEx(processHandle,
                                                              IntPtr.Zero,
                                                              pathBufferLength,
                                                              Kernel32.AllocationType.MEM_COMMIT,
                                                              Kernel32.MemoryProtectionFlags.PAGE_READWRITE);

            if (remotePathBuffer == IntPtr.Zero)
            {
                int lastError = Marshal.GetLastWin32Error();
                Kernel32.CloseHandle(processHandle);
                throw new Win32Exception(lastError);
            }

            Log.Debug($"allocated {pathBufferLength} bytes in remote process memory (0x{remotePathBuffer.ToInt64():x8})");

            // allocate local string buffer and write to the remote process memory
            IntPtr localPathBuffer = Marshal.StringToHGlobalUni(path);

            if (!Kernel32.WriteProcessMemory(processHandle, remotePathBuffer, localPathBuffer, pathBufferLength, out _))
            {
                int lastError = Marshal.GetLastWin32Error();
                Kernel32.VirtualFreeEx(processHandle, remotePathBuffer, pathBufferLength, Kernel32.FreeType.MEM_DECOMMIT);
                Marshal.FreeHGlobal(localPathBuffer);
                Kernel32.CloseHandle(processHandle);
                throw new Win32Exception(lastError);
            }

            Log.Debug($"wrote {pathBufferLength} bytes to remote process memory");
            Marshal.FreeHGlobal(localPathBuffer);

            // get the LoadLibraryW function address
            IntPtr kernelModule   = Kernel32.LoadLibrary(nameof(Kernel32));
            IntPtr loadLibraryPtr = Kernel32.GetProcAddress(kernelModule, "LoadLibraryW");

            Kernel32.FreeLibrary(kernelModule);

            // create remote thread for loading the library
            IntPtr threadHandle = Kernel32.CreateRemoteThread(processHandle,
                                                              IntPtr.Zero,
                                                              0,
                                                              loadLibraryPtr,
                                                              remotePathBuffer,
                                                              0,
                                                              out int threadId);

            if (threadHandle == IntPtr.Zero)
            {
                int lastError = Marshal.GetLastWin32Error();
                Kernel32.VirtualFreeEx(processHandle, remotePathBuffer, pathBufferLength, Kernel32.FreeType.MEM_DECOMMIT);
                Kernel32.CloseHandle(processHandle);
                throw new Win32Exception(lastError);
            }

            Log.Debug($"waiting for library load thread with ID {threadId} to terminate");
            Kernel32.WaitForSingleObject(threadHandle, unchecked ((int)Kernel32.Infinite));
            Log.Debug("library load thread terminated - retrieving module handle");

            // on 64-bit platforms, handles do not fit in a DWORD value, so the module handle gets truncated. We'll use some
            // Toolhelp utilities to retrieve the list of loaded modules so we can release our library later in Eject()
            // TODO: debug this further?
            try {
                state.RemoteModuleHandle = Process.GetProcessById(pid)
                                           .Modules.Cast <ProcessModule>()
                                           .First(m => m.FileName == path)
                                           .BaseAddress;
            } catch {
                Log.Warn("could not retrieve remote module handle!");
            }

            Log.Debug("relasing resources");
            Kernel32.CloseHandle(threadHandle);
            Kernel32.VirtualFreeEx(processHandle, remotePathBuffer, pathBufferLength, Kernel32.FreeType.MEM_DECOMMIT);
            return(state);
        }