├── .gitignore
├── LICENSE
├── README.md
├── crypto_handler.py
├── mcafee_fde.py
├── mcafuse.py
├── requirements.txt
└── utils.py
/.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 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | McAFuse - A FUSE utility to handle McAfee FDE in a digital investigation.
4 |
5 | This program expose a static file system containing 2 files:
6 | - SafeBoot.disk: a fat partition inside the encrypted disk
7 | - encdisk.img: an image of the encrypted disk to analyze
8 |
9 | Copyright (c) 2021 Andrea Canepa
10 | Copyright (c) 2021 Francesco Picasso
11 | Copyright (c) 2021 Giovanni Lagorio
12 |
13 | Permission is hereby granted, free of charge, to any person obtaining a copy
14 | of this software and associated documentation files (the "Software"), to deal
15 | in the Software without restriction, including without limitation the rights
16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17 | copies of the Software, and to permit persons to whom the Software is
18 | furnished to do so, subject to the following conditions:
19 |
20 | The above copyright notice and this permission notice shall be included in all
21 | copies or substantial portions of the Software.
22 |
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29 | SOFTWARE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # McAFuse
2 |
3 | McAFuse was developed as a Master's thesis project in Computer Science of *Andrea Canepa* (@A-725-K) in collaboration with *RealityNet (Reality Net System Solutions)*.
4 |
5 | This project aim is to bring to the **DFIR** community an open source utility to handle encrypted disk images built with `McAfee FDE` toolset during digital investigations. It exposes a static **FUSE** *read-only* filesystem containing 2 files:
6 | - ***SafeBoot.disk***: a plain FAT partition present in the encrypted disk
7 | - ***encdisk.img***: the encrypted disk
8 |
9 | ## Getting started
10 | To run this project you have to clone or download this repository, with the command:
11 | ```
12 | git clone https://github.com/RealityNet/McAFuse
13 | ```
14 | Then you have to install the `Python3` libraries using the requirements file:
15 | ```
16 | python3 -m pip install -r requirements.txt
17 | ```
18 | Finally you can launch `mcafuse.py` with the requested parameters.
19 |
20 | ## How to launch the program
21 | You have to run this command from your terminal, as a *superuser*:
22 | ```
23 | ./mcafuse.py [-h/--help] [--debug] [-i/--info] [-a/--all] [-v/--verbose] MOUNT_POINT DISK_IMAGE [-k/--keyfile KEY_FILE]
24 | ```
25 | where the following are mandatory:
26 | - ***MOUNT_POINT***: the new root of the static filesystem you are going to serve
27 | - ***DISK_IMAGE***: the encrypted image of the disk you want to analyze
28 |
29 | and these are optional:
30 | - ***-k/--keyfile KEY_FILE***: the XML file provided by McAfee FDE installation containing a \<**key**\> tag with *base64* encoded password to decrypt the disk
31 | - ***-h/--help***: print the help and quit immediately
32 | - ***-i/--info***: print the disk information gathered from SafeBootDiskInfo
33 | - ***-a/--all***: expose all disk and not only an encrypted volume
34 | - ***-v/--verbose***: to print more information on the execution
35 | - ***--debug***: print debug information
36 |
37 | In case a keyfile is not specified, only *SafeBoot.disk* will be provided to the final user.
38 |
39 | ## Authors
40 |
41 | * ***Andrea Canepa*** - *Computer Science, UNIGE* - DFIR 2021
42 | * ***Francesco Picasso*** - *RealityNet* - DFIR 2021
43 | * ***Giovanni Lagorio*** - *Computer Science Teacher, UNIGE* - DFIR 2021
--------------------------------------------------------------------------------
/crypto_handler.py:
--------------------------------------------------------------------------------
1 | from base64 import b64decode
2 | from Crypto.Cipher import AES
3 | from utils import SECTOR_SIZE, pack, log
4 | from defusedxml.ElementTree import parse, ParseError
5 |
6 |
7 | AES_KEY_SIZE = 32
8 |
9 |
10 | # CryptoHandler: this class handle all the cryptographic operations, in particular
11 | # it decrypts a sector during a read operation
12 | #
13 | # Params: @xml_file: path to the McAfee generated file containing the key
14 | # @verbose : boolean flag to determine wether print information during execution
15 | class CryptoHandler:
16 | def __init__(self, xml_file, verbose):
17 | self._AESkey = self._get_key_from_XML(xml_file)
18 | self._sector_iv_cipher = AES.new(self._AESkey, AES.MODE_ECB)
19 |
20 | if verbose:
21 | log.info(f'|++| Sector size: {SECTOR_SIZE}')
22 | log.info(f'|++| AES-256-CBC key instantiated: {self._AESkey}')
23 |
24 |
25 | def _find_sector(self, off):
26 | return off // SECTOR_SIZE
27 |
28 |
29 | def _decrypt_sector(self, source, sector_no):
30 | pre_iv = pack(' SECTOR_SIZE:
83 | data += self._decrypt_sector(source, sector_no)
84 | size -= SECTOR_SIZE
85 | sector_no += 1
86 |
87 | # last (potentially) partial sector
88 | clear_sector = self._decrypt_sector(source, sector_no)
89 | data += clear_sector[:size]
90 |
91 | return data[:size_orig]
--------------------------------------------------------------------------------
/mcafee_fde.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import stat
4 | import errno
5 | import pyfuse3
6 |
7 | from utils import read_addr_in_sectors, SECTOR_SIZE, \
8 | bytes2int, read_sectors, build_GUID, \
9 | build_keycheck, log, get_partitions_from_mbr, \
10 | check_signature
11 |
12 |
13 | # McAfeeFde: this class implement the fs operations
14 | #
15 | # Params: @disk_image: encrypted disk image to mirror
16 | # @crypto_hnl: crypto handler object to handle decryption
17 | # @info : boolean flag to determine wether print disk info
18 | # @all_disk : boolean flag to determine wether expose all disk or only a partition
19 | # @verbose : boolean flag to determine wether print information during execution
20 | class McafeeFde(pyfuse3.Operations):
21 | def __init__(self, disk_image, crypto_hnl, info, all_disk, verbose):
22 | super(McafeeFde, self).__init__()
23 |
24 | self._safebootdiskinf_sector = 0 # to print useful info
25 | self._sector_map = dict() # [starting_sector] -> how_many
26 | self._sbfsdisk_data = b'' # safeboot fat partition
27 | self._partition_start = 0 # address in secotrs of the partition considered
28 | self._partition_len = 0 # length of partition in sectors
29 | self._all_disk = all_disk # consider all disk or only a partition
30 | self._verbose = verbose # determine how much information is printed in output
31 | self._crypto_handler = crypto_hnl # if None, expose only SafeBoot.disk
32 |
33 | # backend disk image
34 | self._backend_file_name = disk_image
35 | self._backend_file = open(self._backend_file_name, 'rb')
36 |
37 | # encrypted disk
38 | self.encdisk_name = b'encdisk.img'
39 | self.encdisk_inode = pyfuse3.ROOT_INODE + 1
40 |
41 | # safebootdisk
42 | self.sbfsdisk_name = b'SafeBoot.disk'
43 | self.sbfsdisk_inode = pyfuse3.ROOT_INODE + 2
44 |
45 | # initialize values
46 | self._init_sector_map()
47 | if not self._all_disk:
48 | self._init_encrypted_partition()
49 | if info or self._verbose:
50 | self._print_disk_info()
51 |
52 |
53 | # adjust the offset for read operations selecting the encrypted partition
54 | def _init_encrypted_partition(self):
55 | self._backend_file.seek(0)
56 | first_sector = self._backend_file.read(SECTOR_SIZE)
57 | mbr = first_sector[0x1be:0x1fe]
58 | partitions = get_partitions_from_mbr(mbr)
59 |
60 | if self._verbose:
61 | log.info('\n')
62 | for part in partitions:
63 | part.pretty_print()
64 |
65 | # if there is only 1 partition, it is encrypted
66 | # if there are 2 partitions, usually the 2nd is the encrypted one
67 | how_many_partitions = len(partitions)
68 | if how_many_partitions == 1:
69 | self._partition_start = partitions[0].starting_sector
70 | self._partition_len = partitions[0].total_sectors
71 | if self._verbose:
72 | log.info(f'|++| Chosen the 1st partition.\n'
73 | f'Starting address in sectors: {self._partition_start}\n'
74 | f'\tLength in sectors: {self._partition_len}')
75 | elif how_many_partitions == 2:
76 | self._partition_start = partitions[1].starting_sector
77 | self._partition_len = partitions[1].total_sectors
78 | if self._verbose:
79 | log.info(f'|++| Chosen the 2nd partition.\n'
80 | f'\tStarting address in sectors: {self._partition_start}\n'
81 | f'\tLength in sectors: {self._partition_len}\n')
82 | else:
83 | # TODO: handle more than 2 partitions
84 | raise NotImplemented('This program currenlty does not support disks with more than 2 partitions')
85 |
86 |
87 | # print disk information found in the SafeBoot disk
88 | def _print_disk_info(self):
89 | self._backend_file.seek(self._safebootdiskinf_sector * SECTOR_SIZE)
90 | disk_info = self._backend_file.read(0x5a)
91 | log.info('\n')
92 | print('//\t|+| SafeBoot Disk Info |+|\n|')
93 | print(f'|----- Signature: {disk_info[:0x10].decode()}')
94 | print(f'|------- Disk ID: {disk_info[0x11]}')
95 | print(f'|----- Disk GUID: {build_GUID(disk_info)}')
96 | print(f'|----- Algorithm: {hex(disk_info[0x37])} (AES-256-CBC)')
97 | print(f'|---- Sector Map: {bytes2int(disk_info[0x43:0x47])}')
98 | print(f'|-- Sector Count: {disk_info[0x4b]}')
99 | print(f'|----- Key Check: {build_keycheck(disk_info)}')
100 | print(f'|\n\\\\\t|+| ****************** |+|')
101 |
102 |
103 | # read from the sector map and rebuild Safe Boot partition
104 | def _read_sector_map(self):
105 | for base, how_many in self._sector_map.items():
106 | self._sbfsdisk_data += read_sectors(self._backend_file, base, how_many)
107 |
108 |
109 | # read from disk the sector map [starting_sector]=how_many_sectors
110 | def _init_sector_map(self):
111 | self._backend_file.seek(0x1c) # 0x1c ==> address in sectors of SafeBootDiskInf
112 | self._safebootdiskinf_sector = read_addr_in_sectors(self._backend_file)
113 |
114 | check_signature(self._backend_file_name, b'SafeBootDiskInf', self._safebootdiskinf_sector*SECTOR_SIZE)
115 |
116 | self._backend_file.seek(self._safebootdiskinf_sector*SECTOR_SIZE + 0x43) # 0x43 ==> start of sector map
117 | sectormap_start = read_addr_in_sectors(self._backend_file)
118 | self._backend_file.seek(sectormap_start*SECTOR_SIZE + 0x4)
119 |
120 | entry_no = 0
121 | line = self._backend_file.read(0x10)
122 | first = True
123 | while bytes2int(line[:0x4]) != 0:
124 | starting_sector = bytes2int(line[:0x4])
125 | how_many = bytes2int(line[0x8:0xc])
126 |
127 | # ignore first sector containing only a SafeBoot signature
128 | if first:
129 | starting_sector += 1
130 | how_many -= 1
131 | first = False
132 |
133 | self._sector_map[starting_sector] = how_many
134 | line = self._backend_file.read(0x10)
135 |
136 | self._read_sector_map()
137 |
138 |
139 | async def getattr(self, inode, ctx=None):
140 | entry = pyfuse3.EntryAttributes()
141 |
142 | if inode == pyfuse3.ROOT_INODE:
143 | entry.st_mode = (stat.S_IFDIR | 0o555) # read-only
144 | entry.st_size = 0
145 | elif inode == self.encdisk_inode:
146 | entry.st_mode = (stat.S_IFREG | 0o444) # read-only
147 | if self._all_disk: # all disk
148 | entry.st_size = os.stat(self._backend_file_name).st_size
149 | else: # only a volume
150 | entry.st_size = (self._partition_start + self._partition_len) * SECTOR_SIZE
151 | elif inode == self.sbfsdisk_inode:
152 | entry.st_mode = (stat.S_IFREG | 0o444) # read-only
153 | entry.st_size = len(self._sbfsdisk_data)
154 | else:
155 | raise pyfuse3.FUSEError(errno.ENOENT)
156 |
157 | # set a fake timestamp in files information
158 | timestamp = 824463 * 1e12
159 | entry.st_atime_ns = timestamp
160 | entry.st_ctime_ns = timestamp
161 | entry.st_mtime_ns = timestamp
162 | entry.st_gid = os.getgid()
163 | entry.st_uid = os.getuid()
164 | entry.st_ino = inode
165 |
166 | return entry
167 |
168 |
169 | async def lookup(self, parent_inode, name, ctx=None):
170 | if parent_inode != pyfuse3.ROOT_INODE or \
171 | name != self.encdisk_name or \
172 | name != self.sbfsdisk_name:
173 | raise pyfuse3.FUSEError(errno.ENOENT)
174 |
175 | if name == self.encdisk_name:
176 | return self.getattr(self.encdisk_inode)
177 |
178 | return self.getattr(self.sbfsdisk_inode)
179 |
180 |
181 | async def opendir(self, inode, ctx):
182 | if inode != pyfuse3.ROOT_INODE:
183 | raise pyfuse3.FUSEError(errno.ENOENT)
184 |
185 | return inode
186 |
187 |
188 | async def readdir(self, fh, start_id, token):
189 | assert fh == pyfuse3.ROOT_INODE
190 |
191 | if start_id == 0:
192 | # safeboot
193 | pyfuse3.readdir_reply(
194 | token,
195 | self.sbfsdisk_name,
196 | await self.getattr(self.sbfsdisk_inode),
197 | 1
198 | )
199 | # encrypted disk
200 | if self._crypto_handler is not None:
201 | pyfuse3.readdir_reply(
202 | token,
203 | self.encdisk_name,
204 | await self.getattr(self.encdisk_inode),
205 | 1
206 | )
207 |
208 |
209 | async def open(self, inode, flags, ctx):
210 | if inode != self.encdisk_inode and inode != self.sbfsdisk_inode:
211 | raise pyfuse3.FUSEError(errno.ENOENT)
212 |
213 | # read-only
214 | if flags & os.O_RDWR or flags & os.O_WRONLY:
215 | raise pyfuse3.FUSEError(errno.EACCES)
216 |
217 | return pyfuse3.FileInfo(fh=inode)
218 |
219 |
220 | async def read(self, fh, off, size):
221 | if self._verbose:
222 | log.info(f'read: fh={fh}\toffset={off}\tn_bytes={size}')
223 |
224 | if fh != self.encdisk_inode and fh != self.sbfsdisk_inode:
225 | raise pyfuse3.FUSEError(errno.ENOENT)
226 |
227 | # read SafeBoot.disk
228 | if fh == self.sbfsdisk_inode:
229 | return self._sbfsdisk_data[off:off+size]
230 |
231 | # read encdisk.img
232 | return await self._crypto_handler.decrypt_at_offset(
233 | self._backend_file,
234 | self._partition_start*SECTOR_SIZE + off,
235 | size
236 | )
--------------------------------------------------------------------------------
/mcafuse.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import trio
5 | import pyfuse3
6 |
7 | from mcafee_fde import McafeeFde
8 | from crypto_handler import CryptoHandler
9 | from utils import parse_args, init_logging, log, check_signature, check_if_files_exist
10 |
11 |
12 | def main():
13 | options = parse_args()
14 | init_logging(options.debug)
15 |
16 | if options.verbose:
17 | log.info('|++| Starting McAfuse...')
18 |
19 | crypto_hnl = None
20 | mcafee_fde = None
21 | try:
22 | check_if_files_exist(options.disk_image, options.keyfile, options.mountpoint)
23 | check_signature(options.disk_image, b'#SafeBoot', 0x2)
24 | if options.keyfile:
25 | crypto_hnl = CryptoHandler(options.keyfile, options.verbose)
26 | mcafee_fde = McafeeFde(
27 | options.disk_image,
28 | crypto_hnl,
29 | options.info,
30 | options.all,
31 | options.verbose
32 | )
33 | except (ValueError, FileNotFoundError, NotADirectoryError, NotImplementedError) as ex:
34 | log.error(ex)
35 | return
36 |
37 | fuse_options = set(pyfuse3.default_options)
38 | fuse_options.add('fsname=McAFuse')
39 | fuse_options.add('allow_other') # to allow user to navigate fuse fs
40 |
41 | try:
42 | pyfuse3.init(mcafee_fde, options.mountpoint, fuse_options)
43 | trio.run(pyfuse3.main)
44 | except RuntimeError as re:
45 | log.error(re)
46 | log.error('|!| -- [ERROR] -- The program need administrative privileges, try with sudo')
47 | return
48 | except:
49 | log.warning('|--| Terminated with CTRL-C (SIGINT)...')
50 | log.warning('|--| Unmounting...')
51 | pyfuse3.close(unmount=True)
52 | return
53 |
54 | if options.verbose:
55 | log.info('|++| Gracefully terminating McAfuse...')
56 |
57 | pyfuse3.close()
58 |
59 |
60 | if __name__ == '__main__':
61 | main()
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pyfuse3==3.1.1
2 | trio==0.17.0
3 | defusedxml==0.6.0
4 | pycryptodome==3.9.9
--------------------------------------------------------------------------------
/utils.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 | import logging
3 |
4 | from struct import pack, unpack
5 | from argparse import ArgumentParser
6 |
7 | try:
8 | import faulthandler
9 | except ImportError:
10 | pass
11 | else:
12 | faulthandler.enable()
13 |
14 |
15 | SECTOR_SIZE = 512 # bytes
16 |
17 | log = logging.getLogger(__name__)
18 |
19 |
20 | # simple struct the gather only useful info from MBR structure
21 | class MBREntry:
22 | def __init__(self, entry, index):
23 | self.index = index
24 | self.status = entry[0x0]
25 | self.type = entry[0x4]
26 | self.starting_sector = bytes2int(entry[0x8:0xc])
27 | self.total_sectors = bytes2int(entry[0xc:])
28 |
29 |
30 | def pretty_print(self):
31 | print(f'[*** MBR entry {self.index} ***]')
32 | print(f'|-- Status: {"{:02x}".format(self.status)}')
33 | print(f'|-- Type: {"{:02x}".format(self.type)}')
34 | print(f'|-- Start: {self.starting_sector}')
35 | print(f'|-- Count: {self.total_sectors}\n')
36 |
37 |
38 | # retrieve information of partitions from MBR
39 | def get_partitions_from_mbr(mbr):
40 | partitions = []
41 | for i in range(4):
42 | entry = mbr[i*0x10 : i*0x10+0x10]
43 | if entry == b'\x00'*0x10: # skip empty entries
44 | continue
45 | partitions.append(MBREntry(entry, i))
46 | return partitions
47 |
48 |
49 | # rebuild disk GUID starting from sector SafeBootDiskInfo
50 | def build_GUID(disk_info):
51 | guid = []
52 |
53 | n = ''
54 | for i in range(0x2a, 0x26, -1):
55 | n += '{:02x}'.format(disk_info[i])
56 | guid.append(n)
57 |
58 | guid.append('{:02x}'.format(disk_info[0x2c]) + '{:02x}'.format(disk_info[0x2b]))
59 | guid.append('{:02x}'.format(disk_info[0x2e]) + '{:02x}'.format(disk_info[0x2d]))
60 | for i in range(0x2f, 0x37):
61 | guid.append('{:02x}'.format(disk_info[i]))
62 |
63 | # GUID format: XXXX-XX-XX-X-X-X-X-X-X-X-X, X == bytes in range [0x00, 0xff]
64 | return '-'.join(map(lambda x: str(x), guid)).upper()
65 |
66 |
67 | # rebuild key check value (8 bytes value) starting from sector SafeBootDiskInfo
68 | def build_keycheck(disk_info):
69 | return ''.join(['{:02x}'.format(b) for b in disk_info[0x4d:0x55][::-1]]).upper()
70 |
71 |
72 | # read multiple contiguous sectors
73 | def read_sectors(source, base, how_many):
74 | source.seek(base * SECTOR_SIZE)
75 | return source.read(how_many * SECTOR_SIZE)
76 |
77 |
78 | # convert 4 bytes (32 bits) to an integer value
79 | def bytes2int(b):
80 | return int(unpack('