├── requirements.txt ├── start.py ├── aflcov ├── __main__.py ├── vis.py ├── knowledge.py ├── __init__.py └── analysis.py ├── setup.py ├── README.md ├── LICENSE └── .gitignore /requirements.txt: -------------------------------------------------------------------------------- 1 | angr 2 | tracer 3 | bingraphvis 4 | cfg-explorer 5 | -------------------------------------------------------------------------------- /start.py: -------------------------------------------------------------------------------- 1 | 2 | from aflcov.__main__ import main 3 | 4 | main() 5 | 6 | -------------------------------------------------------------------------------- /aflcov/__main__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logging.getLogger('axt.aflcovexplorer').setLevel(logging.INFO) 3 | 4 | from . import AflCovCFGExplorerCLI 5 | 6 | def main(): 7 | explorer = AflCovCFGExplorerCLI() 8 | explorer.run() 9 | 10 | if __name__ == '__main__': 11 | main() 12 | -------------------------------------------------------------------------------- /aflcov/vis.py: -------------------------------------------------------------------------------- 1 | from bingraphvis.base import Content 2 | 3 | class AflCovInfo(Content): 4 | def __init__(self, project): 5 | super(AflCovInfo, self).__init__('aflcovinfo', ['text']) 6 | self.project = project 7 | 8 | def gen_render(self, n): 9 | node = n.obj 10 | n.content[self.name] = { 11 | 'data': [{ 12 | 'text': { 13 | 'content': "Hit: %d / %d " % (self.project.kb.cov.node_hit_count(node.addr), self.project.kb.cov.nr_of_paths), 14 | 'style':'B', 15 | 'align':'LEFT' 16 | } 17 | }], 18 | 'columns': self.get_columns() 19 | } 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | try: 3 | from setuptools import find_packages 4 | packages = find_packages() 5 | except ImportError: 6 | import os 7 | packages = [x.strip('./').replace('/','.') for x in os.popen('find -name "__init__.py" | xargs -n1 dirname').read().strip().split('\n')] 8 | 9 | setup( 10 | name='afl-cov', 11 | version='0.0.1', 12 | author='Attila Axt', 13 | author_email='axt@load.hu', 14 | license='BSD', 15 | platforms=['Linux'], 16 | packages=packages, 17 | install_requires=[ 18 | 'angr', 19 | 'tracer', 20 | 'bingraphvis', 21 | 'cfg-explorer' 22 | ], 23 | description='AFL fuzzing coverage CFG visualization', 24 | long_description='AFL fuzzing coverage CFG visualization', 25 | url='https://github.com/axt/afl-cov', 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # afl-cov 2 | 3 | AFL fuzzing coverage CFG visualization 4 | 5 | The utility is based on [angr](https://github.com/angr/angr), [tracer](https://github.com/angr/tracer), [qemu](https://github.com/qemu/qemu), [bingraphvis](http://github.com/axt/bingraphvis/) and [cfg-explorer](http://github.com/axt/cfg-explorer/). 6 | 7 | ## Note 8 | 9 | This project is in its very early stage! 10 | 11 | ## Usage 12 | ``` 13 | $ python -m aflcov /your/binary /path/to/afl/fuzz/queue -l 14 | ``` 15 | The command above will build the CFG, run the executable for each of the queue files through `qemu` to collect trace info, calculates the node coverage, and display it on the CFG. 16 | 17 | 18 | ## Limitations 19 | * see limitations of [cfg-explorer](https://github.com/axt/cfg-explorer) 20 | 21 | ## Screenshots 22 | 23 | ![scr1][scr1] 24 | ![scr2][scr2] 25 | 26 | 27 | [scr1]: http://i.imgur.com/gqxvXS0.png 28 | [scr2]: http://i.imgur.com/rTiXZgt.png 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /aflcov/knowledge.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from angr.knowledge_plugins import KnowledgeBasePlugin 3 | 4 | class Coverage(KnowledgeBasePlugin, dict): 5 | 6 | def __init__(self, kb): 7 | super(Coverage, self).__init__() 8 | self._kb = kb 9 | self._nr_of_paths = 0 10 | self._nodes_hit = defaultdict(int) 11 | 12 | self.nodes_cov_partial = set() 13 | self.nodes_cov_full = set() 14 | 15 | def copy(self): 16 | o = IndirectJumps(self._kb) 17 | o._nr_of_paths = self._nr_of_paths 18 | o._nodes_hit = _nodes_hit.copy() 19 | o.nodes_cov_partial.update(self.nodes_cov_partial) 20 | o.nodes_cov_full.update(self.nodes_cov_full) 21 | 22 | def register_new_path(self, nodes_hit=None): 23 | self._nr_of_paths += 1 24 | if nodes_hit: 25 | for addr in nodes_hit: 26 | self.register_node_hit(addr) 27 | 28 | def register_node_hit(self, addr): 29 | self._nodes_hit[addr] += 1 30 | 31 | def node_hit_count(self, addr): 32 | return self._nodes_hit[addr] 33 | 34 | @property 35 | def nr_of_paths(self): 36 | return self._nr_of_paths 37 | 38 | @property 39 | def nodes_hit(self): 40 | return len(self._nodes_hit) 41 | 42 | KnowledgeBasePlugin.register_default('cov', Coverage) 43 | -------------------------------------------------------------------------------- /aflcov/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logging.getLogger('angr.analyses').setLevel(logging.INFO) 3 | 4 | from cfgexplorer import CFGExplorerCLI 5 | 6 | import knowledge 7 | import analysis 8 | 9 | from cfgexplorer import CFGVisEndpoint 10 | from bingraphvis import ColorNodes 11 | from vis import AflCovInfo 12 | 13 | class AflCFGVisEndpoint(CFGVisEndpoint): 14 | def __init__(self, cfg): 15 | super(AflCFGVisEndpoint, self).__init__('cfg', cfg) 16 | 17 | def annotate_vis(self, vis, addr): 18 | kb = self.cfg.project.kb 19 | vis.add_node_annotator(ColorNodes(filter=lambda node: node.obj.addr in kb.cov.nodes_cov_full, fillcolor='salmon')) 20 | vis.add_node_annotator(ColorNodes(filter=lambda node: node.obj.addr in kb.cov.nodes_cov_partial, fillcolor='orchid')) 21 | vis.add_content(AflCovInfo(self.cfg.project)) 22 | 23 | class AflCovCFGExplorerCLI(CFGExplorerCLI): 24 | def __init__(self): 25 | super(AflCovCFGExplorerCLI, self).__init__() 26 | 27 | def _extend_parser(self): 28 | self.parser.add_argument('aflqueue', metavar='aflqueue', type=str, help='afl fuzz queue directory') 29 | 30 | def _postprocess_cfg(self): 31 | self.project.analyses.AflCoverage(self.cfg, self.args.aflqueue) 32 | 33 | def add_endpoints(self): 34 | self.app.add_vis_endpoint(AflCFGVisEndpoint(self.cfg)) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2017, Attila Axt 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 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 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /aflcov/analysis.py: -------------------------------------------------------------------------------- 1 | import logging 2 | l = logging.getLogger('angr.analyses.axt.aflcoverage') 3 | 4 | 5 | import os 6 | from angr.analyses import Analysis, register_analysis 7 | import tracer 8 | 9 | class LightTracer(tracer.Tracer): 10 | def _prepare_paths(self): 11 | pass 12 | 13 | class AflCoverage(Analysis): 14 | def __init__(self, cfg, afl_queue_path, max_samples=None): 15 | super(AflCoverage, self).__init__() 16 | self.cfg = cfg 17 | self.afl_queue_path = afl_queue_path 18 | self.max_samples = max_samples 19 | self.nodes_cov_partial = set() 20 | self.nodes_cov_full = set() 21 | self._analyse() 22 | 23 | def _analyse(self): 24 | queue_files = os.listdir(self.afl_queue_path) 25 | kb = self.project.kb 26 | for i, f in enumerate(queue_files): 27 | binary = self.project.filename 28 | argv = [ binary, self.afl_queue_path + "/" + f] 29 | #will be available in new tracer 30 | #t = tracer.QEMURunner(project=self.project, binary=sample.binary, input="", argv=argv) 31 | t = LightTracer(project=self.project, binary=binary, input="", argv=argv) 32 | kb.cov.register_new_path(set(t.r.trace)) 33 | l.info("Processing [%d/%d] %s, blocks hit: %d" % (i+1, len(queue_files), f, kb.cov.nodes_hit)) 34 | if self.max_samples and i+1 >= self.max_samples: 35 | break 36 | 37 | for pnode in self.cfg.nodes(): 38 | if kb.cov.node_hit_count(pnode.addr) == 0: 39 | continue 40 | partial = False 41 | for node in self.cfg.get_all_nodes(pnode.addr): 42 | for succ in self.cfg.graph.successors(node): 43 | if kb.cov.node_hit_count(succ.addr) == 0: 44 | partial = True 45 | break 46 | if partial: 47 | kb.cov.nodes_cov_partial.add(node.addr) 48 | else: 49 | kb.cov.nodes_cov_full.add(node.addr) 50 | 51 | register_analysis(AflCoverage, 'AflCoverage') 52 | --------------------------------------------------------------------------------