From 6fbae4461ef8787e4c8d3cec109f21886c0f2a3b Mon Sep 17 00:00:00 2001
From: momo5502 <mauriceheumann@gmail.com>
Date: Sun, 21 Feb 2016 19:57:56 +0100
Subject: [PATCH] Experimental guid authentication

---
 src/Components/Loader.cpp          |   1 +
 src/Components/Loader.hpp          |   1 +
 src/Components/Modules/Auth.cpp    | 159 +++++++++++++++++++++++++++++
 src/Components/Modules/Auth.hpp    |  35 +++++++
 src/Components/Modules/Party.cpp   |   1 +
 src/Game/Functions.cpp             |  31 ++++++
 src/Game/Functions.hpp             |   4 +
 src/Game/Structs.hpp               |   2 +-
 src/Proto/auth.proto               |   9 ++
 src/STDInclude.hpp                 |   1 +
 src/Steam/Interfaces/SteamUser.cpp |  26 ++---
 src/Steam/Interfaces/SteamUser.hpp |   2 +
 src/Utils/Cryptography.hpp         |  47 ++++++---
 13 files changed, 294 insertions(+), 25 deletions(-)
 create mode 100644 src/Components/Modules/Auth.cpp
 create mode 100644 src/Components/Modules/Auth.hpp
 create mode 100644 src/Proto/auth.proto

diff --git a/src/Components/Loader.cpp b/src/Components/Loader.cpp
index 012b31b1..82549e56 100644
--- a/src/Components/Loader.cpp
+++ b/src/Components/Loader.cpp
@@ -9,6 +9,7 @@ namespace Components
 		Loader::Register(new Flags());
 		Loader::Register(new Singleton());
 
+		Loader::Register(new Auth());
 		Loader::Register(new Dvar());
 		Loader::Register(new Maps());
 		Loader::Register(new News());
diff --git a/src/Components/Loader.hpp b/src/Components/Loader.hpp
index 33c8b94a..1ff881b3 100644
--- a/src/Components/Loader.hpp
+++ b/src/Components/Loader.hpp
@@ -23,6 +23,7 @@ namespace Components
 	};
 }
 
+#include "Modules\Auth.hpp"
 #include "Modules\Dvar.hpp"
 #include "Modules\Maps.hpp"
 #include "Modules\News.hpp"
diff --git a/src/Components/Modules/Auth.cpp b/src/Components/Modules/Auth.cpp
new file mode 100644
index 00000000..4d041231
--- /dev/null
+++ b/src/Components/Modules/Auth.cpp
@@ -0,0 +1,159 @@
+#include "STDInclude.hpp"
+
+namespace Components
+{
+	Auth::AuthInfo Auth::ClientAuthInfo[18];
+
+	void Auth::Frame()
+	{
+		for (int i = 0; i < *Game::svs_numclients; i++)
+		{
+			Game::client_t* client = &Game::svs_clients[i];
+			Auth::AuthInfo* info = &Auth::ClientAuthInfo[i];
+
+			// State must be 5 or greater here, as otherwise the client will crash when being kicked.
+			// That's due to the hunk being freed by that time, but it hasn't been reallocated, therefore all future allocations will cause a crash.
+			// Additionally, the game won't catch the errors and simply lose the connection, so we even have to add a delay to send the data.
+
+			// Not sure if that's potentially unsafe, though.
+			// Players faking their GUID will be connected for 5 seconds, which allows them to fuck up everything.
+			// I think we have to perform the verification when clients are still in state 3, but for now it works.
+
+			// I think we even have to lock the client into state 3 until the verification is done.
+			// Intercepting the entire connection process to perform the authentication within state 3 solely is necessary, due to having a timeout. 
+			// Not sending a response might allow the player to connect for a few seconds (<= 5) until the timeout is reached.
+			if (client->state >= 5)
+			{
+				if (info->state == Auth::STATE_NEGOTIATING && (Game::Com_Milliseconds() - info->time) > 1000 * 5)
+				{
+					info->state = Auth::STATE_INVALID;
+					info->time = Game::Com_Milliseconds();
+					Game::SV_KickClientError(client, "XUID verification timeout!");
+				}
+				else if (info->state == Auth::STATE_UNKNOWN && info->time && (Game::Com_Milliseconds() - info->time) > 1000 * 5) // Wait 5 seconds (error delay)
+				{
+					Logger::Print("Sending XUID authentication request to %s\n", Network::Address(client->adr).GetString());
+
+					info->state = Auth::STATE_NEGOTIATING;
+					info->time = Game::Com_Milliseconds();
+					info->challenge = Utils::VA("%X", Utils::Cryptography::Rand::GenerateInt());
+					Network::SendCommand(client->adr, "xuidAuthReq", info->challenge);
+				}
+				else if (info->state == Auth::STATE_UNKNOWN && !info->time)
+				{
+					info->time = Game::Com_Milliseconds();
+				}
+			}
+		}
+	}
+
+	void Auth::RegisterClient(int clientNum)
+	{
+		if (clientNum >= 18) return;
+
+		Network::Address address(Game::svs_clients[clientNum].adr);
+
+		if (address.GetType() == Game::netadrtype_t::NA_BOT)
+		{
+			Auth::ClientAuthInfo[clientNum].state = Auth::STATE_VALID;
+		}
+		else
+		{
+			Logger::Print("Registering client %s\n", address.GetString());
+			Auth::ClientAuthInfo[clientNum].time = 0;
+			Auth::ClientAuthInfo[clientNum].state = Auth::STATE_UNKNOWN;
+		}
+	}
+
+	void __declspec(naked) Auth::RegisterClientStub()
+	{
+		__asm
+		{
+			push esi
+			call Auth::RegisterClient
+			pop esi
+
+			imul esi, 366Ch
+			mov eax, 478A18h
+			jmp eax
+		}
+	}
+
+	Auth::Auth()
+	{
+		// Only clients receive the auth request
+		if (!Dedicated::IsDedicated()) 
+		{
+			Network::Handle("xuidAuthReq", [] (Network::Address address, std::string data)
+			{
+				Logger::Print("Received XUID authentication request from %s\n", address.GetString());
+
+				// Only accept requests from the server we're connected to
+				if (address != *Game::connectedHost) return;
+				if (!Steam::User::GuidKey.IsValid()) return;
+
+				Proto::Auth::Response response;
+				response.set_publickey(Steam::User::GuidKey.GetPublicKey());
+				response.set_signature(Utils::Cryptography::ECDSA::SignMessage(Steam::User::GuidKey, data+"1"));
+
+				Network::SendCommand(address, "xuidAuthResp", response.SerializeAsString());
+			});
+		}
+
+		Network::Handle("xuidAuthResp", [] (Network::Address address, std::string data)
+		{
+			Logger::Print("Received XUID authentication response from %s\n", address.GetString());
+
+			Proto::Auth::Response response;
+			response.ParseFromString(data);
+
+			if (response.signature().empty()) return;
+			if (response.publickey().empty()) return;
+
+			for (int i = 0; i < *Game::svs_numclients; i++)
+			{
+				Game::client_t* client = &Game::svs_clients[i];
+				Auth::AuthInfo* info = &Auth::ClientAuthInfo[i];
+
+				if (client->state >= 3 && address == client->adr && info->state == Auth::STATE_NEGOTIATING)
+				{
+					unsigned int id = static_cast<unsigned int>(~0x110000100000000 & client->steamid);
+
+					// Check if guid matches the certificate
+					if (id != (Utils::OneAtATime(response.publickey().data(), response.publickey().size()) & ~0x80000000))
+					{
+						info->state = Auth::STATE_INVALID;
+						Game::SV_KickClientError(client, "XUID doesn't match the certificate!");
+					}
+					else
+					{
+						info->publicKey.Set(response.publickey());
+
+						if (Utils::Cryptography::ECDSA::VerifyMessage(info->publicKey, info->challenge, response.signature()))
+						{
+							info->state = Auth::STATE_VALID;
+							Logger::Print("Verified XUID from %s\n", address.GetString());
+						}
+						else
+						{
+							info->state = Auth::STATE_INVALID;
+							Game::SV_KickClientError(client, "Challenge signature was invalid!");
+						}
+					}
+				}
+			}
+		});
+
+		// Install frame handlers
+		Dedicated::OnFrame(Auth::Frame);
+		Renderer::OnFrame(Auth::Frame);
+
+		// Install registration hook
+		Utils::Hook(0x478A12, Auth::RegisterClientStub, HOOK_JUMP).Install()->Quick();
+	}
+
+	Auth::~Auth()
+	{
+
+	}
+}
diff --git a/src/Components/Modules/Auth.hpp b/src/Components/Modules/Auth.hpp
new file mode 100644
index 00000000..538b31be
--- /dev/null
+++ b/src/Components/Modules/Auth.hpp
@@ -0,0 +1,35 @@
+namespace Components
+{
+	class Auth : public Component
+	{
+	public:
+		Auth();
+		~Auth();
+		const char* GetName() { return "Auth"; };
+
+	private:
+
+		enum AuthState
+		{
+			STATE_UNKNOWN,
+			STATE_NEGOTIATING,
+			STATE_VALID,
+			STATE_INVALID,
+		};
+
+		struct AuthInfo
+		{
+			Utils::Cryptography::ECDSA::Key publicKey;
+			std::string challenge;
+			AuthState state;
+			int time;
+		};
+
+		static AuthInfo ClientAuthInfo[18];
+
+		static void Frame();
+
+		static void RegisterClient(int clientNum);
+		static void RegisterClientStub();
+	};
+}
diff --git a/src/Components/Modules/Party.cpp b/src/Components/Modules/Party.cpp
index dc9b8d90..908b7173 100644
--- a/src/Components/Modules/Party.cpp
+++ b/src/Components/Modules/Party.cpp
@@ -193,6 +193,7 @@ namespace Components
 
 		// Disable host migration
 		Utils::Hook::Set<BYTE>(0x5B58B2, 0xEB);
+		Utils::Hook::Set<BYTE>(0x4D6171, 0);
 
 		// Patch playlist stuff for non-party behavior
 		Utils::Hook::Set<Game::dvar_t**>(0x4A4093, &partyEnable);
diff --git a/src/Game/Functions.cpp b/src/Game/Functions.cpp
index 2c11eb38..c1d796c4 100644
--- a/src/Game/Functions.cpp
+++ b/src/Game/Functions.cpp
@@ -177,6 +177,8 @@ namespace Game
 
 	gentity_t* g_entities = (gentity_t*)0x18835D8;
 
+	netadr_t* connectedHost = (netadr_t*)0xA1E888;
+
 	SOCKET* ip_socket = (SOCKET*)0x64A3008;
 
 	void* ReallocateAssetPool(XAssetType type, unsigned int newSize)
@@ -303,4 +305,33 @@ namespace Game
 
 		return hash;
 	}
+
+	void SV_KickClient(client_t* client, const char* reason)
+	{
+		__asm
+		{
+			push edi
+			push esi
+			mov edi, 0
+			mov esi, client
+			push reason
+			push 0
+			push 0
+			mov eax, 6249A0h
+			call eax
+			add esp, 0Ch
+			pop esi
+			pop edi
+		}
+	}
+
+	void SV_KickClientError(client_t* client, const char* reason)
+	{
+		if (client->state < 5)
+		{
+			Components::Network::Send(client->adr, Utils::VA("error\n%s", reason));
+		}
+
+		SV_KickClient(client, reason);
+	}
 }
diff --git a/src/Game/Functions.hpp b/src/Game/Functions.hpp
index cf6f0a13..a415f3c7 100644
--- a/src/Game/Functions.hpp
+++ b/src/Game/Functions.hpp
@@ -357,6 +357,7 @@ namespace Game
 
 	extern gentity_t* g_entities;
 
+	extern netadr_t* connectedHost;
 	extern SOCKET* ip_socket;
 
 	void* ReallocateAssetPool(XAssetType type, unsigned int newSize);
@@ -372,4 +373,7 @@ namespace Game
 	void MessageBox(std::string message, std::string title);
 
 	unsigned int R_HashString(const char* string);
+
+	void SV_KickClient(client_t* client, const char* reason);
+	void SV_KickClientError(client_t* client, const char* reason);
 }
diff --git a/src/Game/Structs.hpp b/src/Game/Structs.hpp
index 6d831dec..d06b0924 100644
--- a/src/Game/Structs.hpp
+++ b/src/Game/Structs.hpp
@@ -905,7 +905,7 @@ namespace Game
 		// 269044
 		char pad6[9228];
 		// 278272
-		__int64 steamid;
+		unsigned __int64 steamid;
 		// 278280
 		char pad7[403592];
 	} client_t;
diff --git a/src/Proto/auth.proto b/src/Proto/auth.proto
new file mode 100644
index 00000000..375c1a0f
--- /dev/null
+++ b/src/Proto/auth.proto
@@ -0,0 +1,9 @@
+syntax = "proto3";
+
+package Proto.Auth;
+
+message Response 
+{
+	bytes signature = 1;
+	bytes publickey = 2;
+}
diff --git a/src/STDInclude.hpp b/src/STDInclude.hpp
index 721a7f24..51adec2a 100644
--- a/src/STDInclude.hpp
+++ b/src/STDInclude.hpp
@@ -63,6 +63,7 @@
 
 // Protobuf
 #include "proto/network.pb.h"
+#include "proto/auth.pb.h"
 #include "proto/node.pb.h"
 #include "proto/rcon.pb.h"
 
diff --git a/src/Steam/Interfaces/SteamUser.cpp b/src/Steam/Interfaces/SteamUser.cpp
index 5df73753..a8341f18 100644
--- a/src/Steam/Interfaces/SteamUser.cpp
+++ b/src/Steam/Interfaces/SteamUser.cpp
@@ -2,6 +2,8 @@
 
 namespace Steam
 {
+	::Utils::Cryptography::ECDSA::Key User::GuidKey;
+
 	int User::GetHSteamUser()
 	{
 		return NULL;
@@ -26,19 +28,19 @@ namespace Steam
 			}
 			else if (Components::Singleton::IsFirstInstance()) // Hardware guid
 			{
-				DATA_BLOB Data[2];
-				Data[0].pbData = (BYTE *)"AAAAAAAAAA";
-				Data[0].cbData = 10;
-
-				CryptProtectData(Data, NULL, NULL, NULL, NULL, CRYPTPROTECT_LOCAL_MACHINE, &Data[1]);
-
-				subId = ::Utils::OneAtATime(reinterpret_cast<char*>(Data[1].pbData), 52);
-
-				if (!subId)
+				if (!User::GuidKey.IsValid())
 				{
-					Components::Logger::Print("Hardware-based GUID generation failed!\n");
-					subId = (Game::Com_Milliseconds() + timeGetTime());
+					User::GuidKey.Import(::Utils::ReadFile("players/guid.dat"), PK_PRIVATE);
+					
+					if (!User::GuidKey.IsValid())
+					{
+						User::GuidKey = ::Utils::Cryptography::ECDSA::GenerateKey(512);
+						::Utils::WriteFile("players/guid.dat", User::GuidKey.Export(PK_PRIVATE));
+					}
 				}
+
+				std::string publicKey = User::GuidKey.GetPublicKey();
+				subId = ::Utils::OneAtATime(publicKey.data(), publicKey.size());
 			}
 			else // Random guid
 			{
@@ -54,7 +56,7 @@ namespace Steam
 
 	int User::InitiateGameConnection(void *pAuthBlob, int cbMaxAuthBlob, SteamID steamIDGameServer, unsigned int unIPServer, unsigned short usPortServer, bool bSecure)
 	{
-		// TODO: Generate auth ticket!
+		Components::Logger::Print("%s\n", __FUNCTION__);
 		return 0;
 	}
 
diff --git a/src/Steam/Interfaces/SteamUser.hpp b/src/Steam/Interfaces/SteamUser.hpp
index 7d6140ce..187d3425 100644
--- a/src/Steam/Interfaces/SteamUser.hpp
+++ b/src/Steam/Interfaces/SteamUser.hpp
@@ -20,5 +20,7 @@ namespace Steam
 		virtual void EndAuthSession(SteamID steamID);
 		virtual void CancelAuthTicket(unsigned int hAuthTicket);
 		virtual unsigned int UserHasLicenseForApp(SteamID steamID, unsigned int appID);
+
+		static ::Utils::Cryptography::ECDSA::Key GuidKey;
 	};
 }
diff --git a/src/Utils/Cryptography.hpp b/src/Utils/Cryptography.hpp
index 4098a509..48a172f7 100644
--- a/src/Utils/Cryptography.hpp
+++ b/src/Utils/Cryptography.hpp
@@ -20,9 +20,9 @@ namespace Utils
 			public:
 				Key() : KeyStorage(new ecc_key)
 				{
-					ZeroMemory(this->KeyStorage.get(), sizeof(*this->KeyStorage.get()));
+					ZeroMemory(this->KeyStorage.get(), sizeof(*this->GetKeyPtr()));
 				};
-				Key(ecc_key* key) : Key() { if(key) std::memmove(this->KeyStorage.get(), key, sizeof(*key)); };
+				Key(ecc_key* key) : Key() { if(key) std::memmove(this->GetKeyPtr(), key, sizeof(*key)); };
 				Key(ecc_key key) : Key(&key) {};
 				~Key() 
 				{
@@ -34,7 +34,7 @@ namespace Utils
 
 				bool IsValid()
 				{
-					return (!Utils::MemIsSet(this->KeyStorage.get(), 0, sizeof(*this->KeyStorage.get())));
+					return (!Utils::MemIsSet(this->GetKeyPtr(), 0, sizeof(*this->GetKeyPtr())));
 				}
 
 				ecc_key* GetKeyPtr()
@@ -59,20 +59,43 @@ namespace Utils
 				{
 					this->Free();
 
-					if (ecc_ansi_x963_import(reinterpret_cast<const uint8_t*>(pubKeyBuffer.data()), pubKeyBuffer.size(), this->KeyStorage.get()) != CRYPT_OK)
+					if (ecc_ansi_x963_import(reinterpret_cast<const uint8_t*>(pubKeyBuffer.data()), pubKeyBuffer.size(), this->GetKeyPtr()) != CRYPT_OK)
 					{
-						ZeroMemory(this->KeyStorage.get(), sizeof(*this->KeyStorage.get()));
+						ZeroMemory(this->KeyStorage.get(), sizeof(*this->GetKeyPtr()));
 					}
 				}
 
+				void Import(std::string key, int type = PK_PRIVATE)
+				{
+					this->Free();
+
+					if (ecc_import(reinterpret_cast<const uint8_t*>(key.data()), key.size(), this->GetKeyPtr()) != CRYPT_OK)
+					{
+						ZeroMemory(this->KeyStorage.get(), sizeof(*this->GetKeyPtr()));
+					}
+				}
+
+				std::string Export(int type = PK_PRIVATE)
+				{
+					uint8_t buffer[4096] = { 0 };
+					DWORD length = sizeof(buffer);
+
+					if (ecc_export(buffer, &length, type, this->GetKeyPtr()) == CRYPT_OK)
+					{
+						return std::string(reinterpret_cast<char*>(buffer), length);
+					}
+
+					return "";
+				}
+
 				void Free()
 				{
 					if (this->IsValid())
 					{
-						ecc_free(this->KeyStorage.get());
+						ecc_free(this->GetKeyPtr());
 					}
 
-					ZeroMemory(this->KeyStorage.get(), sizeof(*this->KeyStorage.get()));
+					ZeroMemory(this->GetKeyPtr(), sizeof(*this->GetKeyPtr()));
 				}
 
 			private:
@@ -92,9 +115,9 @@ namespace Utils
 			public:
 				Key() : KeyStorage(new rsa_key)
 				{
-					ZeroMemory(this->KeyStorage.get(), sizeof(*this->KeyStorage.get()));
+					ZeroMemory(this->KeyStorage.get(), sizeof(*this->GetKeyPtr()));
 				};
-				Key(rsa_key* key) : Key() { if (key) std::memmove(this->KeyStorage.get(), key, sizeof(*key)); };
+				Key(rsa_key* key) : Key() { if (key) std::memmove(this->GetKeyPtr(), key, sizeof(*key)); };
 				Key(rsa_key key) : Key(&key) {};
 				~Key()
 				{
@@ -111,17 +134,17 @@ namespace Utils
 
 				bool IsValid()
 				{
-					return (!Utils::MemIsSet(this->KeyStorage.get(), 0, sizeof(*this->KeyStorage.get())));
+					return (!Utils::MemIsSet(this->GetKeyPtr(), 0, sizeof(*this->GetKeyPtr())));
 				}
 
 				void Free()
 				{
 					if (this->IsValid())
 					{
-						rsa_free(this->KeyStorage.get());
+						rsa_free(this->GetKeyPtr());
 					}
 
-					ZeroMemory(this->KeyStorage.get(), sizeof(*this->KeyStorage.get()));
+					ZeroMemory(this->GetKeyPtr(), sizeof(*this->GetKeyPtr()));
 				}
 
 			private: