/* * Dropbear - a SSH2 server * * Copyright (c) Matt Johnston * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include "includes.h" #include "session.h" #include "dbutil.h" #include "packet.h" #include "algo.h" #include "buffer.h" #include "dss.h" #include "ssh.h" #include "dbrandom.h" #include "kex.h" #include "channel.h" #include "runopts.h" #include "netio.h" static void checktimeouts(void); static long select_timeout(void); static int ident_readln(int fd, char* buf, int count); static void read_session_identification(void); struct sshsession ses; /* GLOBAL */ /* called only at the start of a session, set up initial state */ void common_session_init(int sock_in, int sock_out) { time_t now; #if DEBUG_TRACE debug_start_net(); #endif TRACE(("enter session_init")) ses.sock_in = sock_in; ses.sock_out = sock_out; ses.maxfd = MAX(sock_in, sock_out); if (sock_in >= 0) { setnonblocking(sock_in); } if (sock_out >= 0) { setnonblocking(sock_out); } ses.socket_prio = DROPBEAR_PRIO_DEFAULT; /* Sets it to lowdelay */ update_channel_prio(); #if 0 && !DROPBEAR_SVR_MULTIUSER /* A sanity check to prevent an accidental configuration option leaving multiuser systems exposed */ errno = 0; getuid(); if (errno != ENOSYS) { dropbear_exit("Non-multiuser Dropbear requires a non-multiuser kernel"); } #endif now = monotonic_now(); ses.connect_time = now; ses.last_packet_time_keepalive_recv = now; ses.last_packet_time_idle = now; ses.last_packet_time_any_sent = 0; ses.last_packet_time_keepalive_sent = 0; #if DROPBEAR_FUZZ if (!fuzz.fuzzing) #endif { if (pipe(ses.signal_pipe) < 0) { dropbear_exit("Signal pipe failed"); } setnonblocking(ses.signal_pipe[0]); setnonblocking(ses.signal_pipe[1]); ses.maxfd = MAX(ses.maxfd, ses.signal_pipe[0]); ses.maxfd = MAX(ses.maxfd, ses.signal_pipe[1]); } ses.writepayload = buf_new(TRANS_MAX_PAYLOAD_LEN); ses.transseq = 0; ses.readbuf = NULL; ses.payload = NULL; ses.recvseq = 0; initqueue(&ses.writequeue); ses.requirenext = SSH_MSG_KEXINIT; ses.dataallowed = 1; /* we can send data until we actually send the SSH_MSG_KEXINIT */ ses.ignorenext = 0; ses.lastpacket = 0; ses.reply_queue_head = NULL; ses.reply_queue_tail = NULL; /* set all the algos to none */ ses.keys = (struct key_context*)m_malloc(sizeof(struct key_context)); ses.newkeys = NULL; ses.keys->recv.algo_crypt = &dropbear_nocipher; ses.keys->trans.algo_crypt = &dropbear_nocipher; ses.keys->recv.crypt_mode = &dropbear_mode_none; ses.keys->trans.crypt_mode = &dropbear_mode_none; ses.keys->recv.algo_mac = &dropbear_nohash; ses.keys->trans.algo_mac = &dropbear_nohash; ses.keys->algo_kex = NULL; ses.keys->algo_hostkey = -1; ses.keys->recv.algo_comp = DROPBEAR_COMP_NONE; ses.keys->trans.algo_comp = DROPBEAR_COMP_NONE; #ifndef DISABLE_ZLIB ses.keys->recv.zstream = NULL; ses.keys->trans.zstream = NULL; #endif /* key exchange buffers */ ses.session_id = NULL; ses.kexhashbuf = NULL; ses.transkexinit = NULL; ses.dh_K = NULL; ses.remoteident = NULL; ses.chantypes = NULL; ses.allowprivport = 0; #if DROPBEAR_PLUGIN ses.plugin_session = NULL; #endif TRACE(("leave session_init")) } void session_loop(void(*loophandler)(void)) { fd_set readfd, writefd; struct timeval timeout; int val; /* main loop, select()s for all sockets in use */ for(;;) { const int writequeue_has_space = (ses.writequeue_len <= 2*TRANS_MAX_PAYLOAD_LEN); timeout.tv_sec = select_timeout(); timeout.tv_usec = 0; DROPBEAR_FD_ZERO(&writefd); DROPBEAR_FD_ZERO(&readfd); dropbear_assert(ses.payload == NULL); /* We get woken up when signal handlers write to this pipe. SIGCHLD in svr-chansession is the only one currently. */ #if DROPBEAR_FUZZ if (!fuzz.fuzzing) #endif { FD_SET(ses.signal_pipe[0], &readfd); } /* set up for channels which can be read/written */ setchannelfds(&readfd, &writefd, writequeue_has_space); /* Pending connections to test */ set_connect_fds(&writefd); /* We delay reading from the input socket during initial setup until after we have written out our initial KEXINIT packet (empty writequeue). This means our initial packet can be in-flight while we're doing a blocking read for the remote ident. We also avoid reading from the socket if the writequeue is full, that avoids replies backing up */ if (ses.sock_in != -1 && (ses.remoteident || isempty(&ses.writequeue)) && writequeue_has_space) { FD_SET(ses.sock_in, &readfd); } /* Ordering is important, this test must occur after any other function might have queued packets (such as connection handlers) */ if (ses.sock_out != -1 && !isempty(&ses.writequeue)) { FD_SET(ses.sock_out, &writefd); } val = select(ses.maxfd+1, &readfd, &writefd, NULL, &timeout); if (ses.exitflag) { dropbear_exit("Terminated by signal"); } if (val < 0 && errno != EINTR) { dropbear_exit("Error in select"); } if (val <= 0) { /* If we were interrupted or the select timed out, we still * want to iterate over channels etc for reading, to handle * server processes exiting etc. * We don't want to read/write FDs. */ DROPBEAR_FD_ZERO(&writefd); DROPBEAR_FD_ZERO(&readfd); } /* We'll just empty out the pipe if required. We don't do any thing with the data, since the pipe's purpose is purely to wake up the select() above. */ ses.channel_signal_pending = 0; if (FD_ISSET(ses.signal_pipe[0], &readfd)) { char x; TRACE(("signal pipe set")) while (read(ses.signal_pipe[0], &x, 1) > 0) {} ses.channel_signal_pending = 1; } /* check for auth timeout, rekeying required etc */ checktimeouts(); /* process session socket's incoming data */ if (ses.sock_in != -1) { if (FD_ISSET(ses.sock_in, &readfd)) { if (!ses.remoteident) { /* blocking read of the version string */ read_session_identification(); } else { read_packet(); } } /* Process the decrypted packet. After this, the read buffer * will be ready for a new packet */ if (ses.payload != NULL) { process_packet(); } } /* if required, flush out any queued reply packets that were being held up during a KEX */ maybe_flush_reply_queue(); handle_connect_fds(&writefd); /* loop handler prior to channelio, in case the server loophandler closes channels on process exit */ loophandler(); /* process pipes etc for the channels, ses.dataallowed == 0 * during rekeying ) */ channelio(&readfd, &writefd); /* process session socket's outgoing data */ if (ses.sock_out != -1) { if (!isempty(&ses.writequeue)) { write_packet(); } } } /* for(;;) */ /* Not reached */ } static void cleanup_buf(buffer **buf) { if (!*buf) { return; } buf_burn(*buf); buf_free(*buf); *buf = NULL; } /* clean up a session on exit */ void session_cleanup() { TRACE(("enter session_cleanup")) /* we can't cleanup if we don't know the session state */ if (!ses.init_done) { TRACE(("leave session_cleanup: !ses.init_done")) return; } /* BEWARE of changing order of functions here. */ /* Must be before extra_session_cleanup() */ chancleanup(); if (ses.extra_session_cleanup) { ses.extra_session_cleanup(); } /* After these are freed most functions will fail */ #if DROPBEAR_CLEANUP /* listeners call cleanup functions, this should occur before other session state is freed. */ remove_all_listeners(); remove_connect_pending(); while (!isempty(&ses.writequeue)) { buf_free(dequeue(&ses.writequeue)); } m_free(ses.newkeys); #ifndef DISABLE_ZLIB if (ses.keys->recv.zstream != NULL) { if (inflateEnd(ses.keys->recv.zstream) == Z_STREAM_ERROR) { dropbear_exit("Crypto error"); } m_free(ses.keys->recv.zstream); } #endif m_free(ses.remoteident); m_free(ses.authstate.pw_dir); m_free(ses.authstate.pw_name); m_free(ses.authstate.pw_shell); m_free(ses.authstate.pw_passwd); m_free(ses.authstate.username); #endif cleanup_buf(&ses.session_id); cleanup_buf(&ses.hash); cleanup_buf(&ses.payload); cleanup_buf(&ses.readbuf); cleanup_buf(&ses.writepayload); cleanup_buf(&ses.kexhashbuf); cleanup_buf(&ses.transkexinit); if (ses.dh_K) { mp_clear(ses.dh_K); } m_free(ses.dh_K); m_burn(ses.keys, sizeof(struct key_context)); m_free(ses.keys); TRACE(("leave session_cleanup")) } void send_session_identification() { buffer *writebuf = buf_new(strlen(LOCAL_IDENT "\r\n") + 1); buf_putbytes(writebuf, (const unsigned char *) LOCAL_IDENT "\r\n", strlen(LOCAL_IDENT "\r\n")); writebuf_enqueue(writebuf); } static void read_session_identification() { /* max length of 255 chars */ char linebuf[256]; int len = 0; char done = 0; int i; /* Servers may send other lines of data before sending the * version string, client must be able to process such lines. * If they send more than 50 lines, something is wrong */ for (i = IS_DROPBEAR_CLIENT ? 50 : 1; i > 0; i--) { len = ident_readln(ses.sock_in, linebuf, sizeof(linebuf)); if (len < 0 && errno != EINTR) { /* It failed */ break; } if (len >= 4 && memcmp(linebuf, "SSH-", 4) == 0) { /* start of line matches */ done = 1; break; } } if (!done) { TRACE(("error reading remote ident: %s\n", strerror(errno))) ses.remoteclosed(); } else { /* linebuf is already null terminated */ ses.remoteident = m_malloc(len); memcpy(ses.remoteident, linebuf, len); } /* Shall assume that 2.x will be backwards compatible. */ if (strncmp(ses.remoteident, "SSH-2.", 6) != 0 && strncmp(ses.remoteident, "SSH-1.99-", 9) != 0) { dropbear_exit("Incompatible remote version '%s'", ses.remoteident); } TRACE(("remoteident: %s", ses.remoteident)) } /* returns the length including null-terminating zero on success, * or -1 on failure */ static int ident_readln(int fd, char* buf, int count) { char in; int pos = 0; int num = 0; fd_set fds; struct timeval timeout; TRACE(("enter ident_readln")) if (count < 1) { return -1; } DROPBEAR_FD_ZERO(&fds); /* select since it's a non-blocking fd */ /* leave space to null-terminate */ while (pos < count-1) { FD_SET(fd, &fds); timeout.tv_sec = 1; timeout.tv_usec = 0; if (select(fd+1, &fds, NULL, NULL, &timeout) < 0) { if (errno == EINTR) { continue; } TRACE(("leave ident_readln: select error")) return -1; } checktimeouts(); /* Have to go one byte at a time, since we don't want to read past * the end, and have to somehow shove bytes back into the normal * packet reader */ if (FD_ISSET(fd, &fds)) { num = read(fd, &in, 1); /* a "\n" is a newline, "\r" we want to read in and keep going * so that it won't be read as part of the next line */ if (num < 0) { /* error */ if (errno == EINTR) { continue; /* not a real error */ } TRACE(("leave ident_readln: read error")) return -1; } if (num == 0) { /* EOF */ TRACE(("leave ident_readln: EOF")) return -1; } #if DROPBEAR_FUZZ fuzz_dump(&in, 1); #endif if (in == '\n') { /* end of ident string */ break; } /* we don't want to include '\r's */ if (in != '\r') { buf[pos] = in; pos++; } } } buf[pos] = '\0'; TRACE(("leave ident_readln: return %d", pos+1)) return pos+1; } void ignore_recv_response() { /* Do nothing */ TRACE(("Ignored msg_request_response")) } static void send_msg_keepalive() { time_t old_time_idle = ses.last_packet_time_idle; struct Channel *chan = get_any_ready_channel(); CHECKCLEARTOWRITE(); if (chan) { /* Channel requests are preferable, more implementations handle them than SSH_MSG_GLOBAL_REQUEST */ TRACE(("keepalive channel request %d", chan->index)) start_send_channel_request(chan, DROPBEAR_KEEPALIVE_STRING); } else { TRACE(("keepalive global request")) /* Some peers will reply with SSH_MSG_REQUEST_FAILURE, some will reply with SSH_MSG_UNIMPLEMENTED, some will exit. */ buf_putbyte(ses.writepayload, SSH_MSG_GLOBAL_REQUEST); buf_putstring(ses.writepayload, DROPBEAR_KEEPALIVE_STRING, strlen(DROPBEAR_KEEPALIVE_STRING)); } buf_putbyte(ses.writepayload, 1); /* want_reply */ encrypt_packet(); ses.last_packet_time_keepalive_sent = monotonic_now(); /* keepalives shouldn't update idle timeout, reset it back */ ses.last_packet_time_idle = old_time_idle; } /* Check all timeouts which are required. Currently these are the time for * user authentication, and the automatic rekeying. */ static void checktimeouts() { time_t now; now = monotonic_now(); if (IS_DROPBEAR_SERVER && ses.connect_time != 0 && now - ses.connect_time >= AUTH_TIMEOUT) { dropbear_close("Timeout before auth"); } /* we can't rekey if we haven't done remote ident exchange yet */ if (ses.remoteident == NULL) { return; } if (!ses.kexstate.sentkexinit && (now - ses.kexstate.lastkextime >= KEX_REKEY_TIMEOUT || ses.kexstate.datarecv+ses.kexstate.datatrans >= KEX_REKEY_DATA)) { TRACE(("rekeying after timeout or max data reached")) send_msg_kexinit(); } if (opts.keepalive_secs > 0 && ses.authstate.authdone) { /* Avoid sending keepalives prior to auth - those are not valid pre-auth packet types */ /* Send keepalives if we've been idle */ if (now - ses.last_packet_time_any_sent >= opts.keepalive_secs) { send_msg_keepalive(); } /* Also send an explicit keepalive message to trigger a response if the remote end hasn't sent us anything */ if (now - ses.last_packet_time_keepalive_recv >= opts.keepalive_secs && now - ses.last_packet_time_keepalive_sent >= opts.keepalive_secs) { send_msg_keepalive(); } if (now - ses.last_packet_time_keepalive_recv >= opts.keepalive_secs * DEFAULT_KEEPALIVE_LIMIT) { dropbear_exit("Keepalive timeout"); } } if (opts.idle_timeout_secs > 0 && now - ses.last_packet_time_idle >= opts.idle_timeout_secs) { dropbear_close("Idle timeout"); } } static void update_timeout(long limit, long now, long last_event, long * timeout) { TRACE2(("update_timeout limit %ld, now %ld, last %ld, timeout %ld", limit, now, last_event, *timeout)) if (last_event > 0 && limit > 0) { *timeout = MIN(*timeout, last_event+limit-now); TRACE2(("new timeout %ld", *timeout)) } } static long select_timeout() { /* determine the minimum timeout that might be required, so as to avoid waking when unneccessary */ long timeout = KEX_REKEY_TIMEOUT; long now = monotonic_now(); if (!ses.kexstate.sentkexinit) { update_timeout(KEX_REKEY_TIMEOUT, now, ses.kexstate.lastkextime, &timeout); } if (ses.authstate.authdone != 1 && IS_DROPBEAR_SERVER) { /* AUTH_TIMEOUT is only relevant before authdone */ update_timeout(AUTH_TIMEOUT, now, ses.connect_time, &timeout); } if (ses.authstate.authdone) { update_timeout(opts.keepalive_secs, now, MAX(ses.last_packet_time_keepalive_recv, ses.last_packet_time_keepalive_sent), &timeout); } update_timeout(opts.idle_timeout_secs, now, ses.last_packet_time_idle, &timeout); /* clamp negative timeouts to zero - event has already triggered */ return MAX(timeout, 0); } const char* get_user_shell() { /* an empty shell should be interpreted as "/bin/sh" */ if (ses.authstate.pw_shell[0] == '\0') { return "/bin/sh"; } else { return ses.authstate.pw_shell; } } void fill_passwd(const char* username) { struct passwd *pw = NULL; if (ses.authstate.pw_name) m_free(ses.authstate.pw_name); if (ses.authstate.pw_dir) m_free(ses.authstate.pw_dir); if (ses.authstate.pw_shell) m_free(ses.authstate.pw_shell); #if 0 if (ses.authstate.pw_passwd) m_free(ses.authstate.pw_passwd); pw = getpwnam(username); if (!pw) { return; } ses.authstate.pw_uid = pw->pw_uid; ses.authstate.pw_gid = pw->pw_gid; ses.authstate.pw_name = m_strdup(pw->pw_name); ses.authstate.pw_dir = m_strdup(pw->pw_dir); ses.authstate.pw_shell = m_strdup(pw->pw_shell); { char *passwd_crypt = pw->pw_passwd; #ifdef HAVE_SHADOW_H /* get the shadow password if possible */ struct spwd *spasswd = getspnam(ses.authstate.pw_name); if (spasswd && spasswd->sp_pwdp) { passwd_crypt = spasswd->sp_pwdp; } #endif if (!passwd_crypt) { /* android supposedly returns NULL */ passwd_crypt = "!!"; } ses.authstate.pw_passwd = m_strdup(passwd_crypt); } #else /* 0 */ ses.authstate.pw_uid = 0; ses.authstate.pw_gid = 0; ses.authstate.pw_name = m_strdup("user"); ses.authstate.pw_dir = m_strdup(conf_home); ses.authstate.pw_shell = m_strdup(conf_shell); if (!ses.authstate.pw_passwd) { /* password hack */ ses.authstate.pw_passwd = m_strdup("!!"); } #endif /* 0 */ } /* Called when channels are modified */ void update_channel_prio() { enum dropbear_prio new_prio; int any = 0; unsigned int i; TRACE(("update_channel_prio")) if (ses.sock_out < 0) { TRACE(("leave update_channel_prio: no socket")) return; } new_prio = DROPBEAR_PRIO_BULK; for (i = 0; i < ses.chansize; i++) { struct Channel *channel = ses.channels[i]; if (!channel || channel->prio == DROPBEAR_CHANNEL_PRIO_EARLY) { if (channel && channel->prio == DROPBEAR_CHANNEL_PRIO_EARLY) { TRACE(("update_channel_prio: early %d", channel->index)) } continue; } any = 1; if (channel->prio == DROPBEAR_CHANNEL_PRIO_INTERACTIVE) { TRACE(("update_channel_prio: lowdelay %d", channel->index)) new_prio = DROPBEAR_PRIO_LOWDELAY; break; } else if (channel->prio == DROPBEAR_CHANNEL_PRIO_UNKNOWABLE && new_prio == DROPBEAR_PRIO_BULK) { TRACE(("update_channel_prio: unknowable %d", channel->index)) new_prio = DROPBEAR_PRIO_DEFAULT; } } if (any == 0) { /* lowdelay during setup */ TRACE(("update_channel_prio: not any")) new_prio = DROPBEAR_PRIO_LOWDELAY; } if (new_prio != ses.socket_prio) { TRACE(("Dropbear priority transitioning %d -> %d", ses.socket_prio, new_prio)) set_sock_priority(ses.sock_out, new_prio); ses.socket_prio = new_prio; } }