{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "slide"
},
"tags": []
},
"source": [
"# Mutation-Based Fuzzing\n",
"\n",
"Most [randomly generated inputs](Fuzzer.ipynb) are syntactically _invalid_ and thus are quickly rejected by the processing program. To exercise functionality beyond input processing, we must increase chances to obtain valid inputs. One such way is so-called *mutational fuzzing* – that is, introducing small changes to existing inputs that may still keep the input valid, yet exercise new behavior. We show how to create such mutations, and how to guide them towards yet uncovered code, applying central concepts from the popular AFL fuzzer."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.183206Z",
"iopub.status.busy": "2024-01-18T17:14:25.182707Z",
"iopub.status.idle": "2024-01-18T17:14:25.247203Z",
"shell.execute_reply": "2024-01-18T17:14:25.246851Z"
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [
{
"data": {
"text/html": [
"\n",
" \n",
" "
],
"text/plain": [
""
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from bookutils import YouTubeVideo\n",
"YouTubeVideo('5ROhc_42jQU')"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "skip"
}
},
"source": [
"**Prerequisites**\n",
"\n",
"* You should know how basic fuzzing works; for instance, from the [\"Fuzzing\"](Fuzzer.ipynb) chapter.\n",
"* You should understand the basics of [obtaining coverage](Coverage.ipynb)."
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "skip"
}
},
"source": [
"## Synopsis\n",
"\n",
"\n",
"To [use the code provided in this chapter](Importing.ipynb), write\n",
"\n",
"```python\n",
">>> from fuzzingbook.MutationFuzzer import \n",
"```\n",
"\n",
"and then make use of the following features.\n",
"\n",
"\n",
"This chapter introduces a `MutationFuzzer` class that takes a list of _seed inputs_ which are then mutated:\n",
"\n",
"```python\n",
">>> seed_input = \"http://www.google.com/search?q=fuzzing\"\n",
">>> mutation_fuzzer = MutationFuzzer(seed=[seed_input])\n",
">>> [mutation_fuzzer.fuzz() for i in range(10)]\n",
"['http://www.google.com/search?q=fuzzing',\n",
" 'http://wwBw.google.com/searh?q=fuzzing',\n",
" 'http8//wswgoRogle.am/secch?qU=fuzzing',\n",
" 'ittp://www.googLe.com/serch?q=fuzzingZ',\n",
" 'httP://wgw.google.com/seasch?Q=fuxzanmgY',\n",
" 'http://www.google.cxcom/search?q=fuzzing',\n",
" 'hFttp://ww.-g\\x7fog+le.com/s%arch?q=f-uzz#ing',\n",
" 'http://www\\x0egoogle.com/seaNrch?q=fuZzing',\n",
" 'http//www.Ygooge.comsarch?q=fuz~Ijg',\n",
" 'http8//ww.goog5le.com/sezarc?q=fuzzing']\n",
"```\n",
"The `MutationCoverageFuzzer` maintains a _population_ of inputs, which are then evolved in order to maximize coverage.\n",
"\n",
"```python\n",
">>> mutation_fuzzer = MutationCoverageFuzzer(seed=[seed_input])\n",
">>> mutation_fuzzer.runs(http_runner, trials=10000)\n",
">>> mutation_fuzzer.population[:5]\n",
"['http://www.google.com/search?q=fuzzing',\n",
" 'http://wwv.oogle>co/search7Eq=fuzing',\n",
" 'http://wwv\\x0eOogleb>co/seakh7Eq\\x1d;fuzing',\n",
" 'http://wwv\\x0eoglebkooqeakh7Eq\\x1d;fuzing',\n",
" 'http://wwv\\x0eoglekol=oekh7Eq\\x1d\\x1bf~ing']\n",
"```\n",
"![](PICS/MutationFuzzer-synopsis-1.svg)\n",
"\n"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"## Fuzzing with Mutations\n",
"\n",
"On November 2013, the first version of [American Fuzzy Lop](http://lcamtuf.coredump.cx/afl/) (AFL) was released. Since then, AFL has become one of the most successful fuzzing tools and comes in many flavors, e.g., [AFLFast](https://github.com/mboehme/aflfast), [AFLGo](https://github.com/aflgo/aflgo), and [AFLSmart](https://github.com/aflsmart/aflsmart) (which are discussed in this book). AFL has made fuzzing a popular choice for automated vulnerability detection. It was the first to demonstrate that vulnerabilities can be detected automatically at a large scale in many security-critical, real-world applications.\n",
"\n",
"![American Fuzzy Lop Command Line User Interface](PICS/afl_screen.png)\n",
"
Figure 1. American Fuzzy Lop Command Line User Interface
\n",
"\n",
"In this chapter, we are going to introduce the basics of mutational fuzz testing; the next chapter will then further show how to direct fuzzing towards specific code goals."
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"## Fuzzing a URL Parser\n",
"\n",
"Many programs expect their inputs to come in a very specific format before they would actually process them. As an example, think of a program that accepts a URL (a Web address). The URL has to be in a valid format (i.e., the URL format) such that the program can deal with it. When fuzzing with random inputs, what are our chances to actually produce a valid URL?"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"To get deeper into the problem, let us explore what URLs are made of. A URL consists of a number of elements:\n",
"\n",
" scheme://netloc/path?query#fragment\n",
" \n",
"where\n",
"\n",
"* `scheme` is the protocol to be used, including `http`, `https`, `ftp`, `file`...\n",
"* `netloc` is the name of the host to connect to, such as `www.google.com`\n",
"* `path` is the path on that very host, such as `search`\n",
"* `query` is a list of key/value pairs, such as `q=fuzzing`\n",
"* `fragment` is a marker for a location in the retrieved document, such as `#result`"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"In Python, we can use the `urlparse()` function to parse and decompose a URL into its parts."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.271452Z",
"iopub.status.busy": "2024-01-18T17:14:25.271233Z",
"iopub.status.idle": "2024-01-18T17:14:25.273609Z",
"shell.execute_reply": "2024-01-18T17:14:25.273286Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"import bookutils.setup"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.275405Z",
"iopub.status.busy": "2024-01-18T17:14:25.275282Z",
"iopub.status.idle": "2024-01-18T17:14:25.277143Z",
"shell.execute_reply": "2024-01-18T17:14:25.276823Z"
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"from typing import Tuple, List, Callable, Set, Any"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.278816Z",
"iopub.status.busy": "2024-01-18T17:14:25.278699Z",
"iopub.status.idle": "2024-01-18T17:14:25.280370Z",
"shell.execute_reply": "2024-01-18T17:14:25.280124Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"source": [
"from urllib.parse import urlparse"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.281948Z",
"iopub.status.busy": "2024-01-18T17:14:25.281846Z",
"iopub.status.idle": "2024-01-18T17:14:25.283952Z",
"shell.execute_reply": "2024-01-18T17:14:25.283678Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"ParseResult(scheme='http', netloc='www.google.com', path='/search', params='', query='q=fuzzing', fragment='')"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"urlparse(\"http://www.google.com/search?q=fuzzing\")"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"We see how the result encodes the individual parts of the URL in different attributes."
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Let us now assume we have a program that takes a URL as input. To simplify things, we won't let it do very much; we simply have it check the passed URL for validity. If the URL is valid, it returns True; otherwise, it raises an exception."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.285835Z",
"iopub.status.busy": "2024-01-18T17:14:25.285701Z",
"iopub.status.idle": "2024-01-18T17:14:25.288071Z",
"shell.execute_reply": "2024-01-18T17:14:25.287722Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"source": [
"def http_program(url: str) -> bool:\n",
" supported_schemes = [\"http\", \"https\"]\n",
" result = urlparse(url)\n",
" if result.scheme not in supported_schemes:\n",
" raise ValueError(\"Scheme must be one of \" + \n",
" repr(supported_schemes))\n",
" if result.netloc == '':\n",
" raise ValueError(\"Host must be non-empty\")\n",
"\n",
" # Do something with the URL\n",
" return True"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Let us now go and fuzz `http_program()`. To fuzz, we use the full range of printable ASCII characters, such that `:`, `/`, and lowercase letters are included."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.289766Z",
"iopub.status.busy": "2024-01-18T17:14:25.289639Z",
"iopub.status.idle": "2024-01-18T17:14:25.372467Z",
"shell.execute_reply": "2024-01-18T17:14:25.372113Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"from Fuzzer import fuzzer"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.374511Z",
"iopub.status.busy": "2024-01-18T17:14:25.374369Z",
"iopub.status.idle": "2024-01-18T17:14:25.376771Z",
"shell.execute_reply": "2024-01-18T17:14:25.376492Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"'\"N&+slk%h\\x7fyp5o\\'@[3(rW*M5W]tMFPU4\\\\P@tz%[X?uo\\\\1?b4T;1bDeYtHx #UJ5w}pMmPodJM,_'"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"fuzzer(char_start=32, char_range=96)"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Let's try to fuzz with 1000 random inputs and see whether we have some success."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.378354Z",
"iopub.status.busy": "2024-01-18T17:14:25.378252Z",
"iopub.status.idle": "2024-01-18T17:14:25.404657Z",
"shell.execute_reply": "2024-01-18T17:14:25.404372Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"source": [
"for i in range(1000):\n",
" try:\n",
" url = fuzzer()\n",
" result = http_program(url)\n",
" print(\"Success!\")\n",
" except ValueError:\n",
" pass"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"What are the chances of actually getting a valid URL? We need our string to start with `\"http://\"` or `\"https://\"`. Let's take the `\"http://\"` case first. These are seven very specific characters we need to start with. The chance of producing these seven characters randomly (with a character range of 96 different characters) is $1 : 96^7$, or"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.406546Z",
"iopub.status.busy": "2024-01-18T17:14:25.406451Z",
"iopub.status.idle": "2024-01-18T17:14:25.409149Z",
"shell.execute_reply": "2024-01-18T17:14:25.408856Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"75144747810816"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"96 ** 7"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"The odds of producing a `\"https://\"` prefix are even worse, at $1 : 96^8$:"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.411176Z",
"iopub.status.busy": "2024-01-18T17:14:25.411009Z",
"iopub.status.idle": "2024-01-18T17:14:25.413657Z",
"shell.execute_reply": "2024-01-18T17:14:25.413353Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"7213895789838336"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"96 ** 8"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"which gives us a total chance of"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.415395Z",
"iopub.status.busy": "2024-01-18T17:14:25.415279Z",
"iopub.status.idle": "2024-01-18T17:14:25.417537Z",
"shell.execute_reply": "2024-01-18T17:14:25.417187Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"1.344627131107667e-14"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"likelihood = 1 / (96 ** 7) + 1 / (96 ** 8)\n",
"likelihood"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"And this is the number of runs (on average) we'd need to produce a valid URL scheme:"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.419292Z",
"iopub.status.busy": "2024-01-18T17:14:25.419171Z",
"iopub.status.idle": "2024-01-18T17:14:25.421364Z",
"shell.execute_reply": "2024-01-18T17:14:25.421078Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"74370059689055.02"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"1 / likelihood"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Let's measure how long one run of `http_program()` takes:"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.423203Z",
"iopub.status.busy": "2024-01-18T17:14:25.423080Z",
"iopub.status.idle": "2024-01-18T17:14:25.424818Z",
"shell.execute_reply": "2024-01-18T17:14:25.424469Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"from Timer import Timer"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.426434Z",
"iopub.status.busy": "2024-01-18T17:14:25.426315Z",
"iopub.status.idle": "2024-01-18T17:14:25.453841Z",
"shell.execute_reply": "2024-01-18T17:14:25.453550Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"2.451037500577513e-05"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"trials = 1000\n",
"with Timer() as t:\n",
" for i in range(trials):\n",
" try:\n",
" url = fuzzer()\n",
" result = http_program(url)\n",
" print(\"Success!\")\n",
" except ValueError:\n",
" pass\n",
"\n",
"duration_per_run_in_seconds = t.elapsed_time() / trials\n",
"duration_per_run_in_seconds"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"That's pretty fast, isn't it? Unfortunately, we have a lot of runs to cover."
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.455824Z",
"iopub.status.busy": "2024-01-18T17:14:25.455627Z",
"iopub.status.idle": "2024-01-18T17:14:25.458517Z",
"shell.execute_reply": "2024-01-18T17:14:25.457986Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"1822838052.1806188"
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"seconds_until_success = duration_per_run_in_seconds * (1 / likelihood)\n",
"seconds_until_success"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"which translates into"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.460570Z",
"iopub.status.busy": "2024-01-18T17:14:25.460430Z",
"iopub.status.idle": "2024-01-18T17:14:25.463022Z",
"shell.execute_reply": "2024-01-18T17:14:25.462730Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"57.76225226825294"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"hours_until_success = seconds_until_success / 3600\n",
"days_until_success = hours_until_success / 24\n",
"years_until_success = days_until_success / 365.25\n",
"years_until_success"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"Even if we parallelize things a lot, we're still in for months to years of waiting. And that's for getting _one_ successful run that will get deeper into `http_program()`."
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"What basic fuzzing will do well is to test `urlparse()`, and if there is an error in this parsing function, it has good chances of uncovering it. But as long as we cannot produce a valid input, we are out of luck in reaching any deeper functionality."
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"## Mutating Inputs\n",
"\n",
"The alternative to generating random strings from scratch is to start with a given _valid_ input, and then to subsequently _mutate_ it. A _mutation_ in this context is a simple string manipulation - say, inserting a (random) character, deleting a character, or flipping a bit in a character representation. This is called *mutational fuzzing* – in contrast to the _generational fuzzing_ techniques discussed earlier.\n",
"\n",
"Here are some mutations to get you started:"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.464877Z",
"iopub.status.busy": "2024-01-18T17:14:25.464630Z",
"iopub.status.idle": "2024-01-18T17:14:25.466444Z",
"shell.execute_reply": "2024-01-18T17:14:25.466158Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"import random"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.468342Z",
"iopub.status.busy": "2024-01-18T17:14:25.468210Z",
"iopub.status.idle": "2024-01-18T17:14:25.470338Z",
"shell.execute_reply": "2024-01-18T17:14:25.470000Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [],
"source": [
"def delete_random_character(s: str) -> str:\n",
" \"\"\"Returns s with a random character deleted\"\"\"\n",
" if s == \"\":\n",
" return s\n",
"\n",
" pos = random.randint(0, len(s) - 1)\n",
" # print(\"Deleting\", repr(s[pos]), \"at\", pos)\n",
" return s[:pos] + s[pos + 1:]"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.472075Z",
"iopub.status.busy": "2024-01-18T17:14:25.471964Z",
"iopub.status.idle": "2024-01-18T17:14:25.474089Z",
"shell.execute_reply": "2024-01-18T17:14:25.473834Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"'A uick brown fox'\n",
"'A quic brown fox'\n",
"'A quick brown fo'\n",
"'A quic brown fox'\n",
"'A quick bown fox'\n",
"'A quick bown fox'\n",
"'A quick brown fx'\n",
"'A quick brown ox'\n",
"'A quick brow fox'\n",
"'A quic brown fox'\n"
]
}
],
"source": [
"seed_input = \"A quick brown fox\"\n",
"for i in range(10):\n",
" x = delete_random_character(seed_input)\n",
" print(repr(x))"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.476522Z",
"iopub.status.busy": "2024-01-18T17:14:25.476160Z",
"iopub.status.idle": "2024-01-18T17:14:25.478640Z",
"shell.execute_reply": "2024-01-18T17:14:25.478340Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [],
"source": [
"def insert_random_character(s: str) -> str:\n",
" \"\"\"Returns s with a random character inserted\"\"\"\n",
" pos = random.randint(0, len(s))\n",
" random_character = chr(random.randrange(32, 127))\n",
" # print(\"Inserting\", repr(random_character), \"at\", pos)\n",
" return s[:pos] + random_character + s[pos:]"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.480288Z",
"iopub.status.busy": "2024-01-18T17:14:25.480138Z",
"iopub.status.idle": "2024-01-18T17:14:25.482295Z",
"shell.execute_reply": "2024-01-18T17:14:25.481967Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"'A quick brvown fox'\n",
"'A quwick brown fox'\n",
"'A qBuick brown fox'\n",
"'A quick broSwn fox'\n",
"'A quick brown fvox'\n",
"'A quick brown 3fox'\n",
"'A quick brNown fox'\n",
"'A quick brow4n fox'\n",
"'A quick brown fox8'\n",
"'A equick brown fox'\n"
]
}
],
"source": [
"for i in range(10):\n",
" print(repr(insert_random_character(seed_input)))"
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.484112Z",
"iopub.status.busy": "2024-01-18T17:14:25.483870Z",
"iopub.status.idle": "2024-01-18T17:14:25.486283Z",
"shell.execute_reply": "2024-01-18T17:14:25.485988Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [],
"source": [
"def flip_random_character(s):\n",
" \"\"\"Returns s with a random bit flipped in a random position\"\"\"\n",
" if s == \"\":\n",
" return s\n",
"\n",
" pos = random.randint(0, len(s) - 1)\n",
" c = s[pos]\n",
" bit = 1 << random.randint(0, 6)\n",
" new_c = chr(ord(c) ^ bit)\n",
" # print(\"Flipping\", bit, \"in\", repr(c) + \", giving\", repr(new_c))\n",
" return s[:pos] + new_c + s[pos + 1:]"
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.487845Z",
"iopub.status.busy": "2024-01-18T17:14:25.487736Z",
"iopub.status.idle": "2024-01-18T17:14:25.489949Z",
"shell.execute_reply": "2024-01-18T17:14:25.489590Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"'A quick bRown fox'\n",
"'A quici brown fox'\n",
"'A\"quick brown fox'\n",
"'A quick brown$fox'\n",
"'A quick bpown fox'\n",
"'A quick brown!fox'\n",
"'A 1uick brown fox'\n",
"'@ quick brown fox'\n",
"'A quic+ brown fox'\n",
"'A quick bsown fox'\n"
]
}
],
"source": [
"for i in range(10):\n",
" print(repr(flip_random_character(seed_input)))"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Let us now create a random mutator that randomly chooses which mutation to apply:"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.492390Z",
"iopub.status.busy": "2024-01-18T17:14:25.492221Z",
"iopub.status.idle": "2024-01-18T17:14:25.494416Z",
"shell.execute_reply": "2024-01-18T17:14:25.494069Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"source": [
"def mutate(s: str) -> str:\n",
" \"\"\"Return s with a random mutation applied\"\"\"\n",
" mutators = [\n",
" delete_random_character,\n",
" insert_random_character,\n",
" flip_random_character\n",
" ]\n",
" mutator = random.choice(mutators)\n",
" # print(mutator)\n",
" return mutator(s)"
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.496011Z",
"iopub.status.busy": "2024-01-18T17:14:25.495887Z",
"iopub.status.idle": "2024-01-18T17:14:25.497895Z",
"shell.execute_reply": "2024-01-18T17:14:25.497660Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"'A qzuick brown fox'\n",
"' quick brown fox'\n",
"'A quick Brown fox'\n",
"'A qMuick brown fox'\n",
"'A qu_ick brown fox'\n",
"'A quick bXrown fox'\n",
"'A quick brown fx'\n",
"'A quick!brown fox'\n",
"'A! quick brown fox'\n",
"'A quick brownfox'\n"
]
}
],
"source": [
"for i in range(10):\n",
" print(repr(mutate(\"A quick brown fox\")))"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"The idea is now that _if_ we have some valid input(s) to begin with, we may create more input candidates by applying one of the above mutations. To see how this works, let's get back to URLs."
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"## Mutating URLs"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"Let us now get back to our URL parsing problem. Let us create a function `is_valid_url()` that checks whether `http_program()` accepts the input."
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.499552Z",
"iopub.status.busy": "2024-01-18T17:14:25.499435Z",
"iopub.status.idle": "2024-01-18T17:14:25.501301Z",
"shell.execute_reply": "2024-01-18T17:14:25.501032Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"source": [
"def is_valid_url(url: str) -> bool:\n",
" try:\n",
" result = http_program(url)\n",
" return True\n",
" except ValueError:\n",
" return False"
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.502823Z",
"iopub.status.busy": "2024-01-18T17:14:25.502722Z",
"iopub.status.idle": "2024-01-18T17:14:25.504358Z",
"shell.execute_reply": "2024-01-18T17:14:25.504076Z"
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"source": [
"assert is_valid_url(\"http://www.google.com/search?q=fuzzing\")\n",
"assert not is_valid_url(\"xyzzy\")"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Let us now apply the `mutate()` function on a given URL and see how many valid inputs we obtain."
]
},
{
"cell_type": "code",
"execution_count": 29,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.506147Z",
"iopub.status.busy": "2024-01-18T17:14:25.506019Z",
"iopub.status.idle": "2024-01-18T17:14:25.508305Z",
"shell.execute_reply": "2024-01-18T17:14:25.507933Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"source": [
"seed_input = \"http://www.google.com/search?q=fuzzing\"\n",
"valid_inputs = set()\n",
"trials = 20\n",
"\n",
"for i in range(trials):\n",
" inp = mutate(seed_input)\n",
" if is_valid_url(inp):\n",
" valid_inputs.add(inp)"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"We can now observe that by _mutating_ the original input, we get a high proportion of valid inputs:"
]
},
{
"cell_type": "code",
"execution_count": 30,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.510191Z",
"iopub.status.busy": "2024-01-18T17:14:25.510008Z",
"iopub.status.idle": "2024-01-18T17:14:25.512347Z",
"shell.execute_reply": "2024-01-18T17:14:25.512058Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"0.8"
]
},
"execution_count": 30,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(valid_inputs) / trials"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"What are the odds of also producing a `https:` prefix by mutating a `http:` sample seed input? We have to insert ($1 : 3$) the right character `'s'` ($1 : 96$) into the correct position ($1 : l$), where $l$ is the length of our seed input. This means that on average, we need this many runs:"
]
},
{
"cell_type": "code",
"execution_count": 31,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.513989Z",
"iopub.status.busy": "2024-01-18T17:14:25.513874Z",
"iopub.status.idle": "2024-01-18T17:14:25.516058Z",
"shell.execute_reply": "2024-01-18T17:14:25.515807Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"10944"
]
},
"execution_count": 31,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"trials = 3 * 96 * len(seed_input)\n",
"trials"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"We can actually afford this. Let's try:"
]
},
{
"cell_type": "code",
"execution_count": 32,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.517678Z",
"iopub.status.busy": "2024-01-18T17:14:25.517529Z",
"iopub.status.idle": "2024-01-18T17:14:25.519236Z",
"shell.execute_reply": "2024-01-18T17:14:25.518972Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"from Timer import Timer"
]
},
{
"cell_type": "code",
"execution_count": 33,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.520757Z",
"iopub.status.busy": "2024-01-18T17:14:25.520644Z",
"iopub.status.idle": "2024-01-18T17:14:25.528570Z",
"shell.execute_reply": "2024-01-18T17:14:25.528309Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Success after 3656 trials in 0.0055189170088851824 seconds\n"
]
}
],
"source": [
"trials = 0\n",
"with Timer() as t:\n",
" while True:\n",
" trials += 1\n",
" inp = mutate(seed_input)\n",
" if inp.startswith(\"https://\"):\n",
" print(\n",
" \"Success after\",\n",
" trials,\n",
" \"trials in\",\n",
" t.elapsed_time(),\n",
" \"seconds\")\n",
" break"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"Of course, if we wanted to get, say, an `\"ftp://\"` prefix, we would need more mutations and more runs – most important, though, we would need to apply _multiple_ mutations."
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"## Multiple Mutations\n",
"\n",
"So far, we have only applied one single mutation on a sample string. However, we can also apply _multiple_ mutations, further changing it. What happens, for instance, if we apply, say, 20 mutations on our sample string?"
]
},
{
"cell_type": "code",
"execution_count": 34,
"metadata": {
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.530327Z",
"iopub.status.busy": "2024-01-18T17:14:25.530203Z",
"iopub.status.idle": "2024-01-18T17:14:25.531860Z",
"shell.execute_reply": "2024-01-18T17:14:25.531587Z"
},
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [],
"source": [
"seed_input = \"http://www.google.com/search?q=fuzzing\"\n",
"mutations = 50"
]
},
{
"cell_type": "code",
"execution_count": 35,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.533560Z",
"iopub.status.busy": "2024-01-18T17:14:25.533236Z",
"iopub.status.idle": "2024-01-18T17:14:25.535574Z",
"shell.execute_reply": "2024-01-18T17:14:25.535330Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0 mutations: 'http://www.google.com/search?q=fuzzing'\n",
"5 mutations: 'http:/L/www.googlej.com/seaRchq=fuz:ing'\n",
"10 mutations: 'http:/L/www.ggoWglej.com/seaRchqfu:in'\n",
"15 mutations: 'http:/L/wwggoWglej.com/seaR3hqf,u:in'\n",
"20 mutations: 'htt://wwggoVgle\"j.som/seaR3hqf,u:in'\n",
"25 mutations: 'htt://fwggoVgle\"j.som/eaRd3hqf,u^:in'\n",
"30 mutations: 'htv://>fwggoVgle\"j.qom/ea0Rd3hqf,u^:i'\n",
"35 mutations: 'htv://>fwggozVle\"Bj.qom/eapRd[3hqf,u^:i'\n",
"40 mutations: 'htv://>fwgeo6zTle\"Bj.\\'qom/eapRd[3hqf,tu^:i'\n",
"45 mutations: 'htv://>fwgeo]6zTle\"BjM.\\'qom/eaR[3hqf,tu^:i'\n"
]
}
],
"source": [
"inp = seed_input\n",
"for i in range(mutations):\n",
" if i % 5 == 0:\n",
" print(i, \"mutations:\", repr(inp))\n",
" inp = mutate(inp)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"As you see, the original seed input is hardly recognizable anymore. By mutating the input again and again, we get a higher variety in the input."
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"To implement such multiple mutations in a single package, let us introduce a `MutationFuzzer` class. It takes a seed (a list of strings) as well as a minimum and a maximum number of mutations. "
]
},
{
"cell_type": "code",
"execution_count": 36,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.537538Z",
"iopub.status.busy": "2024-01-18T17:14:25.537105Z",
"iopub.status.idle": "2024-01-18T17:14:25.538997Z",
"shell.execute_reply": "2024-01-18T17:14:25.538720Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"from Fuzzer import Fuzzer"
]
},
{
"cell_type": "code",
"execution_count": 37,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.541238Z",
"iopub.status.busy": "2024-01-18T17:14:25.541080Z",
"iopub.status.idle": "2024-01-18T17:14:25.543516Z",
"shell.execute_reply": "2024-01-18T17:14:25.543268Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"source": [
"class MutationFuzzer(Fuzzer):\n",
" \"\"\"Base class for mutational fuzzing\"\"\"\n",
"\n",
" def __init__(self, seed: List[str],\n",
" min_mutations: int = 2,\n",
" max_mutations: int = 10) -> None:\n",
" \"\"\"Constructor.\n",
" `seed` - a list of (input) strings to mutate.\n",
" `min_mutations` - the minimum number of mutations to apply.\n",
" `max_mutations` - the maximum number of mutations to apply.\n",
" \"\"\"\n",
" self.seed = seed\n",
" self.min_mutations = min_mutations\n",
" self.max_mutations = max_mutations\n",
" self.reset()\n",
"\n",
" def reset(self) -> None:\n",
" \"\"\"Set population to initial seed.\n",
" To be overloaded in subclasses.\"\"\"\n",
" self.population = self.seed\n",
" self.seed_index = 0"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"In the following, let us develop `MutationFuzzer` further by adding more methods to it. The Python language requires us to define an entire class with all methods as a single, continuous unit; however, we would like to introduce one method after another. To avoid this problem, we use a special hack: Whenever we want to introduce a new method to some class `C`, we use the construct\n",
"\n",
"```python\n",
"class C(C):\n",
" def new_method(self, args):\n",
" pass\n",
"```\n",
"\n",
"This seems to define `C` as a subclass of itself, which would make no sense – but actually, it introduces a new `C` class as a subclass of the _old_ `C` class, and then shadowing the old `C` definition. What this gets us is a `C` class with `new_method()` as a method, which is just what we want. (`C` objects defined earlier will retain the earlier `C` definition, though, and thus must be rebuilt.)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Using this hack, we can now add a `mutate()` method that actually invokes the above `mutate()` function. Having `mutate()` as a method is useful when we want to extend a `MutationFuzzer` later."
]
},
{
"cell_type": "code",
"execution_count": 38,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.545175Z",
"iopub.status.busy": "2024-01-18T17:14:25.545071Z",
"iopub.status.idle": "2024-01-18T17:14:25.546830Z",
"shell.execute_reply": "2024-01-18T17:14:25.546594Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"source": [
"class MutationFuzzer(MutationFuzzer):\n",
" def mutate(self, inp: str) -> str:\n",
" return mutate(inp)"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Let's get back to our strategy, maximizing _diversity in coverage_ in our population. First, let us create a method `create_candidate()`, which randomly picks some input from our current population (`self.population`), and then applies between `min_mutations` and `max_mutations` mutation steps, returning the final result:"
]
},
{
"cell_type": "code",
"execution_count": 39,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.548224Z",
"iopub.status.busy": "2024-01-18T17:14:25.548131Z",
"iopub.status.idle": "2024-01-18T17:14:25.550073Z",
"shell.execute_reply": "2024-01-18T17:14:25.549814Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"source": [
"class MutationFuzzer(MutationFuzzer):\n",
" def create_candidate(self) -> str:\n",
" \"\"\"Create a new candidate by mutating a population member\"\"\"\n",
" candidate = random.choice(self.population)\n",
" trials = random.randint(self.min_mutations, self.max_mutations)\n",
" for i in range(trials):\n",
" candidate = self.mutate(candidate)\n",
" return candidate"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"The `fuzz()` method is set to first pick the seeds; when these are gone, we mutate:"
]
},
{
"cell_type": "code",
"execution_count": 40,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.551606Z",
"iopub.status.busy": "2024-01-18T17:14:25.551504Z",
"iopub.status.idle": "2024-01-18T17:14:25.553509Z",
"shell.execute_reply": "2024-01-18T17:14:25.553260Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"source": [
"class MutationFuzzer(MutationFuzzer):\n",
" def fuzz(self) -> str:\n",
" if self.seed_index < len(self.seed):\n",
" # Still seeding\n",
" self.inp = self.seed[self.seed_index]\n",
" self.seed_index += 1\n",
" else:\n",
" # Mutating\n",
" self.inp = self.create_candidate()\n",
" return self.inp"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"Here is the `fuzz()` method in action. With every new invocation of `fuzz()`, we get another variant with multiple mutations applied. "
]
},
{
"cell_type": "code",
"execution_count": 41,
"metadata": {
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.554994Z",
"iopub.status.busy": "2024-01-18T17:14:25.554878Z",
"iopub.status.idle": "2024-01-18T17:14:25.557250Z",
"shell.execute_reply": "2024-01-18T17:14:25.556975Z"
},
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"'http://www.google.com/search?q=fuzzing'"
]
},
"execution_count": 41,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"seed_input = \"http://www.google.com/search?q=fuzzing\"\n",
"mutation_fuzzer = MutationFuzzer(seed=[seed_input])\n",
"mutation_fuzzer.fuzz()"
]
},
{
"cell_type": "code",
"execution_count": 42,
"metadata": {
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.559126Z",
"iopub.status.busy": "2024-01-18T17:14:25.558988Z",
"iopub.status.idle": "2024-01-18T17:14:25.561336Z",
"shell.execute_reply": "2024-01-18T17:14:25.561058Z"
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"'http://www.gogl9ecom/earch?qfuzzing'"
]
},
"execution_count": 42,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"mutation_fuzzer.fuzz()"
]
},
{
"cell_type": "code",
"execution_count": 43,
"metadata": {
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.562912Z",
"iopub.status.busy": "2024-01-18T17:14:25.562790Z",
"iopub.status.idle": "2024-01-18T17:14:25.564953Z",
"shell.execute_reply": "2024-01-18T17:14:25.564719Z"
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"'htotq:/www.googleom/yseach?q=fzzijg'"
]
},
"execution_count": 43,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"mutation_fuzzer.fuzz()"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"The higher variety in inputs, though, increases the risk of having an invalid input. The key to success lies in the idea of _guiding_ these mutations – that is, _keeping those that are especially valuable._"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"## Guiding by Coverage\n",
"\n",
"To cover as much functionality as possible, one can rely on either _specified_ or _implemented_ functionality, as discussed in the [\"Coverage\"](Coverage.ipynb) chapter. For now, we will not assume that there is a specification of program behavior (although it _definitely_ would be good to have one!). We _will_ assume, though, that the program to be tested exists – and that we can leverage its structure to guide test generation.\n",
"\n",
"Since testing always executes the program at hand, one can always gather information about its execution – the least is the information needed to decide whether a test passes or fails. Since coverage is frequently measured as well to determine test quality, let us also assume we can retrieve coverage of a test run. The question is then: _How can we leverage coverage to guide test generation?_"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"One particularly successful idea is implemented in the popular fuzzer named [American fuzzy lop](http://lcamtuf.coredump.cx/afl/), or *AFL* for short. Just like our examples above, AFL evolves test cases that have been successful – but for AFL, \"success\" means _finding a new path through the program execution_. This way, AFL can keep on mutating inputs that so far have found new paths; and if an input finds another path, it will be retained as well."
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Let us build such a strategy. We start with introducing a `Runner` class that captures the coverage for a given function. First, a `FunctionRunner` class:"
]
},
{
"cell_type": "code",
"execution_count": 44,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.566642Z",
"iopub.status.busy": "2024-01-18T17:14:25.566526Z",
"iopub.status.idle": "2024-01-18T17:14:25.568091Z",
"shell.execute_reply": "2024-01-18T17:14:25.567845Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"from Fuzzer import Runner"
]
},
{
"cell_type": "code",
"execution_count": 45,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.569535Z",
"iopub.status.busy": "2024-01-18T17:14:25.569451Z",
"iopub.status.idle": "2024-01-18T17:14:25.572019Z",
"shell.execute_reply": "2024-01-18T17:14:25.571740Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"source": [
"class FunctionRunner(Runner):\n",
" def __init__(self, function: Callable) -> None:\n",
" \"\"\"Initialize. `function` is a function to be executed\"\"\"\n",
" self.function = function\n",
"\n",
" def run_function(self, inp: str) -> Any:\n",
" return self.function(inp)\n",
"\n",
" def run(self, inp: str) -> Tuple[Any, str]:\n",
" try:\n",
" result = self.run_function(inp)\n",
" outcome = self.PASS\n",
" except Exception:\n",
" result = None\n",
" outcome = self.FAIL\n",
"\n",
" return result, outcome"
]
},
{
"cell_type": "code",
"execution_count": 46,
"metadata": {
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.573569Z",
"iopub.status.busy": "2024-01-18T17:14:25.573451Z",
"iopub.status.idle": "2024-01-18T17:14:25.575722Z",
"shell.execute_reply": "2024-01-18T17:14:25.575351Z"
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"(True, 'PASS')"
]
},
"execution_count": 46,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"http_runner = FunctionRunner(http_program)\n",
"http_runner.run(\"https://foo.bar/\")"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"We can now extend the `FunctionRunner` class such that it also measures coverage. After invoking `run()`, the `coverage()` method returns the coverage achieved in the last run."
]
},
{
"cell_type": "code",
"execution_count": 47,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.578378Z",
"iopub.status.busy": "2024-01-18T17:14:25.578206Z",
"iopub.status.idle": "2024-01-18T17:14:25.894243Z",
"shell.execute_reply": "2024-01-18T17:14:25.893868Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"from Coverage import Coverage, population_coverage, Location"
]
},
{
"cell_type": "code",
"execution_count": 48,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.896307Z",
"iopub.status.busy": "2024-01-18T17:14:25.896138Z",
"iopub.status.idle": "2024-01-18T17:14:25.898669Z",
"shell.execute_reply": "2024-01-18T17:14:25.898416Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"source": [
"class FunctionCoverageRunner(FunctionRunner):\n",
" def run_function(self, inp: str) -> Any:\n",
" with Coverage() as cov:\n",
" try:\n",
" result = super().run_function(inp)\n",
" except Exception as exc:\n",
" self._coverage = cov.coverage()\n",
" raise exc\n",
"\n",
" self._coverage = cov.coverage()\n",
" return result\n",
"\n",
" def coverage(self) -> Set[Location]:\n",
" return self._coverage"
]
},
{
"cell_type": "code",
"execution_count": 49,
"metadata": {
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.900133Z",
"iopub.status.busy": "2024-01-18T17:14:25.900022Z",
"iopub.status.idle": "2024-01-18T17:14:25.902464Z",
"shell.execute_reply": "2024-01-18T17:14:25.902197Z"
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"(True, 'PASS')"
]
},
"execution_count": 49,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"http_runner = FunctionCoverageRunner(http_program)\n",
"http_runner.run(\"https://foo.bar/\")"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Here are the first five locations covered: "
]
},
{
"cell_type": "code",
"execution_count": 50,
"metadata": {
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.904288Z",
"iopub.status.busy": "2024-01-18T17:14:25.904168Z",
"iopub.status.idle": "2024-01-18T17:14:25.906179Z",
"shell.execute_reply": "2024-01-18T17:14:25.905855Z"
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('http_program', 3), ('urlparse', 394), ('urlparse', 400), ('_noop', 104), ('urlsplit', 460)]\n"
]
}
],
"source": [
"print(list(http_runner.coverage())[:5])"
]
},
{
"cell_type": "markdown",
"metadata": {
"run_control": {},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Now for the main class. We maintain the population and a set of coverages already achieved (`coverages_seen`). The `fuzz()` helper function takes an input and runs the given `function()` on it. If its coverage is new (i.e. not in `coverages_seen`), the input is added to `population` and the coverage to `coverages_seen`."
]
},
{
"cell_type": "code",
"execution_count": 51,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.907872Z",
"iopub.status.busy": "2024-01-18T17:14:25.907756Z",
"iopub.status.idle": "2024-01-18T17:14:25.910362Z",
"shell.execute_reply": "2024-01-18T17:14:25.910102Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"source": [
"class MutationCoverageFuzzer(MutationFuzzer):\n",
" \"\"\"Fuzz with mutated inputs based on coverage\"\"\"\n",
"\n",
" def reset(self) -> None:\n",
" super().reset()\n",
" self.coverages_seen: Set[frozenset] = set()\n",
" # Now empty; we fill this with seed in the first fuzz runs\n",
" self.population = []\n",
"\n",
" def run(self, runner: FunctionCoverageRunner) -> Any: # type: ignore\n",
" \"\"\"Run function(inp) while tracking coverage.\n",
" If we reach new coverage,\n",
" add inp to population and its coverage to population_coverage\n",
" \"\"\"\n",
" result, outcome = super().run(runner)\n",
" new_coverage = frozenset(runner.coverage())\n",
" if outcome == Runner.PASS and new_coverage not in self.coverages_seen:\n",
" # We have new coverage\n",
" self.population.append(self.inp)\n",
" self.coverages_seen.add(new_coverage)\n",
"\n",
" return result"
]
},
{
"cell_type": "markdown",
"metadata": {
"run_control": {},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Let us now put this to use:"
]
},
{
"cell_type": "code",
"execution_count": 52,
"metadata": {
"execution": {
"iopub.execute_input": "2024-01-18T17:14:25.911859Z",
"iopub.status.busy": "2024-01-18T17:14:25.911749Z",
"iopub.status.idle": "2024-01-18T17:14:26.628028Z",
"shell.execute_reply": "2024-01-18T17:14:26.627727Z"
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"['http://www.google.com/search?q=fuzzing',\n",
" 'http://www.goog.com/search;q=fuzzilng',\n",
" 'http://ww.6goog\\x0eoomosearch;/q=f}zzilng',\n",
" 'http://uv.Lboo.comoseakrch;q=fuzilng',\n",
" 'http://ww.6goog\\x0eo/mosarch;/q=f}z{il~g',\n",
" 'http://www.googme.com/sear#h?q=fuzzing',\n",
" 'http://www.oogcom/sa3rchq=fuzlnv|',\n",
" 'http://ww.6goog*./mosarch;/q=f}Zz{ilel~g',\n",
" 'http://uv.Lboo.comoseakch;q=fuzilng',\n",
" 'http://www.goom^e.2com/s?ear#h?q=fuzzing',\n",
" 'http://hwww.coole.com+search?R=fuzzig',\n",
" 'http://ww.6g7oog*./mosarch; #/q;f}Zz{ilel~gL',\n",
" \"http://ww.6'oog*R./mosarcx;/q=}Zz{ilel;~g\",\n",
" 'http://www.goofme.com/sear#h?q=fuzzi*yng',\n",
" \"http://sw.6'oog*R/msa'rcx;/qw?}Zz{ileRl;~g\",\n",
" \"http://sw.6'oog*R/msa'rsx;/qw?}Zz{ileRUl;~g\",\n",
" \"http://sw.6'oog*R/msa'rsx;qw?}Zz{ileRU;~g\",\n",
" 'http://wgw.gooBm^e.2com/s?&eir#h?q=]fuzzing',\n",
" \"http://sw.6'ooM*R/mDa'rsx;w?}Zz{ileU+~g\",\n",
" \"http://sw.6L'ooM*R/mKD'rwx;w?}Z~{ileU#zg\",\n",
" 'http://ww6g7ooVg:./mosarc; #/q;f}ZzF{ielW~gL',\n",
" \"http://Jsw.6L'oM*R/mKD'r3w;w?~{ileU#zg\",\n",
" \"http://sw.6'oog*R/msa'rsx;/qw?}Z#z{ileRYUl;~g\",\n",
" \"http://sw6'oog*V/msa'rsx;/w\\x7f}Z#zileRUl;~g\",\n",
" \"http://sw6'oog*/msa'rsx;/g\\x7fp}Z#zileRUl;~g\"]"
]
},
"execution_count": 52,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"seed_input = \"http://www.google.com/search?q=fuzzing\"\n",
"mutation_fuzzer = MutationCoverageFuzzer(seed=[seed_input])\n",
"mutation_fuzzer.runs(http_runner, trials=10000)\n",
"mutation_fuzzer.population"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Success! In our population, _each and every input_ now is valid and has a different coverage, coming from various combinations of schemes, paths, queries, and fragments."
]
},
{
"cell_type": "code",
"execution_count": 53,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:26.629695Z",
"iopub.status.busy": "2024-01-18T17:14:26.629584Z",
"iopub.status.idle": "2024-01-18T17:14:26.632755Z",
"shell.execute_reply": "2024-01-18T17:14:26.632449Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [],
"source": [
"all_coverage, cumulative_coverage = population_coverage(\n",
" mutation_fuzzer.population, http_program)"
]
},
{
"cell_type": "code",
"execution_count": 54,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:26.634189Z",
"iopub.status.busy": "2024-01-18T17:14:26.634104Z",
"iopub.status.idle": "2024-01-18T17:14:26.635771Z",
"shell.execute_reply": "2024-01-18T17:14:26.635517Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt # type: ignore"
]
},
{
"cell_type": "code",
"execution_count": 55,
"metadata": {
"button": false,
"execution": {
"iopub.execute_input": "2024-01-18T17:14:26.637184Z",
"iopub.status.busy": "2024-01-18T17:14:26.637104Z",
"iopub.status.idle": "2024-01-18T17:14:26.746285Z",
"shell.execute_reply": "2024-01-18T17:14:26.745920Z"
},
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"image/png": "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qBH69esHKysrfP/99xg7dixUKhXGjRundbzExEQMHToUY8aMwahRo9CgQQOcPXsWffr0QZMmTTBv3jwoFApcunQJ8fHxmvvpcu4PHTqE5557rtzn4+LigqCgIMTHx2PSpEmPfd7h4eFYtmwZzp49i8aNGwMADhw4AAsLCxw4cADR0dGabQDQsWPHco8zZswYpKamYvfu3fj000/L3eeLL75ATk4OxowZA0mSsHjxYrz44ou4fPlyhVohxo4dC09PT8yaNQv37t0DUNq1eejQIQwZMgS1atXClStXsHr1anTu3Bnnzp0r07I5YcIE1KhRA7Nnz8aVK1cQGxuL8ePH46uvvtLsM2vWLMyfPx+9evVCr169cPLkSXTr1g2FhYVax7px4wbatWuHvLw8REdHw93dHRs3bkS/fv2wZcsWDBgwQGv/mJgY2NnZYfr06bh06RJWrlwJa2trWFhY4O7du5gzZw6OHDmCDRs2IDAwELNmzXrqOanKeS4pKUGPHj3Qpk0bLF68GHFxcZg9ezaKi4sxb948nR7zSa//7t27MXToUHTt2hWLFi0CAJw/fx7x8fFa/ziSHgmq1rKysgQA0b9//wrtn5CQIACIkSNHam2fMmWKACB++eUXzXEVCoV46623tPZbvHixkCRJXL16VQghxJUrV4SlpaX497//rbXfH3/8IaysrLS2d+rUSQAQa9asKVNXXl5emW1jxowR9vb2Ij8/XwghREFBgXB3dxetWrUSRUVFmv02bNggAIhOnTpptn366afCwsJCHDhwQOuYa9asEQBEfHz8Y89RYWGh8PLyEo0bNxb379/XbP/hhx8EADFr1izNtvXr1wsA4rfffnvs8dSioqKEv79/me2zZ88Wj/4qAxAWFhbi7NmzZfYHIGbPnl3m/v369dPab+zYsQKAOH36tGZbeee5e/fuok6dOlrb/P39BQARFxentX3ZsmUCgLh169Zjn2dFz31RUZGQJKnMe+xh3bp1EyEhIY+9XQghbt68KQCIjz76SAghRGZmprCwsBCDBg0S3t7emv2io6OFm5ubUKlUQgghUlJSBACxfv16zT7jxo0r81o8vK+7u7u4c+eOZvt3330nAIjvv//+iTWq3ycdOnQQxcXFWreV95ocPnxYABCffPJJmWNERERonoMQQkyaNElYWlqKzMxMzfmwsbERvXv31tpv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QJIkqFQqLF68GM8//7xeizNnHFBMRERkGDp3Sy1evBhdu3bF8ePHUVhYiLfffhtnz57FnTt3EB8fb4gazU5hsQq3czmBHxERkSHo3HLTuHFjXLx4ER06dED//v1x7949vPjiizh16hSCgoIMUaPZuZGdDyEAG0sLuNnbyF0OERGRWdG55QYAXFxc8M477+i7lmpDvWCmjwsn8CMiItK3SoWbzMxMHDt2DDdv3oRKpdK6bfjw4XopzJxxMDEREZHh6Bxuvv/+ewwbNgy5ublwdnaGJD1oeZAkieGmAtI0g4kZboiIiPRN5zE3b731Fl577TXk5uYiMzMTd+/e1XzduXPHEDWaHfUcN76uvFKKiIhI33QON3/99Reio6PLneuGKib170UzfdlyQ0REpHc6h5vu3bvj+PHjhqil2lB3S/lyjhsiIiK903nMTe/evTF16lScO3cOoaGhsLa21rq9X79+eivOXD0IN2y5ISIi0jedw82oUaMAAPPmzStzmyRJKCkpqXpVZqyguOTBBH4MN0RERHqnc7h59NJv0s2NrNJgY2NlATcHTuBHRESkbzqPuaGqUc9x4+tiq3UZPREREelHhVpuVqxYgdGjR8PW1hYrVqx44r7R0dF6KcxccbwNERGRYVUo3CxbtgzDhg2Dra0tli1b9tj9JEliuHmKNK4GTkREZFAVCjcpKSnlfk+649ILREREhsUxN0aWmsnZiYmIiAypQi03kydPrvABly5dWuliqoP07NKWG64rRUREZBgVCjenTp2q0MF49c/Tpf3dcsNuKSIiIsOoULjZu3evoeuoFvKLSpBxrxAABxQTEREZCsfcGNGN7NJWG4WVBVztrZ+yNxEREVUGw40RqQcTK13t2IVHRERkIAw3RqQeTMwJ/IiIiAyH4caIUjmYmIiIyOAYboxIPYEfBxMTEREZDsONEaWr15VyZcsNERGRoTDcGJFmdmJ2SxERERkMw40RqbulfNktRUREZDAMN0aSX1SCu3lFADjmhoiIyJAYbowk7e/xNnbWlnC2q9DE0ERERFQJDDdGoumScrXlBH5EREQGxHBjJGkcTExERGQUDDdGwsHERERExsFwYyTqMTdKttwQEREZFMONkajDjQ9bboiIiAyK4cZIUjMfDCgmIiIiw5E13MyZMweSJGl9BQcHa+1z+PBhdOnSBQ4ODnB2dkbH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9uzB7t27MXbsWHzwwQfYv39/mZqIyHjYckNEFbJjxw4kJCTAx8cHn332GRISEtC4cWPExsYiISEBO3bseOx9nZ2doVQqER8fr7U9Pj4eDRs2NHTpAIAjR45ovi8uLsaJEycQEhICAPD09EROTg7u3bun2efRS75tbGxQUlJS5rh2dnbo27cvVqxYgX379uHw4cP4448/DPMkiKhC2HJDRBXi7++P9PR03LhxA/3794ckSTh79iwGDhwIX1/fp95/6tSpmD17NoKCgtCsWTOsX78eCQkJ+Pzzz41QPbBq1SrUq1cPISEhWLZsGe7evYvXXnsNABAWFgZ7e3vMnDkT0dHROHr0KDZs2KB1/4CAAKSkpCAhIQG1atWCk5MTvvzyS5SUlGju/9lnn8HOzu6Jl7ITkeGx5YaIKmzfvn1o1aoVbG1tcezYMdSqVatCwQYAoqOjMXnyZLz11lsIDQ1FXFwctm/fjnr16hm46lILFy7EwoUL0bRpUxw8eBDbt2/XjBFyc3PDZ599hh07diA0NBRffvllmUvRBw4ciB49euD555+Hp6cnvvzyS7i6umLdunVo3749mjRpgj179uD777+v0BVYRGQ4nKGYiMzalStXEBgYiFOnTj12SQciMi9suSEiIiKzwnBDREREZoXdUkRERGRW2HJDREREZoXhhoiIiMwKww0RERGZFYYbIiIiMisMN0RERGRWGG6IiIjIrDDcEBERkVlhuCEiIiKzwnBDREREZuX/Aem7iNwlxpCvAAAAAElFTkSuQmCC\n",
"text/plain": [
"