【逆向经验】一篇文章讲透为什么CE搜不到Python游戏的内存值
2026/5/5 14:55:27 网站建设 项目流程

文章目录

    • 概述
    • C 语言的内存模型:CE 的假设
    • Python 的内存模型:三层间接
      • 第一层:PyFloatObject
      • 第二层:dict 存指针,不存值
      • 第三层:不可变对象 = 每次赋值都换地址
    • CE 搜索失败的完整原因
    • 哪些游戏有这个问题
    • 总结

概述

在逆向某游戏(Cocos + Python 2.7)时遇到一个现象:游戏中"当前年剩余时间"(LevelRestTime)是一个不断变化的 float 值(从120倒计时到0),但用 Cheat Engine 无论怎么搜都搜不到。

值没有加密,问题出在Python 的对象内存模型与 CE 的搜索假设根本不兼容。这个问题对所有内嵌 Python/Lua/JS 等脚本引擎的游戏都适用。

C 语言的内存模型:CE 的假设

CE 的设计基于 C/C++ 的内存模型——变量是一块固定地址上的裸值

// C 语言floatLevelRestTime=114.99f;// 4字节, 固定地址 0x12345678// 下一帧LevelRestTime-=0.016f;// 同一个地址, 值变了
内存 @ 0x12345678: 帧1: [42 F9 E6 C2] → 114.99 帧2: [9A F9 E6 C2] → 114.97 帧3: [F2 F8 E6 C2] → 114.95 地址不变, 值在原地递减

CE 的"首次扫描 → 再次扫描"流程完美契合这个模型:同一个地址上的值在变化,筛几轮就锁定了。

Python 的内存模型:三层间接

Python 中一切皆对象,没有"裸值"。一个 float 变量实际上是这样的结构:

第一层:PyFloatObject

PyFloatObject (Python 2.7, 64位, 共24字节): ┌──────────────────────────────┐ │ +0x00 ob_refcnt (8字节) │ 引用计数 │ +0x08 ob_type (8字节) │ → PyFloat_Type 类型指针 │ +0x10 ob_fval (8字节) │ ← 实际的 double 值 └──────────────────────────────┘

注意1:Python 用double(8字节),不是float(4字节)。CE 默认搜 4 字节 float,类型就不对。

注意2:实际值在对象偏移+0x10处,前面有16字节的对象头。

第二层:dict 存指针,不存值

LevelRestTime存在m_dctProp字典中。Python dict 的条目结构:

dict entry (24字节): ┌──────────────────────────────────────────────┐ │ +0x00 hash (8字节) │ │ +0x08 key (8字节) → PyString "LevelRest…"│ │ +0x10 value (8字节) → PyFloatObject* │ ← 指针! └──────────────────────────────────────────────┘

dict 里存的是指向 PyFloatObject 的指针,不是 float 值本身。

第三层:不可变对象 = 每次赋值都换地址

Python 的 float 是不可变对象(immutable)。LevelRestTime -= dt这行代码实际执行的是:

# 不是 "修改原地的值"# 而是 "创建新对象, 替换指针"LevelRestTime=LevelRestTime-dt

等价于:

1. old = PyFloatObject @ 0xAAAA0000, ob_fval = 114.99 2. new = PyFloat_FromDouble(114.99 - 0.016) → 分配新的 PyFloatObject @ 0xBBBB0000, ob_fval = 114.97 3. dict["LevelRestTime"] 的 value 指针从 0xAAAA0000 改为 0xBBBB0000 4. old 对象引用计数归零 → 被垃圾回收(内存释放或回收到 free list)

CE 搜索失败的完整原因

把三层叠加起来,CE 的每一步都踩坑:

帧1: 搜 Double 114.99 → 找到地址 0xAAAA0010 (PyFloatObject.ob_fval) ✓ 第一次能搜到 帧2: 再次扫描, 期望 0xAAAA0010 处的值变为 ~114.97 → 但 0xAAAA0010 处的对象已被回收! → 该地址的内存可能已被其他对象占用, 值是垃圾 → CE: "值不匹配, 排除" ✗ 筛掉了 → 真正的 114.97 在新地址 0xBBBB0010 → 但这个地址不在 CE 的候选列表里 ✗ 找不到
CE 假设Python 现实结果
值是4字节float值是8字节double类型不匹配
值在固定地址每帧创建新对象,地址变化再次扫描失败
修改地址处的值即可dict存的是指针,需要替换指针即使找到也改不动

哪些游戏有这个问题

不只是 Python 游戏。所有使用带 GC 的脚本引擎的游戏都有类似问题:

引擎值对象类型CE 能直接搜吗
CPython 2/3PyFloatObject (不可变)搜不到,地址每次变
Lua 5.xTValue (tagged union)有时能搜到(Lua 用原地 TValue,较友好)
LuaJITGCobj / TValue取决于 JIT 优化,可能搜到
V8 (JS)HeapNumber (不可变)搜不到,和 Python 一样
Mono (C#)值类型在栈/堆struct 类型通常能搜到
IL2CPP编译为 C++,裸值能搜到(最友好)

经验法则:如果脚本语言的数值类型是不可变对象(Python float、JS Number),CE 基本搜不到。如果是原地修改的值类型(Lua TValue、C# struct),CE 通常能搜到。

总结

CE 搜不到 Python 游戏的值,不是因为加密,而是因为内存模型的根本差异:

C/C++ 游戏: 地址固定 → 值原地变化 → CE 完美匹配 Python 游戏: 每次赋值 → 新建对象 → 换指针 → 旧地址失效 → CE 跟丢

面对脚本引擎游戏,放弃 CE 的"扫描-筛选"流程,转向:

  1. 理解脚本引擎的对象模型(PyObject、TValue 等)
  2. 用 Frida 注入到引擎内部,通过引擎自己的 API 读写数据
  3. 操作对象指针而非裸内存值

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询