|
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() |
|
=> ":)"; |
|
} |