Skip to content

Commit

Permalink
feat: added unpacker for Senao/EnGenius FW containers
Browse files Browse the repository at this point in the history
  • Loading branch information
jstucke committed Feb 25, 2025
1 parent 822f7ae commit 29833ee
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 2 deletions.
4 changes: 2 additions & 2 deletions fact_extractor/install/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ def install_apt_dependencies(distribution: str):
apt_install_packages(*APT_DEPENDENCIES[distribution])


def _install_magic():
def _install_magic(version='v0.2.3'):
with OperateInDirectory(BIN_DIR):
sp.run(
[
'wget',
'--output-document',
'firmware.xz',
'https://github.com/fkie-cad/firmware-magic-database/releases/download/v0.2.2/firmware.xz',
f'https://github.com/fkie-cad/firmware-magic-database/releases/download/{version}/firmware.xz',
],
check=True,
)
Expand Down
Empty file.
Empty file.
72 changes: 72 additions & 0 deletions fact_extractor/plugins/unpacking/senao/code/senao.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from __future__ import annotations

import struct
from pathlib import Path
from typing import Iterable

NAME = 'senao'
MIME_PATTERNS = [
'firmware/senao-v1a',
'firmware/senao-v1b',
'firmware/senao-v2a',
'firmware/senao-v2b',
]
VERSION = '0.1.0'

SENAO_V1_MAGIC = bytes.fromhex('12 34 56 78')
SENAO_V2_MAGIC = bytes.fromhex('30 47 16 88')
SENAO_V1_KEY = 0x5678
SENAO_V2_KEY = 0x1688
V2_VERSION_STR = b'3047'


def unpack_function(file_path: str, tmp_dir: str) -> dict:
extraction_dir = Path(tmp_dir)
in_file = Path(file_path)
contents = in_file.read_bytes()
key, payload_offset = _find_key_and_offset(contents)

output_file = extraction_dir / f'{in_file.name}.decrypted'
output_file.write_bytes(bytes(_decrypt_payload(contents[payload_offset:], key)))

return {
'output': f'Found payload at offset {payload_offset}.\nDecrypted with key 0x{key:x}.',
}


def _find_key_and_offset(contents: bytes) -> tuple[int, int]:
if contents[92:96] == SENAO_V1_MAGIC:
key = SENAO_V1_KEY
if contents[96:99] == b'all': # firmware/senao-v1a (long header variant)
# this variant has a variable header length (because of a variable length string field)
# the length of this variable length field is stored as uint32 (be) at offset 0x84
model_name_len = struct.unpack('>I', contents[0x84 : 0x84 + 4])[0]
payload_offset = 0x88 + model_name_len
else: # firmware/senao-v1b (short header variant)
payload_offset = 0x60
else:
key = SENAO_V2_KEY
if contents[0xB4 : 0xB4 + 4] == SENAO_V2_MAGIC: # firmware/senao-v2a (long header variant)
payload_offset = 0xB8
elif contents[0x7C : 0x7C + 4] == SENAO_V2_MAGIC: # firmware/senao-v2b (short header variant)
payload_offset = 0x80
elif contents[0x66 : 0x66 + 4] == V2_VERSION_STR: # firmware/senao-v2a with different key
key = int.from_bytes(contents[0xB4 : 0xB4 + 4], byteorder='big')
payload_offset = 0xB8
elif contents[0x7C : 0x7C + 4] == V2_VERSION_STR: # firmware/senao-v2b with different key
key = int.from_bytes(contents[0xB4 : 0xB4 + 4], byteorder='big')
payload_offset = 0x80
else:
raise ValueError('Invalid input data.') # The signature should make sure that this does not happen
return key, payload_offset


def _decrypt_payload(payload: bytes, key: int) -> Iterable[int]:
for i, char in enumerate(payload):
yield char ^ (key >> (i % 8)) & 0xFF


# ----> 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.
Binary file not shown.
25 changes: 25 additions & 0 deletions fact_extractor/plugins/unpacking/senao/test/test_plugin_senao.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from pathlib import Path

from test.unit.unpacker.test_unpacker import TestUnpackerBase

TEST_DATA_DIR = Path(__file__).parent / 'data'


class TestGenericCarver(TestUnpackerBase):
def test_unpacker_selection_generic(self):
self.check_unpacker_selection('generic/carver', 'generic_carver')

def test_extraction(self):
in_file = TEST_DATA_DIR / 'testfw_1.enc'
assert in_file.is_file(), 'test file is missing'
files, meta_data = self.unpacker._extract_files_from_file_using_specific_unpacker(
str(in_file),
self.tmp_dir.name,
self.unpacker.unpacker_plugins['firmware/senao-v2b'],
)
assert len(files) == 1, 'unpacked file number incorrect'
file = Path(files[0])
contents = file.read_bytes()
assert len(contents) == 44, 'unpacked file size incorrect'
assert contents.startswith(b'foobar'), 'payload not decrypted correctly'
assert meta_data['output'].startswith('Found payload')

0 comments on commit 29833ee

Please sign in to comment.