Skip to content

Instantly share code, notes, and snippets.

@Porges
Last active December 10, 2023 21:27
Show Gist options
  • Save Porges/3a0ae75bf64ae18259496078d813de57 to your computer and use it in GitHub Desktop.
Save Porges/3a0ae75bf64ae18259496078d813de57 to your computer and use it in GitHub Desktop.
On a mission to write the worst code ever
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