Skip to content

Instantly share code, notes, and snippets.

@baratgabor
Last active November 8, 2022 19:04
Show Gist options
  • Save baratgabor/06aef4815226eedc8ef5052e595b3ca4 to your computer and use it in GitHub Desktop.
Save baratgabor/06aef4815226eedc8ef5052e595b3ca4 to your computer and use it in GitHub Desktop.
32 bit ThreadStack0 implementation in C#
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
/// <summary>
/// Only for 32 bit processes. The methods of this static class can find the special symbol ThreadStack0,
/// and follow pointer chains to return the end result in the specified type T.
/// Methods in InfoReader and MemReader are kept public, but be aware that they don't do bitness checks.
/// Usage example:
/// Process process = Process.GetProcessesByName("processname").FirstOrDefault();
/// int[] JumpOffsets = new int[] { -0x1e4, 0xb4, 0x24 };
/// uint ThreadStack0 = ProcessUtils32.CheatengineSpecific.GetThreadStack0(process);
/// uint PointerChainResult = ProcessUtils32.CheatengineSpecific.ReadPointerChain<uint>(process.Handle, ThreadStack0, JumpOffsets);
/// </summary>
public static class ProcessUtils32
{
public static class CheatengineSpecific
{
/// <summary>
/// Returns the address of the ThreadStack0 special symbol.
/// </summary>
/// <param name="process">The target process</param>
public static uint GetThreadStack0(Process process)
{
if (!InfoReader.Is32BitProcess(process.Handle))
throw new ArgumentException("Provided process is not 32 bit.");
const uint BytesToSample = 4096; // Arbitrary (?). 4096 should be enough for both x86 and x64; should be divisible by 4 (x86)
MODULEINFO mi = InfoReader.GetKernel32ModuleInfo(process); // MODULEINFO delivers the Kernel32 module's load address and size (note that load address (lpBaseOfDll) is the same as the module handle)
NT_TIB tib = InfoReader.GetTIB(process); // NT_TIB delivers the stack base address of the process' main thread
// Read sample byte array from the base of the main thread stack
byte[] StackBaseSample = MemReader.ReadToByteArray(process.Handle, (tib.StackBase - BytesToSample), BytesToSample);
int i = 0; // Keep scope bigger than loop
// ThreadStack0 is the first pointer in the main thread's stack that points inside the Kernel32 module.
// To find it, we iterate through each 32 bit (4 byte) value in the stack sample, and check if it is in the target range.
for (i = (StackBaseSample.Length / 4) - 1; i >= 0; --i)
{
UInt32 valueAtPosition = BitConverter.ToUInt32(StackBaseSample, i * 4);
if (valueAtPosition >= (uint)mi.lpBaseOfDll &&
valueAtPosition <= (uint)mi.lpBaseOfDll + mi.SizeOfImage)
break;
}
if (i == 0) // If i reached zero, then iteration finished without finding a match
throw new Exceptions.LocalException(nameof(GetThreadStack0), "ThreadStack0 can't be found in the sampled "+ BytesToSample +" bytes");
// Finally, calculate and return the actual ThreadStack0 address from index i
return (uint)(tib.StackBase - BytesToSample + i * 4);
}
/// <summary>
/// Traces a pointer chain and returns the value found at the last pointer address.
/// </summary>
/// <param name="processHandle">The handle of the target process.</param>
/// <param name="baseAddress">The address from which the pointer tracing should begin.</param>
/// <param name="jumpsWithOffsets">Array that defines the jumps in the chain. Use 0 if offset is not needed.</param>
public static T ReadPointerChain<T>(IntPtr processHandle, uint baseAddress, int[] jumpsWithOffsets)
{
if (!InfoReader.Is32BitProcess(processHandle))
throw new ArgumentException("Provided process is not 32 bit.");
// Process pointer chain up to the last element (excluding the last)
uint Value = baseAddress;
for (int i = 0; i < jumpsWithOffsets.Length-1; i++)
Value = MemReader.ReadToType<uint>(processHandle, (uint)(Value + jumpsWithOffsets[i]));
// Read memory at last pointer address into to the requested type
T t = MemReader.ReadToType<T>(processHandle, (uint)(Value + jumpsWithOffsets.Last()));
return t;
}
}
// Helper class for reading other processes' private memory area with native windows function calls
public static class MemReader
{
/// <summary>
/// Reads target memory area into a type. The size of the area to be read is determined by the size of the type (i.e. int32 = 4 bytes). If you specify a struct as a type parameter, this method can fill the struct with the target memory area.
/// </summary>
/// <param name="processHandle">The handle of the target process</param>
/// <param name="memoryAddress">The private memory address of the process to read from. Effectively a pointer.</param>
/// <returns>Returns T filled with the target memory area</returns>
public static T ReadToType<T>(IntPtr processHandle, uint memoryAddress)
{
// Fall back to byte array read, since our PInvoke can't be generic
byte[] content = ReadToByteArray(processHandle, memoryAddress, (uint)Marshal.SizeOf(typeof(T)));
// Fill our generic variable with the byte array
T t;
GCHandle PinnedStruct = GCHandle.Alloc(content, GCHandleType.Pinned);
try { t = (T)Marshal.PtrToStructure(PinnedStruct.AddrOfPinnedObject(), typeof(T)); }
catch (Exception ex) { throw new Exceptions.LocalException(nameof(ReadToType), "Error trying to fill the return type.", ex); }
finally { PinnedStruct.Free(); }
return t;
}
public static byte[] ReadToByteArray(IntPtr processHandle, uint memoryAddress, uint numberOfBytes)
{
byte[] bytes = new byte[numberOfBytes];
var result = ReadProcessMemory(processHandle, memoryAddress, bytes, (uint)bytes.Length, IntPtr.Zero);
// ReadProcessMemory returns 0 if failed
if (!result)
throw new Exceptions.PInvokeException(nameof(ReadToByteArray), nameof(ReadProcessMemory), Marshal.GetLastWin32Error());
return bytes;
}
#region PInvoke Declarations
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool ReadProcessMemory(
IntPtr hProcess,
uint lpBaseAddress,
byte[] lpBuffer,
uint nSize,
IntPtr lpNumberOfBytesRead);
#endregion
}
// Helper class for reading other processes' process information with native windows function calls
public static class InfoReader
{
/// <summary>
/// Checks if process handle is associated to a 32 bit process.
/// </summary>
/// <param name="processHandle">The handle of the process to check.</param>
/// <returns>Returns true if process is 32 bit, false if it is 64bit.</returns>
public static bool Is32BitProcess(IntPtr processHandle)
{
bool Is32Bit = false;
try { IsWow64Process(processHandle, out Is32Bit); }
catch { throw new Exceptions.PInvokeException(nameof(Is32BitProcess), nameof(IsWow64Process), Marshal.GetLastWin32Error()); }
return Is32Bit;
}
public static THREAD_BASIC_INFORMATION GetTBI(Process process)
{
IntPtr hThread = OpenThread(ThreadAccess.QueryInformation, false, (uint)process.Threads.OfType<ProcessThread>().First().Id);
if (hThread == null) // Some implementations on StackExchange compare against IntPtr.zero - but MSDN says it returns null, not null pointer
throw new Exceptions.PInvokeException(nameof(GetTBI), nameof(OpenThread), Marshal.GetLastWin32Error());
try
{
THREAD_BASIC_INFORMATION tbi = new THREAD_BASIC_INFORMATION();
int result = NtQueryInformationThread(hThread, ThreadInfoClass.ThreadBasicInformation, out tbi, (uint)Marshal.SizeOf(tbi), IntPtr.Zero);
// NtQueryInformationThread returns nonzero if failed
if (result != 0)
throw new Exceptions.PInvokeException(nameof(GetTBI), nameof(NtQueryInformationThread), Marshal.GetLastWin32Error());
return tbi;
}
finally
{
CloseHandle(hThread);
}
}
public static NT_TIB GetTIB(Process process)
{
// Read TBI - prerequise for TIB
THREAD_BASIC_INFORMATION tbi = GetTBI(process);
// Read NT_TIB equivalent memory area of the process directly into our NT_TIB struct
NT_TIB tib = MemReader.ReadToType<NT_TIB>(process.Handle, tbi.TebBaseAddress);
return tib;
}
public static MODULEINFO GetKernel32ModuleInfo(Process process)
{
// GetModuleHandle returns NULL when failed
IntPtr moduleHandle = GetModuleHandle("kernel32.dll");
if (moduleHandle == null)
throw new Exceptions.PInvokeException(nameof(GetKernel32ModuleInfo), nameof(GetModuleHandle), Marshal.GetLastWin32Error());
MODULEINFO mi = new MODULEINFO();
// GetModuleInformation returns 0 when failed
var result = GetModuleInformation(process.Handle, moduleHandle, out mi, (uint)Marshal.SizeOf(mi));
if (!result)
throw new Exceptions.PInvokeException(nameof(GetKernel32ModuleInfo), nameof(GetModuleInformation), Marshal.GetLastWin32Error());
return mi;
}
#region PInvoke Declarations
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenThread(ThreadAccess dwDesiredAccess, bool bInheritHandle, uint dwThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);
[DllImport("psapi.dll", SetLastError = true)]
private static extern bool GetModuleInformation(IntPtr hProcess, IntPtr hModule, out MODULEINFO lpmodinfo, uint cb);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("ntdll.dll", SetLastError = true)]
private static extern int NtQueryInformationThread(
IntPtr threadHandle,
ThreadInfoClass threadInformationClass,
out THREAD_BASIC_INFORMATION threadInformation,
ulong threadInformationLength,
IntPtr returnLengthPtr);
[DllImport("kernel32.dll", SetLastError = true, CallingConvention = CallingConvention.Winapi)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool IsWow64Process(
[In] IntPtr processHandle,
[Out, MarshalAs(UnmanagedType.Bool)] out bool wow64Process);
#endregion
#region PInvoke Data Structures
[StructLayout(LayoutKind.Sequential)]
public struct THREAD_BASIC_INFORMATION
{
public uint ExitStatus; // original: LONG NTSTATUS
public uint TebBaseAddress; // original: PVOID
public CLIENT_ID ClientId;
public uint AffinityMask; // original: ULONG_PTR
public uint Priority; // original: DWORD
public uint BasePriority; // original: DWORD
}
[StructLayout(LayoutKind.Sequential)]
public struct CLIENT_ID
{
public uint UniqueProcess; // original: PVOID
public uint UniqueThread; // original: PVOID
}
[Flags]
private enum ThreadAccess : int
{
Terminate = 0x0001,
SuspendResume = 0x0002,
GetContext = 0x0008,
SetContext = 0x0010,
SetInformation = 0x0020,
QueryInformation = 0x0040,
SetThreadToken = 0x0080,
Impersonate = 0x0100,
DirectImpersonation = 0x0200
}
private enum ThreadInfoClass : int
{
ThreadBasicInformation = 0,
ThreadQuerySetWin32StartAddress = 9
}
#endregion
}
#region PInvoke Data Structures
[StructLayout(LayoutKind.Sequential)]
public struct NT_TIB
{
public uint ExceptionListPointer; // Current Structured Exception Handling (SEH) frame
public uint StackBase; // Bottom of stack (high address)
public uint StackLimit; // Ceiling of stack (low address)
public uint SubSystemTib; // No clue...
// Incomplete representation of winnt.h / NT_TIB, but we're reading it directly from memory, so struct size doesn't matter
}
[StructLayout(LayoutKind.Sequential)]
public struct MODULEINFO
{
public uint lpBaseOfDll;
public uint SizeOfImage;
public uint EntryPoint;
}
#endregion
public class Exceptions
{
[Serializable()]
public class PInvokeException : Exception, ISerializable
{
public string LocalMethod { get; private set; }
public string PInvokeMethod { get; private set; }
public int ErrorCode { get; private set; }
private static Func<string, string, int, string> PInvokeErrorMsg = (LocalMethod, PInvokeMethod, ErrorCode) => String.Format("PInvoke '{0}' failed in method '{1}'. ErrorCode: {2}, Message: {3}", PInvokeMethod, LocalMethod, ErrorCode, new Win32Exception(ErrorCode).Message);
public PInvokeException(string localMethod, string pInvokeMethod, int errorCode) : base (PInvokeErrorMsg(localMethod, pInvokeMethod, errorCode)) {
LocalMethod = localMethod;
PInvokeMethod = pInvokeMethod;
ErrorCode = errorCode;
}
public PInvokeException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}
[Serializable()]
public class LocalException : Exception, ISerializable
{
public string LocalMethod { get; private set; }
public string Reason { get; private set; }
private static Func<string, string, string> LocalErrorMsg = (LocalMethod, Reason) => String.Format("Method '{0}' failed with error: {1}", LocalMethod, Reason);
public LocalException(string localMethod, string reason) : base(LocalErrorMsg(localMethod, reason))
{
LocalMethod = localMethod;
Reason = reason;
}
public LocalException(string localMethod, string reason, Exception innerException) : base(LocalErrorMsg(localMethod, reason), innerException)
{
LocalMethod = localMethod;
Reason = reason;
}
public LocalException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment