Targeted ransomware is a common occurrence nowadays. Recently Garmin confirmed to have been the target of a ransomware attack on July 23, 2020, which led to the interruption of many of their online services. According to Bleeping Computer, the ransomware has been confirmed to be WastedLocker. The article goes on to say that Garmin paid the ransom to receive a decryptor. The NCC Group has previously published a report that attributed WastedLocker to the Evil Corp group which was also responsible for the Dridex malware and BitPaymer.
In this Threat Bulletin, we will look deeper at the WastedLocker sample used in the Garmin attack.
View the VMRay Analyzer Report for WastedLocker Ransomware
In the first stage, a simple crypter is responsible for loading the second-stage payload. However, before that happens it verifies if the registry key “
The GUID is used for the UCOMIEnumConnections interface. If it doesn’t find the key the malware goes into an endless loop (Figure 1). Other variants have been reported using other interfaces.
The crypter itself is obfuscated with junk code and carries the payload inside its executable image in the .rdata section. The payload is stored in an encrypted form. To extract it, the crypter manually parses the PE image in memory and calculates the necessary offsets.
Figure 1: Verification of the interface’s Registry key
The next step, WastedLocker calculates the offset to the second payload. The payload is stored as a blob with the size (4 bytes) preceding it. This size is used by a subsequent call to VirtualAlloc. The second stage payload is then copied over in blocks of 0x62 bytes and finally decrypted (Figure 2).
The encryption scheme is a simple xor feedback-based scheme. After finishing, the crypter jumps to the entry point of the decrypted payload which turns out to be a shellcode.
Figure 2: Copy (top) and decryption routine re-implemented in Python (bottom).
import struct def decrypt_shellcode(shellcode_file): with open(shellcode_file, "rb") as enc_shellcode: enc_payload = enc_shellcode.read() init_key = 0x00402F04 dec = b'' '' i = 0 j = 0 while i < 0xf800: key = inti_key + 2 + (j*4) if i < 0x10: print (hex(key)) p = struct.unpack ( ' <i< span="">', enc_payload[i:i+4]) temp = ((p+(j*4)) ^ key) & 0xFFFFFFFF dec += struct.pack('<i', temp)="" i="" <span="" style="color: #3366ff;">+=</i',></i<>4 j +=1 with open("decrypted_shellcode.bin", "wb") as dec_payload: dec_payload.write(dec) if__name_ == ' _main_': decrypt_shellcode("second_stage_shellcode.bin")
The shellcode is again obfuscated with junk code similar to the crypter. Its execution flow is as follows: it first uses TEB/PEB to find InLoadOrderModuleList to find the addresses of currently loaded modules. It then goes over the export functions of kernel32.dll (if found) to locate GetProcAddress. Having achieved that WastedLocker is able to use it to resolve the necessary functions. Similar to the crypter, it locates the final payload at a certain offset and proceeds to decrypt it.
The decryption routine is also similar to the one used by the initial crypter and is based on the XOR operation (Figure 3).
Figure 3: Disassembly view of the decryption routine (top) and re-implementation in Python (bottom).
import struct def decrypt(shellcode_enc): with open (shellcode_enc, "rb") as blob: enc_payload = blob.read() i = 0 dec = b'' '' while i < 0xe800: p = struct.unpack ( ' <i< span="">', enc_payload[i:i+4]) p = (p + i) & 0xFFFFFFFF) p^ = ((i + 0x3e9) & 0xFFFFFFFF) dec += struct.pack('<i< span="">', p) i +=4 with open("decrypted_shellcode.bin", "wb") as dec_mz: dec_mz.write(dec)</i<></i<>
With a decrypted payload, the shellcode attempts to load it at the image base where the crypter is loaded. It parses the final payload’s headers in memory and extracts relevant information (SizeOfImage, SizeOfOptionalHeader, a pointer to the sections, etc) used later to map the image.
To be able to write at that address the shellcode first changes the page protections of the crypter image to PAGE_EXECUTE_READWRITE. Then it allocates 0xE800 bytes of memory which is the amount that the final payload requires (size on disk) and zeros it out (Figure 4). Following that, the shellcode directly copies the PE headers into the location and maps the sections with proper alignment. Finally, it jumps to the entry point at offset 0x114E.
Figure 4: Allocate memory and change protection of the current image.
Finally, we arrive at the actual WastedLocker payload. It immediately starts by enumerating all sections of its image until it finds the .bss section. This particular section contains all the strings WastedLocker uses (including the targeted ransom note) in an encrypted format. Having located the encrypted strings it proceeds to decrypt them into a newly allocated buffer. The decryption routine works based on feedback from the previous round (Figure 5).
Figure 5: IDA’s pseudo-code of the strings decryption routine.
WastedLocker uses a global structure to hold certain data that it requires for later use (Figure 6). For example, it saves pointers to its file name and the current file path. Later, WastedLocker also saves pointers to the head of a doubly-linked list of strings that it collects from the Registry.
The list of strings is used whenever the ransomware has to generate a name. To collect the strings WastedLocker enumerates the keys under
HKLM\SYSTEM\CurrentControlSet\Control and creates the linked list with the names split between each capital letter or white space, i.e., “AccessiblityList” becomes “Accessibility” and “List” and are stored separately in a doubly-linked list’s node. WastedLocker also makes sure that names are saved only once (Figure 7).
Figure 6: Part of an internal data structure[/caption]
Figure 7: Linked list’s node structure (left) and VMRay function log showing Registry enumeration (right).
The ransomware uses “\\?\C:\Windows\system32\” as a base to enumerate executable files and DLLs inside. On 64-bit systems, if the software is 32-bit this is transparently redirected to C:\Windows\SysWow64\. WastedLocker iterates over the directories with the classic WinAPI approach of using FindFirstFile and FindNextFile and ignores the “.” and “..” directories.
It calls the function recursively until a suitable file is found which is either an EXE or a DLL. For that WastedLocker uses one of the strings it carried in the .bss section – “.exe|.dll” string – which it splits and uses as an input to scan the path of the enumerated file.
If no “.exe” or “.dll” was found in the name the ransomware simply ignores the file and moves on to the next. If an EXE or a DLL was found it allocates a buffer for a node of a linked list containing the file path and some additional information on the file, like the creation time (Figure 8). Basically, WastedLocker collects a doubly-linked list of all executable files in C:\Windows\SysWow64\ into a list for later use.
Figure 8: VMRay function log showing the search. Buffer is only allocated if “.exe” or “.dll” is found in the name.
The next step taken by the WastedLocker is to randomly find a string from the previously generated list of strings for a new executable name and then drop a copy of a file from C:\Windows\System32 (this time from the generated list of files) into %APPDATA% using the new name.
Following that, WastedLocker changes the file’s attributes to “hidden” and constructs an alternate data stream (ADS) by appending a “:bin” filename to it. The ransomware then moves on to reading the original loader data into memory, opening the dropped files ADS, and writing the data inside. Additionally, the payload uses the previously collected information on the file to set the file time to the original one (Figure 9).
Figure 9: VMRay function log depicting the sequence of function calls used to hide in ADS.
Having come this far, the WastedLocker starts a new process of the loader appending the “-r” command line parameter (Figure 9). This new process works in the same way as described in the previous sections, it starts from the initial crypter which extracts the shellcode and then the final ransomware payload up to the parameter parsing of the command line. This time, since the second process was started with the “-r” parameter a new control-flow path is taken.
Figure 10: The second process started with the parameter “-r”.
Now the ransomware tries to create a new service. The name of the service is again taken randomly from the list of strings. It appends the name to the system32 path and verifies that no such path is already present on the system (i.e. there is no other file in the system directory with the chosen name).
If the path is present on the system, WastedLocker chooses another name until it finds something suitable. When the file path is found, the ransomware writes itself to this file, starts vssadmin.exe to delete shadow copies, takeown.exe to take ownership of the new file, icalcs.exe to reset its permissions, and finally creates a new service.
The original binary is then deleted from the %APPDATA% directory (Figure 11). The new service is started with the “-s” parameter. We have observed that the ransomware still attempts encryption even if the service wasn’t started successfully.
Figure 11: Sequence of commands executed by WastedLocker.
When the service is running it can now proceed with the intended encryption process. The WastedLocker service starts by calling the StartServiceCtrlDispatcherW function with a ServiceMain routine that is responsible for the entire encryption process. This function starts by creating a new mutex with a random name (again taken from the usual list of strings) and starts the actual encryption thread.
The encryption routine first creates a log file (lck.log) in the %TEMP% directory. Afterward, it begins looking for drives to encrypt. It targets the following drives: movable, fixed, remote, and share (Figure 11). With a list of drives to encrypt this function looks for files and collects them into a linked list reusing the functions used to enumerate the system32 directory. It then starts the main encryption thread.
Figure 12: VMRay function log depicting the drive types for encryption.
The ransomware uses a list of file extensions and file paths to define files that are excluded from the encryption and then proceeds to encrypt every other file and location (Figure 13). For each file that is going to be encrypted the ransomware drops a ransom note containing the unique encryption key, moves the file with the new extension “[…]wasted” (prepended by the name of the target organization), and encrypts (AES-256 CBC mode) the file in memory by mapping it directly (Figure 14).
Figure 13: File extensions and folders not encrypted by WastedLocker.
Figure 14: VMRay function log showing the sequence of function calls involved into dropping the ransom note (left) and encrypting the file (right).
In the case of this particular sample, which was used in the attack on Garmin the extension was “.garminwasted”. The ransom note with the extension using the scheme “[…]wasted_info” (.garminwasted_info) is containing the same ransom message for each file with the difference being the key used for file encryption. Those keys are encrypted with RSA and encoded with base64 (Figure 15).
Figure 15: Example ransom note dropped for each encrypted file.
In targeted attacks, the actual ransomware is usually deployed after the attackers have an understanding of the victim’s network, often weeks or months after gaining the initial foothold. The ransomware is often configured for a particular target and makes sure that all data is encrypted as well as accessible backups and restore points are deleted. Even with comprehensive backups falling victim to a ransomware attack would mean losing a considerable amount of data which could be expensive and time-consuming. Without offline backups, data would be nearly impossible to recover.
Generally, the security community doesn’t advise paying the ransom as this would most likely finance other malicious activities and one still doesn’t have a guarantee that the data is going to be restored.
Original packed sample
Unpacked WastedLocker payload