Smart Lock
by Pavel Knespl (capyplivl)
A BLE smart lock sends its 8-digit PIN in cleartext over an unencrypted GATT characteristic. Sniffing the unlock capture leaks the PIN directly, and the remote lock service only needs that PIN.
Solve
- Read the BLE capture for ATT writes and notifications:
tshark -r unlock_capture.pcapng -Y btatt -T fields -e btatt.handle -e btatt.opcode -e btatt.value
The interesting frames (all hex ASCII):
- handle
0x0003write3437313933383230=47193820(the PIN) - handle
0x0005notify63...6a=c3d8f2a1-7e4b-4d9c-b6f0-1a2e3c4d5f6a(server challenge) - handle
0x0008writed87d6883829e0bfece6a7c1950681762(16-byte response token) - handle
0x000anotify554e4c4f434b4544=UNLOCKED
- Confirm the PIN by reproducing the response token. The token is
MD5(pin + challenge):
python3 -c "import hashlib;print(hashlib.md5(('47193820'+'c3d8f2a1-7e4b-4d9c-b6f0-1a2e3c4d5f6a').encode()).hexdigest())"
This matches the captured token exactly, proving the PIN is 47193820.
3. (Optional) Verify the algorithm in the app. jadx/apktool are not required; unzip the APK and inspect the dex strings:
unzip -o SmartLock.apk -d apk_unzip
strings apk_unzip/classes.dex | grep -E 'AuthUtils|computeResponse|computeSHA256Digest|^MD5$|MessageDigest'
com/smartlock/app/AuthUtils exposes computeResponse. The method named computeSHA256Digest is a red herring: the only digest-algorithm literal in the dex is MD5 (no SHA-256 string exists), and the code calls MessageDigest.getInstance("MD5"). So the response is MD5(pin || challengeUuid), consistent with step 2.
4. The remote lock just checks the PIN. Run against the live instance:
from pwn import *
r = remote(HOST, PORT)
r.recvuntil(b'PIN:')
r.sendline(b'47193820')
print(r.recvall().decode())
Flag: SVIUSCG{1b3aa3e7895dfb275c671ef059b6f696}