diff --git a/src/Plugins/RunawayProcessKiller/NativeMethods.cs b/src/Plugins/RunawayProcessKiller/NativeMethods.cs new file mode 100644 index 0000000..3b92382 --- /dev/null +++ b/src/Plugins/RunawayProcessKiller/NativeMethods.cs @@ -0,0 +1,180 @@ +using System; +using System.Runtime.InteropServices; + +namespace winsw.Plugins.RunawayProcessKiller +{ + public partial class RunawayProcessKillerExtension + { + internal static class NativeMethods + { + private const string Kernel32 = "kernel32.dll"; + private const string NTDll = "ntdll.dll"; + + [DllImport(Kernel32)] + internal static extern int IsWow64Process(IntPtr hProcess, out int Wow64Process); + + [DllImport(NTDll)] + internal static extern int NtQueryInformationProcess( + IntPtr ProcessHandle, + PROCESSINFOCLASS ProcessInformationClass, + out PROCESS_BASIC_INFORMATION32 ProcessInformation, + int ProcessInformationLength, + IntPtr ReturnLength = default); + + [DllImport(NTDll)] + internal static extern int NtQueryInformationProcess( + IntPtr ProcessHandle, + PROCESSINFOCLASS ProcessInformationClass, + out PROCESS_BASIC_INFORMATION64 ProcessInformation, + int ProcessInformationLength, + IntPtr ReturnLength = default); + + [DllImport(NTDll)] + internal static extern unsafe int NtReadVirtualMemory( + IntPtr ProcessHandle, + IntPtr BaseAddress, + void* Buffer, + IntPtr BufferSize, + IntPtr NumberOfBytesRead = default); + + [DllImport(NTDll)] + internal static extern int NtWow64QueryInformationProcess64( + IntPtr ProcessHandle, + PROCESSINFOCLASS ProcessInformationClass, + out PROCESS_BASIC_INFORMATION64 ProcessInformation, + int ProcessInformationLength, + IntPtr ReturnLength = default); + + [DllImport(NTDll)] + internal static extern unsafe int NtWow64ReadVirtualMemory64( + IntPtr ProcessHandle, + long BaseAddress, + void* Buffer, + long BufferSize, + long NumberOfBytesRead = default); + + internal enum PROCESSINFOCLASS + { + ProcessBasicInformation = 0, + } + + [StructLayout(LayoutKind.Sequential)] + internal readonly struct MEMORY_BASIC_INFORMATION + { + public readonly IntPtr BaseAddress; + private readonly IntPtr AllocationBase; + private readonly uint AllocationProtect; + public readonly IntPtr RegionSize; + private readonly uint State; + private readonly uint Protect; + private readonly uint Type; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct PROCESS_BASIC_INFORMATION32 + { + private readonly int Reserved1; + public readonly int PebBaseAddress; + private fixed int Reserved2[2]; + private readonly uint UniqueProcessId; + private readonly int Reserved3; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct PROCESS_BASIC_INFORMATION64 + { + private readonly long Reserved1; + public readonly long PebBaseAddress; + private fixed long Reserved2[2]; + private readonly ulong UniqueProcessId; + private readonly long Reserved3; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct PEB32 + { + private fixed byte Reserved1[2]; + private readonly byte BeingDebugged; + private fixed byte Reserved2[1]; + private fixed int Reserved3[2]; + private readonly int Ldr; + public readonly int ProcessParameters; + private fixed int Reserved4[3]; + private readonly int AtlThunkSListPtr; + private readonly int Reserved5; + private readonly uint Reserved6; + private readonly int Reserved7; + private readonly uint Reserved8; + private readonly uint AtlThunkSListPtr32; + private fixed int Reserved9[45]; + private fixed byte Reserved10[96]; + private readonly int PostProcessInitRoutine; + private fixed byte Reserved11[128]; + private fixed int Reserved12[1]; + private readonly uint SessionId; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct PEB64 + { + private fixed byte Reserved1[2]; + private readonly byte BeingDebugged; + private fixed byte Reserved2[1]; + private fixed long Reserved3[2]; + private readonly long Ldr; + public readonly long ProcessParameters; + private fixed long Reserved4[3]; + private readonly long AtlThunkSListPtr; + private readonly long Reserved5; + private readonly uint Reserved6; + private readonly long Reserved7; + private readonly uint Reserved8; + private readonly uint AtlThunkSListPtr32; + private fixed long Reserved9[45]; + private fixed byte Reserved10[96]; + private readonly long PostProcessInitRoutine; + private fixed byte Reserved11[128]; + private fixed long Reserved12[1]; + private readonly uint SessionId; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct RTL_USER_PROCESS_PARAMETERS32 + { + private fixed byte Reserved1[16]; + private fixed int Reserved2[10]; + private readonly UNICODE_STRING32 ImagePathName; + private readonly UNICODE_STRING32 CommandLine; + + internal readonly int Environment; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct RTL_USER_PROCESS_PARAMETERS64 + { + private fixed byte Reserved1[16]; + private fixed long Reserved2[10]; + private readonly UNICODE_STRING64 ImagePathName; + private readonly UNICODE_STRING64 CommandLine; + + internal readonly long Environment; + } + + [StructLayout(LayoutKind.Sequential)] + internal readonly struct UNICODE_STRING32 + { + private readonly ushort Length; + private readonly ushort MaximumLength; + private readonly int Buffer; + } + + [StructLayout(LayoutKind.Sequential)] + internal readonly struct UNICODE_STRING64 + { + private readonly ushort Length; + private readonly ushort MaximumLength; + private readonly long Buffer; + } + } + } +} diff --git a/src/Plugins/RunawayProcessKiller/RunawayProcessKiller.csproj b/src/Plugins/RunawayProcessKiller/RunawayProcessKiller.csproj index 7618eac..0c03e9c 100644 --- a/src/Plugins/RunawayProcessKiller/RunawayProcessKiller.csproj +++ b/src/Plugins/RunawayProcessKiller/RunawayProcessKiller.csproj @@ -4,6 +4,7 @@ net20;net40;net461;netcoreapp3.1 latest enable + true winsw.Plugins.RunawayProcessKiller true diff --git a/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs b/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs index 9906c62..a842bd4 100644 --- a/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs +++ b/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs @@ -7,10 +7,11 @@ using System.Xml; using log4net; using winsw.Extensions; using winsw.Util; +using static winsw.Plugins.RunawayProcessKiller.RunawayProcessKillerExtension.NativeMethods; namespace winsw.Plugins.RunawayProcessKiller { - public class RunawayProcessKillerExtension : AbstractWinSWExtension + public partial class RunawayProcessKillerExtension : AbstractWinSWExtension { /// /// Absolute path to the PID file, which stores ID of the previously launched process. @@ -57,6 +58,127 @@ namespace winsw.Plugins.RunawayProcessKiller this.CheckWinSWEnvironmentVariable = checkWinSWEnvironmentVariable; } + private static unsafe string? ReadEnvironmentVariable(IntPtr processHandle, string variable) + { + if (Environment.Is64BitOperatingSystem) + { + if (Environment.Is64BitProcess) + { + return SearchEnvironmentVariable( + processHandle, + variable, + GetEnvironmentAddress64(processHandle).ToInt64(), + (handle, address, buffer, size) => NtReadVirtualMemory(handle, new IntPtr(address), buffer, new IntPtr(size))); + } + + if (IsWow64Process(processHandle, out int isWow64) == 0 || isWow64 == 0) + { + return SearchEnvironmentVariable( + processHandle, + variable, + GetEnvironmentAddressWow64(processHandle), + (handle, address, buffer, size) => NtWow64ReadVirtualMemory64(handle, address, buffer, size)); + } + } + + return SearchEnvironmentVariable( + processHandle, + variable, + GetEnvironmentAddress32(processHandle).ToInt64(), + (handle, address, buffer, size) => NtReadVirtualMemory(handle, new IntPtr(address), buffer, new IntPtr(size))); + } + + private unsafe delegate int ReadMemoryCallback(IntPtr processHandle, long baseAddress, void* buffer, int bufferSize); + + private static unsafe string? SearchEnvironmentVariable(IntPtr processHandle, string variable, long address, ReadMemoryCallback reader) + { + const int BaseBufferSize = 0x1000; + string variableKey = '\0' + variable + '='; + string buffer = new string('\0', BaseBufferSize + variableKey.Length); + fixed (char* bufferPtr = buffer) + { + long startAddress = address; + for (; ; ) + { + int status = reader(processHandle, address, bufferPtr, buffer.Length * sizeof(char)); + int index = buffer.IndexOf("\0\0"); + if (index >= 0) + { + break; + } + + address += BaseBufferSize * sizeof(char); + } + + for (; ; ) + { + int variableIndex = buffer.IndexOf(variableKey); + if (variableIndex >= 0) + { + int valueStartIndex = variableIndex + variableKey.Length; + int valueEndIndex = buffer.IndexOf('\0', valueStartIndex); + string value = buffer.Substring(valueStartIndex, valueEndIndex - valueStartIndex); + return value; + } + + address -= BaseBufferSize * sizeof(char); + if (address < startAddress) + { + break; + } + + int status = reader(processHandle, address, bufferPtr, buffer.Length * sizeof(char)); + } + } + + return null; + } + + private static unsafe IntPtr GetEnvironmentAddress32(IntPtr processHandle) + { + _ = NtQueryInformationProcess( + processHandle, + PROCESSINFOCLASS.ProcessBasicInformation, + out PROCESS_BASIC_INFORMATION32 information, + sizeof(PROCESS_BASIC_INFORMATION32)); + + PEB32 peb; + _ = NtReadVirtualMemory(processHandle, new IntPtr(information.PebBaseAddress), &peb, new IntPtr(sizeof(PEB32))); + RTL_USER_PROCESS_PARAMETERS32 parameters; + _ = NtReadVirtualMemory(processHandle, new IntPtr(peb.ProcessParameters), ¶meters, new IntPtr(sizeof(RTL_USER_PROCESS_PARAMETERS32))); + return new IntPtr(parameters.Environment); + } + + private static unsafe IntPtr GetEnvironmentAddress64(IntPtr processHandle) + { + _ = NtQueryInformationProcess( + processHandle, + PROCESSINFOCLASS.ProcessBasicInformation, + out PROCESS_BASIC_INFORMATION64 information, + sizeof(PROCESS_BASIC_INFORMATION64)); + + PEB64 peb; + _ = NtReadVirtualMemory(processHandle, new IntPtr(information.PebBaseAddress), &peb, new IntPtr(sizeof(PEB64))); + RTL_USER_PROCESS_PARAMETERS64 parameters; + _ = NtReadVirtualMemory(processHandle, new IntPtr(peb.ProcessParameters), ¶meters, new IntPtr(sizeof(RTL_USER_PROCESS_PARAMETERS64))); + return new IntPtr(parameters.Environment); + } + + private static unsafe long GetEnvironmentAddressWow64(IntPtr processHandle) + { + _ = NtWow64QueryInformationProcess64( + processHandle, + PROCESSINFOCLASS.ProcessBasicInformation, + out PROCESS_BASIC_INFORMATION64 information, + sizeof(PROCESS_BASIC_INFORMATION64)); + + PEB64 peb; + _ = NtWow64ReadVirtualMemory64(processHandle, information.PebBaseAddress, &peb, sizeof(PEB64)); + RTL_USER_PROCESS_PARAMETERS64 parameters; + _ = NtWow64ReadVirtualMemory64(processHandle, peb.ProcessParameters, ¶meters, sizeof(RTL_USER_PROCESS_PARAMETERS64)); + return parameters.Environment; + } + public override void Configure(ServiceDescriptor descriptor, XmlNode node) { // We expect the upper logic to process any errors @@ -121,35 +243,15 @@ namespace winsw.Plugins.RunawayProcessKiller } // Ensure the process references the service - string? affiliatedServiceId; - // TODO: This method is not ideal since it works only for vars explicitly mentioned in the start info - // No Windows 10- compatible solution for EnvVars retrieval, see https://blog.gapotchenko.com/eazfuscator.net/reading-environment-variables - StringDictionary previousProcessEnvVars = proc.StartInfo.EnvironmentVariables; string expectedEnvVarName = WinSWSystem.ENVVAR_NAME_SERVICE_ID; - if (previousProcessEnvVars.ContainsKey(expectedEnvVarName)) - { - // StringDictionary is case-insensitive, hence it will fetch variable definitions in any case - affiliatedServiceId = previousProcessEnvVars[expectedEnvVarName]; - } - else if (CheckWinSWEnvironmentVariable) + string? affiliatedServiceId = ReadEnvironmentVariable(proc.Handle, expectedEnvVarName); + if (affiliatedServiceId is null && CheckWinSWEnvironmentVariable) { Logger.Warn("The process " + pid + " has no " + expectedEnvVarName + " environment variable defined. " + "The process has not been started by WinSW, hence it won't be terminated."); - if (Logger.IsDebugEnabled) - { - // TODO replace by String.Join() in .NET 4 - string[] keys = new string[previousProcessEnvVars.Count]; - previousProcessEnvVars.Keys.CopyTo(keys, 0); - Logger.DebugFormat("Env vars of the process with PID={0}: {1}", new object[] { pid, string.Join(",", keys) }); - } return; } - else - { - // We just skip this check - affiliatedServiceId = null; - } // Check the service ID value if (CheckWinSWEnvironmentVariable && !ServiceId.Equals(affiliatedServiceId)) diff --git a/src/Test/winswTests/Extensions/RunawayProcessKillerTest.cs b/src/Test/winswTests/Extensions/RunawayProcessKillerTest.cs index 7b67b32..3bfa09d 100644 --- a/src/Test/winswTests/Extensions/RunawayProcessKillerTest.cs +++ b/src/Test/winswTests/Extensions/RunawayProcessKillerTest.cs @@ -65,7 +65,6 @@ namespace winswTests.Extensions } [Test] - [Ignore(nameof(RunawayProcessKillerExtension) + "isn't working.")] public void ShouldKillTheSpawnedProcess() { var winswId = "myAppWithRunaway";