Meinen Kernel patchen, um KVM-Erkennung zu vermeiden
Diese Reise hat mir beigebracht, meinen eigenen Linux-Kernel zu kompilieren. Ich habe viel über KVM-Interna gelernt und wie Hypervisor-Erkennung technisch funktioniert.
An diesem Thema habe ich schon vor einiger Zeit gearbeitet. Ich spielte damals ein paar Monate Escape From Tarkov. Ich nutze aber hauptsächlich Linux und das Spiel läuft nur unter Windows. Also tat ich, was viele Linux-Nutzer tun: eine virtuelle Maschine starten. Ich installierte Windows und das Spiel, reichte meine starke Host-GPU durch und ließ den Linux-Host die integrierte Intel-GPU nutzen. Dieses Setup heißt PCI Passthrough via OVMF.
Ich startete das Spiel und ging in eine Offline-Runde, um zu prüfen, ob alles funktioniert. Es lief. Die Performance war gut. Dann ging ich online. Nach exakt drei Minuten wurde ich aus dem Spiel geworfen, mit dem Hinweis, dass ein Hypervisor erkannt wurde. Kurz danach bekam ich einen permanenten Bann. Auch wenn ich vermutlich gegen die Nutzungsbedingungen verstoßen hatte, weil ich das Spiel in einer VM starten wollte, war ich nicht begeistert.
Dann hörte ich von diesem YouTube-Kanal. Er hatte einen Guide zu VALORANTs fragwürdigem Anticheat geteilt. Ich wollte dasselbe erreichen: die Präsenz eines Hypervisors verschleiern, um Windows-Spiele in einer VM unter Linux spielen zu können.
Nach viel Herumprobieren an der VM-Konfiguration wurde klar, dass das nicht reicht. Ich recherchierte weiter und fand heraus, dass Battleye verschiedene Techniken nutzt, um Hypervisor zu erkennen. Genauer: RDTSC-Timing-Checks.
RDTSC ist eine Instruktion, die einen CPU-Timestamp liefert. Dieser Timestamp wird normalerweise in die virtuelle Maschine durchgereicht. Es gibt aber Tricks, einen VM-Exit zu erzwingen, indem KVM-Handler ausgelöst werden. Ein VM-Exit passiert, wenn die virtuelle Maschine abgefangen wird und KVM-Logik übernimmt. Danach geht es mit VM-Enter zurück in die VM. Der Trick: Wenn man RDTSC vor und nach einem VM-Exit misst, kann man erkennen, ob ein Hypervisor vorhanden ist.
Was ich brauchte, war ein Offset für die RDTSC-Timing-Werte um die Zeit zwischen VM-Exit und VM-Enter. Zum Glück hatten andere das bereits umgesetzt. Dieser Patch allein war aber nicht der heilige Gral für Battleye-VM-Erkennung.
Nach schlaflosen Stunden und Tagen fand ich weitere Hinweise in arch/x86/kvm/cpuid.c. Die CPUID-Instruktion wird von KVM abgefangen und behandelt eigene Leafs, die die Präsenz eines Hypervisors leicht verraten können. Mehr Details dazu stehen hier.
Ich machte einen wilden Versuch: Handler im Switch-Statement auskommentieren, Kernel neu kompilieren und wieder online gehen. Die Erleichterung nach fünf Minuten ohne Kick war riesig. Ich hatte es geschafft. Ich hatte Battleye-VM-Erkennung selbst umgangen.
Diese Reise hat mir beigebracht, meinen eigenen Linux-Kernel zu kompilieren. Ich habe viel über KVM-Interna gelernt. Es war informativ und technisch spannend, auch wenn der Anlass etwas absurd war.
Meine Slides enthalten ein paar Memes und weitere Details.