跳到正文
本頁目錄

找函數指南

本篇將介紹如何理解 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給出這樣的結果:

GAS
.text:00000001415B8D50 ; const struct Vec3 *__fastcall Actor::getPosition(Actor *__hidden this)
.text:00000001415B8D50 ?getPosition@Actor@@QEBAAEBVVec3@@XZ proc near
.text:00000001415B8D50                                         ; CODE XREF: BarterGoal::_getThrowVector(void)+96↑p
.text:00000001415B8D50                                         ; BarterGoal::_getThrowVector(void)+A6↑p ...
.text:00000001415B8D50                 sub     rsp, 28h
.text:00000001415B8D54                 mov     rax, [rcx+2A0h]
.text:00000001415B8D5B                 test    rax, rax
.text:00000001415B8D5E                 jz      short loc_1415B8D65
.text:00000001415B8D60                 add     rsp, 28h
.text:00000001415B8D64                 retn
.text:00000001415B8D65 ; ---------------------------------------------------------------------------
.text:00000001415B8D65
.text:00000001415B8D65 loc_1415B8D65:                          ; CODE XREF: Actor::getPosition(void)+E↑j
.text:00000001415B8D65                 call    ?terminate@details@gsl@@YAXXZ ; gsl::details::terminate(void)
.text:00000001415B8D65 ; ---------------------------------------------------------------------------
.text:00000001415B8D6A                 db 0CCh
.text:00000001415B8D6A ?getPosition@Actor@@QEBAAEBVVec3@@XZ endp

這個函數的實現非常簡單,它返回了一個 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++
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,這個函數的作用是獲取一個玩家的指針。但是,從前面的偽代碼和上下文可以看出,直接調用 _getServerPlayer 並不可行,我們需要對 this 指針做一個 -16 的偏移才能正確的獲取玩家指針。為了方便開發,LeviLamina 的 Hook 宏提供了 thisFor<T>(),它會替你完成這個偏移,所以在 Hook 中可以直接獲取玩家的指針(LeviLamina 自己的 PlayerChatEvent 就是這樣做的):

C++
// 在 LL_TYPE_INSTANCE_HOOK(..., ServerNetworkHandler, &ServerNetworkHandler::$handle, void,
//                         NetworkIdentifier const& identifier, TextPacket const& packet) 中
auto player = thisFor<NetEventCallback>()->_getServerPlayer(identifier, packet.mSenderSubId);

就此,我們已經實現了我們的目標,我們可以Hook這個函數,實現我們的功能。

具體的代碼實現,限於篇幅,不再贅述。

而LeviLamina在新版本提供了部分數據包的結構體信息,你可以在 TextPacket.h 中看到,LeviLamina 開發者已經補全了這個數據包的結構體信息。

C++
std::string              mMessage;    // this+0x58

利用這個,我們可以直接使用 textPacket.mMessage 代替偏移來獲取玩家的聊天內容。

Danger

但是我們不是官方,提供的結構體信息並不一定完全正確,可能隨著版本的更新,這個結構體會發生變化,而我們可能沒有及時更新,所以,使用結構體替代偏移的時候,一定要注意! 當然,如果你發現了這樣的問題,歡迎提交PR。

總結

本篇介紹瞭如何理解 BDS 中的函數的作用,以及如何在 BDS 中找到你想要的函數。

事實上,本教程只能是一個入門教程,實際開發中你會遇到更加困難的條件和情況,這就需要你付出更多的努力,去學習更多的知識,去解決更多的問題。

希望本教程能夠幫助到你,祝你開發愉快!