diff --git a/src/Prospect.Agent/Prospect.Agent.vcxproj b/src/Prospect.Agent/Prospect.Agent.vcxproj new file mode 100644 index 0000000..d930485 --- /dev/null +++ b/src/Prospect.Agent/Prospect.Agent.vcxproj @@ -0,0 +1,119 @@ + + + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {a9ba7d25-f239-4320-a0dc-85e8001fd669} + ProspectAgent + 10.0 + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + + + + + true + $(ProjectDir)bin\$(Platform)\$(Configuration)\ + $(ProjectDir)bin\intermediate\$(Platform)\$(Configuration)\ + $(LibraryPath) + + + false + $(ProjectDir)bin\$(Platform)\$(Configuration)\ + $(ProjectDir)bin\intermediate\$(Platform)\$(Configuration)\ + $(LibraryPath) + + + true + + + true + + + true + + + + Level3 + true + _DEBUG;PROSPECTAGENT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + NotUsing + pch.h + %(AdditionalIncludeDirectories) + + + Windows + true + false + %(AdditionalDependencies) + + + + + Level3 + true + true + true + NDEBUG;PROSPECTAGENT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + NotUsing + pch.h + %(AdditionalIncludeDirectories) + MinSpace + + + Windows + true + true + true + false + %(AdditionalDependencies) + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Prospect.Agent/Prospect.Agent.vcxproj.filters b/src/Prospect.Agent/Prospect.Agent.vcxproj.filters new file mode 100644 index 0000000..4f007b6 --- /dev/null +++ b/src/Prospect.Agent/Prospect.Agent.vcxproj.filters @@ -0,0 +1,39 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + \ No newline at end of file diff --git a/src/Prospect.Agent/SDK.h b/src/Prospect.Agent/SDK.h new file mode 100644 index 0000000..8b897f6 --- /dev/null +++ b/src/Prospect.Agent/SDK.h @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include +#include "SDK_Memory.h" + +namespace SDK +{ + template + struct TArray + { + friend struct FString; + + TArray() + { + Data = nullptr; + Count = Max = 0; + } + + int Num() const + { + return Count; + } + + T& operator[](int i) + { + return Data[i]; + } + + const T& operator[](int i) const + { + return Data[i]; + } + + bool IsValidIndex(int i) const + { + return i < Num(); + } + + private: + T* Data; + int32_t Count; + int32_t Max; + }; + + struct FString : private TArray + { + FString(const wchar_t* other) + { + const size_t charLen = std::wcslen(other); + + if (charLen == 0) + { + Count = 0; + Max = 0; + Data = nullptr; + } + else + { + Count = charLen + 1; + Max = Count; + Data = static_cast((*GMalloc)->Malloc(Count * 2)); + + memset(Data, 0, Count * 2); + memcpy(Data, other, charLen * 2); + } + } + + FString(const std::wstring& other) : FString(other.c_str()) + { + + } + + bool IsValid() const + { + return Data != nullptr; + } + + const wchar_t* c_str() const + { + return Data; + } + + std::string ToString() const + { + const auto length = std::wcslen(Data); + + std::string str(length, '\0'); + std::use_facet>(std::locale()).narrow(Data, Data + length, '?', &str[0]); + + return str; + } + }; + + struct UPlayFabAPISettings + { + FString VerticalName; + FString BaseServiceHost; + FString TitleId; + FString AdvertisingIdType; + FString AdvertisingIdValue; + bool DisableAdvertising = false; + }; +} diff --git a/src/Prospect.Agent/SDK_Memory.h b/src/Prospect.Agent/SDK_Memory.h new file mode 100644 index 0000000..8927b14 --- /dev/null +++ b/src/Prospect.Agent/SDK_Memory.h @@ -0,0 +1,79 @@ +#pragma once + +#include + +#define DEFAULT_ALIGNMENT 0 + +namespace SDK +{ + class FExec + { + public: + virtual ~FExec() = default; + private: + virtual bool Exec(void* world, void* cmd, void* ar) = 0; + }; + + class FMalloc : FExec + { + public: + virtual void* Malloc(SIZE_T Count, uint32_t Alignment = DEFAULT_ALIGNMENT) = 0; + + virtual void* TryMalloc(SIZE_T Count, uint32_t Alignment = DEFAULT_ALIGNMENT) = 0; + + virtual void* Realloc(void* Original, SIZE_T Count, uint32_t Alignment = DEFAULT_ALIGNMENT) = 0; + + virtual void* TryRealloc(void* Original, SIZE_T Count, uint32_t Alignment = DEFAULT_ALIGNMENT) = 0; + + virtual void Free(void* Original) = 0; + + virtual SIZE_T QuantizeSize(SIZE_T Count, uint32_t Alignment) + { + return Count; + } + + virtual bool GetAllocationSize(void* Original, SIZE_T& SizeOut) + { + return false; + } + + virtual void Trim(bool bTrimThreadCaches) + { + } + + virtual void SetupTLSCachesOnCurrentThread() + { + } + + virtual void ClearAndDisableTLSCachesOnCurrentThread() + { + } + + virtual void InitializeStatsMetadata(); + + virtual bool Exec(void* InWorld, void* Cmd, void* Ar) override + { + return false; + } + + virtual void UpdateStats() = 0; + + virtual void GetAllocatorStats(void* out_Stats) = 0; + + virtual void DumpAllocatorStats(class FOutputDevice& Ar) = 0; + + virtual bool IsInternallyThreadSafe() const + { + return false; + } + + virtual bool ValidateHeap() + { + return(true); + } + + virtual const TCHAR* GetDescriptiveName() = 0; + }; + + FMalloc** GMalloc; +} diff --git a/src/Prospect.Agent/hooks.h b/src/Prospect.Agent/hooks.h new file mode 100644 index 0000000..4e2a47a --- /dev/null +++ b/src/Prospect.Agent/hooks.h @@ -0,0 +1,3 @@ +#pragma once + +#include "SDK.h" \ No newline at end of file diff --git a/src/Prospect.Agent/logger.cpp b/src/Prospect.Agent/logger.cpp new file mode 100644 index 0000000..24eed78 --- /dev/null +++ b/src/Prospect.Agent/logger.cpp @@ -0,0 +1,85 @@ +#include "logger.h" + +#include +#include + +HANDLE h_out = nullptr, h_old_out = nullptr; +HANDLE h_err = nullptr, h_old_err = nullptr; +HANDLE h_in = nullptr, h_old_in = nullptr; + +namespace logger +{ + void Attach() + { + h_old_out = GetStdHandle(STD_OUTPUT_HANDLE); + h_old_err = GetStdHandle(STD_ERROR_HANDLE); + h_old_in = GetStdHandle(STD_INPUT_HANDLE); + + AllocConsole() && AttachConsole(GetCurrentProcessId()); + + h_out = GetStdHandle(STD_OUTPUT_HANDLE); + h_err = GetStdHandle(STD_ERROR_HANDLE); + h_in = GetStdHandle(STD_INPUT_HANDLE); + + SetConsoleMode(h_out, + ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT); + + SetConsoleMode(h_in, + ENABLE_INSERT_MODE | ENABLE_EXTENDED_FLAGS | + ENABLE_PROCESSED_INPUT | ENABLE_QUICK_EDIT_MODE); + } + + void Detach() + { + if (h_out && h_err && h_in) { + FreeConsole(); + + if (h_old_out) + { + SetStdHandle(STD_OUTPUT_HANDLE, h_old_out); + } + + if (h_old_err) + { + SetStdHandle(STD_ERROR_HANDLE, h_old_err); + } + + if (h_old_in) + { + SetStdHandle(STD_INPUT_HANDLE, h_old_in); + } + } + } + + bool Print(const char* fmt, ...) + { + if (!h_out) + { + return false; + } + + char buf[1024]; + va_list va; + + va_start(va, fmt); + _vsnprintf_s(buf, 1024, fmt, va); + va_end(va); + + return !!WriteConsoleA(h_out, buf, static_cast(strlen(buf)), nullptr, nullptr); + } + + char ReadKey() + { + if (!h_in) + { + return -1; + } + + auto key = char{ 0 }; + auto keysread = DWORD{ 0 }; + + ReadConsoleA(h_in, &key, 1, &keysread, nullptr); + + return key; + } +}; diff --git a/src/Prospect.Agent/logger.h b/src/Prospect.Agent/logger.h new file mode 100644 index 0000000..9317173 --- /dev/null +++ b/src/Prospect.Agent/logger.h @@ -0,0 +1,12 @@ +#pragma once + +namespace logger +{ + void Attach(); + + void Detach(); + + bool Print(const char* fmt, ...); + + char ReadKey(); +} \ No newline at end of file diff --git a/src/Prospect.Agent/main.cpp b/src/Prospect.Agent/main.cpp new file mode 100644 index 0000000..687c6da --- /dev/null +++ b/src/Prospect.Agent/main.cpp @@ -0,0 +1,135 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include "logger.h" +#include "SDK.h" +#include "SDK_Memory.h" +#include +#include +#include +#include +#include +#include + +std::wstring g_backendUrl; + +// trim from start (in place) +inline void ltrim(std::wstring &s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { + return !std::isspace(ch); + })); +} + +// trim from end (in place) +inline void rtrim(std::wstring &s) { + s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { + return !std::isspace(ch); + }).base(), s.end()); +} + +inline void trim(std::wstring &s) { + rtrim(s); + ltrim(s); +} + +constexpr uintptr_t OffsetGMalloc = 0x63EC4A0; + +/** + * Function offset, find with "?sdk=". + */ +constexpr uintptr_t OffsetPlayFabApiGetUrl = 0xCEEF00; + +/** + * Function hooks. + */ +typedef SDK::FString(__fastcall* tPlayFabApiGetUrl)(SDK::UPlayFabAPISettings*, const SDK::FString&); + +tPlayFabApiGetUrl OldPlayFabApiGetUrl; + +SDK::FString __fastcall PlayFabApiGetUrlProxy(SDK::UPlayFabAPISettings* thiz, const SDK::FString& callPath) { + logger::Print("[Agent] GetURL called with callPath %s\n", callPath.ToString().c_str()); + + if (!g_backendUrl.empty()) + { + return g_backendUrl + std::wstring(callPath.c_str()); + } + + return std::wstring(L"https://127.0.0.1:8443") + std::wstring(callPath.c_str()); +} + +DWORD WINAPI OnDllAttach(LPVOID base) { + logger::Print("[Agent] DLL Attached\n"); + + const auto hModule = GetModuleHandleW(nullptr); + const auto hModulePtr = reinterpret_cast(hModule); + + // Initialize GMalloc. + SDK::GMalloc = reinterpret_cast(hModulePtr + OffsetGMalloc); + + // Initialize MinHook. + if (MH_Initialize() != MH_OK) { + logger::Print("[Agent] Failed to initialize MinHook\n"); + FreeLibraryAndExitThread(hModule, 1); + } + + // Hook UPlayFabAPISettings::GetUrl. + const auto pTarget = reinterpret_cast(hModulePtr + OffsetPlayFabApiGetUrl); + const auto ppOriginal = reinterpret_cast(&OldPlayFabApiGetUrl); + const auto mhStatus = MH_CreateHook(pTarget, &PlayFabApiGetUrlProxy, ppOriginal); + + if (mhStatus != MH_OK) { + logger::Print("[Agent] Failed to create PlayFabAPIGetUrl hook (%d)\n", mhStatus); + FreeLibraryAndExitThread(hModule, 1); + } + + if (MH_EnableHook(pTarget) != MH_OK) { + logger::Print("[Agent] Failed to enable PlayFabAPIGetUrl hook\n"); + FreeLibraryAndExitThread(hModule, 1); + } + + const wchar_t* filePath = L"backend.txt"; + std::wifstream f(filePath); + if (f) { + std::wstringstream buffer; + buffer << f.rdbuf(); + g_backendUrl = buffer.str(); + trim(g_backendUrl); + if (g_backendUrl.rfind(L"https://", 0) != 0) { + logger::Print("[Agent] URL must start with https://\n"); + g_backendUrl.clear(); + } else { + logger::Print("[Agent] Loaded custom URL: %S\n", g_backendUrl.c_str()); + } + } else { + logger::Print("[Agent] Failed to open file: %S\n", filePath); + } + + logger::Print("[Agent] DLL Initialized\n"); + ExitThread(1); +} + +BOOL WINAPI DllMain( + const _In_ HINSTANCE hinstDll, + const _In_ DWORD fdwReason, + const _In_opt_ LPVOID lpvReserved +) { + if (fdwReason == DLL_PROCESS_ATTACH) { + DisableThreadLibraryCalls(hinstDll); + + // Allocate a console. + AllocConsole(); + freopen_s(reinterpret_cast(stdout), "CONOUT$", "w", stdout); + + // Attach logger. + logger::Attach(); + + // Spawn thread. + CreateThread(nullptr, 0, OnDllAttach, hinstDll, 0, nullptr); + } else if (fdwReason == DLL_PROCESS_DETACH) { + // Detach logger. + logger::Detach(); + } + + return TRUE; +} diff --git a/src/Prospect.Agent/vcpkg-configuration.json b/src/Prospect.Agent/vcpkg-configuration.json new file mode 100644 index 0000000..ba06dad --- /dev/null +++ b/src/Prospect.Agent/vcpkg-configuration.json @@ -0,0 +1,14 @@ +{ + "default-registry": { + "kind": "git", + "baseline": "2960d7d80e8d09c84ae8abf15c12196c2ca7d39a", + "repository": "https://github.com/microsoft/vcpkg" + }, + "registries": [ + { + "kind": "artifact", + "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", + "name": "microsoft" + } + ] +} diff --git a/src/Prospect.Agent/vcpkg.json b/src/Prospect.Agent/vcpkg.json new file mode 100644 index 0000000..53b61fa --- /dev/null +++ b/src/Prospect.Agent/vcpkg.json @@ -0,0 +1,5 @@ +{ + "dependencies": [ + "minhook" + ] +} diff --git a/src/Prospect.sln b/src/Prospect.sln index ef9f299..daef9cf 100644 --- a/src/Prospect.sln +++ b/src/Prospect.sln @@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Unreal.Generator", EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Prospect.Client.Loader", "Prospect.Client.Loader\Prospect.Client.Loader.vcxproj", "{04B1D963-2BCB-4887-9D6D-04CB3366A348}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Prospect.Agent", "Prospect.Agent\Prospect.Agent.vcxproj", "{A9BA7D25-F239-4320-A0DC-85E8001FD669}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -207,6 +209,30 @@ Global {04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Release|x64.Build.0 = Release|x64 {04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Release|x86.ActiveCfg = Release|Win32 {04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Release|x86.Build.0 = Release|Win32 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Debug|x64.ActiveCfg = Debug|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Debug|x64.Build.0 = Debug|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Debug|x86.ActiveCfg = Debug|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Debug|x86.Build.0 = Debug|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Release|x64.ActiveCfg = Release|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Release|x64.Build.0 = Release|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Release|x86.ActiveCfg = Release|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Release|x86.Build.0 = Release|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Debug|x64.ActiveCfg = Debug|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Debug|x64.Build.0 = Debug|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Debug|x86.ActiveCfg = Debug|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Debug|x86.Build.0 = Debug|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Release|x64.ActiveCfg = Release|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Release|x64.Build.0 = Release|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Release|x86.ActiveCfg = Release|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Release|x86.Build.0 = Release|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Debug|x64.ActiveCfg = Debug|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Debug|x64.Build.0 = Debug|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Debug|x86.ActiveCfg = Debug|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Debug|x86.Build.0 = Debug|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Release|x64.ActiveCfg = Release|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Release|x64.Build.0 = Release|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Release|x86.ActiveCfg = Release|x64 + {A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Release|x86.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -219,6 +245,7 @@ Global {14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34} = {168B6247-2D33-4942-AB3F-C5041B3BDFEA} {71E3E262-49C5-4B6C-9670-D0741CEDB63B} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834} {04B1D963-2BCB-4887-9D6D-04CB3366A348} = {31D3D5D2-B011-4F9B-B6CC-D0A35675D797} + {A9BA7D25-F239-4320-A0DC-85E8001FD669} = {31D3D5D2-B011-4F9B-B6CC-D0A35675D797} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {12888C62-D3A2-4DD7-A9A5-1481D95865D0}