2024-05-19 19:04:59 +00:00
|
|
|
#!/usr/bin/python3
|
|
|
|
import argparse
|
|
|
|
import mmap
|
|
|
|
import sys
|
|
|
|
import zlib
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
2024-05-26 19:58:01 +00:00
|
|
|
|
2024-05-19 19:04:59 +00:00
|
|
|
def main(fwfile: str, savedir: str):
|
|
|
|
# Does the file exist?
|
|
|
|
if not Path(fwfile).is_file():
|
|
|
|
print(f"Error: {fwfile} is not a file! Exiting...")
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
# Does our save dir exist?
|
|
|
|
if not Path(savedir).is_dir():
|
|
|
|
print(f"Warning: {savedir} is not a directory, attempting to create it...")
|
|
|
|
Path(savedir).mkdir(parents=True)
|
|
|
|
|
|
|
|
# Start parsing the OTA file
|
2024-05-26 19:58:01 +00:00
|
|
|
with open(fwfile, "rb+") as f:
|
2024-05-19 19:04:59 +00:00
|
|
|
# Read from disk, not memory
|
|
|
|
mm = mmap.mmap(f.fileno(), 0)
|
|
|
|
|
2024-05-26 19:58:01 +00:00
|
|
|
ubntheader = mm.read(0x104) # Read header in
|
|
|
|
ubntheadercrc = int.from_bytes(mm.read(0x4)) # Read header CRC, convert to int
|
2024-05-19 19:04:59 +00:00
|
|
|
|
|
|
|
# Do we have the UBNT header?
|
|
|
|
if ubntheader[0:4].decode("utf-8") != "UBNT":
|
2024-05-26 19:58:01 +00:00
|
|
|
print(
|
|
|
|
f"Error: {fwfile} is missing the UBNT header! Is this the right firmware file?"
|
|
|
|
)
|
2024-05-19 19:04:59 +00:00
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
# Is the header CRC valid?
|
|
|
|
if zlib.crc32(ubntheader) != ubntheadercrc:
|
2024-05-26 19:58:01 +00:00
|
|
|
print(
|
|
|
|
f"Error: {fwfile} has in incorrect CRC for it's header! Please re-download the file!"
|
|
|
|
)
|
2024-05-19 19:04:59 +00:00
|
|
|
sys.exit(1)
|
|
|
|
|
2024-05-26 19:58:01 +00:00
|
|
|
# If we are here, that's a great sign :)
|
2024-05-19 19:04:59 +00:00
|
|
|
ubnt_fw_ver_string = ubntheader[4:].decode("utf-8")
|
|
|
|
print(f"Loaded in firmware file {ubnt_fw_ver_string}")
|
|
|
|
|
|
|
|
# Start parsing out all of the files in the OTA
|
|
|
|
fcount = 1
|
|
|
|
while True:
|
2024-05-26 19:58:01 +00:00
|
|
|
file_header_offset = mm.find(b"\x46\x49\x4C\x45") # FILE in hex bye string
|
2024-05-19 19:04:59 +00:00
|
|
|
# Are we done scanning the file?
|
|
|
|
if file_header_offset == -1:
|
|
|
|
break
|
|
|
|
|
|
|
|
# We found one, seek to it
|
|
|
|
mm.seek(file_header_offset)
|
2024-05-26 19:58:01 +00:00
|
|
|
file_header = mm.read(0x38) # Entire header
|
|
|
|
file_position = file_header[
|
|
|
|
39
|
|
|
|
] # Increments with files read, can be used to validate this is a file for us
|
|
|
|
file_location = (
|
|
|
|
file_header_offset + 0x38
|
|
|
|
) # header location - header = data :)
|
2024-05-19 19:04:59 +00:00
|
|
|
|
|
|
|
# Is this a VALID file?!
|
|
|
|
if fcount == file_position:
|
2024-05-26 19:58:01 +00:00
|
|
|
file_name = (
|
|
|
|
file_header[4:33].decode("utf-8").rstrip("\x00")
|
|
|
|
) # Name/type of FILE
|
2024-05-19 19:04:59 +00:00
|
|
|
file_length = int(file_header[48:52].hex(), 16)
|
2024-05-30 23:06:33 +00:00
|
|
|
# Disabled because it's debug info and older python can't handle it
|
|
|
|
#print(
|
|
|
|
# f"{file_name} is at offset {"0x%0.2X" % file_location}, {file_length} bytes"
|
|
|
|
#)
|
2024-05-19 19:04:59 +00:00
|
|
|
# print(int(file_header[52:56].hex(), 16)) # Maybe reserved memory or partition size? We don't use this tho
|
2024-05-26 19:58:01 +00:00
|
|
|
fcount = fcount + 1 # Increment on find!
|
2024-05-19 19:04:59 +00:00
|
|
|
|
2024-05-26 19:58:01 +00:00
|
|
|
fcontents = mm.read(file_length) # Read into memory
|
|
|
|
file_footer_crc32 = mm.read(0x8)[
|
|
|
|
0:4
|
|
|
|
] # Read in tailing 8 bytes (crc32) footer, but we only want the first 4
|
2024-05-19 19:04:59 +00:00
|
|
|
|
|
|
|
# Does our calculated crc32 match the unifi footer in the img?
|
2024-05-26 19:58:01 +00:00
|
|
|
if hex(zlib.crc32(file_header + fcontents)).lstrip(
|
|
|
|
"0x"
|
|
|
|
) != file_footer_crc32.hex().lstrip("0"):
|
|
|
|
print(
|
|
|
|
f"Error: Contents of {file_name} does not match the Unifi CRC! Please re-download the file!"
|
|
|
|
)
|
|
|
|
sys.exit(1)
|
2024-05-19 19:04:59 +00:00
|
|
|
|
|
|
|
# Write file out since our mmap position is now AT the data, and we parsed the length
|
|
|
|
with open(f"{savedir}/{file_name}.bin", "wb") as wf:
|
2024-05-26 19:58:01 +00:00
|
|
|
wf.write(fcontents) # Write out file
|
2024-05-19 19:04:59 +00:00
|
|
|
print(f"{file_name} has been written to {savedir}/{file_name}.bin")
|
|
|
|
|
2024-05-26 19:58:01 +00:00
|
|
|
del fcontents # Cleanup memory for next run
|
2024-05-19 19:04:59 +00:00
|
|
|
|
|
|
|
print(f"Finished extracting the contents of {fwfile}, enjoy!")
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
parser = argparse.ArgumentParser("ubnt-fw-parse for Dream Machines")
|
|
|
|
parser.add_argument("file", help="The Ubiquiti firmware file to parse", type=str)
|
2024-05-26 19:58:01 +00:00
|
|
|
parser.add_argument(
|
|
|
|
"savedir", help="The directory to save the parsed files to", type=str
|
|
|
|
)
|
2024-05-19 19:04:59 +00:00
|
|
|
args = parser.parse_args()
|
|
|
|
main(args.file, args.savedir)
|