Wenn eine VM geklont wird: doppelte Aktivierungen bei der Gerätebindung erkennen
Ein Geräte-Fingerabdruck identifiziert ein Gerät — und eine geklonte VM ist konstruktionsbedingt dasselbe Gerät zweimal. Snapshot-Wiederherstellung, Golden Images und automatisch skalierte Instanzen reproduzieren genau die Hardware- und Betriebssystemattribute, die eine Gerätebindung zu einem Hash zusammenfasst, sodass zwei laufende Kopien einen identischen Fingerabdruck präsentieren und die Gerätebindung sie nicht auseinanderhalten kann. Die Lösung ist kein besserer Fingerabdruck; es ist eine Aktivierungsidentität, die der Server besitzt, plus ein Instanz-Nonce pro Lauf, den der Client nicht sinnvoll klonen kann, sodass sich zwei Kopien einer Aktivierung in dem Moment verraten, in dem beide nach Hause telefonieren. Dieser Beitrag liest das SDK und die Serverrichtlinie für genau das: warum der Fingerabdruck ausgehebelt wird, wie ein Aktivierungseintrag und eine Laufzeit-Instanz-ID eine legitime Migration von einem live laufenden Klon trennen, und wie Keyright zwischen „der Neueste gewinnt" und „markieren und halten" entscheidet, ohne einen Kunden auszusperren, der einfach umgezogen ist.
Die Gerätebindung ruht auf einer Annahme: Ein Geräte-Fingerabdruck identifiziert ein Gerät. Klone eine VM und diese Annahme ist schlicht falsch. Ein Snapshot, der auf einem zweiten Host wiederhergestellt wird, ein Golden Image, das auf fünfzig Instanzen ausgerollt wird, eine VM-Vorlage, die ein Autoscaler bei Bedarf stanzt — jede davon reproduziert die Hardware- und Betriebssystemattribute, aus denen ein Fingerabdruck aufgebaut ist, was bedeutet, dass jede Kopie denselben Fingerabdruck berechnet. Die Gerätebindung fragt „ist dies das Gerät, auf dem die Lizenz aktiviert wurde?” und für einen Klon lautet die ehrliche Antwort ja.
Der übliche Instinkt also — mehr Attribute in den Hash aufzunehmen, den Fingerabdruck stärker zu machen — ist genau das Falsche. Der Fingerabdruck tut seinen Job. Er wurde entworfen, um über Neustarts, Treiberaktualisierungen und eine getauschte Platte stabil zu sein, und diese Stabilität ist genau das, was das Klonen überlebt. Ein Fingerabdruck, der zwei Klone unterscheiden könnte, wäre einer, der sich bei jedem Neustart ändert, was deine ehrlichen Kunden am Dienstagmorgen aussperren würde.
Das, was zwei Klone unterscheiden kann, befindet sich gar nicht auf dem Gerät. Es ist eine Identität, die der Server prägt und nachverfolgt: ein Aktivierungseintrag, den der Server besitzt, plus eine Laufzeit-Instanz-ID, die jeder laufende Prozess frisch erzeugt und bei jedem Herzschlag meldet. Dieser Beitrag geht den Mechanismus von Anfang bis Ende durch — warum der Fingerabdruck ausgehebelt wird, was eine Aktivierung hinzufügt, wie der Herzschlag „dasselbe Gerät zweimal” in ein beobachtbares Ereignis verwandelt, und wie man handelt, ohne eine Notfallübung in einen Support-Brand zu verwandeln.
Warum der Fingerabdruck ausgehebelt wird, konkret
Ein Gerätebindungs-Fingerabdruck ist ein Hash über Signale, die für eine gegebene Installation stabil sind. Eine typische Ableitung unter Windows zieht eine Handvoll Kennungen, fasst jede lokal zu einem Hash zusammen, damit die rohen Seriennummern nie das Gerät verlassen, und kombiniert sie:
// Simplified: each signal is salted + hashed locally; only the digest is sent.
public static string ComputeFingerprint()
{
var signals = new[]
{
ReadMachineGuid(), // HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid
ReadSystemVolumeSerial(), // volume serial of the OS drive
ReadPrimaryMacAddress(), // first non-virtual NIC
ReadBaseboardSerial() // WMI Win32_BaseBoard.SerialNumber
};
using var sha = SHA256.Create();
var parts = signals.Select(s =>
Convert.ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes("kr:" + s))));
return Convert.ToHexString(
sha.ComputeHash(Encoding.UTF8.GetBytes(string.Join('|', parts))))[..32];
}
Klone jetzt die VM. Die MachineGuid lebt in der Registry-Hive, die auf dem Datenträger-Image liegt — sie wird kopiert. Die Datenträger-Seriennummer ist eine Eigenschaft des Dateisystems, in das Image geschrieben — sie wird kopiert. Die Baseboard-Seriennummer ist, was der Hypervisor meldet, und eine Vorlage meldet jeder daraus gestanzten Instanz denselben Wert — sie wird kopiert. Sogar die MAC wird kopiert, sofern die Plattform nicht so konfiguriert ist, sie zu randomisieren, und viele Snapshot-Wiederherstellungs-Abläufe behalten sie bei. Der Klon führt ComputeFingerprint() aus und erhält f3a9…, den exakten Wert, den das Original bei der Aktivierung erzeugt hat.
Das ist kein schwacher Fingerabdruck. Es ist ein korrekter, ausgehebelt durch die Tatsache, dass das Klonen die Eingaben kopiert. Die einzigen Signale, die sich zwischen Klonen unterscheiden würden, sind jene, die sich auch zwischen Neustarts unterscheiden — Prozessstartzeit, eine flüchtige Boot-ID, eine frisch erzeugte GUID — und diese in den Fingerabdruck einzubinden würde die Stabilität zerstören, die die Gerätebindung nutzbar macht. Der Fingerabdruck und der Klon-Detektor wollen gegensätzliche Eigenschaften desselben Werts, also können sie nicht derselbe Wert sein.
Aktivierungsidentität: was der Server hinzufügt
Die serverseitige Hälfte der Bindung ist die Aktivierung. Wenn ein Client aktiviert, sendet er seinen Fingerabdruck; der Server erfasst eine Aktivierung gegen die Lizenz und gibt zwei Dinge zurück, die der Fingerabdruck allein nie lieferte: eine ActivationId, die der Server zugewiesen hat, und ein undurchsichtiges InstanceSecret, das der Client lokal persistiert.
public sealed record ActivationResult(
string ActivationId, // server-assigned, unique per accepted activation
string InstanceSecret, // opaque; the client stores this and replays it
SignedLicense License); // the usual signed payload for offline enforcement
var activation = await client.ActivateAsync(licenseKey, Computefingerprint(), ct);
await _store.PersistAsync(activation); // written to the client's protected store
Bis hierher gewinnt ein Klon noch: Die ActivationId und das InstanceSecret werden beide auf die Platte geschrieben, also erbt der Klon sie zusammen mit dem Fingerabdruck. Das ist in Ordnung — wir versuchen nicht, den Klon am Starten zu hindern. Wir bauen auf das eine Ding hin, das ein Klon nicht kopieren kann: die Identität eines laufenden Prozesses.
Diese Identität ist die Laufzeit-Instanz-ID. Sie wird einmal, im Speicher, beim Start erzeugt und absichtlich nicht persistiert:
public sealed class RuntimeIdentity
{
// New value every process launch. Never written to disk, so a clone of the
// disk image does not carry it — each copy generates its own on first run.
public string RuntimeInstanceId { get; } = Guid.NewGuid().ToString("N");
public DateTimeOffset StartedAt { get; } = DateTimeOffset.UtcNow;
}
Die originale VM, bereits laufend, hält R-1a im Speicher. Der Klon startet und erzeugt R-9f. Beide tragen die Aktivierung A-7721 auf der Platte. Beide berechnen den Fingerabdruck f3a9…. Der Unterschied, der zählt, existiert jetzt nur zur Laufzeit, und es ist genau der Unterschied, den der Herzschlag zum Server trägt.
Der Herzschlag verwandelt „dasselbe Gerät” in ein beobachtbares Ereignis
Keyright-Clients schlagen bereits zum Server, um ein Lease am Leben zu halten. Die Änderung zur Klon-Erkennung ist klein: Der Herzschlag trägt die Laufzeit-Instanz-ID, und der Server erfasst, welche Laufzeit-IDs er in letzter Zeit gegen jede Aktivierung gesehen hat.
public sealed record HeartbeatRequest(
string LicenseId,
string ActivationId,
string InstanceSecret, // proves the caller holds the activation
string RuntimeInstanceId, // identifies this specific running process
DateTimeOffset StartedAt);
Auf dem Server sucht der Herzschlag-Handler die Aktivierung, prüft das Instanz-Geheimnis und stellt dann eine neue Frage: Wie viele verschiedene Laufzeit-IDs haben innerhalb des Live-Fensters gegen diese Aktivierung geschlagen? Eine ist normal. Zwei oder mehr, zeitlich überlappend, ist ein Duplikat.
public async Task<HeartbeatResult> HeartbeatAsync(HeartbeatRequest req, SeatPolicy policy, CancellationToken ct)
{
await using var _ = await _store.LockActivationAsync(req.ActivationId, ct);
var activation = await _store.FindActivationAsync(req.ActivationId, ct);
if (activation is null || !activation.SecretMatches(req.InstanceSecret))
return HeartbeatResult.ActivationLost(); // revoked, reaped, or forged secret
var now = _clock.GetUtcNow();
activation.RecordRuntimeBeat(req.RuntimeInstanceId, now);
// Distinct runtime ids seen within the live window = concurrently running copies.
var liveRuntimes = activation.LiveRuntimeIds(now, policy.Skew);
if (liveRuntimes.Count > 1)
{
var decision = policy.OnDuplicate(activation, req.RuntimeInstanceId, liveRuntimes);
if (decision == DuplicateAction.EvictCaller)
return HeartbeatResult.ActivationLost(); // this caller is the loser; it must re-activate
if (decision == DuplicateAction.FlagAndHold)
activation.Flag(DuplicateReason.ConcurrentRuntimes, liveRuntimes);
// FlagAndHold and KeepNewest both fall through and renew the winner below.
}
var renewed = _signer.Sign(activation.Lease with { IssuedAt = now, ExpiresAt = now + policy.Ttl });
await _store.UpsertActivationAsync(activation, ct);
return HeartbeatResult.Renewed(renewed);
}
Das entscheidende Detail ist LiveRuntimeIds: Es zählt Laufzeit-IDs, deren jüngster Schlag innerhalb von now - (Ttl + Skew) liegt. Eine Laufzeit-ID, die einmal geschlagen hat und dann verstummt ist, altert aus der Menge heraus, sodass ein Gerät, das du letzte Woche heruntergefahren hast, nicht gegen dich zählt. Zwei IDs, die jetzt schlagen, sind die Klon-Signatur — das Original hat nie angehalten, und die Kopie ist gestartet. Diese zeitliche Überlappung ist das ganze Signal.
Eine Migration ist eine Übergabe; ein Klon ist eine Überlappung
Hier richten die meisten naiven Klon-Detektoren mehr Schmerz an, als sie verhindern. Ein Kunde, der eine VM legitim verschiebt — den alten Host außer Dienst stellt, auf einem neuen wiederherstellt — erzeugt eine Abfolge, die oberflächlich wie ein Klon aussieht: zwei Geräte, eine Aktivierung, derselbe Fingerabdruck. Der Unterschied ist das Timing. Bei einer Migration hört die alte Laufzeit-ID auf zu schlagen, bevor die neue startet; die Mengen überlappen sich nie. Bei einem Klon schlagen beide weiter.
LiveRuntimeIds kodiert diese Unterscheidung bereits, denn eine Laufzeit-ID, die aufgehört hat zu schlagen, altert heraus. Die Richtlinie kann also ehrlich damit umgehen:
public DuplicateAction OnDuplicate(
Activation activation, string caller, IReadOnlyList<string> liveRuntimes)
{
// A genuine migration never reaches here: the old runtime aged out before the
// new one beat, so liveRuntimes.Count == 1. Reaching here means true overlap.
// Newest-wins: the most recently started runtime keeps the activation, the
// older one is evicted on its next heartbeat and must re-activate.
var newest = activation.NewestRuntimeId(liveRuntimes);
if (caller != newest)
return DuplicateAction.EvictCaller;
// The caller is the newest; keep it, but record the overlap for the dashboard.
return activation.OverlapCount > HighWaterMark
? DuplicateAction.FlagAndHold // repeated cloning: hold and surface loudly
: DuplicateAction.KeepNewest;
}
„Der Neueste gewinnt” ist der übliche Standard für interaktive Software: Die Person, die die Anwendung gerade gestartet hat, arbeitet weiter, und die aufgegebene Kopie verliert still ihre Aktivierung. Für Serverlasten kehrst du es oft zu „der Älteste gewinnt” um, damit ein fehlgezündetes Autoscaling nicht die Instanz verdrängt, die tatsächlich Datenverkehr bedient. Keyright lässt dich pro Lizenz wählen, denn die richtige Antwort hängt davon ab, ob ein Mensch oder ein Scheduler den Start auslöst.
Für den ehrlichen Umzügler ist die Verdrängung nicht das Ende — sie ist eine Aufforderung. Wenn der nächste Herzschlag der verdrängten Kopie ActivationLost zurückgibt, reaktiviert der Client, was die Aktivierung in einem einzigen Hin und Her an die neue Laufzeit-ID neu bindet:
var result = await client.HeartbeatAsync(request, ct);
if (!result.Renewed)
{
// Lost the activation — either we were cloned and lost newest-wins, or the
// activation was reaped. Re-activate: rebinds to THIS runtime id cleanly.
var fresh = await client.ReactivateAsync(licenseKey, ComputeFingerprint(), ct);
await _store.PersistAsync(fresh);
_state.Replace(fresh.License);
}
Ein Kunde, der eine VM verschoben hat, sieht im schlimmsten Fall eine erneute Verbindung. Ein Skript, das zehn Klone aus einem Image stanzt, sieht zehn Kopien, die sich um eine Aktivierung streiten, neun davon verlieren „der Neueste gewinnt” in jedem Herzschlag-Zyklus und ein FlagAndHold auf dem Aktivierungseintrag in dem Moment, in dem der Überlappungszähler deine obere Schwelle überschreitet — was das Ereignis ist, das du wirklich auf deinem Dashboard haben willst.
Was man nicht tun sollte
Töte den Prozess nicht bei der ersten Erkennung. Die erste Erkennung ist auch das, wonach eine legitime Wiederherstellung aus einem Backup in den wenigen Sekunden aussieht, bevor die alte Instanz ausaltert, und ein harter Abbruch verwandelt eine Notfallübung in einen kundenseitigen Ausfall. Außerdem bringt er dir nichts gegen einen echten Angreifer, der einen harten Abbruch in Minuten patcht; die Erkennung gehört auf den Server, wo der Angreifer nicht hinreicht, nicht in ein clientseitiges Environment.Exit.
Versuche nicht, den Fingerabdruck klon-sicher zu machen. Jedes Attribut, das stabil genug ist, einen Neustart zu überleben, überlebt einen Klon, und jedes Attribut, das sich zwischen Klonen unterscheiden würde, unterscheidet sich zwischen Neustarts. Die Aufgabe des Fingerabdrucks ist es, eine Lizenz an ein Gerät zu binden; zwei Kopien dieses Geräts zu trennen, ist die Aufgabe der Aktivierung und der Laufzeit-Instanz-ID.
Und behandle ein einzelnes Duplikat nicht als Beweis für Piraterie. Klonen ist, wie moderne Infrastruktur funktioniert — Images, Snapshots, Vorlagen, Notfallwiederherstellung. Das Signal, auf das du reagierst, ist nicht „ein Duplikat ist einmal aufgetreten”; es ist „zwei Kopien sind gleichzeitig am Leben, wiederholt, mehr als es irgendeine legitime Topologie erklärt”. Das ist eine Zahl, die du festlegen, zur Anzeige bringen und verteidigen kannst — was genau das ist, was dir eine Gerätebindung gibt, die das Klonen versteht, und was ein Fingerabdruck allein nie gibt.
Nebula.NET testen
Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.