Skip to content

Commit d5c6311

Browse files
committed
Add detection of process modules
1 parent 1c42e30 commit d5c6311

8 files changed

Lines changed: 118 additions & 57 deletions

File tree

Directory.Build.props

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@
66
<BaseIntermediateOutputPath>$(BuildFolder)obj\$(MSBuildProjectName)\$(Configuration)\</BaseIntermediateOutputPath>
77
<TargetFramework>net472</TargetFramework>
88
<LangVersion>latest</LangVersion>
9-
<Platforms>x64</Platforms>
9+
<Platforms>AnyCpu</Platforms>
1010
<ImplicitUsings>enable</ImplicitUsings>
1111
<Nullable>enable</Nullable>
1212
<WarningLevel>5</WarningLevel>
1313
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
1414
<Version>0.0-dev</Version>
15-
<NoWarn>$(NoWarn);IL2026;IL2104</NoWarn>
1615
</PropertyGroup>
1716
</Project>

Handle2.sln

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Handle2", "src\Handle2\Hand
2020
EndProject
2121
Global
2222
GlobalSection(SolutionConfigurationPlatforms) = preSolution
23-
Debug|x64 = Debug|x64
24-
Debug|x86 = Debug|x86
25-
Release|x64 = Release|x64
26-
Release|x86 = Release|x86
23+
Debug|Any CPU = Debug|Any CPU
24+
Release|Any CPU = Release|Any CPU
2725
EndGlobalSection
2826
GlobalSection(ProjectConfigurationPlatforms) = postSolution
29-
{D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Debug|x64.ActiveCfg = Debug|x64
30-
{D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Debug|x64.Build.0 = Debug|x64
31-
{D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Debug|x86.ActiveCfg = Debug|x64
32-
{D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Debug|x86.Build.0 = Debug|x64
33-
{D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Release|x64.ActiveCfg = Release|x64
34-
{D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Release|x64.Build.0 = Release|x64
35-
{D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Release|x86.ActiveCfg = Release|x64
36-
{D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Release|x86.Build.0 = Release|x64
37-
{FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Debug|x64.ActiveCfg = Debug|x64
38-
{FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Debug|x64.Build.0 = Debug|x64
39-
{FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Debug|x86.ActiveCfg = Debug|x64
40-
{FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Debug|x86.Build.0 = Debug|x64
41-
{FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Release|x64.ActiveCfg = Release|x64
42-
{FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Release|x64.Build.0 = Release|x64
43-
{FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Release|x86.ActiveCfg = Release|x64
44-
{FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Release|x86.Build.0 = Release|x64
27+
{D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28+
{D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
29+
{D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
30+
{D57C5C4B-3FE1-415E-92F8-A345DF5679AC}.Release|Any CPU.Build.0 = Release|Any CPU
31+
{FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32+
{FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Debug|Any CPU.Build.0 = Debug|Any CPU
33+
{FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Release|Any CPU.ActiveCfg = Release|Any CPU
34+
{FABFDFC5-D060-4A71-B0E7-DC9D339F0856}.Release|Any CPU.Build.0 = Release|Any CPU
4535
EndGlobalSection
4636
GlobalSection(SolutionProperties) = preSolution
4737
HideSolutionNode = FALSE

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
An open-source alternative to the [Sysinternals Handle](https://learn.microsoft.com/en-us/sysinternals/downloads/handle)<tr>
44
* Identifies processes that are locking specific files or folders
5+
* Include process modules in the search
6+
* Note: `Sysinternals Handle` doesn't include process modules in the search. Thus, it can fail to find the locking process.
57
* Shows information about all handles in the system
68
* Supports JSON output
79
* Full Unicode support
810
* Note: `Sysinternals Handle` doesn't support Unicode file names ([more details](https://superuser.com/questions/1761951/sysinternals-handle-prints-question-marks-instead-of-non-ascii-symbols))
911

1012
## System requirements
1113

12-
* Windows 7 x64 and higher.
14+
* Windows 7 and higher.
1315

1416
## Usage
1517

@@ -30,18 +32,17 @@ Examples:
3032
3133
Command-line options:
3234
33-
--json (Default: false) Json output
35+
--json (Default: false) JSON output. For details on the meanings of the fields provided, please consult
36+
the HandleInfo and SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX structures in the source code.
3437
35-
--path Required. (Default: false) path
38+
--path Required. Displays the processes locking the path
3639
37-
--dump-all-handles Required. (Default: false) Displays information about all system handles.
40+
--dump-all-handles Required. (Default: false) Displays information about all system handles and modules
3841
3942
--help Display this help screen.
4043
4144
--version Display version information.
4245
```
4346

4447
## How to build
45-
46-
* To work with the codebase, `Visual Studio 2022` can be used.
47-
* To build the project, run `build.ps` script (`git.exe` should be in the PATH)
48+
To build the project, run `build.ps` script (`git.exe` should be in the PATH)

src/Handle2/HandleInfo/HandleInfoRetriever.cs

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ public record struct ProcessInfo(
4040
string? ProcessExecutablePath,
4141
string? UserName,
4242
string? DomainName,
43-
IEnumerable<HandleInfo> Handles);
43+
IEnumerable<HandleInfo> Handles,
44+
IEnumerable<string> ModuleNames);
4445

4546
public static class HandleInfoRetriever
4647
{
@@ -63,7 +64,7 @@ private static void AddHandleTypeAndNameInfo(SafeFileHandle handle, ref HandleIn
6364
}
6465
}
6566

66-
public static IEnumerable<ProcessInfo> GetAllProcInfos()
67+
private static IEnumerable<ProcessInfo> GetProcInfos(Func<string?, bool> handleAndModuleNameFilter)
6768
{
6869
var currentProcess = WinApi.GetCurrentProcess();
6970
var result = new List<ProcessInfo>();
@@ -123,15 +124,21 @@ public static IEnumerable<ProcessInfo> GetAllProcInfos()
123124
watchdog.Disarm();
124125
}
125126

126-
currentHandles.Add(handle);
127+
if(handleAndModuleNameFilter(handle.FullNameIfItIsAFileOrAFolder))
128+
{
129+
currentHandles.Add(handle);
130+
}
127131
}
128132

129-
if (currentHandles.Any())
133+
var moduleNames = ProcessUtils.GetProcessModules(currentOpenedProcess).Where(name => handleAndModuleNameFilter(name)).ToArray();
134+
135+
if (currentHandles.Any() || moduleNames.Any())
130136
{
131137
var procInfo = new ProcessInfo
132138
{
133139
Pid = pid.ToUInt64(),
134-
Handles = currentHandles
140+
Handles = currentHandles,
141+
ModuleNames = moduleNames
135142
};
136143
(procInfo.DomainName, procInfo.UserName) = ProcessUtils.GetOwnerDomainAndUserNames(currentOpenedProcess);
137144
procInfo.ProcessExecutablePath = ProcessUtils.GetProcessExeFullName(currentOpenedProcess);
@@ -149,27 +156,14 @@ public static IEnumerable<ProcessInfo> GetAllProcInfos()
149156
return result;
150157
}
151158

159+
public static IEnumerable<ProcessInfo> GetAllProcInfos()
160+
{
161+
return GetProcInfos(_ => true);
162+
}
163+
152164
public static IEnumerable<ProcessInfo> GetProcInfosLockingPath(string path)
153165
{
154166
path = FileUtils.ToCanonicalPath(path);
155-
var allInfos = GetAllProcInfos();
156-
var result = new List<ProcessInfo>();
157-
158-
foreach(var info in allInfos)
159-
{
160-
var handles = info.Handles.Where(h => h.FullNameIfItIsAFileOrAFolder?.StartsWith(path, StringComparison.InvariantCultureIgnoreCase) == true);
161-
if (handles.Any())
162-
{
163-
result.Add(new ProcessInfo
164-
{
165-
Pid = info.Pid,
166-
ProcessExecutablePath = info.ProcessExecutablePath,
167-
UserName = info.UserName,
168-
DomainName = info.DomainName,
169-
Handles = handles
170-
});
171-
}
172-
}
173-
return result;
167+
return GetProcInfos(fileName => fileName?.StartsWith(path, StringComparison.InvariantCultureIgnoreCase) == true);
174168
}
175169
}

src/Handle2/HandleInfo/Interop/WinApi.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,18 @@ public enum FileFlagsAndAttributes : uint
170170
SecurityAnonymous = 0x00100000
171171
}
172172

173+
public const int ERROR_INSUFFICIENT_BUFFER = 122;
174+
173175
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
174176
public static extern bool QueryFullProcessImageName(
175177
SafeProcessHandle hProcess,
176178
int dwFlags,
177179
StringBuilder? lpExeName,
178180
ref int lpdwSize);
179181

180-
public const int ERROR_INSUFFICIENT_BUFFER = 122;
182+
[DllImport("psapi.dll", SetLastError = true)]
183+
public static extern bool EnumProcessModules(SafeProcessHandle hProcess, IntPtr[] lphModule, int cb, out int lpcbNeeded);
184+
185+
[DllImport("psapi.dll", CharSet = CharSet.Unicode, SetLastError = true)]
186+
public static extern uint GetModuleFileNameEx(SafeProcessHandle hProcess, IntPtr hModule, [Out] StringBuilder lpFilename, int nSize);
181187
}

src/Handle2/HandleInfo/Utils/ProcessUtils.cs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,65 @@ public static (string domain, string user) GetOwnerDomainAndUserNames(SafeProces
4747

4848
public static SafeProcessHandle? OpenProcessToDuplicateHandle(UIntPtr pid)
4949
{
50-
var p = WinApi.OpenProcess(WinApi.ProcessAccessRights.PROCESS_DUP_HANDLE | WinApi.ProcessAccessRights.PROCESS_QUERY_INFORMATION, false, pid);
50+
var p = WinApi.OpenProcess(
51+
WinApi.ProcessAccessRights.PROCESS_DUP_HANDLE |
52+
WinApi.ProcessAccessRights.PROCESS_QUERY_INFORMATION |
53+
WinApi.ProcessAccessRights.PROCESS_VM_READ,
54+
false, pid);
5155
return p.IsInvalid ? null : p;
5256
}
57+
58+
public static IEnumerable<string> GetProcessModules(SafeProcessHandle openedProc)
59+
{
60+
for(var arraySize = 1024; ; arraySize *= 2)
61+
{
62+
var moduleHandles = new IntPtr[arraySize];
63+
if (!WinApi.EnumProcessModules(openedProc, moduleHandles, IntPtr.Size * moduleHandles.Length, out var bytesNeeded))
64+
{
65+
return Array.Empty<string>();
66+
}
67+
68+
var numberOfModules = bytesNeeded / IntPtr.Size;
69+
70+
if (arraySize < numberOfModules)
71+
{
72+
continue;
73+
}
74+
75+
return GetModuleNames(openedProc, moduleHandles.Take(numberOfModules));
76+
}
77+
}
78+
79+
private static IEnumerable<string> GetModuleNames(SafeProcessHandle openedProc, IEnumerable<IntPtr> moduleHandles)
80+
{
81+
foreach (var moduleHandle in moduleHandles)
82+
{
83+
var name = GetModuleName(openedProc, moduleHandle);
84+
if(name != null)
85+
{
86+
yield return name;
87+
}
88+
}
89+
}
90+
91+
private static string? GetModuleName(SafeProcessHandle openedProc, IntPtr moduleHandle)
92+
{
93+
for (var moduleNameLength = 1024; ; moduleNameLength *= 2)
94+
{
95+
var sb = new StringBuilder(moduleNameLength);
96+
var resultLen = WinApi.GetModuleFileNameEx(openedProc, moduleHandle, sb, sb.Capacity);
97+
98+
if (resultLen == 0)
99+
{
100+
return null;
101+
}
102+
103+
if (resultLen == sb.Capacity)
104+
{
105+
continue;
106+
}
107+
108+
return sb.ToString();
109+
}
110+
}
53111
}

src/Handle2/Program.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
Console.WriteLine($" {handle.HandleType,-15} {name}");
3838
}
3939

40+
foreach (var moduleName in procWithHandles.ModuleNames)
41+
{
42+
Console.WriteLine($" {"Module",-15} {moduleName}");
43+
}
44+
4045
Console.WriteLine("------------------------------------------------------------------------------");
4146
}
4247
}
@@ -58,6 +63,11 @@
5863
Console.WriteLine($" {handle.FullNameIfItIsAFileOrAFolder}");
5964
}
6065

66+
foreach (var moduleName in procWithHandles.ModuleNames)
67+
{
68+
Console.WriteLine($" {moduleName}");
69+
}
70+
6171
Console.WriteLine("------------------------------------------------------------------------------");
6272
}
6373
}
@@ -90,6 +100,6 @@ public sealed class Options
90100
[Option("path", Required = true, SetName = "path", HelpText = "Displays the processes locking the path")]
91101
public string? Path { get; set; }
92102

93-
[Option("dump-all-handles", Required = true, Default = false, SetName = "dump-all-handles", HelpText = "Displays information about all system handles.")]
103+
[Option("dump-all-handles", Required = true, Default = false, SetName = "dump-all-handles", HelpText = "Displays information about all system handles and modules")]
94104
public bool DumpAllHandles { get; set; }
95105
}

src/test/ProgramTest.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ public void Dump_all_handle_information()
5151
Assert.That(res.StandardOutput, ContainsIgnoreCase(@"C:\Windows\system32\sihost.exe"));
5252
Assert.That(res.StandardOutput, ContainsIgnoreCase(@"Desktop"));
5353
Assert.That(res.StandardOutput, ContainsIgnoreCase(@"\Default"));
54-
Assert.That(res.StandardOutput, ContainsIgnoreCase(@"C:\Windows\System32\"));
54+
Assert.That(res.StandardOutput, ContainsIgnoreCase(@" File C:\Windows\System32\"));
5555
Assert.That(res.StandardOutput, ContainsIgnoreCase(@"C:\Windows\System32\en-US\KernelBase.dll.mui"));
56+
Assert.That(res.StandardOutput, ContainsIgnoreCase(@" Module C:\WINDOWS\SYSTEM32\ntdll.dll"));
5657
}
5758

5859
[Test]
@@ -66,6 +67,8 @@ public void Dump_all_handle_information_json()
6667
Assert.That(res.StandardOutput, ContainsIgnoreCase(@"C:\\Windows\\System32\\en-US\\propsys.dll.mui"));
6768
Assert.That(res.StandardOutput, Contains.Substring(@"\\Device\\HarddiskVolume"));
6869
Assert.That(res.StandardOutput, Contains.Substring(@" ""HandleType"": ""Section"","));
70+
Assert.That(res.StandardOutput, Contains.Substring(@" ""ModuleNames"": ["));
71+
Assert.That(res.StandardOutput, ContainsIgnoreCase(@" ""C:\\WINDOWS\\SYSTEM32\\ntdll.dll"","));
6972
}
7073

7174
[Test]

0 commit comments

Comments
 (0)