Cetus was discovered by Unit42 at Palo Alto Networks and it is a docker worm that spreads by exploiting insecure Docker daemons. Once the docker instance is infected it will launch XMRing, which will take advantage of the infected instance's CPU/GPU to mine cryptocurrencies. U42 wrote an analysis of the malware which was published their blog. This is going to be just me taking a look at the malware to avoid getting rusty :)

Sample:

b49a3f3cb4c70014e2c35c880d47bc475584b87b7dfcfa6d7341d42a16ebe443

The binary is an position independent ELF, x86-64 bits, and not stripped which makes everything easier. It calls miner_start and then goes into an infinite loop and calls scan_start so I will focus on these two but also going deeper in both. At no time exits so it will constantly scan for new victims.

miner_start

This function starts with MOVs that copy obfuscated data to the stack and prepares a variable called obfuscated_data. So, clearly, we are dealing with what will be a deobfuscation routine... and there it is:

000011f3  lea     rax, [rel miner_start()::'lambda'()::operator()() const::obfuscated_data]
000011fa  lea     rdx, [rax+0xb4]
00001201  nop     dword [rax], eax

00001208  xor     byte [rax], 0x2e  // deobfuscation
0000120b  add     rax, 0x1
0000120f  cmp     rax, rdx
00001212  jne     0x1208

It will convert the data, obfuscated, to a readable state:

(gdb) dump memory data.bin 0x55b79bdde6a0 0x55b79bdde6a0+180
(gdb) !more data.bin
docker-cache -B --donate-level 1 -o pool.minexmr.com:443 -u 85X7JcgPpwQdZXaK2TKJb8baQAXc3zBsnW7JuY7MLi9VYSamf4bFwa7SEAK9Hgp2P53npV19w1zuaK5bft5m2NN71CmNLoh -k --tls -t 1 --rig-id

This means that the malware drops the XMRing executable as docker-cache and executes it with those parameters. The wallet ID can also be seen there and a rig id that will be generated later. And then it comes the weirdest way to concatenate chars to form a command that will be finally executed:

0000130a  movdqa  xmm0, xmmword [rel data_2990]     ; ' -l /var/log/utm'
00001312  mov     edx, 0x67                         ; 'g'
00001317  mov     dword [rax+0x10], 0x6f6c2e70      ; 'p.lo'
0000131e  mov     word [rax+0x14], dx
00001322  mov     rdi, rbp
00001325  movups  xmmword [rax], xmm0
00001328  call    system

Now that the cryptominer was executed as "docker-cache" then it will start the scanning process to find more victims. One thing to notice is that you can check the malware log at that log path mentioned previously and /var/log/stmp.log.

scan_start

The malware will utilize masscan for this process. scan_start is called with 2 arguments: 1) a randomly generated number that will be used as an IP address and 2) the CIDR that is fixed to /16. Then masscan will start scanning on port 2375/tcp.

00002950  char const data_2950[64] = "masscan %d.%d.%d.%d/%d -p 2375 -oL - --max-rate 360 2>/dev/null", 0

As the malware is not dropping masscan and not even checking if it exists, it suggests that the point of entry was a machine with the proper tools already installed or the attacker actively exploiting and injecting the malware. Once it finds an open port the treat function is executed. The treat function will basically try to infect and then prepare the victim to start spreading. It executes docker ps command against the remote docker daemon in order to check if the daemon is insecure or not. If it was able to execute the command sucessfully it will check if the instance was already infected. If it is not the case then it will call install which will do the heavy work of preparing the new container that will host the cryptominer. A new name is created for this unwanted guest by calling new_name which will randomly choose two words from a hardcoded list and concatenate them with a "_" symbol.

0000264e  char const data_264e[8] = "grommet", 0
00002656  char const data_2656[7] = "obelus", 0
0000265d  char const data_265d[8] = "agelast", 0
00002665  char const data_2665[13] = "amatorculist", 0
00002672  char const data_2672[13] = "peristeronic", 0
0000267f  char const data_267f[12] = "hirquiticke", 0
0000268b  char const data_268b[6] = "oxter", 0
00002691  char const data_2691[6] = "quire", 0
00002697  char const data_2697[8] = "baleful", 0
0000269f  char const data_269f[8] = "boorish", 0
000026a7  char const data_26a7[7] = "adroit", 0
000026ae  char const data_26ae[7] = "fecund", 0
000026b5  char const data_26b5[7] = "limpid", 0
000026bc  char const data_26bc[8] = "risible", 0
000026c4  char const data_26c4[8] = "verdant", 0
000026cc  char const data_26cc[8] = "zealous", 0

After a pretty name is chosen, the container is created by using a ubuntu:18.04 image. Once that's done the update called to act against the container. This function will prepare the instance by updating the apt cache (what a nice touch), install masscan and docker.io, copy itself to the remote instance and then save the command to execute the binaries migrated in /root/.bash_aliases. Last but not least, the container is restarted. This same function is executed during the call to treat if the container found during scanning is already infected to keep it up to date.

And that's it. This malware came after some days of reflecting and thinking how big companies already migrated it's whole infraestructure to run in kubernetes pods and how nowadays is common to see how code is already shipped with Dockerfiles which enables the possibility of users running containers on insecure instances. Luckily these daemon settings are not setup by default in common dockerd installations.

What will come next?