Reversing a spam bot
Table of contents
- Table of contents
- Introduction
- Executive summary
- High-level analysis
- Prerequisites
- Detailed analysis (Ghidra)
- IOCs
- Config extractor
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.dllorBCRYPT.dll(OpenSSL is statically linked, no Windows CryptoAPI) WS2_32.dll: raw TCP/UDP sockets are supportedWININET.dll: high-level HTTP is supportedDNSAPI.dll: DNS queries are supportedKERNEL32.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
cmdprompt cd "C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC"vcvarsall x86(setup the build env)cdinto the root of the OpenSSL sourcesperl Configure VC-WIN32 --prefix=C:\opensslms\do_nasmnmake -f ms\ntdll.mak
We need the following built libraries:
libeay32.dll(crypto) +libeay32.pdbfor symbol namesssleay32.dll(ssl) +ssleay32.pdbfor symbol names
Additionally, we need libc from the compiler install folder with the common functions (e.g. strcpy):
libcmt.lib+libcmt.pdbfor 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.

Or, for example, we can identify the unknown function FUN_00417380 as 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.

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.

Renamed functions:
FUN_00540490_strcpyFUN_005404a0_strcatFUN_00540c70_strchrFUN_00540da0_str_strFUN_005410ca_sprintfFUN_00541f40_memsetFUN_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)
- install service (
-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:0x00002000SECURITY_FLAG_IGNORE_CERT_CN_INVALID:0x00001000SECURITY_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()