Abstract
From time to time I browse teh database of Malware Bazare to get a feeling about current developments in the field of malware. In the last month I felt the subjective rise of python based malware. The later motivated me to took a look at python based malware an familiarizing myself with the available tool, techniques and knowledge to analyze these type of threats. Mainly there are two types of python based malware, excluding the simple Python scripts used by adversaries from the discussion:
- PyInstaller based Images
- Binaries compiled using Nuitka
While both types of malware were initially built from Python source code, the analysis differ by any means. For the sake of this blog post I will discuss an easy to analyze PyInstaller based sample.
The discussed sample is a version of “Lunar Stealer”, which was uploaded in the erarly 2026 Malware Bazare and can be downloaded from there. The sample has the following meta-data.
- SHA256:
d4f57b427a281c4dc32d6b8cc12195e0ce262c9f80ac8a8505ff3a0b47536324 - Malware Family: Lunar Stealer
- File Type
PEfile, AMD64, Windows GUI Subsystem - Download Link: Malware Bazar
So lets get started with our initial triage of the file!
Static File Triage of the Sample
The given sample is PE 64 Bit file (AMD64) with a compilation timestamp of Saturday, Januar 3rd 2026 at 04:47:13. Thus, the compilation is very recent. The sub-system is Windows GUI. We observe one exported function only. I.e., the _start symbol. However we have several libraries included:
USER32.dllCOMCTL32.dllKERNEL32.dllADVAPI32.dllGDI32.dll
The imported libraries are common in case of Windows GUI application. We immediately observe the huge amount of imported functions from these libraries, which is uncommon for malware. Checking the function names, we spot some commonly utilized by malware as well:
OpenProcessTokenGetTokenInformationGetProcAddress,LoadLibraryAVirtualProtectEnterCriticalSection,LeaveCriticalSectionand similar- Several Anti-Debugging functions like
IsDebuggerPresent
Otherwise we notice the GDI relevant functions from the Windows forms libraries. So up to here not really surprising. Also the applications sections does look normal. Talking about contained strings it gets interesting. There are a few human readable ones only, mainly referencing Python modules and the pyInstaller. Thus, we likely have to deal with a Python project here.
Finally let us note, that there is beside _start a function called main and several small, unnamed functions.
Before using any public tools to tackle this malware, let us shortly dive a little bit deeper into the sample by taking a look at the assembly level. We start our investigations at the _start entry-point. We observe the usual initialization of the C-Runtime Environment (CRT), followed by the execution of main. The main method just sets two global variables and delegates execution to a function located at 0x140004b50.

Scrolling over this function the strings already suggest, that this is the main routine of the PyInstaller. The strings gave us an abstract understanding on the PyInstallers operation.
- A PyInstaller PKG is sought, first by trying to extract it from the file, then trying to side-load it from the directory.
- Several Environment variables were set.
- Additional libraries were resolved.

All of this is not obfuscated by any means. We have to deal with classic PyInstaller project here. From here one, we could start a cumbersome and manual process: (1) exctracting the PKG from the resource section of the PE file, (2) removing all the irrelevant depdendencies not related to the malware, and finally (3) de-compiling the python byte code. However there are faster options. PyInstaller is a well known project and no wonders here, there are tools to extract .pyc files from the PyInstaller archive. Hence, we do not need to reinvent the wheel. Instead, we will use PyInstXtractor to extract the actual malicious code.
Extracting the PyInstaller PKG archive
To extract embedded PyInstaller PKG archives we have to extract the embedded Python byte code from the sample, optionally decompress it and safe it to disk. All this can be done by using e.g., Binary Ninja only, however as said before, the required reverse engineering will be a cumbersome task. Nevertheless the hard work is already be done for us! The project PyInstXtractor does allow us to unpack python stand-alone executables generated by arbitrary version of PyInstaller.
The application can directly be used on any host supporting Python3 projects, however I prefer to execute the tool inside a docker container for the sake of isolation.
FROM python:3.12-slim
# Install build dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
git \
build-essential \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
# Clone repository
RUN git clone https://github.com/pyinstxtractor/pyinstxtractor-ng.git
WORKDIR /build/pyinstxtractor-ng
# Install Python dependencies
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
pip install --no-cache-dir .
# Optional: remove build tools to keep image minimal
RUN apt-get purge -y build-essential git && \
apt-get autoremove -y && \
rm -rf /build
# Default working directory for runtime
WORKDIR /data
# Default command
ENTRYPOINT ["pyinstxtractor-ng"]
Now, the sample can easily be processed by
docker build -t pyinstxtractor-ng .
docker run --rm -v "$PWD:/data" pyinstxtractor-ng /data/sample.exe
and
docker run --rm -v "$PWD:/data" pyinstxtractor-ng /data/d4f57b427a281c4dc32d6b8cc12195e0ce262c9f80ac8a8505ff3a0b47536324.exe
[!] Error: Failed to decrypt & decompress PYZ.pyz_extracted/setuptools/_distutils/compilers.pyc. Extracting as is.
[!] Error: Failed to decrypt & decompress PYZ.pyz_extracted/setuptools/_distutils/compilers/C.pyc. Extracting as is.
[!] Error: Failed to decrypt & decompress PYZ.pyz_extracted/setuptools/_vendor.pyc. Extracting as is.
[!] Error: Failed to decrypt & decompress PYZ.pyz_extracted/setuptools/_vendor/jaraco.pyc. Extracting as is.
[+] Processing /data/d4f57b427a281c4dc32d6b8cc12195e0ce262c9f80ac8a8505ff3a0b47536324.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 3.11
[+] Length of package: 17729487 bytes
[+] Found 113 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth_setuptools.pyc
[+] Possible entry point: pyi_rth_cryptography_openssl.pyc
[+] Possible entry point: cleaner.pyc
[+] Found 760 files in PYZ archive
[+] Successfully extracted pyinstaller archive: /data/d4f57b427a281c4dc32d6b8cc12195e0ce262c9f80ac8a8505ff3a0b47536324.exe
You can now use a python decompiler on the pyc files within the extracted directory
Unpacking the sample yields a huge amount of files packed with this sample. The next step will be to figure out the ones containing the actual malicious logic. Thanks to clear naming scheme, we can exclude a lot of the files due to being originated from trusted third-party python libraries.
One file does look odd considering the naming scheme, the cleaner.pyc. So I decided to focus my analysis to that file. Since we are dealing with Python byte code here and the version is known, we can use e.g., python-decompile3. But wait, the decompiler is supposed to work with Python 3.7 to 3.8 only! However, here we are dealing with a Python 3.11 file. Anyways, this will not be a show stopper.
Let me introduce you an awesome project named PyLingual. PyLingual is a cross-version compatible online python byte code decompiler. Since we are not restricted by any means of confidentiality or privacy concerns here, we can directly upload our sample to the decompilers page. The result will be a somehow obfuscated Python code:

The code does look very promising! Just consider the very few bytes, i.e., bytes([98, 97, 115, 101, 54, 52]).decode(), which will decode to base64. Also later in the code we will find a large blob of binary data.
Our next goal will be to de-obfuscate the code.
De-Obfuscation cleaner.pyc
The de-obfuscation process is quit straight forward. The sample does have a large binary blob named by eight underscores. I.e., ________. The other variable names are different amount of underscores as well. We can easily provide them more human readable names by simple regex based find & replace operations.
Obfuscation is mainly done by encoding strings and function names by means of byte arrays with UTF-8 equivalent ordinals. This operation is easy to revert.
After de-obfuscating, we notice, that the payload is simply a LZMA archive. Thus, we can save the binary payload data to a file and deflate it.
import lzma
## redacted payload;
## initially named "________"
PAYLOAD = b'\xfd7zXZ\x00\x00\x04\xe6\xd6\xb4F ...'
with open("envelope.dat", "wb") as f:
f.write(lzma.decompress(PAYLOAD))
Thus, we obtain another piece of python code. Also here the same techniques of obfuscation were applied. First of all we notice three larger blobs of data, all named by under-scores of different length. In the end of the file is the actual builder and executer. Below is a reverse pseudo-code of the logic:
## payload blobs of the inner stage
##
PREFIX = "..."
REVERSED_PAYLOAD = "..."
TAIL = "..."
HEAD = ".."
## reconstruct the final payload from the parts
##
RECONSTRUCTED_PAYLOAD
= codecs.decode(PREFIX, 'rot13')
+ HEAD
+ REVERSED_PAYLOAD[::-1]
+ TAIL
## execute the malwares logic
##
exec(marshal.loads(base64.b64decode(RECONSTRUCTED_PAYLOAD)))
So, we can just take the marshaled code object right before execution and write it to disk in form of binary data.

The result will be valid python byte code able to decompile, however we have to take care of the header. marshal serialization / deserialization will usually handle this for us. Hence, if we manually write to disk and decompile, e.g., using PyLingual, we have to manually fix the header.
Fixing the header means simply prepending the correct header for the used python version. Since the cleaner.py was Python version 3.11, the same version applies to the obtained code object for granted. The following script will add the required header and dump the result to disk.
import importlib.util
import marshal
import struct
import time
## Python ≥ 3.7 (PEP 552)
pyc_header = (
importlib.util.MAGIC_NUMBER +
struct.pack("I", 0) + # bitfield
struct.pack("Q", int(time.time())) # timestamp
)
with open("payload.marshal", "rb") as input:
## read in marshaled code
marshaled_code = marshal.loads(input.read())
print(f"Read object from type {type(marshaled_code)}")
## prepend header and write to disk
with open("payload.pyc", "wb") as out:
out.write(pyc_header)
out.write(marshal.dumps(marshaled_code))
Now we have obtained a valid PYC file, which we can upload to PyLingual for decompilaition again.
The obtained code is 647 lines long and roughly looking over it, it becomes obvious, that this is the actual malware logic code.

The sample does have the following properties:
- Line-Count: 647
- SHA256:
46e7ed7d200e0e14b224660a43430e1f8a1451c73a1a0a74356f0d30c3ef2627 - Size: 43106 bytes
- Language: Python3.11
Analyzing the actual Malware Logic
From here onwards, the analysis is straight forward. The code does not pose any obfuscation any more. All strings and python functions were easily readable. We could even try to apply LLM models to summarize the malware’s functionality.
For the sake of this post we will not dive deeper into the malware logic here since it is already easy to read. However let us summarize some of the high level aspects, which directly shine to us, in itemized view.
- Malware is a credential stealer type
- The configuration is stored in a global
__CONFIG__variable discussed later - Stollen credentials and user-data is staged at
f'{localappdata}\\Luna-Logged-{os.getlogin()}.zip' - After uploading the staged file it will be deleted from disk.
- Upload is done to
discordusing the official API endpoint. - Persistent is achieved by copying it self to the Windows
Startupdirectory. - There is a very basic
powershellbased defender disable script. It mainly uses supported well documentedpowershellcommands to disable defender and add itself to the exclusion list. - There is an injector method which will inject javascript code downloaded from
hxxps[://]raw[.]githubusercontent[.]com/Smug246/luna-injection/main/obfuscated-injection.js, however the file is not longer available for additional analysis.
For the sake of completeness, we attach the malware’s main configuration below:
__CONFIG__ = {
'webhook': 'https://discord.com/api/webhooks/1456846954045771929/JNIT4ic1X8Hjp92hABo8VM1WfCr56jFlmAxr5uts9zq7-WaLVv8cU47UUVcSaSkwB-xT',
'ping': True,
'pingtype': 'Everyone',
'error': True,
'startup': True,
'defender': False,
'systeminfo': True,
'backupcodes': True,
'browser': True,
'roblox': True,
'obfuscation': True,
'injection': True,
'minecraft': True,
'wifi': True,
'killprotector': True,
'antidebug_vm': True,
'discord': True,
**{'anti_spam': True, 'self_destruct': False}
}
We can directly see the discord.com/api being used as web-hook for back-home communication. Also there are some games and messengers names from where the malware should dump credentials.
Beside this, there is a debug function with a lot of logging function and specific exclusions (i.e., usernames, computer names, IP addresses, Hardware Ids), which may help in attributing the adversary.

Anyways, the threat intelligence part is out of the scope of this simple blog post. So let us call it a day and stop our analysis here.
Summary
We analysed a sample of the Luna stealer campaign. The malware is a python based credential stealer targeting User sessions and tokens from various messengers like discord. At the same time the stealer tries to dump backup codes stored in the Download directory, WiFi passwords and other files relevant for the attacker.
The initial python file was obfuscated by means of marshaling it, base64 decoding it and splitting the obtained payload into four pieces reassembled at runtime. On top of this, this reassembler stub was obfuscated again by using ROT13 and base64` encoding. For the sake of delivery the so obtained obfuscated python code was wrapped in a PyInstaller project to allow environment independent execution. While it is possible to build the malware on UNIX and MacOS as well, the hard-coded directories indicate that the malware is targeting Windows users only.
Overall, we can summarize that this is a generic credential stealer type malware likely being used by criminals without any attribution to nation state actors or organized cyber crime.