콘텐츠로 건너뛰기

[실습] CVE-2020-3836(cuck00) 이해하기

관련 글과 코드들은 아래 링크에서 확인하실 수 있습니다.

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 함수이다. 해당 타입 선언은 다음과 같다.

https://github.com/apple-oss-distributions/xnu/blob/xnu-6153.61.1/iokit/IOKit/OSMessageNotification.h#L92

// --------------
enum {
	kOSAsyncRef64Count  = 8,
	kOSAsyncRef64Size   = kOSAsyncRef64Count * ((int) sizeof(io_user_reference_t))
};
typedef io_user_reference_t OSAsyncReference64[kOSAsyncRef64Count];

io_user_reference_tuint64_t타입이지만, 해당 값은 사용자 공간에서 가져온 것을 의미한다. 이 기능을 사용하고자 하는 드라이버는 위의 구조체를 직접 만들거나 IOUserClient::setAsyncReference64를 호출할 수 있다. 사실 그 구현 자체는 크게 중요하지 않고, 진짜 중요한 것은 유저랜드로 다시 전송되는 메시지가 어떻게 구성되느냐인데, 이는 IOUserClient::_sendAsyncResult64에 의해 이루어진다.

여기서 버그를 발견할 수 있는데, 주요 부분을 “XXX BELOW HERE …”로 표시해놓았다. 자세히 살펴보자.

https://github.com/apple-oss-distributions/xnu/blob/xnu-6153.61.1/iokit/Kernel/IOUserClient.cpp#L2030

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

Writeup