Mojang 已经在 1.21.20.03 中修复了此问题
本文将介绍我通过 LeviLamina 以及 libhat 通过 Patch 的方式解决 Bedrock Dedicated Server (以下简称 BDS) 中服务端收到空包会在控制台刷 ATTENTION! Received EMPTY UDP packet - potential UDP ports scanning.
的问题。
# 起因
在 BDS 的早期版本中,存在着非常多的严重的远程崩服漏洞,空包崩服就是其中一种,自某个版本后 Mojang 便修复了此漏洞,但同时又在 RakNet 中拉了一坨 printf
来提醒使用者服务器收到了空包,但这又导致了另一个问题,就是攻击者可以通过频繁地发送空包来造成主线程堵塞。此问题已经由社区向反馈差不多一年时间了,Mojang 仍然没有在公开的 BDS 版本中修复此问题,鉴于前些日子官方和 MCC 合作的活动服务器是由 BDS+ScriptAPI 驱动的,但活动服中并无此问题,由此可见 Mojang 早就知道 BDS 有这个问题,但却迟迟不修复。于是,我们只能自己动手来修复这一问题。
# 寻找 printf
函数所在位置
首先编写一个发送空包到 BDS 端口的脚本,这里选择使用 Python。
import socket | |
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
i = 0 | |
while i < 10000: # 发送 10000 个空包到 BDS | |
s.sendto( | |
"", | |
("127.0.0.1", 19132), | |
) | |
i = i + 1 |
将脚本保存为 empty.py
。
然后通过 Visual Studio 打开 / 挂载 BDS 进程,选用其它调试器例如 x64dbg 也可以。
运行此脚本: py empty.py
此时在控制台观察到大量的 ATTENTION! Received EMPTY UDP packet - potential UDP ports scanning.
输出,通过 Visual Studio 或其它调试器暂停主线程,我们可以看到如下堆栈:
ntdll.dll!NtWaitForSingleObject() | |
KernelBase.dll!WaitForSingleObjectEx() | |
bedrock_server.exe!RakNet::UpdateNetworkLoop(void *) | |
ucrtbase.dll!thread_start<unsigned int (__cdecl*)(void *),1>() | |
kernel32.dll!BaseThreadInitThunk() | |
> ntdll.dll!RtlUserThreadStart() |
嗯?似乎没有看到任何有关输出流的调用?没关系,让我们再试一次。
> ntdll.dll!NtWriteFile() | |
KernelBase.dll!WriteFile() | |
ucrtbase.dll!write_text_ansi_nolock() | |
ucrtbase.dll!_write_nolock() | |
ucrtbase.dll!_write_internal() | |
ucrtbase.dll!write_buffer_nolock<char>() | |
ucrtbase.dll!common_flush_and_write_nolock<char>() | |
ucrtbase.dll!__crt_stdio_output::stream_output_adapter<char>::write_character_without_count_update() | |
ucrtbase.dll!__crt_stdio_output::output_processor<char,__crt_stdio_output::stream_output_adapter<char>,__crt_stdio_output::standard_base<char,__crt_stdio_output::stream_output_adapter<char>>>::process() | |
ucrtbase.dll!<lambda>(void)() | |
ucrtbase.dll!__crt_seh_guarded_call<int>::operator()<<lambda_d854c62834386a3b23916ad6dae2782d>,<lambda>(void) &,<lambda>(void)>() | |
ucrtbase.dll!__stdio_common_vfprintf() | |
bedrock_server.exe!printf() | |
bedrock_server.exe!RakNet::RNS2_Berkley::RecvFromLoopInt(void) | |
bedrock_server.exe!RakNet::RNS2_Berkley::RecvFromLoop(void *) | |
ucrtbase.dll!thread_start<unsigned int (__cdecl*)(void *),1>() | |
kernel32.dll!BaseThreadInitThunk() | |
ntdll.dll!RtlUserThreadStart() |
这次我们得到了我们想要的东西,我们看到了我们期望的 printf
,以及 printf
在 RakNet::RNS2_Berkley::RecvFromLoopInt(void)
中调用的有效信息,现在,我们可以打开 IDA 或其它同类型的反编译软件来具体查看 RakNet::RNS2_Berkley::RecvFromLoopInt(void)
中 printf
的具体位置了。
unsigned RakNet::RNS2_Berkley::RecvFromLoopInt(RakNet::RNS2_Berkley *this) | |
{ | |
// 略... | |
while ( !*((_BYTE *)this + 260) ) | |
{ | |
v5 = *((_QWORD *)this + 30); | |
if ( v5 ) | |
{ | |
v6 = (*(__int64 (__fastcall **)(__int64, const char *, __int64))(*(_QWORD *)v5 + 24i64))( | |
v5, | |
"D:\\a\\_work\\1\\s\\handheld\\src-deps\\raknet\\raknet\\RakNetSocket2.cpp", | |
388i64); | |
v7 = v6; | |
if ( v6 ) | |
{ | |
// 略... | |
if ( v12 <= 0 ) | |
{ | |
// 略... | |
if ( v14 ) | |
(*(void (__fastcall **)(__int64, __int64, const char *, __int64))(*(_QWORD *)v14 + 16i64))( | |
v14, | |
v7, | |
"D:\\a\\_work\\1\\s\\handheld\\src-deps\\raknet\\raknet\\RakNetSocket2.cpp", | |
408i64); | |
printf("\n\n ATTENTION! Received EMPTY UDP packet - potential UDP ports scanning.\n\n"); // 找到 Mojang 拉的屎了 | |
} | |
else | |
{ | |
v13 = *((_QWORD *)this + 30); | |
if ( v13 ) | |
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)v13 + 8i64))(v13, v7); | |
} | |
} | |
} | |
} | |
RakNet::LocklessUint32_t::Decrement((RakNet::RNS2_Berkley *)((char *)this + 256)); | |
if ( v18 ) | |
{ | |
if ( _InterlockedExchangeAdd(v18 + 2, 0xFFFFFFFF) == 1 ) | |
{ | |
(**(void (__fastcall ***)(volatile signed __int32 *))v18)(v18); | |
if ( _InterlockedExchangeAdd(v18 + 3, 0xFFFFFFFF) == 1 ) | |
(*(void (__fastcall **)(volatile signed __int32 *))(*(_QWORD *)v18 + 8i64))(v18); | |
} | |
} | |
return 0i64; | |
} |
通过 IDA 伪代码,我们可以看到 Mojang 确确实实在 RecvFromLoopInt
里面拉了坨大的,现在我们要做的事就是 Patch 掉这坨了。
切换到 IDA View,右键选择 Synchrnoize with->Pseudocode-A,我们就可以看到罪魁祸首的汇编了:
loc_14232424B:
lea rcx, aAttentionRecei ; "\n\n ATTENTION! Received EMPTY UDP pac"...
call printf
jmp short loc_1423242B9
我们需要将 lea
和 call
给 Patch 为空字节,接下来切换到 Hex View,同样右键选择 Synchrnoize with->Pseudocode-A,我们就找到了罪魁祸首的 16 进制:
48 8D 0D 9E DB B7 00 E8 D9 EB FD FF
# 通过 LeviLamina 和 libhat 进行 Patch
现在就是我们通过 LeviLamina 和 libhat 来大展拳脚的时候了
#include "libhat/Scanner.hpp" | |
#include "libhat/Signature.hpp" | |
#include "ll/api/memory/Memory.h" | |
void consoleSpamPatch() { | |
std::byte* funcBegin = (std::byte*)LL_RESOLVE_SYMBOL("?RecvFromLoopInt@RNS2_Berkley@RakNet@@IEAAIXZ"); | |
constexpr auto pattern = hat::compile_signature<"48 8D 0D 9E DB B7 00 E8 D9 EB FD FF">(); | |
auto result = hat::find_pattern(funcBegin, funcBegin + 0x250, pattern); | |
if (!result.has_result()) { | |
std::cout << "Can't find signature for RecvFromLoopInt\n"; | |
} | |
std::byte* patchLocation = (std::byte*)result.get(); | |
size_t patchLength = 12; | |
ll::memory::modify(patchLocation, oatchLength, [&]() { | |
std::memset(patchLocation, 0x90 /* nop */, oatchLength); | |
}); | |
} |
我们也可以改用模糊匹配,将一些可能会变动的字节改为 ??
#include "libhat/Scanner.hpp" | |
#include "libhat/Signature.hpp" | |
#include "ll/api/memory/Memory.h" | |
void consoleSpamPatch() { | |
std::byte* funcBegin = (std::byte*)LL_RESOLVE_SYMBOL("?RecvFromLoopInt@RNS2_Berkley@RakNet@@IEAAIXZ"); | |
constexpr auto pattern = hat::compile_signature<"48 8D 0D ?? ?? ?? 00 E8 ?? ?? FD FF">(); | |
auto result = hat::find_pattern(funcBegin, funcBegin + 0x250, pattern); | |
if (!result.has_result()) { | |
std::cout << "Can't find signature for RecvFromLoopInt\n"; | |
} | |
std::byte* patchLocation = (std::byte*)result.get(); | |
size_t patchLength = 12; | |
ll::memory::modify(patchLocation, oatchLength, [&]() { | |
std::memset(patchLocation, 0x90 /* nop */, oatchLength); | |
}); | |
} |
将代码嵌入 Mod 中,然后将 Mod 安装进带有 LeviLamina 的服务端中。
重新执行发送空包的 Python 脚本,服务器的控制台无事发生,就证明我们的 Patch 已经生效了。