From d97c763e93d1c8327ce8eef5117af4c5e253bf08 Mon Sep 17 00:00:00 2001 From: Jack Hadrill Date: Sun, 3 Apr 2022 23:09:05 +0100 Subject: [PATCH] Added initial code for fwutils. --- .vscode/launch.json | 16 +++++++ README.md | 36 ++++++++++++++- requirements.txt | 1 + setup.py | 18 ++++++++ src/fwutils/__init__.py | 0 src/fwutils/__main__.py | 54 ++++++++++++++++++++++ src/fwutils/definitions/__init__.py | 0 src/fwutils/definitions/spa504g.py | 71 +++++++++++++++++++++++++++++ 8 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 src/fwutils/__init__.py create mode 100644 src/fwutils/__main__.py create mode 100644 src/fwutils/definitions/__init__.py create mode 100644 src/fwutils/definitions/spa504g.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..7920ed1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Module", + "type": "python", + "request": "launch", + "module": "fwutils", + "justMyCode": true, + "args": ["x", "../spa50x-30x-7-6-2f.bin"] + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index b3c6ecd..7a7317f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,35 @@ -# fwutils-spa504g +# SPA504G firmware utilities -Firmware utilities for the Cisco SPA504G SIP phone. \ No newline at end of file +Firmware utilities for the Cisco SPA504G SIP phone. + +## Installation + +This utility requires Python 3. + +```bash +$ python3 -m venv venv +$ . ./venv/bin/activate +$ pip install -e . +``` + +## Usage + +The utility will extract files to a new `extracted` directory, in the same location as the specified file. + +``` +usage: fwutils [-h] {x} ... + +Firmware utility for the SPA504G SIP phone. + +positional arguments: + {x} Extract a file. + +options: + -h, --help show this help message and exit +``` + +### Example + +``` +$ fwutils x ./spa50x-30x-7-6-2f.bin +``` diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9d53fd5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +kaitaistruct>=0.9 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..00fa7c4 --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +from setuptools import setup, find_packages + +with open("requirements.txt") as f: + requirements = f.readlines() + +setup( + name="fwutils", + version="0.1", + description="SPA504G firmware utilities.", + author="Butlersaurus", + packages=find_packages("src"), + package_dir={"": "src"}, + include_package_data=True, + entry_points={"console_scripts": ["fwutils = fwutils.__main__:main"]}, + install_requires=requirements +) diff --git a/src/fwutils/__init__.py b/src/fwutils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fwutils/__main__.py b/src/fwutils/__main__.py new file mode 100644 index 0000000..9bc4a7a --- /dev/null +++ b/src/fwutils/__main__.py @@ -0,0 +1,54 @@ +import argparse +from io import BytesIO +import mmap +import pathlib +import zlib +from fwutils.definitions.spa504g import Spa504g + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Firmware utility for the SPA504G SIP phone.") + subparsers = parser.add_subparsers(help="Extract a file.") + + extract_parser = subparsers.add_parser("x") + extract_parser.add_argument("file", type=pathlib.Path, nargs="?", help="The file to extract.") + + args = parser.parse_args() + if not args.file: + parser.error("You must specify a file.") + + return args + + +def main(): + args = parse_args() + file: pathlib.Path = args.file + + file_handle = open(file, "rb") + with mmap.mmap(file_handle.fileno(), 0, access=mmap.ACCESS_READ) as buf: + firmware = Spa504g.from_io(BytesIO(buf)) + + output_dir = file.parent.absolute() / "extracted" + output_dir.mkdir(parents=True, exist_ok=True) + for module in firmware.modules: + output_file = file.stem + output_file += "_" + output_file += hex(module.offset) + output_file += "_" + output_file += hex(module.offset + module.length) + output_file += file.suffix + output_file = pathlib.Path(output_file) + + with open(output_dir / output_file, "wb") as f: + f.write(module.body) + + if module.compressed == 0: # 0 == True + decompressed_data = zlib.decompress(module.body, 15) + output_file = output_file.stem + "_decompressed" + output_file.suffix + with open(output_dir / output_file, "wb") as f: + f.write(decompressed_data) + + f.close() + +if __name__ == "__main__": + main() diff --git a/src/fwutils/definitions/__init__.py b/src/fwutils/definitions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fwutils/definitions/spa504g.py b/src/fwutils/definitions/spa504g.py new file mode 100644 index 0000000..1967c19 --- /dev/null +++ b/src/fwutils/definitions/spa504g.py @@ -0,0 +1,71 @@ +# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild + +from pkg_resources import parse_version +import kaitaistruct +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO + + +if parse_version(kaitaistruct.__version__) < parse_version('0.9'): + raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) + +class Spa504g(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.header = Spa504g.Header(self._io, self, self._root) + self.modules = [None] * (self.header.module_count) + for i in range(self.header.module_count): + self.modules[i] = Spa504g.Module(self._io, self, self._root) + + + class Header(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.magic = self._io.read_bytes(16) + if not self.magic == b"\x53\x6B\x4F\x73\x4D\x6F\x35\x20\x66\x49\x72\x4D\x77\x41\x72\x45": + raise kaitaistruct.ValidationNotEqualError(b"\x53\x6B\x4F\x73\x4D\x6F\x35\x20\x66\x49\x72\x4D\x77\x41\x72\x45", self.magic, self._io, u"/types/header/seq/0") + self.signature = self._io.read_bytes(32) + self.digest = self._io.read_bytes(16) + self.random_sequence = self._io.read_bytes(16) + self.header_length = self._io.read_u4be() + self.module_header_length = self._io.read_u4be() + self.file_length = self._io.read_u4be() + self.version = (self._io.read_bytes(32)).decode(u"utf8") + self.module_count = self._io.read_u4be() + self.padding = self._io.read_bytes((self.header_length - 128)) + + + class Module(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.sector_id = self._io.read_u2be() + self.compressed = self._io.read_u2be() + self.length = self._io.read_u4be() + self.offset = self._io.read_u4be() + self.digest = self._io.read_bytes(16) + self.padding = self._io.read_bytes((self._parent.header.module_header_length - 28)) + + @property + def body(self): + if hasattr(self, '_m_body'): + return self._m_body if hasattr(self, '_m_body') else None + + _pos = self._io.pos() + self._io.seek(self.offset) + self._m_body = self._io.read_bytes(self.length) + self._io.seek(_pos) + return self._m_body if hasattr(self, '_m_body') else None