OpenSSL 是一个强大的安全套接字层密码库,其囊括了目前主流的密码算法,常用的密钥,证书封装管理功能以及SSL协议,并提供了丰富的应用程序可供开发人员测试或其它目的使用。
但是安全研究人员发现,OpenSSL的BIO_*printf()函数中存在几个问题,该函数定义在crypto/bio/b_print.c文件之中。开发人员将会在即将发布的安全补丁中修复这一问题。
这个函数的主要功能是负责解释和转换格式字符串,该函数的输入参数为_dpor()。
dpor()可以以一种渐进的方式对格式字符串中的字符逐个进行扫描,并利用doapr_outch()函数对数据进行输出。
doapr_outchr()函数的前两个参数是一个指向静态分配缓冲区(char** sbuffer)的double类型指针,以及一个指向char类型指针(char **buffer)的指针。值得注意的是,该指针的值是由doapr_outch()函数所分配的动态内存空间来决定的。
第一个参数,即静态缓冲区,应该始终是有效的。其内存空间的大小是由第三个参数所决定的,即t* currlen。
static void
701 doapr_outch(char **sbuffer,
702 char **buffer, size_t *currlen, size_t *maxlen, int c)
703 {
704 /* If we haven't at least one buffer, someone has doe a big booboo */
705 assert(*sbuffer != NULL || buffer != NULL);
706
707 /* |currlen| must always be <= |*maxlen| */
708 assert(*currlen <= *maxlen);
709
710 if (buffer && *currlen == *maxlen) {
711 *maxlen += 1024;
712 if (*buffer == NULL) {
713 *buffer = OPENSSL_malloc(*maxlen);
714 if (!*buffer) {
715 /* Panic! Can't really do anything sensible. Just return */
716 return;
717 }
718 if (*currlen > 0) {
719 assert(*sbuffer != NULL);
720 memcpy(*buffer, *sbuffer, *currlen);
721 }
722 *sbuffer = NULL;
723 } else {
724 *buffer = OPENSSL_realloc(*buffer, *maxlen);
725 if (!*buffer) {
726 /* Panic! Can't really do anything sensible. Just return */
727 return;
728 }
729 }
730 }
731
732 if (*currlen < *maxlen) {
733 if (*sbuffer)
734 (*sbuffer)[(*currlen)++] = (char)c;
735 else
736 (*buffer)[(*currlen)++] = (char)c;
737 }
738
739 return;
740 }
在这里,doapr_outch()函数将会用数据填写静态分配缓冲区sbuffer,直到其存储空间被写满;具体实现方式可查看下列代码段中的第732行代码,但是在第734行代码处,一个字节的数据将会被追加写入至*sbuffer:
732 if (*currlen < *maxlen) {
733 if (*sbuffer)
734 (*sbuffer)[(*currlen)++] = (char)c;
当缓冲区sbuffer被写满之后(*currlen指针将与*maxlen指针相同),调用函数将会允许系统动态分配内存空间,那么下列条件语句的值将会为“真”:
710 if (buffer && *currlen == *maxlen) {
在这里我们可以看到,每处理1024个字节的数据,系统都需要进行一次内存分配。当堆内存分配成功之后,*sbuffer会被清空:
713 *buffer = OPENSSL_malloc(*maxlen);
714 if (!*buffer) {
715 /* Panic! Can't really do anything sensible. Just return */
716 return;
717 }
718 if (*currlen > 0) {
719 assert(*sbuffer != NULL);
720 memcpy(*buffer, *sbuffer, *currlen);
721 }
722 *sbuffer = NULL;
sbuffer被清空之后,数据字节将会被写入基于堆的*buffer,而不是基于栈的*sbuffer:
if (*currlen < *maxlen) {
733 if (*sbuffer)
734 (*sbuffer)[(*currlen)++] = (char)c;
735 else
736 (*buffer)[(*currlen)++] = (char)c;
737 }
BIO_printf/BIO_vprintf与BIO_snprintf/BIO_vsnprintf之间的区别
BIO_printf()函数与BIO_vprintf()函数可以允许doapr_outch()根据一个指向char指针的有效指针来为系统动态分配内存空间。
int BIO_printf(BIO *bio, const char *format, ...)
745 {
746 va_list args;
747 int ret;
748
749 va_start(args, format);
750
751 ret = BIO_vprintf(bio, format, args);
752
753 va_end(args);
754 return (ret);
755 }
756
757 int BIO_vprintf(BIO *bio, const char *format, va_list args)
758 {
759 int ret;
760 size_t retlen;
761 char hugebuf[1024 * 2]; /* Was previously 10k, which is unreasonable
762 * in small-stack environments, like threads
763 * or DOS programs. */
764 char *hugebufp = hugebuf;
765 size_t hugebufsize = sizeof(hugebuf);
766 char *dynbuf = NULL;
767 int ignored;
768
769 dynbuf = NULL;
770 CRYPTO_push_info("doapr()");
771 _dopr(&hugebufp, &dynbuf, &hugebufsize, &retlen, &ignored, format, args);
772 if (dynbuf) {
773 ret = BIO_write(bio, dynbuf, (int)retlen);
774 OPENSSL_free(dynbuf);
775 } else {
776 ret = BIO_write(bio, hugebuf, (int)retlen);
777 }
778 CRYPTO_pop_info();
779 return (ret);
780 }
BIO_vprintf()可以向系统提供静态分配的缓冲区(hugebuf),其大小在hugebufsize中进行了编码处理;并且还提供了一个指向char类型指针的指针(dynbuf)。BIO_print()函数所采用的运行机制与BIO_vprintf()函数的运行机制相同。
相比之下,另外两个*printf函数-BIO_vsnprintf()和BIO_snprintf()只能够使用静态分配的缓冲区,这部分数据由调用函数提供:
int BIO_snprintf(char *buf, size_t n, const char *format, ...)
789 {
790 va_list args;
791 int ret;
792
793 va_start(args, format);
794
795 ret = BIO_vsnprintf(buf, n, format, args);
796
797 va_end(args);
798 return (ret);
799 }
800
801 int BIO_vsnprintf(char *buf, size_t n, const char *format, va_list args)
802 {
803 size_t retlen;
804 int truncated;
805
806 _dopr(&buf, NULL, &n, &retlen, &truncated, format, args);
807
808 if (truncated)
809 /*
810 * In case of truncation, return -1 like traditional snprintf.
811 * (Current drafts for ISO/IEC 9899 say snprintf should return the
812 * number of characters that would have been written, had the buffer
813 * been large enough.)
814 */
815 return -1;
816 else
817 return (retlen <= INT_MAX) ? (int)retlen : -1;
818 }
漏洞信息
doapr_outch()函数中存在的一个问题就是,其他函数在调用这个函数时,如果内存分配失败,系统不会提供任何的提示信息,因为这是一个没有返回值的函数:
*buffer = OPENSSL_malloc(*maxlen);
714 if (!*buffer) {
715 /* Panic! Can't really do anything sensible. Just return */
716 return;
717 }
…
724 *buffer = OPENSSL_realloc(*buffer, *maxlen);
725 if (!*buffer) {
726 /* Panic! Can't really do anything sensible. Just return */
727 return;
缺少错误提示,也就意味着只要还有字符串没有输出完成,_dopr()函数就会继续调用doapr_outch()。
除此之外,在分配内存空间之前,maxlen的值会递增,这也就意味着,即使内存空间分配失败,maxlen仍然可以表示堆内存的空间大小。实际上,无论内存空间的分配成功与否,maxlen的作用都是一样的:
*maxlen += 1024;
712 if (*buffer == NULL) {
713 *buffer = OPENSSL_malloc(*maxlen);
714 if (!*buffer) {
715 /* Panic! Can't really do anything sensible. Just return */
716 return;
717 }
因此,在内存空间分配失败之后调用doapr_outch()函数,下列代码中的条件语句将为“假”:
710 if (buffer && *currlen == *maxlen) {
内存空间的分配失败将会导致*buffer的值被清空,但是buffer(指针)仍然是有效的。
然而,此时*currlen指针的值与*maxlen指针的值就不同了,因为在之前调用的过程中,*maxlen的值只增加了1024。
如果if条件语句中的条件为“假”,系统将会跳过函数中的大部分核心代码:
732 if (*currlen < *maxlen) {
733 if (*sbuffer)
734 (*sbuffer)[(*currlen)++] = (char)c;
735 else
736 (*buffer)[(*currlen)++] = (char)c;
737 }
现在,*currlen实际上就是*maxlen,而*sbuffer的值为空。因此,下列这段代码将会被执行:
736 (*buffer)[(*currlen)++] = (char)c;
buffer变成了空指针,而currlen指针有可能指向任何内容,其指向的内容具体将取决于系统在内存分配过程中的失败信息。
*currlen指针是一个长度为32个字节的整形指针,所以当指针在使用时,它会指向一个大小不超过4GB的虚拟内存空间。但是在32位内存布局中,攻击的成功率完全取决于攻击者的技术水平,特别是当攻击者可以利用某种方式来引起相关系统中发生内存溢出的时候。
然而,一个系统中可以供OpenSSL使用的内存空间还剩多少?当前系统中正在运行的其他应用程序如果也在使用OpenSSL的话,这些应用所需要消耗的资源将会对攻击者的操作产生影响。
即使攻击者能够通过内存消耗的手段来引起内存崩溃,并实现远程代码执行,但这样的操作在攻击实践的过程中是非常困难的。而且,程序的堆内存也有可能会发生崩溃,这样将会导致存储在其中的重要数据丢失,结果将会不堪设想。这也就是我们所说的堆破坏。
而且,即使你能够保证系统中不出现恶意软件,但是堆内存空间不足将会导致系统随时可能发生堆内存崩溃。
另一种能够触发这一漏洞的方法
除此之外,还有一件有趣的事情。现在还有另外一种确切的方法可以引起OPENSSL_realloc()失败。
实际上,OPENSSL_realloc()可以算得上是CRYPTO_realloc()函数的一个宏:
375 void *CRYPTO_realloc(void *str, int num, const char *file, int line)
376 {
377 void *ret = NULL;
378
379 if (str == NULL)
380 return CRYPTO_malloc(num, file, line);
381
382 if (num <= 0)
383 return NULL;
num是一个有符号的32位整数,如果它的值为0或者为负数的话,函数将会返回NULL。
因为在doapr_outch()函数中,*maxlen指针会在每次进行内存分配时增加1024:
711 *maxlen += 1024;
这个值最终将会变成一个负值。然后OPENSSL_realloc()也会不可避免地发生错误,因为CRYPTO_realloc()是不会对一个大小为负值的内容进行内存分配的。
换句话说,如果我们向BIO_printf()提供一个非常大的字符串,那么就肯定能够触发这一漏洞。
受影响的软件
Apache httpd同样也使用了BIO_printf:
https://github.com/apache/httpd/blob/trunk/modules/ssl/ssl_util_ocsp.c#L46
但是,我们目前还没有对其进行分析检测,所以暂时还不知道该漏洞在这一产品中将会被如何利用。
还有一些其他著名的应用程序也使用了BIO_printf():
还没有评论,来说两句吧...