Some time ago, we discussed the interesting malware, Hidden Bee. It is a Chinese miner, composed of userland components, as well as of a bootkit part. One of its unique features is a custom format used for some of the high-level elements (this format was featured in my recent presentation at SAS).

Recently, we stumbled upon a new sample of Hidden Bee. As it turns out, its authors decided to redesign some elements, as well as the used formats. In this post, we will take a deep dive in the functionality of the loader and the included changes.


831d0b55ebeb5e9ae19732e18041aa54 - shared by @James_inthe_box


The Hidden Bee runs silently—only increased processor usage can hint that the system is infected. More can be revealed with the help of tools inspecting the memory of running processes.

Initially, the main sample installs itself as a Windows service:

Hidden Bee service

However, once the next component is downloaded, this service is removed.

The payloads are injected into several applications, such as svchost.exe, msdtc.exe, dllhost.exe, and WmiPrvSE.exe.

If we scan the system with hollows_hunter, we can see that there are some implants in the memory of those processes:

Results of the scan by hollows_hunter

Indeed, if we take a look inside each process' memory (with the help of Process Hacker), we can see atypical executable elements:

Hidden Bee implants are placed in RWX memory

Some of them are lacking typical PE headers, for example:

Executable in one of the multiple customized formats used by Hidden Bee

But in addition to this, we can also find PE files implanted at unusual addresses in the memory:

Manually-loaded PE files in the memory of WmiPrvSE.exe

Those manually-loaded PE files turned out to be legitimate DLLs: OpenCL.dll and cudart32_80.dll (NVIDIA CUDA Runtime, Version 8.0.61 ). CUDA is a technology belonging to NVidia graphic cards. So, their presence suggests that the malware uses GPU in order to boost the mining performance.

When we inspect the memory even closer, we see within the executable implants there are some strings referencing LUA components:

Strings referencing LUA scripting language, used by Hidden Bee components

Those strings are typical for the Hidden Bee miner, and they were also mentioned in the previous reports.

We can also see the strings referencing the mining activity, i.e. the Cryptonight miner.

List of modules:


And we can even retrieve the miner configuration:


Hidden Bee has a long chain of components that finally lead to loading of the miner. On the way, we will find a variety of customized formats: data packages, executables, and filesystems. The filesystems are going to be mounted in the memory of the malware, and additional plugins and configuration are retrieved from there. Hidden Bee communicates with the C&C to retrieve the modules—on the way also using its own TCP-based protocol.

The first part of the loading process is described by the following diagram:

Each of the .spk packages contains a custom 'SPUTNIK' filesystem, containing more executable modules.

Starting the analysis from the loader, we will go down to the plugins, showing the inner workings of each element taking part in the loading process.

The loader

In contrast to most of the malware that we see nowadays, the loader is not packed by any crypter. According the header, it was compiled in November 2018.

While in the former edition the modules in the custom formats were dropped as separate files, this time the next stage is unpacked from inside the loader.

The loader is not obfuscated. Once we load it with typical tools (IDA), we can clearly see how the new format is loaded.

The loading function

Section .shared contains the configuration:

Encrypted configuration. The last 16 bytes after the data block is the key.

The configuration is decrypted with the help of XTEA algorithm.

Decrypting the configuration

The decrypted configuration must start from the magic WORD "pZ." It contains the C&C and the name under which the service will be installed:

Unscrambling the NE format

The NE format was seen before, in former editions of Hidden Bee. It is just a scrambled version of the PE. By observing which fields have been misplaced, we can easily reconstruct the original PE.

The loader, unpacking the next stage

NE is one of the two similar formats being used by this malware. Another similar one starts from a DWORD 0x0EF1FAB9 and is used to further load components. Both of them have an analogical structure that comes from slightly modified PE format:


WORD magic; // 'NE'
WORD pe_offset;
WORD machine_id;

The conversion back to PE format is trivial: It is enough to add the erased magic numbers: MZ and PE, and to move displaced fields to their original offsets. The tool that automatically does the mentioned conversion is available here.

In the previous edition, the parts of Hidden Bee with analogical functionality were delivered in a different, more complex proprietary format than the one currently being analyzed.

Second stage: a downloader (in NE format)

As a result of the conversion, we get the following PE: (fddfd292eaf33a490224ebe5371d3275). This module is a downloader of the next stage. The interesting thing is that the subsystem of this module is set as a driver, however, it is not loaded like a typical driver. The custom loader loads it into a user space just like any typical userland component.

The function at the module's Entry Point is called with three parameters. The first is a path of the main module. Then, the parameters from the configuration are passed. Example:

0012FE9C     00601A34  UNICODE "\"C:\Users\tester\Desktop\new_bee.exe\""
0012FEA0 00407104 UNICODE "NAPCUYWKOxywEgrO"
0012FEA4 00407004 UNICODE ""

Calling the Entry Point of the manually-loaded NE module

The execution of the module can take one of the two paths. The first one is meant for adding persistence: The module installs itself as a service.

If the module detects that it is already running as a service, it takes the second path. In such a case, it proceeds to download the next module from the server. The next module is packed as as Cabinet file.

The downloaded Cabinet file is being passed to the unpacking function

It is first unpacked into a file named "core.sdb". The unpacked module is in a customized format based on PE. This time, the format has a different signature: "NS" and it is different from the aforementioned "NE" format (detailed explanation will be given further).

It is loaded by the proprietary loader.

The loader enumerates all the executables in a directory: %Systemroot%\Microsoft.NET\ and selects the ones with the compatible bitness (in the analyzed case it was selecting 32bit PEs). Once it finds a suitable PE, it runs it and injects the payload there. The injected code is run by adding its entry point to APC queue.

Hidden Bee component injecting the next stage (core.sdb) into a new process

In case it failed to find the suitable executable in that directory, it performs the injection into dllhost.exe instead.

Unscrambling the NS format

As mentioned before, the core.sdb is in yet another format named NS. It is also a customized PE, however, this time the conversion is more complex than the NE format because more structures are customized. It looks like a next step in the evolution of the NE format.

Header of the NS format

We can see that the changes in the PE headers are bigger and more lossy—only minimalist information is maintained. Only few Data Directories are left. Also the sections table is shrunk: Each section header contains only four out of nine fields that are in the original PE.

Additionally, the format allows to pass a runtime argument from the loader to the payload via header: The pointer is saved into an additional field (marked "Filled Data" on the picture).

Not only is the PE header shrunk. Similar customization is done on the Import Table:

Customized part of the NS format's import table

This custom format can also be converted back to the PE format with the help of a dedicated converter, available here.

Third stage: core.sdb

The core.sdb module converted to PE format is available here: a17645fac4bcb5253f36a654ea369bf9.

The interesting part is that the external loader does not complete the full loading process of the module. It only copies the sections. But the rest of the module loading, such as applying relocations and filling imports, is done internally in the core.sdb.

The loading function is just at the Entry Point of core.sdb

The previous component was supposed to pass to the core.sdb an additional buffer with the data about the installed service: the name and the path. During its execution, core.sdb will look up this data. If found, it will delete the previously-created service, and the initial file that started the infection:

Removing the initial service

Getting rid of the previous persistence method suggests that it will be replaced by some different technique. Knowing previous editions of Hidden Bee, we can suspect that it may be a bootkit.

After locking the mutex in a format Global\SC_{%08lx-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}, the module proceeds to download another component. But before it goes to download, first, a few things are checked.

Checks done before download of the next module

First of all, there is a defensive check if any of the known debuggers or sniffers are running. If so, the function quits.

The blacklist

Also, there is a check if the application can open a file '\??\NPF-{0179AC45-C226-48e3-A205-DCA79C824051}'.

If all the checks pass, the function proceeds and queries the following URL, where GET variables contain the system fingerprint:


The hash (sz=) is an MD5 generated from VolumeIDs. Then follows the (os=) identifying version of the operating system, and the identifier of the architecture (ar=), where 0 means 32 bit, 1 means 64bit.

The content downloaded from this URL (starting from a magic DWORD 0xFEEDFACE - 79e851622ac5298198c04034465017c0) contains the encrypted package (in !rbx format), and a shellcode that will be used to unpack it. The shellcode is loaded to the current process and then executed.

The 'FEEDFACE' module contains the shellcode to be loaded

The shellcode's start function uses three parameters: pointer to the functions in the previous module (core sdb), pointer to the buffer with encrypted data, size of the encrypted data.

The loader calling the shellcode

Fourth stage: the shellcode decrypting !rbx

The beginning of the loaded shellcode:

The shellcode does not fill any imports by itself. Instead, it fully relies on the functions from core.sdb module, to which it passes the pointer. It makes use of the following function: malloc, mecpy, memfree, VirtualAlloc.

Example: calling malloc via core.sdb

Its role is to reveal another part. It comes in an encrypted package starting from a marker !rbx. The decryption function is called just at the beginning:

Calling the decrypting function (at Entry Point of the shellcode)

First, the function checks the !rbx marker and the checksum at the beginning of the encrypted buffer:

Checking marker and then checksum

It is decrypted with the help of RC4 algorithm, and then decompressed.

After decryption, the markers at the beginning of the buffer are checked. The expected format must start from predefined magic DWORDs: 0xCAFEBABE,0, 0xBABECAFE:

The !rbx package format

The !rbx is also a custom format with a consistent structure.

DWORD magic; // "!rbx"
DWORD checksum;
DWORD content_size;
BYTE rc4_key[16];
DWORD out_size;
BYTE content[];

The custom file system (BABECAFE)

The full decrypted content has a consistent structure, reminiscent of a file system. According to the previous reports, earlier versions of Hidden Bee used to adapt the ROMS filesystem, adding few modifications. They called their customized version "Mixed ROM FS". Now it seems that their customization process has progressed. Also the keywords suggesting ROMFS cannot be found. The headers starts from the markers in the form of three DWORDS: { 0xCAFEBABE, 0, 0xBABECAFE }.

The layout of BABECAFE FS:

We notice that it differs at many points from ROM FS, from which it evolved.

The structure contains the following files:

/installer/com_x86.dll (6177bc527853fe0f648efd17534dd28b)

The files /pkg/sputnik.spk and /pkg/plugins.spk are both compressed packages in a custom !rsi format.

Beginning of the !rsi package in the BABECAFE FS

Each of the spk packages contain another custom filesystem, identified by the keyword SPUTNIK (possibly the extension 'spk' is derived from the SPUTNIK format). They will be unpacked during the next steps of the execution.

Unpacked plugins.spk: 4c01273fb77550132c42737912cbeb36
Unpacked sputnik.spk: 36f3247dad5ec73ed49c83e04b120523.

Selecting and running modules

Some executables stored in the filesystem are in two version: 32 and 64 bit. Only the modules relevant to the current architecture are loaded. So, in the analyzed case, the loader chooses first: /bin/i386/preload (shellcode) and /bin/i386/coredll.bin (a module in NS custom format). The names are hardcoded in the loader within the loading shellcode:

Searching the modules in the custom file system

After the proper elements are fetched (preload and coredll.bin), they are copied together into a newly-allocated memory area. The coredll.bin is copied just after preload. Then, the preload module is called:

Redirecting execution to preload

The preload is position-independent, and its execution starts from the beginning of the page.

Entering 'preload'

The only role of this shellcode is to prepare and run the coredll.bin. So, it contains a custom loader for the NS format that allocates another memory area and loads the NS file there.

Fifth stage: preload and coredll

After loading coredll, preload redirects the execution there.

coredll at its Entry Point

The coredll patches a function inside the NTDLL— KiUserExceptionDispatcher—redirecting one of the inner calls to its own code:

A patch inside KiUserExceptionDispatcher

Depending on which process the coredll was injected into, it can take one of a few paths of execution.

If it is running for the first time, it will try to inject itself again—this time into rundll32. For the purpose of the injection, it will again unpack the original !rbx package and use its original copy stored there.

Entering the unpacking function
Inside the unpacking function: checking the magic "!rbx"

Then it will choose the modules depending on the bitness of the rundll32:

It selects the pair of modules (preload/coredll.bin) appropriate for the architecture, either from the directory amd64 or from i386:

If the injection failed, it makes another attempt, this time trying to inject into dllhost:

Each time it uses the same, hardcoded parameter (/Processid: {...}) that is passed to the created process:

The thread context of the target process is modified, and then the thread is resumed, running the injected content:

Now, when we look inside the memory of rundll32, we can find the preload and coredll being mapped:

Inside the injected part, the execution follows a similar path: preload loads the coredll and redirects to its Entry Point. But then, another path of execution is taken.

The parameter passed to the coredll decides which round of execution it is. On the second round, another injection is made: this time to dllhost.exe. And finally, it proceeds to the final round, when other modules are unpacked from the BABECAFE filesystem.

Parameter deciding which path to take

The unpacking function first searches by name for two more modules: sputnik.spk and plugins.spk. They are both in the mysterious !rsi format, which reminds us of !rbx, but has a slightly different structure.

Entering the function unpacking the first !rsi package:

The function unpacking the !rsi format is structured similarly to the !rbx unpacking. It also starts from checking the keyword:

Checking "!rsi" keyword

As mentioned before, both !rsi packages are used to store filesystems marked with the keyword "SPUTNIK". It is another custom filesystem invented by the Hidden Bee authors that contain additional modules.

The "SPUTNIK" keyword is checked after the module is unpacked

Unpacking the sputnik.spk resulted in getting the following SPUTNIK module: 455738924b7665e1c15e30cf73c9c377

It is worth noting that the unpacked filesystem has inside of it four executables: two pairs consisting of NS and PE, appropriately 32 and 64 bit. In the currently-analyzed setup, 32 bit versions are deployed.

The NS module will be the next to be run. First, it is loaded by the current executable, and then the execution is redirected there. Interestingly, both !rsi modules are passed as arguments to the entry point of the new module. (They will be used later to retrieve more components.)

Calling the newly-loaded NS executable

Sixth stage: mpsi.dll (unpacked from SPUTNIK)

Entering into the NS module starts another layer of the malware:

Entry Point of the NS module: the !rsi modules, perpended with their size, are passed

The analyzed module, converted to PE is available here: 537523ee256824e371d0bc16298b3849

This module is responsible for loading plugins. It will also create a named pipe through which it is will communicate with other modules. It sets up the commands that are going to be executed on demand.

This is how the beginning of the main function looks:

Like in previous cases, it starts from finishing to load itself (relocations and imports). Then, it patches the function in NTDLL. This is a common prolog in many HiddenBee modules.

Then, we have another phase of loading elements from the supplied packages. The path that will be taken depends on the runtime arguments. If the function received both !rsi packages, it will start by parsing one of them, retrieving loading submodules.

First, the SPUTNIK filesystem must be unpacked from the !rsi package:

After being unpacked, it is mounted. The filesystems are mounted internally in the memory: A global structure is filled with pointers to appropriate elements of the filesystem.

At the beginning, we can see the list of the plugins that are going to be loaded: cloudcompute.api, deepfreeze.api, and netscan.api. Those names are being appended to the root path of the modules.

Each module is fetched from the mounted filesystem and loaded:

Calling the function to load the plugin

Consecutive modules are loaded one after another in the same executable memory area. After the module is loaded, its header is erased. It is a common technique used in order to make dumping of the payload from the memory more difficult.

The cloudcompute.api is a plugin that will load the miner. More about the plugins will be explained in the next section of this post.

Reading its code, we find out that the SPUTNIK modules are filesystems that can be mounted and dismounted on demand. This module will be communicating with others with the help of a named pipe. It will be receiving commands and executing appropriate handlers.

Initialization of the commands' parser:

The function setting up the commands: For each name, a handler is registered. (This is probably the Lua dispatcher, first described here.)

When plugins are run, we can see some additional child processes created by the process running the coredll (in the analyzed case it is inside rundll32):

Also it triggers a firewall alert, which means the malware requested to open some ports (triggered by netscan.api plugin):

We can see that it started listening on one TCP and one UDP port:

The plugins

As mentioned in the previous section, the SPUTNIK filesystem contains three plugins: cloudcompute.api, deepfreeze.api, and netscan.api. If we convert them to PE, we can see that all of them import an unknown DLL: mpsi.dll. When we see the filled import table, we find out that the addresses have been filled redirecting to the functions from the previous NS module:

So we can conclude that the previous element is the mpsi.dll. Although its export table has been destroyed, the functions are fetched by the custom loader and filled in the import tables of the loaded plugins.

First the cloudcompute.api is run.

This plugin retrieves from the filesystem a file named "/etc/ccmain.json" that contains the list of URLs:

Those are addresses from which another set of modules is going to be downloaded:


It also retrieves another component from the SPUTNIK filesystem: /bin/i386/ccmain.bin. This time, it is an executable in NE format (version converted to PE is available here: 367db629beedf528adaa021bdb7c12de)

This is the component that is injected into msdtc.exe.

The HiddenBee module mapped into msdtc.exe

The configuration is also copied into the remote process and is used to retrieve an additional package from the C&C:

This is the plugin responsible for downloading and deploying the Mellifera Miner: core component of the Hidden Bee.

Next, the netscan.api loads module /bin/i386/kernelbase.bin (converted to PE: d7516ad354a3be2299759cd21e161a04)

The miner in APT-style

Hidden Bee is an eclectic malware. Although it is a commodity malware used for cryptocurrency mining, its design reminds us of espionage platforms used by APTs. Going through all its components is exhausting, but also fascinating. The authors are highly professional, not only as individuals but also as a team, because the design is consistent in all its complexity.

Appendix - helper tools for parsing and converting Hidden Bee custom formats

Articles about the previous version (in Chinese):

Our first encounter with the Hidden Bee: