관련 글과 코드들은 아래 링크에서 확인하실 수 있습니다.
https://github.com/wh1te4ever/xnu_1day_practice/blob/main/CVE-2020-3836
버그 알아보기
해당 버그는 주어진 사용자 공간의 mach port name을 제공하면, 커널 공간의 ipc_port 구조체 주소를 유출시킬 수 있는 버그가 존재한다. 아주 오래된 xnu-123.5 버전에도 존재하는 버그이며, 약 20년 이상 유지되었다.
IOKit 드라이버들은 자주 콜백 메커니즘을 사용하며, 보통 mach 포트를 중심으로 구현된다.
적어도 상당수 드라이버들이 사용하는 IOUserClient
에서 제공하는 기능 중 하나는 OSAsyncReference64
함수이다. 해당 타입 선언은 다음과 같다.
// --------------
enum {
kOSAsyncRef64Count = 8,
kOSAsyncRef64Size = kOSAsyncRef64Count * ((int) sizeof(io_user_reference_t))
};
typedef io_user_reference_t OSAsyncReference64[kOSAsyncRef64Count];
io_user_reference_t
는 uint64_t
타입이지만, 해당 값은 사용자 공간에서 가져온 것을 의미한다. 이 기능을 사용하고자 하는 드라이버는 위의 구조체를 직접 만들거나 IOUserClient::setAsyncReference64
를 호출할 수 있다. 사실 그 구현 자체는 크게 중요하지 않고, 진짜 중요한 것은 유저랜드로 다시 전송되는 메시지가 어떻게 구성되느냐인데, 이는 IOUserClient::_sendAsyncResult64
에 의해 이루어진다.
여기서 버그를 발견할 수 있는데, 주요 부분을 “XXX BELOW HERE …”로 표시해놓았다. 자세히 살펴보자.
IOReturn
IOUserClient::_sendAsyncResult64(OSAsyncReference64 reference,
IOReturn result, io_user_reference_t args[], UInt32 numArgs, IOOptionBits options)
{
struct ReplyMsg {
mach_msg_header_t msgHdr;
union{
struct{
OSNotificationHeader notifyHdr;
IOAsyncCompletionContent asyncContent;
uint32_t args[kMaxAsyncArgs];
} msg32;
struct{
OSNotificationHeader64 notifyHdr;
IOAsyncCompletionContent asyncContent;
io_user_reference_t args[kMaxAsyncArgs] __attribute__ ((packed));
} msg64;
} m;
};
ReplyMsg replyMsg;
mach_port_t replyPort;
kern_return_t kr;
// If no reply port, do nothing.
// **XXX BELOW HERE 1**
**replyPort = (mach_port_t) (reference[0] & ~kIOUCAsync0Flags);**
if (replyPort == MACH_PORT_NULL) {
return kIOReturnSuccess;
}
if (numArgs > kMaxAsyncArgs) {
return kIOReturnMessageTooLarge;
}
bzero(&replyMsg, sizeof(replyMsg));
replyMsg.msgHdr.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND /*remote*/,
0 /*local*/);
replyMsg.msgHdr.msgh_remote_port = replyPort;
replyMsg.msgHdr.msgh_local_port = NULL;
replyMsg.msgHdr.msgh_id = kOSNotificationMessageID;
if (kIOUCAsync64Flag & reference[0]) {
replyMsg.msgHdr.msgh_size =
sizeof(replyMsg.msgHdr) + sizeof(replyMsg.m.msg64)
- (kMaxAsyncArgs - numArgs) * sizeof(io_user_reference_t);
replyMsg.m.msg64.notifyHdr.size = sizeof(IOAsyncCompletionContent)
+ numArgs * sizeof(io_user_reference_t);
replyMsg.m.msg64.notifyHdr.type = kIOAsyncCompletionNotificationType;
**// XXX BELOW HERE 2
bcopy(reference, replyMsg.m.msg64.notifyHdr.reference, sizeof(OSAsyncReference64));**
replyMsg.m.msg64.asyncContent.result = result;
if (numArgs) {
bcopy(args, replyMsg.m.msg64.args, numArgs * sizeof(io_user_reference_t));
}
} else {
...
}
...
return kr;
}
해당 IOUserClient::_sendAsyncResult64
함수는 OSAsyncReference64
를 받아 그걸 비롯한 여러 값을 mach 메시지로 유저랜드에 보낸다.
그런데 여기서 OSAsyncReference64
의 첫 번째 요소가 mach_port_t
, 즉 메시지를 보낼 mach 포트이다.
replyPort = (mach_port_t) (reference[0] & ~kIOUCAsync0Flags);
여기서의 mach_port_t
는 그냥 커널에서의 생짜 포인터인데,
커널 포인터를 담은 구조체를 전달해서 그걸 그대로 유저랜드로 보내기 때문에 릭이 발생한다.
bcopy(reference, replyMsg.m.msg64.notifyHdr.reference, sizeof(OSAsyncReference64));
트리거 방법
Siguza님꼐서 게시한 블로그 글에서는 다음과 같았다.
일반적인 IOSurface 초기화(유저클라이언트와 서피스 생성)가 끝난 뒤에는, 해야 할 일은 단지:
setNotify
(외부 메서드 17)를 호출하면서 비동기 함수 중 하나와 mach 포트를 함께 전달합니다.incrementUseCount
를 호출한 뒤 이어서decrementUseCount
(각각 메서드 14와 15)를 호출합니다 — 이들이 원래 무슨 용도로 만들어진 건지는 모르겠지만, 이 카운트가 0에 도달하면 유저랜드로 메시지가 전송됩니다.- 자신의 mach 포트에서 메시지를 수신하고, 공짜로 얻은 커널 포인터를 즐기면 됩니다.
IOUserClient::_sendAsyncResult64 함수까지 어떻게 도달할 수 있었는지 궁금해서 PoC 코드를 실행시켜봤다. 그 결과, IOSurfaceRootUserClient::decrement_surface_use_count
함수를 통해 도달 가능한것을 확인하였다. 여기서부터 거슬로 올라가보면서 분석해보겠다.
- kernel의
IOUserClient::_sendAsyncResult64
함수 중mach_msg_send_from_kernel_proper
call하는 곳에 브포걸었을 때의 BackTrace
(lldb) bt
* thread #2, stop reason = breakpoint 20.1
* frame #0: 0xffffff800746bbeb kernel`IOUserClient::_sendAsyncResult64(reference=0xffffff801875f3d0, result=0, args=<unavailable>, numArgs=2, options=0) at IOUserClient.cpp:2108:8 [opt]
frame #1: 0xffffff7f89344929 <- IOSurface`IOSurfaceRootUserClient::notify_surface+0x30
frame #2: 0xffffff7f89340ce0 <- IOSurface`IOSurfaceRoot::notifySurface+0x3C
frame #3: 0xffffff7f89343a19 IOSurface`IOSurfaceRootUserClient::decrement_surface_use_count+0x67
frame #4: 0xffffff8007466fcb kernel`IOUserClient::externalMethod(this=<unavailable>, selector=<unavailable>, args=<unavailable>, dispatch=<unavailable>, target=<unavailable>, reference=<unavailable>) at IOUserClient.cpp:5888:10 [opt]
frame #5: 0xffffff8007470083 kernel`::is_io_connect_method(connection=0xffffff8015f73610, selector=15, scalar_input=<unavailable>, scalar_inputCnt=<unavailable>, inband_input=<unavailable>, inband_inputCnt=0, ool_input=0, ool_input_size=0, inband_output="", inband_outputCnt=0xffffff80188b460c, scalar_output=0xffffff90a48b3d00, scalar_outputCnt=0xffffff90a48b3cfc, ool_output=0, ool_output_size=0xffffff80157551a8) at IOUserClient.cpp:4495:16 [opt]
frame #6: 0xffffff8006e22c22 kernel`_Xio_connect_method(InHeadP=<unavailable>, OutHeadP=0xffffff80188b45e0) at device_server.c:8389:18 [opt]
frame #7: 0xffffff8006d41998 kernel`ipc_kobject_server(request=0xffffff80157550e0, option=3) at ipc_kobject.c:389:4 [opt]
frame #8: 0xffffff8006d18625 kernel`ipc_kmsg_send(kmsg=0xffffff80157550e0, option=3, send_timeout=0) at ipc_kmsg.c:1937:10 [opt]
frame #9: 0xffffff8006d2f0d5 kernel`mach_msg_overwrite_trap(args=<unavailable>) at mach_msg.c:553:8 [opt]
frame #10: 0xffffff8006e4b485 kernel`mach_call_munger64(state=0xffffff80141abaa0) at bsd_i386.c:618:24 [opt]
frame #11: 0xffffff8006ce3226 kernel`hndl_mach_scall64 + 22
(lldb)
IOSurface::decrement_use_count
함수는 IOSurfaceRoot::notifySurface
함수를 호출시킬 수 있는 래퍼이며,
void __fastcall IOSurface::decrement_use_count(IOSurface *this)
{
if ( OSDecrementAtomic((SInt32 *)(this->field_C0 + 20LL)) == 1 )
{
if ( this->field_25 )
IOSurface::purge(this);
IOSurfaceRoot::notifySurface((IORecursiveLock **)this->field_28, 0, this);
}
}
트리거하기 위해서는 저 v4 변수값이 1을 반환시켜야 한다.
v4를 지정해주는 곳은 IOSurfaceClient::decrement_use_count
함수에 있으며,
__int64 __fastcall IOSurfaceRootUserClient::decrement_surface_use_count(IOSurfaceRootUserClient *this, unsigned int a2)
{
IOSurfaceClient *v2; // r15
IOSurface *IOSurface; // r14
bool v4; // r15
IOLockLock((IOLock *)this->m_lock);
if ( LODWORD(this->i_surfaceClientCapacity) > a2 && (v2 = this->m_IOSurfaceClientArrayPointer[a2]) != 0 )
{
IOSurface = v2->IOSurface;
IOSurface->struct_19F28_vtable->__ZNK9IOSurface6retainEv(IOSurface);
v4 = IOSurfaceClient::decrement_use_count(v2);
IOLockUnlock((IOLock *)this->m_lock);
if ( v4 ) // 0x1a198 = vtable
IOSurface->struct_19F28_vtable->__ZN9IOSurface19decrement_use_countEv(IOSurface);
IOSurface->struct_19F28_vtable->__ZN9IOSurface7releaseEv(IOSurface);
return 0;
}
else
{
IOLockUnlock((IOLock *)this->m_lock);
return 3758097090LL;
}
}
이 경우, +0x24 오프셋을 use_count 필드값으로 추측할 수 있는데, use_count 필드가 감소 전 1이여야 된다.
그래서 decrementUseCount
호출전에 use_count 필드값을 1로 만들기 위해
먼저incrementUseCount
함수를 호출해둘 필요가 있다는 것이다.
bool __fastcall IOSurfaceClient::decrement_use_count(void *this)
{
int v1; // eax
v1 = *((_DWORD *)this + 0x24);
if ( v1 )
*((_DWORD *)this + 0x24) = v1 - 1;
return v1 == 1;
}
각각의 호출번호는 다음과 같고
- 14:
IOSurfaceRootUserClient::increment_surface_use_count
- 15:
IOSurfaceRootUserClient::decrement_surface_use_count
아래는 incrementUseCount
관련 함수 코드이다.
- IOSurfaceRootUserClient::increment_surface_use_count
__int64 __fastcall IOSurfaceRootUserClient::increment_surface_use_count(IOSurfaceRootUserClient *this, unsigned int a2)
{
IOSurfaceClient *v2; // r15
IOSurface *IOSurface; // r14
bool v4; // r15
IOLockLock((IOLock *)this->m_lock);
if ( LODWORD(this->i_surfaceClientCapacity) > a2 && (v2 = this->m_IOSurfaceClientArrayPointer[a2]) != 0 )
{
IOSurface = v2->IOSurface;
IOSurface->struct_19F28_vtable->__ZNK9IOSurface6retainEv(IOSurface);
v4 = IOSurfaceClient::increment_use_count(v2);
IOLockUnlock((IOLock *)this->m_lock);
if ( v4 )
IOSurface->struct_19F28_vtable->__ZN9IOSurface19increment_use_countEv(IOSurface);
IOSurface->struct_19F28_vtable->__ZN9IOSurface7releaseEv(IOSurface);
return 0;
}
else
{
IOLockUnlock((IOLock *)this->m_lock);
return 0xE00002C2LL;
}
}
IOSurfaceRoot::setSurfaceNotify
incrementUseCount
함수를 호출하기 전에는 사용자 공간의 mach 포트와 함께 전달시켜주어야 한다.
호출 번호는 17에 해당한다.
__int64 __fastcall IOSurfaceRoot::setSurfaceNotify(
__int64 a1,
mach_port_t *a2,
mach_vm_address_t *a3,
IOSurfaceRootUserClient *a4)
{
__int64 **v6; // rax
_QWORD *v7; // rax
_QWORD *v8; // r12
_QWORD *v9; // r13
__int64 v10; // rax
_QWORD *v11; // rax
unsigned int v12; // ebx
IORecursiveLockLock(*(IORecursiveLock **)(a1 + 256));
v6 = *(__int64 ***)(a1 + 336);
if ( v6 )
{
while ( v6[4] != (__int64 *)a3[1] || v6[11] != (__int64 *)a4 )
{
v6 = (__int64 **)*v6;
if ( !v6 )
goto LABEL_5;
}
v12 = -536870199;
}
else
{
LABEL_5:
v7 = IOMalloc(0x60u);
if ( v7 )
{
v8 = v7;
v9 = (_QWORD *)(a1 + 336);
memset(v7, 0, 0x60u);
IOUserClient::setAsyncReference64(v8 + 2, *a2, *a3, a3[1]);
v8[11] = a4;
v8[10] = a3[2];
v10 = *(_QWORD *)(a1 + 336);
if ( v10 )
{
if ( *(_QWORD **)(v10 + 8) != v9 )
IOSurfaceRoot::setSurfaceNotify((const void *)(a1 + 336));// panic
*v8 = v10;
v11 = (_QWORD *)(v10 + 8);
}
else
{
*v8 = 0;
v11 = (_QWORD *)(a1 + 344);
}
*v11 = v8;
*v9 = v8;
v8[1] = v9;
v12 = 0;
}
else
{
v12 = -536870211;
}
}
IORecursiveLockUnlock(*(IORecursiveLock **)(a1 + 256));
return v12;
}
Result
seo@seos-iMac-Pro CVE-2020-3836 % ./leak
[*] page size: 0x1000, kr=(os/kern) successful
[*] IOSurface_init success, IOSurface_id=0x2
[*] port: 0x2703, (os/kern) successful
[*] leaked_port_addr: 0xffffff80173028b0
...
(lldb) p/x *(ipc_port_t)0xffffff80173028b0
(ipc_port) $36 = {
ip_object = {
io_bits = 0x80000000
io_references = 0x00000002
io_lock_data = (interlock = 0x0000000000000000)
}
ip_messages = {
data = {
port = {
waitq = {
waitq_type = 0x00000001
waitq_fifo = 0x00000001
waitq_prepost = 0x00000000
waitq_irq = 0x00000000
waitq_isvalid = 0x00000001
waitq_turnstile = 0x00000000
waitq_eventmask = 0x00000000
waitq_interlock = (lock_data = 0x0000000000000000)
waitq_set_id = 0x0000000000000000
waitq_prepost_id = 0x0000000000000000
= {
waitq_queue = {
next = NULL
prev = NULL
}
waitq_prio_queue = (pq_root_packed = 0x0000000000000000)
= {
waitq_ts = NULL
waitq_tspriv = 0x0000000000000000
}
}
}
messages = {
ikmq_base = NULL
}
seqno = 0x00000001
receiver_name = 0x00002703
msgcount = 0x0000
qlimit = 0x0005
qcontext = 0x00000000
}
pset = {
setq = {
wqset_q = {
waitq_type = 0x00000001
waitq_fifo = 0x00000001
waitq_prepost = 0x00000000
waitq_irq = 0x00000000
waitq_isvalid = 0x00000001
waitq_turnstile = 0x00000000
waitq_eventmask = 0x00000000
waitq_interlock = (lock_data = 0x0000000000000000)
waitq_set_id = 0x0000000000000000
waitq_prepost_id = 0x0000000000000000
= {
waitq_queue = {
next = NULL
prev = NULL
}
waitq_prio_queue = (pq_root_packed = 0x0000000000000000)
= {
waitq_ts = NULL
waitq_tspriv = 0x0000000000000000
}
}
}
wqset_id = 0x0000000000000000
= (wqset_prepost_id = 0x0000270300000001, wqset_prepost_hook = 0x0000270300000001)
}
}
}
= {
imq_klist = {
slh_first = NULL
}
imq_inheritor_knote = NULL
imq_inheritor_turnstile = NULL
imq_inheritor_thread_ref = NULL
imq_srp_owner_thread = NULL
}
}
data = {
receiver = 0xffffff801286fb40
destination = 0xffffff801286fb40
timestamp = 0x1286fb40
}
kdata = {
kobject = 0x0000000000000000
imp_task = NULL
sync_inheritor_port = NULL
sync_inheritor_knote = NULL
sync_inheritor_ts = NULL
}
ip_nsrequest = NULL
ip_pdrequest = NULL
ip_requests = NULL
kdata2 = {
premsg = NULL
send_turnstile = NULL
}
ip_context = 0x0000000000000000
ip_sprequests = 0x00000000
ip_spimportant = 0x00000000
ip_impdonation = 0x00000000
ip_tempowner = 0x00000000
ip_guarded = 0x00000000
ip_strict_guard = 0x00000000
ip_specialreply = 0x00000000
ip_sync_link_state = 0x00000000
ip_sync_bootstrap_checkin = 0x00000000
ip_immovable_receive = 0x00000000
ip_no_grant = 0x00000000
ip_immovable_send = 0x00000000
ip_impcount = 0x00000000
ip_mscount = 0x00000001
ip_srights = 0x00000001
ip_sorights = 0x00000000
}
참고한 자료 및 출처
Exploit Code
-
https://github.com/jakeajames/time_waste/blob/master/time_waste/exploit.c#L93
-
https://github.com/staturnzz/chimera_patch/blob/main/src/time_saved.c#L36
Writeup