Security

how to exploit window kernel (1)

Jest 2017. 7. 21. 16:22


이 글은 Windows Device Driver에 대한 Exploit을 작성하는 방법에 대한 연재입니다.

어디까지 작성할 수 있을지는 모르겠지만 최대한 작성해 보도록 하겠습니다.


설명을 위해 존칭은 생략합니다.



Attack Vector


아래는 윈도우 아키텍쳐를 나타내는 그림이다.


https://upload.wikimedia.org/wikipedia/commons/5/5d/Windows_2000_architecture.svg


Windows NT 3.5 이전 Win32 서브 시스템은 Fast LPC를 이용하여 Operating System, Window Manager, GDI와 상호 작용하며,

Win32 API를 제공함으로써 클라이언트-서버 모델의 서버 사이드 구현인 CSRSS 프로세스와의 통신을 할 수 있도록 한다.

이를 위해 많은 부분이 성능을 위한 최적화가 이루어졌다.


하지만 이런 노력에도 불구하고 Windows NT 4.0부터는 서버 사이드 구현의 대부분을 커널 모드로 마이그레이션 해버렸는데, 커널로 옮겨진 Window Manager, GDI를 관리하기 위해 Win32k.sys가 도입되었다.


기존과는 달리 서버 사이드가 커널 모드에서 구현되었기 때문에 Win32k.sys는 커널 모드에서 사용자 모드 클라이언트로 제어를 넘겨야만 할 필요성이 생겼고, 이를 위해 사용자 모드 콜백 매커니즘이 구현되었다.

이 사용자 콜백 매커니즘은 Win32k가 호출을(제어권을) 사용자 모드로 되돌리는 역할을 한다.


따라서 전통적인 Window Kernel Exploit들은 사용자 모드 콜백 구현 부분을 공격하여 커널 모드 권한을 획득하는 것이 주류를 이루었다.(MS11-034, MS11-054).

(심지어 최근에도 사용된다.)


사용자 모드 콜백 공격을 통한 커널 모드 권한 획득에 관한 자세한 정보는 Tarjei Mandt의 Kernel Attacks through User-Mode Callbacks 를 참고하도록 한다. 

이 문서는 Windows Kernel Exploiter들이라면 반드시 읽어봐야 하는 문서이다.


사용자 모드 콜백 구현을 공격하는 것 외에 Window Kernel Exploit 방법으로 디바이스 드라이버를 직접 공격하는 것도 역시 인기있는 공격이다.

위의 그림에서 볼 수 있듯이 디바이스 드라이버는 커널 모드에서 동작하기 때문에, 만약 디바이스 드라이버의 취약점을 공격한다면 커널 모드 권한을 획득할 수 있다.

디바이스 드라이버들은 보통 3rd Party들에 의해 제공되는 경우가 많이 때문에 취약점이 존재할 확률이 상대적으로 높다.


여기서는 디바이스 드라이버를 Attack Vector로 하는 Exploit을 다루도록 한다.



Device Driver


디바이스 드라이버는 하드웨어 디바이스를 제어하기 위한 소프트웨어라고 할 수 있다.

따라서 드라이버는 디바이스와 통신을 할 수 있으며 디바이스의 데이터를 읽거나 쓸 수도 있다.


아래는 가장 간단한 형태의 드라이버 정의이다.


http://msdn.microsoft.com/dynimg/IC535114.png


위의 그림은 디바이스 드라이버의 동작을 많이 단순화 한 그림이다.

Application은 OS를 통해 드라이버 코드와 상호 작용을 하며, 드라이버 코드는 디바이스와의 통신을 통해 Application의 데이터를 디바이스까지 전달하거나 디바이스의 데이터를 Application으로 전달한다.


또 다른 형태의 드라이버를 보자.


https://i-msdn.sec.s-msft.com/dynimg/IC535115.png


위 그림처럼 모든 드라이버가 디바이스와 통신을 하는 것은 아니다.

드라이버는 계층적으로 여러 개가 존재할 수 있으며 드라이버 스택 상하의 데이터를 검증하거나 단순히 전달하는 기능만을 가지기도 한다.


아래는 사용자 모드와 커널 모드 구성 요소 간 통신을 보여주는 다이어그램이다.


https://i-msdn.sec.s-msft.com/dynimg/IC535109.png


그 외 디바이스 드라이버에 대한 자세한 설명은 아래 Microsoft의 페이지를 참고하도록 한다.

https://msdn.microsoft.com/ko-kr/library/windows/hardware/ff554690(v=vs.85).aspx



디바이스 드라이버 샘플 코드


아래는 github에 올라와 있는 디바이스 드라이버의 샘플 코드 중 일부이다.

드라이버 코드는 WDM 또는 WDF를 이용하여 작성할 수 있는데, WDM은 전통적인 드라이버 작성 방식이며 WDF는 Window Driver Foundation의 약자로 비교적 최근에 나온 프레임워크를 이용하는 방식이다.

Win32 API 와 MFC라고 생각하면 이해가 쉬울 것이다.


여기에서는 WDM 코드를 예제로 사용하며 중요한 부분만 설명하고 넘어가도록 하겠다.


아래는 ReadFile/ReadFileEx/WriteFile/WriteFileEx를 사용하지 않는 범용적인 코드 샘플이다.

(https://github.com/Microsoft/Windows-driver-samples/blob/master/general/ioctl/wdm/sys/sioctl.c)


코드를 보자.



DRIVER_INITIALIZE DriverEntry;

DRIVER_UNLOAD SioctlUnloadDriver;


DriverObject->DriverUnload = SioctlUnloadDriver;

드라이버가 로딩될 때의 진입점과 언로딩될 때의 진입점을 선언한다.

일반적으로 사용자 모드 어플리케이션의 main과 같은 역할을 하는 것이 DriverEntry이다.


_Dispatch_type_(IRP_MJ_CREATE)

_Dispatch_type_(IRP_MJ_CLOSE)

DRIVER_DISPATCH SioctlCreateClose;


DriverObject->MajorFunction[IRP_MJ_CREATE] = SioctlCreateClose;

DriverObject->MajorFunction[IRP_MJ_CLOSE] = SioctlCreateClose;


IRP는 I/O Request Packet의 약자이며 드라이버로 전송되는 요청의 대부분은 IRP 구조체를 이용해서 전달된다고 생각하면 된다.

위는 미리 정의된 IRP Type들 중 IRP_MJ_CREATE/IRP_MJ_CLOSE를 SioctlCreateClose 함수에서 처리하겠다고 선언한 것이다.

IRP_MJ_CREATE는 CreateFile()/CreateFileEx(), IRP_MJ_CLOSE는 CloseHandle()과 매핑된다.


IRP 구조체는 아래와 같다.

https://msdn.microsoft.com/ko-kr/library/windows/hardware/ff550694(v=vs.85).aspx


다음 코드를 보자.

_Dispatch_type_(IRP_MJ_DEVICE_CONTROL)

DRIVER_DISPATCH SioctlDeviceControl;


DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = SioctlDeviceControl;


역시 미리 정의된 IRP_MJ_DEVICE_CONTROL을 SioctlDeviceControl 함수에서 처리하겠다고 선언한 것이며, DeviceIoControl과 매핑된다.

이 함수는 일반적인 I/O가 아닌 드라이버를 제어하기 위한 사용자 정의 I/O Control Code를 처리하는 함수이다.


아래는 미리 정의된 Major IRP들의 리스트다.

https://msdn.microsoft.com/en-us/library/windows/hardware/ff550710(v=vs.85).aspx


코드의 다음 부분을 보자.

IoCreateDevice 함수를 이용해 디바이스를 생성한다.

Device type에 따라 여러 종류의 디바이스를 생성할 수 있는데 자세한 것은 MSDN을 참고하기 바란다.

예제 코드에서는 FILE_DEVICE_UNKNOWN을 사용했다.


NT Device name과 Win32 Name 간 Symbolic link를 생성한다.

디바이스는 NT Device name과 Win32 Name를 모두 가져야 하며, 사용자 모드 어플리케이션에서 접근하기 위한 용도로 사용된다.


IRP_MJ_READ / IRP_MJ_WRITE에 대해 어떤 처리도 없이 STATUS_SUCCESS를 반환한다.

(사용자 모드 어플리케이션에서는 ReadFile(Ex) / WriteFile(Ex)를 사용하지 않는다.)


아래는 IRP를 다루는 부분이다.

IoGetCurrentIrpStackLocation() 함수를 이용해 I/O Stack Location에 포함된 IO_STACK_LOCATION 구조체의 구조체의 포인터를 가져온다.

즉, 사용자 모드 어플리케이션에서 IRP 구조체를 이용해서 전달된 정보를 가져오기 위함이며,

모든 드라이버는 현재 request의 파라미터를 얻기 위해서 각 IRP에 대해 이 함수를 반드시 호출해야만 한다.

이후 획득한 포인터를 이용해 사용자 모드 어플리케이션에서 전달된 입/출력 버퍼의 길이를 얻어온다.


아래는 드라이버 코드에서 가장 중요한 부분이다.

switch ( irpSp->Parameters.DeviceIoControl.IoControlCode )

case IOCTL_SIOCTL_METHOD_BUFFERED:

case IOCTL_SIOCTL_METHOD_NEITHER:

case IOCTL_SIOCTL_METHOD_IN_DIRECT:

바로 위의 코드에서 IRP를 이용해 사용자 모드 어플리케이션에서 전달된 입/출력 버퍼의 길이를 얻어왔다.

사용자 모드 어플리케이션에서 버퍼를 통해 전달된 데이터를 처리하는 방식은 IoControlCode로 구분하며 위와 같이 3가지 종류가 있다.

IoControlCode에 종류에 따라 참고해야 할 버퍼의 위치와 처리 방식이 다르기 때문에 유의해야 하는 부분이다.


1) METHOD BUFFERED

METHOD BUFFERED 방식은 일반적으로 작은 양의 데이터를 전송할 경우에 사용되며 버퍼를 이용해 데이터를 전달하는 방식이다.

사용자 모드 어플리케이션에서 전달된 데이터는 시스템 주소 공간의 임시 영역으로 복사되며 드라이버는 해당 공간의 포인터를 사용한다.

해당 버퍼의 위치는 Irp->AssociatedIrp.SystemBuffer이다.


2) METHOD DIRECT (METHOD_IN_DIRECT & METHOD_OUT_DIRECT)

METHOD_IN_DIRECT의 경우 입력 버퍼는 METHOD BUFFERED와 동일하게 Irp->AssociatedIrp.SystemBuffer를 사용하는 반면 출력 버퍼는 MDL을 이용한다.

이 방식은 MDL 생성 시의 overhead는 있지만 별도의 데이터 복사 작업이 없어서 대용량 데이터 전달에 일반적으로 사용된다.


MDL는 Memory Descriptor List의 약자로써, 사용자 모드 페이지를 커널 모드 메모리로 매핑시키는데 사용되는 구조체이다.

이를 위해 사용되는 함수가 MmGetSystemAddressForMdlSafe이며 해당 함수의 리턴 값은 해당 함수에 전달된 사용자 모드 버퍼에 매핑되는 커널 모드 메모리 주소이다. 사용자 모드 어플리케이션과 디바이스 드라이버가 사용하는 가상 주소는 다르지만 실제 가상 주소가 매핑되는 물리 어드레스는 동일하게 된다.


METHOD_OUT_DIRECT의 경우 위와 같이 METHOD_IN_DERECT보다 추가되는 코드가 있다. 바로 MDL에 디바이스에서 사용자 모드 어플리케이션으로 보내고자 하는 데이터를 쓰는 것이다.


3) METHOD NEITHER

METHOD_NEITHER은 앞의 방식들에 비해서 좀 독특한데, 데이터를 보내는 것이 아니라 단순히 사용자 모드 버퍼의 주소를 보내기만 한다.

드라이버는 위의 코드에서 보듯이 Type3InputBuffer를 통해 전달된 사용자 모드 버퍼의 주소에 접근하여야 한다.

따라서 드라이버는 해당 주소에 접근하기 전 아래와 같이 해당 주소가 유효한 주소인지 반드시 확인해야만 한다.



예제에는 없지만 쓰기 전에도 역시 유효한 주소인지 반드시 확인해야만 하며 ProbeForWrite() 함수를 사용한다.


예제에서는 사용자 버퍼 주소로의 직접 접근 외에 MDL을 통해 안전하게 접근하는 방법이 추가되어 있으며 내용은 DIRECT METHOD 때와 동일하므로 생략하도록 한다.


이상으로 디바이스 드라이버 샘플 코드에 대한 간단한 분석을 마친다.

자세한 내용은 이봉석님의 입문자를 위한 디바이스 드라이버 시리즈를 볼 것을 추천한다.

(https://www.youtube.com/watch?v=jsXHLMDIokM)



다음 포스트에는  취약한 드라이버들을 공략해 보도록 하겠다.