Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of DRCOV-compatible code coverage collection. #311

Merged
merged 6 commits into from Jun 2, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -50,4 +50,5 @@ alfink
bambu
kabeor
0ssigeno
liba2k
liba2k
assaf_carlsbad
@@ -166,6 +166,11 @@ $ ./qltool run -f examples/rootfs/x8664_linux/bin/x8664_hello --gdb 127.0.0.1:99

See https://docs.qiling.io/ for more details

With code coverage collection (UEFI only for now):

```
$ ./qltool run -f examples/rootfs/x8664_efi/bin/TcgPlatformSetupPolicy --rootfs examples/rootfs/x8664_efi --coverage-format drcov --coverage-file TcgPlatformSetupPolicy.cov
```
---

#### Remote Debugger
@@ -0,0 +1,27 @@
# Qiling Code Coverage Framework

## Overview

The code coverage framework is capable of collecting code coverage information from targets running under Qiling. Afterwards, the results can be serialized into a format suitable for further processing or manual viewing.
By leveraging the code coverage framework, one can know exactly which parts of the emulated code were executed and which weren't. Needless to say, this is an invaluable ability and can greatly aid any security-oriented research in couple of domains such as general RE, vulnerability research, exploit development, etc.

## Command-line interface

The command-line interface for controlling code coverage is comprised out of two new switches in `qltool`:

- `-c, --coverage-file`: Specifies the name of the output coverage file. This file can later be imported by coverage visualization tools such as [Lighthouse](https://github.com/gaasedelen/lighthouse) in order to visualize the trace:
- `--coverage-format`: Specifies the format of the coverage file. Currently only the `drcov` format is supported. If you wish to add support for additional formats, please read the relevant section.

## Extending the framework to support additional coverage formats

Currently the framework is only capable of omitting code coverage files which comply to the 'drcov' format used by the DynamoRIO [tool of the same name](https://dynamorio.org/dynamorio_docs/page_drcov.html).
If you wish to extend the framework by adding support for new coverage formats, please follow these steps:

- Create a new source module under the `coverage\formats` directory.
- Make the new format "discoverable" by adding its name to the `__all__` list in `coverage\__init__.py`
- Create a new class which inherits from `QlBaseCoverage`.
- Implement all base class methods which are marked with the `@abstractmethod` decorator:
- `FORMAT_NAME`: a user-friendly name for the coverage format name. This name will be presented in the help message of `qltool` as one of the possible choices for a coverage format.
- `def activate(self)`: Starts code coverage collection, for example by registering a new basic block callback.
- `def deactivate(self)`: Stops code coverage collection, for example by de-registering the aforementioned basic block callback.
- `def dump_coverage(self, coverage_file)`: Should open the file specified in `coverage_file` and then write all the collected coverage information into it. Usually the coverage format will dictate some fixed-size header, followed by a variable-length list of the individual basic blocks which were encountered during emulation.
Empty file.
@@ -0,0 +1 @@
__all__ = ["base", "drcov"]
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
# Built on top of Unicorn emulator (www.unicorn-engine.org)

from abc import ABC, abstractmethod

class QlBaseCoverage(ABC):
"""
An abstract base class for concrete code coverage collectors.
To add support for a new coverage format, just derive from this class and implement
all the methods marked with the @abstractmethod decorator.
"""

def __init__(self):
super().__init__()

@property
@staticmethod
@abstractmethod
def FORMAT_NAME():
raise NotImplementedError

@abstractmethod
def activate(self):
pass

@abstractmethod
def deactivate(self):
pass

@abstractmethod
def dump_coverage(self, coverage_file):
pass

@@ -0,0 +1,61 @@
#!/usr/bin/env python3
#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
# Built on top of Unicorn emulator (www.unicorn-engine.org)

from ctypes import Structure
from ctypes import c_uint32, c_uint16
from .base import QlBaseCoverage

# Adapted from https://www.ayrx.me/drcov-file-format
class bb_entry(Structure):
_fields_ = [
("start", c_uint32),
("size", c_uint16),
("mod_id", c_uint16)
]

class QlDrCoverage(QlBaseCoverage):
"""
Collects emulated code coverage and formats it in accordance with the DynamoRIO based
tool drcov: https://dynamorio.org/dynamorio_docs/page_drcov.html

The resulting output file can later be imported by coverage visualization tools such
as Lighthouse: https://github.com/gaasedelen/lighthouse
"""

FORMAT_NAME = "drcov"

def __init__(self, ql):
super().__init__()
self.ql = ql
self.drcov_version = 2
self.drcov_flavor = 'drcov'
self.basic_blocks = []
self.bb_callback = None

@staticmethod
def block_callback(ql, address, size, self):
for mod_id, mod in enumerate(ql.loader.images):
if mod.base <= address <= mod.end:
ent = bb_entry(address - mod.base, size, mod_id)
self.basic_blocks.append(ent)
break

def activate(self):
self.bb_callback = self.ql.hook_block(self.block_callback, user_data=self)

def deactivate(self):
self.ql.hook_del(self.bb_callback)

def dump_coverage(self, coverage_file):
with open(coverage_file, "wb") as cov:
cov.write(f"DRCOV VERSION: {self.drcov_version}\n".encode())
cov.write(f"DRCOV FLAVOR: {self.drcov_flavor}\n".encode())
cov.write(f"Module Table: version {self.drcov_version}, count {len(self.ql.loader.images)}\n".encode())
cov.write("Columns: id, base, end, entry, checksum, timestamp, path\n".encode())
for mod_id, mod in enumerate(self. ql.loader.images):
cov.write(f"{mod_id}, {mod.base}, {mod.end}, 0, 0, 0, {mod.path}\n".encode())
cov.write(f"BB Table: {len(self.basic_blocks)} bbs\n".encode())
for bb in self.basic_blocks:
cov.write(bytes(bb))
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
# Built on top of Unicorn emulator (www.unicorn-engine.org)

from .formats import *
from contextlib import contextmanager

class CoverageFactory():
def __init__(self):
self.coverage_collectors = {subcls.FORMAT_NAME:subcls for subcls in base.QlBaseCoverage.__subclasses__()}

@property
def formats(self):
return self.coverage_collectors.keys()

def get_coverage_collector(self, ql, name):
return self.coverage_collectors[name](ql)

factory = CoverageFactory()

@contextmanager
def collect_coverage(ql, name, coverage_file):
"""
Context manager for emulating a given piece of code with coverage collection turned on.
Example:
with collect_coverage(ql, 'drcov', 'output.cov'):
ql.run(...)
"""

cov = factory.get_coverage_collector(ql, name)
cov.activate()
try:
yield
finally:
cov.deactivate()
if coverage_file:
cov.dump_coverage(coverage_file)
@@ -5,8 +5,12 @@
import pefile
from qiling.const import QL_OS, QL_OS_ALL, QL_ARCH, QL_ENDIAN, QL_OUTPUT
from qiling.exception import QlErrorArch, QlErrorOsType, QlErrorOutput
from collections import namedtuple

class QlLoader():
QlImage = namedtuple('Image', 'base end path')

def __init__(self, ql):
self.ql = ql
self.env = self.ql.env
self.images = []
@@ -83,6 +83,7 @@ def map_and_load(self, path):
self.ql.nprint("[+] PE entry point at 0x%x" % entry_point)
self.install_loaded_image_protocol(IMAGE_BASE, IMAGE_SIZE, entry_point)
self.modules.append((path, IMAGE_BASE, entry_point, pe))
self.images.append(self.QlImage(IMAGE_BASE, IMAGE_BASE + pe.NT_HEADERS.OPTIONAL_HEADER.SizeOfImage, path))
return True
else:
IMAGE_BASE += 0x10000
8 qltool
@@ -8,6 +8,7 @@ import argparse, os, string, sys, ast
from binascii import unhexlify
from qiling import *
from qiling import __version__ as ql_version
from qiling.coverage import utils as cov_utils


def parse_args(parser, commands):
@@ -167,7 +168,9 @@ if __name__ == '__main__':
help='Stop running while encounter any error (only use it with debug mode)')
comm_parser.add_argument('-m','--multithread', action='store_true', default=False, dest='multithread', help='Run in multithread mode')
comm_parser.add_argument('--timeout', required=False, help='Emulation timeout')

comm_parser.add_argument('-c', '--coverage-file', required=False, default=None, dest='coverage_file', help='Code coverage file name')
comm_parser.add_argument('--coverage-format', required=False, default='drcov', dest='coverage_format',
choices=cov_utils.factory.formats, help='Code coverage file format')
options = parser.parse_args()

# var check
@@ -258,7 +261,8 @@ if __name__ == '__main__':
timeout = int(options.timeout)

# ql run
ql.run(timeout=timeout)
with cov_utils.collect_coverage(ql, options.coverage_format, options.coverage_file):
ql.run(timeout=timeout)
exit(ql.os.exit_code)