Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
virtualbox_e1000_0day — VirtualBox E1000 게스트-투-호스트 탈출 | Kitploit
도구/GitHubGitHub/mortenoir1/virtualbox_e1000_0day
Vulnerability AnalysisExploitationPenetration TestingHardware SecurityBinary Exploitation
GitHubmortenoir1/virtualbox_e1000_0day

virtualbox_e1000_0day

VirtualBox E1000 게스트-투-호스트 탈출

저장소 보기
1.4k196227년 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

이유

저는 VirtualBox를 좋아하지만, 제가 0day 취약점을 공개하는 것과는 아무 관련이 없습니다. 그 이유는 현대 정보보안, 특히 보안 연구와 버그 바운티에 대한 저의 불만 때문입니다:

  1. 취약점이 패치될 때까지 반 년을 기다리는 것이 괜찮다고 여겨진다.
  2. 버그 바운티 분야에서는 다음이 괜찮다고 여겨진다:
    1. 제출된 취약점이 검증되고 구매 여부가 결정될 때까지 한 달 이상 기다리는 것.
    2. 결정을 중간에 바꾸는 것. 오늘은 버그 바운티 프로그램이 특정 소프트웨어의 버그를 사겠다고 밝혔지만, 일주일 뒤에 버그와 익스플로잇을 들고 가면 "관심 없음"을 받는다.
    3. 버그 바운티가 구매하려는 소프트웨어의 정확한 목록이 없는 것. 버그 바운티 쪽에는 편리하지만, 연구자에게는 난처하다.
    4. 취약점 가격의 정확한 하한선과 상한선이 없는 것. 가격에 영향을 미치는 요소는 많지만, 연구자들은 무엇이 작업할 가치가 있고 무엇이 아닌지 알아야 한다.
  3. 과대망상과 마케팅 허풍: 취약점에 이름을 붙이고 이를 위한 웹사이트를 만드는 것; 일 년에 수천 개의 컨퍼런스를 여는 것; 보안 연구자로서 자신의 일의 중요성을 과장하는 것; 스스로를 "세상의 구세주"라고 여기는 것. 내려오세요, 각하.

저는 처음 두 가지에 지쳤고, 그래서 제 선택은 전체 공개(full disclosure)입니다. 정보보안계여, 앞으로 나아가십시오.

일반 정보

취약한 소프트웨어: VirtualBox 5.2.20 및 이전 버전.

호스트 OS: 모두, 이 버그는 공유 코드 베이스에 있습니다.

게스트 OS: 모두.

VM 구성: 기본값 (유일한 요구 사항은 네트워크 카드가 Intel PRO/1000 MT Desktop (82540EM)이고 모드가 NAT라는 것입니다).

스스로를 보호하는 방법

패치된 VirtualBox 빌드가 나올 때까지 가상 머신의 네트워크 카드를 PCnet(두 가지 중 하나) 또는 반가상화 네트워크(Paravirtualized Network)로 변경할 수 있습니다. 그렇게 할 수 없다면 모드를 NAT에서 다른 모드로 변경하세요. 전자가 더 안전합니다.

소개

기본 VirtualBox 가상 네트워크 장치는 Intel PRO/1000 MT Desktop (82540EM)이고 기본 네트워크 모드는 NAT입니다. 우리는 이를 E1000이라고 부를 것입니다.

E1000에는 게스트 내의 root/관리자 권한을 가진 공격자가 호스트 ring3로 탈출할 수 있게 하는 취약점이 있습니다. 그런 다음 공격자는 기존 기술을 사용하여 /dev/vboxdrv를 통해 권한을 ring 0로 상승시킬 수 있습니다.

취약점 세부 사항

E1000 101

네트워크 패킷을 보내기 위해 게스트는 일반 PC가 하는 것과 동일한 작업을 수행합니다: 네트워크 카드를 구성하고 네트워크 패킷을 제공합니다. 패킷은 데이터 링크 계층 프레임 및 기타 더 높은 수준의 헤더로 구성됩니다. 어댑터에 제공된 패킷은 Tx 디스크립터(Tx는 전송을 의미)에 래핑됩니다. Tx 디스크립터는 82540EM 데이터시트(317453006EN.PDF, Revision 4.0)에 설명된 데이터 구조입니다. 여기에는 패킷 크기, VLAN 태그, TCP/IP 분할 활성화 플래그 등과 같은 메타정보가 저장됩니다.

82540EM 데이터시트는 레거시(legacy), 컨텍스트(context), 데이터(data)의 세 가지 Tx 디스크립터 유형을 제공합니다. 레거시는 폐기된 것으로 생각됩니다. 나머지 두 가지는 함께 사용됩니다. 우리가 신경 쓰는 유일한 것은 컨텍스트 디스크립터가 최대 패킷 크기를 설정하고 TCP/IP 분할을 전환하며, 데이터 디스크립터가 네트워크 패킷의 물리적 주소와 크기를 보유한다는 것입니다. 데이터 디스크립터의 패킷 크기는 컨텍스트 디스크립터의 최대 패킷 크기보다 작아야 합니다. 일반적으로 컨텍스트 디스크립터는 데이터 디스크립터보다 먼저 네트워크 카드에 공급됩니다.

Tx 디스크립터를 네트워크 카드에 공급하기 위해 게스트는 이를 Tx 링(Tx Ring)에 기록합니다. 이는 미리 정의된 주소의 물리적 메모리에 있는 링 버퍼입니다. 모든 디스크립터가 Tx 링에 기록되면 게스트는 E1000 MMIO TDT 레지스터(Transmit Descriptor Tail)를 업데이트하여 처리할 새 디스크립터가 있음을 호스트에 알립니다.

입력

다음과 같은 Tx 디스크립터 배열을 고려하십시오:``` [context_1, data_2, data_3, context_4, data_5]

구조 필드를 다음과 같이 할당해 보겠습니다 (필드 이름은 사람이 읽기 쉽도록 가상의 것이지만 82540EM 사양에 직접 매핑됩니다):```
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

단계별 분석을 통해 그것들이 왜 그렇게 되어야 하는지 알아보겠습니다.

근본 원인 분석

[context_1, data_2, data_3] 처리

위의 디스크립터들이 지정된 순서대로 Tx Ring에 기록되고 TDT 레지스터가 게스트에 의해 업데이트된다고 가정해 보겠습니다. 이제 호스트는 src/VBox/Devices/Network/DevE1000.cpp 파일에서 e1kXmitPending 함수를 실행합니다(가독성을 위해 대부분의 주석은 제거되었으며 앞으로도 제거될 것입니다):```c 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는 Tx 링에서 5개의 Tx 디스크립터를 모두 읽습니다. 그런 다음 e1kLocateTxPacket이 처음으로 호출됩니다. 이 함수는 모든 디스크립터를 반복하면서 초기 상태를 설정하지만 실제로는 처리하지 않습니다. 이 경우 e1kLocateTxPacket에 대한 첫 번째 호출은 context_1, data_2, data_3 디스크립터를 처리합니다. 나머지 두 디스크립터인 context_4와 data_5는 while 루프의 두 번째 반복에서 처리됩니다(두 번째 반복은 다음 섹션에서 다룹니다). 이 두 부분으로 나뉘는 배열 분할이 취약점을 트리거하는 데 중요하므로 그 이유를 알아봅시다.

e1kLocateTxPacket은 다음과 같습니다:```c
static bool e1kLocateTxPacket(PE1KSTATE pThis)
{
...
    for (int i = pThis->iTxDCurrent; i < pThis->nTxDFetched; ++i)
    {
        E1KTXDESC *pDesc = &pThis->aTxDescriptors[i];
        switch (e1kGetDescType(pDesc))
        {
            case E1K_DTYP_CONTEXT:
                e1kUpdateTxContext(pThis, pDesc);
                continue;
            case E1K_DTYP_LEGACY:
                ...
                break;
            case E1K_DTYP_DATA:
                if (!pDesc->data.u64BufAddr || !pDesc->data.cmd.u20DTALEN)
                    break;
                ...
                break;
            default:
                AssertMsgFailed(("Impossible descriptor type!"));
        }

첫 번째 디스크립터(context_1)는 E1K_DTYP_CONTEXT 유형이므로 e1kUpdateTxContext 함수가 호출됩니다. 이 함수는 디스크립터에 대해 TCP Segmentation이 활성화된 경우 TCP Segmentation Context를 업데이트합니다. context_1에는 해당 조건이 true이므로 TCP Segmentation Context가 업데이트됩니다. (TCP Segmentation Context Update가 실제로 무엇을 하는지는 중요하지 않으며, 아래 코드를 참조하기 위해 이 용어를 사용할 뿐입니다.)

두 번째 디스크립터(data_2)는 E1K_DTYP_DATA 유형이므로 논의에 필요하지 않은 몇 가지 작업이 수행됩니다.

세 번째 디스크립터(data_3)도 E1K_DTYP_DATA 유형이지만 data_3.data_length == 0이므로 아무 작업도 수행되지 않습니다.

현재 세 개의 디스크립터가 초기 처리되고 두 개가 남아 있는 상태입니다. 이제 중요한 부분: switch 문 이후에는 디스크립터의 end_of_packet 필드가 설정되었는지 확인하는 검사가 있습니다. data_3 디스크립터의 경우(data_3.end_of_packet == true) 이 조건이 true입니다. 코드는 몇 가지 작업을 수행한 후 함수에서 반환합니다.```c if (pDesc->legacy.cmd.fEOP) { ... return true; }

`data_3.end_of_packet`가 false였다면 나머지 `context_4`와 `data_5` 디스크립터도 처리되었을 것이고, 취약점은 우회되었을 것입니다. 아래에서 해당 함수의 return이 어떻게 버그로 이어지는지 확인할 수 있습니다.

`e1kLocateTxPacket` 함수의 끝에는 네트워크 패킷을 풀고 네트워크로 보낼 준비가 된 다음 디스크립터들이 있습니다: `context_1`, `data_2`, `data_3`. 그런 다음 `e1kXmitPending`의 내부 루프가 `e1kXmitPacket`을 호출합니다. 이 함수는 모든 디스크립터(이 경우 5개)를 반복하면서 실제로 처리합니다:```c
static int e1kXmitPacket(PE1KSTATE pThis, bool fOnWorkerThread)
{
...
    while (pThis->iTxDCurrent < pThis->nTxDFetched)
    {
        E1KTXDESC *pDesc = &pThis->aTxDescriptors[pThis->iTxDCurrent];
        ...
        rc = e1kXmitDesc(pThis, pDesc, e1kDescAddr(TDBAH, TDBAL, TDH), fOnWorkerThread);
        ...
        if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP)
            break;
    }

각 디스크립터에 대해 e1kXmitDesc 함수가 호출됩니다:```c 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); } } ...

The first descriptor passed to e1kXmitDesc is context_1. The function does nothing with context descriptors.

The second descriptor passed to e1kXmitDesc is data\_2. Since all of our data descriptors have tcp\_segmentation\_enable == true (pDesc->data.cmd.fTSE above) we call e1kFallbackAddToFrame where there will be an integer underflow while data\_5 is processed.```c
static int e1kFallbackAddToFrame(PE1KSTATE pThis, E1KTXDESC *pDesc, bool fOnWorkerThread)
{
    ...
    uint16_t u16MaxPktLen = pThis->contextTSE.dw3.u8HDRLEN + pThis->contextTSE.dw3.u16MSS;
도구 다운로드