From ff8849671af5ac14fc9cc9d37da30f53d3f13d89 Mon Sep 17 00:00:00 2001
From: Caian Benedicto <caianbene@gmail.com>
Date: Wed, 4 Aug 2021 17:05:17 -0300
Subject: [PATCH] Update TamperMachine and disable write-to-code prevention
 (#2506)

* Enable write to memory and improve logging

* Update tamper machine opcodes and improve reporting

* Add Else support

* Add missing private statement
---
 .../Exceptions/CodeRegionTamperedException.cs |  9 ---
 .../HOS/Kernel/Process/ProcessTamperInfo.cs   | 12 ++--
 Ryujinx.HLE/HOS/ModLoader.cs                  |  2 +-
 Ryujinx.HLE/HOS/ProgramLoader.cs              |  3 +-
 Ryujinx.HLE/HOS/Tamper/AtmosphereCompiler.cs  | 36 +++++++++---
 Ryujinx.HLE/HOS/Tamper/AtmosphereProgram.cs   |  5 +-
 .../CodeEmitters/EndConditionalBlock.cs       | 55 ++++++++++++++++---
 Ryujinx.HLE/HOS/Tamper/CompilationContext.cs  | 20 ++++---
 Ryujinx.HLE/HOS/Tamper/ITamperProgram.cs      |  2 +
 Ryujinx.HLE/HOS/Tamper/ITamperedProcess.cs    |  3 +
 Ryujinx.HLE/HOS/Tamper/MemoryHelper.cs        |  6 ++
 Ryujinx.HLE/HOS/Tamper/MemoryRegion.cs        | 12 +++-
 Ryujinx.HLE/HOS/Tamper/Operations/IfBlock.cs  | 19 +++----
 Ryujinx.HLE/HOS/Tamper/TamperedKProcess.cs    | 12 ++--
 Ryujinx.HLE/HOS/TamperMachine.cs              | 26 +++++----
 15 files changed, 156 insertions(+), 66 deletions(-)
 delete mode 100644 Ryujinx.HLE/Exceptions/CodeRegionTamperedException.cs

diff --git a/Ryujinx.HLE/Exceptions/CodeRegionTamperedException.cs b/Ryujinx.HLE/Exceptions/CodeRegionTamperedException.cs
deleted file mode 100644
index 7a61273e1..000000000
--- a/Ryujinx.HLE/Exceptions/CodeRegionTamperedException.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using System;
-
-namespace Ryujinx.HLE.Exceptions
-{
-    public class CodeRegionTamperedException : TamperExecutionException
-    {
-        public CodeRegionTamperedException(string message) : base(message) { }
-    }
-}
diff --git a/Ryujinx.HLE/HOS/Kernel/Process/ProcessTamperInfo.cs b/Ryujinx.HLE/HOS/Kernel/Process/ProcessTamperInfo.cs
index fd10ee983..556703cf6 100644
--- a/Ryujinx.HLE/HOS/Kernel/Process/ProcessTamperInfo.cs
+++ b/Ryujinx.HLE/HOS/Kernel/Process/ProcessTamperInfo.cs
@@ -8,13 +8,17 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
         public IEnumerable<string> BuildIds { get; }
         public IEnumerable<ulong> CodeAddresses { get; }
         public ulong HeapAddress { get; }
+        public ulong AliasAddress { get; }
+        public ulong AslrAddress { get; }
 
-        public ProcessTamperInfo(KProcess process, IEnumerable<string> buildIds, IEnumerable<ulong> codeAddresses, ulong heapAddress)
+        public ProcessTamperInfo(KProcess process, IEnumerable<string> buildIds, IEnumerable<ulong> codeAddresses, ulong heapAddress, ulong aliasAddress, ulong aslrAddress)
         {
-            Process = process;
-            BuildIds = buildIds;
+            Process       = process;
+            BuildIds      = buildIds;
             CodeAddresses = codeAddresses;
-            HeapAddress = heapAddress;
+            HeapAddress   = heapAddress;
+            AliasAddress  = aliasAddress;
+            AslrAddress   = aslrAddress;
         }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/ModLoader.cs b/Ryujinx.HLE/HOS/ModLoader.cs
index a2e9af18a..9cea42a43 100644
--- a/Ryujinx.HLE/HOS/ModLoader.cs
+++ b/Ryujinx.HLE/HOS/ModLoader.cs
@@ -658,7 +658,7 @@ namespace Ryujinx.HLE.HOS
 
                 Logger.Info?.Print(LogClass.ModLoader, $"Installing cheat '{cheat.Name}'");
 
-                tamperMachine.InstallAtmosphereCheat(cheat.Instructions, tamperInfo, exeAddress);
+                tamperMachine.InstallAtmosphereCheat(cheat.Name, cheat.Instructions, tamperInfo, exeAddress);
             }
         }
 
diff --git a/Ryujinx.HLE/HOS/ProgramLoader.cs b/Ryujinx.HLE/HOS/ProgramLoader.cs
index 93ddd7ee6..385b4af50 100644
--- a/Ryujinx.HLE/HOS/ProgramLoader.cs
+++ b/Ryujinx.HLE/HOS/ProgramLoader.cs
@@ -280,7 +280,8 @@ namespace Ryujinx.HLE.HOS
             // Keep the build ids because the tamper machine uses them to know which process to associate a
             // tamper to and also keep the starting address of each executable inside a process because some
             // memory modifications are relative to this address.
-            tamperInfo = new ProcessTamperInfo(process, buildIds, nsoBase, process.MemoryManager.HeapRegionStart);
+            tamperInfo = new ProcessTamperInfo(process, buildIds, nsoBase, process.MemoryManager.HeapRegionStart,
+                process.MemoryManager.AliasRegionStart, process.MemoryManager.CodeRegionStart);
 
             return true;
         }
diff --git a/Ryujinx.HLE/HOS/Tamper/AtmosphereCompiler.cs b/Ryujinx.HLE/HOS/Tamper/AtmosphereCompiler.cs
index 05e248c89..7d7af2085 100644
--- a/Ryujinx.HLE/HOS/Tamper/AtmosphereCompiler.cs
+++ b/Ryujinx.HLE/HOS/Tamper/AtmosphereCompiler.cs
@@ -9,14 +9,36 @@ namespace Ryujinx.HLE.HOS.Tamper
 {
     class AtmosphereCompiler
     {
-        public ITamperProgram Compile(IEnumerable<string> rawInstructions, ulong exeAddress, ulong heapAddress, ITamperedProcess process)
+        private ulong            _exeAddress;
+        private ulong            _heapAddress;
+        private ulong            _aliasAddress;
+        private ulong            _aslrAddress;
+        private ITamperedProcess _process;
+
+        public AtmosphereCompiler(ulong exeAddress, ulong heapAddress, ulong aliasAddress, ulong aslrAddress, ITamperedProcess process)
         {
-            Logger.Debug?.Print(LogClass.TamperMachine, $"Executable address: {exeAddress:X16}");
-            Logger.Debug?.Print(LogClass.TamperMachine, $"Heap address: {heapAddress:X16}");
+            _exeAddress   = exeAddress;
+            _heapAddress  = heapAddress;
+            _aliasAddress = aliasAddress;
+            _aslrAddress  = aslrAddress;
+            _process      = process;
+        }
+
+        public ITamperProgram Compile(string name, IEnumerable<string> rawInstructions)
+        {
+            string[] addresses = new string[]
+            {
+                $"    Executable address: 0x{_exeAddress:X16}",
+                $"    Heap address      : 0x{_heapAddress:X16}",
+                $"    Alias address     : 0x{_aliasAddress:X16}",
+                $"    Aslr address      : 0x{_aslrAddress:X16}"
+            };
+
+            Logger.Debug?.Print(LogClass.TamperMachine, $"Compiling Atmosphere cheat {name}...\n{string.Join('\n', addresses)}");
 
             try
             {
-                return CompileImpl(rawInstructions, exeAddress, heapAddress, process);
+                return CompileImpl(name, rawInstructions);
             }
             catch(TamperCompilationException exception)
             {
@@ -33,9 +55,9 @@ namespace Ryujinx.HLE.HOS.Tamper
             return null;
         }
 
-        private ITamperProgram CompileImpl(IEnumerable<string> rawInstructions, ulong exeAddress, ulong heapAddress, ITamperedProcess process)
+        private ITamperProgram CompileImpl(string name, IEnumerable<string> rawInstructions)
         {
-            CompilationContext context = new CompilationContext(exeAddress, heapAddress, process);
+            CompilationContext context = new CompilationContext(_exeAddress, _heapAddress, _aliasAddress, _aslrAddress, _process);
             context.BlockStack.Push(new OperationBlock(null));
 
             // Parse the instructions.
@@ -124,7 +146,7 @@ namespace Ryujinx.HLE.HOS.Tamper
                 throw new TamperCompilationException($"Reached end of compilation with unmatched conditional(s) or loop(s)");
             }
 
-            return new AtmosphereProgram(process, context.PressedKeys, new Block(context.CurrentOperations));
+            return new AtmosphereProgram(name, _process, context.PressedKeys, new Block(context.CurrentOperations));
         }
     }
 }
diff --git a/Ryujinx.HLE/HOS/Tamper/AtmosphereProgram.cs b/Ryujinx.HLE/HOS/Tamper/AtmosphereProgram.cs
index 1fd0afb41..dac445b0d 100644
--- a/Ryujinx.HLE/HOS/Tamper/AtmosphereProgram.cs
+++ b/Ryujinx.HLE/HOS/Tamper/AtmosphereProgram.cs
@@ -8,10 +8,13 @@ namespace Ryujinx.HLE.HOS.Tamper
         private Parameter<long> _pressedKeys;
         private IOperation _entryPoint;
 
+        public string Name { get; }
+        public bool TampersCodeMemory { get; set; } = false;
         public ITamperedProcess Process { get; }
 
-        public AtmosphereProgram(ITamperedProcess process, Parameter<long> pressedKeys, IOperation entryPoint)
+        public AtmosphereProgram(string name, ITamperedProcess process, Parameter<long> pressedKeys, IOperation entryPoint)
         {
+            Name = name;
             Process = process;
             _pressedKeys = pressedKeys;
             _entryPoint = entryPoint;
diff --git a/Ryujinx.HLE/HOS/Tamper/CodeEmitters/EndConditionalBlock.cs b/Ryujinx.HLE/HOS/Tamper/CodeEmitters/EndConditionalBlock.cs
index 4a01992c8..a25dddde5 100644
--- a/Ryujinx.HLE/HOS/Tamper/CodeEmitters/EndConditionalBlock.cs
+++ b/Ryujinx.HLE/HOS/Tamper/CodeEmitters/EndConditionalBlock.cs
@@ -10,32 +10,73 @@ namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
     /// </summary>
     class EndConditionalBlock
     {
+        const int TerminationTypeIndex = 1;
+
+        private const byte End  = 0; // True end of the conditional.
+        private const byte Else = 1; // End of the 'then' block and beginning of 'else' block.
+
         public static void Emit(byte[] instruction, CompilationContext context)
         {
-            // 20000000
+            Emit(instruction, context, null);
+        }
+
+        private static void Emit(byte[] instruction, CompilationContext context, IEnumerable<IOperation> operationsElse)
+        {
+            // 2X000000
+            // X: End type (0 = End, 1 = Else).
+
+            byte terminationType = instruction[TerminationTypeIndex];
+
+            switch (terminationType)
+            {
+                case End:
+                    break;
+                case Else:
+                    // Start a new operation block with the 'else' instruction to signal that there is the 'then' block just above it.
+                    context.BlockStack.Push(new OperationBlock(instruction));
+                    return;
+                default:
+                    throw new TamperCompilationException($"Unknown conditional termination type {terminationType}");
+            }
 
             // Use the conditional begin instruction stored in the stack.
-            instruction = context.CurrentBlock.BaseInstruction;
-            CodeType codeType = InstructionHelper.GetCodeType(instruction);
+            var upperInstruction = context.CurrentBlock.BaseInstruction;
+            CodeType codeType = InstructionHelper.GetCodeType(upperInstruction);
 
             // Pop the current block of operations from the stack so control instructions
             // for the conditional can be emitted in the upper block.
             IEnumerable<IOperation> operations = context.CurrentOperations;
             context.BlockStack.Pop();
 
+            // If the else operations are already set, then the upper block must not be another end.
+            if (operationsElse != null && codeType == CodeType.EndConditionalBlock)
+            {
+                throw new TamperCompilationException($"Expected an upper 'if' conditional instead of 'end conditional'");
+            }
+
             ICondition condition;
 
             switch (codeType)
             {
                 case CodeType.BeginMemoryConditionalBlock:
-                    condition = MemoryConditional.Emit(instruction, context);
+                    condition = MemoryConditional.Emit(upperInstruction, context);
                     break;
                 case CodeType.BeginKeypressConditionalBlock:
-                    condition = KeyPressConditional.Emit(instruction, context);
+                    condition = KeyPressConditional.Emit(upperInstruction, context);
                     break;
                 case CodeType.BeginRegisterConditionalBlock:
-                    condition = RegisterConditional.Emit(instruction, context);
+                    condition = RegisterConditional.Emit(upperInstruction, context);
                     break;
+                case CodeType.EndConditionalBlock:
+                    terminationType = upperInstruction[TerminationTypeIndex];
+                    // If there is an end instruction above then it must be an else.
+                    if (terminationType != Else)
+                    {
+                        throw new TamperCompilationException($"Expected an upper 'else' conditional instead of {terminationType}");
+                    }
+                    // Re-run the Emit with the else operations set.
+                    Emit(instruction, context, operations);
+                    return;
                 default:
                     throw new TamperCompilationException($"Conditional end does not match code type {codeType} in Atmosphere cheat");
             }
@@ -43,7 +84,7 @@ namespace Ryujinx.HLE.HOS.Tamper.CodeEmitters
             // Create a conditional block with the current operations and nest it in the upper
             // block of the stack.
 
-            IfBlock block = new IfBlock(condition, operations);
+            IfBlock block = new IfBlock(condition, operations, operationsElse);
             context.CurrentOperations.Add(block);
         }
     }
diff --git a/Ryujinx.HLE/HOS/Tamper/CompilationContext.cs b/Ryujinx.HLE/HOS/Tamper/CompilationContext.cs
index 71e64bb84..2dd4029a9 100644
--- a/Ryujinx.HLE/HOS/Tamper/CompilationContext.cs
+++ b/Ryujinx.HLE/HOS/Tamper/CompilationContext.cs
@@ -20,17 +20,21 @@ namespace Ryujinx.HLE.HOS.Tamper
         public Dictionary<byte, Register> StaticRegisters { get; }
         public ulong ExeAddress { get; }
         public ulong HeapAddress { get; }
+        public ulong AliasAddress { get; }
+        public ulong AslrAddress { get; }
 
-        public CompilationContext(ulong exeAddress, ulong heapAddress, ITamperedProcess process)
+        public CompilationContext(ulong exeAddress, ulong heapAddress, ulong aliasAddress, ulong aslrAddress, ITamperedProcess process)
         {
-            Process = process;
-            PressedKeys = new Parameter<long>(0);
-            BlockStack = new Stack<OperationBlock>();
-            Registers = new Dictionary<byte, Register>();
-            SavedRegisters = new Dictionary<byte, Register>();
+            Process         = process;
+            PressedKeys     = new Parameter<long>(0);
+            BlockStack      = new Stack<OperationBlock>();
+            Registers       = new Dictionary<byte, Register>();
+            SavedRegisters  = new Dictionary<byte, Register>();
             StaticRegisters = new Dictionary<byte, Register>();
-            ExeAddress = exeAddress;
-            HeapAddress = heapAddress;
+            ExeAddress      = exeAddress;
+            HeapAddress     = heapAddress;
+            AliasAddress    = aliasAddress;
+            AslrAddress     = aslrAddress;
         }
 
         public Register GetRegister(byte index)
diff --git a/Ryujinx.HLE/HOS/Tamper/ITamperProgram.cs b/Ryujinx.HLE/HOS/Tamper/ITamperProgram.cs
index 06bc22438..63702bf75 100644
--- a/Ryujinx.HLE/HOS/Tamper/ITamperProgram.cs
+++ b/Ryujinx.HLE/HOS/Tamper/ITamperProgram.cs
@@ -4,6 +4,8 @@ namespace Ryujinx.HLE.HOS.Tamper
 {
     interface ITamperProgram
     {
+        string Name { get; }
+        bool TampersCodeMemory { get; set; }
         ITamperedProcess Process { get; }
         void Execute(ControllerKeys pressedKeys);
     }
diff --git a/Ryujinx.HLE/HOS/Tamper/ITamperedProcess.cs b/Ryujinx.HLE/HOS/Tamper/ITamperedProcess.cs
index d9da5d008..c86e10210 100644
--- a/Ryujinx.HLE/HOS/Tamper/ITamperedProcess.cs
+++ b/Ryujinx.HLE/HOS/Tamper/ITamperedProcess.cs
@@ -5,6 +5,9 @@ namespace Ryujinx.HLE.HOS.Tamper
     interface ITamperedProcess
     {
         ProcessState State { get; }
+
+        bool TamperedCodeMemory { get; set; }
+
         T ReadMemory<T>(ulong va) where T : unmanaged;
         void WriteMemory<T>(ulong va, T value) where T : unmanaged;
         void PauseProcess();
diff --git a/Ryujinx.HLE/HOS/Tamper/MemoryHelper.cs b/Ryujinx.HLE/HOS/Tamper/MemoryHelper.cs
index 277b38416..1260ed9ac 100644
--- a/Ryujinx.HLE/HOS/Tamper/MemoryHelper.cs
+++ b/Ryujinx.HLE/HOS/Tamper/MemoryHelper.cs
@@ -15,6 +15,12 @@ namespace Ryujinx.HLE.HOS.Tamper
                 case MemoryRegion.Heap:
                     // Memory address is relative to the heap.
                     return context.HeapAddress;
+                case MemoryRegion.Alias:
+                    // Memory address is relative to the alias region.
+                    return context.AliasAddress;
+                case MemoryRegion.Asrl:
+                    // Memory address is relative to the asrl region, which matches the code region.
+                    return context.AslrAddress;
                 default:
                     throw new TamperCompilationException($"Invalid memory source {source} in Atmosphere cheat");
             }
diff --git a/Ryujinx.HLE/HOS/Tamper/MemoryRegion.cs b/Ryujinx.HLE/HOS/Tamper/MemoryRegion.cs
index 13ba6f18f..fb4b25ff1 100644
--- a/Ryujinx.HLE/HOS/Tamper/MemoryRegion.cs
+++ b/Ryujinx.HLE/HOS/Tamper/MemoryRegion.cs
@@ -20,6 +20,16 @@ namespace Ryujinx.HLE.HOS.Tamper
         /// <summary>
         /// The address of the heap, as determined by the kernel.
         /// </summary>
-        Heap = 0x1
+        Heap = 0x1,
+
+        /// <summary>
+        /// The address of the alias region, as determined by the kernel.
+        /// </summary>
+        Alias = 0x2,
+
+        /// <summary>
+        /// The address of the code region with address space layout randomization included.
+        /// </summary>
+        Asrl = 0x3,
     }
 }
diff --git a/Ryujinx.HLE/HOS/Tamper/Operations/IfBlock.cs b/Ryujinx.HLE/HOS/Tamper/Operations/IfBlock.cs
index 0ba0f8c3d..b7c5684ef 100644
--- a/Ryujinx.HLE/HOS/Tamper/Operations/IfBlock.cs
+++ b/Ryujinx.HLE/HOS/Tamper/Operations/IfBlock.cs
@@ -6,27 +6,26 @@ namespace Ryujinx.HLE.HOS.Tamper.Operations
     class IfBlock : IOperation
     {
         private ICondition _condition;
-        private IEnumerable<IOperation> _operations;
+        private IEnumerable<IOperation> _operationsThen;
+        private IEnumerable<IOperation> _operationsElse;
 
-        public IfBlock(ICondition condition, IEnumerable<IOperation> operations)
+        public IfBlock(ICondition condition, IEnumerable<IOperation> operationsThen, IEnumerable<IOperation> operationsElse)
         {
             _condition = condition;
-            _operations = operations;
-        }
-
-        public IfBlock(ICondition condition, params IOperation[] operations)
-        {
-            _operations = operations;
+            _operationsThen = operationsThen;
+            _operationsElse = operationsElse;
         }
 
         public void Execute()
         {
-            if (!_condition.Evaluate())
+            IEnumerable<IOperation> operations = _condition.Evaluate() ? _operationsThen : _operationsElse;
+
+            if (operations == null)
             {
                 return;
             }
 
-            foreach (IOperation op in _operations)
+            foreach (IOperation op in operations)
             {
                 op.Execute();
             }
diff --git a/Ryujinx.HLE/HOS/Tamper/TamperedKProcess.cs b/Ryujinx.HLE/HOS/Tamper/TamperedKProcess.cs
index e27c371a6..be51264a2 100644
--- a/Ryujinx.HLE/HOS/Tamper/TamperedKProcess.cs
+++ b/Ryujinx.HLE/HOS/Tamper/TamperedKProcess.cs
@@ -11,9 +11,11 @@ namespace Ryujinx.HLE.HOS.Tamper
 
         public ProcessState State => _process.State;
 
+        public bool TamperedCodeMemory { get; set; } = false;
+
         public TamperedKProcess(KProcess process)
         {
-            this._process = process;
+            _process = process;
         }
 
         private void AssertMemoryRegion<T>(ulong va, bool isWrite) where T : unmanaged
@@ -32,11 +34,11 @@ namespace Ryujinx.HLE.HOS.Tamper
                 return;
             }
 
-            // TODO (Caian): It is unknown how PPTC behaves if the tamper modifies memory regions
-            // belonging to code. So for now just prevent code tampering.
-            if ((va >= _process.MemoryManager.CodeRegionStart) && (va + size <= _process.MemoryManager.CodeRegionEnd))
+            // TODO (Caian): The JIT does not support invalidating a code region so writing to code memory may not work
+            // as intended, so taint the operation to issue a warning later.
+            if (isWrite && (va >= _process.MemoryManager.CodeRegionStart) && (va + size <= _process.MemoryManager.CodeRegionEnd))
             {
-                throw new CodeRegionTamperedException($"Writing {size} bytes to address 0x{va:X16} alters code");
+                TamperedCodeMemory = true;
             }
         }
 
diff --git a/Ryujinx.HLE/HOS/TamperMachine.cs b/Ryujinx.HLE/HOS/TamperMachine.cs
index 9cdea94ae..6044368e9 100644
--- a/Ryujinx.HLE/HOS/TamperMachine.cs
+++ b/Ryujinx.HLE/HOS/TamperMachine.cs
@@ -31,7 +31,7 @@ namespace Ryujinx.HLE.HOS
             }
         }
 
-        internal void InstallAtmosphereCheat(IEnumerable<string> rawInstructions, ProcessTamperInfo info, ulong exeAddress)
+        internal void InstallAtmosphereCheat(string name, IEnumerable<string> rawInstructions, ProcessTamperInfo info, ulong exeAddress)
         {
             if (!CanInstallOnPid(info.Process.Pid))
             {
@@ -39,11 +39,13 @@ namespace Ryujinx.HLE.HOS
             }
 
             ITamperedProcess tamperedProcess = new TamperedKProcess(info.Process);
-            AtmosphereCompiler compiler = new AtmosphereCompiler();
-            ITamperProgram program = compiler.Compile(rawInstructions, exeAddress, info.HeapAddress, tamperedProcess);
+            AtmosphereCompiler compiler = new AtmosphereCompiler(exeAddress, info.HeapAddress, info.AliasAddress, info.AslrAddress, tamperedProcess);
+            ITamperProgram program = compiler.Compile(name, rawInstructions);
 
             if (program != null)
             {
+                program.TampersCodeMemory = false;
+
                 _programs.Enqueue(program);
             }
 
@@ -116,27 +118,27 @@ namespace Ryujinx.HLE.HOS
             // Re-enqueue the tampering program because the process is still valid.
             _programs.Enqueue(program);
 
-            Logger.Debug?.Print(LogClass.TamperMachine, "Running tampering program");
+            Logger.Debug?.Print(LogClass.TamperMachine, $"Running tampering program {program.Name}");
 
             try
             {
                 ControllerKeys pressedKeys = (ControllerKeys)Thread.VolatileRead(ref _pressedKeys);
+                program.Process.TamperedCodeMemory = false;
                 program.Execute(pressedKeys);
-            }
-            catch (CodeRegionTamperedException ex)
-            {
-                Logger.Debug?.Print(LogClass.TamperMachine, $"Prevented tampering program from modifing code memory");
 
-                if (!String.IsNullOrEmpty(ex.Message))
+                // Detect the first attempt to tamper memory and log it.
+                if (!program.TampersCodeMemory && program.Process.TamperedCodeMemory)
                 {
-                    Logger.Debug?.Print(LogClass.TamperMachine, ex.Message);
+                    program.TampersCodeMemory = true;
+
+                    Logger.Warning?.Print(LogClass.TamperMachine, $"Tampering program {program.Name} modifies code memory so it may not work properly");
                 }
             }
             catch (Exception ex)
             {
-                Logger.Debug?.Print(LogClass.TamperMachine, $"The tampering program crashed, this can happen while the game is starting");
+                Logger.Debug?.Print(LogClass.TamperMachine, $"The tampering program {program.Name} crashed, this can happen while the game is starting");
 
-                if (!String.IsNullOrEmpty(ex.Message))
+                if (!string.IsNullOrEmpty(ex.Message))
                 {
                     Logger.Debug?.Print(LogClass.TamperMachine, ex.Message);
                 }