Skip to content

Unique Machine Identifier#101

Open
lozatto wants to merge 3 commits intoCS-NextClient:mainfrom
lozatto:Unique-Machine-Identifier
Open

Unique Machine Identifier#101
lozatto wants to merge 3 commits intoCS-NextClient:mainfrom
lozatto:Unique-Machine-Identifier

Conversation

@lozatto
Copy link
Copy Markdown

@lozatto lozatto commented Apr 6, 2026

Technical Proposal: Persistent Hardware Identification System (HWID) v2

Overview

The goal is to implement a robust Hardware ID (HWID) system for Counter-Strike 1.6 by extending the NextClient (Client-side) and NextClientServerApi (Server-side). This ensures a player’s identity is tied to their physical machine, effectively preventing identity spoofing, bypassing bans via IP/SteamID rotation, and improving community integrity.

1. Client-Side Collection (NextClient)

Objective: Generate a unique, anonymized, and highly resilient hardware fingerprint.

  • Mechanism: A multi-path collection strategy (v2) to ensure reliability even if WMI is disabled:
    • Primary (Win8+): Motherboard UUID + MSFT_Disk Serial (Boot disk specific).
    • Secondary (Vista/7): Win32_DiskDrive cross-referenced with logical boot drive.
    • Fallback (Bare Metal): Direct IOCTL_STORAGE_QUERY_PROPERTY calls to bypass WMI corruption.
    • Persistence: Persistent registry-based seed in HKCU\Software\NextClient if all hardware probes fail.
  • Privacy & Security: Raw identifiers are concatenated and hashed via SHA-256 (64 hex chars). Raw data never leaves the client machine.
// Example: Implementation in src/client/hwid_collector.cpp
namespace hwid {
    std::string Collect() {
        // Combines Motherboard UUID and Boot Disk Serial
        std::string seed1 = CollectMotherboardUUID(); 
        std::string seed2 = CollectBootDiskSerial();
        
        // Final anonymized fingerprint
        return Utils::Sha256(seed1 + "|" + seed2);
    }
}

2. Synchronization (NCLM Protocol)

Objective: Securely transmit the HWID during the initial connection handshake.

  • Protocol Modification: Define a new message opcode in src/common/nclm_proto.h:
    • NCLM_C2S_HARDWARE_ID: Sent immediately after the version check and before entering the game.
  • Data Integrity: The server enforces a "First-Packet-Only" rule per session. Any subsequent HWID packets are ignored to prevent mid-game spoofing via memory manipulation.

3. Session Management (Server Module)

Objective: Bind the HWID to the active player session for lookups.

  • Logic:
    1. The server handles the NCLM_C2S_HARDWARE_ID opcode within nclm_server.cpp.
    2. The 64-character hash is stored in the CPlayer object inside player_manager.cpp.
    3. The ID is indexed, allowing for efficient blacklisting or identity verification independent of SteamID or IP address.

4. AMX Mod X Interface (API Layer)

Objective: Expose the HWID to Pawn scripts (e.g., Zombie Plague, Anti-Cheat, or Rank systems).

  • Native Implementation: Adds ncl_get_hardware_id to the NextClient API.
// Native: ncl_get_hardware_id(id, buffer[], len)
static cell AMX_NATIVE_CALL ncl_get_hardware_id(AMX *amx, cell *params) {
    int index = params[1];
    CPlayer* player = g_PlayerManager.GetPlayer(index);
    if (player && player->IsHWIDReady()) {
        return set_amxstring(amx, params[2], player->GetHWID().c_str(), params[3]);
    }
    return 0;
}

Expected Repository Structure

Client Repository (nextclient)

src/
├── client/
│   ├── hwid_collector.cpp      <-- v2 Collection logic (WMI/IOCTL/Registry)
│   ├── hwid_collector.h        <-- Public API (Collect, IsReady, Reset)
│   └── nclm_client.cpp         <-- Handshake packet integration
└── common/
    └── nclm_proto.h            <-- New NCLM_C2S_HARDWARE_ID opcode

Server Repository (NextClientServerApi)

src/
├── server/
│   ├── amxx_api.cpp            <-- AMXX Native registration
│   ├── nclm_server.cpp         <-- Handling HWID incoming packets
│   └── player_manager.cpp      <-- HWID session storage in CPlayer
├── amx/
│   └── nextclient.inc          <-- AMXX include: native ncl_get_hardware_id

Reference: Resolves architectural requirements for advanced ban management and identity validation. Fixes Issue #100.

#pragma comment(lib, "Advapi32.lib")
#pragma comment(lib, "wbemuuid.lib")
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "oleaut32.lib")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be preferable to move library linkage from #pragma directives into CMake (nextclient/engine_mini/CMakeLists.txt), in target_link_libraries

#include "hwid_collector.h"

#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WIN32_LEAN_AND_MEAN and NOMINMAX are not required, as they are already defined in CMakeLists.txt

std::string Sanitize(const std::string& s)
{
const char* kInvalid[] = {
"To be filled by O.E.M.", "Default string", "None", "00000000-0000-0000-0000-000000000000", "0000_0000_0000_", ""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be safer to remove "" from kInvalid. The logic appears somewhat fragile: if the space were missing in find_first_not_of, all valid values would be treated as invalid

std::string t = s.substr(start, end - start + 1);

for (const char* inv : kInvalid)
if (t.find(inv) != std::string::npos || t == inv)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition || t == inv appears redundant, since the substring is already checked immediately before

VariantInit(&vSerial);

bool ok = false;
if (SUCCEEDED(pObj->Get(L"Index", 0, &vIdx, nullptr, nullptr)) && vIdx.vt == VT_I4 || vIdx.vt == VT_UI4)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like parentheses are missing here:
if (SUCCEEDED(pObj->Get(L"Index", 0, &vIdx, nullptr, nullptr)) && (vIdx.vt == VT_I4 || vIdx.vt == VT_UI4))

}

ULONGLONG tick = GetTickCount64();
DWORD pid = GetCurrentProcessId();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be preferable to use random generation here, just in case. While a collision between GetTickCount64 and GetCurrentProcessId is unlikely, it is still possible

return false;

const std::string& hwidStr = hwid::Collect();
if (hwidStr.size() != 64)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should most likely use NCLM_HWID_SIZE instead of 64

std::string combined = seed1 + "|" + seed2;
std::string hwid = Sha256Hex(combined);

if (hwid.size() == 64)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should most likely use NCLM_HWID_SIZE instead of 64

@@ -0,0 +1,6 @@
#pragma once

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add #include "hlsdk.h", the project does not compile due to the missing declaration of sizebuf_t

Comment thread nextclient/engine_mini/src/common/nclm/hwid_collector.cpp
Copy link
Copy Markdown
Author

@lozatto lozatto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.
Everything is clear and well organized, and the changes make sense.

perfect

@lozatto lozatto requested a review from Polarhigh April 22, 2026 19:58
@lozatto
Copy link
Copy Markdown
Author

lozatto commented Apr 22, 2026

I've made the suggested updates; please see if they look good to you @Polarhigh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants