Skip to content

Instantly share code, notes, and snippets.

@PathogenDavid
Last active August 4, 2023 08:36
Show Gist options
  • Save PathogenDavid/ef2ba4f4175698e811fa18c4336472f7 to your computer and use it in GitHub Desktop.
Save PathogenDavid/ef2ba4f4175698e811fa18c4336472f7 to your computer and use it in GitHub Desktop.
Crimes against the .NET Runtime -- Solution to this challenge on the C# Discord: https://discord.com/channels/143867839282020352/578057213084434433/943250054884515840

Crimes Against the .NET Runtime

Follows is my solution to @jaredpar's challenge shared by @333fred on the C# Discord:

Alright, code challenge for today, courtesy of @jaredpar. Write a definition of C such that this null-refs on the call to local.ToString(), and not before:

using System;

C local = null;
if (local != null && local.Prop) {
    Console.WriteLine(local.ToString()); // Null ref on this line
}

The C# Discord maintains a leaderboard of most evil solutions to this puzzle. I wrote the following solution with the goal of being excessively evil. The solution requires running under x64. I only tested .NET 6.0.200 and .NET 7.0.100-preview.1.22114.7. Both debug and release should work, launched from either Visual Studio or dotnet on either Windows or Linux. Needless to say you should never do anything even remotely resembling this in a project that matters.

I'm considering making a detailed video explaining how this works, if you're interested be sure to let me know on Twitter.

using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
C local = null;
if (local != null && local.Prop)
{
Console.WriteLine(local.ToString()); // Null ref on this line
}
#pragma warning disable CS0660
#pragma warning disable CS0661
sealed class C
{
public static bool operator ==(C a, object b)
=> true;
public bool Prop => true;
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool operator !=(C a, object b)
{
// Call DoCrime in a separate stack frame to prevent what I think is maybe probably a stack cookie from blowing away ECX when we return
DoCrime();
// Make sure ecx is 0
MakeSureEcxIs0(0);
return true;
}
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
private unsafe static void DoCrime()
{
// Fred said I could.
if (RuntimeInformation.ProcessArchitecture != Architecture.X64)
{ throw new Exception("This evil is platform-dependent!"); }
// Get the start of Main
RuntimeMethodHandle mainHandle = Assembly.GetEntryAssembly().EntryPoint.MethodHandle;
RuntimeHelpers.PrepareMethod(mainHandle);
byte* rip = (byte*)mainHandle.GetFunctionPointer();
// Decode relative `jmp`
// (Not 100% sure if the function pointer will always point to the trampoline so only do this conditionally.)
if (*rip == 0xE9)
{
int* rel32 = (int*)(rip + 1);
rip = (byte*)(rel32 + 1) + *rel32;
}
// Handle what I'm *pretty* sure is the JIT tracking for tiered JITting
// (This only appears sometimes for reasons I'm not entirely sure of.)
if (*rip == 0x48 && *(rip + 1) == 0xB8)
{
// Skip `mov rax, AddressOfCounterOrWhatever`
rip += 10;
// Skip `dec word ptr [rax]`
if (*rip != 0x66 || *(rip + 1) != 0xFF || *(rip + 2) != 0x08)
{ throw new Exception("Turns out relying on the implementation details of the JIT is a horrible idea."); }
rip += 3;
// Decode conditional relative `jne`
if (*rip != 0x0F && *(rip + 1) != 0x85)
{ throw new Exception("The JIT has forsaken us."); }
int* rel32 = (int*)(rip + 2);
rip = (byte*)(rel32 + 1) + *rel32;
}
// We now have the sart of Main, save this for later
byte* startOfMain = rip;
// Find `cmp [ecx], ecx`, the null check of `local` in Main for `local.Prop`
while (*rip != 0x39 && *(rip + 1) != 0x09)
rip++;
// Move to after that instruction
rip++;
// Find the next `cmp [ecx], ecx`, the null check for `local.ToString()`
// We can avoid this and just execute `rip` right now with a valid C in ECX, but that sometimes results in an access violation
// when `local.ToString` is executed instead of NRE depending on where the JIT put `local`.
while (*rip != 0x39 && *(rip + 1) != 0x09)
rip++;
// Search for the return address on the stack by looking for a code address not within the range of Main we're looking at
byte** stackPointer = stackalloc byte*[1];
do
{
stackPointer++;
} while (*stackPointer <= startOfMain || *stackPointer >= rip); // Don't look for the values of startOfMain or rip exactly, we'll just find those locals
// Replace the return address so we skip to the null check for `local.ToString`
*stackPointer = rip;
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
static void MakeSureEcxIs0(nint x)
{ }
// Replace ToString with a non-virtual method to simplify our nonsense
[MethodImpl(MethodImplOptions.NoInlining)]
public new string ToString()
=> ":)";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment