From 737580ac1bbb2b1feb5638bedd2ee0e74b4f57f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Wed, 7 Aug 2024 14:57:20 +0200 Subject: [PATCH 1/2] added unpacking plugin for alphanetworks IP cam firmware --- .../unpacking/alphanetworks/__init__.py | 0 .../unpacking/alphanetworks/code/__init__.py | 0 .../alphanetworks/code/alphanetworks.py | 80 +++++++++++++++++++ .../unpacking/alphanetworks/test/__init__.py | 0 .../alphanetworks/test/data/test_sample | 11 +++ .../test/data/test_sample_inverted | 7 ++ .../test/test_plugin_alphanetworks.py | 33 ++++++++ pyproject.toml | 2 +- 8 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 fact_extractor/plugins/unpacking/alphanetworks/__init__.py create mode 100644 fact_extractor/plugins/unpacking/alphanetworks/code/__init__.py create mode 100644 fact_extractor/plugins/unpacking/alphanetworks/code/alphanetworks.py create mode 100644 fact_extractor/plugins/unpacking/alphanetworks/test/__init__.py create mode 100644 fact_extractor/plugins/unpacking/alphanetworks/test/data/test_sample create mode 100644 fact_extractor/plugins/unpacking/alphanetworks/test/data/test_sample_inverted create mode 100644 fact_extractor/plugins/unpacking/alphanetworks/test/test_plugin_alphanetworks.py diff --git a/fact_extractor/plugins/unpacking/alphanetworks/__init__.py b/fact_extractor/plugins/unpacking/alphanetworks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fact_extractor/plugins/unpacking/alphanetworks/code/__init__.py b/fact_extractor/plugins/unpacking/alphanetworks/code/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fact_extractor/plugins/unpacking/alphanetworks/code/alphanetworks.py b/fact_extractor/plugins/unpacking/alphanetworks/code/alphanetworks.py new file mode 100644 index 00000000..43d1e5fa --- /dev/null +++ b/fact_extractor/plugins/unpacking/alphanetworks/code/alphanetworks.py @@ -0,0 +1,80 @@ +import string +from base64 import b64decode +from pathlib import Path + +PRINTABLE_CHARS = set(string.printable.encode()) + +NAME = 'alphanetworks' +MIME_PATTERNS = ['firmware/alphanetworks'] +VERSION = '0.1.0' + +FW_BOUNDARY_STR = b'=== Firmware Boundary ===\n' +DDPACK_BOUNDARY_STR = b'=== ddPack Boundary ===\n' +BASE64_STR = b'begin-base64\n' + + +def unpack_function(file_path: str, tmp_dir: str) -> dict: + extraction_dir = Path(tmp_dir) + contents = Path(file_path).read_bytes() + script_end, base64_start = None, None + + ddpack_offset = contents.find(DDPACK_BOUNDARY_STR) + if ddpack_offset != -1: + script_end = ddpack_offset + ddpack_offset += len(DDPACK_BOUNDARY_STR) + + base64_string_index = contents.find(b'begin-base64') + if base64_string_index != -1: + base64_start = base64_string_index + contents[base64_string_index:].find(b'\n') + 1 + # after the base64 block there is a line with "====" + base64_end = base64_start + contents[base64_start:].find(b'\n====\n') + # add some padding at the end in case it is missing + elf_content = b64decode(contents[base64_start:base64_end] + b'===') + elf_file = extraction_dir / 'ddPack.elf' + elf_file.write_bytes(elf_content) + + fw_offset = contents.find(FW_BOUNDARY_STR) + if script_end is None: + script_end = fw_offset + fw_offset += len(FW_BOUNDARY_STR) + + script = contents[:script_end] + script_file = extraction_dir / 'script.sh' + script_file.write_bytes(script) + + fw_content = contents[fw_offset:] + # for whatever reason, the samples beginning with "REDSONIC" are bit-wise inverted, so we + # undo that before saving the firmware image to file + inverted = _fw_is_inverted(fw_content) + if inverted: + fw_content = _invert_bytes(fw_content) + + fw_file = extraction_dir / 'firmware.bin' + fw_file.write_bytes(fw_content) + + return { + 'output': { + 'firmware offset': fw_offset, + 'ddpack offset': ddpack_offset if ddpack_offset != -1 else None, + 'base64 offset': base64_start, + 'inverted': inverted, + } + } + + +def _fw_header_contains_name(file_name: bytes) -> bool: + return file_name and all(c in PRINTABLE_CHARS for c in file_name) + + +def _fw_is_inverted(fw_content: bytes) -> bool: + return fw_content.startswith(_invert_bytes(b'REDSONIC')) + + +def _invert_bytes(byte_str: bytes) -> bytes: + return bytes([c ^ 0xFF for c in byte_str]) + + +# ----> Do not edit below this line <---- +def setup(unpack_tool): + for item in MIME_PATTERNS: + unpack_tool.register_plugin(item, (unpack_function, NAME, VERSION)) diff --git a/fact_extractor/plugins/unpacking/alphanetworks/test/__init__.py b/fact_extractor/plugins/unpacking/alphanetworks/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fact_extractor/plugins/unpacking/alphanetworks/test/data/test_sample b/fact_extractor/plugins/unpacking/alphanetworks/test/data/test_sample new file mode 100644 index 00000000..a149ae0a --- /dev/null +++ b/fact_extractor/plugins/unpacking/alphanetworks/test/data/test_sample @@ -0,0 +1,11 @@ +#!/bin/sh +VENDOR="foo" +HWBOARD="bar" +HWVERSION="1.0.0" + +=== ddPack Boundary === +begin-base64 755 /dev/stdout +Zm9vYmFyMTIzNA +==== +=== Firmware Boundary === +firmware_foobar diff --git a/fact_extractor/plugins/unpacking/alphanetworks/test/data/test_sample_inverted b/fact_extractor/plugins/unpacking/alphanetworks/test/data/test_sample_inverted new file mode 100644 index 00000000..f8f62519 --- /dev/null +++ b/fact_extractor/plugins/unpacking/alphanetworks/test/data/test_sample_inverted @@ -0,0 +1,7 @@ +#!/bin/sh +VENDOR="foo" +HWBOARD="bar" +HWVERSION="1.0.0" + +=== Firmware Boundary === +­ēģŦ°ąļŧ™ˆ ™ž \ No newline at end of file diff --git a/fact_extractor/plugins/unpacking/alphanetworks/test/test_plugin_alphanetworks.py b/fact_extractor/plugins/unpacking/alphanetworks/test/test_plugin_alphanetworks.py new file mode 100644 index 00000000..a11fe2f1 --- /dev/null +++ b/fact_extractor/plugins/unpacking/alphanetworks/test/test_plugin_alphanetworks.py @@ -0,0 +1,33 @@ +from pathlib import Path + +from test.unit.unpacker.test_unpacker import TestUnpackerBase + +TEST_DATA_DIR = Path(__file__).parent / 'data' + + +class TestAlphaNetworksUnpacker(TestUnpackerBase): + def test_unpacker_selection(self): + self.check_unpacker_selection('firmware/alphanetworks', 'alphanetworks') + + def test_extraction_normal(self): + input_file = TEST_DATA_DIR / 'test_sample' + assert input_file.is_file() + unpacked_files, meta_data = self.unpacker.extract_files_from_file(input_file, self.tmp_dir.name) + assert meta_data['output']['base64 offset'] == 109 + assert meta_data['output']['inverted'] is False + assert len(unpacked_files) == 3 + for file in ('script.sh', 'firmware.bin', 'ddPack.elf'): + assert f'{self.tmp_dir.name}/{file}' in unpacked_files + assert Path(f'{self.tmp_dir.name}/ddPack.elf').read_bytes().startswith(b'foobar') + assert Path(f'{self.tmp_dir.name}/firmware.bin').read_bytes() == b'firmware_foobar\n' + + def test_extraction_inverted(self): + input_file = TEST_DATA_DIR / 'test_sample_inverted' + assert input_file.is_file() + unpacked_files, meta_data = self.unpacker.extract_files_from_file(input_file, self.tmp_dir.name) + assert meta_data['output']['ddpack offset'] is None + assert meta_data['output']['inverted'] is True + assert len(unpacked_files) == 2 + for file in ('script.sh', 'firmware.bin'): + assert f'{self.tmp_dir.name}/{file}' in unpacked_files + assert Path(f'{self.tmp_dir.name}/firmware.bin').read_bytes().endswith(b'fw_foobar') diff --git a/pyproject.toml b/pyproject.toml index 748a318a..9f2963f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ ignore = [ fixable = ["ALL"] [tool.ruff.lint.per-file-ignores] -"test*.py" = ["ARG002"] +"test*.py" = ["ARG002", "PLR2004"] "conftest.py" = ["ARG002"] [tool.ruff.lint.isort] From 35222df4a1a7859ac6d331d57a386753ed91fdb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Thu, 23 Jan 2025 15:19:13 +0100 Subject: [PATCH 2/2] requested review changes #146 --- .../plugins/unpacking/alphanetworks/code/alphanetworks.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/fact_extractor/plugins/unpacking/alphanetworks/code/alphanetworks.py b/fact_extractor/plugins/unpacking/alphanetworks/code/alphanetworks.py index 43d1e5fa..e90b4726 100644 --- a/fact_extractor/plugins/unpacking/alphanetworks/code/alphanetworks.py +++ b/fact_extractor/plugins/unpacking/alphanetworks/code/alphanetworks.py @@ -62,10 +62,6 @@ def unpack_function(file_path: str, tmp_dir: str) -> dict: } -def _fw_header_contains_name(file_name: bytes) -> bool: - return file_name and all(c in PRINTABLE_CHARS for c in file_name) - - def _fw_is_inverted(fw_content: bytes) -> bool: return fw_content.startswith(_invert_bytes(b'REDSONIC'))