1010using Serilog ;
1111using System . Windows ;
1212using HCMExternal . ViewModels ;
13+ using NtApiDotNet ;
14+ using System . IO ;
1315
1416namespace 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\n If Steam/MCC is running as admin, then HCM must be run as admin too.\n (But better to run both as non-admin)\n Nerdy 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! \n More info in HCMExternal.log file. Error message: \n " + injectionResult . errorString ;
155+ lastInjectionError = "HCM failed to inject its internal module into the game! \n More info in HCMExternal.log file. Error message: \n " + injectionResult . errorString
156+ + "\n Access 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