UPDATE_TEMPLATE: the command stock learns with, and the field that was killing it
Stock rewrites the stored template on every successful press. Its post-match loop is QUERY_FINGER_STATUS, CAPTURE_IMAGE, 0x1015 UPDATE_TEMPLATE while the finger stays down, with no REPORT_EVENT in it -- so the matcher does not re-run and the verdict cannot change. Forty-six of those against eighty-six captures in one reference session, and the stored body measurably grows: 333278 bytes at enrolment, 360822 at the next session's load, 371734 after one authentication session. This daemon sent none of them. The command shares REPORT_EVENT's event context. The stock wrapper memsets 732 bytes and writes six fields: a zero byte at +0x2a4, the scan-slot count, a zero word, the count of frames folded so far in this press, a flags word of 0x00080000 with bit 6 set on the frame whose event was FingerTouched, and a zero at +0x2d8. Declared length 0x2dc. +0x2d8 is the one that matters, and it matters by staying zero. The dispatcher stub reads it after the handler returns and only if it is non-zero does it read +0x2dc and make that the response length. Every earlier attempt in this project set both fields and varied the declared length across 0x2e0, 0x400 and 0x1000; all of them answered -90, the trustlet gone, and the conclusion recorded was "do not retry until a template is loaded". A loaded template was necessary but not sufficient. The stock HAL sets neither field. Measured on the device with two templates loaded and no finger: both branches answer rc=0 and ENUMERATE still reports 2, so the app did not fault. Those fields are little endian, assembled low-address-first by the handler. An earlier reading called them big endian, off the bfi order, and was wrong. Bit 6 selects which algorithm entry runs: clear takes libfp_template_x_update, set takes the other, which also reads the scan-slot count.
This commit is contained in:
parent
a44e0963ce
commit
e03ce4ae5a
2 changed files with 124 additions and 0 deletions
|
|
@ -34,6 +34,7 @@ export namespace fingerprintd::ta {
|
||||||
CaptureImage = 0x1013,
|
CaptureImage = 0x1013,
|
||||||
QueryEventStatus = 0x101d,
|
QueryEventStatus = 0x101d,
|
||||||
SaveData = 0x1014,
|
SaveData = 0x1014,
|
||||||
|
UpdateTemplate = 0x1015,
|
||||||
ReportEvent = 0x1018,
|
ReportEvent = 0x1018,
|
||||||
WorkMode = 0x1020,
|
WorkMode = 0x1020,
|
||||||
|
|
||||||
|
|
@ -207,6 +208,79 @@ export namespace fingerprintd::ta {
|
||||||
static_assert((SaveMaskTemplate & (1u << 30)) != 0);
|
static_assert((SaveMaskTemplate & (1u << 30)) != 0);
|
||||||
static_assert((SaveMaskCalibration & (1u << 30)) == 0);
|
static_assert((SaveMaskCalibration & (1u << 30)) == 0);
|
||||||
|
|
||||||
|
// ---- UPDATE_TEMPLATE --------------------------------------------------
|
||||||
|
//
|
||||||
|
// TEMPLATE LEARNING. Stock folds the frames of a successful press back
|
||||||
|
// into the stored template and the template GROWS as a result: on the
|
||||||
|
// reference device ff_template_0_0.bin went 333278 bytes at enrolment ->
|
||||||
|
// 360822 at the next session's load -> 371734 after one authentication
|
||||||
|
// session. Over the same session the HAL issued 46 of these against 86
|
||||||
|
// captures. This daemon issued none, so every rate this project has ever
|
||||||
|
// measured was against a day-zero template that no stock user lives with.
|
||||||
|
//
|
||||||
|
// The command shares REPORT_EVENT's ff_trustlet_event_context_t. The stock
|
||||||
|
// wrapper (fingerprint.default.so 0xb8d84, "checking the template...")
|
||||||
|
// memsets 732 bytes and writes exactly six fields:
|
||||||
|
//
|
||||||
|
// +0x2a4 (676) u8 0
|
||||||
|
// +0x2c8 (712) u32 scan-slot count (as REPORT_EVENT)
|
||||||
|
// +0x2cc (716) u32 0
|
||||||
|
// +0x2d0 (720) u32 slot index = frames folded so far in this press
|
||||||
|
// +0x2d4 (724) u32 0x00080000, |0x40 when the frame's event was 5
|
||||||
|
// +0x2d8 (728) u32 0
|
||||||
|
//
|
||||||
|
// and calls it with a declared length of 0x2dc (0xbefec).
|
||||||
|
//
|
||||||
|
// +0x2d8 IS THE FIELD THAT MATTERS, and it matters by staying zero. The
|
||||||
|
// dispatcher stub reads it AFTER the handler returns (0x9ee0-0x9f00) and
|
||||||
|
// only if it is non-zero does it read +0x2dc and compute the response
|
||||||
|
// length as that value + 0x2dc. Every previous attempt in this project set
|
||||||
|
// both fields and varied the declared length (0x2e0 / 0x400 / 0x1000);
|
||||||
|
// all of them answered -90, i.e. the trustlet was gone. The stock HAL
|
||||||
|
// never sets either one.
|
||||||
|
//
|
||||||
|
// Endianness: the handler assembles these bytewise low-address-first
|
||||||
|
// (0x1300c-0x13028), so they are LITTLE endian. An earlier ledger entry
|
||||||
|
// called them big endian; it was read off the bfi order and was wrong.
|
||||||
|
inline constexpr std::size_t UpdateTemplatePayloadSize = 0x2dc; // 732
|
||||||
|
inline constexpr std::size_t UpdZeroByteOff = 0x2a4;
|
||||||
|
inline constexpr std::size_t UpdScanSlotsOff = EvScanSlotsOff; // 712
|
||||||
|
inline constexpr std::size_t UpdZeroAOff = EvZeroAOff; // 716
|
||||||
|
inline constexpr std::size_t UpdSlotIndexOff = EvSlotIndexOff; // 720
|
||||||
|
inline constexpr std::size_t UpdFlagsOff = EvFlagsOff; // 724
|
||||||
|
inline constexpr std::size_t UpdRespLenOff = EvZeroBOff; // 728
|
||||||
|
|
||||||
|
// The flags word. Note it is NOT the event context's 0x08080000: the
|
||||||
|
// update wrapper builds its own value from scratch.
|
||||||
|
inline constexpr std::uint32_t UpdFlagsBase = 0x00080000;
|
||||||
|
// Bit 6 selects which of the algorithm's two update entries runs: clear
|
||||||
|
// takes libfp_template_x_update (0x20bf4), set takes 0x212e0, which also
|
||||||
|
// reads the scan-slot count. Stock sets it on the frame whose event was
|
||||||
|
// FingerTouched, i.e. the first frame of a press.
|
||||||
|
inline constexpr std::uint32_t UpdFlagsTouchFrame = 0x40;
|
||||||
|
|
||||||
|
inline void BuildUpdateTemplate(std::span<std::byte> out, std::uint32_t slotIndex,
|
||||||
|
bool touchFrame,
|
||||||
|
std::uint32_t scanSlots = EvDefaultScanSlots) {
|
||||||
|
std::ranges::fill(out.first(UpdateTemplatePayloadSize), std::byte{0});
|
||||||
|
out[UpdZeroByteOff] = std::byte{0};
|
||||||
|
detail::StoreU32(out, UpdScanSlotsOff, scanSlots);
|
||||||
|
detail::StoreU32(out, UpdZeroAOff, 0);
|
||||||
|
detail::StoreU32(out, UpdSlotIndexOff, slotIndex);
|
||||||
|
detail::StoreU32(out, UpdFlagsOff,
|
||||||
|
UpdFlagsBase | (touchFrame ? UpdFlagsTouchFrame : 0u));
|
||||||
|
// Left zero deliberately. See above: a non-zero value here sends the
|
||||||
|
// stub off to compute a response length from +0x2dc.
|
||||||
|
detail::StoreU32(out, UpdRespLenOff, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static_assert(UpdRespLenOff + 4 == UpdateTemplatePayloadSize,
|
||||||
|
"the response-length trigger is the last word of the payload");
|
||||||
|
static_assert((UpdFlagsBase & UpdFlagsTouchFrame) == 0,
|
||||||
|
"the touch bit must not already be in the base value");
|
||||||
|
static_assert(UpdFlagsTouchFrame == (1u << 6),
|
||||||
|
"the handler tests bit 6 of the LOW byte at +0x2d4");
|
||||||
|
|
||||||
// ---- ENROLL / AUTHENTICATE payloads -----------------------------------
|
// ---- ENROLL / AUTHENTICATE payloads -----------------------------------
|
||||||
|
|
||||||
// ENROLL takes a 69-byte hw_auth_token, a u32 at +69 and a u8 flag at +73
|
// ENROLL takes a 69-byte hw_auth_token, a u32 at +69 and a u8 flag at +73
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,56 @@ int main() {
|
||||||
Check((SaveMaskCalibration & (1u << 30)) == 0, "calibration save clears bit 30");
|
Check((SaveMaskCalibration & (1u << 30)) == 0, "calibration save clears bit 30");
|
||||||
Check(SaveMaskTemplate != SaveMaskCalibration, "the two masks differ");
|
Check(SaveMaskTemplate != SaveMaskCalibration, "the two masks differ");
|
||||||
|
|
||||||
|
// ---- UPDATE_TEMPLATE: template learning
|
||||||
|
{
|
||||||
|
std::vector<std::byte> up(UpdateTemplatePayloadSize);
|
||||||
|
BuildUpdateTemplate(up, /*slotIndex*/ 0, /*touchFrame*/ true);
|
||||||
|
|
||||||
|
// The declared length stock sends. 0x2e0 was tried in this project and
|
||||||
|
// answered -90; the length is not a free parameter.
|
||||||
|
Check(UpdateTemplatePayloadSize == 0x2dc, "declared length is 732, as stock sends");
|
||||||
|
|
||||||
|
Check(Get32(up, UpdScanSlotsOff) == 1, "scan slots default to 1, as REPORT_EVENT");
|
||||||
|
Check(Get32(up, UpdZeroAOff) == 0, "+716 is zero");
|
||||||
|
Check(Get32(up, UpdSlotIndexOff) == 0, "the first folded frame is slot 0");
|
||||||
|
Check(Get32(up, UpdFlagsOff) == 0x00080040, "a touch frame sets bit 6 over the base");
|
||||||
|
|
||||||
|
// THE invariant. The dispatcher stub reads this word after the handler
|
||||||
|
// returns and, if it is non-zero, computes the response length from
|
||||||
|
// +0x2dc. Every attempt in this project that set it killed the app.
|
||||||
|
Check(Get32(up, UpdRespLenOff) == 0,
|
||||||
|
"+728 MUST be zero or the stub computes a response length");
|
||||||
|
|
||||||
|
BuildUpdateTemplate(up, /*slotIndex*/ 3, /*touchFrame*/ false);
|
||||||
|
Check(Get32(up, UpdSlotIndexOff) == 3, "the slot index counts folded frames");
|
||||||
|
Check(Get32(up, UpdFlagsOff) == 0x00080000, "a held frame leaves bit 6 clear");
|
||||||
|
Check(Get32(up, UpdRespLenOff) == 0, "+728 stays zero on every frame");
|
||||||
|
|
||||||
|
// The flags word is NOT the event context's, and confusing the two is
|
||||||
|
// an easy mistake because the payloads are otherwise the same struct.
|
||||||
|
Check(UpdFlagsBase != EvDefaultFlags,
|
||||||
|
"the update flags are 0x00080000, not the event context's 0x08080000");
|
||||||
|
|
||||||
|
// The fields it shares with REPORT_EVENT really are at the same
|
||||||
|
// offsets; that is why one struct serves both commands.
|
||||||
|
Check(UpdScanSlotsOff == EvScanSlotsOff && UpdSlotIndexOff == EvSlotIndexOff
|
||||||
|
&& UpdFlagsOff == EvFlagsOff,
|
||||||
|
"the update payload reuses the event context's field offsets");
|
||||||
|
|
||||||
|
// The event id is deliberately NOT written: stock memsets and never
|
||||||
|
// touches +4, and this command must not re-run the matcher.
|
||||||
|
Check(Get32(up, EvEventOff) == 0, "no event id -- the matcher must not re-run");
|
||||||
|
|
||||||
|
// Every byte outside the written fields stays zero: the whole 732-byte
|
||||||
|
// payload carries three non-zero bytes here -- the scan-slot count,
|
||||||
|
// the slot index, and the one set byte of 0x00080000. Anything else
|
||||||
|
// non-zero means a field was written that stock does not write.
|
||||||
|
std::size_t nonZero = 0;
|
||||||
|
for (std::size_t i = 0; i < UpdateTemplatePayloadSize; i++)
|
||||||
|
if (up[i] != std::byte{0}) nonZero++;
|
||||||
|
Check(nonZero == 3, "only scan slots, slot index and the flags byte are set");
|
||||||
|
}
|
||||||
|
|
||||||
// ---- AUTHENTICATE payload
|
// ---- AUTHENTICATE payload
|
||||||
{
|
{
|
||||||
std::vector<std::byte> au(AuthPayloadSize);
|
std::vector<std::byte> au(AuthPayloadSize);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue