Last active
December 10, 2023 21:27
-
-
Save Porges/3a0ae75bf64ae18259496078d813de57 to your computer and use it in GitHub Desktop.
On a mission to write the worst code ever
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.ComponentModel; | |
using System.Diagnostics; | |
using System.Reflection; | |
using System.Runtime.CompilerServices; | |
using System.Runtime.InteropServices; | |
using Microsoft.Win32.SafeHandles; | |
[DllImport("Kernel32.dll")] | |
static extern uint GetCurrentThreadId(); | |
[DllImport("Kernel32.dll", SetLastError = true)] | |
static extern ThreadHandle OpenThread(uint access, [MarshalAs(UnmanagedType.Bool)] bool inherit, int id); | |
[DllImport("Ntdll.dll")] | |
unsafe static extern int NtQueryInformationThread(SafeHandle handle, uint informationClass, out THREAD_BASIC_INFORMATION info, uint size, uint* returnedSize = null); | |
[DllImport("Kernel32.dll", SetLastError = true)] | |
static extern uint SuspendThread(SafeHandle handle); | |
[DllImport("Kernel32.dll", SetLastError = true)] | |
static extern uint ResumeThread(SafeHandle handle); | |
[DllImport("Kernel32.dll", SetLastError = true)] | |
static extern nuint VirtualQuery(nuint address, out MEMORY_BASIC_INFORMATION info, nuint length); | |
var p1 = new Person("Evil", "Doer"); | |
var p2 = new Person("Foul", "Enabler", p1); | |
var list = new[] { p1, p2 }; | |
// try it with this to get duplication | |
//GC.Collect(GC.MaxGeneration); | |
//GC.WaitForPendingFinalizers(); | |
var threads = Process.GetCurrentProcess().Threads; | |
// suspend all threads and collect their stack allocation bases | |
// so we can avoid scanning their stacks | |
// note that this will also break any debugger attached | |
var stacks = SuspendAll(threads); | |
try | |
{ | |
EnumerateMemory<Person>(stacks); | |
} | |
finally | |
{ | |
ResumeAll(threads); | |
} | |
GC.KeepAlive(list); | |
unsafe static nuint GetStackAddress(SafeHandle handle) | |
{ | |
THREAD_BASIC_INFORMATION info = new(); | |
var result = NtQueryInformationThread(handle, 0, out info, (uint)sizeof(THREAD_BASIC_INFORMATION)); | |
if (result < 0) | |
{ | |
throw new Win32Exception("can't execute NtQueryInformationThread"); | |
} | |
return (nuint)(((NT_TIB*)info.TebBaseAddress)->StackLimit); | |
} | |
unsafe static List<nuint> SuspendAll(ProcessThreadCollection threads) | |
{ | |
var currentId = GetCurrentThreadId(); | |
var stacks = new List<nuint>(); | |
for (int i = 0; i < threads.Count; ++i) | |
{ | |
var id = threads[i].Id; | |
if (id == currentId) | |
{ | |
continue; | |
} | |
using var handle = OpenThread(2097151 /*ALL ACCESS*/, false, id); | |
if (handle.IsInvalid) | |
{ | |
throw new Win32Exception(); | |
} | |
if (SuspendThread(handle) == uint.MaxValue) | |
{ | |
throw new Win32Exception(); | |
} | |
MEMORY_BASIC_INFORMATION info = new(); | |
var address = GetStackAddress(handle); | |
var result = VirtualQuery(address, out info, (uint)sizeof(MEMORY_BASIC_INFORMATION)); | |
if (result == 0) | |
{ | |
throw new Win32Exception(); | |
} | |
stacks.Add(info.AllocationBase); | |
} | |
return stacks; | |
} | |
static void ResumeAll(ProcessThreadCollection threads) | |
{ | |
var currentId = GetCurrentThreadId(); | |
for (int i = 0; i < threads.Count; ++i) | |
{ | |
var id = threads[i].Id; | |
if (id == currentId) | |
{ | |
continue; | |
} | |
using var handle = OpenThread(0x0002 /*SUSPEND/RESUME*/, false, id); | |
if (handle.IsInvalid) | |
{ | |
throw new Win32Exception(); | |
} | |
if (ResumeThread(handle) == uint.MaxValue) | |
{ | |
throw new Win32Exception(); | |
} | |
} | |
} | |
unsafe static void EnumerateMemory<T>(List<nuint> stacks) | |
{ | |
var typeHandle = typeof(T).TypeHandle.Value; | |
MEMORY_BASIC_INFORMATION info = new(); | |
do | |
{ | |
var result = VirtualQuery(info.BaseAddress + info.RegionSize, out info, (nuint)sizeof(MEMORY_BASIC_INFORMATION)); | |
if (result == 0) | |
{ | |
// read all process memory until failure | |
return; | |
} | |
if (stacks.Contains(info.AllocationBase)) | |
{ | |
continue; // it's a stack | |
} | |
// check if it looks like a managed heap | |
if (info.Type == 0x20000 // MEM_PRIVATE, i.e. not an image or mapped file | |
&& info.State == 0x1000 // MEM_COMMIT | |
&& info.Protect == 0x4) // MEM_READWRITE | |
{ | |
for (nuint offset = 0; offset < info.RegionSize; offset += (nuint)sizeof(nint)) | |
{ | |
var start = (nint*)(info.BaseAddress + offset); | |
if (*start == typeHandle) | |
{ | |
Console.WriteLine($"found potential at: 0x{(nuint)start:X}"); | |
//Console.WriteLine($"object header: 0x{*(start - 1):X}"); | |
if (IsValid(typeof(T), start, info.BaseAddress + info.RegionSize)) | |
{ | |
// okay, let's assume it's really an object: what's the worst that can happen? | |
var it = Unsafe.AsRef<T>(&start); | |
Console.WriteLine(it); // this can explode 💥 | |
} | |
} | |
} | |
} | |
} while (true); | |
} | |
unsafe static bool IsValid(Type t, void* ptr, nuint maxPtr) | |
{ | |
var fieldBase = (byte*)ptr + sizeof(nuint); | |
var fields = t.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); | |
foreach (var field in fields) | |
{ | |
if (field.FieldType.IsValueType) | |
{ | |
continue; | |
} | |
// check that the field value points into "valid" memory | |
var offset = OffsetOf(field); | |
var fieldPtr = (nint**)(fieldBase + offset); | |
if ((nuint)fieldPtr >= maxPtr) | |
{ | |
Console.WriteLine("Invalid: field would be outside of allocated region"); | |
return false; // field offset is outside of allocated region | |
} | |
var objectPtr = *fieldPtr; | |
if (objectPtr == null) | |
{ | |
var nullabilityContext = new NullabilityInfoContext(); | |
var info = nullabilityContext.Create(field); | |
if (info.WriteState == NullabilityState.NotNull) | |
{ | |
Console.WriteLine("Invalid: null non-nullable field"); | |
return false; | |
} | |
else | |
{ | |
continue; // null is always valid | |
} | |
} | |
if (!PointsToValidMemory((nuint)objectPtr, out var maxRefPtr)) | |
{ | |
Console.WriteLine("Invalid: field points to invalid memory"); | |
return false; | |
} | |
// check that the thing pointed-to has a valid type for that field | |
var refTypeHandle = *objectPtr; | |
if (Type.GetTypeFromHandle(RuntimeTypeHandle.FromIntPtr(refTypeHandle)) is not Type refType | |
|| !refType.IsAssignableTo(field.FieldType)) | |
{ | |
Console.WriteLine("Invalid: field points to memory of incorrect type"); | |
return false; | |
} | |
// recursively validate the referenced type | |
if (!IsValid(refType, objectPtr, maxRefPtr)) | |
{ | |
Console.WriteLine("Invalid: field pointed to invalid object"); | |
return false; | |
} | |
} | |
// it's probably okay | |
return true; | |
} | |
static int OffsetOf(FieldInfo field) | |
{ | |
// this hackery from https://stackoverflow.com/a/56512720/10311 | |
return Marshal.ReadInt32(field.FieldHandle.Value + (4 + IntPtr.Size)) & 0xFFFFFF; | |
} | |
// checks if the pointer points into something that looks like a managed heap | |
// and returns the maximum pointer value for something in the same allocation | |
unsafe static bool PointsToValidMemory(nuint ptr, out nuint maxPtr) | |
{ | |
MEMORY_BASIC_INFORMATION info = new(); | |
var result = VirtualQuery(ptr, out info, (nuint)sizeof(MEMORY_BASIC_INFORMATION)); | |
if (result == 0) | |
{ | |
return false; | |
} | |
// same check as in EnumerateMemory | |
if (info.Type == 0x20000 && info.State == 0x1000 && info.Protect == 0x4) | |
{ | |
maxPtr = info.BaseAddress + info.RegionSize; | |
return true; | |
} | |
maxPtr = default; | |
return false; | |
} | |
class ThreadHandle : SafeHandleZeroOrMinusOneIsInvalid | |
{ | |
public ThreadHandle() : base(true) | |
{ | |
} | |
[DllImport("Kernel32.dll")] | |
[return: MarshalAs(UnmanagedType.Bool)] | |
static extern bool CloseHandle(nint handle); | |
protected override bool ReleaseHandle() => CloseHandle(handle); | |
} | |
[StructLayout(LayoutKind.Sequential)] | |
unsafe record struct MEMORY_BASIC_INFORMATION | |
{ | |
public nuint BaseAddress; | |
public nuint AllocationBase; | |
public uint AllocationProtect; | |
public ushort PartitionId; | |
public nuint RegionSize; | |
public uint State; | |
public uint Protect; | |
public uint Type; | |
} | |
[StructLayout(LayoutKind.Sequential)] | |
unsafe struct THREAD_BASIC_INFORMATION | |
{ | |
uint ExitStatus; | |
public void* TebBaseAddress; | |
CLIENT_ID ClientId; | |
void* AffinityMask; | |
uint Priority; | |
uint BasePriority; | |
} | |
[StructLayout(LayoutKind.Sequential)] | |
unsafe struct CLIENT_ID | |
{ | |
void* UniqueProcess; | |
void* UniqueThread; | |
} | |
unsafe struct NT_TIB | |
{ | |
void* ExceptionList; | |
public void* StackBase; | |
public void* StackLimit; | |
// other fields elided… | |
} | |
record class Person(string Given, string Family, Person? Parent = null); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment