Bugzilla – Bug 961645
VUL-0: CVE-2016-0778: openssh: Buffer overflow in roaming code
Last modified: 2019-06-16 14:38:17 UTC
Qualys reports: Support for roaming was elegantly added to the OpenSSH client: the calls to read() and write() that communicate with the SSH server were replaced by calls to roaming_read() and roaming_write(), two wrappers that depend on wait_for_roaming_reconnect() to transparently reconnect to the server after a disconnection. The wait_for_roaming_reconnect() routine is essentially a sequence of four subroutines: 239 int 240 wait_for_roaming_reconnect(void) 241 { ... 250 fprintf(stderr, "[connection suspended, press return to resume]"); ... 252 packet_backup_state(); 253 /* TODO Perhaps we should read from tty here */ 254 while ((c = fgetc(stdin)) != EOF) { ... 259 if (c != '\n' && c != '\r') 260 continue; 261 262 if (ssh_connect(host, &hostaddr, options.port, ... 265 options.proxy_command) == 0 && roaming_resume() == 0) { 266 packet_restore_state(); ... 268 fprintf(stderr, "[connection resumed]\n"); ... 270 return 0; 271 } 272 273 fprintf(stderr, "[reconnect failed, press return to retry]"); ... 275 } 276 fprintf(stderr, "[exiting]\n"); ... 278 exit(0); 279 } 1. packet_backup_state() close()s connection_in and connection_out (the old file descriptors that connected the client to the server), and saves the state of the suspended SSH session (for example, the encryption and decryption contexts). 2. ssh_connect() opens new file descriptors, and connects them to the SSH server. 3. roaming_resume() negotiates the resumption of the suspended SSH session with the server, and calls resend_bytes(). 4. packet_restore_state() updates connection_in and connection_out (with the new file descriptors that connect the client to the server), and restores the state of the suspended SSH session. The new file descriptors for connection_in and connection_out may differ from the old ones (if, for example, files or pipes or sockets are opened or closed between two successive ssh_connect() calls), but unfortunately historical code in OpenSSH assumes that they are constant: - In client_loop(), the variables connection_in and connection_out are cached locally, but packet_write_poll() calls roaming_write(), which may assign new values to connection_in and connection_out (if a reconnection occurs), and client_wait_until_can_do_something() subsequently reuses the old, cached values. - client_loop() eventually updates these cached values, and the following FD_ISSET() uses a new, updated file descriptor (the fd connection_out), but an old, out-of-date file descriptor set (the fd_set writeset). - packet_read_seqnr() (old API, or ssh_packet_read_seqnr(), new API) first calloc()ates setp, a file descriptor set for connection_in; next, it loops around memset(), FD_SET(), select() and roaming_read(); last, it free()s setp and returns. Unfortunately, roaming_read() may reassign a higher value to connection_in (if a reconnection occurs), but setp is never enlarged, and the following memset() and FD_SET() may therefore overflow setp (a heap-based buffer overflow): 1048 int 1049 packet_read_seqnr(u_int32_t *seqnr_p) 1050 { .... 1052 fd_set *setp; .... 1058 setp = (fd_set *)xcalloc(howmany(active_state->connection_in + 1, 1059 NFDBITS), sizeof(fd_mask)); .... 1065 for (;;) { .... 1075 if (type != SSH_MSG_NONE) { 1076 free(setp); 1077 return type; 1078 } .... 1083 memset(setp, 0, howmany(active_state->connection_in + 1, 1084 NFDBITS) * sizeof(fd_mask)); 1085 FD_SET(active_state->connection_in, setp); .... 1092 for (;;) { .... 1097 if ((ret = select(active_state->connection_in + 1, setp, 1098 NULL, NULL, timeoutp)) >= 0) 1099 break; .... 1115 } .... 1117 do { .... 1119 len = roaming_read(active_state->connection_in, buf, 1120 sizeof(buf), &cont); 1121 } while (len == 0 && cont); .... 1130 } 1131 /* NOTREACHED */ 1132 } - packet_write_wait() (old API, or ssh_packet_write_wait(), new API) is basically similar to packet_read_seqnr() and may overflow its own setp if roaming_write() (called by packet_write_poll()) reassigns a higher value to connection_out (after a successful reconnection): 1739 void 1740 packet_write_wait(void) 1741 { 1742 fd_set *setp; .... 1746 setp = (fd_set *)xcalloc(howmany(active_state->connection_out + 1, 1747 NFDBITS), sizeof(fd_mask)); 1748 packet_write_poll(); 1749 while (packet_have_data_to_write()) { 1750 memset(setp, 0, howmany(active_state->connection_out + 1, 1751 NFDBITS) * sizeof(fd_mask)); 1752 FD_SET(active_state->connection_out, setp); .... 1758 for (;;) { .... 1763 if ((ret = select(active_state->connection_out + 1, 1764 NULL, setp, NULL, timeoutp)) >= 0) 1765 break; .... 1776 } .... 1782 packet_write_poll(); 1783 } 1784 free(setp); 1785 } ------------------------------------------------------------------------ Mitigating Factors ------------------------------------------------------------------------ This buffer overflow affects all OpenSSH clients >= 5.4, but its impact is significantly reduced by the Mitigating Factors detailed in the Information Leak section, and additionally: - OpenSSH versions >= 6.8 reimplement packet_backup_state() and packet_restore_state(), but introduce a bug that prevents the buffer overflow from being exploited; indeed, ssh_packet_backup_state() swaps two local pointers, ssh and backup_state, instead of swapping the two global pointers active_state and backup_state: 9 struct ssh *active_state, *backup_state; ... 238 void 239 packet_backup_state(void) 240 { 241 ssh_packet_backup_state(active_state, backup_state); 242 } 243 244 void 245 packet_restore_state(void) 246 { 247 ssh_packet_restore_state(active_state, backup_state); 248 } 2269 void 2270 ssh_packet_backup_state(struct ssh *ssh, 2271 struct ssh *backup_state) 2272 { 2273 struct ssh *tmp; .... 2279 if (backup_state) 2280 tmp = backup_state; 2281 else 2282 tmp = ssh_alloc_session_state(); 2283 backup_state = ssh; 2284 ssh = tmp; 2285 } .... 2291 void 2292 ssh_packet_restore_state(struct ssh *ssh, 2293 struct ssh *backup_state) 2294 { 2295 struct ssh *tmp; .... 2299 tmp = backup_state; 2300 backup_state = ssh; 2301 ssh = tmp; 2302 ssh->state->connection_in = backup_state->state->connection_in; As a result, the global pointer backup_state is still NULL when passed to ssh_packet_restore_state(), and crashes the OpenSSH client when dereferenced: # env ROAMING="overflow:A fd_leaks:0" "`pwd`"/sshd -o ListenAddress=127.0.0.1:222 -o UsePrivilegeSeparation=no -f /etc/ssh/sshd_config -h /etc/ssh/ssh_host_rsa_key $ /usr/bin/ssh -V OpenSSH_6.8, LibreSSL 2.1 $ /usr/bin/ssh -o ProxyCommand="/usr/bin/nc -w 15 %h %p" -p 222 127.0.0.1 user@127.0.0.1's password: [connection suspended, press return to resume]Segmentation fault (core dumped) This bug prevents the buffer overflow from being exploited, but not the information leak, because the vulnerable function resend_bytes() is called before ssh_packet_restore_state() crashes. - To the best of our knowledge, this buffer overflow is not exploitable in the default configuration of the OpenSSH client; the conclusion of the File Descriptor Leak section suggests that two non-default options are required: a ProxyCommand, and either ForwardAgent (-A) or ForwardX11 (-X). ------------------------------------------------------------------------ File Descriptor Leak ------------------------------------------------------------------------ A back-of-the-envelope calculation indicates that, in order to increase the file descriptor connection_in or connection_out, and thus overflow the file descriptor set setp in packet_read_seqnr() or packet_write_wait(), a file descriptor leak is needed: - First, the number of bytes calloc()ated for setp is rounded up to the nearest multiple of sizeof(fd_mask): 8 bytes (or 64 file descriptors) on 64-bit systems. - Next, in glibc, this number is rounded up to the nearest multiple of MALLOC_ALIGNMENT: 16 bytes (or 128 file descriptors) on 64-bit systems. - Last, in glibc, a MIN_CHUNK_SIZE is enforced: 32 bytes on 64-bit systems, of which 24 bytes (or 192 file descriptors) are reserved for setp. - In conclusion, a file descriptor leak is needed, because connection_in or connection_out has to be increased by hundreds in order to overflow setp. The search for a suitable file descriptor leak begins with a study of the behavior of the four ssh_connect() methods, when called for a reconnection by wait_for_roaming_reconnect(): 1. The default method ssh_connect_direct() communicates with the server through a simple TCP socket: the two file descriptors connection_in and connection_out are both equal to this socket's file descriptor. In wait_for_roaming_reconnect(), the low-numbered file descriptor of the old TCP socket is close()d by packet_backup_state(), but immediately reused for the new TCP socket in ssh_connect_direct(): the new file descriptors connection_in and connection_out are equal to this old, low-numbered file descriptor, and cannot possibly overflow setp. 2. The special ProxyCommand "-" communicates with the server through stdin and stdout, but (as explained in the Mitigating Factors of the Information Leak section) it cannot possibly reconnect to the server, and is therefore immune to this buffer overflow. 3. Surprisingly, we discovered a file descriptor leak in the ssh_proxy_fdpass_connect() method itself; indeed, the file descriptor sp[1] is never close()d: 101 static int 102 ssh_proxy_fdpass_connect(const char *host, u_short port, 103 const char *proxy_command) 104 { ... 106 int sp[2], sock; ... 113 if (socketpair(AF_UNIX, SOCK_STREAM, 0, sp) < 0) 114 fatal("Could not create socketpair to communicate with " 115 "proxy dialer: %.100s", strerror(errno)); ... 161 close(sp[0]); ... 164 if ((sock = mm_receive_fd(sp[1])) == -1) 165 fatal("proxy dialer did not pass back a connection"); ... 171 /* Set the connection file descriptors. */ 172 packet_set_connection(sock, sock); 173 174 return 0; 175 } However, two different reasons prevent this file descriptor leak from triggering the setp overflow: - The method ssh_proxy_fdpass_connect() communicates with the server through a single socket received from the ProxyCommand: the two file descriptors connection_in and connection_out are both equal to this socket's file descriptor. In wait_for_roaming_reconnect(), the low-numbered file descriptor of the old socket is close()d by packet_backup_state(), reused for sp[0] in ssh_proxy_fdpass_connect(), close()d again, and eventually reused again for the new socket: the new file descriptors connection_in and connection_out are equal to this old, low-numbered file descriptor, and cannot possibly overflow setp. - Because of the waitpid() bug described in the Mitigating Factors of the Information Leak section, the method ssh_proxy_fdpass_connect() calls fatal() before it returns to wait_for_roaming_reconnect(), and is therefore immune to this buffer overflow. 4. The method ssh_proxy_connect() communicates with the server through a ProxyCommand and two different pipes: the file descriptor connection_in is the read end of the second pipe (pout[0]), and the file descriptor connection_out is the write end of the first pipe (pin[1]): 180 static int 181 ssh_proxy_connect(const char *host, u_short port, const char *proxy_command) 182 { ... 184 int pin[2], pout[2]; ... 192 if (pipe(pin) < 0 || pipe(pout) < 0) 193 fatal("Could not create pipes to communicate with the proxy: %.100s", 194 strerror(errno)); ... 240 /* Close child side of the descriptors. */ 241 close(pin[0]); 242 close(pout[1]); ... 247 /* Set the connection file descriptors. */ 248 packet_set_connection(pout[0], pin[1]); 249 250 /* Indicate OK return */ 251 return 0; 252 } In wait_for_roaming_reconnect(), the two old, low-numbered file descriptors connection_in and connection_out are both close()d by packet_backup_state(), and immediately reused for the pipe(pin) in ssh_proxy_connect(): the new connection_out (pin[1]) is equal to one of these old, low-numbered file descriptors, and cannot possibly overflow setp. On the other hand, the pipe(pout) in ssh_proxy_connect() may return high-numbered file descriptors, and the new connection_in (pout[0]) may therefore overflow setp, if hundreds of file descriptors were leaked before the call to wait_for_roaming_reconnect(): - We discovered a file descriptor leak in the pubkey_prepare() function of OpenSSH >= 6.8; indeed, if the client is running an authentication agent that does not offer any private keys, the reference to agent_fd is lost, and this file descriptor is never close()d: 1194 static void 1195 pubkey_prepare(Authctxt *authctxt) 1196 { .... 1200 int agent_fd, i, r, found; .... 1247 if ((r = ssh_get_authentication_socket(&agent_fd)) != 0) { 1248 if (r != SSH_ERR_AGENT_NOT_PRESENT) 1249 debug("%s: ssh_get_authentication_socket: %s", 1250 __func__, ssh_err(r)); 1251 } else if ((r = ssh_fetch_identitylist(agent_fd, 2, &idlist)) != 0) { 1252 if (r != SSH_ERR_AGENT_NO_IDENTITIES) 1253 debug("%s: ssh_fetch_identitylist: %s", 1254 __func__, ssh_err(r)); 1255 } else { .... 1288 authctxt->agent_fd = agent_fd; 1289 } .... 1299 } However, OpenSSH clients >= 6.8 crash in ssh_packet_restore_state() (because of the NULL-pointer dereference discussed in the Mitigating Factors of the Buffer Overflow section) and are immune to the setp overflow, despite this agent_fd leak. - If ForwardAgent (-A) or ForwardX11 (-X) is enabled in the OpenSSH client (it is disabled by default), a malicious SSH server can request hundreds of forwardings, in order to increase connection_in (each forwarding opens a file descriptor), and thus overflow setp in packet_read_seqnr(): # env ROAMING="overflow:A" "`pwd`"/sshd -o ListenAddress=127.0.0.1:222 -o UsePrivilegeSeparation=no -f /dev/null -h /etc/ssh/ssh_host_rsa_key $ /usr/bin/ssh -V OpenSSH_6.6.1p1 Ubuntu-2ubuntu2, OpenSSL 1.0.1f 6 Jan 2014 $ /usr/bin/ssh-agent -- /usr/bin/ssh -A -o ProxyCommand="/usr/bin/socat - TCP4:%h:%p" -p 222 127.0.0.1 user@127.0.0.1's password: [connection suspended, press return to resume][connection resumed] *** Error in `/usr/bin/ssh': free(): invalid next size (fast): 0x00007f0474d03e70 *** Aborted (core dumped) # env ROAMING="overflow:X" "`pwd`"/sshd -o ListenAddress=127.0.0.1:222 -o UsePrivilegeSeparation=no -f /etc/ssh/sshd_config -h /etc/ssh/ssh_host_rsa_key $ /usr/bin/ssh -V OpenSSH_6.4p1, OpenSSL 1.0.1e-fips 11 Feb 2013 $ /usr/bin/ssh -X -o ProxyCommand="/usr/bin/socat - TCP4:%h:%p" -p 222 127.0.0.1 user@127.0.0.1's password: [connection suspended, press return to resume][connection resumed] *** Error in `/usr/bin/ssh': free(): invalid next size (fast): 0x00007fdcc2a3aba0 *** *** Error in `/usr/bin/ssh': malloc(): memory corruption: 0x00007fdcc2a3abc0 *** Finally, a brief digression on two unexpected problems that had to be solved in our proof-of-concept: - First, setp can be overflowed only in packet_read_seqnr(), not in packet_write_wait(), but agent forwarding and X11 forwarding are post- authentication functionalities, and post-authentication calls to packet_read() or packet_read_expect() are scarce, except in the key-exchange code of OpenSSH clients < 6.8: our proof-of-concept effectively forces a rekeying in order to overflow setp in packet_read_seqnr(). - Second, after a successful reconnection, packet_read_seqnr() may call fatal("Read from socket failed: %.100s", ...), because roaming_read() may return EAGAIN (EAGAIN is never returned without the reconnection, because the preceding call to select() guarantees that connection_in is ready for read()). Our proof-of-concept works around this problem by forcing the client to resend MAX_ROAMBUF bytes (2M) to the server, allowing data to reach the client before roaming_read() is called, thus avoiding EAGAIN.
CRD: 2016-01-14 9:00 AM PST
The reproducer in bsc#961642 should be
QA hint for verifying the roaming feature is wiped: Affected client in default config: > debug1: Roaming not allowed by server Original client, when run with "-oUseRoaming=no" or 'UseRoaming no' in ssh_config or ~/.ssh/config will not print this. Expected behavior for the fixed client: Never print this, even when -oUseRoaming=yes is specified or 'UseRoaming yes' is in ssh_config or ~/.ssh/config.
Created attachment 661721 [details] Final upstream patch
This is an autogenerated message for OBS integration: This bug (961645) was mentioned in https://build.opensuse.org/request/show/353722 13.2 / openssh
Official advisory by Qualys: https://www.qualys.com/2016/01/14/cve-2016-0777-cve-2016-0778/openssh-cve-2016-0777-cve-2016-0778.txt SUSE would like to thank Qualys for the detailed report.
SUSE-SU-2016:0117-1: An update that fixes two vulnerabilities is now available. Category: security (critical) Bug References: 961642,961645 CVE References: CVE-2016-0777,CVE-2016-0778 Sources used: SUSE Linux Enterprise Server 11-SECURITY (src): openssh-openssl1-6.6p1-10.1
SUSE-SU-2016:0118-1: An update that fixes two vulnerabilities is now available. Category: security (critical) Bug References: 961642,961645 CVE References: CVE-2016-0777,CVE-2016-0778 Sources used: SUSE Linux Enterprise Server 12-SP1 (src): openssh-6.6p1-33.1, openssh-askpass-gnome-6.6p1-33.1 SUSE Linux Enterprise Server 12 (src): openssh-6.6p1-33.1, openssh-askpass-gnome-6.6p1-33.1 SUSE Linux Enterprise Desktop 12-SP1 (src): openssh-6.6p1-33.1, openssh-askpass-gnome-6.6p1-33.1 SUSE Linux Enterprise Desktop 12 (src): openssh-6.6p1-33.1, openssh-askpass-gnome-6.6p1-33.1
SUSE-SU-2016:0119-1: An update that fixes two vulnerabilities is now available. Category: security (critical) Bug References: 961642,961645 CVE References: CVE-2016-0777,CVE-2016-0778 Sources used: SUSE Linux Enterprise Server for VMWare 11-SP3 (src): openssh-6.2p2-0.24.1, openssh-askpass-gnome-6.2p2-0.24.3 SUSE Linux Enterprise Server 11-SP3 (src): openssh-6.2p2-0.24.1, openssh-askpass-gnome-6.2p2-0.24.3 SUSE Linux Enterprise Desktop 11-SP3 (src): openssh-6.2p2-0.24.1, openssh-askpass-gnome-6.2p2-0.24.3 SUSE Linux Enterprise Debuginfo 11-SP3 (src): openssh-6.2p2-0.24.1, openssh-askpass-gnome-6.2p2-0.24.3
SUSE-SU-2016:0120-1: An update that fixes two vulnerabilities is now available. Category: security (critical) Bug References: 961642,961645 CVE References: CVE-2016-0777,CVE-2016-0778 Sources used: SUSE Linux Enterprise Server 11-SP4 (src): openssh-6.6p1-16.1, openssh-askpass-gnome-6.6p1-16.4 SUSE Linux Enterprise Desktop 11-SP4 (src): openssh-6.6p1-16.1, openssh-askpass-gnome-6.6p1-16.4 SUSE Linux Enterprise Debuginfo 11-SP4 (src): openssh-6.6p1-16.1, openssh-askpass-gnome-6.6p1-16.4
Requesting patch for SLES11SP1 LTSS.
(In reply to Haral Tsitsivas from comment #16) > Requesting patch for SLES11SP1 LTSS. Not affected.
Releasing openSUSE updates, resolving as fixed.
openSUSE-SU-2016:0127-1: An update that fixes two vulnerabilities is now available. Category: security (critical) Bug References: 961642,961645 CVE References: CVE-2016-0777,CVE-2016-0778 Sources used: openSUSE 13.2 (src): openssh-6.6p1-5.3.1, openssh-askpass-gnome-6.6p1-5.3.1
openSUSE-SU-2016:0128-1: An update that fixes two vulnerabilities is now available. Category: security (critical) Bug References: 961642,961645 CVE References: CVE-2016-0777,CVE-2016-0778 Sources used: openSUSE Leap 42.1 (src): openssh-6.6p1-8.1, openssh-askpass-gnome-6.6p1-8.1
This is an autogenerated message for OBS integration: This bug (961645) was mentioned in https://build.opensuse.org/request/show/353786 13.1 / openssh
This is an autogenerated message for OBS integration: This bug (961645) was mentioned in https://build.opensuse.org/request/show/353827 Evergreen:11.4+13.1 / openssh
This is an autogenerated message for OBS integration: This bug (961645) was mentioned in https://build.opensuse.org/request/show/353838 Evergreen:11.4 / openssh
openSUSE-SU-2016:0144-1: An update that fixes three vulnerabilities is now available. Category: security (critical) Bug References: 961642,961645 CVE References: CVE-2016-077,CVE-2016-0777,CVE-2016-0778 Sources used: openSUSE Evergreen 11.4 (src): openssh-5.8p1-11.1, openssh-askpass-gnome-5.8p1-11.1
openSUSE-SU-2016:0145-1: An update that fixes three vulnerabilities is now available. Category: security (critical) Bug References: 961642,961645 CVE References: CVE-2016-077,CVE-2016-0777,CVE-2016-0778 Sources used: openSUSE 13.1 (src): openssh-6.2p2-3.7.1, openssh-askpass-gnome-6.2p2-3.7.1