21 12
发新话题
打印

机器狗0625技术剖析(驱动读写磁盘扇区)——by水中雁

机器狗0625技术剖析(驱动读写磁盘扇区)——by水中雁

原文:http://hi.baidu.com/wildgoose/bl ... d0bbc3a7ef3ff9.html
作者:毒霸引擎组水中雁

由于对磁盘结构不是很熟,仅仅从逆向分析的角度,通过对这个驱动的分析过程,学习了向SCSI端口向扇区写数据的方法。如有错误或纰漏之处,请大家指出。

摘要
这是一个6月23号捕获的机器狗变种。跟之前不同的地方,主要是将以前ring3下写磁盘替换文件的操作使用驱动实现了,本文的主要部分也是对该部分的解析。可以看出,机器狗的作者对内核驱动以及磁盘结构都很熟悉。

三、驱动分析
1、基本信息
文件名:%windir%\system32\drivers\obj2.sys
DeviceName:\\Device\\DogBaby
SymbolicLinkName:\\DosDevices\\DogBaby

2、摘除ntfs的AttachedDevice

3、恢复disk的Hook
1)、摘除disk的AttachedDevice
2)、根据_DRIVER_OBJECT.DriverSection获得_LDR_DATA_TABLE_ENTRY的链表,枚举该链表查找名为CLASSPNP.SYS的模块,找到则获取该模块的基址Base和EntryPoint,如果EntryPoint -Base等于0AE8Fh(入口的RVA),则取g_pClassInternalIoControl = Base +4FC3h。然后注册驱动disk的派遣例程为_DRIVER_OBJECT.IrpInternalDeviceControlDispatch =pClassInternalIoControl。

4、摘除FtDisk的AttachedDevice

5、摘除Atapi的AttachedDevice

6、恢复Atapi的Hook
1)、读取atapi.sys文件,并定位pe头、及块表。
2)、定位到init块,从init块中使用模糊匹配特征C7 ?? 30 ?? ?? ?? ?? C7 ?? 34。
3)、找到的地址为24968h,记为Atapi_24968h。
4)、取Atapi_24968h + 11h的位置,并换算成RVA,得到fnRVA_IdePortDispatch。
5)、取Atapi_24968h + 18h的位置,并换算成RVA,得到fnRVA_IdePortDispatchDeviceControl。
6)、如果上面的方法获取失败,则强制转换:
fnRVA_IdePortDispatch = 67B4h,
fnRVA_IdePortDispatchDeviceControl = 0A592h。
7)、获取这两个函数的SVA
fnIdePortDispatch = fnRVA_IdePortDispatch + DriverStart,
fnIdePortDispatchDeviceControl = fnRVA_IdePortDispatchDeviceControl + DriverStart。
8)、注册Atapi的派遣例程
_DRIVER_OBJECT.IrpDeviceControlDispatch =
fnIdePortDispatchDeviceControl,
_DRIVER_OBJECT.IrpInternalDeviceControlDispatch = fnIdePortDispatch。

7、定位被覆盖文件的逻辑簇号
1)、打开文件conime.exe,失败则打开userinit.bat;并获取FileObject。
2)、根据FileObject获取_FILE_OBJECT.Vpb,再获取_VPB.DeviceObject。
3)、创建一个IRP。设MainFunction为IRP_MJ_FILE_SYSTEM_CONTROL。
4)、设定_IO_STACK_LOCATION.FsControlCode =
FSCTL_GET_RETRIEVAL_POINTERS
//其它IRP初始化略过,详细请看idb文件。
5)、调用自己实现的IOCallDriver,获取指定文件的RETRIEVAL_POINTERS_BUFFER信息。
结构查阅msdn如下:
typedef struct RETRIEVAL_POINTERS_BUFFER {
   DWORD ExtentCount;
   LARGE_INTEGER StartingVcn;
   struct {
     LARGE_INTEGER NextVcn;
     LARGE_INTEGER Lcn;
   } Extents[1];
} RETRIEVAL_POINTERS_BUFFER,
*PRETRIEVAL_POINTERS_BUFFER;
要注意的是按8个字节对齐的,所以DWORD ExtentCount后、LARGE_INTEGER StartingVcn前有4个字节的空间。
ExtentCount记录了该文件分成了n块存储。
StartingVcn记录了起始虚拟簇号。
Extents.NextVcn记录了下一个块的起始虚拟簇号,所以Extents.NextVcn - Extents[i - 1].NextVcn表示i块所占的簇数。
Extents.Lcn记录了第i + 1块的起始逻辑簇号。注意i是从0开始的。
本机器狗使用了不严格的计算方法,仅用到了该结构中的Extents[0].Lcn,即文件的起始逻辑簇号,并没有考虑分多块存储的情况。因此,理论上是存在破坏系统文件的风险的。这也是病毒不负责任的地方。

8、MyRead_WriteSector
从这个函数名可以看出这是一个读写扇区的例程。前缀My表示该函数是由我标识的,用来跟ida自动识别的函数进行区别,以便查找。这个函数很重要,整个驱动中会调用三次,所以先解析。
MyRead_WriteSector(DeviceObj, IRP_ID, pBuffer, dwSectorLowPos, SecNum);
DeviceObj:操作设备对象,这里为Disk。
IRP_ID:3表示读取,4表示写入。
pBuffer:写入设备的数据地址,或读取数据的内存辞职。
dwSectorLowPos:读取或写入的扇区号。其实扇区号为LARGE_INTEGER类型的大数,但这里仅使用了低32位,没有使用高32位。
SecNum:读取或写入的扇区数。
下面为函数的实现介绍。
1)、通过使用MyIoCallDriver(病毒自己实现的IoCallDriver)来向Disk设备发送服务号为IRP_MJ_INTERNAL_DEVICE_CONTRO,通过向SCSI端口读写数据实现的。
2)、填充IRP
mov      [edi+_IRP.MdlAddress], eax; eax为pBuffer对应的Mdl
......     ;省略部分代码
mov      [ebp+UserIosb], ebx   ; ebx == 0
lea      eax, [ebp+UserIosb]
mov      [edi+_IRP.UserIosb], eax
lea      eax, [ebp+Event]
mov      [edi+_IRP.UserEvent], eax
mov      [edi+1Ch], ebx   ; _IRP.Information = 0
mov      [edi+18h], ebx   ; _IRP._IO_STATUS_BLOCK = 0
or       [edi+_IRP.Flags], 5 ; IRP_SYNCHRONOUS_API | IRP_NOCACHE
mov      [edi+0Ch], ebx   ; _IRP.IrpCount = 0
mov      [edi+24h], bl    ; _IRP.Cancel = 0
mov      [edi+38h], ebx   ; _IRP.CancelRoutine = 0
mov      [edi+20h], bl    ; _IRP.RequestorMode = 0
call     KeGetCurrentThread
mov      [edi+50h], eax   ; _IRP.Thread
mov      eax, [edi+60h]   ; _IRP.CurrentStackLocation
sub      eax, 24h         ; IoGetNextStackLocation
mov      byte ptr [eax], IRP_MJ_INTERNAL_DEVICE_CONTROL
mov      [eax+4], esi     ; _IO_STACK_LOCATION.scsi._SCSI_REQUEST_BLOCK
mov      ecx, [ebp+DiskDeviceObj]
mov      [eax+14h], ecx   ; _IO_STACK_LOCATION.DeviceObject = DiskDeviceObj
mov      eax, [edi+60h]
sub      eax, 24h
mov      dword ptr [eax+1Ch], offset fnCompletionRoutine ; _IO_STACK_LOCATION.CompletionRoutine
mov      [eax+20h], esi   ; _IO_STACK_LOCATION.Context
mov      [eax+_IO_STACK_LOCATION.Control], 0E0h
从填充的IRP可以看到一个重要的结构_SCSI_REQUEST_BLOCK。
3)、_SCSI_REQUEST_BLOCK
关于结构的定义可以找相关资料。用来存放向SCSI端口写数据的一些信息。这里看一下对这个srb的填充。
mov      word ptr [esi], 40h ; _SCSI_REQUEST_BLOCK.Length = 40h
mov      [esi+2], bl      ; _SCSI_REQUEST_BLOCK.Function = 0
mov      eax, [ebp+pBuffer]   ;pBuffer指向传入的参数,即存放数据的地址。
mov      [esi+18h], eax   ; _SCSI_REQUEST_BLOCK.DataBuffer = pBUffer
movzx    eax, [ebp+SecNum]
shl      eax, 9           ; eax = SecNum * 512
mov      [esi+10h], eax   ; _SCSI_REQUEST_BLOCK.DataTransferLength = SecNum * 512 //即写入数据的大小
mov      byte ptr [esi+9], 20h ; _SCSI_REQUEST_BLOCK.QueueAction = 20h
mov      [esi+3], bl      ; _SCSI_REQUEST_BLOCK.SrbStatus = 0
mov      [esi+4], bl      ; _SCSI_REQUEST_BLOCK.ScsiStatus = 0
mov      [esi+20h], ebx   ; _SCSI_REQUEST_BLOCK.NextSrb = NULL
mov      [esi+1Ch], ecx   ; _SCSI_REQUEST_BLOCK.SenseInfoBuffer = pSenseInfoBuffer
mov      byte ptr [esi+0Bh], 12h ; _SCSI_REQUEST_BLOCK.SenseInfoBufferLength = 12h
xor      eax, eax
cmp      [ebp+IRP_ID], IRP_MJ_READ ; if(IRP_ID != IRP_MJ_READ)
setnz    al               ; {al = 1;}
dec      eax              ; eax --;
and      eax, 0FFFFFFC0h ; eax &= 0x0ffffffc0;
add      eax, 80h         ; eax += 80h;
; //if IRP_ID == IRP_MJ_READ eax == 40h SRB_FLAGS_DATA_IN
; //else eax == 80h SRB_FLAGS_DATA_OUT
mov      [esi+0Ch], eax   ; _SCSI_REQUEST_BLOCK.SrbFlags = 80h
cmp      [ebp+IRP_ID], IRP_MJ_READ
jnz      short loc_11997
or       eax, 200h SRB_FLAGS_ADAPTER_CACHE_ENABLE
mov      [esi+0Ch], eax   ; _SCSI_REQUEST_BLOCK.SrbFlags = 240h
loc_11997:
or       dword ptr [esi+0Ch], 20h ; _SCSI_REQUEST_BLOCK.SrbFlags = A0h(If Read 260h)
; 20h == SRB_FLAGS_DISABLE_AUTOSENSE
mov      eax, [esi+10h]   ; eax = _SCSI_REQUEST_BLOCK.DataTransferLength
shr      eax, 0Ah
inc      eax
mov      [esi+14h], eax   ; _SCSI_REQUEST_BLOCK.TimeOutValue = _SCSI_REQUEST_BLOCK.DataTransferLength >> 0Ah + 1
mov      eax, [ebp+dwSectorLowPos]
mov      [esi+2Ch], eax   ; _SCSI_REQUEST_BLOCK.QueueSortKey = dwSectorLowPos
mov      byte ptr [esi+0Ah], 0Ah ; _SCSI_REQUEST_BLOCK.CdbLength = 0Ah
填充这个结构读数据和写数据的SrbFlags分别设为SRB_FLAGS_DATA_IN | SRB_FLAGS_ADAPTER_CACHE_ENABLE |
SRB_FLAGS_DISABLE_AUTOSENSE和SRB_FLAGS_DATA_OUT | SRB_FLAGS_DISABLE_AUTOSENSE。
4)、_CDB
在_SCSI_REQUEST_BLOCK结构最后是UCHAR Cdb[16];但是Cdb不一定是16个字节,长度由_SCSI_REQUEST_BLOCK.CdbLength决定,从上面可以看出这里是0Ah个字节。该结构是一个联合体,这里使用的是_CDB10结构。
mov      byte ptr [esi+0Ah], 0Ah ; _SCSI_REQUEST_BLOCK.CdbLength = 0Ah
mov      cl, [ebp+IRP_ID]
add      cl, 11h
shl      cl, 1
mov      [esi+30h], cl    ; _CDB[0] = 28h
mov      cl, [esi+31h]
and      cl, 1Fh
or       cl, 80h
mov      [esi+31h], cl    ; _CDB[1] = 80h
mov      ecx, eax
shr      ecx, 18h
mov      [esi+32h], cl    ; if Read _CDB[2] = 0
mov      ecx, eax
shr      ecx, 10h
mov      [esi+33h], cl    ; if Read _CDB[3] = 0
mov      ecx, eax
shr      ecx, 8
mov      [esi+34h], cl    ; if Read _CDB[4] = 0
mov      [esi+35h], al    ; if Read _CDB[5] = 0
mov      ax, [ebp+SecNum]
mov      [esi+37h], ah    ; if Read _CDB[7] = 0
cmp      ax, bx
setnz    al
mov      [esi+38h], al    ; if Read _CDB[8] = 1
从scsi.h中找到如下:
// 10-byte commands
#define SCSIOP_READ_FORMATTED_CAPACITY 0x23
#define SCSIOP_READ_CAPACITY        0x25
#define SCSIOP_READ                 0x28
#define SCSIOP_WRITE                0x2A
可以看出,_CDB[0]为OperationCode,28h表示SCSIOP_READ,2Ah表示SCSIOP_WRITE。_CDB[1]按位描述了一些属性,可以参考scsi.h,这里将这个字节设为80h。_CDB[2]到_CDB[5]描述的是扇区位置,依次为dwSectorLowPos从高字节到低字节的值。_CDB[7]和_CDB[8]描述了要写书的扇区数。如果小于0,则设为1。
由于本人的语言组织能力较弱,写得比较乱,详细的请看idb文件。这个函数将会完成向指定扇区读写数据。

9、读取主分区表
调用My_Read_WriteSector(DiskDeviceObj, 3, pBuffer, 0, 1)来获取磁盘0号扇区的数据,大小为1个扇区的大小,即512字节。
0号扇区保存了主分区表,结构为:
typedef struct _MBR_SECTOR
{
    UCHAR              BootCode[446];
    PARTITION_ENTRY    Partition[4];
    USHORT            Signature;
} MBR_SECTOR, *PMBR_SECTOR;
这里主要用到了PARTITION_ENTRY结构:
typedef struct _PARTITION_ENTRY
{
    UCHAR active;                  // 能否启动标志
    UCHAR StartHead;                // 该分区起始磁头号
    UCHAR StartSector;              // 起始柱面号高2位:6位起始扇区号
    UCHAR StartCylinder;            // 起始柱面号低8位
    UCHAR PartitionType;            // 分区类型
    UCHAR EndHead;                  // 该分区终止磁头号
    UCHAR EndSector;              // 终止柱面号高2位:6位终止扇区号
    UCHAR EndCylinder;              // 终止柱面号低8位
    ULONG StartLBA;                // 起始扇区号
    ULONG TotalSector;              // 分区尺寸(总扇区数)
} PARTITION_ENTRY, *PPARTITION_ENTRY;
其中active为80h,表示该分区能够启动系统。
PartitionType表示分区类型,如NTFS、FAT32等等,如果为FF表示该分区为扩展分区,扩展分区会再有分区表,划分为逻辑分区。如果你有兴趣,可以查阅其它资料。
本处依次判断Partition[0]到Partition[3]是否为启动分区,如果是,则计算StartLBA,这里比较有意思的是StartLBA[n] = StartLBA[0] + ... +StartLBA[n],这个我是比较奇怪的,从《数据恢复技术》一书中,我的理解是,如果为基本分区,StartLBA直接是分区的起始逻辑扇区,如果是扩展分区,则需要加上的保留扇区数。这里是让我感到不解的,如果自己分析,要自己测试多个系统、多种分区格式的分区表,遂放弃。希望知道的兄弟能够指点迷津,抑或是机器狗的一个bug。但对于大多数系统,Partition[0]即是启动分区,Partition[0].StartLBA也就是该分区的起始扇区位置。
判断Partition[0].PartitionType如果不是PARTITION_TYPE_FAT32、PARTITION_TYPE_FAT32_LBA或PARTITION_TYPE_NTFS,则返回失败。

10、读取启动分区的第一个扇区
定位到启动分区后,调用My_Read_WriteSector(DiskDeviceObj, 3, pBuffer, StartLBA, 1)来读取启动分区的第一个扇区的内容,即分区信息。结构如下:
typedef struct _BBR_SECTOR
{
    USHORT JmpCode;                // 2字节跳转指令,跳转到引导代码
    UCHAR    NopCode;                // 1字节nop指令,填充用,保证跳转指令长3个字节
    UCHAR    OEMName[8];              // 8字节的OEMName
    // 下面开始为: BPB( BIOS Parameter Block )
    USHORT BytesPerSector;          // 每个扇区的字节数 (512 1024 2048 4096)
    UCHAR    SectorsPerCluster;      // 每个簇的扇区数 ( 1 2 4 8 16 32 64 128 )两者相乘不能超过32K(簇最大大小)
    USHORT ReservedSectors;        // 从卷的第一个扇区开始的保留扇区数目,该值不能为0,对于FAT12/FAT16,该值通常为1,对于FAT32,典型值为32
    UCHAR    NumberOfFATs;            // 卷上FAT数据结构的数目,该值通常应为2,[NTFS不使用NumberOfFATs字段,必须为0]
    USHORT RootEntries;            // 对于FAT12/FAT16,该值表示32字节目录项的数目,对于FAT32,该值必须为0;[NTFS不使用]
    USHORT NumberOfSectors16;      // 该卷上的扇区总数,该字段可以为0,如果该字段为0,则NumberOfSectors32不能为0;对于FAT32,该字段必须为0 [FAT32/NTFS不使用该字段]
    UCHAR    MediaDescriptor;        // 介质类型
    USHORT SectorsPerFAT16;        // 该字段标识一个FAT结构占有的扇区数(FAT12/FAT16),对于FAT32卷,该字段必须为0;[FAT32/NTFS不使用该字段]
    USHORT SectorsPerTrack;        // 用于INT 0x13中断的每个磁道的扇区数
    USHORT HeadsPerCylinder;        // 用于INT 0x13中断的每个柱面的磁头数
    ULONG    HiddenSectors;          // 包含该FAT卷的分区之前的隐藏扇区数
    ULONG    NumberOfSectors32;      // 该字段包含该卷上的所有扇区数目,对于FAT32,该字段不为0;FAT12/FAT16可根据实际大小是否超过65536个扇区数决定是否采用该字段; [NTFS不使用该字段]
    // 下面开始为: EBPB ( Extended BIOS Parameter Block )
    ULONG    SectorsPerFAT32;        // 对于FAT32,该字段包含一个FAT的大小,而SectorsPerFAT16字段必须为0;
} BBR_SECTOR, *PBBR_SECTOR;

取得分区信息之后,获取SectorsPerCluster和ReservedSectors。
判断Partition[0].PartitionType是否是PARTITION_TYPE_FAT32或PARTITION_TYPE_FAT32_LBA,如果是,则获取SectorsPerFAT32和NumberOfFATs。

11、计算文件对应的扇区位置
Lcn_HightPart 和Lcn_LowPart由第7步获得。
1)、如果是FAT32分区,dwSectorLowPos = StartLBA + ReservedSectors + SectorsPerFAT32 * NumberOfFATs + SectorsPerCluster * Lcn_LowPart;
FileSectorHighPos = Lcn_HightPart * SectorsPerCluster;
2)、如果是NTFS分区,dwSectorLowPos = StartLBA + ReservedSectors + SectorsPerCluster * Lcn_LowPart;
FileSectorHighPos = Lcn_HightPart * SectorsPerCluster;
FileSectorHighPos在将数据写入扇区时,并没有使用。

12、写入病毒
是由DeviceIoControl的800C004h号控制命令触发的,写入文件的数据由DeviceIoControl传入。调用My_Read_WriteSector(DiskDeviceObj, 4, pBuffer, dwSectorLowPos,dwFileSize / 512 + 1)来实现向扇区的写入。
另外DeviceIoControl的800C008h号控制命令会摘掉ntfs的AttachedDevice。

参考资料
1、《数据恢复技术》
2、http://dev.csdn.net/article/78/78564.shtm
3、http://hi.baidu.com/ptf_phoenix/ ... 9f6ecca7866994.html
4、winddk
5、msdn
还有一些网上的其它资料,不一一列出。
提问请注意详细描述现象、操作过程,如果是病毒报告,应说明病毒名,染毒文件路径、文件名等,什么现象都不描述只发一个日志的帖子将被直接删除。

请新会员关注新手杀毒入门

TOP

拜读。。
感觉涉及了许多磁盘的底层的一些知识。。。
自己水平有限。。。
还需提高。。。

TOP

支持

TOP

无异于天书。惭愧!

TOP

分析日志的时候发现这个病毒    反汇编难度比较的大

TOP

TOP

学习

TOP

虽然 看不明白 但看得出是技术贴 恩 好贴 !

TOP

天书啊

TOP

值得信赖的金山系列软件~`我选择我喜欢~

TOP

kan bu dong

TOP

高手的专利啊!

TOP

天书!

TOP

TOP

TOP

TOP

TOP

a……头大了啊……
好好学习,天天向上!

TOP

这些汇编指令是谁编译出来的!偶样本都没找到

TOP

机器代码要用int 13h

lz,可能是
MOV CX,0001
MOV BX,0012
MOV DX,
MOV DL,0080
MOV AX,4466(XXXX:4466)
INT 13H

TOP

 21 12
发新话题