找回密码
 注册
搜索
热搜: 回贴

逆向RING0程序从这里开始,RING0,逆向工程技术

2010-1-30 18:35| 发布者: admin| 查看: 195| 评论: 0|原作者: 段誉


逆向RING0程序从这里开始,RING0,逆向工程技术
2008年06月24日 星期二 下午 02:06
接触RING 0之前,以为得学很多东西,一大堆驱动开发的知识。不过后来想了想,驱动壳等其他不直接访问硬件的程序为了兼容性,不可能真的直接访问硬件,也就是那些是基于硬件抽象层之上的,而且大部分使用的还是系统提供的API(RING0下使用的API称为NATIVE API)。事情一下子变简单了,除非你想通过逆向硬件厂商驱动,自己编写优化硬件或者超频程序。



虽然这是纯静态分析,但是我希望通过分析整个驱动,你会理解一些RING0下的机制,并且懂得在动态调试中应该如何下断点定位代码。



在开始之前,感谢rockhard的源代码和已编译好的驱动,这样我就可以不必学习WINDDK的使用了。你可以在下面链接的附件中得到:



http://bbs.pediy.com/showthread.php?s=&threadid=35626

初步实现系统级拦截应用程序取硬盘物理序列号



Rockhard发表上述文章时的目标是通过简单修改REGMON驱动部分的源代码完成拦截应用程序取硬盘物理序列号的功能,难免有不足之处。个人对源代码的不成熟评论并不针对Rockhard。



学习逆向时,我的方法是先看看高级语言代码编译后究竟是怎么样的。或许最后我还是得学习WINDDK的使用,编写代码,编译,反汇编,它会解答一些疑问。下面让我来以源码和反汇编代码对照的形式来说明RING0下的一些机制。



NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath )



DriverEntry,驱动程序的入口函数,驱动的一些初始化操作,将在这里进行。象RING3那样,程序通过堆栈访问DriverObject和RegistryPath。而在IDA,反汇编后是这样子:“; int __stdcall start(PDRIVER_OBJECT DriverObject,HANDLE Handle)”第二个参数的名字有点不同,不过不重要,我们知道,其实它是一样的。



.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";



从逆向的角度来看,象这种静态字符变量,如果换成全局变量,或者可以获得更高的运行效率和更小的程序。值得注意的是NATIVE API的调用,第一个参数入栈后的几行代码,仍然是局部变量的初始化,这编译器让我想起扭曲变形的介绍。000105D4的代码是从堆栈中取得DriverEntry的第二参数,它是一个UNICODE_STRING结构,从MSDN中搜索到的说明(如无特别说明,一切资料都是从MSDN中搜索得到):



typedef struct _UNICODE_STRING {

USHORT Length;

USHORT MaximumLength;

PWSTR Buffer;

} UNICODE_STRING *PUNICODE_STRING;



从000105D7处的代码来看,该结构在反汇编是:



WORD Length

WORD MaximumLength

DWORD Buffer(指向字符的指针)



通过逆向RtlInitUnicodeString,可知上述结构中的 MaximumLength成员,其实就相当于Length sizeof(UNICODE_NULL)。关于调用ExAllocatePoolWithTag的第三参数PoolType:



typedef enum _POOL_TYPE {

NonPagedPool,

PagedPool,

NonPagedPoolMustSucceed,

DontUseThisType,

NonPagedPoolCacheAligned,

PagedPoolCacheAligned,

NonPagedPoolCacheAlignedMustS

} POOL_TYPE;



这里看起来跟PUSH 1好象没有什么关系,现在看看源代码:



registryPath.Buffer = ExAllocatePool( PagedPool,

RegistryPath->Length sizeof(UNICODE_NULL));



这里使用的参数是PagedPool,对于enum类型的定义,每个成员代表的数字是从0开始递增。PagedPool刚好在第二个位置,所以它是1。源代码使用的ExAllocatePool,反汇编后的代码使用的是ExAllocatePoolWithTag。MSDN的说法是ExAllocatePool已经被舍弃了,取代的是ExAllocatePoolWithTag(以标识申请内存)。调用API之后是对返回结果的判断:



if (!registryPath.Buffer) {

return STATUS_INSUFFICIENT_RESOURCES;

}



它对应的汇编代码是



.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作为返回参数。接下来的源代码,终于看到编译优化了:



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));



这里本来要调用2个API的。对于前一个RtlZeroMemory调用,编译器使用自己的代码来代替它:



.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字节对齐的填0,然后使用AND取得除以4后的余数,继续填0。第二次调用,编译器同样使用自己的代码来实现这个功能:



.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



现在让我们再来看看系统中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字节对齐,继续填0

0040253A: 5F POP EDI

0040253B: C20800 RETN 0008

0040253E: F3AA REP STOSB

00402540: 5F POP EDI

00402541: C20800 RETN 0008



高级语言似乎不提供修改标志寄存器的功能,CLD可以省掉。在编译过程中比较需要填0的内存是否4字节对齐,把比较语句也省了。这里的代码没有以函数的形式出现,当然连传递参数和保存环境的代码也省了。至于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。简单分析了一下,前者为每种情况都准备了一个独立的处理例程,通过跳转表的形式来实现,后者则是使用了不少比较命令。由于代码量较多,有兴趣的可以自己看看。

编译出来的程序,至少在代码段,看起来跟RING 3没有什么区别,同样可以通过程序实现的功能和驱动导入表,估计程序用了什么API,下断,并最终定位目标功能代码。我们需要做的事,只是熟悉这些API和一些常用的RING0机制,然后就可以象分析RING 3程序一样分析RING 0了。下面我将把重点放在API的解释。程序下一个调用的API是RtlQueryRegistryValues,MSDN可以查得该函数的功能是访问注册表。其中一个参数的结构如下:



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;



关于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调用之前的大段代码和几个API的调用,都是为了初始化此API的Path参数。为什么不能直接使用入口参数RegistryPath的BUFFER,而要另外分配内存,转移数据在作为传入参数?另外在此API的注释中看到这一句话“The table must be allocated from nonpaged pool.”。程序没有申请一块nonpaged内存存放QueryTable结构,而是直接使用堆栈。难道RING 0下的堆栈都是nonpaged的?此外程序此后并没有对该API的返回值或者返回数据作任何处理。这里大胆假设一下到目前为止的代码都是垃圾代码。另外我注意到编译器的对于每行代码几乎都是很机械的编译,下面源代码:



paramTable[0].EntryContext = &startType;

paramTable[0].DefaultType = REG_DWORD;

paramTable[0].DefaultData = &startType;



对应的反汇编代码:



.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重复赋值,显然0001063C处的代码可以省略。接下来是ZwOpenKey。根据NTSTATUS的定义(详见WINDDK中的ntstatus.h),最高位有如下定义:



// 00 – Success ;对应的16进制最高位0

// 01 – Informational ;4

// 10 – Warning ;8

// 11 – Error ;c



比较是否成功调用的代码是:



.text:0001069D test eax, eax

.text:0001069F jl short loc_106CF;最高位为1(调用失败),跳



在RING 3下API调用失败返回的是-1,NATIVE API则是以返回值的最高位来判断调用是否成功。



.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参数,使用的就是DriverEntry的第二个参数。也就是说KeyHandle其实就是一个UNICODE_STRING结构。搞不懂micro$oft,一样的东西搞这么多概念干什么。另外想不明白的是,假如该API调用成功,将会在注册表写入一些数据。但是对程序的运行没有影响。ZwOpenKey调用失败了反而省了几行代码。随后是IoCreateDevice和IoCreateSymbolicLink建立驱动对象和符号连接,API调用失败则把建立的对象和符号连接删除。为IRP_MJ_SHUTDOWN,IRP_MJ_CREATE,IRP_MJ_CLOSE和IRP_MJ_DEVICE_CONTROL分派处理例程。分别有下面对应关系:



名称 描述 调用的API

IRP_MJ_CREATE 请求一个句柄 CreateFile

IRP_MJ_CLOSE 关闭句柄 CloseHandle

IRP_MJ_DEVICE_CONTROL 控制操作(利用IOCTL宏) DeviceIoControl

IRP_MJ_SHUTDOWN 系统关闭 InitiateSystemShutdown



当RING 3程序调用上述API对驱动进行操作时,系统会查找该IRP对应的处理例程地址,并调用该例程。



.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的第一个参数DriverObject。 38h是IRP_MJ_CREATE, 40h是IRP_MJ_CLOSE。具体请查阅WINDDK中的wdm.h。



.text:0001074E mov eax, ds:KeServiceDescriptorTable

.text:00010753 mov dword_1080C, eax



执行完这两行代码,把SSDT存到全局变量中便结束了DriverEntry例程,也就是说驱动的初始化完毕了。实现驱动功能的代码在IRP例程中,这让我想起RING 3下的消息环。下面来看看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



关于pIrp结构,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



该函数根据不同的IRP消息,进入不同的流程。其中从IRP_MJ_DEVICE_CONTROL的处理流程可知,当IoControlCode的低3位为111(二进制,即METHOD_NEITHER)时,驱动程序使用UserBuffer返回数据,反之则使用SystemBuffer。现在来看看sub_103DA,即是源代码中的HDHookDeviceControl函数:



.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



函数将IoControlCode保存在ECX中,经过对比跳转到相关代码中。在用户态程序中,通过下面API与驱动进行通信:



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分派例程的入口下条件断点。然后便可以定位驱动中相关的功能代码。我想看到这里,大家都大概了解RING 0的一些机制,并且能尝试动态调试一些程序了。该程序大部分代码的功能Rockhard在他的贴里已经讲得很清楚了,除了如何取得系统调用号。我们知道系统调用号随着系统版本,甚至SP之间也会有所不同,如何兼容各版本?我对此很感兴趣,先看看源代码:



VOID HookStart( void )

{

if( !IsHooked ) {

RealZwDeviceIoControlFile = SYSCALL( ZwDeviceIoControlFile );

SYSCALL( ZwDeviceIoControlFile ) = (PVOID) HookZwDeviceIoControlFile;

IsHooked = TRUE;

}

}



你能想象这样的代码能取得ZwDeviceIoControlFile在SSDT中的位置吗?现在再让我们看看反汇编代码:



.text:00010381 mov eax, ds:ZwDeviceIoControlFile

.text:00010386 mov ecx, ssdt

.text:0001038C push esi

.text:0001038D mov edx, [eax 1];这里有点奇怪,取函数的机械码?

.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



现在让我们来看看ZwDeviceIoControlFile的代码:



00400BC6: B838000000 MOV EAX, 00000038

00400BCB: 8D542404 LEA EDX, [ESP 04]

00400BCF: CD2E INT 2E

00400BD1: C22800 RETN 0028



入口点加1,便是系统调用号。最后感谢所有看到这里的人


最新评论

QQ|小黑屋|最新主题|手机版|微赢网络技术论坛 ( 苏ICP备08020429号 )

GMT+8, 2024-9-29 11:23 , Processed in 0.238061 second(s), 12 queries , Gzip On, MemCache On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

返回顶部