Using Pointers Found in Cheat Engine in C#

前端 未结 1 785
猫巷女王i
猫巷女王i 2021-02-05 13:34

About the Program

I have a program which writes to the memory of the game I\'m experimenting with. the code works for me just fine when I use a regular static address,

相关标签:
1条回答
  • I figured I would post a solution for this for people in the future.

    One way you can handle this if you don't want to dive into the C++ code saved there and rewrite in C# is to simply use this program on github:

    https://github.com/makemek/cheatengine-threadstack-finder

    The direct download link is here:

    https://github.com/makemek/cheatengine-threadstack-finder/files/685703/threadstack.zip

    You can pass this executable a process ID and parse out the thread address you need.

    Basically, what I did is my process runs the exe, redirects the output, and parses it.

    Then the process closes and we do what we need - I sort of feel like I'm cheating, but it works.

    The output for threadstack.exe typically looks like this:

    PID 6540 (0x198c)
    Grabbing handle
    Success
    PID: 6540 Thread ID: 0x1990
    PID: 6540 Thread ID: 0x1b1c
    PID: 6540 Thread ID: 0x1bbc
    TID: 0x1990 = THREADSTACK 0 BASE ADDRESS: 0xbcff8c
    TID: 0x1b1c = THREADSTACK 1 BASE ADDRESS: 0x4d8ff8c
    TID: 0x1bbc = THREADSTACK 2 BASE ADDRESS: 0x518ff8c
    

    Here is the code I ultimately used to get the address I need:

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, [Out] byte[] lpBuffer, int dwSize, out int lpNumberOfBytesRead);
    
    ////////////////////////////////////////////////////////////////////
    // These are used to find the StardewValley.Farmer structure     //
    //////////////////////////////////////////////////////////////////
    private IntPtr Thread0Address;
    private IntPtr FarmerStartAddress;
    private static int[] FARMER_OFFSETS = { 0x4, 0x478, 0x218, 0x24C };
    private static int FARMER_FIRST = 0x264;
    //////////////////////////////////////////////////////////////////
    
    private async void hookAll()
    {
        SVProcess = Process.GetProcessesByName("Stardew Valley")[0];
        SVHandle = OpenProcess(ProcessAccessFlags.All, true, SVProcess.Id);
        SVBaseAddress = SVProcess.MainModule.BaseAddress;
        Thread0Address = (IntPtr) await getThread0Address();
        getFarmerStartAddress();
    }
    private Task<int> getThread0Address()
    {
        var proc = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = "threadstack.exe",
                Arguments = SVProcess.Id + "",
                UseShellExecute = false,
                RedirectStandardOutput = true,
                CreateNoWindow = true
            }
        };
        proc.Start();
        while (!proc.StandardOutput.EndOfStream)
        {
            string line = proc.StandardOutput.ReadLine();
            if (line.Contains("THREADSTACK 0 BASE ADDRESS: "))
            {
                line = line.Substring(line.LastIndexOf(":") + 2);
                return Task.FromResult(int.Parse(line.Substring(2), System.Globalization.NumberStyles.HexNumber));
            }
        }
        return Task.FromResult(0);
    }
    private void getFarmerStartAddress()
    {
        IntPtr curAdd = (IntPtr) ReadInt32(Thread0Address - FARMER_FIRST);
        foreach (int x in FARMER_OFFSETS)
            curAdd = (IntPtr) ReadInt32(curAdd + x);
        FarmerStartAddress = (IntPtr) curAdd;
    }
    private int ReadInt32(IntPtr addr)
    {
        byte[] results = new byte[4];
        int read = 0;
        ReadProcessMemory(SVHandle, addr, results, results.Length, out read);
        return BitConverter.ToInt32(results, 0);
    }
    

    If you're interested in updating the C++ code, I believe the relevant portion is here.

    It actually doesn't look too complicated - I think you're just grabbing the base address of the kernal32.dll and looking for that address in the thread stack by checking to see if it is >= to the base address or <= to the base address + size while reading each 4 bytes - I would have to play with it though.

    DWORD GetThreadStartAddress(HANDLE processHandle, HANDLE hThread) {
        /* rewritten from https://github.com/cheat-engine/cheat-engine/blob/master/Cheat%20Engine/CEFuncProc.pas#L3080 */
        DWORD used = 0, ret = 0;
        DWORD stacktop = 0, result = 0;
    
        MODULEINFO mi;
    
        GetModuleInformation(processHandle, GetModuleHandle("kernel32.dll"), &mi, sizeof(mi));
        stacktop = (DWORD)GetThreadStackTopAddress_x86(processHandle, hThread);
    
        /* The stub below has the same result as calling GetThreadStackTopAddress_x86() 
        change line 54 in ntinfo.cpp to return tbi.TebBaseAddress
        Then use this stub
        */
        //LPCVOID tebBaseAddress = GetThreadStackTopAddress_x86(processHandle, hThread);
        //if (tebBaseAddress)
        //  ReadProcessMemory(processHandle, (LPCVOID)((DWORD)tebBaseAddress + 4), &stacktop, 4, NULL);
    
        CloseHandle(hThread);
    
        if (stacktop) {
            //find the stack entry pointing to the function that calls "ExitXXXXXThread"
            //Fun thing to note: It's the first entry that points to a address in kernel32
    
            DWORD* buf32 = new DWORD[4096];
    
            if (ReadProcessMemory(processHandle, (LPCVOID)(stacktop - 4096), buf32, 4096, NULL)) {
                for (int i = 4096 / 4 - 1; i >= 0; --i) {
                    if (buf32[i] >= (DWORD)mi.lpBaseOfDll && buf32[i] <= (DWORD)mi.lpBaseOfDll + mi.SizeOfImage) {
                        result = stacktop - 4096 + i * 4;
                        break;
                    }
    
                }
            }
    
            delete buf32;
        }
    
        return result;
    }
    

    You can get the thread base addresses in C# like this:

    https://stackoverflow.com/a/8737521/1274820

    The key is to call the NtQueryInformationThread function. This is not a completely "official" function (possibly undocumented in the past?), but the documentation suggests no alternative for getting the start address of a thread.

    I've wrapped it up into a .NET-friendly call that takes a thread ID and returns the start address as IntPtr. This code has been tested in x86 and x64 mode, and in the latter it was tested on both a 32-bit and a 64-bit target process.

    One thing I did not test was running this with low privileges; I would expect that this code requires the caller to have the SeDebugPrivilege.

    using System;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Linq;
    using System.Runtime.InteropServices;
    
    class Program
    {
        static void Main(string[] args)
        {
            PrintProcessThreads(Process.GetCurrentProcess().Id);
            PrintProcessThreads(4156); // some other random process on my system
            Console.WriteLine("Press Enter to exit.");
            Console.ReadLine();
        }
    
        static void PrintProcessThreads(int processId)
        {
            Console.WriteLine(string.Format("Process Id: {0:X4}", processId));
            var threads = Process.GetProcessById(processId).Threads.OfType<ProcessThread>();
            foreach (var pt in threads)
                Console.WriteLine("  Thread Id: {0:X4}, Start Address: {1:X16}",
                                  pt.Id, (ulong) GetThreadStartAddress(pt.Id));
        }
    
        static IntPtr GetThreadStartAddress(int threadId)
        {
            var hThread = OpenThread(ThreadAccess.QueryInformation, false, threadId);
            if (hThread == IntPtr.Zero)
                throw new Win32Exception();
            var buf = Marshal.AllocHGlobal(IntPtr.Size);
            try
            {
                var result = NtQueryInformationThread(hThread,
                                 ThreadInfoClass.ThreadQuerySetWin32StartAddress,
                                 buf, IntPtr.Size, IntPtr.Zero);
                if (result != 0)
                    throw new Win32Exception(string.Format("NtQueryInformationThread failed; NTSTATUS = {0:X8}", result));
                return Marshal.ReadIntPtr(buf);
            }
            finally
            {
                CloseHandle(hThread);
                Marshal.FreeHGlobal(buf);
            }
        }
    
        [DllImport("ntdll.dll", SetLastError = true)]
        static extern int NtQueryInformationThread(
            IntPtr threadHandle,
            ThreadInfoClass threadInformationClass,
            IntPtr threadInformation,
            int threadInformationLength,
            IntPtr returnLengthPtr);
    
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr OpenThread(ThreadAccess dwDesiredAccess, bool bInheritHandle, int dwThreadId);
    
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool CloseHandle(IntPtr hObject);
    
        [Flags]
        public enum ThreadAccess : int
        {
            Terminate = 0x0001,
            SuspendResume = 0x0002,
            GetContext = 0x0008,
            SetContext = 0x0010,
            SetInformation = 0x0020,
            QueryInformation = 0x0040,
            SetThreadToken = 0x0080,
            Impersonate = 0x0100,
            DirectImpersonation = 0x0200
        }
    
        public enum ThreadInfoClass : int
        {
            ThreadQuerySetWin32StartAddress = 9
        }
    }
    

    Output on my system:

    Process Id: 2168    (this is a 64-bit process)
      Thread Id: 1C80, Start Address: 0000000001090000
      Thread Id: 210C, Start Address: 000007FEEE8806D4
      Thread Id: 24BC, Start Address: 000007FEEE80A74C
      Thread Id: 12F4, Start Address: 0000000076D2AEC0
    Process Id: 103C    (this is a 32-bit process)
      Thread Id: 2510, Start Address: 0000000000FEA253
      Thread Id: 0A0C, Start Address: 0000000076F341F3
      Thread Id: 2438, Start Address: 0000000076F36679
      Thread Id: 2514, Start Address: 0000000000F96CFD
      Thread Id: 2694, Start Address: 00000000025CCCE6
    

    apart from the stuff in parentheses since that requires extra P/Invoke's.


    Regarding SymFromAddress "module not found" error, I just wanted to mention that one needs to call SymInitialize with fInvadeProcess = true OR load the module manually, as documented on MSDN.

    0 讨论(0)
提交回复
热议问题