近来一直在做磁盘驱动的分析,于是对整个拓补图上所有的函数都逆了一遍。(估计有很多前辈逆过,但没人想发这个东西出来)
我这里讲的是原理,可能很多人会觉得没有用,但是其实这个是最有用的,懂了这个磁盘驱动的十三个函数,什么底层的硬件写入都可以写出来,什么高级的HOOK也都没有问题。
下面发些比较粗糙的分析总结,具体的分析就全记自己笔记本上了,毕竟是心血的结晶,不想全发出来,留点自己备用。

下面是磁盘驱动的拓补图:
00 f8ac63d8 f9802193 atapi!IdeReadWrite
01 f8ac6464 f9802bbc atapi!IdeSendCommand+0x3ab
02 f8ac64b0 f98050ad atapi!AtapiStartIo+0x23e
03 f8ac64dc 805414f7 atapi!IdeStartIoSynchronized+0x16f
04 f8ac650c f9805fe1 nt!KeSynchronizeExecution+0x17
05 f8ac6524 f9c1cd28 atapi!IdePortAllocateAccessToken+0x1b
06 f8ac6534 806d3998 PCIIDEX!BmReceiveScatterGatherList+0x24
07 f8ac6560 806d3b7c hal!HalBuildScatterGatherList+0x19e
08 f8ac6590 f9c1cddb hal!HalGetScatterGatherList+0x26
09 f8ac65c4 f98068b1 PCIIDEX!BmSetup+0x5f
0a f8ac65fc 804efe19 atapi!IdePortStartIo+0xeb
0b f8ac661c f9805c9a nt!IoStartPacket+0x7d
0c f8ac6648 804eedf9 atapi!IdePortDispatch+0x4e6
0d f8ac6658 f99dd061 nt!IopfCallDriver+0x31
0e f8ac666c f99dcd58 CLASSPNP!SubmitTransferPacket+0x82
0f f8ac669c f99dce49 CLASSPNP!ServiceTransferRequest+0xe4
10 f8ac66c0 804eedf9 CLASSPNP!ClassReadWrite+0xff
11 f8ac66d0 f9c2436c nt!IopfCallDriver+0x31
12 f8ac66e0 804eedf9 PartMgr!PmReadWrite+0x93
13 f8ac66f0 805cdc06 nt!IopfCallDriver+0x31
XP SP3版可能会多一个CallIdeStartIoSynchronized,夹在KeSynchronizeExecution和IdePortAllocateAccessToken之间
这是Atapi.sys的拓补图,从IdePortDispatch开始,一直到IdeReadWrite,IdeReadWrite以下就是HAL端口输入了。
SubmitTransferPacket,ServiceTransferRequest,ClassReadWrite其实也可以看看,WinDDK里提供了源码,对于理解还是有好处的。

这里讲讲Atapi各个主干函数的主要功能:
IdePortDispatch人如其名:就是用来根据SRB功能号分发SRB。

IdePortStartIo:在IoStartPacket之后确保了每次只处理一个SRB,里面对SRB的处理分为两个部分:其一是DMA
传输模式,其二是原始的中断传输模式。DMA传输模式下,他直接调用BmSetup,以MdlAddress为参数。原始的传输
模式下,他调用MmMapLockedPagesSpecifyCache先把Databuffer之中的内容映射好(因为IoBuildMdl是不对
DataBuffer映射到系统空间的,Windows的设计方案是什么时候该用什么时候映射),DataBuffer有可能是用户空间
的地址,而StartIo例程运行在DISPATCH_LEVEL,就有可能会牛头不对马嘴,所以他这里其实不是根据DataBuffer
建立映射,而是根据MDL结构后面的PFN(物理页面号)数组进行映射。映射好了,会把得到的MappedSystemVa存入
Srb的DataBuffer,以供日后调用,最后调IdePortAllocateAccessToken,直接略过中间的四个跟DMA有关的函数

BmSetup:在[[IdePort1(我这里IDE设备的下层是IdePort1,光驱的下层是IdePort0)的设备扩展(以下都简称设备扩展)+AC]+8F8h](跟DMA有关的一个东西,有些IO内存也在里面)里存入一个数据结构:
+A0h:MdlAddress
+A4h:读或写标志号
+A8h:函数地址
+ACh:设备对象(IdePort1)
+90h:DataBuffer
+98h:DataTransferLength
然后调用HalGetScatterGatherList

HalGetScatterGatherList没什么好说的,就是调HalBuildScatterGatherList。

HalBuildScatterGatherList里面就比较重要了,
1先调HalCalculateScatterGatherListSize计算MDL结构紧跟着的PFN数量,返回PFN数量,和需分配的结构大小。然后对全局
变量HalpOutStandingScatterGatherCount(指明当前SG请求块数量)递增上1,这个全局变量会在DMA中断服务完成之后,如果SG请求被完成,执行DPC函数时会被递减。
2对MDL结构进行解析,将解析后的数据存入新分配的结构中,结构解析如下:
+0h:离散的物理页面数量
+4:当HalBuildScatterGatherList的倒数第二个参数不为0时,为1,为0时,为0.具体意义不明
+8h以后就是一个16字节大小的数组:
+0h:物理页面地址(加上页面内偏移后的)
+4h:页面大小
+8h:此页面有效的数据部分大小
3HalBuildScatterGatherList会对连续的物理块进行整合,整合到一个结构里
4然后调BmReceiveScatterGatherList

BmReceiveScatterGatherList里面调了三个函数:BmRebuildScatterGatherList,BmPrepareController,IdePortAllocateAccessToken。BmRebuildScatterGatherList根据HalBuildScatterGatherList分配的结构,进行DMA传输时的内存基址设置。
具体的设置如下,对[[设备扩展+AC]+84h]表示的IO内存存入:
+0h:第一个物理页面地址
+4h:存入有效长度(USHORT)
+7h:结束标志位
对于有效长度大于10000的项次,会进行拆分处理,拆成两个结构
BmPrepareController进行DMA控制器的初始化。


这中间四个函数是DMA相关

IdePortAllocateAccessToken以下就是磁盘部分了。


IdeStartIoSynchronized的负责范围是:
1对超时时间设置。
2对C7,C8 SRB功能号调用重设总线函数。
3对ABORT_COMMAND获取当前SRB
4调用AtapiStartIo
3对所发现的错误情况用IdePortNotification完成掉,并通知下一SRB就绪,最后插入DPC(没调过,可一猜就知道是调控SRB处理用的)

AtapiStartIo根据SRB功能号进行选择处理,处理的大类有7个,SRB_FUNCTION_IO_CONTROL里又有4个小分类,说起来比较多,就不说了。哈哈。总结几点跟这文章有关的:
1会判断当前CurrentSrb是否存在(CurrentSrb标示着正在处理的SRB,如果SRB请求完成,当中断返回时会清空,具体函数是AtapiInterrupt)
2只有IRP_FUNCTION_EXECUTE_SCSI会调用IdeSendCommand(这点对后面比较重要)


IdeSendCommand:根据Cdb里的OpeationCode进行分发,也就跟AtapiStartIo似的。只不过IdeSendCommand分发的是更细的功能号Cdb[0],而AtapiStartIo分发的是SrbFunction
(对有些功能号直接在设备扩展里复制过来后返回,有些功能号就一定需要DMA了,所以得启动DMA传输)对SCSIOP_READ或者SCSIOP_WRITE会调用IdeReaDWrite

IdeReadWrite:负责对磁盘设备进行启动。因为上面原始方式也好,DMA方式也好,都调用了IdeReadWrite,所以很自然IdeReadWrite里就分成了两种模式。原始模式下和DMA模式是
用pSrb->SrbExtension作标志区分的。
1先判断设备是否忙
2先写扇区数量,再写磁盘扇区SectorBegin,对其最高字节处理后,分成四个字节,依次写入[[设备扩展+ACh]+14h],[[设备扩展+ACh]+18h],[[设备扩展+ACh]+1Ch],
[[设备扩展+ACh]+20h](DMA和无DMA处理情况略有不同)
3再判断是读是写,读则向[[设备扩展+ACh]+24h]端口号写入C8H,写则输入CAH,有DMA能力时,调用BmArm启动DMA传输后返回。
4无DMA能力时,直接用WRITE_PORT_BUFFER_USHORT写入DataBuffer(其实是MappedSystemBufffer,前面已经被覆盖掉了)

在IoStartPacket之后保证了SRB不重入,后面的函数都不会出现并发的情况。(所以BmRebuildScatterGatherList没有进行当前是否有繁忙的判断,因为只有在IdeReadWrite
之后才有可能对另一个SRB中的物理地址进行设置)
在AtapiStartIo之中对CurrentSrb的判断保证了设备不同时执行两份任务。

那如果说要破还原,怎么办。直接模仿BmRebuildScatterGatherList,和IdeReadWrite操作,就可以实现最底层的端口写入。或者你想不麻烦点,那就HOOK IdePortStartIo,先提参数IRP,看看是不是IRP_MJ_SCSI,进而判断是不是写入.是写入则对里面的参数IRP参数中SRB进行替换,替换一下MDL地址,替换一下DataBuffer,再替换一下TransferLength,还有Srb里的cdb数组,那就想写哪里写哪里,
当然也可以自己Call IdePortStartIo.(不过这样比较危险,可能会造成多线程问题,因为StartIo例程应该要保证只有一个SRB能进入,而直接Call的话,就造成了重入)。

其实不单单是IdePortStartIo,整个拓补图所有的函数你都可以HOOK掉,替换参数,所以说拓补图上,凡目力所及,尽可HOOK。

然后说说怎么防止写入,还原保护的问题。你可以直接这么干,HOOK掉IdeReadWrite,对于要处理的写入请求,进行检查,检查看看他有没有你想要的标识码(标识码可以在上层的还原驱动设置,至于设置在哪里呢,你可以试试看CDB[7]CDB[8]CDB[9,CDB[7]据说是扇区数量,可我在逆向中没有发现用到这个的地方,IdeReadWrite是直接根据DataTransferLength来写入扇区数量的),当然这种方法解决不了破还原时HOOK只修改DataBuffer,MDL,TransferLength和CDB数组的问题。 
那还有种方法,那就是对HOOK的IdeReadWrite这样处理,上层的还原驱动和IdeReadWrite进行通信,每次一有磁盘写入,就去查DataBuffer,MDL结构这些关键的地方是否被修改。

好了,就这么多。我还是大一学生,应该会有很多错误的地方。
有错误的地方,非常诚恳地请求指正。