Divide and Hide: How malicious code lived on PyPI for 3 months

The Station 9 research team discovered malicious code that was divided and distributed across different packages, remaining obfuscated for months while getting nearly 2000 downloads.

Henrik Plate
Henrik Plate

Many malicious packages deployed on OSS package repositories include the same or very similar malicious code - oftentimes simple code blocks with a few lines included in files like setup.py. Luckily, the detection and removal of such clones got better and better, i.e. the majority of malicious packages has relatively few downloads.

This blog post presents an interesting malware variant, where the malicious code is spread across different functions, files and even packages. The malicious behavior itself is very simplistic, but the obfuscation technique used demonstrates how adversaries evolve in order to hinder malware detection.

Many researchers and companies started hunting for malicious packages published on open source repositories like PyPI or npm, and it is not uncommon that packages flagged by our pipeline have been removed by the time we go through the findings.

Many of those packages, however, contain the same single block of malicious code in files like setup.py, __init__.py (in case of Python packages) or index.js (npm). This makes it possible to devise relatively simple detection patterns that can be run in a performant and scalable fashion on thousands of packages every day, e.g. patterns for abstract syntax trees or even regular expressions.

This blog post presents a variant where the malicious code has been divided into smaller chunks that were then distributed across several files and even different packages, very likely with the intention to hinder malware detection.

It is maybe for that reason that those two packages managed to have slightly higher dwell times and download numbers compared to packages of other malware campaigns: Both packages have been uploaded to PyPI on April 16th, and were taken down on July 7th following our notification of PyPI administrators (who reacted blazingly fast - they were yanked just 2 minutes after we sent the email). According to PyPI Stats, the packages gisi and ttlo were downloaded 1291 times and 667 times respectively during that time frame.

ttlo and gisi

The functionality of this malware is relatively simple and aims at hijacking Instagram accounts: It uses an SQL select query to search for Instagram session identifiers in the SQLite database that contains Chrome cookies on Windows. When it finds one, it updates its expiry date and exfiltrates the cookie value with help of a POST request to a Telegram chat.

More interesting is how the respective pieces were split by the attacker, especially the functions ttlo(), b() and gisi() in Python files with the same names.

The cookie search and update is part of the Python package gisi (which likely stands for “get instagram session identifier”) while the exfiltration logic was distributed with package ttlo. None of the packages, however, declares a dependency on the other one, thus, it is unclear how the attacker ensures that both are present in the victim’s environment.

Moreover, the malicious code was split across several functions in several distinct files. The decryption of encrypted cookie values, for example, is implemented in function dd() contained in a Python file with the same name.

And both packages contain a dedicated function to decode Base64-encoded strings, which was probably also meant to evade simple detection patterns. Instead of directly calling requests.post(base64.b64decode('aHR[...]Z2U=') [...], the attacker added another level of indirection by creating a string variable a and decoder function b() in a dedicated file. This technique has been used in both packages gisi and ttlo, which changes the call to requests.post(b(a) [...].

So what?

The split of malicious functionality makes it way more difficult for malware scanners. It is no longer sufficient to scan individual files with simple patterns. Understanding the program behavior requires gathering all the packages and analyzing all their files in conjunction in order to find suspicious data or control flows.

Such whole-program analysis, however, is more resource-hungry than simple pattern search, which makes it difficult to scale a corresponding solution to thousands of packages published per day on PyPI, npm and other package repositories. Of course, such malware improvements do not come by surprise: They are part of the typical arms-race between attackers and defenders, which takes place in every single IT security domain.