Lockbit 3.0 Analysis

Lockbit 3.0 Analysis

Lockbit 3.0 Analysis
Image Source

Overview

This post contains my analysis of various components of Lockbit 3.0. It mainly covers various anti-analysis and anti-debugging capabilities built in, multiple checks conducted before encryption, and how the configuration is stored within the sample. This analysis does not go into depth on the encryption mechanism used in Lockbit 3.0.

Anti-Analysis

Password Prompt

When the password option is enabled for Lockbit, a password string must be passed to -pass in order to decrypt the .text contents of Lockbit.

Lockbit Main Logic Decryption with Password

When the password option is enabled the entry point is directed to the .itext section which contains a function to decrypt the .text and redirect the control flow after it has been decrypted.

API Hashing

When Lockbit is first executed, it will aim to resolve a API function call table from a list of API hashes which will be used throughout the lifetime of the execution.

Within the .text section there are multiple API hash tables. The first hash will always refer to the DLL followed by a list of API hashes which end with a 0xCCCCCCCC.

API Hash Table

Lockbit will first invoke multiple functions that loop over each table and resolve each API hash to their corresponding function.

API Hash Table Resolution Functions

The function responsible for populating the function tables will loop over each entry in the API hash table, for each API hash it will resolve to the corresponding pointer to the function. After, it will allocate a buffer on the heap where a trampoline function will be constructed to direct the caller to the resolved function.

Initiation of the API Hash Table resolution routine

After the API hash is resolved and we have a function pointer to the desired function, a trampoline function will be constructed which will be responsible for calling this function.

Trampoline Function Construction

The following shows the construction of a trampoline function, note that the pointer to the function is always encoded when written to the buffer. This makes it difficult to understand what the function points to and can impact the ability to fix the function table if dumping from memory.

Creation of a Trampoline Jump Function

There are 5 different trampoline functions that an be constructed for any given function, which one is used is decided by a random number that is generated. They all provide the same result and differ only in how they encode the pointer to the function.

EncodedAPIHash = ROR(RESOLVED_API_FUNCTION_PTR, RandomByte)
mov eax, EncodedAPIHash
rol eax, RandomByte
jmp eax

API Hash Trampoline Case 1

EncodedAPIHash = XOR(RESOLVED_API_FUNCTION_PTR, 0x10035FFF)
mov eax, EncodedAPIHash
xor eax, 0x10035FFF
jmp eax

API Hash Trampoline Case 2

EncodedAPIHash = ROL(XOR(RESOLVED_API_FUNCTION_PTR, 0x10035FFF), RandomByte)
mov eax, EncodedAPIHash
ror eax, RandomByte
xor eax, EncodedAPIHash
jmp eax

API Hash Trampoline Case 3

EncodedAPIHash = ROR(XOR(RESOLVED_API_FUNCTION_PTR, 0x10035FFF), RandomByte)
mov eax, EncodedAPIHash
rol eax, RandomByte
xor eax, EncodedAPIHash
jmp eax

API Hash Trampoline Case 4

EncodedAPIHash = ROL(RESOLVED_API_FUNCTION_PTR, RandomByte)
mov eax, EncodedAPIHash
ror eax, RandomByte
jmp eax

API Hash Trampoline Case 5

In the end there is a table where each trampoline function will be stored. Rather than invoking these functions directly, they will always go through the trampoline function.

API Function Call Table

Stack Strings

Stack strings are used to encode strings and other data in order to prevent it from being identified through static means. The function responsible for decoding the stack strings will take a pointer to the beginning of the stack string buffer, and the number of encoded DWORD sized entries.

Example of Stack String

For each DWORD in the stack string it will be decoded by XOR'ing with a hardcoded value and then inverting all the bytes.

Stack String Decoding Routine

The following IDAPython script was used in order to find all stack strings, resolve them, and leave a comment indicating the string.

import idc
import idautils

def flip_bytearray(bytearray_data):

	size = len(bytearray_data)
	flipped = bytearray(size)

	for i, byte in enumerate(bytearray_data):
		flipped[size - 1] = bytearray_data[i]
		size = size - 1
	return flipped


def invert_bytearrays(bytearray_data):
	for i, byte in enumerate(bytearray_data):
		if bytearray_data[i] == 0xFF:
			bytearray_data[i] = 0x00
			continue
		bytearray_data[i] = ~bytearray_data[i] & 0xFF	
	return bytearray_data


def xor_bytearrays(bytearray1, bytearray2):
	if len(bytearray1) != len(bytearray2):
		return

	for i, byte in enumerate(bytearray1):
		bytearray1[i] = bytearray1[i] ^ bytearray2[i]
	return bytearray1

def invert_hex_dword(hex_value):
    # Ensure the input is a 32-bit unsigned integer (DWORD)
    dword = hex_value & 0xFFFFFFFF  # Mask to 32 bits
    inverted = ~dword & 0xFFFFFFFF  # Invert and mask to 32 bits
    return inverted
    

def decode_string(stack_string_encoded):

    xor_key = bytearray.fromhex("10035FFF")
    
    try:
        encoded_string = bytearray.fromhex(stack_string_encoded.replace("0x",""))
    except ValueError:
        # Some cases the encoded string will be 4 bytes, but the last byte will only contain one value
        # In this case we add the trailing 0 that IDA omits
        encoded_string = bytearray.fromhex(stack_string_encoded.replace("0x","") + "0")
    
    decoded_string = xor_bytearrays(encoded_string, xor_key)
    decoded_string = invert_bytearrays(decoded_string)
    decoded_string = flip_bytearray(decoded_string)
    return decoded_string

def get_stack_decode_parameters(stack_decode_function_ea):

    '''
    We always pass two paramters to DecodeStackString, this means
    if we go back 0xA bytes we will hit the DWORD array containing stack strings.
    
    Going back 0x3 and taking the 1'th element will give us the size of the stack array.
    
    .text:00416E1C C7 40 08 6C A0 FC EF                    mov     dword ptr [eax+8], 0EFFCA06Ch
    .text:00416E23 6A 03                                   push    3
    .text:00416E25 50                                      push    eax
    .text:00416E26 E8 15 A4 FE FF                          call    DecodeStackString
    '''

    stack_string_array = stack_decode_func - 0xA
    stack_string_array_size = idc.get_bytes(stack_decode_func - 0x3, 0x2, 0)[1]
    
    return (stack_string_array, stack_string_array_size)

def get_list_encoded_string_values(stack_string_array_ea, stack_string_array_size):
    
    # Save inital value
    stack_string_array_inital_ea = stack_string_array_ea
    stack_string_array_inital_ea_next = idc.next_head(stack_string_array_inital_ea, 0xFFFFFFFFFFFFFFFF)
    
    # Move the stack string array ea to before it starts for the loop
    stack_string_array_ea = idc.next_head(stack_string_array_ea, 0xFFFFFFFFFFFFFFFF)
    
    decoded_string_buffer = bytearray(0)
    
    for i in range(0, stack_string_array_size):
        
        # Set the stack string array ea to next stack string
        stack_string_array_ea = idc.prev_head(stack_string_array_ea, 0)
        if print_insn_mnem(stack_string_array_ea) != "mov":
            continue
        
        # Extract the stack string from the assembly line
        # Example: mov dword ptr [eax+4], 0EF9BA02Dh
        stack_string_encoded = get_operand_value(stack_string_array_ea, 1) 
        stack_string_encoded = hex(stack_string_encoded & 0x00000000FFFFFFFF)
        decoded_string = decode_string(stack_string_encoded)
        decoded_string_buffer = decoded_string + decoded_string_buffer

    print("[+] Decoded Bytes: " + decoded_string_buffer.hex())
    try:
        print("[+] Decoded String: " + decoded_string_buffer.decode('utf-16'))
        
        idc.set_cmt(stack_string_array_inital_ea, decoded_string_buffer.decode('utf-16'), 0);
        idc.set_cmt(stack_string_array_inital_ea_next, decoded_string_buffer.hex(), 0);
    except UnicodeDecodeError:
        idc.set_cmt(stack_string_array_inital_ea_next, decoded_string_buffer.hex(), 0);
    
for xref in idautils.XrefsTo(stack_decoding_string_function_ea):
    print(f"[*] Decode Function @ {hex(xref.frm)}")
    stack_decode_func = xref.frm
    (stack_string_array, stack_string_array_size) = get_stack_decode_parameters(stack_decode_func)

    get_list_encoded_string_values(stack_string_array,stack_string_array_size)

Majority of the decoded stack strings will resolve to data that can be interpreted as Unicode strings.

However, there is data that is not human readable encoded using this mechanism. For example, the following is the riid parameter for the CoCreateInstance function.

Anti-Debugging

Heap Corruption

A wrapper function is used to invoke RtlAllocateHeap, before the call to this function the handle to the heap is acquired via the PEB and the Heap->ForceFlags is checked for a value other than 0 indicating the presence of a debugger.

If the presence of a debugger found the pointer to the heap is corrupted and RtlAllocateHeap will likely crash the process as a result.

Heap Based Anti-Debug Technique — RtlCreateHea
There are a lot of anti debug techniques. Some of them are basics and some of them are advanced. Now, I explain a little bit hard one…
『Lockbit 3.0で見つけたアンチデバッグテクニック(その1)』
はじめに(読み飛ばし推奨) 2022年に利用がみられているランサムウェアのLockbit 3.0を解析中です。Lockbit 3.0は、ビルダーが流出した話が…

Detaching from Debugger

ZwSetInformationThread is use with the ThreadHideFromDebugger parameter when the process begins running, and on newly spawned encryption threads. This results in "enables suppression of debug events generated on the thread. Threads that do not generate debug events are essentially invisible to debuggers."

Detaching from Debugger

Preventing Debugger from Attaching

During the beginning DbgUiRemoteBreakin is corrupted by setting the first few bytes of the function to Read/Write, and using SystemFunction040 to encrypt the contents. As a result a debugger will not be able to attach itself to the process.

Corruption of DbgUiRemoteBreakin

Lockbit Configuration Decryption

One of the first steps taken by Lockbit is to decode its embedded configuration file.

Helper Function Decoding

There are multiple steps taken by Lockbit to decode the embedded configuration file. The first phase is to extract helper functions that are encrypted and compressed within the .data section. These helper functions take the form of Assembly instructions that will facilitate the generation of XOR keys used to decrypt the configuration.

Note, that the size of the helper function buffer is stored in the DWORD right before it starts, in this case it is 0x1AE bytes in size.

Encrypted and Compressed Helper Functions

In order to decrypt the helper function buffer each byte is XOR'd with a hardcoded key of 0x30. After the entire buffer is decrypted APLib is used to decompress the contents.

The most important steps is the fact that the helper function buffer has been decrypted and decompressed into a newly allocated buffer on the heap. Since the contents of the this buffer are executable assembly code Lockbit will be able to successfully execute when invoked.

In the end, a trampoline function is created in order to invoke the assembly code. This is the exact same process as used for functions resolved via API hashing.

Decoding Helper Function and Trampoline Function Construction

The helper function that has been decoded in this phase will be used in the process of decoding the core configuration.

The following script was used in IDA in order to decrypt and decompress the helper functions. A new segment was created in IDA in order to write the assembly code to.

import struct
from binascii import crc32
from io import BytesIO
import idc
import idautils

# APLib Code Found on https://github.com/snemes/aplib
class APLib(object):

    __slots__ = 'source', 'destination', 'tag', 'bitcount', 'strict'

    def __init__(self, source, strict=True):
        self.source = BytesIO(source)
        self.destination = bytearray()
        self.tag = 0
        self.bitcount = 0
        self.strict = bool(strict)

    def getbit(self):
        # check if tag is empty
        self.bitcount -= 1
        if self.bitcount < 0:
            # load next tag
            self.tag = ord(self.source.read(1))
            self.bitcount = 7

        # shift bit out of tag
        bit = self.tag >> 7 & 1
        self.tag <<= 1

        return bit

    def getgamma(self):
        result = 1

        # input gamma2-encoded bits
        while True:
            result = (result << 1) + self.getbit()
            if not self.getbit():
                break

        return result

    def depack(self):
        r0 = -1
        lwm = 0
        done = False

        try:

            # first byte verbatim
            self.destination += self.source.read(1)

            # main decompression loop
            while not done:
                if self.getbit():
                    if self.getbit():
                        if self.getbit():
                            offs = 0
                            for _ in range(4):
                                offs = (offs << 1) + self.getbit()

                            if offs:
                                self.destination.append(self.destination[-offs])
                            else:
                                self.destination.append(0)

                            lwm = 0
                        else:
                            offs = ord(self.source.read(1))
                            length = 2 + (offs & 1)
                            offs >>= 1

                            if offs:
                                for _ in range(length):
                                    self.destination.append(self.destination[-offs])
                            else:
                                done = True

                            r0 = offs
                            lwm = 1
                    else:
                        offs = self.getgamma()

                        if lwm == 0 and offs == 2:
                            offs = r0
                            length = self.getgamma()

                            for _ in range(length):
                                self.destination.append(self.destination[-offs])
                        else:
                            if lwm == 0:
                                offs -= 3
                            else:
                                offs -= 2

                            offs <<= 8
                            offs += ord(self.source.read(1))
                            length = self.getgamma()

                            if offs >= 32000:
                                length += 1
                            if offs >= 1280:
                                length += 1
                            if offs < 128:
                                length += 2

                            for _ in range(length):
                                self.destination.append(self.destination[-offs])

                            r0 = offs

                        lwm = 1
                else:
                    self.destination += self.source.read(1)
                    lwm = 0

        except (TypeError, IndexError):
            if self.strict:
                raise RuntimeError('aPLib decompression error')

        return bytes(self.destination)

    def pack(self):
        raise NotImplementedError


# APLib Code Found on https://github.com/snemes/aplib
def decompress(data, strict=False):
    packed_size = None
    packed_crc = None
    orig_size = None
    orig_crc = None

    if data.startswith(b'AP32') and len(data) >= 24:
        # data has an aPLib header
        header_size, packed_size, packed_crc, orig_size, orig_crc = struct.unpack_from('=IIIII', data, 4)
        data = data[header_size : header_size + packed_size]

    if strict:
        if packed_size is not None and packed_size != len(data):
            raise RuntimeError('Packed data size is incorrect')
        if packed_crc is not None and packed_crc != crc32(data):
            raise RuntimeError('Packed data checksum is incorrect')

    result = APLib(data, strict=strict).depack()

    if strict:
        if orig_size is not None and orig_size != len(result):
            raise RuntimeError('Unpacked data size is incorrect')
        if orig_crc is not None and orig_crc != crc32(result):
            raise RuntimeError('Unpacked data checksum is incorrect')

    return result


original_config_offset = 0x00F04DBF # Offset to the encrypted and compressed helper functions
current_config_offset = original_config_offset

size = idc.get_wide_dword(original_config_offset - 4,)
print(hex(size))

# Decrypt Helper Functions with XOR

for byte in range(0,size):
    current_byte = idc.get_bytes(current_config_offset, 1)
    current_byte = int.from_bytes(current_byte)
    current_byte = current_byte ^ 0x30
    
    idc.patch_byte(current_config_offset, current_byte)
    
    current_config_offset = current_config_offset + 1

# Decompress Helper Functions and Write to New Segment

decoded_bytes = idc.get_bytes(original_config_offset, size)
decompressed_bytes = decompress(decoded_bytes)
new_segment = 0x0F190000 # New Segemnt to write code to, this will store decrypted and decompressed assembly code

for index, byte in enumerate(decompressed_bytes):
    idc.patch_byte(new_segment + index, byte)

IDAPython Script to Decode Helper Functions

Configuration Decoding

The configuration is stored in the .pdata section, the following summarizes the components of the configuration:

  • .pdata+0x0: First DWORD used to generate initial XOR key.
  • .pdata+0x4: Second DWORD used to generate initial XOR key.
  • .pdata+0x8: Size of configuration contents.
  • .pdata+0xC: Encrypted and compressed configuration contents.
Core Lockbit Configuration in .pdata

The DeriveXORKey function in this case is a call to the helper function that has been decoded in the previous step. Once called, it will return a value in eax and edx that will be utilized to decrypt the configuration. Note, the DeriveXORKey function depends on the first two 8 bytes of the configuration, as a result we can produce a deterministic value each time for the configuration.

The core of the decryption process is the derivation of XOR keys within the DeriveXORKey function. Once DeriveXORKey derives a key it is returned within as two separate values, in this case within the EAX and EDX registers.

Derive XOR Values for Config Decryption

As a reminder, the DeriveXORKey is the helper function that was decrypted in the prior step. Without the prior step there would be no way to generate the XOR keys required to decrypt the core configuration.

DeriveXORKey Trampoline Jump

The Lockbit configuration is then decrypted by two DWORDs at a time using both of these XOR keys. Each DWORD is XOR'd with a specific byte within the DWORD.

This decryption scheme ensures that each byte is encrypted with a separate XOR key. In addition, it adds a level of complexity for the analyst to be able to determine the final algorithm and reimplement it.

To provide the best clarity behind the algorithm used for decryption reference the following code written in C which emulates the process:

#include <stdio.h>
#include <stdint.h>
#include <Windows.h>
#include "LockbitConfig.h"
#include "aplib.h"


DWORD32 MultiplyAndGetHigh(DWORD32 a, DWORD32 b) {
    // Perform the multiplication, which results in a 64-bit result
    DWORD64 result = (DWORD64)a * (DWORD64)b;

    // Extract the higher 32 bits by shifting right
    return (DWORD32)((result >> 32) & 0xFFFFFFFF); // Right shift by 32 bits
}

VOID ComputeNumbers(_In_ DWORD32 Arg1, _In_ DWORD32 Arg2, _In_ DWORD32 Arg3, _In_ DWORD32 Arg4, _Out_ PDWORD32 XORKey1, _Out_ PDWORD32 XORKey2) {

    *XORKey1 = Arg1 * Arg3;
    *XORKey2 = MultiplyAndGetHigh(Arg1, Arg3) + ((Arg3 * Arg2) + (Arg1 * Arg4));
}

// Value1 will first be pointer to first dword of config
// Value2 will first be pointer to second dword of config
void DeriveXORKey(_In_ PDWORD32 InputValue1, _In_ PDWORD32 InputValue2, _Out_ PDWORD32 XORKey1, _Out_ PDWORD32 XORKey2) {

    /*
        Compute the first round of XOR Keys
    */

    DWORD Output1 = 0;
    DWORD Output2 = 0;
    ComputeNumbers(*InputValue1, *InputValue2, 0x4C957F2D, 0x5851F42D, &Output1, &Output2);

    /*
        Conduct permutations on the first round of XOR keys
    */

    DWORD32 permutedArg3 = Output1 + 0x0F767814F;
    DWORD32 carry = 0;
    if (permutedArg3 < Output1 || permutedArg3 < 0x0F767814F) {
        carry = 1;
    }

    // If previous add had carry flag set, add it to this calculation
    DWORD32 permutedArg4 = Output2 + 0x14057B7E + carry;

    /*
        Updated values used for follow up DeriveXORKey calls
    */

    *InputValue1 = permutedArg3;
    *InputValue2 = permutedArg4;

    /*
        Compute second round of XOR keys and return
    */

    ComputeNumbers(((PDWORD)global_LockbitConfig)[0], ((PDWORD)global_LockbitConfig)[1], permutedArg3, permutedArg4, &Output1, &Output2);

    *XORKey1 = Output1;
    *XORKey2 = Output2;
}

VOID DecodePayload(PBYTE pConfig, SIZE_T size) {


    /*
      These two values are initally set to the first and second DWORD from the Lockbit Conifg

      After each DeriveXORKey execution they get updated with a permuted value, and are
      passed to the following DeriveXORKey call.
  */

    DWORD32 value1 = ((PDWORD)global_LockbitConfig)[0];
    DWORD32 value2 = ((PDWORD)global_LockbitConfig)[1];

    /*
        The xorKey DWORD variables will receive XOR Keys from DeriveXORKey.
    */
    DWORD32 xorKey1 = 0;
    DWORD32 xorKey2 = 0;

    SIZE_T DwordsToDecrypt = 2;

    PBYTE pCurrentConfigOffset = pConfig;
    SIZE_T configSizeCounter = size;

    /*
        Decode the config, the lower 16 bits will be used to decode one DWORD of the config.
        Then the higher 16 bits will be used to decode another DWORD of the config.

    */

    while (TRUE) {

        DeriveXORKey(&value1, &value2, &xorKey1, &xorKey2);

        for (SIZE_T j = 0; j < 2; j++) {

            // XOR with LOWER_8_BITS(xorKey1) - AL of EAX

            *pCurrentConfigOffset = *pCurrentConfigOffset ^ (xorKey1 & 0x000000FF);
            pCurrentConfigOffset++;

            configSizeCounter--;
            if (configSizeCounter == 0) {
                goto END;
            }

            // XOR with HIGHER_8_BITS(xorKey2) - DH of EDX

            *pCurrentConfigOffset = *pCurrentConfigOffset ^ ((xorKey2 >> 8) & 0x000000FF);
            pCurrentConfigOffset++;

            configSizeCounter--;
            if (configSizeCounter == 0) {
                goto END;
            }

            // XOR with HIGHER_8_BITS(xorKey1) - AH of EAX

            *pCurrentConfigOffset = *pCurrentConfigOffset ^ ((xorKey1 >> 8) & 0x000000FF);
            pCurrentConfigOffset++;

            configSizeCounter--;
            if (configSizeCounter == 0) {
                goto END;
            }

            // XOR with LOWER_8_BITS(xorKey2) - DL of EAX

            *pCurrentConfigOffset = *pCurrentConfigOffset ^ (xorKey2 & 0x000000FF);
            pCurrentConfigOffset++;

            configSizeCounter--;
            if (configSizeCounter == 0) {
                goto END;
            }

            // Shift bits right by 0x10
            // This ensures we now use the higher 16 bits for the next loop around
            xorKey1 = xorKey1 >> 0x10;
            xorKey2 = xorKey2 >> 0x10;
        }
    }
END:
    return;
}


int main()
{
    
    /*
        Allocate new heap space for the decoded config
    */

    SIZE_T configSize = ((PDWORD)global_LockbitConfig)[2];

    PBYTE pLockbitConfig = (PBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, configSize);
    if (pLockbitConfig == NULL) {
        return -1;
    }

    memcpy(pLockbitConfig, global_LockbitConfig + 0xC, configSize); // Encoded config starts at global_LockbitConfig + 0xC

    /*
        Decrypt the config
    */

    DecodePayload(pLockbitConfig, configSize);

    /*
        Decompress Config with APLib
    */

    PBYTE pDecompressedConfig = (PBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, configSize * 4);
    if (pDecompressedConfig == NULL) {
        return -1;
    }

    aP_depack_asm(pLockbitConfig, pDecompressedConfig);
    
    return 0;

}

C Implementation of Lockbit 3.0 Configuration Decoding

Configuration Parsing

Once the configuration is decrypted and decompressed various parts of the buffer are interpreted and associated with its corresponding data that is stored.

Decrypted Lockbit Configuration
  • Red: 1024-bit RSA Key
  • Orange: UID and KeyID
  • Purple: Boolean values indicating to enable or disable functionality
  • Green: Base64 encoded configurations separated by NULL terminators.

The following summarizes what is included within the Base64 encoded configuration sections:

Folder Exclusion List: List of folders to be excluded stored as a string hash.

Filename Exclusion List: List of filenames to be excluded stored as a string hash.

File Extension Exclusion List: File extensions to be exclude stored as a string hash.

Computer Hostname Exclusion List: Computer hostnames to exclude stored in plaintext.

Process Termination List: List of process names to terminate stored in plaintext.

Service Termination List: List of services to terminate stored in plaintext.

User Account Impersonation List: List of accounts used to run the ransomware under.

Ransom Note: The ransom note.

Pre-Encryption Checks and Anti-Forensics

Language Check

Language checks are conducted if the LanguageCheck flag in the configuration is enabled, it is conducted with the NtQueryDefaultUILanage function.

Language Check

A full list of languages checked are below:

Language
Russian
Ukrainian
Belarusian
Tajik
Armenian
Azerbaijani
Georgian
Kazakh
Kyrgyz
Turkmen
Uzbek
Tatar

Process Deletion

During execution Lockbit will start a function as a thread responsible for terminating blacklisted processes.

Process Termination Capability

This function will utilize NtQuerySystemInformation to enumerate each process and compare to a predefined list of processes, if there are any matches a handle will be opened to the process and it will be terminated.

Process Terminitation Logic

Service Deletion

If the terminate service flag is set, Lockbit will terminate a predefined list of services. First it will construct a list of services that are present on the system via EnumServicesStatusExW.

Service Enumeration

After, it will loop through each service and compare to a predefined list. If there is a match that service will be stopped and deleted. [ CONFIRM]

Service Termination

Clearing Event Logs

Event Log Clearning Capability
Clear Event Log Logic

Creating a Mutex

A Mutex name will be generated based on the RSA key embedded in the Lockbit configuration.

Mutex Creation

The mutex name will take the form of Global%.8x%.8x%.8x%.8x. after hashing the RSA key with MD5, performing mutations on the result, and then hashing it with MD4.

Privilege Escalation

Impersonate as Other User

Within the Lockbit configuration multiple user accounts in the form of username and passwords can be stored. These credentials can be passed to LogonUserW in order to retrieve a token representing the user.

LogonUserW Wrapper Function

Once the token of a user is retrieved it is stored in a global variable that is used at a later point in time.

Passing User Information to LogonUserW

Checking Administrator Rights

UAC Bypass

If the current process is not running as administrator, and the current Windows version is Vita or above a UAC bypass that uses the CMSTPLUA COM interface will be used, as also mentioned by this Google blog post.

UAC Bypass Check

Process Privilege Enable

In the .text section there is a array of LUID's representing various privileges.

Privilege Array

Lockbit will loop through this table and enable each privileges in the process via RtlAdjustPrivilege.

Loop to Enable Privileges

References

References used to learn and assist with my analysis:

LockBit Ransomware v2.0
Malware Analysis Report - LockBit Ransomware v2.0
Dissecting LockBit v3 ransomware
We analyzed a variant of LockBit v3 ransomware, and rediscovered a bug that allows us to decrypt some data without paying the ransom. We also found a design flaw that may cause permanent data loss.
[12월 Security Report] LockBit 3.0 랜섬웨어
PDF : https://www.somansa.com/security-report/security-note/lockbit30_202212/ 요약 1. 서비스형 랜…
LockBit 3.0 - An In-Depth Analysis Of LockBit Black’s Config
LockBit 3.0 - An In-Depth Analysis Of LockBit Black’s Config
Shining a Light on DARKSIDE Ransomware Operations | Google Cloud Blog
The creators of DARKSIDE ransomware have launched a global crime spree affecting organizations in more than 15 countries and multiple industry verticals.