블로그 이전했습니다. https://jeongzero.oopy.io/
CVE-2018-3295 분석(1)
본문 바로가기
보안/원데이 분석

CVE-2018-3295 분석(1)

728x90


1. Vulnerable software


  • VirtualBox 5.2.20 and prior versions.
  • ubuntu 18.04 ⇒ (guestOS는 아무거나. 본인은 이걸 사용)
  • virtualbox 환경설정 ⇒ 초기 설치시, 기본환경 그대로.

2. Virtualbox build


Virtualbox Escape(1)
19년 7월 24일, BOB 8기 취약점 트랙에서 " Virtualization Bug "라는 주제로, 강의를 들었다. 과제가 나온지 대략 1년만에 해당 과제를 진행하였다. 그 당시 매우 좁밥이였던 터라 강의내용을 이해하는 것도 벅찼다. 물론 지금도 좁밥이지만ㅋ 그래도 꼭 시간날때 기초쌓고 혼자 해보려고 노력했고 드디어 했다.
https://wogh8732.tistory.com/273?category=804777

전에 빌드방식을 그대로 이용

3. 분석


POC 자료

3.1 개요


버박을 처음 설치하고, guestOS를 올리면 다음과 같이 초기설정이 되어있다. 여기서 더 건들일건 없다. 해당 취약점은 Intel Pro/1000 MT Desktop 네트워크 어댑터 (E1000라고 부름) 에서 발생하는 취약점이다.

E1000 이라는 네트워크 어댑터에서 발생하는 취약점인걸 알았으니, 네트워크 어댑터가 어떻게 이용되는지 간단히 알아보았다. (NIC 와 드라이버의 통신에 대해)

3.2 드라이버와 NIC 통신 - 패킷 전송


드라이버가 상위 레이어로부터 패킷을 받는다. 해당 패킷이 최종적으로 NIC를 통해 목적지까지 도달해야한다. 따라서 NIC가 이해할수 있는 형식으로 보내기 위해 transmit descriptor 를 생성한다.

transmit descriptor 에는 패킷 사이즈, 메모리 주소 등의 정보가 들어가게된다. 여기서 말하는 메모리 주소는 NIC가 실제 패킷이 들어있는 물리적 주소에 접근해야하므로 반드시 필요하다. 따라서 드라이버가 패킷의 가상주소가 실제 물리주소로 변경되어 transmit descriptor 에 들어가고, Tx ring에 이를 추가한다.(1)

그후, CPU가 NIC에게 새로운 패킷 전송 요청(NIC의 레지스터에 쓸 새로운 descriptor)이 있다고 알린다. (알린다는 의미는 실제로 PIO 방식으로 CPU 가 직접 NIC의 특정 주소에 데이터를 써서 알리는 형식이다)(2)

PIO : Programmed I/O ⇒ CPU가 직접 Input, Output을 처리함

NIC가 CPU로부터 연락을 받으면, 이제 처리를 해주기위한 세팅을 시작한다. 우선 메모리에 들어있는, Tx ring의 Transmit descriptor 를 가져온다. 이는 CPU의 개입 없이 직접 디바이스가 메모리에 접근해서 가져오는 방식을 취한다.⇒DMA(3)

NIC가 Transmit descriptor 를 가져와서 실제 패킷 주소(아까 말한 가상주소→물리적주소)와 크기 등을 확인하고, 해당 주소에서 실제 패킷을 가져온다(4)

최종적으로 NIC는 이제 실제 패킷을 전송한다.(5) 그 후 패킷을 몇개 전송했는지의 정보를 메모리에 기록하고(6) 인터럽트를 발생시킨다(7). 그러면 드라이버는 발생된 인터럽트를 통해 현재 전송된 패킷 수를 읽어서 전송된 패킷을 반환하다.

여기까지 간단히 패킷 송신 과정을 살펴보았다. 수신 과정도 이와 비슷한 로직으로 수행된다.

패킷 송신과정에서 중요한 부분을 간단히 나타내면 다음과 같다

패킷 송신과정을 처음에 살펴본 이유는 위 사진에서 Tx ring 버퍼를 이용해야하기 때문이다.

82540EM 데이터 시트를 보면, E1000 어댑터 구현을 위한 개발자를 위한 가이드 내용을 찾을수 있다

https://pdos.csail.mit.edu/6.828/2019/readings/hardware/8254x_GBe_SDM.pdf

저 방대한 내용을 다 아는건 사실상 불가능하고, 필요한 부분만 찾아서 확인하고 있다.

3.2 필수 지식


아까 송신 과정에서 Tx desciptor 가 Tx ring에 써진다고 했다. 위 데이터 시트를 보면, Tx descriptor 에는 패킷 사이즈, Vlan 태그, TCP/IP fregment 등의 메타정보가 담겨져 있고, 이러한 Tx descriptor는 크게 3가지 유형을 나타낸다.

  1. Legacy Transmit Descriptor
    49/410

  1. Context Transmit Descriptor
    55/410

  1. Data Transmit Descriptor
    60/410

Legacy는 해당 취약점에서 이용하지 않으므로 제외하겠다. 우선 Context descriptor에서 패킷의 최대 사이즈를 설정하게 되고, ip 패킷 단편화 등의 정보와, 실제 패킷이 존재하는 물리 주소가 들어있다. 따라서 Data descriptor 의 사이즈는 Context descriptor 사이즈보다 작아야 한다. → 여기가 포인트인듯

아까 말한 Tx ring 버퍼에 해당 tx descriptor 가 쌓인다고 했다. 송신이든 수신이든 패킷하나가 보내지거나 수신될때 마다 CPU가 인터럽트를 처리하게 되면 큰 오버헤드가 발생하므로, 주기적으로 모아서 처리를 한다.(Interrupt coalescing)

모든 `Tx descriptor` 이 Tx-ring에 써지면, Guest에서 E1000 MMIO TDT 레지스터를 업데이트를하여 호스트에게 새로 처리할 디스크립터가 있다고 알리게 된다.

이 부분이 위에서 패킷 전송 과정중 2 번의 과정인 것 같다.

따라서 정리를 해보면, guest에서 Tx-ring에 우리가 조작된 Tx-descriptor를 넣은뒤, TdT 레지스터를 업데이트 시키면, guest->host로의 패킷 전송이 발생하고 우리는 이 부분에서 발생하는 취약점을 분석해야한다.

3.3 취약점 설명 - integer underflow


위의 이론을 어느정도 이해했다는 가정하에 취약점 분석을 들어가야한다. 물론 나도 아직 잘 모르겠다

Tx-ring 버퍼에 원하는 디스크립터를 넣는 방법은 익스플로잇한 커널 모듈을 만들면 된다. 해당 모듈 내부에서 tx-descriptor를 세팅하고 tdt 레지스터를 업데이트하면 tx-ring에 들어가게 된다. 해당 POC는 아래의 사이트를 참조하였다.

cchochoy/e1000_fake_driver
Fake e1000 driver exploiting Virtualbox Guest-to-Host vulnerability - cchochoy/e1000_fake_driver
https://github.com/cchochoy/e1000_fake_driver/blob/master/fake_driver/e1k.c

실제 DevE1000.cpp 코드 내부에서 Tx descriptor 구조체 정보를 확인가능하다

union E1kTxDesc    
{
    struct E1kTDLegacy  legacy;
    struct E1kTDContext context;
    struct E1kTDData    data;
};
typedef union  E1kTxDesc E1KTXDESC;

legacy, context, data 필드 총 3개가 Tx-Descriptor 하나로 구성된다. fake_driver에서

Tx descriptor = [context_1, data_2, data_3, context_4, data_5] 형식으로 Tx-ring 버퍼에 저장되게끔 하한다. 각 descriptor에서 필수로 세팅해야하는 정보는 다음과 같다

context_1.header_length = 0
context_1.maximum_segment_size = 0x3010
context_1.tcp_segmentation_enabled = true

data_2.data_length = 0x10
data_2.end_of_packet = false
data_2.tcp_segmentation_enabled = true

data_3.data_length = 0
data_3.end_of_packet = true
data_3.tcp_segmentation_enabled = true

context_4.header_length = 0
context_4.maximum_segment_size = 0xF
context_4.tcp_segmentation_enabled = true

data_5.data_length = 0x4188
data_5.end_of_packet = true
data_5.tcp_segmentation_enabled = true

참조한 POC에서는 위의 구성과 약간 다르지만, 일단 위의 세팅값을 기준으로 설명을 해보자.

TdT 레지스터가 업데이트되면, 게스트는 호스트에게 새로 처리해야하는 descriptor가 있다고 알린다.

DevE1000.cpp: e1kXmitPending() 함수가 호출된다.

static int e1kXmitPending(PE1KSTATE pThis, bool fOnWorkerThread)
{
...
        while (!pThis->fLocked && e1kTxDLazyLoad(pThis))
        {
            while (e1kLocateTxPacket(pThis))
            {
                fIncomplete = false;
                rc = e1kXmitAllocBuf(pThis, pThis->fGSO);
                if (RT_FAILURE(rc))
                    goto out;
                rc = e1kXmitPacket(pThis, fOnWorkerThread);
                if (RT_FAILURE(rc))
                    goto out;
            }
  • e1kTxDLazyLoad : 게스트 OS에서 E1000 메모리로 TX 설명자 목록을 읽는다.
  • e1kLocateTxPacket : 각 컨텍스트에 대한 초기 상태를 설정한다.
  • e1kXmitPacket : 실제로 TX 디스크립터를 처리한다 → 중요

우리가 설정한 descriptor는 첫번째 while(e1kLocateTxPacket()) 에서 5개중 앞의 3개의 descriptor만 처리된다. 자세히 살펴보자.

Context_1 Descriptor

RBX  0x0
 RCX  0x7fdeb8069244 ◂— 0xffffffffffffffff
 RDX  0xe00
*RDI  0x7fdea069d358 ◂— 0
 RSI  0x0
 R8   0x7f7
 R9   0x7fdea252a940 (PGMPhysReleasePageMappingLock::__PRETTY_FUNCTION__) ◂— jbe    0x7fdea252a9b1 /* 'void PGMPhysReleasePageMappingLock(PVM, PPGMPAGEMAPLOCK)' */
 R10  0x0
 R11  0x202
 R12  0x0
 R13  0x7fdeb84a779f ◂— 0x7fdeb84a789800
 R14  0x7fdea1c0f9c0 —▸ 0x7fdea1d109c0 —▸ 0x7fdeb84279c0 —▸ 0x7fdea3fff9c0 —▸ 0x7fdeb86569c0 ◂— ...
 R15  0x2246ed0 —▸ 0x12905a0 —▸ 0x9556a4 ◂— push   rbp
 RBP  0x7fdea1c0e770 —▸ 0x7fdea1c0e7f0 —▸ 0x7fdea1c0e840 —▸ 0x7fdea1c0e950 —▸ 0x7fdea1c0e990 ◂— ...
 RSP  0x7fdea1c0e730 —▸ 0x7fdea069d358 ◂— 0
*RIP  0x7fde746bb7cb (e1kLocateTxPacket(E1kState_st*)+297) ◂— call   0x7fde746b3b23
─────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────
   0x7fde746bb7b9 <e1kLocateTxPacket(E1kState_st*)+279>    add    rax, rdx
   0x7fde746bb7bc <e1kLocateTxPacket(E1kState_st*)+282>    add    rax, 8
   0x7fde746bb7c0 <e1kLocateTxPacket(E1kState_st*)+286>    mov    qword ptr [rbp - 0x20], rax
   0x7fde746bb7c4 <e1kLocateTxPacket(E1kState_st*)+290>    mov    rax, qword ptr [rbp - 0x20]
   0x7fde746bb7c8 <e1kLocateTxPacket(E1kState_st*)+294>    mov    rdi, rax
 ► 0x7fde746bb7cb <e1kLocateTxPacket(E1kState_st*)+297>    call   e1kGetDescType(E1kTxDesc*) <e1kGetDescType(E1kTxDesc*)>
        rdi: 0x7fdea069d358 ◂— 0
        rsi: 0x0
        rdx: 0xe00
        rcx: 0x7fdeb8069244 ◂— 0xffffffffffffffff
 
   0x7fde746bb7d0 <e1kLocateTxPacket(E1kState_st*)+302>    test   eax, eax
   0x7fde746bb7d2 <e1kLocateTxPacket(E1kState_st*)+304>    je     e1kLocateTxPacket(E1kState_st*)+321 <e1kLocateTxPacket(E1kState_st*)+321>
 
   0x7fde746bb7d4 <e1kLocateTxPacket(E1kState_st*)+306>    cmp    eax, 1
   0x7fde746bb7d7 <e1kLocateTxPacket(E1kState_st*)+309>    je     e1kLocateTxPacket(E1kState_st*)+413 <e1kLocateTxPacket(E1kState_st*)+413>
 
   0x7fde746bb7d9 <e1kLocateTxPacket(E1kState_st*)+311>    cmp    eax, -1
──────────────────────────────────────────[ SOURCE (CODE) ]──────────────────────────────────────────
In file: /home/wogh8732/vb/e1000_fake_driver/VirtualBox-5.2.10/src/VBox/Devices/Network/DevE1000.cpp
   5045     uint32_t cbPacket = 0;
   5046 
   5047     for (int i = pThis->iTxDCurrent; i < pThis->nTxDFetched; ++i)
   5048     {
   5049         E1KTXDESC *pDesc = &pThis->aTxDescriptors[i];
 ► 5050         switch (e1kGetDescType(pDesc))
   5051         {
   5052             case E1K_DTYP_CONTEXT:
   5053                 e1kUpdateTxContext(pThis, pDesc);
   5054                 continue;
   5055             case E1K_DTYP_LEGACY:

e1kGetDescType(pDesc) 함수의 반환값에 따라서 descriptor 종류가 갈린다.

DECLINLINE(int) e1kGetDescType(E1KTXDESC *pDesc)
{
    if (pDesc->legacy.cmd.fDEXT)
        return pDesc->context.dw2.u4DTYP;
    return E1K_DTYP_LEGACY;
}

위 함수를 요약하면, e1kGetDescType() 함수의 반환값이 -1이면 legacy, 0이면 context, 1이면 data이다. 우리가 넣은 descriptor라면 제일 먼저 0이 반환될것이고 그에 따른 swich 분기로 갈것이다.

In file: /home/wogh8732/vb/e1000_fake_driver/VirtualBox-5.2.10/src/VBox/Devices/Network/DevE1000.cpp
   5048     {
   5049         E1KTXDESC *pDesc = &pThis->aTxDescriptors[i];
   5050         switch (e1kGetDescType(pDesc))
   5051         {
   5052             case E1K_DTYP_CONTEXT: //0
 ► 5053                 e1kUpdateTxContext(pThis, pDesc);
   5054                 continue;
   5055             case E1K_DTYP_LEGACY:
   5056                 /* Skip empty descriptors. */
   5057                 if (!pDesc->legacy.u64BufAddr || !pDesc->legacy.cmd.u16Length)
   5058                     break;

첫번째 context_1 descriptor에 따라서 e1kUpdateTxContext() 함수가 호출된다.

DECLINLINE(void) e1kUpdateTxContext(PE1KSTATE pThis, E1KTXDESC *pDesc)
{
    if (pDesc->context.dw2.fTSE)
    {
        pThis->contextTSE = pDesc->context;
        uint32_t cbMaxSegmentSize = pThis->contextTSE.dw3.u16MSS + pThis->contextTSE.dw3.u8HDRLEN + 4; /*VTAG*/
        if (RT_UNLIKELY(cbMaxSegmentSize > E1K_MAX_TX_PKT_SIZE)) //0x3fa0
        {
            pThis->contextTSE.dw3.u16MSS = E1K_MAX_TX_PKT_SIZE - pThis->contextTSE.dw3.u8HDRLEN - 4; /*VTAG*/
            LogRelMax(10, ("%s Transmit packet is too large: %u > %u(max). Adjusted MSS to %u.\n",
                           pThis->szPrf, cbMaxSegmentSize, E1K_MAX_TX_PKT_SIZE, pThis->contextTSE.dw3.u16MSS));
.......

해당 함수에선 뒤에서 설명할 취약점에 필요한 부분이다. 자세한건 뒤에서 설명할것이다.

Data_2 Descriptor

In file: /home/wogh8732/vb/e1000_fake_driver/VirtualBox-5.2.10/src/VBox/Devices/Network/DevE1000.cpp
   5059                 cbPacket += pDesc->legacy.cmd.u16Length;
   5060                 pThis->fGSO = false;
   5061                 break;
   5062             case E1K_DTYP_DATA: //1
   5063                 /* Skip empty descriptors. */
 ► 5064                 if (!pDesc->data.u64BufAddr || !pDesc->data.cmd.u20DTALEN)
   5065                     break;
   5066                 if (cbPacket == 0)
   5067                 {
   5068

2번째로 Data_2 Descriptor에 따른 분기가 수행된다. 여기서는 별 볼게 없다. 그다음 Data_3 을 살펴보자

Data_3 Descriptor

In file: /home/wogh8732/vb/e1000_fake_driver/VirtualBox-5.2.10/src/VBox/Devices/Network/DevE1000.cpp
   5059                 cbPacket += pDesc->legacy.cmd.u16Length;
   5060                 pThis->fGSO = false;
   5061                 break;
   5062             case E1K_DTYP_DATA:
   5063                 /* Skip empty descriptors. */
 ► 5064                 if (!pDesc->data.u64BufAddr || !pDesc->data.cmd.u20DTALEN)
   5065                     break;
   5066                 if (cbPacket == 0)
   5067                 {
   5068                     /*
   5069                      * The first fragment: save IXSM and TXSM options
──────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────
00:0000│ rsp  0x7fdea1c0e730 —▸ 0x7fdea069d358 ◂— 0
01:0008│      0x7fdea1c0e738 —▸ 0x7fdea069c550 ◂— xor    dword ptr [r8], r14d /* 0x30233030303145; 'E1000#0' */
02:0010│      0x7fdea1c0e740 ◂— 0x1007fde7486e968
03:0018│      0x7fdea1c0e748 ◂— 0x200000010
04:0020│      0x7fdea1c0e750 —▸ 0x7fdea069d378 ◂— add    byte ptr [rax + 0x21194], al /* 0x211948000 */
05:0028│      0x7fdea1c0e758 ◂— 0xf29fe4a6f49ea900
06:0030│      0x7fdea1c0e760 ◂— 0x8e4ed7b0
07:0038│      0x7fdea1c0e768 ◂— 0x0
────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────
 ► f 0     7fde746bb83f e1kLocateTxPacket(E1kState_st*)+413
   f 1     7fde746bc5dc
   f 2     7fde746bcb8e
   f 3     7fdea20c2557 pdmR3QueueFlush(PDMQUEUE*)+835
   f 4     7fdea20c2191 PDMR3QueueFlushAll+355
   f 5     7fdea2029d26 emR3ForcedActions+2879
   f 6     7fdea203861c emR3HmExecute+3188
   f 7     7fdea202d6bb EMR3ExecuteVM+7300
   f 8     7fdea2186e7e
   f 9     7fdea21864b7 vmR3EmulationThread+50
   f 10     7fded3059c20 rtThreadMain+395
─────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> p pDesc->data.u64BufAddr
$2 = 8884879360
pwndbg> p pDesc->data.cmd.u20DTALEN
$3 = 0

현재 data.cmd.u20DTALEN 이 0이므로 switch case 분기에서 나오게 된다.

...
if (pDesc->legacy.cmd.fEOP)
        {
            /*
             * Non-TSE descriptors have VLE bit properly set in
             * the last fragment.
             */
            if (!fTSE)
            {
                pThis->fVTag = pDesc->data.cmd.fVLE;
                pThis->u16VTagTCI = pDesc->data.dw3.u16Special;
            }
            /*
             * Compute the required buffer size. If we cannot do GSO but still
             * have to do segmentation we allocate the first segment only.
             */
            pThis->cbTxAlloc = (!fTSE || pThis->fGSO) ?
                cbPacket :
                RT_MIN(cbPacket, pThis->contextTSE.dw3.u16MSS + pThis->contextTSE.dw3.u8HDRLEN);
            if (pThis->fVTag)
                pThis->cbTxAlloc += 4;
            LogFlow(("%s e1kLocateTxPacket: RET true cbTxAlloc=%d\n",
                     pThis->szPrf, pThis->cbTxAlloc));
            return true; -> 여기서 return 됨
...

e1kLocateTxPacket() 함수의switch -case 뒤에 위의 코드가 있다. 현재 data_3 descriptor의 값을 보면

pwndbg> p pDesc->legacy.cmd.fEOP
$5 = 1
pwndbg>

이므로 return true가 호출되면서, e1kLocateTxPacket() 가 끝나게 된다. 다시 e1kXmitPending() 함수로 돌아가보자.

...
while (!pThis->fLocked && e1kTxDLazyLoad(pThis))
        {
            while (e1kLocateTxPacket(pThis))
            {
                fIncomplete = false;
                /* Found a complete packet, allocate it. */
                rc = e1kXmitAllocBuf(pThis, pThis->fGSO);
                /* If we're out of bandwidth we'll come back later. */
                if (RT_FAILURE(rc))
                    goto out;
                /* Copy the packet to allocated buffer and send it. */
                rc = e1kXmitPacket(pThis, fOnWorkerThread);
                /* If we're out of bandwidth we'll come back later. */
                if (RT_FAILURE(rc))
                    goto out;
            }
...

현재 e1kLocateTxPacket() 함수가 호출되면서 Context_1, Data_2, Data_3 3개의 descriptor가 패킷 전송을 위한 세팅을 끝맞췄다. 이제 e1kXmitPacket() 함수를 통해 실제 send를 하게 된다. (Context_4, Data_5 Descriptor들은 다음 while 문에서 처리된다.)

static int e1kXmitPacket(PE1KSTATE pThis, bool fOnWorkerThread)
{
...

    while (pThis->iTxDCurrent < pThis->nTxDFetched)
    {
        E1KTXDESC *pDesc = &pThis->aTxDescriptors[pThis->iTxDCurrent];
        E1kLog3(("%s About to process new TX descriptor at %08x%08x, TDLEN=%08x, TDH=%08x, TDT=%08x\n",
                 pThis->szPrf, TDBAH, TDBAL + TDH * sizeof(E1KTXDESC), TDLEN, TDH, TDT));
        rc = e1kXmitDesc(pThis, pDesc, e1kDescAddr(TDBAH, TDBAL, TDH), fOnWorkerThread);
        if (RT_FAILURE(rc))
            break;
    ...
    }

e1kXmitPacket() 함수내부를 보면 반복문을 돌면서 다시한번 TxDescriptor[]를 가져오게 된다. 이는 다시 context_1을 가져올것이다. 그리고 그것을 인자로하여 e1kXmitDesc() 함수가 호출된다.

static int e1kXmitDesc(PE1KSTATE pThis, E1KTXDESC *pDesc, RTGCPHYS addr,
                       bool fOnWorkerThread)
{
...
    switch (e1kGetDescType(pDesc))
    {
        case E1K_DTYP_CONTEXT:
            ...
            break;
        case E1K_DTYP_DATA:
        {
            ...
            if (pDesc->data.cmd.u20DTALEN == 0 || pDesc->data.u64BufAddr == 0)
            {
                E1kLog2(("% Empty data descriptor, skipped.\n", pThis->szPrf));
						}
            else
            {
                if (e1kXmitIsGsoBuf(pThis->CTX_SUFF(pTxSg)))
                {
                    ...
                }
                else if (!pDesc->data.cmd.fTSE)
                {
                    ...
                }
                else
                {
                    STAM_COUNTER_INC(&pThis->StatTxPathFallback);
                    rc = e1kFallbackAddToFrame(pThis, pDesc, fOnWorkerThread);
                }
            }
            ...
-----------------------------------------
pwndbg> p pDesc->data.cmd.fTSE
$3 = 1

첫번째로는 Context_1의 로직이 수행된다. 그리고 두번째로는 E1K_DTYP_DATA: 의 분기로 빠진다.

pDesc→data.cmd.fTSE 가 1이므로 else if 문 안으로 들어오게 되고 e1kFallbackAddToFrame() 함수가 호출된다. 해당 함수의 기능은 Tx 디스크립터 내용을 NIC 메모리에 쓰고 호스트에게 이를 처리하도록 지시하는데, 이 과정에서 취약점이 터진다.

static int e1kFallbackAddToFrame(PE1KSTATE pThis, E1KTXDESC *pDesc, bool fOnWorkerThread)
{
    ...
    uint16_t u16MaxPktLen = pThis->contextTSE.dw3.u8HDRLEN + pThis->contextTSE.dw3.u16MSS;

    /*
     * Carve out segments.
     */
    int rc = VINF_SUCCESS;
    do
    {
        /* Calculate how many bytes we have left in this TCP segment */
        uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;
				// u16MaxPktLen 과 pThis->u16TxPktLen 두 값을 컨트롤할수 있다면?
        if (cb > pDesc->data.cmd.u20DTALEN)
        {
            /* This descriptor fits completely into current segment */
            cb = pDesc->data.cmd.u20DTALEN;
            rc = e1kFallbackAddSegment(pThis, pDesc->data.u64BufAddr, cb, pDesc->data.cmd.fEOP /*fSend*/, fOnWorkerThread);
        }
        else
        {
            ...
        }

        pDesc->data.u64BufAddr    += cb;
        pDesc->data.cmd.u20DTALEN -= cb;
    } while (pDesc->data.cmd.u20DTALEN > 0 && RT_SUCCESS(rc));

    if (pDesc->data.cmd.fEOP)
    {
        ...
        pThis->u16TxPktLen = 0;
        ...
    }

    return VINF_SUCCESS; /// @todo consider rc;
}

do - while 반복문을 돌면서 현재 디스크립트에 남아있는 세그먼트들을 e1kFallbackAddSegment() 함수에서 이제 실제로 물리 주소(pDesc→data.u64BufAddr)에서 E1000의 메모리주소로 cb 바이트 만큼 데이터를 읽고, 호스트로 전송하게 된다.

위 코드에서 중요한 변수들의 의미는 다음과 같다

  • pThis->contextTSE.dw3.u8HDRLEN : 디스크립터에 남아있는 TCP 세그먼트 헤더 사이즈
  • pThis->contextTSE.dw3.u16MSS : 전송될 수 있는 최대 세그먼트 사이즈
  • u16MaxPktLen : 위 두개를 더한 값으로 처리할수 있는 최대 패킷 사이즈
  • pThis->u16TxPktLen : tx packet buffer 들어있는 데이터 크기
  • cb : 처리해야하는 데이터 크기
  • pDesc→data.cmd.u20DTALEN : 현재 descriptor가 가리키고 있는 데이터의 총 길이

  • u16MaxPktLen 이 값은 위에서 pThis->contextTSE.dw3.u8HDRLEN, pThis->contextTSE.dw3.u16MSS 두 값을 더한것인데, 여기서 하나 조건을 만족시켜야하는게 있다. (토글 펼치면 설명있음)
    DECLINLINE(void) e1kUpdateTxContext(PE1KSTATE pThis, E1KTXDESC *pDesc)
    {
        if (pDesc->context.dw2.fTSE)
        {
            pThis->contextTSE = pDesc->context;
            uint32_t cbMaxSegmentSize = pThis->contextTSE.dw3.u16MSS + pThis->contextTSE.dw3.u8HDRLEN + 4; /*VTAG*/
            if (RT_UNLIKELY(cbMaxSegmentSize > E1K_MAX_TX_PKT_SIZE)) //0x3fa0
            {
                pThis->contextTSE.dw3.u16MSS = E1K_MAX_TX_PKT_SIZE - pThis->contextTSE.dw3.u8HDRLEN - 4; /*VTAG*/
                LogRelMax(10, ("%s Transmit packet is too large: %u > %u(max). Adjusted MSS to %u.\n",
                               pThis->szPrf, cbMaxSegmentSize, E1K_MAX_TX_PKT_SIZE, pThis->contextTSE.dw3.u16MSS));
    .......

    위에서 Context Descriptor 는 e1kUpdateTxContext() 함수에서 초기화가 일어난다고 했다. 코드를 보면 cbMaxSegmentSzie 와 0x3fa0 를 비교하는 로직이 있는데, RT_UNLIKELY 요거는 성능향상을 위해 컴파일러에게 일반적으로는 false를 뱉어야 정상적인 루틴이라고 생각하면 된다.

    어쨋든 cbMaxSegmentSize가 0x3fa0보다 작으려면, pThis->contextTSE.dw3.u16MSS + pThis->contextTSE.dw3.u8HDRLEN + 4 요 값을 잘 0x3fa0보다 작게끔 해야한다. 즉 cbMaxSegmentSize에 E1K_MAX_TX_PKT_SIZE - 4 이하의 값이 들어가게 하면 된다. 이 조건을 만족시켰다고 하고 다시 e1kFallbackAddToFrame() 함수 설명으로 넘어가자.

따라서 context_1이 e1kUpdateTxContext() 에서 초기화될때 위 조건을 통과할수 있도록 값을 잘 줘야한다. 현재는 u16MaxPktLen = 0x3010 으로 세팅했다. 연산을 통해 cb는 0x3010이 되고, 조건문 (0x3010 > 0) 을 만족해 cb값을 현재 data_2 디스크립터가 가리키는 데이터 사이즈로 업데이트 된다. (이는 0x10으로 세팅했다.)

결론적으로는 u16MaxPktLen - pThis->u16TxPktLen; 에서 인티저 언더플로우가 발생한다.
data_2에서 잘 세팅을 해줘야 뒤에 data_5가 처리될때 터진다. 취약점 설명은 뒤에서 할것임

현재Data_2 가 처리되는 상황이다. e1kFallbackAddSegment() 함수의 호출 직전의 상황은 다음과 같다

Data_2 descriptor

NameValue
u16MaxPktLen0x3010
pThis→u16TxPktLen0x00
cb0x10

이제 e1kFallbackAddSegment() 함수가 호출되고 여기서 실제의 호스트로 보내는 동작이 발생한다. 해당 함수를 보면

static void e1kFallbackAddSegment(PE1KSTATE pThis, RTGCPHYS PhysAddr, uint16_t u16Len, bool fSend, bool fOnWorkerThread)
{
...
		//u16len(cb) 값을 언더플로우를 이용해서 변경시키면, 아래 함수에서 bof가 터짐. 이건 dat5에서 설명함
		PDMDevHlpPhysRead(pThis->CTX_SUFF(pDevIns), PhysAddr,
                      pThis->aTxPacketFallback + pThis->u16TxPktLen, u16Len);

		pThis->u16TxPktLen += u16Len;

...
}

위와 같이 u16len(cb)와 u16TxPktLen을 더하는 로직이 있다. 따라서 e1kFallbackAddSegment() 함수가 끝나면 pThis→u16TxPktLen 은 0x10의 값을 가진다.

    if (pDesc->data.cmd.fEOP)
    {
        ...
        pThis->u16TxPktLen = 0;
        ...
    }

만약 pDesc→data.cmd.fEOP 필드 값을 True로 만들면 해당 u16TxPktLen 은 0이 될것이다. 하지만 현재 Data_2 디스크립터는 해당 값을 False로 만들어서 0x10이 유지되게 했다.

참고로 pDesc→data.cmd.fEOP 의미는 해당 세그먼트가 마지막인지 아닌지를 뜻한다. 1이면 마지막 패킷임 → 단편화 관련.

자 이제 data_2 descriptor는 이렇게 처리되고, e1kXmitPacket() 함수 내부의 루프를 돌아 한번더 e1kXmitDesc() 함수가 호출된다.

In file: /home/wogh8732/vb/e1000_fake_driver/VirtualBox-5.2.10/src/VBox/Devices/Network/DevE1000.cpp
   4872             STAM_COUNTER_INC(pDesc->data.cmd.fTSE?
   4873                              &pThis->StatTxDescTSEData:
   4874                              &pThis->StatTxDescData);
   4875             E1K_INC_ISTAT_CNT(pThis->uStatDescDat);
   4876             STAM_PROFILE_ADV_START(&pThis->CTX_SUFF_Z(StatTransmit), a);
 ► 4877             if (pDesc->data.cmd.u20DTALEN == 0 || pDesc->data.u64BufAddr == 0)
   4878             {
   4879                 E1kLog2(("% Empty data descriptor, skipped.\n", pThis->szPrf));
   4880             }
   4881             else
   4882             {
──────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────
00:0000│ rsp  0x7f1b690c5690 ◂— 0x7f00690c56d0
01:0008│      0x7f1b690c5698 ◂— 0x2115d5020
02:0010│      0x7f1b690c56a0 —▸ 0x7f1b43ace378 ◂— add    byte ptr [rax + 0x20f14], al /* 0x20f148000 */
03:0018│      0x7f1b690c56a8 —▸ 0x7f1b43acd550 ◂— xor    dword ptr [r8], r14d /* 0x30233030303145; 'E1000#0' */
04:0020│      0x7f1b690c56b0 ◂— 0x10
05:0028│      0x7f1b690c56b8 ◂— 0x4e4eda10
06:0030│      0x7f1b690c56c0 ◂— 0x3a73b7c4766
07:0038│      0x7f1b690c56c8 ◂— 0x9a8e0bce
────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────
 ► f 0     7f1b3753ab63
   f 1     7f1b3753bd27
   f 2     7f1b3753c64a
   f 3     7f1b3753cb8e
   f 4     7f1b69888557 pdmR3QueueFlush(PDMQUEUE*)+835
   f 5     7f1b69888191 PDMR3QueueFlushAll+355
   f 6     7f1b697efd26 emR3ForcedActions+2879
   f 7     7f1b697fe61c emR3HmExecute+3188
   f 8     7f1b697f36bb EMR3ExecuteVM+7300
   f 9     7f1b6994ce7e
   f 10     7f1b6994c4b7 vmR3EmulationThread+50
─────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> p* pDesc->data.u64BufAddr
Cannot access memory at address 0x20f148000
pwndbg> p pDesc->data.cmd.u20DTALEN
$8 = 0

헌데 data_3 에서는 pDesc->data.cmd.u20DTALEN 의 값을 0으로 세팅하면 else문에 들어있는 e1kFallbackAddToFrame() 함수쪽으로 분기하지 않고, 그냥 e1kXmitDesc() 함수가 끝나게 된다. 따라서 data_3는 다음의 값을 갖는다.

그다음 다시 while문 아래 로직을 수행하게 되는데

static int e1kXmitPacket(PE1KSTATE pThis, bool fOnWorkerThread)
{
...

    while (pThis->iTxDCurrent < pThis->nTxDFetched)
    {
        E1KTXDESC *pDesc = &pThis->aTxDescriptors[pThis->iTxDCurrent];
        E1kLog3(("%s About to process new TX descriptor at %08x%08x, TDLEN=%08x, TDH=%08x, TDT=%08x\n",
                 pThis->szPrf, TDBAH, TDBAL + TDH * sizeof(E1KTXDESC), TDLEN, TDH, TDT));
        rc = e1kXmitDesc(pThis, pDesc, e1kDescAddr(TDBAH, TDBAL, TDH), fOnWorkerThread);
        if (RT_FAILURE(rc))
            break;

				...
			
				++pThis->iTxDCurrent;
				-> 여기 아래!
        if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP)
            break;
				
    ...
    }

		LogFlow(("%s e1kXmitPacket: RET %Rrc current=%d fetched=%d\n",
             pThis->szPrf, rc, pThis->iTxDCurrent, pThis->nTxDFetched));
    return rc;
--------------------------------
pwndbg> p pDesc->legacy.cmd.fEOP
$10 = 1
pwndbg> p e1kGetDescType(pDesc)
$11 = 1

if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP)

위 조건이 만족되면 e1kXmitPacket() 함수는 종료된다. e1kXmitPacket() 함수의 역할은 실제로 Tx-descriptor 들을 처리하는것인데, 현재 우리가 5개의 descriptor 중 3개만 처리되고 아직 context_4, data_4 descriptor 가 남아있음에도 종료가 된다. 이점을 잊지말자.

→ 이러한 이유는 Context-Descriptor의 세팅이 된후 그다음 Data-Descriptor 가 처리됨.

이제 다시 e1kXmitPending() 함수의 while문으로 돌아온다. 이제 context_4, data_5 descriptor 가 처리되게 된다.

static int e1kXmitPending(PE1KSTATE pThis, bool fOnWorkerThread)
{
...
        while (!pThis->fLocked && e1kTxDLazyLoad(pThis))
        {
            while (e1kLocateTxPacket(pThis))
            {
                fIncomplete = false;
                rc = e1kXmitAllocBuf(pThis, pThis->fGSO);
                if (RT_FAILURE(rc))
                    goto out;
                rc = e1kXmitPacket(pThis, fOnWorkerThread);
                if (RT_FAILURE(rc))
                    goto out;
            }

자. 여기서부터가 중요하다. 우선 현재의 상황의 간단히 정리해보자.

  1. context_1, data_2, data_3 가 현재 처리됨. 순서는 다음과 같음
    • e1kXmitPending() 내부 while에서
      1. e1kTxDLazyLoad() 함수로 게스트 OS에서 E1000 메모리로 TX 설명자 목록을 읽음
      1. while() 문 첫 loop 에서 e1kLocateTxPacket() 에서 context_1, data_2, data_3 부분이 초기화 됨
      1. e1kXmitPacket() 함수에서 실제로 context_1, data_2, data_3 descriptor 내용이 처리가 됨.

        data_3 descriptor 처리 과정에서 pThis->u16TxPktLen 값이 0x10 으로 세팅되고 끝남.

    • e1kXmitPending() 내부 두번째 wihle loop가 이제 실행되려고 함.

e1kLocateTxPacket() 함수 내부에서 context_4, data_5 descriptor의 초기화가 일어난다. 이때

context_4.header_length = 0 // pThis->contextTSE.dw3.u8HDRLEN
context_4.maximum_segment_size = 0xF // pThis->contextTSE.dw3.u16MSS
context_4.tcp_segmentation_enabled = true

data_5.data_length = 0x4034
data_5.end_of_packet = true
data_5.tcp_segmentation_enabled = true

다음과 같이 값이 세팅된다면, 이제 실제 data_5가 e1kXmitPacket() 내부에서 처리될때 인티저 언더플로우가 발생한다.

우선 e1kLocateTxPacket() 내부에서 context_4 → e1kUpdateTxContext() 가 호출되는데,

   0x7f7d8973c432    mov    dword ptr [rbp - 0x1c], eax
 ► 0x7f7d8973c435    cmp    dword ptr [rbp - 0x1c], 0x3fa0
   0x7f7d8973c43c    seta   al
   0x7f7d8973c43f    movzx  eax, al
   0x7f7d8973c442    test   rax, rax
   0x7f7d8973c445    je     0x7f7d8973c53f <0x7f7d8973c53f>
    ↓
   0x7f7d8973c53f    mov    rax, qword ptr [rbp - 0x28]
──────────────────────────────────────────[ SOURCE (CODE) ]──────────────────────────────────────────
In file: /home/wogh8732/vb/e1000_fake_driver/VirtualBox-5.2.10/src/VBox/Devices/Network/DevE1000.cpp
   5000 {
   5001     if (pDesc->context.dw2.fTSE)
   5002     {
   5003         pThis->contextTSE = pDesc->context;
   5004         uint32_t cbMaxSegmentSize = pThis->contextTSE.dw3.u16MSS + pThis->contextTSE.dw3.u8HDRLEN + 4; /*VTAG*/
 ► 5005         if (RT_UNLIKELY(cbMaxSegmentSize > E1K_MAX_TX_PKT_SIZE))
   5006         {
   5007             pThis->contextTSE.dw3.u16MSS = E1K_MAX_TX_PKT_SIZE - pThis->contextTSE.dw3.u8HDRLEN - 4; /*VTAG*/
   5008             LogRelMax(10, ("%s Transmit packet is too large: %u > %u(max). Adjusted MSS to %u.\n",
   5009                            pThis->szPrf, cbMaxSegmentSize, E1K_MAX_TX_PKT_SIZE, pThis->contextTSE.dw3.u16MSS));
   5010         }
──────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────
00:0000│ rsp  0x7f7daef116f0 —▸ 0x7f7dad9a0388 ◂— 0
01:0008│      0x7f7daef116f8 —▸ 0x7f7dad99f550 ◂— xor    dword ptr [r8], r14d /* 0x30233030303145; 'E1000#0' */
02:0010│      0x7f7daef11700 ◂— 0x139e4ed7b0
03:0018│      0x7f7daef11708 ◂— 0x32000263a75900
04:0020│      0x7f7daef11710 ◂— 0x320002650d1752
05:0028│      0x7f7daef11718 ◂— 0x0
06:0030│ rbp  0x7f7daef11720 —▸ 0x7f7daef11770 —▸ 0x7f7daef117f0 —▸ 0x7f7daef11840 —▸ 0x7f7daef11950 ◂— ...
07:0038│      0x7f7daef11728 —▸ 0x7f7d8973c7f6 (e1kLocateTxPacket(E1kState_st*)+340) ◂— jmp    0x7f7d8973ca82
────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────
 ► f 0     7f7d8973c435
   f 1     7f7d8973c7f6 e1kLocateTxPacket(E1kState_st*)+340
   f 2     7f7d8973d5dc
   f 3     7f7d8973db8e
   f 4     7f7daf0c4557 pdmR3QueueFlush(PDMQUEUE*)+835
   f 5     7f7daf0c4191 PDMR3QueueFlushAll+355
   f 6     7f7daf02bd26 emR3ForcedActions+2879
   f 7     7f7daf03a61c emR3HmExecute+3188
   f 8     7f7daf02f6bb EMR3ExecuteVM+7300
   f 9     7f7daf188e7e
   f 10     7f7daf1884b7 vmR3EmulationThread+50
─────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/wx $rbp-0x1c
0x7f7daef11704:	0x00000013

우리가 context_4에 세팅해준 값에 따라서

cbMaxSegmentSize = pThis->contextTSE.dw3.u16MSS + pThis->contextTSE.dw3.u8HDRLEN + 4

위의 값은 0 + 0xf + 4 = 0x13 의 값을 갖게된다.

따라서 RT_UNLIKELY(cbMaxSegmentSize > E1K_MAX_TX_PKT_SIZE) 로직에 따라 else로 빠지게 되면서 pThis->contextTSE.dw3.u16MSS, pThis->contextTSE.dw3.u8HDRLEN 값은 그대로 0xf, 0 값을 가지게 된다.

이제 e1kXmitPacket() 함수가 호출되면서, context_4 이 처리되고 그다음에 data_5가 처리된다. data_5 가 처리되는 부분을 보면, e1kXmitDesc() 내부에서 또 다시 e1kFallbackAddToFrame() 함수가 호출된다.

static int e1kFallbackAddToFrame(PE1KSTATE pThis, E1KTXDESC *pDesc, bool fOnWorkerThread)
{
    ...
    uint16_t u16MaxPktLen = pThis->contextTSE.dw3.u8HDRLEN + pThis->contextTSE.dw3.u16MSS;
			// -> u16MaxPktLen = 0 + 0xf = 0xf
    /*
     * Carve out segments.
     */
    int rc = VINF_SUCCESS;
    do
    {
        /* Calculate how many bytes we have left in this TCP segment */
        uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;
				//  0xf - 0x10 = 0xffffffff!!!
        if (cb > pDesc->data.cmd.u20DTALEN)
        {
            /* This descriptor fits completely into current segment */
            cb = pDesc->data.cmd.u20DTALEN;
            rc = e1kFallbackAddSegment(pThis, pDesc->data.u64BufAddr, cb, pDesc->data.cmd.fEOP /*fSend*/, fOnWorkerThread);
        }
        else
        {
            ...
        }

        pDesc->data.u64BufAddr    += cb;
        pDesc->data.cmd.u20DTALEN -= cb;
    } while (pDesc->data.cmd.u20DTALEN > 0 && RT_SUCCESS(rc));

    if (pDesc->data.cmd.fEOP)
    {
        ...
        pThis->u16TxPktLen = 0;
        ...
    }

    return VINF_SUCCESS; /// @todo consider rc;
}

e1kUpdateTxContext() 함수에서 context_4의 초기화를 통해

  • pThis->contextTSE.dw3.u8HDRLEN : 0
  • pThis->contextTSE.dw3.u16MSS : 0xf

의 값을 가지게 되었다. 따라서

다음과 같이 구성되어 u16MaxPktLen(0xf) - pThis->u16TxPktLen(0x10) 의 결과가 0xffffffff이 나오게 된다. 즉 인티저 언더플로우가 발생하게 된다.

0x7f7d8973b3c2    movzx  edx, word ptr [rbp - 0x1e]
   0x7f7d8973b3c6    mov    rax, qword ptr [rbp - 0x28]
   0x7f7d8973b3ca    movzx  eax, word ptr [rax + 0x51f8]
   0x7f7d8973b3d1    movzx  eax, ax
   0x7f7d8973b3d4    sub    edx, eax
 ► 0x7f7d8973b3d6    mov    eax, edx
   0x7f7d8973b3d8    mov    dword ptr [rbp - 0x14], eax
   0x7f7d8973b3db    mov    rax, qword ptr [rbp - 0x30]
   0x7f7d8973b3df    mov    eax, dword ptr [rax + 8]
   0x7f7d8973b3e2    and    eax, 0xfffff
   0x7f7d8973b3e7    cmp    dword ptr [rbp - 0x14], eax
──────────────────────────────────────────[ SOURCE (CODE) ]──────────────────────────────────────────
In file: /home/wogh8732/vb/e1000_fake_driver/VirtualBox-5.2.10/src/VBox/Devices/Network/DevE1000.cpp
   4406      */
   4407     int rc = VINF_SUCCESS;
   4408     do
   4409     {
   4410         /* Calculate how many bytes we have left in this TCP segment */
 ► 4411         uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;
   4412         if (cb > pDesc->data.cmd.u20DTALEN)
   4413         {
   4414             /* This descriptor fits completely into current segment */
   4415             cb = pDesc->data.cmd.u20DTALEN;
   4416             rc = e1kFallbackAddSegment(pThis, pDesc->data.u64BufAddr, cb, pDesc->data.cmd.fEOP /*fSend*/, fOnWorkerThread);
──────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────
00:0000│ rsp  0x7f7daef11640 —▸ 0x7f7dad9a0398 ◂— add    byte ptr [rax], al /* 0x211a00000 */
01:0008│      0x7f7daef11648 ◂— 0x7f00ad99f550
02:0010│      0x7f7daef11650 —▸ 0x7f7dad9a0398 ◂— add    byte ptr [rax], al /* 0x211a00000 */
03:0018│      0x7f7daef11658 —▸ 0x7f7dad99f550 ◂— xor    dword ptr [r8], r14d /* 0x30233030303145; 'E1000#0' */
04:0020│      0x7f7daef11660 ◂— 0xf0000
05:0028│      0x7f7daef11668 —▸ 0x7f7d00000000 ◂— 0x55c6f2e03020
06:0030│      0x7f7daef11670 —▸ 0x7f7d68113c10 ◂— 0xb1b10001
07:0038│      0x7f7daef11678 ◂— 0xefc2838763a75900
────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────
 ► f 0     7f7d8973b3d6
   f 1     7f7d8973bfb7
   f 2     7f7d8973cd27
   f 3     7f7d8973d64a
   f 4     7f7d8973db8e
   f 5     7f7daf0c4557 pdmR3QueueFlush(PDMQUEUE*)+835
   f 6     7f7daf0c4191 PDMR3QueueFlushAll+355
   f 7     7f7daf02bd26 emR3ForcedActions+2879
   f 8     7f7daf03a61c emR3HmExecute+3188
   f 9     7f7daf02f6bb EMR3ExecuteVM+7300
   f 10     7f7daf188e7e
─────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/gx $rdx
0xffffffff:

그럼이제 이 취약점을 가지고 어떤것을 할수 있는지 확인해보자.

4. 참고자료


728x90

'보안 > 원데이 분석' 카테고리의 다른 글

[linux kernel] CVE-2016-0728 분석  (1) 2021.02.25
dact-0.8.42 RCE  (0) 2021.02.08
CVE-2018-3295 분석(2)  (2) 2020.12.31
CVE-2019-2525, CVE-2019-2548(2)  (0) 2020.08.26
CVE-2019-2525, CVE-2019-2548(1)  (0) 2020.08.26