| # Generator of fuzzed qcow2 images |
| # |
| # Copyright (C) 2014 Maria Kustova <maria.k@catit.be> |
| # |
| # This program is free software: you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation, either version 2 of the License, or |
| # (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| # GNU General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| # |
| |
| from __future__ import absolute_import |
| import random |
| import struct |
| from . import fuzz |
| from math import ceil |
| from os import urandom |
| from itertools import chain |
| |
| MAX_IMAGE_SIZE = 10 * (1 << 20) |
| # Standard sizes |
| UINT32_S = 4 |
| UINT64_S = 8 |
| |
| |
| class Field(object): |
| |
| """Atomic image element (field). |
| |
| The class represents an image field as quadruple of a data format |
| of value necessary for its packing to binary form, an offset from |
| the beginning of the image, a value and a name. |
| |
| The field can be iterated as a list [format, offset, value, name]. |
| """ |
| |
| __slots__ = ('fmt', 'offset', 'value', 'name') |
| |
| def __init__(self, fmt, offset, val, name): |
| self.fmt = fmt |
| self.offset = offset |
| self.value = val |
| self.name = name |
| |
| def __iter__(self): |
| return iter([self.fmt, self.offset, self.value, self.name]) |
| |
| def __repr__(self): |
| return "Field(fmt='%s', offset=%d, value=%s, name=%s)" % \ |
| (self.fmt, self.offset, str(self.value), self.name) |
| |
| |
| class FieldsList(object): |
| |
| """List of fields. |
| |
| The class allows access to a field in the list by its name. |
| """ |
| |
| def __init__(self, meta_data=None): |
| if meta_data is None: |
| self.data = [] |
| else: |
| self.data = [Field(*f) |
| for f in meta_data] |
| |
| def __getitem__(self, name): |
| return [x for x in self.data if x.name == name] |
| |
| def __iter__(self): |
| return iter(self.data) |
| |
| def __len__(self): |
| return len(self.data) |
| |
| |
| class Image(object): |
| |
| """ Qcow2 image object. |
| |
| This class allows to create qcow2 images with random valid structures and |
| values, fuzz them via external qcow2.fuzz module and write the result to |
| a file. |
| """ |
| |
| def __init__(self, backing_file_name=None): |
| """Create a random valid qcow2 image with the correct header and stored |
| backing file name. |
| """ |
| cluster_bits, self.image_size = self._size_params() |
| self.cluster_size = 1 << cluster_bits |
| self.header = FieldsList() |
| self.backing_file_name = FieldsList() |
| self.backing_file_format = FieldsList() |
| self.feature_name_table = FieldsList() |
| self.end_of_extension_area = FieldsList() |
| self.l2_tables = FieldsList() |
| self.l1_table = FieldsList() |
| self.refcount_table = FieldsList() |
| self.refcount_blocks = FieldsList() |
| self.ext_offset = 0 |
| self.create_header(cluster_bits, backing_file_name) |
| self.set_backing_file_name(backing_file_name) |
| self.data_clusters = self._alloc_data(self.image_size, |
| self.cluster_size) |
| # Percentage of fields will be fuzzed |
| self.bias = random.uniform(0.2, 0.5) |
| |
| def __iter__(self): |
| return chain(self.header, self.backing_file_format, |
| self.feature_name_table, self.end_of_extension_area, |
| self.backing_file_name, self.l1_table, self.l2_tables, |
| self.refcount_table, self.refcount_blocks) |
| |
| def create_header(self, cluster_bits, backing_file_name=None): |
| """Generate a random valid header.""" |
| meta_header = [ |
| ['>4s', 0, "QFI\xfb", 'magic'], |
| ['>I', 4, random.randint(2, 3), 'version'], |
| ['>Q', 8, 0, 'backing_file_offset'], |
| ['>I', 16, 0, 'backing_file_size'], |
| ['>I', 20, cluster_bits, 'cluster_bits'], |
| ['>Q', 24, self.image_size, 'size'], |
| ['>I', 32, 0, 'crypt_method'], |
| ['>I', 36, 0, 'l1_size'], |
| ['>Q', 40, 0, 'l1_table_offset'], |
| ['>Q', 48, 0, 'refcount_table_offset'], |
| ['>I', 56, 0, 'refcount_table_clusters'], |
| ['>I', 60, 0, 'nb_snapshots'], |
| ['>Q', 64, 0, 'snapshots_offset'], |
| ['>Q', 72, 0, 'incompatible_features'], |
| ['>Q', 80, 0, 'compatible_features'], |
| ['>Q', 88, 0, 'autoclear_features'], |
| # Only refcount_order = 4 is supported by current (07.2014) |
| # implementation of QEMU |
| ['>I', 96, 4, 'refcount_order'], |
| ['>I', 100, 0, 'header_length'] |
| ] |
| self.header = FieldsList(meta_header) |
| |
| if self.header['version'][0].value == 2: |
| self.header['header_length'][0].value = 72 |
| else: |
| self.header['incompatible_features'][0].value = \ |
| random.getrandbits(2) |
| self.header['compatible_features'][0].value = random.getrandbits(1) |
| self.header['header_length'][0].value = 104 |
| # Extensions start at the header last field offset and the field size |
| self.ext_offset = struct.calcsize( |
| self.header['header_length'][0].fmt) + \ |
| self.header['header_length'][0].offset |
| end_of_extension_area_len = 2 * UINT32_S |
| free_space = self.cluster_size - self.ext_offset - \ |
| end_of_extension_area_len |
| # If the backing file name specified and there is enough space for it |
| # in the first cluster, then it's placed in the very end of the first |
| # cluster. |
| if (backing_file_name is not None) and \ |
| (free_space >= len(backing_file_name)): |
| self.header['backing_file_size'][0].value = len(backing_file_name) |
| self.header['backing_file_offset'][0].value = \ |
| self.cluster_size - len(backing_file_name) |
| |
| def set_backing_file_name(self, backing_file_name=None): |
| """Add the name of the backing file at the offset specified |
| in the header. |
| """ |
| if (backing_file_name is not None) and \ |
| (not self.header['backing_file_offset'][0].value == 0): |
| data_len = len(backing_file_name) |
| data_fmt = '>' + str(data_len) + 's' |
| self.backing_file_name = FieldsList([ |
| [data_fmt, self.header['backing_file_offset'][0].value, |
| backing_file_name, 'bf_name'] |
| ]) |
| |
| def set_backing_file_format(self, backing_file_fmt=None): |
| """Generate the header extension for the backing file format.""" |
| if backing_file_fmt is not None: |
| # Calculation of the free space available in the first cluster |
| end_of_extension_area_len = 2 * UINT32_S |
| high_border = (self.header['backing_file_offset'][0].value or |
| (self.cluster_size - 1)) - \ |
| end_of_extension_area_len |
| free_space = high_border - self.ext_offset |
| ext_size = 2 * UINT32_S + ((len(backing_file_fmt) + 7) & ~7) |
| |
| if free_space >= ext_size: |
| ext_data_len = len(backing_file_fmt) |
| ext_data_fmt = '>' + str(ext_data_len) + 's' |
| ext_padding_len = 7 - (ext_data_len - 1) % 8 |
| self.backing_file_format = FieldsList([ |
| ['>I', self.ext_offset, 0xE2792ACA, 'ext_magic'], |
| ['>I', self.ext_offset + UINT32_S, ext_data_len, |
| 'ext_length'], |
| [ext_data_fmt, self.ext_offset + UINT32_S * 2, |
| backing_file_fmt, 'bf_format'] |
| ]) |
| self.ext_offset = \ |
| struct.calcsize( |
| self.backing_file_format['bf_format'][0].fmt) + \ |
| ext_padding_len + \ |
| self.backing_file_format['bf_format'][0].offset |
| |
| def create_feature_name_table(self): |
| """Generate a random header extension for names of features used in |
| the image. |
| """ |
| def gen_feat_ids(): |
| """Return random feature type and feature bit.""" |
| return (random.randint(0, 2), random.randint(0, 63)) |
| |
| end_of_extension_area_len = 2 * UINT32_S |
| high_border = (self.header['backing_file_offset'][0].value or |
| (self.cluster_size - 1)) - \ |
| end_of_extension_area_len |
| free_space = high_border - self.ext_offset |
| # Sum of sizes of 'magic' and 'length' header extension fields |
| ext_header_len = 2 * UINT32_S |
| fnt_entry_size = 6 * UINT64_S |
| num_fnt_entries = min(10, (free_space - ext_header_len) / |
| fnt_entry_size) |
| if not num_fnt_entries == 0: |
| feature_tables = [] |
| feature_ids = [] |
| inner_offset = self.ext_offset + ext_header_len |
| feat_name = 'some cool feature' |
| while len(feature_tables) < num_fnt_entries * 3: |
| feat_type, feat_bit = gen_feat_ids() |
| # Remove duplicates |
| while (feat_type, feat_bit) in feature_ids: |
| feat_type, feat_bit = gen_feat_ids() |
| feature_ids.append((feat_type, feat_bit)) |
| feat_fmt = '>' + str(len(feat_name)) + 's' |
| feature_tables += [['B', inner_offset, |
| feat_type, 'feature_type'], |
| ['B', inner_offset + 1, feat_bit, |
| 'feature_bit_number'], |
| [feat_fmt, inner_offset + 2, |
| feat_name, 'feature_name'] |
| ] |
| inner_offset += fnt_entry_size |
| # No padding for the extension is necessary, because |
| # the extension length is multiple of 8 |
| self.feature_name_table = FieldsList([ |
| ['>I', self.ext_offset, 0x6803f857, 'ext_magic'], |
| # One feature table contains 3 fields and takes 48 bytes |
| ['>I', self.ext_offset + UINT32_S, |
| len(feature_tables) / 3 * 48, 'ext_length'] |
| ] + feature_tables) |
| self.ext_offset = inner_offset |
| |
| def set_end_of_extension_area(self): |
| """Generate a mandatory header extension marking end of header |
| extensions. |
| """ |
| self.end_of_extension_area = FieldsList([ |
| ['>I', self.ext_offset, 0, 'ext_magic'], |
| ['>I', self.ext_offset + UINT32_S, 0, 'ext_length'] |
| ]) |
| |
| def create_l_structures(self): |
| """Generate random valid L1 and L2 tables.""" |
| def create_l2_entry(host, guest, l2_cluster): |
| """Generate one L2 entry.""" |
| offset = l2_cluster * self.cluster_size |
| l2_size = self.cluster_size / UINT64_S |
| entry_offset = offset + UINT64_S * (guest % l2_size) |
| cluster_descriptor = host * self.cluster_size |
| if not self.header['version'][0].value == 2: |
| cluster_descriptor += random.randint(0, 1) |
| # While snapshots are not supported, bit #63 = 1 |
| # Compressed clusters are not supported => bit #62 = 0 |
| entry_val = (1 << 63) + cluster_descriptor |
| return ['>Q', entry_offset, entry_val, 'l2_entry'] |
| |
| def create_l1_entry(l2_cluster, l1_offset, guest): |
| """Generate one L1 entry.""" |
| l2_size = self.cluster_size / UINT64_S |
| entry_offset = l1_offset + UINT64_S * (guest / l2_size) |
| # While snapshots are not supported bit #63 = 1 |
| entry_val = (1 << 63) + l2_cluster * self.cluster_size |
| return ['>Q', entry_offset, entry_val, 'l1_entry'] |
| |
| if len(self.data_clusters) == 0: |
| # All metadata for an empty guest image needs 4 clusters: |
| # header, rfc table, rfc block, L1 table. |
| # Header takes cluster #0, other clusters ##1-3 can be used |
| l1_offset = random.randint(1, 3) * self.cluster_size |
| l1 = [['>Q', l1_offset, 0, 'l1_entry']] |
| l2 = [] |
| else: |
| meta_data = self._get_metadata() |
| guest_clusters = random.sample(range(self.image_size / |
| self.cluster_size), |
| len(self.data_clusters)) |
| # Number of entries in a L1/L2 table |
| l_size = self.cluster_size / UINT64_S |
| # Number of clusters necessary for L1 table |
| l1_size = int(ceil((max(guest_clusters) + 1) / float(l_size**2))) |
| l1_start = self._get_adjacent_clusters(self.data_clusters | |
| meta_data, l1_size) |
| meta_data |= set(range(l1_start, l1_start + l1_size)) |
| l1_offset = l1_start * self.cluster_size |
| # Indices of L2 tables |
| l2_ids = [] |
| # Host clusters allocated for L2 tables |
| l2_clusters = [] |
| # L1 entries |
| l1 = [] |
| # L2 entries |
| l2 = [] |
| for host, guest in zip(self.data_clusters, guest_clusters): |
| l2_id = guest / l_size |
| if l2_id not in l2_ids: |
| l2_ids.append(l2_id) |
| l2_clusters.append(self._get_adjacent_clusters( |
| self.data_clusters | meta_data | set(l2_clusters), |
| 1)) |
| l1.append(create_l1_entry(l2_clusters[-1], l1_offset, |
| guest)) |
| l2.append(create_l2_entry(host, guest, |
| l2_clusters[l2_ids.index(l2_id)])) |
| self.l2_tables = FieldsList(l2) |
| self.l1_table = FieldsList(l1) |
| self.header['l1_size'][0].value = int(ceil(UINT64_S * self.image_size / |
| float(self.cluster_size**2))) |
| self.header['l1_table_offset'][0].value = l1_offset |
| |
| def create_refcount_structures(self): |
| """Generate random refcount blocks and refcount table.""" |
| def allocate_rfc_blocks(data, size): |
| """Return indices of clusters allocated for refcount blocks.""" |
| cluster_ids = set() |
| diff = block_ids = set([x / size for x in data]) |
| while len(diff) != 0: |
| # Allocate all yet not allocated clusters |
| new = self._get_available_clusters(data | cluster_ids, |
| len(diff)) |
| # Indices of new refcount blocks necessary to cover clusters |
| # in 'new' |
| diff = set([x / size for x in new]) - block_ids |
| cluster_ids |= new |
| block_ids |= diff |
| return cluster_ids, block_ids |
| |
| def allocate_rfc_table(data, init_blocks, block_size): |
| """Return indices of clusters allocated for the refcount table |
| and updated indices of clusters allocated for blocks and indices |
| of blocks. |
| """ |
| blocks = set(init_blocks) |
| clusters = set() |
| # Number of entries in one cluster of the refcount table |
| size = self.cluster_size / UINT64_S |
| # Number of clusters necessary for the refcount table based on |
| # the current number of refcount blocks |
| table_size = int(ceil((max(blocks) + 1) / float(size))) |
| # Index of the first cluster of the refcount table |
| table_start = self._get_adjacent_clusters(data, table_size + 1) |
| # Clusters allocated for the current length of the refcount table |
| table_clusters = set(range(table_start, table_start + table_size)) |
| # Clusters allocated for the refcount table including |
| # last optional one for potential l1 growth |
| table_clusters_allocated = set(range(table_start, table_start + |
| table_size + 1)) |
| # New refcount blocks necessary for clusters occupied by the |
| # refcount table |
| diff = set([c / block_size for c in table_clusters]) - blocks |
| blocks |= diff |
| while len(diff) != 0: |
| # Allocate clusters for new refcount blocks |
| new = self._get_available_clusters((data | clusters) | |
| table_clusters_allocated, |
| len(diff)) |
| # Indices of new refcount blocks necessary to cover |
| # clusters in 'new' |
| diff = set([x / block_size for x in new]) - blocks |
| clusters |= new |
| blocks |= diff |
| # Check if the refcount table needs one more cluster |
| if int(ceil((max(blocks) + 1) / float(size))) > table_size: |
| new_block_id = (table_start + table_size) / block_size |
| # Check if the additional table cluster needs |
| # one more refcount block |
| if new_block_id not in blocks: |
| diff.add(new_block_id) |
| table_clusters.add(table_start + table_size) |
| table_size += 1 |
| return table_clusters, blocks, clusters |
| |
| def create_table_entry(table_offset, block_cluster, block_size, |
| cluster): |
| """Generate a refcount table entry.""" |
| offset = table_offset + UINT64_S * (cluster / block_size) |
| return ['>Q', offset, block_cluster * self.cluster_size, |
| 'refcount_table_entry'] |
| |
| def create_block_entry(block_cluster, block_size, cluster): |
| """Generate a list of entries for the current block.""" |
| entry_size = self.cluster_size / block_size |
| offset = block_cluster * self.cluster_size |
| entry_offset = offset + entry_size * (cluster % block_size) |
| # While snapshots are not supported all refcounts are set to 1 |
| return ['>H', entry_offset, 1, 'refcount_block_entry'] |
| # Size of a block entry in bits |
| refcount_bits = 1 << self.header['refcount_order'][0].value |
| # Number of refcount entries per refcount block |
| # Convert self.cluster_size from bytes to bits to have the same |
| # base for the numerator and denominator |
| block_size = self.cluster_size * 8 / refcount_bits |
| meta_data = self._get_metadata() |
| if len(self.data_clusters) == 0: |
| # All metadata for an empty guest image needs 4 clusters: |
| # header, rfc table, rfc block, L1 table. |
| # Header takes cluster #0, other clusters ##1-3 can be used |
| block_clusters = set([random.choice(list(set(range(1, 4)) - |
| meta_data))]) |
| block_ids = set([0]) |
| table_clusters = set([random.choice(list(set(range(1, 4)) - |
| meta_data - |
| block_clusters))]) |
| else: |
| block_clusters, block_ids = \ |
| allocate_rfc_blocks(self.data_clusters | |
| meta_data, block_size) |
| table_clusters, block_ids, new_clusters = \ |
| allocate_rfc_table(self.data_clusters | |
| meta_data | |
| block_clusters, |
| block_ids, |
| block_size) |
| block_clusters |= new_clusters |
| |
| meta_data |= block_clusters | table_clusters |
| table_offset = min(table_clusters) * self.cluster_size |
| block_id = None |
| # Clusters allocated for refcount blocks |
| block_clusters = list(block_clusters) |
| # Indices of refcount blocks |
| block_ids = list(block_ids) |
| # Refcount table entries |
| rfc_table = [] |
| # Refcount entries |
| rfc_blocks = [] |
| |
| for cluster in sorted(self.data_clusters | meta_data): |
| if cluster / block_size != block_id: |
| block_id = cluster / block_size |
| block_cluster = block_clusters[block_ids.index(block_id)] |
| rfc_table.append(create_table_entry(table_offset, |
| block_cluster, |
| block_size, cluster)) |
| rfc_blocks.append(create_block_entry(block_cluster, block_size, |
| cluster)) |
| self.refcount_table = FieldsList(rfc_table) |
| self.refcount_blocks = FieldsList(rfc_blocks) |
| |
| self.header['refcount_table_offset'][0].value = table_offset |
| self.header['refcount_table_clusters'][0].value = len(table_clusters) |
| |
| def fuzz(self, fields_to_fuzz=None): |
| """Fuzz an image by corrupting values of a random subset of its fields. |
| |
| Without parameters the method fuzzes an entire image. |
| |
| If 'fields_to_fuzz' is specified then only fields in this list will be |
| fuzzed. 'fields_to_fuzz' can contain both individual fields and more |
| general image elements as a header or tables. |
| |
| In the first case the field will be fuzzed always. |
| In the second a random subset of fields will be selected and fuzzed. |
| """ |
| def coin(): |
| """Return boolean value proportional to a portion of fields to be |
| fuzzed. |
| """ |
| return random.random() < self.bias |
| |
| if fields_to_fuzz is None: |
| for field in self: |
| if coin(): |
| field.value = getattr(fuzz, field.name)(field.value) |
| else: |
| for item in fields_to_fuzz: |
| if len(item) == 1: |
| for field in getattr(self, item[0]): |
| if coin(): |
| field.value = getattr(fuzz, |
| field.name)(field.value) |
| else: |
| # If fields with the requested name were not generated |
| # getattr(self, item[0])[item[1]] returns an empty list |
| for field in getattr(self, item[0])[item[1]]: |
| field.value = getattr(fuzz, field.name)(field.value) |
| |
| def write(self, filename): |
| """Write an entire image to the file.""" |
| image_file = open(filename, 'w') |
| for field in self: |
| image_file.seek(field.offset) |
| image_file.write(struct.pack(field.fmt, field.value)) |
| |
| for cluster in sorted(self.data_clusters): |
| image_file.seek(cluster * self.cluster_size) |
| image_file.write(urandom(self.cluster_size)) |
| |
| # Align the real image size to the cluster size |
| image_file.seek(0, 2) |
| size = image_file.tell() |
| rounded = (size + self.cluster_size - 1) & ~(self.cluster_size - 1) |
| if rounded > size: |
| image_file.seek(rounded - 1) |
| image_file.write("\0") |
| image_file.close() |
| |
| @staticmethod |
| def _size_params(): |
| """Generate a random image size aligned to a random correct |
| cluster size. |
| """ |
| cluster_bits = random.randrange(9, 21) |
| cluster_size = 1 << cluster_bits |
| img_size = random.randrange(0, MAX_IMAGE_SIZE + 1, cluster_size) |
| return (cluster_bits, img_size) |
| |
| @staticmethod |
| def _get_available_clusters(used, number): |
| """Return a set of indices of not allocated clusters. |
| |
| 'used' contains indices of currently allocated clusters. |
| All clusters that cannot be allocated between 'used' clusters will have |
| indices appended to the end of 'used'. |
| """ |
| append_id = max(used) + 1 |
| free = set(range(1, append_id)) - used |
| if len(free) >= number: |
| return set(random.sample(free, number)) |
| else: |
| return free | set(range(append_id, append_id + number - len(free))) |
| |
| @staticmethod |
| def _get_adjacent_clusters(used, size): |
| """Return an index of the first cluster in the sequence of free ones. |
| |
| 'used' contains indices of currently allocated clusters. 'size' is the |
| length of the sequence of free clusters. |
| If the sequence of 'size' is not available between 'used' clusters, its |
| first index will be append to the end of 'used'. |
| """ |
| def get_cluster_id(lst, length): |
| """Return the first index of the sequence of the specified length |
| or None if the sequence cannot be inserted in the list. |
| """ |
| if len(lst) != 0: |
| pairs = [] |
| pair = (lst[0], 1) |
| for i in range(1, len(lst)): |
| if lst[i] == lst[i-1] + 1: |
| pair = (lst[i], pair[1] + 1) |
| else: |
| pairs.append(pair) |
| pair = (lst[i], 1) |
| pairs.append(pair) |
| random.shuffle(pairs) |
| for x, s in pairs: |
| if s >= length: |
| return x - length + 1 |
| return None |
| |
| append_id = max(used) + 1 |
| free = list(set(range(1, append_id)) - used) |
| idx = get_cluster_id(free, size) |
| if idx is None: |
| return append_id |
| else: |
| return idx |
| |
| @staticmethod |
| def _alloc_data(img_size, cluster_size): |
| """Return a set of random indices of clusters allocated for guest data. |
| """ |
| num_of_cls = img_size/cluster_size |
| return set(random.sample(range(1, num_of_cls + 1), |
| random.randint(0, num_of_cls))) |
| |
| def _get_metadata(self): |
| """Return indices of clusters allocated for image metadata.""" |
| ids = set() |
| for x in self: |
| ids.add(x.offset/self.cluster_size) |
| return ids |
| |
| |
| def create_image(test_img_path, backing_file_name=None, backing_file_fmt=None, |
| fields_to_fuzz=None): |
| """Create a fuzzed image and write it to the specified file.""" |
| image = Image(backing_file_name) |
| image.set_backing_file_format(backing_file_fmt) |
| image.create_feature_name_table() |
| image.set_end_of_extension_area() |
| image.create_l_structures() |
| image.create_refcount_structures() |
| image.fuzz(fields_to_fuzz) |
| image.write(test_img_path) |
| return image.image_size |