{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"# Greybox Fuzzing with Grammars\n",
"\n",
"In this chapter, we introduce important extensions to our syntactic fuzzing techniques, all leveraging _syntactic_ parts of _existing inputs_.\n",
"\n",
"1. We show how to leverage _dictionaries_ of input fragments during fuzzing. The idea is to integrate such dictionaries into a _mutator_, which would then inject these fragments (typically keywords and other items of importance) into population.\n",
"\n",
"2. We show how to combine [parsing](Parser.ipynb) and [fuzzing](GrammarFuzzer.ipynb) with grammars. This allows to _mutate_ existing inputs while preserving syntactical correctness, and to _reuse_ fragments from existing inputs while generating new ones. The combination of language-based parsing and generating, as demonstrated in this chapter, has been highly successful in practice: The _LangFuzz_ fuzzer for JavaScript has found more than 2,600 bugs in JavaScript interpreters this way.\n",
"\n",
"3. In the previous chapters, we have used grammars in a _black-box_ manner – that is, we have used them to generate inputs regardless of the program being tested. In this chapter, we introduce mutational _greybox fuzzing with grammars_: Techniques that make use of _feedback from the program under test_ to guide test generations towards specific goals. As in [lexical greybox fuzzing](GreyboxFuzzer.ipynb), this feedback is mostly _coverage_, allowing us to direct grammar-based testing towards uncovered code parts. This part is inspired by the _AFLSmart_ fuzzer, which combines parsing and mutational fuzzing."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:32.267797Z",
"iopub.status.busy": "2022-01-24T09:56:32.267041Z",
"iopub.status.idle": "2022-01-24T09:56:32.397852Z",
"shell.execute_reply": "2022-01-24T09:56:32.398308Z"
},
"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(\"JPSfCCkuNo0\")"
]
},
{
"cell_type": "markdown",
"metadata": {
"button": false,
"new_sheet": false,
"run_control": {
"read_only": false
},
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"**Prerequisites**\n",
"\n",
"* We build on several concepts from [the chapter on greybox fuzzing (without grammars)](GreyboxFuzzer.ipynb).\n",
"* As the title suggests, you should know how to fuzz with grammars [from the chapter on grammars](Grammars.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.GreyboxGrammarFuzzer import \n",
"```\n",
"\n",
"and then make use of the following features.\n",
"\n",
"\n",
"This chapter introduces advanced methods for language-based grey-box fuzzing inspired by the _LangFuzz_ and _AFLSmart_ fuzzers.\n",
"\n",
"### Fuzzing with Dictionaries\n",
"\n",
"Rather than mutating strings randomly, the `DictMutator` class allows to insert tokens from a dictionary, thus increasing fuzzer performance. The dictionary comes as a list of strings, out of which random elements are picked and inserted - in addition to the given mutations such as deleting or inserting individual bytes.\n",
"\n",
"```python\n",
">>> dict_mutator = DictMutator([\"\", \"\", \"\", \"='a'\"])\n",
">>> seeds = [\"HelloWorld \"]\n",
">>> for i in range(10):\n",
">>> print(dict_mutator.mutate(seeds[0]))\n",
"HelloWorld ='a'body>\n",
"HelloWorld \n",
"<(tml>HelloWorld \n",
"HelloWorld \n",
"World \n",
"HelloWorld \n",
"HlloWorld \n",
"HelloWorld \n",
"HelloWorhd \n",
"HelloWorld \n",
"\n",
"```\n",
"This `DictMutator` can be used as an argument to `GreyboxFuzzer`:\n",
"\n",
"```python\n",
">>> runner = FunctionCoverageRunner(my_parser)\n",
">>> dict_fuzzer = GreyboxFuzzer(seeds, dict_mutator, PowerSchedule())\n",
">>> dict_fuzzer_outcome = dict_fuzzer.runs(runner, trials=5)\n",
"```\n",
"![](PICS/GreyboxGrammarFuzzer-synopsis-1.svg)\n",
"\n",
"### Fuzzing with Input Fragments\n",
"\n",
"The `LangFuzzer` class introduces a _language-aware_ fuzzer that can recombine fragments from existing inputs – inspired by the highly effective `LangFuzz` fuzzer. At its core is a `FragmentMutator` class that that takes a [_parser_](Parser.ipynb) as argument:\n",
"\n",
"```python\n",
">>> parser = EarleyParser(XML_GRAMMAR, tokens=XML_TOKENS)\n",
">>> mutator = FragmentMutator(parser)\n",
"```\n",
"The fuzzer itself is initialized with a list of seeds, the above `FragmentMutator`, and a power schedule:\n",
"\n",
"```python\n",
">>> seeds = [\"HelloWorld \"]\n",
">>> schedule = PowerSchedule()\n",
">>> lang_fuzzer = LangFuzzer(seeds, mutator, schedule)\n",
">>> for i in range(10):\n",
">>> print(lang_fuzzer.fuzz())\n",
"HelloWorld \n",
"HelloWorld \n",
"HelloWorld \n",
"HelloWorld \n",
"HelloWorld \n",
"HelloWorld \n",
"HelloWorld \n",
"HelloWorld \n",
"HelloWorld \n",
"HelloWorld \n",
"\n",
"```\n",
"![](PICS/GreyboxGrammarFuzzer-synopsis-2.svg)\n",
"\n",
"### Fuzzing with Input Regions\n",
"\n",
"The `GreyboxGrammarFuzzer` class uses two mutators:\n",
"* a _tree mutator_ (a `RegionMutator` object) that can parse existing strings to identify _regions_ in that string to be swapped or deleted.\n",
"* a _byte mutator_ to apply bit- and character-level mutations.\n",
"\n",
"```python\n",
">>> tree_mutator = RegionMutator(parser)\n",
">>> byte_mutator = Mutator()\n",
"```\n",
"The _schedule_ for the `GreyboxGrammarFuzzer` class can be a regular `PowerSchedule` object. However, a more sophisticated schedule is provided by `AFLSmartSchedule`, which assigns more [energy](GreyboxFuzzer.ipynb#Power-Schedules) to seeds that have a higher degree of validity.\n",
"\n",
"```python\n",
">>> schedule = AFLSmartSchedule(parser)\n",
"```\n",
"The `GreyboxGrammarFuzzer` constructor takes a set of seeds as well as the two mutators and the schedule:\n",
"\n",
"```python\n",
">>> aflsmart_fuzzer = GreyboxGrammarFuzzer(seeds, byte_mutator, tree_mutator, schedule)\n",
"```\n",
"As it relies on code coverage, it is typically combined with a `FunctionCoverageRunner`:\n",
"\n",
"```python\n",
">>> runner = FunctionCoverageRunner(my_parser)\n",
">>> aflsmart_outcome = aflsmart_fuzzer.runs(runner, trials=5)\n",
"```\n",
"![](PICS/GreyboxGrammarFuzzer-synopsis-3.svg)\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"## Background\n",
"First, we [recall](GreyboxFuzzer.ipynb#Ingredients-for-Greybox-Fuzzing) a few basic ingredients for mutational fuzzers.\n",
"* **Seed**. A _seed_ is an input that is used by the fuzzer to generate new inputs by applying a sequence of mutations.\n",
"* **Mutator**. A _mutator_ implements a set of mutation operators that applied to an input produce a slightly modified input.\n",
"* **PowerSchedule**. A _power schedule_ assigns _energy_ to a seed. A seed with higher energy is fuzzed more often throughout the fuzzing campaign.\n",
"* **AdvancedMutationFuzzer**. Our _mutational blackbox fuzzer_ generates inputs by mutating seeds in an initial population of inputs.\n",
"* **GreyboxFuzzer**. Our _greybox fuzzer_ dynamically adds inputs to the population of seeds that increased coverage.\n",
"* **FunctionCoverageRunner**. Our _function coverage runner_ collects coverage information for the execution of a given Python function.\n",
"\n",
"Let's try to get a feeling for these concepts."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:32.403574Z",
"iopub.status.busy": "2022-01-24T09:56:32.402653Z",
"iopub.status.idle": "2022-01-24T09:56:32.404600Z",
"shell.execute_reply": "2022-01-24T09:56:32.405133Z"
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"import bookutils"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:32.408864Z",
"iopub.status.busy": "2022-01-24T09:56:32.408172Z",
"iopub.status.idle": "2022-01-24T09:56:32.410266Z",
"shell.execute_reply": "2022-01-24T09:56:32.410933Z"
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"from typing import List, Set, Dict, Sequence, cast"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:32.415706Z",
"iopub.status.busy": "2022-01-24T09:56:32.414822Z",
"iopub.status.idle": "2022-01-24T09:56:34.806458Z",
"shell.execute_reply": "2022-01-24T09:56:34.806903Z"
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"from Fuzzer import Fuzzer\n",
"from GreyboxFuzzer import Mutator, Seed, PowerSchedule\n",
"from GreyboxFuzzer import AdvancedMutationFuzzer, GreyboxFuzzer\n",
"from MutationFuzzer import FunctionCoverageRunner"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"The following command applies a mutation to the input \"Hello World\"."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:34.811882Z",
"iopub.status.busy": "2022-01-24T09:56:34.810995Z",
"iopub.status.idle": "2022-01-24T09:56:34.814523Z",
"shell.execute_reply": "2022-01-24T09:56:34.814954Z"
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"'Lello World'"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"Mutator().mutate(\"Hello World\")"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"The default power schedule assigns energy uniformly across all seeds. Let's check whether this works.\n",
"\n",
"We choose 10k times from a population of three seeds. As we see in the `hits` counter, each seed is chosen about a third of the time."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:34.863487Z",
"iopub.status.busy": "2022-01-24T09:56:34.862718Z",
"iopub.status.idle": "2022-01-24T09:56:34.866315Z",
"shell.execute_reply": "2022-01-24T09:56:34.866775Z"
},
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"{'A': 3387, 'B': 3255, 'C': 3358}"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"population = [Seed(\"A\"), Seed(\"B\"), Seed(\"C\")]\n",
"schedule = PowerSchedule()\n",
"hits = {\n",
" \"A\": 0,\n",
" \"B\": 0,\n",
" \"C\": 0\n",
"}\n",
"\n",
"for i in range(10000):\n",
" seed = schedule.choose(population)\n",
" hits[seed.data] += 1\n",
"\n",
"hits"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Before explaining the function coverage runner, lets import Python's HTML parser as example..."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:34.871310Z",
"iopub.status.busy": "2022-01-24T09:56:34.870398Z",
"iopub.status.idle": "2022-01-24T09:56:34.873286Z",
"shell.execute_reply": "2022-01-24T09:56:34.872633Z"
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"from html.parser import HTMLParser"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"... and create a _wrapper function_ that passes each input into a new parser object."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:34.877558Z",
"iopub.status.busy": "2022-01-24T09:56:34.876984Z",
"iopub.status.idle": "2022-01-24T09:56:34.878874Z",
"shell.execute_reply": "2022-01-24T09:56:34.879285Z"
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"source": [
"def my_parser(inp: str) -> None:\n",
" parser = HTMLParser()\n",
" parser.feed(inp)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"The `FunctionCoverageRunner` constructor takes a Python `function` to execute. The function `run()` takes an input, passes it on to the Python `function`, and collects the coverage information for this execution. The function `coverage()` returns a list of tuples `(function name, line number)` for each statement that has been covered in the Python `function`."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:34.884970Z",
"iopub.status.busy": "2022-01-24T09:56:34.884235Z",
"iopub.status.idle": "2022-01-24T09:56:34.887150Z",
"shell.execute_reply": "2022-01-24T09:56:34.887644Z"
},
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"[('goahead', 140),\n",
" ('__init__', 93),\n",
" ('unescape', 130),\n",
" ('updatepos', 49),\n",
" ('updatepos', 52)]"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"runner = FunctionCoverageRunner(my_parser)\n",
"runner.run(\"Hello World\")\n",
"cov = runner.coverage()\n",
"\n",
"list(cov)[:5] # Print 5 statements covered in HTMLParser"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"Our greybox fuzzer takes a seed population, mutator, and power schedule. Let's generate 5000 fuzz inputs starting with an \"empty\" seed corpus."
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:34.891636Z",
"iopub.status.busy": "2022-01-24T09:56:34.890940Z",
"iopub.status.idle": "2022-01-24T09:56:34.892899Z",
"shell.execute_reply": "2022-01-24T09:56:34.893316Z"
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"import time\n",
"import random"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:34.898344Z",
"iopub.status.busy": "2022-01-24T09:56:34.897431Z",
"iopub.status.idle": "2022-01-24T09:56:34.899613Z",
"shell.execute_reply": "2022-01-24T09:56:34.900199Z"
},
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [],
"source": [
"n = 5000\n",
"seed_input = \" \" # empty seed\n",
"runner = FunctionCoverageRunner(my_parser)\n",
"fuzzer = GreyboxFuzzer([seed_input], Mutator(), PowerSchedule())"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:34.985775Z",
"iopub.status.busy": "2022-01-24T09:56:34.940972Z",
"iopub.status.idle": "2022-01-24T09:56:35.915364Z",
"shell.execute_reply": "2022-01-24T09:56:35.915805Z"
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [],
"source": [
"start = time.time()\n",
"fuzzer.runs(runner, trials=n)\n",
"end = time.time()"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:35.921141Z",
"iopub.status.busy": "2022-01-24T09:56:35.920263Z",
"iopub.status.idle": "2022-01-24T09:56:35.923665Z",
"shell.execute_reply": "2022-01-24T09:56:35.924079Z"
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"'It took the fuzzer 1.01 seconds to generate and execute 5000 inputs.'"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"\"It took the fuzzer %0.2f seconds to generate and execute %d inputs.\" % (end - start, n)"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:35.928383Z",
"iopub.status.busy": "2022-01-24T09:56:35.927706Z",
"iopub.status.idle": "2022-01-24T09:56:35.930286Z",
"shell.execute_reply": "2022-01-24T09:56:35.930689Z"
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"'During this fuzzing campaign, we covered 40 statements.'"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"\"During this fuzzing campaign, we covered %d statements.\" % len(runner.coverage())"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"## Fuzzing with Dictionaries\n",
"\n",
"To fuzz our HTML parser, it may be useful to inform a mutational fuzzer about important keywords in the input – that is, important HTML keywords. The general idea is to have a _dictionary_ of pre-defined useful inputs that could then be inserted as such when mutating an input.\n",
"\n",
"This concept is illustrated in the following diagram. When mutating an input, we may insert given keywords from the dictionary (in red).\n",
"\n",
"![](PICS/GreyboxGrammarFuzzer-Dict.gif)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"To implement this concept, we extend our mutator to consider keywords from a dictionary."
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:35.936482Z",
"iopub.status.busy": "2022-01-24T09:56:35.935712Z",
"iopub.status.idle": "2022-01-24T09:56:35.937963Z",
"shell.execute_reply": "2022-01-24T09:56:35.938473Z"
},
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [],
"source": [
"class DictMutator(Mutator):\n",
" \"\"\"Mutate strings using keywords from a dictionary\"\"\"\n",
"\n",
" def __init__(self, dictionary: List[str]) -> None:\n",
" \"\"\"Constructor. `dictionary` is the list of keywords to use.\"\"\"\n",
" super().__init__()\n",
" self.dictionary = dictionary\n",
" self.mutators.append(self.insert_from_dictionary)\n",
"\n",
" def insert_from_dictionary(self, s: str) -> str:\n",
" \"\"\"Returns s with a keyword from the dictionary inserted\"\"\"\n",
" pos = random.randint(0, len(s))\n",
" random_keyword = random.choice(self.dictionary)\n",
" return s[:pos] + random_keyword + s[pos:]"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Let's try to add a few HTML tags and attributes and see whether the coverage with `DictMutator` increases."
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:36.000209Z",
"iopub.status.busy": "2022-01-24T09:56:35.958284Z",
"iopub.status.idle": "2022-01-24T09:56:41.729946Z",
"shell.execute_reply": "2022-01-24T09:56:41.730435Z"
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"'It took the fuzzer 5.78 seconds to generate and execute 5000 inputs.'"
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"runner = FunctionCoverageRunner(my_parser)\n",
"dict_mutator = DictMutator([\"\", \"\", \"\", \"='a'\"])\n",
"dict_fuzzer = GreyboxFuzzer([seed_input], dict_mutator, PowerSchedule())\n",
"\n",
"start = time.time()\n",
"dict_fuzzer.runs(runner, trials=n)\n",
"end = time.time()\n",
"\n",
"\"It took the fuzzer %0.2f seconds to generate and execute %d inputs.\" % (end - start, n)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"Clearly, it takes longer. In our experience, this means more code is covered:"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:41.735803Z",
"iopub.status.busy": "2022-01-24T09:56:41.734848Z",
"iopub.status.idle": "2022-01-24T09:56:41.738060Z",
"shell.execute_reply": "2022-01-24T09:56:41.738487Z"
},
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"'During this fuzzing campaign, we covered 88 statements.'"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"\"During this fuzzing campaign, we covered %d statements.\" % len(runner.coverage())"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"How do the fuzzers compare in terms of coverage over time?"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:41.742438Z",
"iopub.status.busy": "2022-01-24T09:56:41.741843Z",
"iopub.status.idle": "2022-01-24T09:56:41.744468Z",
"shell.execute_reply": "2022-01-24T09:56:41.745139Z"
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"from Coverage import population_coverage"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:41.749972Z",
"iopub.status.busy": "2022-01-24T09:56:41.749131Z",
"iopub.status.idle": "2022-01-24T09:56:41.751410Z",
"shell.execute_reply": "2022-01-24T09:56:41.751900Z"
},
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt # type: ignore"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {
"execution": {
"iopub.execute_input": "2022-01-24T09:56:41.836435Z",
"iopub.status.busy": "2022-01-24T09:56:41.796053Z",
"iopub.status.idle": "2022-01-24T09:56:46.233383Z",
"shell.execute_reply": "2022-01-24T09:56:46.234027Z"
},
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"image/png": "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ekFBAfPnz+fVV1/lzjvv5I033qgwb3369OHTTz/9TvqECRM47bTTeOmllygsLGT37t1MmjSJpUuXsnjxYiCoHivy8MMPY2YsWbKETz/9lGHDhvH5558DsHjxYhYtWkS9evXo3Lkz1113HW3btuXuu++madOmFBYWMnToUD7++GN69OgBQLNmzYoDxRtvvMHixYvp1asXTz31FKNHp/CPrbZ46RrI+yDVuch87uARaNEJ2pRdFZ1pEhZELChyPAEsd/fJUZtmAVcAk8L3l6PSf2ZmzxI0qO9It/aQIgMGDGDevHnMmzePm266ifXr1zNv3jwaNWrEwIEDYzrGeeedB8BJJ510yA2+PGXNDTN79myeeeYZALKzs2nUqBHbtm0r8zjvvPMO110XTAHepUsXjjnmmOIgMnToUBo1CqbU7tq1K2vXrqVt27bMmDGDKVOmUFBQwMaNG1m2bFlxELn44ouLjz1mzBieeuopJk+ezHPPPcf8+fNj+m5SBZ+8BI2PgaO6pTonma/NydD5R6nORVIlsiQyEPg3YImZLQ7T/oMgeMwwsyuBtcBF4bZXgbOAlcAeoMo/UcsrMSTSwIEDmTdvHkuWLKFbt260bduW3//+9xxxxBEx//KuV68eENz0Cwpim5F10aJFnHDCCXHnOxZF+YKDefviiy+47777+OCDD2jSpAmjRo065DmNww8/vHj5/PPP58477+QHP/gBJ510Es2aNUtofmu1vAWwYREU7IPuF8Jpt6Y6R5KBElZx5u7vuLu5ew937xW+XnX3Le4+1N07uvsP3X1ruL+7+7Xufpy7d3f3tB3jfcCAAbzyyis0bdqU7OxsmjZtyvbt23n33XcZMGDAd/bPzc1l165dVTrnmjVruOWWW4pLENGGDh3KI488AgSN2zt27Cj3nIMGDWLatGkAfP7553z55Zd07ty5zHPv3LmTww8/nEaNGvH111/z2muvlblv/fr1GT58OOPGjVNVVqL97Sp49ZZguame3pfEyLgn1muC7t27s3nzZk499dRD0ho1akTz5s2/s/8ll1zC7373O3r37s2qVatiPs+qVavo3bs3J5xwAhdddBETJkwo9cb8wAMPMGfOHLp3785JJ53EsmXLaNasGQMHDqRbt27ceuuhv1DHjx9PJBKhe/fuXHzxxUydOvWQEkhJPXv2pHfv3nTp0oVLL720wiq7yy67jKysLIYNGxbzd5U47N8NPS6GW1dD9wsq3l8kDmk9x3rfvn295KRUy5cvT3iVjlTNfffdx44dO7jrrrsSdg79PwAmtYMel8BZ96Y6J1LDmNlCd6+W1v+MG4BRarZzzz2XVatWMXv27FRnJfMVHICcuhXvJ1IFCiKSVC+99FKqs1Cz7d4EHzwOhflVP1bBPsguuxpSpDooiIjUJMtehn/eA1k5VHnYipx60DI1PRSl9lAQEalJIoXB+y0roEHT1OZFJAbqnSVSo6RvRxepnRRERGoSjwTvpj9NSQ/6n1rNbrzxRu6///7i9eHDhzNmzJji9ZtvvpnJkycza9YsJk2aBASj+C5btqx4n8GDB1Oy63K8fvOb35S5rX379nTv3p3u3bvTtWtXbr/99uInzTds2HDIWF6luf/++9mzZ0/x+llnncX27durJd+1VlGXe02sJWlCQaSaFQ15AhQPBf/JJ58Ub583b17xoItFc32UDCLVqbwgAjBnzhyWLFnC/PnzWb16NVdffTUQ29DwJYPIq6++SuPGjauc57LEOvxLWlNJRNKM/qdWswEDBvDuu+8CB4eCz83NZdu2bezfv/+QoeB/9rOfMW/ePGbNmsWtt95Kr169ip9Yf/755znllFPo1KkTb7/9NhDMHz969Gi6d+9O7969mTNnDkDxsYqMGDGCuXPnMnHiRPbu3UuvXr247LLLys13w4YNefTRR5k5cyZbt25lzZo1dOsWDNhXWFjILbfcQrdu3ejRowd//OMfefDBB9mwYQNDhgxhyJAhQFCy2bx5MwCTJ0+mW7dudOvWrbhkVjSR1lVXXcWJJ57IsGHD2Lt3LwB//vOfOfnkk+nZsyfnn39+cXAaNWoU11xzDf369eO2226jY8eOfPPNN0AQpI8//vji9cxQ1Caikoikh8zunfXaRPhqSfUe86jucOakMjcncij48oZoL82kSZN46KGHiod7r8gRRxxBhw4dWLFiBS1btixOnzJlCmvWrGHx4sXk5OSwdetWmjZtyuTJk5kzZ853hnJZuHAhTz31FO+//z7uTr9+/TjttNNo0qQJK1asYPr06fz5z3/moosu4sUXX+Tyyy/nvPPO46qrrgLg9ttv54knnigeBywvL4958+YVj0A8bdo0brjhBt544w169uxJixYtYvp+aaG4Oku/7yQ96H9qAkQP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\n",
"text/plain": [
"