On Tuesday 24/04, a colleague from our red team noticed a post on security.stackexchange.com. The post was made on 15/04 and the user wants someone to look at the code from the GitHub repository linked in the post to verify if there is something weird with the code. The user reports that the program works as expected, but that he started to receive alerts for login attempts for multiple accounts.
Several days after my colleague told me about this, the post was removed and is no longer accessible.
Investigating the GitHub account ApfelsaftDevs (not available anymore), we can see that the account was created on 14 June 2022.
The account stays dormant with no repositories and starts to become active at the end of February/beginning of March. Especially in April, we see activity almost every day until the account was taken down. Even during weekends.
The account contained many different repositories with tools which can be used for all different kinds of use cases, some more legitimate than others.
Name and description analysis
Based on the name and description, we can see that there are tools which have a somewhat legitimate use case. For example, a tool for Discord which can be used to send direct messages to many accounts at once. But, we can also see other repositories which clearly have only malicious intent.
What all of these tools have in common is that they are trojanized with Kekw malware. This malware is loaded via malicious Python packages when the tool is run. It’s these malicious Python packages that kept getting removed, mostly within 24-48 hours, requiring the threat actor to update all the repositories on a nearly daily basis.
Looking at one of the commits for these repositories, we can see that 2 lines are changed at the very beginning of the main.py Python script. Line number 2 downloads the malicious package, and line number 3 imports the package.
The blog from Cyble Research and Intelligence Labs (CRIL) does a great job analysing the malicious Python package. The blog from Cyble mentions Anti-VM variables being used. After a quick analysis, the values within these variables seem to come from a “virustotal-vm-blacklist” repository on GitHub. I could quickly identify two repositories with this information.
The first repository is from Zed1242 and looks like it’s the original repository on which the other one is based.
The repository by 6nz contains a lot more information and is more frequently updated. It’s 1969 commits ahead, but only 3 commits behind on the first repository at the time of writing showcasing that it’s more up-to-date.
In the commit description, we can also see signs of automation. How this automation works is something I haven’t looked into and is out of the scope of this blog.
As shown at the start of the blog, the malicious PyPi packages seem to be most likely spread, and downloaded, via these tools. This is something the blog from Cyble, tweets by security company ESET, and various news articles (1, 2, 3, 4) don’t mention.
After contact with the researcher from ESET where I mentioned the GitHub repositories that were being used to spread the packages, all but one was taken offline shortly after by GitHub. One GitHub account stayed online (PatrickPogoda), which allowed me to keep tracking this threat. This account had the same repositories as “ApfelsaftDev” and kept updating on a nearly daily basis until it was taken offline in mid-May.
The first of many
The very first version that I analysed was a Python script with almost no obfuscation and more than 1700 lines of code. This is most likely the same version that was analysed by Cyble.
PyPi package name: pyfontingtools
A simple script like this gets detected quite easily by most well-known AV companies.
What about second script?
Yes, that’s a Lord of the Rings reference and not a typo. Not only do hobbits have seconds, but also threat actors.
In the script, only one line of code is obfuscated by using Fernet encryption. As this code isn’t analysed in the blog from Cyble, let’s take a look at this ourselves.
When decrypted we get another Python script with only one function called “inject”. The script is quite straightforward without obfuscation.
How does it work ?
The script imports, and downloads, the necessary legitimate modules.
Several variables are defined and the script checks for the existence of %localappdata%/exodus (C:\Users\<username>\AppData\local\exodus).
If it doesn’t exist, the function returns and the script stops.
If the folder does exist, the script continues and starts listing all files in this folder and adds them to the “apps” variable.
The URL from which the file app.asar is downloaded and the user agent header to be used is defined. These are then used to download the file.
The .asar file is an archive format designed for Electron applications.
The full user agent used: “Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36”
A Discord webhook is defined as well as the exodus.wallet and the location of a .zip file which will be created by the script.
After defining these variables, the script loops through all files and folders in the exodus.wallet folder and puts all that information in the archive exoduswallet.zip defined just above.
Upload the zip file to gofile[.]io, store the download link in the exolink variable and send that information to the Discord webhook allowing the attacker to download the zip file.
The exodus.exe process is killed
The last step of the function is to write the app.asar file to every value in the apps list, which was defined on line 16.
In the same loop, the value in the khook variable is written to every value in the apps list as a file called LICENSE.
After writing the function, it still needs to be executed. This happens at the end of the script.
The script gets a low score of 3/60 on VirusTotal.
App.asar has only 2 hits on VirusTotal.
It looks like this second script is purely focused on stealing Exodus wallets, while the initial script is a more generic stealer script. ESET was able to compare it to several differently named stealers in their tweets linked earlier, thanks to their extensive database.
Less is more
Defenders evolve their capabilities but so do attackers.
Pypi package pyfontingtools version 1.0.0 contained the full malicious script. This script gets detected easily as shown earlier (17 VT hits).
The threat actor adapted and changed the uploading process. For new packages, the initial PyPi 1.0.0 upload would only print a simple “Hello World”.
Only minutes later, the threat actor uploads a new version, 1.1.0. This new version contains only 4 lines of code.
This time, I’m analysing v1.1.0 of the PyPi package “syssqlitedbpackageV1”
The threat actor again resorts to Fernet encryption to hide the actual payload. The entire command on line 4 contains 96211 characters!
After decrypting, it becomes clear why the Fernet command was so long. It’s the exact same script as the first one (pyfontingtools).
Let’s use diff to verify!
The output shows two lines being different. This is probably an issue due to copy-paste for one script and decrypting the other. No real code is different.
Changing the script with the payload encrypted does the job well when it comes to AV detection. 0 hits on VT.
It’s morphing time!
One day later, a new version of syssqlitedbpackageV1 is uploaded. I happened to notice this very shortly after the threat actor uploaded a new version. I swear I’m not the threat actor 🙂
Not only the code is modified, but also the author has changed! While analysing many different PyPi packages used by the threat actor, these two authors (Jozef M and NHJonas) were always used. The maintainer account was almost always a newly created and different account for each malicious package.
Version 1.2.0 of the PyPi package “syssqlitedbpackageV1”
Let’s take a look at the code for this new version!
This time the script is only 49 lines of code. Again, including a very long line of Fernet encrypted code.
Several modules are imported and a bunch more are downloaded.
Get the current user.
Loop through the %localappdata%\Programs\Python folder and look for directories that start with Python.
For every Python version found, copy the “Cryptodome” folder and copy it to the “Crypto” folder in the same parent folder.
Several blank code lines are followed by defining the pl variable.
The subprocess module is imported and the creationflags variable is set to create a new process group (to allow management of child processes) and spawns the process invisible to the user (CREATE_NO_WINDOW). Then the now well-known execution of a Fernet encrypted command is defined.
Set the location of %appdata% and append pl.py to it On lines 29 and 30, variable pl is used to write to the file %appdata%\pl.py defined above.
Creationflags variable is set again with slightly different values. This time to be used outside the pl variable. The “DETACHED_PROCESS” flag will create a new process separate from the parent process. This is probably done to evade detection and to make an incident response investigation more difficult.
The freshly created Python script pl.py is executed.
The script will then write the Python script run.py, downloaded from the kewltd[.]ru domain, to any of the hardcoded folders within %appdata%. On line 40, the file will get the hidden attribute (“+h”) as well as indicating that the process should be run in a shell environment (“shell=True”).
In the end, the script executes all the run.py scripts placed in the different hardcoded folders in the startup_folder variable.
The run.py script is heavily obfuscated and above my current level of being able to reverse it. If you’re reading this and want to give it a go, you can retrieve it from the urlscan.io scan I did. However, the threat actor did leave a message for anyone looking into the script.
Now that the clear text script has been analysed, it’s time to dive into the Fernet encrypted code!
Fernet encrypted code
To start, the script has a few changes but it’s mostly the same. So, what has changed?
Whereas the package pyfontingtoolsV1 (the very first script) used the os.system module to download all the necessary PyPi packages, this version uses the subprocess.Popen module to start cmd.exe to launch pip install installing the hardcoded modules. Both times with creationflags variable at the end (not visible in the second screenshot).
In this version of the script, the dedicated Exodus wallet stealer is not encrypted anymore with another Fernet command but it’s now in plain text inside the script.
Although version 1.2.0 contains some potentially malicious code, having the main payload encrypted with Fernet it again successfully evades all AV detections on VirusTotal.
PyPi takes action
PyPi was impacted quite heavily by the influx of malicious packages being uploaded daily and took several actions.
On 20 May 2023, PyPi temporarily suspends new user and new project registrations. One day later, around 30 hours after the announcement, the suspension has been lifted again.
New user and new project name registration on PyPI is temporarily suspended. The volume of malicious users and malicious projects being created on the index in the past week has outpaced our ability to respond to it in a timely fashion, especially with multiple PyPI administrators on leave.
While we re-group over the weekend, new user and new project registration is temporarily suspended
On 25 May 2023, PyPi announces they will go one step further and start enforcing 2FA for maintainers of projects or organizations by the end of 2023.
Today, as part of that long term effort to secure the Python ecosystem, we are announcing that every account that maintains any project or organization on PyPI will be required to enable 2FA on their account by the end of 2023.
Between now and the end of the year, PyPI will begin gating access to certain site functionality based on 2FA usage. In addition, we may begin selecting certain users or projects for early enforcement.
Whoever is responsible for developing the Kekw malware shows resiliency in updating the malware to avoid detection. The threat actor also most likely uses some form of automation to upload new packages on PyPi and update all the different GitHub repositories. Initially on multiple accounts but after actions on GitHub side, only on one account.
On the other hand, PyPi also did a great job removing the malicious packages as soon as possible. Temporarily stopping registrations must’ve been a tough decision to make. With the enforcement of 2FA, threats like these will most likely become less rampant. Enforcing 2FA provides the added benefit that a supply chain compromise becomes a bit harder as well.
Despite all of this, the threat actor has not yet vanished off the face of the earth and very recently made a comeback after 1-2 weeks of downtime.
More about that in my next blog which will come (hopefully) very soon!