一.读写内存所需的基本参数
接下来我将使用GTA5游戏举例
1.通过进程名称获取进程pid
from psutil import process_iter
def get_process_id_by_name(process_name):
for process in process_iter(["pid", "name"]):
if process.info["name"] == process_name:
return process.info["pid"]
return None
注意进程名称和游戏名称不一样,如下:
GTA5.exe是它的进程名称,Grand Theft Auto V则是它的游戏名称,在这里我们获取它的pid的使用的是进程名,也就是 GTA5.exe
2.通过pid获取进程句柄
我们要读写某个进程的时候,都必须先获取它的句柄,在python里面可以使用ctypes完成这一功能,代码如下:
import ctypes
PROCESS_ALL_ACCESS = 0x1F0FFF
#获取句柄
process = ctypes.windll.kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, pid)
#关闭句柄
ctypes.windll.kernel32.CloseHandle(process)
3.游戏基地址
ce里面显示的绿色的地址,都是游戏基地址+一些偏移量构成的地址,这些我们也可以称为基地址。如下所示:
所以图中的 GTA5.exe 则是游戏真正的基地址,那么加号后面的十六进制的数,就是偏移量。这种类型构成的地址都可以称为基地址,但是真正的基地址就一个 那就是 GTA5.exe
这里ce没有显示GTA5.exe的地址,是因为ce自动给游戏基地址用进程名注册了符号,代替了它的地址。我们可以手动添加,看到GTA5.exe指代的地址具体是什么。如下所示:
所以7FF7C3020000就是游戏的基地址,那我们如何使用python获取游戏的基地址呢?如下所示:
#方法一:使用win32api的库
import ctypes
from psutil import process_iter
from win32process import EnumProcessModules
from win32api import OpenProcess, CloseHandle
PROCESS_ALL_ACCESS = 0x1F0FFF
def get_base_address_by_pid(pid):
try:
process_handle = OpenProcess(PROCESS_ALL_ACCESS, False, pid)
base_address = EnumProcessModules(process_handle)[0]
CloseHandle(process_handle)
return base_address
except:
return None
#方法二:使用pymem的库
import pymem
def get_base_address_by_name(process_name):
try:
pm = pymem.Pymem(process_name)
base_address = pm.base_address
pm.close_process()
return base_address
except:
return None
print(get_base_address_by_pid(get_process_id_by_name("GTA5.exe")))
print(hex(get_base_address_by_pid(get_process_id_by_name("GTA5.exe"))))
两种方法都可以使用,运行结果如下:
由此能看出这个python读取的基地址都是十进制的,我们转换为十六进制就和ce读取的一样了 。
二.读写内存
1.找到需要读取的内存的地址
由于游戏重启以后,系统会分配不同的内存地址给他,因此我们需要找出该动态地址的指针,这里就不讲述如何使用ce查找了,如下所示:
因此我们就需要利用基地址+偏移量的方式,计算出血量所在的地址,用使用偏移量计算地址的代码如下:
import ctypes
def address_by_offsets(base_address, offsets): #base_address表示基地址,offsets表示偏移量列表
#获取当前进程的ID(PID)
PID = get_process_id_by_name("GTA5.exe")
#获取当前进程的句柄
process = ctypes.windll.kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, PID)
#创建一个缓冲区buffer,缓冲区的大小为ctypes.sizeof(ctypes.c_uint64),并用零填充。
#创建一个无符号64位整型变量bytes_read,用于存储读取的字节数
buffer_size = ctypes.sizeof(ctypes.c_uint64)
buffer = ctypes.create_string_buffer(buffer_size)
bytes_read = ctypes.c_uint64()
if base_address != None:
for offset in offsets[:-1]: #读取的过程
base_address += offset
ctypes.windll.kernel32.ReadProcessMemory(
process,
ctypes.c_uint64(base_address),
buffer,
buffer_size,
ctypes.byref(bytes_read),
)
base_address = ctypes.cast(
buffer, ctypes.POINTER(ctypes.c_uint64)
).contents.value
ctypes.windll.kernel32.CloseHandle(process)
return base_address + offsets[-1]
base_address=get_base_address_by_pid(get_process_id_by_name("GTA5.exe"))
print(address_by_offsets(base_address,[0x025B24D8,0x8,0x280]))
print(hex(address_by_offsets(base_address,[0x025B24D8,0x8,0x280])))
由于我们要知道的是血量的地址,而不是血量的具体值,所以我们读取内存的时候,只需要读取到倒数第二个偏移量即可,根据上面ce的图片显示,它的偏移量就是 [0x025B24D8,0x8,0x280] 也就是读取到前面ce图中的 0x8 即可,最后再加上最后一个偏移量 0x280 则得到 血量的地址 1B0DFAC9220 。相信大家结合上面的两张图就能看懂这段根据偏移量计算最终地址的代码.运行结果如下:
发现转换成16进制以后,和ce显示的地址一样
2.读写内存
通过上一段代码,获取到地址以后,那么我们就可以对指定地址进行读写了,代码如下:
import ctypes
from struct import pack
from ctypes import c_float, c_ubyte, c_ulong
#读取内存
def read_memory(address, data_type=ctypes.c_ulong):
pid =get_process_id_by_name("GTA5.exe")#进程pid
process = ctypes.windll.kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, pid)#获取句柄
buffer_size = ctypes.sizeof(ctypes.c_uint64)
buffer = ctypes.create_string_buffer(buffer_size)
bytes_read = ctypes.c_uint64()
ctypes.windll.kernel32.ReadProcessMemory(
process, ctypes.c_uint64(address), buffer, buffer_size, ctypes.byref(bytes_read)
)
ctypes.windll.kernel32.CloseHandle(process)
return ctypes.cast(buffer, ctypes.POINTER(data_type)).contents.value#返回获取的值
#写入内存
def write_memory(address, type, data):
pid = get_process_id_by_name("GTA5.exe") #进程pid
data = pack(type, data) #使用pack函数将需要写入的值打包成相应类型的数据流
process = ctypes.windll.kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, pid)
if process:
buffer_size = len(data)
buffer = ctypes.create_string_buffer(data)
bytes_written = ctypes.c_uint64()
success = ctypes.windll.kernel32.WriteProcessMemory(
process,
ctypes.c_uint64(address),
buffer,
buffer_size,
ctypes.byref(bytes_written),
)
ctypes.windll.kernel32.CloseHandle(process)
return success != 0 #表示写入成功或者失败
else:
return False
c_float, c_ubyte, c_ulong,c_ulonglong 分别对应ce里面的 单浮点数类型,字节类型,四字节类型,八字节类型。根据上面的读写,以及计算地址的函数,则可以将函数整合一下。如下所示:
#读取
def read(address, offsets, type):
return read_memory(address_by_offsets(address, offsets), type)
#写入
def write(address, offsets, type, data):
return write_memory(address_by_offsets(address, offsets), type, data)
base_address=get_base_address_by_pid(get_process_id_by_name("GTA5.exe"))
print(read(base_address,[0x025B24D8,0x8,0x280],ctypes.c_float))
运行结果如下所示:
到此就完成了读取,写入和这个类似就不多赘述。
3.特征码的匹配
特征码在ce里面是通过字节数组来扫描的,因此查看血量的基地址,如下图:
"GTA5.exe"+025B24D8 则是他的基地址,也就是7FF7C55D24D8 ,它的值则是 1B0DCCB7FE0 ,显然这个值是和血量地址相关的,每次重启游戏,这个值都会变动,显然它的字节数组不能用作特征。因此,我们则需要在内存中找到一个基地址,这基地址的值是和7FF7C55D24D8 相关的。我把这个基地址称为特征地址。特征地址通过一定的运算可以得到我们想要的那个基地址。运算方法如下:
def calculate_ptr(address):
return address + read_memory(address + 3) + 7
通过一个简单的debug,就可以计算出我们想要的血量的特征地址是什么。代码如下:
print('debug开始')
base_address #用前面讲到的函数获取
start=0x000000
end=0x2000000
test="0x7ff7c55d24d8" #血量的基地址
def slove(part0,part1):
print('计算开始')
for i in range(part0,part1):
result=Memory.calculate_ptr(base_address+i)
if hex(result)==test:#0x7ff7e95551c4
print(hex(i))
print(hex(result))
print('计算结束')
return
print('没有结果')
slove(start,end)
结果如下:
发现"GTA5.exe"+0x7ab9 则是我们想要的特征地址,将他放入ce中查看字节数组。如下所示:
这样我们就获得了我们需要的特征码,那么怎么找出特征码中的正确的特征呢?使用ce反汇编该区域查看。如下所示:
通过分析得到 48 8B 0D ?? ?? ?? ?? 8B C3 ?? ?? ?? 74 为特征码 ,在ce中搜索该字节数组,如果结果唯一,那么该特征码可以使用。如图所示:
那么如何在python里面实现特征码的匹配呢?代码如下所示:
import pymem
#将特征码转换成字节流
def convert_pattern_to_regex(byte_pattern):
pattern_parts = byte_pattern.split()
regex_pattern = b""
for part in pattern_parts:
if part == "??":
regex_pattern += b"."
else:
regex_pattern += bytes.fromhex(part)
# print(regex_pattern)
return regex_pattern
#模式串匹配,这里使用了pymem库完成
def match_pattern(process_name, module_name, byte_pattern):
pm = pymem.Pymem(process_name)
module = pymem.process.module_from_name(pm.process_handle, module_name)
if not module:
print(f"Module {module_name} not found in process {process_name}")
return None
regex_pattern = convert_pattern_to_regex(byte_pattern)
addresses = pymem.pattern.pattern_scan_module(
pm.process_handle, module, regex_pattern, return_multiple=True
)
if addresses:
for address in addresses:
print(f"Pattern found at address: 0x{address:X}")
return addresses
else:
print("Pattern not found")
return None
match_pattern("GTA5.exe", "GTA5.exe", "48 8B 0D ?? ?? ?? ?? 8B C3 ?? ?? ?? 74")
结果如下:
成功匹配到特征地址,此时通过上面的calucuteptr则可以计算出血量的基地址。代码如下:
pattern_address=match_pattern("GTA5.exe", "GTA5.exe", "48 8B 0D ?? ?? ?? ?? 8B C3 ?? ?? ?? 74")
health_base=calculate_ptr(pattern_address[0])
print(hex(health_base))
health=read(health_base,[0x0,0x8,0x280],ctypes.c_float)
print(health)
为什么现在偏移从前面的 [0x025B24D8,0x8,0x280] 变成了现在的 [0x0,0x8,0x280],是因为刚开始的read的第一个参数base_address 是 GTA5.exe ,而现在的base_address则是我们已经算出来的那个基地址也就是GTA5.exe+0x7ab9,所以最开始就不用偏移了,所以偏移量就是0x0。结果如下:
到此,使用python完成读写,以及模式串就都完成了。
感谢你的观看,如果有疑问,可以回复我,我可以一一回答