下面这是kruglinski关于学习驱动编写的文章。虽然是入门级文章,但是也需要对驱动有一些了解后才可以看的比较透彻。
1.驱动程序的结构:
1.1、一个入口点(DriverEntry):用于创建设备对象及符号连接,以及其它初使化操作,如分配池内存等.
1.2、一个出口(DriverUnload):删除符号连接与设备对象,并释放已经分配的各种资源,如池内存等
1.3、几个DispatchHandler:用于响应Ring3程序的请求及其它驱动事件,并做相关处理
2.内存管理
2.1、分配系统池内存(ExAllocatePool):它有点像C中malloc,只不过存在分页和紧急选项
2.2、释放系统池内存(ExFreePool):它有点像C中的free
2.3、注意:Ring3中所有堆内存都是可分页的,实质上是因为Ring3程序的中断请求级非常低,当它访问分页内存时,IO管理器有机会处理页错误中断,从而调入需要的内存页,但在驱动程序中我们可以随时调整中断请求级以减少驱动程序运行的时间,这样当驱动程序的当前中断请求级高于DISPATCH_LEVEL时,IO管理器没有机会处理页错误,导致内存访问异常(此时一般出现蓝屏)
2.4、如果需要频繁分配和释放相同大小的内存,可以使用后备列表来管理系统堆内存以提高性能
3.输出输出
3.1、几种响应
IRP_MJ_DEVICE_CONTROL,响应系统的DeviceIocontrol
IRP_MJ_READ,响应系统的ReadFileEx
IRP_MJ_WRITE,响应系统的WriteFileEx
3.2、访问输入输出参数:
3.2.1. Buffered方式:使用IoGetCurrentIrpStackLocation得到调用者堆栈区域指针IrpStack(PIO_STACK_LOCATION类型)
Irp->AssociatedIrp.SystemBuffer;输出输出缓冲
IrpStack->Parameters.DeviceIoControl.InputBufferLength;输入缓冲长度
IrpStack->Parameters.DeviceIoControl.OutputBufferLength;输出缓冲长度
IrpStack->Parameters.DeviceIoControl.IoControlCode;设备控制代码
如果需要输出参数,在填写完SystemBuffer后要设置IRP的IoStatus成员的Information指示输出数据的长度.
3.2.2. Direct方式:MmGetSystemAddressForMdlSafe映射IRP->MdlAddress地址到内核空间
MmGetMdlByteCount,得到MDL大小,以字节为单位.通常应该在IRQL<=DISPATCH_LEVEL的情况下使用MDL,那么如何输出呢?和Buffered方式一样从一个地方读,处理完再写到同一个地方吗?(问题1)
3.2.3. Neither方式:这个方式比恐怖,除非确定驱动不是分层的并且运行在PASSIVE_LEVEL级,一般不使用这种方式.比如写一个简单的Dump核心数据结构的驱动,该驱动只由我们的一个程序控制,那么可以直接把用户模式的地址传给驱动使用
注意:《Programming the Microsoft Windows Driver Model》一书说到"决不(或几乎从不)直接引用用户模式的内存地址"
4.安装KMD
4.1、需要Administrator权限,调用OpenSCManager打服务管理器,用CreateService以SERVICE_KERNEL_DRIVER类型将驱动安装为服务
5.启动KMD
5.1、可以在CreateService时指定SERVICE_AUTO_START自动加载,或是指定SERVICE_DEMAND_START安装类型再调用StartService手工加载
6.停止与卸载KMD
6.1、可以使用ControlService指定SERVICE_CONTROL_STOP停止驱动,然后可以使用DeleteService卸载驱动.最后CloseServiceHandle
7.Ring3访问KMD
7.1、需要Administrator权限,CreateFile打开设备对象
7.2、DeviceIoControl,与驱动进行交互操作,可以使用各种自定义的数据结构进行输入输出.
7.3、ReadFile,读驱动数据
7.4、Writefile,写数据给驱动
7.5、CloseHandle,关闭设备对象
8.过滤/挂钩IRP请求
8.1、挂钩某个IRP处理函数:
a,调用IoGetDeviceObjectPointer返回的设备对象(PDEVICE_OBJECT)
b,由设备对象得到驱动对象PDEVICE_OBJECT->DriverObject(PDRIVER_OBJECT),这里是否应该调用ObReferenceObjectByPointer函数增加驱动对象的引用计数,以防止该驱动程序在我们的驱动程序前被卸载呢?(问题2)
c,再由驱动对象得到中断请求派遣函数表(PDRIVER_OBJECT->MajorFunction)
d,保存PDRIVER_OBJECT->MajorFunction[IRP_MJ_XXXXXXX]值(原中断请求派遣函数地址)
e,修改PDRIVER_OBJECT->MajorFunction[IRP_MJ_XXXXXXX]值(让它指向我们自定义的函数),使用锁总线前缀lock的xchg指令进行赋值操作(让代码多线程和多处理器安全)
f,结束处理同System Service Hook
8.2、过滤某个设备的IRP请求:
a,初使化IRP请求派遣函数表MajorFunction,将它们全都指向一个派遣函数DispatchAny
b,再为MajorFunction指定几个我们感兴趣的IRP请求派遣函数
c,得到设备对象指针:IoGetDeviceObjectPointer
d,将设备加到设备堆栈上:IoAttachDeviceToDeviceStack,并保存下层设备对象,以供IoCallDriver时使用.
e,在DispatchAny中将所有IRP传给下层驱动:IoSkipCurrentIrpStackLocation,IoCallDriver
f,在指定的几个请求派遣函数中对IRP进行处理,如果有必要,可以将IRP传给下层驱动.
思考:
a,知道了FileMon使用过滤型驱动原理后,发现它的确和我写的File监控驱动一样没有办法监视到USB和PGP盘的操作,因为都在程序中硬编码了卷标名.在驱动中我挂接了所有的ZwXXXFile操作,然后使用ZwQueryObject得到句柄对应的对象属性,再得到卷标,最后匹配某个盘符(符号链接)与该卷标对应,从而得到文件全路径名.
b,确定一个派遣函数在哪个驱动中:
1.使用ZwQuerySystemInformation得到Module Lists
2.使用8.1的方法得到某个设备派遣函数地址,比较函数地址是否在某个Driver Module的地址空间范围内.
c,挂接\Device\Tcp的IRP_MJ_DEVICE_CONTROL函数隐藏端口
《Subverting the Windows Kernel》一书中使用了该方法,的确比较Cool(有点像我在Ring3挂COM接口的方法,都是通过修改某个FunctionTable中的项目来实现),但是这是可以被b方法发现,如果用b方法查询某个函数地址没有存在对应的sys地址空间中,那么可以肯定rootkit也Hook了ZwQuerySystemInformation使得我们得到了不真实的Module Lists.
想到一个方法可以针C对这样的完整性检测进行攻击,首先分配一块不可分页内存,在其中写入一些花指令和时间差反跟踪代码,然后再jmp到我们的函数中,最后让\Device\Tcp的IRP_MJ_DEVICE_CONTROL的函数指向这块内存.(可行吗?问题3)
初学驱动开发,感觉对386保护模式以及DDK还需要进一步了解,否则很多操作系统核心方面的东东无法深入了解,所以说基础很重要,Rootkit或是Kernel Hacking只是这些基础技术的特殊应用.这两个星期中感觉自己学到了不少东西,得到了Eva无私帮助,无数次的问他都给我一一详细的解答,否则我没有办法进步的这么快,虽然只是刚入门但至少可以看懂《Subverting the Windows Kernel》和《Windows Internals》中的大部分内容,在这里要特别感谢Eva和那些开放核心技术资料的人,你们才是真正的Hacker!
参考资料:
《MSDN 2001》
《The Undocumented Functions Microsoft Windows NT/2000》
《Windows NT(2000) Native API Reference》
《Four-F KMD教程》,罗云彬和刘松翻译
《Programming the Microsoft Windows Driver Model》
《Undocumented Windows NT》
《Undocumented Windows 2000 Secrets》
《Subverting the Windows Kernel》
《Windows Internals,4th》
《Developing Your Own Unix-Like OS On IBM PC》
//--------------THE---END-----
《DDK学习笔记》2
同步--经常被人们所遗忘在角落的重要素.
同步真的有那么重要吗?很多人对此有疑问,我真的为此感到很不解,包括有人在多线程程序中使用全局变量,并使用非线程安全的语句来进行同步操作,虽然有了那么些同步的意识,但做法还是不那么正确,所以觉得还是应该写一下,特别是在这篇笔记中写了些关于核心态代码同步的技巧.
现在思考一下一个爆破系统中的同步,假如有这样的程序(伪码),安置线程进入爆破现场安放爆破装置然后离开,引爆线程等安置线程离开后引爆爆破装置,使用一个全局BOOL型变量表示安放是否完成是否已经撤离爆破场(不考虑Sleep等让出CPU时间的操作).
BOOL bRetreat=FALSE;
IgniteThread;引爆线程
SetterThread;安置线程
SetterThread()
{
do
{
bRetreat=FALSE;
进入现场
安放爆破装置;
撤离现场;
bRetreat=TRUE;
}
while(任务没有完成);
}
IgniteThread()
{
do
{
if(!bRetreat)
等待安置线程安放爆破装置
引爆;
}
while(任务没有完成);
妈的while怎么显示不出来,Blogchina真有点奇怪
}
这段代码看起有问题吗?引爆线程会一直等待安置线程撤离现场才引爆啊!好像没有什么问题吧!如果这是核爆炸试验,而安置原子弹的是普通人而不是机器人,下面会让你浑身冒冷汗.
在x86系统上对于
if(!bRetreat)
这样的语句,特别当bRetreat是全局变量时,非常有可能生成这样的机器指令序列:
LTEST:
mov eax,bRetreat
test eax,eax
je LWAIT
引爆
LWAIT:
jmp LTEST
问题出来了,当
mov eax,bRetreat执行完成时,事实上线程是可能被打断的,而在这时bRetreat的值可能发生变化,然引爆线程却没有机会发现这些变化,
例如:
在mov eax,bRetreat执行时bRetreat的值为TRUE,而后在test eax,eax前引爆线程被打断,安放线程将bRetreat的值变设为FALSE,随后引爆线程又Resume了,它会直接执行引爆操作,因为此时eax的值为TRUE,而此时安置线程(包括被安装线程控制的机器和人员)还没有离开现场.于是Boom.......一切灰飞烟灰.虽然可以直接使用test bRetreat,FALSE代替前两条指令,但也不是多处理器安全的.
呵呵!当然在现实生活中我们不可能都成为原子弹引爆系统或神六飞船控制系统的设计师,但这还是会引起电脑上的核爆炸---BSOD!特别是在驱动程序中.
同步实例
我们经常要在KMD中做Hook操作,然后做一些操作过滤,和日志记录.而被Hook的函数在任何时间可能会被任何进程中的线程调用,这时我们的驱动在不同进程的上下文里,同步不做好BSOD肯定是不可避免了.现在好像找到感觉了,不再那么怕在驱动使用多线程和动态内存,也不那么怕BSOD了.也许这些得益于最近对内核态的同步的深入学习.
2.1 Hook系统函数后卸载驱动时产生BSOD
通常在Hook系统函数时我们会这样做
NTSTATUS NTAPI HookedNtAPI(....)
{
NTSTATUS ns=RealNTAPI(...);
return ns;
}
当一个调用被阻塞时我们所在上下文的线程会被挂起等待函数调用完成,这时如果我们的驱动映像从内存中移除,那么当函数返回时肯定会出现一个内存访问引常引起的BSOD.因为当RealNTAPI返回时所在的空间已经不可用,原来的HookedNTAPI点用的空间已经被释放.那么应该怎么做!当然是添加引用计数,就像这个HookedNtAPI是一个COM对象一样,如果有调用被挂起就等待,直到引用计数为零时才允许退出.可以添加一个ULONG类型的全局变量nSuspendCalls来表示全局的引用计数.在每个Hook函数中调用原系统函数前增加nSuspendCalls计数,函数退时减少计数,当nSuspendCalls=0时允许卸载驱动.
ULONG nSuspendCalls=0;
....
NTSTATUS NTAPI HookedNtAPI1(....)
{
NTSTATUS ns=0;
InterlockedIncrement(&nSuspendCalls);
ns=RealNTAPI1(...);
InterlockedDecrement(&nSuspendCalls);
return ns;
}
NTSTATUS NTAPI HookedNtAPI2(....)
{
NTSTATUS ns=0;
InterlockedIncrement(&nSuspendCalls);
ns=RealNTAPI2(...);
InterlockedDecrement(&nSuspendCalls);
return ns;
}
....
而在驱动的Unload例程里我是这样做的
VOID
DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING dosDeviceName;
ULONG uWaits=100;//随便写
//如果nSuspendCalls不等于0,就会一直等待
while(InterlockedExchange(&uWaits,nSuspendCalls))
{
KeSleep(200);
}
//
// Delete the symbolic link
//
RtlInitUnicodeString(&dosDeviceName, DOS_DEVICE_NAME);
IoDeleteSymbolicLink(&dosDeviceName);
//
// Delete the device object
//
IoDeleteDevice(DriverObject->DeviceObject);
dprintf("[TdiMon] unloaded\n");
}
DriverUnload里使用多线程多处理器安全的互锁函数得到nSuspendCalls的值,如果不为0就让出CPU时间等待200毫秒,直到引用计数为0驱动退出.通常不会出现不对称的调用情况,可以认为DriverUnload里的while不会产生死循环,因为当函数钩子被解除后引用计数就不会再增加而是越来越少(当那些被挂起的函数返回后).KeSleep是怎么来的,好像没有类似Ring3里可用的Sleep函数啊!不知道有没有,反正我没有找到,于是我就写了这样一个函数,它以毫秒为单位阻塞当前线程让出CPU.
void KeSleep(ULONG uMiniseconds)
{
KTIMER ktimer;
LARGE_INTEGER liTimerout;
liTimerout.QuadPart=-(LONG)(uMiniseconds*10000);
KeInitializeTimer(&ktimer);
KeWaitForSingleObject(&ktimer,Executive,KernelMode,FALSE,&liTimerout);
}
函数内的KeWaitForSingleObject会阻塞线程直到超时,其实使用KEvent之类的也行.
2.2 临界区
如果我们要在一个列表(由LIST_ENTRY组织起来的某种数据结构)中记录日志,同样的Hook函数也会在不同的进程/线程上下文中运行,所以对于列表的操作也应该互斥,否则可能会在添加/摘除列表项时出现BSOD.这里可以使用的内核同步对象很多包括临界区对象,事件对象,互斥体,自旋锁...,需要注意的是运行于DISPATCH_LEVEL级的代码是不应该被阻塞的.
2.3 生产者 VS 消费者
这种模型通常由一个线程/进程产生数据,另一个线程/进程负责对数据进行处理,在Ring3层我对于这些同步已经非常熟(早在1年多前就开始写多线程工作代码),在Ring0层的一个应用实例是Eva给我的一个记录文件操作日志的驱动,其中Hook函数使用自旋锁对操作进行互斥并放入一个列表排队,而一个系统工作线程(消费者)同样使用这个自旋锁将列表的操作进行互斥,并将列表中的记录写入文件.在写入线程中使用一个事件对象指示停止写入操作,同时又可以将线程阻塞一秒,非常巧妙.然后进入对列表的互锁操作ExInterlockedRemoveHeadList,内核态有这么多进行同步的函数,这样的同步方法明显不同于我在Ring3所做的,通常的Ring3层生产者/消费者模型都使用一个信号量来实现,可以初使化多个空位.这可以用来控制线程数量,或是数据处理排队.
例如一个扫描程序可以由一个主调度程序来创建扫描线程,首先:
static HANDLE hSempMaxThread=CreateSemaphore(NULL,最大线程数,最大数程数,NULL);
然后在调度线程中:
void scheduler(...)
{
do
{
准备一下个IP地址及扫描参数;
WaitForSingleObject(hSempMaxThread,INFINITE);//减少信号量计数,原子操作
创建一个线程做为针对某个地址的扫描任务;
}(扫描任务没有完成);
}
在扫描线程中:
ScanTask(...)
{
取得扫描参数;
进行扫描工作;
ReleaseSemaphore(hSempMaxThread,1,NULL);//增加信号增计数,原子操作
}
程序退出时CloseHandle(hSempMaxThread);
当达到最大线程数时调度线程中的WaitForSingleObject会阻塞,直到有一个扫描任务子线程退出.当然这里应该考虑用户界面中退出应用程序的情况.
又比如一个流媒体传输程序:
static HANDLE hSempMaxQueue=CreateSemaphore(NULL,0,最大帧排队数,NULL);
void Capturer(...)
{
调用DirectShow CaptureGraphBuilder取得摄影头数据帧;
放入一个list容器,注意同步;
ReleaseSemaphore(hSempMaxQueue,1,NULL);//增加数据帧计数,原子操作
}
void FrameSender(...)
{
WaitForSingleObject(hSempMaxQueue,INFINITE);//减少数据帧计数,原子操作
从全局list容器里取像图像帧,注意同步;
发送到客户端;
}
上面的两个同步例子都比使用C代码直接操作全局变量要安全且高效,条件不满足时线程都会阻塞,而不会占用任何CPU时间.
在驱动层通常操作都需要同步,而不像Ring3程序那么随意,使用全局变量和随意的C代码进行同步的方法不可取,否则BSOD将常伴你左右,虽然内核对象也以全局变量的形式出现,但对他们的访问都使用多线程多处理器安全的原子操作函数(KeWaitForSingleObject,KeReleaseSemaphore,ExInterlockedRemoveHeadList等).
下一篇DDK笔记打算写内存操作,
努力......