Merge pull request #2 from 3v1n0/vfs0090-merge

Vfs0090 merge
This commit is contained in:
uunicorn 2020-06-12 00:32:17 +12:00 committed by GitHub
commit 6ab756ae55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 944 additions and 172 deletions

View file

@ -3,23 +3,56 @@ Validity fingerprint sensor library.
Originally designed to capture some of my findings for 138a:0097, but if you manage to get it working for some other Validity sensor - pull requests are welcome.
## Setting up
Use Wine to extract [official Lenovo device driver](https://support.lenovo.com/es/es/downloads/DS120491) (also [part of the SCCM package](https://support.lenovo.com/ec/th/downloads/DS112113)). The only reason you need to do this is to find `6_07f_lenovo_mis.xpfwext` and copy it to this project location.
You can just install that .exe and your file will be in `$WINEPREFIX/drive_c/DRIVERS/WIN/FPR/WBF_Drivers/6_07f_Lenovo.xpfwext` in case you use the device driver, or in
`$WINEPREFIX/drive_c/DRIVERS/SCCM/tp_t460s_*/*/Security/MT7FP22W/6_07f_Lenovo.xpfwext` if you used the SCCM package.
Otherwise just use [innounp](https://sourceforge.net/projects/innounp/files/latest/download) and extract the exe file (`n1cgn08w.exe` in the device driver case or `tp_t460s_*.exe` in the SSCM one) with a command such as:
WINEPREFIX=$XDG_RUNTIME_DIR/tmpwine wine ./innounp.exe -x -e n1cgn08w.exe 6_07f_Lenovo.xpfwext
This file contains firmware and without it the pairing wont work. You will not need wine after this.
To install Python dependencies run
```
$ pip3 install -r requirements.txt
```
## Factory reset
## Initialization
### Automatic factory reset, pairing and firmware flashing
This repo includes `validity-sensors-initializer.py`, a simple tool that
helps initializing Validity fingerprint readers under linux, loading their
binary firmware and initializing them.
This tool currently only supports these sensors:
- 138a:0090 Validity Sensors, Inc. VFS7500 Touch Fingerprint Sensor
- 138a:0097 Validity Sensors, Inc.
Which are present in various ThinkPad and HP laptops.
These devices communicate with the laptop via an encrypted protocol and they
need to be paired with the host computer in order to work and compute the
TLS keys.
Such initialization is normally done by the Windows driver, however thanks to
the amazing efforts of Viktor Dragomiretskyy (uunicorn), and previously of
Nikita Mikhailov, we have reverse-engineerd the pairing process, and so it's
possible to do it under Linux with only native tools as well.
The procedure is quite simple:
- Device is factory-reset and its flash repartitioned
- A TLS key is negotiated, generated via host hw ID and serial
- Windows driver is downloaded from Lenovo to extract the device firmware
- The device firmware is uploaded to the device
- The device is calibrated
Once the chip is paired with the computer via this tool, it's possible to use
it in libfprint using the driver at
- https://github.com/3v1n0/libfprint/
---
### Getting the firmware
It's possible to just extract [official Lenovo device driver for vfs0097](https://support.lenovo.com/us/en/downloads/DS121407) or [driver for vfs0090](https://support.lenovo.com/us/en/downloads/DS120491) (also [part of the SCCM package](https://support.lenovo.com/ec/th/downloads/DS112113) using [innoextract](https://constexpr.org/innoextract/) (available for all the distros), or `wine`.
The only reason you need to do this is to find `6_07f_lenovo_mis.xpfwext` (for vfs0097) or `6_07f_Lenovo.xpfwext` (for vfs0090) and copy it to this project location.
innoextract n1mgf03w.exe -e -I 6_07f_lenovo_mis.xpfwext # vfs0097
innoextract n1cgn08w.exe -e -I 6_07f_Lenovo.xpfwext # vfs0090
### Factory reset
If your device was previously paired with another OS or computer, you need to do a factory reset.
This will erase all fingers from the internal database and make the device ready for pairing.
```
@ -27,7 +60,7 @@ $ python3 factory-reset.py
$
```
## Pairing
### Pairing
After performing a factory reset you need to pair your device with a host computer.
This must be done only once, before you can enroll/identify/verify fingers.
```

View file

@ -6,8 +6,8 @@ from pydbus.generic import signal
import pkg_resources
from time import sleep
from prototype import *
from proto97.db import subtype_to_string
from proto97.sensor import cancel_capture
from proto9x.db import subtype_to_string
from proto9x.sensor import cancel_capture
import pwd
print("Starting up")

View file

@ -1,6 +1,6 @@
from proto97.usb import usb
from proto97.sensor import factory_reset
from proto9x.usb import usb
from proto9x.sensor import factory_reset
usb.open()
factory_reset()

62
led-dance.py Normal file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env python3
import os
from binascii import unhexlify
from enum import Enum
from proto9x.flash import read_flash
from proto9x.sensor import *
from proto9x.tls import tls as vfs_tls
from proto9x.usb import usb as vfs_usb
from time import sleep
from usb import core as usb_core
class VFS(Enum):
DEV_90 = 0x0090
DEV_97 = 0x0097
if __name__ == "__main__":
if os.geteuid() != 0:
raise Exception('This script needs to be executed as root')
usb_dev = None
for d in VFS:
dev = usb_core.find(idVendor=0x138a, idProduct=d.value)
if dev:
usb_dev = dev
if not usb_dev:
raise Exception('No supported validity device found')
vfs_usb.open(product=usb_dev.idProduct)
vfs_usb.send_init()
try:
with open('/sys/class/dmi/id/product_name', 'r') as node:
product_name = node.read().strip()
with open('/sys/class/dmi/id/product_serial', 'r') as node:
product_serial = node.read().strip()
except:
product_name = 'VirtualBox'
product_serial = '0'
vfs_tls.set_hwkey(product_name=product_name, serial_number=product_serial)
# try to init TLS session from the flash
vfs_tls.parseTlsFlash(read_flash(1, 0, 0x1000))
vfs_tls.open()
for i in range(0, 10):
glow_start_scan()
sleep(0.05)
glow_end_enroll()
sleep(0.05)
led_script = unhexlify(
'39ff100000ff03000001ff002000000000ffff0000ffff0000ff03000001ff00' \
'200000000000000000ffff0000ff03000001ff002000000000ffff0000000000' \
'0000000000000000000000000000000000000000000000000000000000000000' \
'0000000000000000000000000000000000000000000000000000000000')
assert_status(tls.app(led_script))

14
pair.py
View file

@ -1,13 +1,13 @@
from time import sleep
from proto97.usb import usb
from proto97.tls import tls
from proto97.flash import read_flash
from proto97.init_flash import init_flash
from proto97.upload_fwext import upload_fwext
from proto97.calibrate import calibrate
from proto97.init_db import init_db
from proto9x.usb import usb
from proto9x.tls import tls
from proto9x.flash import read_flash
from proto9x.init_flash import init_flash
from proto9x.upload_fwext import upload_fwext
from proto9x.calibrate import calibrate
from proto9x.init_db import init_db
#usb.trace_enabled=True
#tls.trace_enabled=True

View file

@ -1,119 +0,0 @@
import usb.core as ucore
from binascii import *
from .util import assert_status, unhex
from struct import unpack
from usb.util import claim_interface, release_interface
from queue import Queue
from threading import Thread
from usb.core import USBError
init_hardcoded=unhex('''
06020000015cb560afa595d0dcf4fca09ebb69301e2b9e24a5dfb6f2602833427d3bd65222c418ff15b54587ab2854c4fe9aea74fa55567
2036fa3ec73c6912c58b1b49cbdcc7664165482cd70d5bb95d9d469626fdc2e012d74157c5792260968a2a4572b1ccccc26ec2e2ed0ddda
1b0931d3d0566460915d43e0a654a0582706be9113ea88f0c15ba058507efb0dadecb4a4cc64444dddf050b6e8d4ebb34ebba33e8651c75
e5fb28f85c83197df1de460d9e1cb82208753ceff0ef60a823dba75d05548f5b3a5a0e2972232f7403bd6869da90e5371a0ab8ad23972f1
597630f5ff7c8b8272800563477288b5591bbb0341d3975efc1778225767fa35480ff7f8dd633e4034ac32e4af58b86ebc63552cb35b12b
285255deaf3a32bf46cdc5ad3bc1c9ed1bcc112c72143f9aec568e2cacfa89ba0c7bb65590d8b93e6871a33c6c6983c0acd04e737ff55ee
e024ca6b9a48332c1a69a5a3fdd24b964cf7e7c55229bb0b48a6e339eb2c42d07ec850a4ee780660ad6c77ffa302a63bd19426134c4533d
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):
self.trace_enabled = False
self.queue = Queue(maxsize=10)
self.quit = None
def purge_int_queue(self):
try:
while True:
self.queue.get_nowait()
except:
pass
def open(self):
self.dev = ucore.find(idVendor=0x138a, idProduct=0x0097)
self.dev.default_timeout = 15000
self.thread = Thread(target=lambda: self.int_thread())
self.thread.daemon = True
self.thread.start()
def send_init(self):
#self.dev.set_configuration()
# TODO analyse responses, detect hardware type
assert_status(self.cmd(unhexlify('01')))
assert_status(self.cmd(unhexlify('19')))
# 43 -- get partition header(?) (02 -- fwext partition)
# c28c745a in response is a FwextBuildtime = 0x5A748CC2
rsp=self.cmd(unhexlify('4302'))
assert_status(self.cmd(init_hardcoded))
(err,), rsp = unpack('<H', rsp[:2]), rsp[2:]
if err != 0:
# fwext is not loaded
print('Clean slate')
self.cmd(init_hardcoded_clean_slate)
def cmd(self, out):
self.trace('>cmd> %s' % hexlify(out).decode())
self.dev.write(1, out)
resp = self.dev.read(129, 100*1024)
resp = bytes(resp)
self.trace('<cmd< %s' % hexlify(resp).decode())
return resp
def read_82(self):
try:
resp = self.dev.read(130, 1024*1024, timeout=10000)
resp = bytes(resp)
self.trace('<130< %s' % hexlify(resp).decode())
return resp
except Exception as e:
self.trace('<130< Error: %s' % repr(e))
return None
def int_thread(self):
try:
while True:
resp = self.dev.read(131, 1024, timeout=0)
resp = bytes(resp)
self.trace('<int< %s' % hexlify(resp).decode())
self.queue.put(resp)
except USBError as e:
self.trace('<int< Exception on interrupt thread: %s' % repr(e))
if self.quit != None:
self.quit(e)
finally:
self.trace('<int< Interrupt thread is dead')
def wait_int(self):
resp = self.queue.get()
return resp
def trace(self, s):
if self.trace_enabled:
print(s)
usb=Usb()

27
proto9x/blobs.py Normal file
View file

@ -0,0 +1,27 @@
from enum import Enum, auto
class Blobs(Enum):
init_hardcoded = auto()
init_hardcoded_clean_slate = auto()
reset_blob = auto()
db_write_enable = auto()
identify_prg = auto()
enroll_prg = auto()
calibrate_prg = auto()
def __load_blob(blob):
from .usb import usb
if usb.usb_dev().idVendor == 0x138a:
if usb.usb_dev().idProduct == 0x0090:
from . import blobs_90 as blobs
elif usb.usb_dev().idProduct == 0x0097:
from . import blobs_97 as blobs
globals()[blob] = getattr(blobs, blob)
return globals()[blob]
for p in dir(Blobs):
if isinstance(getattr(Blobs, p), Blobs):
globals()[p] = lambda bname=p: __load_blob(bname)

368
proto9x/blobs_90.py Normal file
View file

@ -0,0 +1,368 @@
from .util import unhex
init_hardcoded=unhex('''
06020000013917b3dda91383b5bcac64fa4ad35dce96570a9d2d974b80926a431f9cd46248980a263c6fcef6a82839a90b59ac590848859
afac817b7d53bf51cd3205c1b8f43048be8253c3bd247937c837aca8b18d3cc8ee8c8971ac4f688813cf3d8550d71496985b7ec07ff2dc7
896d330fdab263a0ee433a5c4bc910439d1c6161853feb03f5502209502e7308beb7919473cfe69f422c30502d226a4d0a34d96c8c77956
cf69db8ef6cf927a3b57849d4aa8ad4b44266923e34b82a39c8146ba3cd708c70dfedb50c2de61feb45b1d4f19584297203f5fdc865795f
ec9d6449f3ba9b6f1e4bed698ee151e83d4d8702f76a4006cfa24d9b797888203b2269f8a77d524034ac32e4af58b86ebc63552cb35b12b
285255deaf3a32bf46cdc5ad3bc1c9ed1bcc112c72143f9aec568e2cacfa89ba0c7bb65590d8b93e6871a33c6c6983c0acd04e737ff55ee
e024ca6b9a48332c1a69a5a3fdd24b964cf7e7c55229bb0b48a6e339eb2c42d07ec850a4ee780660ad6c77ffa302a63bd19426134c4533d
6f967441163fb78b73547c68a493b2f800d3cdab827b116762789992aae3c8ab345a49edd312dfd2a27bc501427dc7fa00ac3c5c36551db
b3d5cad8d5bd7cea37e58a31307a6d50e6ae379a53f1366678c0741a3d872b8dcfefa7f63128dc8245
''')
init_hardcoded_clean_slate=unhex('')
reset_blob=unhex('''
0602000001d6123241376f110935a7ae38b614ced9952eaa38e2984842d0552d984b69ab5c1a70175513319970225d9ca25be063a7d70a2
9dff803240c099ba5386910b69c3b7ca8d0b26ec9a5684fe92af9c6d8068ace0954eb8582adc6208aa6c116b1778ff531dd2b6817a10d66
7e0385d18ac7ccb47eb67f1f54b5b94c84d65529c6e9fe9039218ae9cde123f38ffa52a953ed589d0d8bb48680aeea6544b7ceedbea3216
615404921b43859facf8ed4448e585354c568fe8d9561efb3fc9b88af8e6c3c7c02ac9b44adbf780d23730135c1f9840b67b302b6efe9db
19ee1f5918740dc0259b3cbdd4f10e59d1a517f54e932bf01d2b1e5ad5c4f71af23373632e06160f83799a0a1b2479d9bad01892667d136
e27463a0eac485541f24018bd80463cea17ada264cc760b38a0ac87b27539b72e1c4f05d73f6c9c777b2e70c366f1b79d31ae2dd1eebbda
992390c64c988c5e04e3b4ababa8c0fbaa33da18a097b609562f609ecde2443fa9b315667c95fb62abedbb8887dbcd965a1cf351c795ca4
f5cc6e22e844cc813fcceb34741f5c988dd38d31ea3bd3cb373162efd3d5744268c1c9a5b679c97e2356c7b1de146eb675597884fda01e6
6360b807e04c4e557ccde2a29df1030c0da4750c9314c551b6a5061711c6aa1a01564933218e2a9938d8a158a17da154969dcb3aedfb8bc
e12024b908e7e75b9a8f44383a4a7c3a35632054ef1bffa4137e0f4d724c3a06a96fe986a339ca4f27744708feb6523a31aa4e2a19d1567
964d52b568259cdcc602c7ebb468bc5846a9183a56917e3f440e522206e7e0e06a07aed6b680e0bc818217bb4dfb2010ff503be6d3e555e
e1af32edb3f02f6906cdcef6053a851e799913157220c563ddcc47cf7665fede1336f152df1d1c6d145a535e59dc41b7963bb520e66dc12
a21eb5a900f939b7838f274d9ebb7cd0c2951f2b9379b54f20bc50ee6ec2dd09d7af7b9f027261c38f056f3f1b953cf21bda1f5a9caf3b0
7d160f80c186fc5eaa42074d041eaee81518fd2b463112ae6a6c07a185e8c24101ee14dddc4409ce7fcf9a18955bbeb8c7146dcd92ab165
e20484c2b316acb3d9dd2215c5b7f4eb8954202628635a46b463e1b2d35c466eedc3c49951222840959db8eb1112f32fa952d72f2ce2f62
97646ebaf53dc1c0ec484fb703938f4341f21c157fb58dc101e3918053d8abab75c9300b2912f840bacdfb4e27416bfa3a945de6cea14a6
0f09d57e6ec7ee83f5616a9fbf0952017420f857b35fcaafdf7af50446c1e7d2624f9cc4a187af0c6ce6c118ccd4234d991402c0b81dd38
bb523a2ecfcb16896fe16aefe7bab3ded4f746049e1d38e6f8d7130bccbac4b33bf543d9c4929b7a6247d1d4f9d5724e6040f5d4a357041
fa07a8ad998110ca30a3ce6ac9fed580034adb66df8fa8643ad3eccfcff6ccb8df0756ff426aa9dee01d656fe699f091611c5d68688b3ee
28e320d9ee2966a5cf146c13013fc4ce353c203689a38e0437805711141900ddfa13c5425e5c6aa5447c13a35893696ae04587901992071
dedce8155f273581f0ba35b7b23c7b865149945be1bda2233a7b5b4e2f5530170ddd8fee44229a2bbca2622172b36831445d4c9ec9be1dd
b14de7eab8593a6e5389cc260fb453e3062b5862ceba83dd355ab50d859bd47215c844086ef0f8d074412590eb8d1633bc21345dc1aaca9
420fcf354ae43efcdb9a0b3c9422c6634c6e15a01d21d1cc645bb6f3e556de2c17471d4c68aca86ebc700efd31fe9eba3c31d18c33d4b94
8441bc6f6682962018dd0704bdeb78fdfb457c70075a45397668c85ac26e13c890d1c980c2b85602f99538c649fe97a01542f112918b68c
dc0b9e59f814e25e3dbea24c8a36b064ed7f2c924c0c45e712a9ec9339eebbe4278b1ed7fefcd7d60c27ad80a978161e36f718f3b52fd78
37454f04b43438ea78dbe7badf356ce99e4dc3c9749d90de6d630c88e58d8f6f828759dc14d742ea4b6232efd05da4cc200732a4fab8b9b
c8896233432333d344acbdeba7eb64634e8a91919efd81f4b848511c181f3f45ba45841cb235b47745882cdd44baa44ec8f7247c470f412
31b2c663243068cc309aa890ef1b79f8fc438a517b903d4e35a1dbe1ecbaa2df887a1fa18af39c4cfe90104d330fa1b99ef3f1ee8276cad
45074bfb5ec5fd72cf5b9969cbc479375bd2be271d0a8322e50f1b0c3b2c66cc0088a3e4ca6d727679833243386c441e46c25084e37621c
4a3a6774568f65b9d2f506fe698322973ddcb94886159f55a967880f56933b066ada7fdeb861ee431369cd926ee3b088f9c5e852aed064f
a4004fc658efacc3fec6cdc1150bd1b3183ff52cb62d598baf172126afb3d8b3eba65db880f3307078d9f263bf261bec797db705300f635
796775d0e8d2a964f2b51ad769561b2244a48b5f9767f71da4856c329eb5a8a58ccab1993fc0f93f3491991312406c2797e5b08a0ef7172
8e8cc3adcd23d29b7248c328b192489bec81c56be0087cf38653e4148a8065121ff7c6ad4f8aa4bcaa375f566a8c617d59abcd6c3b0aa13
ea4590e5e37f47bc276f0f3cbfbcc0f159e4e411e3b42a5fd2c440bab916117710208ef6f113ea82709d5e0f5cdb68fc376fe707f2600db
2f5bbef6569fbd6cec364621f30445b161d3faef69fa840c87a301604e351b6b25db6a7d20c86c1d3d4d28ebf88850f60d7ac0c1e36e1d6
29f76f07aa65de78bc9b33324ae53312cd15d5cf54175dd0dc9b2b90583d29290a72569a5e3f1d7edec3195da0ddc547567c948b1b8bed5
df3fcdb56f9b0c625aa288fd57815961014298da72d516ce020dc73563c0d34a7463fa869adbe24963d8f12c80d708b8f8510abd66d9047
bd6d14db5815948fe2708ac584a6e0a6a0be66bd315ae1fb7dac0be365910ab0eeec78c9f4b4209415943ff055bce11c621628c5932060e
b516c260d4c3da41b046a2876cc11eb59078d550bc883aa5722139a076123c4dd2debb7da792bcdaa5ace368f8f7d5c95ba0e7397393afe
1d005cb89bba287b6ca2440295bd9c3e83a195ffcd3a853617b1d4c8b9fe1a7f5e4d4817e7e22d0d41255d044b271a653de5c6ab31b17d6
79c5227d1fba14f5de53125424a5fafc7cedbdf560374c7d4e52d04b6fcbb725108ddc8e08e5b4fefe9ef565fb02cfaa3e56de4377c6cb8
36cd0207bfd002522cc850380c39fb66bb92e309c26cd05c97d54ba055ad500f23c2cac8ce1fce29b4c623657a42058196c3fe635ecdb7e
77a01a2114adac63f141169df1f3cdf1c86fb6e34fc419d686e620a427df4d9034970d6618896473b213f1933c533985f45c9197679ca03
a6c6b9b9893492ef8a712b38d317618b6ca000bf0b5134845d6da83c9d93c3785b36ad9155cce63f74fdec88e03b69b6db32265cbf57344
cda57e5f55d26e26576125feddaeac0141cd43dc5e906babc5162ad9c90bf623816ee21de2914c9142f32e28f489858cf931edae30e5b53
a5e6ad8c6d74fcd0f490cd8d01904d07a8df31c6938f792319bf7d03a04b69c58b96b23d4993d10846d436c618401ff97558841489c0b7b
1be5a8d452d4070026d4e240a9ad749b44cf8011b1aef37b789782b0f8e17460d3c43e92555c413489baf2ba1e01ca64eb58f3db61b3921
981bdc7f005858015a987c457a9df8a35cfb70b27777e7043eb0ca06a65473ab3c503955b5a743c89d341c0a32e69047e5d9cdaca901e06
c1b2d11fe1eb275b8d292a992a5bdfbe29d436c03fc0c85f2e5c5da3ba8a18a8b395aef869cc217afd64dd2a748d21535d89a94f64d3704
5f0b28f73315f3d8f420d3082bc3c33a96151812ae683fcde4c9b64e96ce5c463c93c15af02649c7c56ed308c9d386216c88b1eaa87717c
96852e80e6e544d4c3731ec34affe43bd59d1e5a3e286f23ac596c301d1311e98616b6870ae41dc3f4b62c1e8019c7d94bfbcac812a74da
d0e12efdd0842a750c4e65e311c0b7687c180475acca570988f1399f85dec169f4674895f9ba21719b752f4b5134f036af8a6250caf0692
8c5d4c63bab25a9f530f6f6b3b8cc4befaefac68a2b100d64a9fc90004dc2c7ffe32ebaa31ca3714a47d32b23ccd34631943870cca989a1
1e3988ad9a884491fdaeceadd08e6eab8761d6e40b5c6bb5c6be57d879635c7899aff67d3e0ce02fcea2bcffd437434e98480a7388e86be
96c08be01d38157a2021e6b6e7767b253e5e490d19c007d3a55f54d42d5068d574e94c252241dd0ba637e81b35255d26c4011564dc04b8e
824c51c0635c45be6568ca3e4b8fe4b18ef259fdf0598005f153d7c880294445647c293a791ae63fa0bc7188588a4a80acf08a8d25eb376
043a591c6dbd91e3ede155bbba2dad764ac478087e0bf5db54c1e36a751d4d1fbe04ffd5dfa60dadf6d5d4cbe1724a6bcc6376b72c0ee90
1d2444819d4d6d368a7ec539200a66a2a85420a23cc40cdd5b6b7abcec543f0e77a2cac8f9165f17d0bcb8d64c8d17951ae64d3e3cc4054
eb2b56265164ce17f7810f6736835e7ad6653dcb4826121ab3c3482c9c47aecd708a96875efcd744e61fd884549e09c969f78d74be12336
f7c0a3861710dde88783aedb9975ce0ea97df46600a24dbd1580aa33ddf6cfcf845013d2b1d8f271287813c4ef56d358e6d9ce5311beabb
9f978115ece80210c397b9bdddb0aa060c1845a8efb7b4672c8fbeccbb992f8b5a1d8cd4ba9db59f7a513368ef6ead104e3cde8a0df7898
647b89f4f7a7d9a3a8a49e5b3467e9959e63f6ecc18fe8db37e0e3fff24685be43174a62457cac876c1bd7667671562f5989111561e76c0
166bd50b80764861f2e893dd3c183e85f538be504e24467c633ee630aaa1162bb77beda78d8fe812cb503a3c5b9b2ae1d8d08bd8b551a93
d1c11f9e0dadcb23032e9ddb5164a9f7f7f405b13d4c6b4348cd1aab236d2461e62c16c4be9cb7b13f227f94e588fa1b469e3f8813ef488
b1813f99b4eea2a4bc4a01ba6a8d91efc7d49ec61414b415915515344d085369918e28320bf0827001dd044f5d2b362326eedcd1ec30893
fccb0e7ea4d064eab96d4d948424c738af494b95ca6b581127b09c79117b4eb1fb0084084ad954b99660eb0bb1e4dfb8b3916457ecf7b00
ae37b46e1468fe4c27368334ada48647ad511539d9a4e044cf7808bc3d9cc5cd3a6609d3f0b7c24705e6cd9558d2fc8d054102aa13c8bae
4feb6f99854f5d51a556afaa754a4eb2825bd0b188f22dacb1921529353354fbc76e34e504815f86fa3ec7fec2ff8bcac70fed8dd8b4d35
c5d4c40b2bbe8fb0f66a01d8250e248f91ec938fa3436d40e9bc42b5c10aaf00cda166d084210222f03c594b28c6c381431eb562dfdcd06
4a003c230571df8d5817047d79c64a10c71b45cc35462794b4374b4f33566b0dd35aba9142c16d8b449a4845c11ccac5ace613764a2d367
655fa0c3f61851bd78f3c1ec9be3363b55052d997286b93de7299fec7292592510acb12164538959fe0985ded8e9b3e8ad3573091f64f2a
ba3d266e400360d9559592965e2bb090f15a79b0e18e25d5e53136cbf46f8d5c92f963b0345e4b4a953dfe62d87c8d19bf12078893f3ff7
504b7d87e1cb22988e50bcda330944d6e21b31339c54a9f1f62f74d64e42c4ce9ea11cb28370620ddd019d72cf86f23f8d6392feba71040
e6711f6b87d4574b3531230c2cd42811bebd42731daa3fbc22437fcbcd453d5496245d8e1d8d41a44d3388fdc5f44d5a6c2efd50aced0c3
9182b9e13cee5afd257269a752152d2c1ad32276aedd10543d4a43c936a817f97e8504c9829a5ee7191efc6588e0fc51b68bedbc571d026
e88e56df0cf58079ecc9f79e1da3862e197f65129cbb88cd328daa33cbdc84a10e79ec54dfb16d095d6a17661788a5b11c9b33b60180155
a28d58e51c1bab325ec736923a5c5e68a332fccefc15e804260e0e28d2bbca5dee308429b262a1afcff57494706d7b6af55f9d1d297148b
9cb0da0191f71a63990a69d742bcdaccca45e3b870618b87a34ab545c9fe56d8abf0595053eb34daa9d805c94cdbd7664d4b6deefacd61f
305c6d34514d8af7e49c1fee3e61b2d5655fe1da9cd02be10a56090ecb656330f1dcc99c9dacf92db21797f8c683ea72a823f3f4916d1cf
43a5f550e4167a6b2faadc4c76b3996e7c388e9e213356f3f55d11b65451ce82275ba73bec51220107c3aef6c16a59a8bc45c1cd18dad20
1e967e95a0b5a1d5e79a654b8c8e8dd490d7443a3fdc3c5cc296cf161bcf7f5f11ae5e6e283b0aaacfc47e34002ab474fabd907b3b7d3fd
e6e0d70572c72e6120688d4d2d6b527e3f2efbaf43b2c8b98acba51342d1343c2359e7e9977847941bfe4ec45960a73ac7e27f11bdd9d03
14c4607510d9a3988504980728d94672ef5d0ced3b26318aa0d6505d27d6853c1351194b162616497a90a4811158e887f85e43c1447cd7b
15f1cb914ef5f00756933f21781f6d50528f27e65e73063f2c9cbc31cdffd07729276ef30715c07b70203348f16fc79457f8313b41bb4bc
c2297ce9072dd39d9cf572d77b63390df5cdc71a531470b0a03114422091bdbb9287035bc7e8883bc06515fd85d5f6a4ff6c28c0fc174bb
f2a6deb39264e9d4d0a157f7a82160d19ca3d3320e277c67a1450597ee4ef17136908657292fb1535c54e0f33a74c2df98808dc5f0572a2
c872f2abe3c26b61eaa51a1f431f38c1731477e56d0b7294f82450f81d3b4e24ac3b918cdafd657076dc04d0d2478c66177c52f32e63578
ff91cf4b9c3479ff9e50d011325675b0f061f9f39a066709ec2e04799d8c6b7f5c0e6cb0f4736a9c30c2965dd1b6d130c4619c0642bf464
779147524055003d7d106e33b8d4f2878ca1036dc26ba7c043feec62f23e5e614ed216a7688fd491141f2f0ff03901e27188c4c10ca63c9
b16c681811946b709443fec717c0293cf956fe10f1cbc4949e90d6022e1df5187a4a9f33757b532e7afd708cd920c8cd3fb9706976ef925
3099aea257ba89a27d015f2fad5e413fc90b1501d0bf332ae72975acb694657958ac58063b9503940bd0b5da8667d14a6f14778de42fc5b
49aa2f31d63823b458d9d30c9cac2de6437ba07a4dc7f64c58505f8490066b8a308272afdc5f52f9d3847d45adcb4987a57e51e7f87e7b9
7be0864cad5c32fd7d7a523b707d62fe4271b699cca423d88a7b03208615466b37e914d11bf0ac09af27bdafdb8881f594afaca9af804fb
399357f78428a6146f4177029430ae8144efaace1396a67553501159c6b28f519fa2b6c526d0592bd769ebb8ef8d4a9cb06015a3cbbe490
238ed5409c5c6d5c76d050afb81e794edb0cae2b5b32de7936a6dfc55bdf9629c7439ca6e0abc5a72639de5e499884f00d3893216f3b7af
5306ea1e1c6528ffb6d216748c48e66e38195bae12b18e1dfebcdb94a769786c0d5f117692718b2e421cb6cecc31ae303a3cbf0d091aa66
e0868531a206edc80b6b82cbdec468ee5f6995e1781fdfafeebf6e9dc29bedb852d31ab250b6114ea7914db57f2a29005c75d699f96cf67
3c08fc8184afa71a1d1d574fdb4790ba1b903e4be7dab6759419dd5e2676e3674673517666fc2a6dff15ceb63b68898c2c04105a654ccdd
a3004961e2fa04bbd64be6d17e072033137e9139b408e5627afce4086ef3b62ebc10d793ddae623ca685571384b72020c361a3c65737ada
0a99626e1cc4edbf3a2d5ea7b79ee52a29399c83e3337501f11ebe19677884fc547d4856e0c056b309c9c06936b76e59179b6234dd297ad
206a44e367938874c620d304465008cde3b9cd961fe0a2194633b60da9ffa3acdcde56ddd2e9d531c88254c1abfc82eb32d176fd6ee7ef4
88e55ff9146e351d3f88f86458b7d1c015cbeb9a2830c94c139b88dd3722f21ffa0ed5f0fe2f52292d63e7a4e8296aaf6e7c676d50fae02
74633b322b117a25b4ef14de3eba71c8f71c3f1d7b2b6e0a72872168d9fb4dc7736f07f74f061e9793dc11b928fbcf0640f507f258415e2
704e590d8d8a7a3c301509a9480386e9c0dbeda03aa6ee639d321a068f1ff464de5cb8c9e1d8a18544489ebe4a10bf2a38d6b6758da979d
d9d57cb34bf06aa039f8c28ff482f777fdf49bb0e23fd5a13997e74268bd34157b8e9759745446620b67c07a631eded4da64fd52a843252
2a8cbef463cf342c29d824eab6dda3beae4fa865a91bb93bcf19dcbf83c30a463f2722237ec6c3219e4ea1a5c8332cd31a672816c6d6a3a
025a2d4fe2465a70fe31e408174914901f9cd25555ad35ee447bedb46079a10e28d45dc1ddc089501cf6d2897ef5604cfb926b1f91eea87
b3b4b5cf1de1f19de0cac590ee322b901019935650d7401f8594f07d0c5b45ff2a070c2aa99ceb579f9234d40a084bac2d327e39e4696c5
709e8143311f8cbb1b9a9e90542f2a5c7bc31ba55fc75040ccdbb019df60f9f5be3462952c53ed5ae5d2b963e209aeec9152285b57459d9
c30f13f0824d4df7391cd37f4c01bacca23be86522d89a3cea30d530870194c5b9f3e26854fd5c560ed8c1278727de19ecbcc98e70681f7
f502300177bec56c7592dcaea9de7dc84e3a5bfe84518aadee912441f36a21ea72d02ad646b5627a57fa2a936f354a83de8c720bfb77500
75652162815241de1eca2b82b26695a5be54681a352726c05f921192e2617c152ba1b3ea9886d21bdd9fa1af5ca716db253337549497552
fa8a88d90b3aa29cc883bd6b70c3b484d61cb13a48a198d66fde544d2220853023428a472b3950a7e2454a964245e941d3afd17961e7715
a8fcb573daf5aabad00c04f2063762b042cbddbf70e247205800cc3f683744328d36fb099de9a55f160113034f2f07d6b17bdbfca8c900d
8ff46542836fece814f23de9c0eb1ff4d7b2ac19404aeee4ccd730479715eb68eba290f14d4e59bfffef6b32cfdccdb1887e5ac5275edee
510d8f15c4019f8d81b5067eef86d7e0e5a22a5d9c3f9f02a3f127b790734222b35c6b58500d9e71c52af72cc857e81d674acb5a20f8899
37b11fcd1afef357199593d4dc1ed3bd330a9884dc2ec63289aa46bc78c3ecc8bce6b233073f19838d533daecc396e4dedeaad64fe2df5b
18ec9e5d6ffb986c2ec995e396ce4eaea867d1a37a0e37b05d62da3e33f1886fe494407c281a576656d9e877475cdde83724a74139a1a0d
7f46c2bfe50bdf54eefc378fc5989bf3cfb8a50c767e9f387319c63ae3a19baa1f9b2ecdcd1c520cf89812f5afb7b9e1f8c4430d30ef2ad
9ab44ec9040e004c34a98aa5365aa2c47fef4d61ae4460315695db1d9b5c209edb3bb2a19c0d3936d926de49eb38871ab49168876df13e1
2297a0ba4735945cdfbd8e7f9213538f279f1e73962c1d93cf093c641a5e892fea0bee34981795431ec58874ca3324682e294c5919ae542
c9a24c5517653da873a6c808f6e2ee17bf97fb096af3027ba73ff3bbe73b6666bcbf5fb343dedcca608d754253024fc1cbf745be30b9e19
260837e7dfc018735124e321b493ae356ea682ed8b3e543f8ea78efa6fcc670a6f08b7690ef640358563dca8437c8124b54ec5c818c40fd
0e9001ef579c8cee7ded90a0eb57f0be6a77a8bf460131ba700299188d6850e0b6bfe61ac5088cc425fe4b49de058f7b771e97f10851f3e
f4d73f18368f206d66c46cf14d53509eeb2b0e0f7917e0424721729a624f4979f99ad06a6ebc5bfff88df86731395ee8ffdfa2f6f890450
4ffcac2ca833db95abed61d36a611a0f2d8e911fa31e9b3d7c3b93e810298e674e1275c34789cbb4db0ad81453f79806900c7a6cf4d2bc3
b376c3b3a85b87a96d0f391132e67beaf0efc2efe6797c8c9667b0b8a9e366747ab433056e68ceb4423b12c0e9547dd7993bb45794b3e3f
e3ea8263af967a319496755b6dcbaba889ff3a8a63c19de97607fb0a4907f05e6c1b8751d170d5e479e22ee9aca002e3afe564129e20a35
c6de7725d4565e354c00c0dd46a78d3bf64bee3766ab1e9713307816dca44473cf08a41db614dfe24a1f85829ec3b32d8ac299362996d89
63d06213219dcc77057ecceb8b87238900462faa16d24138c24e9f000e40e6c2986d00d4c9e2daa5f88228921d40055c34b12a4c0a95e12
f4261dac024a8057cfa00cc7513f7a6f7f8eec5638f561ff4ef24d7ff8e8ccacd833fd641d7de42cee3d88e92dded6db82c947b20d0e99d
d91a4f464a3e84920a4e79b2f4bc5368c131ad2d1d36a448dd1c35e3a763a9ea77ce1ae501e5c3a3ff654d7b7f3572003aeb1eb3b9c7615
dd1fceab65205278c43a937fe45d8ae44de67bfd69f406b55eb92f1b22bdaf71be9c40b1ce2e490ee7a3d0efe2eb8db0945c4d9f9879594
cf15b2d2ad0e608237977a3b8109af5995a4d04a782e74acaa512c6f4124758facb7fe82b4f11ed4e1d009d4fd72c104c1f8af43bc6ee8e
72bedbdd806066404656ff6657f6a896141e8e3c64d29462f7d9a5a8547f8e70eecd879ab3ce3a83853f686f3057796a65ddc9528e774e8
da5a58e5b95cab51d1630c11076800c102e3a9aeddbc6393fcbf45336e380b2c243b2654abe2c60f14982174dc451e0d2cca2c2875f28de
5dd33015d30083e4e757339ee4e8a8beea16706e0fa40505794cb6add99d4c47d3fd474c6dc6b7c12be07635f88520a396d68c1ed46cf58
d12c5cbd71e8fd611b1814c9b6570f862376e10801b09f2a96f9fe2fb30704573552b6daf5fe225956eebf6670dfec598881774883fb71f
1079caa7610ab316b49045282bcb95d674cb97f755db08adfb3dd48c759c1f3b846645f6e598d40d781cf90f9f6b4b68c6e4eafb04ca121
0c3e32c9bdbbc5cc00e5cb73f6f7a8989b9f77173cf9c48e563d25c08e6421902bbf6eda178eba038a58b6314b9928e05b415274065a294
e797d69ae030ccaf3527e5be9e7793152e34179524191d8fb034f65894c862afb9f5277355588930d06f5e1ecdd161244c5620c7927e558
7fe7859a3c9f553a60c61dbdbb50700e94fac299688c825698738fc9057a03427491ab3cf23060d1d9e651848cf3346673ba64e252013a5
e91fa1f208446d65120e98586e07a3f6c6b70bc10c4fdc8aaba169eb6e467d2cc12c3d5fe16b2730d12da8b30dd168f3f9174e1dc940890
3f9b8d7dfa02cb5f5ddb014f0e4604c4920057261a4d38b62b9c0b365c6b067d10e6b71bccae65284802f3a402a0e4106bab6a2f1d3908c
a87e8001150949e86c18d7169d0e2f96e46eca56059dfa543f121999b734561c4813e6893bcf180a0ed97dcf51c254170d991a8da792a25
e2bb40f6e5819350cd5f19ba46634b8c7e734b6631a90f0bf7be8ac658a6ab74119c24ccc808a2ae0908722bc71a51495b232698014d9cc
86658b7d01b5f2d3dbc402719a750c6737d0ab4d38890f361236c17d8d42a993922063a19c59a199c92310d472743f2317921246a009d6b
8b33856583d98bcf028fed0c906e054a0fd83d3724d535f1fc7c06e03c504aa1d5254ea9d100658ba42771aa41717a7666c11b7cfcc21db
52b598f9754621372107f06f435d01f624b258f9d987a8e3c4699fc1c8eaefbbb2c6bfa52c24f2e7fa61a8894a598c6eca124978b566131
cb23a1ea29d5a877156726954b4e025e9cdc6615baf34af6192c5ee9f23fc48c8ca9cbf66290e52c93503925b8df6718fc9820c84480bf1
50cd0679c0042b52ad5dd97d3cbf59ccce7ffc168bfd49de2f00034f9c2d16f6c438e6c1d156186ff15950a1b829abcbb6c339f443cd23e
6d0be60e818fa0571c1cfc22d11f60423f54f884e4939d7b742a0af3a14531d9341e4e88c98f800ee5d05c9ff29daae165db0aa48e4ad79
25e25ee0acf03bf09393c5ccbbd62343656bedb3a68d7826b01bf9deeebeb32e32058853923f235095cbf5fe9099185ef9601b1e59e96cb
e812a8ee818a5ae1135de863f5241fbccd6277ce0261487ac5925c8a6d303666a045665385b3177e4f4945bac6fe4ba1e3bfa407b964b8d
02f5dd122e1806ebd8c5f92a260b635248d6fbd993c03ae92f75c16a14ce240b1742ea5d0c25ef1629ab0c6bb59ba849cd7a0c15284a780
0ae3b61b13becbed1337d37987e36ebd9b75a8e64754a8109b5efce08306244aa62cf7d0ff73b73f2eaa363d6cfb993be1196ecb43edf51
e702e7575fc3940a36c2075223a7ea6c06466f8d689d6a8f9b2309d893073ad6ad0bb44044b1bc7d0c093b049c07772e39d9e4717733887
157460332241e807e07b0a848715ae8eb085eb96116d1e5e3e98b70321af069c52d34f5e8f99198d18c0a7e0e702fe4a1241956b09ab6b9
e5040ba2a9f0d9c5fe87eae988913c5e08554d32feeb3e60148b1fb1ea67b2ff55285e928efe9039197fe9783eb0ec13d28aa74fcf9e554
9b48ab7098927df50a6cb1bad0dedf8236cb099c1746855b180be40154aca88b4e966cb89ca2813e57dcb0fb7d36f4fbab5ad82a98522df
ddd9e50b9f6335c97b7d36cf8f082d4efdf887cf09dfa42a4926bbb1b58d033c6ad34373045e0e2c9ea83a537127c3c7b2c99b53187623a
a5b429c6ce47499adecdeb536ebe3d884d4109bc06de2f54c221258c15d658ea5dd02039d44af44ca6541e6f4eb7242ed1973ddb5cb0d08
aa78db6ac14d7efd2b20db298ad9bc1cd6c8b3620c8f035496ca386450a81b73022bb9d6bdf7b5142ea8e49e923a6515404f166c24d1e01
3b1b040759e083212dd9f2dd0b9bcd7d66f2eaae71b4da533dbaad18f34c86e72d0b836d8a78c0ab0e9fe657616c67c021b99313f0661ff
e4628a9d3caf75c54727c1720ac71bb484fae167b24d2c197a07cfa10343dbebb02a23e45c7e6cea8f8fe242a3d7407cbcb1553d05a643c
609c9783b43581b957e473569c3b2cf88c1ef91eb4a62392b20a15297f8c85cd7a1cc20b794abae302e3cd3ca869b4dc2258c57ea2849e8
1f31ff2961ef1f02bd7170677848c1d2aa5f4b854682148e651f34bb90fbad9606bf1b096f47c0a662d03869ab963308b7454d921bcf04c
35272d5684b31c6182a4b446eb991a92ffe783b497aa09bc969169a843e795c577c94ddfb2781bdea0f23bcb5b98c12cac376085219ec53
54bfb1986bf9e8e2369f4dc1cc59f0e4971d2710037e35cfa228482fbc8fc197fc8a483937e72c54728efbb7e26b3b7122143e62e0f96c6
bf7a5673444f55af3a224f980ece97cb3152ea3a36a45a47a8190d255e8f8983aa353b99d8441969b5ba7f8c2130766052b7d9aa4962d73
2d618b561b38ba4a7d5851cb1baa6d032d4078114f5e943bc53af0eb52e422361acd6e088c98a477000ae91b11242f083554a921837b69f
be571ee7460e8877a74d50f02d3c8061591c73591cc67f13a4c5ee8ee00211a4142408a7530c0de5c606d7d76479bec4d9c0ed8d96a1274
b589488fe0e73c8be1f69b1a5650ffda224c263544357b22561f64ec9e3b82525022af207610073d6b26ecb3a3f534c95c5bb0536eecafa
f7d83586e88e7174becc82eeec7d8ab52f86caf55fb12d6e08d29242a38cc7b4b94a6219e463426e5a4b38a9f33932c1da2de02ac96a5fb
a25002790ab95cfe61d720dc29cbac8adca48da51bbd22cb79a81fec9e9a3af21e7d22581c4ffb7375920ba798871a0cb69ce6c98fcec09
b4520d27d4d062c151865704ca63de90b733c7b49b6a94ba5477e2c585a1e81fffc5d491d85027a9cb77334988a915e5a79917607a00354
f1ece6334d9f3ecffb5b8c10f1d059c2f63baad95c239a9546553045cf3c9f318d7169d24db36ce3b088609e766882040011b7bc05bd271
b67bf8b372fbc750bc3edd134d3c59e87ee13abb4c2ae32f66b42c5b48ac382102c7039619a94f8bab8ff56ed5fec6b44cec3ebdf74c7c1
862b334cab340202d1a86b56b4981e25d83a33f5bcbc48b260c0b0f092f133c65dd1dd6450c0ae5c04d4ed44b16237f0618e4d810dfccab
1e884693e683a46c79b76e7028f0a12ffca9115df65e7e7d5249cc85eeb1d5b5a736f47226895c0445ddd714eaa636ef4d866b54b7f7187
38f539c063f6e84ed8255390e410b2400ae390a907b2d428de5c10f78e499d8f433f1d3ead0872f6f8469b35baa4c702a7b69ea9a6e657d
6422c00f300bc15cdfa9a41eab53096bdf3dcfb2d80ed70219818cd697bf03759aa888bcee86a01741e316cd28441207c12fc9b36ee8cbd
c2d774656b58441799fa05155c1614c4b735c081c5af2e18d9ad8abc733d2924fb8d6ce8b733d56d67d9482aa49bac974e7de109bf6d32c
924e96172455c4bcb0aa85b6812c704a2af87c683a1f2a9a9101d0fe0ca70a6a6d5ca1fff045b823235c334536b9c429da3272327be745e
fd912040a6445c245f5afa146350f9f193651f8db41ae4d4afa7ab81198043be7cf59dc6de0283f27fe848cbdb2962cacfb96803d9e7ebb
497ab1b0e65f802bdd27b9a36333ee24ae7f5e94a8cfadde40e17b01d6c72d1463f68ef98169eec313f97a02e0634cbe89e2dd4c4d53217
fab8bbece82b1ce9dff25ccc6f7c86f8e743e269492aa5cd7fae7b0ec5ff323b53d70ecf0bd8529898531c6b85f42a304b152bccfae92ba
ed9732d68584c507b510cb27c73277388eea9567b7c3b1f7611d2e2562a86930281e815c0919e87c9ab923cd8be7175b7dbe8413e37fc51
2e0d4a0a8d47ac53df1e19576369539f7b5d74be7f93a2a866a041fe3a8d23196057099afdcb41353c6eba769edae13e502c8a33310f700
9faf5b858b9fda982c3ccd28db2c9cecb7022d981c47967bafb8cb4be54f0d426d5ec7e414e3ea0eb9a0e4218ced00ceb393f4ebeade224
df16af55305155ab2af8fc8a983ba64faee53aaf1cb6e5a1794d3fd0506e0685e4b002d69148aaa795a28d3106edd9a6a001ead88c221ac
539cc44a8a282455c25804a6c26ddabbeb0c917e835a1f71acf6920a33c2732d75465077cda205ae87c9d23d8b8f67d565881ce3b4c48b6
e06d93e310fb1f2fa0085493682ee124d582d85361e29c0cdc2f68e93a124671d9e70c4328793406c5622864dd903c7a5a0244cd24d4a8b
61f4d1647d524e5de8acd571219455d57438b0907a230ed3e47a9bd3f291548aa15a8006308e7f1a8593e27f9fa55ef280266a3f1016212
db2921e80ae9c5d838a24071ac873fe8245e2e877d466f69eb7c87dd9176f31d38476ebfa9f287a699b7d4634902d22b843c575714c84ab
f02f04edf7f56d7646df65c155933b643dca2a2e7621660635e215401ee7004ded4aa9e023b13bf1cdf44720218a02975b9c14707a5baf2
25bfc4937ead5193d533ec78246de1dd94a23f56ad47de9d5b6a47ce60af33324f5afdf1981c2b460da884ecbe4a7f082527fceba6dfbdf
a5dd32d7cbc08a0f4f24b8411821c0b22e1053029dfb9b0d0e61c997f9b4efd9783c8c384cb72a042c1ff23d3eb802a367502ff73e952ec
cfd9b6fa422193defc9f040df291ee198b931fe376b5a4acc56cae472c734b08a4eba293503bd534a14e935b925322d5bb083cfd2ec1600
893ace9e0772339d17f724f4e54f1d9a998fa93dcef407ae4bfc144938a7cacbbf506327b6ef761f6b8bcda139b2852b5d10f325effb71c
bb19425c6a212261604b3ab229fa991d8c0011fc3bc3592d0bee2243db7485ab0fba3520538ab9f527cd5a1dab5205de3261c8283c93a9b
6aaf1e66c32b7a445526cffc142ddeff1c172c583def9bdc2d9f73882e65b9cc7490739a064c62e04f4ddba4957badd6b50c64a13d8790e
2820dd20d6c1218462af6a070c768e8c953b779c732de4ff756f87ecf2695caed49461216883bf5539415359691a57e69b08c0630fb2732
eeee2d6e6d99df1cbffc2212e79c8cd2ab4f8052acf0bd2bdea1d3337cf5b1773f5a1a095e431ed7e467fb386dbab83d30b4924f6bb4d9e
f0851f3cd218c48441af9520e2bdc12db9e4d3b22ca685e9547d2a24a5b9aef06edb4944fde879a8ca7fd2522fca7591017e16881637d12
01f13547b777fee849eec6cd95d2bb18859b5367e1ea78728bc7a7cdc8deb7b92103f65fd799d23dc7e31adbe62b729dd222fbf3e15a97e
f72aea07e183577f591a081845ea340797b79c6c8742e8826a6d7df773ec8564025fbb570bfbc001740e3437384df6324c7605526d9a630
a5d4483f1
''')
db_write_enable=unhex('''
06020000015ddf5d76a8e3bab8b10d9f82f0b31652d5b208dff1d20745b7c5442e09db880c80ca103d5b2071d8bdcaab452b900d1785be2
de478669e72a9d082273f0f6176b700ffb65a026af052e76de5f9e247e994bed7c79e34815b465a8070a76da7d365525d7285dfb152d24c
732a471f031d22ebb67d6e27bfff74b3ab1c4780461a460c0ea5710fbd32ab59a6ca9ca3e3e24264640b15245a6c48b6bd20c6848c2307e
854af75b059e86793dc7a30474a1b1b5484c58dc5a4535eb71911bf9b10f82c27362d68b4baa1ace63a33b7cf3de616810ee5033601febb
cc08b0f55166f4ca08d42d3361426712f7e5a4842e9d2e906c87e4c5c6694eec448c109abe3238128fbbb8b8f383e3b66133f0c9d38ca53
5d47ee9dd8841fcb205510452bd0f733d416b6c763f0b7a647e1992ede714b6c4569de329440549a5a1f6646dc88b08288fc6f9c4f55d90
ccc86487af195ed635e4c9a5a2df5df3bc4e39ff0ad6bc70a54d142a01ba9ef22839859ea1cb9b227cf9407a9f61b3cf8273549d759c3ff
b6db4b33bf9a40f6de2680cde8ad01158ed1c6771bba9fbe230c9e9b8a4edff941f43c4e629097c6149a5ac13ebe757b43a52af3f263f8a
a36ec69989c62539b656a49dfdb14ea066ffec203c41411de29251bc40f75860976834acf368db991d0c88cc5d9f8229be691f55c1dc298
dd0690aa09f46a7a0dcb92909a654a54cb584ed65280236bf368ab2ae909086dcbafa6fbe26556cd724ee9e6b7b3ce2290c5cb832bd4301
66047ab2281c55e7d9d29637bbadfa40b4b9a5008e86239a5998d39a5dda6cfeb7d0bff10bf56c523878db6de81b7ed824fdfdd9515f5d4
90345b47172bdc9db6bbb10f19d39f061fbe53c3397dcb8fe30d5ca032ccf2d26184635b691705b471d81dc86ee31f144512072d9ef7559
e9ef9db8a08fea98abb6706833a9684855578ec04b72ae1bc4e559c87dc1dcbac3115c2c6697cdb45762402836580f29330cb0fe8e4116b
dcd4da603082ca420f5de71b92657c5a6ca96f51e6ed9945ffd39e9c79441ba5cba40b2c09af4d16c3d7957a2d8ab7469819c0c6dd177e3
e7dea2240a1d96756b5fde207543167417a74e702df1781f65165c4ed7bc993e72789af03c2cdcdaa7b558423fd064328d57f2ec9a8ab2b
ca9bda6f625e103a79a12561675e05b448f5dec7554649fb10ee3d28599306de19f63ce50e4c346cabff9f5ccd36072831664c308e63184
cbf31e5541f46e01500d6ed045d7d44eb8087355c5bd5a65c58c705b5299e52cebcbc79254d20b6835c76cfd47d99f9b1f4a96169249a5e
28d6ca753efdb8a008edffe2a7f68ae23bb7d6f84f40751f0232251d4ad4dedf0fb4b8b5f8fc4701bc4578cd8e1c4bb491d16196e952e0c
e684e63ce9b608ef61551011328866545a6999e0881ffc758e41b451e1c2c8f571294eb1d5828131af9735003d00488200dc3b9f6cb6a7d
2bc1239367b372914d4a3a3ecafc89372497d9621c1629c1aeb40d9b6f17efe1fdf19583ae2867efaf0c682d59ef72397c7d709da80425c
30b91b1716893039205b9dc46ef8979aa5f8004c6bb698b35c4b600b45cd9dfc2a929fec37d392ce9e3b83240707226402d89bb034c3a23
07413ba39eb1afdcabbabe9742fe2c62acb679a57ee903de0a788fee19d550b78c4f2095e388766724061b39a1e54c585ddbce238a385e5
0c4ba9866c16ea8f438cc58aedacd6c2652b0ab0847794f80968daaf8a70422c28545caf3e95c38c54e142c8ebf26d5b5adcf905d8f105d
ef1df70997ccaf59e256e7b178e4bc9ee8d6437f23f59e7dcc8fb3f18769fa67bba0b4791cf8c0a4e330751e671be3235a9ffd83272eceb
af2d3b91a91599b1d4f84aca5660dca24acfe5fa78e95f944b4930db9e81a6f3ed23c90b2a446ad7832b6f4bd8e6b932116f4aedab500a4
dfe311d54b3ff9ddd3c73819b5c88a47c87a117c3a008e49361ba1ad4632d38ee4f2fd8822c079a278b491ed5b9488cdfff5ea48dbf8aee
793a2d2e837a9f5fd195833c104da32c3a1363ac846d2a59ffca0c3b1293415069db5e459edf60d704de47138c315df3a8440b378ee88e1
cdb8889b08e0cbeb4ed98311e5218369535e03062507d7c1d13b522e3eec020ffd4cb1b2297760adb4ef7c76a5fd2e78a60bee3cf246f24
8fad7fe6fa8e9c65b52515a7ce73c47c20c04c32f377e1d0ddc20e7e96f7fea55fd85d8456849ffe4cc3c6a03c5b9f21d055e4d498934be
ff14deec1e77f857004d929409e23e54eb19e72a733a3fb3abd035b72a4a4c2076874061baa5526e855be24b413758cdcbe1150bc916514
254ecb95848fc717718265a6901966f22c33dd6ae48ff9cb5731446addb3ff2ea17b1c3586e52afa23bb44ea6c2bec062c59adcb791736a
a2da95eb8445a6381f3941917bed7fcf01ab199228eadd1968847e4b9a40022d2b0ec68318abf97c6d46286a5
''')
# 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 blob has only 1 byte difference from the above
enroll_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
''')
calibrate_prg=unhex('''
0298006103230000002000080000200080000001003200700000000080202005002420000050207736282
0010030200100082170000c210000482102004c210000582000005c20000060200000682005006c200129
70200121742001887820018084202000942001809c200902a0200b19b4200000b8203b04bc201400c0200
200c4200100c82002003300100000000080cc200000f503d0200000a1013200440000000080dc20e803e0
206401e420d002e8200001f0200500f8200500fc200000b8203b000008040014080000080800000808000
01408300008080000140831001c081a0032000c0000000080501101004c1126003400080310071d10071d
10071d10071d10071c01065810080101000007c8078c06100000204f80007f000003070107010c07032c0
8fc80095a800afc08fb800b5a095b800afb08fa800b5b095c800afa08f9800b5c095d800af908f8800b5d
095e800af808f7800b5e095f800af708f6800b5f0960800af608f5800b600961800af508f4800b6109628
00af408f3800b620963800af308f2800b630964800af208f1800b640965800af108f0800b650966800af0
08ef800b660967800aef08ee800b670968800aee08ed800b68096c800aed08ec800b6c096d800aec08eb8
00b6d096e800aeb08ea800b6e096f800aea08e9800b6f0970800ae908e8800b700971800ae808e7800b71
0972800ae708e6800b720973800ae608e5800b730974800ae508e4800b740975800ae408e3800b7509768
00ae308e2800b760977800ae208e1800b770978800ae108e0800b780979800ae008df800b79097a800adf
08de800b7a097b800ade08dd800b7b097c800add08dc800b7c097d800adc08db800b7d097e800adb08da8
00b7e097f800ada08d9800b7f0980800ad908d8800b800981800ad808d7800b810982800ad708d6800b82
0983800ad608d5800b830984800ad508d4800b840985800ad408d3800b850986800ad308d2800b8609878
00ad208d1800b870988800ad108d0800b880989800ad008cf800b89098a800acf08ce800b8a098b800ace
08cd800b8b098c800acd08cc800b8c098d800acc08cb800b8d098e800acb08ca800b8e098f800aca08c98
00b8f0990800ac908c8800b900991800ac808c7800b910992800ac708c6800b920993800ac608c5800b93
0994800ac508c4800b940995800ac408c3800b950996800ac308c2800b960997800ac208c1800b9709988
00ac108c0800b980999800ac008bf800b99099a800abf08be800b9a099b800abe08bd800b9b099c800abd
08bc800b9c099d800abc08bb800b9d099e800abb08ba800b9e099f800aba08b9800b9f09a0800ab908b88
00ba00801800ab808b7800a010802800ab708b6800a020803800ab608b5800a0308048020030704040200
00000000002f0004009000000029000400000000003500040010000000170000003000cc0103000000ff0
000001d000003ff00000025000003ff00000022000003101112131415161718191a1b1c1d1e1f20212223
2425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4
e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778
797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f2b23203
c2d182e1e30182e1c321d341d341e321c301e1e241e201f201d1c321a301e1c211e21341f1e202024201f
1e20201f212221221d221e23341e1d1e1d20341f1d193b341c1d1e35201e201c20221f341c1e1e1c221f2
01d21201e1c1f34242221201f20221f201e241e241d2020221e2420231d221e211e1f1e1e341c321e3220
301d2d302f2d2c2b23223a211c7e7f807f808080808080808080808080808080808080818081808180818
080808080818081808080818081808180818081808180818081808180808081808180808081807f808081
8080808180818081808080818081808180818081808180808081808180818081808180818081808180818
0818081808180818081808080808080808080807f807f807f807f7f7e7e
''')

View file

@ -1,6 +1,35 @@
from .util import unhex
init_hardcoded=unhex('''
06020000015cb560afa595d0dcf4fca09ebb69301e2b9e24a5dfb6f2602833427d3bd65222c418ff15b54587ab2854c4fe9aea74fa55567
2036fa3ec73c6912c58b1b49cbdcc7664165482cd70d5bb95d9d469626fdc2e012d74157c5792260968a2a4572b1ccccc26ec2e2ed0ddda
1b0931d3d0566460915d43e0a654a0582706be9113ea88f0c15ba058507efb0dadecb4a4cc64444dddf050b6e8d4ebb34ebba33e8651c75
e5fb28f85c83197df1de460d9e1cb82208753ceff0ef60a823dba75d05548f5b3a5a0e2972232f7403bd6869da90e5371a0ab8ad23972f1
597630f5ff7c8b8272800563477288b5591bbb0341d3975efc1778225767fa35480ff7f8dd633e4034ac32e4af58b86ebc63552cb35b12b
285255deaf3a32bf46cdc5ad3bc1c9ed1bcc112c72143f9aec568e2cacfa89ba0c7bb65590d8b93e6871a33c6c6983c0acd04e737ff55ee
e024ca6b9a48332c1a69a5a3fdd24b964cf7e7c55229bb0b48a6e339eb2c42d07ec850a4ee780660ad6c77ffa302a63bd19426134c4533d
69192ef2e16591df26394791a4ecb994a24f5a7f70f1eb2604e6bfb67a452cb74ead8b0d9808f890ac386750cbac06ee03a852145094053
b2b074b9905de5cdbf2272b67e51f159164778d6d2ef7a1ccb81df9f896ddb38ce11e8140cf6cb9c82
''')
init_hardcoded_clean_slate=unhex('''
06020000012c40c9d271378bc0912ef5dced69bd81b7fc16972c7b46e621af54a00e2cc6baca6eb83ea30222dfc6c925262006ae93412ea
cf482f2034ee7b13297474b7e1e91f279cac2ccb7195443e4dd3328cfd292ade073fcc2eaa8f07b77231130ba997f921b9be7b4fb6cc691
0d2976b3e050913b27dbe73afd6e964260b9435ebab5117e71f7cb68464d4b6f8afc7e1a421f671f5854a1d0c8ab93ed3b88b2bc1a42875
e40b15f0e78496ac40e4a4a7fd39a97534ce1866479e0384f0789bbfc2fea0ce982bf7a9df97d60b237edbe1b26c9791043a96b81e435d6
de5971c758d374905df95b0cddabfbf531749ba191f07a6f5e2722852f137a53513a9ec6ab30c3f09aa6ce21b391e55cf81dcda6422011b
f163317a9a4382546141d45f2274bd660103bd3af705f3ed12e493bc4f834d5d7f162e2c3405cf857b00129789a3353bf7fab7796e267e3
062d55660dbbb857911ac8e871c460dd31c56a86a5631475f0f2ee5e9ce2af0faec0931a640ba2394025f29ffeca3a7e99c15a78ce1f1f7
808cedd7601b9b6382d72ca873257d4f6af70e29e22afea15e36e0282b8f0bfc68ffa3417d212b8bbe11bb73b363a19872e6e947d45de30
fbc493ca083a0a4650615d8628606362081ca6df5d67527971d1776ad76a7a28c932f0317b59cb4a82a14b2bcb7b01fb662be1496d24d91
9140ec80068b21a818daa2fb8e05f63edbd4bd5797c74a28b3e7cf81c9045248584977711341fca3f08ba91ff853b62dc24ce4bba4ed57f
47bd458545d805b6bb14fe0cde01440b60bf7be937f6444a8e2a10ed8fa9ddb8604bb95fe411b97112e78dbf5a4a0f004669c93765a9f38
665cb55f5658895c1c06a7aedf694bfb3afa9b8b1dea5ab85c821ac20b0663b95023642fda36ad78e3e00140b966f404f7e55f0b416ea43
b4c74c39900830abc6906a1004bef1b5b7dbbbeb5ec1b22604ac86429b9f56511b746a7124c449b8c9498f49144abc2d64f6a114f1d7f91
aa41249faeef4d838e280cb5d6fc19cfe86c75f
''')
reset_blob=unhex('''
06020000013920c0cdd8e7e68d6ef897ee686fbf657f83b9514341e1d3c0835a28c9cd1ccd0016644f74d
d02a73b85d95f4d352417cee76ba0bca3ce474159c62ade3e10797715a13f15a292f5d57c6d46cf8f5ef6

View file

@ -37,7 +37,7 @@ def persist_calib_data(calib_data):
write_flash_all(6, 0, calib_data)
def calibrate():
def calibrate(calib_data_path='calib-data.bin'):
# no idea what this is:
write_hw_reg32(0x8000205c, 7)
if read_hw_reg32(0x80002080) != 2:
@ -53,8 +53,8 @@ def calibrate():
print('FWExt version %d.%d (%s), %d modules' % (fwi.major, fwi.minor, ctime(fwi.buildtime), len(fwi.modules)))
if isfile('calib-data.bin'):
with open('calib-data.bin', 'rb') as f:
if isfile(calib_data_path):
with open(calib_data_path, 'rb') as f:
calib_data=f.read()
print('Calibration data loaded from the file.')
else:
@ -75,12 +75,11 @@ def calibrate():
rsp=tls.cmd(calibrate_prg)
assert_status(rsp)
print(rsp.hex())
# ^ TODO check what the rest of the rsp means
calib_data=usb.read_82()
print('len=%d' % len(calib_data))
with open('calib-data.bin', 'wb') as f:
with open(calib_data_path, 'wb') as f:
f.write(calib_data)
lines=[calib_data[i:i+0x90+8] for i in range(0, len(calib_data), 0x90+8)] # TODO work out where "bytes per line" constant is comming from

View file

@ -1,5 +1,5 @@
from .tls import tls, psk_encryption_key, psk_validation_key, hs_key, crt_hardcoded
from .tls import tls, hs_key, crt_hardcoded
from hashlib import sha256
import hmac
from struct import pack, unpack
@ -40,9 +40,9 @@ def encrypt_key(client_private, client_public):
m = m + bytes([l])*l
iv = get_random_bytes(AES.block_size)
aes = AES.new(psk_encryption_key, AES.MODE_CBC, iv)
aes = AES.new(tls.psk_encryption_key, AES.MODE_CBC, iv)
c = iv + aes.encrypt(m)
sig = hmac.new(psk_validation_key, c, sha256).digest()
sig = hmac.new(tls.psk_validation_key, c, sha256).digest()
return b'\x02' + c + sig
def make_cert(client_public):

View file

@ -16,12 +16,6 @@ from .util import assert_status
import pickle
# Info about the host computer
product_name, serial_number='VirtualBox', '0'
hw_key= bytes(product_name, 'ascii') + b'\0' + \
bytes(serial_number, 'ascii') + b'\0'
password_hardcoded=unhexlify('717cd72d0962bc4a2846138dbb2c24192512a76407065f383846139d4bec2033')
gwk_sign_hardcoded=unhexlify('3a4c76b76a97981d1274247e166610e77f4d9c9d07d3c728e532916bdd28b454')
@ -51,10 +45,6 @@ def prf(secret, seed, length):
return res[:length]
# pre-TLS keys
psk_encryption_key=prf(password_hardcoded, b'GWK' + hw_key, 0x20)
psk_validation_key=prf(psk_encryption_key, b'GWK_SIGN' + gwk_sign_hardcoded, 0x20)
def hs_key():
key=password_hardcoded[:0x10]
seed=password_hardcoded[0x10:] + b'\xaa'*2
@ -92,12 +82,23 @@ class Tls():
def __init__(self, usb):
self.usb = usb
self.reset()
self.set_hwkey(product_name='VirtualBox', serial_number='0')
def reset(self):
self.trace_enabled = False
self.secure_rx = False
self.secure_tx = False
# Info about the host computer
def set_hwkey(self, product_name, serial_number):
hw_key = bytes(product_name, 'ascii') + b'\0' + \
bytes(serial_number, 'ascii') + b'\0'
# pre-TLS keys
self.psk_encryption_key = prf(password_hardcoded, b'GWK' + hw_key, 0x20)
self.psk_validation_key = prf(self.psk_encryption_key,
b'GWK_SIGN' + gwk_sign_hardcoded, 0x20)
def cmd(self, cmd):
if self.secure_rx and self.secure_tx:
rsp=self.app(cmd)
@ -133,6 +134,7 @@ class Tls():
print(s)
def app(self, b):
b = b() if callable(b) else b
return self.parse_tls_response(self.usb.cmd(self.make_app_data(b)))
def update_neg(self, b):
@ -491,12 +493,12 @@ class Tls():
raise Exception('Unknown private key prefix %02x' % prefix)
c, hs = body[:-0x20], body[-0x20:]
sig=hmac.new(psk_validation_key, c, sha256).digest()
sig=hmac.new(self.psk_validation_key, c, sha256).digest()
if hs != sig:
raise Exception('Signature verification failed. This device was probably paired with another computer.')
iv, c = c[:AES.block_size], c[AES.block_size:]
aes=AES.new(psk_encryption_key, AES.MODE_CBC, iv)
aes=AES.new(self.psk_encryption_key, AES.MODE_CBC, iv)
m=aes.decrypt(c)
m=m[:-m[-1]] # unpad (standard this time)

View file

@ -1,6 +1,7 @@
from binascii import hexlify, unhexlify
from time import ctime
from os.path import basename
from .tls import tls
from .usb import usb
@ -8,7 +9,15 @@ from .sensor import reboot, write_hw_reg32, read_hw_reg32, identify_sensor
from .flash import flush_changes, read_flash, erase_flash, write_flash_all, write_fw_signature, get_fw_info
from .util import assert_status
def upload_fwext():
def default_fwext_name():
if usb.usb_dev().idVendor == 0x138a:
if usb.usb_dev().idProduct == 0x0090:
return '6_07f_Lenovo.xpfwext'
elif usb.usb_dev().idProduct == 0x0097:
return '6_07f_lenovo_mis.xpfwext'
def upload_fwext(fw_path=None):
# no idea what this is:
write_hw_reg32(0x8000205c, 7)
if read_hw_reg32(0x80002080) != 2:
@ -19,7 +28,14 @@ def upload_fwext():
# ^ TODO -- what is the real reason to detect HW at this stage?
# just a guess: perhaps it is used to construct fwext filename
with open('6_07f_lenovo_mis.xpfwext', 'rb') as f:
default_name = default_fwext_name()
if not fw_path:
fw_path = default_name
elif basename(fw_path) != default_name:
print('WARNING: Your device fw is supposed to be called {}'.format(
default_name))
with open(fw_path, 'rb') as f:
fwext=f.read()
fwext=fwext[fwext.index(b'\x1a')+1:]

100
proto9x/usb.py Normal file
View file

@ -0,0 +1,100 @@
import usb.core as ucore
from binascii import *
from .util import assert_status, unhex
from struct import unpack
from usb.util import claim_interface, release_interface
from queue import Queue
from threading import Thread
from usb.core import USBError
from .blobs import init_hardcoded, init_hardcoded_clean_slate
class Usb():
def __init__(self):
self.trace_enabled = False
self.queue = Queue(maxsize=10)
self.quit = None
def purge_int_queue(self):
try:
while True:
self.queue.get_nowait()
except:
pass
def open(self, vendor=0x138a, product=0x0097):
self.dev = ucore.find(idVendor=vendor, idProduct=product)
self.dev.default_timeout = 15000
self.thread = Thread(target=lambda: self.int_thread())
self.thread.daemon = True
self.thread.start()
def usb_dev(self):
return self.dev
def send_init(self):
#self.dev.set_configuration()
# TODO analyse responses, detect hardware type
assert_status(self.cmd(unhexlify('01')))
assert_status(self.cmd(unhexlify('19')))
# 43 -- get partition header(?) (02 -- fwext partition)
# c28c745a in response is a FwextBuildtime = 0x5A748CC2
rsp=self.cmd(unhexlify('4302'))
assert_status(self.cmd(init_hardcoded))
(err,), rsp = unpack('<H', rsp[:2]), rsp[2:]
if err != 0:
# fwext is not loaded
print('Clean slate')
self.cmd(init_hardcoded_clean_slate)
def cmd(self, out):
if callable(out):
out = out()
if not out:
return 0
self.trace('>cmd> %s' % hexlify(out).decode())
self.dev.write(1, out)
resp = self.dev.read(129, 100*1024)
resp = bytes(resp)
self.trace('<cmd< %s' % hexlify(resp).decode())
return resp
def read_82(self):
try:
resp = self.dev.read(130, 1024*1024, timeout=10000)
resp = bytes(resp)
self.trace('<130< %s' % hexlify(resp).decode())
return resp
except Exception as e:
self.trace('<130< Error: %s' % repr(e))
return None
def int_thread(self):
try:
while True:
resp = self.dev.read(131, 1024, timeout=0)
resp = bytes(resp)
self.trace('<int< %s' % hexlify(resp).decode())
self.queue.put(resp)
except USBError as e:
self.trace('<int< Exception on interrupt thread: %s' % repr(e))
if self.quit != None:
self.quit(e)
finally:
self.trace('<int< Interrupt thread is dead')
def wait_int(self):
resp = self.queue.get()
return resp
def trace(self, s):
if self.trace_enabled:
print(s)
usb=Usb()

View file

@ -1,10 +1,10 @@
from proto97.tls import tls
from proto97.usb import usb
from proto97.db import db
from proto97.flash import read_flash
from proto97.sensor import *
from proto97.sid import *
from proto9x.tls import tls
from proto9x.usb import usb
from proto9x.db import db
from proto9x.flash import read_flash
from proto9x.sensor import *
from proto9x.sid import *
def open97():
usb.open()

View file

@ -1,4 +1,3 @@
fastecdsa==1.7.4
pycrypto==2.6.1
git+https://github.com/fabiant7t/pycrypto#egg=pycrypto
pyusb==1.0.2

View file

@ -0,0 +1,256 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# 2020 - Marco Trevisan
#
# Initializer for ThinkPad's validity sensors 0090 and 0097
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
import os
import subprocess
import sys
import tempfile
import urllib.request
from enum import Enum, auto
from time import sleep
from usb import core as usb_core
from proto9x.calibrate import calibrate
from proto9x.flash import read_flash
from proto9x.init_db import init_db
from proto9x.init_flash import init_flash
from proto9x.sensor import factory_reset
from proto9x.tls import tls as vfs_tls
from proto9x.upload_fwext import upload_fwext
from proto9x.usb import usb as vfs_usb
class VFS(Enum):
DEV_90 = 0x0090
DEV_97 = 0x0097
DEFAULT_URIS = {
VFS.DEV_90: {
'driver': 'https://download.lenovo.com/pccbbs/mobiles/n1cgn08w.exe',
'referral': 'https://support.lenovo.com/us/en/downloads/DS120491',
},
VFS.DEV_97: {
'driver': 'https://download.lenovo.com/pccbbs/mobiles/n1mgf03w.exe',
'referral': 'https://download.lenovo.com/pccbbs/mobiles/n1mgf03w.exe'
}
}
DEFAULT_FW_NAMES = {
VFS.DEV_90: '6_07f_Lenovo.xpfwext',
VFS.DEV_97: '6_07f_lenovo_mis.xpfwext',
}
class VFSInitializer():
def __init__(self, args, usb_dev, dev_type):
self.args = args
self.usb_dev = usb_dev
self.dev_type = dev_type
self.dev_str = repr(usb_dev)
print('Found device {}'.format(self.dev_str))
try:
if self.args.simulate_virtualbox:
raise(Exception())
with open('/sys/class/dmi/id/product_name', 'r') as node:
self.product_name = node.read().strip()
with open('/sys/class/dmi/id/product_serial', 'r') as node:
self.product_serial = node.read().strip()
except:
self.product_name = 'VirtualBox'
self.product_serial = '0'
if self.args.host_product:
self.product_name = self.args.host_product
if self.args.host_serial:
self.product_serial = self.args.host_serial
vfs_tls.set_hwkey(product_name=self.product_name,
serial_number=self.product_serial)
def open_device(self, init=False):
print('Opening device',hex(self.dev_type.value))
vfs_usb.open(product=self.dev_type.value)
if init:
vfs_usb.send_init()
# try to init TLS session from the flash
vfs_tls.parseTlsFlash(read_flash(1, 0, 0x1000))
vfs_tls.open()
def restart(self):
vfs_tls.reset()
self.open_device(init=True)
def download_and_extract_fw(self, fwdir, fwuri=None):
fwuri = fwuri if fwuri else DEFAULT_URIS[self.dev_type]['driver']
fwarchive = os.path.join(fwdir, 'fwinstaller.exe')
fwname = DEFAULT_FW_NAMES[self.dev_type]
print('Downloading {} to extract {}'.format(fwuri, fwname))
req = urllib.request.Request(fwuri)
req.add_header('Referer', DEFAULT_URIS[self.dev_type].get('referral', ''))
req.add_header('User-Agent', 'Mozilla/5.0 (X11; U; Linux)')
with urllib.request.urlopen(req) as response:
with open(fwarchive, 'wb') as out_file:
out_file.write(response.read())
subprocess.check_call(['innoextract',
'--output-dir', fwdir,
'--include', fwname,
'--collisions', 'overwrite',
fwarchive
])
fwpath = subprocess.check_output([
'find', fwdir, '-name', fwname]).decode('utf-8').strip()
print('Found firmware at {}'.format(fwpath))
if not fwpath:
raise Exception('No {} found in the archive'.format(fwname))
return fwpath
def sleep(self, sec=3):
print('Sleeping...')
sleep(sec)
def try_factory_reset(self):
self.open_device()
try:
print('Factory reset...')
factory_reset()
except Exception as e:
print('Factory reset failed with {}, this should not happen, but ' \
'we can ignore it, if pairing works...'.format(e))
def pair(self, fwpath):
print('Pairing the sensor with device {}'.format(self.product_name))
max_retries = 5
for i in range(0, max_retries):
try:
self.open_device()
print('Initializing flash...')
init_flash()
break
except Exception as e:
err = e
self.sleep()
print('Try {} failed with error: {}'.format(i+1, e))
finally:
max_retries -= 1
if max_retries == 0:
print('Device didn\'t show up after reset, retry...')
raise(err)
self.sleep()
self.restart()
print('Uploading firmware...')
upload_fwext(fw_path=fwpath)
self.sleep()
self.restart()
if self.args.calibration_data:
calib_data_file = self.args.calibration_data.name
else:
calib_data_file = 'calib-data.bin'
print('Calibrating, re-using {}, if any...'.format(calib_data_file))
if os.path.exists(calib_data_file):
calibrate(calib_data_path=calib_data_file)
else:
try:
calib_data_file = os.path.join(tempfile.mkdtemp(), 'calib-data.bin')
calibrate(calib_data_path=calib_data_file)
print('Calibration data saved at {}'.format(calib_data_file))
except Exception as e:
print('Calibration failed using device data ({}), ' \
'You can try loading a local `calib-data.bin` blob, ' \
'the device should work anyway, so skipping...'.format(e))
print('Init database...')
init_db()
vfs_tls.reset()
print('That\'s it, pairing with {} finished'.format(self.dev_str))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--driver-uri')
parser.add_argument('--firmware-path', type=argparse.FileType('r'))
parser.add_argument('--calibration-data', type=argparse.FileType('r'))
parser.add_argument('--host-product')
parser.add_argument('--host-serial')
parser.add_argument('--simulate-virtualbox', action='store_true')
args = parser.parse_args()
if args.simulate_virtualbox and (args.host_product or args.host_serial):
parser.error("--simulate-virtualbox is incompatible with host params.")
if os.geteuid() != 0:
raise Exception('This script needs to be executed as root')
usb_dev = None
for d in VFS:
dev = usb_core.find(idVendor=0x138a, idProduct=d.value)
if dev:
dev_type = d
usb_dev = dev
if not usb_dev:
raise Exception('No supported validity device found')
try:
subprocess.check_call(['innoextract', '--version'],
stdout=subprocess.DEVNULL)
except Exception as e:
print('Impossible to run innoextract: {}'.format(e))
sys.exit(1)
vfs_initializer = VFSInitializer(args, usb_dev, dev_type)
with tempfile.TemporaryDirectory() as fwdir:
if args.firmware_path:
fwpath = args.firmware_path.name
else:
fwpath = vfs_initializer.download_and_extract_fw(fwdir,
fwuri=args.driver_uri)
input('The device will be now reset to factory and associated to the ' \
'current laptop.\nPress Enter to continue (or Ctrl+C to cancel)...')
vfs_initializer.try_factory_reset()
vfs_initializer.sleep()
vfs_initializer.pair(fwpath)