如何通过Python或Cheat Engine基于THREADSTACK0读取wotblitz内存
问题根因
你的代码无法正常读取内存,核心问题有两个:
- 错误理解
THREADSTACK0指针基址的含义:它不是游戏进程模块的固定偏移,而是对应线程栈的起始地址,进程每次启动、线程重新创建时这个地址都会随机变化,你硬编码计算基址的写法完全无效。 - 错误使用
ReadWriteMemory的多级指针接口:多级指针不能直接把所有偏移求和后加基址计算最终地址,必须从基址开始逐次读取指针值、叠加当前偏移,逐级解引用才能拿到正确的最终地址。
实现步骤
ReadWriteMemory库本身没有封装获取线程栈基址的能力,需要调用Windows原生API拿到游戏主线程的栈起始地址(也就是CE里的THREADSTACK0值),再按照多级指针的规则传入偏移链即可。
注意:请先确认你在CE中找到指针时附加的是游戏主线程,否则拿到的栈基址不匹配会导致读取失败。
修正后的完整代码
from ReadWriteMemory import ReadWriteMemory, ReadWriteMemoryError from time import sleep as wait import ctypes from ctypes import wintypes # Windows API 常量与结构体定义 TH32CS_SNAPTHREAD = 0x00000004 THREAD_QUERY_INFORMATION = 0x0040 ThreadBasicInformation = 0 class THREADENTRY32(ctypes.Structure): _fields_ = [ ("dwSize", wintypes.DWORD), ("cntUsage", wintypes.DWORD), ("th32ThreadID", wintypes.DWORD), ("th32OwnerProcessID", wintypes.DWORD), ("tpBasePri", wintypes.LONG), ("tpDeltaPri", wintypes.LONG), ("dwFlags", wintypes.DWORD) ] class THREAD_BASIC_INFORMATION(ctypes.Structure): _fields_ = [ ("ExitStatus", wintypes.DWORD), ("TebBaseAddress", wintypes.LPVOID), ("UniqueProcessId", ctypes.c_void_p), ("UniqueThreadId", ctypes.c_void_p), ("AffinityMask", wintypes.LPVOID), ("Priority", wintypes.DWORD), ("BasePriority", wintypes.DWORD) ] # 初始化API函数 kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) ntdll = ctypes.WinDLL('ntdll', use_last_error=True) kernel32.CreateToolhelp32Snapshot.argtypes = [wintypes.DWORD, wintypes.DWORD] kernel32.CreateToolhelp32Snapshot.restype = wintypes.HANDLE kernel32.Thread32First.argtypes = [wintypes.HANDLE, ctypes.POINTER(THREADENTRY32)] kernel32.Thread32First.restype = wintypes.BOOL kernel32.Thread32Next.argtypes = [wintypes.HANDLE, ctypes.POINTER(THREADENTRY32)] kernel32.Thread32Next.restype = wintypes.BOOL kernel32.OpenThread.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD] kernel32.OpenThread.restype = wintypes.HANDLE kernel32.CloseHandle.argtypes = [wintypes.HANDLE] kernel32.CloseHandle.restype = wintypes.BOOL ntdll.NtQueryInformationThread.argtypes = [ wintypes.HANDLE, ctypes.c_int, ctypes.POINTER(THREAD_BASIC_INFORMATION), wintypes.ULONG, ctypes.POINTER(wintypes.ULONG) ] ntdll.NtQueryInformationThread.restype = wintypes.LONG def get_main_thread_stack_base(pid): """获取指定PID进程主线程的栈基址(对应CE的THREADSTACK0)""" snapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) if snapshot == wintypes.HANDLE(-1): raise ctypes.WinError(ctypes.get_last_error()) te32 = THREADENTRY32() te32.dwSize = ctypes.sizeof(THREADENTRY32) main_tid = None # 找到目标进程的第一个线程(即主线程) if kernel32.Thread32First(snapshot, ctypes.byref(te32)): while True: if te32.th32OwnerProcessID == pid: main_tid = te32.th32ThreadID break if not kernel32.Thread32Next(snapshot, ctypes.byref(te32)): break kernel32.CloseHandle(snapshot) if not main_tid: raise RuntimeError("找不到目标进程主线程") # 打开线程查询TEB地址,计算栈基址 h_thread = kernel32.OpenThread(THREAD_QUERY_INFORMATION, False, main_tid) if not h_thread: raise ctypes.WinError(ctypes.get_last_error()) tbi = THREAD_BASIC_INFORMATION() ret_len = wintypes.ULONG() status = ntdll.NtQueryInformationThread( h_thread, ThreadBasicInformation, ctypes.byref(tbi), ctypes.sizeof(tbi), ctypes.byref(ret_len) ) kernel32.CloseHandle(h_thread) if status != 0: raise RuntimeError(f"查询线程信息失败,状态码:{status}") # 根据程序指针宽度自动适配StackBase在NT_TIB结构中的偏移 ptr_size = ctypes.sizeof(ctypes.c_void_p) stack_base_offset = 0x4 if ptr_size == 4 else 0x8 stack_base = ctypes.c_size_t.from_address(tbi.TebBaseAddress + stack_base_offset).value return stack_base # 初始化进程连接 rwm_svc = ReadWriteMemory() try: process = rwm_svc.get_process_by_name('wotblitz.exe') process.open() except ReadWriteMemoryError: print('wotblitz.exe not running!') exit(1) # 指针配置 POINTER_CONFIG = { "threadstack_offset": -0x00000EEC, # THREADSTACK0对应的减偏移 "offsets": [0x4, 0x8, 0x148, 0x0, 0x30, 0x19c, 0x24] # 多级偏移链 } def read_damage_value(): # 每次读取前重新获取当前线程栈基址,避免线程重建后地址失效 threadstack0 = get_main_thread_stack_base(process.pid) base_ptr_addr = threadstack0 + POINTER_CONFIG["threadstack_offset"] # 正确调用多级指针接口:传入第一级指针地址 + 偏移列表 final_addr = process.get_pointer(base_ptr_addr, POINTER_CONFIG["offsets"]) # 根据内存值实际类型调整读取方法,读浮点用read_float,读长整型用read_long return process.read_int(final_addr) if __name__ == "__main__": while True: wait(0.1) dmg_val = read_damage_value() print(f"\r当前伤害值:{dmg_val}", end="")
注意事项
- 如果读取值不对,先确认你在CE中用的偏移链和代码里写的完全一致,同时确认读取值的类型(int/float/long等)和内存中存储的类型匹配,
ReadWriteMemory提供了read_int/read_float/read_double等对应不同类型的读取方法。 - 权限不足会导致无法查询线程信息,请用管理员权限运行Python脚本。
- 如果游戏有内核级反作弊机制,用户态的内存读取可能会被拦截,该方法不适用。
内容的提问来源于stack exchange,提问作者West Prime
相关产品推荐
相关产品推荐

