Introduction

This quick blog post highlights some of the flaws found in the Zoom application when attempting to do integrity checking, these checks verify that the DLLs inside the folder are signed by Zoom and also that no 3rd party DLLs are loaded at runtime. We can trivially disable this DLL, by replacing it with our own or simply unloading it from the process.

This post highlights how we can bypass Zoom's anti-tampering detection, which aims to stop DLLs from being loaded or existing ones modified. The functionality is all implemented by Zoom themselves within a DLL named DllSafeCheck.dll.

I have also included a YARA rule at the end of this blog post, in case this technique is used by an advisory in the future.

I'll cover these flaws:

  • The DLL is not pinned, meaning an attacker from a 3rd party process could simply inject a remote thread, and call FreeLibrary after getting a handle to the DLL.
  • Ironically while all the DLLs checked by the anti-tampering DLL MUST have a valid Microsoft Authenticode signature to pass the checks, the anti-tampering DLLs integrity or signing status are NOT checked at all. This seems like an oversight from the Zoom developers considering all the checks that are currently performed in the DllSafeCheck DLL.

Zoom Client

Zoom is entirely programmed in C++ and makes heavy use of the Windows API. The executable and the DLLs that are used are installed to %APPDATA%\Zoom\bin and is completely writeable. All of the executables that are used are signed by Zoom themselves, as we can see below when extracting the certificate.

PS AppData\Roaming\Zoom\bin_00> Get-PfxCertificate util.dll

Thumbprint                                Subject
----------                                -------
0F9ADA46756C17EFFFD467D10654E2A766566CB3  CN="Zoom Video Communications, Inc.", O="Zoom Video Communications, Inc.", L=San Jose, S=California, C=US, SERIALNUMBER=4969967, OID.2.5.4.15=Pr...

Most of the functionality within Zoom resides within the DLLs. Below, we can see the DLLs which are included within the export table. Take notice of the DllSafeCheck.dll; the is the library we will be analysing.

Looking further at the use of DllSafeCheck.dll, we can see that it exports a function named HackCheck.

If we then cross-reference the calls to this function using our favourite disassembler, IDA in this instance, we can see that it is called at the entry point of the program within WinMain before any other operations are completed. Below, we can see the function prologue and the immediate call to HackCheck.

DllSafeCheck.dll

As abovementioned, the Zoom client will call the HackCheck function (which is the only export from the DLL, apart from DllMain), upon execution. Two events are created to detect the loading and unloading of the DLL, by resolving LdrUnregisterDllNotification and LdrRegisterDllNotification to register it.

To start, the export first starts by verifying that it is not running on an old version of Windows, using a mixture of VerSetConditionMask and VerifyVersionInfoW. After the Windows version has passed these checks, it will continue execution. It then will gather the Windows process token information through the usual means of getting a handle for the current process, then calling GetTokenInformation. This data is then saved for further use.

A path to Zoom's %APPDATA% folder is then constructed, and a log file named dllsafecheck.txt is created. A thread is then created, which waits for log events to be sent to it. Below, we can see the creation of this file.

We then get to the core functionality of the DLL, which is scanning the modules which are loaded in the current process and making sure that they're signed by Zoom. It will gather a list of the modules, and then check to see if they are signed, below, we can see the enumeration of the certificate chain to check against the hardcoded Zoom Video Communications, Inc. string.

if ( v10->csCertChain )
{
    do
    {
        v12 = WTHelperGetProvCertFromChain(v10, v11);
        if ( !v12 )
            break;
            
        v13 = v12->pCert;
        if ( v13 )
        {
            v15 = CertGetNameStringW(v13, 4u, 0, 0, 0, 0); // get alloc len
            v16 = v15;
            if ( v15 )
            {
                v14 = HeapAlloc(NULL, 0, 2 * v15);
                if ( v14 )
                {
                    v20 = 0;
                    do
                    {
                        v14[v20++] = 0;
                    }
                    while (v20 < (2 * v15));

                    if (!CertGetNameStringW(v13, 4u, 0, 0, (LPWSTR)v14, v16))
                    {
                        HeapFree(NULL, 0, v14);
                        v14 = 0;
                    }
                }

                v10 = v26;
            }
            else
            {
                v14 = 0;
            }
        }
        else
        {
            v14 = 0;
        }

    if ( !v25 )
        v25 = L"Zoom Video Communications, Inc.";

If the executable is not signed by Zoom, it will prompt the user to ask if it wants it to be run in the process.

Trivial to unload from process

Ironically while all the DLLs checked by the anti-tampering DLL must have a valid Microsoft Authenticode signature to pass the checks, the anti-tampering DLLs integrity or signing status are not checked at all. This seems like an oversight from the Zoom developers considering all the checks that are currently performed in the DllSafeCheck DLL.

An immediate issue is that this DLL can be trivially unloaded, rendering the anti-tampering mechanism null and void. The DLL is not pinned, meaning an attacker from a 3rd party process could simply inject a remote thread, and call FreeLibrary after getting a handle to the DLL.

One possible fix for this would be to perform GetModuleHandleExA, and passing in the GET_MODULE_HANDLE_EX_FLAG_PIN flag. This ensures that the module stays loaded within the process until it terminates, rendering FreeLibrary calls useless.

HMODULE hSafeCheck = NULL;
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_PIN, "DllSafeCheck.dll", &hSafeCheck))
{
    // Loaded module successfully
}

We can unload it using the traditional, and well-documented method of:

  1. HANDLE of Zoom process using OpenProcess
  2. Enumerate the loaded modules in the process, using EnumProcessModules, and find a handle to DllSafeCheck.dll
  3. Resolve the address of "FreeLibrary" using GetProcAddress
  4. Create a thread in the process using CreateRemoteThread, with the starting routine as the FreeLibrary address, and the parameter as the handle to DllSafeCheck.
  5. The anti-tampering DLL is now unloaded
  6. We can now inject any DLL we want

I've created simple POC (basic CreateRemoteThread DLL injection, nothing fancy) for unloading the anti-tampering DLL and injecting our own. You can contact me at [email protected] if you want to see it.

Anti-tampering DLL can be replaced on disk

When loading the DLL, Zoom does not check the signature of the integrity of the file. I'm not sure why this is not checked at all, considering all of the checks which are done in the DllSafecheck DLL regarding executable signature vertification. This remains a mystery. A threat actor could leverage this to enable their unsigned, non-Zoom DLL to be loaded into the context of a signed executable as a host for their malicious code.

The folder which Zoom resides in is writeable, which also contributes to this attack.

A simple DLL named DllSafeCheck.dll can be compiled implementing the HackCheck export. For clarity, the malicious DLL which is used is not signed. We can see the result of querying the executable signature below.

PS AppData\Roaming\Zoom\bin_00> Get-AuthenticodeSignature DllSafeCheck.dll

SignerCertificate                         Status                                 Path
-----------------                         ------                                 ----
                                          NotSigned                              DllSafeCheck.dll

The following code was used for this PoC:

VOID __declspec(dllexport) CheckHack()
{
	MessageBox(NULL, L"LloydLabs", L"Oops!", MB_APPLMODAL);
}

Here, we can see the the alert when loading Zoom.

How could a threat actor realistically exploit this?

A malicious DLL could be bundled with Zoom, and sent to a victim - this would result in the payload (e.g. Cobalt Strike), being executed under the context of the Zoom process. A threat actor could also abuse these issues to persist both across reboot and in memory on a target system, this is a much cleaner approach compared to the alternatives of registering some startup event.

YARA rule

import "pe"
rule Zoom_Plant {
    meta:
        date = "2020-04-03"
        author = "LloydLabs"
        url = "https://blog.syscall.party"
	
    condition:
        pe.characteristics & pe.DLL and pe.exports("HackCheck") and pe.number_of_exports == 1 and (pe.issuer contains "Zoom Video Communications, Inc.")
}

Conclusion

Thank you for reading this brief blog, if you wish to contact me I can be emailed at: [email protected] - I'm a 3rd year undergraduate student, and open to opportunities and collaboration. Cheers!