首页 -> 安全研究

安全研究

绿盟月刊
绿盟安全月刊->第36期->技术专题
期刊号: 类型: 关键词:
Apache+OpenSSL远程缓冲区溢出机理分析

作者:yozhang <tomjoin00@163.com>
日期:2002-11-01

1.    简介
前一阶段,网上针对Apache+OpenSSL的远程攻击的讨论比较多,而且国内有新的蠕虫出现,其利用的就是该ssl漏洞。就溢出代码而言,流传比较广的是Solar Eclipse编写的openssl-too-open..c,该程序可以对低于0.9.6d版本的OpenSSL进行KEY_ARG溢出攻击,从而获取远程系统中的apache用户权限。下面就从实现机理的角度对其攻击实现过程进行分析。
要实现KEY_ARG缓冲区溢出,就需要理解SSL2建立连接的握手过程。下面是连接建立示意图:

       客户端                  服务器端

     CLIENT_HELLO -->
                       <-- SERVER_HELLO
CLIENT_MASTER_KEY -->
                       <-- SERVER_VERIFY
  CLIENT_FINISHED -->
                       <-- SERVER_FINISHED

CLIENT_HELLO消息中包含客户端支持的密码列表,会话id和查询数据。如果客户端想重用一个己经建立的会话,那就使用会话id,否则就令该id为空。
服务器端返回一个SERVER_HELLO的消息,里面列出了所有支持的密码,同时还包含使用自身RSA公钥加密的认证。与客户端相类似,服务器端也发回一个连接id,这个id将会被客户端用来确认加密是否工作。
客户端产生一个随机的master key,然后用服务器的公钥对其加密,并将其放入CLIENT_MASTER_KEY消息中,最后发送给服务器端。该消息中同时包含由客户端选择的密码和一个KEY_ARG域。
现在客户端和服务器端都有master key,并且都可以从中产生会话密钥。这样,以后所有的信息都可以实现加密传输。
服务器返回一个SERVER_VERIFY的消息,里面包含来自CLIENT_HELLO消息的查询数据。如果密钥交换成功,客户端就可以解密该消息,由服务器返回的查询数据将会与客户端发送的查询数据进行匹配,以判断加密是否成功。
客户端发送一个CLIENT_FINISHED的消息,其中包含来自SERVER_HELLO的连接id。服务器接收到该消息后,对其进行解密,并检查由客户端发来的连接id是否与服务器发送的连接id相匹配。
最后,服务器发送一个SERVER_FINISHED消息,完成握手连接过程。该消息中包含由服务器产生的一个会话id。如果客户端想重用这个会话,就可以在下次连接建立中使用CLIENT_HELLO消息来发送该id。

2.KEY_ARG缓冲区溢出
这个bug存在于ssl/s2_srvr.c中的get_client_master_key()函数中。该函数读取CLIENT_MASTER_KEY分组并对它进行处理。它从客户端读取KEY_ARG_LENGTH值,并将其完整地拷贝到一个固定大小的数组中。这个数组是SSL_SESSION结构的一部分。如果客户端发送一个长于8个字节的KEY_ARG,那么在SSL_SESSION结构中的其他变量就会被用户提供的数据所覆盖。
下面就是SSL_SESSION结构的定义:
typedef struct ssl_session_st
    {
    int ssl_version;    /* ssl版本信息 */

    /* 下面定义的变量仅用于SSLv2 */
    unsigned int key_arg_length;
    unsigned char key_arg[SSL_MAX_KEY_ARG_LENGTH];
    int master_key_length;
    unsigned char master_key[SSL_MAX_MASTER_KEY_LENGTH];
    /* session_id */
    unsigned int session_id_length;
    unsigned char session_id[SSL_MAX_SSL_SESSION_ID_LENGTH];
    /* 用于判断会话是否被重用*/
    unsigned int sid_ctx_length;
    unsigned char sid_ctx[SSL_MAX_SID_CTX_LENGTH];

    int not_resumable;

    /* cert用于验证连接是否建立*/
    struct sess_cert_st  *sess_cert;
    long verify_result; /* 仅用于服务器 */

    int references;
    long timeout;
    long time;

    SSL_CIPHER *cipher;
    unsigned long cipher_id;  /* 当ASN.1被加载时,需要使用该变量加载cipher结构*/

    STACK_OF(SSL_CIPHER) *ciphers; /* 共用的ciphers */

    CRYPTO_EX_DATA ex_data;

    /* 双向链表用于高效地删除会话id,以获取最大数量的cache*/
    struct ssl_session_st *prev,*next;
    } SSL_SESSION;

从中我们可以得到该结构的大小,而且因为它是在堆栈中取得分配的空间,因此如果我们将下一个内存空间块覆盖,然后使得溢出代码在SSL_SESSION结构中调用free(),就可以获得普通用户权限。
当我们发送了一个CLIENT_MASTER_KEY消息后,我们将接收到来自服务器端的一个SERVER_VERIFY报文,然后可以使用CLIENT_FINISHED消息来回应。服务器使用该消息的内容来验证密钥是否交换成功。如果返回一个错误的连接id,服务器将会放弃连接并释放SSL_SESSION结构,而正是我们所需要的。
我们可以使用8字节长的随机字符串覆盖KEY_ARG数组,其结构如下:

unsigned char overwrite_next_chunk[] =
    "AAAA"                              /* int master_key_length; */
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"  /* unsigned char master_key[SSL_MAX_MASTER_KEY_LENGTH]; */
    "AAAA"                              /* unsigned int session_id_length; */
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"  /* unsigned char session_id[SSL_MAX_SSL_SESSION_ID_LENGTH]; */
    "AAAA"                              /* unsigned int sid_ctx_length; */
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"  /* unsigned char sid_ctx[SSL_MAX_SID_CTX_LENGTH]; */
    "AAAA"                              /* unsigned int sid_ctx_length; */
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"  /* unsigned char sid_ctx[SSL_MAX_SID_CTX_LENGTH]; */
    "AAAA"                              /* int not_resumable; */
    "\x00\x00\x00\x00"                  /* struct sess_cert_st *sess_cert; */
    "\x00\x00\x00\x00"                  /* X509 *peer; */
    "AAAA"                              /* long verify_result; */
    "\x01\x00\x00\x00"                  /* int references; */
    "AAAA"                              /* int timeout; */
    "AAAA"                              /* int time */  
    "AAAA"                              /* int compress_meth; */
    "\x00\x00\x00\x00"                  /* SSL_CIPHER *cipher; */
    "AAAA"                              /* unsigned long cipher_id; */
    "\x00\x00\x00\x00"                  /* STACK_OF(SSL_CIPHER) *ciphers; */
    "\x00\x00\x00\x00\x00\x00\x00\x00"  /* CRYPTO_EX_DATA ex_data; */
    "AAAAAAAA"                          /* struct ssl_session_st *prev,*next; */
    "\x00\x00\x00\x00"                  /* Size of previous chunk */
    "\x11\x00\x00\x00"                  /* Size of chunk, in bytes */
    "fdfd"                              /* Forward and back pointers */
    "bkbk"
    "\x10\x00\x00\x00"                  /* Size of previous chunk */
    "\x10\x00\x00\x00"                  /* Size of chunk, PREV_INUSE is set */

其中的A字符并不会影响OpenSSL的控制流,因此必须使用其它的字节来完成溢出工作。例如,peer和sess_cert的指针必须为NULL,因为在释放SSL_SESSION结构之前,SSL中的清除代码将在调用free()函数时使用到它们。
Free()调用将把bk指针的值写入到内存地址的fd+12字节处(见上,fd和bk分别为前向和后向指针)。我们可以将shellcode的地址放入bk指针中,那么我们就可以将其写到GOT表中的free()表项中。关于GOT表的内容可以参考其它文章,这里不再讨论。
3.获取shellcode地址
要实现溢出,我们需要有一个地方来存放shellcode,并需要知道它们的确切地址。实现方法就是使用SERVER_FINISHED消息。因为这个消息中包括会话id,而该id可以从SSL_SESSION结构中读取。由于服务器需要从session_id[]数组中读取session_id_length的字节数,并把它们发送给客户端,所以我们可以重写这个session_id_length变量来完成溢出连接的建立。如果session_id_length足够长,SERVER_FINISHED消息将包含SSL_SESSION结构的内容。
要获取session结构的内容,我们就要用8字节的随机字符串覆盖KEY_ARG数组,字符串结构如下:
unsigned char overwrite_session_id_length[] =
    "AAAA"                              /* int master_key_length; */
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"  /* unsigned char master_key[SSL_MAX_MASTER_KEY_LENGTH]; */
    "\x70\x00\x00\x00";                 /* unsigned int session_id_length; */
现在来看一下当我们发送完连接请求之后堆栈的状态。一般情况下,堆栈里面包含已分配的内存块和一个大的top空闲内存块,该top块覆盖了所有的空闲内存空间,所有申请地内存块都要从top空闲块中获取。当服务器收到了连接请求,它就产生一个子进程,该进程给SSL_SESSION结构分配空间。
下一个分配的内存块是一个包含STACK_OF(SSL_CIPHER)结构的16字节大小的块。该内存块也是从top空闲块首部开始分配,因此它正好位于SSL_SESSION结构之上。该块的地址存储在session->ciphers变量之中。产生的内存结构如下:

                    | top chunk  |
                    |------------|
session->ciphers    |  16 bytes  | <- STACK_OF(SSL_CIPHER)结构
指针位置   ->       |------------|
                    | 368 bytes  | <- SSL_SESSION 结构
                    |------------|

我们可以在SERVER_FINISHED消息中读取SSL_SESSION结构中的session->ciphers指针,将其值减368,就是SSL_SESSION结构的地址,这也就是我们要溢出的地址。
4.Fork()的使用
我们将使用缓冲区溢出获取shellcode的地址,并覆盖写入mallco块,但在将其发送给服务器之前,我们需要知道shellcode的地址。解决方法就是发送2个请求,第一个请求覆写session_id_length,通过完成握手连接获得SERVER_FINISHED消息,然后我们调整shellcode代码并打开第二个连接用来发送溢出代码。
如果服务器使用的是Apache,那么一般情况下,这两次连接产生的子进程就会有完全相同的内存布局,并且malloc()会把会话结构放在相同的位置。当然,并不能保证总是这样。因为Apache子进程可以处理多个请求,而这会改变我们使用的两个子进程的内存分配模式。
要保证是两个完全不同的子进程,我们就需要在发送两个攻击连接之前请求足够多的连接以消耗完Apache所有守护子进程,迫使其产生新的子进程来接受连接请求。注意:如果网络繁忙或内存分配模式不同,甚至是GOT地址错误都会导致远处溢出失败。
关于openssl溢出命令的使用我就不再介绍了,大家可以自己试一试。最后,感谢ruby、caiworm和dany9对我生活和工作上的支持。
    
                                                                作者:yozhang
2002.10.21

参考文献
1.    Openssl-too-open.c,Solar Eclipse
2.    http://community.core-sdi.com/~juliano/
3.    谢希仁著.计算机网络.电子工业出版社
版权所有,未经许可,不得转载