Skip to content

Commit

Permalink
added unpacking plugin for alphanetworks IP cam firmware (#146)
Browse files Browse the repository at this point in the history
* added unpacking plugin for alphanetworks IP cam firmware
  • Loading branch information
jstucke authored Jan 23, 2025
1 parent 3a77476 commit 6833cdf
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 0 deletions.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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_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))
Empty file.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/sh
VENDOR="foo"
HWBOARD="bar"
HWVERSION="1.0.0"

=== Firmware Boundary ===
­º»¬°±¶¼™ˆ ™ž
Original file line number Diff line number Diff line change
@@ -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')

0 comments on commit 6833cdf

Please sign in to comment.