Skip to content

Commit ef77482

Browse files
authored
Refactor analysis module and backends (#27)
* Fix plugin settings * Remove unused code in analysis module * Refactor metrics with a schema dataclass * Fix minor bugs in frontend CLI and binary ninja
1 parent a6e2856 commit ef77482

File tree

9 files changed

+99
-67
lines changed

9 files changed

+99
-67
lines changed

__init__.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,26 @@
2727
"""
2828
{
2929
"title" : "Skip Stripped Symbols",
30-
"description" : "Ignore stripped symbols",
30+
"description" : "Ignore stripped symbols.",
3131
"type" : "boolean",
3232
"default" : false
3333
}
3434
""",
3535
)
3636

37-
# TODO: DEFAULT_SCORE_WEIGHTS
3837
Settings().register_setting(
3938
"fuzzable.score_weights",
4039
"""
4140
{
4241
"title" : "Override Score Weights",
43-
"description" : "Reset",
42+
"description" : "Change default score weights for each metric.",
4443
"type" : "array",
4544
"elementType" : "string",
46-
"default" : [0.3, 0.3, 0.05, 0.05, 0.3]
45+
"default" : {}
4746
}
48-
""",
47+
""".format(
48+
DEFAULT_SCORE_WEIGHTS
49+
),
4950
)
5051

5152
PluginCommand.register(

fuzzable/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def analyze(
6363
if debug:
6464
log.setLevel(logging.DEBUG)
6565

66-
if not target.is_file() or target.is_dir():
66+
if not target.is_file() and not target.is_dir():
6767
error(f"Target path `{target}` does not exist.")
6868

6969
try:

fuzzable/analysis/__init__.py

Lines changed: 16 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,10 @@
88
import enum
99
import typing as t
1010

11-
SCIKIT = True
12-
try:
13-
import skcriteria as skc
14-
from skcriteria.madm import simple
15-
except Exception:
16-
SCIKIT = False
17-
18-
from ..metrics import CallScore
11+
import skcriteria as skc
12+
from skcriteria.madm import simple
13+
14+
from ..metrics import CallScore, METRICS
1915
from ..config import INTERESTING_PATTERNS, RISKY_GLIBC_CALL_PATTERNS
2016

2117
# Type sig for a finalized list
@@ -81,11 +77,6 @@ def _rank_fuzzability(self, unranked: t.List[CallScore]) -> Fuzzability:
8177
This should be the tail call for run, as it produces the finalized results
8278
"""
8379

84-
# TODO: deprecate this.
85-
if not SCIKIT:
86-
return self._rank_simple_fuzzability(unranked)
87-
88-
# normalize
8980
nl_normalized = AnalysisBackend._normalize(
9081
[score.natural_loops for score in unranked]
9182
)
@@ -108,13 +99,7 @@ def _rank_fuzzability(self, unranked: t.List[CallScore]) -> Fuzzability:
10899
objectives,
109100
weights=self.score_weights,
110101
alternatives=names,
111-
criteria=[
112-
"fuzz_friendly",
113-
"sinks",
114-
"loop",
115-
"coverage",
116-
"cyclomatic_complexity",
117-
],
102+
criteria=[metric.identifier for metric in METRICS[3:8]],
118103
)
119104

120105
dec = simple.WeightedSumModel()
@@ -135,19 +120,9 @@ def _rank_fuzzability(self, unranked: t.List[CallScore]) -> Fuzzability:
135120
sorted_results = [y for _, y in sorted(zip(ranks, new_unranked))]
136121
return sorted_results
137122

138-
def _rank_simple_fuzzability(self, unranked: t.List[CallScore]) -> Fuzzability:
139-
nl_normalized = AnalysisBackend._normalize(
140-
[score.natural_loops for score in unranked]
141-
)
142-
for score, new_nl in zip(unranked, nl_normalized):
143-
score.natural_loops = new_nl
144-
145-
cc_normalized = AnalysisBackend._normalize(
146-
[score.cyclomatic_complexity for score in unranked]
147-
)
148-
for score, new_cc in zip(unranked, cc_normalized):
149-
score.cyclomatic_complexity = new_cc
150-
123+
@staticmethod
124+
def _rank_simple_fuzzability(unranked: t.List[CallScore]) -> Fuzzability:
125+
"""Not used anymore."""
151126
return sorted(unranked, key=lambda obj: obj.simple_fuzzability, reverse=True)
152127

153128
@staticmethod
@@ -201,7 +176,8 @@ def is_toplevel_call(self, target: t.Any) -> bool:
201176
@abc.abstractmethod
202177
def risky_sinks(self, func: t.Any) -> int:
203178
"""
204-
HEURISTIC
179+
FUZZABILITY HEURISTIC
180+
205181
Checks to see if one or more of the function's arguments is
206182
potentially user-controlled, and flows into an abusable call.
207183
"""
@@ -215,7 +191,8 @@ def _is_risky_call(name: str) -> bool:
215191
@abc.abstractmethod
216192
def get_coverage_depth(self, func: t.Any) -> int:
217193
"""
218-
HEURISTIC
194+
FUZZABILITY HEURISTIC
195+
219196
Calculates and returns a `CoverageReport` that highlights how much
220197
a fuzzer would ideally explore at different granularities.
221198
"""
@@ -224,7 +201,8 @@ def get_coverage_depth(self, func: t.Any) -> int:
224201
@abc.abstractmethod
225202
def natural_loops(self, func: t.Any) -> int:
226203
"""
227-
HEURISTIC
204+
FUZZABILITY HEURISTIC
205+
228206
Detection of loops is at a basic block level by checking the dominance frontier,
229207
which denotes the next successor the current block node will definitely reach. If the
230208
same basic block exists in the dominance frontier set, then that means the block will
@@ -235,7 +213,8 @@ def natural_loops(self, func: t.Any) -> int:
235213
@abc.abstractmethod
236214
def get_cyclomatic_complexity(self) -> int:
237215
"""
238-
HEURISTIC
216+
FUZZABILITY HEURISTIC
217+
239218
Calculates the complexity of a given function using McCabe's metric. We do not
240219
account for connected components since we assume that the target is a singular
241220
connected component.

fuzzable/analysis/binja.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@
2424

2525
from .. import generate
2626
from . import AnalysisBackend, AnalysisMode, Fuzzability, DEFAULT_SCORE_WEIGHTS
27-
from ..metrics import CallScore
28-
from ..cli import COLUMNS, CSV_HEADER
27+
from ..metrics import CallScore, METRICS
2928

3029

3130
class _BinjaAnalysisMeta(type(AnalysisBackend), type(BackgroundTaskThread)):
@@ -87,8 +86,10 @@ def run(self) -> t.Optional[Fuzzability]:
8786

8887
# if headless, handle displaying results back
8988
if not self.headless:
90-
csv_result = CSV_HEADER
91-
csv_result = ", ".join([f'"{column}"' for column in COLUMNS])
89+
csv_result = ",".join([metric.identifier for metric in METRICS])
90+
91+
columns = [metric.friendly_name for metric in METRICS]
92+
csv_result = ", ".join([f'"{column}"' for column in columns])
9293

9394
# TODO: reuse rich for markdown
9495
markdown_result = f"""# Fuzzable Targets

fuzzable/cli.py

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
from rich.console import Console
1313
from rich.table import Table
1414

15-
from .analysis import Fuzzability
15+
from .analysis import Fuzzability, CallScore
16+
from .metrics import METRICS
1617
from .log import log
1718

1819
from pathlib import Path
@@ -23,20 +24,6 @@
2324
bg=typer.colors.RED,
2425
)
2526

26-
COLUMNS = [
27-
"Function Signature",
28-
"Location",
29-
"Fuzzability Score",
30-
"Fuzz-Friendly Name",
31-
"Risky Data Sinks",
32-
"Natural Loops",
33-
"Cyclomatic Complexity",
34-
"Coverage Depth",
35-
]
36-
37-
# TODO: merge with the one above
38-
CSV_HEADER = '"name", "loc, "fuzz_friendly", "risky_sinks", "natural_loops", "cyc_complex", "cov_depth", "fuzzability"\n'
39-
4027

4128
def error(string: str) -> None:
4229
"""Pretty-prints an error message and exits"""
@@ -56,7 +43,7 @@ def print_table(
5643
) -> None:
5744
"""Pretty-prints fuzzability results for the CLI"""
5845
table = Table(title=f"\nFuzzable Report for Target `{target}`")
59-
for column in COLUMNS:
46+
for column in [metric.friendly_name for metric in METRICS]:
6047
table.add_column(column, style="magenta")
6148

6249
for row in fuzzability:
@@ -86,13 +73,15 @@ def print_table(
8673
rprint("\n")
8774

8875

89-
def export_results(export, results) -> None:
76+
def export_results(export: Path, results: t.List[CallScore]) -> None:
77+
"""Given a file format and generated results, write to path."""
9078
writer = open(export, "w")
9179
ext = export.suffix
9280
if ext == ".json":
9381
writer.write(json.dumps([res.asdict() for res in results]))
9482
elif ext == ".csv":
95-
writer.write(CSV_HEADER.replace('"', ""))
83+
csv_header = ",".join([metric.identifier for metric in METRICS])
84+
writer.write(csv_header + "\n")
9685
for res in results:
9786
writer.write(res.csv_row)
9887
elif ext == ".md":

fuzzable/config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"""
77
import typing as t
88

9-
from os.path import dirname, abspath
109
from pathlib import Path
1110

1211

fuzzable/metrics.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,36 @@
33
44
Dataclass definitions for various metrics collected during qthe risk analysis.
55
"""
6-
import json
76
import functools
87
import typing as t
98

109
from dataclasses import dataclass, field, asdict
1110

1211

12+
@dataclass
13+
class MetricSchema:
14+
# shorthand name
15+
identifier: str
16+
17+
# how is displayed in the CLI/disassembly frontend
18+
friendly_name: str
19+
20+
21+
# Stores all the static analysis metrics that fuzzable currently supports.
22+
# This list should be expanded if additional metrics are to be introduced,
23+
# alongside a new base method in the AnalysisBackend
24+
METRICS: t.List[MetricSchema] = [
25+
MetricSchema(identifier="name", friendly_name="Function Signature"),
26+
MetricSchema(identifier="loc", friendly_name="Location"),
27+
MetricSchema(identifier="fuzzability", friendly_name="Fuzzability Score"),
28+
MetricSchema(identifier="fuzz_friendly", friendly_name="Fuzz-Friendly Name"),
29+
MetricSchema(identifier="risky_sinks", friendly_name="Risky Data Sinks"),
30+
MetricSchema(identifier="natural_loops", friendly_name="Natural Loops"),
31+
MetricSchema(identifier="cyc_complex", friendly_name="Cyclomatic Complexity"),
32+
MetricSchema(identifier="cov_depth", friendly_name="Coverage Depth"),
33+
]
34+
35+
1336
@dataclass
1437
class CoverageReport:
1538
"""TODO"""

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ pygments==2.12.0; python_full_version >= "3.6.3" and python_full_version < "4.0.
218218
pyparsing==3.0.9; python_full_version >= "3.6.8" and python_version >= "3.7" \
219219
--hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc \
220220
--hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb
221+
pypcode==1.0.7; python_version >= "3.6"
221222
pyquery==1.4.3
222223
pysmt==0.9.6.dev21; python_version >= "3.8"
223224
python-dateutil==2.8.2; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.8"

tests/test_main.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""
2+
test_main.py
3+
4+
Tests main functionality, including
5+
"""
6+
7+
import unittest
8+
9+
from pathlib import Path
10+
11+
from fuzzable.analysis import AnalysisMode
12+
from fuzzable.analysis.angr import AngrAnalysis
13+
from fuzzable.analysis.ast import AstAnalysis
14+
15+
16+
class TestMain(unittest.TestCase):
17+
def test_basic(self):
18+
data = [1, 2, 3]
19+
result = sum(data)
20+
self.assertEqual(result, 6)
21+
22+
def test_analysis_binary(self):
23+
target = Path("examples/binaries/libbasic.so.1")
24+
analyzer = AngrAnalysis(target, mode=AnalysisMode.RANK)
25+
analyzer.run()
26+
27+
def test_analysis_source_file(self):
28+
target = Path("examples/source/libbasic.c")
29+
analyzer = AstAnalysis([target], mode=AnalysisMode.RANK)
30+
analyzer.run()
31+
32+
def test_analysis_source_folder(self):
33+
target = Path("examples/source/libyaml")
34+
analyzer = AstAnalysis(target, mode=AnalysisMode.RANK)
35+
analyzer.run()
36+
37+
38+
if __name__ == "__main__":
39+
unittest.main()

0 commit comments

Comments
 (0)