【逆向经验】一篇文章讲透为什么CE搜不到Python游戏的内存值

文章目录

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

概述

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

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

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

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

c 复制代码
// C 语言
float LevelRestTime = 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 这行代码实际执行的是:

python 复制代码
# 不是 "修改原地的值"
# 而是 "创建新对象, 替换指针"
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/3 PyFloatObject (不可变) 搜不到,地址每次变
Lua 5.x TValue (tagged union) 有时能搜到(Lua 用原地 TValue,较友好)
LuaJIT GCobj / 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. 操作对象指针而非裸内存值
相关推荐
Zik----2 小时前
CILP模型讲解
人工智能·python·多模态
陈eaten2 小时前
汇编使用AES指令集实现AES解密
汇编·python·aes解密·aes指令集
SilentSamsara2 小时前
闭包的本质:Python 如何捕获自由变量
开发语言·python·青少年编程·pycharm
十五年专注C++开发2 小时前
浅谈LLVM
开发语言·c++·qt·clang·llvm
段一凡-华北理工大学2 小时前
【高炉炼铁领域炉温监测、预警、调控智能体设计与应用】~系列文章10:实时预警机制:跑在问题前面!
网络·人工智能·python·知识图谱·高炉炼铁·工业智能体
小熊Coding2 小时前
童年游戏冒险岛(Python版本)
python·游戏·pygame
白夜11172 小时前
C++(标签派发 Tag Dispatching)
开发语言·c++·笔记·算法
WJ.Polar3 小时前
Scapy基本应用
linux·运维·网络·python
CSCN新手听安3 小时前
【Qt】Qt窗口(六)QMessageBox消息对话框的使用
开发语言·c++·qt