Skip to content
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

added unpacking plugin for alphanetworks IP cam firmware #146

Merged
merged 3 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')
Loading