Chances are you are reading this blog post using your web browser. Chances also are your web browser has various extensions that provide additional functionality. We usually trust that the extensions installed from official browser stores are safe. But that is not always the case as we recently found.
This blog post brings more technical details on CacheFlow: a threat that we first reported about in December 2020. We described a huge campaign composed of dozens of malicious Chrome and Edge browser extensions with more than three million installations in total. We alerted both Google and Microsoft about the presence of these malicious extensions on their respective extension stores and are happy to announce that both companies have since taken all of them down as of December 18, 2020.
CacheFlow was notable in particular for the way that the malicious extensions would try to hide their command and control traffic in a covert channel using the Cache-Control
HTTP header of their analytics requests. We believe this is a new technique. In addition, it appears to us that the Google Analytics-style traffic was added not just to hide the malicious commands, but that the extension authors were also interested in the analytics requests themselves. We believe they tried to solve two problems, command and control and getting analytics information, with one solution.
We found that CacheFlow would carry out its attack in the following sequence:
Based on our telemetry, the top three countries where Avast users downloaded and installed the CacheFlow extensions were Brazil, Ukraine, and France.
We initially learned about this campaign by reading a Czech blog post by Edvard Rejthar from CZ.NIC. He discovered that the Chrome extension “Video Downloader for FaceBook™” (ID pfnmibjifkhhblmdmaocfohebdpfppkf
) was stealthily loading an obfuscated piece of JavaScript that had nothing to do with the extension’s advertised functionality. Continuing from his findings, we managed to find many other extensions that were doing the same thing. These other extensions offered various legitimate functionality, with many of them being video downloaders for popular social media platforms. After reverse engineering the obfuscated JavaScript, we found that the main malicious payload delivered by these extensions was responsible for malicious browser redirects. Not only that, but the cybercriminals were also collecting quite a lot of data about the users of the malicious extensions, such as all of their search engine queries or information about everything they clicked on.
The extensions exhibited quite a high level of sneakiness by employing many tricks to lower the chances of detection. First of all, they avoided infecting users who were likely to be web developers. They determined this either through the extensions the user had installed or by checking if the user accessed locally-hosted websites. Furthermore, the extensions delayed their malicious activity for at least three days after installation to avoid raising red flags early on. When the malware detected that the browser developer tools were opened, it would immediately deactivate its malicious functionality. CacheFlow also checked every Google search query and if the user was googling for one of the malware’s command and control (C&C) domains, it reported this to its C&C server and could deactivate itself as well.
According to user reviews on the Chrome Web Store, it seems that CacheFlow was active since at least October 2017. All of the stealthiness described above could explain why it stayed undetected for so long.
The covert channel
First, we’ll show the hidden backdoor that the extensions used to download and execute arbitrary JavaScript. Specifically, we’ll describe the backdoor from the Chrome extension “Downloader for Instagram” v5.7.3 (ID olkpikmlhoaojbbmmpejnimiglejmboe
), but this analysis applies to the other extensions as well, since the malicious code hidden in them is very similar in functionality.
It is generally a good idea to start the analysis of unknown Chrome extensions from the manifest.json
file. The manifest of “Downloader for Instagram” gives us some interesting pieces of information.
First of all, the content_security_policy
is defined in such a way that it is possible to use the infamous eval
function to load additional JavaScript. However, looking for the string eval
in the extension’s source code did not yield any interesting results. As we’ll show later, the extension does use the eval
function quite a lot, but it hides its usage, so it is not immediately apparent.
Secondly, the extension asks for quite a lot of permissions and it is not immediately clear why these permissions would be needed to download videos from Instagram. Especially interesting is the management
permission, which allows the extension to control other extensions. The combination of the webRequest
and the <all_urls>
permissions is also interesting. Together, these two permissions make it possible for the extension to intercept pretty much any web request coming from the browser.
Finally, the manifest defines two background scripts: js/jquery.js
and js/background.js
. These scripts are persistent, which means that they will keep running unless the extension gets disabled.
One of these background scripts, background.js
, is where the suspicious webRequest
API is used. This script accesses the HTTP response headers of all intercepted web requests and stores their values in localStorage.
The content of localStorage is then read by the other persistent malicious background script: jquery.js
. While this script appears at first glance to be the legitimate jQuery library, some additional functions were inserted into it. One of those additional functions is misleadingly named parseRelative
, while all it does is return the window.localStorage
object.
Another inserted and misleadingly named function is initAjax
.
This function is particularly interested in the content of localStorage['cache-control']
, which should at this point be set to the value of the last received Cache-Control
HTTP response header. The function splits the content of this header with a comma and attempts to decrypt each part using a custom function named strrevsstr
, before finally parsing it out as a JSON string.
The obvious question now is why would the extension expect to intercept requests that contain an encrypted JSON string in the Cache-Control
response header?
The answer is that the threat actors are using the content of the Cache-Control
header as a covert channel to send hidden commands to the malicious extension.
As a part of the malicious extension’s regular functionality, analytics requests about some events are sent to https://stats.script-protection[.]com/__utm.gif
. These are standard analytics requests that bear resemblance to Google Analytics. The catch is, that the server used by this extension might respond to the analytics requests with a specially formed Cache-Control
header, which the client will decrypt, parse out and execute.
To see what the commands could look like, we simulated the extension and sent a fake analytics HTTP request to https://stats.script-protection[.]com/__utm.gif
. After a couple of attempts, we received a specially crafted Cache-Control
header.
Note that the response will contain the encoded command only when some conditions are met. First of all, the GET parameter it
has to be set at least three days into the past. Since this parameter contains the time when the extension was installed, this effectively ensures that the extension will not exhibit any malicious behavior during the first three days. There is also a check based on the IP address, since we repeatedly did not receive any commands from one source IP address, even though we did receive a command for the same GET request from another IP address. As the logic behind these checks is safely hidden on the C&C server, there might be additional checks that we are not aware of.
When the content of the received Cache-Control
header is decoded using the custom strrevsstr
function as outlined above, we get the command in the following JSON. As was seen in the initAjax
function, all of the attributes from this JSON get stored in the global window
object.
Upon receiving such a command, the extension downloads the second stage from command['uu']
in a function named siblingAfter
, which is also hidden inside jquery.js
. The dollar sign from command['jj']
here represents the jQuery
object, so the function uses the jQuery.get
function to download the next stage from command['uu']
and to store it in localStorage.dataDefault
.
Finally, there is yet another function hidden in jquery.js
, which executes the downloaded JavaScript using the eval
function from command['ee']
.
The downloaded JavaScript is an obfuscated intermediary downloader. Its purpose is to download the third-stage payload from ulkon.johnoil[.]com
using an XHR request. Unfortunately, because the server will only send the next stage under certain conditions, getting a response containing the third stage can be quite tricky. If it gets successfully downloaded, it is encrypted and stored persistently in localStorage. It then gets executed whenever a tab is updated using the chrome.tabs.onUpdated
listener.
The payload
The payload starts out by testing if it can make use of eval
and localStorage
. If either of those two is not working properly, CacheFlow would not be able to perform most of its malicious functionality.
Additionally, the payload periodically checks if developer tools are opened. If they are, it deactivates itself in an attempt to avoid detection. The check for developer tools is also performed whenever the current window gets resized, which might be because the user just opened developer tools.
As was already mentioned, the malware authors have gone to extreme lengths to make sure that the hidden malicious payloads do not get discovered. We believe they were not satisfied with the previous check and decided to further profile the victim in order to avoid infecting users who seemed more tech-savvy. One of the ways they did this was by enumerating the other extensions installed by the victim and checking them against a hardcoded list of extension IDs. Each extension on the list was assigned a score and if the sum of scores of installed extensions exceeded a certain threshold, the list of extensions would be sent to the C&C server, which could then command the malicious payload to deactivate. Examples of the extensions on the list were “Chrome extension source viewer”, “Link Redirect Trace”, or “JWT Debugger”. We believe this “weighting” system helped to better differentiate actual developer systems which would have several of these extensions and a higher score from casual users who would have fewer extensions and thus a lower score.
Another way to profile the potential victim was to check the URLs they were browsing. Whenever the victim navigated to a URL identified by an IP address from one of the private IPv4 ranges or to a URL with a TLD .dev
, .local
, or .localhost
, the malware would send the visited URL to its C&C server. The malware also checked all Google (and only Google) queries against a regular expression that matched its C&C domains and internal identifiers. This way, it would know that somebody was taking a deeper look into the extension and could take actions to hide itself. Interestingly, the domains were not fully specified in the regular expressions, with some characters being represented as the dot special character. We assume that this was an attempt to make it harder to create a domain blocklist based on the regular expression.
At this point, the malware also attempted to gather information about the victim. This information included birth dates, email addresses, geolocation, and device activity. For instance, the birth dates were retrieved from the personal information entered into the victim’s Google account. Once again, the attackers focused only on Google: we did not see any similar attempts to get Microsoft account information. To retrieve the birthday, CacheFlow made an XHR request to https://myaccount.google.com/birthday and parsed out the birth date from the response.
Note that while it may seem that making such a cross-origin request would not be allowed by the browser, this is all perfectly possible under the extension security model since the extension has the <all_urls>
permission. This permission gives the extension access to all hosts, so it can make arbitrary cross-site requests.
In order to make it harder for Google to realize that CacheFlow was abusing its services to gather personal information, it also registered a special chrome.webRequest.onBeforeSendHeaders
listener. This listener removes the referer
request header from all the relevant XHR requests, so Google would not easily know who is actually making the request.
Finally, to perform its main malicious functionality, the payload injects another piece of JavaScript into each tab using the chrome.tabs.executeScript
function.
The injected script
The injected script implements two pieces of functionality. The first one is about hijacking clicks. When the victim clicks on a link, the extension sends information about the click to orgun.johnoil[.]com
and might receive back a command to redirect the victim to a different URL. The second functionality concerns search engine results. When the victim is on a search engine page, the extension gathers the search query and results. This information is then sent to the C&C server, which might respond with a command to redirect some of the search results.
Link hijacking
The link hijacking is implemented by registering an onclick
listener over the whole document
.
The listener is then only interested in main button presses (usually “left clicks”) and clicks on elements with the tag name a
or area
. If the click meets all the criteria, an XHR request to https://orgun.johnoil[.]com/link/
is sent. This request contains one GET parameter, a
, which holds concatenated information about the click and is encrypted using the custom strsstr
function. This information includes the current location, the target URL, various identifiers, and more.
We simulated a fake request about a click to a link leading to https://facebook[.]com
and received the following response:
ayiudvh3jk6lNjkzMTQ0eAgYGAQRFhNYTVxbE04IBlFDFgEEHBtYQV0HThdXEwJRBANSUVBEDghQCgNOWUMXAhskaiohB3Z4YQlvSU8oaygLZkhBYCJlAW9Rf18Ecyg1TmZdFEQZABACXHxaInY0MklVQlUeTAozcyUSOwABdW9oAXUXGjswNQpgTkkzZSZFMxMJanwqQj1NDixsflIuWAl6kj3hvduiya
Upon receiving such a response, the malware first makes sure that it starts with a certain randomly generated string and ends with the same string, but in reverse. This string (ayiudvh3jk6l
highlighted in the example above) was generated by the extension and was also included in the a
parameter that was sent in the XHR request. The extension then takes the middle portion of the response and decrypts it using the strrevsstr
function (which is the inversion of strsstr
). This yields the following string:
ayiudvh3jk6lhttps://go.lnkam[.]com/link/r?u=https%3A%2F%2Fwww.facebook[.]com%2F&campaign_id=b7YMMAqMdAL7wyzNe5m3wz&source=uvm3rdsqc9zo69l6kj3hvduiya
Once again, the malware checks the beginning and the end of the decrypted string for the same randomly generated string as used before and extracts the middle portion of it. If it begins with the substring http
, the malware proceeds to perform the link hijack. It does this by temporarily changing the href
attribute of the element that the user clicked on and executing the click
method on it to simulate a mouse click. As a fallback mechanism, the malware just simply sets window.location['href']
to the link hijack URL.
Modification of search results
The second functionality is performed only if the victim is currently on a Google, Bing, or Yahoo search page. If they are, the malware first gathers the search query string and the results. The way this is performed varies based on the search engine. For Google, the search query string is found as the value of the first element named q
. If that somehow fails, the malware alternatively tries to get the search query from the q
GET parameter.
The search results on Google are obtained by searching for elements with the class name rc
and then iterating over their child a
elements.
Once gathered, the search query and results are sent in an XHR request to servscrpt[.]de
. A salted MD5 checksum of the results is included in the request as well, we believe in an attempt to discover fake requests (but this check can obviously be trivially bypassed by recomputing the MD5 checksum). The XHR response contains a list of domains whose links the malware should hijack. The hijack itself is performed by registering an onmousedown
listener on the a
element. Once fired, the listener calls the preventDefault
function on the event and then window.open
to redirect the user to the malicious URL.
Interestingly, CacheFlow also modifies some of the hijacked search results by adding a clickable logo to them. We believe this is done in order to make those results stand out and thus increase the chances of the victim clicking on them. However, the position of the logo is not aligned well, which makes the search result look odd and suspicious, since Google, Microsoft, or Yahoo would probably put a bit more effort into formatting it.
The logo is added by creating a brand new div
element which holds an img
element. Once created and formatted, this element is inserted into the DOM, so that it appears to the left of the original search result. The logo is obtained from the serviceimg[.]de
domain, which serves a unique 90×45 logo per domain.
Conclusion
In this blog post, we provided technical details about CacheFlow: a huge network of malicious browser extensions that infected millions of users worldwide. We described how the malicious extensions were hijacking their victims’ clicks and modifying their search engine results. Since CacheFlow was well capable of hiding itself, we covered in detail the techniques it was using to hide the fact that it was executing malicious code in the background. We believe that understanding how these techniques work will help other malware researchers in discovering and analyzing similar threats in the future.
Indicators of Compromise
The full list of IoCs is available at https://github.com/avast/ioc/tree/master/CacheFlow.
Name | Hash |
manifest.json | 2bc86c14609928183bf3d94e1b6f082a07e6ce0e80b1dffc48d3356b6942c051 |
background.js | bdd2ec1f2e5cc0ba3980f7f96cba5bf795a6e012120db9cab0d8981af3fa7f20 |
jquery.js | 3dad00763b7f97c27d481242bafa510a89fed19ba60c9487a65fa4e86dcf970d |
Intermediary downloader | 4e236104f6e155cfe65179e7646bdb825078a9fea39463498c5b8cd99d409e7a |
Payload | ebf6ca39894fc7d0e634bd6747131efbbd0d736e65e68dcc940e3294d3c93df4 |
Injected script | 0f99ec8031d482d3cefa979fbd61416558e03a5079f43c2d31aaf4ea20ce28a0 |
Chrome Extension Name | Extension ID |
Direct Message for Instagram | mdpgppkombninhkfhaggckdmencplhmg |
DM for Instagram | fgaapohcdolaiaijobecfleiohcfhdfb |
Invisible mode for Instagram Direct Message | iibnodnghffmdcebaglfgnfkgemcbchf |
Downloader for Instagram | olkpikmlhoaojbbmmpejnimiglejmboe |
App Phone for Instagram | bhfoemlllidnfefgkeaeocnageepbael |
Stories for Instagram | nilbfjdbacfdodpbdondbbkmoigehodg |
Universal Video Downloader | eikbfklcjampfnmclhjeifbmfkpkfpbn |
Video Downloader for FaceBook™ | pfnmibjifkhhblmdmaocfohebdpfppkf |
Vimeo™ Video Downloader | cgpbghdbejagejmciefmekcklikpoeel |
Zoomer for Instagram and FaceBook | klejifgmmnkgejbhgmpgajemhlnijlib |
VK UnBlock. Works fast. | ceoldlgkhdbnnmojajjgfapagjccblib |
Odnoklassniki UnBlock. Works quickly. | mnafnfdagggclnaggnjajohakfbppaih |
Upload photo to Instagram™ | oknpgmaeedlbdichgaghebhiknmghffa |
Spotify Music Downloader | pcaaejaejpolbbchlmbdjfiggojefllp |
The New York Times News | lmcajpniijhhhpcnhleibgiehhicjlnk |
FORBES | lgjogljbnbfjcaigalbhiagkboajmkkj |
Скачать фото и видео из Instagram | akdbogfpgohikflhccclloneidjkogog |
Edge Extension Name | Extension ID |
Direct Message for Instagram™ | lnocaphbapmclliacmbbggnfnjojbjgf |
Instagram Download Video & Image | bhcpgfhiobcpokfpdahijhnipenkplji |
App Phone for Instagram | dambkkeeabmnhelekdekfmabnckghdih |
Universal Video Downloader | dgjmdlifhbljhmgkjbojeejmeeplapej |
Video Downloader for FaceBook™ | emechknidkghbpiodihlodkhnljplpjm |
Vimeo™ Video Downloader | hajlccgbgjdcjaommiffaphjdndpjcio |
Volume Controller | dljdbmkffjijepjnkonndbdiakjfdcic |
Stories for Instagram | cjmpdadldchjmljhkigoeejegmghaabp |
Upload photo to Instagram™ | jlkfgpiicpnlbmmmpkpdjkkdolgomhmb |
Pretty Kitty, The Cat Pet | njdkgjbjmdceaibhngelkkloceihelle |
Video Downloader for YouTube | phoehhafolaebdpimmbmlofmeibdkckp |
SoundCloud Music Downloader | pccfaccnfkjmdlkollpiaialndbieibj |
Instagram App with Direct Message DM | fbhbpnjkpcdmcgcpfilooccjgemlkinn |
Downloader for Instagram | aemaecahdckfllfldhgimjhdgiaahean |
URL |
abuse-extensions[.]com |
ampliacion[.]xyz |
a.xfreeservice[.]com |
b.xfreeservice[.]com |
c.xfreeservice[.]com |
browser-stat[.]com |
check-stat[.]com |
check4.scamprotection[.]net |
connecting-to-the[.]net |
cornewus[.]com |
downloader-ig[.]com |
exstats[.]com |
ext-feedback[.]com |
extstatistics[.]com |
figures-analysis[.]com |
huffily.mydiaconal[.]com |
jastats[.]com |
jokopinter[.]com |
limbo-urg[.]com |
mydiaconal[.]com |
notification-stat[.]com |
orgun.johnoil[.]com |
outstole.my-sins[.]com |
peta-line[.]com |
root.s-i-z[.]com |
s3.amazonaws[.]com/directcdn/j6dle93f17c30.js |
s3.amazonaws[.]com/wwwjs/ga9anf7c53390.js |
s3.amazonaws[.]com/wwwjs/hc8e0ccd7266c.js |
s3.amazonaws[.]com/protectscript/instagram-downloader.js |
safenewtab[.]com |
script-protection[.]com |
server-status[.]xyz |
serviceimg[.]de |
servscrpt[.]de |
stats.script-protection[.]com |
statslight[.]com |
ulkon.johnoil[.]com |
user-experience[.]space |
user-feedbacks[.]com |
user.ampliacion[.]xyz |
xf.gdprvalidate[.]de/partner/8otb939m/index.php |