Update Aug 14: Fixed sha256 of sample (previous sha256 was another malicious covid-19 contact tracing app, but not exactly the one I analyzed in this article).
Update Aug 17: Added link to dummy server’s code.
Aarogya Setu is the Indian open source COVID-19 contact tracing app. Like many other COVID-19 tracing apps, it has many malicious copy-cats (see here). In this article, we are going to focus on one of the malicious apps (sha256: 885d07d1532dcce08ae8e0751793ec30ed0152eee3c1321e2d051b2f0e3fa3d7
) and how it communicates with its CnC.
This malware was first seen by @malwrhunterteam in May 2020, distributed by phishing link http://prettysavantwholesale[.]com/Aarogya-Setu-v1.5.apk (no longer active). It poses as the Aarogya Setu app.
Communication with the CnC
The malware connects a the remote CnC. The code below shows the creation of the socket. The CnC host and port — like all other malware settings — are saved in the malware’s shared preferences file (com.android.tester_preferences.xml
), and use meaningless 5-digit labels as keys to settings. For example, 10334 stores the CnC port, and 10335 the CnC host.
If those settings are not present in the shared preferences file, the default values are taken from the malware’ resources.
Replacing the CnC with a dummy server
The CnC (204.48.26.131
)is no longer responding. To understand what it was receiving, I built a dummy server which displays incoming messages.
First, we need to divert packets sent to the CnC to the local host (127.0.0.1
). On Linux systems, an iptable command does that:
sudo iptables -t nat -A OUTPUT -p all -d 204.48.26.131 -j DNAT — to-destination 127.0.0.1
Then, we implement a dummy server, with a socket server listening on port 29491, launch it and wait for the malicious app to connect:
$ python3 spyserv.py
Logger configured
Starting up server on 127.0.0.1 port 29491
Listening… (max clients=10)
Connecting (‘192.168.0.42’, 59674) — client no. (1)…
IN: b’33\x00\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x00+(\xcd-0402\xb1\xc8+\xcd\xc9\x01\x00}4.\xed\r\x00\x00\x00'
Packet structure
If we search through references using the socket, we see the code writes to the socket as follows:
- The message is gzipped
- The packet is prefixed by the gzip’s length and a
0x00
.
Uncompressing messages
So, we update our dummy server’s implementation to automatically gunzip messages. There is a first packet of length 1439
we’ll discuss later. Then, we notice 2 messages of 33
bytes decompress to pump10248null
. We’ll explain those first.
[SpyServer] 08/14/2020 10:09:20 IN: b’1439\x00\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x00\xadU\xc9\xd2\xab\xb8\x0e~\x95^\xf5\x86\xba\xc5\x14\x02,za\xc0 [..]
[SpyServer] 08/14/2020 10:09:20 Packet indicates length=1439
[SpyServer] 08/14/2020 10:09:20 get_msg_header() returns marker=4 msg_len=1439
[SpyServer] 08/14/2020 10:09:20 decompress(): marker=4 msg_len=1439
[SpyServer] 08/14/2020 10:09:20 msg_len=1439 msg=b’1033210249510249null & null10249/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH [..]
LShrSoXErMBSp2N7jllAdHD4dmK1lQHleQSHuAdU3ciRGBIva9zOuaYkJBYMQlJ2dx3czKTkWcDH/2Q==10249MD4G10249100&true10249–1024910248null’
[SpyServer] 08/14/2020 10:09:34 Packet indicates length=33
[SpyServer] 08/14/2020 10:09:34 get_msg_header() returns marker=2 msg_len=33
[SpyServer] 08/14/2020 10:09:34 decompress(): marker=2 msg_len=33
[SpyServer] 08/14/2020 10:09:34 msg_len=33 msg=b’pump10248null’
[SpyServer] 08/14/2020 10:09:56 IN: b’33\x00\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x00+(\xcd-0402\xb1\xc8+\xcd\xc9\x01\x00}4.\xed\r\x00\x00\x00'
[SpyServer] 08/14/2020 10:09:56 Packet indicates length=33
[SpyServer] 08/14/2020 10:09:56 get_msg_header() returns marker=2 msg_len=33
[SpyServer] 08/14/2020 10:09:56 decompress(): marker=2 msg_len=33
[SpyServer] 08/14/2020 10:09:56 msg_len=33 msg=b’pump10248null’
Those “pump” messages (likely heartbeat messages) come from this piece of code:
This calls the method send_gzip_array()
we showed at Figure 5. Precisely, it creates a message “pump” + “10248” + “null” and gzips it. Which is exactly what we see in our server :)
Before that, our server’s logs show a longer message which ends with LShrSoXErMBSp2N7jllAdHD4dmK1lQHleQSHuAdU3ciRGBIva9zOuaYkJBYMQlJ2dx3czKTkWcDH/2Q==10249MD4G10249100&true10249–1024910248null
.The values 10249
are typically commands from the malware. We notice a longer value ending by the noticeable ==
which typically indicates Base64. So, we get all that Base64 encoded value and decode it: echo "..." | base64 -d
.
It outputs binary info beginning with JFIF
: that’s a JPEG image! We dump the decoded bytes in a file and view the resulting image.
If we read the beginning of the message again, we have 1033210249510249null & null10249/9j/…
. We search for 10332
in the code and find the spot that generates this message:
Precisely, the message that will be sent (gzipped) contains:
- “10332”
- “10249”. This is a delimiter we see in numerous other messages.
- an integer representing the screen mode. In our case, we had value “5” which means the screen was on. See Figure 10.
- “10249”
- SMS origin: in case a SMS was received, this will be a message like “SMS[ originating phone number]”. In our case, there was no SMS, so this is “null”.
- “&”
- phonenumber: in case a call was placed, this will contain the outgoing phone number. Null in our case.
- “10249”
- Wallpaper: a screenshot of the current wallpaper, resized to 48x48 pixels (
0x30
). See Figure 11. - “10249”
- Network type. In our case
MD4G
which a string the malware uses forNETWORK_TYPE_LTE
. - “10249”
- Battery level and whether the phone is plugged or not. In our case, the emulator was considered to be 100% charged, and plugged 😉. See Figure 12.
- “10249–10249”
- “10248null”. This footer is added to all messages that get gzipped (see Figure 5)
Happily, everything gets an explanation when you disassemble code 😃
— Cryptax