## 10.2 用 Immunity fuzzing 驅動
我們需要使用 Immunity 強大的調試功能,掛鉤住 DeviceIoControl 函數,在數據到達目 標驅動之前,截獲它們,這就是我們 Driver Fuzzing 的基礎。如果一切順利,最后可以將一 些列工作寫出自動化的 PyCommand,我們只要喝著茶看著 Immunity 完成一切工作:截獲 DeviceIoControl,變形緩沖區數據,記錄相關信息,將控制權交還給目標程序。之所以要對 數據進行記錄,是因為每次成功的 fuzzing 都會引起系統奔潰,而記錄可以更好的還原崩潰 時發送的數據。
提示
確保不要在自己的機器上進行實驗。除非你想見到無數次的藍屏,重啟,最后就是 硬盤報銷的聲音,哈哈!老天保佑,我們還可以使用虛擬機,雖然它的模擬在某些底層細節 上不是很好,不過這可比硬盤便宜。
開動代碼。新建一個 Python 腳本 ioctl_fuzzer.py。
```
#ioctl_fuzzer.py
import struct
import random
from immlib import *
class ioctl_hook( LogBpHook ):
def init ( self ):
self.imm = Debugger()
self.logfile = "C:\ioctl_log.txt"
LogBpHook. init ( self )
def run( self, regs ):
"""
We use the following offsets from the ESP register to trap the arguments to DeviceIoControl:
ESP+4 -> hDevice
ESP+8 -> IoControlCode
ESP+C -> InBuffer
ESP+10 -> InBufferSize
ESP+14 -> OutBuffer
ESP+18 -> OutBufferSize
ESP+1C -> pBytesReturned
ESP+20 -> pOverlapped
"""
in_buf = ""
# read the IOCTL code
ioctl_code = self.imm.readLong( regs['ESP'] + 8 )
# read out the InBufferSize
inbuffer_size = self.imm.readLong( regs['ESP'] + 0x10 )
# now we find the buffer in memory to mutate
inbuffer_ptr = self.imm.readLong( regs['ESP'] + 0xC )
# grab the original buffer
in_buffer = self.imm.readMemory( inbuffer_ptr, inbuffer_size )
mutated_buffer = self.mutate( inbuffer_size )
# write the mutated buffer into memory
self.imm.writeMemory( inbuffer_ptr, mutated_buffer )
# save the test case to file
self.save_test_case( ioctl_code, inbuffer_size, in_buffer, mutated_buffer )
def mutate( self, inbuffer_size ):
counter = 0
mutated_buffer = ""
# We are simply going to mutate the buffer with random bytes
while counter < inbuffer_size:
mutated_buffer += struct.pack( "H", random.randint(0, 255) )[0]
counter += 1
return mutated_buffer
def save_test_case( self, ioctl_code,inbuffer_size, in_buffer, mutated_buffer ):
message = "***** | |\n"
message += "IOCTL Code: 0x%08x\n" % ioctl_code
message += "Buffer Size: %d\n" % inbuffer_size
message += "Original Buffer: %s\n" % in_buffer
message += "Mutated Buffer: %s\n" % mutated_buffer.encode("HEX")
message += "***** | |\n\n"
fd = open( self.logfile, "a" )
fd.write( message )
fd.close()
def main(args):
imm = Debugger()
deviceiocontrol = imm.getAddress( "kernel32.DeviceIoControl" )
ioctl_hooker = ioctl_hook()
ioctl_hooker.add( "%08x" % deviceiocontrol, deviceiocontrol )
return "[*] IOCTL Fuzzer Ready for Action!"
```
這里沒有用到任何新的 Immunity 知識,只是繼承了 LogBpHook 類,做了很小的擴展, 這一切都在第五章做了詳細的介紹。代碼非常清晰明了,顯示獲得傳遞給驅動的 IOCT 代 碼,輸入緩沖區長度,輸入緩沖區位置。接著通過對輸入數據的變形,創建一個包含了隨即 字符的新緩沖區,長度和輸入緩沖區一樣。之后將新緩沖區的數據寫入原緩沖區,保存測試 樣例。最后把控制權交還給用戶程序。
記得把 ioctl_fuzzer.py 放到 PyCommands 目錄下。這樣我們就能使用 ioctl_fuzzer 命令 fuzz 任何使用 IOCTLs 了的程序(嗅探器,防火墻,或者殺毒軟件)。表 10-1 是 Wireshark 的 fuzz 結果。
```
*****
IOCTL Code: 0x00120003
Buffer Size: 36
Original Buffer: 0000000000000000000100000001000000000000000000000000000000000000
Mutated Buffer: a4100338ff334753457078100f78bde62cdc872747482a51375db5aa2255c46e
*****
*****
IOCTL Code: 0x00001ef0
Buffer Size: 4
Original Buffer: 28010000
Mutated Buffer: ab12d7e6
*****
```
Listing 10-1: Wireshark 的 fuzzing 輸出
在 我 們 將 一 大 堆 的 垃 圾 扔 給 驅 動 器 之 后 , 在 于 發 現 了 兩 個 可 用 的 IOCTL 代 碼 0x00001ef0 和 0x0012003 。如果要繼續測試,就必須不斷的和用戶模式下的 Wireshark 進行 交互,這樣 Wireshark 就會調用不同 IOCTL 代碼,最后祈禱上帝讓其中一個 IOCTL 處理代 碼發生崩潰。
雖然這樣做很簡單,也確實很夠找出漏洞。不過還是不夠聰明。舉個例子,我們并不知 道正在 fuzzing 的設備名,(不過可以通過 hook CreateFileW,然后觀察被 DeviceIoControl 使用了的句柄,從而逆推得到設備名 ),而且 fuzz 的 IOCTL 代碼并不全,我們在用戶模式 下對程序進行的操作是有限的,這樣程序對驅動功能的調用也是有限的。這就像碰運氣。我 們期待的是一個更加聰明的 fuzzer,它能對所有的 IOCTL 不間斷的 fuzzing,直到你的硬盤 報銷,或者在這之前發現一個漏洞。
這可能嗎,可能,先從我們偉大的 Immunity 攜帶的 driverlib 庫開始。使用 driverlib 我 們能枚舉出驅動程序所有的設備名和 IOCTL 代碼。把這些結合起來就能夠實現一個高效, 獨立,全自動化的 fuzzer 了,這是一個偉大的進步,解放雙手,不做野蠻人。Let’s get cracking。
### 10.3.1 找出設備名
用 Immunity 內建的 driverlib 庫找出設備名很就當。讓我們看看 driverlib 是怎么實現這 個功能的。
```
def getDeviceNames( self ):
string_list = self.imm.getReferencedStrings( self.module.getCodebase() )
for entry in string_list:
if "\\Device\\" in entry[2]:
self.imm.log( "Possible match at address: 0x%08x" % entry[0], address =
entry[0] )
self.deviceNames.append( entry[2].split("\"")[1] )
self.imm.log("Possible device names: %s" % self.deviceNames)
return self.deviceNames
```
Listing 10-2: driverlib 庫找出設備名的方法
代碼通過檢索驅動中所有被引用了的字符串,找出其中包含了 "\Device\"的項。這項就可能是驅動程序注冊了的符號鏈接,用來讓用戶模式下的程序調用的。我們就使用 C:\WINDOWS\System32\beep.sys 測試以下看看。以下操作都在 Immunity 中進行。
```
*** Immunity Debugger Python Shell v0.1
*** Immlib instanciated as 'imm' PyObject READY.
>>> import driverlib
>>> driver = driverlib.Driver()
>>> driver.getDeviceNames() ['\\Device\\Beep']
>>>
```
我們很簡單的使用三行代碼就找到了一個可用的設備名 \\Device\\Beep,這省去了我們通 過反匯編一行行查找代碼的時間。Simple is Beautiful!下面看看 driverlib 是如何查找 IOCTL dispatch function(IOCTL 調度函數)和 IO IOCTL codes( IOCTL 代碼)的。
任何驅動要實現 IOCTL 接口,都必須有一個 IOCTL dispatch 負責處理各種 IOCTL 請求。 當驅動被加載的似乎后,第一個訪問的函數就是 DriverEntry。DriverEntry 的主要框架如下:
```
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
UNICODE_STRING uDeviceName;
UNICODE_STRING uDeviceSymlink; P
DEVICE_OBJECT gDeviceObject;
RtlInitUnicodeString( &uDeviceName, L"\\Device\\GrayHat" );
RtlInitUnicodeString( &uDeviceSymlink, L"\\DosDevices\\GrayHat" )
// Register the device
IoCreateDevice( DriverObject, 0, &uDeviceName, FILE_DEVICE_NETWORK, 0, FALSE, &gDeviceObject );
// We access the driver through its symlink
IoCreateSymbolicLink(&uDeviceSymlink, &uDeviceName);
// Setup function pointers
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IOCTLDispatch;
DriverObject->DriverUnload = DriverUnloadCallbac
DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverCreateCloseCa
DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverCreateCloseCa
return STATUS_SUCCESS;
}
```
Listing 10-3: DriverEntry 的 C 源碼實現
這是一個非常基礎的 DriverEntry 代碼框架,但是很直觀的說明了設備是如何初始化的。 要注意的是這行:
```
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IOCTLDispatch
```
這行告示驅動器 IOCTLDispatch 負責所有 IOCTL 請求。當一個驅動器編譯完成后,這 行程序的匯編偽代碼如下:
```
mov dword ptr [REG+70h], CONSTANT
```
這指令集看起來有些特殊,REG 和 CONSTANT 都是匯編代碼,IOCTLDispatch 指針將 被 存 儲 在 (REG) 位 移 0x70 的 地 方 上 。 使 用 這 些 指 令 , 我 們 就 能 找 出 IOCTL 處 理 代 碼 CONSTANT,也就是 IOCTLDispatch,接著順藤摸瓜找出 IOCTL 代碼。driverlib 的具體實 現如下:
```
def getIOCTLDispatch( self ):
search_pattern = "MOV DWORD PTR [R32+70],CONST"
dispatch_address = self.imm.searchCommandsOnModule( self.module
.getCodebase(), search_pattern )
# We have to weed out some possible bad matches
for address in dispatch_address:
instruction = self.imm.disasm( address[0] )
if "MOV DWORD PTR" in instruction.getResult():
if "+70" in instruction.getResult():
self.IOCTLDispatchFunctionAddress = instruction.getImmConst() self.IOCTLDispatchFunction =
self.imm.getFunction( self.IOCTLDispatchFunctio
break
# return a Function object if successful
return self.IOCTLDispatchFunction
```
Listing 10-4: 找出 IOCTL dispatch function 的方法
最新的 Immunity 中還有另一種列舉函數搜索的方法,不過原理都一樣。一旦我們找到 了合適的函數,就將這個函數對象返回,在后面 IOCTL 代碼查找中將會用它。
下面來看看 IOCTL dispatch 的函數是如何實現的,以及如何查找出所有的 IOCTL 代碼。
### 10.3.3 找出 IOCTL 代碼
IOCTL dispatch 根據傳入的值(也就是 IOCTL 代碼)執行相應的操作。這也是我們千方 百計要找出所有 IOCTL 的原因,因為 IOCTL 就相當于用戶模式下你調用的"函數"。讓我們 先看一段用 C 實現的 IOCTL dispatch,之后我們反匯編它們,并從中找出 IOCTL 代碼。
```
NTSTATUS IOCTLDispatch( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp )
{
ULONG FunctionCode;
PIO_STACK_LOCATION IrpSp;
// Setup code to get the request initialized
IrpSp = IoGetCurrentIrpStackLocation(Irp);
FunctionCode = IrpSp->Parameters.DeviceIoControl.IoControlCode;
// Once the IOCTL code has been determined, perform a
// specific action
switch(FunctionCode)
{
case 0x1337:
// ... Perform action A case 0x1338:
// ... Perform action B case 0x1339:
// ... Perform action C
}
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest( Irp, IO_NO_INCREMENT );
return STATUS_SUCCESS;
}
```
Listing 10-5: 一 段 簡 單 的 IOCTL dispatch 代 碼 支 持 三 種 IOCTL 代 碼 (0x13370x1338, 0x1339)
當函數從 IOCTL 請求中檢索到 IOCTL 代碼的時候,就將代碼傳遞個 switch{}語句,然 后根據 IOCTL 代碼執行相應的操作。switch 語句在匯編之后有可能是以下兩種形式。
```
// Series of CMP statements against a constant
CMP DWORD PTR SS:[EBP-48], 1339 # Test for 0x1339
JE 0xSOMEADDRESS # Jump to 0x1339 action
CMP DWORD PTR SS:[EBP-48], 1338 # Test for 0x1338 JE 0xSOMEADDRESS
CMP DWORD PTR SS:[EBP-48], 1337 # Test for 0x1337 JE 0xSOMEADDRESS
// Series of SUB instructions decrementing the IOCTL code
MOV ESI, DWORD PTR DS:[ESI + C] # Store the IOCTL code in ESI
SUB ESI, 1337 # Test for 0x1337
JE 0xSOMEADDRESS # Jump to 0x1337 action SUB ESI, 1 # Test for 0x1338
JE 0xSOMEADDRESS # Jump to 0x1338 action
SUB ESI, 1 # Test for 0x1339
JE 0xSOMEADDRESS # Jump to 0x1339 action
```
Listing 10-6: 兩種不同的 switch{}反匯編指令
switch{} 的反匯編指令有很多種,不過最常見的就是上面兩種。在第一種情況下,我 們可以通過一些列的 CMP 指令,找到進行比較的常量,這些就是 IOCTL 代碼。第二種情 況,稍微復雜點,它由一系列的 SUB 指令接條件跳轉實現。關鍵的一行如下:
```
SUB ESI, 1337
```
這一行告訴了我們,最小的 IOCTL 代碼就是 0x1337。從這里開始,0x1337 作為第一個 常量,每行 SUB 指令減去多少,我能就加上多少,每次加出來的新的值作為一個新的 IOCTL 代碼。不斷累加,直到 switch 結束。具體實現可以看 Immunity 目錄下的 Libs\driverlib.py。 代碼自動化的找出了 IOCTL dispatch 和所有的 IOCTL codes。
現在 driverlib 為我們完成了最臟最累的活。接下來讓我們做些高雅的事!用 driverlib 捕捉驅動程序中所有的設備名和 IOCTL 代碼,并且將結果保存到 Python pickle 中。接著用 它們構建 IOCTL fuzzer。Let’s get fuzzy!
- 序
- 1 搭建開發環境
- 1.1 操作系統準備
- 1.2 獲取和安裝 Python2.5
- 1.3 配置 Eclipse 和 PyDev
- 2 調試器設計
- 2.1 通用 CPU 寄存器
- 2.2 棧
- 2.3 調試事件
- 2.4 斷點
- 3 自己動手寫一個 windows 調試器
- 3.2 獲得 CPU 寄存器狀態
- 3.3 實現調試事件處理
- 3.4 全能的斷點
- 4 PyDBG---純 PYTHON 調試器
- 4.1 擴展斷點處理
- 4.2 處理訪問違例
- 4.3 進程快照
- 5 IMMUNITY----最好的調試器
- 5.1 安裝 Immunity 調試器
- 5.2 Immunity Debugger 101
- 5.3 Exploit 開發
- 5.4 搞定反調試機制
- 6 HOOKING
- 6.1 用 PyDbg 實現 Soft Hooking
- 6.2 Hard Hooking
- 7 Dll 和代碼注入
- 7.1 創建遠線程
- 7.2 邪惡的代碼
- 8 FUZZING
- 8.1 Bug 的分類
- 8.2 File Fuzzer
- 8.3 改進你的 Fuzzer
- 9 SULLEY
- 9.1 安裝 Sulley
- 9.2 Sulley primitives
- 9.3 獵殺 WarFTPD
- 10 Fuzzing Windows 驅動
- 10.1 驅動通信
- 10.2 用 Immunity fuzzing 驅動
- 10.4 構建 Driver Fuzzer
- 11 IDAPYTHON --- IDA 腳本
- 11.1 安裝 IDAPython
- 11.2 IDAPython 函數
- 11.3 腳本例子
- 12 PyEmu
- 12.1 安裝 PyEmu
- 12.2 PyEmu 一覽
- 12.3 IDAPyEmu