A couple of improvements

- make TLS session persistent:
  once open, you can save TLS state with tls.save() and
  restore it next time by calling tls.load() instad of tls.open().
  State is saved in tls.dict file.
- a small standalone script added which can be used to keep an open
  file descriptor to the device. Without it tls.load() wont work.
- identify() function added
- added methods to associate blobs with fingers. Windows seems to be
  using these blobs to save certificates for the users to establish
  trust with potentially removable devices (it is sort of documented in
  winbio specs)
This commit is contained in:
Viktor Dragomiretskyy 2019-05-21 22:30:20 +12:00
parent a7e203f958
commit ef6ce24a26
6 changed files with 282 additions and 14 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
__pycache__
tls.dict

57
db97.py
View file

@ -23,6 +23,12 @@ class User():
def __repr__(self):
return '<User: dbid=%04x identity=%s fingers=%s>' % (self.dbid, repr(self.identity), repr(self.fingers))
def subtype_to_string(s):
if s < 0xf5 or s > 0xfe:
return 'Unknown'
return 'WINBIO_FINGER_UNSPECIFIED_POS_%02d' % (s - 0xf5 + 1)
def parse_user_storage(rsp):
assert_status(rsp[:2])
rsp=rsp[2:]
@ -136,6 +142,24 @@ e3d473d5f1897e05db677c7ff6e260048255fdbe35fae42c791ea57ac0f2334e6ab5c650f59f3b27
eb1e84deb687d2dc3d5cf4b03dbe35d165e
''')
class DbRecord():
def __init__(self):
self.dbid = 0
self.type = 0
self.storage = 0
self.value = None
self.children = None
def __repr__(self):
return '<DbRecord: dbid=%d type=%d storage=%d value=%s children=%s>' % (
self.dbid,
self.type,
self.storage,
repr(self.value),
repr(self.children)
)
class Db():
def __init__(self, tls):
self.tls = tls
@ -163,9 +187,34 @@ class Db():
else:
return parse_user(rsp)
def get_record_value(self, dbid):
rsp = self.tls.app(pack('<BH', 0x49, dbid))
assert_status(rsp)
rec = DbRecord()
rec.dbid, rec.type, rec.storage, sz = unpack('<xxHHHHxx', rsp[:12])
rec.value = rsp[12:12+sz]
return rec
def get_record_children(self, dbid):
rsp = self.tls.app(pack('<BH', 0x46, dbid))
assert_status(rsp)
rec = DbRecord()
rec.dbid, rec.type, rec.storage, sz, cnt = unpack('<xxHHHHHxx', rsp[:14])
rsp = rsp[14:]
rec.children=[]
for i in range(0, cnt):
dbid, typ = unpack('<HH', rsp[i:i+4])
rec.children += [{ 'dbid': dbid, 'type': typ }]
return rec
def del_record(self, dbid):
assert_status(self.tls.app(pack('<BH', 0x48, dbid)))
def new_record(self, parent, typ, storage, data):
assert_status(self.tls.app(b'\x45'))
assert_status(self.tls.app(wtf_hardcoded))
@ -189,6 +238,12 @@ class Db():
rec = self.new_record(userid, 0xb, stg.dbid, template)
return rec
def new_data(self, parent, data):
stg = self.get_user_storage(name='StgWindsor')
data = pack('<HH', 1, len(data)) + data
rec = self.new_record(parent, 0x8, stg.dbid, data)
return rec
def flush_changes(self):
assert_status(self.tls.app(b'\x1a'))
@ -198,5 +253,5 @@ class Db():
for u in usrs:
print('%2d: User %s with %d fingers:' % (u.dbid, repr(u.identity), len(u.fingers)))
for f in u.fingers:
print(' %2d: %02x (WINBIO_FINGER_UNSPECIFIED_POS_%02d)' % (f['dbid'], f['subtype'], f['subtype'] - 0xf5 + 1))
print(' %2d: %02x (%s)' % (f['dbid'], f['subtype'], subtype_to_string(f['subtype'])))

40
holdthedoor.py Normal file
View file

@ -0,0 +1,40 @@
# There seems to be either a bug or a feature which renders the scanner unusable if
# there were too many attemps to establish TLS connection with it. While device
# seems to magically fix itself after a while (8 hours?) it is extremely annoying
# when hacking.
#
# It is probably not that bad if all communication is done by a single service process which
# only initiates TLS connection once during system startup and then simply serves
# requests via DBus, DCOM or whatever. It is not that nice when you have a
# standalone program which you are hacking and restarting all the time.
#
# So, to workaround the problem I'm trying to save and restore the TLS state
# between the prototype invocation. This works fine with one exception. As soon as
# you close the last file descriptor associated with a USB device, the kernel automatically
# resets the device, effectively killing the established TLS state.
#
# This script helps to work around this last problem. It keeps an open descriptor which
# prevents kernel from resetting the device configuration. It does not interfere with
# the main process and does not hold the claim on the inface. It just sits there
# doing nothing until you decide to quit.
#
# The same can be achived by running something like "read 4</dev/bus/usb/001/011" from
# a command line, but in this case you need to figure out what is the current bus/device
# number yourself.
import usb.core
from usb.util import claim_interface, release_interface
from time import sleep
dev = usb.core.find(idVendor=0x138a, idProduct=0x0097)
# make sure we at least opened device descriptor
claim_interface(dev, 0)
sleep(0.2)
# release the iface, but keep the device open
release_interface(dev, 0)
# sit here, until the user press enter
raw_input()

View file

@ -14,10 +14,17 @@ db = Db(tls)
def open97():
usb.open()
usb.send_init()
tls.open()
tls.save()
#usb.trace_enabled = True
#tls.trace_enabled = True
def load97():
#usb.trace_enabled = True
#tls.trace_enabled = True
usb.open()
tls.load()
def glow_start_scan():
cmd=unhexlify('3920bf0200ffff0000019900200000000099990000000000000000000000000020000000000000000000000000ffff000000990020000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')
@ -27,9 +34,40 @@ def glow_end_enroll():
cmd=unhexlify('39f4010000f401000001ff002000000000ffff0000000000000000000000000020000000000000000000000000f401000000ff0020000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')
assert_status(tls.app(cmd))
# FIXME this must be very specific to a particular device:
def start_enroll_prg():
cmd=unhex('''
# FIXME this must be very specific to a particular device and constructed on the fly from multiple hardcoded tables:
identify_prg=unhex('''
02980000002300000020000800002000800000010032007000000000802020050024200000502077362820010030200100082170000c210
000482102004c210000582000005c20000060200000682005006c20012970200121742001887820018084202000942001809c200902a020
0b19b4200000b8203b04bc201400c0200200c4200100c82002003300100000000080cc200000f503d0200000a1013200440000000080dc2
0e803e0206401e420d002e8200001f0200500f8200500fc200000b8203b0000080400140800000808000008080000140830000808000014
0831001c081a0032000c0000000080501101004c1126003400080310071d10071d10071d10071d10071c01065810080101000007c8078c0
6100000204f80007f000003070107010c07032c08fc80095a800afc08fb800b5a095b800afb08fa800b5b095c800afa08f9800b5c095d80
0af908f8800b5d095e800af808f7800b5e095f800af708f6800b5f0960800af608f5800b600961800af508f4800b610962800af408f3800
b620963800af308f2800b630964800af208f1800b640965800af108f0800b650966800af008ef800b660967800aef08ee800b670968800a
ee08ed800b68096c800aed08ec800b6c096d800aec08eb800b6d096e800aeb08ea800b6e096f800aea08e9800b6f0970800ae908e8800b7
00971800ae808e7800b710972800ae708e6800b720973800ae608e5800b730974800ae508e4800b740975800ae408e3800b750976800ae3
08e2800b760977800ae208e1800b770978800ae108e0800b780979800ae008df800b79097a800adf08de800b7a097b800ade08dd800b7b0
97c800add08dc800b7c097d800adc08db800b7d097e800adb08da800b7e097f800ada08d9800b7f0980800ad908d8800b800981800ad808
d7800b810982800ad708d6800b820983800ad608d5800b830984800ad508d4800b840985800ad408d3800b850986800ad308d2800b86098
7800ad208d1800b870988800ad108d0800b880989800ad008cf800b89098a800acf08ce800b8a098b800ace08cd800b8b098c800acd08cc
800b8c098d800acc08cb800b8d098e800acb08ca800b8e098f800aca08c9800b8f0990800ac908c8800b900991800ac808c7800b9109928
00ac708c6800b920993800ac608c5800b930994800ac508c4800b940995800ac408c3800b950996800ac308c2800b960997800ac208c180
0b970998800ac108c0800b980999800ac008bf800b99099a800abf08be800b9a099b800abe08bd800b9b099c800abd08bc800b9c099d800
abc08bb800b9d099e800abb08ba800b9e099f800aba08b9800b9f09a0800ab908b8800ba00801800ab808b7800a010802800ab708b6800a
020803800ab608b5800a030804802003070404020000000000002f000400900000002900040000000000350004001000000017000000260
02800fbb20f00f2220f00300000006001020040010a00018000000a0200000b19000050c360ea010910002e001c00020018002300000090
0090004d01000090017c013c323232640a02013000cc0103000000ff0000001d000003ff00000025000003ff00000022000003101112131
415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b
4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f8081828
38485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f2b23203c2d182e1e30182e1c321d341d341e321c301e1e241e201f
201d1c321a301e1c211e21341f1e202024201f1e20201f212221221d221e23341e1d1e1d20341f1d193b341c1d1e35201e201c20221f341
c1e1e1c221f201d21201e1c1f34242221201f20221f201e241e241d2020221e2420231d221e211e1f1e1e341c321e3220301d2d302f2d2c
2b23223a211c7e7f807f8080808080808080808080808080808080808180818081808180808080808180818080808180818081808180818
08180818081808180808081808180808081807f808081808080818081808180808081808180818081808180818080808180818081808180
81808180818081808180818081808180818081808080808080808080807f807f807f807f7f7e7e
''')
# The following blog has only 1 byte difference from the above
enroll_prg=unhex('''
02980000002300000020000800002000800000010032007000000000802020050024200000502077362820010030200100082170000c210
000482102004c210000582000005c20000060200000682005006c20012970200121742001887820018084202000942001809c200902a020
0b19b4200000b8203b04bc201400c0200200c4200100c82002003300100000000080cc200000f503d0200000a1013200440000000080dc2
@ -60,8 +98,9 @@ c1e1e1c221f201d21201e1c1f34242221201f20221f201e241e241d2020221e2420231d221e211e1
08180818081808180808081808180808081807f808081808080818081808180808081808180818081808180818080808180818081808180
81808180818081808180818081808180818081808080808080808080807f807f807f807f7f7e7e
''')
assert_status(tls.app(cmd))
def start_scan(cmd):
assert_status(tls.app(cmd))
def wait_for_finger():
while True:
@ -84,8 +123,8 @@ def wait_till_finished():
def stop_prg():
return tls.app(unhexlify('5100200000'))
def capture_enroll():
start_enroll_prg()
def capture(prg):
start_scan(prg)
b=usb.wait_int()
if b[0] != 0:
@ -176,7 +215,7 @@ def enroll(identity, subtype):
while True:
glow_start_scan()
err = capture_enroll()
err = capture(enroll_prg)
if err != 0:
print('Error %08x, try again' % err)
continue
@ -206,3 +245,85 @@ def enroll(identity, subtype):
return recid
def parse_dict(x):
rc={}
while len(x) > 0:
(t, l), x = unpack('<HH', x[:4]), x[4:]
rc[t], x = x[:l], x[l:]
return rc
def identify():
glow_start_scan()
err = capture(identify_prg)
if err != 0:
raise Exception('Capture failed: %08x' % err)
try:
# which finger?
stg_id=0 # match against any storage
usr_id=0 # match against any user
cmd=pack('<BBBHHHHH', 0x5e, 2, 0xff, stg_id, usr_id, 1, 0,0)
rsp=tls.app(cmd)
assert_status(rsp)
b = usb.wait_int()
if b[0] != 3:
raise Exception('Identification failed: %s' % hexlify(b).decode())
rsp = tls.app(unhexlify('6000000000'))
assert_status(rsp)
rsp = rsp[2:]
finally:
# finish
assert_status(tls.app(unhexlify('6200000000')))
(l,), rsp = unpack('<H', rsp[:2]), rsp[2:]
if l != len(rsp):
raise Exception('Response size does not match')
rsp=parse_dict(rsp)
#for k in rsp:
# print('%04x: %s (%d)' % (k, hexlify(rsp[k]).decode(), len(rsp[k])))
#0001: 09000000 (4)
#0003: f500 (2)
#0004: 8dee792532d3432d41c872fd4d6d590fbc855ad449cf2753cd919eb9c94675c6 (32)
#0005: 0000000000000000000000000000000000000000000000000000000000000000 (32)
#0008: 0a00 (2)
#0002: 010b0000 (4)
#0006: 00000000000000000000000000000000000000000000000000000000000000000000000000000000 (40)
usrid, subtype, hsh, fingerid = rsp[1], rsp[3], rsp[4], rsp[8]
usrid, = unpack('<L', usrid)
subtype, = unpack('<H', subtype)
fingerid, = unpack('<H', fingerid)
usr = db.get_user(usrid)
finger_record = db.get_record_children(fingerid)
# Device won't let you add more than one data blob
if len(finger_record.children) > 1:
raise Exception('Expected only one child record for finger')
print('Recognised finger %02x (%s) from user %s' % (subtype, subtype_to_string(subtype), repr(usr.identity)))
print('Template hash: %s' % hexlify(hsh).decode())
if len(finger_record.children) > 0:
if finger_record.children[0]['type'] != 8:
raise Exception('Expected data blob as a finger child')
blob_id = finger_record.children[0]['dbid']
blob = db.get_record_value(blob_id).value
tag, sz = unpack('<HH', blob[:4])
val = blob[4:4+sz]
print('Data blob associated with the finger: %04x: %s' % (tag, hexlify(val).decode()))
return rsp

View file

@ -11,6 +11,7 @@ from fastecdsa.point import Point
from fastecdsa.keys import gen_private_key, get_public_key
from fastecdsa.ecdsa import sign
from fastecdsa.encoding.der import DEREncoder
import pickle
# Info about the host computer
@ -128,6 +129,27 @@ class Tls():
self.encryption_key = key_block[0x40:0x40+0x20]
self.decryption_key = key_block[0x60:0x60+0x20]
def save(self):
with open('tls.dict', 'wb') as f:
pickle.dump({
'sign_key': self.sign_key,
'validation_key': self.validation_key,
'encryption_key': self.encryption_key,
'decryption_key': self.decryption_key,
'secure_rx': self.secure_rx,
'secure_tx': self.secure_tx
}, f)
def load(self):
with open('tls.dict', 'rb') as f:
d=pickle.load(f)
self.sign_key = d['sign_key']
self.validation_key = d['validation_key']
self.encryption_key = d['encryption_key']
self.decryption_key = d['decryption_key']
self.secure_rx = d['secure_rx']
self.secure_tx = d['secure_tx']
def decrypt(self, c):
iv, c = c[:0x10], c[0x10:]
aes=AES.new(self.decryption_key, AES.MODE_CBC, iv)

View file

@ -2,6 +2,9 @@
import re
import usb.core
from binascii import *
from util import assert_status
from struct import unpack
from usb.util import claim_interface, release_interface
def unhex(x):
return unhexlify(re.sub('\W', '', x))
@ -17,6 +20,22 @@ e024ca6b9a48332c1a69a5a3fdd24b964cf7e7c55229bb0b48a6e339eb2c42d07ec850a4ee780660
69192ef2e16591df26394791a4ecb994a24f5a7f70f1eb2604e6bfb67a452cb74ead8b0d9808f890ac386750cbac06ee03a852145094053
b2b074b9905de5cdbf2272b67e51f159164778d6d2ef7a1ccb81df9f896ddb38ce11e8140cf6cb9c82
''')
init_hardcoded_clean_slate=unhex('''
06020000012c40c9d271378bc0912ef5dced69bd81b7fc16972c7b46e621af54a00e2cc6baca6eb83ea30222dfc6c925262006ae93412ea
cf482f2034ee7b13297474b7e1e91f279cac2ccb7195443e4dd3328cfd292ade073fcc2eaa8f07b77231130ba997f921b9be7b4fb6cc691
0d2976b3e050913b27dbe73afd6e964260b9435ebab5117e71f7cb68464d4b6f8afc7e1a421f671f5854a1d0c8ab93ed3b88b2bc1a42875
e40b15f0e78496ac40e4a4a7fd39a97534ce1866479e0384f0789bbfc2fea0ce982bf7a9df97d60b237edbe1b26c9791043a96b81e435d6
de5971c758d374905df95b0cddabfbf531749ba191f07a6f5e2722852f137a53513a9ec6ab30c3f09aa6ce21b391e55cf81dcda6422011b
f163317a9a4382546141d45f2274bd660103bd3af705f3ed12e493bc4f834d5d7f162e2c3405cf857b00129789a3353bf7fab7796e267e3
062d55660dbbb857911ac8e871c460dd31c56a86a5631475f0f2ee5e9ce2af0faec0931a640ba2394025f29ffeca3a7e99c15a78ce1f1f7
808cedd7601b9b6382d72ca873257d4f6af70e29e22afea15e36e0282b8f0bfc68ffa3417d212b8bbe11bb73b363a19872e6e947d45de30
fbc493ca083a0a4650615d8628606362081ca6df5d67527971d1776ad76a7a28c932f0317b59cb4a82a14b2bcb7b01fb662be1496d24d91
9140ec80068b21a818daa2fb8e05f63edbd4bd5797c74a28b3e7cf81c9045248584977711341fca3f08ba91ff853b62dc24ce4bba4ed57f
47bd458545d805b6bb14fe0cde01440b60bf7be937f6444a8e2a10ed8fa9ddb8604bb95fe411b97112e78dbf5a4a0f004669c93765a9f38
665cb55f5658895c1c06a7aedf694bfb3afa9b8b1dea5ab85c821ac20b0663b95023642fda36ad78e3e00140b966f404f7e55f0b416ea43
b4c74c39900830abc6906a1004bef1b5b7dbbbeb5ec1b22604ac86429b9f56511b746a7124c449b8c9498f49144abc2d64f6a114f1d7f91
aa41249faeef4d838e280cb5d6fc19cfe86c75f
''')
class Usb():
def __init__(self):
@ -25,15 +44,25 @@ class Usb():
def open(self):
self.dev = usb.core.find(idVendor=0x138a, idProduct=0x0097)
self.dev.default_timeout = 5000
def send_init(self):
self.dev.set_configuration()
# TODO analyse responses
self.cmd(unhexlify('01'))
self.cmd(unhexlify('19'))
self.cmd(unhexlify('4302'))
# TODO is this one always the same?
self.cmd(init_hardcoded)
# TODO analyse responses, detect hardware type
assert_status(self.cmd(unhexlify('01')))
assert_status(self.cmd(unhexlify('19')))
rsp=self.cmd(unhexlify('4302'))
assert_status(self.cmd(init_hardcoded))
(err,), rsp = unpack('<H', rsp[:2]), rsp[2:]
if err != 0:
print('Clean slate')
self.cmd(init_hardcoded_clean_slate)
self.cmd(unhexlify('3e'))
# why twice?
self.cmd(unhexlify('3e'))
def cmd(self, out):