Constructing True, False, and NOP Payloads for x86 & x64.
How to craft machine opcodes in managed C# byte arrays to hot-patch unmanaged C++ functions, handle calling conventions, and manage CPU cache coherency.
This is the long version of the same material. Slides have to cut every idea down to one line; here there is no such limit, so each part is worked through until you could do it yourself.
You do not have to read it in order. The contents below map the whole piece, and each section stands on its own if you only need one thing from it.
Contents
- Why C# Needs Native Byte Arrays โ In modding frameworks (MelonLoader, BepInEx) and memory injectors, C# controls native C++ engines.
- Constructing 'Return True' (x86 & x64) โ In the C/C++ ABI, boolean return values reside in the lowest byte of the accumulator (AL register).
- Constructing 'Return False' (x86 & x64) โ XORing a register with itself is the fastest, cleanest standard to clear return values.
- Alternative 'Return False' Variations โ Comparing direct move vs XOR techniques across different native return types.
- Constructing NOP (0x90) Payloads โ NOP instructs the CPU to advance the Instruction Pointer without modifying any registers or flags.
- When and How to Apply NOP in C++ โ NOP is primarily used to erase conditional branches, security checks, and sub-calls.
- The Calling Convention Pitfall (x86 vs x64) โ Stack cleanup rules dictate whether plain 'ret' (0xC3) is sufficient or dangerous.
- Step 1: Unlocking Protected Memory โ Executable native memory (.text section) is marked PAGE_EXECUTE_READ. Direct writes cause Access Violation.
- Step 2: Writing the Byte Array Safely โ Applying the C# byte array to the native target address using Marshal.Copy.
- Step 3: Flushing the CPU Instruction Cache โ The #1 reason native patches randomly fail: CPU Harvard Architecture incoherency.
- Production-Ready Native Patch Class โ Encapsulate patch state, automated byte backups, and toggle functionality in clean C#.
- Quick Reference: C# Native Patching Opcodes โ Master summary of machine opcodes and C# byte arrays for native C++ patching.
1. Why C# Needs Native Byte Arrays
Before the detail, the whole of Why C# Needs Native Byte Arrays comes down to one sentence: In modding frameworks (MelonLoader, BepInEx) and memory injectors, C# controls native C++ engines. Everything below is an unpacking of that sentence.
Terms used in this section
| Term | Meaning |
|---|
| IL2CPP / Native DLLs | C++ code compiles directly to raw machine instructions without CLR metadata. |
| In-Memory Patching | Modifying executable bytes (.text section) at runtime to alter function logic. |
| Byte Array Payloads | C# byte[] arrays represent the exact x86/x64 machine opcodes written to RAM. |
IL2CPP / Native DLLs means c++ code compiles directly to raw machine instructions without CLR metadata. The term comes up often, so it is worth learning its exact shape โ using a term loosely usually means picturing the concept loosely too.
By In-Memory Patching we mean modifying executable bytes (.text section) at runtime to alter function logic. Note how narrow the definition is; a lot of confusion comes from people using this term more broadly than it is meant.
Byte Array Payloads: C# byte[] arrays represent the exact x86/x64 machine opcodes written to RAM. You will meet it again in later sections, usually without a second explanation.
Note: Unlike managed Harmony patches that rewrite MSIL bytecode, native patching directly rewrites CPU instructions.
The note above looks small but it is often the whole of a debugging session. Treat it as a required step, not a suggestion.
By now Why C# Needs Native Byte Arrays should feel reasonable. If it does not, reread the first point โ that is usually where the gap is.
2. Constructing 'Return True' (x86 & x64)
This section is about Constructing 'Return True' (x86 & x64). In the C/C++ ABI, boolean return values reside in the lowest byte of the accumulator (AL register). Written as one sentence it looks simple, but this is exactly where the difference shows between someone who memorised the steps and someone who knows why the steps are what they are.
The key points
0xB0, 0x01, 0xC3 is only 3 bytes longโfits comfortably in any function prologue.
The heart of this point is one sentence. 0xB0, 0x01, 0xC3 is only 3 bytes longโfits comfortably in any function prologue. Resist the urge to jump straight to practice; make sure it is clear, because the rest of this section assumes it.
This point saves debugging time. Someone who already holds "0xB0, 0x01, 0xC3 is only 3 bytes longโfits comfortably in any function prologue" can narrow the search immediately, while someone who does not will guess one thing at a time.
This is a classic source of bugs. Someone copies code that worked under one set of conditions, uses it under another without checking "0xB0, 0x01, 0xC3 is only 3 bytes longโfits comfortably in any function prologue", and spends hours looking in the wrong place.
Architecture-universal: Identical opcode encoding in both 32-bit (x86) and 64-bit (x64).
Architecture-universal: Identical opcode encoding in both 32-bit (x86) and 64-bit (x64). It sounds like a small technical detail, but that sentence decides how everything around it behaves. Read it wrong and every conclusion after it is off too.
It is on the list because it separates a solution that happens to work from one that is actually right. And a solution that is right stays right after the code changes.
The real risk is not that the code fails; it is that the code succeeds for the wrong reason. Without confirming "Architecture-universal: Identical opcode encoding in both 32-bit (x86) and 64-bit (x64)", you have no way to tell which one is happening.
Directly sets C++ bool (1 byte) without corrupting stack frames.
The line "Directly sets C++ bool (1 byte) without corrupting stack frames" is worth reading slowly. It is not a formal rule to memorise; it is a picture of what actually happens when the code runs.
The practical value is here. Once you have "Directly sets C++ bool (1 byte) without corrupting stack frames" in hand, you can predict the result before running anything, and that is far faster than trial and error.
Skip this point and the symptoms usually do not show up straight away. The code still runs, the result still appears, and it is wrong โ and a bug that does not crash is the bug that takes longest to find.
Here is the simplest form of what was just described. Read it whole first, then look at the line-by-line notes.
The code for this section (PatchTrue.cs):
<span class="token comment">// Assembly: mov al, 1; ret</span>
<span class="token comment">// 0xB0 0x01 -> mov al, 1 (Set 8-bit AL to true)</span>
<span class="token comment">// 0xC3 -> ret (Return to caller)</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> PatchTrue <span class="token operator">=</span>
<span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span> <span class="token number">0xB0</span><span class="token punctuation">,</span> <span class="token number">0x01</span><span class="token punctuation">,</span> <span class="token number">0xC3</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
Reading the code line by line
- Line 1 is a comment โ Assembly: mov al, 1; ret
- Line 2 is a comment โ 0xB0 0x01 -> mov al, 1 (Set 8-bit AL to true)
- Line 3 is a comment โ 0xC3 -> ret (Return to caller)
- Line 4 defines PatchTrue; this is the part you call from elsewhere.
- Line 5 carries out the next step in that order.
Change one value here and predict the result before running it. That is the fastest way to turn reading into understanding.
The numbers
| Value | What it measures |
|---|
| 3 Bytes | Payload Size |
| x86 / x64 | Architecture |
| AL = 1 | Register State |
Look at the 3 Bytes in the payload Size row. Numbers like this are useful for comparing two approaches, not for promising the same result elsewhere.
On architecture: x86 / x64. The point is not the number but that the gap is wide enough to feel in everyday use.
The figure AL = 1 for register State is a measurement under particular conditions, not a promise. What you can take from it is the order of magnitude, not the exact digits.
Common mistakes
- Assuming "0xB0, 0x01, 0xC3 is only 3 bytes longโfits comfortably in any function prologue" holds forever. Conditions move, and nothing announces it when they do.
- Copying this part from another example without checking whether "Architecture-universal: Identical opcode encoding in both 32-bit (x86) and 64-bit (x64)" is also true in your case. Examples are always written for particular conditions.
- Skipping "Directly sets C++ bool (1 byte) without corrupting stack frames" because it looks trivial, then coming back to it after hunting for the cause somewhere else.
How to check you have it
- Can you explain the reasoning behind "0xB0, 0x01, 0xC3 is only 3 bytes longโfits comfortably in any function prologue", rather than just state that it is so?
- If the conditions change, do you know which parts move with them because of "Architecture-universal: Identical opcode encoding in both 32-bit (x86) and 64-bit (x64)"?
- Have you tried a case that deliberately breaks "Directly sets C++ bool (1 byte) without corrupting stack frames", and seen the difference clearly?
In short, Constructing 'Return True' (x86 & x64) is not about memorising syntax; it is about knowing what actually happens underneath.
3. Constructing 'Return False' (x86 & x64)
Let us take Constructing 'Return False' (x86 & x64) apart. XORing a register with itself is the fastest, cleanest standard to clear return values. I will start with the shape of it and then work down to the details people trip over.
The key points
Clears Entire Register: In x64, writing to 32-bit EAX automatically zeroes upper RAX.
Clears Entire Register: In x64, writing to 32-bit EAX automatically zeroes upper RAX. The wording is deliberately narrow so that it is not ambiguous โ widen it and you will immediately find cases that break it.
Why does this point matter? Because "Clears Entire Register: In x64, writing to 32-bit EAX automatically zeroes upper RAX" is an assumption almost all the surrounding code makes silently. While it holds, everything runs; the moment it is broken, the failure shows up somewhere you would never think to look.
Breaking this point rarely produces a clear error message. What you get instead is behaviour that looks random but is perfectly consistent once you account for "Clears Entire Register: In x64, writing to 32-bit EAX automatically zeroes upper RAX".
Universal Compatibility: Works for C++ bool (1-byte AL), Win32 BOOL (4-byte int), and pointers.
The heart of this point is one sentence. Universal Compatibility: Works for C++ bool (1-byte AL), Win32 BOOL (4-byte int), and pointers. Resist the urge to jump straight to practice; make sure it is clear, because the rest of this section assumes it.
This point saves debugging time. Someone who already holds "Universal Compatibility: Works for C++ bool (1-byte AL), Win32 BOOL (4-byte int), and pointers" can narrow the search immediately, while someone who does not will guess one thing at a time.
This is a classic source of bugs. Someone copies code that worked under one set of conditions, uses it under another without checking "Universal Compatibility: Works for C++ bool (1-byte AL), Win32 BOOL (4-byte int), and pointers", and spends hours looking in the wrong place.
Zero Null-Bytes: Avoids null terminators that could disrupt string-based pattern scanners.
Zero Null-Bytes: Avoids null terminators that could disrupt string-based pattern scanners. It sounds like a small technical detail, but that sentence decides how everything around it behaves. Read it wrong and every conclusion after it is off too.
It is on the list because it separates a solution that happens to work from one that is actually right. And a solution that is right stays right after the code changes.
The real risk is not that the code fails; it is that the code succeeds for the wrong reason. Without confirming "Zero Null-Bytes: Avoids null terminators that could disrupt string-based pattern scanners", you have no way to tell which one is happening.
Here is the simplest form of what was just described. Read it whole first, then look at the line-by-line notes.
The code for this section (PatchFalse.cs):
<span class="token comment">// Assembly: xor eax, eax; ret</span>
<span class="token comment">// 0x31 0xC0 -> xor eax, eax (Clears EAX and zero-extends RAX)</span>
<span class="token comment">// 0xC3 -> ret (Return to caller)</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> PatchFalse <span class="token operator">=</span>
<span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span> <span class="token number">0x31</span><span class="token punctuation">,</span> <span class="token number">0xC0</span><span class="token punctuation">,</span> <span class="token number">0xC3</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
Reading the code line by line
- Line 1 is a comment โ Assembly: xor eax, eax; ret
- Line 2 is a comment โ 0x31 0xC0 -> xor eax, eax (Clears EAX and zero-extends RAX)
- Line 3 is a comment โ 0xC3 -> ret (Return to caller)
- Line 4 defines PatchFalse; this is the part you call from elsewhere.
- Line 5 carries out the next step in that order.
Change one value here and predict the result before running it. That is the fastest way to turn reading into understanding.
The numbers
| Value | What it measures |
|---|
| 3 Bytes | Payload Size |
| EAX / RAX | Cleared |
| AL = 0 | Boolean Value |
The figure 3 Bytes for payload Size is a measurement under particular conditions, not a promise. What you can take from it is the order of magnitude, not the exact digits.
EAX / RAX on cleared gives a sense of how large the difference is. Measure it again in your own case before basing a decision on it.
Look at the AL = 0 in the boolean Value row. Numbers like this are useful for comparing two approaches, not for promising the same result elsewhere.
Common mistakes
- Assuming "Clears Entire Register: In x64, writing to 32-bit EAX automatically zeroes upper RAX" holds forever. Conditions move, and nothing announces it when they do.
- Copying this part from another example without checking whether "Universal Compatibility: Works for C++ bool (1-byte AL), Win32 BOOL (4-byte int), and pointers" is also true in your case. Examples are always written for particular conditions.
- Skipping "Zero Null-Bytes: Avoids null terminators that could disrupt string-based pattern scanners" because it looks trivial, then coming back to it after hunting for the cause somewhere else.
How to check you have it
- Have you tried a case that deliberately breaks "Clears Entire Register: In x64, writing to 32-bit EAX automatically zeroes upper RAX", and seen the difference clearly?
- Could you rewrite this part from scratch without notes?
- Can you explain the reasoning behind "Zero Null-Bytes: Avoids null terminators that could disrupt string-based pattern scanners", rather than just state that it is so?
By now Constructing 'Return False' (x86 & x64) should feel reasonable. If it does not, reread the first point โ that is usually where the gap is.
4. Alternative 'Return False' Variations
Now the part people skip when they are learning in a hurry: Alternative 'Return False' Variations. Comparing direct move vs XOR techniques across different native return types. Yet once it is clear, the rest of the material gets much easier to follow.
Here is the simplest form of what was just described. Read it whole first, then look at the line-by-line notes.
The code for this section (FalseVariants.cs):
<span class="token comment">// Variation A: mov al, 0; ret (3 bytes)</span>
<span class="token class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> patchFalseAl <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span> <span class="token number">0xB0</span><span class="token punctuation">,</span> <span class="token number">0x00</span><span class="token punctuation">,</span> <span class="token number">0xC3</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token comment">// Variation B: mov eax, 0; ret (6 bytes - Win32 BOOL)</span>
<span class="token class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> patchFalseEax <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span>
<span class="token number">0xB8</span><span class="token punctuation">,</span> <span class="token number">0x00</span><span class="token punctuation">,</span> <span class="token number">0x00</span><span class="token punctuation">,</span> <span class="token number">0x00</span><span class="token punctuation">,</span> <span class="token number">0x00</span><span class="token punctuation">,</span> <span class="token number">0xC3</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
Reading the code line by line
- Line 1 is a comment โ Variation A: mov al, 0; ret (3 bytes)
- Line 2 stores the result in patchFalseAl. That value is what the following lines work with.
- Line 4 is a comment โ Variation B: mov eax, 0; ret (6 bytes - Win32 BOOL)
- Line 5 stores the result in patchFalseEax. That value is what the following lines work with.
- Line 6 carries out the next step in that order.
- Line 7 carries out the next step in that order.
Change one value here and predict the result before running it. That is the fastest way to turn reading into understanding.
Terms used in this section
| Term | Meaning |
|---|
| 0x31 0xC0 (XOR) | Preferred by compilers; smaller footprint (2 bytes) and highest CPU throughput. |
| 0xB0 0x00 (MOV AL) | Only touches AL. Caution: Leaves garbage in upper 56 bits of RAX in 64-bit. |
0x31 0xC0 (XOR) means preferred by compilers; smaller footprint (2 bytes) and highest CPU throughput. The term comes up often, so it is worth learning its exact shape โ using a term loosely usually means picturing the concept loosely too.
By 0xB0 0x00 (MOV AL) we mean only touches AL. Caution: Leaves garbage in upper 56 bits of RAX in 64-bit. Note how narrow the definition is; a lot of confusion comes from people using this term more broadly than it is meant.
Note: Always prefer 'xor eax, eax' (0x31, 0xC0) over 'mov al, 0' to prevent dirty high-register bugs!
The note above looks small but it is often the whole of a debugging session. Treat it as a required step, not a suggestion.
In short, Alternative 'Return False' Variations is not about memorising syntax; it is about knowing what actually happens underneath.
5. Constructing NOP (0x90) Payloads
This section is about Constructing NOP (0x90) Payloads. NOP instructs the CPU to advance the Instruction Pointer without modifying any registers or flags. Written as one sentence it looks simple, but this is exactly where the difference shows between someone who memorised the steps and someone who knows why the steps are what they are.
The key points
0x90 is machine hardware shorthand for xchg eax, eax (a 1-cycle do-nothing operation).
The line "0x90 is machine hardware shorthand for xchg eax, eax (a 1-cycle do-nothing operation)" is worth reading slowly. It is not a formal rule to memorise; it is a picture of what actually happens when the code runs.
The practical value is here. Once you have "0x90 is machine hardware shorthand for xchg eax, eax (a 1-cycle do-nothing operation)" in hand, you can predict the result before running anything, and that is far faster than trial and error.
Skip this point and the symptoms usually do not show up straight away. The code still runs, the result still appears, and it is wrong โ and a bug that does not crash is the bug that takes longest to find.
Used to overwrite unwanted instructions without altering subsequent program alignment.
Used to overwrite unwanted instructions without altering subsequent program alignment. The wording is deliberately narrow so that it is not ambiguous โ widen it and you will immediately find cases that break it.
Why does this point matter? Because "Used to overwrite unwanted instructions without altering subsequent program alignment" is an assumption almost all the surrounding code makes silently. While it holds, everything runs; the moment it is broken, the failure shows up somewhere you would never think to look.
Breaking this point rarely produces a clear error message. What you get instead is behaviour that looks random but is perfectly consistent once you account for "Used to overwrite unwanted instructions without altering subsequent program alignment".
Here is the simplest form of what was just described. Read it whole first, then look at the line-by-line notes.
The code for this section (NopGenerator.cs):
<span class="token comment">// Single NOP (x86 & x64)</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name"><span class="token keyword">byte</span></span> Nop <span class="token operator">=</span> <span class="token number">0x90</span><span class="token punctuation">;</span>
<span class="token comment">// Helper to generate dynamic NOP sequences in C#</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token function">CreateNopArray</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> count<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> nops <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span></span><span class="token punctuation">[</span>count<span class="token punctuation">]</span><span class="token punctuation">;</span>
Array<span class="token punctuation">.</span><span class="token function">Fill</span><span class="token punctuation">(</span>nops<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token keyword">byte</span><span class="token punctuation">)</span><span class="token number">0x90</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> nops<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
Reading the code line by line
- Line 1 is a comment โ Single NOP (x86 & x64)
- Line 2 defines x90; this is the part you call from elsewhere.
- Line 4 is a comment โ Helper to generate dynamic NOP sequences in C#
- Line 5 defines CreateNopArray; this is the part you call from elsewhere.
- Line 6 stores the result in nops. That value is what the following lines work with.
- Line 7 calls Array.Fill and works with what it hands back.
- Line 8 hands the result back to the caller and closes this part off.
Change one value here and predict the result before running it. That is the fastest way to turn reading into understanding.
The numbers
| Value | What it measures |
|---|
| 0x90 | Opcode Byte |
| 1 Cycle | CPU Latency |
| Dynamic | Array Size |
On opcode Byte: 0x90. The point is not the number but that the gap is wide enough to feel in everyday use.
The figure 1 Cycle for cPU Latency is a measurement under particular conditions, not a promise. What you can take from it is the order of magnitude, not the exact digits.
Dynamic on array Size gives a sense of how large the difference is. Measure it again in your own case before basing a decision on it.
Common mistakes
- Skipping "0x90 is machine hardware shorthand for xchg eax, eax (a 1-cycle do-nothing operation)" because it looks trivial, then coming back to it after hunting for the cause somewhere else.
- Concluding something is right just because the result looks right, without confirming "Used to overwrite unwanted instructions without altering subsequent program alignment" first.
How to check you have it
- If the conditions change, do you know which parts move with them because of "0x90 is machine hardware shorthand for xchg eax, eax (a 1-cycle do-nothing operation)"?
- Have you tried a case that deliberately breaks "Used to overwrite unwanted instructions without altering subsequent program alignment", and seen the difference clearly?
In short, Constructing NOP (0x90) Payloads is not about memorising syntax; it is about knowing what actually happens underneath.
6. When and How to Apply NOP in C++
Now the part people skip when they are learning in a hurry: When and How to Apply NOP in C++. NOP is primarily used to erase conditional branches, security checks, and sub-calls. Yet once it is clear, the rest of the material gets much easier to follow.
Terms used in this section
| Term | Meaning |
|---|
| Bypassing Jumps (JE / JNE) | Short jumps (0x74 / 0x75) take 2 bytes. Overwrite both with 0x90, 0x90 to fall through. |
| Disabling CALL Instructions | Relative CALL (0xE8 xx xx xx xx) is 5 bytes. Overwrite with 5x 0x90 to suppress execution. |
| Neutralizing Anti-Tamper | Wiping memory integrity loops or anti-cheat heartbeat triggers in-place. |
Bypassing Jumps (JE / JNE): Short jumps (0x74 / 0x75) take 2 bytes. Overwrite both with 0x90, 0x90 to fall through. You will meet it again in later sections, usually without a second explanation.
The term Disabling CALL Instructions is used for relative CALL (0xE8 xx xx xx xx) is 5 bytes. Overwrite with 5x 0x90 to suppress execution. If you find it elsewhere meaning something else, the context is probably different rather than the definition wrong.
Neutralizing Anti-Tamper means wiping memory integrity loops or anti-cheat heartbeat triggers in-place. The term comes up often, so it is worth learning its exact shape โ using a term loosely usually means picturing the concept loosely too.
Note: CRITICAL: You must NOP the exact byte length of the instruction being replaced. Replacing 5 bytes with 4 bytes causes invalid opcode crashes (0xC000001D)!
The note above looks small but it is often the whole of a debugging session. Treat it as a required step, not a suggestion.
Before moving on, make sure you are comfortable with When and How to Apply NOP in C++. The next section is built directly on top of it.
7. The Calling Convention Pitfall (x86 vs x64)
This section is about The Calling Convention Pitfall (x86 vs x64). Stack cleanup rules dictate whether plain 'ret' (0xC3) is sufficient or dangerous. Written as one sentence it looks simple, but this is exactly where the difference shows between someone who memorised the steps and someone who knows why the steps are what they are.
Here is the simplest form of what was just described. Read it whole first, then look at the line-by-line notes.
The code for this section (StdcallPatch.cs):
<span class="token comment">// x86 __stdcall with 1 pointer parameter (4 bytes on stack):</span>
<span class="token comment">// Assembly: mov al, 1; ret 4</span>
<span class="token class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> patchStdcallTrue <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span>
<span class="token number">0xB0</span><span class="token punctuation">,</span> <span class="token number">0x01</span><span class="token punctuation">,</span> <span class="token comment">// mov al, 1</span>
<span class="token number">0xC2</span><span class="token punctuation">,</span> <span class="token number">0x04</span><span class="token punctuation">,</span> <span class="token number">0x00</span> <span class="token comment">// ret 4 (0x0004 bytes popped)</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
Reading the code line by line
- Line 1 is a comment โ x86 __stdcall with 1 pointer parameter (4 bytes on stack):
- Line 2 is a comment โ Assembly: mov al, 1; ret 4
- Line 3 stores the result in patchStdcallTrue. That value is what the following lines work with.
- Line 4 carries out the next step in that order.
- Line 5 carries out the next step in that order.
- Line 6 carries out the next step in that order.
Change one value here and predict the result before running it. That is the fastest way to turn reading into understanding.
Terms used in this section
| Term | Meaning |
|---|
| x64 (Windows / Linux Fastcall) | Caller always cleans the stack frame. 'ret' (0xC3) is universally valid for all functions. |
| x86 __cdecl | Caller cleans stack. Plain 'ret' (0xC3) works safely. |
| x86 __stdcall / __thiscall | Callee must clean stack! Requires 'ret N' (0xC2, argBytesLow, argBytesHigh). |
x64 (Windows / Linux Fastcall): Caller always cleans the stack frame. 'ret' (0xC3) is universally valid for all functions. You will meet it again in later sections, usually without a second explanation.
The term x86 __cdecl is used for caller cleans stack. Plain 'ret' (0xC3) works safely. If you find it elsewhere meaning something else, the context is probably different rather than the definition wrong.
x86 __stdcall / __thiscall means callee must clean stack! Requires 'ret N' (0xC2, argBytesLow, argBytesHigh). The term comes up often, so it is worth learning its exact shape โ using a term loosely usually means picturing the concept loosely too.
Note: In 32-bit C++ target games, using 0xC3 on a __stdcall function desynchronizes the ESP stack pointer, crashing on return!
The note above looks small but it is often the whole of a debugging session. Treat it as a required step, not a suggestion.
In short, The Calling Convention Pitfall (x86 vs x64) is not about memorising syntax; it is about knowing what actually happens underneath.
8. Step 1: Unlocking Protected Memory
Before the detail, the whole of Step 1: Unlocking Protected Memory comes down to one sentence: Executable native memory (.text section) is marked PAGE_EXECUTE_READ. Direct writes cause Access Violation. Everything below is an unpacking of that sentence.
The key points
VirtualProtect temporarily changes memory page attributes to read-write-execute (0x40).
The heart of this point is one sentence. VirtualProtect temporarily changes memory page attributes to read-write-execute (0x40). Resist the urge to jump straight to practice; make sure it is clear, because the rest of this section assumes it.
This point saves debugging time. Someone who already holds "VirtualProtect temporarily changes memory page attributes to read-write-execute (0x40)" can narrow the search immediately, while someone who does not will guess one thing at a time.
This is a classic source of bugs. Someone copies code that worked under one set of conditions, uses it under another without checking "VirtualProtect temporarily changes memory page attributes to read-write-execute (0x40)", and spends hours looking in the wrong place.
Must save lpflOldProtect to restore original page protections after writing.
Must save lpflOldProtect to restore original page protections after writing. It sounds like a small technical detail, but that sentence decides how everything around it behaves. Read it wrong and every conclusion after it is off too.
It is on the list because it separates a solution that happens to work from one that is actually right. And a solution that is right stays right after the code changes.
The real risk is not that the code fails; it is that the code succeeds for the wrong reason. Without confirming "Must save lpflOldProtect to restore original page protections after writing", you have no way to tell which one is happening.
Leaving pages as PAGE_EXECUTE_READWRITE permanently is an easy signature for integrity scanners.
The line "Leaving pages as PAGE_EXECUTE_READWRITE permanently is an easy signature for integrity scanners" is worth reading slowly. It is not a formal rule to memorise; it is a picture of what actually happens when the code runs.
The practical value is here. Once you have "Leaving pages as PAGE_EXECUTE_READWRITE permanently is an easy signature for integrity scanners" in hand, you can predict the result before running anything, and that is far faster than trial and error.
Skip this point and the symptoms usually do not show up straight away. The code still runs, the result still appears, and it is wrong โ and a bug that does not crash is the bug that takes longest to find.
Here is the simplest form of what was just described. Read it whole first, then look at the line-by-line notes.
The code for this section (MemoryProtection.cs):
<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">DllImport</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"kernel32.dll"</span><span class="token punctuation">,</span> SetLastError <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">VirtualProtect</span><span class="token punctuation">(</span>
<span class="token class-name">IntPtr</span> lpAddress<span class="token punctuation">,</span> <span class="token class-name">UIntPtr</span> dwSize<span class="token punctuation">,</span>
<span class="token class-name"><span class="token keyword">uint</span></span> flNewProtect<span class="token punctuation">,</span> <span class="token keyword">out</span> <span class="token class-name"><span class="token keyword">uint</span></span> lpflOldProtect<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token class-name"><span class="token keyword">uint</span></span> PAGE_EXECUTE_READWRITE <span class="token operator">=</span> <span class="token number">0x40</span><span class="token punctuation">;</span>
Reading the code line by line
- Line 1 calls DllImport and works with what it hands back.
- Line 2 defines VirtualProtect; this is the part you call from elsewhere.
- Line 3 carries out the next step in that order.
- Line 4 carries out the next step in that order.
- Line 6 stores the result in PAGE_EXECUTE_READWRITE. That value is what the following lines work with.
Change one value here and predict the result before running it. That is the fastest way to turn reading into understanding.
Common mistakes
- Concluding something is right just because the result looks right, without confirming "VirtualProtect temporarily changes memory page attributes to read-write-execute (0x40)" first.
- Assuming "Must save lpflOldProtect to restore original page protections after writing" holds forever. Conditions move, and nothing announces it when they do.
- Copying this part from another example without checking whether "Leaving pages as PAGE_EXECUTE_READWRITE permanently is an easy signature for integrity scanners" is also true in your case. Examples are always written for particular conditions.
How to check you have it
- Could you rewrite this part from scratch without notes?
- Can you explain the reasoning behind "Must save lpflOldProtect to restore original page protections after writing", rather than just state that it is so?
- If the conditions change, do you know which parts move with them because of "Leaving pages as PAGE_EXECUTE_READWRITE permanently is an easy signature for integrity scanners"?
In short, Step 1: Unlocking Protected Memory is not about memorising syntax; it is about knowing what actually happens underneath.
9. Step 2: Writing the Byte Array Safely
This section is about Step 2: Writing the Byte Array Safely. Applying the C# byte array to the native target address using Marshal.Copy. Written as one sentence it looks simple, but this is exactly where the difference shows between someone who memorised the steps and someone who knows why the steps are what they are.
The key points
Marshal.Copy is built into .NET BCL and highly optimized via SIMD/memcpy.
Marshal.Copy is built into .NET BCL and highly optimized via SIMD/memcpy. The wording is deliberately narrow so that it is not ambiguous โ widen it and you will immediately find cases that break it.
Why does this point matter? Because "Marshal.Copy is built into .NET BCL and highly optimized via SIMD/memcpy" is an assumption almost all the surrounding code makes silently. While it holds, everything runs; the moment it is broken, the failure shows up somewhere you would never think to look.
Breaking this point rarely produces a clear error message. What you get instead is behaviour that looks random but is perfectly consistent once you account for "Marshal.Copy is built into .NET BCL and highly optimized via SIMD/memcpy".
No unsafe compiler flag or pointer pinning (fixed) strictly required.
The heart of this point is one sentence. No unsafe compiler flag or pointer pinning (fixed) strictly required. Resist the urge to jump straight to practice; make sure it is clear, because the rest of this section assumes it.
This point saves debugging time. Someone who already holds "No unsafe compiler flag or pointer pinning (fixed) strictly required" can narrow the search immediately, while someone who does not will guess one thing at a time.
This is a classic source of bugs. Someone copies code that worked under one set of conditions, uses it under another without checking "No unsafe compiler flag or pointer pinning (fixed) strictly required", and spends hours looking in the wrong place.
Here is the simplest form of what was just described. Read it whole first, then look at the line-by-line notes.
The code for this section (WriteBytes.cs):
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">WriteNativeBytes</span><span class="token punctuation">(</span><span class="token class-name">IntPtr</span> target<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> patch<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token class-name">UIntPtr</span> size <span class="token operator">=</span> <span class="token punctuation">(</span>UIntPtr<span class="token punctuation">)</span>patch<span class="token punctuation">.</span>Length<span class="token punctuation">;</span>
<span class="token comment">// 1. Make memory writable</span>
<span class="token function">VirtualProtect</span><span class="token punctuation">(</span>target<span class="token punctuation">,</span> size<span class="token punctuation">,</span> <span class="token number">0x40</span><span class="token punctuation">,</span> <span class="token keyword">out</span> <span class="token class-name"><span class="token keyword">uint</span></span> oldProtect<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// 2. Copy managed byte[] to native unmanaged pointer</span>
Marshal<span class="token punctuation">.</span><span class="token function">Copy</span><span class="token punctuation">(</span>patch<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> target<span class="token punctuation">,</span> patch<span class="token punctuation">.</span>Length<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// 3. Restore original protection</span>
<span class="token function">VirtualProtect</span><span class="token punctuation">(</span>target<span class="token punctuation">,</span> size<span class="token punctuation">,</span> oldProtect<span class="token punctuation">,</span> <span class="token keyword">out</span> _<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
Reading the code line by line
- Line 1 defines WriteNativeBytes; this is the part you call from elsewhere.
- Line 2 stores the result in size. That value is what the following lines work with.
- Line 3 is a comment โ 1. Make memory writable
- Line 4 calls VirtualProtect and works with what it hands back.
- Line 5 is a comment โ 2. Copy managed byte[] to native unmanaged pointer
- Line 6 calls Marshal.Copy and works with what it hands back.
- Line 7 is a comment โ 3. Restore original protection
- Line 8 calls VirtualProtect and works with what it hands back.
Change one value here and predict the result before running it. That is the fastest way to turn reading into understanding.
Common mistakes
- Skipping "Marshal.Copy is built into .NET BCL and highly optimized via SIMD/memcpy" because it looks trivial, then coming back to it after hunting for the cause somewhere else.
- Concluding something is right just because the result looks right, without confirming "No unsafe compiler flag or pointer pinning (fixed) strictly required" first.
How to check you have it
- Could you rewrite this part from scratch without notes?
- Can you explain the reasoning behind "No unsafe compiler flag or pointer pinning (fixed) strictly required", rather than just state that it is so?
Before moving on, make sure you are comfortable with Step 2: Writing the Byte Array Safely. The next section is built directly on top of it.
10. Step 3: Flushing the CPU Instruction Cache
Now the part people skip when they are learning in a hurry: Step 3: Flushing the CPU Instruction Cache. The #1 reason native patches randomly fail: CPU Harvard Architecture incoherency. Yet once it is clear, the rest of the material gets much easier to follow.
Here is the simplest form of what was just described. Read it whole first, then look at the line-by-line notes.
The code for this section (FlushCache.cs):
<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">DllImport</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"kernel32.dll"</span><span class="token punctuation">,</span> SetLastError <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">FlushInstructionCache</span><span class="token punctuation">(</span>
<span class="token class-name">IntPtr</span> hProcess<span class="token punctuation">,</span> <span class="token class-name">IntPtr</span> lpBaseAddress<span class="token punctuation">,</span> <span class="token class-name">UIntPtr</span> dwSize<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Call immediately after restoring memory protection:</span>
<span class="token function">FlushInstructionCache</span><span class="token punctuation">(</span><span class="token function">GetCurrentProcess</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> target<span class="token punctuation">,</span> <span class="token punctuation">(</span>UIntPtr<span class="token punctuation">)</span>patch<span class="token punctuation">.</span>Length<span class="token punctuation">)</span><span class="token punctuation">;</span>
Reading the code line by line
- Line 1 calls DllImport and works with what it hands back.
- Line 2 defines FlushInstructionCache; this is the part you call from elsewhere.
- Line 3 carries out the next step in that order.
- Line 5 is a comment โ Call immediately after restoring memory protection:
- Line 6 calls FlushInstructionCache and works with what it hands back.
Change one value here and predict the result before running it. That is the fastest way to turn reading into understanding.
Terms used in this section
| Term | Meaning |
|---|
| L1 Data Cache (D-Cache) | Where Marshal.Copy writes modified bytes. |
| L1 Instruction Cache (I-Cache) | Where CPU fetches opcodes to execute. |
| Cache Incoherency | Without Flush, CPU core executes stale instructions already in I-Cache! |
By L1 Data Cache (D-Cache) we mean where Marshal.Copy writes modified bytes. Note how narrow the definition is; a lot of confusion comes from people using this term more broadly than it is meant.
L1 Instruction Cache (I-Cache): Where CPU fetches opcodes to execute. You will meet it again in later sections, usually without a second explanation.
The term Cache Incoherency is used for without Flush, CPU core executes stale instructions already in I-Cache. If you find it elsewhere meaning something else, the context is probably different rather than the definition wrong.
Before moving on, make sure you are comfortable with Step 3: Flushing the CPU Instruction Cache. The next section is built directly on top of it.
11. Production-Ready Native Patch Class
Before the detail, the whole of Production-Ready Native Patch Class comes down to one sentence: Encapsulate patch state, automated byte backups, and toggle functionality in clean C#. Everything below is an unpacking of that sentence.
The key points
Reads and stores original bytes before applying the patch for 100% clean rollback.
Reads and stores original bytes before applying the patch for 100% clean rollback. It sounds like a small technical detail, but that sentence decides how everything around it behaves. Read it wrong and every conclusion after it is off too.
It is on the list because it separates a solution that happens to work from one that is actually right. And a solution that is right stays right after the code changes.
The real risk is not that the code fails; it is that the code succeeds for the wrong reason. Without confirming "Reads and stores original bytes before applying the patch for 100% clean rollback", you have no way to tell which one is happening.
Allows runtime feature toggles (hotkeys, config switches) without restarting the target process.
The line "Allows runtime feature toggles (hotkeys, config switches) without restarting the target process" is worth reading slowly. It is not a formal rule to memorise; it is a picture of what actually happens when the code runs.
The practical value is here. Once you have "Allows runtime feature toggles (hotkeys, config switches) without restarting the target process" in hand, you can predict the result before running anything, and that is far faster than trial and error.
Skip this point and the symptoms usually do not show up straight away. The code still runs, the result still appears, and it is wrong โ and a bug that does not crash is the bug that takes longest to find.
Facilitates multi-patch management and orderly unpatching upon application shutdown.
Facilitates multi-patch management and orderly unpatching upon application shutdown. The wording is deliberately narrow so that it is not ambiguous โ widen it and you will immediately find cases that break it.
Why does this point matter? Because "Facilitates multi-patch management and orderly unpatching upon application shutdown" is an assumption almost all the surrounding code makes silently. While it holds, everything runs; the moment it is broken, the failure shows up somewhere you would never think to look.
Breaking this point rarely produces a clear error message. What you get instead is behaviour that looks random but is perfectly consistent once you account for "Facilitates multi-patch management and orderly unpatching upon application shutdown".
Here is the simplest form of what was just described. Read it whole first, then look at the line-by-line notes.
The code for this section (NativeMemoryPatch.cs):
<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">NativeMemoryPatch</span> <span class="token punctuation">{</span>
<span class="token keyword">public</span> <span class="token return-type class-name">IntPtr</span> Address <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> Original <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> Patch <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> IsApplied <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">private</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">Enable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">Apply</span><span class="token punctuation">(</span>Patch<span class="token punctuation">)</span><span class="token punctuation">;</span> IsApplied <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">Disable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">Apply</span><span class="token punctuation">(</span>Original<span class="token punctuation">)</span><span class="token punctuation">;</span> IsApplied <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
Reading the code line by line
- Line 1 defines NativeMemoryPatch; this is the part you call from elsewhere.
- Line 2 defines get; this is the part you call from elsewhere.
- Line 3 defines get; this is the part you call from elsewhere.
- Line 4 defines get; this is the part you call from elsewhere.
- Line 5 defines set; this is the part you call from elsewhere.
- Line 6 defines Enable; this is the part you call from elsewhere.
- Line 7 defines Disable; this is the part you call from elsewhere.
Change one value here and predict the result before running it. That is the fastest way to turn reading into understanding.
Common mistakes
- Copying this part from another example without checking whether "Reads and stores original bytes before applying the patch for 100% clean rollback" is also true in your case. Examples are always written for particular conditions.
- Skipping "Allows runtime feature toggles (hotkeys, config switches) without restarting the target process" because it looks trivial, then coming back to it after hunting for the cause somewhere else.
- Concluding something is right just because the result looks right, without confirming "Facilitates multi-patch management and orderly unpatching upon application shutdown" first.
How to check you have it
- Have you tried a case that deliberately breaks "Reads and stores original bytes before applying the patch for 100% clean rollback", and seen the difference clearly?
- Could you rewrite this part from scratch without notes?
- Can you explain the reasoning behind "Facilitates multi-patch management and orderly unpatching upon application shutdown", rather than just state that it is so?
In short, Production-Ready Native Patch Class is not about memorising syntax; it is about knowing what actually happens underneath.
12. Quick Reference: C# Native Patching Opcodes
Before the detail, the whole of Quick Reference: C# Native Patching Opcodes comes down to one sentence: Master summary of machine opcodes and C# byte arrays for native C++ patching. Everything below is an unpacking of that sentence.
Terms used in this section
| Term | Meaning |
|---|
| Return True (3b) | new byte[] { 0xB0, 0x01, 0xC3 } // mov al, 1; ret. |
| Return False (3b) | new byte[] { 0x31, 0xC0, 0xC3 } // xor eax, eax; ret. |
| NOP Pad (1b) | new byte[] { 0x90 } // No Operation. |
| x86 __stdcall True (5b) | new byte[] { 0xB0, 0x01, 0xC2, 0x04, 0x00 } // ret 4. |
| Golden Pipeline | VirtualProtect -> Marshal.Copy -> RestoreProtect -> FlushInstructionCache. |
By Return True (3b) we mean new byte[] { 0xB0, 0x01, 0xC3 } // mov al, 1; ret. Note how narrow the definition is; a lot of confusion comes from people using this term more broadly than it is meant.
Return False (3b): new byte[] { 0x31, 0xC0, 0xC3 } // xor eax, eax; ret. You will meet it again in later sections, usually without a second explanation.
The term NOP Pad (1b) is used for new byte[] { 0x90 } // No Operation. If you find it elsewhere meaning something else, the context is probably different rather than the definition wrong.
x86 __stdcall True (5b) means new byte[] { 0xB0, 0x01, 0xC2, 0x04, 0x00 } // ret 4. The term comes up often, so it is worth learning its exact shape โ using a term loosely usually means picturing the concept loosely too.
By Golden Pipeline we mean virtualProtect -> Marshal.Copy -> RestoreProtect -> FlushInstructionCache. Note how narrow the definition is; a lot of confusion comes from people using this term more broadly than it is meant.
Note: Keep this reference at hand when developing mods, reverse engineering native libraries, or writing unit tests against native binaries!
The note above looks small but it is often the whole of a debugging session. Treat it as a required step, not a suggestion.
By now Quick Reference: C# Native Patching Opcodes should feel reasonable. If it does not, reread the first point โ that is usually where the gap is.
Glossary
| Term | Meaning |
|---|
| IL2CPP / Native DLLs | C++ code compiles directly to raw machine instructions without CLR metadata. |
| In-Memory Patching | Modifying executable bytes (.text section) at runtime to alter function logic. |
| Byte Array Payloads | C# byte[] arrays represent the exact x86/x64 machine opcodes written to RAM. |
| 0x31 0xC0 (XOR) | Preferred by compilers; smaller footprint (2 bytes) and highest CPU throughput. |
| 0xB0 0x00 (MOV AL) | Only touches AL. Caution: Leaves garbage in upper 56 bits of RAX in 64-bit. |
| Bypassing Jumps (JE / JNE) | Short jumps (0x74 / 0x75) take 2 bytes. Overwrite both with 0x90, 0x90 to fall through. |
| Disabling CALL Instructions | Relative CALL (0xE8 xx xx xx xx) is 5 bytes. Overwrite with 5x 0x90 to suppress execution. |
| Neutralizing Anti-Tamper | Wiping memory integrity loops or anti-cheat heartbeat triggers in-place. |
| x64 (Windows / Linux Fastcall) | Caller always cleans the stack frame. 'ret' (0xC3) is universally valid for all functions. |
| x86 __cdecl | Caller cleans stack. Plain 'ret' (0xC3) works safely. |
| x86 __stdcall / __thiscall | Callee must clean stack! Requires 'ret N' (0xC2, argBytesLow, argBytesHigh). |
| L1 Data Cache (D-Cache) | Where Marshal.Copy writes modified bytes. |
| L1 Instruction Cache (I-Cache) | Where CPU fetches opcodes to execute. |
| Cache Incoherency | Without Flush, CPU core executes stale instructions already in I-Cache! |
| Return True (3b) | new byte[] { 0xB0, 0x01, 0xC3 } // mov al, 1; ret. |
| Return False (3b) | new byte[] { 0x31, 0xC0, 0xC3 } // xor eax, eax; ret. |
| NOP Pad (1b) | new byte[] { 0x90 } // No Operation. |
| x86 __stdcall True (5b) | new byte[] { 0xB0, 0x01, 0xC2, 0x04, 0x00 } // ret 4. |
| Golden Pipeline | VirtualProtect -> Marshal.Copy -> RestoreProtect -> FlushInstructionCache. |
Frequently asked
Why C# Needs Native Byte Arrays?
In short: In modding frameworks (MelonLoader, BepInEx) and memory injectors, C# controls native C++ engines. The Why C# Needs Native Byte Arrays section above covers it fully, including the cases that change the answer.
Why does Constructing 'Return True' (x86 & x64) matter?
The short answer is in the Constructing 'Return True' (x86 & x64) section. In the C/C++ ABI, boolean return values reside in the lowest byte of the accumulator (AL register). The long one needs some context, and that context is in the points there.
How do I get started with Constructing 'Return False' (x86 & x64)?
XORing a register with itself is the fastest, cleanest standard to clear return values. That holds for most cases; the Constructing 'Return False' (x86 & x64) section explains when the answer needs adjusting.
What does Alternative 'Return False' Variations actually mean?
It is a fair question, and the answer is not one sentence. Comparing direct move vs XOR techniques across different native return types. Read the Alternative 'Return False' Variations section for the full version.
Why does Constructing NOP (0x90) Payloads matter?
In short: NOP instructs the CPU to advance the Instruction Pointer without modifying any registers or flags. The Constructing NOP (0x90) Payloads section above covers it fully, including the cases that change the answer.
When and How to Apply NOP in C++?
The short answer is in the When and How to Apply NOP in C++ section. NOP is primarily used to erase conditional branches, security checks, and sub-calls. The long one needs some context, and that context is in the points there.
What does The Calling Convention Pitfall (x86 vs x64) actually mean?
Stack cleanup rules dictate whether plain 'ret' (0xC3) is sufficient or dangerous. That holds for most cases; the The Calling Convention Pitfall (x86 vs x64) section explains when the answer needs adjusting.
Why does Step 1: Unlocking Protected Memory matter?
It is a fair question, and the answer is not one sentence. Executable native memory (.text section) is marked PAGE_EXECUTE_READ. Direct writes cause Access Violation. Read the Step 1: Unlocking Protected Memory section for the full version.
How do I get started with Step 2: Writing the Byte Array Safely?
In short: Applying the C# byte array to the native target address using Marshal.Copy. The Step 2: Writing the Byte Array Safely section above covers it fully, including the cases that change the answer.
What does Step 3: Flushing the CPU Instruction Cache actually mean?
The short answer is in the Step 3: Flushing the CPU Instruction Cache section. The #1 reason native patches randomly fail: CPU Harvard Architecture incoherency. The long one needs some context, and that context is in the points there.
Why does Production-Ready Native Patch Class matter?
Encapsulate patch state, automated byte backups, and toggle functionality in clean C#. That holds for most cases; the Production-Ready Native Patch Class section explains when the answer needs adjusting.
How do I get started with Quick Reference: C# Native Patching Opcodes?
It is a fair question, and the answer is not one sentence. Master summary of machine opcodes and C# byte arrays for native C++ patching. Read the Quick Reference: C# Native Patching Opcodes section for the full version.
Recap
- Why C# Needs Native Byte Arrays โ In modding frameworks (MelonLoader, BepInEx) and memory injectors, C# controls native C++ engines.
- Constructing 'Return True' (x86 & x64): In the C/C++ ABI, boolean return values reside in the lowest byte of the accumulator (AL register).
- Constructing 'Return False' (x86 & x64) โ XORing a register with itself is the fastest, cleanest standard to clear return values.
- Alternative 'Return False' Variations: Comparing direct move vs XOR techniques across different native return types.
- Constructing NOP (0x90) Payloads โ NOP instructs the CPU to advance the Instruction Pointer without modifying any registers or flags.
- When and How to Apply NOP in C++: NOP is primarily used to erase conditional branches, security checks, and sub-calls.
- The Calling Convention Pitfall (x86 vs x64) โ Stack cleanup rules dictate whether plain 'ret' (0xC3) is sufficient or dangerous.
- Step 1: Unlocking Protected Memory: Executable native memory (.text section) is marked PAGE_EXECUTE_READ. Direct writes cause Access Violation.
- Step 2: Writing the Byte Array Safely โ Applying the C# byte array to the native target address using Marshal.Copy.
- Step 3: Flushing the CPU Instruction Cache: The #1 reason native patches randomly fail: CPU Harvard Architecture incoherency.
- Production-Ready Native Patch Class โ Encapsulate patch state, automated byte backups, and toggle functionality in clean C#.
- Quick Reference: C# Native Patching Opcodes: Master summary of machine opcodes and C# byte arrays for native C++ patching.
What to take away
- Understand the idea before you copy the code; code you do not understand is code you cannot fix when it breaks.
- Change one thing at a time so you know exactly which change moved the result.
- Write down the steps that worked โ a short note today saves hours of debugging next month.
- If a section does not make sense yet, do not skip it; the next one is usually built on top of it.
Wrapping up
If this helped, save it so you can find it again when you need it. Still stuck on a part of it? Leave a comment โ the questions that keep coming back tend to become the next article.
Follow @unreliablecode for more on reverse engineering, coding and the tools behind them.
Discussie (0)