trapdoor 设计思路

原理和设计

本插件并没有用到什么十分高深的技巧或者技术,完全是在巨人的肩膀上进行的一些微小的工作,
毕竟tr最初的名字就叫MCBEToolSet.下面简单介绍下tr的原理和设计。

原理

HOOK和CALL

Tr的核心就是基于Win32的hook技术,这也是它不能跨平台的最本质原因(linux上也有相应的技术,只不过没有采用)。

整个BDS的运行是就是各种函数依次执行的结果(其它软件也是),不难想到,只要我们可以改动部分函数的逻辑就能在宏观上
影响玩家,这就是tr的核心原理。

hook技术给我们带来了下面两个核心的接口:

  1. HOOK
  2. CALL

HOOK就是能监听特定函数的执行,能让开发者在某个函数执行前和执行后插入部分代码,以及禁止某个函数的执行,下面是表示hook
接口的伪代码:

1
2
3
4
5
def hook(function,before,after,execute):
before()
if execute:
function()
after()

function就是你要监听的函数,before和after就是运行前后要执行的回调代码,而execute就代表是否要执行该函数。
举个简单的例子,我在before里面获取当前时间,然后在after里面获取当前时间,然后将两个时间相减就获得了function()的执行时间,
这就是msptprof的基本原理。

CALL给开发者提供了手动调用某个函数的能力,下面是该接口的伪代码:

1
2
def call(function,args):
function(args)

举个例子就是获取玩家坐标,可以手动调用Actor::getPos(Player*)这个函数即可.

RVA及其计算

RVA

知道最基本的原理后另一个问题来了,我们该如何在工程中实现上述想法呢?这个不用担心,前人早就提供了造好的轮子,能让开发者较为方便地对某个函数进行HOOK和CALL. Tr采用了目前BDS插件圈最常见的方案,也就是微软的Detours库,api/lib/mod.h里面的THOOKSYMCALL宏就是对该库的简单封装。

下面要解决的问题就是如何确定要hook的函数原型(包括参数列表和返回值等)和其在内存中的地址。
前者可以通过开发者手动指定,这也是上面两个宏需要传原型的原因。后者就需要一定的计算了。这里涉及
的底层知识比较多就简单说几句。

我们可以简单地把(经过分页处理后的虚拟)内存理解成一个简单的街道,进程就是街道中的房子,而函数就是房子内的某个家具,编译软件就是造房子的过程。由于CPU在运行软件的时候需要知道每个函数的具体位置,因此编译软件的时候就要确定每个函数的地址(也就是家具相对于街道的位置信息), 但是编译器并不知道操作系统会把进程放在哪个地址,因此编译期间传入的地址都是相对房子角落的,等CPU访问该地址的时候 就把这个相对房子角落的地址加上房子在街道的位置就构成了绝对地址,这样才能正常访问。

在上面的比喻中,家具相对房子的地址(也就是进程空间的地址)就是所谓的RVA(相对虚拟地址),这个地址是能在编译期确定的。不仅如此,Win32的API还提供了获取房子相对街道地址的函数,也就是GetModuleHandle函数。api/lib/mod.h中的getVA()函数就执行了这个地址相加的过程。

获取RVA

让我们继续往下,来看这个RVA如何获取。既然RVA是编译器确定的,那么想获取RVA就只能问编译器了。很巧的是编译器确实提供了这个功能。以MSVC为例,在特定的编译选项下,MSVC编译完成的时候会生成一个pdb格式的二进制附加文件,这个文件提供了该软件所有函数的RVA信息,而BDS的win版release中确实包含了一个pdb文件。

不仅如此,微软还提供了读取这个pdb文件的工具cvdump,我们可以执行如下命令:

1
cvdump -headers -p bdrock_server.exe > dump_sym.txt

打开dump_sym.txt文件你就能看到这样一个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Microsoft (R) Debugging Information Dumper  Version 14.00.23611
Copyright (C) Microsoft Corporation. All rights reserved.
*** PUBLICS
...
S_PUB32: [0001:0079ACB0], Flags: 00000002, ?tick@ServerLevel@@UEAAXXZ
...
*** SECTION HEADERS


SECTION HEADER #1
.text name
1A1C3EC virtual size
1000 virtual address
1A1C400 size of raw data
400 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
60000020 flags
Code
(no align specified)
Execute Read

其中*** PUBLICS*** SECTION HEADERS之间的每一行就是编译器提供给我们的一个函数的信息,我们以

1
S_PUB32: [0001:00452070], Flags: 00000002, ?tick@ServerLevel@@UEAAXXZ

为例看下这行,0001是段号,表示该函数属于哪个段(段相当于房子内的某个区域),0079ACB0是段内地址,?tick@ServerLevel@@UEAAXXZ就是所谓的符号,这个字符串包含了该函数的原型信息。为了计算RVA,我们需要知道该函数所属段的首地址。下面再来看*** SECTION HEADERS下面的部分,不难发现这里正好就是段信息,1000 virtual address给出了该段的相对地址就是1000。综上所述,函数?tick@ServerLevel@@UEAAXXZ的RVA就是0x0079ACB0+0x1000 = 0x0079BCB0.

api/lib/SymHook.h中的地址都是这么算出来的。由于手工计算过于麻烦,所以社区就诞生了一些工具来自动执行这个计算和导出RVA的过程,这也是一般情况下Tr更新的主要成本。

设计

Tr源码结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
├───api //游戏接口
│ ├───block //方块接口
│ ├───commands //命令注册和解析
│ ├───entity //实体接口
│ ├───graphics //粒子效果接口
│ ├───language //多语言支持
│ ├───lib //hook库和其它第三方库
│ ├───tools //工具,包括日志,向游戏内部发信息,简单的线程池实现等等
│ └───world //Level,Dimension接口
├───mod //具体的功能
│ ├───config //配置文件
│ ├───eval //计算器功能
│ ├───fakePlayer //假人通信功能
│ ├───function //漏斗计数器,转方块等功能
│ ├───os //os相关功能
│ ├───player //玩家相关功能
│ ├───spawn //刷怪相关功能
│ ├───test //一些不太全面的测试
│ ├───tick //tick和prof的实现
└───village //村庄相关功能