逆向RING0程序從這里開始
作 者: 笨笨雄
時 間: 2006-12-06 21:41
鏈 接: http://bbs.pediy.com/showthread.php?threadid=35999
詳細信息:
【文章標題】: 逆向RING0程序從這里開始
【文章作者】: 笨笨雄
【作者郵箱】: nemo314@gmail.com
【使用工具】: IDA
接觸RING 0之前,以為得學(xué)很多東西,一大堆驅(qū)動開發(fā)的知識。不過后來想了想,驅(qū)動殼等其他不直接訪問硬件的程序為了兼容性,不可能真的直接訪問硬件,也就是那些是基于硬件抽象層之上的,而且大部分使用的還是系統(tǒng)提供的API(RING0下使用的API稱為NATIVE API)。事情一下子變簡單了,除非你想通過逆向硬件廠商驅(qū)動,自己編寫優(yōu)化硬件或者超頻程序。
雖然這是純靜態(tài)分析,但是我希望通過分析整個驅(qū)動,你會理解一些RING0下的機制,并且懂得在動態(tài)調(diào)試中應(yīng)該如何下斷點定位代碼。
在開始之前,感謝rockhard的源代碼和已編譯好的驅(qū)動,這樣我就可以不必學(xué)習(xí)WINDDK的使用了。你可以在下面鏈接的附件中得到:
http://bbs.pediy.com/showthread.php?s=&threadid=35626
初步實現(xiàn)系統(tǒng)級攔截應(yīng)用程序取硬盤物理序列號
Rockhard發(fā)表上述文章時的目標是通過簡單修改REGMON驅(qū)動部分的源代碼完成攔截應(yīng)用程序取硬盤物理序列號的功能,難免有不足之處。個人對源代碼的不成熟評論并不針對Rockhard。
學(xué)習(xí)逆向時,我的方法是先看看高級語言代碼編譯后究竟是怎么樣的。或許最后我還是得學(xué)習(xí)WINDDK的使用,編寫代碼,編譯,反匯編,它會解答一些疑問。下面讓我來以源碼和反匯編代碼對照的形式來說明RING0下的一些機制。
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath )
DriverEntry,驅(qū)動程序的入口函數(shù),驅(qū)動的一些初始化操作,將在這里進行。象RING3那樣,程序通過堆棧訪問DriverObject和RegistryPath。而在IDA,反匯編后是這樣子:“; int __stdcall start(PDRIVER_OBJECT DriverObject,HANDLE Handle)”第二個參數(shù)的名字有點不同,不過不重要,我們知道,其實它是一樣的。
.text:000105A0 push 7
.text:000105A2 pop ecx
.text:000105A3 mov esi, offset s_DeviceHdhook ; "\\Device\\HDHOOK"
.text:000105A8 lea edi, [ebp+regnameNt]
.text:000105AE push 9
.text:000105B0 rep movsd
.text:000105B2 movsw
.text:000105B4 pop ecx
.text:000105B5 mov esi, offset s_DosdevicesHdh ; "\\DosDevices\\HDHOOK"
.text:000105BA lea edi, [ebp+regnameDos]
.text:000105C0 push 206B6444h ; Tag
.text:000105C5 rep movsd
.text:000105C7 movsw
.text:000105C9 mov esi, offset s_Start ; "Start"
.text:000105CE lea edi, [ebp+SourceString]
.text:000105D1 movsd
.text:000105D2 movsd
.text:000105D3 movsd
.text:000105D4 mov esi, [ebp+Handle]
.text:000105D7 movzx eax, word ptr [esi]
.text:000105DA inc eax
.text:000105DB inc eax
.text:000105DC push eax ; NumberOfBytes
.text:000105DD push 1 ; PoolType
.text:000105DF call ds:ExAllocatePoolWithTag
前面一大堆代碼,都是因為下面3個局部變量的定義,編譯器會生成一段代碼,先將這些字符移進堆棧,然后再使用。
WCHAR deviceNameBuffer[] = L"\\Device\\"DRIVER_NAME;
WCHAR deviceLinkBuffer[] = L"\\DosDevices\\"DRIVER_NAME;
WCHAR startValueBuffer[] = L"Start";
從逆向的角度來看,象這種靜態(tài)字符變量,如果換成全局變量,或者可以獲得更高的運行效率和更小的程序。值得注意的是NATIVE API的調(diào)用,第一個參數(shù)入棧后的幾行代碼,仍然是局部變量的初始化,這編譯器讓我想起扭曲變形的介紹。000105D4的代碼是從堆棧中取得DriverEntry的第二參數(shù),它是一個UNICODE_STRING結(jié)構(gòu),從MSDN中搜索到的說明(如無特別說明,一切資料都是從MSDN中搜索得到):
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING *PUNICODE_STRING;
從000105D7處的代碼來看,該結(jié)構(gòu)在反匯編是:
WORD Length
WORD MaximumLength
DWORD Buffer(指向字符的指針)
通過逆向RtlInitUnicodeString,可知上述結(jié)構(gòu)中的 MaximumLength成員,其實就相當于Length + sizeof(UNICODE_NULL)。關(guān)于調(diào)用ExAllocatePoolWithTag的第三參數(shù)PoolType:
typedef enum _POOL_TYPE {
NonPagedPool,
PagedPool,
NonPagedPoolMustSucceed,
DontUseThisType,
NonPagedPoolCacheAligned,
PagedPoolCacheAligned,
NonPagedPoolCacheAlignedMustS
} POOL_TYPE;
這里看起來跟PUSH 1好象沒有什么關(guān)系,現(xiàn)在看看源代碼:
registryPath.Buffer = ExAllocatePool( PagedPool,
RegistryPath->Length + sizeof(UNICODE_NULL));
這里使用的參數(shù)是PagedPool,對于enum類型的定義,每個成員代表的數(shù)字是從0開始遞增。PagedPool剛好在第二個位置,所以它是1。源代碼使用的ExAllocatePool,反匯編后的代碼使用的是ExAllocatePoolWithTag。MSDN的說法是ExAllocatePool已經(jīng)被舍棄了,取代的是ExAllocatePoolWithTag(以標識申請內(nèi)存)。調(diào)用API之后是對返回結(jié)果的判斷:
if (!registryPath.Buffer) {
return STATUS_INSUFFICIENT_RESOURCES;
}
它對應(yīng)的匯編代碼是
.text:000105E5 mov edi, eax
.text:000105E7 xor ebx, ebx
.text:000105E9 cmp edi, ebx
.text:000105EB mov [ebp+Path], edi
.text:000105EE jnz short loc_105FA
.text:000105EE
.text:000105F0 mov eax, 0C000009Ah
.text:000105F5 jmp loc_1075A
注意到000105F0,STATUS_INSUFFICIENT_RESOURCES=0C000009Ah,仍然使用EAX作為返回參數(shù)。接下來的源代碼,終于看到編譯優(yōu)化了:
registryPath.Length = RegistryPath->Length + sizeof(UNICODE_NULL);
registryPath.MaximumLength = registryPath.Length;
RtlZeroMemory( registryPath.Buffer, registryPath.Length );
RtlMoveMemory( registryPath.Buffer, RegistryPath->Buffer,
RegistryPath->Length );
RtlZeroMemory( ¶mTable[0], sizeof(paramTable));
這里本來要調(diào)用2個API的。對于前一個RtlZeroMemory調(diào)用,編譯器使用自己的代碼來代替它:
.text:000105FA mov ax, [esi]
.text:000105FD add ax, 2
.text:00010601 movzx ecx, ax
.text:00010604 mov edx, ecx
.text:00010606 xor eax, eax
.text:00010608 shr ecx, 2
.text:0001060B rep stosd
.text:0001060D mov ecx, edx
.text:0001060F and ecx, 3
.text:00010612 rep stosb
先是4字節(jié)對齊的填0,然后使用AND取得除以4后的余數(shù),繼續(xù)填0。第二次調(diào)用,編譯器同樣使用自己的代碼來實現(xiàn)這個功能:
.text:00010624 add esp, 0Ch
.text:00010627 xor eax, eax
.text:00010629 lea edi, [ebp+QueryTable]
.text:0001062F push 0Eh
.text:00010631 pop ecx
.text:00010632 rep stosd
現(xiàn)在讓我們再來看看系統(tǒng)中RtlZeroMemory的代碼:
00402520: 57 PUSH EDI
00402521: 8B7C2408 MOV EDI, [ESP+08]
00402525: 8B4C240C MOV ECX, [ESP+0C]
00402529: 33C0 XOR EAX, EAX
0040252B: FC CLD ;這一句用來保證DF=0
0040252C: 8BD1 MOV EDX, ECX
0040252E: 83E203 AND EDX, 00000003
00402531: C1E902 SHR ECX, 02
00402534: F3AB REP STOSD
00402536: 0BCA OR ECX, EDX
00402538: 7504 JNZ 40253E;非4字節(jié)對齊,繼續(xù)填0
0040253A: 5F POP EDI
0040253B: C20800 RETN 0008
0040253E: F3AA REP STOSB
00402540: 5F POP EDI
00402541: C20800 RETN 0008
高級語言似乎不提供修改標志寄存器的功能,CLD可以省掉。在編譯過程中比較需要填0的內(nèi)存是否4字節(jié)對齊,把比較語句也省了。這里的代碼沒有以函數(shù)的形式出現(xiàn),當然連傳遞參數(shù)和保存環(huán)境的代碼也省了。至于RtlMoveMemory,編譯器用下面代碼代替:
.text:00010614 movzx eax, word ptr [esi]
.text:00010617 push eax ; size_t
.text:00010618 push dword ptr [esi+4] ; void *
.text:0001061B push [ebp+Path] ; void *
.text:0001061E call ds:memmove
選擇使用MEMMOVE而不是RtlMoveMemory。簡單分析了一下,前者為每種情況都準備了一個獨立的處理例程,通過跳轉(zhuǎn)表的形式來實現(xiàn),后者則是使用了不少比較命令。由于代碼量較多,有興趣的可以自己看看。
編譯出來的程序,至少在代碼段,看起來跟RING 3沒有什么區(qū)別,同樣可以通過程序?qū)崿F(xiàn)的功能和驅(qū)動導(dǎo)入表,估計程序用了什么API,下斷,并最終定位目標功能代碼。我們需要做的事,只是熟悉這些API和一些常用的RING0機制,然后就可以象分析RING 3程序一樣分析RING 0了。下面我將把重點放在API的解釋。程序下一個調(diào)用的API是RtlQueryRegistryValues,MSDN可以查得該函數(shù)的功能是訪問注冊表。其中一個參數(shù)的結(jié)構(gòu)如下:
typedef struct _RTL_QUERY_REGISTRY_TABLE {
PRTL_QUERY_REGISTRY_ROUTINE QueryRoutine;
ULONG Flags;
PWSTR Name;
PVOID EntryContext;
ULONG DefaultType;
PVOID DefaultData;
ULONG DefaultLength;
} RTL_QUERY_REGISTRY_TABLE, *PRTL_QUERY_REGISTRY_TABLE;
關(guān)于Flags標記,可用的常量如下:
RTL_QUERY_REGISTRY_SUBKEY
RTL_QUERY_REGISTRY_TOPKEY
RTL_QUERY_REGISTRY_REQUIRED
RTL_QUERY_REGISTRY_NOVALUE
RTL_QUERY_REGISTRY_NOEXPAND
RTL_QUERY_REGISTRY_DIRECT
RTL_QUERY_REGISTRY_DELETE
對于這類型常量定義,從1(二進制)開始,第二個是10(二進制),第三個是100(二進制),如此類推。源代碼中使用的是RTL_QUERY_REGISTRY_DIRECT,所以有如下代碼:
.text:0001064F mov [ebp+QueryTable.Flags], 20h
這里有點想不明白,在此API調(diào)用之前的大段代碼和幾個API的調(diào)用,都是為了初始化此API的Path參數(shù)。為什么不能直接使用入口參數(shù)RegistryPath的BUFFER,而要另外分配內(nèi)存,轉(zhuǎn)移數(shù)據(jù)在作為傳入?yún)?shù)?另外在此API的注釋中看到這一句話“The table must be allocated from nonpaged pool.”。程序沒有申請一塊nonpaged內(nèi)存存放QueryTable結(jié)構(gòu),而是直接使用堆棧。難道RING 0下的堆棧都是nonpaged的?此外程序此后并沒有對該API的返回值或者返回數(shù)據(jù)作任何處理。這里大膽假設(shè)一下到目前為止的代碼都是垃圾代碼。另外我注意到編譯器的對于每行代碼幾乎都是很機械的編譯,下面源代碼:
paramTable[0].EntryContext = &startType;
paramTable[0].DefaultType = REG_DWORD;
paramTable[0].DefaultData = &startType;
對應(yīng)的反匯編代碼:
.text:00010634 lea eax, [ebp+var_4]
.text:00010637 push 4
.text:00010639 mov [ebp+QueryTable.EntryContext], eax
.text:0001063C lea eax, [ebp+var_4]
.text:0001063F pop edi
.text:00010640 mov [ebp+QueryTable.DefaultData], eax
編譯器將相同的賦值語句歸類了,但是卻對EAX重復(fù)賦值,顯然0001063C處的代碼可以省略。接下來是ZwOpenKey。根據(jù)NTSTATUS的定義(詳見WINDDK中的ntstatus.h),最高位有如下定義:
// 00 – Success ;對應(yīng)的16進制最高位0
// 01 – Informational ;4
// 10 – Warning ;8
// 11 – Error ;c
比較是否成功調(diào)用的代碼是:
.text:0001069D test eax, eax
.text:0001069F jl short loc_106CF;最高位為1(調(diào)用失敗),跳
在RING 3下API調(diào)用失敗返回的是-1,NATIVE API則是以返回值的最高位來判斷調(diào)用是否成功。
.text:00010675 lea eax, [ebp+Handle]
.text:00010678 push 20006h ; DesiredAccess
.text:0001067D push eax ; KeyHandle
.text:0001067E mov [ebp+ObjectAttributes.RootDirectory], ebx
.text:00010681 mov [ebp+ObjectAttributes.Attributes], 40h
.text:00010688 mov [ebp+ObjectAttributes.ObjectName], esi
.text:0001068B mov [ebp+ObjectAttributes.SecurityDescriptor], ebx
.text:0001068E mov [ebp+ObjectAttributes.SecurityQualityOfService], ebx
.text:00010691 call ds:ZwOpenKey
由上面代碼可知,該API的KeyHandle參數(shù),使用的就是DriverEntry的第二個參數(shù)。也就是說KeyHandle其實就是一個UNICODE_STRING結(jié)構(gòu)。搞不懂micro$oft,一樣的東西搞這么多概念干什么。另外想不明白的是,假如該API調(diào)用成功,將會在注冊表寫入一些數(shù)據(jù)。但是對程序的運行沒有影響。ZwOpenKey調(diào)用失敗了反而省了幾行代碼。隨后是IoCreateDevice和IoCreateSymbolicLink建立驅(qū)動對象和符號連接,API調(diào)用失敗則把建立的對象和符號連接刪除。為IRP_MJ_SHUTDOWN,IRP_MJ_CREATE,IRP_MJ_CLOSE和IRP_MJ_DEVICE_CONTROL分派處理例程。分別有下面對應(yīng)關(guān)系:
名稱 描述 調(diào)用的API
IRP_MJ_CREATE 請求一個句柄 CreateFile
IRP_MJ_CLOSE 關(guān)閉句柄 CloseHandle
IRP_MJ_DEVICE_CONTROL 控制操作(利用IOCTL宏) DeviceIoControl
IRP_MJ_SHUTDOWN 系統(tǒng)關(guān)閉 InitiateSystemShutdown
當RING 3程序調(diào)用上述API對驅(qū)動進行操作時,系統(tǒng)會查找該IRP對應(yīng)的處理例程地址,并調(diào)用該例程。
.text:0001071B mov eax, offset sub_104B4
.text:00010720 cmp edi, ebx
.text:00010722 mov [esi+70h], eax
.text:00010725 mov [esi+40h], eax
.text:00010728 mov [esi+38h], eax
.text:0001072B mov [esi+78h], eax
此處ESI指向的是DriverEntry的第一個參數(shù)DriverObject。+38h是IRP_MJ_CREATE,+40h是IRP_MJ_CLOSE。具體請查閱WINDDK中的wdm.h。
.text:0001074E mov eax, ds:KeServiceDescriptorTable
.text:00010753 mov dword_1080C, eax
執(zhí)行完這兩行代碼,把SSDT存到全局變量中便結(jié)束了DriverEntry例程,也就是說驅(qū)動的初始化完畢了。實現(xiàn)驅(qū)動功能的代碼在IRP例程中,這讓我想起RING 3下的消息環(huán)。下面來看看sub_104B4:
.text:000104B4 sub_104B4 proc near ; DATA XREF: start+187 o
.text:000104B4 ;按照IRPDispatch例程的定義
.text:000104B4 arg_0 = dword ptr 8 ;DeviceObject
.text:000104B4 arg_4 = dword ptr 0Ch ;pIrp
關(guān)于pIrp結(jié)構(gòu),MSDN中的定義不完整。對照源代碼可知:
+0ch DWORD AssociatedIrp.SystemBuffer
+18h DWORD IoStatus.Status
+1Ch DWORD IoStatus.Information
+3ch DWORD UserBuffer
+60h DWORD irpStack
irpStack:
+00h BYTE MajorFunction ,查詢WINDDK中的WDM.h可知
#define IRP_MJ_CREATE 0x00
#define IRP_MJ_CLOSE 0x02
#define IRP_MJ_DEVICE_CONTROL 0x0e
#define IRP_MJ_SHUTDOWN 0x10
+04h DWORD Parameters.DeviceIoControl.OutputBufferLength
+08h DWORD Parameters.DeviceIoControl.InputBufferLength
+0ch DWORD Parameters.DeviceIoControl.IoControlCode
+18h DWORD FileObject
該函數(shù)根據(jù)不同的IRP消息,進入不同的流程。其中從IRP_MJ_DEVICE_CONTROL的處理流程可知,當IoControlCode的低3位為111(二進制,即METHOD_NEITHER)時,驅(qū)動程序使用UserBuffer返回數(shù)據(jù),反之則使用SystemBuffer。現(xiàn)在來看看sub_103DA,即是源代碼中的HDHookDeviceControl函數(shù):
.text:000103EB cmp ecx, 83050000h
.text:000103F1 jz loc_104A7
.text:000103F1
.text:000103F7 cmp ecx, 83050004h
.text:000103FD jz loc_104A0
.text:000103FD
.text:00010403 cmp ecx, 83050008h
.text:00010409 jz short loc_1047E
.text:00010409
.text:0001040B cmp ecx, 8305000Ch
.text:00010411 jz short loc_10452
.text:00010411
.text:00010413 cmp ecx, 83050010h
.text:00010419 jz short loc_10426
函數(shù)將IoControlCode保存在ECX中,經(jīng)過對比跳轉(zhuǎn)到相關(guān)代碼中。在用戶態(tài)程序中,通過下面API與驅(qū)動進行通信:
BOOL DeviceIoControl(
HANDLE hDevice, // handle to device of interest
DWORD dwIoControlCode, // control code of operation to perform
LPVOID lpInBuffer, // pointer to buffer to supply input data
DWORD nInBufferSize, // size of input buffer
LPVOID lpOutBuffer, // pointer to buffer to receive output data
DWORD nOutBufferSize, // size of output buffer
LPDWORD lpBytesReturned, // pointer to variable to receive output byte count
LPOVERLAPPED lpOverlapped // pointer to overlapped structure for asynchronous operation
);
也就是說,攔截該API,取得dwIoControlCode,在IRP分派例程的入口下條件斷點。然后便可以定位驅(qū)動中相關(guān)的功能代碼。我想看到這里,大家都大概了解RING 0的一些機制,并且能嘗試動態(tài)調(diào)試一些程序了。該程序大部分代碼的功能Rockhard在他的貼里已經(jīng)講得很清楚了,除了如何取得系統(tǒng)調(diào)用號。我們知道系統(tǒng)調(diào)用號隨著系統(tǒng)版本,甚至SP之間也會有所不同,如何兼容各版本?我對此很感興趣,先看看源代碼:
VOID HookStart( void )
{
if( !IsHooked ) {
RealZwDeviceIoControlFile = SYSCALL( ZwDeviceIoControlFile );
SYSCALL( ZwDeviceIoControlFile ) = (PVOID) HookZwDeviceIoControlFile;
IsHooked = TRUE;
}
}
你能想象這樣的代碼能取得ZwDeviceIoControlFile在SSDT中的位置嗎?現(xiàn)在再讓我們看看反匯編代碼:
.text:00010381 mov eax, ds:ZwDeviceIoControlFile
.text:00010386 mov ecx, ssdt
.text:0001038C push esi
.text:0001038D mov edx, [eax+1];這里有點奇怪,取函數(shù)的機械碼?
.text:00010390 mov esi, [ecx]
.text:00010392 mov edx, [esi+edx*4]
.text:00010395 pop esi
.text:00010396 mov dword_10814, edx
.text:0001039C mov eax, [eax+1]
.text:0001039F mov ecx, [ecx]
.text:000103A1 mov dword ptr [ecx+eax*4], offset sub_1030E
現(xiàn)在讓我們來看看ZwDeviceIoControlFile的代碼:
00400BC6: B838000000 MOV EAX, 00000038
00400BCB: 8D542404 LEA EDX, [ESP+04]
00400BCF: CD2E INT 2E
00400BD1: C22800 RETN 0028
入口點加1,便是系統(tǒng)調(diào)用號。最后感謝所有看到這里的人