LLM-gestütztes JS-Reverse-Engineering: Vier Aufgaben für KI – und drei, die bei dir bleiben

Zuletzt aktualisiert: 2026-07-30 10:16:33

Ein webpack-generiertes Signature-SDK liegt vor dir: Variablen heißen nur noch a oder _0x3f2b, der Kontrollfluss wurde plattgewalzt, String-Literale stecken in einem Array und werden über Indizes geholt. Das Ziel ist eindeutig: eine eigenständige Implementierung bauen, deren Ausgabe Byte für Byte dem Original entspricht.

Der naheliegende Reflex ist heute, das komplette Bundle in ein LLM zu kippen und zu fragen: „Was macht dieser Code?“ Genau da scheitern die meisten. Das Modell liefert eine plausibel klingende Erklärung, du implementierst sie – und das Ergebnis weicht vom Ziel schon beim ersten Byte ab.

Das liegt nicht daran, dass das Modell grundsätzlich zu schwach wäre. Die Arbeitsteilung stimmt nur nicht. Bei der Deobfuskierung sind Modelle gut in Mustererkennung und Hypothesenbildung, aber schlecht bei der faktischen Verifikation. Sie erkennen das Gerüst einer kryptografischen Primitive unter einem Haufen Bitoperationen. Ob diese Einordnung tatsächlich korrekt ist, müssen jedoch deine Tests entscheiden.

Darauf baut dieser Workflow in vier Stufen auf – ebenso wie auf drei Aufgaben, die ich einem Modell grundsätzlich nicht überlasse.

Stufe 1: Die mechanische Zerlegung erledigt ein Skript

Der erste Gedanke lautet oft: „Das Kontextfenster ist riesig, also einfach alles hineinkopieren.“ Lass es. Dafür gibt es zwei gute Gründe.

Zum einen ist es Verschwendung. Der Großteil eines obfuskierten Bundles besteht aus Polyfills, Runtime-Shims und Business-Modulen, die mit deinem Ziel nichts zu tun haben. Du bezahlst dafür, sie in den Kontext zu laden, und bekommst dafür weniger fokussierte Aufmerksamkeit zurück.

Wichtiger ist aber: Je größer der Kontext, desto mehr Ansatzpunkte hat eine Halluzination. Das Modell verknüpft dann womöglich Eigenschaften zweier völlig unabhängiger Module und präsentiert eine in sich stimmige Schlussfolgerung, die im Code schlicht nicht existiert. Solche Fehler sind deutlich schwerer zu erkennen als offensichtlicher Unsinn.

Die Zerlegung ist deterministische Arbeit. Erledige sie per Skript:

  • Nutze ein AST-Tool wie @babel/parser oder acorn, um das Bundle in Module und Funktionen aufzuteilen und nach Scope zu indexieren;

  • extrahiere alle Zahlenliterale und String-Konstanten, gruppiert nach Häufigkeit und Bitbreite;

  • erstelle den Call-Graph und markiere Knoten mit Eingangsgrad 0 als Einstiegspunkte sowie Knoten mit Ausgangsgrad 0 als Leaf-Primitiven;

  • finde Funktionen mit auffällig hoher Dichte an Bitoperationen – wenn ^, >>>, << und & in einer Funktion geballt auftreten, sitzt dort meist der algorithmische Kern.

Genau diese Leaf-Primitiven willst du dem Modell zeigen. Sie umfassen meist nur einige Dutzend Zeilen, hängen nicht vom Zustand höherer Schichten ab und besitzen klar abgegrenzte Ein- und Ausgaben. Eine Leaf-Funktion mit 40 Zeilen plus den von ihr referenzierten Konstanten ist eine Granularität, mit der Modelle zuverlässig umgehen.

Am Ende dieser Stufe sollte eine „Liste potenzieller Primitiven“ stehen. Jeder Eintrag enthält den Funktionskörper, die referenzierten Konstanten und die Aufrufstellen. Jeder weitere Modellaufruf arbeitet mit genau einem Eintrag daraus – immer einzeln.

Stufe 2: Das Modell ordnet die Algorithmusfamilie zu

Hier ist das Modell kaum zu ersetzen.

Kryptografische Verfahren und Kodierungen hinterlassen eindeutige Fingerabdrücke: bestimmte Konstanten, charakteristische Kombinationen von Shift-Weiten, typische Schleifenstrukturen. Menschen erkennen sie durch Erfahrung; ein Modell hat öffentliche Implementierungen in großer Zahl gesehen und kommt hier besonders schnell ans Ziel.

Ein paar öffentliche Beispiele dafür, wie solche Fingerabdrücke aussehen:

  • 0x811c9dc5 und 0x01000193 sind zusammen Offset-Basis und Primzahl des 32-Bit-Hashes FNV-1a – die Standardwerte stehen auf der FNV-Referenzseite von Mitautor Landon Curt Noll, dort dezimal als 2166136261 und 16777619 angegeben;

  • 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574 sind die Little-Endian-Wörter des ASCII-Strings "expand 32-byte k" – die Konstanten des ChaCha20-Ausgangszustands gemäß RFC 8439 §2.3;

  • eine Struktur wie a += b; d ^= a; d = rotl(d, 16); c += d; b ^= c; b = rotl(b, 12); mit den vier Rotationsweiten 16/12/8/7 ist die Signatur einer ChaCha20-Quarter-Round; Salsa20 verwendet dagegen 7/9/13/18 – allein diese vier Zahlen unterscheiden beide;

  • eine Konstantentabelle mit 64 Einträgen, die mit 0xd76aa478 beginnt, ist die T-Tabelle von MD5 (RFC 1321 §3.4);

  • eine 256 Byte große Tabelle mit dem Anfang 0x63, 0x7c, 0x77, 0x7b ist die AES-S-Box (FIPS 197, Tabelle 4);

  • 0xEDB88320 ist das reflektierte CRC-32-Polynom, also der Wert aus der gzip-Spezifikation RFC 1952.

Entscheidend ist die Art der Frage. Auf „Was macht dieser Code?“ bekommst du Fließtext. Benötigt wird jedoch ein überprüfbares, strukturiertes Urteil. Der Prompt muss daher drei Dinge erzwingen: Kandidaten, Belege und Gegenbelege.

Below is a leaf function extracted from an obfuscated bundle, together with
every numeric constant it references.

<function>
{{function body}}
</function>

<constants>
{{constant list, with locations}}
</constants>

Answer in the following structure. Do not write prose.

1. Candidate algorithm families (at most 3, ranked by likelihood)
   For each: the name, and which class it belongs to
   (hash / stream cipher / block cipher / encoding / compression / checksum)

2. Supporting evidence
   Every piece of evidence must point to a specific constant value or a
   specific line above. "Structurally similar" and other unverifiable
   phrasing is not allowed.

3. Counter-evidence and deviation points
   If this were the standard implementation of that algorithm, what should
   appear here but doesn't? What appears that a standard implementation
   would never contain? Are these deviations a "variant," or "I got it wrong"?

4. The minimal test to decide this hypothesis
   Give 3 concrete inputs and, if the hypothesis holds, the shape of the
   output each should produce. Include boundary cases.

Abschnitt 3 macht diesen Prompt so wertvoll. Abweichungen von der Standardimplementierung zeigen genau die Stellen, an denen der Code verändert wurde: ein eigenes Alphabet, eine ausgetauschte Konstante, eine geänderte Rundenzahl. Nur mit diesen Abweichungen musst du dich beim Rewrite wirklich beschäftigen; den Rest übernimmst du aus der öffentlichen Implementierung. Wie du das Modell dazu bringst, wirklich jede Abweichung offenzulegen, behandelt ausführlich der Beitrag zum Algorithmus-Fingerprinting.

Abschnitt 4 übersetzt das Urteil des Modells direkt in die nächsten Tests und spart damit eine zusätzliche Schleife.

In dieser Stufe tauchen übrigens oft nicht standardisierte Kodierungen auf. Der Test ist rein mechanisch: Alphabet mit 65 Zeichen – 64 Zeichen plus ein Padding-Symbol –, Gruppierung in 6-Bit-Blöcken und Ausgabelänge ceil(n/3)*4: Das ist die Base64-Familie. Ein umsortiertes Alphabet weist auf eine eigene Tabelle hin. Ebenso ist LZW erkennbar, wenn ein Wörterbuch bei 256 beginnt und wächst, während die Breite der Ausgabecodes mit dem Wörterbuch ansteigt. All das lässt sich allein über Ein- und Ausgabelängen prüfen, ohne eine einzige Codezeile zu lesen.

Stufe 3: Aus der Hypothese wird ein Differentialtest

Das Modell hat dir eine Hypothese geliefert. Solange der Test nicht geschrieben ist, bleibt sie genau das: ein Satz.

Hier gibt es keine Abkürzung. Dies ist im gesamten Workflow das einzige Tor, das Halluzinationen stoppt – warum es das einzige ist, erklärt der Beitrag zum Differentialtest als Halluzinations-Gate. Behandle die Originalimplementierung als Blackbox und vergleiche deinen Rewrite Fall für Fall damit:

# Differential-test skeleton: original as black box, rewrite compared case by case
CASES = [
    b"",                      # empty input: exposes initial state and padding logic
    b"\x00",                  # single zero byte
    b"\xff",                  # single high byte: checks sign-bit handling
    b"a" * 63,                # one below the block boundary
    b"a" * 64,                # exactly one block
    b"a" * 65,                # one above: checks padding and carry
    bytes(range(256)),        # full byte coverage: checks alphabet mapping
]

for case in CASES:
    assert rewritten(case) == blackbox(case), case.hex()

Ein paar Faustregeln:

Übergänge an Grenzen liefern die meisten Informationen. Bei Blockalgorithmen zeigt sich die Padding-Logik besonders deutlich bei 64n und 64n±1. Schlägt im gesamten Lauf exakt ein Fall fehl, verrät dir seine Eingabelänge direkt, welche Schicht falsch liegt.

Führe dieselbe Eingabe zweimal aus. Unterscheiden sich die Ergebnisse, mischt die Implementierung eine Zufallszahl oder einen Zeitstempel ein. Dann musst du den Injektionspunkt finden und von außen überschreibbar machen; andernfalls ist ein Differentialvergleich überhaupt nicht möglich. Das ist beim Rewrite von Signaturlogik der häufigste Stolperstein: Nicht der Algorithmus ist falsch, sondern die Entropiequelle wurde noch nicht herausgelöst.

Schichte für Schicht freilegen, nicht das Gesamtergebnis vergleichen. Zuerst muss der innerste Hash übereinstimmen, danach die Kodierungsschicht und erst dann die Zusammensetzung. Wenn die gesamte Ausgabe abweicht, weißt du nicht, wo der Fehler liegt. Nach dem Auftrennen zeigt die erste fehlschlagende Schicht den Defekt.

Feste Testvektoren als Fixture einchecken. Eine Tabelle aus bekannten Eingaben und bekannten Ausgaben erlaubt dir nach einem Upstream-Update schnell zu entscheiden: „Habe ich es falsch implementiert, oder wurde es geändert?“ Mit der Zeit wird diese Fixture immer wertvoller.

Die Aufgabe des Modells in dieser Stufe besteht darin, Testfälle zu erzeugen und Unterschiede zu erklären – nicht darüber zu urteilen, was richtig ist. Über richtig und falsch entscheidet assert.

Stufe 4: Ausliefern – mit abgestuften Transportebenen

Wenn alle Hypothesen bestehen, wird daraus Code, der langfristig betrieben werden kann. Dabei gibt es eine klare Priorität: Je früher die Stufe, desto entschiedener solltest du dafür kämpfen.

  1. Ein nativer Rewrite in der Zielsprache. Vollständig vom ursprünglichen Runtime-Kontext gelöst und nur auf die Standardbibliothek angewiesen. Nur diese Variante braucht keinen zusätzlichen Prozess, keine weitere Abhängigkeit und lässt sich sauber in CI integrieren.

  2. Eine lokale JS-Engine mit einem minimalen Fragment. Manche Logik lässt sich kurzfristig nicht wirtschaftlich bereinigen. Dann behältst du einen kleinen Ausschnitt des ursprünglichen JS und führst ihn in einem lokalen Node/V8 aus. Beachte: Ein JS-Engine-Kontext ist nicht threadsicher. Wird ein kompilierter Kontext aus mehreren Threads aufgerufen, muss er gesperrt werden:

class Signer:
    def __init__(self, source: Path) -> None:
        self._context = execjs.get("Node").compile(source.read_text())
        self._lock = threading.Lock()  # V8 context is not thread-safe

    def call(self, fn: str, *args):
        with self._lock:
            return self._context.call(fn, *args)
  1. Eine passive Browser-Bridge. Für Zustand, den du nur aus einer echten Page-Runtime bekommst, bleibt der Browser vorerst die einzige Option. Das ist eine Übergangslösung: Kennzeichne sie klar in den Interface-Notizen und lass sie niemals zur Standardimplementierung werden.

Diese Abstufung gehört in die Projektkonventionen. Mit jeder niedrigeren Stufe wachsen Abhängigkeitsfläche, Fehlermöglichkeiten und Deployment-Kosten um eine Größenordnung: Stufe 1 ist eine reine Funktion, Stufe 3 ein externer Prozess, bei dem jemand eine Session am Leben halten muss. Standardmäßig für Stufe 1 zu kämpfen verhindert einen Großteil der langfristigen Kosten, die „Hauptsache, es läuft“ unbemerkt verursacht. Wie diese drei Stufen abzugrenzen sind und wie die passive Bridge nicht ausufert, beschreibt der Beitrag zur Purification Ladder.

Ein Richtwert: Ein obfuskiertes Signature-SDK lässt sich nach allen vier Stufen als eigenständige Implementierung mit weniger als 600 Zeilen umsetzen, die ausschließlich vom eingebauten crypto der Runtime abhängt. Dieses Kompressionsverhältnis entsteht nicht dadurch, dass das Modell die Originaldatei „verstanden“ hätte. Sobald die Algorithmusfamilie korrekt identifiziert ist, kann der überwiegende Teil des Codes direkt aus der öffentlichen Implementierung übernommen werden.

Welches Modell für welche Stufe?

Die vier Stufen verlangen völlig unterschiedliche Fähigkeiten. Ein einziges Modell für alles einzusetzen, verschwendet entweder Geld oder Präzision:

Stufe

Tatsächlich benötigte Fähigkeit

Meine Wahl

model id

Strukturelle Zuordnung nach dem Split

Langer Kontext, liest den Call-Graph eines ganzen Moduls in einem Durchgang

Kimi K3

kimi-k3

Identifikation der Algorithmusfamilie und Gegenbelege

Starkes Reasoning; findet Abweichungen und argumentiert gegen die eigene These

Claude Opus 5

claude-opus-5

Symbolnamen in großer Menge ersetzen, Kommentare ergänzen

Günstig, bewältigt Hunderte Aufrufe mit hoher Parallelität

Claude Sonnet 5

claude-sonnet-5

Zuordnung von Abweichungen bei fehlschlagenden Tests

Mittleres Reasoning; erklärt anhand konkreter Bytes

GPT-5.6 Sol

gpt-5.6-sol

Stufe 2 verdient besondere Aufmerksamkeit. Die Identifikation der Algorithmusfamilie ist der einzige Schritt, bei dem ein Modellwechsel das Ergebnis sichtbar verändert. Hier wird nämlich getestet, wie viele öffentliche Implementierungen ein Modell gesehen hat und ob es bereit ist, gegen sich selbst zu argumentieren. Bei derselben Leaf-Funktion liefert ein schwächeres Modell selbstsicher eine falsche Antwort; ein stärkeres streicht seinen eigenen Kandidaten im Abschnitt mit den Gegenbelegen wieder durch.

Verlass dich nicht auf mein Wort, sondern teste den Unterschied selbst. Das Protokoll:

  1. Wähle drei Leaf-Funktionen aus deinem eigenen obfuskierten Bundle, darunter mindestens eine, deren Ergebnis du bereits kennst, als Kontrolle.

  2. Gib denselben Input mit dem dreiteiligen Prompt aus Stufe 2 getrennt an claude-opus-5 und gpt-5.6-sol.

  3. Bewerte nur zwei Punkte: Hat das Modell die richtige Kandidatenfamilie getroffen? Und argumentiert der Abschnitt mit den Gegenbelegen tatsächlich gegen die eigene These, oder formuliert er den Kandidaten nur anders?

  4. Die Qualität der Gegenbelege ist dein Auswahlkriterium. Sie bestimmt direkt, wie viele nutzlose Tests du in Stufe 3 schreiben wirst.

Für Stufe 3 und 4 ist die Modellauswahl kaum entscheidend – alles, was läuft, reicht aus. Stufe 1 braucht lediglich ein Long-Context-Modell, damit du kein eigenes Chunked Retrieval bauen musst.

Nicht die Modellwahl bremst – sondern der Wechselaufwand

Vier Modelle über drei Anbieter bedeuten drei SDKs, drei Auth-Schemata und drei Fehlerformate. Den Client umzuschreiben, nur um etwas Geld zu sparen, lohnt sich kaum. Genau deshalb nutzen die meisten am Ende doch ein Modell für alles.

AIReiter nimmt diese Schicht heraus: ein Key, ein OpenAI-kompatibles Interface, alle vier Modelle dahinter. Zum Wechseln änderst du lediglich das Feld model im Request-Body.

# Algorithm-family ID: the reasoning tier
curl https://aireiter.com/api/v1/chat/completions \
  -H "Authorization: Bearer $AIREITER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-5",
    "messages": [{"role": "user", "content": "<Stage 2 prompt + leaf function + constants>"}]
  }'

# Bulk symbol renaming: change the model field, leave everything else
#   "model": "claude-sonnet-5"

Wenn du bereits das OpenAI SDK verwendest, setze base_url auf https://aireiter.com/api/v1 und ändere sonst nichts. Beim Anthropic SDK verwendest du mit demselben Key POST /api/v1/messages.

Preislich liegen Claude-Modelle 30 % unter Listenpreis, GPT-Modelle kosten die Hälfte. Für diesen Workflow ist das relevanter, als es zunächst klingt: Das massenhafte Umbenennen von Symbolen in Stufe 3 kommt leicht auf Hunderte Aufrufe, und der Long-Context-Durchlauf in Stufe 1 umfasst pro einzelner Eingabe einige Hunderttausend Tokens. Diese beiden Posten machen den Großteil der Kosten aus – und der Rabatt greift genau beim teuersten Teil.

  • API-Key holen

  • Ohne Registrierung ausprobieren – führe zunächst einige Prompts manuell aus, vergleiche die Abschnitte mit den Gegenbelegen beider Modelle und automatisiere erst, wenn du dich für eines entschieden hast.

Drei Aufgaben, die nicht an das Modell gehen

Erstens: Lass es nicht direkt die finale Implementierung schreiben. Bittest du das Modell um „den vollständigen Rewrite“, bekommst du Code, der vollständig wirkt und läuft, aber subtile Abweichungen enthält. Du kannst sie dann nicht lokalisieren, weil du den Code nicht selbst Schicht für Schicht verifiziert hast. Sinnvoll ist es, für jede Primitive eine Hypothese einzuholen, diese einzeln zu prüfen und die Implementierung selbst zusammenzusetzen. Das dauert länger, aber du weißt, warum jede Zeile so aussieht, wie sie aussieht.

Zweitens: Lass es nicht über die Korrektheit des Ergebnisses entscheiden. „Kannst du bestätigen, dass diese Implementierung richtig ist?“ führt in eine Sackgasse – Modelle neigen dazu, dir zuzustimmen. Die einzige Instanz für richtig und falsch ist der Differentialtest. Sagt das Modell „richtig“, aber der Test schlägt fehl? Der Test gewinnt. Sagt das Modell „falsch“, aber alle Tests bestehen? Auch dann gewinnt der Test.

Drittens: Überlass ihm nicht die Compliance-Entscheidung. Ob du das Ziel untersuchen darfst, ob du deine Erkenntnisse veröffentlichen darfst und wie du gewonnene Daten verwenden darfst, hängt von deiner Rechtsordnung, den Bedingungen des Ziels und deinem konkreten Zweck ab. Das Modell hat dafür keine faktische Grundlage; seine Antwort ahmt lediglich den Ton bekannter Disclaimer nach. Diese Entscheidung triffst du selbst – oder deine tatsächliche Rechtsberatung.

Fazit

Die Rolle des Modells in dieser Art von Arbeit ist klar umrissen: Es ist ein Mustererkenner, der öffentliche Implementierungen von Algorithmen gesehen hat und dir in Sekunden plausible Kandidatenhypothesen liefern kann. Es ist weder die Quelle der Antworten noch die Instanz zur Verifikation.

Das Grundgerüst des Workflows hat nichts damit zu tun, ob KI beteiligt ist: mechanische Zerlegung, Hypothese, Verifikation, Bereinigung. Diese vier Schritte waren lange vor Sprachmodellen der Prozess. Das Modell verkürzt nur die Hypothesenphase – von tagelanger Recherche in Referenzen auf Minuten. Die anderen drei Schritte kosten weiterhin exakt so viel wie immer.

Wenn die Modellaufrufe für diese vier Stufen erst einmal in Skripten festgehalten sind, läuft der gesamte Workflow schnell. Dann bleibt als Reibung vor allem der Modellwechsel – ein Infrastrukturproblem, das sich mit der Wahl deines Modells über ein einheitliches Interface löst.