From 7c6309bf39b1abd5a16fd0e63135e139add0c3a4 Mon Sep 17 00:00:00 2001 From: QuestEscaper Date: Sun, 10 Nov 2019 22:40:02 +0100 Subject: [PATCH] Initial commit --- README.md | 248 ++++++++++++++++++++++ ble_companion_client.py | 383 ++++++++++++++++++++++++++++++++++ extract_incremental_ota.patch | 114 ++++++++++ oem_dump_partition.py | 83 ++++++++ 4 files changed, 828 insertions(+) create mode 100644 README.md create mode 100644 ble_companion_client.py create mode 100644 extract_incremental_ota.patch create mode 100644 oem_dump_partition.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..708fd9e --- /dev/null +++ b/README.md @@ -0,0 +1,248 @@ +# Oculus Quest Research + +## Updates + +All OTAs can be downloaded from the [`updates`](https://github.com/QuestEscape/updates) repository. You will also find there the original factory firmware. They can be extracted using the [`extract_android_ota_payload`](https://github.com/cyxx/extract_android_ota_payload) tool by [@cyxx](https://github.com/cyxx). + +For extracting the incremental OTAs, you will need to apply a poorly written patch of ours. It is available in this repository under the name `extract_incremental_ota.patch`. + +## Boot chain + +### EDL + +You can go into EDL mode by holding `Vol-`, `Vol+` and `Power`, or by using Fastboot. + +### ABL + +You can go into Fastboot mode by holding `Vol-` and `Power`, or by using ADB (if enabled). + +There are 3 known versions of ABL at time of writing: +- `213561.4150.0` +- `256550.6810.0` +- `333700.2680.0-396520.6170.115`. + +#### Commands + +The Oculus Quest has a few OEM-specific commands. ABL has been slightly modified to disallow the use of specific commands if the device state doesn't match some condition. Below is the complete list of the Fastboot commands. + +* DU = requires the device to be unlocked +* CR = requires the device to be critically unlocked +* RD = requires that the device is not a retail unit + +| Command | Requires | Notes | +| ------- | -------- | ----- | +| `continue` | - | - | +| `reboot` | - | - | +| `reboot-bootloader` | - | - | +| `oem device-info` | - | displays information about the device | +| `oem reboot-edl` | - | allows to reboot into emergency download mode | +| `oem reboot-sideload` | - | allows to reboot into sideloading mode | +| `oem shutdown` | - | shuts down the device | +| `getvar` | - | - | +| `oem sha1` | - | computes the hash of a partition | +| `oem unlock` | - | unlocks the device | +| `oem lock` | - | locks the device | +| `flash` | - | - | +| `erase` | - | - | +| `oem partition-info` | - | list the partitions | +| `boot` | DU or CU | - | +| `oem select-display-panel` | DU or CU | - | +| `oem set-verity` | DU or CU | enables/disables dmverity | +| `oem set-verified-boot` | DU or CU | enables/disables verified boot | +| `oem get-kernel-flavor` | DU or CU | get the kernel flavor | +| `set_active` | - | - | DU or CU | - | +| `oem update-all-slots` | DU or CU | - | +| `oem off-mode-charge` | CU | - | +| `oem enable-charger-screen` | CU | - | +| `oem disable-charger-screen` | CU | - | +| `oem set-retail-keymaster` | CU | enables/disables retail keymaster | +| `oem read-persist` | CU | reads the `private` partition | +| `oem write-persist` | CU | writes the `private` partition | +| `oem set-serial-number` | RD | changes the device serial number | +| `oem set-retail-device` | CU or RD | changes the device retail status | + +`flash` and `erase` are only allowed on a short list of partitions (if the device is not critically unlocked): +* `system` +* `boot` +* `userdata` +* `vision` (added in `256550.6810.0`) + +The `oem set-retail-keymaster` command was added in `333700.2680.0`. + +##### Oversight + +There was an oversight in the `oem sha1` command in versions `213561.4150.0` and `256550.6810.0`. This command takes two arguments: the partition name and a size. The second argument specifies how much data will be read and hashed. By specifying incremental sizes and brute-forcing the last byte each time, it is possible to dump a whole partition. + +Version `333700.2680.0` and later have a minimal size of 512 bytes, which prevents you from dumping a partition if you don't know what it begins with. + +We have implemented this process in the `oem_dump_partition.py` script (which makes use of the [PyUSB](https://github.com/pyusb/pyusb) library). In practice, dumping the n-th byte of a partition takes 3 times n seconds (most of the time is spent by the device). This greatly limits the partitions that can be dumped that way. + +#### Partitions + +Here is the list of partitions obtained using the `oem partition-info`: + +| Name | Lun | Start | End | Size | +| ---- | --- | ----- | --- | ---- | +| `ssd` | 0 | 6 | 7 | 1 | +| `persist` | 0 | 8 | 8199 | 8191 | +| `misc` | 0 | 8200 | 8455 | 255 | +| `keystore` | 0 | 8456 | 8583 | 127 | +| `frp` | 0 | 8584 | 8711 | 127 | +| `system_a` | 0 | 8712 | 664071 | 655359 | +| `system_b` | 0 | 664072 | 1319431 | 655359 | +| `private` | 0 | 1319432 | 1335815 | 16383 | +| `vision` | 0 | 1335816 | 1466887 | 131071 | +| `userdata` | 0 | 1466888 | 15161338 | 13694450 | +| `xbl_a` | 1 | 6 | 1018 | 1012 | +| `xbl_b` | 2 | 6 | 1018 | 1012 | +| `cdt` | 3 | 6 | 6 | 0 | +| `ddr` | 3 | 7 | 262 | 255 | +| `rpm_a` | 4 | 6 | 133 | 127 | +| `tz_a` | 4 | 134 | 645 | 511 | +| `hyp_a` | 4 | 646 | 773 | 127 | +| `pmic_a` | 4 | 774 | 901 | 127 | +| `modem_a` | 4 | 902 | 29061 | 28159 | +| `bluetooth_a` | 4 | 29062 | 29317 | 255 | +| `ovrtz_a` | 4 | 29318 | 33413 | 4095 | +| `abl_a` | 4 | 33414 | 33669 | 255 | +| `keymaster_a` | 4 | 33670 | 33797 | 127 | +| `boot_a` | 4 | 33798 | 50181 | 16383 | +| `cmnlib_a` | 4 | 50182 | 50309 | 127 | +| `cmnlib64_a` | 4 | 50310 | 50437 | 127 | +| `devcfg_a` | 4 | 50438 | 50469 | 31 | +| `rpm_b` | 4 | 50470 | 50597 | 127 | +| `tz_b` | 4 | 50598 | 51109 | 511 | +| `hyp_b` | 4 | 51110 | 51237 | 127 | +| `pmic_b` | 4 | 51238 | 51365 | 127 | +| `modem_b` | 4 | 51366 | 79525 | 28159 | +| `bluetooth_b` | 4 | 79526 | 79781 | 255 | +| `ovrtz_b` | 4 | 79782 | 83877 | 4095 | +| `abl_b` | 4 | 83878 | 84133 | 255 | +| `keymaster_b` | 4 | 84134 | 84261 | 127 | +| `boot_b` | 4 | 84262 | 100645 | 16383 | +| `cmnlib_b` | 4 | 100646 | 100773 | 127 | +| `cmnlib64_b` | 4 | 100774 | 100901 | 127 | +| `devcfg_b` | 4 | 100902 | 100933 | 31 | +| `sec` | 4 | 100934 | 100937 | 3 | +| `devinfo` | 4 | 100938 | 100938 | 0 | +| `dip` | 4 | 100939 | 101194 | 255 | +| `apdp` | 4 | 101195 | 101258 | 63 | +| `msadp` | 4 | 101259 | 101322 | 63 | +| `dpo` | 4 | 101323 | 101323 | 0 | +| `splash` | 4 | 101324 | 109679 | 8355 | +| `limits` | 4 | 109680 | 109680 | 0 | +| `toolsfv` | 4 | 109681 | 109936 | 255 | +| `logfs` | 4 | 109937 | 111984 | 2047 | +| `sti` | 4 | 111985 | 112496 | 511 | +| `logdump` | 4 | 112497 | 128880 | 16383 | +| `storsec` | 4 | 128881 | 128912 | 31 | +| `modemst1` | 5 | 6 | 517 | 511 | +| `modemst2` | 5 | 518 | 1029 | 511 | +| `fsg` | 5 | 1030 | 1541 | 511 | +| `fsc` | 5 | 1542 | 1542 | 0 | + +Offsets and sizes are in blocks of 4096 bytes each. + +### Unlocking + +Unlocking the device (in a legitimate way) is done by flashing the `unlock_token` partition. The data being flashed is made of two parts: a "bootloader script" and its signature (which strangely precedes it). + +The "bootloader signature" has the following format: +``` +01 :4 :unlock_serial_len +``` + +The `unlock_serial` field must of course match the device's. + +The signature is verified using RSA-PSS-SHA-256 and the following public key: +``` +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm+zQa4coLC8LhrK4mYpO +EyCDeTDhhgFp34sCHHklNRh9yZLEjv21XWN6VMTdg4oVAjNNPEvRsGD/AmeTDYh/ +g3sMHwWa7H5Plv77np+g9+ogIP/MMCr8OcBNmlmF4sg8RppIkqgqkA/ZJKQDZtEp +JHVeaYCx+llsbYVRXU2NpbQ0t40tuKyaDdze9tP8D1JppLzSaijTpcKmvDkPKerz +MT12Z0zV2Rvg8EdMOr+h/nQb36cMWhPewxyJoAKgcMhoWJiBiEpWO1hfAXt9//C7 +bODv7Ygo5CLCM5A49ZP+lHsgBv0Mf4GTCJGLwJ1wBFoy3Dtlxe0/Jlu2RlgUAI1q +TwIDAQAB +-----END PUBLIC KEY----- +``` + +## Companion System + +The companion is the Oculus application that you install on your Android or iOS smartphone. It communicates with the headset using Bluetooth Low Energy (BLE). It acts as a client, so there exists a server on the Quest (`CompanionServer.apk`). + +### System Applications + +The system applications are located in `/system/app` and `/system/priv-app` folders. They are only available in their "odexed" form, meaning that the `.apk` file only contains the resources, and that the byte-code is in the `.odex` file. + +We have used [smali](https://github.com/JesusFreke/smali) by [@JesusFreke](https://github.com/JesusFreke) to convert the `.odex` file to `.smali` files, and then [dex2jar](https://github.com/pxb1988/dex2jar) by [@pxb1988](https://github.com/pxb1988) to convert the `.smali` files to `.class` files (and regroup them into a single `.jar`). +``` +java -jar baksmali-2.3.jar deodex CompanionServer.odex -b boot.oat +java -jar smali-2.3.jar assemble out/ -o CompanionServer.dex +d2j-dex2jar.sh CompanionServer.dex -o CompanionServer.jar +``` + +Finally, the `.jar` file can be opened into [bytecode-viewer](https://github.com/Konloch/bytecode-viewer) by [@Konloch](https://github.com/), which includes 6 decompilers. We have found Procyon to work the best on this particular application, but having the ability to have multiple decompilers side-to-side, as well as the raw byte-code, makes reverse engineering a lot easier. + +### Bluetooh Low Energy + +#### GATT Server + +To communicate with the client, as well as the controllers, the server exposes a GATT service called `Companion` (UUID: `0000FEB8-0000-1000-8000-00805F9B34FB`). This service exposes two GATT characteristics: `ccs` (UUID: `7a442881-509c-47fa-ac02-b06a37d9eb76`) and `status` (UUID: `7a442666-509c-47fa-ac02-b06a37d9eb76`). Each of the them has a GATT descriptor called `Configuration` (UUID: `00002902-0000-1000-8000-00805f9b34fb`). + +A custom protocol is used between the client and the server. It uses the `ccs` characteristic: writing the value sends data, reading the value receives data. The protocol is composed of a transport layer, a presentation layer, and an application layer (that uses Protobuf). + +#### Transport Layer + +The transport layer goal is pretty simple: it makes data fit within the limited MTU by: + +- spliting data into chunks of size `mtu - 2` +- prefixed these chunks with an 2-byte header + - that contains a big endian sequence number + - whose high bit is set when it is the final chunk + +#### Authentication Layer + +The authentication layer protects the communications. On each new connection, the server generates a key pair that will be used to secure the channel. The client does the same. The two public keys are then exchanged during the `Hello` phase, and used to derive a secret key. It is this secret key that is used to encrypt/decrypt the later messages. + +The cryptography-related functions are located inside the `libauthentication.so` native library, which wraps the well-known crypto library [`libsodium`](https://github.com/jedisct1/libsodium). The standard functions are used, which facilitates writing a client. + +#### Application Layer + +The application layer defines a protocol that makes use of Protobuf serialized data. The packets from the client to the server are message of type `Request`, and the ones from the server to the client of type `Response`. + +The body of these messages can itself be Protobuf serialized data. For example, during the `Hello` phase, the client will send a `HelloRequest` message and the server will reply with a `HelloResponse` message that itself contains another message. + + +#### Messages + +Here is the list of methods implemented in version `256550.6810.0`: +``` +ADB_MODE_SET, ADB_MODE_STATUS, APP_LAUNCH, AUTHENTICATE, AUTOSLEEP_TIME_SET, +AUTOSLEEP_TIME_STATUS, AUTOWAKE_SET, AUTOWAKE_STATUS, CONTROLLER_PAIR, +CONTROLLER_SCAN, CONTROLLER_SCAN_AND_PAIR, CONTROLLER_SET_HANDEDNESS, +CONTROLLER_STATUS, CONTROLLER_UNPAIR, CONTROLLER_VERIFY_CONNECTABLE, +CRASH_REPORTS_ENABLED_SET, CRASH_REPORTS_ENABLED_STATUS, DEV_MODE_SET, +DEV_MODE_STATUS, HEALTH_AND_SAFETY_WARNING_SET, HELLO, HMD_CAPABILITIES, +HMD_STATUS, HMD_VERSION, LINE_FREQUENCY_SET, LINE_FREQUENCY_STATUS, LOCALE_SET, +MANAGED_MODE_SET, MANAGED_MODE_STATUS, MIRROR_REQUEST, MTP_MODE_SET, +MTP_MODE_STATUS, NAME_SET, NUX_COMPLETED, OCULUS_INSERT_LINKED_ACCOUNT, +OCULUS_LOGIN_DEPRECATED, OCULUS_LOGOUT, OCULUS_SET_ACCESS_TOKEN, +OCULUS_SET_USER_SECRET, OTA_ENABLED_SET, OTA_ENABLED_STATUS, PING, PIN_LOCK, +PIN_RESET, PIN_SET, PIN_STATUS, PIN_UNLOCK, PIN_VERIFY, SYSTEM_UNLOCK, +TEXT_SEND, TIME_SET, UNKNOWN, VERIFY_MULTIPLE_CONTROLLERS_CONNECTABLE, +WIFI_CONNECT, WIFI_DISABLE, WIFI_ENABLE, WIFI_FORGET, WIFI_RECONNECT, +WIFI_SCAN, WIFI_STATUS, WIPE_DATA +``` + +#### Implementation + +We have written a bare-bones client implementation in Python that uses the [Core Bluetooth](https://developer.apple.com/documentation/corebluetooth) framework of macOS via the [`PyObjC`](https://bitbucket.org/ronaldoussoren/pyobjc/) bridge. You can find the client in this repository under the name `ble_companion_client.py`. + +## Kernel + +The kernel used by Oculus Quest was vulnerable to CVE-2018-9568 up to version `256550.6810.0` ([this commit](https://github.com/facebookincubator/oculus-linux-kernel/commit/589280fc40ddbcc2287024c8b672568a0fdd68e7#diff-56c7c22bc6dcdc2c4ff303ab61738ff2R1526) fixes the vulnerability). An exploit for it should be available in the [`exploit`](https://github.com/QuestEscape/exploit) repository. + +## Miscellaneous + +The internal server used to generate unlock codes is located at [https://our.internmc.facebook.com/intern/oculus/oem_unlock](https://our.internmc.facebook.com/intern/oculus/oem_unlock). diff --git a/ble_companion_client.py b/ble_companion_client.py new file mode 100644 index 0000000..db819d0 --- /dev/null +++ b/ble_companion_client.py @@ -0,0 +1,383 @@ +import struct +import sys +import threading + +from Foundation import * +from PyObjCTools import AppHelper + +from protobuf3.message import Message +from protobuf3.fields import UInt32Field, EnumField, BytesField, BoolField +from enum import Enum + +from nacl.public import PrivateKey, PublicKey, Box +from nacl.utils import random + +from IPython.terminal.embed import InteractiveShellEmbed + +class Method(Enum): + ADB_MODE_SET = 6005 + ADB_MODE_STATUS = 6006 + APP_LAUNCH = 7001 + AUTHENTICATE = 2 + AUTOSLEEP_TIME_SET = 6013 + AUTOSLEEP_TIME_STATUS = 6014 + AUTOWAKE_SET = 6011 + AUTOWAKE_STATUS = 6012 + CONTROLLER_PAIR = 3002 + CONTROLLER_SCAN = 3001 + CONTROLLER_SCAN_AND_PAIR = 3006 + CONTROLLER_SET_HANDEDNESS = 3005 + CONTROLLER_STATUS = 3003 + CONTROLLER_UNPAIR = 3004 + CONTROLLER_VERIFY_CONNECTABLE = 3007 + CRASH_REPORTS_ENABLED_SET = 6009 + CRASH_REPORTS_ENABLED_STATUS = 6010 + DEV_MODE_SET = 6001 + DEV_MODE_STATUS = 6002 + HEALTH_AND_SAFETY_WARNING_SET = 9101 + HELLO = 1 + HMD_CAPABILITIES = 8003 + HMD_STATUS = 8001 + HMD_VERSION = 8002 + LINE_FREQUENCY_SET = 6016 + LINE_FREQUENCY_STATUS = 6017 + LOCALE_SET = 9001 + MANAGED_MODE_SET = 11001 + MANAGED_MODE_STATUS = 11002 + MIRROR_REQUEST = 10001 + MTP_MODE_SET = 6003 + MTP_MODE_STATUS = 6004 + NAME_SET = 6015 + NUX_COMPLETED = 9201 + OCULUS_INSERT_LINKED_ACCOUNT = 2101 + OCULUS_LOGIN_DEPRECATED = 2001 + OCULUS_LOGOUT = 2002 + OCULUS_SET_ACCESS_TOKEN = 2004 + OCULUS_SET_USER_SECRET = 2003 + OTA_ENABLED_SET = 6007 + OTA_ENABLED_STATUS = 6008 + PING = 0 + PIN_LOCK = 4003 + PIN_RESET = 4006 + PIN_SET = 4001 + PIN_STATUS = 4002 + PIN_UNLOCK = 4004 + PIN_VERIFY = 4005 + SYSTEM_UNLOCK = 9301 + TEXT_SEND = 12001 + TIME_SET = 9002 + UNKNOWN = 99999 + VERIFY_MULTIPLE_CONTROLLERS_CONNECTABLE = 3008 + WIFI_CONNECT = 1002 + WIFI_DISABLE = 1006 + WIFI_ENABLE = 1005 + WIFI_FORGET = 1004 + WIFI_RECONNECT = 1007 + WIFI_SCAN = 1001 + WIFI_STATUS = 1003 + WIPE_DATA = 5001 + +class Request(Message): + version = UInt32Field(field_number=1) + method = EnumField(field_number=2, enum_cls=Method) + seq = UInt32Field(field_number=3) + body = BytesField(field_number=4) + +class ResponseCode(Enum): + SUCCESS = 0 + FAIL = 1 + FAIL_RETRY = 2 + +class Response(Message): + seq = UInt32Field(field_number=1) + code = EnumField(field_number=2, enum_cls=ResponseCode) + body = BytesField(field_number=3) + +class ErrorCode(Enum): + ALREADY_IN_PROGRESS = 7 + APP_LAUNCH_ERROR = 501 + APP_NOT_INSTALLED = 502 + AUTHENTICATION_FAILURE = 5 + BAD_ACCESS_TOKEN = 13 + BAD_ARGUEMENT = 4 + BAD_LOCK_PIN = 401 + BAD_PERIPHERAL_ADDRESS = 31 + BAD_PERIPHERAL_DEVICE = 30 + BAD_REQUEST = 1 + BATTERY_TOO_LOW = 3 + CONTROLLER_BLOCKED_BY_UPDATE = 604 + CONTROLLER_INTERNAL_ERROR = 603 + CONTROLLER_PAIR_FAILED = 601 + CONTROLLER_PAIR_REQUIRED = 602 + DEVICE_BLE_ERROR = 15 + DEVICE_WIFI_ERROR = 14 + PIN_LOCK_NOT_SET = 402 + TIMED_OUT = 6 + TOO_MANY_PIN_TRIES = 403 + UNKNOWN_ERROR = 0 + UNSUPPORTED_METHOD = 2 + USER_PIN_REQUIRED = 8 + WIFI_AUTH_TIMEOUT = 17 + WIFI_INVALID_AUTH = 12 + WIFI_IP_CONFIG_FAIL = 18 + WIFI_NO_INTERNET = 16 + WIFI_NO_NETWORK = 11 + +class ErrorDetails(Message): + code = EnumField(field_number=1, enum_cls=ErrorCode) + debugDetails = BytesField(field_number=2) + localizedUserFacingDescription = BytesField(field_number=3) + +class HelloRequest(Message): + clientPublicKey = BytesField(field_number=1) + clientChallenge = BytesField(field_number=2) + knownCertFingerprint = BytesField(field_number=3) + +class HelloSignedData(Message): + serverPublicKey = BytesField(field_number=1) + authenticationChallenge = BytesField(field_number=2) + deviceNeedsToBeUnlocked = BoolField(field_number=3) + +class HelloResponse(Message): + signedData = BytesField(field_number=1) + signature = BytesField(field_number=2) + serverCertificate = BytesField(field_number=3) + +class AppLaunchRequest(Message): + appId = BytesField(field_number=1) + packageName = BytesField(field_number=2) + +class DevModeRequest(Message): + mode = UInt32Field(field_number=1) + +class DevModeResponse(Message): + status = UInt32Field(field_number=1) + +class OtaEnabledRequest(Message): + enable = BoolField(field_number=1) + +class OtaEnabledResponse(Message): + enabled = BoolField(field_number=1) + +class State(Enum): + STATE_INIT = 0 + EXCHANGE_HELLO = 1 + CHALLENGE_RESPONSE = 2 + WAIT_FOR_COMMAND = 3 + +class BleModule(object): + COMPANION_DEVICE_UUID = '7A1FAD2E-AA0E-4840-8E48-AF278FA86911' + COMPANION_CCS_UUID = '7A442881-509C-47FA-AC02-B06A37D9EB76' + COMPANION_STATUS_UUID = '7A442666-509C-47FA-AC02-B06A37D9EB76' + + def centralManagerDidUpdateState_(self, manager): + self.manager = manager + manager.scanForPeripheralsWithServices_options_(None, None) + + def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(self, manager, peripheral, data, rssi): + if BleModule.COMPANION_DEVICE_UUID in repr(peripheral.UUID): + self.peripheral = peripheral + manager.connectPeripheral_options_(peripheral, None) + manager.stopScan() + + def centralManager_didConnectPeripheral_(self, manager, peripheral): + peripheral.setDelegate_(self) + peripheral.discoverServices_([]) + + def peripheral_didDiscoverServices_(self, peripheral, services): + service = peripheral.services()[0] + peripheral.discoverCharacteristics_forService_([], service) + + def peripheral_didDiscoverCharacteristicsForService_error_(self, peripheral, service, error): + for characteristic in service.characteristics(): + if BleModule.COMPANION_CCS_UUID in repr(characteristic.UUID): + self.ccs = characteristic + self.recv_message(b"") + if BleModule.COMPANION_STATUS_UUID in repr(characteristic.UUID): + self.status = characteristic + + def peripheral_didUpdateValueForCharacteristic_error_(self, peripheral, characteristic, error): + value = characteristic.value() + if value not in [b'\xff', b'----']: + self.recv_transport(value) + peripheral.readValueForCharacteristic_(characteristic) + + def peripheral_didWriteValueForCharacteristic_error_(self, peripheral, characteristic, error): + peripheral.readValueForCharacteristic_(characteristic) + + def send_ble_module(self, data): + # print(">", data) + value = NSData.dataWithBytes_length_(data, len(data)) + self.peripheral.writeValue_forCharacteristic_type_(value, self.ccs, 0) + +class CompanionClient(BleModule): + def __init__(self): + super(CompanionClient, self).__init__() + + self.mtu = 20 + self.value = bytearray() + self.prev_seq = -1 + + self.secure = False + self.pub_key = None + self.priv_key = PrivateKey.generate() + self.box = None + + self.seq = 0 + self.state = State.STATE_INIT + self.handler = self.handle_not_implemented + + def recv_transport(self, data): + # print("<", data) + seq = struct.unpack('>H', data[:2])[0] + self.value.extend(data[2:]) + assert seq & 0x7fff == self.prev_seq + 1 + + resp = None + if seq & 0x8000: + resp = bytes(self.value) + self.recv_authentication(resp) + self.value.clear() + self.prev_seq = -1 + else: + self.prev_seq = seq + + def send_transport(self, data): + chunks = [] + seq = 0 + for off in range(0, len(data), self.mtu - 2): + size = min(self.mtu - 2, len(data) - off) + header = struct.pack('>H', seq) + chunks.append(header + data[off:off + size]) + seq += 1 + + last = bytearray(chunks[-1]) + last[0] |= last[0] | 0x80 + chunks[-1] = bytes(last) + + for chunk in chunks: + self.send_ble_module(chunk) + + def recv_authentication(self, data): + if self.secure: + data = self.box.decrypt(data) + self.recv_message(data) + + def send_authentication(self, data): + if self.secure: + data = self.box.encrypt(data) + self.send_transport(data) + + def recv_message(self, data): + resp = Response() + resp.parse_from_bytes(data) + + if self.state == State.STATE_INIT: + self.send_hello_request() + self.state = State.EXCHANGE_HELLO + + elif self.state == State.EXCHANGE_HELLO: + self.recv_hello_response(resp.body) + self.state = State.WAIT_FOR_COMMAND + + elif self.state == State.WAIT_FOR_COMMAND: + if self.handler: + self.handler(resp.code, resp.body) + + def send_message(self, method, body=None, handler=None): + if handler: + self.handler = handler + else: + self.handler = self.handle_not_implemented + + req = Request() + req.version = 0 + req.method = method + req.seq = self.seq + self.seq += 1 + if body: + req.body = body.encode_to_bytes() + self.send_authentication(req.encode_to_bytes()) + + def send_hello_request(self): + hello = HelloRequest() + hello.clientPublicKey = self.priv_key.public_key.encode() + hello.clientChallenge = b"\x00" + self.send_message(Method.HELLO, hello) + + def recv_hello_response(self, body): + hello = HelloResponse() + hello.parse_from_bytes(body) + + signed_data = HelloSignedData() + signed_data.parse_from_bytes(hello.signedData) + + self.secure = True + self.pub_key = PublicKey(signed_data.serverPublicKey) + self.box = Box(self.priv_key, self.pub_key) + + # please forgive me for writing this monstrosity + shell = InteractiveShellEmbed() + threading.Thread(target=shell, kwargs={"local_ns": {"client": self}}).start() + + def handle_not_implemented(self, code, body): + if code == ResponseCode.SUCCESS: + print(code) + print(body) + else: + details = ErrorDetails() + details.parse_from_bytes(body) + print(details.code) + if details.debugDetails: + print(details.debugDetails.decode('utf-8')) + if details.localizedUserFacingDescription: + print(details.localizedUserFacingDescription.decode('utf-8')) + + def ping(self): + def handler(code, body): + print("Pong!") + self.send_message(Method.PING, handler=handler) + + def launch_app(self, appId, packageName): + def handler(code, body): + print("Success") + req = AppLaunchRequest() + req.appId = appId + req.packageName = packageName + self.send_message(Method.APP_LAUNCH, req, handler=handler) + + def dev_mode_status(self): + def handler(code, body): + resp = DevModeResponse() + resp.parse_from_bytes(body) + print("Status: %d" % resp.status) + self.send_message(Method.DEV_MODE_STATUS, handler=handler) + + def dev_mode_set(self, mode): + def handler(code, body): + print("Success") + req = DevModeRequest() + req.mode = mode + self.send_message(Method.DEV_MODE_SET, req, handler=handler) + + def ota_enabled_status(self): + def handler(code, body): + resp = OtaEnabledResponse() + resp.parse_from_bytes(body) + print("Enabled: %s" % resp.enabled) + self.send_message(Method.OTA_ENABLED_STATUS, handler=handler) + + def ota_enabled_set(self, enable): + def handler(code, body): + print("Success") + req = OtaEnabledRequest() + req.enable = enable + self.send_message(Method.OTA_ENABLED_SET, req, handler=handler) + +if __name__ == '__main__': + try: + central_manager = CBCentralManager.alloc() + central_manager.initWithDelegate_queue_options_(CompanionClient(), None, None) + AppHelper.runConsoleEventLoop() + except KeyboardInterrupt: + sys.exit() diff --git a/extract_incremental_ota.patch b/extract_incremental_ota.patch new file mode 100644 index 0000000..f51e389 --- /dev/null +++ b/extract_incremental_ota.patch @@ -0,0 +1,114 @@ +diff --git a/extract_android_ota_payload.py b/extract_android_ota_payload.py +index 7688ecd..6c0b593 100644 +--- a/extract_android_ota_payload.py ++++ b/extract_android_ota_payload.py +@@ -1,5 +1,6 @@ + #!/usr/bin/env python + ++import argparse + import hashlib + import os + import os.path +@@ -87,7 +88,7 @@ def decompress_payload(command, data, size, hash): + print("Hash mismatch") + return r + +-def parse_payload(payload_f, partition, out_f): ++def parse_payload(payload_f, partition, out_f, block_size, output_dir, source_dir): + BLOCK_SIZE = 4096 + for operation in partition.operations: + e = operation.dst_extents[0] +@@ -101,10 +102,57 @@ def parse_payload(payload_f, partition, out_f): + elif operation.type == update_metadata_pb2.InstallOperation.REPLACE_BZ: + r = decompress_payload('bzcat', data, e.num_blocks * BLOCK_SIZE, operation.data_sha256_hash) + out_f.write(r) ++ elif operation.type in [ ++ update_metadata_pb2.InstallOperation.SOURCE_COPY, ++ update_metadata_pb2.InstallOperation.SOURCE_BSDIFF ++ ]: ++ src_blocks = [] ++ for src_extent in operation.src_extents: ++ src_blocks.extend(range(src_extent.start_block, src_extent.start_block + src_extent.num_blocks)) ++ ++ dst_blocks = [] ++ for dst_extent in operation.dst_extents: ++ dst_blocks.extend(range(dst_extent.start_block, dst_extent.start_block + dst_extent.num_blocks)) ++ ++ name = partition.partition_name + '.img' ++ ++ if operation.type == update_metadata_pb2.InstallOperation.SOURCE_COPY: ++ with open(os.path.join(source_dir, name)) as in_f: ++ for src_block, dst_block in zip(src_blocks, dst_blocks): ++ in_f.seek(src_block * block_size) ++ out_f.seek(dst_block * block_size) ++ out_f.write(in_f.read(block_size)) ++ ++ elif operation.type == update_metadata_pb2.InstallOperation.SOURCE_BSDIFF: ++ src_data = bytearray() ++ with open(os.path.join(source_dir, name)) as in_f: ++ for src_block in src_blocks: ++ in_f.seek(src_block * block_size) ++ src_data.extend(in_f.read(block_size)) ++ ++ src = os.path.join(output_dir, name + '.src') ++ with open(src, 'wb') as src_f: ++ src_f.write(src_data) ++ ++ patch = os.path.join(output_dir, name + '.patch') ++ with open(patch, 'wb') as patch_f: ++ patch_f.write(data) ++ ++ dst = os.path.join(output_dir, name + '.dst') ++ subprocess.call(['bspatch', src, dst, patch]) ++ ++ with open(dst, 'rb') as dst_f: ++ for dst_block in dst_blocks: ++ out_f.seek(dst_block * block_size) ++ out_f.write(dst_f.read(block_size)) ++ ++ os.remove(src) ++ os.remove(patch) ++ os.remove(dst) + else: + raise PayloadError('Unhandled operation type (%d)' % operation.type) + +-def main(filename, output_dir): ++def main(filename, output_dir, source_dir): + if filename.endswith('.zip'): + print("Extracting 'payload.bin' from OTA file...") + ota_zf = zipfile.ZipFile(filename) +@@ -121,25 +169,20 @@ def main(filename, output_dir): + fname = os.path.join(output_dir, name) + out_f = open(fname, 'w') + try: +- parse_payload(payload, p, out_f) ++ parse_payload(payload, p, out_f, payload.manifest.block_size, output_dir, source_dir) + except PayloadError as e: + print('Failed: %s' % e) + out_f.close() + os.unlink(fname) + + if __name__ == '__main__': +- try: +- filename = sys.argv[1] +- except: +- print('Usage: %s payload.bin [output_dir]' % sys.argv[0]) +- sys.exit() ++ parser = argparse.ArgumentParser() ++ parser.add_argument('filename') ++ parser.add_argument('-o', '--output_dir', default=os.getcwd()) ++ parser.add_argument('-s', '--source_dir') ++ args = parser.parse_args() + +- try: +- output_dir = sys.argv[2] +- except IndexError: +- output_dir = os.getcwd() ++ if not os.path.exists(args.output_dir): ++ os.makedirs(args.output_dir) + +- if not os.path.exists(output_dir): +- os.makedirs(output_dir) +- +- main(filename, output_dir) ++ main(args.filename, args.output_dir, args.source_dir) diff --git a/oem_dump_partition.py b/oem_dump_partition.py new file mode 100644 index 0000000..0806f40 --- /dev/null +++ b/oem_dump_partition.py @@ -0,0 +1,83 @@ +import argparse +import hashlib +import os + +import usb.core +import usb.util + + +class Device(object): + def __init__(self): + self.buf = bytearray() + self.pos = 0 + + def connect(self, vid, pid): + self.usb = usb.core.find(idVendor=vid, idProduct=pid) + if self.usb.is_kernel_driver_active(0): + self.act = True + self.usb.detach_kernel_driver(0) + else: + self.act = False + self.ep_in = self.usb[0][(0, 0)][0] + self.ep_out = self.usb[0][(0, 0)][1] + + def disconnect(self): + usb.util.dispose_resources(self.usb) + if self.act: + self.usb.attach_kernel_driver(0) + + def recv(self, length): + while self.pos + length > len(self.buf): + if self.pos > 0: + self.buf = self.buf[self.pos:] + self.pos = 0 + self.buf.extend(self.ep_in.read(512)) + self.pos += length + return self.buf[self.pos - length:self.pos] + + def send(self, buffer): + self.ep_out.write(buffer) + + +def main(args): + dev = Device() + dev.connect(0x2833, 0x0081) + + bs = bytearray() + hh = hashlib.sha1() + + if os.path.isfile(args.name + ".bin"): + with open(args.name + ".bin", "rb") as f: + bs = bytearray(f.read()) + hh.update(bytes(bs)) + + for block in range(len(bs) // 4096, args.size + 1): + for index in range(4096): + offset = block * 4096 + index + 1 + dev.send(b"oem sha1 %s %d" % (args.name.encode("utf-8"), offset)) + expected = dev.recv(48)[4:-4].decode("utf-8") + + found = -1 + for n in range(256): + h = hh.copy() + h.update(bytes(bytearray([n]))) + h = h.hexdigest() + if h.upper() == expected: + found = n + break + assert found >= 0, "something went wrong" + + bs.append(found) + hh.update(bytes(bytearray([found]))) + + with open(args.name + ".bin", "wb") as f: + f.write(bs) + + dev.disconnect() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("name", type=str) + parser.add_argument("size", type=int) + main(parser.parse_args())