Blog by: Alex Oudenaarden & Patrick van Looy

Intro

After some time of respite in the world of ransomware, different players in the field started rebranding their operations. Recently, one of the most active groups returned. The notorious LockBit group introduced a new strain of their ransomware, LockBit 3.0. More specifically, the threat actor dubbed its latest release LockBit Black, enriching it with new extortion tactics and introducing an option to pay in Zcash, in addition to the already offered Bitcoin and Monero crypto payment alternatives.

Additionally, as a first in the ransomware world, LockBit launched a bug bounty program with rewards ranging from a thousand to a million dollars. Confident of its privacy, the affiliate manager, known as LockBitSupp, offers one million dollars for anyone who can reveal his real identity.

Within the infosec community, several analyses showed that LockBit 3.0 seems heavily based on the BlackMatter codebase, hence the name LockBit Black [1, 2]. Following our previous whitepaper on LockBit, Northwave obtained a sample of the latest strain and performed an in-depth analysis. Additionally, Northwave tried to recover the config and extracted information that could help during incident response engagements and support the root cause analysis. This blog presents the method for extracting the config from a LockBit Black sample and describes the included fields and functions.

Furthermore, Northwave published an open-source IDAPython script for analysing the config and scripts to perform the necessary string decryption and API call deobfuscation. Finally, Northwave will add a command-line tool for dumping the content of the config. All of this is available on Northwave’s GitHub.

Config decryption

Unlike BlackCat, which included its config in plaintext up until recently, the LockBit 3.0 authors exert extreme effort to hide their configuration details.

The malware starts by unpacking itself in place (a good hint is given that the .text segment is Read-Write-eXecute) and then initialises its custom Import Address Table for calling API functions.

After these two initial steps, the malware begins executing, starting with a call to a big __setup function. This entire function mainly concerns running functionality to configure and prepare the environment for successful ransomware execution. Many of these steps depend on values from the config; hence, we see the config decryption function being called very early in the setup stage.

Again, LockBit takes extra measures here to protect its most important data. Rather than just decrypting once, it takes numerous decryption, decompression and decoding steps to extract the entire config. From our analysis, we observed that LockBit splits its config decryption into three main parts:

  • Config blob decryption
  • Variable block processing
  • String block processing

Blob decryption

Before one can access any of the config’s individual sections, full decryption must occur. In the sample, we can see that it takes a pointer to the start of the config and takes two steps (custom decrypt, aplib decompress) to decrypt it fully.

The `load_and_decrypt_config_value` function contains logic for the config’s decryption, its values and the decryption of values in the data segment. This function works as follows:

1. Take the pointer to the start of the encrypted block, read out the 4 bytes preceding it to get the size, and allocate it.
2. Copy the encrypted blob into the newly made allocation using the given size.
3. Pass the allocation and its size to the decryption function.

4. Once again, take the pointer to the start of the config section, and read out the first two dwords. The custom_rand function uses these two dwords as a “seed”.
5. Call the custom_rand function with the two dwords and a pointer to the config section as arguments. This function returns 2 “random” dwords used as a key in step 6.
6. Use bytes from the two random dwords and use them to decrypt the encrypted blob (8 bytes at a time).

NOTE: The custom_rand function is generated dynamically and needs to be either decrypted separately or viewed inside a debugger.

As the decryption uses dwords generated from a given seed that is constant for a given sample, it is unnecessary to decrypt each blob 8 bytes at a time. Instead, with careful analysis, it is possible to convert the decryption scheme into a “keystream” generator that can generate a potentially infinitely long stream of key bytes based on the seed for a given sample.

We have taken some time to write this out in c for clarity. Below you will find the decryption scheme transformed into a key stream generator that one can use to decrypt any encrypted blob in the sample.

/*
    These variables might need updating
    for newer samples. You can find these
    inside of the function that implements
    the logic inside of "custom_rand()"
*/
uint32_t VAR1 = 0x5851f42d;
uint32_t VAR2 = 0x4C957f2d;
uint64_t VAR3 = 0x14057b7EF767814F;
uint64_t
some_algo(uint32_t v1, uint32_t v2 ,uint32_t k1 ,uint32_t k2)
{
    uint32_t tmp;
    uint64_t tmp2;
    if( !(v1 | k2) )
        return k1 * v2;
tmp =(k2*v2)+(k1*v1);
tmp2 = ((uint64_t)k1 * v2);
return ((tmp2 & 0xFFFFFFFF ) | (((tmp2 >> 32) + tmp) << 32));
}
uint64_t
custom_rand(uint32_t *state, uint32_t *consts)
{
    uint64_t v;
    v = some_algo(VAR1, VAR2, state[0], state[1]);
    v += VAR3;
    state[0] = (v & 0xFFFFFFFF);
    state[1] = (v >> 32);
return some_algo(state[1], state[0], consts[0], consts[1]);
C
 }
  /*
      Rather than decrypting the config the way the
      binary does, we can create a long stream of key
      bytes ahead of time.
*/
  uint8_t *gen_keystream(uint8_t *start, size_t *bytes_generated)
  {
      int     i;
      uint64_t    x;
      uint8_t     *ret;
      uint32_t    *tmp_ptr;
      size_t  len;
      tmp_ptr = (uint32_t *)start;
      // Get first 2 dwords from config
      uint32_t consts[2]  = {tmp_ptr[0], tmp_ptr[1]};
      uint32_t state[2]   = {tmp_ptr[0], tmp_ptr[1]};
      len = tmp_ptr[2] + 8;
      ret = malloc(len);
      // Perform the byte mangling according to spec
      for (i = 0; i < len; i += 8)
      {
          x = custom_rand(state, consts);
          ret[i + 0] = ((x & 0x00000000000000FF) >> 0 );
          ret[i + 1] = ((x & 0x0000FF0000000000) >> 40);
          ret[i + 2] = ((x & 0x000000000000FF00) >> 8 );
          ret[i + 3] = ((x & 0x000000FF00000000) >> 32);
          ret[i + 4] = ((x & 0x0000000000FF0000) >> 16);
          ret[i + 5] = ((x & 0xFF00000000000000) >> 56);
          ret[i + 6] = ((x & 0x00000000FF000000) >> 24);
          ret[i + 7] = ((x & 0x00FF000000000000) >> 48);
}
      *bytes_generated = len;
return ret; }

Variable block

After decrypting and aplib decompressing the entire config blob, the parameters inside of the variable block become visible. In our sample, this block was 0xB8 bytes long and contained three values. More on the meaning of the values later.

String block

The string block is slightly more obtuse as opposed to the variable block. The string block is formatted using a header with an array of offsets from the base of the string block to base64 encoded strings. There is no clear way of finding the size of this array except for analysing the config decryption function. In the case of our sample, it supports up to ten entries. However, there are not guaranteed to be ten because individual offset entries can be 0, in which case the string is ignored.

The first eight entries just require a base64 decode. The last two entries need additional decryption.

For a complete overview of the decryption steps described above, please take a look at our example IDApython script for dealing with LockBit’s config.

Config values

After decrypting the config, the following values will remain.

Variable block

offset value
+00h RSA-1024 key; First 8 bytes also used as an ID in some cases
+80h Company ID
+90h Unused
+A0h 24 byte array of boolean values. This is used to enable or disable functionality (see Boolean Config below for more details)
+B0h Start of base64 block offset array
+D8 Start of base64 block

Base64 block

entry # value
01 Folder exclusion hash list; Each entry is a dword hash of a folder name such as {“Windows”, “Program Files”, “$recycle.bin”} that will be excluded by the encryption loop (For more info on the hash algorithm, see the github
02 File exclusion hash list; Each entry is a dword hash of a filename such as {“desktop.ini”, “autorun.inf”, “ntldr”} that will be excluded by the encryption loop
03 File extension exclusion hash list; Each entry is a dword hash of a file extension such as {“ANI”, “CAB”, “COM”, “SYS”, “MSI”} that will be excluded by the encryption loop
04 Computername hash list; A list of hashed computer names that will be excluded from specific actions (such as setting the desktop wallpaper, enabling safeboot, disabling privacy settings experience, etc)
05 unused
06 Plaintext list of software names such as {“sql”, “oracle”, “synctime”}; The malware will attempt to terminate the software in this list
07 Plaintext list of service names such as {“msexchangem”, “veeam”, “sql”}; The malware will attempt to disable and remove the services in this list
08 unused
09 Plaintext list of credentials in the format of username:pass used to set the default logon for the system
10 The ransomnote

Boolean config

Functionality activated if the value is set unless states otherwise.

index Value Note
00 LB3_ENCRYPT_ANY_BIG_FILE If this value is not set it will only perform proper encryption of the following big file formats {.MDF, .NDF, .EDB, .MDB, .ACCDB}, any other file formats will only get a quick encryption done
01 LB3_RANDOMISE_FILENAME
02 LB3_AUTHENTICATE_USING_CREDS This flag is used in combination with the credentials stored in the 9th entry of the base64 block
03 LB3_SKIP_HIDDEN_FILES
04 LB3_LANGUAGE_CHECK Perform the common slavic and related languages check and don’t encrypt the system if one is set on the system
05 LB3_ENCRYPT_MICROSOFT_EXCHANGE The functionality belonging to this flag attempts to mount some extra volumes in addition to specifically looking for a microsoft exchange folder to encrypt
06 LB3_ENCRYPT_NETWORK_SHARES
07 LB3_KILL_PROCESSES Used in combination with entry 6 of the base64 block
08 LB3_KILL_SERVICES Used in combination with entry 7 of the base64 block
09 LB3_CREATE_MUTEX
10 LB3_PRINT_SIMPLIFIED_RANSOMNOTE Enumerate local and network connected printers and print a simplified ransomnote
11 LB3_SET_BACKGROUND
12 LB3_REGISTER_ICON
13 LB3_ENABLE_C2_LOGGING LockBit has the ability to communicate some basic information back to a C2 server
14 LB3_SELF_DESTRUCT LockBit carries an additional executable with it that is activated if this flag is set and some error conditions are encountered (such as mutex already being set). It appears that the entire purposLockBite of this executable is to self-destruct the malware. It terminates the ransomware process, overwrites and renames the executable on disk (it actually renames it 26 times for each letter of the alphabet..), removes the executable and then uses a shell command to remove itself as well. LB3_SELF_DESTRUCT_2 and LB3_SELF_DESTRUCT_3 appear to be used to perform some additional cleaning in some cases.
15 LB3_ATTEMPT_UAC_BYPASS
16 LB3_SELF_DESTRUCT_2 See LB3_SELF_DESTRUCT’s note
17 LB3_RESTART_PROCESS_WITH_PSEX_FLAG
18 LB3_RESTART_PROCESS_WITH_GSPD_FLAG
19 LB3_UNKNOWN
20 LB3_SELF_DESTRUCT_3 See LB3_SELF_DESTRUCT’s note
21 LB3_CLEAR_EVENT_LOGS
22 LB3_PROPAGATE_THROUGH_NETWORK
23 LB3_RESERVED The last byte in the array is not actually used, but is allocated

Conclusion

While LockBit Black looks to be taking much of its code from the BlackMatter ransomware, the codebase has seen quite a refresh. With the config and other parts changing substantially, we deemed it time for an update to the existing knowledge. We hope this analysis and the tools provided will benefit the infosec community and support CERTs worldwide dealing with LockBit cases.

Need help?

Do you need help with a cyber incident now? Our NW-CERT is available 24*7 to help you recover quickly and securely from any cyber incident. Talk to one of our incident response coordinators directly by calling +31 850 437 909.

Northwave is a Dutch Cybersecurity company headquartered in Utrecht (the Netherlands), with a subsidiary in Leipzig (Germany) and Brussels (Belgium). Northwave obtained a license from the Dutch Ministry of Justice and Security to conduct private investigations into (cyber) incidents. Northwave’s Computer Emergency Response Team (NW-CERT) members are certified as private investigators and possess extensive experience in digital forensics and cybersecurity. With hundreds of cases yearly, the NW-CERT has gained a vast experience in incident response and crisis management. For more info about the NW-CERT, visit the website.