├── README.md ├── yaffs_tags.ksy ├── yaffs.ksy ├── yealink_rom.ksy ├── yealink_crypto.py ├── yealink_rom_dump.py └── yaffs_decrypt.py /README.md: -------------------------------------------------------------------------------- 1 | # Yealink firmware reverse engineering 2 | 3 | This repository contains some script utilities used during the reverse engineering 4 | of Yealink VOIP phones firmware. 5 | 6 | For more information about this research, please read the [blogpost](https://www.synacktiv.com/posts/reverse-engineering/no-grave-but-the-sip-reversing-a-voip-phone-firmware.html) 7 | -------------------------------------------------------------------------------- /yaffs_tags.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: yaffstag 3 | endian: le 4 | encoding: ASCII 5 | title: Yaffs tag 6 | doc: | 7 | Yealink custom yaffs2 tag 8 | (c) Synacktiv 2019 9 | seq: 10 | - id: padding2 11 | size: 1 12 | - id: unknown 13 | type: u4 14 | - id: unknown_4 15 | size: 3 16 | - id: obj_id_lsb1 17 | type: u1 18 | - id: ecc2 19 | size: 7 20 | - id: obj_id_lsb2 21 | type: u1 22 | - id: obj_id_msb 23 | type: u2 24 | - id: seq_id 25 | type: u4 26 | - id: nbytes 27 | type: u2 28 | - id: unknown_and_ecc 29 | size: 5 30 | - id: unknown_2 31 | size: 4 32 | - id: crypto_magic 33 | contents: [0x15, 0x08, 0x86, 0x19] 34 | - id: unknown_3 35 | type: u2 36 | - id: unknown_and_ecc_2 37 | size: 7 38 | instances: 39 | obj_id: 40 | value: obj_id_lsb1 | obj_id_lsb2 << 8 | obj_id_msb << 16 41 | -------------------------------------------------------------------------------- /yaffs.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: yaffs 3 | endian: le 4 | encoding: ASCII 5 | title: Yaffs header 6 | doc: | 7 | Yealink partial yaffs header 8 | (c) Synacktiv 2019 9 | 10 | seq: 11 | - id: obj_type 12 | type: u4 13 | enum: obj_type 14 | - id: parent_obj_id 15 | type: u4 16 | - id: unused_checksum 17 | contents: [0xFF, 0xFF] 18 | - id: name 19 | type: strz 20 | size: 256 21 | - id: yst_mode 22 | type: u4 23 | - id: yst_uid 24 | type: u4 25 | - id: yst_gid 26 | type: u4 27 | - id: yst_atime 28 | type: u4 29 | - id: yst_mtime 30 | type: u4 31 | - id: yst_ctime 32 | type: u4 33 | - id: padding 34 | type: u2 35 | - id: file_size_low 36 | type: u4 37 | - id: equiv_id 38 | type: u4 39 | - id: alias 40 | type: 41 | switch-on: obj_type 42 | cases: 43 | 'obj_type::symlink': strz 44 | _ : u1 45 | 46 | size: 128 47 | enums: 48 | obj_type: 49 | 0x00: unknown 50 | 0x01: file 51 | 0x02: symlink 52 | 0x03: directory 53 | 0x04: hardlink 54 | 0x05: special 55 | -------------------------------------------------------------------------------- /yealink_rom.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: yealinkrom 3 | endian: be 4 | encoding: ASCII 5 | title: Yealink ROM update format 6 | 7 | doc: | 8 | Yealink phones ROM updates 9 | Both the ROM and the blocks have a similar header, 10 | but the blocks can be encrypted 11 | (c) Synacktiv 2019 12 | 13 | seq: 14 | - id: header 15 | size: 0x80 16 | type: rom_header 17 | - id: rawdata 18 | size-eos: true 19 | if: header.common_header.crypto_type != 0 20 | - id: blocks 21 | type: block 22 | repeat: expr 23 | repeat-expr: header.rom_blocks 24 | if: header.common_header.crypto_type == 0 25 | 26 | types: 27 | rom_header: 28 | seq: 29 | - id: magic 30 | contents: [0xAD, 0x24, 0xEC, 0x0B] 31 | - id: common_header 32 | type: common_header 33 | - id: rom_verify 34 | type: u4 35 | - id: rom_blocks 36 | type: u4 37 | - id: rom_dev_id 38 | type: u4 39 | - id: rom_dev_name 40 | type: strz 41 | size: 16 42 | - id: rom_oem_name 43 | type: strz 44 | size: 16 45 | - id: rom_version 46 | type: strz 47 | size: 16 48 | - id: unknown_buffer 49 | size: 8 50 | - id: rom_hw_id 51 | type: u4 52 | - id: rom_sw_id 53 | type: u4 54 | - id: rom_rom_cid_array 55 | size: 16 56 | - id: unknown_buffer_3 57 | size: 12 58 | block: 59 | seq: 60 | - id: hdr 61 | size: 0x80 62 | type: block_header 63 | - id: data 64 | size: hdr.block_total - hdr.common_header.header_size 65 | raw_data: 66 | seq: 67 | - id: data 68 | size-eos: true 69 | block_header: 70 | seq: 71 | - id: magic 72 | contents: [0xEB, 0x9F, 0x56, 0xC9] 73 | - id: common_header 74 | type: common_header 75 | - id: block_total 76 | type: u4 77 | - id: verify 78 | type: u4 79 | - id: block_type 80 | type: u4 81 | enum: block_type 82 | - id: blk_id 83 | type: u4 84 | - id: blk_name 85 | type: strz 86 | size: 16 87 | - id: blk_version 88 | size: 16 89 | - id: unknown_buffer 90 | size: 8 91 | - id: cipher 92 | type: u4 93 | - id: raw_or_file 94 | type: u4 95 | - id: unknown_buffer2 96 | size: 40 97 | common_header: 98 | seq: 99 | - id: header_size 100 | type: u4 101 | - id: header_crc 102 | type: u4 103 | - id: crypto_type 104 | type: u1 105 | - id: header_unknownflag 106 | type: u1 107 | - id: header_format 108 | type: u2 109 | - id: header_flags 110 | type: u4 111 | - id: length 112 | type: u4 113 | enums: 114 | block_type: 115 | 0x00: none 116 | 0x01: bin 117 | 0x02: tool 118 | 0x03: file 119 | 0x04: nand_raw 120 | 0x05: nand_oob 121 | 0x06: nor_raw 122 | 0x07: script 123 | 0x08: execute 124 | 0x09: emmc_raw 125 | 0x0A: unknown 126 | -------------------------------------------------------------------------------- /yealink_crypto.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | #coding: utf-8 4 | """ 5 | Yealink updates cryptographic functions 6 | (c) Synacktiv 2019 7 | 8 | Licensed under the "THE BEER-WARE LICENSE" (Revision 42): 9 | Tristan P. wrote this file. As long as you retain this notice you 10 | can do whatever you want with this stuff. If we meet some day, and you think 11 | this stuff is worth it, you can buy me a beer or coffee in return 12 | """ 13 | 14 | from binascii import unhexlify, hexlify 15 | 16 | from Crypto.Cipher import AES 17 | 18 | def get_crypto(crypto_type): 19 | """ 20 | Returns the string corresponding to the algorithm according to 21 | the update format 22 | """ 23 | output = "" 24 | if crypto_type == 0x80: 25 | output = "AES" 26 | elif crypto_type == 0x81: 27 | output = "DES" 28 | elif crypto_type == 4: 29 | output = "CYPHER4 / ADD-XOR COMB 512" 30 | elif crypto_type == 0: 31 | output = "NULL CIPHER" 32 | elif crypto_type == 1: 33 | output = "CYPHER1 / LZMA compression" 34 | elif crypto_type == 2: 35 | output = "CYPHER2 Unknown" 36 | elif crypto_type == 3: 37 | output = "CYPHER3 / SUB-XOR 256" 38 | else: 39 | raise NotImplementedError 40 | 41 | return output 42 | 43 | 44 | def cypher3_decrypt(enc): 45 | """ 46 | Method of decryption number 3 inside yealink firmwares 47 | """ 48 | xorkey = unhexlify("75A467AFD195CF5A7B871B33C147EBD6BEA73A03CEBCF02A749066B36B3E09CC") 49 | addkey = unhexlify("82180D326A92124E80172CE9A2F2222577AB49DFD5C70693699BF6415F615935") 50 | out = bytearray() 51 | mindex = 0 52 | for mindex, cur_char in enumerate(enc): 53 | tmp = cur_char - addkey[mindex % len(xorkey)] 54 | tmp = tmp ^ xorkey[mindex % len(xorkey)] 55 | tmp = tmp & 0xFF 56 | out.append(tmp) 57 | return out 58 | 59 | def cypher3_encrypt(enc): 60 | """ 61 | Referenced as "cypher3_encode" inside yealink firmwares 62 | """ 63 | xorkey = unhexlify("75A467AFD195CF5A7B871B33C147EBD6BEA73A03CEBCF02A749066B36B3E09CC") 64 | addkey = unhexlify("82180D326A92124E80172CE9A2F2222577AB49DFD5C70693699BF6415F615935") 65 | out = bytearray() 66 | mindex = 0 67 | for mindex, cur_char in enumerate(enc): 68 | tmp = cur_char ^ xorkey[mindex % len(xorkey)] 69 | tmp = tmp + addkey[mindex % len(xorkey)] 70 | tmp = tmp & 0xFF 71 | out.append(tmp) 72 | return out 73 | 74 | def cypher4_encrypt(data): 75 | """ 76 | Slightly more complicated cryptography, including a "mix" step 77 | """ 78 | c4_subkey = unhexlify("0591E065B04C9C76F99DC0689533FA12CBFE83BFF35A38A37B8169D6BE25136D3B06CC72266197B842451A96D3E3CE2BAF2DDCE81F57F4C89F8B630AA0312819") 79 | c4_xorkey = unhexlify("FD14E58F5DC66BA992F24E0C01244954713DC9372E2A5BB94AC5BCC4203404F607364716BA73FCC24F53E1395F988693A117670878C1BD3A1BB443E6AB4BD4DD") 80 | 81 | out = bytearray() 82 | for index, cur_char in enumerate(data): 83 | tmp = cur_char 84 | tmp = (cur_char - c4_subkey[index % 64]) & 0xFF 85 | tmp = tmp ^ c4_xorkey[index % 64] 86 | out.append(tmp) 87 | 88 | out_mix = [c for c in out] 89 | for j in range(0, len(data)-32, 32): 90 | start_index = j 91 | for end_index in range(j+31, start_index, -1): 92 | print("Start index: %d End index: %d" % (start_index, end_index)) 93 | start_byte = out_mix[start_index] 94 | end_byte = out_mix[end_index] 95 | 96 | out_mix[end_index] = start_byte 97 | 98 | start_index += 1 99 | out_mix[start_index] = (end_byte - start_byte)&0xFF 100 | 101 | return bytes(out_mix) 102 | 103 | def cypher4_decrypt(data): 104 | c4_subkey = unhexlify("0591E065B04C9C76F99DC0689533FA12CBFE83BFF35A38A37B8169D6BE25136D3B06CC72266197B842451A96D3E3CE2BAF2DDCE81F57F4C89F8B630AA0312819") 105 | c4_xorkey = unhexlify("FD14E58F5DC66BA992F24E0C01244954713DC9372E2A5BB94AC5BCC4203404F607364716BA73FCC24F53E1395F988693A117670878C1BD3A1BB443E6AB4BD4DD") 106 | 107 | mdata = [c for c in data] 108 | 109 | for i in range(0, len(data)-32, 32): 110 | start_index = i 111 | for end_index in range(i+31, start_index, -1): 112 | start_byte = data[start_index] 113 | end_byte = data[end_index] 114 | mdata[start_index] = end_byte 115 | mdata[end_index] = (start_byte + end_byte)&0xFF 116 | 117 | start_index += 1 118 | 119 | out = bytearray() 120 | for index, cur_char in enumerate(mdata): 121 | tmp = cur_char ^ c4_xorkey[index % 64] 122 | tmp = (tmp + c4_subkey[index % 64])&0xFF 123 | out.append(tmp) 124 | 125 | return bytes(out) 126 | 127 | def aes_decrypt(data): 128 | """ 129 | This key is hardcoded inside the firmware updater library 130 | However, it does not work for the AES encrypted firmwares... 131 | """ 132 | rom_cypher_key = unhexlify("7AE5AB76DF05569CC74508E3E300971059735C041195A4178BA6DBBF77EADC9F") 133 | dec = AES.new(rom_cypher_key, AES.MODE_ECB) 134 | 135 | out = dec.decrypt(data[:len(data)-len(data)%0x10]) 136 | return out 137 | 138 | def decrypt_data(data, crypto_type): 139 | """ 140 | Decipher the data according to the specified algorithm 141 | """ 142 | if crypto_type == 0: 143 | return data 144 | if crypto_type == 3: 145 | return cypher3_decrypt(data) 146 | if crypto_type == 128: 147 | return aes_decrypt(data) 148 | raise NotImplementedError 149 | 150 | 151 | def tests(): 152 | """ 153 | Testing cyphers implementation 154 | """ 155 | buf = b"\xf8\xbc\x74\xe1\x3a\x27\xe1\xa8\x04\x8f\x47\x1c\x63\x39\x0d\xfb" 156 | verif = b"\x03\x00\x00\x00\x01\x00\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00" 157 | out = cypher3_decrypt(buf) 158 | if verif != out: 159 | print("Error in cypher3_decrypt") 160 | print(buf) 161 | print(verif) 162 | else: 163 | print("cypher3 decryption seems OK") 164 | 165 | out = cypher3_encrypt(verif) 166 | 167 | if out != buf: 168 | print("Error in cypher3_encrypt") 169 | else: 170 | print("cypher3 encryption seems OK") 171 | 172 | 173 | tmp = cypher4_encrypt(b"\x00"*10) 174 | if cypher4_decrypt(tmp) != b"\x00"*10: 175 | print("Error in cypher4") 176 | print(hexlify(tmp)) 177 | print(hexlify(cypher4_decrypt(tmp))) 178 | else: 179 | print("Cypher4 at least is reversible") 180 | aes_test_data = unhexlify("675ff10cdf9f7299f6ca71387153bf71") 181 | aes_decrypt(aes_test_data) 182 | 183 | if __name__ == "__main__": 184 | tests() 185 | -------------------------------------------------------------------------------- /yealink_rom_dump.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | #!/usr/bin/env python3 3 | """ 4 | Yealink update file parser 5 | Copyright 2019 Synacktiv 6 | 7 | Usage: compile the KSY file with kaitai-struct-compiler and then use 8 | this script to parse / dump the yealink firmware update file 9 | 10 | Licensed under the "THE BEER-WARE LICENSE" (Revision 42): 11 | Tristan P. wrote this file. As long as you retain this notice you 12 | can do whatever you want with this stuff. If we meet some day, and you think 13 | this stuff is worth it, you can buy me a beer or coffee in return 14 | 15 | """ 16 | 17 | import argparse 18 | import struct 19 | import logging 20 | import lzma 21 | 22 | from binascii import hexlify 23 | from tqdm import tqdm 24 | 25 | import yealinkrom 26 | from yealink_crypto import get_crypto, decrypt_data 27 | 28 | 29 | def get_block_type(block_type): 30 | """ 31 | Wrapper to return the string corresponding to a block type 32 | """ 33 | types = { 34 | yealinkrom.Yealinkrom.BlockType.none: "none", 35 | yealinkrom.Yealinkrom.BlockType.bin: "bin", 36 | yealinkrom.Yealinkrom.BlockType.tool: "tool", 37 | yealinkrom.Yealinkrom.BlockType.file: "file", 38 | yealinkrom.Yealinkrom.BlockType.nand_raw: "raw nand", 39 | yealinkrom.Yealinkrom.BlockType.nand_oob: "raw oob", 40 | yealinkrom.Yealinkrom.BlockType.nand_oob: "raw oob", 41 | yealinkrom.Yealinkrom.BlockType.nor_raw: "raw nor", 42 | yealinkrom.Yealinkrom.BlockType.script: "script", 43 | yealinkrom.Yealinkrom.BlockType.execute: "execute", 44 | } 45 | try: 46 | return types[block_type] 47 | except KeyError: 48 | return "unknown" 49 | 50 | def decrypt_block(mupdate, data): 51 | """ 52 | Decrypt a block according to it's crypto type 53 | Example: kernel.bin is almost always encrypted with cypher3 54 | """ 55 | 56 | cypher = mupdate.header.common_header.crypto_type 57 | block_header = decrypt_data(data[:0x80], cypher) 58 | header_size = struct.unpack(">I", block_header[0x4:0x8])[0] 59 | block_total = struct.unpack(">I", block_header[24:28])[0] 60 | 61 | output = block_header 62 | logging.debug("Header size 0x%x", header_size) 63 | logging.debug("block size 0x%x", block_total) 64 | 65 | 66 | if len(data) == block_total: 67 | logging.debug("Decrypting last block") 68 | output += decrypt_data(data[header_size:], cypher) 69 | elif decrypt_data(data[block_total-header_size:block_total], 70 | cypher).startswith(b"\xeb\x9f\x56\xc9"): 71 | output += decrypt_data(data[header_size:block_total-header_size], cypher) 72 | logging.debug("decrypt with header") 73 | elif decrypt_data(data[block_total:block_total+0x8], cypher).startswith(b"\xeb\x9f\x56\xc9"): 74 | output += decrypt_data(data[header_size:block_total], cypher) 75 | logging.debug("without header") 76 | else: 77 | logging.error("Error, invalid data") 78 | return output 79 | 80 | def decrypt_rom(mupdate, fname): 81 | """ 82 | Decrypts all the ROM according to the header crypto flag 83 | Returns a decrypted chunk with the correct header 84 | """ 85 | logging.info("Decrypting the ROM file") 86 | # Remove cipher indicator from rom header 87 | rom_header = open(fname, "rb").read(0x80) 88 | rom_header = rom_header[:12] + b"\x00" + rom_header[13:] 89 | output = rom_header 90 | data = mupdate.rawdata 91 | 92 | for _ in tqdm(range(mupdate.header.rom_blocks), "Decrypting blocks"): 93 | block = decrypt_block(mupdate, data) 94 | data = data[len(block):] 95 | output += block 96 | return output 97 | 98 | def print_common_header(hdr): 99 | """ 100 | Print the informations contained in the header common to both 101 | the blocks and the ROM 102 | """ 103 | print("header size 0x%x" % hdr.header_size) 104 | print("header CRC32: 0x%x" % hdr.header_crc) 105 | print("header format 0x%x" % hdr.header_format) 106 | print("Image cryptography: %s" % get_crypto(hdr.crypto_type)) 107 | print("header flags 0x%x" % hdr.header_flags) 108 | print("content size 0x%x" % hdr.length) 109 | 110 | def print_block_info(blk): 111 | """ 112 | Print informations contained in the block header 113 | """ 114 | print("Blk name %s" % (blk.blk_name)) 115 | print("Blk version %s" % (blk.blk_version)) 116 | print("Blk total 0x%x" % (blk.block_total)) 117 | print("Blk cryptography: %s" % get_crypto(blk.cipher)) 118 | print("Blk type: %s" % (get_block_type(blk.block_type))) 119 | 120 | def dump_block(blk): 121 | """ 122 | Dump a block to its name 123 | """ 124 | name = blk.hdr.blk_name 125 | logging.info("Dumping block %s of size %d", 126 | name + ".bin", 127 | blk.hdr.common_header.length) 128 | 129 | data = blk.data[:blk.hdr.common_header.length] 130 | 131 | if blk.hdr.cipher == 1: 132 | logging.info("Block is compressed, decompressing it") 133 | data = lzma.decompress(data) 134 | elif blk.hdr.cipher == 3: 135 | logging.info("Block is ciphered with CIPHER3, decrypting") 136 | data = decrypt_data(data, blk.hdr.cipher) 137 | elif blk.hdr.cipher != 0: 138 | logging.error("Block cryptography is not yet supported") 139 | with open(name + ".bin", "wb") as out: 140 | out.write(data) 141 | 142 | def show_rom_info(mupdate, fname=""): 143 | """ 144 | Show the rom header informations, and walks the blocks 145 | """ 146 | print("ROM magic: 0x%s" % hexlify(mupdate.header.magic)) 147 | print_common_header(mupdate.header.common_header) 148 | 149 | print("ROM verify: 0x%x" % mupdate.header.rom_verify) 150 | print("ROM blocks: 0x%x" % mupdate.header.rom_blocks) 151 | print("ROM dev ID: %d" % mupdate.header.rom_dev_id) 152 | print("ROM hw ID: %d" % mupdate.header.rom_hw_id) 153 | print("ROM sw ID: %d" % mupdate.header.rom_sw_id) 154 | print("ROM dev name: %s" % mupdate.header.rom_dev_name) 155 | print("ROM oem name: %s" % mupdate.header.rom_oem_name) 156 | print("ROM version: %s" % mupdate.header.rom_version) 157 | 158 | for i in range(16): 159 | rom_cid = mupdate.header.rom_rom_cid_array[i] 160 | if rom_cid != 0: 161 | print("Rom rom cid %d : %d" %(i, rom_cid)) 162 | 163 | if mupdate.header.common_header.crypto_type == 0: 164 | logging.info("Firmware is unencrypted, showing blocks info") 165 | for i in range(mupdate.header.rom_blocks): 166 | print_common_header(mupdate.blocks[i].hdr.common_header) 167 | print_block_info(mupdate.blocks[i].hdr) 168 | else: 169 | data = decrypt_rom(mupdate, fname) 170 | data = bytes(data) 171 | update = yealinkrom.Yealinkrom.from_bytes(data) 172 | show_rom_info(update) 173 | 174 | 175 | def show_info(fname): 176 | """ 177 | Dump the informations contained in the structure 178 | """ 179 | mupdate = yealinkrom.Yealinkrom.from_file(fname) 180 | show_rom_info(mupdate, fname) 181 | 182 | 183 | def dump_blocks(update): 184 | """ 185 | Dump all the blocks of a ROM image 186 | """ 187 | for i in tqdm(range(update.header.rom_blocks), "Dumping blocks"): 188 | dump_block(update.blocks[i]) 189 | 190 | 191 | def dump(fname): 192 | """ 193 | Dump the blocks 194 | """ 195 | mupdate = yealinkrom.Yealinkrom.from_file(fname) 196 | 197 | if mupdate.header.common_header.crypto_type == 0: 198 | logging.info("Firmware is unencrypted, directly dumping blocks") 199 | dump_blocks(mupdate) 200 | else: 201 | logging.info("The firmware looks encrypted, we have to decrypt it") 202 | data = decrypt_rom(mupdate, fname) 203 | data = bytes(data) 204 | mupdate = yealinkrom.Yealinkrom.from_bytes(data) 205 | 206 | dump_blocks(mupdate) 207 | 208 | 209 | def main(): 210 | """ 211 | Argument parsing and dispatching 212 | """ 213 | actions = { 214 | "dump": dump, 215 | "info": show_info 216 | } 217 | parser = argparse.ArgumentParser("ROM parsing script for yealink firmwares") 218 | parser.add_argument("action", choices=actions.keys()) 219 | parser.add_argument("update", help="the update file") 220 | 221 | parser.add_argument("--verbose", "-v", help="verbose", action="store_true") 222 | args = parser.parse_args() 223 | 224 | if args.verbose: 225 | logging.basicConfig(level=logging.DEBUG) 226 | else: 227 | logging.basicConfig(level=logging.INFO) 228 | 229 | actions[args.action](args.update) 230 | 231 | 232 | if __name__ == "__main__": 233 | main() 234 | -------------------------------------------------------------------------------- /yaffs_decrypt.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | #!/usr/bin/env python3 3 | 4 | """ 5 | YAFFS2 parser for yealink encrypted filesystem 6 | (c) 2019 - Synacktiv - www.synacktiv.com 7 | 8 | The Yaffs and Yaffs structure are issued from two Kaitai struct generated files 9 | 10 | Licensed under the "THE BEER-WARE LICENSE" (Revision 42): 11 | Tristan P. wrote this file. As long as you retain this notice you 12 | can do whatever you want with this stuff. If we meet some day, and you think 13 | this stuff is worth it, you can buy me a beer or coffee in return 14 | """ 15 | 16 | import argparse 17 | import struct 18 | import logging 19 | import os 20 | 21 | from tqdm import tqdm 22 | from yaffs import Yaffs 23 | from yaffstag import Yaffstag 24 | 25 | from yealink_crypto import cypher3_decrypt 26 | 27 | 28 | class YaffsObject(): 29 | """ 30 | Custom variation of object 31 | """ 32 | 33 | def __init__(self, object_header, tag): 34 | """ 35 | Initialize an object from a kaitai generated header and spare 36 | """ 37 | 38 | self.pid = object_header.parent_obj_id 39 | self.type = object_header.obj_type 40 | self.file_size = object_header.file_size_low 41 | if object_header.name: 42 | self.name = object_header.name 43 | else: 44 | # root folder? 45 | self.name = "/" 46 | self.obj_id = tag.obj_id 47 | logging.debug("Got a new object: 0x%x : %s", self.obj_id, self.name) 48 | logging.debug("Object size: 0x%x", self.file_size) 49 | 50 | def __repr__(self): 51 | mstr = "YaffsObject(0x%x, %s)" % (self.obj_id, self.name) 52 | return mstr 53 | 54 | def ls(self, level=0): 55 | """ 56 | This should be implemented in the specific object 57 | """ 58 | raise NotImplementedError 59 | 60 | @staticmethod 61 | def factory(header, spare): 62 | """ 63 | Generates an object according to the type specified in the headers 64 | """ 65 | try: 66 | # the first 512 bytes may be ciphered 67 | object_header = Yaffs.from_bytes(header) 68 | tag = Yaffstag.from_bytes(spare) 69 | except Exception as e: 70 | logging.exception(e) 71 | logging.warning("Error, cannot generate the datastructures from the supplied buffers") 72 | 73 | raise e 74 | 75 | if object_header.obj_type == Yaffs.ObjType.directory: 76 | return YaffsDirectory(object_header, tag) 77 | if object_header.obj_type == Yaffs.ObjType.file: 78 | return YaffsFile(object_header, tag) 79 | if object_header.obj_type == Yaffs.ObjType.symlink: 80 | return YaffsSymlink(object_header, tag) 81 | logging.warning("Unknown type %s", object_header.obj_type) 82 | return YaffsObject(object_header, tag) 83 | 84 | class YaffsDirectory(YaffsObject): 85 | """ 86 | Implementation of a directory 87 | """ 88 | 89 | def __init__(self, header, spare): 90 | YaffsObject.__init__(self, header, spare) 91 | self.children = [] 92 | 93 | def add_children(self, obj): 94 | """ 95 | Add a children of a directory 96 | """ 97 | if obj.obj_id != 1: 98 | self.children.append(obj) 99 | 100 | def ls(self, level=0): 101 | if level == 0: 102 | print(self.name) 103 | else: 104 | print("%s/%s" % (' '*level, self.name)) 105 | for child in self.children: 106 | child.ls(level+1) 107 | 108 | def __repr__(self): 109 | mstr = "YaffsDirectory(0x%x, %s)" % (self.obj_id, self.name) 110 | return mstr 111 | 112 | class YaffsFile(YaffsObject): 113 | """ 114 | A file, containing one or more data blocks 115 | """ 116 | def __init__(self, header, spare): 117 | YaffsObject.__init__(self, header, spare) 118 | self.nb_blocks = int(self.file_size / 0x800) + 1 119 | if self.file_size == 0: 120 | self.nb_blocks = 0 121 | self.data_blocks = [None for _ in range(self.nb_blocks)] 122 | 123 | def add_data_block(self, block, tag): 124 | """ 125 | Add a chunk of data on the file 126 | """ 127 | if tag.seq_id > len(self.data_blocks): 128 | logging.error("Too many data blocks for this file") 129 | raise IOError 130 | if self.data_blocks[tag.seq_id -1] is not None: 131 | logging.error("Already got data for this seq id") 132 | else: 133 | self.data_blocks[tag.seq_id-1] = block.data[:tag.nbytes] 134 | 135 | def get_data(self): 136 | """ 137 | Return the reconstructed file 138 | """ 139 | data = bytearray() 140 | for i, block in enumerate(self.data_blocks): 141 | if block is not None: 142 | data += block 143 | else: 144 | logging.warning("Missing data block %d/%d on file %s", i, 145 | self.nb_blocks, self.name) 146 | return data 147 | 148 | def ls(self, level=0): 149 | """ 150 | List the file and its size 151 | """ 152 | print("%s%s %d" % (" "*level, self.name, self.file_size)) 153 | 154 | def __repr__(self): 155 | mstr = "YaffsFile(0x%x, %s)" % (self.obj_id, self.name) 156 | return mstr 157 | 158 | class YaffsSymlink(YaffsObject): 159 | """ 160 | A symlink pointing to another object 161 | """ 162 | def __init__(self, header, tag): 163 | YaffsObject.__init__(self, header, tag) 164 | 165 | self.alias = header.alias 166 | 167 | def ls(self, level=0): 168 | """ 169 | List the symlink and its alias 170 | """ 171 | print("%s%s -> %s" % (" "*level, self.name, self.alias)) 172 | 173 | def __repr__(self): 174 | mstr = "YaffsFile(0x%x, %s)" % (self.obj_id, self.name) 175 | return mstr 176 | 177 | 178 | class YaffsBlock(): 179 | """ 180 | The block, composed of 1 2048 bytes chunk + 64 bytes tag (the tag being OOB) 181 | """ 182 | YAFFS_BLOCK_HEADERS = [struct.pack("I", i) for i in range(5)] 183 | 184 | def __init__(self, data): 185 | self.spare = data[0x800:0x800+0x40] 186 | self.is_header = False 187 | 188 | decrypted = cypher3_decrypt(data[:0x200]) 189 | 190 | for header in self.YAFFS_BLOCK_HEADERS: 191 | if decrypted.startswith(header): 192 | # we got a correct yaffs chunk header 193 | self.is_header = True 194 | break 195 | if self.is_header: 196 | self.data = decrypted + data[0x200:0x800] 197 | else: 198 | self.data = data[:0x800] 199 | 200 | 201 | class YaffsFileSystem(): 202 | """ 203 | Semi ordered collection of Yaffs objects 204 | """ 205 | 206 | def __init__(self): 207 | self.objects = {} 208 | self.directories = {} 209 | self.files = {} 210 | 211 | def add_object(self, obj): 212 | """ 213 | Add an object to the filesystem 214 | """ 215 | logging.debug("Adding object with id %d pid %d name: %s", obj.obj_id, obj.pid, obj.name) 216 | logging.debug("Object name: %s", obj.name) 217 | 218 | if obj.obj_id in self.objects: 219 | logging.warning("error: object already in the list!") 220 | else: 221 | self.objects[obj.obj_id] = obj 222 | 223 | if obj.type == Yaffs.ObjType.directory: 224 | self.directories[obj.obj_id] = obj 225 | 226 | elif obj.type == Yaffs.ObjType.file: 227 | self.files[obj.obj_id] = obj 228 | 229 | if obj.pid not in self.directories.keys(): 230 | logging.warning("Parent %d of object %s not known", obj.pid, obj.name) 231 | else: 232 | self.directories[obj.pid].add_children(obj) 233 | 234 | def add_block(self, block): 235 | """ 236 | Add a new object or a data block to an existing object 237 | """ 238 | 239 | if block.is_header: 240 | obj = YaffsObject.factory(block.data, block.spare) 241 | self.add_object(obj) 242 | else: 243 | tag = Yaffstag.from_bytes(block.spare) 244 | logging.debug("Got data block %d for file %d", tag.seq_id, tag.obj_id) 245 | 246 | if tag.obj_id not in self.files.keys(): 247 | logging.error("Unknown file ID %d", tag.obj_id) 248 | else: 249 | self.files[tag.obj_id].add_data_block(block, tag) 250 | 251 | def add_blocks(self, blocks): 252 | """ 253 | Construct the filesystem from a list of blocks 254 | """ 255 | for block in tqdm(blocks, "Reconstructing objects"): 256 | self.add_block(block) 257 | logging.info("Reconstructing objects and structure: DONE") 258 | 259 | 260 | def reconstruct_filesystem(fname): 261 | """ 262 | Tries to reconstruct a valid YAFFS filesystem from a dumped file 263 | """ 264 | blocks = [] 265 | 266 | data = open(fname, "rb").read() 267 | filesystem = YaffsFileSystem() 268 | 269 | blocks = [] 270 | for index in tqdm(range(0, len(data)-0x840, 0x840), "Generating blocks"): 271 | blocks.append(YaffsBlock(data[index:index+0x840])) 272 | filesystem.add_blocks(blocks) 273 | return filesystem 274 | 275 | def dump_directories(root, filesystem): 276 | """ 277 | Recreates the filesystem arborescence inside the new directory 278 | """ 279 | def walk_directory(base_dir): 280 | """ 281 | Recursively walk directory 282 | """ 283 | os.mkdir(base_dir.name) 284 | os.chdir(base_dir.name) 285 | for child in base_dir.children: 286 | if isinstance(child, YaffsDirectory): 287 | walk_directory(child) 288 | elif isinstance(child, YaffsFile): 289 | with open(child.name, "wb") as out: 290 | out.write(child.get_data()) 291 | os.chdir("..") 292 | 293 | if os.path.exists(root): 294 | logging.error("Existing directory, aborting") 295 | raise OSError 296 | 297 | os.mkdir(root) 298 | os.chdir(root) 299 | for child in filesystem.directories[1].children: 300 | if isinstance(child, YaffsDirectory): 301 | walk_directory(child) 302 | os.chdir("..") 303 | 304 | 305 | def dump_fs(args): 306 | """ 307 | Dump the content of all the files in a new directory 308 | """ 309 | filesystem = reconstruct_filesystem(args.file) 310 | if filesystem is None: 311 | logging.error("Error reconstructing filesystem, aborting") 312 | return 313 | 314 | logging.info("Dumping filesystem in directory %s", args.output) 315 | dump_directories(args.output, filesystem) 316 | logging.info("Extraction has finished, good luck!") 317 | 318 | 319 | def list_fs(args): 320 | """ 321 | Tree like printing of the filesystem 322 | """ 323 | filesystem = reconstruct_filesystem(args.file) 324 | if filesystem is None: 325 | logging.error("Error reconstructing filesystem, aborting") 326 | elif 1 not in filesystem.directories.keys(): 327 | logging.error("No root directory in the filesystem") 328 | else: 329 | filesystem.directories[1].ls() 330 | 331 | def main(): 332 | """ 333 | Argument dispatching 334 | """ 335 | parser = argparse.ArgumentParser("Yealink YAFFS2 parser") 336 | 337 | subparsers = parser.add_subparsers(help="Subcommands") 338 | ls_arg = subparsers.add_parser("ls", help="List the content of the filesystem") 339 | dump = subparsers.add_parser("dump", help="Dump the content of the filesystem") 340 | 341 | dump.add_argument("--output", "-o", help="The output directory", default="dumped") 342 | 343 | ls_arg.set_defaults(func=list_fs) 344 | dump.set_defaults(func=dump_fs) 345 | 346 | parser.add_argument("file", help="The file to parse") 347 | parser.add_argument("--verbose", "-v", help="verbose", action="store_true") 348 | args = parser.parse_args() 349 | 350 | if args.verbose: 351 | logging.basicConfig(level=logging.DEBUG) 352 | else: 353 | logging.basicConfig(level=logging.INFO) 354 | 355 | # execute the user's action 356 | args.func(args) 357 | 358 | 359 | if __name__ == "__main__": 360 | main() 361 | --------------------------------------------------------------------------------