1792 words
9 minutes
CVE-2021-24086

Intro#

TOOR 팀 활동을 하며 분석하게된 윈도우 커널 드라이버 원데이 취약점에 관한 글입니다.

이번에 알아볼 취약점은 2021년 2월 9일에 공개된 DoS 취약점입니다.

윈도우 tcpip.sys에서 IPv6의 재조립 패킷을 처리하는 도중 잘못된 처리로 인하여 취약점이 발생합니다.

본 글은 선행 연구를 진행하신 다른 연구원분들의 글들을 읽고 제 나름 분석을 진행하며 취약점을 공부하며 이해하고 정리해본 결과로 작성하게된 글입니다. 나름의 분석을 해봤지만 맞지 않는 부분이 있을 수 있으며, 만약 이를 발견하셨을 시 피드백해주시면 적극 반영하도록 하겠습니다. 취약점 및 PoC 분석에 많은 도움이된 자료는 다음과 같습니다.

Vuln#

Env#

디핑 결과 분석은 현재 공개된 PoC Repository에 있는 두 파일을 활용했습니다.

동적 분석을 진행한 victim 빌드 버전은 다음과 같습니다.

Identifying the differences in the tcpip.sys driver#

주어진 몇몇의 정보를 토대로 IPv6의 Fragment 패킷 재조립 과정에서 취약점이 발생한다는 사실을 알 수 있습니다.

MSRC에 나와있듯 취약점은 재조립 패킷 처리를 비활성화 함으로써 완화시킬 수 있습니다. 이를 통해서 재조립 과정에서 취약점이 발생할 수 있다 생각할 수 있습니다.

또한 주어진 두 파일의 IPv6를 디핑해보면 다음과 같은 두 함수에서 유사도에 차이가 생겼음을 알 수 있습니다.

IPv6pReassembleDatagram에 다음과 같은 차이가 생겼는데 특정한 연산의 결과에 대해 0xFFFF보다 큰지 체크하는 로직이 생겨났습니다.

PoC와 함께 RCA를 알아봅시다.

RCA#

PoC를 작동시켜보면 다음과 같이 Ipv6pReassembleDatagram의 코드 중 NdisGetDataBuffer 호출문에서의 반환값이 NULL이 될때 NULL 포인터 역참조로 인해서 BSOD가 발생하는 모습을 볼 수 있습니다.

NdisGetDataBuffer에서 다음과 같이 NULL이 반환되고

이는 스택에 저장되었다. 이후에 참조되지만 NULL 포인터이므로 역참조 했을 때 크래시가 발생합니다.

위의 흐름을 보면 알 수 있듯, 해당 로직에선 NdisGetDataBuffer가 반환한 포인터가 NULL이 아닌 것으로 신뢰하고 있기 때문에 발생하는 모습을 볼 수 있습니다.

Ipv6pReassembleDatagram은 IPv6의 Fragmentation 패킷을 처리할 때 호출됩니다. 취약점에서 집중해야할 주요 함수들은 다음과 같습니다.

해당 부분의 BytesNeeded는 재조립되어야할 패킷 중 Unfragmentable part의 길이를 구하는 부분입니다. 이는 재조립될 패킷을 저장할 영역을 할당할 때 길이정보로 사용됩니다.

BytesNeeded = v3 + 40;

앞서 계산한 BytesNeeded로 필요한 데이터 영역을 할당합니다. 일때 유심하게봐야할 것은 (unsigned __int16)BytesNeeded로 2바이트로 크기 잘린다는 것입니다. 여기서 v15로 할당된 메모리 영역은 이후에 취약점을 살펴볼 때 봤던 함수인 NdisGetDataBuffer에서 사용됩니다.

NdisGetDataBuffer Call the NdisGetDataBuffer function to gain access to a contiguous block of data from a NET_BUFFER structure.

if ( (int)NetioRetreatNetBuffer(v15, (unsigned __int16)BytesNeeded, 0i64) < 0 )

앞서 v15에 할당한 메모리 영역을 다시 NdisGetDataBuffer 요구하는 모습입니다. 이때 BytesNeeded는 잘림없이 그대로 들어가게됩니다.

BytesNeededa = NdisGetDataBuffer((PNET_BUFFER)v15, BytesNeeded, 0i64, 1u, 0);

이때 문제점이 발생합니다. 만약 BytesNeeded == 0x1FFFF인 상황이 발생하면 어떻게 될까요? 위에서 NetioRetreatNetBuffer에 의해서 할당한 메모리의 크기는 0xFFFF지만 NdisGetDataBuffer에서는 0x1FFFF를 요구하게됩니다. 할당된 메모리보다 사용을 위해 요구하는 메모리의 크기가 더 크면 NdisGetDataBuffer는 어떻게 행동하는지 문서를 확인해봅시다.

Return value NdisGetDataBuffer returns a pointer to the start of the contiguous data or it returns NULL.

If the DataLength member of the NET_BUFFER_DATA structure in the NET_BUFFER structure that the NetBuffer parameter points to is less than the value in >the BytesNeeded parameter, the return value is NULL.

할당된 메모리 길이(DataLength) 보다 파라미터로 요구한 BytesNeeded가 더 클 경우 NULL을 반환한다고 되어있습니다.

위에서 알아봤듯, 계산된 IPv6 헤더 크기(40) + 확장 헤더의 크기가 0xFFFF보다 커지는 상황이되면 BSOD가 발생합니다. 어떻게 이러한 패킷을 만들어내는지 PoC를 확인해봅시다.

PoC#

현재 공개된 PoC를 확인해봅시다.

PoC 코드는 간단합니다. PoC에서는 공격을 위해서 중첩된 Fragment 패킷을 이용합니다.

취약점 트리거를 위해 다음과 같이 거대한 헤더를 갖는 Reassemble 패킷을 만들어냅니다.

reassembled_pkt = IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ ... \
/ IPv6ExtHdrFragment(
id = second_pkt_id, m = 1,
nh = 17, offset = 0
) \
/ UDP(dport = 31337, sport = 31337, chksum=0x7e7f)

다음은 재조립을 유도하기 위해서 이후 재조립에 사용될 패킷을 전송합니다.

sendp(frags, iface= args.iface)
reassembled_pkt_2 = Ether() \
/ IPv6(dst = args.target) \
/ IPv6ExtHdrFragment(id = second_pkt_id, m = 0, offset = 1, nh = 17) \
/ 'doar-e ftw'
sendp(reassembled_pkt_2, iface = args.iface)

이를 통해 길이가 0xFFFF가 넘는 재조립 패킷을 만들 수 있습니다.

다음과 같이 0xFFE8 + 0x28 형태의 Nested fragment 패킷을 전송하여 길이 연산결과가 0x10010되게 만듭니다.

이때 잘림 현상에 의해서 NetioRetreatNetBuffer로 전달되는 메모리 할당 길이는 0x10이 되고

NdisGetDataBuffer로 요구하는 데이터의 크기는 0x10010이됩니다. 따라서 NdisGetDataBufferNULL을 반환하게되고 NULL 포인터 역참조가 발생합니다.

Video#

Patch#

Ipv6pReassembleDatagram의 코드가 다음과 같이 패치된 것을 볼 수 있습니다.

해당 코드에서 나타나있는 v30BytesNeeded를 구할 때 사용한 v3(확장 헤더 영역의 길이)와 데이터 길이로 구성되는 재조합된 패킷의 총 길이를 의미합니다.

이 길이가 0xFFFF가 넘어갈 때 해당 재조립 패킷을 폐기시켜 앞서 알아본 바이트 잘림에 의한 NdisGetDataBufferNULL을 반환하는 상황을 방지합니다.

Mitigation#

취약점은 해당 취약점에 대한 보안 업데이트를 다운로드 받아 적용하거나 IPv6의 재조립 패킷을 처리하는 중 발생하기 때문에 다음과 같이 IPv6의 재조립 기능을 비활성화시켜 취약점을 완화시킬 수도 있습니다.

Terminal window
Netsh int ipv6 set global reassemblylimit=0

References#