-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Coordinator backup #161
Comments
If the key is unknown, I think it should be treated as unknown. Implicit key rotation or anything that can permanently affect a production network isn't something I think we should do. My suggestions:
Restoration for the XBee isn't possible either, since it is not possible to use a specific PAN ID. Is this implemented with newer firmwares? |
It is possible to restore the PAN ID using zigpy-xbee/zigpy_xbee/zigbee/application.py Line 149 in 2f2e1ae
What wasn't possible before is restoring IEEE. It is possible on newer firmwares, but is tricky (requires you to set an encryption key and use the encrypted backup file that is stored in internal flash and can be accessed using YMODEM protocol). |
Oh, wait, I confused PAN ID with Extended PAN ID! |
@Shulyaka have you tested using Centralized trust center backup mentioned on https://www.digi.com/resources/documentation/digidocs/pdfs/90001539.pdf ? |
Yes, I have, but there are two caveats:
Here is a file from my test network if you want to try it: backup_TC41A06E60.zip. The encryption key is |
@Shulyaka Could you change the encryption key to all null bytes and post another backup? I don't have an XBee 3 to test with, unfortunately, and it doesn't look like the S2C supports trust center backups. The protocol to read and write the encryption key (in addition to the actual backup data) seems to be done through XCTU, which might use undocumented serial commands and transform the key before actually writing it to the device. |
Sure, will test it |
Probably you already know that, I'll say it anyway if not. A good reverse engineering technique is to change known small changes (like change the EE from 2 to 1 and stuff like that) and compare. Probably also, changing the KY trust center encryption key and changing nothing else, will give us a perspective on where the key is located and what's located where. If I understood correctly the digi forum discussion, the file doesn't fit on AES block size, probably meaning a decryption will need to happen in portions of the backup file. Some things in that file are not related to its actual backup content, but some kind of metadata/headers/nonce/... My xbee series 3 is being used at the moment, and I only have one usb serial connected to it, so I can't use it to test. If you could upload a couple of these backups, including its changes, I can try to help. |
From your file, looks like the nonce first 4 bytes are the counter, and the 16 bytes left are the IV. The Version I guess is your firmware version, 1012. So the rest should be the encrypted file and we could try to extract its bytes and actually decrypt because the IV is known now. IV: |
I think the first four bytes of each
I haven't had much luck bruteforcing the format, unfortunately. The nonce being 128 bits makes it seem like it's the counter's initial value but no combination of endianness and no offset within the file seems to produce anything useful: from pathlib import Path
# pip install pycryptodome
from Crypto.Cipher import AES
from Crypto.Util import Counter
KEY = b'ZigBeeAlliance09ZigBeeAlliance09'
with pathlib.Path('~/Downloads/backup_TC41A06E60.xbee').expanduser().open('rb') as f:
# Version
assert f.read(9) == b'<Version>'
tag_size = int.from_bytes(f.read(4), 'little')
version = f.read(tag_size)
assert f.read(5) == b'<end>'
print('Version', version)
# Nonce
assert f.read(7) == b'<Nonce>'
tag_size = int.from_bytes(f.read(4), 'little')
nonce = f.read(tag_size)
assert f.read(5) == b'<end>'
print('Nonce', nonce)
rest = f.read()
for temp_key in (
KEY,
KEY[::-1],
b'\x00' * 32,
):
for temp_nonce in (
int.from_bytes(nonce, "little"),
int.from_bytes(nonce, "big"),
0, # Just in case :)
):
for start_offset in range(len(rest)):
for little_endian in (True, False):
ciphertext = rest[start_offset:]
ctr = Counter.new(128, initial_value=temp_nonce, little_endian=little_endian)
aes = AES.new(temp_key, AES.MODE_CTR, counter=ctr)
plaintext = aes.decrypt(ciphertext)
if bytes.fromhex('41A06E60') in plaintext or bytes.fromhex('41A06E60')[::-1] in plaintext:
print(plaintext) |
I just noticed your code while was working on something similar: import binascii
from Crypto.Cipher import AES
from Crypto.Util import Counter
with open('backup_TC41A06E60.xbee', 'rb') as f:
iv = f.read(52)[31:-5] # all between <nonce><end>
ciphertext = f.read()
enckey = b'ZigBeeAlliance09ZigBeeAlliance09'
print(f'KEY: {enckey.hex(" ")} / Size: {len(enckey)}')
print(f'IV: {iv.hex(" ")} / Size: {len(iv)}')
#print(f'CIPHERTEXT: {ciphertext.hex(" ")} / Size: {len(ciphertext)}')
iv_int = int(binascii.hexlify(iv), 16)
ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)
cipher = AES.new(enckey, AES.MODE_CTR, counter=ctr)
print(f'\nDECRYPTED: {cipher.decrypt(ciphertext).hex(" ")}') We need to know the AT parameters values so we can compare with the output and try to come with something. And some other backup files with one or other parameter changed. |
Here are two backup files from the same device with the backup key set to all zeroes (b'\x00' * 32): |
Could you also include the network key? Just so that we have some concrete bytestring to search for to test if decryption was successful. |
I played around with alternate block cipher modes, nonstandard constructions, and various CTR schemes. Unfortunately, I haven't had any luck decrypting it. I think there may be a key derivation function involved somewhere. It would be useful to MITM the serial traffic to see what exactly is being sent to the coordinator during backup/restore, as the KDF could be happening within the application itself and not on the MCU. |
I am thinking if it would be possible to disassemble the firmware... |
I played around with it but didn't have any luck with Ghidra, unfortunately 😓. The XBee3 actually runs EmberZNet and seems to be based on (or is) the EFR32 Cortex M33. The EFR32xG21 datasheet contains the RAM layout:
That's about as far as I got, however. If you want to try it out, here is the raw firmware binary. I extracted it from the firmware GBL: xbee3-fw.bin.zip |
I will try, but I am not experienced in it :) |
I'd like to start a discussion on how to back up the network key on XBee coordinators.
The issue is that on XBee devices the network key is write-only, so we have to remember it from the moment we set it. Luckily, we have a backup mechanism that we can use. Currently, however, we only use the last backup to restore the state if the device is not already configured, and optionally for verification.
Current flow:
Suggestions:
ControllerApplication.initialize()
function to use last_backup to set the initial network_info (instead of default) before trying to load it from the device. Something like:So the radio library would only update what it can, and the rest will remain from the backup.
last_backup
as a new optional parameter toControllerApplication.load_network_info and let the radio library (
zigpy-xbee`) handle it as it wishes.zigpy
, overloadControllerApplication.initialize()
function inzigpy_xbee.zigbee.ControllerApplication
, load last backup inside it and restore the network key in memory there:Do not try to read the key from backup, but perform implicit key rotation when making a new backup and the network key is not known. We can simply generate a new network key and write it to the device (and the backup), and it will be distributed to all devices in the network. Might be a bit tricky to implement it to do it only when the backup is created, and I also don't really like the idea of doing implicit actions like rotating the key when a user might not expect it.
There is also a native backup functionality in newer firmwares (the 'BK' AT command), it requires an additional investigation and is not available for legacy modules.
The text was updated successfully, but these errors were encountered: