一、前言

CVE-2024-37079 和 CVE-2024-37080 是两个存在于DCERPC组件中的漏洞,攻击者可以通过构造恶意请求,在远程触发 vCenter Server 上的任意代码执行。这些漏洞位于 DCE/RPC 协议中,具有 vCenter Server 网络访问权限的远程攻击者可以通过发送特制的网络数据包,在 DCERPC 协议实施过程中触发堆溢出漏洞,从而实现远程代码执行。。目前两个漏洞已报送厂商并于6月份修复完成。本文将详细讲解两个漏洞的成因。这两个漏洞已于2024年6月报送厂商并修复完成。本文将详细分析这两个漏洞的成因。

二、DCE/RPC协议介绍

DCE/RPC协议(Distributed Computing Environment / Remote Procedure Call)源于20世纪80年代,由Open Software Foundation(OSF)开发,作为开放分布式计算架构的一部分。其最初设计目的是为跨网络的异构计算机系统提供一种统一的远程过程调用(RPC)机制,从而简化不同系统之间的通信。随着网络技术的发展,DCERPC逐渐被广泛应用,特别是在Unix、Windows NT等系统中。微软在采用DCERPC协议的基础上进行了扩展,用于Windows系统中的重要服务和应用程序,尤其在Windows NT中用于SMB(Server Message Block)协议。由于微软的广泛采用,DCE/RPC成为了企业网络和操作系统中不可或缺的通信协议。

DCE/RPC协议为了实现远程过程调用,使用接口定义语言(IDL)定义接口。在IDL代码中定义了UUID,版本,接口,方法,参数,返回值以及使用到的复杂数据类型等信息。服务端和客户端通过共享这些信息完成远程过程调用的通信,其中UUID作为接口标识用于客户端向服务端发送绑定请求,建立上下文。

DCE/RPC远程过程调用通信的流程(如图一)通常如下:

  1. 客户端初始化请求:客户端向服务器发起绑定请求(Bind),选择所需的远程接口,提供接口UUID和版本号,并协商通信协议和数据传输语法(如NDR)。
  2. 服务器响应绑定:服务器接收到客户端的绑定请求后,确认是否支持请求的接口UUID、版本号和传输语法。如果支持,返回绑定确认(Bind Ack),否则返回绑定失败(Bind Nack)。
  3. 客户端发送调用请求:在成功绑定后,客户端构造远程过程调用的请求(Request)消息,包含所调用的方法及其参数,并将这些参数进行编组(序列化)。
  4. 服务器处理请求:服务器接收到请求消息后,解组请求中的参数,调用相应的本地方法进行处理。处理完成后,服务器将结果重新编组并发送响应(Response)消息。
  5. 客户端接收响应:客户端接收到响应消息后,解组数据并获取调用结果。如果调用过程中发生错误,服务器会返回故障消息(Fault),客户端需进行相应处理。
  6. 上下文修改请求:如果客户端需要更改通信中的上下文(如修改接口、更新权限或改变数据传输语法),客户端可以发送Alter Context Request,请求修改现有的上下文参数。
  7. 上下文修改响应:服务器接收到Alter Context Request后,检查请求的上下文修改是否有效。如果上下文修改成功,服务器返回Alter Context Response,确认新的上下文已生效。如果修改失败,服务器返回错误信息,客户端可以重新发送修改请求或终止会话。
  8. 客户端解除绑定:在完成所有调用后,客户端可以选择发送解除绑定请求(Unbind),通知服务器结束通信。

三、漏洞分析

2.1 CVE-2024-37079

CVE-2024-37079出现在bind认证数据包的响应处理过程中。负责处理认证的函数rpc__cn_assoc_process_auth_tlr 的函数声明如下,其中参数header_size((pres_cont_list->n_context_elem - 1) * 0x18) + 0x1c + 0x20

INTERNAL void rpc__cn_assoc_process_auth_tlr
(
  rpc_cn_assoc_p_t        assoc,
  rpc_cn_packet_p_t       req_header,
  unsigned32              req_header_size,
  rpc_cn_packet_p_t       resp_header,
  unsigned32              *header_size,
  unsigned32              *auth_len,
  rpc_cn_sec_context_p_t  *sec_context,
  boolean		  old_client,
  unsigned32              *st
)

pres_cont_list->n_context_elem 的值来源于与数据包的Num Ctx Items字段

在调用rpc__cn_assoc_process_auth_tlr之前会先由do_alter_cont_req_action_rtn 函数中对请求的Bind数据包中的Ctx Item 数量进行检测

// rpc_g_cn_large_frag_size = 0x1000
// header_size = 0x1C
result_list_len = rpc_g_cn_large_frag_size - header_size;
rpc__cn_assoc_syntax_negotiate (assoc,
                                pres_cont_list,
                                &result_list_len,
                                pres_result_list,
                                &assoc->assoc_status);
        
void rpc__cn_assoc_syntax_negotiate
(
  rpc_cn_assoc_p_t                assoc,
  rpc_cn_pres_cont_list_p_t       pres_cont_list,
  unsigned32                      *size,
  rpc_cn_pres_result_list_t       *pres_result_list,
  unsigned32                      *st
)
{
	// sizeof (rpc_cn_pres_result_list_t) = 0x1c
	// sizeof (rpc_cn_pres_result_t) = 0x18
	i = sizeof (rpc_cn_pres_result_list_t) +
        (sizeof (rpc_cn_pres_result_t) *
         (pres_cont_list->n_context_elem - 1))
	if ( i > *size )  // size = 0xFE4
  {
    *st = 0x1BEEF;
    *size = 0;
  }
  else
  {
		......
	}
	......
}

分析代码中pres_cont_list->n_context_elem的值需要同时满足以下条件:

  • 0x1C + 0x18 * (pres_cont_list->n_context_elem - 1) ≤ 0xFE4
  • 0x18 * (pres_cont_list->n_context_elem - 1) ≤ 0xFC8
  • (pres_cont_list->n_context_elem - 1) ≤ 0xA8
  • pres_cont_list->n_context_elem ≤ 0xA9

推断得出pres_cont_list→n_context_elem的最大值为0xA9,而我们发现的漏洞就恰恰是这个边界值0xA9的问题。我们继续分析,如果Bind数据包中的Num Ctx Items设置为0xA9,rpc__cn_assoc_syntax_negotiate的初步检测就会通过,认为其在允许范围内。rpc__cn_assoc_process_auth_tlr 函数的参数header_size = ((0xA9 - 1) * 0x18) + 0x1c + 0x20 = 0xFFC,在后续创建response packet时,会使用此header_size作为response header size,如下代码所示:

*header_size = ((*header_size + 3) & ~0x3);
resp_auth_tlr = (rpc_cn_auth_tlr_t *)
                ((unsigned8 *)(resp_header) + *header_size);
(void) memset(resp_auth_tlr, 0, (rpc_g_cn_large_frag_size - *header_size));
resp_auth_tlr->auth_type = req_auth_tlr->auth_type;
resp_auth_tlr->auth_level = req_auth_tlr->auth_level;
resp_auth_tlr->stub_pad_length = 0;
resp_auth_tlr->reserved = 0;
resp_auth_tlr->key_id = req_auth_tlr->key_id;
*header_size += RPC_CN_PKT_SIZEOF_COM_AUTH_TLR;  // RPC_CN_PKT_SIZEOF_COM_AUTH_TLR = 0x8
*auth_len = rpc_g_cn_large_frag_size - *header_size;
RPC_CN_AUTH_FMT_SRVR_RESP(
      sec->sec_status,
      authn_protocolc,
      sec,
      req_auth_tlr->auth_value,
      req_header->bind.hdr.common_hdr.auth_len,
      resp_auth_tlr->auth_value,
      auth_len);

可以看到中间有一行代码*header_size += RPC_CN_PKT_SIZEOF_COM_AUTH_TLR; ,如果此时的header_size为0xFFC,在加8后变为了0x1004。在下一行代码*auth_len = rpc_g_cn_large_frag_size - *header_size; 中,出现了*auth_len = 0x1000 - 0x1004; 的下溢情况,导致*auth_len 被赋值为0xFFFFFFFC。auth_len在这里起到一个约束的作用,它的值表明返回包被Ctx Items填充后还剩下多少空闲空间。在VMware DCERPC的设计里,返回包的最大大小应该是0x1000,然而auth_len 的值在此处可以被下溢变成0xFFFFFFFC,就会导致返回包的总大小小于0x1000的约束条件失效。

auth_len在后续会被传入到RPC_CN_AUTH_FMT_SRVR_RESP 函数中,该函数是一个动态调用,实际调用的函数由身份验证类型决定,其功能是将认证结果krb_message封装到响应包中。

auth_type为RPC_C_AUTHN_WINNT举例,RPC_CN_AUTH_FMT_SRVR_RESP 会动态调用rpc__ntlmauth_cn_fmt_srvr_resp,该函数定义如下:

void __fastcall rpc__ntlmauth_cn_fmt_srvr_resp(
        unsigned32 verify_st,
        rpc_cn_assoc_sec_context_p_t assoc_sec,
        rpc_cn_sec_context_p_t sec,
        pointer_t req_auth_value,
        unsigned32 req_auth_value_len,
        pointer_t auth_value,
        unsigned32 *auth_value_len)
{
  length = assoc_sec->krb_message.length;
  data = assoc_sec->krb_message.data;
  assoc_sec->krb_message.length = 0;
  assoc_sec->krb_message.data = 0LL;
  output_token.value = data;
  v10 = *auth_value_len;
  output_token.length = length;
  if ( length > v10 )
  {
    gss_release_buffer(&minor_status, &output_token, length, req_auth_value, req_auth_value_len, auth_value);
    *auth_value_len = 0;
  }
  else
  {
    *auth_value_len = length;
    memcpy(auth_value, data, length);
    gss_release_buffer(&minor_status, &output_token, v11, v12, v13, v14);
  }
}

该函数在将krb_message写入 auth_value 之前,会先检测其长度是否小于前面提到的 auth_len ,而 auth_len 可以通过下溢篡改成0xFFFFFFFC,导致这里的检测可以被绕过。在这种情况下,响应包的堆内存中实际的空闲长度为0x4,然而krb_message 长度是大于0x4的,因此在 memcpy 时就会造成堆溢出。

2.2 CVE-2024-37080

第二个漏洞(CVE-2024-37080),出现在对数据包认证数据的提取过程中。在receive_dispatch函数中,如果数据包中Auth Length字段的值非零,就会检测Auth TLR的值是否合法,检测代码如下:

auth_tlr = (rpc_cn_auth_tlr_t *) ((unsigned8 *)(pktp) +
                fragbuf_p->data_size -
                (auth_len + RPC_CN_PKT_SIZEOF_COM_AUTH_TLR));
            if ( ((unsigned8 *)(auth_tlr) < (unsigned8 *)(pktp)) ||
                ((unsigned8 *)(auth_tlr) > (unsigned8 *)(pktp) + fragbuf_p->data_size) ||
                ((unsigned8 *)(auth_tlr) + auth_len < (unsigned8 *)(pktp)) ||
                ((unsigned8 *)(auth_tlr) + auth_len > (unsigned8 *)(pktp) + fragbuf_p->data_size) )
            {
								......
                st = rpc_s_protocol_error;
                break;
            }

检测代码逻辑是首先找到auth_tlr字段的地址,其逻辑是使用整个数据包的末尾地址减去auth_len和8字节的auth_tlr头部长度。接着检测这个获取到的auth_tlr 地址是否在整个包的范围内,以及auth_tlr末尾地址是否在整个包的范围内。

这里的检测显然时不充分的,它不能保证数据包里正确地携带对应长度的auth数据,例如,数据包中可以设置 auth_len 为1,但是不携带任何auth数据也能通过这个检测。

经过上述检测后,会进入以下处理代码:

if ((ptype != RPC_C_CN_PKT_BIND) &&
                (ptype != RPC_C_CN_PKT_ALTER_CONTEXT) &&
                (ptype != RPC_C_CN_PKT_BIND_ACK) &&
                (ptype != RPC_C_CN_PKT_ALTER_CONTEXT_RESP))
            {
                rpc_authn_protocol_id_t authn_protocol = rpc_c_authn_none;

                rpc__cn_assoc_sec_lkup_by_id (assoc,
                                              key_id,
                                              &sec_context,
                                              &auth_st);

                if (auth_st == rpc_s_ok)
                {
                    authn_protocol = RPC_CN_AUTH_CVT_ID_WIRE_TO_API (auth_tlr->auth_type, &auth_st);
                }

                if (auth_st == rpc_s_ok)
                {

                    RPC_CN_AUTH_RECV_CHECK (authn_protocol,			// <----------[1]
                                            &assoc->security,
                                            sec_context,
                                            (rpc_cn_common_hdr_t *)pktp,
                                            fragbuf_p->data_size,
                                            0, /* cred_len */
                                            auth_tlr,
                                            unpack_ints,
                                            &auth_st);

在[1]处,type为schnauth时会进入函数rpc__schnauth_cn_recv_check,该函数如下:

INTERNAL void rpc__schnauth_cn_unwrap_pdu
(
    rpc_cn_assoc_sec_context_p_t    assoc_sec ATTRIBUTE_UNUSED,
    rpc_cn_sec_context_p_t          sec,
    rpc_cn_common_hdr_p_t           pdu,
    unsigned32                      pdu_len,
    unsigned32                      cred_len,
    rpc_cn_auth_tlr_p_t             auth_tlr,
    boolean32                       unpack_ints,
    unsigned32                      *st
)
{
    unsigned32 status = rpc_s_ok;
    rpc_schnauth_cn_info_p_t schn_auth;
    rpc_cn_schnauth_tlr_p_t schn_tlr;
    unsigned32 sec_level;
    struct schn_auth_ctx *sec_ctx;
    struct schn_blob input_token, output_token;
    struct schn_tail tail;
    unsigned16 auth_len = 0;

    schn_auth = (rpc_schnauth_cn_info_p_t)sec->sec_cn_info;
    schn_tlr = (rpc_cn_schnauth_tlr_p_t)auth_tlr->auth_value;

    switch (auth_tlr->auth_level) {
    case rpc_c_protect_level_pkt_integ:
        sec_level = SCHANNEL_SEC_LEVEL_INTEGRITY;
	break;

    case rpc_c_protect_level_pkt_privacy:
        sec_level = SCHANNEL_SEC_LEVEL_PRIVACY;
	break;

    default:
	*st = rpc_s_unsupported_protect_level;
	return;
    }

    sec_ctx = &schn_auth->sec_ctx;

    auth_len = RPC_CN_PKT_AUTH_LEN((rpc_cn_packet_p_t)pdu);
    if (unpack_ints) {
        SWAB_INPLACE_16(auth_len);
    }

    input_token.base = (uint8*)(pdu) + RPC_CN_PKT_SIZEOF_RESP_HDR;
    input_token.len  = pdu_len - (RPC_CN_PKT_SIZEOF_RESP_HDR +		// <-------[2]
				  RPC_CN_PKT_SIZEOF_COM_AUTH_TLR +
				  auth_len);

    output_token.base = NULL;
    output_token.len  = 0;

    memcpy(tail.signature,   schn_tlr->signature,  8);
    memcpy(tail.seq_number,  schn_tlr->seq_number, 8);
    memcpy(tail.digest,      schn_tlr->digest,     8);
    memcpy(tail.nonce,       schn_tlr->nonce,      8);

    status = schn_unwrap(sec_ctx, sec_level, &input_token, &output_token,
			 &tail);

    memcpy(input_token.base, output_token.base, output_token.len);
    schn_free_blob(&output_token);

    *st = status;
}

注意看[2]处代码中input_token.len的赋值来源,可以理解为input_token.len = frag->data_size - (resp_header + auth_tlr_header + auth_len)。将其中的常量简化一下,就是input_token.len = frag->data_size - (24 + 8 + auth_len)。根据前面的分析,当前对数据包的检查并不能保证 auth_lenauth内容相匹配,这里存在一种情况,如果发送的数据包不包含auth内容,header大小小于24,并在 auth_len字段中写入值,这里就会产生整数溢出,导致在接下来的拷贝时造成堆溢出。

四、总结

在本文中,我们深入探讨了这两个关键漏洞的成因,这两个漏洞分别展示了在软件开发和系统设计中常见的安全挑战。

文中两个漏洞的成因都揭示了在编写和审查代码时,对输入验证和边界检查的重要性。开发人员必须时刻警惕潜在的安全风险,采取适当的防御措施,如使用安全的编程实践、进行代码审查和安全测试,以及及时更新和修补已知漏洞。通过这些努力,我们可以显著降低软件系统遭受攻击的风险,确保用户数据的安全和系统的稳定性。