Fingerprint Driver Driving

- 15 mins read

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 (:

(::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:

  1. Extract all IN packets’ data after the capture command.
  2. Concatenate them in order (based on USB transaction sequence).
  3. Analyze the resulting binary data for structure (header, image data).
  4. If it’s raw pixels, determine the width and height .
  5. Prepend a BMP header with the correct dimensions and bit depth to create a viewable image.
  6. 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) or 1.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) or URB_BULK (data transfers).
    • in (device-to-host) or out (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:

  1. 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:

  1. Phase 1: USB communication framework and basic command handling

  2. Phase 2: Image capture and reconstruction logic

  3. Phase 3: Error handling and stability improvements

  4. 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?

  1. 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.
  2. Wrap it in a kernel module skeleton that just:

    • Registers the USB device.
    • Logs connection/disconnection.
    • Passes bulk IN data to /dev/ft9201fingerprint for 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:

EndpointDirectionTypeMaxPacket
0x02OUTBulk16
0x83INBulk32

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:

DirectionRequestMeaning
IN0x58status read
OUT0x34MCU 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:

ErrorReal cause
ctrl_transfer timeoutskipped bootstrap
pipe errorsession not opened
scan trigger rejectedpipeline 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

bmRequestTypeMeaning
0x40Vendor OUT
0xC0Vendor IN

Important opcodes discovered

OpcodeMeaning
0x58status read
0x60bootstrap response
0x64bootstrap command
0x57session open
0x69session step
0x68session response
0x3Bsensor config
0x67finger detect
0x52prepare scan
0x53start 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