Using Pointers Found in Cheat Engine in C#

2020-05-26 09:45发布

问题:

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, but for some reason I can't seem to do it once I find a working pointer. For example, I find this in Cheat Engine after pointer scanning a few times:

This address works every time I load the game and edit the value. The problem is I don't understand how to use it in my program. Here's my declared variables which I attempted to plug in the values:

bool UnlimitedAmmo = false;
string AmmoPointer = "031B7324"; // <--- The address
int[] AmmoOffset = { 0x2c, 0x1e8, 0x3c8, 0x6d4, 0x508 }; // <--- It's pointers
int AmmoToFill = 1337; // <--- The Amount of ammo to give

I'm passing in these variables as follows:

MyMemory.ReadProcess = MyProcess[0];
MyMemory.Open();

int PointerAddress = HexToDec(AmmoPointer);
int[] PointerOffest = AmmoOffset;
int BytesWritten;
byte[] ValueToWrite = BitConverter.GetBytes(AmmoToFill);
string WrittenAddress = MyMemory.PointerWrite((IntPtr)PointerAddress, ValueToWrite, 
  PointerOffest, out BytesWritten);
MyMemory.CloseHandle();

I once used a static address (for a different game) and my code worked fine once I plugged the address and offset. I'm stumped this time. Any help and explanations would be deeply appreciated. Thanks in advance.

回答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.