|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import struct |
| 4 | +from dataclasses import dataclass |
| 5 | +from pathlib import Path |
| 6 | + |
| 7 | +NAME = 'd-link dcs' |
| 8 | +MIME_PATTERNS = ['firmware/dlink-dcs', 'firmware/dlink-dcs-enc'] |
| 9 | +VERSION = '0.1.0' |
| 10 | + |
| 11 | +# loosely based on the "D-Link Firmware unpacker V1.0" from http://www.hardwarefetish.com |
| 12 | + |
| 13 | +HEADER_MAGIC = 0xAA7EC55B |
| 14 | +HEADER_SIZE = 0x10 |
| 15 | +SECTION_HEADER_MAGIC = 0xA55A |
| 16 | +SECTION_HEADER_SIZE = 0x40 |
| 17 | +# fmt: off |
| 18 | +RANDOM_TABLES = [ |
| 19 | + [ |
| 20 | + 0x07CC, 0x33A8, 0xAEA8, 0x5A00, 0xDD42, 0x518E, 0x45B2, 0x0DED, 0x6D05, 0xD447, 0x9C3F, 0x4678, 0xB113, 0x7BF8, |
| 21 | + 0x4510, 0xB3F8, 0x9C21, 0xFD90, 0x8055, 0xB43A, 0xCE09, 0x5294, 0x6100, 0xD7DC, 0x989A, 0x4EC7, 0xC3D3, 0xFF7E, |
| 22 | + 0xEF08, 0xA4DA, 0xD9D3, 0xF6D4, 0xD883, 0x887B, 0x50D4, 0xB5C5, 0xDA09, 0x9687, 0xC3B3, 0x470F, 0x6ACE, 0x5FF2, |
| 23 | + 0x8D87, 0x1BE1, 0xDBEA, 0xD297, 0xCFDA, 0x780B, 0xD027, 0x502F, 0x2C45, 0x9E31, 0xA2C3, 0x8D45, 0x760D, 0x3B5D, |
| 24 | + 0xDC0D, 0x39E1, 0x3ADB, 0xCB15, 0xDEBB, 0x14AE, 0xC1E9, 0xB73E, 0x9D2A, 0x12BD, 0x6D04, 0x7733, 0xA944, 0x30B7, |
| 25 | + 0xBE42, 0x1413, 0x90A9, 0x4BC9, 0x2FF4, 0x6C93, 0x1E60, 0xFFCE, 0xE49E, 0xEE88, 0x4FFE, 0x10E4, 0x8CB9, 0xF2C1, |
| 26 | + 0x9E29, 0x02C6, 0x2E1F, 0x7A36, 0x3CA7, 0x68FA, 0x454B, 0x1B63, 0x7DA9, 0x0734, 0xD2A1, 0x1AD3, 0x19F2, 0x3FA5, |
| 27 | + 0x9206, 0xC336, 0x705C, 0x5049, 0xD749, 0x0105, 0x9C12, 0x073E, 0x6D98, 0xBA73, 0x070C, 0x5237, 0xA8FB, 0x570A, |
| 28 | + 0x631B, 0x35B4, 0x49CC, 0x0144, 0x387A, 0x77EB, 0x7B7B, 0x7522, 0xE0E5, 0xC0C6, 0x9085, 0x5E8E, 0xC7FB, 0x6326, |
| 29 | + 0x7961, 0xE1ED, 0xA2CC, 0x0B68, 0xA523, 0x1328, 0x5BB1, 0x7C6D, 0x142E, 0xF7C3, 0x83AB, 0x81C6, 0xB236, 0x8AB7, |
| 30 | + 0xD3FD, 0x5B31, 0xE1C2, 0x3718, 0x90E5, 0x2B8E, 0x385D, 0xC960, 0xA379, 0xB3D8, 0x3E82, 0x845E, 0x749E, 0xCF07, |
| 31 | + 0xE2ED, 0x3C99, 0x322D, 0x5C4E, 0x1E86, 0xD4F9, 0x67B6, 0xC3AA, 0xE822, 0xC367, 0x4017, 0xFC50, 0xBB2B, 0xC3C2, |
| 32 | + 0x7E16, 0x6D61, 0x4E79, 0x5214, 0xC893, 0x303B, 0x892C, 0x5978, 0x5BC9, 0xC189, 0x22D8, 0xFF42, 0x7561, 0x615A, |
| 33 | + 0x83A1, 0xEA00, 0x3061, 0x668E, 0x2699, 0x628F, 0xC2DC, 0x4520, 0x3788, 0x2A93, 0x08CA, 0x1FAA, 0xEDFA, 0x48E1, |
| 34 | + 0x1BFA, 0xA925, 0x0CA3, |
| 35 | + ], |
| 36 | + [ |
| 37 | + 0x9A11, 0x1687, 0x5B1C, 0xEC25, 0xDF1A, 0x8B58, 0x7551, 0x3892, 0xE721, 0x36DB, 0x5B6B, 0xE664, 0xAC3C, 0xBCC5, |
| 38 | + 0x6A05, 0x963C, 0xED27, 0xD093, 0xBCD6, 0x4FB6, 0x936F, 0x01F6, 0x873E, 0xBE02, 0x0AC0, 0xA6E9, 0xABFD, 0x53A1, |
| 39 | + 0xC2E3, 0x5522, 0x6044, 0x5CF4, 0x6BA9, 0xBB60, 0x4919, 0x4AC3, 0x46B8, 0xBE6B, 0x8356, 0x2DDA, 0xF546, 0xDEC1, |
| 40 | + 0x143E, 0xA182, 0x9B86, 0x7E43, 0x37BF, 0x88AD, 0x4ED6, 0xF495, 0xD863, 0xE245, 0xF68B, 0x5FA2, 0xA048, 0x014B, |
| 41 | + 0x068B, 0x4C45, 0x54EC, 0xC96E, 0xA167, 0xB530, 0x2663, 0x0D11, 0x7090, 0x6F7C, 0x57D4, 0xB749, 0x2DE7, 0xDB2A, |
| 42 | + 0xE523, 0x232D, 0xB9EB, 0xF961, 0xC4B0, 0x5572, 0x77A4, 0xFC6F, 0xDE1F, 0xC67A, 0xF104, 0xB683, 0xA8BF, 0xE78F, |
| 43 | + 0x1625, 0x4907, 0xE8DA, 0x1CB0, 0x954C, 0x3DC6, 0xE61E, 0x36B4, 0xF2F6, 0x0C81, 0x43C5, 0x6386, 0x7BFE, 0x9B99, |
| 44 | + 0x1ACF, 0xA9E5, 0x76C4, 0xFFF2, 0xCD13, 0x30AF, 0xF953, 0x91C3, 0x8621, 0x70F7, 0x8E32, 0x6441, 0x3771, 0x7F36, |
| 45 | + 0x1AC4, 0xE031, 0x66C5, 0x30E9, 0x2938, 0x4F9F, 0x4D99, 0xBE85, 0x8D65, 0x33B7, 0xF539, 0x805B, 0x4039, 0x38FE, |
| 46 | + 0xE3E1, 0xBC37, 0xD497, 0xFEB1, 0x661C, 0x4B5B, 0xFEA3, 0x332F, 0x7C0B, 0xF7F7, 0xC4F2, 0x022C, 0x68EE, 0x5324, |
| 47 | + 0x666D, 0xA060, 0xD25A, 0x8131, 0x8091, 0x391F, 0xB21A, 0xA9C9, 0x88BE, 0xFFB3, 0x684E, 0x1623, 0x336B, 0x5D87, |
| 48 | + 0x967E, 0x73A4, 0x9685, 0x7A60, 0x2FDB, 0x6B1D, 0x7911, 0x95F7, 0xB678, 0x77B4, 0xC927, 0x3283, 0x6FAB, 0x8E19, |
| 49 | + 0x34B0, 0xD89A, 0xE13E, 0x9B1D, 0x78FA, 0xB398, 0x1C4F, 0xF98B, 0xECB8, 0xCE69, 0xA354, 0x7576, 0xCE1D, 0x0BA3, |
| 50 | + 0x8B9A, 0x0188, 0x692A, 0x2218, 0x752C, 0xFFB0, 0x9C78, 0xA507, 0x6ACD, 0x1589, 0x3AFE, 0x2145, 0x8D3E, 0x0425, |
| 51 | + 0x53C9, 0xFCE9, 0x923F, 0x8879, 0xD583, 0x737D, 0x2396, 0x4E7D, 0x2715, 0x3FE5, 0x4808, 0x13CD, 0x0E4F, 0xEB5D, |
| 52 | + 0x8944, |
| 53 | + ], |
| 54 | + [ |
| 55 | + 0xDC6C, 0xF700, 0x14DE, 0xDDF4, 0x602A, 0x36F6, 0x5320, 0x5FDA, 0xD36F, 0xF827, 0xCAA7, 0xE8F8, 0x3325, 0xEBED, |
| 56 | + 0x7636, 0x374B, 0x3FB6, 0x7320, 0xC98A, 0xC82F, 0x48A3, 0x3D07, 0xEBC5, 0x9721, 0x641C, 0x2BAB, 0xDF29, 0x77EA, |
| 57 | + 0x39FA, 0xCA86, 0x012E, 0x1666, 0xC186, 0x160C, 0xF45A, 0x21B1, 0x4D02, 0x477A, 0x818B, 0x2071, 0x3FA1, 0x4C33, |
| 58 | + 0x096A, 0x72C6, 0x3820, 0x7FA0, 0xAA11, 0x77D6, 0xF2C0, 0x739B, 0x4005, 0x3B64, 0xB0A2, 0x2BCA, 0xD285, 0x14BF, |
| 59 | + 0x5775, 0xB1AE, 0x8CA9, 0x916F, 0x7C35, 0x8DD7, 0xA7D5, 0x3DBB, 0xA3E3, 0x9C2F, 0x5F6C, 0xF0E5, 0xE3A9, 0xE0F8, |
| 60 | + 0x1157, 0x234A, 0x2D2B, 0x1AC1, 0x9611, 0x654B, 0x9A61, 0x4022, 0xDD21, 0x8D22, 0xB3BE, 0x1D26, 0xC886, 0x6460, |
| 61 | + 0x48F0, 0x9B0B, 0x791F, 0xA066, 0x4CB9, 0x05C8, 0x31D5, 0xC8EE, 0x939F, 0xD9AB, 0x06AA, 0x3782, 0x75DA, 0x6616, |
| 62 | + 0x2868, 0x5984, 0x470E, 0x39BF, 0x7CCE, 0x7439, 0x5480, 0x12DF, 0xD984, 0xEEE1, 0x5302, 0xB6A5, 0x7C03, 0x06C0, |
| 63 | + 0xD3CB, 0x4489, 0x6B20, 0x1CBC, 0xDF94, 0xE440, 0xBD22, 0x2C4E, 0xEA08, 0xEEF7, 0xF53C, 0x7DA8, 0xC8A2, 0xFBE6, |
| 64 | + 0xB52A, 0x3E7D, 0x61FD, 0xDD92, 0x9801, 0xA90B, 0x1751, 0x14CF, 0x1D45, 0x6BD1, 0x27AF, 0xF6C9, 0x5AB3, 0x7AB1, |
| 65 | + 0xAD6F, 0xD6B6, 0x8171, 0x813A, 0x1B40, 0xEC91, 0x9DF6, 0xFAD4, 0xD0D1, 0x5B18, 0x2722, 0xBADA, 0x4A10, 0x1C5F, |
| 66 | + 0x3882, 0x12B2, 0x1845, 0xEDAC, 0x512F, 0x7A42, 0xCB3F, 0xE930, 0x234E, 0xE290, 0xFE00, 0x4093, 0x4E62, 0x25AF, |
| 67 | + 0x375C, 0xA915, 0xA060, 0xE4CB, 0x7FCB, 0x21D1, 0x6606, 0x9B0B, 0x0E62, 0x03FC, 0x95E0, 0xDF34, 0x5F15, 0xBD02, |
| 68 | + 0x9A0E, 0xA925, 0xD961, 0xD290, 0xBBD7, 0xF1A7, 0xC03C, 0x0D07, 0x6BE9, 0x8B7B, 0xF637, 0x8F37, 0x6E0C, 0xF437, |
| 69 | + 0xCFCA, 0xBC6E, 0x19E6, 0x0727, 0x6583, 0xBA46, 0xEBF2, 0xE54E, 0xDC17, 0x51F8, 0x805A, 0xEA7A, 0x55F5, 0x163A, |
| 70 | + 0xC9AE, 0xB50A, 0xD33C, 0x63BC, 0x5E2F, 0xAC9E, 0x364C, 0x1A06, 0x9E45, 0xF688, 0x270D, 0x0A2E, 0x8204, |
| 71 | + ] |
| 72 | +] |
| 73 | +# fmt: on |
| 74 | +SECTION_TYPES = { |
| 75 | + 0: 'AUTO', |
| 76 | + 1: 'JFFS2', |
| 77 | + 2: 'YAFFS', |
| 78 | + 3: 'NONE', |
| 79 | +} |
| 80 | + |
| 81 | + |
| 82 | +class RandomNumberGenerator: |
| 83 | + def __init__(self): |
| 84 | + self._state = [0] * 3 |
| 85 | + |
| 86 | + def _get_rand(self, index: int) -> int: |
| 87 | + result = RANDOM_TABLES[index][self._state[index]] |
| 88 | + self._state[index] = (self._state[index] + 1) % len(RANDOM_TABLES[index]) |
| 89 | + return result |
| 90 | + |
| 91 | + def get_random(self) -> int: |
| 92 | + return self._get_rand(0) ^ self._get_rand(1) ^ self._get_rand(2) |
| 93 | + |
| 94 | + |
| 95 | +@dataclass |
| 96 | +class DlinkDcsHeader: |
| 97 | + filesize: int |
| 98 | + checksum: int |
| 99 | + payload_key: int |
| 100 | + section_count: int |
| 101 | + header_key: int |
| 102 | + |
| 103 | + @classmethod |
| 104 | + def from_bytes(cls, data: bytes, key: int) -> DlinkDcsHeader: |
| 105 | + size, checksum, payload_key, section_count, header_key = struct.unpack('<IHHHH', data) |
| 106 | + return cls( |
| 107 | + size ^ _pad_key(key), |
| 108 | + checksum ^ key, |
| 109 | + payload_key ^ key, |
| 110 | + section_count ^ key, |
| 111 | + header_key ^ key, |
| 112 | + ) |
| 113 | + |
| 114 | + def __str__(self): |
| 115 | + return ( |
| 116 | + f'====== Header ======\n' |
| 117 | + f'size: {self.filesize:,} bytes\n' |
| 118 | + f'checksum: 0x{self.checksum:x}\n' |
| 119 | + f'payload key: 0x{self.payload_key:x}\n' |
| 120 | + f'section count: {self.section_count}\n' |
| 121 | + f'header key: 0x{self.header_key:x}' |
| 122 | + ) |
| 123 | + |
| 124 | + |
| 125 | +@dataclass |
| 126 | +class SectionHeader: |
| 127 | + magic: int |
| 128 | + mtd: int |
| 129 | + section_type: int |
| 130 | + size: int |
| 131 | + offset: int |
| 132 | + padding: bytes |
| 133 | + |
| 134 | + @classmethod |
| 135 | + def from_bytes(cls, data: bytes) -> SectionHeader: |
| 136 | + values = struct.unpack('<HBBII52s', data) |
| 137 | + return cls(*values) |
| 138 | + |
| 139 | + def __str__(self): |
| 140 | + return ( |
| 141 | + f'mtd: 0x{self.mtd:x}\n' |
| 142 | + f'section_type: 0x{self.section_type:x} ({SECTION_TYPES.get(self.section_type, "unknown")})\n' |
| 143 | + f'size: {self.size:,} bytes\n' |
| 144 | + f'offset: 0x{self.offset:x}' |
| 145 | + ) |
| 146 | + |
| 147 | + |
| 148 | +def unpack_function(file_path: str, tmp_dir: str) -> dict: |
| 149 | + with Path(file_path).open(mode='rb') as fp: |
| 150 | + encoded_magic = struct.unpack('<I', fp.read(4))[0] |
| 151 | + key = _derive_key(encoded_magic) |
| 152 | + header = DlinkDcsHeader.from_bytes(fp.read(12), key) |
| 153 | + # the key should be stored in the last field (in the XOR encrypted file it should be 0) |
| 154 | + assert header.header_key == key, 'sanity check for machine code failed' |
| 155 | + payload = _decrypt_payload(fp.read(), header.payload_key ^ header.header_key) |
| 156 | + dump_log = _dump_sections(payload, header, Path(tmp_dir)) |
| 157 | + return {'output': str(header) + dump_log} |
| 158 | + |
| 159 | + |
| 160 | +def _derive_key(encoded_magic) -> int: |
| 161 | + key_full = encoded_magic ^ HEADER_MAGIC |
| 162 | + key_lower = key_full & 0xFFFF |
| 163 | + key_upper = (key_full >> 16) & 0xFFFF |
| 164 | + assert key_lower == key_upper, f'sanity check for encryption key failed: {key_full:x}' |
| 165 | + return key_lower |
| 166 | + |
| 167 | + |
| 168 | +def _pad_key(key: int) -> int: |
| 169 | + # pad 16-bit key to 32 bits |
| 170 | + return (key << 16) ^ key |
| 171 | + |
| 172 | + |
| 173 | +def _decrypt_payload(data: bytes, key: int) -> bytes: |
| 174 | + if len(data) % 2 != 0: |
| 175 | + # length must be even => pad with zero if odd |
| 176 | + data += b'\x00' |
| 177 | + rng = RandomNumberGenerator() |
| 178 | + result = bytearray(len(data)) |
| 179 | + for i in range(0, len(data), 2): |
| 180 | + decoded = rng.get_random() ^ key ^ struct.unpack_from('<H', data, i)[0] |
| 181 | + struct.pack_into('<H', result, i, decoded) |
| 182 | + return bytes(result) |
| 183 | + |
| 184 | + |
| 185 | +def _dump_sections(payload: bytes, header: DlinkDcsHeader, output_dir: Path) -> str: |
| 186 | + data_offset = header.section_count * SECTION_HEADER_SIZE |
| 187 | + log = '' |
| 188 | + for idx in range(header.section_count): |
| 189 | + section_header_offset = idx * SECTION_HEADER_SIZE |
| 190 | + section_header = SectionHeader.from_bytes( |
| 191 | + payload[section_header_offset : section_header_offset + SECTION_HEADER_SIZE] |
| 192 | + ) |
| 193 | + assert section_header.magic == SECTION_HEADER_MAGIC, 'wrong section magic' |
| 194 | + assert section_header.size < header.filesize |
| 195 | + |
| 196 | + output_file = output_dir / f'section_{idx:03d}' |
| 197 | + output_file.write_bytes(payload[data_offset : data_offset + section_header.size]) |
| 198 | + data_offset += section_header.size |
| 199 | + log += f'\n\n====== Section {idx} ======\n' + str(section_header) + f'\nsaved as {output_file.name}' |
| 200 | + return log |
| 201 | + |
| 202 | + |
| 203 | +# ----> Do not edit below this line <---- |
| 204 | +def setup(unpack_tool): |
| 205 | + for item in MIME_PATTERNS: |
| 206 | + unpack_tool.register_plugin(item, (unpack_function, NAME, VERSION)) |
0 commit comments