Fingerprint Driver Driving
The FT9201 fingerprint scanner has essentially no Linux support.
What looked like a cheap USB fingerprint reader turned into a months-long reverse engineering rabbit hole involving USB traces, stripped Windows drivers, vendor protocols, firmware state machines, and a surprising amount of yelling at a device that refused to wake up.
This post documents the first half of that journey: figuring out how the scanner actually talks.
Note:
This project is still ongoing.
At the time of writing, I have not yet produced a fully functional Linux driver for the FT9201 fingerprint scanner. The goal of this post is to document the reverse-engineering process, discoveries, failures, and protocol analysis performed so far.
If you’re attempting to support this device on Linux, hopefully these notes save you from repeating the same mistakes I made.
helloooo
The FT9201 fingerprint scanner has essentially no Linux support.
What looked like a cheap USB fingerprint reader turned into a months-long reverse engineering rabbit hole involving USB traces, stripped Windows drivers, vendor protocols, firmware state machines, and a surprising amount of yelling at a device that refused to wake up.
This post documents the first half of that journey: figuring out how the scanner actually talks.
Click bait? Yes. Rage bait? Yes.
I got hella ragebaited trying to extract the firmware binary from wireshark and joining it bit by bit, but will come to that in a while. Let’s start with how well the yea uk this started and yes I lowkey love incorrect grammar cuz that’s how my brain thinks.
tldr for serious people: Reverse Engineering the FT9201 Fingerprint Scanner or even better Chasing Linux Support for an Unsupported Fingerprint Sensor :D
{Timelapse to months back}
So my friend recently approached me, about how he has this one fingerprint scanner, specifically the FT9201Fingerprint.
This scanner has virtually zero Linux support. It’s a relatively cheap USB peripheral with non-existent documentation, just a package QR code linking to a random Chinese file host and a promise that it will “auto-magically work” on Windows (which, honestly, it kind of does).
Throwing both of its drivers into Cutter shows the debug symbols are stripped, but they left a bunch of debug strings behind. Judging from those and some leftover C++ type and method names, the host machine does all the heavy lifting, while the sensor just acts like a… well, a sensor.
Since the drivers auto-downloaded, sharing them here is probably fine license-wise. Not that Windows ever bothered to ask my permission to install them anyway (:
C:\Windows\System32\drivers\UMDF\ftUsbWbioDriver.dllC:\Windows\System32\WinBioPlugIns\ftWbioEngineAdapter.dll
(::Brain Time::)
My initial mental model was:
Host → Capture Command → Image Data
This turned out to be completely wrong. Alternatively, the device might automatically send data once the image is captured, requiring the host to read continuously.
In summary, the steps to decode the data would be:
- Extract all IN packets’ data after the capture command.
- Concatenate them in order (based on USB transaction sequence).
- Analyze the resulting binary data for structure (header, image data).
- If it’s raw pixels, determine the width and height .
- Prepend a BMP header with the correct dimensions and bit depth to create a viewable image.
- Use an image viewer to open the BMP and verify the fingerprint image.
flimsy
Digging in I figured the log structure(or more like assumed based on experiments) Each entry contains:
- Packet Number: Sequential identifier (e.g., 3174, 3175).
- Timestamp: Time elapsed (in seconds) since the capture began.
- Source/Destination: Either
host(the computer) or1.13.x(a USB device endpoint). - Protocol: Always
USB. - Length: Size of the packet in bytes.
- URB Type/Direction:
URB_CONTROL(control transfers for device setup/commands) orURB_BULK(data transfers).in(device-to-host) orout(host-to-device).
So let’s plan say first would be to reverse engineer the communication protocol(which I think that’s all, if I do it, it is full game on done).
So I did packet analysis! Log USB packets using a tool like Wireshark or USBPcap. Identify command/response patterns (e.g., enrollment start, image capture).
But wait there is more, let’s do some interfacing with the device.
Anyways, so now let’s talk about work. Note that I am developing on Fedora 40.
Let’s try a series of attempts to build upon our research:
- Device Identification:
# Find device details
lsusb | grep -i "fingerprint"
Output:
Bus 001 Device 003: ID 2808:93a9 Focal-systems.Corp FT9201Fingerprint.̚
Bus 003 Device 015: ID 2808:a658 Realtek USB2.0 Finger Print Bridge FocalTech Fingerprint Device
Yes so it does detects! Honestly I was not even expecting this, but hehe let’s go.
Find VID and PID of your scanner
lsusb
Output:
Bus 001 Device 005: ID 2808:93a9 Focal-systems.Corp FT9201Fingerprint.̚
VID (Vendor ID): 2808 PID (Product ID): 93a9
pause pause pause
Now hold on write down what you want your driver to do….hmmm, i guess:
should be able to scan when the scanner is plugged in and out properly, since we have referred to scanner as a scanner and nothing much
we want it to respond properly to input, like when we touch the scanner, it should capture the fingerprint in a set time frame and respond within a specified time
how do we discover what this time, read on existing fingerprint drivers for this device maybe, if you find some code online
also figure out a way to test out the scanner properly
we do have a confirmation some sought off that there is no linux drivers for this
understand if linux driver development changes across different linux distros
from the above protocol between the host and the device we can judge that we might do this in phases
figure out what sought off information we need to send in each phase, can we send the confirmation of each phase by updating the same bit string?
But the Enroll and Ack should happen only once?
Or, we do all 4 phases, whenever the system calls the driver? Yes this makes more sense
essentially how the device/scanner captures the image isnt our business, but is it? Let’s read that also
Let’s try setting a timeline:
Phase 1: USB communication framework and basic command handling
Phase 2: Image capture and reconstruction logic
Phase 3: Error handling and stability improvements
Phase 4: Integration with Linux authentication systems
We are in Phase 1 rn.
I think of doing some reading on Linux drivers, which I learnt are essentially Kernel Modules. And I read on the internet that one good way would be to develop and test out in VMs, but now I am curious, how we would do that if I have to test a usb driver, I m 50-50 on that, I think, yes, we might be able to test the inital logic, I dont want my fedora to blow up in my face, locked out leaving me all lonely and jobless.
Soooo….I got distracted and started looking up how to to Qemu stuff. No I am not giving you intro to Qemu. Just know Qemu is used to run VMs, at least, that’s all IK rn T_T (dont hate me pls).
But nah me too lazy, okay let’s get back.
To do for today?
Build a userspace driver (via
libusb) to:- Detect the device (VID=2808, PID=93a9).
- Send start capture commands.
- Receive bulk IN transfers.
- Dump the raw image data.
- Wrap it in a BMP header so you can view the fingerprint.
Wrap it in a kernel module skeleton that just:
- Registers the USB device.
- Logs connection/disconnection.
- Passes bulk IN data to
/dev/ft9201fingerprintfor userspace reading.
An intro to libfprint:
Open source library to provide access to fingerprint scanning devices.
Learnt more from links: https://fprint.freedesktop.org/libfprint-dev/intro.html https://fprint.freedesktop.org/libfprint-dev/advanced-topics.html
Attempt 1: Make the device TALK
opened device 2808:93a9
Claimed interface 0
Sent 16-byte bulk packets to EP 0x02
Waited on EP 0x83
Got LIBUSB_ERROR_TIMEOUT
We did all that, let’s make the device talk. But fingerprint sensors almost NEVER respond to random single-byte commands.
They usually require:
- Proper handshake
- Structured packet format
- Header + length + checksum
- Maybe control transfer first
- Sometimes interrupt endpoint instead of bulk
Let’s add a Protocol Handshake.
USB works, we now know
->Device alive
->No kernel interference
->Control endpoint functional
->Bulk endpoint functional
->No spontaneous streaming
So the ONLY missing thing:
Correct command format.
State Polling Experiment
Scanner confirmed register map. Control register behavior suggests read/write capability. Now implementing a polling loop to monitor key registers (0x01, 0x02, etc.) for changes when the sensor is touched, which would identify the Status/Interrupt register
Important Finding
When I tried:
Control OUT 0x01 with payload
I got:
LIBUSB_ERROR_PIPE
That means:
Device STALLED the control endpoint.
Which means:
- That request is defined
- But my data stage was invalid
This is gold. That means:
0x01 is a real command
But requires specific payload structure
We are very close.
So for next experiment I accidentally, brute-force enumerated the vendor command table.
Attempt 2: NVM it doesn’t wanna talk, BRUTE FORCE
Device has full vendor control API
- It returns structured 8-byte packets
- Some commands return real data
- Bulk endpoint still idle
- No streaming until correct sequence
Pinpointing the Register Shift
I’ve confirmed a critical register, specifically
Reg 0x02
, shifted from
0x04
to
0x00
.
This occurred after initialization, but before the extensive dot printing began. The transition appeared stable, not a transient flip. This implies it’s settled at 0x00.
Interpreting the Status Change
I’m now leaning towards
0x02
representing a Status register, with 0x04 indicating an initialization or busy state, and 0x00 representing a ready/idle state. The timing aligns; 0x04 was seen in the initial baseline read, then transitioned and remained at
0x00.
This shift strongly implies a state change. I have confirmed that the device uses Control Transfers for register access, and registers 0x00 - 0x0F seem to be configuration or status-related.
Testing Write Operations
I’ve moved on to write operations. I observed that reads from registers 0x0C through 0x0F all failed, while reads of register 0x10
did not. This has led me to believe that 0x0C might be write-only. Let’s experiment further and try to write various values (1, 2, 0x0C, 0xFF) to register 0x01(likely the Mode Register) using the wValue field of the control transfer. Afterwards, I’ll read back
0x01 to check for changes. Furthermore, I’ll write 0x01 to register 0x0C to test my hypothesis.
I’ll be monitoring 0x02 (Status) and Bulk IN transfers throughout. My new plan centers around implementing and using userspace_fuzz_write.c.
Polling identified Register 0x02 as Status (changed 0x04 -> 0x00). Scanner showed valid registers. Now attempting to WRITE specific values (1, 2, 0xFF) to Register 0x01 (Control?) and 0x0C (Command?) to trigger state changes or image capture.
Discovering Trigger Mechanism
OH WAIT, we have a breakthroughhhhhhh~
Writing to register 0x57 caused a status register change, indicating a key trigger.
So now we be focusing on the registers that might control finger presence and initiating the capture process. I’m going to start probing candidate registers with touch/release events to see how their values change, and if this triggers an active status change.
I’ll test 0x63, 0x6A, 0x65, 0x68, and 0x3C first for finger presence, and 0x57 for initiating capture, and I’ll keep an eye on bulk IN transfers.
The 0x57 command, and the sensor state registers are now clearly highlighted. So we have two primary modes: monitor for polling state registers (0x63, 0x6A, 0x65, 0x68, 0x3C, 0x02) and printing changes, and trigger to send a control OUT and then poll Bulk IN, with 0x57 as a focus.
State Monitor Running
So now I’m gonna be monitoring registers 0x02, 0x57, 0x63, 0x6A, 0x65, 0x68, 0x3C, 0x00, 0x01.
Let’s touch the sensor for a few seconds repeatedly and observe some value changes.
After that, I will try to FORCE a capture using the new
trigger 57
command.
Monitor Mode
When I ran: (see github repo)
sudo ./state_scan monitor
And touched the sensor…
Nothing changed.
No register changed when touching.
That means:
The sensor is NOT in finger-detection mode.
It is in idle / low-power / uninitialized state.
Okay let’s do a few more trigger attempts! Till now we have tried:
0x01 0x02 0x10 0x3C 0x63 0x6A
All:
- ACKed
- Did NOT change status register 0x02
- Did NOT cause bulk streaming
Hmmm… we are missing:
The initialization / mode-enable sequence.
This device is almost certainly:
- In firmware standby
- Not sensor-active
- Waiting for specific handshake
fighting firmware state machine.
From the Windows driver:
fw9391_init_chip fw9391_config_device_mode fw9391_set_img_scan_mode fw9391_query_event_status fw9391_fifo_read fw9366_sram_write_bulk fw9366_sram_read_bulk_withecc fw9391_config_spi_mode
This tells us something huge:
- The FT9201 device contains an internal chip called FW9391.
- The Windows driver is doing low-level chip initialization.
- It is NOT just sending start capture.
- It is configuring SRAM, SPI mode, image scan mode, AFE, interrupts.
This is NOT a simple:
Send START_CAPTURE Read IMAGE
Device. This is a:
Reset chip Init SRAM Configure SPI Configure AFE Set scan mode Enable interrupt Wait for event Read FIFO
Device. Yes that’s not a coherent sentence.
I was trying to skip 10 steps. Ahhhhhh now it all makes senseeeee.
Why Control Endpoint Did Nothing
Because the Windows driver is likely:
- Using bulk transfers for chip register access
- Writing structured command packets
- Doing SRAM operations
- Using internal protocol
The control poking was only touching a thin vendor shim.
The real API is probably:
Bulk OUT 16-byte framed commands Bulk IN structured responses
Look at This String
fw9391_config_device_mode
(0=idle
1=wait touch
2=wait leave
3=finger dect
4=wait image 5
=scan image
6=gesture mode)
That is the firmware state machine.
This confirms:
The chip has internal modes.
And the driver explicitly configures them.
I never configured device mode. :( SAD BUT HAPPY :)
So sensor never armed. (or was armed?)
My previous assumption:
Device is waiting for capture command.
Wrong.
Correct model:
Device needs full chip initialization and mode configuration.
Soooo phew… this is tiring…, till now I successfully:
- Verified USB layer
- Mapped vendor control API
- Identified firmware chip family
- Discovered internal mode state machine
- Determined missing initialization layer
The chip is FW9391
FT9201 is likely a bridge/packaging name.
The real sensor silicon is FW9391.
THE GRAND CONCLUSION IS»»»>
There is an internal mode state machine
From the string:
config mode = %d (0=idle 1=wait touch 2=wait leave 3=finger detect 4=wait image 5=scan image 6=gesture mode)
That’s ahhhh niceeeee!! This is the internal firmware state enum. I’m currently stuck in mode 0 (idle).
TL;DR
The FocalTech FT9201 fingerprint scanner is a USB vendor device that:
• Boots as a firmware loader (PID 93a9)
• Loads runtime firmware (PID a658)
• Performs a proprietary vendor handshake protocol
• Runs a continuous scan pipeline
• Streams fingerprint images via BULK IN endpoint
The protocol is:
- Not Goodix
- Not TLS
- Not encrypted
- Pure vendor USB control + bulk transfers
The entire driver is basically replaying the Windows USB trace state machine.
1. Hardware Identity
From USB descriptors:
Vendor ID : 2808 (FocalTech / Focal Systems)
Product ID: 93a9 (Bootloader mode)
Runtime PID: a658 (after firmware upload)
USB interface:
| Endpoint | Direction | Type | MaxPacket |
|---|---|---|---|
| 0x02 | OUT | Bulk | 16 |
| 0x83 | IN | Bulk | 32 |
Interface class = 0xFF vendor-specific
Everything is proprietary. (I hope my Open Source license protects me .)
2. High-level architecture
The sensor is actually 3 devices in one:
USB → Realtek bridge → FocalTech MCU → Fingerprint sensor
We discovered this from command patterns.
The protocol has 6 layers:
Layer 0 — USB enumeration (ignore)
Layer 1 — Bootloader / firmware upload
Layer 2 — Realtek/FocalTech bootstrap
Layer 3 — Runtime mode switch
Layer 4 — Vendor session handshake
Layer 5 — Sensor pipeline init
Layer 6 — Scan loop (repeating forever)
Our earlier bugs came from skipping Layers 2 & 3.
3. Windows USB trace analysis
We extracted every vendor command from the Windows Hello capture.
This was the master timeline.
First meaningful packet
0xc0 58 read status
This marks start of vendor protocol.
# 4. Layer 1 — Firmware bootloader stage
Bootloader commands:
| Direction | Request | Meaning |
|---|---|---|
| IN | 0x58 | status read |
| OUT | 0x34 | MCU init |
Sequence:
C0 58
40 34 0070
40 34 0070
C0 58
C0 58
C0 58
This boots the MCU.
After this the device becomes responsive.
5. Layer 2 : Focaltech bootstrap
This was the missing piece that caused the timeouts.
Windows sends this 5 times:
OUT 0x64 wValue=0x9001
IN 0x60 read 4 bytes
Repeated 5 times.
Meaning
This is a bridge authentication / warm-up handshake.
It is required before any session commands work.
If skipped → every command times out.
6. Layer 3: Runtime mode switch
Next Windows sends:
40 64 0603
40 34 0002
40 35 0004
40 03 0001
C0 30 read
This transitions from:
MCU boot mode → streaming runtime mode
Think of this as:
bootloader → driver mode
7. Layer 4: Vendor session handshake
Now the device is ready to start a capture session.
Sequence:
40 57
repeat 3x:
40 69
C0 68
We call this:
Session open handshake
This creates a live communication channel.
8. Layer 5: Sensor pipeline initialization
Now the sensor hardware itself is configured.
Commands:
40 34 0070
40 34 0070
40 3B 0000 34
40 3B 000E 35
40 3B 0001 1
40 3B 000F 65
40 3B 00BB 48
40 3B 0001 31
40 3B 0001 30
This is calibration and gain configuration.
After this the sensor becomes ready.
# 9. Layer 6: Finger detection loop
Windows then polls forever:
C0 67 (repeated every 80ms)
This is:
finger present? yes/no
Once finger detected -> capture cycle begins.
10. Capture cycle
This block repeats forever in the trace:
C0 58 read status
C0 58 read status
40 52 00ff
40 52 0003
40 53 1402 ← scan trigger
C0 58 read status
40 3B 0001 31
40 3B 0001 30
C0 58 read status
This triggers a frame capture.
11. Image transfer
Immediately after trigger:
BULK IN endpoint starts streaming packets
Large payloads appear only when finger present.
These packets are the fingerprint image.
Sooo, why did earlier attempts fail?
Hit errors because:
| Error | Real cause |
|---|---|
| ctrl_transfer timeout | skipped bootstrap |
| pipe error | session not opened |
| scan trigger rejected | pipeline not initialized |
We were jumping into the middle of the state machine.
Windows always runs full chain.
And finally we have the complete device state machine!
POWER ON
↓
BOOTLOADER
↓
MCU INIT (0x34)
↓
BOOTSTRAP (0x64/0x60 x5)
↓
RUNTIME MODE SWITCH
↓
SESSION HANDSHAKE (0x57/0x69/0x68)
↓
PIPELINE INIT (0x3B ladder)
↓
FINGER POLL LOOP (0x67)
↓
SCAN TRIGGER (0x52/0x53)
↓
BULK IMAGE STREAM
↓
Repeat forever
This is the FT9201 driver blueprint.
Important Protocol Patterns discovered
Note: everything seems to be vendor commands.
Control request types
| bmRequestType | Meaning |
|---|---|
| 0x40 | Vendor OUT |
| 0xC0 | Vendor IN |
Important opcodes discovered
| Opcode | Meaning |
|---|---|
| 0x58 | status read |
| 0x60 | bootstrap response |
| 0x64 | bootstrap command |
| 0x57 | session open |
| 0x69 | session step |
| 0x68 | session response |
| 0x3B | sensor config |
| 0x67 | finger detect |
| 0x52 | prepare scan |
| 0x53 | start scan |
Next steps?
We still don’t know:
• exact image format
• image resolution
• image bit depth
But we now have the complete control protocol. Yayyyyyyyy
Next post shall cover more about writing the driver code finally!
Code appears: https://github.com/euphoricair7/fpdriver