We’ve been thinking a lot about security these past couple of months here at OSR. No, I’m not talking about the fact that our 401K is in the toilet, because that’s been true for a long time. And I’m not talking about that warm fuzzy feeling you get when you curl up with your significant other. I’m taking about security and device drivers – even more specifically, security that’s applied to devices and operations performed on them. While reading this article, please keep in mind that everything we discuss only applies to device’s that are directly opened and communicated with from user mode.
Just Gotta Have Faith?
Face it. Most of the drivers we write for strange devices rely on the good intentions (or ignorance) of their users. Let’s say you have a driver that controls communications with a satellite. The driver implements a ton of IOCTLs that allow people to send and receive data from the satellite, enable and disable individual transponders, and perhaps even move the satellite around some. In most cases, these IOCTLs all receive the same protection: Little or none.
You have an application that’s run by lots of folks, in lots of situations, to receive data from the satellite. Fine. You also have a “move the satellite around in orbit” application, that should only be run by certain engineering team members. Perhaps this critical application is sophisticated enough to check to see if the user that’s running it is a member of the group “ADMINISTRATORS”.
But what happens when some summer hire in the customer service group (and clearly not a member of the Admins group) gets pissed off and writes their own little application that issues an IOCTL_SAT_MOVE that requests the satellite to go jetting off towards Venus? If your driver is like about 95% of the drivers out there, what happens is that the satellite indeed heads for Venus. And given the problem your driver has caused, you might want to consider following it. Oh well, I’m sure your company has both a sense of humor and a satellite to spare.
Use Protection
So, before you lose any more satellites, let’s discuss some mechanisms by which we can avoid this problem. The first and easiest is to specify the protection to be applied to your Device Objects in your driver’s INF file.
If your device is in its own device class, you can specify a default security descriptor to be applied to all devices in the class as part of the AddReg section that’s referenced from the ClassInstall32 section of your INF file. The security descriptor is described using (appropriately enough) “security descriptor definition language” (SDDL) which is fully documented in the SDK. Your ClassInstall32 and AddReg sections might look something like that in Figure 1.
[ClassInstall32]
Addreg=AcmeSatellite
[AcmeSatellite]
HKR,,,,"Satellites and Stuff"
HKR,,Icon,,103 ; Empty
HKR,,Security,,"D:P(A;CIOI;GR;;;WD)(A;CIOI;GA;;;BA)(A;CIOI;GA;;;SY)"
HKR,,DeviceCharacteristics,0x10001,0x00000100 ; FILE_DEVICE_SECURE_OPEN
Figure 1 — ClassInstall32 & AddReg
All that the ClassInstall32 section in Figure 1 does is refer to an AddReg section. As usual, this AddReg section establishes the name and attractive icon for the class. You could also specify a DLL with your device’s property pages here. The Security entry allows you to specify a default Security Descriptor (SD) for all devices in your device class. This SD will be applied to both to your device’s PDOs and FDOs.
Creating (or even reading) the SD in SDDL can be confusing, at least initially. Once you figure it out, it’s really pretty straight forward. Look in the SDK index under “Security Descriptor Definition Language” and you’ll have more information than you ever wanted to know. But to help get you started, the SD format is:
O:owner-sidG:group-sidD:dacl-flags(ace)(ace)S:sacl-flags(ace)(ace)
Anything you don’t need, you leave out. So, the SDDL string in the AddReg section in Figure 1 contains only a DACL (because it starts D:). A DACL supplies “discretionary access control”, which is “discretionary” in the sense that it is at a user’s discretion to establish it (not in the sense that it’s “discretionary” for the operating system to enforce it!)
There’s one DACL flag supplied in the SD in Figure 1, P. This means the DACL is “protected” (SE_DACL_PROTECTED), i.e. it can’t be modified by any inheritable ACEs. Following the “P”, the SD in Figure 1 has three Access Control Entries (ACEs).
Each ACE specifies the access allowed by a specific group, for example: (A;CICO;GR;;;WD). The A in the ACE means that this is an access allowed ACE. So it’s going to describe the access to the device that will be available to a group. The CIOI specifies flags for the ACE header. CI here means that child containers inherit this ACE as the effective ACE. OI means that child objects inherit this ACE as the effective ACE. The GR specifies the allowed access, which in this case is Generic Read. The WD specifies the group. WD means “World” or “Everyone” as this is more typically labeled.
The other ACEs can be decoded similarly: (A;CIOI;GA;;;BA)(A;CIOI;GA;;;SY). BA (which is the “Built-in Administrators” group) gets GA (i.e. Generic All) access. SY (the System group) also gets Generic All access. See, SDDL may be ugly, but it’s really not so hard to understand.
You might also notice that the INF file segment shown in Figure 1 also specifies DeviceCharacteristics. This statement allows you to specify default values for the DeviceObject->DeviceCharacteristics field. In the case above, we specified the value 0x100 which works out to be FILE_DEVICE_SECURE_OPEN. Incidentally, you can also use this same AddReg section to specify a default Device Type for your device. And, believe it or not, this is all documented in the DDK. Just look for the phrase “INF AddReg Directive” in the DDK index.
Does this Solve the Satellite Problem?
This device protection scheme solves the satellite problem. Your IOCTLs to do nice, non-destructive, type things to the satellite are created with FILE_READ_ACCESS. These can include those in Figure 2.
#define IOCTL_SAT_READ \
CTL_CODE(FILE_DEVICE_SAT, 2048, METHOD_IN_DIRECT, FILE_READ_ACCESS);
#define IOCTL_SAT_GET_STATUS \
CTL_CODE(FILE_DEVICE_SAT, 2047, METHOD_BUFFERED, FILE_ANY_ACCESS);
#define IOCTL_SAT_MOVE \
CTL_CODE(FILE_DEVICE_SAT, 2046, METHOD_BUFFERED, FILE_WRITE_ACCESS);
Figure 2 — IOCTLs via FILE_READ_ACCESS
It’s important to realize that as long as you’re using IOCTLs (and not IRP_MJ_READ and IRP_MJ_WRITE) to talk to this driver, you could also include an IOCTL that can write data to the satellite, and have that IOCTL only require FILE_READ_ACCESS. It may look strange, but there’s no reason that it won’t work (See Figure 3).
#define IOCTL_SAT_WRITE \
CTL_CODE(FILE_DEVICE_SAT, 2045, METHOD_OUT_DIRECT, FILE_READ_ACCESS);
Figure 3 — Write Operation IOCTL (set for FILE_READ)
Remember, the sole purpose of the Access argument to the CTL_CODE macro is to tell the I/O Manager what bits to check in the mask of granted access that’s associated with the file handle. There’s no reason that these bits must reflect the actual I/O operation in any way.
More Control
Let’s say you want yet more control. Or, even more likely, you want more flexibility. In general, it’s not a great idea to have device drivers set system policy. At some point, somebody is likely to want to change something. In our example, perhaps they’ll want to change the group of people who can issue satellite maneuvering commands from Administrators (which is a pretty dumb group to use in the first place, actually) to a custom defined group such as Engineering.
Probably the best way to accomplish this aim, and to create a much more refined and flexible security implementation, is to:
- Create an application that allows an appropriate privileged user to build a Security Descriptor and send that SD to the driver using an IOCTL.
- Have the driver receive the SD built by the app and store it away.
- Have the driver check the access of the requestor against the stored SD, whenever a restricted operation is requested
This really isn’t nearly as hard as it sounds. Creating that app to build the SD is pretty much a snap (for an app person that is) using the functions in ACLUI.DLL – It’s all described in the SDK of course. In fact, if you’re really clever, you could even add a security property sheet to your device property pages. Just make sure you build a Self Relative SD, and send it to your driver via an IOCTL.
The driver probably wants to be careful about from whom it accepts such SD information. For example, it might want to check to ensure that the caller has a specific privilege. The code might look something like that shown in Figure 4.
case IOCTL_OSR_SET_SECURITY: {
LUID neededPrivilege = {0};
BOOLEAN userHasAccess = FALSE;
ULONG bufLength =
ioStack->Parameters.DeviceIoControl.InputBufferLength;
SECURITY_SUBJECT_CONTEXT SubjectSecurityContext;
PSECURITY_DESCRIPTOR incomingSd=NULL;
neededPrivilege =
RtlConvertLongToLuid(SE_REMOTE_SHUTDOWN_PRIVILEGE);
userHasAccess = SeSinglePrivilegeCheck(neededPrivilege,
Irp->RequestorMode);
if(userHasAccess == FALSE) {
code = STATUS_PRIVILEGE_NOT_HELD;
break;
}
incomingSd = (PSECURITY_DESCRIPTOR)
Irp->AssociatedIrp.SystemBuffer;
__try {
if( (bufLength != 0) &&
(incomingSd != NULL) ) {
if(!RtlValidSecurityDescriptor(incomingSd)) {
code = STATUS_INVALID_PARAMETER;
break;
}
} else {
code = STATUS_INVALID_PARAMETER;
break;
}
mySd = ExAllocatePoolWithTag(NonPagedPool,
bufLength,
'DSoN');
RtlCopyMemory(mySd,
incomingSd,
bufLength);
code = STATUS_SUCCESS;
} __except(EXCEPTION_EXECUTE_HANDLER) {
if(mySd != NULL) {
ExFreePool(mySd);
}
code = STATUS_INVALID_PARAMETER;
}
break;
}
Figure 4 — Confirming Proper Privileges
The code in Figure 4 only uses functions from WDM.H, believe it or not. It first checks to see if the caller has “SeRemoteShutdownPrivilege” – You should substitute whatever privilege you like here. We chose this priv because it’s almost exclusively held only by Administrators. Check the privilege set, and use whatever one makes sense for you.
Once the priv check is passed, the code in Figure 4 (which is designed to use METHOD_BUFFERED, in case that’s not obvious) validates the received buffer and if it’s valid, then calls RtlValidSecurityDescrptor(…) to validate the received SD. If that works, the driver allocates a hunk of pool, and copies the (self relative) SD to it. Note that the checks are done in a __try/__except block.
Now, when the driver receives an IOCTL that it wants to protect, it checks the access of the requestor against the stored SD. This check is pretty simple, and is shown in Figure 5.
case IOCTL_OSR_PROTECTED2: {
BOOLEAN accessGranted=FALSE;
SECURITY_SUBJECT_CONTEXT subjectContext;
ACCESS_MASK grantedAccess=FALSE;
code = STATUS_ACCESS_DENIED;
SeCaptureSubjectContext(&subjectContext);
accessGranted = SeAccessCheck(mySd, // Security descriptor
&subjectContext, // Subject Context
FALSE, // Subject context locked??
FILE_WRITE_DATA, // Desired access
0, // Previously granted access
NULL, // Privileges (output, optional)
IoGetFileObjectGenericMapping(), // Generic mapping
Irp->RequestorMode, // Access mode
&grantedAccess, // mask of granted access
&code); // completion status for function
if(accessGranted) {
code = STATUS_SUCCESS;
}
SeReleaseSubjectContext(&subjectContext);
if(accessGranted) {
DoProtectedStuffHere(DeviceObject, Irp);
}
break;
}