[하루한줄] CVE-2021-31956 Exploiting the Windows Kernel (NTFS with WNF) – Part 1

URL

Target

  • < Windows 10 20H2

Explain

CVE-2021-31956은 카스퍼스키가 5월에 실제 공격에서 발견한 제로데이 취약점입니다. 이 취약점은 ntfs.sys 드라이버의 NtfsQueryEaUserEaList라는 함수에서 발생합니다.

__int64 __fastcall NtfsQueryEaUserEaList(__int64 a1, __int64 eas_blocks_for_file, __int64 a3, __int64 out_buf, unsigned int out_buf_length, unsigned int *a6, char a7)
{

  unsigned int padding; // er15
  padding = 0;

   for ( i = a6; ; i = (unsigned int *)((char *)i + *i) )
    {
      if ( i == v11 )
      {
        v15 = occupied_length;
        out_buf_pos = (_DWORD *)(out_buf + padding + occupied_length);
        if ( (unsigned __int8)NtfsLocateEaByName(
                                ea_blocks_for_file,
                                *(unsigned int *)(a3 + 4),
                                &DestinationString,
                                &ea_block_pos) )
        {
          ea_block = (FILE_FULL_EA_INFORMATION *)(ea_blocks_for_file + ea_block_pos);
          ea_block_size = ea_block->EaNameLength + ea_block->EaValueLength + 9;           // Attacker controlled from Ea
          if ( ea_block_size <= out_buf_length - padding )                                // The check which can underflow
          {
            memmove(out_buf_pos, ea_block, ea_block_size);
            *out_buf_pos = 0;
            goto LABEL_8;
          }
        }

           *((_BYTE *)out_buf_pos + *((unsigned __int8 *)v11 + 4) + 8) = 0;
LABEL_8:
            v18 = ea_block_size + padding + v15;
            occupied_length = v18;
            if ( !a7 )
            {
              if ( v23 )
                *v23 = (_DWORD)out_buf_pos - (_DWORD)v23;
              if ( *v11 )
              {
                v23 = out_buf_pos;
                out_buf_length -= ea_block_size + padding;
                padding = ((ea_block_size + 3) & 0xFFFFFFFC) - ea_block_size;
                goto LABEL_24;
              }
            }

이 함수에서는 반복문을 돌면서 NTFS Ea(Extended Attribute)의 내용을 output buffer에 복사합니다. 복사할 내용의 길이는 ea_block_size = ea_block->EaNameLength + ea_block->EaValueLength + 9;를 통해 EA 이름의 길이와 값의 길이에 9를 더해 계산 됩니다. 그리고 복사 직전에는 ea_block_size <= out_buf_length - padding를 통해 복사할 블록의 사이즈가 남은 output buffer의 사이즈보다 큰지 확인합니다. 복사된 내용은 32비트로 정렬되기 때문에 이 때 이전 블록의 사이즈에 맞춰 padding = ((ea_block_size + 3) & 0xFFFFFFFC) - ea_block_size;로 계산된 패딩 사이즈를 빼고 계산 됩니다. 즉 padding 사이즈는 1~3 사이의 값을 가지게 됩니다. out_buf_length는 인자로 전달된 값에서 ea_black_size와 padding을 더한 값을 계속 뺍니다. 그렇다면 우연히 정확하게 out_buf_length가 딱 정확히 0이 될 상황이 발생할 수 있겠죠? 네. out_buf_length가 0이 되는 순간 ea_block_size <= out_buf_length - padding에서 integer underflow가 발생합니다. 따라서 다음 EA 블럭의 데이터를 output buffer가 할당된 바로 뒤의 버퍼에 덮어쓸 수 있습니다. output buffer는 pool로 할당이 되기 때문에 pool overflow? oob write? 가 되겠네요.

이 글에서는 취약점 내용뿐만 아니라 트리거 방법, 익스 방법에 대한 내용도 다루고 있습니다. 그리고 2편에서는 reliable하게 익스하는 방법에 대해서 다룬다고 하니 관심 있으신 분들은 한번 살펴보시길 바랍니다. 저도 angr 시리즈가 끝나면 윈도우 커널 취약점들 분석해서 연구글 올려보도록 하겠습니다.