[Translation] Exploiting System Mechanic Driver Part 1

서론

중간고사 망한 L0ch입니다. 아직 3과목이나 남았지만 눈에 띄는 글이 있어 망한 김에 시험공부는 던지고 번역해봤습니다. 인생..

교수님 이제 그만 절 놓아주세요..

커널 드라이버 취약점에 대해 알기 쉽게 정리한 글이 있어서 파트 2로 나눠서 정리해봤습니다. 파트 1에서는 드라이버 구조에 대한 소개와 IOCTL, 드라이버 퍼징에 대한 내용을 다루며 파트 2에서는 본격적인 드라이버 취약점의 root cause 분석과 익스플로잇에 대해 다룹니다!

원문 글 : Exploiting System Mechanic Driver


지난달 last&VoidSec은 NULLCON에서 Ashfaq Ansari(@HackSysTeam)의 Windows Kernel Exploitation Advanced 과정을 수강했다. 이 과정은 흥미로웠으며 core kernel 공간의 개념과 mitigation 우회 및 exploit에 대해 다루었다. 마지막 실습인 Write an exploit for System Mechanics 에서는 더 이상의 힌트는 제공되지 않았으며 우리는 새로 습득한 지식과 교육 자료에 대한 이해와 리버스 엔지니어링을 테스트하기 위해 도전했다. 이 실습을 어떻게 수행했는지, 그리고 사전 지식 없이 실제 드라이버를 어떻게 활용했는지에 대해 자세히 알아보도록 한다.

Windows drivers 101

드라이버를 리버스 엔지니어링하고 취약점을 찾기 전에 먼저 드라이버가 무엇이며 어떻게 작동하는지 살펴봐야 한다. Windows에서 드라이버는 기본적으로 특정 이벤트가 발생할 때 커널 컨텍스트에서 실행되는 코드를 포함하는 로드 가능한 모듈로 정의할 수 있다. 이벤트는 운영체제가 수행하는 인터럽트 또는 프로세스일 수 있으면 커널은 이러한 인터럽트를 처리하고 요청을 수행하기 위해 적절한 드라이버를 실행한다. 따라서 드라이버 = 커널의 DLL이라고 생각할 수 있다. 드라이버는 프로세스 탐색기에서 시스템 프로세스(PID 4가 있는 모듈) 내에서 로드된 모듈로 나열된다.

exploiting-driver/Untitled.png

PID 4(System Process)에 로드된 모듈

Driver Entry

이제 드라이버의 구조를 살펴보겠다. 대부분의 코드와 마찬가지로 드라이버에는 DriverEntry라는 일종의 main 함수가 존재한다. 이 기능은 다음과 같이 Microsoft 문서에 정의되어 있다.

NTSTATUS DriverEntry (
  _In_ PDRIVER_OBJECT DriverObject,
  _In_ PUNICODE_STRING RegistryPath
) ;

인수 앞의 _In_ 은 인수가 DriverEntry 함수에 전달되는 입력값이어야 한다는 의미이다. DriverObject 인수는 드라이버에 대한 정보를 보유하는 DRIVER_OBJECT 데이터 구조에 대한 포인터를 나타내며 이후에 더 자세히 설명하도록 하겠다. RegistryPath 인수는 드라이버 이미지(커널이 드라이버 코드를 로드하는 .sys 파일)의 레지스트리 경로를 포함하는 UNICODE_STRING 구조체 (UTF-16 문자열 및 기타 제어정보를 포함하는 구조체)에 대한 포인터이다.

유저 모드에서 액세스 하려면 드라이버가 표준 유저 프로세스에 액세스할 수 있도록 장치와 심볼릭 링크(a.k.a symlink)를 만들어야 한다. 장치는 프로세스가 드라이버와 상호작용 할 수 있도록 하는 인터페이스이며 심볼릭 링크는 Win32함수를 호출하는 동안 사용할 수 있는 장치 이름(alias-별칭)이다.

심볼릭 링크의 예: C:\는 저장장치를 위한 심볼릭 링크에 불과하다. SysinternalsWinObj 툴을 사용해 root namespace 아래의 GLOBAL?? 디렉터리에서 C:를 찾을 수 있다.

exploiting-driver/Untitled%201.png

드라이버는 IoCreateDeviceIoCreateSymbolicLink를 사용하여 장치 및 심볼릭 링크를 만든다. 드라이버를 리버스 엔지니어링 할 때 위 두 함수가 연속적으로 호출이 장치와 심볼릭 링크를 인스턴스화 하는 부분인 것을 확인할 수 있다. 대부분의 드라이버는 하나의 장치만 사용하므로 대부분 한 번만 발생하며 일반적으로 장치 이름은 \Device\VulnerableDevice 형식을 사용한다. 반면 심볼릭 링크는 \\.\VulnerableDeviceSymlink 형식과 유사하다. 이제 드라이버의 “frontend”에 대해 설명했으므로 “backend”인 디스패치 루틴에 대해 설명하겠다.

Dispatch Routines

드라이버는 장치에서 호출되는 함수에 따라 다른 작업(a.k.a 함수/루틴)을 실행한다. 드라이버는 WriteFile, ReadFile 또는 DeviceIoControl API가 해당 장치에서 호출될 때 각각 다르게 작동할 수 있다. 이 동작은 DriverObject구조체의 함수 포인터 배열인 MajorFunctions멤버를 통해 드라이버 개발자가 제어한다. WriteFile, ReadFile 또는 DeviceIoControl과 같은 API는 MajorFunctions 내에 해당 인덱스가 있어 관련 함수 포인터가 호출된다. 일부 매크로를 사용하여 관련 인덱스를 기억할 수 있으며 다음은 몇 가지 예다.

  • IRP_MJ_CREATE : CreateFile 호출 후 호출되는 함수 포인터를 포함하는 인덱스
  • IRP_MJ_READ : ReadFile과 같은 함수와 관련된 인덱스
  • IRP_MJ_DEVICE_CONTROL : DeviceIoControl에 해당하는 인덱스

드라이버 개발자가 MyDriverRead 라는 함수를 정의했으며 프로세스가 드라이버 장치에서 ReadFile API를 호출할 때 호출되기를 원한다고 가정해본다. DriverEntry 내부 (또는 호출된 함수)에 다음 코드를 작성하면 된다.

DriverObject->MajorFunctions[IRP_MJ_READ] = MyDriverRead;

이와 같은 함수는 디스패치 루틴의 이름을 사용한다. 그러나 MajorFunctions는 제한된 크기의 배열이므로 드라이버에 많은 디스패치 루틴을 할당하지 못한다. 이 문제점은 유저 모드 함수 DeviceIoControl 를 사용하는 이유가 된다.

DEVICEIOCONTROL & IOCTL Codes

IRP_MJ_DEVICE_CONTROL로 정의된 MajorFunctions 내에는 특정 인덱스가 있다. 이 인덱스에는 드라이버 장치에서 DeviceIoControl API 호출 후 호출되는 디스패치 루틴의 함수 포인터가 저장된다. 이 함수는 인수 중 하나가 IOCTL로 알려진 32 비트 정수이기 때문에 매우 중요하다. 이 I/O 코드는 드라이버에 전달되고 DeviceIoControl을 통해 전달되는 IOCTL에 따라 각각의 작업을 수행한다. 인덱스 IRP_MJ_DEVICE_CONTROL의 디스패치 루틴은 코드의 다음과 같은 스위치 케이스처럼 작동한다.

switch(IOCTL)
{
    case 0xDEADBEEF:
        DoThis();
        break;
    case 0xC0FFEE;
        DoThat();
        break;
    case 0x600DBABE;
    DoElse();
    break;
}

이러한 방식으로 개발자는 프로세스에서 전송하는 IOCTL 코드에 따라 드라이버가 함수를 호출하도록 할 수 있다. 이러한 종류의 “코드 지문”은 드라이버를 리버스 엔지니어링 하는 동안 찾기가 매우 쉽기 때문에 매우 중요하다. 어떤 IOCTL이 어떤 코드로 연결되는지 알면 드라이버를 더 쉽게 분석하고 퍼징할 수 있다.

Reverse Engineering: Finding the IOCTL

드라이버 리버스 엔지니어링을 시작하기 전에 첫 번째로 해야 할 일은 통신에 사용하는 IOCTL 코드와 장치 이름 (symlink)을 찾는 것이다. 우리의 경우 대상 애플리케이션은 iolo – System Mechanic Pro v.15.5.0.61 (amp.sys)이며 설치 시 WinObj를 활용하여 다음과 같이 장치 이름과 권한을 복구했다.

exploiting-driver/Untitled%202.png

이제 장치 이름 \Device\AMP을 수집했으니 드라이버 amp.sys를 디스어셈블러 (IDA 사용)에 로드하고 누락된 경우 다음과 같은 필요한 구조체를 추가한다.

  • DRIVER_OBJECT
  • IRP
  • IO_STACK_LOCATION

DriverEntry 함수에 도달하면 드라이버가 생각보다 조금 더 복잡하다는 것을 알 수 있다. Imports 섹션에서 IoDeviceControl API를 xref하기로 결정했으며 그 결과는 SUB_2CFE0(DriverCreateDevice로 변경) 하나뿐이다.

exploiting-driver/Untitled%203.png

DeviceName이 인스턴스화 되고 DriverObject가 전달되는 것을 볼 수 있으므로 올바른 함수에 도달했음을 알 수 있고 디컴파일을 진행했다.

exploiting-driver/Untitled%204.png

MajorFunction[14] (offset 0x0e)을 살펴보면 IRP_MJ_DEVICE_CONTROL 드라이버를 찾을 수 있다. 이는 시스템 정의 IOCTL 세트가 있는 경우 DispatchDeviceControl 루틴에서 드라이버가 지원해야 하는 요청이다. SUB_2C580을 디컴파일하면 이 드라이버에 대한 IOCTL 코드에 도달할 수 있다. 아래의 디컴파일 코드를 보고 직접 IOCTL 코드를 찾아보자.

__int64 __fastcall sub_2C580(__int64 a1, IRP *a2)
{
  BOOLEAN v3; // [rsp+20h] [rbp-38h]
  ULONG v4; // [rsp+24h] [rbp-34h]
  _IO_STACK_LOCATION *v5; // [rsp+28h] [rbp-30h]
  unsigned int v6; // [rsp+30h] [rbp-28h]
  PNAMED_PIPE_CREATE_PARAMETERS v7; // [rsp+38h] [rbp-20h]
  a2->IoStatus.Information = 0i64;
  v5 = a2->Tail.Overlay.CurrentStackLocation;
  if ( v5->Parameters.Read.ByteOffset.LowPart == 2252803 )
  {
    v4 = v5->Parameters.Create.Options;
    v7 = v5->Parameters.CreatePipe.Parameters;
    v3 = IoIs32bitProcess(a2);
    v6 = sub_166D0(v3, v7, v4);
  }
  else
  {
    v6 = -1073741808;
  }
  a2->IoStatus.Status = v6;
  IofCompleteRequest(a2, 0);
  return v6;
}

찾을 수 없거나 분석이 추가된 코드를 원한다면 다음 코드를 살펴보자.

__int64 __fastcall Driver_IRP_MJ_DEVICE_CONTROL(DEVICE_OBJECT *DeviceObject, IRP *Irp)
{
  __int64 result; // rax
  _BYTE Is32BitProcess; // [rsp+20h] [rbp-38h]
  _DWORD bufferSize; // [rsp+24h] [rbp-34h]
  _QWORD IoStackLocation; // [rsp+28h] [rbp-30h]
  NTSTATUS status; // [rsp+30h] [rbp-28h]
  _QWORD userBuffer; // [rsp+38h] [rbp-20h]
  _QWORD; // [rsp+68h] [rbp+10h]
  Irp->IoStatus.Information = 0i64;
  IoStackLocation = Irp->Tail.Overlay.CurrentStackLocation;
  if ( IoStackLocation->Parameters.Read.ByteOffset.LowPart == 0x226003 )// IOCTL Code
  {
    bufferSize = IoStackLocation->Parameters.Create.Options;
    userBuffer = &IoStackLocation->Parameters.CreatePipe.Parameters->NamedPipeType;
    Is32BitProcess = IoIs32bitProcess(Irp);
    status = DriverVulnerableFunction(Is32BitProcess, userBuffer, bufferSize);
  }
  else
  {
    status = 0xC0000010;                        // STATUS_INVALID_DEVICE_REQUEST
  }
  Irp->IoStatus.Status = status;
  IofCompleteRequest(Irp, 0);
  return (unsigned int)status;
}

IOCTL 코드 0x226003는 IOCTL 요청과 함께 전달된 데이터 버퍼에 액세스할 때 커널이 사용하는 방법을 이해하기 위해 추가로 디코딩될 수 있다. OSR 온라인 IOCTL 디코더 툴을 사용하여 다음 정보를 복구할 수 있다.

exploiting-driver/Untitled%205.png

METHOD_NEITHER는 IOCTL 요청과 함께 전달된 데이터 버퍼에 액세스하는 데 사용되는 가장 취약한 방식이다. 이를 사용할 때 I/O 관리자는 유저 데이터에 대해 어떤 종류의 유효성 검사도 수행하지 않고 원시 데이터를 드라이버에 그대로 전달한다. 유효성 검사 없이 유저 데이터를 관리하는 코드에 있는 버그 또는 취약점을 발견할 확률이 더 높다. 이제 IOCTL 코드 0x226003와 장치 이름 \Device\AMP을 구했으니 드라이버를 퍼징하고 취약점을 찾을 수 있다.

Fuzzing

IOCTL 코드를 검색한 후 ioctlbf로 드라이버를 퍼징했다. Ioctlbf 구문은 이해하기 매우 쉽다. 먼저 장치 이름 -d 를 지정한 다음 퍼징할 IOCTL 코드를 -i로 지정한 뒤 -u 매개 변수를 사용하여 제공된 IOCTL 코드만 퍼징하도록 한다. (드라이버에 IOCTL 코드가 하나만 있다는 것을 이미 알고 있으므로 무차별 대입이 필요 없다.)

exploiting-driver/Untitled%206.png

ioctlbf를 실행한 직후 다음 메시지 amp+6c8d와 함께 디버기 머신에서 크래시가 발생했다.

Access violation - code c0000005 (!!! second chance !!!)
fffff801`3ae96c8d 488b0e          mov     rcx,qword ptr [rsi]
PROCESS_NAME:  ioctlbf.EXE
READ_ADDRESS:  0000000000000000 
ERROR_CODE: (NTSTATUS) 0xc0000005 - The instruction at 0x%p referenced memory at 0x%p. The memory could not be %s.
EXCEPTION_CODE_STR:  c0000005
EXCEPTION_PARAMETER1:  0000000000000000
EXCEPTION_PARAMETER2:  0000000000000000
STACK_TEXT:  
ffff9304`c35c66e0 ffffe60b`ecd87bb0     : 00000000`00000001 00000000`00000000 fffff801`35c23f8b ffff9304`c35c6700 : amp+0x6c8d
ffff9304`c35c66e8 00000000`00000001     : 00000000`00000000 fffff801`35c23f8b ffff9304`c35c6700 00000000`00000001 : 0xffffe60b`ecd87bb0
ffff9304`c35c66f0 00000000`00000000     : fffff801`35c23f8b ffff9304`c35c6700 00000000`00000001 ffffe60b`e5303c80 : 0x1

널 포인터 역 참조와 유사한 오류이다. 이제 드라이버를 리버스 엔지니어링해 access violation이 발생한 이유와 이를 악용할 수 있는 방법이 있는지 이해하기로 했다.


다음 파트는 취약점의 root cause 분석과 익스플로잇에 대한 번역으로 돌아오겠습니다!