CVE-2016-0799分析

  • A+
所属分类:漏洞解析

0x00 内容简介


最近openssl又除了一系列问题,具体可以看这里CVE-2016-0799只是其中一个比较简单的漏洞。造成漏洞的原因主要有两个。

  • doapr_outch中有可能存在整数溢出导致申请内存大小为负数
  • doapr_outch函数在申请内存失败时没有做异常处理

0x01 源码分析

首先,去github上找到了这一次漏洞修复的commit,可以看到主要修改的是doapr_outch函数。

CVE-2016-0799分析

有了一个大致的了解之后,将代码切换到bug修复之前的版本。函数源码如下:

#!cpp
 static void                                                     
 doapr_outch(char **sbuffer,
             char **buffer, size_t *currlen, size_t *maxlen, int c)
 {
     /* If we haven't at least one buffer, someone has doe a big booboo */
     assert(*sbuffer != NULL || buffer != NULL);
             if (*buffer == NULL) {
     /* |currlen| must always be <= |*maxlen| */
     assert(*currlen <= *maxlen);
 
     if (buffer && *currlen == *maxlen) {
        *maxlen += 1024;
        if (*buffer == NULL) {   
             *buffer = OPENSSL_malloc(*maxl
                 /* Panic! Can't really do anything sensible. Just return */
                return; //这里没有做异常处理直接返回了
             }           
            if (*currlen > 0) {
                assert(*sbuffer != NULL);
                memcpy(*buffer, *sbuffer, *currlen);
             }           
             *sbuffer = NULL;
         } else {        
             *buffer = OPENSSL_realloc(*buffer, *maxlen);
             if (!*buffer) {
                /* Panic! Can't really do anything sensible. Just return */
                return; //这里没有做异常处理直接返回了
            }           
         }               
     }                   
 
     if (*currlen < *maxlen) {
         if (*sbuffer)   
             (*sbuffer)[(*currlen)++] = (char)c;
         else            
            (*buffer)[(*currlen)++] = (char)c;
    }                   

     return;             
 }

我是看完了一篇国外的分析文章之后了解了整个漏洞的流程,这里我就试图反向的思考一下这个漏洞。希望可以提高从代码补丁中寻找重现流程的能力。

1.1 寻找内存改写的方式

因为通过补丁已经知道是doapr_outch函数导致的堆腐败问题,所以doapr_outch一定存在改写数据的代码段。可以看到除了728-734行代码是对内存的改写外,没有其他地方操作内存的内容了。

#!cpp
if (*currlen < *maxlen) {
   if (*sbuffer)   
      (*sbuffer)[(*currlen)++] = (char)c; //这里
    else            
       (*buffer)[(*currlen)++] = (char)c; //这里
 }

这里改写内存的方式可以用伪代码简单总结一下:

#!c
base[offset]=c

所以想要向指定的内存写入数据的话需要控制baseoffset两个参数。而写入的数据是c。如果控制了baseoffset那么每次调用函数就可以改写一个字节。

如果是有经验的开发人员可以很容易看出外部在调用的时候一定是循环调用了doapr_outch,看一看函数调用处的代码。

#!c
 static void
 fmtstr(char **sbuffer,
        char **buffer,
       size_t *currlen,
        size_t *maxlen, const char *value, int flags, int min, int max)
 {
     int padlen, strln;
    int cnt = 0;
 
     if (value == 0)
        value = "<NULL>";
     for (strln = 0; value[strln]; ++strln) ;
     padlen = min - strln;
     if (padlen < 0)
        padlen = 0;
     if (flags & DP_F_MINUS)
        padlen = -padlen;
 
     while ((padlen > 0) && (cnt < max)) {
         doapr_outch(sbuffer, buffer, currlen, maxlen, ' ');
         --padlen;
        ++cnt;
     }
    while (*value && (cnt < max)) {
         doapr_outch(sbuffer, buffer, currlen, maxlen, *value++); //这里!
        ++cnt;
    }
     ...
 }

可以看到,确实是通过循环来改写内存的。

1.2 副作用编程

函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并且降低程序的可读性。严格的函数式语言要求函数必须无副作用。

副作用编程带来的不必要麻烦有一句更通俗的话可以来说明。开发一时爽,调试火葬场。这里再来看一下

doapr_outch的函数声明

#!c
static void doapr_outch(char **, char **, size_t *, size_t *, int);

从声明不难看出sbufferbuffercurrlenmaxlen这几个参数在函数第n次运行时候如果被改变了,那么第n+1次运行的时候,这些参数将使用上次改变了的值。

再结合代码写入处内存改写的方式,就可以肯定sbufferbuffer一定有一个或者全部被改写了,导致进入了意料之外的逻辑。

#!c
     if (*currlen < *maxlen) {
         if (*sbuffer)   
             (*sbuffer)[(*currlen)++] = (char)c; //这里
         else            
             (*buffer)[(*currlen)++] = (char)c; //这里
     }

因为Malloc或者Realloc出来的地址一定不是可控的,而系统传进来的sbuffer也一定不可控,再结合上面的代码,如果sbuffer或者buffer指向NULL的话,基址就是固定的了。

718行的代码会将sbuffer设置为空指针。而buffer编程空指针只能是申请内存失败的时候。

在结合上728-733行代码,要做到这一步一定要满足的条件是*sbuffer*buffer都指向NULL,导致代码进入改写*buffer为基址的内存块。其他任何情况都无法做到内存开始地址可控。

所以再分代码,看流程是否可能将*sbuffer*buffer赋值为NULL

1.3 改写sbuffer与buffer

#!c
  static void                                                     
 doapr_outch(char **sbuffer,
             char **buffer, size_t *currlen, size_t *maxlen, int c)
 {
     /* If we haven't at least one buffer, someone has doe a big booboo */
     assert(*sbuffer != NULL || buffer != NULL);
             if (*buffer == NULL) {
     /* |currlen| must always be <= |*maxlen| */
     assert(*currlen <= *maxlen);
 
     if (buffer && *currlen == *maxlen) {
         *maxlen += 1024;
         if (*buffer == NULL) {   
             *buffer = OPENSSL_malloc(*maxl
                 /* Panic! Can't really do anything sensible. Just return */
                 return; //这里没有做异常处理直接返回了
             }           
             if (*currlen > 0) {
                 assert(*sbuffer != NULL);
                 memcpy(*buffer, *sbuffer, *currlen);
             }           
             *sbuffer = NULL;//这里!
        ...
     if (*currlen < *maxlen) {
         if (*sbuffer)   
             (*sbuffer)[(*currlen)++] = (char)c;
         else            
             (*buffer)[(*currlen)++] = (char)c;
     }                   
 
     return;             
 }

在循环调用doapr_outch之后,当*currlen == *maxlen成立的时候就会进入内存申请模块,因为*buffer还没有申请过所以进入上面一个分支,申请内存后将*sbuffer设为NULL。

还需要将*buffer设为NULL。

#!c
     if (buffer && *currlen == *maxlen) {
         *maxlen += 1024;
         if (*buffer == NULL) {   
             *buffer = OPENSSL_malloc(*maxl
                 /* Panic! Can't really do anything sensible. Just return */
                 return; //这里没有做异常处理直接返回了
             }           
             if (*currlen > 0) {
                 assert(*sbuffer != NULL);
                 memcpy(*buffer, *sbuffer, *currlen);
             }           
             *sbuffer = NULL;
         } else {        
             *buffer = OPENSSL_realloc(*buffer, *maxlen);
             if (!*buffer) {
                 /* Panic! Can't really do anything sensible. Just return */
                 return; //这里没有做异常处理直接返回了
             }           
         }               
     }

再一次*currlen == *maxlen之后,又会进入内存分配阶段,这次会进入Realloc的分支,那么只要realloc失败的话,*buffer就会被赋值为NULL。

最简单的情况就是堆上内存用完了,这个时候buffer就是NULL了,这个时候就可以根据currlen以及后续的c来改写目标地址的数据了。但是堆上内存用完,导致申请内存返回NULL,是一件不可控的事情。

那么除了这种情况,还有什么情况下,realloc会返回NULL呢。

#!c
 void *CRYPTO_realloc(void *str, int num, const char *file, int line)
 {
      void *ret = NULL;
      if (str == NULL)
      return CRYPTO_malloc(num, file, line);
      if (num <= 0)
      return NULL;

可以注意到在708行,对*maxlen做了增加1024的操作,那么如果maxlen怎么1024之后超过int的范围,就会导致realloc传入的size是一个负数。这个时候buffer就会因为realloc的参数错误被设置为NULL。然后因为出错,函数退出。

1.3 出错不处理

#!c
 while (*value && (cnt < max)) {
     doapr_outch(sbuffer, buffer, currlen, maxlen, *value++); //这里!
     ++cnt;
 }

从这里可以看到,*buffer被设置为NULL,返回出来了。但是外面的循环什么都没干,又继续执行了。

这个时候就可以做内存改写了。currlen与c都是与我们传递的字符串相关的,这个很好理解了。

0x02 小结


  • 开发过程中出错一定要处理
  • 数据类型不同,在隐形的转换时,一定要小心

接下来要做的事情就是根据对漏洞的理解编写一个POC来调试。这样可以加深对漏洞的理解。在开发中也能更好的引以为戒。

0x03 参考

1.OpenSSL CVE-2016-0799: heap corruption via BIO_printf

https://guidovranken.wordpress.com/2016/02/27/openssl-cve-2016-0799-heap-corruption-via-bio_printf/

PS:

这是我的学习分享博客http://turingh.github.io/

欢迎大家来探讨,不足之处还请指正。

原文地址:http://drops.wooyun.org/papers/13433

avatar

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: