Powrót do Bloga

Wyjaśnienie exploitu wydajności serwera UEFN: Jak zabezpieczyć Netcode w Unreal Engine

Opublikowano 24 lutego 2026
Wyjaśnienie exploitu wydajności serwera UEFN: Jak zabezpieczyć Netcode w Unreal Engine

Każdy deweloper Multiplayer zna ten koszmarny scenariusz: pojedynczy złośliwy użytkownik łączy się z serwerem, wykonuje pozornie niegroźną sekwencję działań i nagle tick rate spada z 60Hz do wartości jednocyfrowych. Cały serwer staje w miejscu, co dotyka dziesiątki niewinnych graczy.

Niedawno na forach Unreal Engine deweloper Vysena Woyka zgłosił krytyczny exploit wydajności serwera UEFN. Raport opisuje w 100% powtarzalną technikę, która powoduje poważną degradację całego serwera na mapach Unreal Editor for Fortnite (UEFN). Exploit przybiera na sile wraz z dołączaniem kolejnych graczy, nie wymaga absolutnie żadnych narzędzi zewnętrznych i przy dłuższym działaniu może doprowadzić do całkowitej niestabilności serwera.

Ponieważ dokładne kroki reprodukcji są trzymane w tajemnicy, aby zapobiec powszechnym nadużyciom, wielu deweloperów zastanawia się: Jak taki exploit właściwie działa od środka? a co ważniejsze, Jak chronić własne dedykowane serwery Unreal Engine przed podobnymi atakami?

W tej technicznej analizie przyjrzymy się architekturze degradacji wydajności po stronie serwera w Unreal Engine. Zbadamy typowe wektory, których złośliwi gracze używają do przeciążania dedicated servers, dowiemy się, jak zaimplementować rygorystyczną walidację po stronie serwera przy użyciu C++ oraz jak zaprojektować infrastrukturę dla maksymalnej odporności.

Anatomia exploitu serwera Unreal Engine

Aby zrozumieć, jak gracz może unieruchomić serwer bez zewnętrznych narzędzi hakerskich, musisz zrozumieć, jak Unreal Engine obsługuje swoją główną pętlę gry (Main Game Loop). Dedykowane serwery Unreal Engine są w przeważającej mierze jednowątkowe, jeśli chodzi o Game Logic. Podczas gdy zadania takie jak Physics Simulation (za pomocą silnika Chaos) i asynchroniczne ładowanie mogą być oddelegowane do worker threads, główna funkcja Tick Twoich Actorów, Replication Serialization oraz wykonywanie RPC (Remote Procedure Call) odbywają się w Game Thread.

Jeśli serwer działa z częstotliwością 30 ticków na sekundę (30Hz), ma dokładnie 33,3 milisekundy na przetworzenie wszystkich player inputs, aktualizację Game State, obliczenie fizyki i serializację danych sieciowych dla następnej klatki. Jeśli gracz może zmusić serwer do wykonania operacji, której przetworzenie zajmuje 50 milisekund, tick rate serwera natychmiast spada do 20Hz.

Kiedy tick rate serwera spada tak drastycznie, nie otrzymujesz tylko wizualnych opóźnień — dochodzi do katastrofalnej rozbieżności stanów (state divergence). Omówiliśmy to szczegółowo w naszym przewodniku technicznym The Unreal Engine Multiplayer Sync Bug Ruining Your World States And How To Fix It.

Bez użycia memory injectors czy packet editors, exploity wydajnościowe w grze zazwyczaj opierają się na jednym z trzech wektorów: RPC Flooding, Physics/Collision Overload lub Replication Saturation.

Wektor 1: RPC Flooding i błędy walidacji

Najczęstszym sposobem na zawieszenie lub spowolnienie serwera Unreal Engine jest spamowanie Server RPCs. Jeśli klient przypisze Server RPC do kółka myszy lub wejścia z odblokowanym framerate, może wysyłać setki żądań na sekundę do serwera.

Jeśli Twój Server RPC zawiera złożoną logikę — taką jak spawn Actora, wykonanie line trace (Raycast) lub iterację przez duże tablice — serwer jest zmuszony do wykonywania tej kosztownej logiki setki razy na klatkę.

Unreal Engine udostępnia makro WithValidation dla RPC, ale wielu deweloperów używa go tylko do sprawdzania, czy wskaźnik jest prawidłowy, całkowicie ignorując Rate Limiting.

Rozwiązanie: Implementacja RPC Rate Limiter w C++

Aby chronić serwer, musisz wdrożyć rygorystyczne Rate Limiting dla całej komunikacji klient-serwer. Oto sprawdzone podejście do ograniczania Server RPCs przy użyciu niestandardowego Actor Component w C++.

Najpierw definiujemy logikę ograniczania w pliku nagłówkowym:

// RateLimiterComponent.h
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "RateLimiterComponent.generated."

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class MULTIPLAYER_API URateLimiterComponent : public UActorComponent
{
    GENERATED_BODY()

public:	
    URateLimiterComponent();

    // Checks if the action is allowed. Returns false if the client is spamming.
    UFUNCTION(BlueprintCallable, Category = "Security")
    bool CanExecuteAction(FName ActionName, float CooldownTime);

private:
    // Maps action names to the last time they were executed
    TMap<FName, float> LastExecutionTimes;

    // Threshold for maximum allowed actions per second before flagging the player
    const int32 MaxActionsPerSecond = 20;
    int32 CurrentActionCount;
    float LastResetTime;
};

Następnie implementujemy logikę walidacji w pliku CPP. Zauważ, że używamy czasu serwera (GetWorld()->GetTimeSeconds()), aby upewnić się, że klient nie może sfałszować swojego lokalnego czasu w celu obejścia cooldownu.

// RateLimiterComponent.cpp
#include "RateLimiterComponent.h"

URateLimiterComponent::URateLimiterComponent()
{
    PrimaryComponentTick.bCanEverTick = false;
    CurrentActionCount = 0;
    LastResetTime = 0.0f;
}

bool URateLimiterComponent::CanExecuteAction(FName ActionName, float CooldownTime)
{
    // Only run this logic on the server
    if (!GetOwner()->HasAuthority())
    {
        return false;
    }

    float CurrentTime = GetWorld()->GetTimeSeconds();

    // Reset the global action counter every second
    if (CurrentTime - LastResetTime >= 1.0f)
    {
        CurrentActionCount = 0;
        LastResetTime = CurrentTime;
    }

    // Global spam check
    CurrentActionCount++;
    if (CurrentActionCount > MaxActionsPerSecond)
    {
        UE_LOG(LogTemp, Warning, TEXT("Player %s is exceeding global RPC limits!"), *GetOwner()->GetName());
        return false;
    }

    // Specific action cooldown check
    if (LastExecutionTimes.Contains(ActionName))
    {
        float LastTime = LastExecutionTimes[ActionName];
        if (CurrentTime - LastTime < CooldownTime)
        {
            // Client is spamming this specific action
            return false;
        }
    }

    // Update the execution time and allow the action
    LastExecutionTimes.Add(ActionName, CurrentTime);
    return true;
}

Teraz, implementując funkcję Server_PerformAction_Validate, możesz dynamicznie odrzucić RPC, jeśli klient go spamuje:

bool AMyPlayerController::Server_PerformExpensiveAction_Validate()
{
    // If the rate limiter returns false, the RPC is rejected and the client is disconnected
    if (URateLimiterComponent* RateLimiter = GetComponentByClass<URateLimiterComponent>())
    {
        return RateLimiter->CanExecuteAction(FName("ExpensiveAction"), 0.5f);
    }
    return true;
}

Wektor 2: Physics i Collision Overload

Innym powszechnym wektorem exploitu (i mocno podejrzewanym w środowiskach typu sandbox, takich jak UEFN) jest przeciążenie fizyki. Jeśli gracze mogą spawnować obiekty, upuszczać przedmioty lub manipulować Physics Bodies, mogą celowo układać setki obiektów w ograniczonej przestrzeni.

Kiedy Physics Bodies nakładają się na siebie, silnik fizyki Chaos próbuje rozwiązać kolizje. Jeśli 500 obiektów zostanie wtłoczonych w tę samą przestrzeń współrzędnych, obliczenia kolizji rosną wykładniczo, powodując całkowitą blokadę procesora na serwerze.

Co więcej, jeśli te obiekty mają ustawioną flagę bGenerateOverlapEvents na true, serwer będzie wywoływał OnComponentBeginOverlap setki tysięcy razy na klatkę.

Rozwiązanie: Agresywny Collision Culling

Aby zapobiec degradacji serwera opartej na fizyce, musisz oddzielić fizykę wizualną od walidacji kolizji po stronie serwera.

  1. Wyłącz Overlaps dla upuszczonych przedmiotów: Jeśli gracz upuści przedmiot, wyłącz bGenerateOverlapEvents na serwerze po tym, jak przedmiot przestanie się poruszać.
  2. Ogranicz Spawn: Na sztywno zakoduj maksymalną gęstość obiektów fizycznych na sektor siatki.
  3. Ogranicz logikę Overlap: Jeśli musisz używać overlapów, nie wykonuj złożonej logiki bezpośrednio wewnątrz zdarzenia nakładania się. Zamiast tego ustaw flagę "dirty" i przetwórz nakładanie się w kontrolowanej partii podczas funkcji Tick.

Wektor 3: Replication Saturation i dławienie przepustowości

System replikacji w Unreal Engine jest potężny, ale silnie obciąża procesor. Serwer musi iterować po każdym replikowanym Actorze, sprawdzać, czy jest on istotny dla konkretnego klienta, porównywać jego właściwości z ostatnim potwierdzonym stanem i serializować zmiany.

Złośliwi gracze mogą to wykorzystać, gwałtownie zmieniając replikowane zmienne (np. dane personalizacji postaci lub stan ekwipunku). Zmusza to serwer do ciągłej serializacji dużych bloków danych, co nasyca zarówno procesor serwera, jak i limity przepustowości.

Rozwiązanie: Optymalizacja NetUpdateFrequency

Nigdy nie zostawiaj NetUpdateFrequency na domyślnej wartości (100.0) dla niekrytycznych aktorów. Musisz dynamicznie skalować częstotliwość replikacji w zależności od bliskości gracza i stanu akcji.

Dodatkowo należy użyć DefaultEngine.ini, aby wymusić rygorystyczne limity przepustowości na dedykowanym serwerze. Zapobiega to wymuszaniu przez pojedynczego złośliwego klienta przetwarzania masowych strumieni pakietów:

[/Script/OnlineSubsystemUtils.IpNetDriver]
MaxClientRate=15000
MaxInternetClientRate=10000
NetServerMaxTickRate=30
LanServerMaxTickRate=30
ConnectionTimeout=15.0
InitialConnectTimeout=30.0

Ograniczając MaxClientRate, serwer po prostu odrzuci nadmiarowe pakiety od klienta próbującego zalać kanał sieciowy, oszczędzając cykle procesora dla uczciwych graczy.

Odporność infrastruktury: Obsługa nieuniknionego

Nawet przy idealnym kodzie C++, exploity typu zero-day będą się zdarzać. Kiedy exploit taki jak błąd wydajności serwera UEFN uderzy w Twoją grę, węzły serwerowe nieuchronnie odnotują 100% zużycia procesora i zawieszą się.

Jeśli cała architektura Twojej floty serwerów jest podatna na pojedynczy punkt awarii, ryzykujesz trwały odpływ graczy. Budowanie odpornej infrastruktury z odpowiednim routingiem fallback to coś, co mocno promujemy, podobnie jak omawialiśmy to w naszej analizie architektonicznej The Stop Killing Games Campaign Vs Live Ops Architecting Server Fallbacks.

Gdy serwer ulegnie awarii z powodu exploitu, Twój backend musi natychmiast wykryć martwy węzeł, uruchomić nową instancję i płynnie przenieść dotkniętych graczy z powrotem do kolejki matchmakingu bez utraty ich trwałych danych.

Samodzielne zbudowanie tego wymaga skonfigurowania niestandardowych load balancers, shardingu baz danych, orkiestracji kontenerów (np. Kubernetes) i zarządzania certyfikatami SSL — to łatwo 4-6 miesięcy dedykowanej pracy inżynierskiej. Dzięki horizOn te usługi backendowe są wstępnie skonfigurowane. Nasza infrastruktura automatycznie monitoruje stan serwerów, skaluje instancje w oparciu o obciążenie procesora i zarządza routingiem sesji graczy, pozwalając Ci skupić się na naprawianiu kodu gry zamiast na walce z infrastrukturą.

5 najlepszych praktyk dla stabilności serwera

Aby zabezpieczyć swoją grę Multiplayer w Unreal Engine przed exploitami wydajnościowymi, natychmiast wdróż te pięć zasad architektonicznych:

  1. Wprowadź rygorystyczne limity RPC: Nigdy nie ufaj częstotliwości wejściowej klienta. Użyj komponentu rate limiter w C++ opisanego powyżej, aby wymusić twarde cooldowny na każdym Server RPC.
  2. Sanityzuj wektory ruchu: Speed hacki i exploity teleportacji działają poprzez wysyłanie ogromnych wektorów do serwera. Zawsze ograniczaj żądania AddMovementInput i SetActorLocation po stronie serwera względem maksymalnej teoretycznej prędkości ruchu postaci.
  3. Używaj Replication Graph: Jeśli Twoja gra obsługuje ponad 40 graczy, domyślny system replikacji stanie się wąskim gardłem. Zaimplementuj Replication Graph w Unreal Engine, aby grupować aktorów przestrzennie i drastycznie zmniejszyć obciążenie procesora przy sprawdzaniu istotności (relevance checks).
  4. Wyłącz wizualizacje po stronie serwera: Dedykowane serwery nigdy nie powinny ładować UI, systemów cząsteczkowych ani animacji skeletal mesh. Upewnij się, że ustawienia projektu rygorystycznie usuwają te zasoby z buildu dedykowanego serwera, aby zwolnić pamięć i cykle procesora.
  5. Monitoruj Tick Rate dynamicznie: Zaimplementuj podsystem po stronie serwera, który monitoruje średni delta time. Jeśli serwer wykryje spadek tick rate poniżej 15Hz przez ponad 5 sekund, powinien automatycznie wstrzymać nieistotne zadania w tle (np. spawn AI lub generowanie zdarzeń ambientowych), aby odzyskać wydajność.

Podsumowanie

Niedawny exploit wydajności serwera UEFN to brutalne przypomnienie, że tworzenie gier Multiplayer to w dużej mierze ćwiczenie z cyberbezpieczeństwa. Nie możesz po prostu ufać, że gracze będą wchodzić w interakcję z Twoją grą zgodnie z przeznaczeniem. Każdy RPC, każda interakcja fizyczna i każda replikowana zmienna to potencjalny wektor ataku.

Zmieniając podejście na model „Server-Authoritative, Client-Distrusted”, głęboko optymalizując logikę replikacji w C++ i wdrażając rygorystyczne limity, możesz uzbroić swoją grę przeciwko tego typu katastrofalnym awariom wydajności.

Łącząc pancerny kod gry z automatycznie skalowalną, samonaprawiającą się infrastrukturą serwerową, tworzysz środowisko, w którym exploity stają się drobnymi niedogodnościami, a nie katastrofami niszczącymi grę. Gotowy na skalowanie swojego backendu Multiplayer bez bólów głowy związanych z DevOps? Wypróbuj horizOn za darmo i pozwól nam zająć się orkiestracją Twoich serwerów.


Źródło: [CRITICAL] Server Performance Exploit