Использование указателей, найденных в Cheat Engine в С#

О программе

У меня есть программа, которая записывает в память игры, в которой я экспериментирую. код работает для меня очень хорошо, когда я использую обычный статический адрес, но по какой-то причине я не могу это сделать, как только найду рабочий указатель. Например, я нахожу это в Cheat Engine после сканирования указателя несколько раз:

введите описание изображения здесь

Этот адрес работает каждый раз, когда я загружаю игру и редактирую ее. Проблема в том, что я не понимаю, как ее использовать в моей программе. Здесь мои объявленные переменные, которые я попытался подключить к значениям:

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

Я передаю эти переменные следующим образом:

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();

Я когда-то использовал статический адрес (для другой игры), и мой код работал нормально, как только я подключил адрес и смещение. На этот раз я в тупик. Любая помощь и объяснения будут глубоко оценены. Спасибо заранее.

Ответ 1

Я решил, что я опубликую решение для этого для людей в будущем.

Один из способов справиться с этим, если вы не хотите погружаться в код С++, сохраненный там, и переписать на С#, просто использовать эту программу для github:

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

Прямая ссылка для скачивания находится здесь:

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

Вы можете передать этому исполняемому файлу идентификатор процесса и проанализировать нужный вам адрес потока.

В основном, то, что я сделал, это мой процесс, который запускает exe, перенаправляет вывод и анализирует его.

Затем процесс закрывается, и мы делаем то, что нам нужно - я чувствую, что я обманываю, но он работает.

Вывод для threadstack.exe обычно выглядит следующим образом:

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

Вот код, который я в конечном счете использовал для получения нужного вам адреса:

[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);
}

Final

Если вы заинтересованы в обновлении кода на С++, я считаю, что соответствующая часть здесь.

На самом деле это не выглядит слишком сложным - я думаю, что вы просто захватываете базовый адрес kernal32.dll и ищете этот адрес в стеке потоков, проверяя, соответствует ли он >= базовому адресу или <= до base address + size при чтении каждого 4 байта - мне пришлось бы играть с ним, хотя.

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 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;
}

Вы можете получить базовые адреса потоков в С# следующим образом:

fooobar.com/info/246209/...

Ключ должен вызвать функцию NtQueryInformationThread. Это не полностью "официальная" функция (возможно, недокументированная в прошлом?), Но в документации нет альтернативы для получения начального адреса потока.

Я завернул его в .NET-дружественный вызов, который принимает идентификатор потока и возвращает начальный адрес как IntPtr. Этот код был протестирован в режиме x86 и x64, а в последнем он был протестирован как в 32-битном, так и в 64-битном целевом процессе.

Одна вещь, которую я не тестировал, выполняла это с низкими привилегиями; Я ожидаю, что этот код требует, чтобы вызывающий абонент имел 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
    }
}

Вывод в моей системе:

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

кроме содержимого в круглых скобках, поскольку для этого требуется дополнительное P/Invoke.


Относительно ошибки SymFromAddress "модуль не найден" я просто хотел упомянуть, что нужно вызвать SymInitialize с помощью fInvadeProcess = true ИЛИ загрузить модуль вручную, как описано в MSDN.