找函数指南
本篇将介绍如何理解 BDS 中的函数的作用,以及如何在 BDS 中找到你想要的函数。
如何开始?
在开始之前,你最好已经阅读了Hook指南。并且对 C++ 和底层知识有一定的了解。
需要的工具
IDA Pro
:用于反编译 BDS 的二进制文件。
Note
如果你没有购买 IDA Pro,可以使用IDA Free,但是 IDA Free 没有将函数反编译为C语言的功能。
为什么要理解BDS的函数?
试想一个场景,你需要获取玩家的坐标,你会怎么做?
很明显,如果你打开过 IDA Pro,查看过生成的函数列表,你会发现 BDS 是高度面向对象的,而如果你搜索过 Player
, 这个关键词,你会发现这个类里有很多的函数,但是却没看到 getPos
,getPosition
这样的函数。
为什么呢?
这时候,我们打开 LeviLamina 关于 Player 类的 Fake Header
文件,你会发现这个类的定义是这样的:
C++ |
---|
| class Player : public ::Mob {
...
};
|
很明显,Player 继承了 Mob 类,而依次类推,Mob 又是继承的 Actor,在 Actor 类中,我们终于找到了
C++ |
---|
| class Vec3 const& getPosition() const;
|
这样一个函数,我们依靠函数的名称猜测出这个函数可能的作用,并且返回值是一个 Vec3
类型的引用,这更符合我们的预期。
但是,为了更加确信这个函数的作用,我们还需要查看这个函数的实现。
我们打开IDA Pro,搜索 Actor::getPosition
IDA Pro给出这样的结果:
这个函数的实现非常简单,它返回了一个 Vec3
类型的指针,这个指针是 this
指针加上偏移量 84
的值。
根据经验,一个对象指针加上一个偏移量,那么这个多半是一个成员变量。
而后续我们的实践证明了,这个函数确实是返回了玩家的坐标(或者说这个函数用于返回一个实体的坐标)。
事实上,这个函数非常简单,函数的命名也十分直观,寻找到它的过程也非常简单,而在我们的开发中,依靠这样的方法来寻找目标是最基本的技能。
确定模组目的后,如何寻找函数?
有时候,我们寻找函数的目的不是为了调用,而是为了修改函数的行为,使其符合我们的需求。
例如,如果我们想要实现一个功能,玩家聊天的内容转发到控制台,我们就需要找到BDS中处理玩家聊天的函数。(是的,原生BDS并没有这个功能)
那么,我们该如何寻找这个函数呢?
通过查阅资料,我们知道 Minecraft 基岩版客户端与服务端之间的通信使用的是 RakNet
协议,而 RakNet
协议使用的是 UDP
协议。
这就意味着,BDS 中会收到来自客户端的数据包。
查阅资料可知,处理聊天的数据包是 TextPacket
,我们打开 IDA Pro,搜索 TextPacket
。
顺利的找到了收取 TextPacket
的函数:
C++ |
---|
| public: virtual void __cdecl ServerNetworkHandler::handle(class NetworkIdentifier const & __ptr64,class TextPacket const & __ptr64) __ptr64
//symbol: ?handle@ServerNetworkHandler@@UEAAXAEBVNetworkIdentifier@@AEBVTextPacket@@@Z
|
利用我们在Hook指南中学到的知识,我们可以很容易的 Hook 这个函数,实现我们的功能。(吗?)
好吧,事实上我们遇到第二个问题,这里似乎没有一个 std::string
类型的参数,我们似乎无法获取到玩家的聊天内容。
但是真的如此吗?
我们打开 IDA Pro,观察这个函数的伪代码:
C++ |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79 | void __fastcall ServerNetworkHandler::handle(
ServerNetworkHandler *this,
const struct NetworkIdentifier *a2,
const struct TextPacket *a3)
{
const struct Player *v5; // rax
const struct Player *v6; // rdi
const struct PlayerCapabilities::ISharedController *v7; // rax
__int64 v8; // rax
__int64 v9; // rcx
__int64 v10; // rax
__int128 v11; // [rsp+28h] [rbp-D8h] BYREF
int v12; // [rsp+38h] [rbp-C8h]
char *v13; // [rsp+40h] [rbp-C0h] BYREF
char v14; // [rsp+C0h] [rbp-40h]
char v15; // [rsp+C8h] [rbp-38h]
char v16[16]; // [rsp+D0h] [rbp-30h] BYREF
char v17[32]; // [rsp+E0h] [rbp-20h] BYREF
__int128 v18; // [rsp+100h] [rbp+0h]
int v19; // [rsp+110h] [rbp+10h]
char v20; // [rsp+118h] [rbp+18h]
__int128 v21; // [rsp+120h] [rbp+20h]
__int64 v22; // [rsp+130h] [rbp+30h]
char v23[32]; // [rsp+138h] [rbp+38h] BYREF
__int16 v24; // [rsp+158h] [rbp+58h]
v5 = (const struct Player *)(*(__int64 (__fastcall **)(char *, const struct NetworkIdentifier *, _QWORD))(*((_QWORD *)this - 2) + 8i64))(
(char *)this - 16,
a2,
*((unsigned __int8 *)a3 + 16));
v6 = v5;
if ( v5 )
{
PlayerCapabilities::PlayerData::PlayerData((PlayerCapabilities::PlayerData *)v16, v5);
v7 = (const struct PlayerCapabilities::ISharedController *)(*(__int64 (__fastcall **)(_QWORD))(***((_QWORD ***)this + 9)
+ 2200i64))(**((_QWORD **)this + 9));
if ( TextPacket::_shouldHandleTextPacketForPlayer(a3, (struct PlayerCapabilities::IPlayerData *)v16, v7) )
{
if ( *((_BYTE *)a3 + 48) == 1 && (!*((_QWORD *)a3 + 13) || *(_BYTE *)std::string::at((char *)a3 + 88, 0i64) != 47) )
{
EntityContext::getWeakRef((char *)v6 + 8, &v11);
std::string::string(v17, (char *)a3 + 88);
v18 = 0i64;
if ( *((_QWORD *)&v11 + 1) )
{
v18 = v11;
_InterlockedIncrement((volatile signed __int32 *)(*((_QWORD *)&v11 + 1) + 12i64));
}
v19 = v12;
v20 = 0;
v21 = 0i64;
v22 = 0i64;
std::string::string(v23, (char *)a3 + 56);
v24 = 257;
v9 = *((_QWORD *)&v11 + 1);
if ( *((_QWORD *)&v11 + 1)
&& _InterlockedExchangeAdd((volatile signed __int32 *)(*((_QWORD *)&v11 + 1) + 12i64), 0xFFFFFFFF) == 1 )
{
(*(void (__fastcall **)(__int64))(*(_QWORD *)v9 + 8i64))(v9);
}
v10 = (*(__int64 (__fastcall **)(_QWORD))(***((_QWORD ***)this + 9) + 1440i64))(**((_QWORD **)this + 9));
v13 = v17;
v14 = 0;
v15 = 0;
if ( (unsigned int)ServerNetworkEventCoordinator::sendEvent(v10, &v13) == 1 )
ServerNetworkHandler::_displayGameMessage(
(ServerNetworkHandler *)((char *)this - 16),
v6,
(struct ChatEvent *)v17);
ChatEvent::~ChatEvent((ChatEvent *)v17);
}
}
else if ( *((_QWORD *)a3 + 13) > 0x200ui64 )
{
v8 = (*(__int64 (__fastcall **)(const struct Player *))(*(_QWORD *)v6 + 2048i64))(v6);
(*(void (__fastcall **)(__int64, const struct Player *, __int64))(*(_QWORD *)v8 + 968i64))(v8, v6, 1i64);
}
}
}
|
看起来很复杂哈?
但是我们并不需要一次性全部理解,我们只需要明确我们需要获取的信息:玩家
,聊天内容
。
而,聊天内容大抵是std::string
类型的,我们可以在这个函数中搜索std::string
,找到这样一段代码:
C++ |
---|
| std::string::string(v17, (char *)a3 + 88);
...
std::string::string(v23, (char *)a3 + 56);
|
好吧,光是字符串的构造函数有这么多,但是我们观察到 v23 并没有后续的调用,而 v17 却有,我们可以猜测 v17 是我们想要的字符串。
而下文的代码:
C++ |
---|
| if ( (unsigned int)ServerNetworkEventCoordinator::sendEvent(v10, &v13) == 1 )
ServerNetworkHandler::_displayGameMessage(
(ServerNetworkHandler *)((char *)this - 16),
v6,
(struct ChatEvent *)v17);
|
也极大的支持了我们的猜测。
而 Player 也很容易的从第一行可以得到:
C++ |
---|
| v5 = (const struct Player *)(*(__int64 (__fastcall **)(char *, const struct NetworkIdentifier *, _QWORD))(*((_QWORD *)this - 2) + 8i64))(
(char *)this - 16,
a2,
*((unsigned __int8 *)a3 + 16));
|
这很明显是一个虚函数的调用,最终返回了一个 Player 指针。
我们可以直接抄下来,但是这样很不优雅。
而查阅 LeviLamina 提供的有关 ServerNetworkHandler 的内容
我们可以得知,ServerNetworkHandler 类提供了一个虚函数 _getServerPlayer
,这个函数的作用是获取一个玩家的指针。
但是,上下文中的:
C++ |
---|
| // Do not use _getServerPlayer directly
// Please get SubID from the packet
[[nodiscard]] inline optional_ref<ServerPlayer>
getServerPlayer(class NetworkIdentifier const& source, ::SubClientId subId) {
auto& handler = ll::memory::dAccess<ServerNetworkHandler>(this, -16);
return {handler._getServerPlayer(source, subId)};
}
|
和前面的伪代码可以看出,直接调用 _getServerPlayer 并不可行,我们需要对NetworkIdentifier 指针做一个 -16 的偏移才能正确的获取玩家指针,为了方便开发,LeviLamina提供了一个封装,我们可以跳过这个步骤,直接使用这个封装来获取玩家的指针。
就此,我们已经实现了我们的目标,我们可以Hook这个函数,实现我们的功能。
具体的代码实现,限于篇幅,不再赘述。
而LeviLamina在新版本提供了部分数据包的结构体信息,你可以在 TextPacket.h 中看到,LLDEV 已经补全了这个数据包的结构体信息。
C++ |
---|
| std::string mMessage; // this+0x58
|
利用这个,我们可以直接使用 textPacket.mMessage
代替偏移来获取玩家的聊天内容。
Danger
但是我们不是官方,提供的结构体信息并不一定完全正确,可能随着版本的更新,这个结构体会发生变化,而我们可能没有及时更新,所以,使用结构体替代偏移的时候,一定要注意!
当然,如果你发现了这样的问题,欢迎提交PR。
总结
本篇介绍了如何理解 BDS 中的函数的作用,以及如何在 BDS 中找到你想要的函数。
事实上,本教程只能是一个入门教程,实际开发中你会遇到更加困难的条件和情况,这就需要你付出更多的努力,去学习更多的知识,去解决更多的问题。
希望本教程能够帮助到你,祝你开发愉快!