Skip to content

Commit 0a8bdf0

Browse files
committed
Refactored HCMExternal MCCHookService to use NtApiDotNet instead of System.Diagnostics for process handles. This lets us find the correct MCC using less permissiosn (ie even if HCM is usermode and MCC is admin) and add lots of extra debugging info at the external level.
1 parent 8342e77 commit 0a8bdf0

File tree

3 files changed

+101
-81
lines changed

3 files changed

+101
-81
lines changed

HCMExternal/HCMExternal.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@
212212
<PackageReference Include="gong-wpf-dragdrop" Version="3.2.1" />
213213
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
214214
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
215+
<PackageReference Include="NtApiDotNet" Version="1.1.33" />
215216
<PackageReference Include="Serilog" Version="2.12.0" />
216217
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
217218
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />

HCMExternal/Models/MCCHookState.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Runtime.CompilerServices;
77
using System.Text;
88
using System.Threading.Tasks;
9+
using NtApiDotNet;
910

1011
namespace HCMExternal.Models
1112
{
@@ -41,9 +42,9 @@ public MCCHookStateEnum State
4142

4243

4344

44-
// Handle to MCC process. Null if no MCC.
45-
private Process? _MCCProcess = null;
46-
public Process? MCCProcess
45+
// Handle to valid MCC process with PROCESS_QUERY_LIMITED_INFORMATION
46+
private NtProcess? _MCCProcess = null;
47+
public NtProcess? MCCProcess
4748
{
4849
get { return _MCCProcess; }
4950
set { _MCCProcess = value; OnPropertyChanged(); }

HCMExternal/Services/MCCState/MCCHookService.cs

Lines changed: 96 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
using Serilog;
1111
using System.Windows;
1212
using HCMExternal.ViewModels;
13+
using NtApiDotNet;
14+
using System.IO;
1315

1416
namespace HCMExternal.Services.MCCHookService
1517
{
@@ -29,6 +31,7 @@ private MCCHookState MCCHookState {
2931

3032
// last injection error
3133
private string? lastInjectionError;
34+
private bool debugPrivilegeEnabled = false;
3235

3336
// injected service
3437
private InterprocService mInterprocService { get; init; }
@@ -50,6 +53,9 @@ public MCCHookService(InterprocService ips, MCCHookStateViewModel vm, ErrorDialo
5053

5154
// Timer does not start immediately; only when BeginStateMachineLoop is called.
5255
StateMachineLoopTimer = new System.Threading.Timer(StateMachineLoopEventHandler, null, System.Threading.Timeout.Infinite, 1000);
56+
57+
debugPrivilegeEnabled = NtToken.EnableDebugPrivilege();
58+
Log.Information("Debug privilege: " + debugPrivilegeEnabled);
5359
}
5460

5561
// starts timer, called at end of application startup
@@ -84,6 +90,7 @@ private enum InternalStatusFlag
8490
Shutdown = 3,
8591
}
8692

93+
private StringWriter _mccAccessInfo = new();
8794

8895

8996
// main loop parsing MCCHookState and advancing the state machine
@@ -106,45 +113,21 @@ private void StateMachineLoop()
106113
if (MCCHookState.MCCProcess == null) // failed, try again next time
107114
break;
108115

109-
if (MCCExitedTooRecently())
110-
{
111-
Log.Debug("MCC exited too recently, bailing");
112-
MCCHookState.MCCProcess = null;
113-
break;
114-
}
115116

116117
try
117118
{
118-
// safety check that the process isn't actually closed
119-
if (MCCHookState.MCCProcess.HasExited) // this property can give access denied ex, hence being in try-catch
120-
{
121-
Log.Debug("Found MCC process but it had already exited, bailing");
122-
MCCHookState.MCCProcess = null;
123-
break;
124-
}
125-
126-
MCCHookState.MCCVersion = MCCHookState.MCCProcess.MainModule?.FileVersionInfo;
119+
Log.Verbose("Getting mcc version information from filepath: "
120+
+ MCCHookState.MCCProcess.GetImageFilePath(false)
121+
);
122+
MCCHookState.MCCVersion = FileVersionInfo.GetVersionInfo(MCCHookState.MCCProcess.GetImageFilePath(false));
127123
}
128-
catch (Exception ex)
124+
catch (FileNotFoundException ex)
129125
{
130-
lastInjectionError = "HCM failed to access the MCC process\nIf Steam/MCC is running as admin, then HCM must be run as admin too.\n(But better to run both as non-admin)\nNerdy details:\n" + ex.Message;
131-
AdvanceStateMachine(MCCHookStateEnum.MCCAccessError);
132-
ShowHCMInternalErrorDialog();
133-
return;
126+
Log.Error("Could not get file version info of MCC, proceeding anyway. Error: \n"
127+
+ ex.Message + "\n" + ex.Source + "\n" + ex.StackTrace);
134128
}
135129

136-
137-
// Subscribe to process exit (to reset state back to MCCNotFound)
138-
MCCHookState.MCCProcess.EnableRaisingEvents = true;
139-
MCCHookState.MCCProcess.Exited += (o, i) =>
140-
{
141-
Log.Information("MCC process exited!");
142-
MCCHookState.MCCVersion = null;
143-
MCCHookState.MCCProcess = null;
144-
_lastMCCExit = DateTime.Now;
145-
AdvanceStateMachine(MCCHookStateEnum.MCCNotFound);
146-
return;
147-
};
130+
logAccessInformation(MCCHookState.MCCProcess, _mccAccessInfo);
148131

149132

150133
AdvanceStateMachine(MCCHookStateEnum.InternalInjecting); // advance to next state (inject HCMInternal)
@@ -159,7 +142,7 @@ private void StateMachineLoop()
159142
break;
160143

161144
case MCCHookStateEnum.InternalInjecting:
162-
(bool successFlag, string errorString) injectionResult = mInterprocService.Setup((UInt32)MCCHookState.MCCProcess?.Id);
145+
(bool successFlag, string errorString) injectionResult = mInterprocService.Setup((UInt32)MCCHookState.MCCProcess?.ProcessId);
163146

164147

165148
if (injectionResult.successFlag)
@@ -169,7 +152,8 @@ private void StateMachineLoop()
169152
else
170153
{
171154
AdvanceStateMachine(MCCHookStateEnum.InternalInjectError);
172-
lastInjectionError = "HCM failed to inject its internal module into the game! \nMore info in HCMExternal.log file. Error message: \n" + injectionResult.errorString;
155+
lastInjectionError = "HCM failed to inject its internal module into the game! \nMore info in HCMExternal.log file. Error message: \n" + injectionResult.errorString
156+
+ "\nAccess Rights Debugging: \n" + _mccAccessInfo;
173157
ShowHCMInternalErrorDialog();
174158
}
175159
break;
@@ -216,12 +200,14 @@ private void StateMachineLoop()
216200
return;
217201
}
218202

219-
if (currentInternalStateSuccess == InternalStatusFlag.Shutdown)
203+
if (currentInternalStateSuccess == InternalStatusFlag.Shutdown || (MCCHookState.MCCProcess == null || MCCHookState.MCCProcess.IsDeleting))
220204
{
221205
_lastMCCExit = DateTime.Now;
222206
AdvanceStateMachine(MCCHookStateEnum.MCCNotFound);
223207
return;
224208
}
209+
210+
225211
break;
226212

227213
}
@@ -264,64 +250,65 @@ public void ShowHCMInternalErrorDialog()
264250
}
265251
}
266252

267-
268-
private Process? GetMCCProcess()
253+
private NtProcess? GetMCCProcess()
269254
{
270255
try
271256
{
272257

273-
bool isCorrectProcessName(string actualProcessName)
258+
bool filterToValidMCC(NtProcess process)
274259
{
275-
string[] MCCProcessNames = { "MCC-Win64-Shipping", "MCC-Win64-Winstore-Shipping", "MCCWinstore-Win64-Shipping" };
260+
string[] MCCProcessNames = { "MCC-Win64-Shipping.exe", "MCC-Win64-Winstore-Shipping.exe", "MCCWinstore-Win64-Shipping.exe" };
276261
foreach (string desiredProcessName in MCCProcessNames)
277262
{
278-
if (String.Equals(actualProcessName, desiredProcessName, StringComparison.OrdinalIgnoreCase))
263+
264+
265+
266+
if (String.Equals(process.Name, desiredProcessName, StringComparison.OrdinalIgnoreCase)) // must be mcc
267+
{
268+
if (process.IsDeleting)
269+
{
270+
Log.Debug("Skipping zombie (or terminating) MCC at process ID: " + process.ProcessId);
271+
continue;
272+
}
273+
274+
if (DateTime.Now - process.CreateTime < TimeSpan.FromSeconds(3))
275+
{
276+
// We don't want to attach on young MCC because of weird issues with LoadLibrary if it's called from multiple threads (within the same process) at once
277+
// TODO: Make this less dumb than just picking 3 seconds since some peoples computers are slower/faster than that.
278+
Log.Verbose("Skipping super young mcc at id " + process.ProcessId + ", age: " + (DateTime.Now - process.CreateTime));
279+
continue;
280+
}
281+
282+
if (MCCExitedTooRecently())
283+
continue;
284+
279285
return true;
286+
}
280287
}
281288
return false;
282289
}
283290

284291

285-
//Process? mostRecentMCCProcess = Process.GetProcesses()
286-
// .Where(process => isCorrectProcessName(process.ProcessName) && process.HasExited == false)
287-
// .OrderByDescending(process => process.StartTime) // we don't want old zombie processes
288-
// .DefaultIfEmpty(null)
289-
// .FirstOrDefault();
290-
291-
// Could do this in one big linq expression but we want to be able to debug/log for access violations at each step of the way
292-
Log.Debug("Getting all processes on computer");
293-
var allProcesses = Process.GetProcesses();
294-
295-
Log.Debug("Filtering to MCC process names");
296-
var mccProcesses = allProcesses.Where(process => isCorrectProcessName(process.ProcessName));
292+
var validMCCprocesses = NtProcess.GetProcesses(ProcessAccessRights.QueryLimitedInformation).Where(filterToValidMCC);
297293

298-
Log.Debug("Filtering exited processes");
299-
mccProcesses = mccProcesses.Where(process => process.HasExited == false);
300294

301-
Log.Debug("Sorting mcc processes by age");
302-
mccProcesses = mccProcesses.OrderByDescending(process => process.StartTime);
303-
Log.Verbose("Total mcc process count: " + mccProcesses.Count());
304-
305-
Log.Debug("Grabbing most recent mcc process or null");
306-
Process? mostRecentMCCProcess = mccProcesses.DefaultIfEmpty(null).First();
307-
308-
309-
if (mostRecentMCCProcess != null)
295+
if (validMCCprocesses.Any())
310296
{
311-
// We don't want to inject while MCC is booting up since LoadLibrary is occupied
312-
Log.Verbose("Found MCC");
313-
TimeSpan MCCProcessAge = DateTime.Now - mostRecentMCCProcess.StartTime;
314-
Log.Verbose("MCC age: " + MCCProcessAge);
315-
if (MCCProcessAge < TimeSpan.FromSeconds(3))
297+
if (validMCCprocesses.Count() == 1)
316298
{
317-
// We don't want to attach on young MCC because of weird issues with LoadLibrary if it's called from multiple threads (within the same process) at once
318-
// TODO: Make this less dumb than just picking 3 seconds since some peoples computers are slower/faster than that.
319-
Log.Verbose("MCC process too young: " + MCCProcessAge);
320-
return null;
299+
return validMCCprocesses.First();
300+
}
301+
else // more than one, so we want to sort by creation date and grab the youngest.
302+
{
303+
return validMCCprocesses.OrderByDescending(process => process.CreateTime).First();
321304
}
322305
}
323-
324-
return mostRecentMCCProcess;
306+
else
307+
{
308+
Log.Verbose("No process found");
309+
return null;
310+
}
311+
325312
}
326313
catch (Exception ex)
327314
{
@@ -344,16 +331,47 @@ private bool AnticheatIsEnabled()
344331
return false;
345332
}
346333

347-
// HCM can inadvertently try to inject on a closing MCC without this check. Safety buffer of 3s.
348-
private DateTime _lastMCCExit = DateTime.MinValue;
334+
// Just logs stuff about the MCC process that might help diagnose Access Denied issues
335+
private void logAccessInformation(NtProcess mccHandle, TextWriter writer)
336+
{
337+
try
338+
{
339+
340+
writer.WriteLine("hcm Debug privilege enabled: " + debugPrivilegeEnabled);
341+
writer.WriteLine("MCC Protected: " + mccHandle.Protected);
342+
writer.WriteLine("MCC Protected Access: " + NtProcess.TestProtectedAccess(NtProcess.Current, mccHandle));
343+
writer.WriteLine("MCC Protection.Level: " + mccHandle.Protection.Level);
344+
writer.WriteLine("MCC Protection.Audit: " + mccHandle.Protection.Audit);
345+
346+
var mccIntegrity = mccHandle.GetIntegrityLevel(false);
347+
var hcmIntegrity = NtProcess.Current.GetIntegrityLevel(false);
348+
349+
writer.WriteLine("hcm IntegrityLevel: " + (hcmIntegrity.IsSuccess ? hcmIntegrity.Result : "Could not read integrity level"));
350+
writer.WriteLine("MCC IntegrityLevel: " + (mccIntegrity.IsSuccess ? mccIntegrity.Result : "Could not read integrity level"));
351+
352+
var mccHandleReadControl = NtProcess.Open(mccHandle.ProcessId, ProcessAccessRights.ReadControl, false);
353+
if (mccHandleReadControl.IsSuccess)
354+
writer.WriteLine("MCC maximum access: " + mccHandleReadControl.Result.GetMaximumAccess());
355+
else
356+
writer.WriteLine("Could not check MCC maximum access: " + mccHandleReadControl.Status);
357+
358+
359+
}
360+
catch (NtException ex)
361+
{
362+
writer.WriteLine("error logging access information: " + ex.Message + "\n" + ex.StackTrace);
363+
}
364+
365+
}
366+
367+
private DateTime? _lastMCCExit = null;
349368
private bool MCCExitedTooRecently()
350369
{
351-
if (_lastMCCExit == DateTime.MinValue) return false;
370+
if (_lastMCCExit == null) return false;
352371

353372
Log.Verbose("MCC last exit time was " + (DateTime.Now - _lastMCCExit) + " seconds ago");
354373
return (DateTime.Now - _lastMCCExit) < TimeSpan.FromSeconds(6);
355374
}
356375

357-
358376
}
359377
}

0 commit comments

Comments
 (0)