Skip to content

Commit

Permalink
Process tracker feature
Browse files Browse the repository at this point in the history
- Added the ProcessTracker class, with its tests and documentation
  • Loading branch information
Doublevil authored Aug 15, 2023
2 parents 71b8078 + 674b4ec commit 4d3caad
Show file tree
Hide file tree
Showing 7 changed files with 463 additions and 33 deletions.
50 changes: 48 additions & 2 deletions doc/GetStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ This documentation aims to provide you with basic directions on how to get start
Let's go through the basics of hacking a health value in an imaginary game.

```csharp
// Open a process named "mygame.exe" that is currently running
var myGame = ProcessMemory.OpenProcess("mygame.exe");
// Open a process named "mygame" that is currently running
var myGame = ProcessMemory.OpenProcess("mygame");

// Build a path that points to the health value or anything else you want to hack
// To determine these, use tools like Cheat Engine (keep reading the docs for more info)
Expand Down Expand Up @@ -58,3 +58,49 @@ Here are common reasons for your programs to fail:
The `StateWatcher` class gives you a convenient way to access the internal data of your target process, with automatic refreshes.

Check the [StateWatcher](StateWatcher.md) documentation to learn how to set it up.

## Handle process exit and restart

The `ProcessMemory` class has a `ProcessDetached` event that you can use to react to your target process exiting or crashing. Note that it will also fire when disposing the instance.

```csharp
var myGame = ProcessMemory.OpenProcess("mygame");
myGame.ProcessDetached += (_, _) => { Console.WriteLine("Target process is detached."); }
```

However, a `ProcessMemory` instance that has been detached **cannot be reattached**.

If you want to handle your target process exiting and potentially restarting (or starting after your hacking program), use the `ProcessTracker` class.

```csharp
var tracker = new ProcessTracker("mygame");
tracker.Attached += (_, _) => { Console.WriteLine("Target is attached."); }
tracker.Detached += (_, _) => { Console.WriteLine("Target is detached."); }

var myGame = tracker.GetProcessMemory();
```

The `GetProcessMemory` method will return an attached `ProcessMemory` instance, or null if the target process is not running. It will automatically handle target process restarts by creating a new `ProcessMemory` instance when the existing one has been detached.

Just make sure to use the freshest instance from the tracker in your reading/writing methods, and not a stored `ProcessMemory` variable:

```csharp
public MyHackingClass()
{
Tracker = new ProcessTracker("mygame");
}
public void Update()
=> DoSomeHacking(Tracker.GetProcessMemory()); // ✓ This will handle restarts
```

VS

```csharp
public MyHackingClass()
{
var tracker = new ProcessTracker("mygame");
MyGame = tracker.GetProcessMemory();
}
public void Update()
=> DoSomeHacking(MyGame); // ✖ This will NOT handle restarts.
```
11 changes: 3 additions & 8 deletions src/MindControl/ProcessMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,11 @@ public class ProcessMemory : IDisposable
/// By default, this value will be <see cref="MemoryProtectionStrategy.RemoveAndRestore"/>.
/// </summary>
public MemoryProtectionStrategy DefaultStrategy { get; set; } = MemoryProtectionStrategy.RemoveAndRestore;

/// <summary>
/// Event handler used for the process detach event.
/// </summary>
public delegate void ProcessDetachedEventHandler(object sender, EventArgs e);

/// <summary>
/// Event raised when the process detaches for any reason.
/// </summary>
public event ProcessDetachedEventHandler? ProcessDetached;
public event EventHandler? ProcessDetached;

/// <summary>
/// Attaches to a process with the given name and returns the resulting <see cref="ProcessMemory"/> instance.
Expand Down Expand Up @@ -84,7 +79,7 @@ public static ProcessMemory OpenProcess(Process target)
/// Builds a new instance that attaches to the given process.
/// </summary>
/// <param name="process">Target process.</param>
private ProcessMemory(Process process) : this(process, new Win32Service()) {}
public ProcessMemory(Process process) : this(process, new Win32Service()) {}

/// <summary>
/// Builds a new instance that attaches to the given process.
Expand All @@ -93,7 +88,7 @@ public static ProcessMemory OpenProcess(Process target)
/// </summary>
/// <param name="process">Target process.</param>
/// <param name="osService">Service that provides system-specific process-oriented features.</param>
public ProcessMemory(Process process, IOperatingSystemService osService)
private ProcessMemory(Process process, IOperatingSystemService osService)
{
_process = process;
_osService = osService;
Expand Down
127 changes: 127 additions & 0 deletions src/MindControl/ProcessTracker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.Diagnostics;

namespace MindControl;

/// <summary>
/// Provides a <see cref="ProcessMemory"/> for a process identified by its name.
/// The tracker is able to re-attach to a process with the same name after it has been closed and reopened.
/// </summary>
public class ProcessTracker : IDisposable
{
private readonly string _processName;
private readonly SemaphoreSlim _instanceSemaphore = new(1, 1);
private ProcessMemory? _processMemory;

/// <summary>
/// Gets the name of the process tracked by this instance.
/// </summary>
public string ProcessName => _processName;

/// <summary>
/// Gets a value indicating if the process is currently attached.
/// </summary>
public bool IsAttached => _processMemory?.IsAttached == true;

/// <summary>
/// Event raised when attaching to the target process.
/// </summary>
public event EventHandler? Attached;

/// <summary>
/// Event raised when detached from the target process.
/// </summary>
public event EventHandler? Detached;

/// <summary>
/// Builds a tracker for the process with the given name.
/// </summary>
/// <param name="processName">Name of the process to track.</param>
public ProcessTracker(string processName)
{
_processName = processName;
}

/// <summary>
/// Gets the <see cref="ProcessMemory"/> instance for the currently attached process.
/// If no process is attached yet, make an attempt to attach to the target process by its name.
/// Returns null if no process with the target name can be found.
/// </summary>
public ProcessMemory? GetProcessMemory()
{
// Use a semaphore here to make sure we never attach twice or attach while detaching.
_instanceSemaphore.Wait();
try
{
if (_processMemory?.IsAttached != true)
{
_processMemory = AttemptToAttachProcess();

if (_processMemory != null)
{
_processMemory.ProcessDetached += OnProcessDetached;
Attached?.Invoke(this, EventArgs.Empty);
}
}
}
finally
{
_instanceSemaphore.Release();
}
return _processMemory;
}

/// <summary>
/// Callback. Called when the process memory detaches.
/// </summary>
private void OnProcessDetached(object? sender, EventArgs e) => Detach();

/// <summary>
/// Makes sure the memory instance is detached.
/// </summary>
private void Detach()
{
// Reserve the semaphore to prevent simultaneous detach/attach operations.
_instanceSemaphore.Wait();

try
{
if (_processMemory != null)
{
_processMemory.ProcessDetached -= OnProcessDetached;
_processMemory?.Dispose();
Detached?.Invoke(this, EventArgs.Empty);
}
}
catch
{
// Swallow the exception - we don't care about something happening while detaching.
}

_processMemory = null;
_instanceSemaphore.Release();
}

/// <summary>
/// Attempts to locate and attach the target process, and returns the resulting process memory instance.
/// </summary>
private ProcessMemory? AttemptToAttachProcess()
{
var process = GetTargetProcess();
return process == null ? null : new ProcessMemory(process);
}

/// <summary>
/// Gets the first process running with the target name. Returns null if no process with the given name is found.
/// </summary>
private Process? GetTargetProcess() => Process.GetProcessesByName(_processName).MinBy(p => p.Id);

/// <summary>
/// Releases the underlying process memory if required.
/// </summary>
public void Dispose()
{
Detach();
_instanceSemaphore.Dispose();
GC.SuppressFinalize(this);
}
}
2 changes: 1 addition & 1 deletion src/MindControl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ MindControl is a .net hacking library for Windows that allows you to manipulate
Here is a quick example to get you started.

```csharp
var myGame = ProcessMemory.OpenProcess("mygame.exe"); // A process with this name must be running
var myGame = ProcessMemory.OpenProcess("mygame"); // A process with this name must be running
var hpAddress = new PointerPath("mygame.exe+1D005A70,1C,8"); // See the docs for how to determine these
// Read values
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using NUnit.Framework;

namespace MindControl.Test.ProcessMemoryTests;

/// <summary>
/// Tests the features of the <see cref="ProcessMemory"/> class related to attaching to a process.
/// </summary>
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public class ProcessMemoryAttachTest : ProcessMemoryTest
{
/// <summary>
/// This test only ensures that the setup works, i.e. that opening a process as a
/// <see cref="MindControl.ProcessMemory"/> instance won't throw an exception.
/// </summary>
[Test]
public void OpenProcessTest() { }

/// <summary>
/// Tests that the Dispose method detaches from the process and raises the relevant event.
/// </summary>
[Test]
public void DisposeTest()
{
var hasRaisedEvent = false;
TestProcessMemory!.ProcessDetached += (_, _) => { hasRaisedEvent = true; };
TestProcessMemory.Dispose();
Assert.Multiple(() =>
{
Assert.That(hasRaisedEvent, Is.True);
Assert.That(TestProcessMemory.IsAttached, Is.False);
});
}

/// <summary>
/// Tests that the <see cref="ProcessMemory.ProcessDetached"/> event is raised when the process exits.
/// </summary>
[Test]
public void ProcessDetachedOnExitTest()
{
var hasRaisedEvent = false;
TestProcessMemory!.ProcessDetached += (_, _) => { hasRaisedEvent = true; };

// Go to the end of the process, then wait for a bit to make sure the process exits before we assert the results
ProceedUntilProcessEnds();
Thread.Sleep(1000);

Assert.Multiple(() =>
{
Assert.That(hasRaisedEvent, Is.True);
Assert.That(TestProcessMemory.IsAttached, Is.False);
});
}
}
47 changes: 25 additions & 22 deletions test/MindControl.Test/ProcessMemoryTests/ProcessMemoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,20 @@ public class ProcessMemoryTest
[SetUp]
public void Initialize()
{
_targetProcess = new Process
_targetProcess = StartTargetAppProcess();
string? line = _targetProcess.StandardOutput.ReadLine();
if (!IntPtr.TryParse(line, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out OuterClassPointer))
throw new Exception($"Could not read the outer class pointer output by the app: \"{line}\".");

TestProcessMemory = ProcessMemory.OpenProcess(_targetProcess);
}

/// <summary>
/// Starts the target app and returns its process.
/// </summary>
public static Process StartTargetAppProcess()
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
Expand All @@ -33,22 +46,8 @@ public void Initialize()
StandardOutputEncoding = Encoding.UTF8
}
};

try
{
_targetProcess.Start();
}
catch (Exception e)
{
throw new Exception(
"An error occurred while attempting to start the testing target app. Please check that the MindControl.Test.TargetApp project has been built in Release before running the tests.", e);
}

string? line = _targetProcess.StandardOutput.ReadLine();
if (!IntPtr.TryParse(line, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out OuterClassPointer))
throw new Exception($"Could not read the outer class pointer output by the app: \"{line}\".");

TestProcessMemory = ProcessMemory.OpenProcess(_targetProcess);
process.Start();
return process;
}

/// <summary>
Expand All @@ -60,6 +59,8 @@ public void CleanUp()
TestProcessMemory?.Dispose();
_targetProcess?.Kill();
_targetProcess?.Dispose();
// Make sure the process is exited before going on, otherwise it could cause other tests to fail.
Thread.Sleep(250);
}

private int _currentStep = 0;
Expand Down Expand Up @@ -88,11 +89,13 @@ protected void ProceedToNextStep()
_targetProcess!.StandardOutput.ReadLine();
}
}

/// <summary>
/// This test only ensures that the setup works, i.e. that opening a process as a <see cref="MindControl.ProcessMemory"/>
/// instance won't throw an exception.
/// Sends input to the target app process in order to make it continue to the end.
/// </summary>
[Test]
public void OpenProcessTest() { }
protected void ProceedUntilProcessEnds()
{
ProceedToNextStep();
ProceedToNextStep();
}
}
Loading

0 comments on commit 4d3caad

Please sign in to comment.