/* * AstraGuard C SDK v1.3.0 * Single-header C89/C99 license validation client * * Dependencies: libcurl, cJSON (or any JSON parser - see AG_JSON_* macros) * * Usage: * #define ASTRAGUARD_IMPLEMENTATION (in exactly ONE .c file) * #include "astraguard.h" * * Quick start: * ag_client_t* client = ag_create("https://api.astraguard.io", "PRODUCT-UUID"); * ag_set_response_key(client, "YOUR-RESPONSE-KEY-BASE64"); - required, see below * ag_result_t result = ag_validate(client, "XXXX-XXXX-XXXX-XXXX", NULL); * if (!result.valid) { fprintf(stderr, "License invalid: %s\n", result.reason); exit(1); } * ag_destroy(client); * * ag_set_response_key() (Dashboard -> Products -> Security -> Response Key) * is required for ag_validate()/ag_activate()/ag_verify() to ever return * valid=1 - since 1.3.0 the SDK fails closed (reason="response_key_not_set") * rather than trusting an unsigned server response. * * Copyright (c) 2026 AstraGuard. MIT License. */ #ifndef ASTRAGUARD_H #define ASTRAGUARD_H #ifdef __cplusplus extern "C" { #endif #include /* size_t, NULL */ /* ── Version ─────────────────────────────────────────────────────────────── */ /* 1.3.0: response verification is now fail-closed. Previously, an * integration that never called ag_set_response_key() had every server * response trusted with NO verification at all - exactly the "SDK doesn't * verify by default" gap reported against this SDK. ag_validate()/ * ag_activate()/ag_verify() now return valid=0, reason="response_key_not_set" * instead of silently accepting a claimed "valid":true. ag_verify_response_token() * was fixed the same way (previously returned 1/"valid" with no key set). * This is a deliberate, security-motivated behavior change, not additive - * any integration relying on the old silent pass-through must call * ag_set_response_key() (Dashboard -> Products -> Security -> Response Key) * to keep working. See docs.astraguard.io for details. * 1.2.0: full response-body signature verification (X-AstraGuard-Signature) * added for when a key IS configured - that part was already correct. */ #define AG_VERSION_MAJOR 1 #define AG_VERSION_MINOR 3 #define AG_VERSION_PATCH 0 #define AG_VERSION_STR "1.3.0" /* ── Configuration ───────────────────────────────────────────────────────── */ #ifndef AG_MAX_KEY_LEN # define AG_MAX_KEY_LEN 64 #endif #ifndef AG_MAX_HWID_LEN # define AG_MAX_HWID_LEN 256 #endif #ifndef AG_MAX_REASON_LEN # define AG_MAX_REASON_LEN 64 #endif #ifndef AG_MAX_VARS # define AG_MAX_VARS 32 #endif #ifndef AG_MAX_VAR_KEY_LEN # define AG_MAX_VAR_KEY_LEN 64 #endif #ifndef AG_MAX_VAR_VAL_LEN # define AG_MAX_VAR_VAL_LEN 512 #endif #ifndef AG_MAX_FEATURES # define AG_MAX_FEATURES 16 #endif #ifndef AG_MAX_FEATURE_LEN # define AG_MAX_FEATURE_LEN 64 #endif #ifndef AG_MAX_URL_LEN # define AG_MAX_URL_LEN 256 #endif #ifndef AG_TIMEOUT_SECONDS # define AG_TIMEOUT_SECONDS 10 #endif /* ── Types ───────────────────────────────────────────────────────────────── */ typedef struct ag_variable { char key[AG_MAX_VAR_KEY_LEN]; char value[AG_MAX_VAR_VAL_LEN]; } ag_variable_t; typedef struct ag_security_flags { int block_vm; /* 1 = blockVm enabled on server */ int block_debug; /* 1 = blockDebug enabled on server */ int integrity_check; /* 1 = integrityCheck enabled on server */ char integrity_hash[128]; /* expected SHA-256 hex of binary */ } ag_security_flags_t; typedef struct ag_result { int valid; /* 1 = license is valid */ char reason[AG_MAX_REASON_LEN]; /* error code when valid == 0 */ char expires_at[32]; /* ISO-8601 or empty for lifetime */ int is_lifetime; /* 1 = no expiry */ int remaining_days; /* -1 = lifetime, 0 = expired today */ ag_variable_t variables[AG_MAX_VARS]; /* cloud variables */ int variable_count; char features[AG_MAX_FEATURES][AG_MAX_FEATURE_LEN]; /* feature flags */ int feature_count; ag_security_flags_t security; /* security flags from server */ /* Anti-MITM tokens (verify rt with your auth key) */ char rt[128]; /* HMAC-SHA256 response token */ long long rts; /* timestamp (ms since epoch) */ char rn[64]; /* echoed nonce */ } ag_result_t; typedef struct ag_announcement { char id[40]; char title[256]; char message[1024]; char type[16]; /* "info", "update", "warning" */ char link[AG_MAX_URL_LEN]; char created_at[32]; } ag_announcement_t; /* Opaque client handle */ typedef struct ag_client ag_client_t; /* ── Public API ──────────────────────────────────────────────────────────── */ /* * ag_create() - allocate a new client * api_url: base URL, e.g. "https://api.astraguard.io" * product_id: UUID from dashboard * Returns NULL on allocation failure. */ ag_client_t* ag_create(const char* api_url, const char* product_id); /* * ag_destroy() - free all resources */ void ag_destroy(ag_client_t* client); /* * ag_set_hwid() - override the auto-generated HWID * If not called, HWID is generated automatically (ComputerName_Username on Windows, * hostname_username on POSIX). */ void ag_set_hwid(ag_client_t* client, const char* hwid); /* * ag_set_version() - set client version (checked against product minVersion) */ void ag_set_version(ag_client_t* client, const char* version); /* * ag_set_nonce() - set a per-request nonce for replay-attack protection * The server echoes it back in result.rn. Generate a random string each call. */ void ag_set_nonce(ag_client_t* client, const char* nonce); /* * ag_validate() - validate a license key (binds HWID on first call) * key: license key string, e.g. "AAAA-BBBB-CCCC-DDDD" * hwid: optional HWID override (NULL = use auto-generated or previously set) */ ag_result_t ag_validate(ag_client_t* client, const char* key, const char* hwid); /* * ag_activate() - first-time activation (same as validate, kept for clarity) */ ag_result_t ag_activate(ag_client_t* client, const char* key, const char* hwid); /* * ag_verify() - lightweight heartbeat check (does not rebind HWID) */ ag_result_t ag_verify(ag_client_t* client, const char* key); /* * ag_validate_or_exit() - validate and call exit(1) + show error if invalid * Convenience wrapper. Pass title for the error message (may be NULL). */ void ag_validate_or_exit(ag_client_t* client, const char* key, const char* error_title); /* * ag_get_variable() - get a cloud variable value from the last validate result * Returns NULL if the key was not found. * The returned pointer is valid until the next ag_validate/ag_verify call. */ const char* ag_get_variable(ag_client_t* client, const char* key); /* * ag_has_feature() - check if a feature flag is enabled * Returns 1 if enabled, 0 otherwise. */ int ag_has_feature(ag_client_t* client, const char* feature_name); /* * ag_get_remaining_days() - days until expiry from the last validate result * Returns -1 for lifetime licenses, 0 if expired, positive days otherwise. */ int ag_get_remaining_days(ag_client_t* client); /* * ag_get_hwid() - return the HWID string used by this client * Populated after the first ag_validate() call (or after ag_set_hwid). */ const char* ag_get_hwid(ag_client_t* client); /* * ag_get_security_flags() - security flags from the last validate response */ ag_security_flags_t ag_get_security_flags(ag_client_t* client); /* * ag_get_announcements() - fetch in-app announcements for a license key * Writes up to max_count entries into out_announcements. * Returns the number of announcements written, or -1 on error. */ int ag_get_announcements(ag_client_t* client, const char* key, ag_announcement_t* out_announcements, int max_count); /* * ag_set_response_key() - set Base64-encoded response authentication key * Get this from: GET /products/:id/response-key in the dashboard. * When set, ag_verify_response_token() verifies every server response * with HMAC-SHA256 to detect MITM attacks / fake servers. * Embed XOR-obfuscated in your binary. */ void ag_set_response_key(ag_client_t* client, const char* b64_key); /* * ag_verify_response_token() - verify the HMAC response token (anti-MITM) * Call after ag_validate() when a response key is set. * Returns 1 only if the signature is valid. Returns 0 if verification * fails OR if no response key is configured - there is no unsigned * response this function can honestly call "verified". * A return value of 0 means either the response was tampered with * (possible MITM attack) or ag_set_response_key() was never called. */ int ag_verify_response_token(ag_client_t* client, const ag_result_t* result); /* * ag_last_error() - human-readable error string from the last failed call */ const char* ag_last_error(ag_client_t* client); /* * ag_check_debugger() - returns 1 if a debugger is detected, 0 otherwise * Windows-only; always returns 0 on other platforms. * Checks: IsDebuggerPresent, CheckRemoteDebuggerPresent, NtQueryInformationProcess * DebugPort, heap flags, hardware breakpoints (Dr0-Dr3). * Call before ag_validate() when your product has blockDebug enabled. */ int ag_check_debugger(void); /* * ag_check_vm() - returns 1 if running inside a virtual machine, 0 otherwise * Windows-only; always returns 0 on other platforms. * Uses multi-vector scoring (threshold ≥3 detections = VM): * CPUID hypervisor bit, hypervisor vendor string, VM registry keys, * VM MAC address prefixes, VM guest service processes. */ int ag_check_vm(void); /* ── Implementation ──────────────────────────────────────────────────────── */ #ifdef ASTRAGUARD_IMPLEMENTATION #include #include #include #ifdef _WIN32 # include # include # include # include # pragma comment(lib, "iphlpapi.lib") # pragma comment(lib, "advapi32.lib") #else # include # include # include #endif /* libcurl is required */ #include /* ── Internal structures ─────────────────────────────────────────────────── */ struct ag_client { char api_url[AG_MAX_URL_LEN]; char product_id[64]; char hwid[AG_MAX_HWID_LEN]; char version[32]; char nonce[64]; char last_error[256]; /* Response authentication key (decoded from Base64, for HMAC verification) */ unsigned char response_key[64]; int response_key_len; /* 0 = not set */ ag_result_t last_result; int hwid_set; }; /* ── Internal: HTTP response buffer ─────────────────────────────────────── */ typedef struct { char* data; size_t size; size_t capacity; } ag_buf_t; static size_t ag__write_cb(void* ptr, size_t sz, size_t nmemb, void* userdata) { ag_buf_t* buf = (ag_buf_t*)userdata; size_t add = sz * nmemb; if (buf->size + add + 1 > buf->capacity) { size_t newcap = buf->capacity ? buf->capacity * 2 : 4096; while (newcap < buf->size + add + 1) newcap *= 2; char* newdata = (char*)realloc(buf->data, newcap); if (!newdata) return 0; buf->data = newdata; buf->capacity = newcap; } memcpy(buf->data + buf->size, ptr, add); buf->size += add; buf->data[buf->size] = '\0'; return add; } /* Response Integrity implementation phase (docs/architecture/ * ASTRAGUARD_SDK_RESPONSE_INTEGRITY_PLAN.md §5.1): captures the raw * X-AstraGuard-Signature header value (out-param - C has no convenient * multi-return). HTTP header names are case-insensitive per spec; strncasecmp * is POSIX/BSD and available via almost everywhere including * MinGW - avoided here anyway in favor of a tiny manual compare so this * stays plain C89/C99 with no extra platform header. */ #define AG_SIG_HEADER_LEN 128 static int ag__header_name_matches(const char* line, const char* name) { size_t i; for (i = 0; name[i]; i++) { char a = line[i], b = name[i]; if (a >= 'A' && a <= 'Z') a = (char)(a + 32); if (b >= 'A' && b <= 'Z') b = (char)(b + 32); if (line[i] == '\0' || a != b) return 0; } return 1; } static size_t ag__header_cb(char* buffer, size_t size, size_t nitems, void* userdata) { char* out = (char*)userdata; size_t total = size * nitems; static const char* prefix = "X-AstraGuard-Signature:"; if (total > strlen(prefix) && ag__header_name_matches(buffer, prefix)) { const char* v = buffer + strlen(prefix); size_t vlen = total - strlen(prefix); size_t start = 0, end; while (start < vlen && (v[start] == ' ' || v[start] == '\t')) start++; end = vlen; while (end > start && (v[end-1] == '\r' || v[end-1] == '\n' || v[end-1] == ' ' || v[end-1] == '\t')) end--; { size_t copyLen = end - start; if (copyLen >= AG_SIG_HEADER_LEN) copyLen = AG_SIG_HEADER_LEN - 1; memcpy(out, v + start, copyLen); out[copyLen] = '\0'; } } return total; } /* ── Internal: minimal JSON string extraction ────────────────────────────── */ /* Extracts the value of a top-level JSON string/bool/number field. Works for flat objects only - sufficient for the /validate response. Returns 1 on success, 0 if key not found. */ static int ag__json_str(const char* json, const char* key, char* out, size_t outlen) { char search[128]; const char* p; const char* end; size_t len; snprintf(search, sizeof(search), "\"%s\"", key); p = strstr(json, search); if (!p) return 0; p += strlen(search); while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; if (*p != ':') return 0; p++; while (*p == ' ' || *p == '\t') p++; if (*p == '"') { p++; end = p; while (*end && !(*end == '"' && *(end-1) != '\\')) end++; len = (size_t)(end - p); if (len >= outlen) len = outlen - 1; memcpy(out, p, len); out[len] = '\0'; return 1; } else { /* bool / number */ end = p; while (*end && *end != ',' && *end != '}' && *end != ']' && *end != '\n') end++; /* trim trailing whitespace */ while (end > p && (*(end-1) == ' ' || *(end-1) == '\t')) end--; len = (size_t)(end - p); if (len >= outlen) len = outlen - 1; memcpy(out, p, len); out[len] = '\0'; return 1; } } static int ag__json_bool(const char* json, const char* key, int def) { char val[16]; if (!ag__json_str(json, key, val, sizeof(val))) return def; return (strncmp(val, "true", 4) == 0) ? 1 : 0; } /* ── Internal: HWID generation ───────────────────────────────────────────── */ /* * Uses spoofer-stable sources: Windows MachineGuid (unique per OS install, * not targeted by game anti-cheat HWID spoofers) + ComputerName as salt. * End-users can run EAC/BattlEye HWID spoofers without losing their license. */ static void ag__gen_hwid(char* out, size_t outlen) { #ifdef _WIN32 char comp[MAX_COMPUTERNAME_LENGTH + 1] = {0}; DWORD complen = sizeof(comp); GetComputerNameA(comp, &complen); /* MachineGuid: unique per Windows installation. * Game anti-cheats don't check this, so spoofers never touch it. * Stored at HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid */ char guid[64] = {0}; HKEY hKey = NULL; if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography", 0, KEY_READ | KEY_WOW64_64KEY, &hKey) == ERROR_SUCCESS) { DWORD size = (DWORD)sizeof(guid); RegQueryValueExA(hKey, "MachineGuid", NULL, NULL, (LPBYTE)guid, &size); RegCloseKey(hKey); } if (guid[0] != '\0') { snprintf(out, outlen, "%s_%s", guid, comp); } else { /* Fallback: ComputerName only (original behavior) */ char user[256] = {0}; DWORD userlen = sizeof(user); GetUserNameA(user, &userlen); snprintf(out, outlen, "%s_%s", comp, user); } #else char hostname[HOST_NAME_MAX + 1] = {0}; char username[256] = {0}; struct passwd* pw = getpwuid(getuid()); gethostname(hostname, sizeof(hostname)); if (pw) strncpy(username, pw->pw_name, sizeof(username) - 1); snprintf(out, outlen, "%s_%s", hostname, username); #endif } /* ── Internal: parse /validate JSON response ─────────────────────────────── */ static ag_result_t ag__parse_result(const char* json) { ag_result_t r; char tmp[64]; memset(&r, 0, sizeof(r)); r.remaining_days = -1; if (!json || !*json) { strncpy(r.reason, "empty_response", sizeof(r.reason) - 1); return r; } ag__json_str(json, "valid", tmp, sizeof(tmp)); r.valid = (strncmp(tmp, "true", 4) == 0) ? 1 : 0; ag__json_str(json, "reason", r.reason, sizeof(r.reason)); ag__json_str(json, "expiresAt", r.expires_at, sizeof(r.expires_at)); ag__json_str(json, "rt", r.rt, sizeof(r.rt)); ag__json_str(json, "rn", r.rn, sizeof(r.rn)); { char rts_str[32] = {0}; ag__json_str(json, "rts", rts_str, sizeof(rts_str)); if (rts_str[0]) r.rts = (long long)atof(rts_str); } /* isLifetime */ r.is_lifetime = ag__json_bool(json, "isLifetime", 0); if (r.is_lifetime) r.remaining_days = -1; /* security flags */ { const char* sec = strstr(json, "\"security\""); if (sec) { sec = strchr(sec, '{'); if (sec) { r.security.block_vm = ag__json_bool(sec, "blockVm", 0); r.security.block_debug = ag__json_bool(sec, "blockDebug", 0); r.security.integrity_check = ag__json_bool(sec, "integrityCheck", 0); ag__json_str(sec, "integrityHash", r.security.integrity_hash, sizeof(r.security.integrity_hash)); } } } /* features array: scan for ["f1","f2",...] */ { const char* farr = strstr(json, "\"features\""); if (farr) { farr = strchr(farr, '['); if (farr) { farr++; while (*farr && *farr != ']' && r.feature_count < AG_MAX_FEATURES) { while (*farr == ' ' || *farr == ',') farr++; if (*farr == '"') { const char* end; size_t flen; farr++; end = strchr(farr, '"'); if (!end) break; flen = (size_t)(end - farr); if (flen >= AG_MAX_FEATURE_LEN) flen = AG_MAX_FEATURE_LEN - 1; memcpy(r.features[r.feature_count], farr, flen); r.features[r.feature_count][flen] = '\0'; r.feature_count++; farr = end + 1; } else if (*farr == ']') { break; } else { farr++; } } } } } /* variables object: {"key":"val",...} */ { const char* vobj = strstr(json, "\"variables\""); if (vobj) { vobj = strchr(vobj, '{'); if (vobj) { vobj++; while (*vobj && *vobj != '}' && r.variable_count < AG_MAX_VARS) { while (*vobj == ' ' || *vobj == ',') vobj++; if (*vobj == '"') { /* key */ const char* kend; size_t klen; vobj++; kend = strchr(vobj, '"'); if (!kend) break; klen = (size_t)(kend - vobj); if (klen >= AG_MAX_VAR_KEY_LEN) klen = AG_MAX_VAR_KEY_LEN - 1; memcpy(r.variables[r.variable_count].key, vobj, klen); r.variables[r.variable_count].key[klen] = '\0'; vobj = kend + 1; /* : */ while (*vobj == ' ' || *vobj == ':') vobj++; /* value */ if (*vobj == '"') { const char* vend; size_t vlen; vobj++; vend = strchr(vobj, '"'); if (!vend) break; vlen = (size_t)(vend - vobj); if (vlen >= AG_MAX_VAR_VAL_LEN) vlen = AG_MAX_VAR_VAL_LEN - 1; memcpy(r.variables[r.variable_count].value, vobj, vlen); r.variables[r.variable_count].value[vlen] = '\0'; vobj = vend + 1; r.variable_count++; } else { /* skip non-string value */ while (*vobj && *vobj != ',' && *vobj != '}') vobj++; } } else if (*vobj == '}') { break; } else { vobj++; } } } } } return r; } /* ── Internal: HTTP POST ─────────────────────────────────────────────────── */ /* sig_header_out: optional (may be NULL) - caller-owned buffer of at least * AG_SIG_HEADER_LEN bytes, filled with the raw X-AstraGuard-Signature header * value if the server sent one, left untouched (caller must zero-init) if * not. */ static int ag__post(ag_client_t* c, const char* path, const char* body, ag_buf_t* resp, char* sig_header_out) { CURL* curl; CURLcode res; struct curl_slist* headers = NULL; char url[AG_MAX_URL_LEN + 64]; snprintf(url, sizeof(url), "%s%s", c->api_url, path); curl = curl_easy_init(); if (!curl) { strncpy(c->last_error, "curl_easy_init failed", sizeof(c->last_error) - 1); return 0; } headers = curl_slist_append(headers, "Content-Type: application/json"); headers = curl_slist_append(headers, "Accept: application/json"); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_POST, 1L); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, ag__write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, resp); if (sig_header_out) { curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, ag__header_cb); curl_easy_setopt(curl, CURLOPT_HEADERDATA, sig_header_out); } curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)AG_TIMEOUT_SECONDS); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); /* Public-key pinning removed: CURLOPT_PINNEDPUBLICKEY pins the LEAF cert, * which rotates on every TLS renewal (~90 days) and silently broke every * client with a "network error". MITM protection is retained via full CA * chain verification (CURLOPT_SSL_VERIFYPEER=1, VERIFYHOST=2 above). */ res = curl_easy_perform(curl); curl_slist_free_all(headers); curl_easy_cleanup(curl); if (res != CURLE_OK) { strncpy(c->last_error, curl_easy_strerror(res), sizeof(c->last_error) - 1); return 0; } return 1; } /* ── Internal: HTTP GET ──────────────────────────────────────────────────── */ static int ag__get(ag_client_t* c, const char* path, ag_buf_t* resp) { CURL* curl; CURLcode res; struct curl_slist* headers = NULL; char url[AG_MAX_URL_LEN + 512]; snprintf(url, sizeof(url), "%s%s", c->api_url, path); curl = curl_easy_init(); if (!curl) { strncpy(c->last_error, "curl_easy_init failed", sizeof(c->last_error) - 1); return 0; } headers = curl_slist_append(headers, "Accept: application/json"); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, ag__write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, resp); curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)AG_TIMEOUT_SECONDS); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); /* Public-key pinning removed - see note in ag__post above. CA chain * verification (VERIFYPEER/VERIFYHOST) still protects against MITM. */ res = curl_easy_perform(curl); curl_slist_free_all(headers); curl_easy_cleanup(curl); if (res != CURLE_OK) { strncpy(c->last_error, curl_easy_strerror(res), sizeof(c->last_error) - 1); return 0; } return 1; } /* ── Public API implementation ───────────────────────────────────────────── */ ag_client_t* ag_create(const char* api_url, const char* product_id) { ag_client_t* c; if (!api_url || !product_id) return NULL; c = (ag_client_t*)calloc(1, sizeof(ag_client_t)); if (!c) return NULL; strncpy(c->api_url, api_url, AG_MAX_URL_LEN - 1); strncpy(c->product_id, product_id, sizeof(c->product_id) - 1); /* Remove trailing slash from URL */ { size_t len = strlen(c->api_url); if (len > 0 && c->api_url[len - 1] == '/') c->api_url[len - 1] = '\0'; } curl_global_init(CURL_GLOBAL_DEFAULT); return c; } void ag_destroy(ag_client_t* client) { if (client) { curl_global_cleanup(); free(client); } } void ag_set_hwid(ag_client_t* client, const char* hwid) { if (!client || !hwid) return; strncpy(client->hwid, hwid, AG_MAX_HWID_LEN - 1); client->hwid_set = 1; } void ag_set_version(ag_client_t* client, const char* version) { if (!client || !version) return; strncpy(client->version, version, sizeof(client->version) - 1); } void ag_set_nonce(ag_client_t* client, const char* nonce) { if (!client || !nonce) return; strncpy(client->nonce, nonce, sizeof(client->nonce) - 1); } /* Forward declaration - defined further below (with ag__hmac_sha256 and the * rest of the HMAC/Base64 helpers), but needed here since ag__do_validate * must verify the full response body BEFORE parsing it (docs/architecture/ * ASTRAGUARD_SDK_RESPONSE_INTEGRITY_PLAN.md §4.3). */ static int ag__verify_full_body_signature(const unsigned char* key, int key_len, const char* raw_body, const char* signature_header); /* Response Integrity: decides whether a parsed "valid":true result may be * trusted when no response key is configured to verify it against. Kept as * a small, pure, offline-testable helper (no network, no client struct) * rather than inlined into ag__do_validate - mirrors the C++ SDK's * RESPONSE_KEY_NOT_SET behavior. Only forces failure on a claimed * valid:true; an already-invalid result is left as-is untouched, since an * unsigned "invalid" response is at worst a DoS, never a license bypass. */ static void ag__enforce_response_key_required(ag_result_t* result, int response_key_len) { if (result->valid && response_key_len <= 0) { memset(result, 0, sizeof(*result)); result->remaining_days = -1; strncpy(result->reason, "response_key_not_set", sizeof(result->reason) - 1); } } static ag_result_t ag__do_validate(ag_client_t* c, const char* key, const char* hwid_override, const char* endpoint) { char body[1024]; ag_buf_t resp; ag_result_t result; const char* hwid; char sig_header[AG_SIG_HEADER_LEN]; memset(&result, 0, sizeof(result)); memset(&resp, 0, sizeof(resp)); memset(sig_header, 0, sizeof(sig_header)); if (!c || !key) { strncpy(result.reason, "invalid_args", sizeof(result.reason) - 1); return result; } /* Resolve HWID */ if (hwid_override && hwid_override[0]) { hwid = hwid_override; } else if (c->hwid_set && c->hwid[0]) { hwid = c->hwid; } else { ag__gen_hwid(c->hwid, sizeof(c->hwid)); c->hwid_set = 1; hwid = c->hwid; } /* Build JSON body */ if (c->version[0] && c->nonce[0]) { snprintf(body, sizeof(body), "{\"key\":\"%s\",\"hwid\":\"%s\",\"productId\":\"%s\"," "\"version\":\"%s\",\"nonce\":\"%s\"}", key, hwid, c->product_id, c->version, c->nonce); } else if (c->version[0]) { snprintf(body, sizeof(body), "{\"key\":\"%s\",\"hwid\":\"%s\",\"productId\":\"%s\",\"version\":\"%s\"}", key, hwid, c->product_id, c->version); } else { snprintf(body, sizeof(body), "{\"key\":\"%s\",\"hwid\":\"%s\",\"productId\":\"%s\"}", key, hwid, c->product_id); } if (!ag__post(c, endpoint, body, &resp, sig_header)) { strncpy(result.reason, "network_error", sizeof(result.reason) - 1); free(resp.data); return result; } /* Response Integrity implementation phase: verify the FULL response * body against X-AstraGuard-Signature BEFORE parsing it - nothing from * an unsigned or tampered body may ever populate result.features / * result.variables / result.security (docs/architecture/ * ASTRAGUARD_SDK_RESPONSE_INTEGRITY_PLAN.md §4.3). Skipped only when no * response key is configured at all - reuses the SAME existing * `result.reason` string field every other failure path here already * uses (e.g. "network_error", "invalid_args"), per §4.1's decision to * add no new public type. This only ever ADDS a rejection on top of the * existing, unrelated, opt-in ag_verify_response_token() narrow check - * it never replaces or weakens it. */ if (c->response_key_len > 0 && !ag__verify_full_body_signature(c->response_key, c->response_key_len, resp.data ? resp.data : "", sig_header)) { strncpy(result.reason, "response_tampered", sizeof(result.reason) - 1); free(resp.data); c->last_result = result; return result; } result = ag__parse_result(resp.data); free(resp.data); /* No key configured at all skips the full-body check above entirely - * without this, that meant a "valid":true response was trusted blindly * with zero verification (the exact "no local-proxy verification by * default" gap reported against this SDK). */ ag__enforce_response_key_required(&result, c->response_key_len); c->last_result = result; return result; } /* ── Base64 decode (for response key) ───────────────────────────────────── */ static int ag__b64_decode(const char* in, unsigned char* out, int max_out) { static const char tbl[256] = { -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63, 52,53,54,55,56,57,58,59,60,61,-1,-1,-1, 0,-1,-1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14, 15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1, -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40, 41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 }; int out_len = 0; int i = 0; int in_len = (int)strlen(in); while (i < in_len && out_len + 3 <= max_out) { unsigned int a = (unsigned int)tbl[(unsigned char)in[i]]; unsigned int b = (i+1 < in_len) ? (unsigned int)tbl[(unsigned char)in[i+1]] : 0; unsigned int c = (i+2 < in_len) ? (unsigned int)tbl[(unsigned char)in[i+2]] : 0; unsigned int d = (i+3 < in_len) ? (unsigned int)tbl[(unsigned char)in[i+3]] : 0; if (a == (unsigned int)-1) break; out[out_len++] = (unsigned char)((a << 2) | (b >> 4)); if (i+2 < in_len && in[i+2] != '=') out[out_len++] = (unsigned char)((b << 4) | (c >> 2)); if (i+3 < in_len && in[i+3] != '=') out[out_len++] = (unsigned char)((c << 6) | d); i += 4; } return out_len; } /* ── HMAC-SHA256 (for response verification) ─────────────────────────────── */ static void ag__sha256(const unsigned char* data, size_t len, unsigned char out[32]) { /* Minimal SHA-256 implementation (no external deps) */ static const unsigned int K[64] = { 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 }; unsigned int h[8] = { 0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a, 0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19 }; unsigned char block[64]; size_t i, j; unsigned long long bit_len = (unsigned long long)len * 8; #define CH(x,y,z) (((x)&(y))^(~(x)&(z))) #define MAJ(x,y,z) (((x)&(y))^((x)&(z))^((y)&(z))) #define ROTR(x,n) (((x)>>(n))|((x)<<(32-(n)))) #define S0(x) (ROTR(x,2)^ROTR(x,13)^ROTR(x,22)) #define S1(x) (ROTR(x,6)^ROTR(x,11)^ROTR(x,25)) #define G0(x) (ROTR(x,7)^ROTR(x,18)^((x)>>3)) #define G1(x) (ROTR(x,17)^ROTR(x,19)^((x)>>10)) size_t blocks = (len + 8) / 64 + 1; for (i = 0; i < blocks; i++) { unsigned int w[64], a, b, c, d, e, f, g, h2, T1, T2; size_t base = i * 64; memset(block, 0, 64); for (j = 0; j < 64 && base+j < len; j++) block[j] = data[base+j]; if (base+j == len) block[j] = 0x80; if (i == blocks-1) { block[56] = (unsigned char)(bit_len >> 56); block[57] = (unsigned char)(bit_len >> 48); block[58] = (unsigned char)(bit_len >> 40); block[59] = (unsigned char)(bit_len >> 32); block[60] = (unsigned char)(bit_len >> 24); block[61] = (unsigned char)(bit_len >> 16); block[62] = (unsigned char)(bit_len >> 8); block[63] = (unsigned char)(bit_len); } for (j = 0; j < 16; j++) w[j] = ((unsigned int)block[j*4]<<24)|((unsigned int)block[j*4+1]<<16)| ((unsigned int)block[j*4+2]<<8)|(unsigned int)block[j*4+3]; for (j = 16; j < 64; j++) w[j] = G1(w[j-2]) + w[j-7] + G0(w[j-15]) + w[j-16]; a=h[0];b=h[1];c=h[2];d=h[3];e=h[4];f=h[5];g=h[6];h2=h[7]; for (j = 0; j < 64; j++) { T1 = h2 + S1(e) + CH(e,f,g) + K[j] + w[j]; T2 = S0(a) + MAJ(a,b,c); h2=g; g=f; f=e; e=d+T1; d=c; c=b; b=a; a=T1+T2; } h[0]+=a;h[1]+=b;h[2]+=c;h[3]+=d;h[4]+=e;h[5]+=f;h[6]+=g;h[7]+=h2; } #undef CH #undef MAJ #undef ROTR #undef S0 #undef S1 #undef G0 #undef G1 for (i = 0; i < 8; i++) { out[i*4] = (unsigned char)(h[i]>>24); out[i*4+1] = (unsigned char)(h[i]>>16); out[i*4+2] = (unsigned char)(h[i]>>8); out[i*4+3] = (unsigned char)(h[i]); } } static void ag__hmac_sha256(const unsigned char* key, int key_len, const char* msg, size_t msg_len, unsigned char out[32]) { unsigned char k_ipad[64], k_opad[64], tk[32]; unsigned char* buf; size_t buf_len; int i; if (key_len > 64) { ag__sha256(key, (size_t)key_len, tk); key = tk; key_len = 32; } memset(k_ipad, 0x36, 64); memset(k_opad, 0x5c, 64); for (i = 0; i < key_len; i++) { k_ipad[i] ^= key[i]; k_opad[i] ^= key[i]; } buf_len = 64 + msg_len; buf = (unsigned char*)malloc(buf_len); if (!buf) { memset(out, 0, 32); return; } memcpy(buf, k_ipad, 64); memcpy(buf + 64, msg, msg_len); ag__sha256(buf, buf_len, out); free(buf); buf_len = 64 + 32; buf = (unsigned char*)malloc(buf_len); if (!buf) { memset(out, 0, 32); return; } memcpy(buf, k_opad, 64); memcpy(buf + 64, out, 32); ag__sha256(buf, buf_len, out); free(buf); } static void ag__bytes_to_hex(const unsigned char* in, int in_len, char* out, int max_out) { static const char H[] = "0123456789abcdef"; int i, j = 0; for (i = 0; i < in_len && j + 2 < max_out; i++) { out[j++] = H[(in[i] >> 4) & 0xF]; out[j++] = H[in[i] & 0xF]; } out[j] = '\0'; } /* Response Integrity implementation phase (docs/architecture/ * ASTRAGUARD_SDK_RESPONSE_INTEGRITY_PLAN.md §3/§6): verifies the FULL * response body against an X-AstraGuard-Signature header ("sha256="). * A free function (not tangled into ag__do_validate) specifically so the * test file - which #includes this header with ASTRAGUARD_IMPLEMENTATION * defined, same translation unit - can call it directly with a * hand-constructed key/body/header, entirely offline. Mirrors the C++ SDK's * Security::verifyFullBodySignature() split (docs/architecture/ * ASTRAGUARD_SDK_RESPONSE_INTEGRITY_PLAN.md §5.2 - one shared verify * function, not reimplemented per call site). */ static int ag__verify_full_body_signature(const unsigned char* key, int key_len, const char* raw_body, const char* signature_header) { static const char* prefix = "sha256="; size_t prefix_len = strlen(prefix); char expected_hex[65]; unsigned char hmac[32]; size_t i, provided_len; if (key_len <= 0 || !signature_header || !signature_header[0]) return 0; if (strncmp(signature_header, prefix, prefix_len) != 0) return 0; /* unrecognized format - never silently accepted */ ag__hmac_sha256(key, key_len, raw_body, strlen(raw_body), hmac); ag__bytes_to_hex(hmac, 32, expected_hex, (int)sizeof(expected_hex)); provided_len = strlen(signature_header + prefix_len); if (provided_len != strlen(expected_hex)) return 0; /* Constant-time, case-insensitive compare (server always sends lowercase * hex, but this must never depend on that for correctness). */ { unsigned char diff = 0; for (i = 0; i < provided_len; i++) { char a = expected_hex[i]; char b = signature_header[prefix_len + i]; if (b >= 'A' && b <= 'Z') b = (char)(b + 32); diff |= (unsigned char)(a ^ b); } return diff == 0 ? 1 : 0; } } static void ag__bytes_to_b64(const unsigned char* in, int in_len, char* out, int max_out) { static const char enc[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; int i = 0, j = 0; while (i < in_len && j + 4 < max_out) { unsigned int a = in[i], b = (i+1>2]; out[j++] = enc[((a&3)<<4)|(b>>4)]; out[j++] = (i+1>6)] : '='; out[j++] = (i+2response_key_len = ag__b64_decode(b64_key, client->response_key, (int)sizeof(client->response_key)); } int ag_verify_response_token(ag_client_t* client, const ag_result_t* result) { char payload[512], expected_b64[256], actual_b64[256]; unsigned char hmac[32]; if (!client || !result) return 0; /* Fail-closed: no response key configured means there is nothing to * verify the token against - there is no unsigned response that can be * honestly called "verified". Previously returned 1 ("valid") here, * which let callers wrongly treat an unsigned response as authenticated. */ if (client->response_key_len <= 0) return 0; /* If server did not return a token, verification fails */ if (!result->rt[0]) return 0; /* Payload format: "{nonce}|{rts}|{valid_bit}|{productId}" */ snprintf(payload, sizeof(payload), "%s|%lld|%s|%s", result->rn, result->rts, result->valid ? "1" : "0", client->product_id); ag__hmac_sha256(client->response_key, client->response_key_len, payload, strlen(payload), hmac); ag__bytes_to_b64(hmac, 32, expected_b64, (int)sizeof(expected_b64)); /* Base64-decode the received rt for comparison (or compare as-is if server sends b64) */ strncpy(actual_b64, result->rt, sizeof(actual_b64) - 1); actual_b64[sizeof(actual_b64) - 1] = '\0'; /* Constant-time comparison */ { size_t elen = strlen(expected_b64), alen = strlen(actual_b64); unsigned char diff = (unsigned char)(elen != alen); size_t n = elen < alen ? elen : alen; size_t i; for (i = 0; i < n; i++) diff |= (unsigned char)expected_b64[i] ^ (unsigned char)actual_b64[i]; return diff == 0 ? 1 : 0; } } ag_result_t ag_validate(ag_client_t* client, const char* key, const char* hwid) { return ag__do_validate(client, key, hwid, "/validate"); } ag_result_t ag_activate(ag_client_t* client, const char* key, const char* hwid) { return ag__do_validate(client, key, hwid, "/activate"); } ag_result_t ag_verify(ag_client_t* client, const char* key) { return ag__do_validate(client, key, NULL, "/validate"); } void ag_validate_or_exit(ag_client_t* client, const char* key, const char* error_title) { ag_result_t r = ag_validate(client, key, NULL); if (!r.valid) { const char* title = error_title ? error_title : "License Error"; fprintf(stderr, "%s: %s\n", title, r.reason[0] ? r.reason : "License validation failed."); #ifdef _WIN32 { char msg[512]; snprintf(msg, sizeof(msg), "%s\n\nReason: %s", title, r.reason[0] ? r.reason : "License validation failed."); MessageBoxA(NULL, msg, "AstraGuard", MB_OK | MB_ICONERROR); } #endif exit(1); } } const char* ag_get_variable(ag_client_t* client, const char* key) { int i; if (!client || !key) return NULL; for (i = 0; i < client->last_result.variable_count; i++) { if (strcmp(client->last_result.variables[i].key, key) == 0) return client->last_result.variables[i].value; } return NULL; } int ag_has_feature(ag_client_t* client, const char* feature_name) { int i; if (!client || !feature_name) return 0; for (i = 0; i < client->last_result.feature_count; i++) { if (strcmp(client->last_result.features[i], feature_name) == 0) return 1; } return 0; } int ag_get_remaining_days(ag_client_t* client) { if (!client) return -1; return client->last_result.remaining_days; } const char* ag_get_hwid(ag_client_t* client) { if (!client) return NULL; if (!client->hwid_set || !client->hwid[0]) { ag__gen_hwid(client->hwid, sizeof(client->hwid)); client->hwid_set = 1; } return client->hwid; } ag_security_flags_t ag_get_security_flags(ag_client_t* client) { ag_security_flags_t empty; memset(&empty, 0, sizeof(empty)); if (!client) return empty; return client->last_result.security; } int ag_get_announcements(ag_client_t* client, const char* key, ag_announcement_t* out, int max_count) { char path[AG_MAX_URL_LEN + AG_MAX_KEY_LEN]; ag_buf_t resp; const char* p; int count = 0; if (!client || !key || !out || max_count <= 0) return -1; memset(&resp, 0, sizeof(resp)); snprintf(path, sizeof(path), "/announcements/public?key=%s", key); if (!ag__get(client, path, &resp)) { free(resp.data); return -1; } /* Parse "announcements": [...] array */ p = resp.data ? strstr(resp.data, "\"announcements\"") : NULL; if (p) { p = strchr(p, '['); if (p) { p++; while (*p && *p != ']' && count < max_count) { const char* obj_start; const char* obj_end; char obj_buf[2048]; size_t obj_len; while (*p == ' ' || *p == ',' || *p == '\n' || *p == '\r') p++; if (*p != '{') { p++; continue; } /* Find matching } */ { int depth = 0; const char* q = p; while (*q) { if (*q == '{') depth++; else if (*q == '}') { depth--; if (depth == 0) break; } q++; } obj_start = p; obj_end = q; obj_len = (size_t)(obj_end - obj_start + 1); if (obj_len >= sizeof(obj_buf)) obj_len = sizeof(obj_buf) - 1; memcpy(obj_buf, obj_start, obj_len); obj_buf[obj_len] = '\0'; p = obj_end + 1; } memset(&out[count], 0, sizeof(out[count])); ag__json_str(obj_buf, "id", out[count].id, sizeof(out[count].id)); ag__json_str(obj_buf, "title", out[count].title, sizeof(out[count].title)); ag__json_str(obj_buf, "message", out[count].message, sizeof(out[count].message)); ag__json_str(obj_buf, "type", out[count].type, sizeof(out[count].type)); ag__json_str(obj_buf, "link", out[count].link, sizeof(out[count].link)); ag__json_str(obj_buf, "created_at", out[count].created_at, sizeof(out[count].created_at)); count++; } } } free(resp.data); return count; } const char* ag_last_error(ag_client_t* client) { if (!client) return "null client"; return client->last_error[0] ? client->last_error : "no error"; } /* ── Security checks ─────────────────────────────────────────────────────── */ #ifdef _WIN32 int ag_check_debugger(void) { typedef LONG (WINAPI* pNtQIP)(HANDLE, UINT, PVOID, ULONG, PULONG); BOOL remote; HMODULE hNt; HANDLE heap; /* Check 1: IsDebuggerPresent */ if (IsDebuggerPresent()) return 1; /* Check 2: CheckRemoteDebuggerPresent */ remote = FALSE; CheckRemoteDebuggerPresent(GetCurrentProcess(), &remote); if (remote) return 1; /* Check 3: NtQueryInformationProcess - DebugPort (class 7) */ hNt = GetModuleHandleA("ntdll.dll"); if (hNt) { pNtQIP fn = (pNtQIP)(void*)GetProcAddress(hNt, "NtQueryInformationProcess"); if (fn) { HANDLE port = NULL; if (fn(GetCurrentProcess(), 7, &port, sizeof(port), NULL) == 0 && port != NULL) return 1; } } /* Check 4: Heap flags - debugger sets HEAP_TAIL_CHECKING_ENABLED (0x40) */ heap = GetProcessHeap(); if (heap) { ULONG flags = 0; #ifdef _WIN64 flags = *(ULONG*)((BYTE*)heap + 0x70); #else flags = *(ULONG*)((BYTE*)heap + 0x44); #endif if (flags & 0x40) return 1; } /* Check 5: Hardware breakpoints - Dr0-Dr3 non-zero means breakpoint set */ { CONTEXT ctx; memset(&ctx, 0, sizeof(ctx)); ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; if (GetThreadContext(GetCurrentThread(), &ctx)) { if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3) return 1; } } return 0; } int ag_check_vm(void) { int score = 0; /* Check 1: CPUID hypervisor bit (ECX bit 31 of leaf 1) - 1 pt */ { int info[4]; memset(info, 0, sizeof(info)); __cpuid(info, 1); if (info[2] & (1 << 31)) score += 1; } /* Check 2: Hypervisor vendor string via CPUID leaf 0x40000000 - 3 pts */ { int info[4]; char v[13]; memset(info, 0, sizeof(info)); memset(v, 0, sizeof(v)); __cpuid(info, 0x40000000); memcpy(v, (char*)&info[1], 4); memcpy(v + 4, (char*)&info[2], 4); memcpy(v + 8, (char*)&info[3], 4); if (strncmp(v, "VMwareVMware", 12) == 0 || strstr(v, "KVMKVMKVM") || strstr(v, "VBoxVBox") || strstr(v, "Microsoft Hv") || strstr(v, "XenVMMXenVMM")) { score += 3; } } /* Check 3: VM registry keys - 2 pts */ { static const char* vmKeys[] = { "SOFTWARE\\VMware, Inc.\\VMware Tools", "SOFTWARE\\Oracle\\VirtualBox Guest Additions", "SYSTEM\\CurrentControlSet\\Services\\vmhgfs", "SYSTEM\\CurrentControlSet\\Services\\VBoxGuest", "SOFTWARE\\Microsoft\\Virtual Machine\\Guest\\Parameters", NULL }; int i; for (i = 0; vmKeys[i]; i++) { HKEY hk; if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, vmKeys[i], 0, KEY_READ, &hk) == ERROR_SUCCESS) { RegCloseKey(hk); score += 2; break; } } } /* Check 4: VM MAC address prefixes - 2 pts */ { IP_ADAPTER_INFO info[16]; DWORD sz = sizeof(info); static const unsigned char pfx[4][3] = { {0x00,0x0C,0x29}, {0x00,0x50,0x56}, {0x00,0x05,0x69}, {0x08,0x00,0x27} }; if (GetAdaptersInfo(info, &sz) == ERROR_SUCCESS) { PIP_ADAPTER_INFO a; for (a = info; a; a = a->Next) { int i; for (i = 0; i < 4; i++) { if (a->AddressLength >= 3 && a->Address[0] == pfx[i][0] && a->Address[1] == pfx[i][1] && a->Address[2] == pfx[i][2]) { score += 2; a = NULL; /* break outer */ break; } } if (!a) break; } } } /* Check 5: VM guest service processes - 2 pts */ { static const char* vmProcs[] = { "vmtoolsd.exe", "vmwaretray.exe", "vmwareuser.exe", "vboxservice.exe","vboxtray.exe", "vmsrvc.exe", "vmusrvc.exe", "xenservice.exe", "xenguestservice.exe", "prl_cc.exe", "prl_tools.exe", "qemu-ga.exe", NULL }; HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hSnap != INVALID_HANDLE_VALUE) { PROCESSENTRY32 pe; pe.dwSize = sizeof(pe); if (Process32First(hSnap, &pe)) { do { char name[MAX_PATH]; int j, k; /* lowercase copy */ for (k = 0; pe.szExeFile[k] && k < (int)(sizeof(name)-1); k++) name[k] = (char)(pe.szExeFile[k] >= 'A' && pe.szExeFile[k] <= 'Z' ? pe.szExeFile[k] + 32 : pe.szExeFile[k]); name[k] = '\0'; for (j = 0; vmProcs[j]; j++) { if (strcmp(name, vmProcs[j]) == 0) { score += 2; CloseHandle(hSnap); goto vm_done; } } } while (Process32Next(hSnap, &pe)); } CloseHandle(hSnap); } } vm_done: return score >= 3 ? 1 : 0; } #else /* non-Windows stubs */ int ag_check_debugger(void) { return 0; } int ag_check_vm(void) { return 0; } #endif /* _WIN32 */ #endif /* ASTRAGUARD_IMPLEMENTATION */ #ifdef __cplusplus } /* extern "C" */ #endif #endif /* ASTRAGUARD_H */