├── examples ├── filetree.rec ├── dumps │ └── just_to_create_dumps_folder.txt ├── carving.rec └── tree_explorer.rec ├── CHANGELOG.md ├── util ├── time.py ├── func_parser.py ├── table.py ├── hexdump.py ├── filetree.py └── carving.py ├── mmls.py ├── vol.py ├── LICENSE ├── .gitignore ├── part └── refs │ ├── object.py │ ├── allocator.py │ ├── object_tree.py │ ├── tree_control.py │ ├── vol.py │ ├── entry_block.py │ └── attribute.py ├── media ├── mbr.py └── gpt.py ├── list_entryblock.py ├── README.md └── igor.py /examples/filetree.rec: -------------------------------------------------------------------------------- 1 | file examples/dumps/Windows8ReFSImages/Physical_Image_9.001 2 | filetree 3 | -------------------------------------------------------------------------------- /examples/dumps/just_to_create_dumps_folder.txt: -------------------------------------------------------------------------------- 1 | This file is here only to make sure the dumps folder appears in the git repository. 2 | -------------------------------------------------------------------------------- /examples/carving.rec: -------------------------------------------------------------------------------- 1 | file examples/dumps/Windows8ReFSImages/Physical_Image_9.001 2 | find_entryblocks -f -F 3 | list_folders 4 | list_filenames 5 | list_dataruns 6 | datastream carving-example-linux-3.2.9.tar 0x13100000 0x7368,3220 0x7360,4 0x1000,16296 7 | datastream carving-example-README 0x73 0x7050,4 8 | bye 9 | -------------------------------------------------------------------------------- /examples/tree_explorer.rec: -------------------------------------------------------------------------------- 1 | file examples/dumps/Windows8ReFSImages/Physical_Image_9.001 2 | find_entryblocks 3 | tree_control 4 | tree_control_extension 0x167 5 | object_tree 0x57 6 | entryblock 0x72 7 | entryblock 0x700e 8 | entryblock 0x7012 9 | datastream tree_explorer-README.txt 0x30 0x701c,4 10 | bye 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.1 (2018-09-19) 2 | 3 | - Added changelog file 4 | 5 | ### Fixed (2 changes) 6 | 7 | - Options of igor `filetree` command 8 | - Text references to `find_entryblocks` command in igor 9 | 10 | ## 0.1 (2018-09-16) 11 | 12 | - First release of the `pyrefs` library including the `igor` command line utility. 13 | -------------------------------------------------------------------------------- /util/time.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime,timedelta 2 | 3 | def bytes2time(dt): 4 | us = dt / 10 5 | try: 6 | td = timedelta(microseconds=us) 7 | time = datetime(1601,1,1) + td 8 | except: 9 | return '' 10 | if time.year < 2000 or time.year > datetime.today().year: 11 | return ' {}'.format(time) 12 | return time 13 | -------------------------------------------------------------------------------- /util/func_parser.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | class FuncArgumentParserError(Exception): pass 4 | 5 | class FuncArgumentParserHelp(Exception): pass 6 | 7 | class FuncArgumentParser(argparse.ArgumentParser): 8 | def exit(self, status=0, message=None): 9 | if message: 10 | print(message) 11 | raise FuncArgumentParserHelp() 12 | 13 | def error(self, message): 14 | raise FuncArgumentParserError(message) 15 | -------------------------------------------------------------------------------- /mmls.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import media.mbr as mbr 3 | import media.gpt as gpt 4 | 5 | def mmls(): 6 | parser = argparse.ArgumentParser(description='Read MBR from provided dump.') 7 | parser.add_argument('dump', action='store', 8 | type=argparse.FileType(mode='rb')) 9 | args = parser.parse_args() 10 | mbr_data = mbr.readMBR(args.dump) 11 | # we make the supposition that if it is a GPT media then only one 12 | # partition is declared in the MBR 13 | if len(mbr_data) == 1 and mbr_data[0][1]['ptype'] == mbr.MBR_PARTTYPE_GPT: 14 | gpt_data = gpt.readGPT(args.dump, mbr_data[0][1]['start']) 15 | gpt.print_gpt(gpt_data) 16 | else: 17 | print(mbr_data) 18 | 19 | if __name__ == '__main__': 20 | mmls() 21 | -------------------------------------------------------------------------------- /vol.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import argparse 3 | import media.mbr as mbr 4 | import media.gpt as gpt 5 | import part.refs.vol as refs 6 | 7 | parser = argparse.ArgumentParser(description='Read from provided dump.') 8 | parser.add_argument('dump', action='store', 9 | type=argparse.FileType(mode='rb')) 10 | args = parser.parse_args() 11 | mbr_data = mbr.readMBR(args.dump) 12 | # we make the supposition that if it is a GPT media then only one 13 | # partition is declared in the MBR 14 | if not (len(mbr_data) == 1 and mbr_data[0][1]['ptype'] == mbr.MBR_PARTTYPE_GPT): 15 | print(mbr_data) 16 | sys.exit() 17 | 18 | gpt_data = gpt.readGPT(args.dump, mbr_data[0][1]['start']) 19 | gpt.print_gpt(gpt_data) 20 | 21 | for part in gpt_data['parts']: 22 | if part['type'] == gpt.GUID_PART_TYPE_W_BASIC_DATA_PART: 23 | if refs.is_refs_part(args.dump, part['first_lba']): 24 | refs_fsstat = refs.fsstat(args.dump, part['first_lba'], part['last_lba']) 25 | refs.dump_fsstat(refs_fsstat) 26 | else: 27 | print('no ReFS partition') 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dafti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /util/table.py: -------------------------------------------------------------------------------- 1 | def print_table(_meta, _entries): 2 | # first we transform the entries 3 | entries = [] 4 | for _e in _entries: 5 | entry = {} 6 | for col in _meta: 7 | entry[col['key']] = _e[col['key']] if 'transform' not in col.keys() else col['transform'](_e[col['key']]) 8 | entries.append(entry) 9 | # transform the values and compute columns lengths 10 | lengths = {m['key']:len(m['header']) for m in _meta} 11 | values = [] 12 | for e in entries: 13 | value = {} 14 | for m in _meta: 15 | form = m['format'] if 'format' in m.keys() else '' 16 | val = ('{:' + form + '}').format(e[m['key']]) 17 | value[m['key']] = val 18 | lengths[m['key']] = max(len(val), lengths[m['key']]) 19 | values.append(value) 20 | lengths = {k:v+1 for k,v in lengths.items()} 21 | # print the table header 22 | for m in _meta: 23 | align = m['align'] if 'align' in m.keys() else '' 24 | print(('{:' + align + str(lengths[m['key']]) + '} ').format( 25 | m['header']), end='') 26 | print('') 27 | # print the table contents 28 | for v in values: 29 | for m in _meta: 30 | align = m['align'] if 'align' in m.keys() else '' 31 | print(('{:' + align + str(lengths[m['key']]) + '} ').format( 32 | v[m['key']]), end='') 33 | print('') 34 | 35 | -------------------------------------------------------------------------------- /util/hexdump.py: -------------------------------------------------------------------------------- 1 | def hexdump(data, address=0, width=16): 2 | max_address = address + len(data) 3 | address_len = len('{:#x}'.format(max_address)) + 1 4 | data_format = ' {:02x}' 5 | text_format = ' {}' 6 | header_format = '{:<' + str(address_len) + '} ' + (data_format * width) + text_format 7 | buf = [ x % 0x100 for x in range(width) ] 8 | print(header_format.format('', *buf, '')) 9 | backup = None 10 | skip = -1 11 | line_range = list(range(0, len(data), width)) 12 | last_range = line_range[-1] 13 | dot_line = '.' * len(('{:#0' + str(address_len) + 'x}:').format(0)) 14 | for index in line_range: 15 | buf = data[index:index + width] 16 | dec = ''.join([ chr(x) if x >= 32 and x <= 126 else '.' for x in buf ]) 17 | white_format = ' ' * (width - len(buf)) 18 | line_format = '{:#0' + str(address_len) + 'x}:' + (data_format * len(buf)) + white_format + text_format 19 | line = line_format.format(address + index, *buf, dec) 20 | if skip == -1: 21 | print(line) 22 | skip = 0 23 | elif index == last_range: 24 | if skip > 0: 25 | print(backup) 26 | print(line) 27 | elif list(filter(lambda x: x != 0, buf)): 28 | if skip: 29 | print(backup) 30 | skip = 0 31 | print(line) 32 | else: 33 | if skip == 0: 34 | skip = 1 35 | backup = line 36 | elif skip == 1: 37 | backup_format = dot_line + (data_format * len(buf)) + white_format + text_format 38 | backup = backup_format.format(*buf, dec) 39 | else: 40 | skip = 2 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # vim 107 | .*.swp 108 | -------------------------------------------------------------------------------- /part/refs/object.py: -------------------------------------------------------------------------------- 1 | from struct import Struct 2 | 3 | OBJ_HEADER_1_FORMAT = Struct(': {val:#x} ({val})'.format(val=al['node_header_unknown'])) 35 | print('- offset to first pointer: {val:#x} ({val})'.format(val=al['offset_first_ptr'])) 36 | print('- number of pointers in this node: {}'.format(al['num_ptrs_in_node'])) 37 | print('- offset to end of node: {val:#x} ({val})'.format(val=al['offset_end_node'])) 38 | -------------------------------------------------------------------------------- /media/mbr.py: -------------------------------------------------------------------------------- 1 | from struct import unpack, Struct 2 | from media.gpt import readGPT 3 | 4 | SECTOR_SIZE = 512 5 | MBR_SIZE = SECTOR_SIZE 6 | MBR_TERMINATOR = 0x55aa 7 | MBR_TERMINATOR_OFFSET = 510 8 | MBR_PART_TABLE_OFFSET = 0x1be 9 | MBR_PART_TABLE_SIZE = 4 10 | MBR_PART_ENTRY_SIZE = 16 11 | MBR_PARTTYPE_NOPART = 0 12 | MBR_PARTTYPE_REFS_EXFAT_NTFS = 7 13 | MBR_PARTTYPE_REFS_EXFAT_GPT = 0xee 14 | MBR_PARTTYPE_GPT = 0xee 15 | 16 | ''' 17 | MBR Partition format 18 | 19 | offset,size,name, interpretation 20 | 21 | 0 ubyte boot flag 0x80 means bootable, 0 means not bootable 22 | 1 3 bytes CHS obsolete 23 | 4 ubyte partition type 0x7 means NTFS, Exfat or ReFS 24 | 5 3 bytes CHS obsolete 25 | 8 ulong 1st sector address of first sector 26 | 12 ulong size size in sectors 27 | ''' 28 | #https://docs.python.org/3/library/struct.html 29 | MBR_PART_FORMAT = Struct('H') 31 | 32 | def readMBR(stream): 33 | data = stream.read(MBR_SIZE) 34 | mbr = [] 35 | terminator, = MBR_TERMINATOR_FORMAT.unpack_from(data, MBR_TERMINATOR_OFFSET) 36 | if terminator != MBR_TERMINATOR: 37 | print('not an MBR, missing MBR terminator ({})'.format(MBR_TERMINATOR)) 38 | return None #we should raise an Exception. https://docs.python.org/fr/3.5/tutorial/errors.html 39 | else: 40 | for p in range(MBR_PART_TABLE_SIZE): 41 | # by convention, _ is the variable name for ignored values in Python 42 | mbr_entry = MBR_PART_FORMAT.unpack_from(data, MBR_PART_TABLE_OFFSET + (p * MBR_PART_ENTRY_SIZE)) 43 | bflag, _, ptype, _, start, size = mbr_entry 44 | if ptype != 0: 45 | mbr.append( (p, { 'bflag':bflag, 'ptype':ptype, 'start':start, 'size':size } ) ) 46 | # else: 47 | # print('Ignored entry') 48 | # print(mbr) 49 | return mbr 50 | 51 | 52 | -------------------------------------------------------------------------------- /part/refs/object_tree.py: -------------------------------------------------------------------------------- 1 | from struct import Struct 2 | 3 | OT_HEADER_1_FORMAT = Struct(' ({size},{size:#x})'.format(ot['_absolute_offset'], size=ot['_structure_size'])) 51 | print('- entryblock number: {:#x}'.format(ot['eb_number'])) 52 | print('- counter: {}'.format(ot['counter'])) 53 | print('- node id: {:#x}'.format(ot['node_id'])) 54 | print('- node descriptor length: {val:#x} ({val})'.format(val=ot['node_desc_length'])) 55 | print('- number of records in node: {}'.format(ot['num_records_in_node'])) 56 | print('- offset to next free record: {val:#x} ({val})'.format(val=ot['offset_next_free_rec'])) 57 | print('- free space in node: {val:#x} ({val})'.format(val=ot['free_space'])) 58 | print('- : {val:#x} ({val})'.format(val=ot['node_header_unknown'])) 59 | print('- offset to first pointer: {val:#x} ({val})'.format(val=ot['offset_first_ptr'])) 60 | print('- number of pointers in this node: {}'.format(ot['num_ptrs_in_node'])) 61 | print('- offset to end of node: {val:#x} ({val})'.format(val=ot['offset_end_node'])) 62 | if ot['records']: 63 | print('- records: <{:#x}>'.format(ot['records_offset'])) 64 | for i, rec in enumerate(ot['records']): 65 | print(' - record {}: <{:#x}>'. format(i, rec['_record_offset'])) 66 | print(' - record length: {val:#x} ({val})'.format(val=rec['record_length'])) 67 | print(' - node id: {:#x}'.format(rec['nodeid'])) 68 | print(' - entryblock number: {:#x}'.format(rec['eb_num'])) 69 | print(' - identifier(?): {:#x}'.format(rec['id'])) 70 | -------------------------------------------------------------------------------- /part/refs/tree_control.py: -------------------------------------------------------------------------------- 1 | from struct import Struct 2 | 3 | TC_HEADER_FORMAT = Struct(' ({size},{size:#x})'.format(tc['_absolute_offset'], 29 | size=tc['_structure_size'])) 30 | print('- entryblock number: {:#x}'.format(tc['eb_number'])) 31 | print('- offset of extents: {val:#x} ({val})'.format(val=tc['offset_extents'])) 32 | print('- number of extents: {}'.format(tc['num_extents'])) 33 | print('- offset of record: {val:#x} ({val})'.format(val=tc['offset_record'])) 34 | print('- length of record: {val:#x} ({val})'.format(val=tc['length_record'])) 35 | if tc['extent_pointers']: 36 | print('- extent pointers: ', end='') 37 | for pt in tc['extent_pointers']: 38 | print('{:#x} '.format(pt), end='') 39 | print('') 40 | 41 | TC_EXT_HEADER_FORMAT = Struct(' ({size},{size:#x})'.format(tc_e['_absolute_offset'], 80 | size=tc_e['_structure_size'])) 81 | print('- entryblock number: {:#x}'.format(tc_e['eb_number'])) 82 | print('- counter: {}'.format(tc_e['counter'])) 83 | print('- node id: {:#x}'.format(tc_e['node_id'])) 84 | print('- length of record: {val:#x} ({val})'.format(val=tc_e['length_record'])) 85 | print('- num_records: {}'.format(tc_e['num_records'])) 86 | if tc_e['record_offsets']: 87 | print('- record offsets: ', end='') 88 | for pt in tc_e['record_offsets']: 89 | print('{:#x} '.format(pt), end='') 90 | print('') 91 | if tc_e['records']: 92 | print('- records: <{:#x}>'.format(tc_e['_records_offset'])) 93 | for i, rec in enumerate(tc_e['records']): 94 | print(' - record {}: <{:#x}>'.format(i, rec['_record_offset'])) 95 | print(' - entryblock number: {:#x}'.format(rec['eb_number'])) 96 | -------------------------------------------------------------------------------- /media/gpt.py: -------------------------------------------------------------------------------- 1 | from struct import unpack, Struct 2 | from binascii import hexlify 3 | 4 | SECTOR_SIZE = 512 5 | GPT_HEADER_OFFSET = 0 6 | GPT_HEADER_SIZE = SECTOR_SIZE 7 | GPT_HEADER_SIGNATURE = b'EFI PART' 8 | 9 | ''' 10 | 0 signature 11 | 8 version, L 12 | 12 size, L 13 | 16 CRC32, L 14 | 20 L 15 | 24 header LBA, Q 16 | ''' 17 | GPT_HEADER_FORMAT = Struct('<8sLLLLQQQQ16sQLLL') 18 | GPT_PART_FORMAT = Struct('<16s16sQQQ72s') 19 | 20 | GUID_LE_FORMAT = Struct('HHL') 22 | GUID_UNUSED_PART_STRING = '00000000-0000-0000-0000-000000000000' 23 | GUID_PART_TYPE_W_BASIC_DATA_PART = 'ebd0a0a2-b9e5-4433-87c0-68b6b72699c7' 24 | GUID_PART_TYPE_W_MS_RESV_PART = 'e3c9e316-0b5c-4db8-817d-f92df00215ae' 25 | GUID_TRANSLATION = { 26 | GUID_PART_TYPE_W_MS_RESV_PART: 'Windows - Microsoft Reserved Partition', 27 | GUID_PART_TYPE_W_BASIC_DATA_PART: 'Windows - Basic data partition' 28 | } 29 | 30 | def guid_string(guid): 31 | _guid_le = GUID_LE_FORMAT.unpack_from(guid, 0) 32 | _guid_bg = GUID_BG_FORMAT.unpack_from(guid, 8) 33 | res = '{:08x}-{:04x}-{:04x}-{:04x}-{:04x}{:08x}'.format( 34 | _guid_le[0], _guid_le[1], _guid_le[2], 35 | _guid_bg[0], _guid_bg[1], _guid_bg[2]) 36 | return res 37 | 38 | def _gpt_init(header): 39 | gpt = {'signature': header[0], 40 | 'revision': header[1], 41 | 'header_size': header[2], 42 | 'header_crc': header[3], 43 | 'cur_lba': header[5], 44 | 'bkp_lba': header[6], 45 | 'first_lba': header[7], 46 | 'last_lba': header[8], 47 | 'disk_guid': guid_string(header[9]), 48 | 'part_lba': header[10], 49 | 'npart': header[11], 50 | 'part_size': header[12], 51 | 'part_crc': header[13] 52 | } 53 | return gpt 54 | 55 | def _gpt_add_partitions(gpt, part_list): 56 | gpt['parts'] = part_list 57 | 58 | def _gpt_part_init(entry, index): 59 | part = {'index': index, 60 | 'type': guid_string(entry[0]), 61 | 'guid': guid_string(entry[1]), 62 | 'first_lba': entry[2], 63 | 'last_lba': entry[3], 64 | 'attr': entry[4], 65 | 'name': entry[5].decode('utf-16le')} 66 | return part 67 | 68 | def _read_gpt_partitions(stream, part_lba, num_part, part_size): 69 | parts = [] 70 | for pi in range(num_part): 71 | part_offset = (part_lba * SECTOR_SIZE) + (pi * part_size) 72 | stream.seek(part_offset, 0) 73 | part_block = stream.read(part_size) 74 | part = GPT_PART_FORMAT.unpack_from(part_block, 0) 75 | part = _gpt_part_init(part, pi) 76 | if part['type'] != GUID_UNUSED_PART_STRING: 77 | parts.append(part) 78 | return parts 79 | 80 | def readGPT(stream, offset=1): 81 | stream.seek(offset * SECTOR_SIZE, 0) 82 | _header = stream.read(GPT_HEADER_SIZE) 83 | header = GPT_HEADER_FORMAT.unpack_from(_header, GPT_HEADER_OFFSET) 84 | gpt = _gpt_init(header) 85 | if gpt['signature'] != GPT_HEADER_SIGNATURE: 86 | print('no gpt header magic found') 87 | return {} 88 | parts = _read_gpt_partitions(stream, gpt['part_lba'], 89 | gpt['npart'], gpt['part_size']) 90 | _gpt_add_partitions(gpt, parts) 91 | return gpt 92 | 93 | def print_gpt_part(part, prefix=''): 94 | print('{}Partition {}:'.format(prefix, part['index'])) 95 | print('{} Type (GUID type): {} ({})'.format(prefix, 96 | GUID_TRANSLATION[part['type']], part['type'])) 97 | print('{} GUID: {}'.format(prefix, part['guid'])) 98 | print('{} First LBA: {}'.format(prefix, part['first_lba'])) 99 | print('{} Last LBA: {}'.format(prefix, part['last_lba'])) 100 | print('{} Attributes: {}'.format(prefix, part['attr'])) 101 | print('{} Name: {}'.format(prefix, part['name'])) 102 | 103 | def print_gpt(gpt): 104 | print('GPT media') 105 | print('Signature: {}'.format(gpt['signature'].decode('utf-8'))) 106 | print('Revision: {}'.format(gpt['revision'])) 107 | print('Header size: {}'.format(gpt['header_size'])) 108 | print('Header CRC: {}'.format(gpt['header_crc'])) 109 | print('Current LBA: {}'.format(gpt['cur_lba'])) 110 | print('Backup LBA: {}'.format(gpt['bkp_lba'])) 111 | print('First LBA: {}'.format(gpt['first_lba'])) 112 | print('Last LBA: {}'.format(gpt['last_lba'])) 113 | print('Disk GUID: {}'.format(gpt['disk_guid'])) 114 | print('Partitions LBA: {}'.format(gpt['part_lba'])) 115 | print('Number of partitions: {}'.format(gpt['npart'])) 116 | print('Partition size: {}'.format(gpt['part_size'])) 117 | print('Partition CRC: {}'.format(gpt['part_crc'])) 118 | print('Partition list:') 119 | for part in gpt['parts']: 120 | print_gpt_part(part, ' ') 121 | -------------------------------------------------------------------------------- /part/refs/vol.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from struct import Struct 3 | from util.hexdump import hexdump 4 | 5 | SECTOR_SIZE = 512 6 | REFS_VR_JUMPINSTRUCTION_OFFSET = 0x0 7 | REFS_VR_JUMPINSTRUCTION_SIZE = 3 8 | REFS_VR_FILESYSTEMNAME_OFFSET = 0x3 9 | REFS_VR_FILESYSTEMNAME_SIZE = 8 10 | REFS_VR_RESV_OFFSET = 0x7 11 | REFS_VR_RESV_SIZE = 5 12 | REFS_VR_STRUCT_ID_OFFSET = 0x10 13 | REFS_VR_STRUCT_ID_SIZE = 4 14 | REFS_VR_NUMBYTES_OFFSET = 0x14 15 | REFS_VR_NUMBYTES_SIZE = 2 16 | REFS_VR_CHECKSUM_OFFSET = 0x16 17 | REFS_VR_CHECKSUM_SIZE = 2 18 | REFS_VR_BACKUP_OFFSET = 0x18 19 | REFS_VR_BACKUP_SIZE = 8 20 | REFS_VR_BYTES_PER_SECTOR_OFFSET = 0x20 21 | REFS_VR_BYTES_PER_SECTOR_SIZE = 4 22 | REFS_VR_SECTORS_PER_CLUSTER_OFFSET = 0x24 23 | REFS_VR_SECTORS_PER_CLUSTER_SIZE = 4 24 | REFS_VR_VERSION_MAJOR_OFFSET = 0x28 25 | REFS_VR_VERSION_MAJOR_SIZE = 1 26 | REFS_VR_VERSION_MINOR_OFFSET = 0x29 27 | REFS_VR_VERSION_MINOR_SIZE = 1 28 | REFS_VR_UNKNOWN_OFFSET = 0x2a 29 | REFS_VR_UNKNOWN_SIZE = 14 30 | REFS_VR_SN_OFFSET = 0x38 31 | REFS_VR_SN_SIZE = 8 32 | REFS_VR_SIZE = 64 33 | 34 | REFS_VR_JUMPINSTRUCTION_POS = 0 35 | REFS_VR_FILESYSTEMNAME_POS = 1 36 | REFS_VR_RESV_POS = 2 37 | REFS_VR_STRUCT_ID_POS = 3 38 | REFS_VR_NUMBYTES_POS = 4 39 | REFS_VR_CHECKSUM_POS = 5 40 | REFS_VR_BACKUP_POS = 6 41 | REFS_VR_BYTES_PER_SECTOR_POS = 7 42 | REFS_VR_SECTORS_PER_CLUSTER_POS = 8 43 | REFS_VR_VERSION_MAJOR_POS = 9 44 | REFS_VR_VERSION_MINOR_POS = 10 45 | REFS_VR_UNKNOWN_POS = 11 46 | REFS_VR_SN_POS = 12 47 | 48 | REFS_VR_FILESYSTEMNAME_SIGNATURE = 'ReFS\x00\x00\x00\x00' 49 | REFS_VR_STRUCT_ID_SIGNATURE = 'FSRS' 50 | 51 | REFS_VR_FORMAT = Struct('<3s8s5s4sHHQLLBB14sQ') 52 | _JUMPINSTRUCTION_FORMAT = Struct('BBB') 53 | 54 | def is_refs_part(dump, lba): 55 | """Check the given LBA for the ReFS signature and 56 | filesystem strings to decide if it is an ReFS volume.""" 57 | dump.seek(lba * SECTOR_SIZE, 0) 58 | header = dump.read(REFS_VR_SIZE) 59 | signature = header[REFS_VR_FILESYSTEMNAME_OFFSET:REFS_VR_FILESYSTEMNAME_OFFSET + REFS_VR_FILESYSTEMNAME_SIZE].decode('ascii') 60 | fs = header[0x10:0x14].decode('ascii') 61 | return (signature == REFS_VR_FILESYSTEMNAME_SIGNATURE and 62 | fs == REFS_VR_STRUCT_ID_SIGNATURE) 63 | 64 | def _check_is_refs_part(dump, lba): 65 | if not is_refs_part(dump, lba): 66 | print("ERROR(part.refs.fsstat): no ReFS partition") 67 | sys.exit(1) 68 | 69 | def _dec_volume_record(dump, lba): 70 | dump.seek(lba * SECTOR_SIZE, 0) 71 | vr_raw = dump.read(REFS_VR_SIZE) 72 | vr_entries = REFS_VR_FORMAT.unpack_from(vr_raw, 0) 73 | ji_entries = _JUMPINSTRUCTION_FORMAT.unpack_from(vr_entries[REFS_VR_JUMPINSTRUCTION_OFFSET], 0) 74 | ji = ji_entries[0] + (ji_entries[1] * 256) + (ji_entries[2] * 256 * 256) 75 | vr = {'absolute_lba': lba, 76 | 'jump_instruction': ji, 77 | 'filesystem_name': vr_entries[REFS_VR_FILESYSTEMNAME_POS], 78 | 'reserved': vr_entries[REFS_VR_RESV_POS], 79 | 'struct_id': vr_entries[REFS_VR_STRUCT_ID_POS], 80 | 'numbytes': vr_entries[REFS_VR_NUMBYTES_POS], 81 | 'checksum': vr_entries[REFS_VR_CHECKSUM_POS], 82 | 'backup': vr_entries[REFS_VR_BACKUP_POS], 83 | 'bytes_per_sector': vr_entries[REFS_VR_BYTES_PER_SECTOR_POS], 84 | 'sectors_per_cluster': vr_entries[REFS_VR_SECTORS_PER_CLUSTER_POS], 85 | 'version_major': vr_entries[REFS_VR_VERSION_MAJOR_POS], 86 | 'version_minor': vr_entries[REFS_VR_VERSION_MINOR_POS], 87 | 'unknown': vr_entries[REFS_VR_UNKNOWN_POS], 88 | 'serial_number': vr_entries[REFS_VR_SN_POS]} 89 | return vr 90 | 91 | def fsstat(dump, lba, last_lba): 92 | _check_is_refs_part(dump, lba) 93 | vr = _dec_volume_record(dump, lba) 94 | vr_backup = _dec_volume_record(dump, last_lba) 95 | fs = {'volume_record': vr, 96 | 'volume_record_backup': vr_backup} 97 | return fs 98 | 99 | def _dump_volume_record(vr): 100 | print('- Jump instruction: {:#08x}'.format(vr['jump_instruction'])) 101 | print('- Filesystem name: {}'.format(vr['filesystem_name'].decode('utf-8'))) 102 | print('- Struct identifier: {}'.format(vr['struct_id'].decode('utf-8'))) 103 | print('- Size of volume record: {}'.format(vr['numbytes'])) 104 | print('- FSRS checksum: {:#06x}'.format(vr['checksum'])) 105 | print('- Volume record backup LBA offset: {}'.format(vr['backup'])) 106 | print('- Bytes per sector: {}'.format(vr['bytes_per_sector'])) 107 | print('- Sectors per cluster: {}'.format(vr['sectors_per_cluster'])) 108 | print('- Filesystem major version: {}'.format(vr['version_major'])) 109 | print('- Filesystem minor version: {}'.format(vr['version_minor'])) 110 | print('- Volume serial number: {}'.format(vr['serial_number'])) 111 | 112 | def _dump_fsstat_volume_record_main(fs): 113 | vr = fs['volume_record'] 114 | print('Volume record (lba: {}):'.format(vr['absolute_lba'])) 115 | _dump_volume_record(vr) 116 | 117 | def _dump_fsstat_volume_record_backup(fs): 118 | vr = fs['volume_record_backup'] 119 | print('Volume record backup (lba: {}):'.format(vr['absolute_lba'])) 120 | _dump_volume_record(vr) 121 | 122 | def dump_fsstat(fs): 123 | _dump_fsstat_volume_record_main(fs) 124 | _dump_fsstat_volume_record_backup(fs) 125 | -------------------------------------------------------------------------------- /util/filetree.py: -------------------------------------------------------------------------------- 1 | import part.refs.attribute as rattr 2 | import part.refs.entry_block as reb 3 | import util.carving as carving 4 | 5 | def filetree(dump, v_offset, v_end_offset, nodeid, block_list = None, block_size = 16 * 1024): 6 | if not block_list: 7 | block_list = carving.find_blocks(dump, v_offset, v_end_offset, block_size) 8 | tree = _filetree(dump, nodeid, block_list) 9 | print(tree) 10 | return tree 11 | 12 | def _filetree(dump, nodeid, complete_block_list): 13 | node_block_list = [ reb.read_entryblock(dump, x['offset']) 14 | for x in complete_block_list 15 | if x['nodeid'] == nodeid ] 16 | if not node_block_list: 17 | return None 18 | node_ext_block_list = [ x for x in node_block_list if x['_contains_extents'] ] 19 | node_rec_block_list = [ x for x in node_block_list if x['_contains_records'] ] 20 | rec_block_list = [] 21 | if node_ext_block_list: 22 | root_ext_block_list = [] 23 | for block in node_ext_block_list: 24 | root = True 25 | for _block in node_ext_block_list: 26 | if block['eb_number'] in [ ext['eb_number'] for ext in _block['extents'] ]: 27 | root = False 28 | break 29 | if root: 30 | root_ext_block_list.append(block) 31 | max_counter = max([ x['counter'] for x in root_ext_block_list ]) 32 | ext_block = [ x for x in root_ext_block_list if x['counter'] == max_counter ][0] 33 | rec_block_list.append(ext_block) 34 | while [ x for x in rec_block_list if x['_contains_extents'] ]: 35 | for i in range(len(rec_block_list)): 36 | block = rec_block_list[i] 37 | if block['_contains_extents']: 38 | blocks = [] 39 | for blockid in [ b['eb_number'] for b in block['extents'] ]: 40 | blocks.append([ b for b in node_block_list if b['eb_number'] == blockid ][0]) 41 | rec_block_list[i] = blocks 42 | else: 43 | rec_block_list[i] = [rec_block_list[i]] 44 | _flatten = [ i for sub in rec_block_list for i in sub ] 45 | rec_block_list = _flatten 46 | else: 47 | if not node_rec_block_list: 48 | print('ERROR: didn\'t found any entryblock with extensions or records in any of the entryblocks of node {:#x} ({} entryblocks).'.format(nodeid, len(node_block_list))) 49 | return None 50 | max_counter = max([ x['counter'] for x in node_rec_block_list ]) 51 | block = [ x for x in node_rec_block_list if x['counter'] == max_counter ][0] 52 | rec_block_list.append(block) 53 | dm = _get_directory_metadata(rec_block_list) 54 | if not dm: 55 | return None 56 | rootname = _get_directory_metadata_name(dm) 57 | files = _get_files(rec_block_list) 58 | folders = _get_folders(rec_block_list, dump, complete_block_list) 59 | folder = {'name': rootname, 60 | 'nodeid': rec_block_list[0]['node_id'], 61 | # 'entryblocks': block_list, 62 | 'files': files, 63 | 'folders': folders} 64 | return folder 65 | 66 | def _get_directory_metadata(blocks): 67 | for block in blocks: 68 | if 'pointers_data' in block.keys(): 69 | ptrs = [ ptr 70 | for ptr in block['pointers_data'] 71 | if ptr['type'] == rattr.ATTR_TYPE_DIRECTORY_METADATA ] 72 | if ptrs: 73 | return ptrs[0] 74 | else: 75 | reb.dump_entryblock(block) 76 | # should never reach this point 77 | print('ERROR: no directory metadata record found for node {}.'.format(blocks[0]['node_id'])) 78 | return None 79 | 80 | def _get_directory_metadata_name(attr): 81 | return [ ptr['name'] 82 | for ptr in attr['pointers_data'] 83 | if ptr['type'] == rattr.DM_SUBATTR_TYPE_FOLDER ][0] 84 | 85 | def _get_files(blocks): 86 | files = [] 87 | for block in blocks: 88 | bfiles = [ {'name': ptr['filename'], 89 | 'blockid': block['eb_number']} 90 | for ptr in block['pointers_data'] 91 | if ptr['type'] == rattr.ATTR_TYPE_FILENAME ] 92 | files.extend(bfiles) 93 | return files 94 | 95 | def _get_folders(blocks, dump, complete_block_list): 96 | folders = [] 97 | for block in blocks: 98 | bfolders = [ {'name': ptr['foldername'], 99 | 'blockid': block['eb_number'], 100 | 'tree': _filetree(dump, ptr['nodeid'], complete_block_list)} 101 | for ptr in block['pointers_data'] 102 | if ptr['type'] == rattr.ATTR_TYPE_FILENAME_FOLDER ] 103 | folders.extend(bfolders) 104 | return folders 105 | 106 | def dump_filetree(tree, level=0, name='.'): 107 | if not tree: 108 | return 109 | print('D {} {} ({}) {:#x}'.format(level * '.', 110 | name, 111 | tree['name'].decode('utf-16le') if tree['name'] else '', 112 | tree['nodeid'])) 113 | for f in tree['files']: 114 | print('F {} {} {:#x}'.format((level + 1) * '.', 115 | f['name'].decode('utf-16le') if f['name'] else '', 116 | f['blockid'])) 117 | for f in tree['folders']: 118 | dump_filetree(f['tree'], level + 1, f['name'].decode('utf-16le')) 119 | -------------------------------------------------------------------------------- /util/carving.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | from struct import Struct 4 | import part.refs.entry_block as reb 5 | import part.refs.tree_control as rtc 6 | import part.refs.attribute as refs_attr 7 | import part.refs.allocator as alloc 8 | import part.refs.object_tree as rot 9 | from util.hexdump import hexdump 10 | from util.table import print_table 11 | 12 | SECTOR_SIZE = 512 13 | ENTRYBLOCK_SIZE = 16 * 1024 14 | CLUSTER_SIZE = 4 * ENTRYBLOCK_SIZE 15 | counter_offset=0x8 16 | nodeid_offset=0x18 17 | childid_offset=0x20 18 | ENTRYBLOCK_FORMAT=Struct(''}, 78 | {'key': 'fnas', 'header': 'FNAs', 'align': '>', 79 | 'transform': lambda x: 'N/A' if x == None else len(x)}, 80 | {'key': 'folderids', 'header': 'Folders', 'align': '>', 81 | 'transform': lambda x: 'N/A' if x == None else len(x)} ] 82 | if blocks: 83 | print_table(columns, blocks) 84 | 85 | def blocks_with_filename_attributes(dump, blocks, block_size = ENTRYBLOCK_SIZE): 86 | blocks_found = [] 87 | for block in blocks: 88 | dump.seek(block['offset']) 89 | data = dump.read(block_size) 90 | fileids = find_bytes([0x30,0,1,0], data) 91 | if fileids: 92 | block['fnas'] = [ x + block['offset'] - 0x10 for x in fileids ] 93 | blocks_found.append(block) 94 | else: 95 | block['fnas'] = [] 96 | return blocks_found 97 | 98 | def blocks_with_folder_attributes(dump, blocks, block_size = ENTRYBLOCK_SIZE): 99 | blocks_found = [] 100 | for block in blocks: 101 | dump.seek(block['offset'], 0) 102 | data = dump.read(block_size) 103 | _folderids = find_bytes([0x30,0,2,0], data) 104 | folderids = [] 105 | # this loop tries to check that the pattern found is in a filename_folder 106 | # attribute 107 | # TODO: find a better way to determine if the pattern is in a filename_folder 108 | # attribute or in a folder attribute 109 | for fid in _folderids: 110 | dump.seek(fid + block['offset'] - 0x10 + 0x4, 0) 111 | data = dump.read(2) 112 | offset = data[0] + (data[1] * 256) 113 | if offset == 0x10: 114 | folderids.append(fid) 115 | if folderids: 116 | block['folderids'] = [ x + block['offset'] - 0x10 for x in folderids ] 117 | blocks_found.append(block) 118 | else: 119 | block['folderids'] = [] 120 | return blocks_found 121 | 122 | def blocks_with_child_attributes(dump, blocks, block_size = ENTRYBLOCK_SIZE): 123 | blocks_found = [] 124 | for block in blocks: 125 | dump.seek(block['offset']) 126 | data = dump.read(block_size) 127 | ids = find_bytes([0x20,0,0,0x80], data) 128 | if ids: 129 | block['childids'] = [ x + block['offset'] - 0x10 for x in ids ] 130 | blocks_found.append(block) 131 | return blocks_found 132 | -------------------------------------------------------------------------------- /list_entryblock.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | from struct import Struct 4 | import part.refs.entry_block as reb 5 | import part.refs.tree_control as rtc 6 | import part.refs.attribute as refs_attr 7 | import part.refs.allocator as alloc 8 | import part.refs.object_tree as rot 9 | from util.hexdump import hexdump 10 | 11 | SECTOR_SIZE = 512 12 | ENTRYBLOCK_SIZE = 16 * 1024 13 | 14 | def blocks_with_file_attributes(dump, blocks, block_size): 15 | blocks_found = [] 16 | for block in blocks: 17 | dump.seek(block['offset']) 18 | data = dump.read(block_size) 19 | fileids = find_bytes([0x30,0,1,0], data) 20 | folderids = find_bytes([0x30,0,2,0], data) 21 | if fileids or folderids: 22 | block['fileids'] = [ x + block['offset'] - 0x10 for x in fileids ] 23 | block['folderids'] = [ x + block['offset'] - 0x10 for x in folderids ] 24 | blocks_found.append(block) 25 | return blocks_found 26 | 27 | def blocks_with_child_attributes(dump, blocks, block_size): 28 | blocks_found = [] 29 | for block in blocks: 30 | dump.seek(block['offset']) 31 | data = dump.read(block_size) 32 | ids = find_bytes([0x20,0,0,0x80], data) 33 | if ids: 34 | block['childids'] = [ x + block['offset'] - 0x10 for x in ids ] 35 | blocks_found.append(block) 36 | return blocks_found 37 | 38 | def find_bytes(entry, block): 39 | hits = [] 40 | for i in range(0, len(block) - (len(entry) - 1)): 41 | match = True 42 | for l in range(0, len(entry)): 43 | match = entry[l] == block[i + l] 44 | if not match: 45 | break 46 | if match: 47 | hits.append(i) 48 | return hits 49 | 50 | def find_blocks(dump, offset, end, step): 51 | blocks = [] 52 | for i in range(offset,end,step): 53 | dump.seek(i, 0) 54 | data = dump.read(4) 55 | entryblock, = ENTRYBLOCK_FORMAT.unpack_from(data, 0) 56 | if (i - offset) / step == entryblock: 57 | # if entryblock != 0: 58 | # if reb.is_entry_block(dump, i, offset): 59 | dump.seek(i+nodeid_offset,0) 60 | data = dump.read(4) 61 | nodeid, = NODEID_FORMAT.unpack_from(data, 0) 62 | dump.seek(i+childid_offset,0) 63 | data = dump.read(4) 64 | childid, = NODEID_FORMAT.unpack_from(data, 0) 65 | dump.seek(i+counter_offset, 0) 66 | counter = dump.read(1)[0] 67 | steps = int((i - offset) / step) 68 | block = {'offset': i, 'entryblock': entryblock, 69 | 'counter': counter, 'nodeid': nodeid, 70 | 'childid': childid} 71 | blocks.append(block) 72 | return blocks 73 | 74 | parser = argparse.ArgumentParser(description='ReFS carving on provided dump.') 75 | parser.add_argument('dump', action='store', 76 | type=argparse.FileType(mode='rb')) 77 | args = parser.parse_args() 78 | 79 | # ref 80 | offset = 0x8100000 81 | end = (41940991*512)+512 # complete partition 82 | # phys 9 83 | # offset = 65664 * 512 84 | # end = 1114239 * 512 85 | # end = offset + (20000 * 1024 * 16) 86 | # end = 0x16d34000 87 | step= 16 * 1024 88 | counter_offset=0x8 89 | nodeid_offset=0x18 90 | childid_offset=0x20 91 | ENTRYBLOCK_FORMAT=Struct('3} {:#010x} {:#010x}'.format(b['offset'], 100 | b['offset'] + step, b['entryblock'], b['counter'], b['nodeid'], b['childid'])) 101 | 102 | blocks_with_files = blocks_with_file_attributes(args.dump, blocks, step) 103 | print("==================================================================") 104 | print("Blocks found with files found") 105 | print("==================================================================") 106 | for b in blocks_with_files: 107 | print('{:#010x} - {:#010x}: {:#06x} {:>3} {:#06x} {:#06x} {:4} {:4}'.format(b['offset'], 108 | b['offset'] + step, b['entryblock'], b['counter'], b['nodeid'], b['childid'], 109 | len(b['fileids']), len(b['folderids']))) 110 | # 111 | print("==================================================================") 112 | print("Files found") 113 | print("==================================================================") 114 | for block in blocks_with_files: 115 | for fid in block['fileids']: 116 | attr = refs_attr.read_attribute(args.dump, fid) 117 | try: 118 | filename = attr['filename'].decode('utf-16le') 119 | except: 120 | filename = attr['filename'] 121 | print('{:#010x} {:#06x} {:#06x} {:#06x} {:#010x} {}'.format( 122 | block['offset'], block['entryblock'], 123 | block['nodeid'], block['childid'], attr['absolute_offset'], filename)) 124 | # 125 | print("==================================================================") 126 | print("Folders found") 127 | print("==================================================================") 128 | for block in blocks_with_files: 129 | for fid in block['folderids']: 130 | attr = refs_attr.read_attribute(args.dump, fid) 131 | try: 132 | foldername = attr['foldername'].decode('utf-16le') 133 | except: 134 | foldername = attr['foldername'] 135 | print('{:#010x} {:#06x} {:#06x} {:#06x} {:#010x} {}'.format( 136 | block['offset'], block['entryblock'], 137 | block['nodeid'], block['childid'], attr['absolute_offset'], foldername)) 138 | 139 | args.dump.close() 140 | -------------------------------------------------------------------------------- /part/refs/entry_block.py: -------------------------------------------------------------------------------- 1 | from struct import Struct 2 | import part.refs.attribute as rattr 3 | 4 | SECTOR_SIZE = 512 5 | ENTRYBLOCK_SIZE = 16 * 1024 6 | 7 | EB_NUM_OFFSET = 0 8 | EB_OWN_OFFSET_OFFSET = 0x38 9 | EB_LENGTH_OWN_OFFSET_OFFSET = 0x3c 10 | EB_NUM_POINTERS_EXTENT_OFFSET = 0x58 11 | EB_NUM_SIZE = 8 12 | EB_OWN_OFFSET_SIZE = 4 13 | EB_LENGTH_OWN_OFFSET_SIZE = 4 14 | EB_NUM_POINTERS_EXTENT_SIZE = 4 15 | 16 | EB_HEADER_FORMAT = Struct(' ({size},{size:#x})'.format(eb['_absolute_offset'], 131 | size=eb['_structure_size'])) 132 | print('- entryblock number: {:#x}'.format(eb['eb_number'])) 133 | print('- counter: {}'.format(eb['counter'])) 134 | print('- node id: {:#x}'.format(eb['node_id'])) 135 | print('- node descriptor length: {val:#x} ({val})'.format(val=eb['node_desc_length'])) 136 | if 'num_extents' in eb: 137 | print('- number of extents: {}'.format(eb['num_extents'])) 138 | if 'num_records' in eb: 139 | print('- number of records: {}'.format(eb['num_records'])) 140 | if eb['_contains_records']: 141 | print('- node header offset: {val:#x} ({val})'.format(val=eb['node_header_offset'])) 142 | print('- node header length: {val:#x} ({val})'.format(val=eb['header_length'])) 143 | print('- offset to next free record: {val:#x} ({val})'.format(val=eb['offset_free_record'])) 144 | print('- free space in node: {val:#x} ({val})'.format(val=eb['free_space'])) 145 | print('- node header unknown: {val:#x} ({val})'.format(val=eb['header_unknown'])) 146 | print('- offset to first pointer: {val:#x} ({val}) -> entryblock offset: {ebo:#x}'.format( 147 | val=eb['offset_first_pointer'], 148 | ebo=eb['offset_first_pointer']+eb['node_header_offset'])) 149 | print('- number of pointers in node: {}'.format(eb['num_pointers'])) 150 | print('- offset to end node: {val:#x} ({val}) -> entryblock offset: {ebo:#x}'.format( 151 | val=eb['offset_end_node'], 152 | ebo=eb['offset_end_node']+eb['node_header_offset'])) 153 | print('- pointers:', end='') 154 | if eb['pointers']: 155 | for ptr in eb['pointers']: 156 | print(' {:#x}'.format(ptr), end='') 157 | print('') 158 | print('- pointers data:') 159 | for ptr_i, ptr in enumerate(eb['pointers_data']): 160 | print(' - pointer {}: <{:#x}>'.format(ptr_i, ptr['_absolute_offset'])) 161 | rattr.dump_attribute(ptr, ' ') 162 | else: 163 | print('None') 164 | print('- pointers data: None') 165 | elif eb['_contains_extents']: 166 | print('- extent table offset: {val:#x} ({val})'.format(val=eb['extent_table_offset'])) 167 | print('- extent table length: {val:#x} ({val})'.format(val=eb['extent_table_length'])) 168 | print('- extent table unknown 0: {val:#x} ({val})'.format(val=eb['extent_table_unknown0'])) 169 | print('- extent table unknown 1: {val:#x} ({val})'.format(val=eb['extent_table_unknown1'])) 170 | print('- extent table unknown 2: {val:#x} ({val})'.format(val=eb['extent_table_unknown2'])) 171 | print('- offset to first extent pointer: {val:#x} ({val})'.format( 172 | val=eb['offset_first_extent_pointer'])) 173 | print('- number of extent pointers: {}'.format(eb['num_extent_pointers'])) 174 | print('- offset en of extent pointers: {val:#x} ({val})'.format( 175 | val=eb['offset_end_of_extent_pointers'])) 176 | print('- extent table unknown 3: {val:#x} ({val})'.format(val=eb['extent_table_unknown3'])) 177 | print('- extent pointers:', end='') 178 | for ptr in eb['extent_pointers']: 179 | print(' {:#x}'.format(ptr), end='') 180 | print('') 181 | print('- extents:') 182 | for ext_i, ext in enumerate(eb['extents']): 183 | print(' - extent {}: <{:#x}>'.format(ext_i, ext['_absolute_offset'])) 184 | _dump_extent(ext, ' ') 185 | 186 | def _dump_extent(ext, prefix): 187 | print('{}- size: {val:#x} ({val})'.format(prefix, val=ext['size'])) 188 | print('{}- unknown 0: {val:#x} ({val})'.format(prefix, val=ext['unknown0'])) 189 | print('{}- unknown 1: {val:#x} ({val})'.format(prefix, val=ext['unknown1'])) 190 | print('{}- header length: {val:#x} ({val})'.format(prefix, val=ext['header_length'])) 191 | print('{}- body length: {val:#x} ({val})'.format(prefix, val=ext['body_length'])) 192 | print('{}- entryblock id: {val:#x} ({val})'.format(prefix, val=ext['eb_number'])) 193 | print('{}- 0x0000000808020000: {:#018x}'.format(prefix, ext['0x0000000808020000'])) 194 | print('{}- crc(?): {:#018x}'.format(prefix, ext['crc'])) 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | `pyrefs` provides a series of libraries helpful to parse and analyze Resilient 4 | FileSystems (ReFS). 5 | An interactive application to analyze a ReFS dump named `Igor` is also 6 | provided. 7 | 8 | `pyrefs` is still very immature, as of today it has been only tested with 9 | partitions using ReFS version 1.1. 10 | The code is neither far from perfect, and it would need a good refactoring. 11 | We are working on that, but be aware when you retrieve `pyrefs`. 12 | 13 | Finally, `pyrefs` naming convention for ReFS structures is mainly guided by 14 | Henry Georges thesis work. 15 | You can find Henry Georges's thesis and the code he developed to analyze ReFS 16 | here . 17 | 18 | ## Authors 19 | 20 | - Daniel Gracia Pérez \<\> 21 | - Eric Jouenne \<\> 22 | - Tony Gerard \<\> 23 | - Francois-Xavier Babin \<\> 24 | 25 | ## Thanks 26 | 27 | We would like to thank: Henry Georges, his thesis document has been the 28 | foundation of this project; Willi Ballenthin, for the ReFS dump images on his 29 | website and his ReFS articles; and all the other authors providing information 30 | on the ReFS filesystem. 31 | 32 | We would like to also thank Juan Romero, Olivier Gilles, Isabelle Isabelle 33 | Moukala-Mouberi and Claudia Duhoo, who performed a similar work than ours. 34 | Your feedback was unvaluable. 35 | 36 | # Dependencies 37 | 38 | `pyrefs` was developed using Python 3 (Python 3.6.6) at the time of this 39 | release, and has been tested to work on Windows and Linux platforms. 40 | 41 | No special dependencies, if you find any please let us know. 42 | 43 | # Sources organization 44 | 45 | ``` 46 | . 47 | +-- README.md 48 | +-- LICENSE 49 | +-- igor.py 50 | +-- part 51 | | +-- refs 52 | | +-- allocator.py 53 | | +-- attribute.py 54 | | +-- entry_block.py 55 | | +-- object.py 56 | | +-- object_tree.py 57 | | +-- tree_control.py 58 | | +-- vol.py 59 | +-- media 60 | | +-- gpt.py 61 | | +-- mbr.py 62 | +-- util 63 | | +-- carving.py 64 | | +-- filetree.py 65 | | +-- hexdump.py 66 | | +-- table.py 67 | | +-- time.py 68 | +-- examples 69 | +-- carving.rec 70 | +-- filetree.rec 71 | +-- tree_explorer.rec 72 | ``` 73 | 74 | # Igor 75 | 76 | An interactive command line tool is provided to facilitate the analysis of 77 | those not willing to play with the code. 78 | We call it `Igor` and it can be launched with Python. 79 | 80 | `Igor` provides a limited toolset to analyze ReFS partitions exploiting the 81 | `pyrefs` library, the following sections enumerate them. 82 | 83 | ## Examples 84 | 85 | Some examples of Igor usage are provided in the `examples` folder as Igor 86 | records. 87 | To execute/playback the examples contained in the `examples` folder download 88 | the `Windows8ReFSImages.zip` file provided by Willi Ballenthin at 89 | , copy it to the 90 | `examples/dump` folder and uncompress it. 91 | Then you can use igor to test the records in the examples folder with the 92 | command `playback examples/`. 93 | 94 | ## Command reference 95 | 96 | ### file 97 | 98 | Usage: `file [-h] [-i] [-f] [-F] dump` 99 | 100 | Load the provided dump file for analysis, and automatically select the ReFS 101 | partition for you. 102 | You can also initialize the list of entryblocks of the partition. 103 | 104 | Positional arguments: 105 | 106 | - `dump`: File to use as dump for the analysis. 107 | 108 | Optional arguments: 109 | 110 | - `-h`, `--help`: show this help message and exit 111 | - `-i`, `--initiliaze-entryblocks`: find blocks in provided dump 112 | - `-f`, `--files`: find files in provided dump (only considered if -i defined) 113 | - `-F`, `--folders`: find folders in provided dump (only considered if -i 114 | defined) 115 | 116 | ### vol 117 | 118 | Usage: `vol [-h]` 119 | 120 | Dump the volume record information from the current ReFS partition. 121 | 122 | Optional arguments: 123 | 124 | - `-h`, `--help`: show this help message and exit 125 | 126 | Dump the volume record information from the current ReFS partition. 127 | 128 | ### part 129 | 130 | Usage: `part [-h] [partidx]` 131 | 132 | Show available partitions in the currently loaded dump if no parameter is 133 | given, switch to the provided partition if any provided. 134 | 135 | Positional arguments: 136 | 137 | - `partidx`: Partition index of the partition to use for analysis 138 | 139 | Optional arguments: 140 | 141 | - `-h`, `--help`: show this help message and exit 142 | 143 | ### find\_entryblocks 144 | 145 | Usage: `find_entryblocks [-h] [-f] [-F]` 146 | 147 | Find and show all the entryblocks in current partition. If requested number of 148 | files and folders information will be also collected. 149 | 150 | Optional arguments: 151 | 152 | - `-h`, `--help`: show this help message and exit 153 | - `-f`, `--files`: Collect information on the number of files in the 154 | entryblocks. 155 | - `-F`, `--folders`: Collect information on the number of folders in the 156 | entryblocks. 157 | 158 | ### find\_pattern 159 | 160 | Usage: `find_pattern [-h] pattern` 161 | 162 | Find a data pattern in all the blocks of the current partition. Special 163 | characters (including spaces, carriage return, etc.) are not allowed in the 164 | pattern, think of escaping them. 165 | 166 | Positional arguments: 167 | 168 | - `pattern`: Pattern to find in the current partition blocks. 169 | 170 | Optional arguments: 171 | 172 | - `-h`, `--help`: show this help message and exit 173 | 174 | ### list\_filenames 175 | 176 | Usage: `list_filenames [-h]` 177 | 178 | List the found filenames from the list of entryblocks with filenames. 179 | 180 | Optional arguments: 181 | 182 | - `-h`, `--help`: show this help message and exit 183 | 184 | ### list\_folders 185 | 186 | Usage: `list_folders [-h]` 187 | 188 | List the found filename folders from the list of entryblocks with folders. 189 | 190 | Optional arguments: 191 | 192 | - `-h`, `--help`: show this help message and exit 193 | 194 | ### entryblock 195 | 196 | Usage: `entryblock [-h] entryblock_identifier` 197 | 198 | Dump the provided entryblock identifier as EntryBlock. 199 | 200 | Positional arguments: 201 | 202 | - `entryblock_identifier`: entryblock identifier of the EntryBlock to dump 203 | 204 | Optional arguments: 205 | 206 | - `-h`, `--help`: show this help message and exit 207 | 208 | ### tree\_control 209 | 210 | Usage: `tree_control [-h] [entryblock_identifier]` 211 | 212 | Parse the given entryblock identifier as a TreeControl. 213 | If no entryblock identifier is provided it uses 0x1e as entryblock identifier. 214 | 215 | Positional arguments: 216 | 217 | - `entryblock_identifier`: entryblock identifier of the TreeControl to dump 218 | 219 | Optional arguments: 220 | 221 | - `-h`, `--help`: show this help message and exit 222 | 223 | ### tree\_control\_extension 224 | 225 | Usage: `tree_control_extension [-h] entryblock_identifier` 226 | 227 | Parse the given entryblock identifier as a TreeControlExtension. 228 | 229 | Positional arguments: 230 | 231 | - `entryblock_identifier`: entryblock identifier of the TreeControlExtension 232 | to dump 233 | 234 | Optional arguments: 235 | 236 | - `-h`, `--help`: show this help message and exit 237 | 238 | ### object\_tree 239 | 240 | Usage: `object_tree [-h] entryblock_identifier` 241 | 242 | Parse the given entryblock identifier as an ObjectTree. 243 | 244 | Positional arguments: 245 | 246 | - `entryblock_identifier`: entryblock identifier of the ObjectTree to dump 247 | 248 | Optional arguments: 249 | 250 | - `-h`, `--help`: show this help message and exit 251 | 252 | ### allocator 253 | 254 | Usage: `allocator [-h] entryblock_identifier` 255 | 256 | Parse the given entryblock identifier as an Allocator. 257 | 258 | Positional arguments: 259 | 260 | - `entryblock_identifier`: entryblock identifier of the Allocator to dump 261 | 262 | Optional arguments: 263 | 264 | - `-h`, `--help`: show this help message and exit 265 | 266 | ### attribute 267 | 268 | Usage: `attribute [-h] dump_offset` 269 | 270 | Parse the given dump offset (in bytes) as an Attribute. 271 | 272 | Positional arguments: 273 | 274 | - `dump_offset`: dump offset (in bytes) of the Attribute to dump 275 | 276 | Optional arguments: 277 | 278 | - `-h`, `--help`: show this help message and exit 279 | 280 | ### datastream 281 | 282 | Usage: 283 | ``` 284 | datastream [-h] output_filename output_size 285 | entryblock_identifier,number_of_blocks 286 | [entryblock_identifier,number_of_blocks ...] 287 | ``` 288 | 289 | Extract data from dump from the given datarun. 290 | Dataruns can be extracted by exploring the EntryBlocks (command `entryblock`) 291 | or using the `list_dataruns` command. 292 | 293 | Positional arguments: 294 | 295 | - `output_filename`: name of the generated output file 296 | - `output_size`: total amount of data to extract 297 | - `entryblock_identifier,number_of_blocks`: datarun to follow for the data extraction 298 | 299 | Optional arguments: 300 | 301 | - `-h`, `--help`: show this help message and exit 302 | 303 | ### list\_dataruns 304 | 305 | Usage: `list_dataruns [-h]` 306 | 307 | Retrieve list of all the files dataruns. 308 | 309 | Optional arguments: 310 | 311 | - `-h`, `--help`: show this help message and exit 312 | 313 | ### filetree 314 | 315 | Usage: `filetree [-h] node_id` 316 | 317 | Extract the file tree structure from the given node (use node 0x600 by 318 | default). 319 | 320 | Positional arguments: 321 | 322 | - `node_id`: node identifier of the node to extract the file tree structure 323 | from 324 | 325 | Optional arguments: 326 | 327 | - `-h`, `--help`: show this help message and exit 328 | 329 | ### hexdump 330 | 331 | Usage: `hexdump [-h] dump_offset size` 332 | 333 | Hexdump the number of bytes at the provided offset. 334 | 335 | Positional arguments: 336 | 337 | - `dump_offset`: dump offset of the hexdump start 338 | - `size`: number of bytes to dump 339 | 340 | Optional arguments: 341 | 342 | - `-h`, `--help`: show this help message and exit 343 | 344 | ### hexblock 345 | 346 | Usage: `hexblock [-h] entryblock_id` 347 | 348 | Hexdump the block with the provided entryblock identifier. 349 | 350 | Positional arguments: 351 | 352 | - `entryblock_id`: entryblock identifier to hexdump 353 | 354 | Optional arguments: 355 | 356 | - `-h`, `--help`: show this help message and exit 357 | 358 | ### bye or \ 359 | 360 | Usage: `bye [-h]` 361 | 362 | Exit the program. Are you sure? 363 | 364 | Optional arguments: 365 | 366 | - `-h`, `--help`: show this help message and exit 367 | 368 | ### record 369 | 370 | Usage: `record [-h] output_file` 371 | 372 | Save the following commands to selected file. 373 | Saved file can be used afterwards for replay with the `playback` command. 374 | 375 | Positional arguments: 376 | 377 | - `output_file`: file onto which commands will be saved 378 | 379 | Optional arguments: 380 | 381 | - `-h`, `--help`: show this help message and exit 382 | 383 | ### playback 384 | 385 | Usage: `playback [-h] input_file` 386 | 387 | Execute the sequence of commands defined in the input file. 388 | 389 | Positional arguments: 390 | 391 | - `input_file`: file from which commands to execute will be read 392 | 393 | Optional arguments: 394 | 395 | - `-h`, `--help`: show this help message and exit 396 | -------------------------------------------------------------------------------- /part/refs/attribute.py: -------------------------------------------------------------------------------- 1 | from util.time import bytes2time 2 | from struct import Struct 3 | 4 | ATTR_SIZE_OFFSET = 0 5 | ATTR_SIZE_SIZE = 4 6 | ATTR_SIZE_FORMAT = Struct(''.format(prefix, ptr_i, ptr['_absolute_offset'])) 138 | print('{} - entry size: {val:#x} ({val})'.format(prefix, val=ptr['size'])) 139 | print('{} - number of blocks/clusters: {}'.format(prefix, ptr['num_blocks'])) 140 | print('{} - block identifier: {:#x}'.format(prefix, ptr['blockid'])) 141 | else: 142 | print('{}- pointers: None'.format(prefix)) 143 | print('{}- pointers data: None'.format(prefix)) 144 | 145 | ATTR_FN_DATARUN_HEADER_FORMAT = Struct(''.format(prefix, ptr_i, ptr['_absolute_offset'])) 189 | _dump_filename_attribute_datarun_entry(ptr, prefix + ' ') 190 | else: 191 | print('{}- pointers: None'.format(prefix)) 192 | print('{}- pointers data: None'.format(prefix)) 193 | 194 | ATTR_FILENAME_HEADER_FORMAT = Struct(' 0: 212 | dump.seek(offset + ATTR_TYPE_OFFSET + ATTR_TYPE_SIZE, 0) 213 | fn = dump.read(f_size) 214 | else: 215 | fn = 'DEADBEEF'.encode('utf-16le') 216 | attr['filename'] = fn 217 | attr['_offset_metadata'] = attr['next_struct_offset'] 218 | attr['metadata'] = read_filename_attribute_metadata(dump, offset + attr['_offset_metadata']) 219 | attr['_offset_datarun'] = attr['_offset_metadata'] + attr['metadata']['size'] 220 | if attr['metadata']['physical_size'] == 0: 221 | attr['datarun'] = None 222 | else: 223 | attr['datarun'] = read_filename_attribute_datarun(dump, offset + attr['_offset_datarun']) 224 | return attr 225 | 226 | def _dump_filename_attribute(attr, prefix=''): 227 | print('{}- size: {val:#x} ({val})'.format(prefix, val=attr['size'])) 228 | print('{}- header length: {val:#x} ({val})'.format(prefix, val=attr['header_length'])) 229 | print('{}- filename + attribute type length: {val:#x} ({val})'.format(prefix, val=attr['length'])) 230 | print('{}- next structure offset: {val:#x} ({val})'.format(prefix, val=attr['next_struct_offset'])) 231 | print('{}- record remaining data: {val:#x} ({val})'.format(prefix, val=attr['record_rem_data'])) 232 | print('{}- record type: {:#x} (filename attribute)'.format(prefix, attr['type'])) 233 | print('{}- filename: {}'.format(prefix, attr['filename'].decode('utf-16le'))) 234 | print('{}- metadata: <{:#x}>'.format(prefix, attr['metadata']['_absolute_offset'])) 235 | _dump_filename_attribute_metadata(attr['metadata'], prefix + ' ') 236 | if attr['datarun']: 237 | print('{}- datarun: <{:#x}>'.format(prefix, attr['datarun']['_absolute_offset'])) 238 | _dump_filename_attribute_datarun(attr['datarun'], prefix + ' ') 239 | else: 240 | print('{}- datarun: None'.format(prefix)) 241 | 242 | ATTR_FILENAME_FOLDER_HEADER_1_FORMAT = Struct(' 0: 267 | dump.seek(offset + attr['offset_identifier'] + ATTR_TYPE_SIZE, 0) 268 | fn = dump.read(f_size) 269 | else: 270 | fn = 'DEADBEEF'.encode('utf-16le') 271 | attr['foldername'] = fn 272 | dump.seek(offset + attr['header_length'], 0) 273 | data = dump.read(ATTR_FILENAME_FOLDER_BODY_FORMAT.size) 274 | fields = ATTR_FILENAME_FOLDER_BODY_FORMAT.unpack_from(data, 0) 275 | attr['nodeid'] = fields[0] 276 | attr['created'] = fields[2] 277 | attr['modified'] = fields[3] 278 | attr['metadata_modified'] = fields[4] 279 | attr['last_accessed'] = fields[5] 280 | return attr 281 | 282 | def _dump_filename_folder_attribute(attr, prefix=''): 283 | print('{}- size: {val:#x} ({val})'.format(prefix, val=attr['size'])) 284 | print('{}- offset of identifier: {val:#x} ({val})'.format(prefix, val=attr['offset_identifier'])) 285 | print('{}- header remaining bytes: {val:#x} ({val})'.format(prefix, val=attr['header_rem_data'])) 286 | print('{}- unknown field 0: {:#x}'.format(prefix, attr['unknown0'])) 287 | print('{}- header length: {val:#x} ({val})'.format(prefix, val=attr['header_length'])) 288 | print('{}- record remaining data: {val:#x} ({val})'.format(prefix, val=attr['record_rem_data'])) 289 | print('{}- unknown field 1: {:#x}'.format(prefix, attr['unknown1'])) 290 | print('{}- type: {:#x} (filename folder attribute)'.format(prefix, attr['type'])) 291 | print('{}- foldername: {}'.format(prefix, attr['foldername'].decode('utf-16le'))) 292 | print('{}- node identifier: {:#x}'.format(prefix, attr['nodeid'])) 293 | print('{}- created: {} ({:#x})'.format(prefix, bytes2time(attr['created']), attr['created'])) 294 | print('{}- modified: {} ({:#x})'.format(prefix, bytes2time(attr['modified']), attr['modified'])) 295 | print('{}- metadata modified: {} ({:#x})'.format(prefix, bytes2time(attr['metadata_modified']), 296 | attr['metadata_modified'])) 297 | print('{}- last accessed: {} ({:#x})'.format(prefix, bytes2time(attr['last_accessed']), 298 | attr['last_accessed'])) 299 | 300 | ATTR_CHILD_HEADER_FORMAT = Struct(''.format(prefix, attr['_absolute_offset'] + attr['_offset_body'])) 405 | print('{} - length: {val:#x} ({val})'.format(prefix, val=attr['body_length'])) 406 | print('{} - offset_first_timestamp: {val:#x} ({val})'.format(prefix, 407 | val=attr['offset_first_timestamp'])) 408 | print('{} - created: {} ({:#x})'.format(prefix, bytes2time(attr['created']), attr['created'])) 409 | print('{} - modified: {} ({:#x})'.format(prefix, bytes2time(attr['modified']), 410 | attr['modified'])) 411 | print('{} - metadata modified: {} ({:#x})'.format(prefix, 412 | bytes2time(attr['metadata_modified']), 413 | attr['metadata_modified'])) 414 | print('{} - last accessed: {} ({:#x})'.format(prefix, bytes2time(attr['last_accessed']), 415 | attr['last_accessed'])) 416 | print('{} - node id: {:#x}'.format(prefix, attr['nodeid'])) 417 | print('{}- pointers section: <{:#x}>'.format(prefix, 418 | attr['_absolute_offset'] + attr['_offset_psec'])) 419 | print('{} - length: {val:#x} ({val})'.format(prefix, val=attr['psec_length'])) 420 | print('{} - offset to first pointer: {:#x}'.format(prefix, attr['offset_first_pointer'])) 421 | print('{} - number of pointers: {}'.format(prefix, attr['num_pointers'])) 422 | print('{} - offset end pointers area(?): {val:#x} ({val})'.format(prefix, 423 | val=attr['offset_end_pointers_area?'])) 424 | print('{} - pointers:'.format(prefix), end='') 425 | if attr['pointers']: 426 | for ptr in attr['pointers']: 427 | print(' {:#x}'.format(ptr), end='') 428 | print('') 429 | print('{}- pointers data:'.format(prefix)) 430 | for ptr_i, ptr in enumerate(attr['pointers_data']): 431 | print('{} - pointer {}: <{:#x}>'.format(prefix, ptr_i, ptr['_absolute_offset'])) 432 | _dump_directory_metadata_subattribute(ptr, prefix + ' ') 433 | else: 434 | print(' None') 435 | print('{} - pointers data: None'.format(prefix)) 436 | 437 | DM_SUBATTR_SI30_HEADER_FORMAT = Struct(',.') 26 | 27 | class PyReFSShell(cmd.Cmd): 28 | intro = None 29 | # intro = '''Hello master! Welcome home. 30 | # I will try to help you to analyze your ReFS partition dumps. 31 | # Type 'help' or '?' to list commands.\n''' 32 | prompt = '> ' 33 | rec_file = None 34 | dump_filename = None 35 | dump_file = None 36 | part = None 37 | parts = None 38 | blocks = None 39 | 40 | def __init__(self, **args): 41 | cmd.Cmd.__init__(self, args) 42 | self._init_func_args() 43 | 44 | def _init_func_args(self): 45 | file_argparser = FuncArgumentParser( 46 | prog='file', 47 | description='Load the provided dump file for analysis, and ' + 48 | 'automatically select the ReFS partition for ' + 49 | 'you. You can also initialize the list of ' + 50 | 'entryblocks of the partition.') 51 | file_argparser.add_argument('dump', action='store', 52 | help='file to use as dump for the analysis') 53 | file_argparser.add_argument('-i', '--initiliaze-entryblocks', action='store_true', 54 | default=False, dest='initialize_entryblocks', 55 | help='find blocks in provided dump') 56 | file_argparser.add_argument('-f', '--files', action='store_true', 57 | default=False, 58 | help='find files in provided dump (only considered if -i defined)') 59 | file_argparser.add_argument('-F', '--folders', action='store_true', 60 | default=False, 61 | help='find folders in provided dump (only considered if -i defined)') 62 | vol_argparser = FuncArgumentParser( 63 | prog='vol', 64 | description='Dump the volume record information from the current ReFS partition.') 65 | part_argparser = FuncArgumentParser( 66 | prog='part', 67 | description='Show available partitions in the currently loaded dump if no parameter' + 68 | ' is given, switch to the provided partition if any provided.') 69 | part_argparser.add_argument('partidx', action='store', type=int, 70 | default=None, nargs='?', 71 | help='Partition index of the partition to use for analysis') 72 | feb_argparser = FuncArgumentParser( 73 | prog='find_entryblocks', 74 | description='Find and show all the entryblocks in current partition.' + 75 | ' If requested number of files and folders information will be also' + 76 | ' collected.') 77 | feb_argparser.add_argument('-f', '--files', action='store_true', 78 | default=False, dest='files', 79 | help='Collect information on the number of files in the entryblocks.') 80 | feb_argparser.add_argument('-F', '--folders', action='store_true', 81 | default=False, dest='folders', 82 | help='Collect information on the number of folders in the entryblocks.') 83 | fp_argparser = FuncArgumentParser( 84 | prog='find_pattern', 85 | description='Find a data pattern in all the blocks of the current partition.' + 86 | ' Special characters (including spaces, carriage return, etc.)' + 87 | ' are not allowed in the pattern, think of escaping them.') 88 | fp_argparser.add_argument('pattern', action='store', 89 | type=str, 90 | help='Pattern to find in the current partition blocks.') 91 | lfiles_argparser = FuncArgumentParser( 92 | prog='list_filenames', 93 | description='List the found filenames from the list of ' + 94 | 'entryblocks with filenames.') 95 | lfolders_argparser = FuncArgumentParser( 96 | prog='list_folders', 97 | description='List the found filename folders from the list ' + 98 | 'of entryblocks with folders.') 99 | eb_argparser = FuncArgumentParser( 100 | prog='entryblock', 101 | description='Dump the provided entryblock identifier as ' + 102 | 'EntryBlock.') 103 | eb_argparser.add_argument('entryblock_identifier', action='store', 104 | type=lambda x: int(x, 0), 105 | help='entryblock identifier of the EntryBlock to dump') 106 | tc_argparser = FuncArgumentParser( 107 | prog='tree_control', 108 | description='Parse the given entryblock identifier as a ' + 109 | 'TreeControl. If no entryblock identifier is ' + 110 | 'provided it uses 0x1e as entryblock identifier.') 111 | tc_argparser.add_argument('entryblock_identifier', action='store', 112 | type=lambda x: int(x, 0), nargs='?', default=0x1e, 113 | help='entryblock identifier of the TreeControl to dump') 114 | tce_argparser = FuncArgumentParser( 115 | prog='tree_control_extension', 116 | description='Parse the given entryblock identifier as a ' + 117 | 'TreeControlExtension.') 118 | tce_argparser.add_argument('entryblock_identifier', action='store', 119 | type=lambda x: int(x, 0), 120 | help='entryblock identifier of the TreeControlExtension to ' + 121 | 'dump') 122 | ot_argparser = FuncArgumentParser( 123 | prog='object_tree', 124 | description='Parse the given entryblock identifier as an ' + 125 | 'ObjectTree.') 126 | ot_argparser.add_argument('entryblock_identifier', action='store', 127 | type=lambda x: int(x, 0), 128 | help='entryblock identifier of the ObjectTree to ' + 129 | 'dump') 130 | alloc_argparser = FuncArgumentParser( 131 | prog='allocator', 132 | description='Parse the given entryblock identifier as an ' + 133 | 'Allocator.') 134 | alloc_argparser.add_argument('entryblock_identifier', action='store', 135 | type=lambda x: int(x, 0), 136 | help='entryblock identifier of the Allocator to dump') 137 | attr_argparser = FuncArgumentParser( 138 | prog='attribute', 139 | description='Parse the given dump offset (in bytes) as an ' + 140 | 'Attribute.') 141 | attr_argparser.add_argument('dump_offset', action='store', 142 | type=lambda x: int(x, 0), 143 | help='dump offset (in bytes) of the Attribute to dump') 144 | ds_argparser = FuncArgumentParser( 145 | prog='datastream', 146 | description='Extract data from dump from the given datarun. ' + 147 | 'Dataruns can be extracted by exploring the ' + 148 | 'EntryBlocks (command `entryblock`) or using ' + 149 | 'the `list_dataruns` command.' 150 | ) 151 | ds_argparser.add_argument('output_filename', action='store', 152 | help='name of the generated output file') 153 | ds_argparser.add_argument('output_size', action='store', 154 | type=lambda x: int(x, 0), 155 | help='total amount of data to extract') 156 | ds_argparser.add_argument('dataruns', action='store', 157 | type=_datarun_arg, nargs='+', 158 | metavar='entryblock_identifier,number_of_blocks', 159 | help='datarun to follow for the data extraction') 160 | ldr_argparser = FuncArgumentParser( 161 | prog='list_dataruns', 162 | description='Retrieve list of all the files dataruns.') 163 | ft_argparser = FuncArgumentParser( 164 | prog='filetree', 165 | description='Extract the file tree structure from the given ' + 166 | 'node (use node 0x600 by default).') 167 | ft_argparser.add_argument('node_id', action='store', 168 | type=lambda x: int(x, 0), nargs='?', default=0x600, 169 | help='node identifier of the node to extract the file tree ' + 170 | 'structure from') 171 | hd_argparser = FuncArgumentParser( 172 | prog='hexdump', 173 | description='Hexdump the number of bytes at the provided ' + 174 | 'offset.') 175 | hd_argparser.add_argument('dump_offset', action='store', 176 | type=lambda x: int(x, 0), 177 | help='dump offset of the hexdump start') 178 | hd_argparser.add_argument('size', action='store', 179 | type=lambda x: int(x, 0), 180 | help='number of bytes to dump') 181 | hb_argparser = FuncArgumentParser( 182 | prog='hexblock', 183 | description='Hexdump the block with the provided ' + 184 | 'entryblock identifier.') 185 | hb_argparser.add_argument('entryblock_id', action='store', 186 | type=lambda x: int(x, 0), 187 | help='entryblock identifier to hexdump') 188 | bye_argparser = FuncArgumentParser( 189 | prog='bye', 190 | description='Exit the program. Are you sure?') 191 | record_argparser = FuncArgumentParser( 192 | prog='record', 193 | description='Save the following commands to selected file. ' + 194 | 'Saved file can be used afterwards for replay ' + 195 | 'with the \'playback\' command.') 196 | record_argparser.add_argument('output_file', action='store', 197 | help='file onto which commands will be saved') 198 | playback_argparser = FuncArgumentParser( 199 | prog='playback', 200 | description='Execute the sequence of commands defined in ' + 201 | 'the input file.') 202 | playback_argparser.add_argument('input_file', action='store', 203 | help='file from which commands to execute will be read') 204 | self._args = {file_argparser.prog: file_argparser, 205 | vol_argparser.prog: vol_argparser, 206 | part_argparser.prog: part_argparser, 207 | feb_argparser.prog: feb_argparser, 208 | fp_argparser.prog: fp_argparser, 209 | lfiles_argparser.prog: lfiles_argparser, 210 | lfolders_argparser.prog: lfolders_argparser, 211 | eb_argparser.prog: eb_argparser, 212 | tc_argparser.prog: tc_argparser, 213 | tce_argparser.prog: tce_argparser, 214 | ot_argparser.prog: ot_argparser, 215 | alloc_argparser.prog: alloc_argparser, 216 | attr_argparser.prog: attr_argparser, 217 | ds_argparser.prog: ds_argparser, 218 | ldr_argparser.prog: ldr_argparser, 219 | ft_argparser.prog: ft_argparser, 220 | hd_argparser.prog: hd_argparser, 221 | hb_argparser.prog: hb_argparser, 222 | bye_argparser.prog: bye_argparser, 223 | record_argparser.prog: record_argparser, 224 | playback_argparser.prog: playback_argparser 225 | } 226 | 227 | def _check_func_args(self, func, arg): 228 | parser = self._args[func] 229 | out = {'return': True, 'args': None} 230 | try: 231 | args = parser.parse_args(arg.split()) 232 | except FuncArgumentParserHelp: 233 | return out 234 | except FuncArgumentParserError: 235 | print('Master your command is badly formatted.') 236 | return out 237 | out = {'return': False, 'args': args} 238 | return out 239 | 240 | def preloop(self): 241 | print('''Hello master! Welcome home. 242 | I will try to help you to analyze your ReFS partition dumps. 243 | Type 'help' or '?' to list commands, and 'bye' or to exit. 244 | Type 'help ' for a short description of the command. 245 | Type ' -h' for the usage instructions of the command.''') 246 | if not self.dump_filename: 247 | print('''Master I just realized you haven't set a dump to analyze. 248 | Please use the 'file' command to set it.''') 249 | 250 | def do_file(self, arg): 251 | '''Use the provided dump file for your ReFS analysis. 252 | I will try to automatically select the right partition for you. 253 | You can also initialize the list of entryblocks of the partition.''' 254 | cargs = self._check_func_args('file', arg) 255 | if cargs['return']: 256 | return 257 | args = cargs['args'] 258 | self.dump_filename = args.dump 259 | print('Master I will try to follow your wishes by loading `{}`.'.format(self.dump_filename)) 260 | try: 261 | self.dump_file = open(self.dump_filename, 'rb') 262 | except: 263 | print('I tried hard Master, but I couldn\'t open the requested file.') 264 | print('Are you sure it exists?') 265 | return 266 | mbr_data = mbr.readMBR(self.dump_file) 267 | gpt_part = None 268 | for part_index, part in mbr_data: 269 | if part['ptype'] == mbr.MBR_PARTTYPE_GPT: 270 | gpt_part = part 271 | break 272 | if not gpt_part: 273 | print('ARGH Master. I couldn\'t find a GPT volume in the dump' + 274 | ' you provided me. Please, try with another file.') 275 | self.dump_filename = None 276 | self.dump_file.close() 277 | self.dump_file = None 278 | return 279 | gpt_data = gpt.readGPT(self.dump_file, gpt_part['start']) 280 | parts = [ p for p in gpt_data['parts'] 281 | if p['type'] == gpt.GUID_PART_TYPE_W_BASIC_DATA_PART 282 | and vol.is_refs_part(self.dump_file, p['first_lba']) ] 283 | if not parts: 284 | print('ARGH Master. I couldn\'t find a ReFS' + 285 | ' partition in the dump file you provided.' + 286 | ' Please, try with another file.', 287 | gpt.GUID_TRANSLATION[gpt.GUID_PART_TYPE_W_BASIC_DATA_PART]) 288 | self.dump_filename = None 289 | self.dump_file.close() 290 | self.dump_file = None 291 | return 292 | print(('Master, {} ReFS partitions were found.\n' + 293 | 'I will be using partition {}, but you can list the available' + 294 | ' partitions and switch between them with the \'part\' command.').format( 295 | len(parts), parts[0]['index'])) 296 | for part in parts: 297 | gpt.print_gpt_part(part) 298 | self.parts = parts 299 | self.part = parts[0] 300 | self.prompt = '\n{} part {} - first lba: {} last lba: {}\n> '.format( 301 | self.dump_filename, 302 | self.part['index'], 303 | self.part['first_lba'], 304 | self.part['last_lba']) 305 | if args.initialize_entryblocks: 306 | f_args = '' 307 | if args.files: 308 | f_args = '-f' 309 | if args.folders: 310 | f_args = f_args + ' -F' if f_args != '' else '-F' 311 | self.do_find_entryblocks(f_args) 312 | return 313 | 314 | def do_vol(self, arg): 315 | 'Dump the volume record information from the current ReFS partition.' 316 | cargs = self._check_func_args('vol', arg) 317 | if cargs['return']: 318 | return 319 | args = cargs['args'] 320 | _vol = vol.fsstat(self.dump_file, 321 | self.part['first_lba'], 322 | self.part['last_lba']) 323 | vol.dump_fsstat(_vol) 324 | return 325 | 326 | def do_part(self, arg): 327 | '''Show available partitions information ('part') and select from them 328 | ('part ').''' 329 | cargs = self._check_func_args('part', arg) 330 | if cargs['return']: 331 | return 332 | args = cargs['args'] 333 | if args.partidx is None: 334 | print('Master you have {} partition{} available, here they are.'.format( 335 | len(self.parts), 's' if len(self.parts) > 1 else '')) 336 | for p in self.parts: 337 | gpt.print_gpt_part(self.part) 338 | return 339 | if args.partidx not in [ p['index'] for p in self.parts ]: 340 | print('Master I don\'t have the partition index {} you provided.'.format( 341 | args.partidx)) 342 | return 343 | if args.partidx == self.part['index']: 344 | print('Nothing done, as we are already using partition {}.'.format( 345 | args.partidx)) 346 | return 347 | self.part = self.parts[args.partidx] 348 | print('Switched to partition {}, enjoy Master.'.format(arg)) 349 | self.prompt = '\n{} part {} - first lba: {} last lba: {}\n> '.format( 350 | self.dump_filename, 351 | self.part['index'], 352 | self.part['first_lba'], 353 | self.part['last_lba']) 354 | gpt.print_gpt_part(self.part) 355 | 356 | def do_find_entryblocks(self, arg): 357 | '''Extract and show all the entryblocks used in the current partition. 358 | If requested also extract an estimation of the number of filenames and folders 359 | found per entryblock.''' 360 | cargs = self._check_func_args('find_entryblocks', arg) 361 | if cargs['return']: 362 | return 363 | args = cargs['args'] 364 | if not self.dump_file: 365 | print('Master you have not defined a dump to analyze.') 366 | print('Please provide a dump file.') 367 | return 368 | offset = self.part['first_lba'] 369 | end_offset = self.part['last_lba'] 370 | if self.blocks == None: 371 | print(('Looking for blocks between lba {} and lba {}.' + 372 | ' This may take a while Master. A coffee?').format( 373 | offset, 374 | end_offset)) 375 | self.blocks = carving.find_blocks(self.dump_file, offset, end_offset) 376 | print('Master I found {} blocks.'.format(len(self.blocks))) 377 | if args.files: 378 | blocks = carving.blocks_with_filename_attributes(self.dump_file, self.blocks) 379 | print('Master I found {} blocks with the filename attribute.'.format(len(blocks))) 380 | if args.folders: 381 | blocks = carving.blocks_with_folder_attributes(self.dump_file, self.blocks) 382 | print('Master I found {} blocks with the filename folder attribute.'.format(len(blocks))) 383 | carving.print_blocks(self.blocks) 384 | 385 | def do_find_pattern(self, arg): 386 | '''Find a data pattern in all the blocks of the current partition. 387 | Special characters (including spaces, carriage return, etc.) are not allowed in 388 | the pattern, think of escaping them.''' 389 | cargs = self._check_func_args('find_pattern', arg) 390 | if cargs['return']: 391 | return 392 | args = cargs['args'] 393 | _pattern = ast.literal_eval('"{}"'.format(args.pattern)) 394 | offset = self.part['first_lba'] 395 | end_offset = self.part['last_lba'] 396 | pattern = [ ord(x) for x in _pattern ] 397 | print('Master I will be analyzing your pattern \'{}\' ({}) but it may take a while.'.format( 398 | args.pattern, [ '{:#x}'.format(x) for x in pattern ])) 399 | print('Do you want a cup of tea?') 400 | blocks = carving.find_data_blocks_with_pattern(self.dump_file, pattern, offset, end_offset) 401 | print('Master I found {} blocks with your wiseful pattern.'.format(len(blocks))) 402 | # print table of found blocks 403 | columns = [ {'key': 'addr', 'header': 'Address', 'align': '<', 'format': '#x'}, 404 | {'key': 'lba_offset', 'header': 'LBA offset', 'align': '<', 'format': '#x'}, 405 | {'key': 'block_offset', 'header': 'Block offset', 'align': '<', 'format': '#x'} ] 406 | if blocks: 407 | print_table(columns, blocks) 408 | 409 | def do_list_filenames(self, arg): 410 | 'List the found filenames from the list of entryblocks with filenames.' 411 | cargs = self._check_func_args('list_filenames', arg) 412 | if cargs['return']: 413 | return 414 | args = cargs['args'] 415 | if not self.blocks: 416 | print('Master, first you need to look for the blocks and blocks with filenames.') 417 | print('Please Master, use \'find_entryblocks -f\' first.') 418 | return 419 | listed = 0 420 | for block in self.blocks: 421 | if block['fnas']: 422 | for fid in block['fnas']: 423 | attr = rattr.read_attribute(self.dump_file, fid) 424 | try: 425 | filename = attr['filename'].decode('utf-16le') 426 | except: 427 | filename = attr['filename'] 428 | listed = listed + 1 429 | print('{:#010x} {:#06x} {:#06x} {:#06x} {:#010x} {}'.format( 430 | block['offset'], block['entryblock'], 431 | block['nodeid'], block['childid'], attr['_absolute_offset'], filename)) 432 | print('Master I listed {} filenames.'.format(listed)) 433 | if not listed: 434 | print('Master are you sure you performed \'find_entryblocks -F\' before?') 435 | 436 | def do_list_folders(self, arg): 437 | 'List the found filename folders from the list of entryblocks with folders.' 438 | cargs = self._check_func_args('list_folders', arg) 439 | if cargs['return']: 440 | return 441 | args = cargs['args'] 442 | if not self.blocks: 443 | print('Master, first you need to look for the blocks and blocks with folders.') 444 | print('Please Master, use \'find_entryblocks -F\' first.') 445 | return 446 | listed = 0 447 | for block in self.blocks: 448 | if block['folderids']: 449 | for fid in block['folderids']: 450 | attr = rattr.read_attribute(self.dump_file, fid) 451 | try: 452 | foldername = attr['foldername'].decode('utf-16le') 453 | except: 454 | foldername = attr['foldername'] 455 | listed = listed + 1 456 | print('{:#010x} {:#06x} {:#06x} {:#06x} {:#010x} {}'.format( 457 | block['offset'], block['entryblock'], 458 | block['nodeid'], block['childid'], attr['_absolute_offset'], foldername)) 459 | print('Master I listed {} folders.'.format(listed)) 460 | if not listed: 461 | print('Master are you sure you performed \'find_entryblocks -F\' before?') 462 | 463 | def do_hexdump(self, arg): 464 | 'Hexdump the number of bytes at the provided offset.' 465 | cargs = self._check_func_args('hexdump', arg) 466 | if cargs['return']: 467 | return 468 | args = cargs['args'] 469 | offset = args.dump_offset 470 | size = args.size 471 | self.dump_file.seek(offset, 0) 472 | data = self.dump_file.read(size) 473 | hexdump(data, offset) 474 | 475 | def do_hexblock(self, arg): 476 | 'Hexdump the block with the provided entryblock identifier.' 477 | cargs = self._check_func_args('hexblock', arg) 478 | if cargs['return']: 479 | return 480 | args = cargs['args'] 481 | ebid = args.entryblock_id 482 | if not self.blocks: 483 | print('Master, first you need to look for the blocks.') 484 | print('Please Master use \'find_entryblocks\' first or use \'hexdump\'.') 485 | return 486 | blks = [ b for b in self.blocks if b['entryblock'] == ebid ] 487 | if not blks: 488 | print('Master, are you sure such an entryblock exist?') 489 | return 490 | blk = blks[0] 491 | self.dump_file.seek(blk['offset'], 0) 492 | data = self.dump_file.read(16 * 1024) 493 | hexdump(data, blk['offset']) 494 | 495 | def do_entryblock(self, arg): 496 | 'Dump the provided entryblock identifier as entryblock.' 497 | cargs = self._check_func_args('entryblock', arg) 498 | if cargs['return']: 499 | return 500 | args = cargs['args'] 501 | ebid = args.entryblock_identifier 502 | if not self.blocks: 503 | print('Master, first you need to look for the blocks.') 504 | print('Please Master use \'find_entryblocks\' first.') 505 | return 506 | blocks = [ b for b in self.blocks if b['entryblock'] == ebid ] 507 | if len(blocks) != 1: 508 | print('Master, I couldn\'t find the block you asked for.') 509 | return 510 | block = blocks[0] 511 | eb = reb.read_entryblock(self.dump_file, block['offset']) 512 | reb.dump_entryblock(eb) 513 | 514 | def do_tree_control(self, arg): 515 | '''Parse the given entryblock identifier as a TreeControl. 516 | If no entryblock identifier is provided it uses 0x1e as entryblock 517 | identifier.''' 518 | cargs = self._check_func_args('tree_control', arg) 519 | if cargs['return']: 520 | return 521 | args = cargs['args'] 522 | ebid = args.entryblock_identifier 523 | if not self.blocks: 524 | print('Master, first you need to look for the blocks.') 525 | print('Please Master use \'find_entryblocks\' first.') 526 | return 527 | blocks = [ b for b in self.blocks if b['entryblock'] == ebid ] 528 | if len(blocks) != 1: 529 | print('Master, I couldn\'t find the block you asked for.') 530 | return 531 | block = blocks[0] 532 | tc = rtc.read_tree_control(self.dump_file, block['offset']) 533 | rtc.dump_tree_control(tc) 534 | 535 | def do_tree_control_extension(self, arg): 536 | 'Parse the given entryblock identifier as a TreeControlExtension.' 537 | cargs = self._check_func_args('tree_control_extension', arg) 538 | if cargs['return']: 539 | return 540 | args = cargs['args'] 541 | ebid = args.entryblock_identifier 542 | if not self.blocks: 543 | print('Master, first you need to look for the blocks.') 544 | print('Please Master use \'find_entryblocks\' first.') 545 | return 546 | blocks = [ b for b in self.blocks if b['entryblock'] == ebid ] 547 | if len(blocks) != 1: 548 | print('Master, I couldn\'t find the block you asked for.') 549 | return 550 | block = blocks[0] 551 | tce = rtc.read_tree_control_ext(self.dump_file, block['offset']) 552 | rtc.dump_tree_control_ext(tce) 553 | 554 | def do_object_tree(self, arg): 555 | 'Parse the given entryblock identifier as an ObjectTree.' 556 | cargs = self._check_func_args('object_tree', arg) 557 | if cargs['return']: 558 | return 559 | args = cargs['args'] 560 | ebid = args.entryblock_identifier 561 | if not self.blocks: 562 | print('Master, first you need to look for the blocks.') 563 | print('Please Master use \'find_entryblocks\' first.') 564 | return 565 | blocks = [ b for b in self.blocks if b['entryblock'] == ebid ] 566 | if len(blocks) != 1: 567 | print('Master, I couldn\'t find the block you asked for.') 568 | return 569 | block = blocks[0] 570 | ot = rot.read_object_tree(self.dump_file, block['offset']) 571 | rot.dump_object_tree(ot) 572 | 573 | def do_allocator(self, arg): 574 | 'Parse the given entryblock identifier as an Allocator.' 575 | cargs = self._check_func_args('allocator', arg) 576 | if cargs['return']: 577 | return 578 | args = cargs['args'] 579 | ebid = args.entryblock_identifier 580 | if not self.blocks: 581 | print('Master, first you need to look for the blocks.') 582 | print('Please Master use \'find_entryblocks\' first.') 583 | return 584 | blocks = [ b for b in self.blocks if b['entryblock'] == ebid ] 585 | if len(blocks) != 1: 586 | print('Master, I couldn\'t find the block you asked for.') 587 | return 588 | block = blocks[0] 589 | al = ralloc.read_allocator(self.dump_file, block['offset']) 590 | ralloc.dump_allocator(al) 591 | 592 | def do_attribute(self, arg): 593 | 'Parse the given dump offset (in bytes) as an Attribute.' 594 | cargs = self._check_func_args('attribute', arg) 595 | if cargs['return']: 596 | return 597 | args = cargs['args'] 598 | offset = args.dump_offset 599 | attr = rattr.read_attribute(self.dump_file, offset) 600 | rattr.dump_attribute(attr) 601 | 602 | def do_datastream(self, arg): 603 | '''Extract data from dump from the given datarun. 604 | Dataruns can be extracted by exploring the EntryBlocks (command `entryblock`) 605 | or using the `list_dataruns` command.''' 606 | cargs = self._check_func_args('datastream', arg) 607 | if cargs['return']: 608 | return 609 | args = cargs['args'] 610 | ofn = args.output_filename 611 | size = args.output_size 612 | dataruns = args.dataruns 613 | of = open(ofn, 'wb') 614 | for offset, length in dataruns: 615 | offset = (offset * 1024 * 16) + (self.part['first_lba'] * 512) 616 | length = length * 1024 * 16 617 | if length > size: 618 | length = size 619 | size = size - length 620 | self.dump_file.seek(offset, 0) 621 | data = self.dump_file.read(length) 622 | of.write(data) 623 | of.close() 624 | 625 | def do_list_dataruns(self, arg): 626 | 'Retrieve list of all the files dataruns.' 627 | cargs = self._check_func_args('list_dataruns', arg) 628 | if cargs['return']: 629 | return 630 | files = 0 631 | drs = 0 632 | for block in self.blocks: 633 | if block['fnas']: 634 | for fid in block['fnas']: 635 | files = files + 1 636 | attr = rattr.read_attribute(self.dump_file, fid) 637 | dataruns = [] 638 | if attr['datarun'] and attr['datarun']['pointers_data']: 639 | for ptr in attr['datarun']['pointers_data']: 640 | if ptr['pointers_data']: 641 | datarun = [ (x['blockid'], x['num_blocks']) 642 | for x in ptr['pointers_data'] ] 643 | dataruns.append((ptr['logical_size'], datarun)) 644 | drs = drs + 1 645 | try: 646 | filename = attr['filename'].decode('utf-16le') 647 | except: 648 | filename = attr['filename'] 649 | print('{:#010x} {:#06x} {:#06x} {:6} {}'.format( 650 | block['offset'], block['entryblock'], 651 | block['nodeid'], block['counter'], filename)) 652 | for length, datarun in dataruns: 653 | print(' size: {:#x} datarun: '.format(length), end='') 654 | for run in datarun: 655 | blockid,num = run 656 | print(' {:#x},{}'.format(blockid,num), end='') 657 | print('') 658 | if files: 659 | print('Master I listed {} data runs from {} files.'.format(drs, files)) 660 | else: 661 | print('Master I could not find any file, did you already execute \'find_entryblocks -f\'?') 662 | 663 | def do_filetree(self, arg): 664 | 'Extract the file tree structure from the given node (use node 0x600 by default).' 665 | cargs = self._check_func_args('filetree', arg) 666 | if cargs['return']: 667 | return 668 | args = cargs['args'] 669 | nodeid = args.node_id 670 | block_list = None 671 | if self.blocks: 672 | block_list = self.blocks 673 | offset = self.part['first_lba'] 674 | end_offset = self.part['last_lba'] 675 | tree = filetree(self.dump_file, offset, end_offset, nodeid, block_list) 676 | dump_filetree(tree) 677 | 678 | def do_bye(self, arg): 679 | 'Exit the program. Are you sure?' 680 | cargs = self._check_func_args('bye', arg) 681 | if cargs['return']: 682 | return 683 | print('''It's been a great to serve you master. 684 | Bye!!!!''') 685 | self.close() 686 | return True 687 | 688 | def do_EOF(self, arg): 689 | 'Exit the program. Really are you sure?' 690 | print('\nI would have liked a \'bye\' Master, but I follow your orders.') 691 | return self.do_bye(arg) 692 | 693 | # ----- record and playback ----- 694 | def do_record(self, arg): 695 | ''''Save the following commands to selected file. 696 | Saved file can be used afterwards for replay with the \'playback\' command.''' 697 | cargs = self._check_func_args('record', arg) 698 | if cargs['return']: 699 | return 700 | args = cargs['args'] 701 | self.close() 702 | try: 703 | self.rec_file = open(args.output_file, 'w') 704 | except: 705 | print('Master I couldn\'t open file to save commands.') 706 | self.rec_file = None 707 | 708 | def do_playback(self, arg): 709 | 'Execute the sequence of commands defined in the input file.' 710 | cargs = self._check_func_args('playback', arg) 711 | if cargs['return']: 712 | return 713 | args = cargs['args'] 714 | self.close() 715 | try: 716 | with open(args.input_file) as f: 717 | self.cmdqueue.extend(f.read().splitlines()) 718 | except: 719 | print('Master I couldn\'t open file to read commands to execute.') 720 | 721 | def precmd(self, line): 722 | if self.rec_file and 'playback' not in line: 723 | print(line, file=self.rec_file) 724 | return line 725 | 726 | def close(self): 727 | if self.rec_file: 728 | self.rec_file.close() 729 | self.rec_file = None 730 | 731 | if __name__ == '__main__': 732 | parser = argparse.ArgumentParser(description='ReFS carving on provided dump.') 733 | parser.add_argument('-d', '--dump', action='store', required=False, type=str, 734 | help='ReFS dump to analyze') 735 | args = parser.parse_args() 736 | 737 | if args.dump: 738 | rshell = PyReFSShell() 739 | rshell.onecmd('file ' + args.dump) 740 | rshell.cmdloop() 741 | else: 742 | PyReFSShell().cmdloop() 743 | --------------------------------------------------------------------------------