侧边栏壁纸
博主头像
学习笔记

行动起来,活在当下

  • 累计撰写 9 篇文章
  • 累计创建 2 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Pymem 使用笔记:用 Python 操作 Windows 进程内存

brian
2026-04-01 / 0 评论 / 0 点赞 / 5 阅读 / 0 字
温馨提示:
部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

最近在研究 Windows 进程相关的东西,发现了 pymem 这个库,用起来比直接调 ctypes 操作 Windows API 方便很多。这篇文章记录一下它的基本用法,以及一些实际踩坑的经验。

它能做什么

简单说,pymem 封装了 Windows 底层的 ReadProcessMemoryWriteProcessMemory 等 API,让你可以用 Python 直接读写另一个进程的内存。

常见使用场景:

  • 内存分析:检查某个程序运行时的内存状态,排查内存泄漏

  • 逆向工程:定位程序关键数据的内存地址,辅助分析

  • 进程调试:在不中断程序的情况下读取或修改运行时数据

  • 游戏辅助研究(学习用途):理解内存地址与游戏数据的映射关系

⚠️ 注意:操作其他进程内存涉及系统权限问题,实际使用中必须确保你有合法的操作权限,不要用于未经授权的场景。

安装

pip install pymem

安装完就能用,无需额外配置。目前最新版本是 1.14.0,支持 32 位和 64 位进程。

基础用法

1. 附加到进程

Pymem 类是核心入口,支持通过进程名或 PID 附加:

import pymem
import pymem.process

# 通过进程名附加(需要目标进程正在运行)
pm = pymem.Pymem('notepad.exe')

print(f"进程 ID: {pm.process_id}")
print(f"进程句柄: {pm.process_handle}")

进程名区分大小写,附加失败会抛出异常,建议加 try/except 处理。

2. 枚举模块与定位基址

附加成功后,第一件事通常是找到目标模块的基址,因为大部分静态数据的地址都是 基址 + 偏移 的形式:

# 枚举目标进程加载的所有 DLL 模块
modules = list(pm.list_modules())
print(f"模块总数: {len(modules)}")

for module in modules[:3]:
    print(f"名称: {module.name}")
    print(f"基址: {hex(module.lpBaseOfDll)}")
    print(f"大小: {module.SizeOfImage} 字节")

主模块(即 .exe 本身)通常在列表的第一个,基址每次运行可能不同(ASLR),所以不能硬编码,要动态获取。

3. 读取内存数据

pymem 提供了一系列类型化的读取方法:

address = 0x7ff600001234  # 示例地址

# 读取整数(4字节)
value = pm.read_int(address)

# 读取浮点数
f_value = pm.read_float(address)

# 读取长整型(8字节)
l_value = pm.read_long(address)

# 读取字符串(注意编码)
try:
    s = pm.read_string(address, encoding='utf-8')
    print(f"字符串: {s}")
except pymem.exception.MemoryReadError:
    print("地址不可读,可能是无效地址或没有读取权限")

读取失败会抛出 MemoryReadError,地址是否可读取决于目标进程的内存页保护属性。

4. 写入内存数据

写入和读取是对应的:

# 写入整数
pm.write_int(address, 9999)

# 写入浮点数
pm.write_float(address, 3.14)

# 写入字节数组
pm.write_bytes(address, b'\x90\x90\x90\x90', 4)

写入操作更容易导致目标程序崩溃,建议先备份原始值,改完如果有问题能还原。

5. 处理多级指针链

这是实际使用中最麻烦的地方。很多程序里,数据并不在固定地址,而是通过多层指针间接引用。比如:

[模块基址 + 0x1000] → 某个对象指针
→ [对象指针 + 0x20] → 嵌套对象指针
→ [嵌套对象 + 0x50] → 最终数据

pymemRemotePointer 可以处理这种场景:

from pymem.memory import RemotePointer

def resolve_pointer_chain(base_addr: int, offsets: list) -> int:
    """
    解析多级指针链,返回最终目标地址

    :param base_addr: 起始地址(通常是 模块基址 + 静态偏移)
    :param offsets: 每一级的偏移量列表
    :return: 最终地址,失败返回 0
    """
    try:
        ptr = RemotePointer(pm.process_handle, base_addr)

        for offset in offsets[:-1]:
            if ptr.value == 0:
                raise ValueError(f"遇到空指针,当前偏移: 0x{offset:X}")
            ptr = RemotePointer(pm.process_handle, ptr.value + offset)

        if ptr.value == 0:
            raise ValueError("最终级指针为空")

        return ptr.value + offsets[-1]

    except Exception as e:
        print(f"指针链解析失败: {e}")
        return 0


# 使用示例
module_base = pm.base_address  # 主模块基址
target_addr = resolve_pointer_chain(module_base + 0x1000, [0x20, 0x50])

if target_addr:
    value = pm.read_int(target_addr)
    print(f"目标值: {value}")

实际使用的一些注意点

1. 权限问题
附加到系统进程(如 lsass.exewinlogon.exe)通常需要管理员权限,普通用户级进程一般没问题。

2. 地址有效性
直接使用硬编码地址几乎必然失败,ASLR(地址空间布局随机化)会在每次启动时随机化基址。正确做法是先拿模块基址,再加偏移。

3. 64 位 vs 32 位
64 位进程的指针是 8 字节,读取时用 read_longlongread_long;32 位进程用 read_int。混用会读到错误数据,甚至崩溃。

4. 编码问题
读取字符串前最好知道目标程序用的是什么编码(UTF-8、GBK、UTF-16 LE),猜错了读出来是乱码。

总结

pymem 的接口设计比较简洁,把繁琐的 Windows API 调用屏蔽掉了,适合做进程内存分析的快速原型。如果你的需求是:

  • 学习内存操作原理 → 适合,代码直观

  • 轻量级进程数据读取 → 适合

  • 高频率读写或性能敏感场景 → 建议用 C/C++ 或更底层的方式

库的 GitHub 地址:https://github.com/srounet/Pymem,文档相对简单,有问题直接看源码比较清楚。

博主关闭了所有页面的评论