이 글은 SVRD의 MS08-061: The case of the kernel mode double-fetch를 번역한 것입니다.

MS08-061은 win32k.sys의 여러가지 취약점을 패치합니다. 이 취약점들을 이용하면 임의의 코드를 커널 모드로 실행할 수 있게 됩니다. 로컬에서만 공격 가능하고, 원격지에서 공격할 수는 없는 것으로 확인된 버그들입니다.

이 취약점들 중 하나는 커널 모드에서 사용자 공간의 데이터를 읽어오는 과정에서 발생할 수 있는 경쟁 조건(race condition)으로 인한 것입니다. 보통 사용자 공간의 데이터에 접근할 때, 커널 모드 코드는 원본의 스냅샷을 만들어 사용하는 방식으로 사용자 공간의 데이터를 여러번 참조하지 않도록 합니다. 이렇게 하지 않으면 "double fetch"라 불리는 문제가 나타납니다.

커널에서 사용자 공간에 위치한 데이터를 먼저 복사해놓으면, 커널 주소 공간에 복사된 사본만 가지고 작업하므로 사용자 공간의 원본에 어떤 변경이 일어나더라도 영향 받지 않게 됩니다. 이렇게 해놓으면 사용자 공간에서 데이터를 가져올 때마다 값이 달라짐으로 인해 발생하는 경쟁 조건을 피할 수 있습니다.

MS08-061은 부적절하게 메모리풀을 할당하면서 오버플로우가 일어난 경우입니다. 아래 취약 코드를 한 번 보도록 하지요:

// 공격자가 lParam 값을 제어
void win32k_entry_point(...) {
   […]
      // lParam은 이미 ProbeForRead를 통과한 상태
      my_struct = (PMY_STRUCT)lParam;
      if (my_struct ->lpData) {
           cbCapture = sizeof(MY_STRUCT) + my_struct->cbData; // [1]
    […]
            // my_struct ->lpData는 이미 ProbeForRead를 통과한 상태
    […]
            if ( my_allocation = UserAllocPoolWithQuota(cbCapture, TAG_SMS_CAPTURE)) != NULL) {  
                 RtlCopyMemory(my_allocation, my_struct->lpData, my_struct->cbData); // [2]
            }
      }
   […]
}
위의 코드에서 보시다시피, [1]과 [2] 양쪽에서 같은 사용자 공간 데이터를 가져오고 있습니다. 커널은 두 번 다 동일한 값을 가져오도록 보장하지 않기 때문에, [1]에서 먼저 크기를 읽어와서 풀을 작게 할당한 다음, [2]에서 실제로 메모리 복사를 실행할 때 큰 값을 읽어들이면서 오버플로우가 발생하게 됩니다. 병렬성과 우선순위가 높은 상태에서 lParam 값을 계속 바꾸는 스레드를 돌린다면 쉽게 공격이 가능합니다.

커널 개발자는 사용자 공간에서 데이터를 가져오는 경우라면 double fetch가 발생하지 않도록 데이터를 먼저 고정해두어야 합니다. 위의 코드는 아래의 코드처럼 바꾸어야겠죠.

// 공격자가 lParam 값을 제어
void win32k_entry_point(...) {
  […]
      // lParam은 이미 ProbeForRead를 통과한 상태         
     my_struct = (PMY_STRUCT)lParam;
     cbData_captured= my_struct->cbData;
     lpData_captured = my_struct->lpData;
     if (lpData_captured) {
         cbCapture = sizeof(MY_STRUCT) + cbData_captured
    […]
         // lpData_captured has already passed successfully the ProbeForRead
    […]
         if ( my_allocation = UserAllocPoolWithQuota(cbCapture, TAG_SMS_CAPTURE)) != NULL)           {  
             RtlCopyMemory(my_allocation, lpData_captured, cbData_captured);  
         }
      }
    […]
}