BM200 protocol findings and test bench
Spec with pseudocode and test vectors, APK disassembly, analysis tools, shelved reference Python and the macOS BLE probe. Findings are not yet validated against a real tag. The vendor XAPK and nested lib clone are excluded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EinWd3UT2Xw7UakFtTp6hn
This commit is contained in:
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Vendor app package
|
||||
*.xapk
|
||||
*.apk
|
||||
|
||||
# Separate git repo (upstream clone with its own bm200-protocol branch)
|
||||
/bm2-battery-monitor/
|
||||
|
||||
# Test bench output and Python leftovers
|
||||
probe-*.jsonl
|
||||
.venv/
|
||||
__pycache__/
|
||||
.DS_Store
|
||||
273
BM200_PROTOCOL.md
Normal file
273
BM200_PROTOCOL.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# ANCEL BM200 BLE protocol: implementation spec
|
||||
|
||||
Language-neutral spec for capturing live readings and backfilling history from an ANCEL BM200, with no vendor app or cloud.
|
||||
|
||||
**Source:** bytecode of `com.dc.bm6_ancel` 2.0.6 (BM6 protocol family). **Not yet tested against a real tag.** The test vectors check an implementation against this reading of the app; they are not captures. See [Unverified](#unverified) before relying on it.
|
||||
|
||||
The existing `BM2Client` in this repo does not work with this device: the key, frame format and startup all differ.
|
||||
|
||||
## 1. Transport
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Advertised name | `BM6` or `Battery Guard` |
|
||||
| Service | `0000fff0-0000-1000-8000-00805f9b34fb` |
|
||||
| Write commands | `0000fff3-0000-1000-8000-00805f9b34fb` |
|
||||
| Notifications | `0000fff4-0000-1000-8000-00805f9b34fb` |
|
||||
|
||||
The relay must hold a GATT connection and write commands. Readings do not come from passive scanning: the app only uses advertisements to detect the tag (iBeacon prefix `02 15`) and then connects.
|
||||
|
||||
## 2. Crypto
|
||||
|
||||
```
|
||||
KEY = 6c 65 61 67 65 6e 64 ff fe 30 31 30 30 30 30 39 // "leagend" FF FE "0100009"
|
||||
IV = 16 zero bytes
|
||||
AES-128-CBC, no padding
|
||||
|
||||
encrypt(cmd):
|
||||
return AES_CBC_ENCRYPT(KEY, IV, right_pad_with_zeros(cmd, 16))
|
||||
|
||||
decrypt(frame):
|
||||
// IV resets for every block
|
||||
if length(frame) == 0 or length(frame) % 16 != 0: reject frame
|
||||
return concat(AES_CBC_DECRYPT(KEY, IV, block) for each 16-byte block of frame)
|
||||
```
|
||||
|
||||
## 3. Commands
|
||||
|
||||
| Name | Plaintext | Encrypted bytes written to FFF3 |
|
||||
|---|---|---|
|
||||
| VERSION | `d15501` | `1ad6f05e4990ab24b84d57df81d8766e` |
|
||||
| ACCEPT | `d1550200` | `9cb18029774c5cfc57c9f114d40db4e8` |
|
||||
| LIVE | `d15507` | `697ea0b5d54cf024e794772355554114` |
|
||||
| HISTORY(10) | `d1550500000000000a` | `bb43a7f6b8c3454a614c83b9366a0b47` |
|
||||
| HISTORY(22320) | `d15505000000005730` | `8e8dd229b057e0567f1c7e0ef0a0ec11` |
|
||||
| HISTORY(51840) | `d1550500000000ca80` | `965d8b8fad4794162ccee2d35a44baa4` |
|
||||
|
||||
`HISTORY(n)` is `d1 55 05 00 00 00` followed by `n` as 3 bytes, big-endian.
|
||||
|
||||
Do not send:
|
||||
|
||||
- `d1550211`: the app sends this when its cloud check rejects a tag as not genuine.
|
||||
- `d15508xx`: this writes battery type to the tag. The app sends it, but it changes tag configuration, so this spec leaves it out.
|
||||
|
||||
## 4. Session
|
||||
|
||||
```
|
||||
on_connect:
|
||||
subscribe FFF4
|
||||
decoder = new Decoder()
|
||||
|
||||
write VERSION; wait for Version (timeout 10 s)
|
||||
write ACCEPT; wait for AcceptAck (timeout 10 s)
|
||||
|
||||
// The app always does this short read first and discards the result.
|
||||
write HISTORY(10); wait for HistoryBlock (timeout 10 s)
|
||||
|
||||
write HISTORY(history_count(since))
|
||||
block = wait for HistoryBlock (timeout 10 min)
|
||||
for record in block.records: // oldest first
|
||||
if since is null or record.time > since:
|
||||
deliver(record)
|
||||
since = record.time
|
||||
persist(since) // only after delivery
|
||||
|
||||
write LIVE
|
||||
until disconnected or no frame for 120 s:
|
||||
deliver each LiveReading as it arrives
|
||||
|
||||
on_disconnect or timeout:
|
||||
wait 5 s, then reconnect
|
||||
|
||||
history_count(since):
|
||||
if since is null: return MAX_RECORDS
|
||||
missing = floor((now - since) / 120 seconds)
|
||||
return min(max(missing, 0) + 10, MAX_RECORDS) // +10 overlap, as in the app
|
||||
```
|
||||
|
||||
`MAX_RECORDS`: the app uses 22320 (31 days) or 51840 (72 days, for one device variant only). Start with 22320.
|
||||
|
||||
The timeouts and the reconnect delay are this spec's choices, not app values. The 10-minute history timeout is a guess based on transfer size: 22320 records is about 5,600 notifications.
|
||||
|
||||
`since` advances only from history records, never from live readings. A gap is then backfilled even if some live frames got through during it.
|
||||
|
||||
The app also requests crank data (`D15503`) between history and live. This spec skips that because it is out of scope.
|
||||
|
||||
## 5. Decoder
|
||||
|
||||
Each notification is decoded on its own, except history, which spans many frames. Feed frames in arrival order. For a remote relay, record `received_at` on the relay, because history timestamps are derived from it.
|
||||
|
||||
```
|
||||
state: history_buffer = null, history_base = 0
|
||||
|
||||
feed(frame, received_at):
|
||||
h = uppercase_hex(decrypt(frame)) // offsets below are hex-digit positions
|
||||
|
||||
// Live frames can arrive in the middle of a history transfer.
|
||||
if h starts with "D15507FF": return nothing // ack
|
||||
if h starts with "D15507": return parse_live(h, received_at)
|
||||
|
||||
if history_buffer is not null:
|
||||
if h contains "FEFEFE": return nothing // app discards these frames
|
||||
history_buffer += h
|
||||
end = index_of(history_buffer, "FFFEFE") // any digit alignment, as in the app
|
||||
if end < 0: return nothing
|
||||
data = history_buffer[0 .. end)
|
||||
history_buffer = null
|
||||
return HistoryBlock(parse_history(data, history_base))
|
||||
|
||||
if h starts with "D15501":
|
||||
return Version(hex(h[6..8)) + hex(h[8..10)) / 10)
|
||||
if h starts with "D15502":
|
||||
return AcceptAck(hex(h[6..8)))
|
||||
if h starts with "D15505":
|
||||
if hex(h[6..10)) == 0: return HistoryBlock(empty)
|
||||
history_buffer = ""
|
||||
history_base = history_base_time(received_at)
|
||||
return nothing
|
||||
|
||||
return nothing // unknown: log it, don't fail
|
||||
```
|
||||
|
||||
### Live frame
|
||||
|
||||
```
|
||||
parse_live(h, received_at):
|
||||
magnitude = hex(h[8..10))
|
||||
return LiveReading:
|
||||
time = received_at
|
||||
temperature = -magnitude if hex(h[6..8)) == 1 else magnitude
|
||||
status = hex(h[10..12))
|
||||
soc = hex(h[12..14))
|
||||
voltage = hex(h[14..18)) / 100
|
||||
rapid_acceleration = hex(h[18..22))
|
||||
rapid_deceleration = hex(h[22..26))
|
||||
```
|
||||
|
||||
| Byte | Field | Notes |
|
||||
|---|---|---|
|
||||
| 0-2 | `D1 55 07` | |
|
||||
| 3 | temperature sign | 1 = negative |
|
||||
| 4 | temperature | unit assumed to be °C |
|
||||
| 5 | status | 2 = engine running; store other values raw |
|
||||
| 6 | state of charge | percent |
|
||||
| 7-8 | voltage | big-endian, ÷100 |
|
||||
| 9-10 | rapid acceleration count | big-endian |
|
||||
| 11-12 | rapid deceleration count | big-endian |
|
||||
|
||||
### History
|
||||
|
||||
Records are 4 bytes, 8 hex digits: `VVV SS TT F`. Record 0 is the **newest**, and records are 2 minutes apart. They carry no timestamp, so times are derived from when the transfer starts.
|
||||
|
||||
```
|
||||
history_base_time(t):
|
||||
base = t floored to the whole minute
|
||||
if local_minute(base) is even: base -= 60 seconds
|
||||
return base
|
||||
|
||||
parse_history(data, base):
|
||||
while data starts with "00000000": data = data[8..] // see note below
|
||||
|
||||
records = []; pos = 0; index = 0
|
||||
while pos + 8 <= length(data):
|
||||
item = data[pos .. pos+8); pos += 8
|
||||
flags = hex(item[7])
|
||||
mark = "start" if (flags & 7) == 2
|
||||
"stop" if (flags & 7) == 4
|
||||
null otherwise
|
||||
if mark == "stop": pos += 8 // 4 extra bytes: rapid accel, rapid decel (2 bytes each)
|
||||
|
||||
voltage = hex(item[0..3)) / 100
|
||||
if voltage != 0: // zero slots are dropped but still use a time slot
|
||||
magnitude = hex(item[5..7))
|
||||
records.append(
|
||||
time = base - index * 120 seconds,
|
||||
voltage = voltage,
|
||||
soc = hex(item[3..5)),
|
||||
temperature = -magnitude if (flags & 8) != 0 else magnitude,
|
||||
mark = mark)
|
||||
index += 1
|
||||
|
||||
return reverse(records) // oldest first
|
||||
```
|
||||
|
||||
Things to design for:
|
||||
|
||||
- **Timestamps are approximate.** They assume the newest record was taken at the base time, so the same record can come out a slot different between reads. Do not use `(time, voltage)` as a strict unique key. Filter with `since`.
|
||||
- **Leading zero records.** This spec strips all of them. The app's loop has a quirk that can also skip real records after them, which is not reproduced here.
|
||||
- **The tag's clock is not used.** A tag that was out of range for longer than its history depth has an unrecoverable gap.
|
||||
|
||||
## 6. Expected outputs
|
||||
|
||||
Assume `received_at = 1700000000` (2023-11-14T22:13:20Z) and timezone UTC unless stated. Each case starts with a new decoder, and frames are fed in order. Floats are shown as decimal values.
|
||||
|
||||
### Single frames
|
||||
|
||||
| Case | Encrypted frame | Plaintext | Expected |
|
||||
|---|---|---|---|
|
||||
| Version | `13e4a294119b956c21350446e9fe3527` | `d15501010700…` | `Version{version: 1.7}` |
|
||||
| Accept ack | `9cb18029774c5cfc57c9f114d40db4e8` | `d1550200…` | `AcceptAck{result: 0}` |
|
||||
| Live ack | `8ae0358523fe154976de18fc102eb1d4` | `d15507ff…` | nothing |
|
||||
| Empty history | `91c83073bb2dddcd6544ee0650ad075b` | `d155050000…` | `HistoryBlock{records: []}` |
|
||||
| Live | `e3e117645ad018edcbeb11c769a15326` | `d155070019025505080003000400…` | see below |
|
||||
| Live, negative temperature | `9889e1f5002c3594e26a8a87317004ea` | `d155070105025505080003000400…` | same, with `temperature: -5` |
|
||||
| 15 bytes of `00` | | | rejected, no state change |
|
||||
|
||||
```json
|
||||
{"time": "2023-11-14T22:13:20Z", "voltage": 12.88, "soc": 85, "temperature": 25, "status": 2, "rapid_acceleration": 3, "rapid_deceleration": 4}
|
||||
```
|
||||
|
||||
### History, with a live frame in the middle
|
||||
|
||||
| # | Encrypted frame | Plaintext | Expected |
|
||||
|---|---|---|---|
|
||||
| 1 | `46f6b39dec35c6faa5a92df16e2b7026` | `d155050003…` | nothing |
|
||||
| 2 | `0c3f001bd08004a48534d5831b249422` | `00000000 50055194 00030004 4f050140` | nothing |
|
||||
| 3 | `6adc3339e32094bf1b6e3837daf1ee04` | `d155070019005505080000000000…` | `LiveReading` with `status: 0`, `rapid_acceleration: 0`, `rapid_deceleration: 0`, otherwise as above |
|
||||
| 4 | `18e26d814ecb10ef977633c9fa77808b` | `4e0000aa fffefe00…` | block below |
|
||||
|
||||
```json
|
||||
{"records": [
|
||||
{"time": "2023-11-14T22:09:00Z", "voltage": 12.48, "soc": 0, "temperature": -10, "mark": "start"},
|
||||
{"time": "2023-11-14T22:11:00Z", "voltage": 12.64, "soc": 80, "temperature": 20, "mark": null},
|
||||
{"time": "2023-11-14T22:13:00Z", "voltage": 12.80, "soc": 85, "temperature": 25, "mark": "stop"}
|
||||
]}
|
||||
```
|
||||
|
||||
### Base time on an even minute
|
||||
|
||||
`received_at = 1700000040` (22:14:00Z).
|
||||
|
||||
| # | Encrypted frame | Plaintext | Expected |
|
||||
|---|---|---|---|
|
||||
| 1 | `2e6a4e19b27a6a3332696e9d43e6a916` | `d155050001…` | nothing |
|
||||
| 2 | `44b1770f090e4520f8e324ed87101c66` | `50055190 fffefe00…` | one record, `time: 2023-11-14T22:13:00Z`, `voltage: 12.80`, `soc: 85`, `temperature: 25`, `mark: null` |
|
||||
|
||||
### `FEFEFE` frame is discarded
|
||||
|
||||
`received_at = 1700000000`.
|
||||
|
||||
| # | Encrypted frame | Plaintext | Expected |
|
||||
|---|---|---|---|
|
||||
| 1 | `2e6a4e19b27a6a3332696e9d43e6a916` | `d155050001…` | nothing |
|
||||
| 2 | `7e5cb931800dbc0c789ccd531167dc82` | `fefefe00…` | nothing |
|
||||
| 3 | `44b1770f090e4520f8e324ed87101c66` | `50055190 fffefe00…` | one record, same values as the previous case |
|
||||
|
||||
### Session order
|
||||
|
||||
A compliant session writes exactly this to FFF3 when `since` is null. Replying to each write with a scripted fake tag is an easy test:
|
||||
|
||||
```
|
||||
VERSION, ACCEPT, HISTORY(10), HISTORY(22320), LIVE
|
||||
```
|
||||
|
||||
## Unverified
|
||||
|
||||
Check these on the first hardware session, logging raw frames alongside decoded values:
|
||||
|
||||
1. **Whether the skipped steps matter.** The tag may require the battery-type write or the crank read before it streams live frames. If `LIVE` gets no data, add `D15503` first, then look at `D15508`.
|
||||
2. **Whether `ACCEPT` is needed.** It is included because the app sends it when offline.
|
||||
3. **History transfer speed and depth**, and whether this tag accepts 51840.
|
||||
4. **Live frame rate.** The app doesn't show it. The 120 s silence timeout is conservative for that reason.
|
||||
5. **Status values other than 2**, and the temperature unit.
|
||||
6. **Single connection.** Assume the tag serves one central at a time, so close the vendor app during testing.
|
||||
379323
findings/disassembly/app.txt
Normal file
379323
findings/disassembly/app.txt
Normal file
File diff suppressed because it is too large
Load Diff
8216
findings/disassembly/ble.txt
Normal file
8216
findings/disassembly/ble.txt
Normal file
File diff suppressed because it is too large
Load Diff
460
findings/disassembly/cls.txt
Normal file
460
findings/disassembly/cls.txt
Normal file
@@ -0,0 +1,460 @@
|
||||
classes2.dex Lcom/dc/bm6_ancel/R$anim;
|
||||
classes2.dex Lcom/dc/bm6_ancel/R$array;
|
||||
classes2.dex Lcom/dc/bm6_ancel/R$attr;
|
||||
classes2.dex Lcom/dc/bm6_ancel/R$color;
|
||||
classes2.dex Lcom/dc/bm6_ancel/R$drawable;
|
||||
classes2.dex Lcom/dc/bm6_ancel/R$id;
|
||||
classes2.dex Lcom/dc/bm6_ancel/R$layout;
|
||||
classes2.dex Lcom/dc/bm6_ancel/R$mipmap;
|
||||
classes2.dex Lcom/dc/bm6_ancel/R$raw;
|
||||
classes2.dex Lcom/dc/bm6_ancel/R$string;
|
||||
classes2.dex Lcom/dc/bm6_ancel/R$style;
|
||||
classes2.dex Lcom/dc/bm6_ancel/R$styleable;
|
||||
classes2.dex Lcom/dc/bm6_ancel/app/AlarmReceiver;
|
||||
classes2.dex Lcom/dc/bm6_ancel/app/IBeaconReceiver$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/app/IBeaconReceiver$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/app/IBeaconReceiver;
|
||||
classes2.dex Lcom/dc/bm6_ancel/app/LanguageReceiver;
|
||||
classes2.dex Lcom/dc/bm6_ancel/app/MyApp$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/app/MyApp$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/app/MyApp$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/app/MyApp$d;
|
||||
classes2.dex Lcom/dc/bm6_ancel/app/MyApp$e;
|
||||
classes2.dex Lcom/dc/bm6_ancel/app/MyApp;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/BleService$4;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/BleService$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/BleService$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/BleService$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/BleService$d;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/BleService$e;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/BleService$f;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/BleService$g;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/BleService$h;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/BleService$i;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/BleService$j;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/BleService$k;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/BleService$l;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/BleService;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/a$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/a$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/a$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/a$d;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/a$e;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/d;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/e;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/f;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/g;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/h$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/h;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/BleDataOperation$9;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/BleDataOperation$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/BleDataOperation$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/BleDataOperation$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/BleDataOperation$d;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/BleDataOperation$e;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/BleDataOperation$f;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/BleDataOperation$g;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/BleDataOperation$h;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/BleDataOperation$i$a$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/BleDataOperation$i$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/BleDataOperation$i;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/BleDataOperation$j$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/BleDataOperation$j;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/BleDataOperation;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/d;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/e;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/f;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/g;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/h;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/i;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/j;
|
||||
classes2.dex Lcom/dc/bm6_ancel/ble/operation/k;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityAboutBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityAddBatteryBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityAddNoBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityBatteryBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityBleTipsBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityChargeBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityChargeDesBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityCodeBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityCrankBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityCrankDesBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityFeedBackBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityFlashBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityHistoryBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityHistoryDesBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityLanguageBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityLoginBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityMainBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivitySeekDetailBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityShareBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityStepBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityTripDetailBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityTripLogBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityUpgradeBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityUpgradeSingleBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ActivityWebBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/AddDialogItemBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/AddDialogLayoutBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/AlarmFragment1RecyclerItemBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/AlarmLoadingLayoutBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/AlarmUpLayoutBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/BaseActivityBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/BaseFragmentBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/BaseToolbarBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/BatteryItemBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/BleTipsItemBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/BluetoothTipsBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/DayItemBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/DialogDayBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/DialogMonthBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/DialogTipBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/DialogWaitBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/EmptyData1Binding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/EmptyDataBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/EmptyEquipmentBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/EmptyNoLoginBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/EquipmentItemBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ExportDeviceItemBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FeedSubmitLayoutBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FirstEditParameterLayoutBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentAlarm1Binding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentAlarm2Binding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentBatteryBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentBatteryMuchBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentSeekBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentSettingBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentSocBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentStep1Binding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentStep2Binding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentStep3Binding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentSuperBatteryBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentSuperChargeBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentSuperCrankBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentSuperSeekBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentSuperTripBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentTempBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentTripBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/FragmentVoltageBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/HistoryDataPopItemBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/HistoryPopBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/HistoryPopItemBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/LanguageItemBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/LastVersionBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/LastVersionSingleBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/LockDialogLayoutBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/LoginLayoutBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/MainPopupItemBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/MonthItemBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/NewVersionBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/NewVersionSingleBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/NoDataViewBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/NoDeviceLayoutBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/OilTipsLayoutBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ParameterItemBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/PermissionAccountTipsBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/PermissionAddBatterTipsBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/PermissionBluetoothTipsBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/PermissionLocationTipsBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/PopRecycleItemBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/PrivacyLayoutBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/RestartBtTipsBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/RestartPhoneTipsBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/SearchAlarmAlarmBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/SearchEquipmentBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/SeekPopBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/SuggestionImagesLayoutBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/TabcustomlayoutBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/TagItemBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/TripItemBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/TripItemHeaderBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/UpgradeDialogLayoutBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/UpgradeViewBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/databinding/ViewVoltChartBinding;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/base/BaseActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/base/BaseFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/base/MvpActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/base/MvpFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/FlashActivity$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/FlashActivity$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/FlashActivity$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/FlashActivity$d;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/FlashActivity$e;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/FlashActivity$f;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/FlashActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$d;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$e;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$f;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$g$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$g;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$h$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$h;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$i;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$j;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$k;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$l;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$m;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$n;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity$o;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/activity/MainActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/main/adapt/MyPagerAdapt;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/AlarmTime;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/BUPoint;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/BatteryInfo$1;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/BatteryInfo$2;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/BatteryInfo;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/ChargeBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/CrankVoltBean$1;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/CrankVoltBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/CurveDataBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/DayItem;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/DayItemStatus;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/Events;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/FtpBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/GPSBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/HistoricalVolt;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/HistoryBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/HistoryBeanNet$1;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/HistoryBeanNet;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/HistorySyncTime;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/HttpResponse;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/LanguageBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/MonthItem;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/MonthItemStatus;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/MsgEvent;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/NearByDevice;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/ParkBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/RealTimeBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/ResultLatLngBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/SP_Con;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/ShareData;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/ShowBean$DeviceBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/ShowBean$SyncBean$1;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/ShowBean$SyncBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/ShowBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/TagBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/TipsBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/TravelBean$1;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/TravelBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/TripListAllBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/TripPageBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/TripPointBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/TripResult;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/UpgradeBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/UploadHistory;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/UserInfo$GrantsBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/UserInfo;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/VersionBean;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/VersionNet;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/VoltPoint;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/YSFUser;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/GsonBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/AddBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/AddShowBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/AllDataBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/BaseBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/ChargeBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/CodeEmailBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/CrankBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/EditBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/EmailBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/GuestBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/HistoryBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/HistoryQueryBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/LoginBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/MacBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/MacYearBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/ParkBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/ParkClockBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/ParkIdBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/RealBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/SerialNoBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/SoCBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/ThirdLoginBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/TripBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/TripIdBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/TripPageBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/UpdateTripBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/UploadCrankBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/UploadHistoryBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/UploadRealBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/model/body/UploadTripBody;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddBatteryActivity$MyParameterAdapt$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddBatteryActivity$MyParameterAdapt$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddBatteryActivity$MyParameterAdapt;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddBatteryActivity$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddBatteryActivity$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddBatteryActivity$g;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddBatteryActivity$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddBatteryActivity$d;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddBatteryActivity$e;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddBatteryActivity$f;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddBatteryActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddNoActivity$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddNoActivity$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddNoActivity$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddNoActivity$d;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddNoActivity$e;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddNoActivity$f;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddNoActivity$g;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/AddNoActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/BatteryActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/BleTipsActivity$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/BleTipsActivity$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/BleTipsActivity$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/BleTipsActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/ChargeActivity$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/ChargeActivity$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/ChargeActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/ChargeDesActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/CrankActivity$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/CrankActivity$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/CrankActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/CrankDesActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/HistoryActivity$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/HistoryActivity$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/HistoryActivity$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/HistoryActivity$d;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/HistoryActivity$e;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/HistoryActivity$f;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/HistoryActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/HistoryDesActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/activity/StepActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/adapt/BatterySelectAdapt;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/BatteryFragment$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/BatteryFragment$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/BatteryFragment$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/BatteryFragment$d;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/BatteryFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/BatteryMuchFragment$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/BatteryMuchFragment$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/BatteryMuchFragment$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/BatteryMuchFragment$d;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/BatteryMuchFragment$e;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/BatteryMuchFragment$f;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/BatteryMuchFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/SuperBatteryFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/SuperChargeFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/SuperCrankFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/charge/Step1Fragment$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/charge/Step1Fragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/charge/Step2Fragment$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/charge/Step2Fragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/charge/Step3Fragment$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/charge/Step3Fragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/history/SocFragment$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/history/SocFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/history/TempFragment$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/history/TempFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/history/VoltageFragment$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/battery/frag/history/VoltageFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/login/LoginActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/seek/activity/SeekDetailActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/seek/frag/Alarm1Fragment$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/seek/frag/Alarm1Fragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/seek/frag/Alarm2Fragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/seek/frag/AlarmDialogFragment$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/seek/frag/AlarmDialogFragment$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/seek/frag/AlarmDialogFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/seek/frag/AlarmTimeLoadingDialogFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/seek/frag/AlarmTimeUpDialogFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/seek/frag/SeekFragment$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/seek/frag/SeekFragment$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/seek/frag/SeekFragment$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/seek/frag/SeekFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/seek/frag/SuperSeekFragment$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/seek/frag/SuperSeekFragment$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/seek/frag/SuperSeekFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/AboutActivity$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/AboutActivity$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/AboutActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/FeedBackActivity$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/FeedBackActivity$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/FeedBackActivity$c$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/FeedBackActivity$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/FeedBackActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/LanguageActivity$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/LanguageActivity$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/LanguageActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/ShareActivity$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/ShareActivity$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/ShareActivity$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/ShareActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/TripLogActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/upgrade/UpgradeLayout$e;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/UpgradeActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/UpgradeSingleActivity$a$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/UpgradeSingleActivity$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/UpgradeSingleActivity$b$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/UpgradeSingleActivity$b$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/UpgradeSingleActivity$b$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/UpgradeSingleActivity$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/UpgradeSingleActivity$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/UpgradeSingleActivity$d;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/UpgradeSingleActivity$e;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/activity/UpgradeSingleActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/fragment/SettingFragment$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/fragment/SettingFragment$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/alarm/AlarmView$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/setting/fragment/SettingFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/trip/activity/TripDetailActivity$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/trip/activity/TripDetailActivity$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/trip/activity/TripDetailActivity$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/trip/activity/TripDetailActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/trip/frag/SuperTripFragment$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/trip/frag/SuperTripFragment$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/trip/frag/SuperTripFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/trip/frag/TripFragment$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/trip/frag/TripFragment$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/trip/frag/TripFragment$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/trip/frag/TripFragment$d;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/trip/frag/TripFragment;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/web/CodeActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/web/WebActivity$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/web/WebActivity$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/mvp/view/web/WebActivity;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/MyLinearLayout;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/alarm/AlarmView$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/alarm/AlarmView;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/alarm/TextThumbSeekBar;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/chart/BTChart;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/chart/ChartView;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/chart/CurveSurfaceView;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/chart/MuchCurveSurfaceView;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/chart/StripeProgress$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/chart/StripeProgress$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/chart/StripeProgress;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/chart/VoltChart;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/chart/VoltChartBg;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/chart/VoltCurve$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/chart/VoltCurve;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/glide/MyAppGlideModule;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/recycler/CenterLayoutManager$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/recycler/CenterLayoutManager;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/recycler/DeviceItemDecoration;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/recycler/FloatingItemDecoration;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/recycler/GridItemDecoration;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/recycler/LineItemDecoration2;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/recycler/LineItemDecoration;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/recycler/MarginDecoration;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/recycler/SpaceItemDecoration;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/textview/MarqueeTextView;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/upgrade/UpgradeLayout$a$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/upgrade/UpgradeLayout$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/upgrade/UpgradeLayout$b$a;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/upgrade/UpgradeLayout$b$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/upgrade/UpgradeLayout$b$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/upgrade/UpgradeLayout$b;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/upgrade/UpgradeLayout$c;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/upgrade/UpgradeLayout$d;
|
||||
classes2.dex Lcom/dc/bm6_ancel/util/upgrade/UpgradeLayout;
|
||||
classes2.dex Lcom/dc/bm6_ancel/wxapi/WXEntryActivity;
|
||||
87
findings/disassembly/urls.txt
Normal file
87
findings/disassembly/urls.txt
Normal file
@@ -0,0 +1,87 @@
|
||||
classes.dex 'http://'
|
||||
classes.dex 'http://dashif.org/guidelines/last-segment-number'
|
||||
classes.dex 'http://dashif.org/guidelines/thumbnail_tile'
|
||||
classes.dex 'http://dashif.org/guidelines/trickmode'
|
||||
classes.dex 'http://dashif.org/thumbnail_tile'
|
||||
classes.dex 'http://ns.adobe.com/xap/1.0/'
|
||||
classes.dex 'http://ns.adobe.com/xap/1.0/<2F><>'
|
||||
classes.dex 'http://schemas.android.com/apk/res/android'
|
||||
classes.dex 'http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense'
|
||||
classes.dex 'http://www.w3.org/ns/ttml#parameter'
|
||||
classes.dex 'https://'
|
||||
classes.dex 'https://aomedia.org/emsg/ID3'
|
||||
classes.dex 'https://default.url'
|
||||
classes.dex 'https://developer.apple.com/streaming/emsg-id3'
|
||||
classes.dex 'https://x'
|
||||
classes2.dex '86b99e87879824543197c5bd7c349c3e'
|
||||
classes2.dex 'LEAGEND2018!'
|
||||
classes2.dex 'LEAGEND2018VERYG'
|
||||
classes2.dex 'http://'
|
||||
classes2.dex 'http://schemas.android.com/apk/res-auto'
|
||||
classes2.dex 'http://schemas.android.com/apk/res/android'
|
||||
classes2.dex 'https://'
|
||||
classes2.dex 'https://accounts.google.com'
|
||||
classes2.dex 'https://accounts.google.com/o/oauth2/revoke?token='
|
||||
classes2.dex 'https://doc.quicklynks.com/bm200/privacy-policy/index.html'
|
||||
classes2.dex 'https://doc.quicklynks.com/bm200/terms-of-service/index.html'
|
||||
classes2.dex 'https://erp.quicklynks.com/dacheng-erp/faq_co/list/075caef9dce37db6/'
|
||||
classes2.dex 'https://login.live.com'
|
||||
classes2.dex 'https://login.yahoo.com'
|
||||
classes2.dex 'https://plus.google.com/'
|
||||
classes2.dex 'https://twitter.com'
|
||||
classes2.dex 'https://www.facebook.com'
|
||||
classes2.dex 'https://www.googleapis.com/auth/appstate'
|
||||
classes2.dex 'https://www.googleapis.com/auth/datastoremobile'
|
||||
classes2.dex 'https://www.googleapis.com/auth/drive'
|
||||
classes2.dex 'https://www.googleapis.com/auth/drive.appdata'
|
||||
classes2.dex 'https://www.googleapis.com/auth/drive.apps'
|
||||
classes2.dex 'https://www.googleapis.com/auth/drive.file'
|
||||
classes2.dex 'https://www.googleapis.com/auth/games'
|
||||
classes2.dex 'https://www.googleapis.com/auth/games.firstparty'
|
||||
classes2.dex 'https://www.googleapis.com/auth/games_lite'
|
||||
classes2.dex 'https://www.googleapis.com/auth/plus.login'
|
||||
classes2.dex 'https://www.googleapis.com/auth/plus.me'
|
||||
classes2.dex 'https://www.googleapis.com/auth/userinfo.email'
|
||||
classes2.dex 'https://www.googleapis.com/auth/userinfo.profile'
|
||||
classes2.dex 'https://www.linkedin.com'
|
||||
classes2.dex 'https://www.paypal.com'
|
||||
classes3.dex 'http://'
|
||||
classes3.dex 'http://localhost/'
|
||||
classes3.dex 'http://pingma.qq.com:80/mstat/report'
|
||||
classes3.dex 'https://'
|
||||
classes3.dex 'https://alogsus.umeng.com'
|
||||
classes3.dex 'https://alogus.umeng.com'
|
||||
classes3.dex 'https://aspect-upush.umeng.com/occa/v1/event/report'
|
||||
classes3.dex 'https://audid.umeng.com/v3/a/audid/req'
|
||||
classes3.dex 'https://ccs.umeng.com/aa'
|
||||
classes3.dex 'https://cnlogs.umeng.com/common_inout_logs'
|
||||
classes3.dex 'https://cnlogs.umeng.com/ext_event'
|
||||
classes3.dex 'https://cnlogs.umeng.com/uapp_ekverr_logs'
|
||||
classes3.dex 'https://developer.umeng.com/docs/66632/detail/'
|
||||
classes3.dex 'https://dsp.ads.umeng.com/api/v2/evt'
|
||||
classes3.dex 'https://dsp.ads.umeng.com/api/v2/rq'
|
||||
classes3.dex 'https://long.open.weixin.qq.com/connect/l/qrconnect?f=json&uuid=%s'
|
||||
classes3.dex 'https://msg.umengcloud.com/admsg/v3/getmsg'
|
||||
classes3.dex 'https://msg.umengcloud.com/admsg/v3/launch'
|
||||
classes3.dex 'https://msg.umengcloud.com/admsg/v3/stats'
|
||||
classes3.dex 'https://msg.umengcloud.com/v3/alias'
|
||||
classes3.dex 'https://msg.umengcloud.com/v3/alias/delete'
|
||||
classes3.dex 'https://msg.umengcloud.com/v3/alias/set'
|
||||
classes3.dex 'https://msg.umengcloud.com/v3/tag/add'
|
||||
classes3.dex 'https://msg.umengcloud.com/v3/tag/delete'
|
||||
classes3.dex 'https://msg.umengcloud.com/v3/tag/get'
|
||||
classes3.dex 'https://offmsg.umeng.com/log/switch'
|
||||
classes3.dex 'https://offmsg.umeng.com/log/upload'
|
||||
classes3.dex 'https://one.umops.com/api/deferredLink'
|
||||
classes3.dex 'https://open.weixin.qq.com/connect/sdk/qrconnect?appid=%s&noncestr=%s×tamp=%s&scope=%s&signature=%s'
|
||||
classes3.dex 'https://preulogs.umeng.com/push_cloud_activation'
|
||||
classes3.dex 'https://pslog.umeng.com'
|
||||
classes3.dex 'https://pslog.umeng.com/'
|
||||
classes3.dex 'https://resolve.umeng.com/resolve'
|
||||
classes3.dex 'https://sss.umeng.com/api/v2/al'
|
||||
classes3.dex 'https://ucc.umeng.com/v2/inn/fetch'
|
||||
classes3.dex 'https://ulogs.umeng.com'
|
||||
classes3.dex 'https://ulogs.umeng.com/push_cloud_activation'
|
||||
classes3.dex 'https://ulogs.umengcloud.com'
|
||||
classes3.dex 'https://utoken.umeng.com'
|
||||
classes3.dex 'https://yumao.puata.info/cc_info'
|
||||
258
findings/mac_test/bm2/bm200.py
Normal file
258
findings/mac_test/bm2/bm200.py
Normal file
@@ -0,0 +1,258 @@
|
||||
"""ANCEL BM200 (BM6 protocol family), from the com.dc.bm6_ancel 2.0.6 APK.
|
||||
|
||||
BM200Decoder is transport-free: feed it raw FFF4 notifications from any relay.
|
||||
BM200Client does the GATT session (bleak needed only for this part).
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import struct
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Callable, List, Optional, Type, TypeVar, Union
|
||||
|
||||
from bm2.encryption import decrypt, encrypt
|
||||
|
||||
logger = logging.getLogger("bm200")
|
||||
|
||||
BM200_KEY = b"leagend\xff\xfe0100009"
|
||||
UUID_NOTIFY = "0000fff4-0000-1000-8000-00805f9b34fb"
|
||||
UUID_WRITE = "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||
|
||||
CMD_VERSION = bytes.fromhex("d15501")
|
||||
CMD_ACCEPT = bytes.fromhex("d1550200") # never send d1550211: the app's "not genuine" verdict
|
||||
CMD_LIVE = bytes.fromhex("d15507")
|
||||
|
||||
RECORD_INTERVAL = 120 # seconds between history records
|
||||
MAX_RECORDS_31D = 22320 # app's cap; it uses 51840 (72 days) only for "bm6intact" devices
|
||||
|
||||
|
||||
def history_command(count: int) -> bytes:
|
||||
return bytes.fromhex("d15505000000") + struct.pack(">L", count)[1:]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Version:
|
||||
version: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class AcceptAck:
|
||||
result: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class LiveReading:
|
||||
time: datetime
|
||||
voltage: float
|
||||
soc: int
|
||||
temperature: int
|
||||
status: int # raw; 2 = engine running (app treats 2 -> other as engine off)
|
||||
rapid_acceleration: int
|
||||
rapid_deceleration: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class HistoryRecord:
|
||||
time: datetime
|
||||
voltage: float
|
||||
soc: int
|
||||
temperature: int
|
||||
mark: Optional[str] # "start" / "stop" of the engine
|
||||
|
||||
|
||||
@dataclass
|
||||
class HistoryBlock:
|
||||
records: List[HistoryRecord] # oldest first
|
||||
|
||||
|
||||
Event = Union[Version, AcceptAck, LiveReading, HistoryBlock]
|
||||
|
||||
|
||||
def _history_base(received_at: float) -> float:
|
||||
# Same as the app: floor to the minute, then step back onto an odd local minute.
|
||||
base = int(received_at) // 60 * 60
|
||||
return base - 60 if datetime.fromtimestamp(base).minute % 2 == 0 else base
|
||||
|
||||
|
||||
def parse_history(data: str, base: float) -> List[HistoryRecord]:
|
||||
"""data: hex up to the FFFEFE marker. Record 0 is the newest, 2 minutes apart."""
|
||||
# ponytail: strips all leading zero records; the app's loop also skips some records after them
|
||||
while data.startswith("00000000"):
|
||||
data = data[8:]
|
||||
|
||||
records = []
|
||||
pos = index = 0
|
||||
while pos + 8 <= len(data):
|
||||
item = data[pos:pos + 8]
|
||||
pos += 8
|
||||
flags = int(item[7], 16)
|
||||
mark = {0b010: "start", 0b100: "stop"}.get(flags & 0b111)
|
||||
if mark == "stop":
|
||||
pos += 8 # stop records carry 4 more bytes: rapid acceleration/deceleration counts
|
||||
voltage = int(item[:3], 16) / 100
|
||||
if voltage != 0: # zero-voltage slots are dropped but still take up a time slot
|
||||
temperature = int(item[5:7], 16)
|
||||
records.append(HistoryRecord(time=datetime.fromtimestamp(base - index * RECORD_INTERVAL, timezone.utc),
|
||||
voltage=voltage,
|
||||
soc=int(item[3:5], 16),
|
||||
temperature=-temperature if flags & 0b1000 else temperature,
|
||||
mark=mark))
|
||||
index += 1
|
||||
records.reverse()
|
||||
return records
|
||||
|
||||
|
||||
class BM200Decoder:
|
||||
def __init__(self) -> None:
|
||||
self._history: Optional[str] = None
|
||||
self._history_base = 0.0
|
||||
|
||||
def feed(self, frame: Union[bytes, bytearray], received_at: Optional[float] = None) -> List[Event]:
|
||||
"""Raises ValueError if frame is not whole AES blocks."""
|
||||
if not frame or len(frame) % 16:
|
||||
raise ValueError(f"bad frame length {len(frame)}")
|
||||
now = time.time() if received_at is None else received_at
|
||||
h = decrypt(frame, BM200_KEY).hex().upper()
|
||||
|
||||
# Live frames interleave with history and must not end up in it.
|
||||
if h.startswith("D15507"):
|
||||
return [] if h.startswith("D15507FF") else [self._live(h, now)]
|
||||
|
||||
if self._history is not None:
|
||||
return self._history_frame(h)
|
||||
|
||||
if h.startswith("D15501"):
|
||||
return [Version(int(h[6:8], 16) + int(h[8:10], 16) / 10)]
|
||||
if h.startswith("D15502"):
|
||||
return [AcceptAck(int(h[6:8], 16))]
|
||||
if h.startswith("D15505"):
|
||||
if int(h[6:10], 16) == 0:
|
||||
return [HistoryBlock([])]
|
||||
self._history = ""
|
||||
self._history_base = _history_base(now)
|
||||
return []
|
||||
|
||||
logger.debug("ignored frame %s", h)
|
||||
return []
|
||||
|
||||
def _history_frame(self, h: str) -> List[Event]:
|
||||
assert self._history is not None
|
||||
if "FEFEFE" in h: # app: "empty history marker, discard"
|
||||
return []
|
||||
self._history += h
|
||||
end = self._history.find("FFFEFE")
|
||||
if end < 0:
|
||||
return []
|
||||
data, self._history = self._history[:end], None
|
||||
return [HistoryBlock(parse_history(data, self._history_base))]
|
||||
|
||||
@staticmethod
|
||||
def _live(h: str, now: float) -> LiveReading:
|
||||
temperature = int(h[8:10], 16)
|
||||
return LiveReading(time=datetime.fromtimestamp(now, timezone.utc),
|
||||
voltage=int(h[14:18], 16) / 100,
|
||||
soc=int(h[12:14], 16),
|
||||
temperature=-temperature if int(h[6:8], 16) == 1 else temperature,
|
||||
status=int(h[10:12], 16),
|
||||
rapid_acceleration=int(h[18:22], 16),
|
||||
rapid_deceleration=int(h[22:26], 16))
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class BM200Client:
|
||||
def __init__(self, mac: str, since: Optional[datetime] = None, max_records: int = MAX_RECORDS_31D,
|
||||
history_timeout: float = 600, live_timeout: float = 120):
|
||||
self._mac = mac
|
||||
self.since = since # newest history record delivered so far
|
||||
self._max_records = max_records
|
||||
self._history_timeout = history_timeout
|
||||
self._live_timeout = live_timeout
|
||||
|
||||
def history_count(self) -> int:
|
||||
if self.since is None:
|
||||
return self._max_records
|
||||
missing = (datetime.now(timezone.utc) - self.since).total_seconds() / RECORD_INTERVAL
|
||||
return min(max(int(missing), 0) + 10, self._max_records) # +10 overlap, as in the app
|
||||
|
||||
async def run(self,
|
||||
on_reading: Callable[[Union[LiveReading, HistoryRecord]], None],
|
||||
on_history_done: Optional[Callable[[datetime], None]] = None,
|
||||
on_frame: Optional[Callable[[bytes], None]] = None) -> None:
|
||||
"""Connect forever: on each connection backfill records newer than since, then stream live."""
|
||||
import bleak.exc
|
||||
from bleak import BleakClient
|
||||
|
||||
while True:
|
||||
try:
|
||||
async with BleakClient(self._mac) as client:
|
||||
logger.info("connected")
|
||||
await self._session(client, on_reading, on_history_done, on_frame)
|
||||
except (bleak.exc.BleakError, OSError, asyncio.TimeoutError) as e:
|
||||
logger.warning("session ended: %r", e)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _session(self, client: Any, on_reading: Callable[..., None],
|
||||
on_history_done: Optional[Callable[[datetime], None]],
|
||||
on_frame: Optional[Callable[[bytes], None]]) -> None:
|
||||
decoder = BM200Decoder()
|
||||
queue: "asyncio.Queue[Event]" = asyncio.Queue()
|
||||
last_frame = time.monotonic()
|
||||
|
||||
def handler(_: Any, data: bytearray) -> None:
|
||||
nonlocal last_frame
|
||||
last_frame = time.monotonic()
|
||||
if on_frame:
|
||||
on_frame(bytes(data))
|
||||
try:
|
||||
events = decoder.feed(data)
|
||||
except ValueError as e:
|
||||
logger.warning("%s", e)
|
||||
return
|
||||
for event in events:
|
||||
if isinstance(event, LiveReading):
|
||||
on_reading(event)
|
||||
else:
|
||||
queue.put_nowait(event)
|
||||
|
||||
async def send(cmd: bytes) -> None:
|
||||
await client.write_gatt_char(UUID_WRITE, encrypt(cmd, BM200_KEY))
|
||||
|
||||
async def expect(kind: Type[T], timeout: float = 10) -> T:
|
||||
while True:
|
||||
event = await asyncio.wait_for(queue.get(), timeout)
|
||||
if isinstance(event, kind):
|
||||
return event
|
||||
|
||||
await client.start_notify(UUID_NOTIFY, handler)
|
||||
|
||||
# Same order as the app without its cloud check. Its battery-type write (D15508) is
|
||||
# deliberately left out: it changes tag configuration.
|
||||
await send(CMD_VERSION)
|
||||
logger.info("firmware %s", (await expect(Version)).version)
|
||||
await send(CMD_ACCEPT)
|
||||
await expect(AcceptAck)
|
||||
|
||||
# The app always reads and discards 10 records before the real read.
|
||||
await send(history_command(10))
|
||||
await expect(HistoryBlock)
|
||||
|
||||
await send(history_command(self.history_count()))
|
||||
block = await expect(HistoryBlock, self._history_timeout)
|
||||
for record in block.records:
|
||||
if self.since is None or record.time > self.since:
|
||||
on_reading(record)
|
||||
self.since = record.time
|
||||
if self.since is not None and on_history_done:
|
||||
on_history_done(self.since)
|
||||
|
||||
await send(CMD_LIVE)
|
||||
while client.is_connected:
|
||||
if time.monotonic() - last_frame > self._live_timeout:
|
||||
raise asyncio.TimeoutError("no frames")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
__all__ = ["BM200Client", "BM200Decoder", "HistoryBlock", "HistoryRecord", "LiveReading", "parse_history"]
|
||||
24
findings/mac_test/bm2/encryption.py
Normal file
24
findings/mac_test/bm2/encryption.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from typing import Any, cast, Union
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
BM2_ENCRYPTION_KEY = b"\x6c\x65\x61\x67\x65\x6e\x64\xff\xfe\x31\x38\x38\x32\x34\x36\x36"
|
||||
|
||||
AesBlockSize = 16
|
||||
|
||||
|
||||
def create_aes(key: bytes = BM2_ENCRYPTION_KEY) -> Any:
|
||||
return AES.new(key, AES.MODE_CBC, bytes([0] * AesBlockSize))
|
||||
|
||||
|
||||
def pad_to_block(x: bytes) -> bytes:
|
||||
required_size = (len(x) + (AesBlockSize - 1)) // AesBlockSize * AesBlockSize
|
||||
return x.ljust(required_size, b"\x00")
|
||||
|
||||
|
||||
def encrypt(data: bytes, key: bytes = BM2_ENCRYPTION_KEY) -> bytes:
|
||||
return cast(bytes, create_aes(key).encrypt(pad_to_block(data)))
|
||||
|
||||
|
||||
def decrypt(encrypted_data: Union[bytes, bytearray], key: bytes = BM2_ENCRYPTION_KEY) -> bytes:
|
||||
return cast(bytes, b''.join(create_aes(key).decrypt(bytes(encrypted_data[i:i + AesBlockSize])) for i in range(0, len(encrypted_data), AesBlockSize)))
|
||||
154
findings/mac_test/probe.py
Normal file
154
findings/mac_test/probe.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""BM200 hardware probe. Test bench only, not the product.
|
||||
|
||||
python probe.py scan
|
||||
python probe.py run <id> # handshake, history(10), live
|
||||
python probe.py run <id> --no-accept # is ACCEPT needed?
|
||||
python probe.py run <id> --crank # try this if LIVE stays silent
|
||||
python probe.py run <id> --history 360 # time a larger read (12 hours)
|
||||
|
||||
Every frame goes to probe-<time>.jsonl. Never sends D15508 (battery type) or D1550211.
|
||||
"""
|
||||
import argparse
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
from bleak import BleakClient, BleakScanner
|
||||
|
||||
from bm2.bm200 import BM200_KEY, UUID_NOTIFY, UUID_WRITE, CMD_ACCEPT, CMD_LIVE, CMD_VERSION, BM200Decoder, HistoryBlock, LiveReading, history_command
|
||||
from bm2.encryption import decrypt, encrypt
|
||||
|
||||
CMD_CRANK = bytes.fromhex("d15503")
|
||||
NAMES = ("bm6", "battery guard")
|
||||
|
||||
|
||||
async def scan(seconds):
|
||||
found = await BleakScanner.discover(timeout=seconds, return_adv=True)
|
||||
matches = 0
|
||||
for device, adv in found.values():
|
||||
name = adv.local_name or device.name or ""
|
||||
if name.lower() in NAMES:
|
||||
matches += 1
|
||||
print(f"{device.address} {name!r} rssi={adv.rssi}")
|
||||
if not matches:
|
||||
sys.exit(f"no tag among {len(found)} devices. Check Bluetooth permission, and disconnect the vendor app.")
|
||||
|
||||
|
||||
def jsonable(event):
|
||||
return {"type": type(event).__name__, **dataclasses.asdict(event)}
|
||||
|
||||
|
||||
async def run(args):
|
||||
log = open(f"probe-{int(time.time())}.jsonl", "w")
|
||||
decoder = BM200Decoder()
|
||||
frames, live_times = [], []
|
||||
history_done = asyncio.Event()
|
||||
got_frame = asyncio.Event()
|
||||
|
||||
def record(direction, data, plain, events=()):
|
||||
log.write(json.dumps({"t": time.time(), "dir": direction, "cipher": data.hex(), "plain": plain.hex(),
|
||||
"events": [jsonable(e) for e in events]}, default=str) + "\n")
|
||||
log.flush()
|
||||
|
||||
def handler(_, data):
|
||||
data = bytes(data)
|
||||
frames.append(time.monotonic())
|
||||
got_frame.set()
|
||||
try:
|
||||
plain, events = decrypt(data, BM200_KEY), decoder.feed(data)
|
||||
except ValueError as e:
|
||||
print(f" rx BAD {data.hex()}: {e}")
|
||||
return
|
||||
record("rx", data, plain, events)
|
||||
if not events:
|
||||
print(f" rx {plain.hex()}")
|
||||
for event in events:
|
||||
if isinstance(event, LiveReading):
|
||||
live_times.append(time.monotonic())
|
||||
print(f" live {event.voltage} V, soc {event.soc}, temp {event.temperature}, status {event.status}")
|
||||
elif isinstance(event, HistoryBlock):
|
||||
print(f" history: {len(event.records)} records")
|
||||
for r in event.records[-3:]:
|
||||
print(f" {r.time.isoformat()} {r.voltage} V, soc {r.soc}, temp {r.temperature}, {r.mark}")
|
||||
history_done.set()
|
||||
else:
|
||||
print(f" {event}")
|
||||
|
||||
print(f"connecting to {args.id}")
|
||||
async with BleakClient(args.id, timeout=30) as client:
|
||||
for service in client.services:
|
||||
for char in service.characteristics:
|
||||
print(f" {service.uuid[4:8]}/{char.uuid[4:8]} {char.properties}")
|
||||
write_char = client.services.get_characteristic(UUID_WRITE)
|
||||
if write_char is None:
|
||||
sys.exit("no FFF3 characteristic: this is not the expected tag")
|
||||
with_response = "write" in write_char.properties
|
||||
|
||||
async def send(name, cmd, wait=5.0):
|
||||
print(f"> {name}")
|
||||
data = encrypt(cmd, BM200_KEY)
|
||||
record("tx", data, cmd.ljust(16, b"\0"))
|
||||
got_frame.clear()
|
||||
await client.write_gatt_char(UUID_WRITE, data, response=with_response)
|
||||
try:
|
||||
await asyncio.wait_for(got_frame.wait(), wait)
|
||||
except asyncio.TimeoutError:
|
||||
print(f" no reply within {wait:.0f}s")
|
||||
|
||||
async def read_history(count, timeout):
|
||||
history_done.clear()
|
||||
started = time.monotonic()
|
||||
await send(f"HISTORY({count})", history_command(count))
|
||||
try:
|
||||
await asyncio.wait_for(history_done.wait(), timeout)
|
||||
print(f" took {time.monotonic() - started:.1f}s")
|
||||
except asyncio.TimeoutError:
|
||||
print(f" history did not finish within {timeout:.0f}s")
|
||||
|
||||
await client.start_notify(UUID_NOTIFY, handler)
|
||||
|
||||
print("> listening before any command, 5s")
|
||||
await asyncio.sleep(5)
|
||||
print(f" {len(frames)} unsolicited frames")
|
||||
|
||||
await send("VERSION", CMD_VERSION)
|
||||
if not args.no_accept:
|
||||
await send("ACCEPT", CMD_ACCEPT)
|
||||
await read_history(10, 30)
|
||||
if args.history:
|
||||
await read_history(args.history, args.history_timeout)
|
||||
if args.crank:
|
||||
await send("CRANK", CMD_CRANK)
|
||||
await asyncio.sleep(3)
|
||||
|
||||
await send("LIVE", CMD_LIVE)
|
||||
print(f"> collecting live frames, {args.live_seconds}s")
|
||||
await asyncio.sleep(args.live_seconds)
|
||||
|
||||
gaps = [b - a for a, b in zip(live_times, live_times[1:])]
|
||||
print(f"\nlive frames: {len(live_times)}")
|
||||
if gaps:
|
||||
print(f"interval: avg {sum(gaps) / len(gaps):.1f}s, min {min(gaps):.1f}s, max {max(gaps):.1f}s")
|
||||
print(f"log: {log.name}")
|
||||
log.close()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
sub.add_parser("scan").add_argument("--seconds", type=float, default=10)
|
||||
r = sub.add_parser("run")
|
||||
r.add_argument("id", help="address from scan (a UUID on macOS)")
|
||||
r.add_argument("--no-accept", action="store_true")
|
||||
r.add_argument("--crank", action="store_true")
|
||||
r.add_argument("--history", type=int, metavar="COUNT", help="extra history read after the first 10 records")
|
||||
r.add_argument("--history-timeout", type=float, default=600)
|
||||
r.add_argument("--live-seconds", type=float, default=120)
|
||||
args = parser.parse_args()
|
||||
asyncio.run(scan(args.seconds) if args.command == "scan" else run(args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2
findings/mac_test/requirements.txt
Normal file
2
findings/mac_test/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
bleak
|
||||
pycryptodome
|
||||
258
findings/reference-python/bm200.py
Normal file
258
findings/reference-python/bm200.py
Normal file
@@ -0,0 +1,258 @@
|
||||
"""ANCEL BM200 (BM6 protocol family), from the com.dc.bm6_ancel 2.0.6 APK.
|
||||
|
||||
BM200Decoder is transport-free: feed it raw FFF4 notifications from any relay.
|
||||
BM200Client does the GATT session (bleak needed only for this part).
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import struct
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Callable, List, Optional, Type, TypeVar, Union
|
||||
|
||||
from bm2.encryption import decrypt, encrypt
|
||||
|
||||
logger = logging.getLogger("bm200")
|
||||
|
||||
BM200_KEY = b"leagend\xff\xfe0100009"
|
||||
UUID_NOTIFY = "0000fff4-0000-1000-8000-00805f9b34fb"
|
||||
UUID_WRITE = "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||
|
||||
CMD_VERSION = bytes.fromhex("d15501")
|
||||
CMD_ACCEPT = bytes.fromhex("d1550200") # never send d1550211: the app's "not genuine" verdict
|
||||
CMD_LIVE = bytes.fromhex("d15507")
|
||||
|
||||
RECORD_INTERVAL = 120 # seconds between history records
|
||||
MAX_RECORDS_31D = 22320 # app's cap; it uses 51840 (72 days) only for "bm6intact" devices
|
||||
|
||||
|
||||
def history_command(count: int) -> bytes:
|
||||
return bytes.fromhex("d15505000000") + struct.pack(">L", count)[1:]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Version:
|
||||
version: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class AcceptAck:
|
||||
result: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class LiveReading:
|
||||
time: datetime
|
||||
voltage: float
|
||||
soc: int
|
||||
temperature: int
|
||||
status: int # raw; 2 = engine running (app treats 2 -> other as engine off)
|
||||
rapid_acceleration: int
|
||||
rapid_deceleration: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class HistoryRecord:
|
||||
time: datetime
|
||||
voltage: float
|
||||
soc: int
|
||||
temperature: int
|
||||
mark: Optional[str] # "start" / "stop" of the engine
|
||||
|
||||
|
||||
@dataclass
|
||||
class HistoryBlock:
|
||||
records: List[HistoryRecord] # oldest first
|
||||
|
||||
|
||||
Event = Union[Version, AcceptAck, LiveReading, HistoryBlock]
|
||||
|
||||
|
||||
def _history_base(received_at: float) -> float:
|
||||
# Same as the app: floor to the minute, then step back onto an odd local minute.
|
||||
base = int(received_at) // 60 * 60
|
||||
return base - 60 if datetime.fromtimestamp(base).minute % 2 == 0 else base
|
||||
|
||||
|
||||
def parse_history(data: str, base: float) -> List[HistoryRecord]:
|
||||
"""data: hex up to the FFFEFE marker. Record 0 is the newest, 2 minutes apart."""
|
||||
# ponytail: strips all leading zero records; the app's loop also skips some records after them
|
||||
while data.startswith("00000000"):
|
||||
data = data[8:]
|
||||
|
||||
records = []
|
||||
pos = index = 0
|
||||
while pos + 8 <= len(data):
|
||||
item = data[pos:pos + 8]
|
||||
pos += 8
|
||||
flags = int(item[7], 16)
|
||||
mark = {0b010: "start", 0b100: "stop"}.get(flags & 0b111)
|
||||
if mark == "stop":
|
||||
pos += 8 # stop records carry 4 more bytes: rapid acceleration/deceleration counts
|
||||
voltage = int(item[:3], 16) / 100
|
||||
if voltage != 0: # zero-voltage slots are dropped but still take up a time slot
|
||||
temperature = int(item[5:7], 16)
|
||||
records.append(HistoryRecord(time=datetime.fromtimestamp(base - index * RECORD_INTERVAL, timezone.utc),
|
||||
voltage=voltage,
|
||||
soc=int(item[3:5], 16),
|
||||
temperature=-temperature if flags & 0b1000 else temperature,
|
||||
mark=mark))
|
||||
index += 1
|
||||
records.reverse()
|
||||
return records
|
||||
|
||||
|
||||
class BM200Decoder:
|
||||
def __init__(self) -> None:
|
||||
self._history: Optional[str] = None
|
||||
self._history_base = 0.0
|
||||
|
||||
def feed(self, frame: Union[bytes, bytearray], received_at: Optional[float] = None) -> List[Event]:
|
||||
"""Raises ValueError if frame is not whole AES blocks."""
|
||||
if not frame or len(frame) % 16:
|
||||
raise ValueError(f"bad frame length {len(frame)}")
|
||||
now = time.time() if received_at is None else received_at
|
||||
h = decrypt(frame, BM200_KEY).hex().upper()
|
||||
|
||||
# Live frames interleave with history and must not end up in it.
|
||||
if h.startswith("D15507"):
|
||||
return [] if h.startswith("D15507FF") else [self._live(h, now)]
|
||||
|
||||
if self._history is not None:
|
||||
return self._history_frame(h)
|
||||
|
||||
if h.startswith("D15501"):
|
||||
return [Version(int(h[6:8], 16) + int(h[8:10], 16) / 10)]
|
||||
if h.startswith("D15502"):
|
||||
return [AcceptAck(int(h[6:8], 16))]
|
||||
if h.startswith("D15505"):
|
||||
if int(h[6:10], 16) == 0:
|
||||
return [HistoryBlock([])]
|
||||
self._history = ""
|
||||
self._history_base = _history_base(now)
|
||||
return []
|
||||
|
||||
logger.debug("ignored frame %s", h)
|
||||
return []
|
||||
|
||||
def _history_frame(self, h: str) -> List[Event]:
|
||||
assert self._history is not None
|
||||
if "FEFEFE" in h: # app: "empty history marker, discard"
|
||||
return []
|
||||
self._history += h
|
||||
end = self._history.find("FFFEFE")
|
||||
if end < 0:
|
||||
return []
|
||||
data, self._history = self._history[:end], None
|
||||
return [HistoryBlock(parse_history(data, self._history_base))]
|
||||
|
||||
@staticmethod
|
||||
def _live(h: str, now: float) -> LiveReading:
|
||||
temperature = int(h[8:10], 16)
|
||||
return LiveReading(time=datetime.fromtimestamp(now, timezone.utc),
|
||||
voltage=int(h[14:18], 16) / 100,
|
||||
soc=int(h[12:14], 16),
|
||||
temperature=-temperature if int(h[6:8], 16) == 1 else temperature,
|
||||
status=int(h[10:12], 16),
|
||||
rapid_acceleration=int(h[18:22], 16),
|
||||
rapid_deceleration=int(h[22:26], 16))
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class BM200Client:
|
||||
def __init__(self, mac: str, since: Optional[datetime] = None, max_records: int = MAX_RECORDS_31D,
|
||||
history_timeout: float = 600, live_timeout: float = 120):
|
||||
self._mac = mac
|
||||
self.since = since # newest history record delivered so far
|
||||
self._max_records = max_records
|
||||
self._history_timeout = history_timeout
|
||||
self._live_timeout = live_timeout
|
||||
|
||||
def history_count(self) -> int:
|
||||
if self.since is None:
|
||||
return self._max_records
|
||||
missing = (datetime.now(timezone.utc) - self.since).total_seconds() / RECORD_INTERVAL
|
||||
return min(max(int(missing), 0) + 10, self._max_records) # +10 overlap, as in the app
|
||||
|
||||
async def run(self,
|
||||
on_reading: Callable[[Union[LiveReading, HistoryRecord]], None],
|
||||
on_history_done: Optional[Callable[[datetime], None]] = None,
|
||||
on_frame: Optional[Callable[[bytes], None]] = None) -> None:
|
||||
"""Connect forever: on each connection backfill records newer than since, then stream live."""
|
||||
import bleak.exc
|
||||
from bleak import BleakClient
|
||||
|
||||
while True:
|
||||
try:
|
||||
async with BleakClient(self._mac) as client:
|
||||
logger.info("connected")
|
||||
await self._session(client, on_reading, on_history_done, on_frame)
|
||||
except (bleak.exc.BleakError, OSError, asyncio.TimeoutError) as e:
|
||||
logger.warning("session ended: %r", e)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _session(self, client: Any, on_reading: Callable[..., None],
|
||||
on_history_done: Optional[Callable[[datetime], None]],
|
||||
on_frame: Optional[Callable[[bytes], None]]) -> None:
|
||||
decoder = BM200Decoder()
|
||||
queue: "asyncio.Queue[Event]" = asyncio.Queue()
|
||||
last_frame = time.monotonic()
|
||||
|
||||
def handler(_: Any, data: bytearray) -> None:
|
||||
nonlocal last_frame
|
||||
last_frame = time.monotonic()
|
||||
if on_frame:
|
||||
on_frame(bytes(data))
|
||||
try:
|
||||
events = decoder.feed(data)
|
||||
except ValueError as e:
|
||||
logger.warning("%s", e)
|
||||
return
|
||||
for event in events:
|
||||
if isinstance(event, LiveReading):
|
||||
on_reading(event)
|
||||
else:
|
||||
queue.put_nowait(event)
|
||||
|
||||
async def send(cmd: bytes) -> None:
|
||||
await client.write_gatt_char(UUID_WRITE, encrypt(cmd, BM200_KEY))
|
||||
|
||||
async def expect(kind: Type[T], timeout: float = 10) -> T:
|
||||
while True:
|
||||
event = await asyncio.wait_for(queue.get(), timeout)
|
||||
if isinstance(event, kind):
|
||||
return event
|
||||
|
||||
await client.start_notify(UUID_NOTIFY, handler)
|
||||
|
||||
# Same order as the app without its cloud check. Its battery-type write (D15508) is
|
||||
# deliberately left out: it changes tag configuration.
|
||||
await send(CMD_VERSION)
|
||||
logger.info("firmware %s", (await expect(Version)).version)
|
||||
await send(CMD_ACCEPT)
|
||||
await expect(AcceptAck)
|
||||
|
||||
# The app always reads and discards 10 records before the real read.
|
||||
await send(history_command(10))
|
||||
await expect(HistoryBlock)
|
||||
|
||||
await send(history_command(self.history_count()))
|
||||
block = await expect(HistoryBlock, self._history_timeout)
|
||||
for record in block.records:
|
||||
if self.since is None or record.time > self.since:
|
||||
on_reading(record)
|
||||
self.since = record.time
|
||||
if self.since is not None and on_history_done:
|
||||
on_history_done(self.since)
|
||||
|
||||
await send(CMD_LIVE)
|
||||
while client.is_connected:
|
||||
if time.monotonic() - last_frame > self._live_timeout:
|
||||
raise asyncio.TimeoutError("no frames")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
__all__ = ["BM200Client", "BM200Decoder", "HistoryBlock", "HistoryRecord", "LiveReading", "parse_history"]
|
||||
58
findings/reference-python/bm200.yaml
Normal file
58
findings/reference-python/bm200.yaml
Normal file
@@ -0,0 +1,58 @@
|
||||
# ANCEL BM200: live voltage only. History needs bm2.bm200 (reassembly across frames).
|
||||
# The writes are AES-encrypted D15501, D1550200, D15507; without the last one the tag sends no live frames.
|
||||
esp32_ble_tracker:
|
||||
|
||||
ble_client:
|
||||
- mac_address: !secret bm2_mac
|
||||
id: bm2_battery_meter
|
||||
on_connect:
|
||||
then:
|
||||
- delay: 1s
|
||||
- ble_client.ble_write:
|
||||
id: bm2_battery_meter
|
||||
service_uuid: 'fff0'
|
||||
characteristic_uuid: 'fff3'
|
||||
value: [0x1a, 0xd6, 0xf0, 0x5e, 0x49, 0x90, 0xab, 0x24, 0xb8, 0x4d, 0x57, 0xdf, 0x81, 0xd8, 0x76, 0x6e]
|
||||
- delay: 500ms
|
||||
- ble_client.ble_write:
|
||||
id: bm2_battery_meter
|
||||
service_uuid: 'fff0'
|
||||
characteristic_uuid: 'fff3'
|
||||
value: [0x9c, 0xb1, 0x80, 0x29, 0x77, 0x4c, 0x5c, 0xfc, 0x57, 0xc9, 0xf1, 0x14, 0xd4, 0x0d, 0xb4, 0xe8]
|
||||
- delay: 500ms
|
||||
- ble_client.ble_write:
|
||||
id: bm2_battery_meter
|
||||
service_uuid: 'fff0'
|
||||
characteristic_uuid: 'fff3'
|
||||
value: [0x69, 0x7e, 0xa0, 0xb5, 0xd5, 0x4c, 0xf0, 0x24, 0xe7, 0x94, 0x77, 0x23, 0x55, 0x55, 0x41, 0x14]
|
||||
|
||||
sensor:
|
||||
- platform: ble_client
|
||||
type: characteristic
|
||||
ble_client_id: bm2_battery_meter
|
||||
name: Voltage
|
||||
service_uuid: 'fff0'
|
||||
characteristic_uuid: 'fff4'
|
||||
unit_of_measurement: 'V'
|
||||
accuracy_decimals: 2
|
||||
state_class: measurement
|
||||
device_class: voltage
|
||||
force_update: true
|
||||
expire_after: 5min
|
||||
notify: true
|
||||
lambda: |-
|
||||
if (x.size() != 16) return NAN;
|
||||
mbedtls_aes_context aes;
|
||||
mbedtls_aes_init(&aes);
|
||||
unsigned char output[16];
|
||||
unsigned char key[16] = { 108, 101, 97, 103, 101, 110, 100, 255, 254, 48, 49, 48, 48, 48, 48, 57 };
|
||||
unsigned char iv[16] = {};
|
||||
mbedtls_aes_setkey_dec(&aes, key, 128);
|
||||
mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_DECRYPT, 16, iv, (uint8_t*)&x[0], output);
|
||||
mbedtls_aes_free(&aes);
|
||||
|
||||
// D1 55 07 only; 0xFF in byte 3 is the command ack
|
||||
if (output[0] != 0xD1 || output[1] != 0x55 || output[2] != 0x07 || output[3] == 0xFF) return NAN;
|
||||
return ((output[7] << 8) | output[8]) / 100.0f;
|
||||
filters:
|
||||
- filter_out: nan
|
||||
71
findings/reference-python/bm200_logger.py
Normal file
71
findings/reference-python/bm200_logger.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Writes BM200 readings to stdout as JSON lines; pipe that into your logging system.
|
||||
|
||||
Direct BLE: bm200_logger.py --bm200-addr B0:B1:xx:xx:xx:xx --state-file bm200.state
|
||||
From relay: relay-feed | bm200_logger.py --stdin
|
||||
(one raw FFF4 notification per line: "<hex>" or "<epoch> <hex>")
|
||||
"""
|
||||
import argparse
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from bm2.bm200 import BM200Client, BM200Decoder, HistoryBlock, LiveReading
|
||||
|
||||
logging.basicConfig(level=logging.INFO, stream=sys.stderr, format="[%(asctime)s] %(message)s")
|
||||
|
||||
|
||||
def emit(reading: Any) -> None:
|
||||
row = dataclasses.asdict(reading)
|
||||
row["type"] = "live" if isinstance(reading, LiveReading) else "history"
|
||||
row["time"] = reading.time.isoformat()
|
||||
print(json.dumps(row), flush=True)
|
||||
|
||||
|
||||
def from_stdin() -> None:
|
||||
decoder = BM200Decoder()
|
||||
for line in sys.stdin:
|
||||
parts = line.split()
|
||||
if not parts:
|
||||
continue
|
||||
try:
|
||||
events = decoder.feed(bytes.fromhex(parts[-1]), float(parts[0]) if len(parts) > 1 else None)
|
||||
except ValueError as e: # relay input: skip it, keep logging
|
||||
logging.warning("bad line %r: %s", line, e)
|
||||
continue
|
||||
for event in events:
|
||||
if isinstance(event, LiveReading):
|
||||
emit(event)
|
||||
elif isinstance(event, HistoryBlock):
|
||||
for record in event.records:
|
||||
emit(record)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--bm200-addr", metavar="ADDR")
|
||||
parser.add_argument("--state-file", type=Path, help="remembers the newest history record between runs")
|
||||
parser.add_argument("--stdin", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.stdin:
|
||||
return from_stdin()
|
||||
if not args.bm200_addr:
|
||||
parser.error("--bm200-addr or --stdin required")
|
||||
|
||||
since = None
|
||||
if args.state_file and args.state_file.exists():
|
||||
since = datetime.fromisoformat(args.state_file.read_text().strip())
|
||||
|
||||
def save(newest: datetime) -> None:
|
||||
if args.state_file:
|
||||
args.state_file.write_text(newest.isoformat())
|
||||
|
||||
asyncio.run(BM200Client(args.bm200_addr, since).run(emit, save))
|
||||
|
||||
|
||||
main()
|
||||
24
findings/reference-python/encryption.py
Normal file
24
findings/reference-python/encryption.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from typing import Any, cast, Union
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
BM2_ENCRYPTION_KEY = b"\x6c\x65\x61\x67\x65\x6e\x64\xff\xfe\x31\x38\x38\x32\x34\x36\x36"
|
||||
|
||||
AesBlockSize = 16
|
||||
|
||||
|
||||
def create_aes(key: bytes = BM2_ENCRYPTION_KEY) -> Any:
|
||||
return AES.new(key, AES.MODE_CBC, bytes([0] * AesBlockSize))
|
||||
|
||||
|
||||
def pad_to_block(x: bytes) -> bytes:
|
||||
required_size = (len(x) + (AesBlockSize - 1)) // AesBlockSize * AesBlockSize
|
||||
return x.ljust(required_size, b"\x00")
|
||||
|
||||
|
||||
def encrypt(data: bytes, key: bytes = BM2_ENCRYPTION_KEY) -> bytes:
|
||||
return cast(bytes, create_aes(key).encrypt(pad_to_block(data)))
|
||||
|
||||
|
||||
def decrypt(encrypted_data: Union[bytes, bytearray], key: bytes = BM2_ENCRYPTION_KEY) -> bytes:
|
||||
return cast(bytes, b''.join(create_aes(key).decrypt(bytes(encrypted_data[i:i + AesBlockSize])) for i in range(0, len(encrypted_data), AesBlockSize)))
|
||||
58
findings/reference-python/test_bm200.py
Normal file
58
findings/reference-python/test_bm200.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""Run: PYTHONPATH=src python test_bm200.py
|
||||
|
||||
Frames here are built from the APK's parser, not captured from a tag.
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from bm2.bm200 import BM200_KEY, BM200Decoder, HistoryBlock, LiveReading, Version, _history_base, history_command
|
||||
from bm2.encryption import encrypt
|
||||
|
||||
|
||||
def enc(hex_str: str) -> bytes:
|
||||
return encrypt(bytes.fromhex(hex_str), BM200_KEY)
|
||||
|
||||
|
||||
def test() -> None:
|
||||
d = BM200Decoder()
|
||||
now = 1_700_000_000.0
|
||||
|
||||
assert d.feed(enc("d15501010700"))[0] == Version(1.7)
|
||||
assert history_command(0xCA80).hex() == "d1550500000000ca80"
|
||||
|
||||
# temp sign=1 temp=5 status=2 soc=0x55 volt=0x0508 acc=3 dec=4
|
||||
live = d.feed(enc("d155070105025505080003000400"), now)[0]
|
||||
assert isinstance(live, LiveReading)
|
||||
assert (live.temperature, live.status, live.soc, live.voltage) == (-5, 2, 0x55, 12.88)
|
||||
assert (live.rapid_acceleration, live.rapid_deceleration) == (3, 4)
|
||||
assert d.feed(enc("d15507ff")) == []
|
||||
|
||||
assert d.feed(enc("d155050003"), now) == []
|
||||
# newest first: leading zeros, stop record + 4 trailer bytes, plain record
|
||||
assert d.feed(enc("00000000" + "50055194" + "00030004" + "4f050140"), now) == []
|
||||
# a live frame in the middle of a transfer stays out of the history
|
||||
assert isinstance(d.feed(enc("d155070005005505080000000000"), now)[0], LiveReading)
|
||||
# negative-temperature start record, then the end marker
|
||||
block = d.feed(enc("4e0000aa" + "fffefe00"), now)[0]
|
||||
assert isinstance(block, HistoryBlock)
|
||||
r = block.records
|
||||
assert [x.voltage for x in r] == [12.48, 12.64, 12.80] # oldest first
|
||||
assert [x.mark for x in r] == ["start", None, "stop"]
|
||||
assert (r[2].soc, r[2].temperature) == (0x55, 0x19)
|
||||
assert (r[0].soc, r[0].temperature) == (0, -0x0a)
|
||||
base = _history_base(now)
|
||||
assert datetime.fromtimestamp(base).minute % 2 == 1
|
||||
assert r[2].time == datetime.fromtimestamp(base, timezone.utc)
|
||||
assert (r[2].time - r[0].time).total_seconds() == 240
|
||||
|
||||
# back to normal once the block ended
|
||||
assert d.feed(enc("d155050000"), now)[0] == HistoryBlock([])
|
||||
|
||||
try:
|
||||
d.feed(b"\x00" * 15)
|
||||
raise AssertionError("short frame accepted")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
test()
|
||||
print("ok")
|
||||
192
findings/tools/dex.py
Normal file
192
findings/tools/dex.py
Normal file
@@ -0,0 +1,192 @@
|
||||
"""Minimal read-only DEX parser + Dalvik disassembler (stdlib only).
|
||||
usage: dex.py strings <apk> <regex> | classes <apk> <regex> | dis <apk> <class-regex> [method-regex]
|
||||
"""
|
||||
import re, struct, sys, zipfile
|
||||
|
||||
|
||||
def uleb(b, o):
|
||||
r = s = 0
|
||||
while True:
|
||||
c = b[o]; o += 1
|
||||
r |= (c & 0x7f) << s; s += 7
|
||||
if not c & 0x80:
|
||||
return r, o
|
||||
|
||||
|
||||
class Dex:
|
||||
def __init__(s, b):
|
||||
s.b = b
|
||||
u = lambda off: struct.unpack_from('<I', b, off)[0]
|
||||
s.nstr, s.ostr = u(0x38), u(0x3c)
|
||||
s.ntyp, s.otyp = u(0x40), u(0x44)
|
||||
s.oproto = u(0x4c)
|
||||
s.nfld, s.ofld = u(0x50), u(0x54)
|
||||
s.nmth, s.omth = u(0x58), u(0x5c)
|
||||
s.ncls, s.ocls = u(0x60), u(0x64)
|
||||
|
||||
def str(s, i):
|
||||
o = struct.unpack_from('<I', s.b, s.ostr + i * 4)[0]
|
||||
_, o = uleb(s.b, o)
|
||||
e = s.b.index(0, o)
|
||||
return s.b[o:e].decode('utf-8', 'replace')
|
||||
|
||||
def typ(s, i):
|
||||
return s.str(struct.unpack_from('<I', s.b, s.otyp + i * 4)[0])
|
||||
|
||||
def fld(s, i):
|
||||
c, t, n = struct.unpack_from('<HHI', s.b, s.ofld + i * 8)
|
||||
return f'{s.typ(c)}->{s.str(n)}:{s.typ(t)}'
|
||||
|
||||
def mth(s, i):
|
||||
c, p, n = struct.unpack_from('<HHI', s.b, s.omth + i * 8)
|
||||
return f'{s.typ(c)}->{s.str(n)}'
|
||||
|
||||
def classes(s):
|
||||
for i in range(s.ncls):
|
||||
yield struct.unpack_from('<8I', s.b, s.ocls + i * 32)
|
||||
|
||||
def methods(s, cd):
|
||||
off = cd[6]
|
||||
if not off:
|
||||
return
|
||||
b = s.b
|
||||
sf, o = uleb(b, off); inf, o = uleb(b, o); dm, o = uleb(b, o); vm, o = uleb(b, o)
|
||||
for _ in range(sf + inf):
|
||||
_, o = uleb(b, o); _, o = uleb(b, o)
|
||||
for n in (dm, vm):
|
||||
idx = 0
|
||||
for _ in range(n):
|
||||
d, o = uleb(b, o); _, o = uleb(b, o); code, o = uleb(b, o)
|
||||
idx += d
|
||||
yield idx, code
|
||||
|
||||
def dis(s, code):
|
||||
b = s.b
|
||||
insns = struct.unpack_from('<I', b, code + 12)[0]
|
||||
u = struct.unpack_from(f'<{insns}H', b, code + 16)
|
||||
pc, out = 0, []
|
||||
while pc < insns:
|
||||
w = u[pc]; op = w & 0xff; aa = w >> 8
|
||||
if w in (0x100, 0x200, 0x300): # data payloads
|
||||
if w == 0x100: l = 4 + u[pc + 1] * 2
|
||||
elif w == 0x200: l = 2 + u[pc + 1] * 4
|
||||
else:
|
||||
n = struct.unpack_from('<I', b, code + 16 + (pc + 2) * 2)[0]
|
||||
width = u[pc + 1]
|
||||
data = b[code + 16 + (pc + 4) * 2:code + 16 + (pc + 4) * 2 + n * width]
|
||||
out.append(f'{pc:04x} array-data w={width} {data.hex()}')
|
||||
l = 4 + (n * width + 1) // 2
|
||||
pc += l
|
||||
continue
|
||||
line, l = s.op(op, aa, u, pc)
|
||||
out.append(f'{pc:04x} {line}')
|
||||
pc += l
|
||||
return out
|
||||
|
||||
def op(s, op, aa, u, pc):
|
||||
a4, b4 = aa & 15, aa >> 4
|
||||
sx16 = lambda v: v - 65536 if v > 32767 else v
|
||||
if op == 0: return 'nop', 1
|
||||
if op in (1, 4, 7): return f'move v{a4},v{b4}', 1
|
||||
if op in (2, 5, 8): return f'move/from16 v{aa},v{u[pc+1]}', 2
|
||||
if op in (3, 6, 9): return f'move/16 v{u[pc+1]},v{u[pc+2]}', 3
|
||||
if 0xa <= op <= 0xd: return f'move-result v{aa}', 1
|
||||
if op == 0xe: return 'return-void', 1
|
||||
if 0xf <= op <= 0x11: return f'return v{aa}', 1
|
||||
if op == 0x12: return f'const/4 v{a4},{(b4 ^ 8) - 8}', 1
|
||||
if op == 0x13: return f'const/16 v{aa},{sx16(u[pc+1])} ', 2
|
||||
if op == 0x14: return f'const v{aa},0x{u[pc+1] | u[pc+2] << 16:x}', 3
|
||||
if op == 0x15: return f'const/high16 v{aa},0x{u[pc+1] << 16:x}', 2
|
||||
if op == 0x16: return f'const-wide/16 v{aa},{sx16(u[pc+1])}', 2
|
||||
if op == 0x17: return f'const-wide/32 v{aa},0x{u[pc+1] | u[pc+2] << 16:x}', 3
|
||||
if op == 0x18: return f'const-wide v{aa},0x{u[pc+1] | u[pc+2] << 16 | u[pc+3] << 32 | u[pc+4] << 48:x}', 5
|
||||
if op == 0x19: return f'const-wide/high16 v{aa},0x{u[pc+1]:x}<<48', 2
|
||||
if op == 0x1a: return f'const-string v{aa},{s.str(u[pc+1])!r}', 2
|
||||
if op == 0x1b: return f'const-string v{aa},{s.str(u[pc+1] | u[pc+2] << 16)!r}', 3
|
||||
if op == 0x1c: return f'const-class v{aa},{s.typ(u[pc+1])}', 2
|
||||
if op in (0x1d, 0x1e): return f'monitor v{aa}', 1
|
||||
if op == 0x1f: return f'check-cast v{aa},{s.typ(u[pc+1])}', 2
|
||||
if op == 0x20: return f'instance-of v{a4},v{b4},{s.typ(u[pc+1])}', 2
|
||||
if op == 0x21: return f'array-length v{a4},v{b4}', 1
|
||||
if op == 0x22: return f'new-instance v{aa},{s.typ(u[pc+1])}', 2
|
||||
if op == 0x23: return f'new-array v{a4},v{b4},{s.typ(u[pc+1])}', 2
|
||||
if op == 0x24: return f'filled-new-array {s.typ(u[pc+1])}', 3
|
||||
if op == 0x25: return f'filled-new-array/range {s.typ(u[pc+1])}', 3
|
||||
if op == 0x26: return f'fill-array-data v{aa},+{u[pc+1] | u[pc+2] << 16:x}', 3
|
||||
if op == 0x27: return f'throw v{aa}', 1
|
||||
if op == 0x28: return f'goto {pc + ((aa ^ 0x80) - 0x80):04x}', 1
|
||||
if op == 0x29: return f'goto {pc + sx16(u[pc+1]):04x}', 2
|
||||
if op == 0x2a: return 'goto/32', 3
|
||||
if op in (0x2b, 0x2c): return f'switch v{aa}', 3
|
||||
if 0x2d <= op <= 0x31: return f'cmp v{aa},v{u[pc+1] & 255},v{u[pc+1] >> 8}', 2
|
||||
if 0x32 <= op <= 0x37:
|
||||
return f"if-{['eq','ne','lt','ge','gt','le'][op-0x32]} v{a4},v{b4},{pc + sx16(u[pc+1]):04x}", 2
|
||||
if 0x38 <= op <= 0x3d:
|
||||
return f"if-{['eq','ne','lt','ge','gt','le'][op-0x38]}z v{aa},{pc + sx16(u[pc+1]):04x}", 2
|
||||
if 0x3e <= op <= 0x43 or 0x73 == op or 0x79 <= op <= 0x7a or 0xe3 <= op: return f'unused {op:x}', 1
|
||||
if 0x44 <= op <= 0x4a: return f'aget v{aa},v{u[pc+1] & 255},v{u[pc+1] >> 8}', 2
|
||||
if 0x4b <= op <= 0x51: return f'aput v{aa},v{u[pc+1] & 255},v{u[pc+1] >> 8}', 2
|
||||
if 0x52 <= op <= 0x58: return f'iget v{a4},v{b4},{s.fld(u[pc+1])}', 2
|
||||
if 0x59 <= op <= 0x5f: return f'iput v{a4},v{b4},{s.fld(u[pc+1])}', 2
|
||||
if 0x60 <= op <= 0x66: return f'sget v{aa},{s.fld(u[pc+1])}', 2
|
||||
if 0x67 <= op <= 0x6d: return f'sput v{aa},{s.fld(u[pc+1])}', 2
|
||||
if 0x6e <= op <= 0x72:
|
||||
n = b4; g = a4; w = u[pc+2]
|
||||
regs = [w & 15, w >> 4 & 15, w >> 8 & 15, w >> 12, g][:n]
|
||||
return f"invoke {s.mth(u[pc+1])} ({','.join('v%d' % r for r in regs)})", 3
|
||||
if 0x74 <= op <= 0x78:
|
||||
return f'invoke/range {s.mth(u[pc+1])} (v{u[pc+2]}..+{aa})', 3
|
||||
if 0x7b <= op <= 0x8f: return f'unop{op:x} v{a4},v{b4}', 1
|
||||
bin_ = ['add','sub','mul','div','rem','and','or','xor','shl','shr','ushr']
|
||||
if 0x90 <= op <= 0xaf:
|
||||
n = bin_[(op - 0x90) % 11] if op < 0xa6 else 'fp'
|
||||
return f'{n} v{aa},v{u[pc+1] & 255},v{u[pc+1] >> 8}', 2
|
||||
if 0xb0 <= op <= 0xcf:
|
||||
n = bin_[(op - 0xb0) % 11] if op < 0xc6 else 'fp'
|
||||
return f'{n}/2addr v{a4},v{b4}', 1
|
||||
if 0xd0 <= op <= 0xd7:
|
||||
n = ['add','rsub','mul','div','rem','and','or','xor'][op - 0xd0]
|
||||
return f'{n}/lit16 v{a4},v{b4},{sx16(u[pc+1])}', 2
|
||||
if 0xd8 <= op <= 0xe2:
|
||||
n = ['add','rsub','mul','div','rem','and','or','xor','shl','shr','ushr'][op - 0xd8]
|
||||
lit = u[pc+1] >> 8
|
||||
return f'{n}/lit8 v{aa},v{u[pc+1] & 255},{(lit ^ 0x80) - 0x80}', 2
|
||||
return f'?? {op:x}', 1
|
||||
|
||||
|
||||
def dexes(apk):
|
||||
z = zipfile.ZipFile(apk)
|
||||
for n in sorted(z.namelist()):
|
||||
if re.fullmatch(r'classes\d*\.dex', n):
|
||||
yield n, Dex(z.read(n))
|
||||
|
||||
|
||||
def main():
|
||||
cmd, apk, rx = sys.argv[1:4]
|
||||
rx = re.compile(rx)
|
||||
for name, d in dexes(apk):
|
||||
if cmd == 'strings':
|
||||
for i in range(d.nstr):
|
||||
t = d.str(i)
|
||||
if rx.search(t) and len(t) < 300:
|
||||
print(name, repr(t))
|
||||
elif cmd == 'classes':
|
||||
for cd in d.classes():
|
||||
t = d.typ(cd[0])
|
||||
if rx.search(t):
|
||||
print(name, t)
|
||||
elif cmd == 'dis':
|
||||
mrx = re.compile(sys.argv[4]) if len(sys.argv) > 4 else None
|
||||
for cd in d.classes():
|
||||
t = d.typ(cd[0])
|
||||
if not rx.search(t):
|
||||
continue
|
||||
for idx, code in d.methods(cd):
|
||||
m = d.mth(idx)
|
||||
if code and (not mrx or mrx.search(m)):
|
||||
print('\n###', m)
|
||||
print('\n'.join(d.dis(code)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
26
findings/tools/gen.py
Normal file
26
findings/tools/gen.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import sys,dataclasses,json
|
||||
sys.path.insert(0,sys.argv[1])
|
||||
from bm2.bm200 import *
|
||||
from bm2.bm200 import BM200_KEY,history_command
|
||||
from bm2.encryption import encrypt
|
||||
E=lambda h:encrypt(bytes.fromhex(h),BM200_KEY).hex()
|
||||
print("CMD")
|
||||
for n,c in [("version","d15501"),("accept","d1550200"),("live","d15507"),("hist10",history_command(10).hex()),("hist22320",history_command(22320).hex()),("hist51840",history_command(51840).hex())]:
|
||||
print(n,c,E(c))
|
||||
def show(evs):
|
||||
for e in evs:
|
||||
d=dataclasses.asdict(e); print(" ->",type(e).__name__,json.dumps(d,default=lambda x:x.isoformat()))
|
||||
now=1700000000.0
|
||||
def run(name,frames,t=now):
|
||||
print(name); d=BM200Decoder()
|
||||
for f in frames:
|
||||
print(" plain",f); print(" cipher",E(f)); show(d.feed(bytes.fromhex(E(f)),t))
|
||||
run("version",["d15501010700"])
|
||||
run("ack",["d1550200"])
|
||||
run("liveack",["d15507ff"])
|
||||
run("live",["d155070019025505080003000400"])
|
||||
run("liveneg",["d155070105025505080003000400"])
|
||||
run("histempty",["d155050000"])
|
||||
run("hist",["d155050003","00000000"+"50055194"+"00030004"+"4f050140","d155070019005505080000000000","4e0000aa"+"fffefe00"])
|
||||
run("histeven",["d155050001","50055190fffefe00"],1700000040.0)
|
||||
run("feskip",["d155050001","fefefe00","50055190fffefe00"])
|
||||
Reference in New Issue
Block a user