Skip to content

Add support for Synaptics 06cb:00bb (HP Spectre x360 13-ap0xxx) — blocked on hw_key derivation #283

Description

@STYNET

Add support for 06cb:00bb (HP Spectre x360 13-ap0xxx) — most of the groundwork is done, blocked on one key-derivation step

Device

  • USB ID: 06cb:00bb ("Synaptics WBDI Fingerprint Reader - USB 102")
  • Laptop: HP Spectre x360 Convertible 13-ap0xxx
  • Previously the only reference to this exact PID was nmikhailov/Validity90#71, open without resolution since 2019.

What I did

I captured the real Windows Hello driver (HP SoftPaqs SP92631 fingerprint driver + SP96872 Intel SGX PSW — this variant uses an SGX enclave, synaTEE102.signed.dll, for the crypto) talking to the physical sensor, via a libvirt VM with the sensor passed through as a USB hostdev, captured host-side with usbmon/tshark (~4300 packets, ~10 successful TLS 1.2 handshakes with the real chip). Then cross-checked everything against this repo's real source (not just filenames) and against static analysis of the two driver DLLs (synaWudfBioUsb102SGX.dll, synaTEE102.signed.dll).

Findings

1. init_hardcoded for 06cb:00bb, found and verified

usb.py: send_init() sends 01, 19, 4302, then init_hardcoded. The real Windows driver sends the exact same sequence, and the value it sends for the 4th step is identical across all 25 occurrences in the capture (spanning ~66 min, multiple VM reboots) — i.e. it's static, not session-randomized. I found this exact 581-byte value (06 02 00 00 01 header + 576-byte payload) verbatim inside synaTEE102.signed.dll and synaWudfBioUsb102SGX.dll, as one entry (offset 0x1ad034 / 1757236 in the former) of a table of 77 similar entries — presumably one per supported sensor variant.

# validitysensor/blobs_bb.py
from .util import unhex

init_hardcoded = unhex(
    "0602000001"
    "4a231406e5542fc6dc3b1aedebe68f55596ad3ca13f6e019994c6f71672fff756fbde0511d09d45978b12ba415b3694a0e76348c8cfe9dbb9abf86813fc0c67c1005519a6f87360c2fb3e12bd0a9e012b06d9f5c9b44ccc6645b0fbd47afe45c8c874fcb88fbfd18fb7a9b3241351f256acce689f9586a52b01f8fdcb66cdf3b340b1f9f386d58ca24fdfcdfbcebefb5f3a3c2a08357721040235a20ce1ee2f4f7856e0d9c27b92cd9b975c86f2c8cab1179868f795da674004b93c15e6ac8aa825a1907f2003cb9e6df096423167b2cabae98c0cd3fd200d11c7e0ee1ba5a725f7f2022886f3caa5f68b688ba61bc5cb0190db569efa0a57aa9d76ecdc7440c8920ea02768734221260d083bebb39c176d129c01d1a0f1388497171402ba041afd925d71e76ce4905e44fa5fd528759a2c9f12895864b5aa394dc71a4a17161dd82197a10742fa5f3135c5e78820e36653fa3db535f57c71897242939d7da50f81070ce9ab81c61af6ac29a6c6c4a5df73ffd08542fb540e417939ed117298051d27736c2faf1c5577a2133b6f60ea7484c692faae2a49c51c8e6f69af77774bad51a9adeea3109d3611d6a437cdc0c356e46a8f9a6d3054c5519c17c986e54f61f8329006ce184c275984799dbdf5512188fa8ff10aa2ddc25eb697ebdcb156509309ade5d7909a734bf35ec69e062cb941c2ea4af0958110da93bd2b5f17fc9b1ebdbd8020a3c36f22a68f707226cec716126d6a830219521669bd59fa0e4bd35db6ef0aa2999d1c0e7acf67e598696cd58cc4bdb1b7c037ee9a085f784c4"
)
init_hardcoded_clean_slate = None  # never observed — err was always 0 on this device
reset_blob = None
db_write_enable = None

2. 06cb:00bb's on-flash parse_tls_flash() blocks decode 100% cleanly with the existing format

The 4096-byte read_tls_flash() response from a real device decodes perfectly as 3 concatenated blocks in your existing id/sz/hash/body format:

Block Offset id sz
private key 45 4 161
cert 242 3 184
ECDH 1502 6 400

The ECDH sub-fields land exactly where handle_ecdh() expects them (x @ body+0x08, y @ body+0x4c, a 71-byte valid DER ECDSA signature @ body+0x90 with its 4-byte LE length prefix). So no new flash format for this variant — it's identical to what's already implemented.

3. password_hardcoded / gwk_sign_hardcoded are shared, not per-variant

I disassembled synaTEE102.signed.dll (x86-64 PE, objdump) looking for the GWK/GWK_SIGN/HS_KEY_PAIR_GEN PRF-label strings (all present verbatim — same protocol), found the two functions that build a 32-byte buffer byte-by-byte right before each PRF call, and extracted both constants. Both are byte-for-byte identical to the values already hardcoded in tls.py. So this isn't the blocker either.

4. The actual blocker: handle_priv()'s HMAC check fails, and I can't determine what hw_key this driver variant actually uses

With everything above correct, tls.parse_tls_flash()handle_priv() still raises Signature verification failed. This device was probably paired with another computer.

set_hwkey() assumes hw_key = product_name + '\0' + serial + '\0' from /sys/class/dmi/id/product_name/product_serial (Windows-side equivalent presumably SMBIOS too). I tried, all offline against the real captured HMAC:

  • The real laptop's actual DMI identity (HP Spectre x360 Convertible 13-ap0xxx + real serial)
  • The exact identity the capturing VM reported to Windows itself (confirmed via Get-CimInstance Win32_ComputerSystem/Win32_BIOS inside the VM that did the real capture: Manufacturer=QEMU, Model="Ubuntu 24.04 PC (Q35 + ICH9, 2009)", SerialNumber="") — the VM has no libvirt <sysinfo>/<smbios> override, so this is QEMU's generic default SMBIOS, and it's what the real driver saw when it successfully completed ~10 TLS handshakes with the physical chip.
  • The Windows MachineGuid (HKLM\SOFTWARE\Microsoft\Cryptography) from that same VM/install.
  • ~10 formatting variants of the above (reversed order, UTF-16, trailing space/no null terminators, manufacturer-only, braces around the GUID, raw GUID bytes in both byte orders, empty/empty).

None matched. Notably, strings on synaWudfBioUsb102SGX.dll (the non-enclave wrapper) shows imports for RegQueryValueExA/W, RegOpenKeyExW, CryptUnprotectData, CryptUnprotectMemory — no SMBIOS/WMI-related strings at all in either DLL. That suggests this driver variant may source (part of) its key material from the registry (possibly DPAPI-protected) rather than purely from product_name/serial — but I wasn't able to trace the actual call site without a decompiler (I only have objdump; happy to share the raw disassembly or binaries if that helps).

What I'm attaching / can provide

  • flash_read_4096B.bin — the real, complete 4096-byte read_tls_flash() response from this physical 06cb:00bb chip (read-only capture, includes the still-encrypted private key blob, cert, and ECDH block — nothing here is secret since the private key stays encrypted).
  • host_cert_blob_581B.bin — the init_hardcoded value above, as raw bytes.
  • The two HP driver DLLs (synaWudfBioUsb102SGX.dll, synaTEE102.signed.dll) if useful for someone with better disassembly tooling — from the public HP SoftPaq SP92631 (https://ftp.hp.com/pub/softpaq/sp92501-93000/sp92631.exe).
  • The full .pcapng capture (~4300 packets, real Windows driver ↔ real chip, ~10 complete TLS handshakes) if useful.

Happy to test any hypothesis against the real hardware (carefully — this is a match-on-chip sensor, I want to avoid sending anything destructive) or capture more traffic if it would help.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions