进程的地址空间

一个很基本 (但也很困难) 的问题

进程的状态机模型

  • 进程状态 = 内存 + 寄存器
  • 到底什么是 “进程的内存”?
  1. 以下程序的 (可能) 输出是什么?

printf("%p\n", main);

  1. 何种指针访问不会引发segmentation fault?
1
2
3
char *p = random();
*p; // Load
*p = 1; // Store

进程的地址空间

RTFM: /proc/[pid]/maps (man 5 proc)

  • 进程地址空间中的每一段
    • 地址 (范围) 和权限 (rwxsp)
    • 对应的文件: offset, dev, inode, pathname
      • TFM 里有更详细的解释
  • 和 readelf (-l) 里的信息互相验证

更多的提问:我们能 “控制” pmap 的输出吗?

  • 修改堆 (bss) 内存的大小
  • 在栈上分配大数组……

管理进程地址空间

状态机的视角

  • 地址空间 = 带访问权限的内存段
    • 不存在 (不可访问)
    • 不存在 (可读/写/执行)
  • 管理 = 增加/删除/修改一段可访问的内存

你会提供怎样的系统调用?

Memory Map 系统调用

在状态机状态上增加/删除/修改一段可访问的内存

  • MAP_ANONYMOUS: 匿名 (申请) 内存
  • fd: 把文件 “搬到” 进程地址空间中 (例子:加载器)
  • 更多的行为请参考手册 (复杂性暴增)
1
2
3
4
5
6
7
// 映射
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);

// 修改映射权限
int mprotect(void *addr, size_t length, int prot);

使用 mmap

Example 1: 申请大量内存空间

  • 瞬间完成内存分配
    • mmap/munmap 为 malloc/free 提供了机制
    • libc 的大 malloc 会直接调用一次 mmap 实现
  • 不妨 strace/gdb 看一下

Example 2: Everything is a file

  • 映射大文件、只访问其中的一小部分
1
2
3
4
with open('/dev/sda', 'rb') as fp:
mm = mmap.mmap(fp.fileno(),
prot=mmap.PROT_READ, length=128 << 30)
hexdump.hexdump(mm[:512])

入侵进程地址空间

Hacking Address Spaces

进程 (状态机) 在 “无情执行指令机器” 上执行

  • 状态机是一个封闭世界
  • 但如果允许一个进程对其他进程的地址空间有访问权?
    • 意味着可以任意改变另一个程序的行为

一些 “入侵” 进程地址空间的例子

  • 调试器 (gdb)
    • gdb 可以任意观测和修改程序的状态
  • Profiler (perf)
    • M3 中借助它理解程序的性能瓶颈

物理入侵进程地址空间

金手指:直接物理劫持内存

  • 听起来很离谱,但 “卡带机” 时代的确可以做到!
  • 今天我们有 Debug Registers 和 Intel Processor Trace
  • 帮助系统工具 “合法入侵” 地址空间

Game Genie: 一个 Look-up Table (LUT)

c15-2-1.webp

  • 简单、优雅:当 CPU 读地址 aa 时读到xx,则替换为yy
    • Technical Notes (专利, How did it work?)

Game Genie as a Firmware

  • 配置好 LUT、加载卡带上的代码 (像是一个 “Boot Loader”)

c15-2-2.webp

随着游戏越来越大……

地址空间那么大,哪个才是 “金钱”?

  • 包含动态分配的内存,每次地址都不一样
  • 思路:Everything is a state machine
    • 观察状态机的 trace,就知道哪个是金钱了

查找 + Filter

  • 进入游戏时exp=4950exp=4950
    打了个怪exp=5100exp=5100
    符合495051004950→5100变化的内存地址是很少的
    • 好了,出门就是满级了

入侵进程地址空间:金山游侠

一招制胜

  • 包含非常贴心的 “游戏内呼叫” 功能 (Hack DirectX)

c15-2-3.webp

  • 它就是专为游戏设计的 “调试器”

给进程发送 GUI 事件

按键精灵

大量重复固定的任务 (例如 2 秒 17 枪)

c15-3-1.webp

现按键精灵

给进程发送键盘/鼠标事件

  • 做个驱动 (可编程键盘/鼠标)
  • 利用操作系统/窗口管理器提供的 API
    • xdotool
      • (我们用这玩意测试 vscode 的插件)
    • ydotool
    • evdev (按键显示脚本;主播常用)

2024 年的应用:实现 AI Copilot Agent

  • 文本/截图 → AI 分析 → 执行动作

改变进程对时间的感知

调整游戏的逻辑更新速度

比如神秘公司神秘游戏慢到难以忍受的跑图和战斗

  • 今天游戏市场已经内卷到新手成长路线不顺滑就会被喷了

c15-4-1.webp

变速齿轮

c15-4-2.webp

变速齿轮:原理

程序 = 状态机

  • “计算指令” 是不能感知时间的
    • spin count 计时会出现 “机器变快,游戏没法玩” 的情况
    • syscall 是感知时间的唯一方法

“劫持” 和时间相关的syscall/库函数

  • 改变程序对时间的认知
  • 就像手表调快/慢了一样

定制游戏外挂

“劫持代码” 的本质是 debugger 行为

  • 游戏也是程序,也是状态机
  • 外挂就是 “为这个游戏专门设计的 gdb”

例子:锁定生命值

  • 创建,线程 spin modify:
    while (1) hp = 9999;

  • 但还是可能出现 hp < 0 的判定 (一刀秒)

  • 可以 patch 掉判定的代码 (软件动态更新)

代码注入

用一段代码 “勾住” (hook) 函数的执行

  • 然后就可以入侵程序逻辑 (从而为所欲为) 了

c15-4-3.gif

关于外挂/代码注入

也可以用来做 “好” 的事情

  • “软件动态更新”:在不停止系统的时候打热补丁 Live kernel patching
    • 技术,无论是计算机系统、编程语言还是人工智能,都是给人类带来福祉的——我们甚至可以开发游戏外挂辅助你练习、提高成绩。

    • 与此同时,强大的技术总有 “负面” 的用途。用任何技术损害他人的利益,都是一件可耻的事情 (academic integrity)。同样,如果你希望在人生这场 game (博弈) 中走得更远。

状态机的视角自然地将我们引入 “内存到底是什么” 的问题——它的答案同样也很自然:带有访问权限控制的连续内存段。我们可以通过 mmap、munmap、mprotect 三个系统调用调整状态机的地址空间,包括分配匿名的内存、映射文件内容到内存、修改访问权限等。更有趣的是操作系统有 “能够实现一切应用程序” 的需求,调试器也不在话下——这也给了我们入侵其他进程地址空间的机制。