gemesa@home:~$

Reversing a spam bot

Table of contents

Introduction

This post analyses a spam bot malware sample using Ghidra and Python. The primary goal is to reverse engineer all cryptographic subsystems (e.g. config encryption, C2 obfuscation, email composition and signing) and document the vulnerabilities that can be exploited (e.g. for detection and decryption). Additionally, we build a Python config decryptor and provide YARA and capa rules for detection.

Executive summary

This sample is an x86 PE executable that installs as a Windows service with SERVICE_AUTO_START persistence. Encrypted configuration is stored in PE resource RT_RCDATA/0xE2 using Blowfish-ECB with a hardcoded 16-byte key. The config contains the C2 host, two ports and a service display name in the format host:port1/port2;ServiceName.

C2 communication uses UDP with a XOR cipher keyed by the MD5 hash of the ASCII string "UNREGISTERED". No authentication is performed. HTTP downloads (SMTP credentials, email templates, email lists, etc.) bypass TLS certificate validation (via WinINet flags 0x80803100). Downloaded payloads pass through base64 decode –> XOR decryption –> ZIP extraction with no authentication.

The SMTP engine uses a multi-threaded architecture with a protocol state machine. Authentication supports STARTTLS, AUTH LOGIN, CRAM-MD5 and AUTH PLAIN, but is vulnerable to STARTTLS stripping. Email attachments use the obsolete PKWARE Encryption (ZipCrypto). Email signing supports both DomainKeys and DKIM, but defaults to rsa-sha1.

The following subroutines related to cryptographic capabilities have been reversed and their vulnerabilities documented. They are grouped into three categories: custom cryptographic subroutines, PRNG weaknesses and protocol/configuration issues.

Cryptographic subroutines:

# Severity Name Address Impact
1 Critical Static XOR key (MD5 of “UNREGISTERED”) 0x004680d0 Decrypt all C2 traffic globally.
2 High Blowfish-ECB hardcoded key 0x00472da0 Extract config from any sample.
3 High Traditional PKWARE Encryption (ZipCrypto) 0x0047f7b0 Decrypt all attachments.
4 Medium HMAC-MD5 for CRAM-MD5 0x0044bf00 Offline dictionary attack on credentials.
5 Low DomainKeys hardcoded rsa-sha1 0x00433f40 Signature rejection.
6 Low DKIM defaults to rsa-sha1 0x00431880 Signature rejection.

PRNG weaknesses:

# Severity Name Address Impact
1 High Predictable PRNG seed 0x0047f7b0 Replay rand() to recover ZipCrypto headers.
2 Medium weak_random 15-bit output space 0x0044e700 Predictable random tokens across SMTP engine.

Protocol and configuration flaws:

# Severity Name Address Impact
1 Critical No C2 authentication 0x00426e80 Spoof commands, hijack botnet.
2 Critical TLS cert validation disabled 0x00466b60 MITM on all HTTPS.
3 Critical No download authentication check 0x00466b60 Replace campaign data via MITM.
4 High STARTTLS stripping 0x004437d0 Capture SMTP credentials.

High-level analysis

Let’s shorten the binary name first so it is easier to work with in the following chapters.

$ mv 36684cef31a4a43473d5b12d30aabf94f56e407b0eeec14b7c1cb87efa3159b6 spambot.exe

Hashes

$ md5sum < spambot.exe
88b43801b38acea324467ece3f12a0e0  -

$ sha1sum < spambot.exe
1004d87f61c461c3d4d4f0ff7f40c5da0a279e4e  -

$ sha256sum < spambot.exe
36684cef31a4a43473d5b12d30aabf94f56e407b0eeec14b7c1cb87efa3159b6  -

file

It is an x86 PE executable:

$ file spambot.exe
spambot.exe: PE32 executable for MS Windows 6.00 (GUI), Intel i386, 5 sections

DiE

Built with an older toolchain:

$ diec spambot.exe
PE32
    Linker: Microsoft Linker(12.00.31101)
    Compiler: Microsoft Visual C/C++(18.00.40629)[C]
    Tool: Visual Studio(2013)

Although the entropy of .text and .reloc seem a bit high (but not extremely high, e.g. 7+), the sample is not packed. The presence of the usual sections supports this.

$ diec --entropy spambot.exe 
Total 6.59308: packed
  0|PE Header|0|1024|2.89846: not packed
  1|Section(0)['.text']|1024|1436160|6.53491: packed
  2|Section(1)['.rdata']|1437184|263680|5.62472: not packed
  3|Section(2)['.data']|1700864|23552|4.72442: not packed
  4|Section(3)['.rsrc']|1724416|2560|5.06288: not packed
  5|Section(4)['.reloc']|1726976|46592|6.57773: packed

strings

There are too many strings to list here but some relevant ones are shown below. Based on these we can assume that the malware uses OpenSSL, can download from remote server(s), can send emails, supports the SMTP protocol, supports DNS and MX resolution.

$ strings spambot.exe
...
OpenSSL 1.0.2g  1 Mar 2016
...
Downloading settings...
...
Emails were sent by another thread
...
EHLO
HELO
...
Resolving MX %s using DNS server 
...

capa

capa gives us further hints about the capabilities (not an exhaustive list):

  • Create or Modify System Process::Windows Service [T1543.003] (persistence)
  • Service Stop [T1489] (likely self-uninstall)
  • Create Thread [C0038] (multithreaded architecture)
  • HTTP Communication::Connect to Server [C0002.009]
  • Socket Communication::Create TCP Socket [C0001.011]
  • Socket Communication::Create UDP Socket [C0001.010]
  • DNS Communication::Resolve [C0011.001]
  • Cryptographic Hash::MD5 [C0029.001]
  • Decrypt Data::AES [C0031.001]
  • Encrypt Data::AES [C0027.001]
  • Encrypt Data::Blowfish [C0027.002]
  • Encrypt Data::RC4 [C0027.009]
  • Generate Pseudo-random Sequence::RC4 PRGA [C0021.004]
  • Hashed Message Authentication Code [C0061]

Note: some of these capabilities (especially some OpenSSL functions) might be dead code or false positives.

Imports

If we check the imports, we can conclude that:

  • no CRYPT32.dll or BCRYPT.dll (OpenSSL is statically linked, no Windows CryptoAPI)
  • WS2_32.dll: raw TCP/UDP sockets are supported
  • WININET.dll: high-level HTTP is supported
  • DNSAPI.dll: DNS queries are supported
  • KERNEL32.dll: threading and file I/O are supported
$ x86_64-w64-mingw32-objdump -p spambot.exe | grep "DLL Name"                                                    
	DLL Name: WS2_32.dll
	DLL Name: WININET.dll
	DLL Name: IPHLPAPI.DLL
	DLL Name: RPCRT4.dll
	DLL Name: DNSAPI.dll
	DLL Name: KERNEL32.dll
	DLL Name: USER32.dll
	DLL Name: ADVAPI32.dll
	DLL Name: ole32.dll
	DLL Name: OLEAUT32.dll

Prerequisites

After the initial analysis we can move to a more in-depth analysis using Ghidra. But before we do that, we need to do some preparations.

GhidraFindcrypt

GhidraFindcrypt is a Ghidra plugin that identifies cryptographic algorithms by scanning the binary for known magic constants, IVs, etc.. Automatically adds labels during auto-analysis (e.g. CRYPT_Blowfish_p_init and CRYPT_CRC32_m_tab).

BSim

BSim is a Ghidra plugin that matches unknown functions against known libraries using cosine similarity on decompiler-derived feature vectors.

Since we know the compiler and OpenSSL versions (Microsoft Visual C/C++(18.00.40629)[C] and OpenSSL 1.0.2g 1 Mar 2016) we can prepare some binaries that will greatly help/speed up the reversing process. This means that we need to build OpenSSL from source using that compiler version. We can use the build artifacts (crypto and SSL libs) with BSim to resolve functions based on similarity. We can do the same with libc functions, for that we need the libc library that is shipped with the compiler (libcmt.lib). We will also need the .pdb for each file as well (debug info/symbol names).

Prerequisites:

Build instructions (refer to INSTALL.W32 in the OpenSSL sources root):

  • install the prerequisites
  • unzip the OpenSSL sources
  • open a cmd prompt
  • cd "C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC"
  • vcvarsall x86 (setup the build env)
  • cd into the root of the OpenSSL sources
  • perl Configure VC-WIN32 --prefix=C:\openssl
  • ms\do_nasm
  • nmake -f ms\ntdll.mak

We need the following built libraries:

  • libeay32.dll (crypto) + libeay32.pdb for symbol names
  • ssleay32.dll (ssl) + ssleay32.pdb for symbol names

Additionally, we need libc from the compiler install folder with the common functions (e.g. strcpy):

  • libcmt.lib + libcmt.pdb for symbol names

We manually batch import libcmt.lib (1274 obj files), then auto-analyze them all via cmd line:

/home/gemesa/git-repos/ghidra/build/dist/ghidra_12.1_DEV/support/analyzeHeadless /home/gemesa/git-repos/group-ib/ghidra-project group-ib/libcmt.lib -process -recursive

Then we generate and commit signatures to our BSim server:

./support/bsim generatesigs ghidra:/home/gemesa/git-repos/group-ib/ghidra-project/group-ib.gpr?/libcmt.lib ~/git-repos/bsim-xml/ --bsim postgresql://localhost/repo
./support/bsim commitsigs postgresql://localhost/repo ~/git-repos/bsim-xml/

After this, we can use these signatures while manually reversing the binary. For example, we can use BSim to identify the unknown FUN_00540490 function as _strcpy.

BSim strcpy

Or, for example, we can identify the unknown function FUN_00417380 as md5_block_asm_data_order:

BSim md5_block_asm_data_order

We could blindly mass-replace the unknown functions with recognized ones, but that could introduce a lot of false positives which could make the analysis harder in the end. So we will just use this while going through the code manually.

Ordinal mapping

Also, since some functions are imported via ordinal, we need an ordinal to name mapping. It might be worth writing a Ghidra script to automatically add the name as a comment to each line but we will just do manual lookup each time we need to.

Ordinal

We can get it like this:

$ x86_64-w64-mingw32-objdump -p ws2_32.dll | grep -A 1000 "Export Table"
The Export Tables (interpreted .rdata section contents)

...
[Ordinal/Name Pointer] Table -- Ordinal Base 1
	          Ordinal   Hint Name
	[  23] +base[  24]  0000 FreeAddrInfoEx
	[  24] +base[  25]  0001 FreeAddrInfoExW
	[  25] +base[  26]  0002 FreeAddrInfoW
	[  26] +base[  27]  0003 GetAddrInfoExA
	[  27] +base[  28]  0004 GetAddrInfoExCancel
	[  28] +base[  29]  0005 GetAddrInfoExOverlappedResult
	[  29] +base[  30]  0006 GetAddrInfoExW
	[  30] +base[  31]  0007 GetAddrInfoW
	[  31] +base[  32]  0008 GetHostNameW
	[  32] +base[  33]  0009 GetNameInfoW
	[  33] +base[  34]  000a InetNtopW
	[  34] +base[  35]  000b InetPtonW
	[  35] +base[  36]  000c ProcessSocketNotifications
	[  36] +base[  37]  000d SetAddrInfoExA
	[  37] +base[  38]  000e SetAddrInfoExW
	[ 499] +base[ 500]  000f WEP
	[  38] +base[  39]  0010 WPUCompleteOverlappedRequest
	[  39] +base[  40]  0011 WPUGetProviderPathEx
	[  40] +base[  41]  0012 WSAAccept
	[  41] +base[  42]  0013 WSAAddressToStringA
	[  42] +base[  43]  0014 WSAAddressToStringW
	[  43] +base[  44]  0015 WSAAdvertiseProvider
	[ 101] +base[ 102]  0016 WSAAsyncGetHostByAddr
	[ 102] +base[ 103]  0017 WSAAsyncGetHostByName
	[ 104] +base[ 105]  0018 WSAAsyncGetProtoByName
	[ 103] +base[ 104]  0019 WSAAsyncGetProtoByNumber
	[ 106] +base[ 107]  001a WSAAsyncGetServByName
	[ 105] +base[ 106]  001b WSAAsyncGetServByPort
	[ 100] +base[ 101]  001c WSAAsyncSelect
	[ 107] +base[ 108]  001d WSACancelAsyncRequest
	[ 112] +base[ 113]  001e WSACancelBlockingCall
	[ 115] +base[ 116]  001f WSACleanup
	[  44] +base[  45]  0020 WSACloseEvent
	[  45] +base[  46]  0021 WSAConnect
	[  46] +base[  47]  0022 WSAConnectByList
	[  47] +base[  48]  0023 WSAConnectByNameA
	[  48] +base[  49]  0024 WSAConnectByNameW
	[  49] +base[  50]  0025 WSACreateEvent
	[  57] +base[  58]  0026 WSADuplicateSocketA
	[  58] +base[  59]  0027 WSADuplicateSocketW
	[  59] +base[  60]  0028 WSAEnumNameSpaceProvidersA
	[  60] +base[  61]  0029 WSAEnumNameSpaceProvidersExA
	[  61] +base[  62]  002a WSAEnumNameSpaceProvidersExW
	[  62] +base[  63]  002b WSAEnumNameSpaceProvidersW
	[  63] +base[  64]  002c WSAEnumNetworkEvents
	[  64] +base[  65]  002d WSAEnumProtocolsA
	[  65] +base[  66]  002e WSAEnumProtocolsW
	[  66] +base[  67]  002f WSAEventSelect
	[ 110] +base[ 111]  0030 WSAGetLastError
	[  67] +base[  68]  0031 WSAGetOverlappedResult
	[  68] +base[  69]  0032 WSAGetQOSByName
	[  69] +base[  70]  0033 WSAGetServiceClassInfoA
	[  70] +base[  71]  0034 WSAGetServiceClassInfoW
	[  71] +base[  72]  0035 WSAGetServiceClassNameByClassIdA
	[  72] +base[  73]  0036 WSAGetServiceClassNameByClassIdW
	[  73] +base[  74]  0037 WSAHtonl
	[  74] +base[  75]  0038 WSAHtons
	[  75] +base[  76]  0039 WSAInstallServiceClassA
	[  76] +base[  77]  003a WSAInstallServiceClassW
	[  77] +base[  78]  003b WSAIoctl
	[ 113] +base[ 114]  003c WSAIsBlocking
	[  78] +base[  79]  003d WSAJoinLeaf
	[  79] +base[  80]  003e WSALookupServiceBeginA
	[  80] +base[  81]  003f WSALookupServiceBeginW
	[  81] +base[  82]  0040 WSALookupServiceEnd
	[  82] +base[  83]  0041 WSALookupServiceNextA
	[  83] +base[  84]  0042 WSALookupServiceNextW
	[  84] +base[  85]  0043 WSANSPIoctl
	[  85] +base[  86]  0044 WSANtohl
	[  86] +base[  87]  0045 WSANtohs
	[  87] +base[  88]  0046 WSAPoll
	[  88] +base[  89]  0047 WSAProviderCompleteAsyncCall
	[  89] +base[  90]  0048 WSAProviderConfigChange
	[  90] +base[  91]  0049 WSARecv
	[  91] +base[  92]  004a WSARecvDisconnect
	[  92] +base[  93]  004b WSARecvFrom
	[  93] +base[  94]  004c WSARemoveServiceClass
	[  94] +base[  95]  004d WSAResetEvent
	[  95] +base[  96]  004e WSASend
	[  96] +base[  97]  004f WSASendDisconnect
	[  97] +base[  98]  0050 WSASendMsg
	[  98] +base[  99]  0051 WSASendTo
	[ 108] +base[ 109]  0052 WSASetBlockingHook
	[  99] +base[ 100]  0053 WSASetEvent
	[ 111] +base[ 112]  0054 WSASetLastError
	[ 116] +base[ 117]  0055 WSASetServiceA
	[ 117] +base[ 118]  0056 WSASetServiceW
	[ 118] +base[ 119]  0057 WSASocketA
	[ 119] +base[ 120]  0058 WSASocketW
	[ 114] +base[ 115]  0059 WSAStartup
	[ 120] +base[ 121]  005a WSAStringToAddressA
	[ 121] +base[ 122]  005b WSAStringToAddressW
	[ 122] +base[ 123]  005c WSAUnadvertiseProvider
	[ 109] +base[ 110]  005d WSAUnhookBlockingHook
	[ 123] +base[ 124]  005e WSAWaitForMultipleEvents
	[ 124] +base[ 125]  005f WSApSetPostRoutine
	[ 125] +base[ 126]  0060 WSCDeinstallProvider
	[ 126] +base[ 127]  0061 WSCDeinstallProvider32
	[ 127] +base[ 128]  0062 WSCDeinstallProviderEx
	[ 128] +base[ 129]  0063 WSCEnableNSProvider
	[ 129] +base[ 130]  0064 WSCEnableNSProvider32
	[ 130] +base[ 131]  0065 WSCEnumNameSpaceProviders32
	[ 131] +base[ 132]  0066 WSCEnumNameSpaceProvidersEx32
	[ 132] +base[ 133]  0067 WSCEnumProtocols
	[ 133] +base[ 134]  0068 WSCEnumProtocols32
	[ 134] +base[ 135]  0069 WSCEnumProtocolsEx
	[ 135] +base[ 136]  006a WSCGetApplicationCategory
	[ 136] +base[ 137]  006b WSCGetApplicationCategoryEx
	[ 137] +base[ 138]  006c WSCGetProviderInfo
	[ 138] +base[ 139]  006d WSCGetProviderInfo32
	[ 139] +base[ 140]  006e WSCGetProviderPath
	[ 140] +base[ 141]  006f WSCGetProviderPath32
	[ 141] +base[ 142]  0070 WSCInstallNameSpace
	[ 142] +base[ 143]  0071 WSCInstallNameSpace32
	[ 143] +base[ 144]  0072 WSCInstallNameSpaceEx
	[ 144] +base[ 145]  0073 WSCInstallNameSpaceEx2
	[ 145] +base[ 146]  0074 WSCInstallNameSpaceEx32
	[ 146] +base[ 147]  0075 WSCInstallProvider
	[ 147] +base[ 148]  0076 WSCInstallProvider64_32
	[ 148] +base[ 149]  0077 WSCInstallProviderAndChains64_32
	[ 149] +base[ 150]  0078 WSCInstallProviderEx
	[ 151] +base[ 152]  0079 WSCSetApplicationCategory
	[ 152] +base[ 153]  007a WSCSetApplicationCategoryEx
	[ 153] +base[ 154]  007b WSCSetProviderInfo
	[ 154] +base[ 155]  007c WSCSetProviderInfo32
	[ 155] +base[ 156]  007d WSCUnInstallNameSpace
	[ 156] +base[ 157]  007e WSCUnInstallNameSpace32
	[ 157] +base[ 158]  007f WSCUnInstallNameSpaceEx2
	[ 158] +base[ 159]  0080 WSCUpdateProvider
	[ 159] +base[ 160]  0081 WSCUpdateProvider32
	[ 160] +base[ 161]  0082 WSCUpdateProviderEx
	[ 161] +base[ 162]  0083 WSCWriteNameSpaceOrder
	[ 162] +base[ 163]  0084 WSCWriteNameSpaceOrder32
	[ 163] +base[ 164]  0085 WSCWriteProviderOrder
	[ 164] +base[ 165]  0086 WSCWriteProviderOrder32
	[ 165] +base[ 166]  0087 WSCWriteProviderOrderEx
	[ 166] +base[ 167]  0088 WahCloseApcHelper
	[ 167] +base[ 168]  0089 WahCloseHandleHelper
	[ 168] +base[ 169]  008a WahCloseNotificationHandleHelper
	[ 169] +base[ 170]  008b WahCloseSocketHandle
	[ 170] +base[ 171]  008c WahCloseThread
	[ 171] +base[ 172]  008d WahCompleteRequest
	[ 172] +base[ 173]  008e WahCreateHandleContextTable
	[ 173] +base[ 174]  008f WahCreateNotificationHandle
	[ 174] +base[ 175]  0090 WahCreateSocketHandle
	[ 175] +base[ 176]  0091 WahDestroyHandleContextTable
	[ 176] +base[ 177]  0092 WahDisableNonIFSHandleSupport
	[ 177] +base[ 178]  0093 WahEnableNonIFSHandleSupport
	[ 178] +base[ 179]  0094 WahEnumerateHandleContexts
	[ 179] +base[ 180]  0095 WahInsertHandleContext
	[ 180] +base[ 181]  0096 WahNotifyAllProcesses
	[ 181] +base[ 182]  0097 WahOpenApcHelper
	[ 182] +base[ 183]  0098 WahOpenCurrentThread
	[ 183] +base[ 184]  0099 WahOpenHandleHelper
	[ 184] +base[ 185]  009a WahOpenNotificationHandleHelper
	[ 185] +base[ 186]  009b WahQueueUserApc
	[ 186] +base[ 187]  009c WahReferenceContextByHandle
	[ 187] +base[ 188]  009d WahRemoveHandleContext
	[ 188] +base[ 189]  009e WahWaitForNotification
	[ 189] +base[ 190]  009f WahWriteLSPEvent
	[ 150] +base[ 151]  00a0 __WSAFDIsSet
	[   0] +base[   1]  00a1 accept
	[   1] +base[   2]  00a2 bind
	[   2] +base[   3]  00a3 closesocket
	[   3] +base[   4]  00a4 connect
	[ 190] +base[ 191]  00a5 freeaddrinfo
	[ 191] +base[ 192]  00a6 getaddrinfo
	[  50] +base[  51]  00a7 gethostbyaddr
	[  51] +base[  52]  00a8 gethostbyname
	[  56] +base[  57]  00a9 gethostname
	[ 192] +base[ 193]  00aa getnameinfo
	[   4] +base[   5]  00ab getpeername
	[  52] +base[  53]  00ac getprotobyname
	[  53] +base[  54]  00ad getprotobynumber
	[  54] +base[  55]  00ae getservbyname
	[  55] +base[  56]  00af getservbyport
	[   5] +base[   6]  00b0 getsockname
	[   6] +base[   7]  00b1 getsockopt
	[   7] +base[   8]  00b2 htonl
	[   8] +base[   9]  00b3 htons
	[  10] +base[  11]  00b4 inet_addr
	[  11] +base[  12]  00b5 inet_ntoa
	[ 193] +base[ 194]  00b6 inet_ntop
	[ 194] +base[ 195]  00b7 inet_pton
	[   9] +base[  10]  00b8 ioctlsocket
	[  12] +base[  13]  00b9 listen
	[  13] +base[  14]  00ba ntohl
	[  14] +base[  15]  00bb ntohs
	[  15] +base[  16]  00bc recv
	[  16] +base[  17]  00bd recvfrom
	[  17] +base[  18]  00be select
	[  18] +base[  19]  00bf send
	[  19] +base[  20]  00c0 sendto
	[  20] +base[  21]  00c1 setsockopt
	[  21] +base[  22]  00c2 shutdown
	[  22] +base[  23]  00c3 socket
...

Detailed analysis (Ghidra)

We use the following convention while renaming unknown functions: FUN_00540490 –> FUN_00540490__strcpy.

We start by sorting the functions by reference count and by renaming a couple of std lib functions that are used hundreds of times in the code.

Note: some functions are already renamed by Ghidra’s FID (FunctionID) analyzer. The Ghidra developers maintain some FID databases with common signatures.

Functions (ref count)

Renamed functions:

  • FUN_00540490_strcpy
  • FUN_005404a0_strcat
  • FUN_00540c70_strchr
  • FUN_00540da0_str_str
  • FUN_005410ca_sprintf
  • FUN_00541f40_memset
  • FUN_0053fe40_memcpy

entry, WinMain, initialization

Go to entry and identify WinMain based on the signature

int entry(void)

{
...
  pauVar3 = (undefined1 (*) [16])__wincmdln();
  iVar2 = FUN_0046b060_WinMain(0x400000,0,pauVar3);
                    /* WARNING: Subroutine does not return */
  _exit(iVar2);
}

In WinMain:

  • COM initialization via CoInitialize
  • FUN_0046dd00_app_ctx_init: init for multithreading
  • Winsock initialization via Ordinal_115/WSAStartup
  • cmdline dispatch via FUN_0046b170_cmdline_dispatch
void FUN_0046b060_WinMain(undefined4 param_1,undefined4 param_2,undefined1 (*cmdline) [16])

{
...
  CoInitialize((LPVOID)0x0);
  FUN_0046dd00_app_ctx_init(&DAT_005a6d0c,0,param_1,(undefined4 *)0x0);
                    /* WSAStartup */
  Ordinal_115(0x202,local_1b0);
  local_8 = 0;
  FUN_0046b170_cmdline_dispatch(cmdline);
...
}

Command line arguments

Depending on the passed command line arguments (-r, -u, no args), different behavior:

  • no args (first run)
    • install service (SERVICE_AUTO_START, ImagePath = "spambot.exe -r")
    • run the main bot logic (detailed later)
  • -r (every boot, launched by SCM)
    • register with SCM (handshake + control handler)
    • run the main bot logic (detailed later)
  • -u (uninstall)
    • stop service, delete service, clean up
void __cdecl FUN_0046b170_cmdline_dispatch(undefined1 (*param_1) [16])

{
...
                    /* FUN_00465220_svc_set_name sets DAT_005a6be8_svc_obj = this (the service
                       object)
                       then *this is overwritten to &PTR_FUN_005677a0_vtable
                       where PTR_FUN_005677a0_vtable[0] = FUN_00472da0_bot_main
                       
                       so the dispatch chain in FUN_004650a0_svc_thread_proc is:
                          (**(code **)*DAT_005a6be8_svc_obj)()
                          = (**(code **)svc_obj)()
                          = (*(PTR_FUN_005677a0_vtable[0]))()
                          = FUN_00472da0_bot_main() */
  FUN_00472d50_svc_init(local_140);
...
                    /* service dispatch */
  if ((param_1 != (undefined1 (*) [16])0x0) && ((*param_1)[0] != '\0')) {
    pauVar1 = FUN_00540da0_str_str(param_1,(undefined1 (*) [16])"-r");
    if (pauVar1 != (undefined1 (*) [16])0x0) {
      FUN_004656c0_service_dispatch((int)local_140);
...
    }
                    /* service uninstall */
    pauVar1 = FUN_00540da0_str_str(param_1,(undefined1 (*) [16])"-u");
    if (pauVar1 != (undefined1 (*) [16])0x0) {
      FUN_00465570_service_uninstall((int)local_140);
...
    }
  }
  FUN_00472da0_bot_main();
...
}

Bot main

Config decryption

Config decryption is done via Blowfish-ECB and a hardcoded 16 byte key. The encrypted config is stored under resource 0xE2. The format is the following:

"host:port1/port2;ServiceName"

Using an obsolete algorithm such as Blowfish might seem like a weakness at first glance. But using a modern alternative (e.g. AES) would not help here because the key is hardcoded in both cases.

void FUN_00472da0_bot_main(void)

{
...
                    /* 0xe2 = resource ID
                       0xa = RT_RCDATA = Application-defined resource (raw data). */
  hRes = FindResourceA((HMODULE)0x0,&DAT_000000e2,&DAT_0000000a);
  hGlob = LoadResource((HMODULE)0x0,hRes);
  pData = LockResource(hGlob);
  FUN_00540490_strcpy(buf,pData);
  len = FUN_0044e5b0_hex_decode(decoded,(char *)buf,0x100);
  if (0 < (int)len) {
    key[0] = 7;
    key[1] = 0x54;
    key[2] = 8;
    key[3] = 0xd5;
    key[4] = 0x67;
    key[5] = 0x85;
    key[6] = 0x9e;
    key[7] = 0xe2;
    key[8] = 0xaf;
    key[9] = 0xe3;
    key[10] = 0xc6;
    key[0xb] = 0xa9;
    key[0xc] = 0x8f;
    key[0xd] = 0x3b;
    key[0xe] = 0xc;
    key[0xf] = 0x5b;
    iv = 0;
    local_5ff8 = 0;
                    /* FUN_00421d20 is recognized by capa (Data::Blowfish) */
    FUN_00421d20_blowfish_key_schedule(bf_ctx,(undefined8 *)key,0x10,&iv);
                    /* processes 8-byte blocks, no IV/chaining = ECB mode */
    FUN_004220e0_blowfish_ecb_decrypt(bf_ctx,(byte *)decoded,len,0);
    FUN_00540490_strcpy(buf,decoded);
  }
                    /* default port1 */
  port1 = 50001;
                    /* default port2 */
  port2 = 50002;
  p1 = FUN_00540c70_strchr(buf,':');
  p3 = (uint *)0x0;
  if (p1 != (uint *)0x0) {
    *(undefined1 *)p1 = 0;
    p1 = (uint *)((int)p1 + 1);
    p2 = FUN_00540c70_strchr(p1,'/');
    if (p2 != (uint *)0x0) {
      *(undefined1 *)p2 = 0;
      p2 = (uint *)((int)p2 + 1);
      p3 = FUN_00540c70_strchr(p2,';');
      if (p3 != (uint *)0x0) {
        *(undefined1 *)p3 = 0;
        p3 = (uint *)((int)p3 + 1);
      }
      port2 = _atol((char *)p2);
    }
    port1 = _atol((char *)p1);
  }
...

The Blowfish algorithm can be recognized based on the P-array and S-box constants:

                             CRYPT_Blowfish_p_init                           XREF[1]:     FUN_00421d20_blowfish_key_schedu
        005605c0 88 6a 3f        db[32]
                 24 d3 08 
                 a3 85 2e 
           005605c0 [0]            88h, 6Ah, 3Fh, 24h,
           005605c4 [4]            D3h,  8h, A3h, 85h,
           005605c8 [8]            2Eh, 8Ah, 19h, 13h,
           005605cc [12]           44h, 73h, 70h,  3h,
           005605d0 [16]           22h, 38h,  9h, A4h,
           005605d4 [20]           D0h, 31h, 9Fh, 29h,
           005605d8 [24]           98h, FAh, 2Eh,  8h,
           005605dc [28]           89h, 6Ch, 4Eh, ECh
                             CRYPT_Blowfish_s_init (005605e0+0x28)           XREF[0,1]:   FUN_00421d20_blowfish_key_schedu
        005605e0 e6 21 28        db[128]
                 45 77 13 
                 d0 38 cf 
           005605e0 [0]            E6h, 21h, 28h, 45h,
           005605e4 [4]            77h, 13h, D0h, 38h,
           005605e8 [8]            CFh, 66h, 54h, BEh,
           005605ec [12]           6Ch,  Ch, E9h, 34h,
           005605f0 [16]           B7h, 29h, ACh, C0h,
           005605f4 [20]           DDh, 50h, 7Ch, C9h,
           005605f8 [24]           B5h, D5h, 84h, 3Fh,
           005605fc [28]           17h,  9h, 47h, B5h,
           00560600 [32]           D9h, D5h, 16h, 92h,
           00560604 [36]           1Bh, FBh, 79h, 89h,
           00560608 [40]           A6h,  Bh, 31h, D1h,
           0056060c [44]           ACh, B5h, DFh, 98h,
           00560610 [48]           DBh, 72h, FDh, 2Fh,
           00560614 [52]           B7h, DFh, 1Ah, D0h,
           00560618 [56]           EDh, AFh, E1h, B8h,
           0056061c [60]           96h, 7Eh, 26h, 6Ah,
           00560620 [64]           45h, 90h, 7Ch, BAh,
           00560624 [68]           99h, 7Fh, 2Ch, F1h,
           00560628 [72]           47h, 99h, A1h, 24h,
           0056062c [76]           F7h, 6Ch, 91h, B3h,
           00560630 [80]           E2h, F2h,  1h,  8h,
           00560634 [84]           16h, FCh, 8Eh, 85h,
           00560638 [88]           D8h, 20h, 69h, 63h,
           0056063c [92]           69h, 4Eh, 57h, 71h,
           00560640 [96]           A3h, FEh, 58h, A4h,
           00560644 [100]          7Eh, 3Dh, 93h, F4h,
           00560648 [104]          8Fh, 74h, 95h,  Dh,
           0056064c [108]          58h, B6h, 8Eh, 72h,
           00560650 [112]          58h, CDh, 8Bh, 71h,
           00560654 [116]          EEh, 4Ah, 15h, 82h,
           00560658 [120]          1Dh, A4h, 54h, 7Bh,
           0056065c [124]          B5h, 59h, 5Ah, C2h

capa also recognizes it at address 0x421DAA:

encrypt data using blowfish
namespace  data-manipulation/encryption/blowfish                                
author     @_re_fox                                                             
scope      basic block                                                          
att&ck     Defense Evasion::Obfuscated Files or Information [T1027]             
mbc        Defense Evasion::Obfuscated Files or Information::Encryption-Standard
           Algorithm [E1027.m05], Cryptography::Encrypt Data::Blowfish          
           [C0027.002]                                                          
basic block @ 0x421D7B in function 0x421D20
  or:
    or:
      bytes: 886a3f24d308a3852e8a191344737003223809a4d0319f2998fa2e08896c4eece62128457713d038
cf6654be6c0ce934b729acc0dd507cc9b5d5843f170947b5d9d516921bfb7989 = ps @ 0x421D91
      bytes: a60b31d1acb5df98db72fd2fb7df1ad0edafe1b8967e266a45907cba997f2cf14799a124f76c91b3
e2f2010816fc8e85d8206963694e5771a3fe58a47e3d93f48f74950d58b68e7258cd8b71ee4a1582
1da4547bb5595ac239d5309c1360f22a23b0d1c5f0856028187941caef38dbb8b0dc798e0e183a60
8b0e9e6c3e8a1eb0c17715d7274b31bdda2faf78605c6055f32555e694ab55aa629848574014e863
6a39ca55b610ab2a345ccc = ks0 sbox1 @ 0x421DAA

Service installation

After the config is decrypted, the service is installed via FUN_00465300_install_service:

void FUN_00472da0_bot_main(void)
...
                    /* default service name: Enterprise Mailing Service */
  if (p3 == (uint *)0x0) {
    FUN_00465300_install_service("Enterprise Mailing Service",(uint *)"-r",(LPCSTR)0x0,'\x01');
  }
  else {
    FUN_00465300_install_service((LPCSTR)p3,(uint *)"-r",(LPCSTR)0x0,'\x01');
  }
...
void FUN_00465300_install_service
               (LPCSTR svc_name_param,uint *cmd_line_param,LPCSTR lpDependencies,char param_4)

{
...
    GetModuleFileNameA((HMODULE)0x0,(LPSTR)exe_path,0x7f8);
    if (cmd_line_param != (uint *)0x0) {
      FUN_00540490_strcpy(exe_path_with_flag,exe_path);
      FUN_005404a0_strcat(exe_path_with_flag,(uint *)" ");
      FUN_005404a0_strcat(exe_path_with_flag,cmd_line_param);
    }
    if (param_4 == '\0') {
      dwStartType = 3;
    }
    else {
      dwStartType = SERVICE_AUTO_START;
    }
    if (svc_name_param == (LPCSTR)0x0) {
      display_name = (LPCSTR)(svc_name + 4);
    }
    else {
      display_name = svc_name_param;
    }
    local_1018 = CreateServiceA(hSCM,(LPCSTR)(svc_name + 4),display_name,SERVICE_ALL_ACCESS,
                                SERVICE_WIN32_OWN_PROCESS,dwStartType,SERVICE_ERROR_NORMAL,
                                (LPCSTR)exe_path_with_flag,(LPCSTR)0x0,(LPDWORD)0x0,lpDependencies,
                                (LPCSTR)0x0,(LPCSTR)0x0);
...
}

It also registers Event Logs, probably to avoid broken event log messages. This helps the malware to blend in with legitimate services.

void FUN_00465300_install_service
               (LPCSTR svc_name_param,uint *cmd_line_param,LPCSTR lpDependencies,char param_4)

{
...
                    /* Event Log registration, probably just to blend in
                       and avoid broken event log messages */
      local_100c = RegOpenKeyA((HKEY)&DAT_80000002,
                               "SYSTEM\\CurrentControlSet\\Services\\EventLog\\Application\\",
                               &local_102c);
      if ((local_100c == 0) &&
         (local_100c = RegCreateKeyA(local_102c,(LPCSTR)(svc_name + 4),&local_101c), local_100c == 0
         )) {
        sVar3 = _strlen((char *)exe_path);
        local_100c = RegSetValueExA(local_101c,"EventMessageFile",0,1,(BYTE *)exe_path,sVar3 + 1);
        if (local_100c == 0) {
          local_1028[0] = '\a';
          local_1028[1] = '\0';
          local_1028[2] = '\0';
          local_1028[3] = '\0';
          local_100c = RegSetValueExA(local_101c,"TypesSupported",0,4,local_1028,4);
        }
      }
    }
  }
  FUN_0054114e(local_8 ^ (uint)&stack0xfffffffc);
  return;
}

Bot ID and thread spawn

In FUN_00472da0_bot_main we can see that an UUID is generated, probably as a victim ID. It is passed (along with the decrypted config parameters) to FUN_00429da0_bot_init:

void FUN_00472da0_bot_main(void)

{
...
  UuidCreate(&uuid);
  UuidToStringA(&uuid,&str);
  src_ptr = str;
  dst_ptr = bot_id;
                    /* copy the UUID, uppercase each char,
                       skip - characters */
  while (*src_ptr != '\0') {
    ch = (int)(char)*src_ptr;
    iVar3 = _toupper(ch);
    *(char *)dst_ptr = (char)iVar3;
    src_ptr = src_ptr + 1;
    if ((char)*dst_ptr != '-') {
      dst_ptr = (uint *)((int)dst_ptr + 1);
    }
  }
  *(char *)dst_ptr = '\0';
  RpcStringFreeA(&str);
  FUN_004295f0_bot_obj_ctor(bot_obj);
  local_8 = 0;
  FUN_00429da0_bot_init(bot_obj,buf,port1,port2,bot_id,0,'\x01');
...

FUN_00429da0_bot_init starts 3 threads:

void __thiscall
FUN_00429da0_bot_init
          (void *this,uint *param_1,undefined4 param_2,undefined4 param_3,uint *param_4,
          undefined4 param_5,char param_6)

{
...
  CreateThread((LPSECURITY_ATTRIBUTES)0x0,0,FUN_00426e80_thread_c2_udp,local_c,0,&local_14);
  CreateThread((LPSECURITY_ATTRIBUTES)0x0,0,FUN_004279a0_thread_http_downloads,local_c,0,&local_14);
  CreateThread((LPSECURITY_ATTRIBUTES)0x0,0,FUN_00427c90_thread_smtp_send,local_c,0,&local_14);
  return;
}

Thread 1: C2 protocol (UDP)

Static XOR key

The UDP packets are XOR encrypted but the key is hardcoded (there is no per-bot keying, per-session keying etc.) which is a critical vulnerability. This means every sample, every instance uses the same key globally. The C2 traffic can be decrypted.

void FUN_00426e80_thread_c2_udp(void *bot_obj_param)

{
...
      FUN_004680d0_xor_cipher((int)(tx_pkt + 1),pkt_len + -1);
                    /* closesocket */
      Ordinal_3(*(undefined4 *)((int)bot_obj + 0x54));
                    /* socket */
      uVar2 = Ordinal_23(2,2,0x11);
      *(undefined4 *)((int)bot_obj + 0x54) = uVar2;
      FUN_00541f40_memset((uint *)&server_addr,0,0x10);
                    /* htons */
      server_addr.sin_port = Ordinal_9(*(undefined2 *)((int)bot_obj + 0x29c8));
      server_addr.sin_family = 2;
      server_addr.sin_addr.S_un =
           (_union_1226)FUN_00435ed0_resolve_host((uint *)((int)bot_obj + 0x2944));
      sock_timeout = 10000;
                    /* setsockopt */
      Ordinal_21(*(undefined4 *)((int)bot_obj + 0x54),0xffff,0x1005,&sock_timeout,4);
      sock_broadcast = 0;
                    /* setsockopt */
      Ordinal_21(*(undefined4 *)((int)bot_obj + 0x54),0xffff,0x1001,&sock_broadcast,4);
                    /* sendto */
      iVar3 = Ordinal_20(*(undefined4 *)((int)bot_obj + 0x54),tx_pkt,pkt_len,0,&server_addr,0x10);
...
}

The encryption is implemented in FUN_004680d0_xor_cipher by MD5 hashing the hardcoded string "UNREGISTERED". This always produces the same 16 byte key.

void __cdecl FUN_004680d0_xor_cipher(int buf_param,int len_param)

{
  uint i;
  byte key [16];
  uint local_8;
  
  local_8 = DAT_005a5590 ^ (uint)&stack0xfffffffc;
  FUN_0044b900_md5_hash((int)key,(undefined8 *)"UNREGISTERED",12);
  for (i = 0; (int)i < len_param; i = i + 1) {
    *(byte *)(buf_param + i) = *(byte *)(buf_param + i) ^ key[i & 0xf];
  }
  FUN_0054114e(local_8 ^ (uint)&stack0xfffffffc);
  return;
}

The MD5 IV constants are present in FUN_0044bc70_md5_init:

void __cdecl FUN_0044b900_md5_hash(int param_1,undefined8 *param_2,uint param_3)

{
  undefined8 *_Memory;
  uint local_60 [22];
  uint local_8;
  
  local_8 = DAT_005a5590 ^ (uint)&stack0xfffffffc;
  _Memory = (undefined8 *)FUN_0053e123(param_3 + 8);
  FUN_0053fe40_memcpy(_Memory,param_2,param_3 + 1);
  FUN_0044bc70_md5_init(local_60);
  FUN_0044bcf0_md5_update((int *)local_60,_Memory,param_3);
  FUN_0044be40_md5_final(param_1,local_60);
  FID_conflict:_free(_Memory);
  FUN_0054114e(local_8 ^ (uint)&stack0xfffffffc);
  return;
}
void __cdecl FUN_0044bc70_md5_init(undefined4 *param_1)

{
  param_1[5] = 0;
  param_1[4] = 0;
  *param_1 = 0x67452301;
  param_1[1] = 0xefcdab89;
  param_1[2] = 0x98badcfe;
  param_1[3] = 0x10325476;
  return;
}

capa also recognizes the MD5 algorithm at address 0x44BC70:

hash data with MD5
namespace   data-manipulation/hashing/md5                                       
author      moritz.raabe@mandiant.com, anushka.virgaonkar@mandiant.com,         
            michael.hunhoff@mandiant.com                                        
scope       function                                                            
mbc         Cryptography::Cryptographic Hash::MD5 [C0029.001]                   
references  https://github.com/rwfpl/rewolf-x86-virtualizer/blob/master/src/tes…
function @ 0x44BC70
  or:
    and: = magic initialization constants from MD4 and MD5
      number: 0x67452301 = A, also used in SHA1, RIPEMD-128, RIPEMD-160, RIPEMD-256, and RIPEMD-320 @ 0x44BCA4
      number: 0xEFCDAB89 = B, also used in SHA1, RIPEMD-128, RIPEMD-160, RIPEMD-256, and RIPEMD-320 @ 0x44BCB6
      number: 0x98BADCFE = C, also used in SHA1, RIPEMD-128, RIPEMD-160, RIPEMD-256, and RIPEMD-320 @ 0x44BCC7
      number: 0x10325476 = D, also used in SHA1, RIPEMD-128, RIPEMD-160, RIPEMD-256, and RIPEMD-320 @ 0x44BCD9
      not:
        number: 0xC3D2E1F0 = likely SHA1 but also used in RIPEMD-160 and RIPEMD-320

No C2 authentication

Furthermore, there is no proper validation for the received C2 packets/commands. Only the length (24 bytes) and the command type (0. byte = 2) are verified. There is no authentication implemented which is another critical vulnerability. This means that C2 commands can be forged and the botnet can be disrupted and hijacked.

void FUN_00426e80_thread_c2_udp(void *bot_obj_param)

{
...
                    /* setsockopt */
      Ordinal_21(*(undefined4 *)((int)bot_obj + 0x54),0xffff,0x1006,&sock_timeout,4);
                    /* recvfrom */
      recv_len = Ordinal_17(*(undefined4 *)((int)bot_obj + 0x54),&rx_type,0x18,0,&server_addr,
                            &addr_len);
      if ((recv_len == 0x18) && (rx_type == '\x02')) {
        if (*(char *)((int)bot_obj + 8) != '\0') break;
        FUN_004680d0_xor_cipher((int)rx_payload,0x17);
...

Thread 2: HTTP downloads

This thread is responsible for downloading the settings (SMTP credentials, recipient list, body templates, proxies etc.).

undefined4 FUN_004279a0_thread_http_downloads(void *bot_obj_param)

{
...
                    /* SMTP credentials, etc */
    iVar3 = FUN_00466b60_http_download(1,bot_obj,&download_ctx);
...

TLS cert validation disabled

void FUN_00466b60_http_download(undefined4 param_1,undefined4 param_2,undefined4 *param_3)

{
...
      local_254 = HttpOpenRequestA(hConn,"GET",local_114,0,0,0,0x80803100,0);
...

The following Wininet.h security options are used:

  • SECURITY_FLAG_IGNORE_CERT_DATE_INVALID: 0x00002000
  • SECURITY_FLAG_IGNORE_CERT_CN_INVALID: 0x00001000
  • SECURITY_FLAG_IGNORE_UNKNOWN_CA: 0x00000100

This is another critical vulnerability, as these make the bot accept any certificate. This makes MITM trivial.

No download authentication check

Combining the previous vulnerability with this one (no authentication at any layer when processing downloaded files) means that if we know the XOR key, we can craft settings/campaign data and the bots will blindly accept/apply them.

The processing steps are the following:

  • base64 decode
  • XOR (static key)
  • ZIP extract
  • apply settings
void FUN_00466b60_http_download(undefined4 param_1,undefined4 param_2,undefined4 *param_3)

{
...
      local_254 = HttpOpenRequestA(hConn,"GET",local_114,0,0,0,0x80803100,0);
...
            iVar1 = InternetReadFile(local_254,(int)recv_buf + recv_len,recv_buf_size - recv_len,
                                     &bytes_read);
...
            decoded_len = FUN_0044ee50_base64_decode
                                    ((byte *)(decoded_buf + 1),(uint *)((int)recv_buf + 2),'\x01');
...
            FUN_004680d0_xor_cipher((int)(decoded_ptr + 1),decoded_len);
...
            zip_handle = FUN_00473270_zip_open(decoded_ptr,decoded_len + 4,(uint *)0x0);
...
              while (iVar1 = FUN_004733a0_zip_enum_entry(zip_handle,zip_index,&zip_entry_info),
...
                zip_result = FUN_00473480_zip_extract_entry
                                       (zip_handle,zip_index,(int)zip_entry_buf,zip_entry_size + 1);
...
                  FUN_004659b0_apply_config_entry(local_244,entry_id,(int)recv_buf,zip_entry_size);
...

Thread 3: SMTP sending engine

Thread 2 downloads campaign data, then spawns the SMTP workers. Thread 3 manages the task queue.

STARTTLS stripping

The SMTP authentication method is selected by searching the EHLO response string for keywords in order: STARTTLS –> LOGIN –> CRAM-MD5 –> AUTH PLAIN (fallback). Since the EHLO exchange occurs before TLS negotiation (i.e. in plaintext), a MITM attacker can strip "STARTTLS" from the server response, forcing the bot to fall back to AUTH PLAIN over an unencrypted connection.

State Method Trigger
0x13B0 STARTTLS strstr(response, "STARTTLS") != NULL
0x13C4 AUTH LOGIN strstr(response, "LOGIN") != NULL
0x13CE CRAM-MD5 strstr(response, "CRAM-MD5") != NULL
0x13BA AUTH PLAIN fallback
void __thiscall FUN_004437d0_state_machine(void *this,void *param_1)

{
...
                  ((char *)((int)abStack_109 + 1),(byte *)"Got positive response for HELO from %s");
        FUN_004485c0(this,(int)param_1,(uint *)((int)abStack_109 + 1));
      }
      if ((*(int *)((int)this + 0x130) == 0) ||
...
      }
      else {
        pauVar9 = FUN_00540da0_str_str
                            ((undefined1 (*) [16])((int)this + 0x134),
                             (undefined1 (*) [16])"STARTTLS");
        if (pauVar9 == (undefined1 (*) [16])0x0) {
          pauVar9 = FUN_00540da0_str_str
                              ((undefined1 (*) [16])((int)this + 0x134),(undefined1 (*) [16])"LOGIN"
                              );
          if (pauVar9 == (undefined1 (*) [16])0x0) {
            pauVar9 = FUN_00540da0_str_str
                                ((undefined1 (*) [16])((int)this + 0x134),
                                 (undefined1 (*) [16])"CRAM-MD5");
            if (pauVar9 == (undefined1 (*) [16])0x0) {
                    /* fallback: plain auth */
              *(undefined4 *)((int)this + 0x1c) = 0x13ba;
            }
            else {
              *(undefined4 *)((int)this + 0x1c) = 0x13ce;
            }
          }
          else {
            *(undefined4 *)((int)this + 0x1c) = 0x13c4;
          }
        }
        else {
          *(undefined4 *)((int)this + 0x1c) = 0x13b0;
        }
...
  case 0x13ba:
    uVar10 = FUN_005410ca_sprintf((char *)((int)this + 0x134),(byte *)"AUTH PLAIN\r\n");
    *(undefined4 *)((int)this + 0x30) = uVar10;
    *(undefined4 *)((int)this + 0x3c) = 2;
    FUN_005410ca_sprintf((char *)((int)abStack_109 + 1),(byte *)"Sending AUTH PLAIN to %s");
    FUN_004485c0(this,(int)param_1,(uint *)((int)abStack_109 + 1));
    break;
...

HMAC-MD5 for CRAM-MD5

CRAM-MD5 is deprecated. The captured challenge-response pairs can be brute-forced offline since MD5 is fast. This is a medium vulnerability.

void __thiscall FUN_004437d0_state_machine(void *this,void *param_1)

{
...
  case 0x13d0:
...
    _Memory = (void *)FUN_0044bb80_hmac_md5((char *)puVar6,(char *)local_290);
void __cdecl FUN_0044bb80_hmac_md5(char *param_1,char *param_2)

{
...
  FUN_0044bf00_hmac_md5_core
            ((undefined8 *)param_1,sVar2,(undefined8 *)param_2,sVar1,(undefined8 *)pbVar4);
...
void __cdecl
FUN_0044bf00_hmac_md5_core
          (undefined8 *param_1,uint param_2,undefined8 *param_3,uint param_4,undefined8 *param_5)

{
  int local_154;
  uint local_150 [22];
  uint local_f8 [22];
  uint local_a0 [17];
  uint local_5c [17];
  undefined8 local_18 [2];
  uint local_8;
  
  local_8 = DAT_005a5590 ^ (uint)&stack0xfffffffc;
  if (0x40 < (int)param_4) {
    FUN_0044bc70_md5_init(local_150);
    FUN_0044bcf0_md5_update((int *)local_150,param_3,param_4);
    FUN_0044be40_md5_final((int)local_18,local_150);
    param_3 = local_18;
    param_4 = 0x10;
  }
  FUN_00541f40_memset(local_5c,0,0x41);
  FUN_00541f40_memset(local_a0,0,0x41);
  FUN_0053fe40_memcpy((undefined8 *)local_5c,param_3,param_4);
  FUN_0053fe40_memcpy((undefined8 *)local_a0,param_3,param_4);
  for (local_154 = 0; local_154 < 0x40; local_154 = local_154 + 1) {
    *(byte *)((int)local_5c + local_154) = *(byte *)((int)local_5c + local_154) ^ 0x36;
    *(byte *)((int)local_a0 + local_154) = *(byte *)((int)local_a0 + local_154) ^ 0x5c;
  }
  FUN_0044bc70_md5_init(local_f8);
  FUN_0044bcf0_md5_update((int *)local_f8,(undefined8 *)local_5c,0x40);
  FUN_0044bcf0_md5_update((int *)local_f8,param_1,param_2);
  FUN_0044be40_md5_final((int)param_5,local_f8);
  FUN_0044bc70_md5_init(local_f8);
  FUN_0044bcf0_md5_update((int *)local_f8,(undefined8 *)local_a0,0x40);
  FUN_0044bcf0_md5_update((int *)local_f8,param_5,0x10);
  FUN_0044be40_md5_final((int)param_5,local_f8);
  FUN_0054114e(local_8 ^ (uint)&stack0xfffffffc);
  return;
}

capa also recognizes the CRAM-MD5 algorithm at address 0x44BF00:

authenticate HMAC (3 matches)
namespace   data-manipulation/hmac                                              
author      moritz.raabe@mandiant.com                                           
scope       function                                                            
mbc         Cryptography::Hashed Message Authentication Code [C0061]            
references  https://tools.ietf.org/html/rfc2104,                                
            https://tools.ietf.org/html/rfc4634, https://github.com/ogay/hmac   
function @ 0x44BF00
  and:
    number: 0x36 = inner padding byte value @ 0x44BFDC
    number: 0x5C = outer padding byte value @ 0x44BFF7
    match: contain loop @ 0x44BF00
      or:
        characteristic: loop @ 0x44BF00
    count(characteristic(nzxor)): 2 or more @ 0x44BFDC, 0x44BFF7
    optional: = block size
      number: 0x40 = MD5, SHA-1, SHA-224, or SHA-256 @ 0x44BF13, 0x44BFC8, 0x44C018, 0x44C066

Traditional PKWARE Encryption (ZipCrypto)

Another high vulnerability is the use of the traditional PKWARE Encryption (ZipCrypto).

The bot creates password-protected ZIP attachments to bypass email gateway antivirus scanners. The encryption uses Traditional PKWARE Encryption (APPNOTE.TXT section 6.1.5), which the spec itself acknowledges as weak (section 6.0.1: “This form of encryption is considered weak by today’s standards”).

Can be cracked with bkcrack.

    6.1.5 Initializing the encryption keys
        
    Key(0) <- 305419896
    Key(1) <- 591751049
    Key(2) <- 878082192

    loop for i <- 0 to length(password)-1
        update_keys(password(i))
    end loop

    Where update_keys() is defined as:

    update_keys(char):
      Key(0) <- crc32(key(0),char)
      Key(1) <- Key(1) + (Key(0) & 000000ffH)
      Key(1) <- Key(1) * 134775813 + 1
      Key(2) <- crc32(key(2),key(1) >> 24)
    end update_keys

    Where crc32(old_crc,char) is a routine that given a CRC value and a
    character, returns an updated CRC value after applying the CRC-32
    algorithm described elsewhere in this document.
void __thiscall
FUN_0047f7b0_zip_and_add_file(void *this,uint *param_1,int param_2,int param_3,char param_4)

{
...
                    /* initialize the 3 ZipCrypto keys (PKWARE spec section 6.1.5) */
            *(undefined4 *)((int)this + 0x30) = 0x12345678;
            *(undefined4 *)((int)this + 0x34) = 0x23456789;
            *(undefined4 *)((int)this + 0x38) = 0x34567890;
                    /* loop through each byte of the password, updating keys */
            for (local_4ac = *(char **)this; (local_4ac != (char *)0x0 && (*local_4ac != '\0'));
                local_4ac = local_4ac + 1) {
              FUN_0047eb70_zipcrypto_update_keys((uint *)((int)this + 0x30),*local_4ac);
...
void __cdecl FUN_0047eb70_zipcrypto_update_keys(uint *param_1,char param_2)

{
  *param_1 = *param_1 >> 8 ^ *(uint *)(CRYPT_CRC32_m_tab + (((int)param_2 ^ *param_1) & 0xff) * 4);
  param_1[1] = (*param_1 & 0xff) + param_1[1];
  param_1[1] = param_1[1] * 0x8088405 + 1;
  param_1[2] = param_1[2] >> 8 ^
               *(uint *)(CRYPT_CRC32_m_tab + ((param_1[1] >> 0x18 ^ param_1[2]) & 0xff) * 4);
  return;
}

Where CRYPT_CRC32_m_tab is a precomputed CRC32 table:

                             CRYPT_CRC32_m_tab                               XREF[20]:    FUN_00474290:004742c8(*), 
...
                                                                                          FUN_0047eb70_zipcrypto_update_ke
                                                                                          [more]
        00567cf0 00 00 00        db[1024]
                 00 96 30 
                 07 77 2c 
           00567cf0 [0]             0h,  0h,  0h,  0h,
           00567cf4 [4]            96h, 30h,  7h, 77h,
           00567cf8 [8]            2Ch, 61h,  Eh, EEh,
           00567cfc [12]           BAh, 51h,  9h, 99h,
           00567d00 [16]           19h, C4h, 6Dh,  7h,
           00567d04 [20]           8Fh, F4h, 6Ah, 70h,
           00567d08 [24]           35h, A5h, 63h, E9h,
           00567d0c [28]           A3h, 95h, 64h, 9Eh,
           00567d10 [32]           32h, 88h, DBh,  Eh,
           00567d14 [36]           A4h, B8h, DCh, 79h,
           00567d18 [40]           1Eh, E9h, D5h, E0h,
           00567d1c [44]           88h, D9h, D2h, 97h,
           00567d20 [48]           2Bh, 4Ch, B6h,  9h,
           00567d24 [52]           BDh, 7Ch, B1h, 7Eh,
           00567d28 [56]            7h, 2Dh, B8h, E7h,
           00567d2c [60]           91h, 1Dh, BFh, 90h,
           00567d30 [64]           64h, 10h, B7h, 1Dh,
           00567d34 [68]           F2h, 20h, B0h, 6Ah,
           00567d38 [72]           48h, 71h, B9h, F3h,
           00567d3c [76]           DEh, 41h, BEh, 84h,
           00567d40 [80]           7Dh, D4h, DAh, 1Ah,
           00567d44 [84]           EBh, E4h, DDh, 6Dh,
           00567d48 [88]           51h, B5h, D4h, F4h,
           00567d4c [92]           C7h, 85h, D3h, 83h,
           00567d50 [96]           56h, 98h, 6Ch, 13h,
           00567d54 [100]          C0h, A8h, 6Bh, 64h,
           00567d58 [104]          7Ah, F9h, 62h, FDh,
           00567d5c [108]          ECh, C9h, 65h, 8Ah,
           00567d60 [112]          4Fh, 5Ch,  1h, 14h,
           00567d64 [116]          D9h, 6Ch,  6h, 63h,
           00567d68 [120]          63h, 3Dh,  Fh, FAh,
           00567d6c [124]          F5h,  Dh,  8h, 8Dh,
           00567d70 [128]          C8h, 20h, 6Eh, 3Bh,
           00567d74 [132]          5Eh, 10h, 69h, 4Ch,
           00567d78 [136]          E4h, 41h, 60h, D5h,
           00567d7c [140]          72h, 71h, 67h, A2h,
           00567d80 [144]          D1h, E4h,  3h, 3Ch,
           00567d84 [148]          47h, D4h,  4h, 4Bh,
           00567d88 [152]          FDh, 85h,  Dh, D2h,
           00567d8c [156]          6Bh, B5h,  Ah, A5h,
           00567d90 [160]          FAh, A8h, B5h, 35h,
           00567d94 [164]          6Ch, 98h, B2h, 42h,
           00567d98 [168]          D6h, C9h, BBh, DBh,
           00567d9c [172]          40h, F9h, BCh, ACh,
           00567da0 [176]          E3h, 6Ch, D8h, 32h,
           00567da4 [180]          75h, 5Ch, DFh, 45h,
           00567da8 [184]          CFh,  Dh, D6h, DCh,
           00567dac [188]          59h, 3Dh, D1h, ABh,
           00567db0 [192]          ACh, 30h, D9h, 26h,
           00567db4 [196]          3Ah,  0h, DEh, 51h,
           00567db8 [200]          80h, 51h, D7h, C8h,
           00567dbc [204]          16h, 61h, D0h, BFh,
           00567dc0 [208]          B5h, F4h, B4h, 21h,
           00567dc4 [212]          23h, C4h, B3h, 56h,
           00567dc8 [216]          99h, 95h, BAh, CFh,
           00567dcc [220]           Fh, A5h, BDh, B8h,
           00567dd0 [224]          9Eh, B8h,  2h, 28h,
           00567dd4 [228]           8h, 88h,  5h, 5Fh,
           00567dd8 [232]          B2h, D9h,  Ch, C6h,
           00567ddc [236]          24h, E9h,  Bh, B1h,
           00567de0 [240]          87h, 7Ch, 6Fh, 2Fh,
           00567de4 [244]          11h, 4Ch, 68h, 58h,
           00567de8 [248]          ABh, 1Dh, 61h, C1h,
           00567dec [252]          3Dh, 2Dh, 66h, B6h,
           00567df0 [256]          90h, 41h, DCh, 76h,
           00567df4 [260]           6h, 71h, DBh,  1h,
           00567df8 [264]          BCh, 20h, D2h, 98h,
           00567dfc [268]          2Ah, 10h, D5h, EFh,
           00567e00 [272]          89h, 85h, B1h, 71h,
           00567e04 [276]          1Fh, B5h, B6h,  6h,
           00567e08 [280]          A5h, E4h, BFh, 9Fh,
           00567e0c [284]          33h, D4h, B8h, E8h,
           00567e10 [288]          A2h, C9h,  7h, 78h,
           00567e14 [292]          34h, F9h,  0h,  Fh,
           00567e18 [296]          8Eh, A8h,  9h, 96h,
           00567e1c [300]          18h, 98h,  Eh, E1h,
           00567e20 [304]          BBh,  Dh, 6Ah, 7Fh,
           00567e24 [308]          2Dh, 3Dh, 6Dh,  8h,
           00567e28 [312]          97h, 6Ch, 64h, 91h,
           00567e2c [316]           1h, 5Ch, 63h, E6h,
           00567e30 [320]          F4h, 51h, 6Bh, 6Bh,
           00567e34 [324]          62h, 61h, 6Ch, 1Ch,
           00567e38 [328]          D8h, 30h, 65h, 85h,
           00567e3c [332]          4Eh,  0h, 62h, F2h,
           00567e40 [336]          EDh, 95h,  6h, 6Ch,
           00567e44 [340]          7Bh, A5h,  1h, 1Bh,
           00567e48 [344]          C1h, F4h,  8h, 82h,
           00567e4c [348]          57h, C4h,  Fh, F5h,
           00567e50 [352]          C6h, D9h, B0h, 65h,
           00567e54 [356]          50h, E9h, B7h, 12h,
           00567e58 [360]          EAh, B8h, BEh, 8Bh,
           00567e5c [364]          7Ch, 88h, B9h, FCh,
           00567e60 [368]          DFh, 1Dh, DDh, 62h,
           00567e64 [372]          49h, 2Dh, DAh, 15h,
           00567e68 [376]          F3h, 7Ch, D3h, 8Ch,
           00567e6c [380]          65h, 4Ch, D4h, FBh,
           00567e70 [384]          58h, 61h, B2h, 4Dh,
           00567e74 [388]          CEh, 51h, B5h, 3Ah,
           00567e78 [392]          74h,  0h, BCh, A3h,
           00567e7c [396]          E2h, 30h, BBh, D4h,
           00567e80 [400]          41h, A5h, DFh, 4Ah,
           00567e84 [404]          D7h, 95h, D8h, 3Dh,
           00567e88 [408]          6Dh, C4h, D1h, A4h,
           00567e8c [412]          FBh, F4h, D6h, D3h,
           00567e90 [416]          6Ah, E9h, 69h, 43h,
           00567e94 [420]          FCh, D9h, 6Eh, 34h,
           00567e98 [424]          46h, 88h, 67h, ADh,
           00567e9c [428]          D0h, B8h, 60h, DAh,
           00567ea0 [432]          73h, 2Dh,  4h, 44h,
           00567ea4 [436]          E5h, 1Dh,  3h, 33h,
           00567ea8 [440]          5Fh, 4Ch,  Ah, AAh,
           00567eac [444]          C9h, 7Ch,  Dh, DDh,
           00567eb0 [448]          3Ch, 71h,  5h, 50h,
           00567eb4 [452]          AAh, 41h,  2h, 27h,
           00567eb8 [456]          10h, 10h,  Bh, BEh,
           00567ebc [460]          86h, 20h,  Ch, C9h,
           00567ec0 [464]          25h, B5h, 68h, 57h,
           00567ec4 [468]          B3h, 85h, 6Fh, 20h,
           00567ec8 [472]           9h, D4h, 66h, B9h,
           00567ecc [476]          9Fh, E4h, 61h, CEh,
           00567ed0 [480]           Eh, F9h, DEh, 5Eh,
           00567ed4 [484]          98h, C9h, D9h, 29h,
           00567ed8 [488]          22h, 98h, D0h, B0h,
           00567edc [492]          B4h, A8h, D7h, C7h,
           00567ee0 [496]          17h, 3Dh, B3h, 59h,
           00567ee4 [500]          81h,  Dh, B4h, 2Eh,
           00567ee8 [504]          3Bh, 5Ch, BDh, B7h,
           00567eec [508]          ADh, 6Ch, BAh, C0h,
           00567ef0 [512]          20h, 83h, B8h, EDh,
           00567ef4 [516]          B6h, B3h, BFh, 9Ah,
           00567ef8 [520]           Ch, E2h, B6h,  3h,
           00567efc [524]          9Ah, D2h, B1h, 74h,
           00567f00 [528]          39h, 47h, D5h, EAh,
           00567f04 [532]          AFh, 77h, D2h, 9Dh,
           00567f08 [536]          15h, 26h, DBh,  4h,
           00567f0c [540]          83h, 16h, DCh, 73h,
           00567f10 [544]          12h,  Bh, 63h, E3h,
           00567f14 [548]          84h, 3Bh, 64h, 94h,
           00567f18 [552]          3Eh, 6Ah, 6Dh,  Dh,
           00567f1c [556]          A8h, 5Ah, 6Ah, 7Ah,
           00567f20 [560]           Bh, CFh,  Eh, E4h,
           00567f24 [564]          9Dh, FFh,  9h, 93h,
           00567f28 [568]          27h, AEh,  0h,  Ah,
           00567f2c [572]          B1h, 9Eh,  7h, 7Dh,
           00567f30 [576]          44h, 93h,  Fh, F0h,
           00567f34 [580]          D2h, A3h,  8h, 87h,
           00567f38 [584]          68h, F2h,  1h, 1Eh,
           00567f3c [588]          FEh, C2h,  6h, 69h,
           00567f40 [592]          5Dh, 57h, 62h, F7h,
           00567f44 [596]          CBh, 67h, 65h, 80h,
           00567f48 [600]          71h, 36h, 6Ch, 19h,
           00567f4c [604]          E7h,  6h, 6Bh, 6Eh,
           00567f50 [608]          76h, 1Bh, D4h, FEh,
           00567f54 [612]          E0h, 2Bh, D3h, 89h,
           00567f58 [616]          5Ah, 7Ah, DAh, 10h,
           00567f5c [620]          CCh, 4Ah, DDh, 67h,
           00567f60 [624]          6Fh, DFh, B9h, F9h,
           00567f64 [628]          F9h, EFh, BEh, 8Eh,
           00567f68 [632]          43h, BEh, B7h, 17h,
           00567f6c [636]          D5h, 8Eh, B0h, 60h,
           00567f70 [640]          E8h, A3h, D6h, D6h,
           00567f74 [644]          7Eh, 93h, D1h, A1h,
           00567f78 [648]          C4h, C2h, D8h, 38h,
           00567f7c [652]          52h, F2h, DFh, 4Fh,
           00567f80 [656]          F1h, 67h, BBh, D1h,
           00567f84 [660]          67h, 57h, BCh, A6h,
           00567f88 [664]          DDh,  6h, B5h, 3Fh,
           00567f8c [668]          4Bh, 36h, B2h, 48h,
           00567f90 [672]          DAh, 2Bh,  Dh, D8h,
           00567f94 [676]          4Ch, 1Bh,  Ah, AFh,
           00567f98 [680]          F6h, 4Ah,  3h, 36h,
           00567f9c [684]          60h, 7Ah,  4h, 41h,
           00567fa0 [688]          C3h, EFh, 60h, DFh,
           00567fa4 [692]          55h, DFh, 67h, A8h,
           00567fa8 [696]          EFh, 8Eh, 6Eh, 31h,
           00567fac [700]          79h, BEh, 69h, 46h,
           00567fb0 [704]          8Ch, B3h, 61h, CBh,
           00567fb4 [708]          1Ah, 83h, 66h, BCh,
           00567fb8 [712]          A0h, D2h, 6Fh, 25h,
           00567fbc [716]          36h, E2h, 68h, 52h,
           00567fc0 [720]          95h, 77h,  Ch, CCh,
           00567fc4 [724]           3h, 47h,  Bh, BBh,
           00567fc8 [728]          B9h, 16h,  2h, 22h,
           00567fcc [732]          2Fh, 26h,  5h, 55h,
           00567fd0 [736]          BEh, 3Bh, BAh, C5h,
           00567fd4 [740]          28h,  Bh, BDh, B2h,
           00567fd8 [744]          92h, 5Ah, B4h, 2Bh,
           00567fdc [748]           4h, 6Ah, B3h, 5Ch,
           00567fe0 [752]          A7h, FFh, D7h, C2h,
           00567fe4 [756]          31h, CFh, D0h, B5h,
           00567fe8 [760]          8Bh, 9Eh, D9h, 2Ch,
           00567fec [764]          1Dh, AEh, DEh, 5Bh,
           00567ff0 [768]          B0h, C2h, 64h, 9Bh,
           00567ff4 [772]          26h, F2h, 63h, ECh,
           00567ff8 [776]          9Ch, A3h, 6Ah, 75h,
           00567ffc [780]           Ah, 93h, 6Dh,  2h,
           00568000 [784]          A9h,  6h,  9h, 9Ch,
           00568004 [788]          3Fh, 36h,  Eh, EBh,
           00568008 [792]          85h, 67h,  7h, 72h,
           0056800c [796]          13h, 57h,  0h,  5h,
           00568010 [800]          82h, 4Ah, BFh, 95h,
           00568014 [804]          14h, 7Ah, B8h, E2h,
           00568018 [808]          AEh, 2Bh, B1h, 7Bh,
           0056801c [812]          38h, 1Bh, B6h,  Ch,
           00568020 [816]          9Bh, 8Eh, D2h, 92h,
           00568024 [820]           Dh, BEh, D5h, E5h,
           00568028 [824]          B7h, EFh, DCh, 7Ch,
           0056802c [828]          21h, DFh, DBh,  Bh,
           00568030 [832]          D4h, D2h, D3h, 86h,
           00568034 [836]          42h, E2h, D4h, F1h,
           00568038 [840]          F8h, B3h, DDh, 68h,
           0056803c [844]          6Eh, 83h, DAh, 1Fh,
           00568040 [848]          CDh, 16h, BEh, 81h,
           00568044 [852]          5Bh, 26h, B9h, F6h,
           00568048 [856]          E1h, 77h, B0h, 6Fh,
           0056804c [860]          77h, 47h, B7h, 18h,
           00568050 [864]          E6h, 5Ah,  8h, 88h,
           00568054 [868]          70h, 6Ah,  Fh, FFh,
           00568058 [872]          CAh, 3Bh,  6h, 66h,
           0056805c [876]          5Ch,  Bh,  1h, 11h,
           00568060 [880]          FFh, 9Eh, 65h, 8Fh,
           00568064 [884]          69h, AEh, 62h, F8h,
           00568068 [888]          D3h, FFh, 6Bh, 61h,
           0056806c [892]          45h, CFh, 6Ch, 16h,
           00568070 [896]          78h, E2h,  Ah, A0h,
           00568074 [900]          EEh, D2h,  Dh, D7h,
           00568078 [904]          54h, 83h,  4h, 4Eh,
           0056807c [908]          C2h, B3h,  3h, 39h,
           00568080 [912]          61h, 26h, 67h, A7h,
           00568084 [916]          F7h, 16h, 60h, D0h,
           00568088 [920]          4Dh, 47h, 69h, 49h,
           0056808c [924]          DBh, 77h, 6Eh, 3Eh,
           00568090 [928]          4Ah, 6Ah, D1h, AEh,
           00568094 [932]          DCh, 5Ah, D6h, D9h,
           00568098 [936]          66h,  Bh, DFh, 40h,
           0056809c [940]          F0h, 3Bh, D8h, 37h,
           005680a0 [944]          53h, AEh, BCh, A9h,
           005680a4 [948]          C5h, 9Eh, BBh, DEh,
           005680a8 [952]          7Fh, CFh, B2h, 47h,
           005680ac [956]          E9h, FFh, B5h, 30h,
           005680b0 [960]          1Ch, F2h, BDh, BDh,
           005680b4 [964]          8Ah, C2h, BAh, CAh,
           005680b8 [968]          30h, 93h, B3h, 53h,
           005680bc [972]          A6h, A3h, B4h, 24h,
           005680c0 [976]           5h, 36h, D0h, BAh,
           005680c4 [980]          93h,  6h, D7h, CDh,
           005680c8 [984]          29h, 57h, DEh, 54h,
           005680cc [988]          BFh, 67h, D9h, 23h,
           005680d0 [992]          2Eh, 7Ah, 66h, B3h,
           005680d4 [996]          B8h, 4Ah, 61h, C4h,
           005680d8 [1000]          2h, 1Bh, 68h, 5Dh,
           005680dc [1004]         94h, 2Bh, 6Fh, 2Ah,
           005680e0 [1008]         37h, BEh,  Bh, B4h,
           005680e4 [1012]         A1h, 8Eh,  Ch, C3h,
           005680e8 [1016]         1Bh, DFh,  5h, 5Ah,
           005680ec [1020]         8Dh, EFh,  2h, 2Dh

Predictable PRNG seed

Additionally, the PRNG used by ZipCrypto is predictable. This is a high vulnerability (though largely redundant since ZipCrypto is already broken independently).

srand(time(NULL)) seeds once with a Unix timestamp. rand() is fully deterministic from the seed. A single 32-bit state updated via _holdrand = _holdrand * 0x343FD + 0x269EC3 with no entropy mixed after seeding. The bot’s Date: header leaks the approximate seed. This lets an attacker replay rand() to reconstruct all 12 encryption header bytes.

void __thiscall
FUN_0047f7b0_zip_and_add_file(void *this,uint *param_1,int param_2,int param_3,char param_4)

{
...
                    /* seed PRNG once with current time */
            if (DAT_005a6dbc_g_prng_seeded == '\0') {
              time_val = FUN_00541e44_time((longlong *)0x0);
              FUN_00542fe8_srand(time_val);
            }
                    /* generate 11 random header bytes */
            for (i_rand = 0; i_rand < 0xc; i_rand = i_rand + 1) {
              rand_val = FUN_00542fc7_rand();
              enc_header[i_rand] = (byte)((int)rand_val >> 7);
            }
                    /* overwrite byte 11 with CRC32 check byte */
            enc_header[0xb] = (byte)((uint)file_crc32 >> 8);
                    /* encrypt all 12 header bytes through ZipCrypto cipher */
            for (enc_idx = 0; enc_idx < 0xc; enc_idx = enc_idx + 1) {
              iVar3 = FUN_0047ec90_zipcrypto_encrypt_byte
                                ((uint *)((int)this + 0x30),enc_header[enc_idx]);
              enc_header[enc_idx] = (byte)iVar3;
            }
            if ((*(int *)this != 0) && (param_4 == '\0')) {
                    /* write encrypted header to ZIP output */
              FUN_0047ef60_zip_write_output(this,(undefined8 *)enc_header,0xc);
              *(int *)((int)this + 0x18) = *(int *)((int)this + 0x18) + 0xc;
            }
...
void __cdecl FUN_00542fe8_srand(ulong param_1)

{
  _ptiddata p_Var1;
  
  p_Var1 = __getptd();
  p_Var1->_holdrand = param_1;
  return;
}
uint FUN_00542fc7_rand(void)

{
  _ptiddata p_Var1;
  uint uVar2;
  
  p_Var1 = __getptd();
  uVar2 = p_Var1->_holdrand * 0x343fd + 0x269ec3;
  p_Var1->_holdrand = uVar2;
  return uVar2 >> 0x10 & 0x7fff;
}

Weak PRNG

There is another weak random function used in various places in thread 3. The full usage chain was not traced exhaustively but this seems a medium vulnerability.

weak_random(max) wraps rand(), which outputs only 15 bits (0–32767). Any random tokens generated through it have at most 32,768 possible values regardless of the requested range. Since it shares the same seeded rand() state, all outputs are reproducible once the PRNG seed is known.

int __cdecl FUN_0044e700_weak_random(int param_1)

{
  uint uVar1;
  undefined4 local_8;
  
  uVar1 = FUN_00542fc7_rand();
  local_8 = (int)(uVar1 * param_1) / 0x7fff;
  if (param_1 <= local_8) {
    local_8 = param_1 + -1;
  }
  if (local_8 < 0) {
    local_8 = 0;
  }
  return local_8;
}

DomainKeys hardcoded rsa-sha1

SHA1 is broken and does not prove the email was not tampered with. Mail servers that check this will increasingly reject or distrust these signatures. Low vulnerability.

void __thiscall FUN_00433f40_domainkeys_sign(void *this,byte *param_1,uint *param_2)

{
...
              FUN_005410ca_sprintf
                        (local_8fb0,
                         (byte *)
                         "DomainKey-Signature: a=rsa-sha1; q=dns; c=%s;\r\n\ts=%s; d=%s;\r\n\th=%s;\ r\n\tb=%s;\r\n%s"
                        );
...

DKIM defaults to rsa-sha1

In DKIM signing, 3 of 4 dispatch paths default to rsa-sha1. SHA1 is broken and does not prove the email was not tampered with. Mail servers that check this will increasingly reject or distrust these signatures. Low vulnerability.

void FUN_00431880_dkim_sign(char *param_1,char param_2,char param_3)

{
...
  if (param_3 == '\0') {
    local_1e0 = "rsa-sha1";
  }
  else {
    local_1e0 = "rsa-sha256";
  }
...
void __thiscall FUN_00432810_dkim_dispatch(void *this,char *param_1)

{
...
                    /* rsa-sha1 */
      iVar2 = FUN_00431880_dkim_sign(param_1,'\0','\0');
...
                    /* rsa-sha1 */
          iVar2 = FUN_00431880_dkim_sign(param_1,'\0','\0');
...
                    /* rsa-sha256 */
          iVar2 = FUN_00431880_dkim_sign(param_1,'\x01','\x01');
...
        if (*(int *)((int)this + 0x170) != 2) {
                    /* rsa-sha1 */
          iVar2 = FUN_00431880_dkim_sign(param_1,'\x01','\0');
...

Dead/unused crypto

The binary contains AES, DES, 3DES and RC4 implementations from the statically linked OpenSSL 1.0.2g library (confirmed by internal source paths like .\crypto\evp\e_aes.c). None are called by application code.

The capa matches for RC4 are false positives. The matched functions are zlib/deflate internals.

IOCs

YARA

Note: the rules are available here as well.

$ yara rules/spambot.yar ../tmp/spambot.exe
spambot_xor_cipher ../tmp/spambot.exe
spambot_blowfish_config ../tmp/spambot.exe
spambot_zipcrypto ../tmp/spambot.exe
spambot_hmac_md5 ../tmp/spambot.exe
spambot_dkim_domainkeys ../tmp/spambot.exe
spambot_weak_prng ../tmp/spambot.exe
/*
    YARA rules for spambot malware family.
    Target: 36684cef31a4a43473d5b12d30aabf94f56e407b0eeec14b7c1cb87efa3159b6
    https://www.virustotal.com/gui/file/36684cef31a4a43473d5b12d30aabf94f56e407b0eeec14b7c1cb87efa3159b6/detection
*/

rule spambot_xor_cipher
{
    meta:
        description = "Detects the XOR cipher function that derives its key from MD5(\"UNREGISTERED\")."
        author = "gemesa@protonmail.com"
        hash = "36684cef31a4a43473d5b12d30aabf94f56e407b0eeec14b7c1cb87efa3159b6"
        reference = "FUN_004680d0_xor_cipher"

    strings:
        $key_string = "UNREGISTERED" ascii

        /*
        004680e6 6a 0c           PUSH       12
        004680e8 68 ac 73        PUSH       s_UNREGISTERED_005673ac                          = "UNREGISTERED"
                 56 00
        004680ed 8d 45 ec        LEA        EAX=>key,[EBP + -0x14]
        004680f0 50              PUSH       EAX
        004680f1 e8 0a 38        CALL       FUN_0044b900_md5_hash                            undefined FUN_0044b900_md5_hash(
                 fe ff
        */
        $md5_call = { 6A 0C 68 ?? ?? ?? ?? 8D 45 EC 50 E8 }

        /*
        00468116 83 e0 0f        AND        EAX,0xf
        */
        $xor_mask = { 83 E? 0F }

    condition:
        uint16(0) == 0x5A4D and all of them
}

rule spambot_blowfish_config
{
    meta:
        description = "Detects Blowfish-ECB config decryption from PE resource."
        author = "gemesa@protonmail.com"
        hash = "36684cef31a4a43473d5b12d30aabf94f56e407b0eeec14b7c1cb87efa3159b6"
        reference = "FUN_00472da0_bot_main (key init + resource load)"

    strings:
        /*
        00472e70 c6 45 e0 07     MOV        byte ptr [EBP + key[0]],0x7
        00472e74 c6 45 e1 54     MOV        byte ptr [EBP + key[1]],0x54
        00472e78 c6 45 e2 08     MOV        byte ptr [EBP + key[2]],0x8
        00472e7c c6 45 e3 d5     MOV        byte ptr [EBP + key[3]],0xd5
        00472e80 c6 45 e4 67     MOV        byte ptr [EBP + key[4]],0x67
        00472e84 c6 45 e5 85     MOV        byte ptr [EBP + key[5]],0x85
        00472e88 c6 45 e6 9e     MOV        byte ptr [EBP + key[6]],0x9e
        00472e8c c6 45 e7 e2     MOV        byte ptr [EBP + key[7]],0xe2
        00472e90 c6 45 e8 af     MOV        byte ptr [EBP + key[8]],0xaf
        00472e94 c6 45 e9 e3     MOV        byte ptr [EBP + key[9]],0xe3
        00472e98 c6 45 ea c6     MOV        byte ptr [EBP + key[10]],0xc6
        00472e9c c6 45 eb a9     MOV        byte ptr [EBP + key[11]],0xa9
        00472ea0 c6 45 ec 8f     MOV        byte ptr [EBP + key[12]],0x8f
        00472ea4 c6 45 ed 3b     MOV        byte ptr [EBP + key[13]],0x3b
        00472ea8 c6 45 ee 0c     MOV        byte ptr [EBP + key[14]],0xc
        00472eac c6 45 ef 5b     MOV        byte ptr [EBP + key[15]],0x5b
        */
        $key_init = { C6 45 ?? 07 C6 45 ?? 54 C6 45 ?? 08 C6 45 ?? D5 }

        /*
                                CRYPT_Blowfish_p_init                           XREF[1]:     FUN_00421d20_blowfish_key_schedu
        005605c0 88 6a 3f        db[32]
                 24 d3 08 
                 a3 85 2e 
           005605c0 [0]            88h, 6Ah, 3Fh, 24h,
           005605c4 [4]            D3h,  8h, A3h, 85h,
           005605c8 [8]            2Eh, 8Ah, 19h, 13h,
           005605cc [12]           44h, 73h, 70h,  3h,
           005605d0 [16]           22h, 38h,  9h, A4h,
           005605d4 [20]           D0h, 31h, 9Fh, 29h,
           005605d8 [24]           98h, FAh, 2Eh,  8h,
           005605dc [28]           89h, 6Ch, 4Eh, ECh
        */
        $bf_parray = { 88 6A 3F 24 D3 08 A3 85 }

        /*
                             LAB_00472def                                    XREF[1]:     00472de9(j)  
        00472def 6a 0a           PUSH       DAT_0000000a
        00472df1 68 e2 00        PUSH       DAT_000000e2
                 00 00
        00472df6 6a 00           PUSH       0x0
                             0xe2 = resource ID
                             0xa = RT_RCDATA = Application-defined resource (raw data).
        00472df8 ff 15 30        CALL       dword ptr [->KERNEL32.DLL::FindResourceA]        = 0019fa9e
                 01 56 00
        */
        $find_resource = { 6A 0A 68 ?? ?? ?? ?? 6A 00 FF 15 }

    condition:
        uint16(0) == 0x5A4D and $bf_parray and ($key_init or $find_resource)
}

rule spambot_zipcrypto
{
    meta:
        description = "Detects ZipCrypto (Traditional PKWARE Encryption) initialization."
        author = "gemesa@protonmail.com"
        hash = "36684cef31a4a43473d5b12d30aabf94f56e407b0eeec14b7c1cb87efa3159b6"
        reference = "FUN_0047f7b0_zip_and_add_file, FUN_0047eb70_zipcrypto_update_keys"

    strings:
        /*
                             initialize the 3 ZipCrypto keys (PKWARE spec section 6.1.5)
        0047fda5 c7 44 10        MOV        dword ptr [EAX + EDX*0x1 + 0x30],0x12345678
                 30 78 56 
                 34 12
        0047fdad b9 04 00        MOV        this,0x4
                 00 00
        0047fdb2 c1 e1 00        SHL        this,0x0
        0047fdb5 8b 95 68        MOV        EDX,dword ptr [EBP + local_49c]
                 fb ff ff
        0047fdbb c7 44 0a        MOV        dword ptr [EDX + this*0x1 + 0x30],0x23456789
                 30 89 67 
                 45 23
        0047fdc3 b8 04 00        MOV        EAX,0x4
                 00 00
        0047fdc8 d1 e0           SHL        EAX,0x1
        0047fdca 8b 8d 68        MOV        this,dword ptr [EBP + local_49c]
                 fb ff ff
        0047fdd0 c7 44 01        MOV        dword ptr [this + EAX*0x1 + 0x30],0x34567890
                 30 90 78 
                 56 34
        */
        $zip_keys = { 78 56 34 12 [0-32] 89 67 45 23 [0-32] 90 78 56 34 }

        /*
        0047ebec 69 04 0a        IMUL       EAX,dword ptr [EDX + ECX*0x1],0x8088405
                 05 84 08 08
        0047ebf3 83 c0 01        ADD        EAX,0x1
        */
        $update_keys = { 05 84 08 08 }

    condition:
        uint16(0) == 0x5A4D and all of them
}

rule spambot_hmac_md5
{
    meta:
        description = "Detects the custom HMAC-MD5 implementation used for CRAM-MD5 authentication."
        author = "gemesa@protonmail.com"
        hash = "36684cef31a4a43473d5b12d30aabf94f56e407b0eeec14b7c1cb87efa3159b6"
        reference = "FUN_0044bf00_hmac_md5_core"

    strings:
        /*
        0044bca4 c7 04 01        MOV        dword ptr [ECX + EAX*0x1],0x67452301
                 01 23 45 67
        0044bcab ba 04 00        MOV        EDX,0x4
                 00 00
        0044bcb0 c1 e2 00        SHL        EDX,0x0
        0044bcb3 8b 45 08        MOV        EAX,dword ptr [EBP + param_1]
        0044bcb6 c7 04 10        MOV        dword ptr [EAX + EDX*0x1],0xefcdab89
                 89 ab cd ef
        0044bcbd b9 04 00        MOV        ECX,0x4
                 00 00
        0044bcc2 d1 e1           SHL        ECX,0x1
        0044bcc4 8b 55 08        MOV        EDX,dword ptr [EBP + param_1]
        0044bcc7 c7 04 0a        MOV        dword ptr [EDX + ECX*0x1],0x98badcfe
                 fe dc ba 98
        0044bcce b8 04 00        MOV        EAX,0x4
                 00 00
        0044bcd3 6b c8 03        IMUL       ECX,EAX,0x3
        0044bcd6 8b 55 08        MOV        EDX,dword ptr [EBP + param_1]
        0044bcd9 c7 04 0a        MOV        dword ptr [EDX + ECX*0x1],0x10325476
                 76 54 32 10
        */
        $md5_init_A = { 01 23 45 67 }
        $md5_init_B = { 89 AB CD EF }
        $md5_init_C = { FE DC BA 98 }
        $md5_init_D = { 76 54 32 10 }

        /*
        0044bfd1 8b 8d b0        MOV        ECX,dword ptr [EBP + local_154]
                 fe ff ff
        0044bfd7 0f b6 54        MOVZX      EDX,byte ptr [EBP + ECX*0x1 + -0x58]
                 0d a8
        0044bfdc 83 f2 36        XOR        EDX,0x36
        0044bfdf 8b 85 b0        MOV        EAX,dword ptr [EBP + local_154]
                 fe ff ff
        0044bfe5 88 54 05 a8     MOV        byte ptr [EBP + EAX*0x1 + -0x58],DL
        0044bfe9 8b 8d b0        MOV        ECX,dword ptr [EBP + local_154]
                 fe ff ff
        0044bfef 0f b6 94        MOVZX      EDX,byte ptr [EBP + ECX*0x1 + 0xffffff64]
                 0d 64 ff 
                 ff ff
        0044bff7 83 f2 5c        XOR        EDX,0x5c
        */
        $ipad = { 0F B6 54 0D ?? 83 F2 36 }
        $opad = { 0F B6 94 0D ?? ?? ?? ?? 83 F2 5C }

        /*
                             s_CRAM-MD5_00563640                             XREF[1]:     FUN_004437d0_state_machine:00444
        00563640 43 52 41        ds         "CRAM-MD5"
                 4d 2d 4d 
                 44 35 00
        */
        $cram_md5 = "CRAM-MD5" ascii

    condition:
        uint16(0) == 0x5A4D and
        all of ($md5_init_*) and
        $ipad and
        $opad and
        $cram_md5
}

rule spambot_dkim_domainkeys
{
    meta:
        description = "Detects the DomainKeys and DKIM email signing with rsa-sha1."
        author = "gemesa@protonmail.com"
        hash = "36684cef31a4a43473d5b12d30aabf94f56e407b0eeec14b7c1cb87efa3159b6"
        reference = "FUN_00433f40_domainkeys_sign, FUN_00431880_dkim_sign"

    strings:
        $dk_sig   = "DomainKey-Signature: a=rsa-sha1" ascii
        $dkim_sig = "DKIM-Signature:" ascii
        $rsa_sha1 = "rsa-sha1" ascii
        $dk_query = "_domainkey.%s" ascii

    condition:
        uint16(0) == 0x5A4D and 3 of them
}

rule spambot_weak_prng
{
    meta:
        description = "Detects the weak PRNG (MSVC rand with time(NULL) seed) used for ZipCrypto headers."
        author = "gemesa@protonmail.com"
        hash = "36684cef31a4a43473d5b12d30aabf94f56e407b0eeec14b7c1cb87efa3159b6"
        reference = "FUN_00542fc7_rand, FUN_0044e700_weak_random"

    strings:
        /*
                             FUN_00542fc7_rand                               XREF[2]:     FUN_0044e700_weak_random:0044e70
                                                                                          FUN_0047f7b0_zip_and_add_file:00
        00542fc7 e8 a7 63        CALL       __getptd                                         _ptiddata __getptd(void)
                 00 00
        00542fcc 69 48 14        IMUL       ECX,dword ptr [EAX + 0x14],0x343fd
                 fd 43 03 00
        00542fd3 81 c1 c3        ADD        ECX,0x269ec3
                 9e 26 00
        00542fd9 89 48 14        MOV        dword ptr [EAX + 0x14],ECX
        00542fdc c1 e9 10        SHR        ECX,0x10
        00542fdf 81 e1 ff        AND        ECX,0x7fff
                 7f 00 00
        00542fe5 8b c1           MOV        EAX,ECX
        00542fe7 c3              RET
        */
        $rand_mul = { FD 43 03 00 }
        $rand_add = { C3 9E 26 00 }

        /*
                             FUN_0044e700_weak_random                        XREF[20]:    FUN_0044d9b0:0044da7c(c), 
        ...
        0044e700 55              PUSH       EBP
        0044e701 8b ec           MOV        EBP,ESP
        0044e703 51              PUSH       ECX
        0044e704 e8 be 48        CALL       FUN_00542fc7_rand                                uint FUN_00542fc7_rand(void)
                 0f 00
        0044e709 0f af 45 08     IMUL       EAX,dword ptr [EBP + param_1]
        0044e70d 99              CDQ
        0044e70e b9 ff 7f        MOV        ECX,0x7fff
                 00 00
        0044e713 f7 f9           IDIV       ECX
        */
        $divisor = { FF 7F 00 00 }

    condition:
        uint16(0) == 0x5A4D and all of them
}

capa

Note: the rules are available here as well.

$ capa -r rules/ ../tmp/spambot.exe                       
┌─────────────┬────────────────────────────────────────────────────────────────────────────────────┐
│ md5         │ 88b43801b38acea324467ece3f12a0e0                                                   │
│ sha1        │ 1004d87f61c461c3d4d4f0ff7f40c5da0a279e4e                                           │
│ sha256      │ 36684cef31a4a43473d5b12d30aabf94f56e407b0eeec14b7c1cb87efa3159b6                   │
│ analysis    │ static                                                                             │
│ os          │ windows                                                                            │
│ format      │ pe                                                                                 │
│ arch        │ i386                                                                               │
│ path        │ /home/gemesa/git-repos/group-ib/tmp/spambot.exe                                    │
└─────────────┴────────────────────────────────────────────────────────────────────────────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ATT&CK Tactic                 ┃ ATT&CK Technique                                                 ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ DEFENSE EVASION               │ Deobfuscate/Decode Files or Information [T1140]                  │
│                               │ Obfuscated Files or Information [T1027]                          │
└───────────────────────────────┴──────────────────────────────────────────────────────────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ MBC Objective                   ┃ MBC Behavior                                                   ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ CRYPTOGRAPHY                    │ Cryptographic Hash::MD5 [C0029.001]                            │
│                                 │ Encrypt Data [C0027]                                           │
│                                 │ Encrypt Data::Blowfish [C0027.002]                             │
│                                 │ Hashed Message Authentication Code [C0061]                     │
│ DATA                            │ Encode Data::XOR [C0026.002]                                   │
└─────────────────────────────────┴────────────────────────────────────────────────────────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Capability                                             ┃ Namespace                               ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ decrypt config using Blowfish-ECB with hardcoded key   │ data-manipulation/encryption/blowfish   │
│ encrypt C2 traffic using XOR with static MD5 key       │ data-manipulation/encryption/xor        │
│ initialize ZipCrypto key state (2 matches)             │ data-manipulation/encryption/zipcrypto  │
│ hash data using custom MD5                             │ data-manipulation/hashing/md5           │
│ authenticate using HMAC-MD5 for CRAM-MD5 (2 matches)   │ data-manipulation/hmac                  │
└────────────────────────────────────────────────────────┴─────────────────────────────────────────┘
rule:
  meta:
    name: decrypt config using Blowfish-ECB with hardcoded key
    namespace: data-manipulation/encryption/blowfish
    authors:
      - gemesa@protonmail.com
    description: >-
      Detects the Blowfish key schedule function (FUN_00421d20) identified by
      the P-array and S-box initialization constants. The application uses
      Blowfish-ECB with a hardcoded 16-byte key to decrypt C2 configuration
      stored in PE resource RT_RCDATA/0xE2.
    scopes:
      static: function
      dynamic: unsupported
    att&ck:
      - Defense Evasion::Deobfuscate/Decode Files or Information [T1140]
    mbc:
      - Cryptography::Encrypt Data::Blowfish [C0027.002]
    references:
      - https://www.virustotal.com/gui/file/36684cef31a4a43473d5b12d30aabf94f56e407b0eeec14b7c1cb87efa3159b6
    examples:
      - 88B43801B38ACEA324467ECE3F12A0E0:0x00421D20
  features:
    - and:
      - or:
        - bytes: 886A3F24D308A385 = Blowfish P-array P[0..1] (LE)
        - bytes: 243F6A8885A308D3 = Blowfish P-array P[0..1] (BE)
        - bytes: E62128457713D038CF6654BE6C0CE934 = Blowfish P-array P[8..11] (LE)
        - bytes: 452821E638D01377BE5466CF34E90C6C = Blowfish P-array P[8..11] (BE)
      - or:
        - bytes: A60B31D1ACB5DF98DB72FD2FB7DF1AD0 = Blowfish S-box 0 (first 16 bytes, LE)
        - bytes: D1310BA698DFB5AC2FFD72DBD01ADFB7 = Blowfish S-box 0 (first 16 bytes, BE)
        - bytes: E9707A4B4429B3B52E0975DB232619C4 = Blowfish S-box 1 (first 16 bytes, LE)
        - bytes: 4B7A70E9B5B32944DB75092EC4192623 = Blowfish S-box 1 (first 16 bytes, BE)
        - bytes: 685A3DE9F74081941C264CF634296994 = Blowfish S-box 2 (first 16 bytes, LE)
        - bytes: E93D5A68948140F7F64C261C94692934 = Blowfish S-box 2 (first 16 bytes, BE)
        - bytes: 37CE393ACFF5FAD33777C2AB1B2DC55A = Blowfish S-box 3 (first 16 bytes, LE)
        - bytes: 3A39CE37D3FAF5CFABC275375AC52D1B = Blowfish S-box 3 (first 16 bytes, BE)
      - number: 0x10 = key length (16 bytes)

rule:
  meta:
    name: authenticate using HMAC-MD5 for CRAM-MD5
    namespace: data-manipulation/hmac
    authors:
      - gemesa@protonmail.com
    description: >-
      Detects the custom HMAC-MD5 implementation (FUN_0044bf00) used for
      CRAM-MD5 SMTP authentication. Identified by the ipad (0x36) and
      opad (0x5C) XOR constants with MD5 block size (0x40).
    scopes:
      static: function
      dynamic: unsupported
    mbc:
      - Cryptography::Hashed Message Authentication Code [C0061]
    references:
      - https://www.virustotal.com/gui/file/36684cef31a4a43473d5b12d30aabf94f56e407b0eeec14b7c1cb87efa3159b6
    examples:
      - 88B43801B38ACEA324467ECE3F12A0E0:0x0044BF00
  features:
    - and:
      - number: 0x36 = HMAC ipad byte
      - number: 0x5C = HMAC opad byte
      - number: 0x40 = MD5/SHA1 block size
      - characteristic: loop
      - count(characteristic(nzxor)): 2 or more

rule:
  meta:
    name: hash data using custom MD5
    namespace: data-manipulation/hashing/md5
    authors:
      - gemesa@protonmail.com
    description: >-
      Detects the custom MD5 initialization function (FUN_0044bc70) by the
      four standard IV constants. Used to derive the XOR cipher key and
      as the hash primitive inside the HMAC-MD5 implementation.
    scopes:
      static: function
      dynamic: unsupported
    mbc:
      - Cryptography::Cryptographic Hash::MD5 [C0029.001]
    references:
      - https://www.virustotal.com/gui/file/36684cef31a4a43473d5b12d30aabf94f56e407b0eeec14b7c1cb87efa3159b6
    examples:
      - 88B43801B38ACEA324467ECE3F12A0E0:0x0044BC70
  features:
    - and:
      - number: 0x67452301 = MD5 IV A
      - number: 0xEFCDAB89 = MD5 IV B
      - number: 0x98BADCFE = MD5 IV C
      - number: 0x10325476 = MD5 IV D
      - not:
        - number: 0xC3D2E1F0 = SHA1 IV E (excludes SHA1)

rule:
  meta:
    name: encrypt C2 traffic using XOR with static MD5 key
    namespace: data-manipulation/encryption/xor
    authors:
      - gemesa@protonmail.com
    description: >-
      Detects the XOR cipher function (FUN_004680d0) that derives a 16-byte
      key from MD5("UNREGISTERED") and applies it cyclically over a buffer.
    scopes:
      static: function
      dynamic: unsupported
    att&ck:
      - Defense Evasion::Obfuscated Files or Information [T1027]
    mbc:
      - Cryptography::Encrypt Data [C0027]
      - Data::Encode Data::XOR [C0026.002]
    references:
      - https://www.virustotal.com/gui/file/36684cef31a4a43473d5b12d30aabf94f56e407b0eeec14b7c1cb87efa3159b6
    examples:
      - 88B43801B38ACEA324467ECE3F12A0E0:0x004680D0
  features:
    - and:
      - string: "UNREGISTERED"
      - number: 0x0C = length of "UNREGISTERED"
      - number: 0x0F = AND mask for 16-byte key cycling
      - characteristic: nzxor
      - characteristic: loop

rule:
  meta:
    name: initialize ZipCrypto key state
    namespace: data-manipulation/encryption/zipcrypto
    authors:
      - gemesa@protonmail.com
    description: >-
      Detects ZipCrypto key initialization (FUN_0047f7b0) by the three
      PKWARE spec key constants (section 6.1.5).
    scopes:
      static: function
      dynamic: unsupported
    att&ck:
      - Defense Evasion::Obfuscated Files or Information [T1027]
    mbc:
      - Cryptography::Encrypt Data [C0027]
    references:
      - https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
      - https://www.virustotal.com/gui/file/36684cef31a4a43473d5b12d30aabf94f56e407b0eeec14b7c1cb87efa3159b6
    examples:
      - 88B43801B38ACEA324467ECE3F12A0E0:0x0047F7B0
  features:
    - and:
      - number: 0x12345678 = ZipCrypto key(0) init
      - number: 0x23456789 = ZipCrypto key(1) init
      - number: 0x34567890 = ZipCrypto key(2) init

Config extractor

We extract the previously mentioned Blowfish-ECB encrypted config and the XOR static key. Additionally, we extract the hardcoded, plain-text, fake SMTP relay domains. They are purely cosmetic strings injected into fabricated Received: headers to make the spam look legitimate (FUN_00450e10_forge_received_header).

$ python extract.py ../tmp/spambot.exe
[+] Blowfish key:   075408d567859ee2afe3c6a98f3b0c5b
[+] Resource ID:    0xE2
[+] C2 server:      195.2.240.119
[+] 1. port:        50055
[+] 2. port:        50056
[+] Service name:   Enterprise Mailing Service
[+] XOR key:        c65b6b5a311f669f99365e0b45ca6907  (MD5("UNREGISTERED"))
[+] Fake relay domains (32):
    asx121.turbo-inline.com
    external.newsubdomain.com
    group21.345mail.com
    m1.gns.snv.thisdomainl.com
    mail.gimmicc.net
    mail.naihautsui.co.kr
    mail.webhostings4u.com
    mailout.endmonthnow.com
    mmx09.tilkbans.com
    mts.locks.grgtween.net
    mtu23.bigping.com
    mtu67.syds.piswix.net
    mx.reskind.net
    mx03.listsystemsf.net
    mxs.perenter.com
    nntp.pinxodet.net
    public.micromail.com.au
    qnx.mdrost.com
    qrx.quickslick.com
    relay-x.misswldrs.com
    relay.2yahoo.com
    relay37.vosimerkam.net
    rly04.hottestmile.com
    rsmail.alkoholic.net
    smtp-server1.cfdenselr.com
    smtp.doneohx.com
    smtp.endend.nl
    smtp.mixedthings.net
    smtp18.yenddx.com
    smtp4.cyberemailings.com
    snmp.otwaloow.com
    webmail.halftomorrow.com

The config extractor uses byte-signature pattern matching to locate and extract all values directly from the binary. No addresses, keys or strings are hardcoded in the script. This makes it resilient to recompilation or minor patching across variants.

Value Signature Extraction
Blowfish key 16× C6 45 xx yy (consecutive MOV byte [EBP+off], imm8) validated by 6A 10 (PUSH 16) nearby Read the immediate byte from each instruction.
Resource ID 6A 0A 68 [ID] 6A 00 FF 15 (PUSH RT_RCDATA; PUSH id; PUSH 0; CALL [FindResourceA]) Read 4-byte immediate after 0x68.
XOR key 6A xx 68 [VA] 8D 45 EC 50 E8 (PUSH len; PUSH addr; LEA EAX,[EBP-0x14]; PUSH; CALL md5_hash) Resolve VA to string, compute MD5(string).
Fake relay domains 55 8B EC B8 D0 12 00 00 E8 (function prologue with unique 0x12D0 stack frame) Scan function body for MOV instructions referencing .rdata, walk back to string start, filter by domain regex.
#!/usr/bin/env python3
"""Spambot config extractor. Decrypts C2 config, extracts XOR key and fake relay domains."""

import re, sys, struct, hashlib
from pathlib import Path

try:
    import pefile
except ImportError:
    sys.exit("Missing: pip install pefile")

try:
    from Crypto.Cipher import Blowfish
except ImportError:
    sys.exit("Missing: pip install pycryptodome")

BLOWFISH_KEY_LEN = 16


def extract_blowfish_key(raw):
    """Find 16 consecutive MOV byte ptr [EBP+off], imm8 with sequential offsets.

    Validated by checking for PUSH 0x10 (key length) within 40 bytes after,
    which is the blowfish_key_schedule(ctx, key, 16, iv) call setup.
    """
    """
    Related disassembly in FUN_00472da0_bot_main:
        00472e70 c6 45 e0 07     MOV        byte ptr [EBP + key[0]],0x7
        00472e74 c6 45 e1 54     MOV        byte ptr [EBP + key[1]],0x54
        00472e78 c6 45 e2 08     MOV        byte ptr [EBP + key[2]],0x8
        00472e7c c6 45 e3 d5     MOV        byte ptr [EBP + key[3]],0xd5
        00472e80 c6 45 e4 67     MOV        byte ptr [EBP + key[4]],0x67
        00472e84 c6 45 e5 85     MOV        byte ptr [EBP + key[5]],0x85
        00472e88 c6 45 e6 9e     MOV        byte ptr [EBP + key[6]],0x9e
        00472e8c c6 45 e7 e2     MOV        byte ptr [EBP + key[7]],0xe2
        00472e90 c6 45 e8 af     MOV        byte ptr [EBP + key[8]],0xaf
        00472e94 c6 45 e9 e3     MOV        byte ptr [EBP + key[9]],0xe3
        00472e98 c6 45 ea c6     MOV        byte ptr [EBP + key[10]],0xc6
        00472e9c c6 45 eb a9     MOV        byte ptr [EBP + key[11]],0xa9
        00472ea0 c6 45 ec 8f     MOV        byte ptr [EBP + key[12]],0x8f
        00472ea4 c6 45 ed 3b     MOV        byte ptr [EBP + key[13]],0x3b
        00472ea8 c6 45 ee 0c     MOV        byte ptr [EBP + key[14]],0xc
        00472eac c6 45 ef 5b     MOV        byte ptr [EBP + key[15]],0x5b
        00472eb0 c7 85 08        MOV        dword ptr [EBP + iv],0x0
                 a0 ff ff 
                 00 00 00 00
        00472eba c7 85 0c        MOV        dword ptr [EBP + local_5ff8],0x0
                 a0 ff ff 
                 00 00 00 00
        00472ec4 8d 8d 08        LEA        ECX=>iv,[EBP + 0xffffa008]
                 a0 ff ff
        00472eca 51              PUSH       ECX
        00472ecb 6a 10           PUSH       0x10
        00472ecd 8d 55 e0        LEA        EDX=>key,[EBP + -0x20]
        00472ed0 52              PUSH       EDX
        00472ed1 8d 8d 50        LEA        ECX=>bf_ctx,[EBP + 0xffffa050]
                 a0 ff ff
                             FUN_00421d20 is recognized by capa (Data::Blowfish)
        00472ed7 e8 44 ee        CALL       FUN_00421d20_blowfish_key_schedule               undefined FUN_00421d20_blowfish_
                 fa ff
    """

    i = 0
    while i < len(raw) - BLOWFISH_KEY_LEN * 4:
        if raw[i] == 0xC6 and raw[i + 1] == 0x45:
            base_off = raw[i + 2]
            key = bytearray()
            ok = True
            for n in range(BLOWFISH_KEY_LEN):
                pos = i + n * 4
                if (
                    raw[pos] != 0xC6
                    or raw[pos + 1] != 0x45
                    or raw[pos + 2] != (base_off + n) & 0xFF
                ):
                    ok = False
                    break
                key.append(raw[pos + 3])
            if ok and len(key) == BLOWFISH_KEY_LEN:
                # Verify: PUSH 0x10 (key length) should appear shortly after.
                tail = raw[i + BLOWFISH_KEY_LEN * 4 : i + BLOWFISH_KEY_LEN * 4 + 40]
                if b"\x6a\x10" in tail:
                    return bytes(key)
        i += 1
    return None


DOMAIN_RE = re.compile(
    r"^[a-zA-Z0-9][a-zA-Z0-9._-]{2,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}(?:\.[a-zA-Z]{2,})?$"
)


def va_to_offset(pe, va):
    try:
        return pe.get_offset_from_rva(va - pe.OPTIONAL_HEADER.ImageBase)
    except pefile.PEFormatError:
        return None


def read_cstring(data, off, maxlen=256):
    """Read null-terminated ASCII string at offset."""
    if off < 0 or off >= len(data):
        return None
    end = data.find(b"\x00", off, off + maxlen)
    if end <= off:
        return None
    raw = data[off:end]
    try:
        return raw.decode("ascii") if all(0x20 <= c < 0x7F for c in raw) else None
    except UnicodeDecodeError:
        return None


def get_section(pe, name):
    for s in pe.sections:
        if s.Name.rstrip(b"\x00") == name.encode():
            return s
    return None


def extract_resource_id(raw):
    """Find the PE resource ID from the FindResourceA call pattern."""
    """
    Related disassembly in FUN_00472da0_bot_main:
                                 LAB_00472def                                    XREF[1]:     00472de9(j)  
        00472def 6a 0a           PUSH       DAT_0000000a
        00472df1 68 e2 00        PUSH       DAT_000000e2
                 00 00
        00472df6 6a 00           PUSH       0x0
                             0xe2 = resource ID
                             0xa = RT_RCDATA = Application-defined resource (raw data).
        00472df8 ff 15 30        CALL       dword ptr [->KERNEL32.DLL::FindResourceA]        = 0019fa9e
                 01 56 00
    """
    sig = b"\x6a\x0a\x68"
    tail = b"\x6a\x00\xff\x15"
    start = 0
    while True:
        pos = raw.find(sig, start)
        if pos == -1:
            return None
        if raw[pos + 7 : pos + 11] == tail:
            return struct.unpack_from("<I", raw, pos + 3)[0]
        start = pos + 1


def extract_config(pe, key, res_id):
    if not hasattr(pe, "DIRECTORY_ENTRY_RESOURCE"):
        return None
    for t in pe.DIRECTORY_ENTRY_RESOURCE.entries:
        if t.id != 10:  # RT_RCDATA
            continue
        for i in t.directory.entries:
            if i.id != res_id:
                continue
            d = i.directory.entries[0]
            res = pe.get_data(d.data.struct.OffsetToData, d.data.struct.Size)
            hexstr = res.decode("ascii", errors="ignore").strip().rstrip("\x00")
            if len(hexstr) % 2:
                hexstr = hexstr[:-1]
            enc = bytes.fromhex(hexstr)
            if len(enc) % 8:
                enc += b"\x00" * (8 - len(enc) % 8)
            plain = Blowfish.new(key, Blowfish.MODE_ECB).decrypt(enc)
            return plain.rstrip(b"\x00").decode("ascii", errors="replace")
    return None


def parse_config(s):
    cfg = {
        "raw": s,
        "server": s,
        "port": 50001,
        "port2": 50002,
        "service": "Enterprise Mailing Service",
    }
    r = s
    if ";" in r:
        r, cfg["service"] = r.rsplit(";", 1)
    if "/" in r:
        r, p = r.rsplit("/", 1)
        try:
            cfg["port2"] = int(p)
        except ValueError:
            pass
    if ":" in r:
        srv, p = r.rsplit(":", 1)
        cfg["server"] = srv
        try:
            cfg["port"] = int(p)
        except ValueError:
            pass
    else:
        cfg["server"] = r
    return cfg


def extract_xor_key(pe, raw):
    """Find the md5_hash call that derives the XOR cipher key.

    Searches for LEA EAX,[EBP-0x14]; PUSH EAX; CALL (the 16-byte MD5
    output buffer setup), then walks back 7 bytes to read the preceding
    PUSH len; PUSH va. The virtual address is resolved to a file offset
    to read the plaintext string, which is then MD5-hashed to produce
    the 16-byte XOR key.
    """
    """
    Related disassembly in FUN_004680d0_xor_cipher:
                                 LAB_004680e6                                    XREF[1]:     004680e0(j)  
        004680e6 6a 0c           PUSH       12
        004680e8 68 ac 73        PUSH       s_UNREGISTERED_005673ac                          = "UNREGISTERED"
                 56 00
        004680ed 8d 45 ec        LEA        EAX=>key,[EBP + -0x14]
        004680f0 50              PUSH       EAX
        004680f1 e8 0a 38        CALL       FUN_0044b900_md5_hash                            undefined FUN_0044b900_md5_hash(
                 fe ff
    """

    needle = b"\x8d\x45\xec\x50\xe8"
    start = 0
    while True:
        pos = raw.find(needle, start)
        if pos == -1 or pos < 7:
            return None
        if raw[pos - 7] == 0x6A and raw[pos - 5] == 0x68:
            slen = raw[pos - 6]
            sva = struct.unpack_from("<I", raw, pos - 4)[0]
            soff = va_to_offset(pe, sva)
            if soff and 4 <= slen <= 64:
                s = read_cstring(raw, soff, slen + 16)
                if s and s.isalpha() and s.isupper():
                    return hashlib.md5(s.encode()).digest(), s
        start = pos + 1


def extract_relay_domains(pe, raw):
    """Find the Received header forgery function and extract its domain list.

    Locates the function by its unique 0x12D0-byte stack frame, then scans
    the function body for MOV instructions that reference .rdata virtual
    addresses. Each address is resolved to a file offset and walked backward
    to the preceding NUL byte to recover the full string. Results are
    filtered through a domain regex and deduplicated.
    """
    """
    Related disassembly in FUN_00450e10_forge_received_headers:
        00450e10 55              PUSH       EBP
        00450e11 8b ec           MOV        EBP,ESP
        00450e13 b8 d0 12        MOV        EAX,0x12d0
                 00 00
        00450e18 e8 93 07        CALL       __alloca_probe                                   undefined __alloca_probe(void)
                 0f 00
    """
    rdata = get_section(pe, ".rdata")
    if not rdata:
        return []
    base = pe.OPTIONAL_HEADER.ImageBase
    rva_lo = base + rdata.VirtualAddress
    rva_hi = rva_lo + rdata.Misc_VirtualSize
    rd_off = rdata.PointerToRawData
    rd_data = raw[rd_off : rd_off + rdata.SizeOfRawData]

    # Find function prologue.
    func = raw.find(b"\x55\x8b\xec\xb8\xd0\x12\x00\x00\xe8")
    if func == -1:
        return []

    # Scan function body for MOV instructions referencing .rdata VAs.
    body = raw[func : func + 0x1200]
    vas = set()
    # (opcode_prefix, offset_to_va_bytes)
    """
    Related disassembly in FUN_00450e10_forge_received_headers:
        00450f2b a1 80 49        MOV        EAX,[s_345mail.com_00564978+8]                   = "345mail.com"
                 56 00
        ...
        00450f36 8b 0d 84        MOV        ECX,dword ptr [s_ail.com_00564978+0xc]           = "ail.com"
                 49 56 00

    """
    for prefix, off in [
        (b"\xa1", 1),
        (b"\xa0", 1),
        (b"\xbe", 1),
        (b"\x8b\x0d", 2),
        (b"\x8b\x15", 2),
        (b"\x8b\x35", 2),
        (b"\x8a\x0d", 2),
        (b"\x8a\x15", 2),
        (b"\x66\xa1", 2),
        (b"\x66\x8b\x0d", 3),
        (b"\x66\x8b\x15", 3),
    ]:
        i = 0
        while True:
            i = body.find(prefix, i)
            if i == -1 or i + off + 4 > len(body):
                break
            va = struct.unpack_from("<I", body, i + off)[0]
            if rva_lo <= va < rva_hi:
                vas.add(va)
            i += 1

    # Resolve VAs to strings, walking back to find full domain.
    domains, seen = [], set()
    for va in vas:
        foff = va_to_offset(pe, va)
        if foff is None:
            continue
        rel = foff - rd_off
        while rel > 0 and rd_data[rel - 1] != 0:
            rel -= 1
        if rel in seen:
            continue
        seen.add(rel)
        s = read_cstring(rd_data, rel, 128)
        if s and len(s) >= 6 and DOMAIN_RE.match(s):
            domains.append(s)

    return sorted(set(domains), key=str.lower)


def main():
    if len(sys.argv) < 2:
        sys.exit(f"Usage: {sys.argv[0]} <sample.exe>")

    path = sys.argv[1]
    if not Path(path).exists():
        sys.exit(f"[!] File not found: {path}.")

    pe = pefile.PE(path)
    raw = Path(path).read_bytes()

    # Blowfish key.
    bf_key = extract_blowfish_key(raw)
    if bf_key is None:
        sys.exit("[!] Blowfish key not found.")
    print(f"[+] Blowfish key:   {bf_key.hex()}")

    # Resource ID.
    res_id = extract_resource_id(raw)
    if res_id is None:
        sys.exit("[!] Resource ID not found.")
    print(f"[+] Resource ID:    0x{res_id:X}")

    # Config.
    plain = extract_config(pe, bf_key, res_id)
    if plain is None:
        sys.exit(f"[!] Resource RT_RCDATA/0x{res_id:X} not found.")
    cfg = parse_config(plain)
    print(f"[+] C2 server:      {cfg['server']}")
    print(f"[+] 1. port:        {cfg['port']}")
    print(f"[+] 2. port:        {cfg['port2']}")
    print(f"[+] Service name:   {cfg['service']}")

    # XOR key.
    xor = extract_xor_key(pe, raw)
    if xor:
        key, src = xor
        print(f'[+] XOR key:        {key.hex()}  (MD5("{src}"))')
    else:
        print("[!] XOR key not found.")

    # Relay domains.
    domains = extract_relay_domains(pe, raw)
    if domains:
        print(f"[+] Fake relay domains ({len(domains)}):")
        for d in domains:
            print(f"    {d}")
    else:
        print("[!] Relay domains not found.")


if __name__ == "__main__":
    main()