Flash CTF – Armorless

Solution

Enumeration

We are provided with two files: .vmss and .vmem, along with a ZIP archive containing encrypted files under the Documents directory. The goal is to perform memory analysis and recover the original file flag.pdf.

The .vmss and .vmem files are part of a VMware snapshot. To analyze the memory, we must convert them into a proper memory dump using the vmss2core tool (reference):

vmss2core.exe -W8 Win10x64-456e5d85.vmss Win10x64-456e5d85.vmem

This generates a memory.dmp file, which can be examined using tools such as Volatility 3 and MemprocFS.


Malware Hunting

The encrypted files under Documents were all modified at 12:05:19, suggesting that the ransomware was executed around that time.

However, running pslist did not reveal any processes running at that specific time. This implies the ransomware process likely terminated before the snapshot, making it invisible to standard process listings.

Instead, we pivot to analyzing historical process execution. Amcache is a good source for this purpose. Using Volatility 3’s amcache plugin:

vol3 -f memory.dmp windows.registry.amcache

Relevant output:

File    c:\windows\system32\runtimebṙoker.exe           2025-07-01 12:05:18.000000 UTC  [...]  d1ef4d2a69addc1fabde574b42c25673a2d09d69

This binary was executed exactly 1 second before the files were encrypted. Moreover, it uses a visually deceptive character () to mimic RuntimeBroker.exe, increasing suspicion that this is the ransomware.

We then locate the executable in memory:

vol3 -f memory.dmp windows.filescan | grep -i runtimebṙoker

Dumping the binary:

vol3 -f memory.dmp windows.dumpfiles --virtaddr 0xce05c1bd1db0

Ransomware Reversing

Running strings on the dumped binary reveals it is a PyInstaller-packed Python executable. We extract its contents using pyinstxtractor.

The decompiled code (transform.pyc) was protected with PyArmor. We can unpack it with PyArmor-Unpacker.

There are three methods to unpack PyArmor. You can choose any of them freely. After deobfuscating, you will obtain the original source code.

import os
import random
import platform
from Crypto.Hash import SHA256
import math
import time

def obfuscate_vector(IIlIlIlllIIllIIlIlIlIIIlIIlIIlIllllIllll):
    return (lambda .0: [ (x * 17 ^ 42) % 256 for lIIIIlIlIIIlIIIIlIIIllIIllIIIIIIIIlIIIIl in .0 ])(range(len(IIlIlIlllIIllIIlIlIlIIIlIIlIIlIllllIllll) + 13))


def pseudo_hash(IlIlllIllIlIIIllIllIllIIIllIllIIlIIlIllI):
    llIlIlIlIIllIlIlIIllIIIlIllIIlIlIIlllIII = int(time.time() * 1000) % 65536
    return IlIlllIllIlIIIllIllIllIIIllIllIIlIIlIllI * llIlIlIlIIllIlIlIIllIIIlIllIIlIlIIlllIII ^ (llIlIlIlIIllIlIlIIllIIIlIllIIlIlIIlllIII << 3) % 0xDEADBEEFL


def shuffle_entropy(IIIIlIIIIIlllIlIIIIlIIlIllIlIIllIIllIIlI, IIIlIlIlllIllIllIIllIIIllIIIllIlIIIIIIll):
    IlllIIIlIIllIIIlllIIIlIIIIIllIIIlIlIIlII = list(IIIIlIIIIIlllIlIIIIlIIlIllIlIIllIIllIIlI)
    for IIIlllllllIlIIllIlllllIIIlIIlIIlIlIlIlIl in range(IIIlIlIlllIllIllIIllIIIllIIIllIlIIIIIIll % 7):
        IlllIIIlIIllIIIlllIIIlIIIIIllIIIlIlIIlII = (lambda .0 = None: [ (ord(IlllIIIlIIllIIIlllIIIlIIIIIllIIIlIlIIlII[i]) + i * 3) % 127 for lIIlIllIllllIIIlIIlllIlIllIlIIllIllIlIlI in .0 ])(range(len(IlllIIIlIIllIIIlllIIIlIIIIIllIIIlIlIIlII)))

    return ''.join((lambda .0: pass)(IlllIIIlIIllIIIlllIIIlIIIIIllIIIlIlIIlII))


def randomize_noise(IIIllIllIllllIIIIIlIIIIllllIIIIIlIIIIllI):
    return sum((lambda .0: pass)(range(IIIllIllIllllIIIIIlIIIIllllIIIIIlIIIIllI % 64)))


def fake_encrypt_alpha(IIlIIIIIIIIIlIIllIIlIlIlIIIlIIlllIlllIlI, IIIIlIIIIIlllIlIIIIlIIlIllIlIIllIIllIIlI):
    if len(IIlIIIIIIIIIlIIllIIlIlIlIIIlIIlllIlllIlI) % 2 == 1:
        IIlIIIIIIIIIlIIllIIlIlIlIIIlIIlllIlllIlI += b'\x01'
    lllIllIIIllllIlIIIlllIIlIllIlIllllIlIlII = bytearray()
    for lIIlIllIllllIIIlIIlllIlIllIlIIllIllIlIlI in range(0, len(IIlIIIIIIIIIlIIllIIlIlIlIIIlIIlllIlllIlI), 2):
        lIIIIlIlIIIlIIIIlIIIllIIllIIIIIIIIlIIIIl = IIlIIIIIIIIIlIIllIIlIlIlIIIlIIlllIlllIlI[lIIlIllIllllIIIlIIlllIlIllIlIIllIllIlIlI]
        lllllIllllIllIllIllIIIIllIlIIIlIlIIIIlIl = IIlIIIIIIIIIlIIllIIlIlIlIIIlIIlllIlllIlI[lIIlIllIllllIIIlIIlllIlIllIlIIllIllIlIlI + 1] if lIIlIllIllllIIIlIIlllIlIllIlIIllIllIlIlI + 1 < len(IIlIIIIIIIIIlIIllIIlIlIlIIIlIIlllIlllIlI) else 0
        IlIIIllIIIIIlIIlIIIIlIIIlIIlIlIIlllIlllI = (lIIIIlIlIIIlIIIIlIIIllIIllIIIIIIIIlIIIIl ^ ord(IIIIlIIIIIlllIlIIIIlIIlIllIlIIllIIllIIlI[lIIlIllIllllIIIlIIlllIlIllIlIIllIllIlIlI % len(IIIIlIIIIIlllIlIIIIlIIlIllIlIIllIIllIIlI)])) << 5
        IlIIIllIIIIIlIIlIIIIlIIIlIIlIlIIlllIlllI = (IlIIIllIIIIIlIIlIIIIlIIIlIIlIlIIlllIlllI | lllllIllllIllIllIllIIIIllIlIIIlIlIIIIlIl >> 3) & 65535
        lllIllIIIllllIlIIIlllIIlIllIlIllllIlIlII.append(IlIIIllIIIIIlIIlIIIIlIIIlIIlIlIIlllIlllI >> 7 & 255)
        lllIllIIIllllIlIIIlllIIlIllIlIllllIlIlII.append(IlIIIllIIIIIlIIlIIIIlIIIlIIlIlIIlllIlllI & 255)

    return bytes(lllIllIIIllllIlIIIlllIIlIllIlIllllIlIlII)


def fake_encrypt_beta(IIlIlIlllIIllIIlIlIlIIIlIIlIIlIllllIllll, IlllIIIlIIllIIIlllIIIlIIIIIllIIIlIlIIlII):
    IlIIllIlIIIIlIlIllllIlIlIlIllIlIllIIlllI = bytearray()
    for lIIlIllIllllIIIlIIlllIlIllIlIIllIllIlIlI, IlllIIIllIlllIllIIIIlIlllIIIllIllllllllI in enumerate(IIlIlIlllIIllIIlIlIlIIIlIIlIIlIllllIllll):
        llIlIlIlIIllIlIlIIllIIIlIllIIlIlIIlllIII = (IlllIIIllIlllIllIIIIlIlllIIIllIllllllllI ^ ord(IlllIIIlIIllIIIlllIIIlIIIIIllIIIlIlIIlII[lIIlIllIllllIIIlIIlllIlIllIlIIllIllIlIlI % len(IlllIIIlIIllIIIlllIIIlIIIIIllIIIlIlIIlII)])) * 11
        llIlIlIlIIllIlIlIIllIIIlIllIIlIlIIlllIII = llIlIlIlIIllIlIlIIllIIIlIllIIlIlIIlllIII >> 2 ^ llIlIlIlIIllIlIlIIllIIIlIllIIlIlIIlllIII << 4
        IlIIllIlIIIIlIlIllllIlIlIlIllIlIllIIlllI.append(llIlIlIlIIllIlIlIIllIIIlIllIIlIlIIlllIII % 256)

    return bytes(IlIIllIlIIIIlIlIllllIlIlIlIllIlIllIIlllI)


def encrypt(IIlIIIIIIIIIlIIllIIlIlIlIIIlIIlllIlllIlI, IIIIlIIIIIlllIlIIIIlIIlIllIlIIllIIllIIlI):
    if len(IIlIIIIIIIIIlIIllIIlIlIlIIIlIIlllIlllIlI) % 2 != 0:
        IIlIIIIIIIIIlIIllIIlIlIlIIIlIIlllIlllIlI += b'\x00'
    IlIllIlIIIlllllIllIllIlIIIlIIIIllIlIIIII = bytearray()
    for lIIlIllIllllIIIlIIlllIlIllIlIIllIllIlIlI in range(0, len(IIlIIIIIIIIIlIIllIIlIlIlIIIlIIlllIlllIlI), 2):
        lllllIlIIllllIllIlIllIlIIIIIlIIIllIllIII = IIlIIIIIIIIIlIIllIIlIlIlIIIlIIlllIlllIlI[lIIlIllIllllIIIlIIlllIlIllIlIIllIllIlIlI]
        IlllIIIllIlllIllIIIIlIlllIIIllIllllllllI = IIlIIIIIIIIIlIIllIIlIlIlIIIlIIlllIlllIlI[lIIlIllIllllIIIlIIlllIlIllIlIIllIllIlIlI + 1]
        lIIIIlIlIIIlIIIIlIIIllIIllIIIIIIIIlIIIIl = (lllllIlIIllllIllIlIllIlIIIIIlIIIllIllIII << 8 | IlllIIIllIlllIllIIIIlIlllIIIllIllllllllI) ^ ord(IIIIlIIIIIlllIlIIIIlIIlIllIlIIllIIllIIlI[lIIlIllIllllIIIlIIlllIlIllIlIIllIllIlIlI % len(IIIIlIIIIIlllIlIIIIlIIlIllIlIIllIIllIIlI)])
        lllllIllllIllIllIllIIIIllIlIIIlIlIIIIlIl = (lIIIIlIlIIIlIIIIlIIIllIIllIIIIIIIIlIIIIl << 3 | lIIIIlIlIIIlIIIIlIIIllIIllIIIIIIIIlIIIIl >> 13) & 65535
        IlIllIlIIIlllllIllIllIlIIIlIIIIllIlIIIII.append(lllllIllllIllIllIllIIIIllIlIIIlIlIIIIlIl >> 8 & 255)
        IlIllIlIIIlllllIllIllIlIIIlIIIIllIlIIIII.append(lllllIllllIllIllIllIIIIllIlIIIlIlIIIIlIl & 255)

    return bytes(IlIllIlIIIlllllIllIllIlIIIlIIIIllIlIIIII)


def protect_pytransform():
    import pytransform

    def assert_builtin(func):
        type = ''.__class__.__class__
        builtin_function = type(''.join)
        if type(func) is not builtin_function:
            raise RuntimeError('%s() is not a builtin' % func.__name__)


    def check_obfuscated_script():
        CO_SIZES = (55, 52, 49, 46, 42, 40, 38, 36)
        CO_NAMES = set([
            'pytransform',
            'pyarmor_runtime',
            '__pyarmor__',
            '__name__',
            '__file__'])
        co = pytransform.sys._getframe(3).f_code
        if not set(co.co_names) <= CO_NAMES or len(co.co_code) in CO_SIZES:
            raise RuntimeError('unexpected obfuscated script')


    def check_mod_pytransform():

        def _check_co_key(co, v):
            return (len(co.co_names), len(co.co_consts), len(co.co_code)) == v

        for v1, v2, v3 in (('dllmethod', ((0, 3, 16), (0, 1, 10), None)), ('init_pytransform', ((0, 1, 10), None, None)), ('init_runtime', ((0, 1, 10), None, None)), ('_load_library', ((33, 22, 700), None, None)), ('get_registration_code', ((0, 1, 10), None, None)), ('get_expired_days', ((0, 1, 10), None, None)), ('get_hd_info', ((12, 10, 124), None, None)), ('get_license_info', ((11, 24, 402), None, None)), ('get_license_code', ((1, 2, 10), None, None)), ('format_platform', ((18, 18, 288), None, None)), ('pyarmor_init', ((3, 1, 22), None, None)), ('pyarmor_runtime', ((12, 5, 128), None, None)), ('_match_features', ((1, 2, 32), None, None))):
            co = getattr(pytransform, k).__code__
            if not _check_co_key(co, v1):
                raise RuntimeError('unexpected pytransform.py')
            if not None and _check_co_key(co.co_consts[1], v2):
                raise RuntimeError('unexpected pytransform.py')
            if not None and _check_co_key(co.__closure__[0].cell_contents.__code__, v3):
                raise RuntimeError('unexpected pytransform.py')



    def check_lib_pytransform():
        platname = pytransform.sys.platform
        if platname.startswith('darwin'):
            return None
        if None.startswith('darwin'):
            pass
        elif platname.startswith('win'):
            pass
        elif platname.startswith('cygwin'):
            pass

        libname = '_pytransform.so'
        with open(filename, 'rb') as f:
            buf = bytearray(f.read())
        value = sum(buf)
        if getattr(pytransform.sys, 'frozen', False) and sys.platform == 'darwin':
            value += 1217
        if value not in (120398550,):
            raise RuntimeError('unexpected %s' % filename)
        return '_pytransform.dll' if getattr(pytransform.sys, 'frozen', False) else '_pytransform.dll'

    assert_builtin(sum)
    assert_builtin(open)
    assert_builtin(len)
    check_obfuscated_script()
    check_mod_pytransform()
    check_lib_pytransform()

protect_pytransform()
if __name__ == '__main__':
    if platform.node() != 'Win10':
        exit()
    IIlllIIIllIIlllllllIlIIlIlIlIlIlIIlIIIll = os.path.join(os.path.expanduser('~'), 'Documents')
    for llIlIlIlIIlIIIIIlIlllIlllIlIIlIlIIlIlllI, lIIIllIIllIlIlIllIIlIIllIlIllIlIIIlllIIl, llllIllIllIlIIllIlIIlIlIlIlIllIIIIIlIlll in os.walk(IIlllIIIllIIlllllllIlIIlIlIlIlIlIIlIIIll):
        for llllIllIllIlllIllIIIIIlIllIIIlIIlllIIIII in llllIllIllIlIIllIlIIlIlIlIlIllIIIIIlIlll:
            if llllIllIllIlllIllIIIIIlIllIIIlIIlllIIIII.endswith('.enc'):
                continue
            IIIIIIIlllllllIlIIllllllIIIIlIlIlIlIlllI = os.path.join(llIlIlIlIIlIIIIIlIlllIlllIlIIlIlIIlIlllI, llllIllIllIlllIllIIIIIlIllIIIlIIlllIIIII)
            if not os.path.isfile(IIIIIIIlllllllIlIIllllllIIIIlIlIlIlIlllI):
                continue
            with open(IIIIIIIlllllllIlIIllllllIIIIlIlIlIlIlllI, 'rb') as lIIlIllIlIlIIlIlllIIllIIIlIlIlllIIlIIlIl:
                IIlIlIlllIIllIIlIlIlIIIlIIlIIlIllllIllll = lIIlIllIlIlIIlIlllIIllIIIlIlIlllIIlIIlIl.read()
            lIllllIIlIIllIllllIIIlIlIIIllllIllIIIlII = int(os.path.getmtime(IIIIIIIlllllllIlIIllllllIIIIlIlIlIlIlllI))
            random.seed(lIllllIIlIIllIllllIIIlIlIIIllllIllIIIlII)
            IIIIlIIIIIlllIlIIIIlIIlIllIlIIllIIllIIlI = SHA256.new(str(random.randint(1, 13374953)).encode('utf-8')).digest().decode('latin1')
            IllIlIllIIIIIlllIIlIIlIIIIllIIlIllIllllI = IIlIlIlllIIllIIlIlIlIIIlIIlIIlIllllIllll[:10]
            IlIIlIIlIIIIIlIIlIIlIllIIlIIIlIlIIIlIIIl = IIlIlIlllIIllIIlIlIlIIIlIIlIIlIllllIllll[10:]
            lIlIIIlIIlIIlllIlIllIllIlIlIIlllIIIllIIl = encrypt(IlIIlIIlIIIIIlIIlIIlIllIIlIIIlIlIIIlIIIl, IIIIlIIIIIlllIlIIIIlIIlIllIlIIllIIllIIlI)
            IIlllIIIlIllIllllIlIllIlIIIIIIIlIllIlllI = IllIlIllIIIIIlllIIlIIlIIIIllIIlIllIllllI + lIlIIIlIIlIIlllIlIllIllIlIlIIlllIIIllIIl
            with open(IIIIIIIlllllllIlIIllllllIIIIlIlIlIlIlllI + '.enc', 'wb') as lIIlIllIlIlIIlIlllIIllIIIlIlIlllIIlIIlIl:
                lIIlIllIlIlIIlIlllIIllIIIlIlIlllIIlIIlIl.write(IIlllIIIlIllIllllIlIllIlIIIIIIIlIllIlllI)

Some functions are intentionally fake and only serve to confuse the code, but it’s not difficult to remove them and clean up the source and recover the core encryption logic:

def encrypt(data, key):
    if len(data) % 2 != 0:
        data += b'\x00'
    enc = bytearray()
    for i in range(0, len(data), 2):
        a = data[i]
        b = data[i + 1]
        x = ((a << 8) | b) ^ ord(key[i % len(key)])
        y = ((x << 3) | (x >> 13)) & 0xFFFF
        enc.append((y >> 8) & 0xFF)
        enc.append(y & 0xFF)
    return bytes(enc)

The program encrypts all files in Documents, excluding those ending with .enc. It seeds a PRNG with the file’s mtime and derives the key as:

random.seed(os.path.getmtime(file))
key = SHA256.new(str(random.randint(1, 13374953)).encode()).digest().decode('latin1')

The encryption preserves the first 10 bytes of the file and encrypts the rest using a custom cipher. The original files are deleted after encryption.


Recovering the Key via MFT

To decrypt the file, we must obtain its modification time (mtime). Even though the original file was deleted, we can recover metadata from the MFT using:

vol3.py -f memory.raw windows.mftscan.MFTScan | grep flag.pdf

Volatility 3 Framework 2.26.2

Offset  Record Type     Record Number   Link Count      MFT Type        Permissions     Attribute Type  Created Modified        Updated Accessed        Filename
* 0xb320dcb0    FILE    130711  1       Removed Archive FILE_NAME       2025-07-01 11:57:20.000000 UTC  2025-06-25 13:31:43.000000 UTC  2025-07-01 11:57:20.000000 UTC  2025-07-01 11:57:20.000000 UTC  flag.pdf
* 0xb330e0b0    FILE    93268   2       File    Archive FILE_NAME       2025-07-01 12:05:19.000000 UTC  2025-07-01 12:05:19.000000 UTC  2025-07-01 12:05:19.000000 UTC  2025-07-01 12:05:19.000000 UTC  flag.pdf.enc
* 0xb61e7998    FILE    93268   2       File    Archive FILE_NAME       2025-07-01 12:05:19.000000 UTC  2025-07-01 12:05:19.000000 UTC  2025-07-01 12:05:19.000000 UTC  2025-07-01 12:05:19.000000 UTC  flag.pdf.enc

The modification timestamp is: 2025-06-25 13:31:43, which corresponds to Unix epoch 1750836703.


Decryption Script

Based on the analysis, we reconstruct the decryption algorithm as follows:

import random
from Crypto.Hash import SHA256

def decrypt_partial(enc, key):
    dec = bytearray(enc[:10])
    for i in range(10, len(enc), 2):
        if i + 1 >= len(enc):
            break
        a = enc[i]
        b = enc[i + 1]
        y = (a << 8) | b
        x = ((y >> 3) | (y << 13)) & 0xFFFF
        x ^= ord(key[(i - 10) % len(key)])
        dec.append(x >> 8)
        dec.append(x & 0xFF)
    while dec and dec[-1] == 0:
        dec.pop()
    return bytes(dec)

if __name__ == '__main__':
    with open("flag.pdf.enc", 'rb') as f:
        data = f.read()

    random.seed(1750836703)
    key = SHA256.new(str(random.randint(1, 13374953)).encode()).digest().decode('latin1')
    output = decrypt_partial(data, key)

    with open("flag.pdf", "wb") as f:
        f.write(output)

    print("Done")

Result

After running the script and opening the decrypted flag.pdf, the flag is successfully recovered.