Introduction
I wanted to have some fun on patch diffing : on the latest “patch tuesday” update, Microsoft released a patch for an information disclosure vulnerability in the storage spaces controller. This vulnerability has been found by Quang Linh, working at STAR Labs. Because leaking information is often easier than a full blown RCE/LPE exploit, I selected this vuln because it would take less time to analyze on my spare time. The following article will be organized as follows:
- patch diffing and discovery
- triggering it
- getting a controlled leak?
- conclusion
- some spaceport.sys documentation
Patch diffing and discovery
The first question to answer is : where should I look at? Indeed, Windows bulletins are now more abstract than ever. Fortunately, previous vulnerabilities found on this component by Fabien Periguaud(@0xf4b) and the ZDI bulletin both point to look at the spaceport.sys
driver. I got the december and january versions of this driver, loaded it in IDA, and used bindiff to look for the modified code. Here are the results:
- no difference in unmatched functions
- 3 changed function in the “matched functions” window, which is nice from an analysis point of view (
SpIoctlCreateTier
,SP_POOL::SetTierInfo
,SP_POOL::SetSpaceInfoTransaction
):
Only by looking at the names, one can see the vulnerability seems related to Tier
object. Let’s analyse the changes further. In the SpIoctlCreateTier
function, the following code has been changed (you can click on the image for a better view):
First, Microsoft introduced a safe addition ensuring no integer overflow occurs. The members of this addition are the offset where GUIDs should be copied from the IRP’s system buffer, and the total size taken by those GUIDs. Furthermore, the result of this operation is checked against the total length field of the IRP’s system buffer written inside the said buffer. (A check at the beginning of the function ensures this length is indeed equal to the length of the IRP’s buffer.)
In the SpIdsCopyHelper
function called afterwards, memory is allocated with a size corresponding to the total size taken by the GUIDs, and the GUIDs pointed by the given offset are then copied into this newly allocated memory. This allocated memory is then integrated within the SDB_TIER
object created. This SDB_TIER
object itself is registered inside an SDB_POOL_CONFIG
object, and it finally gets destroyed.
Before the patch, by providing a malicious offset inside the buffer, one could copy content of memory outside of the IRP’s system buffer into the SDB_TIER
object:
The remaining two patched functions are also patched in the same way. Once again, it was possible to give an arbitrary offset inside the IRP buffer leading to the same problem.
Triggering it
From the little analysis made on previous paragraph, here are the necessary steps to trigger the vulnerability:
- Ensure we have a pool on the machine : this is automatically done by Microsoft once you have three disks on the machine. If I’m not mistaken, you cannot create pool while there are less disks.
- Call one of those function that permits to leak memory
- retrieve the leaked memory that has been registered inside the
SDB_POOL_CONFIG
object. This can be done by callingSpIoctlGetTierInfo
orSpIoctlgetSpaceInfo
respectively to the function used to leak memory.
Unfortunately, the step 2 is protected by two checks:
First, one needs a valid pool ID. This is not a problem : pools’ IDs can be listed using SpIoctlGetPools
, which is not protected from a standard user. Second, one needs to pass the SpAccessCheckPool
function, which -as its name suggests- checks if you have the necessary rights to do the operation. This function in fact checks against the SpControlExt
object’s security descriptor if the current context of the operation has the valid right (the access mask associated with the underlying SeAccessCheck
function is TOKEN_MANDATORY_POLICY_VALID_MASK
). In a default configuration, that means the access is validated for the local administrators group and the system account. One could find that strange, because admin to kernel is not considered a security boundary by Microsoft. However, as pointed out by Quang Linh on Twitter, an administrator could have set up a security descriptor allowing full access to a pool for a given user, thus allowing a simple user to trigger the vulnerability on the said pool. Here is a way to do so in Powershell (certainly not the optimal one):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Get-WmiObject -Namespace "root/microsoft/windows/storage" -Class MSFT_StoragePool
# from the result, get the path of the given pool
# now we can set up the security descriptor
# first get it
Invoke-WmiMethod -path '<your_path>' -name GetSecurityDescriptor
...
SecurityDescriptor : O:BAG:SYD:(A;;FX;;;WD)(A;;FA;;;SY)(A;;FA;;;BA)
...
# now set additional access rights to the given user sid
Invoke-WmiMethod -path '<your_path>' -name SetSecurityDescriptor -ArgumentList "O:BAG:SYD:(A;;FA;;;WD)(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;<user_sid>)"
Well, time to trigger the vulnerability!
You can find the associated code within this github repository. To be able to run the code, you need to be administrator and to give the name of a pool for whom a tier can be created (tiers cannot be created on the primordial pool apparently).
Getting a controlled leak?
One of the question that arises from the vulnerability is: can it be used to leak a given kernel object of interest? Here are a few observations obtained through the use of Windbg (whose results are to be taken with caution because I did not go deep in the reverse of this part):
- The system buffer appears to be allocated in the NonPagedPoolNxCacheAligned pool.
- Whenever I checked the buffer, it seems to be aligned on a page. Because I was able to get the same leak randomly, it may seems this page is taken from a pool of pages for Io operations. Perhaps there is some kind of lookaside list in the Io manager?
- This page appears to be formed of the chunk corresponding to the system buffer, and then empty/free space.
I also tried quickly to allocate pool blocks, and then freed particular ones, in order to create holes that could be used by the IRP buffer. This test was a fail, but it may be due to the test itself that was badly executed.
So in the end, I have no idea if it’s possible to get a given object behind the buffer of an IRP. Perhaps people like Yarden Shafir, Corentin Bayet or guys of KunlunLab would know. If so, I would love to hear about it. Anyway, I’ll certainly go back to this for a future post.
Conclusion
Because one needs to be already in the local administrators group to trigger the vulnerability, its real impact appears low. Indeed, from an attacker perspective, this vulnerability appears useless as a mean to gain more information than what the attacker could already get with his rights. However, this is still a a somewhat memory corruption vulnerability that got corrected here, and it may prevent the introduction of the same vulnerable pattern inside spaceport.sys in the future.
A bit of spaceport.sys
documentation
The following documentation was obtained through reverse engineering, in order to trigger the leak. Given the low impact of this vulnerability, I did not bother to go deep in the reverse and as such the following documentation is quite harsh. Please be indulgent.
- SpIoctlGetPools:
- purpose : lists all the pools on the machine
- IOCTL code: 0xE70004
- input: an empty buffer whose length is at least equal to 4 bytes.
- output : a structure like the following, where N is equal to the number of pools on the machine
1 2 3 4
typedef struct { ULONG nbPools; // == N GUID listGuids[N]; // a list of all the pools' GUIDs } POOLSLIST, *PPOOLSLIST;
- SpIoctlGetPoolInfo
- purpose : get the information about a pool
- IOCTL code: 0xE70008
- no particular access check
-
input:
a buffer whose size is superior to 0x28, with the following fields:
- the input buffer size, equal to 0x28, as a dword at offset 0
- the pool GUID at offset 4
This is equivalent to fill in the two fields size and poolGUID in the same structure than for output.
- output: the following structure:
1 2 3 4 5 6 7 8 9 10 11 12 13
typedef struct { int size; GUID poolGUID; int field_14; wchar_t friendlyName[256]; wchar_t description[1024]; __int16 field_A18; //because needs more reversing BYTE gapA1A[82]; //because needs more reversing int thinProvisioningAlertThresholds; BYTE gapA70[143]; //because needs more reversing char field_AFF; //because needs more reversing }POOLINFO, *PPOOLINFO;
- SpIoctlCreateTier
- purpose : to create a tier associated with a pool
- SpAccessCheck security verification
- IOCTL code: 0xE7D410
-
input: a structure like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
typedef struct { int length_bis; int length; GUID PoolGUID; GUID TierGUID; GUID spaceGUID; wchar_t friendlyName[256]; wchar_t description[1024]; int usage; int field_A3C; __int64 field_A40; BYTE gapA48[16]; int field_A58; int field_A5C; __int64 field_A60; int mediatype; int field_A6C; int faultDomainAwareness; int AllocationUnitSize; int field_A78; int numOfGuids; int offsetGuids; int field_A84; int physicalDiskRedundancy; int NumberOfDataCopies; int field_A90; int NumberOfColumns; int Interleave; int field_A9C; int field_AA0; int field_AA4; __int64 field_AA8; char additionalData[]; }POOLTIER, *PPOOLTIER;
- output: no particular output
- SpIoctlDeleteTier
- purpose : to delete a tier
- SpAccessCheck security verification
-
IOCTL code: 0xE7D414
- input: a structure containing the GUID of the pool whose tier is attached, and the GUID of the tier:
1 2 3 4 5
typedef struct { GUID poolGUID; GUID tierGUID; } POOLDELETETIER, *PPOOLDELETETIER;
- output: no particular output
- SpIoctlGetTierInfo
- purpose : get the information about a tier
- no particular access checks
- IOCTL code: 0xE71408
- input: a structure like this one (which is the starts of a POOLTIER structure):
1 2 3 4 5 6 7
typedef struct { int unk; int length; //should be set to 0x28 GUID PoolGUID; GUID TierGUID; }
- output : a POOLTIER structure