Multiple out-of-bounds read and write vulnerabilities exist in the ControlVault WBDI Driver Broadcom Storage Adapter functionality of Dell ControlVault3 5.14.3.0. A specially crafted WinBioControlUnit call can lead to memory corruption. An attacker can issue an api call to trigger this vulnerability.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
Broadcom BCM5820X
Dell ControlVault3 5.14.3.0
ControlVault3 - https://dell.com/ BCM5820X - https://www.broadcom.com/products/embedded-and-networking-processors/secure/bcm5820x
7.3 - CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H
CWE-805 - Buffer Access with Incorrect Length Value
Dell ControlVault is a hardware based solution that can securely store passwords, biometric templates and security codes. It can interface with smart cards, Near-field Communication (NFC) devices and fingerprint readers. The hardware solution is based on the Broadcom BCM5820X chip series.
Control Vault WBDI Driver is Broadcom’s implementation of the Windows Biometric Driver Interface (WBDI) leveraging the Control Vault hardware to provide biometrics features using Windows APIs. The relevant features are implemented inside three DLLs (BrcmEngineAdapter.dll, BrcmSensorAdapter.dll, BrcmStorageAdapter.dll). The vulnerabilities in this reports are all found inside Broadcom’s implementation of the Storage Adapter. Biometrics drivers are instantiated by the WinBio Service running under svchost as SYSTEM.
When implementing the various WinBio interfaces, an Adapter can provide a mechanism akin to a DeviceIoControl by implementing the ControlUnit and/or ControlUnitPrivileged functions. Any unprivilege user can access the ControlUnit function by first opening a WinBio session (WinBioOpenSession) locking the relevant BiometricUnit (WinBioLockUnit) and finally issuing the control command (WinBioControlUnit). These control commands are vendor-defined and thus will vary between vendors/models. One can also choose to limit access to the more critical APIs by implementing sensitive features under the ControlUnitPrivileged function instead .
BrcmStorageAdapter.dll does not implement any ControlUnitPrivileged function, but instead has 4 different commands inside the ControlUnit function. For reference, the prototype of any ControlUnit function is as follow:
NTSTATUS __stdcall StorageAdapterControlUnit(
PWINBIO_PIPELINE Pipeline,
ULONG ControlCode,
PUCHAR SendBuffer,
SIZE_T SendBufferSize,
PUCHAR ReceiveBuffer,
SIZE_T ReceiveBufferSize,
PSIZE_T ReceiveDataSize,
PULONG OperationStatus)
The key point to note is that the ControlCode specifies which functionality the caller wants to trigger, and SendBuffer, SendBufferSize, ReceiveBuffer ReceiveBufferSize are parameters that are controlled via the caller’s request.
The BrcmStorageAdapter fails to properly bound-check the Receive/Send buffers which leads to multiple out-of-bound reads (if the SendBuffer is smaller than expected) and out-of-bound writes (if the ReceiveBuffer is too small).
Memory corruptions inside the WinBioSVC can lead to Denial of Service (crashing the service), information leaks and possibly code execution as SYSTEM.
This vulnerability is triggered when submitting a WinBioControlUnit call to the StorageAdapter with the ControlCode 2 (WBIO_USH_GET_IDENTITY) with 4 <= ReceiveBuferSize < 80 This will lead to an out-of-bound write of up to 75 bytes. These can be either null-bytes or potentially attacker controlled data if another vulnerability is leveraged to place attacked-controlled data as an WINBIO_IDENTITY object inside the storage’s database (see TALOS-XXXXX).
We can see the relevant code here:
// ControlCode:
// 0: WBIO_USH_GET_TEMPLATE
// 2: WBIO_USH_GET_IDENTITY
// 3: WBIO_USH_CREATE_CHALLENGE
// 4: WBIO_USH_ADD_RECORD
NTSTATUS __stdcall StorageAdapterControlUnit(
PWINBIO_PIPELINE Pipeline,
ULONG ControlCode,
PUCHAR SendBuffer,
SIZE_T SendBufferSize,
PUCHAR ReceiveBuffer,
SIZE_T ReceiveBufferSize,
PSIZE_T ReceiveDataSize,
PULONG OperationStatus)
{
(...) // vaiable init
log_stuff((int)L"StorageAdapterControlUnit() enter\n");
if ( !Pipeline
|| !SendBuffer
|| !SendBufferSize
|| (ReceiveBuffer_ = ReceiveBuffer) == 0i64
|| (ReceiveBufferSize_ = ReceiveBufferSize) == 0 //[0]
|| (ReceiveDataSize_ = ReceiveDataSize) == 0i64
|| (OperationStatus_ = OperationStatus) == 0i64 )
{
log_stuff((int)L"StorageAdapterControlUnit() ERROR: invalid parameter\n");
status = E_POINTER;
goto DONE;
}
(....)
if ( !ControlCode )
{
//Case 0
}
control_code_minus_2 = ControlCode - 2;
if ( control_code_minus_2 )
{
// case 3-4
}
// Case ControlCode == 2
log_stuff((int)L"StorageAdapterControlUnit() WBIO_USH_GET_IDENTITY\n");
*(_OWORD *)ReceiveBuffer_ = 0i64;
*((_OWORD *)ReceiveBuffer_ + 1) = 0i64; //[1]
*((_OWORD *)ReceiveBuffer_ + 2) = 0i64;
*((_OWORD *)ReceiveBuffer_ + 3) = 0i64;
*((_OWORD *)ReceiveBuffer_ + 4) = 0i64;
*ReceiveDataSize_ = 0i64;
if ( ReceiveBufferSize_ < 4 ) //[2]
goto BAD_ReceiveBufferToSmall;
if ( !WBFUSH_ExistsCVObject(*(_DWORD *)SendBuffer, &objType) )
{
log_stuff((int)L"StorageAdapterControlUnit. Object does not exist\n");
status = 0;
*OperationStatus_ = 8;
goto DONE;
}
if ( objType != CV_TYPE_FINGERPRINT )
{
log_stuff((int)L"StorageAdapterControlUnit. Object is not an FP template\n");
status = 0;
*OperationStatus_ = 10;
goto DONE;
}
v21 = StorageAdapterQueryByCVHandle(Pipeline, *(_DWORD *)SendBuffer); // [5]
if ( v21 >= 0 )
{
v22 = Pipeline->StorageInterface;
if ( v22 )
{
FirstRecord_ = v22->FirstRecord;
if ( FirstRecord_ )
{
status = FirstRecord_(Pipeline);
if ( status >= 0 )
{
status = BrcmStorageAdapterGetCurrentRecord(Pipeline, &v32);
if ( status >= 0 )
{
Indentity = v32.Indentity;
status = 0;
*(_OWORD *)ReceiveBuffer_ = *(_OWORD *)&v32.Indentity->Type; //[3]
*((_OWORD *)ReceiveBuffer_ + 1) = *(_OWORD *)&Indentity->Value.SecureId[12];
*((_OWORD *)ReceiveBuffer_ + 2) = *(_OWORD *)&Indentity->Value.SecureId[28];
*((_OWORD *)ReceiveBuffer_ + 3) = *(_OWORD *)&Indentity->Value.SecureId[44];
*((_QWORD *)ReceiveBuffer_ + 8) = *(_QWORD *)&Indentity->Value.SecureId[60];
*((_DWORD *)ReceiveBuffer_ + 18) = *(_DWORD *)&Indentity->Value.SecureId[68];
ReceiveBuffer_[76] = v32.Subfactor; // [4]
*ReceiveDataSize_ = 0x50i64;
*OperationStatus_ = 0;
goto DONE;
}
// Error Handling
DONE:
log_stuff((int)L"StorageAdapterControlUnit() exit\n");
return status;
}
We can see at [0] the checks to ensure the relevant size and buffer aren’t 0. At [1] 0x50 null-bytes are written in the ReceiveBuffer (note the OWORD* cast explaining the amount of data written); at [2] the ReceiveBuffer’s size is checked to be at least 4 (too little too late). Then the logic of the function continues normally and tries to retrieve a WINBIO_IDENTITY object from the local storage database based on a handle provided by the caller function. If the content of this WINBIO_IDENTITY object is controlled by the attacker (and it can be), then we have potentially up to 72 (76-4) attacker-controlled bytes written out-of-bound. Subsequently, the Subfactor is also potentially written out of bound (as a BYTE), but this value is not fully attacker controlled as only a limited range is allowed by the Storage Api.
Note: There might also be an OOB-Read at [5] if 0< SendBufferSize < 4 but memory alignment considerations may make this difficult to target in any meaningful way.
Here we can seen an example of crash that occurs soon after triggering the Out-of-bound write:
0:013> !analyze -v
*******************************************************************************
* *
* Exception Analysis *
* *
*******************************************************************************
KEY_VALUES_STRING: 1
Key : AV.Dereference
Value: NullPtr
Key : AV.Type
Value: Write
Key : Analysis.CPU.mSec
Value: 718
Key : Analysis.Elapsed.mSec
Value: 28299
Key : Analysis.IO.Other.Mb
Value: 10
Key : Analysis.IO.Read.Mb
Value: 11
Key : Analysis.IO.Write.Mb
Value: 32
Key : Analysis.Init.CPU.mSec
Value: 640
Key : Analysis.Init.Elapsed.mSec
Value: 31213
Key : Analysis.Memory.CommitPeak.Mb
Value: 110
Key : Analysis.Version.DbgEng
Value: 10.0.27793.1000
Key : Analysis.Version.Description
Value: 10.2410.02.02 amd64fre
Key : Analysis.Version.Ext
Value: 1.2410.2.2
Key : Failure.Bucket
Value: SVCHOSTGROUP_WbioSvcGroup_NULL_POINTER_WRITE_NULL_INSTRUCTION_PTR_INVALID_POINTER_WRITE_c0000005_wbiosrvc.dll!std::list_std::shared_ptr_Binding_,std::allocator_std::shared_ptr_Binding_____::_Unchecked_erase
Key : Failure.Exception.Code
Value: 0xffffffffc0000005
Key : Failure.Exception.IP.Address
Value: 0x7ff8cb794dc8
Key : Failure.Exception.IP.Module
Value: wbiosrvc
Key : Failure.Exception.IP.Offset
Value: 0x14dc8
Key : Failure.Hash
Value: {f0bb19bb-0b4e-e7c4-4847-8fb3f45a7134}
Key : ProblemClass.Collapse.After.BUCKET_ID
Value: SVCHOSTGROUP_WbioSvcGroup_NULL_POINTER_WRITE_NULL_INSTRUCTION_PTR_INVALID_POINTER_WRITE
Key : ProblemClass.Collapse.After.DEFAULT_BUCKET_ID
Value: SVCHOSTGROUP_WbioSvcGroup_NULL_POINTER_WRITE_NULL_INSTRUCTION_PTR_INVALID_POINTER_WRITE
Key : ProblemClass.Collapse.After.PRIMARY_PROBLEM_CLASS
Value: SVCHOSTGROUP_WbioSvcGroup_NULL_POINTER_WRITE
Key : ProblemClass.Collapse.Before.BUCKET_ID
Value: SVCHOSTGROUP_WbioSvcGroup_NULL_POINTER_WRITE_NULL_INSTRUCTION_PTR_INVALID_POINTER_WRITE
Key : ProblemClass.Collapse.Before.DEFAULT_BUCKET_ID
Value: SVCHOSTGROUP_WbioSvcGroup_NULL_POINTER_WRITE_NULL_INSTRUCTION_PTR_INVALID_POINTER_WRITE
Key : ProblemClass.Collapse.Before.PRIMARY_PROBLEM_CLASS
Value: SVCHOSTGROUP_WbioSvcGroup_NULL_POINTER_WRITE
Key : Timeline.OS.Boot.DeltaSec
Value: 1948
Key : Timeline.Process.Start.DeltaSec
Value: 617
Key : WER.OS.Branch
Value: vb_release
Key : WER.OS.Version
Value: 10.0.19041.1
Key : WER.Process.Version
Value: 10.0.19041.4355
FILE_IN_CAB: sensor_dump1.dmp
NTGLOBALFLAG: 0
APPLICATION_VERIFIER_FLAGS: 0
CONTEXT: (.ecxr)
rax=0000000000000000 rbx=00000187b06f2640 rcx=00000187af619810
rdx=00000076fc37f6f0 rsi=00000187af619810 rdi=00000187af63b610
rip=00007ff8cb794dc8 rsp=00000076fc37f670 rbp=00000076fc37f700
r8=0000000000000000 r9=fffffffffffe0898 r10=00000fff1bc71f6c
r11=280a112240100000 r12=0000000000000001 r13=0000000000000002
r14=00000076fc37f6f0 r15=00000187af619810
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010200
wbiosrvc!std::list<std::shared_ptr<Binding>,std::allocator<std::shared_ptr<Binding> > >::_Unchecked_erase+0x28:
00007ff8`cb794dc8 4c8900 mov qword ptr [rax],r8 ds:00000000`00000000=????????????????
Resetting default scope
EXCEPTION_RECORD: (.exr -1)
ExceptionAddress: 00007ff8cb794dc8 (wbiosrvc!std::list<std::shared_ptr<Binding>,std::allocator<std::shared_ptr<Binding> > >::_Unchecked_erase+0x0000000000000028)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 0000000000000001
Parameter[1]: 0000000000000000
Attempt to write to address 0000000000000000
PROCESS_NAME: svchost.exe
WRITE_ADDRESS: 0000000000000000
ERROR_CODE: (NTSTATUS) 0xc0000005 - The instruction at 0x%p referenced memory at 0x%p. The memory could not be %s.
EXCEPTION_CODE_STR: c0000005
EXCEPTION_PARAMETER1: 0000000000000001
EXCEPTION_PARAMETER2: 0000000000000000
GROUP: WbioSvcGroup
FAULTING_SERVICE_NAME: WbioSrvc
STACK_TEXT:
00000076`fc37f670 00007ff8`cb7d0249 : 00000187`af63b610 00000076`fc37f7b0 00000187`af63b610 00007ff8`cb7d066e : wbiosrvc!std::list<std::shared_ptr<Binding>,std::allocator<std::shared_ptr<Binding> > >::_Unchecked_erase+0x28
00000076`fc37f6a0 00007ff8`cb7c51aa : 00000076`fc37f7b0 00000187`b0d302d0 00000000`ffffffff 00000187`b0d2a240 : wbiosrvc!BindingManager::Release+0x79
00000076`fc37f730 00007ff8`cb7c1baa : 00000187`af645ad0 00000076`fc37f7b0 00000187`b0d2a250 00000076`fc37f960 : wbiosrvc!CSensorPool::ReleaseBinding+0x16
00000076`fc37f760 00007ff8`cb803564 : 00000187`af645ad0 00000187`b0d2a240 00000187`b0d2a240 00000187`b0d2a250 : wbiosrvc!CBiometricServiceProvider::ReleaseBinding+0x276
00000076`fc37f860 00007ff8`cb7ac9b7 : 00000000`00000006 00000000`ffffffff 00000000`00000006 00000000`ffffffff : wbiosrvc!CWinBioSrvUnlockUnit::Execute+0x204
00000076`fc37f9e0 00007ff8`cb786e53 : 00000000`00000000 00000000`00000000 00000187`b0df7a50 00000000`00000000 : wbiosrvc!_sensorControlThreadRoutine+0x9a7
00000076`fc37fde0 00007ff8`cb786ea5 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : wbiosrvc!CThread::ThreadMain+0x1f
00000076`fc37fe10 00007ff8`e01f7374 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : wbiosrvc!CThread::ThreadStartup+0x45
00000076`fc37fe40 00007ff8`e04bcc91 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0x14
00000076`fc37fe70 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21
SYMBOL_NAME: wbiosrvc!std::list<std::shared_ptr<Binding>,std::allocator<std::shared_ptr<Binding> > >::_Unchecked_erase+28
MODULE_NAME: wbiosrvc
IMAGE_NAME: wbiosrvc.dll
STACK_COMMAND: ~13s; .ecxr ; kb
FAILURE_BUCKET_ID: SVCHOSTGROUP_WbioSvcGroup_NULL_POINTER_WRITE_NULL_INSTRUCTION_PTR_INVALID_POINTER_WRITE_c0000005_wbiosrvc.dll!std::list_std::shared_ptr_Binding_,std::allocator_std::shared_ptr_Binding_____::_Unchecked_erase
OS_VERSION: 10.0.19041.1
BUILDLAB_STR: vb_release
OSPLATFORM_TYPE: x64
OSNAME: Windows 10
IMAGE_VERSION: 10.0.19041.4355
FAILURE_ID_HASH: {f0bb19bb-0b4e-e7c4-4847-8fb3f45a7134}
Followup: MachineOwner
---------
The crash is a null-pointer write in the winbiosvc that hosts the StorageAdapter. This is because the Out-of-bound write corrupted adjacent memory leading to the crash. As one of the dll loaded along with the adapter lacks ASLR, this could potentially be turned into an actually targeted corruption of its data section, assuming one can control the data being written (step [3] in the previous code description).
This vulnerability is triggered when submitting a WinBioControlUnit call to the StorageAdapter with the ControlCode 0 (WBIO_USH_GET_TEMPLATE) and with either 0 < ReceiveBuferSize < 4 and/or 0 < SendBufferSize < 76. The former will lead to an out-of-bound write of up to 3 bytes and the latter will trigger an out-of-bound read of up to 75 bytes.
We can see the relevant code here:
// ControlCode:
// 0: WBIO_USH_GET_TEMPLATE
// 2: WBIO_USH_GET_IDENTITY
// 3: WBIO_USH_CREATE_CHALLENGE
// 4: WBIO_USH_ADD_RECORD
NTSTATUS __stdcall StorageAdapterControlUnit(
PWINBIO_PIPELINE Pipeline,
ULONG ControlCode,
PUCHAR SendBuffer,
SIZE_T SendBufferSize,
PUCHAR ReceiveBuffer,
SIZE_T ReceiveBufferSize,
PSIZE_T ReceiveDataSize,
PULONG OperationStatus)
{
(...) // vaiable init
log_stuff((int)L"StorageAdapterControlUnit() enter\n");
if ( !Pipeline
|| !SendBuffer
|| !SendBufferSize
|| (ReceiveBuffer_ = ReceiveBuffer) == 0i64
|| (ReceiveBufferSize_ = ReceiveBufferSize) == 0 //[0]
|| (ReceiveDataSize_ = ReceiveDataSize) == 0i64
|| (OperationStatus_ = OperationStatus) == 0i64 )
{
log_stuff((int)L"StorageAdapterControlUnit() ERROR: invalid parameter\n");
status = E_POINTER;
goto DONE;
}
(....)
if ( !ControlCode )
{
log_stuff((int)L"StorageAdapterControlUnit() WBIO_USH_GET_TEMPLATE\n");
*(_DWORD *)ReceiveBuffer_ = 0; // oob write [1]
*ReceiveDataSize_ = 0i64;
if ( ReceiveBufferSize_ < 4 ) // [2]
goto BAD_ReceiveBufferToSmall;
SubFactor = SendBuffer[76]; // [3]
if ( SubFactor == 0xFF ) // OOB Read ?
{
log_stuff((int)L"StorageAdapterControlUnit() ERROR: Unsupported sub factor\n");
status = 0x80070057;
goto DONE;
}
StorageInterface = Pipeline->StorageInterface;
if ( StorageInterface && (QueryBySubject = StorageInterface->QueryBySubject) != 0i64 )
{
v28 = QueryBySubject(Pipeline, (PWINBIO_IDENTITY)SendBuffer, SubFactor); // [4]
if ( v28 >= 0 )
{
v29 = Pipeline->StorageInterface;
if ( v29 )
{
FirstRecord = v29->FirstRecord;
if ( FirstRecord )
{
status = FirstRecord(Pipeline);
if ( status >= 0 )
{
status = BrcmStorageAdapterGetCurrentRecord(Pipeline, &v32);
if ( status >= 0 )
{
memmove(ReceiveBuffer_, (const void *)v32.TemplateBlob, v32.TemplateBlobSize);
*ReceiveDataSize_ = 4i64;
status = 0;
*OperationStatus_ = 0;
goto DONE;
}
goto LABEL_60;
}
LABEL_63:
log_stuff((int)L"StorageAdapterControlUnit. WbioStorageFirstRecord Failed\n");
*OperationStatus_ = 1;
goto DONE;
}
BAD_E_NOTIMPL:
status = E_NOTIMPL;
goto LABEL_63;
}
LABEL_62:
status = E_POINTER;
goto LABEL_63;
}
log_stuff((int)L"StorageAdapterControlUnit. WbioStorageQueryBySubject Failed\n");
if ( v28 == WINBIO_E_DATABASE_NO_RESULTS )
{
*OperationStatus_ = 1;
status = 0;
goto DONE;
}
}
else
{
log_stuff((int)L"StorageAdapterControlUnit. WbioStorageQueryBySubject Failed\n");
}
status = 0;
goto DONE;
}
control_code_minus_2 = ControlCode - 2;
if ( control_code_minus_2 )
{
// case 3-4
}
// Case ControlCode == 2
(...)
(...) // Error Handling
DONE:
log_stuff((int)L"StorageAdapterControlUnit() exit\n");
return status;
}
We have a check at [0] to ensure the pointers are defined and the sizes are not null. Then, a null-DWORD is written at [0], potentially out-of-bound if 0< ReceiveBufferSize < 4, before a bound checks is performed at [1]. Beyond the initial null-check at [0], no subsequent check is done on the SendBuffer which can then be read out-of-bound at [3] and [4]. The latter call assumes the SendBuffer contains a WINBIO_IDENTITY object and tries to look it up in the local database. If an attacker can control part of the content of the local database, this could be used as an oracle to leak a couple of bytes of heap memory.
This vulnerability is triggered when submitting a WinBioControlUnit call to the StorageAdapter with the ControlCode 3 (WBIO_USH_CREATE_CHALLENGE) and with 0 < ReceiveBuferSize < 4. Up to three null-bytes will be written past the end of the ReceiveBuffer.
The relevant code is as follow:
// ControlCode:
// 0: WBIO_USH_GET_TEMPLATE
// 2: WBIO_USH_GET_IDENTITY
// 3: WBIO_USH_CREATE_CHALLENGE
// 4: WBIO_USH_ADD_RECORD
NTSTATUS __stdcall StorageAdapterControlUnit(
PWINBIO_PIPELINE Pipeline,
ULONG ControlCode,
PUCHAR SendBuffer,
SIZE_T SendBufferSize,
PUCHAR ReceiveBuffer,
SIZE_T ReceiveBufferSize,
PSIZE_T ReceiveDataSize,
PULONG OperationStatus)
{
(...) // vaiable init
log_stuff((int)L"StorageAdapterControlUnit() enter\n");
if ( !Pipeline
|| !SendBuffer
|| !SendBufferSize
|| (ReceiveBuffer_ = ReceiveBuffer) == 0i64
|| (ReceiveBufferSize_ = ReceiveBufferSize) == 0 //[0]
|| (ReceiveDataSize_ = ReceiveDataSize) == 0i64
|| (OperationStatus_ = OperationStatus) == 0i64 )
{
log_stuff((int)L"StorageAdapterControlUnit() ERROR: invalid parameter\n");
status = E_POINTER;
goto DONE;
}
(....)
if ( !ControlCode )
{
// case 0
(...)
}
control_code_minus_2 = ControlCode - 2;
if ( control_code_minus_2 )
{
control_code_minus_3 = control_code_minus_2 - 1;
if ( control_code_minus_3 )
{
if ( control_code_minus_3 == 1 )
{
//case 4
}
}
else {
//case 3
*(_DWORD *)ReceiveBuffer_ = 0; //[1]
*ReceiveDataSize_ = 0i64;
if ( ReceiveBufferSize_ >= 4 ) //[2]
{
(...) //WBIO_USH_CREATE_CHALLENGE function's logic
goto DONE;
}
BAD_ReceiveBufferToSmall:
log_stuff((int)L"StorageAdapterControlUnit. Receive buffer too small\n");
status = TPC_E_INSUFFICIENT_BUFFER;
goto DONE;
}
}
//case 2
DONE:
log_stuff((int)L"StorageAdapterControlUnit() exit\n");
return status;
}
We can see that at [0] the code ensures the ReceiveBuffer is not null or zero-sized. At [1] a 4 null-bytes are written into ReceiveBuffer and at [2] the code verify that ReceiveBuffer size is at least 4 bytes. This leads to up to 3-bytes of out-of-bound write in the case 0 < ReceiveBuferSize < 4. Memory alignment constraints may make this bug difficult to trigger.
This vulnerability is triggered when submitting a WinBioControlUnit call to the StorageAdapter with the ControlCode 4 (WBIO_USH_ADD_RECORD) and with 0 < SendBufferSize < 104. A various amount of bytes can be read ouf of bound past the end the SendBuffer. The constraints surrounding this exploitation are pretty tough and may make the exploitation of this vulnerability less likely or limited to a Denial of Services.
The relevant code is as follow:
// ControlCode:
// 0: WBIO_USH_GET_TEMPLATE
// 2: WBIO_USH_GET_IDENTITY
// 3: WBIO_USH_CREATE_CHALLENGE
// 4: WBIO_USH_ADD_RECORD
NTSTATUS __stdcall StorageAdapterControlUnit(
PWINBIO_PIPELINE Pipeline,
ULONG ControlCode,
PUCHAR SendBuffer,
SIZE_T SendBufferSize,
PUCHAR ReceiveBuffer,
SIZE_T ReceiveBufferSize,
PSIZE_T ReceiveDataSize,
PULONG OperationStatus)
{
(...) // vaiable init
log_stuff((int)L"StorageAdapterControlUnit() enter\n");
if ( !Pipeline
|| !SendBuffer
|| !SendBufferSize
|| (ReceiveBuffer_ = ReceiveBuffer) == 0i64
|| (ReceiveBufferSize_ = ReceiveBufferSize) == 0 //[0]
|| (ReceiveDataSize_ = ReceiveDataSize) == 0i64
|| (OperationStatus_ = OperationStatus) == 0i64 )
{
log_stuff((int)L"StorageAdapterControlUnit() ERROR: invalid parameter\n");
status = E_POINTER;
goto DONE;
}
(....)
if ( !ControlCode )
{
// case 0
(...)
}
control_code_minus_2 = ControlCode - 2;
if ( control_code_minus_2 )
{
control_code_minus_3 = control_code_minus_2 - 1;
if ( control_code_minus_3 )
{
if ( control_code_minus_3 == 1 )
{
//case 4
log_stuff((int)L"StorageAdapterControlUnit() WBIO_USH_ADD_RECORD\n");
if ( *(_QWORD *)(SendBuffer + 84) == *(_QWORD *)&StorageContext->Challenge.field_0 //[1]
&& *(_QWORD *)(SendBuffer + 92) == *((_QWORD *)&StorageContext->Challenge.field_0 + 1)
&& *((_DWORD *)SendBuffer + 25) == StorageContext->Challenge.field_10 )
{
if ( WBFUSH_ExistsCVObject(*((_DWORD *)SendBuffer + 20), &objType) ) // [2]
{
if ( objType == CV_TYPE_FINGERPRINT )
{
v32.Subfactor = SendBuffer[76]; // [3]
v32.IndexVector = 0i64;
v32.IndexElementCount = 0i64;
v32.PayloadBlob = 0i64;
v32.PayloadBlobSize = 0i64;
v32.Indentity = (WINBIO_IDENTITY *)SendBuffer; // [4]
v32.TemplateBlob = (__int64)(SendBuffer + 80);// templateBlob_cv_handle
v32.TemplateBlobSize = 4i64;
if ( StorageContext->BrcmStorageAdapterAddRecord(Pipeline, &v32) >= 0 ) // [5]
{
*ReceiveDataSize_ = 0i64;
status = 0;
*OperationStatus_ = 0;
}
else
{
log_stuff((int)L"StorageAdapterControlUnit. BrcmAddRecord\n");
*OperationStatus_ = 9;
status = 0;
}
}
else
{
log_stuff((int)L"StorageAdapterControlUnit. Not a FP template\n");
status = 0;
*OperationStatus_ = 10;
}
}
else
{
log_stuff((int)L"StorageAdapterControlUnit. WBFUSH_ExistsCVObject Failed\n");
status = 0;
*OperationStatus_ = 8;
}
}
else
{
log_stuff((int)L"StorageAdapterControlUnit. WBIO_USH_ADD_RECORD authorization Failed\n");
status = 0;
*OperationStatus_ = 7;
}
}
else {
//case 3
(...)
BAD_ReceiveBufferToSmall:
log_stuff((int)L"StorageAdapterControlUnit. Receive buffer too small\n");
status = TPC_E_INSUFFICIENT_BUFFER;
goto DONE;
}
}
//case 2
DONE:
log_stuff((int)L"StorageAdapterControlUnit() exit\n");
return status;
}
We can see that at [0] the code ensures the SendBuffer is not null or zero-sized. At [1] data is read out of bound (range SendBuffer[84:104]) that is compared to a 20-byte Challenge. This Challenge is null by default but can be changed during runtime operations (in which case the StorageContext->Challenge will not be attacker-controlled). If the challenge-check is successful, data is read from SendBuffer[80:84] as a template handle which will be used at [2] as a lookup into the ControlVault device to confirm it maches a template stored on the device. Upon success, SendBuffer[0:76] is stored in the local database as a WINBIO_IDENTITY object (at [4], [5]) assuming certains checks are met. Heap shaping techniques might be used to trigger a situation where a ~70 bytes hole is created between two attacker-controlled blobs in which case the stored identity in the local database could be retrieved and used to leak data via the WBIO_USH_GET_IDENTITY command. The many constraints on this attack make it less likely to be successful, while a Denial-Of-Service due to readding an unmapped memory region is the more likely outcome of this attack; this would then trigger a crash of the WinBio Service.
2025-04-22 - Vendor Disclosure
2025-06-13 - Vendor Patch Release
2025-11-17 - Public Release
Discovered by Philippe Laulheret of Cisco Talos.