From aa7380853372d754f6be9102c9983804a8cc5ab2 Mon Sep 17 00:00:00 2001 From: Dakota Soles Date: Wed, 17 May 2023 17:12:14 -0500 Subject: [PATCH 1/7] added new tools --- requirements.txt | 3 +- src/ecstatic/Tester.py | 271 +++++++++--------- src/ecstatic/readers/AmanDroidReader.py | 44 +++ src/ecstatic/readers/DroidSafeReader.py | 37 +++ src/ecstatic/readers/ReaderFactory.py | 9 + .../readers/callgraph/TAJSCallGraphReader.py | 45 +++ .../callgraph/WALAJSCallGraphReader.py | 30 ++ src/ecstatic/runners/AmanDroidRunner.py | 76 +++++ src/ecstatic/runners/DroidSafeRunner.py | 64 +++++ src/ecstatic/runners/RunnerFactory.py | 9 + src/ecstatic/runners/TAJSRunner.py | 84 ++++++ src/ecstatic/runners/WALAJSRunner.py | 32 +++ .../amandroid_config.json | 20 ++ .../droidsafe_config.json | 50 ++++ src/resources/grammars/amandroid.json | 9 + src/resources/grammars/droidsafe_grammer.json | 33 +++ src/resources/tools/amandroid/Dockerfile | 24 ++ src/resources/tools/amandroid/__init__.py | 17 ++ src/resources/tools/droidsafe/Dockerfile | 29 ++ src/resources/tools/droidsafe/__init__.py | 17 ++ src/resources/tools/droidsafe/droidsafe.sh | 25 ++ .../tools/wala-js/wala-js/Dockerfile | 23 ++ .../tools/wala-js/wala-js/__init__.py | 17 ++ 23 files changed, 836 insertions(+), 132 deletions(-) create mode 100644 src/ecstatic/readers/AmanDroidReader.py create mode 100644 src/ecstatic/readers/DroidSafeReader.py create mode 100644 src/ecstatic/readers/callgraph/TAJSCallGraphReader.py create mode 100644 src/ecstatic/readers/callgraph/WALAJSCallGraphReader.py create mode 100644 src/ecstatic/runners/AmanDroidRunner.py create mode 100644 src/ecstatic/runners/DroidSafeRunner.py create mode 100644 src/ecstatic/runners/TAJSRunner.py create mode 100644 src/ecstatic/runners/WALAJSRunner.py create mode 100644 src/resources/configuration_spaces/amandroid_config.json create mode 100644 src/resources/configuration_spaces/droidsafe_config.json create mode 100644 src/resources/grammars/amandroid.json create mode 100644 src/resources/grammars/droidsafe_grammer.json create mode 100644 src/resources/tools/amandroid/Dockerfile create mode 100644 src/resources/tools/amandroid/__init__.py create mode 100644 src/resources/tools/droidsafe/Dockerfile create mode 100644 src/resources/tools/droidsafe/__init__.py create mode 100755 src/resources/tools/droidsafe/droidsafe.sh create mode 100644 src/resources/tools/wala-js/wala-js/Dockerfile create mode 100644 src/resources/tools/wala-js/wala-js/__init__.py diff --git a/requirements.txt b/requirements.txt index 24f12795..8a28fd0a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ pytest~=6.2.5 frozendict~=2.1.3 jsonschema~=4.3.2 networkx~=2.6.3 +requests==2.28.1 docker~=5.0.3 matplotlib~=3.5.1 jsonpickle~=2.1.0 @@ -14,4 +15,4 @@ dill~=0.3.5.1 pathos~=0.2.9 hypothesis~=6.52.4 regex~=2022.7.25 -enum-actions~=0.1.2 \ No newline at end of file +enum-actions~=0.1.2 diff --git a/src/ecstatic/Tester.py b/src/ecstatic/Tester.py index 69cbd777..272262a8 100644 --- a/src/ecstatic/Tester.py +++ b/src/ecstatic/Tester.py @@ -15,9 +15,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . + import argparse import importlib -from importlib.resources import as_file import json import logging import os.path @@ -29,105 +29,138 @@ from multiprocessing.dummy import Pool from pathlib import Path from typing import List, Optional -from enum_actions import enum_action - +from datetime import datetime from tqdm import tqdm +import os +import csv +import shutil +from src.ecstatic.debugging.JavaBenchmarkDeltaDebugger import JavaBenchmarkDeltaDebugger from src.ecstatic.debugging.JavaViolationDeltaDebugger import JavaViolationDeltaDebugger from src.ecstatic.fuzzing.generators import FuzzGeneratorFactory from src.ecstatic.fuzzing.generators.FuzzGenerator import FuzzGenerator, FuzzOptions from src.ecstatic.readers import ReaderFactory +from src.ecstatic.readers.AbstractReader import AbstractReader from src.ecstatic.runners import RunnerFactory from src.ecstatic.runners.AbstractCommandLineToolRunner import AbstractCommandLineToolRunner from src.ecstatic.util.BenchmarkReader import BenchmarkReader from src.ecstatic.util.PotentialViolation import PotentialViolation from src.ecstatic.util.UtilClasses import FuzzingCampaign, Benchmark, \ - BenchmarkRecord + BenchmarkRecord, FinishedAnalysisJob from src.ecstatic.util.Violation import Violation from src.ecstatic.violation_checkers import ViolationCheckerFactory from src.ecstatic.violation_checkers.AbstractViolationChecker import AbstractViolationChecker - logger = logging.getLogger(__name__) class ToolTester: - def __init__(self, generator, runner: AbstractCommandLineToolRunner, debugger: Optional[JavaViolationDeltaDebugger], + def __init__(self, + generator, + runner: AbstractCommandLineToolRunner, + reader: AbstractReader, results_location: str, - num_processes: int, fuzzing_timeout: int, checker: AbstractViolationChecker, - seed: int): - self.generator: FuzzGenerator = generator + num_processes: int, + num_iterations: int): + self.generator = generator self.runner: AbstractCommandLineToolRunner = runner - self.debugger: JavaViolationDeltaDebugger = debugger + self.reader: AbstractReader = reader self.results_location: str = results_location self.unverified_violations = list() self.num_processes = num_processes - self.fuzzing_timeout = fuzzing_timeout - self.checker = checker - self.seed = seed + self.num_iterations = num_iterations + def read_violation_from_file(self, file: str) -> Violation: with open(file, 'rb') as f: return pickle.load(f) + + + def generate_comparable_results(self, tool, file, reader) -> set: + match tool.lower(): + case "flowdroid" | "tajs" | "droidsafe" | "amandroid": + return set(reader.import_file(file)) + case "wala-js" | "wala" | "doop" | "soot": + results = [] + with open(file, "r") as f: + for line in f: + results.append(reader.process_line(line)) + return set(results) + + + def move_nd_files(self, file, tool, benchmark): + output_path = Path('/results') / 'non_determinism' / tool / benchmark + Path(output_path).mkdir(exist_ok=True, parents=True) + nd_dir_path_t = os.path.join(output_path, file) + if not os.path.exists(nd_dir_path_t): + os.mkdir(nd_dir_path_t) + + for campaign_index in range(self.num_iterations): + nd_file_path_s = os.path.join(self.results_location, f'iteration{campaign_index}/{file}') + shutil.copyfile(nd_file_path_s, os.path.join(nd_dir_path_t, f'run_{campaign_index}')) + + + def generate_result_csv(self, results, tool, benchmark): + header = ['configuration', 'program', 'nondeterminism', 'error'] + + output_path = Path('/results') / 'out_csv' + Path(output_path).mkdir(exist_ok=True, parents=True) + uuid = datetime.now().strftime('%y%m%dT%H%M%S') + with open(os.path.join(output_path, f'{tool}_{benchmark}_{uuid}.csv'), 'w', encoding='UTF8') as f: + writer = csv.writer(f) + writer.writerow(header) + writer.writerows(results) + def main(self): - campaign_index = 0 start_time = time.time() - while True: + for campaign_index in range(self.num_iterations): campaign, generator_state = self.generator.generate_campaign() campaign: FuzzingCampaign - print(f"Got new fuzzing campaign: {campaign_index}.") + print(f"Running iteration: {campaign_index}.") campaign_start_time = time.time() # Make campaign folder. - if campaign_index == 0: - campaign_folder = os.path.join(self.results_location, f'campaign{campaign_index}') - else: - campaign_folder = Path(self.results_location) / str(self.seed) / self.generator.strategy.name / \ - (f'full_campaign{campaign_index}' if - self.generator.full_campaigns else f'campaign{campaign_index}') + campaign_folder = os.path.join(self.results_location, f'iteration{campaign_index}') Path(campaign_folder).mkdir(exist_ok=True, parents=True) - with open(Path(campaign_folder) / "fuzzer_state.json", 'w') as f: - json.dump(generator_state, f) - + # Run all jobs. partial_run_job = partial(self.runner.run_job, output_folder=campaign_folder) + results: List[FinishedAnalysisJob] = [] with Pool(self.num_processes) as p: - results = [] for r in tqdm(p.imap(partial_run_job, campaign.jobs), total=len(campaign.jobs)): results.append(r) - results = [r for r in results if r is not None and r.results_location is not None] - print(f'Campaign {campaign_index} finished (time {time.time() - campaign_start_time} seconds)') - violations_folder = Path(campaign_folder) / 'violations' - self.checker.output_folder = violations_folder - print(f'Now checking for violations.') - Path(violations_folder).mkdir(exist_ok=True) - violations: List[PotentialViolation] = self.checker.check_violations(results) - print(f"Total potential violations: {len(violations)}") - if self.debugger is not None: - with Pool(max(int(self.num_processes / 2), - 1)) as p: # /2 because each delta debugging process needs 2 cores. - direct_violations = [v for v in violations if not v.is_transitive] - print(f'Delta debugging {len(direct_violations)} cases with {self.num_processes} cores.') - p.map(partial(self.debugger.delta_debug, campaign_directory=campaign_folder, - timeout=self.runner.timeout), direct_violations) - self.generator.feedback(violations) - print(f'Done with campaign {campaign_index}!') - campaign_index += 1 - # if self.uid is not None and self.gid is not None: - # logger.info("Changing permissions of folder.") - # os.chown(campaign_folder, int(self.uid), int(self.gid)) - # for root, dirs, files in os.walk(campaign_folder): - # files = map(lambda x: os.path.join(root, x), files) - # map(lambda x: os.chown(x, int(self.uid), self.gid), files) - if time.time() - start_time > self.fuzzing_timeout * 60: - break + print(f'Iteration {campaign_index} finished (time {time.time() - campaign_start_time} seconds)') print('Testing done!') - -def files(param): - pass - + nd_results = [] + locations = str(self.results_location).rsplit('/', 2) + tool_name = locations[len(locations) - 2] + benchmark_name = locations[len(locations) - 1] + + for file in os.listdir(os.path.join(self.results_location, 'iteration0')): + if file.endswith(f'.{get_file_type(tool_name)}.raw'): + nd_result_record = [file.split('_', 1)[0], file.rsplit('_', 1)[-1].replace(f'.{get_file_type(tool_name)}.raw', '')] + file_s = f'{self.results_location}/iteration0/{file}' + results_s = self.generate_comparable_results(tool_name, file_s, self.reader) + nondeterminism = False + error = False + for campaign_index in range(1, self.num_iterations): + if not os.path.exists(f'{self.results_location}/iteration{campaign_index}/{file}'): + error = True + else: + file_t = f'{self.results_location}/iteration{campaign_index}/{file}' + results_t = self.generate_comparable_results(tool_name, file_t, self.reader) + if not results_s == results_t: + nondeterminism = True + self.move_nd_files(file, tool_name, benchmark_name) + break + nd_result_record.append(nondeterminism) + nd_result_record.append(error) + nd_results.append(nd_result_record) + + self.generate_result_csv(nd_results, tool_name, benchmark_name) + def main(): p = argparse.ArgumentParser() @@ -138,26 +171,12 @@ def main(): help="Number of fuzzing campaigns (i.e., one seed, all of its mutants, and violation detection)") p.add_argument("-j", "--jobs", type=int, default=32, help="Number of parallel jobs to do at once.") - p.add_argument("--adaptive", help="Remove configuration option settings that have already " - "exhibited violations from future fuzzing campaigns.", - action="store_true") p.add_argument('--timeout', help='Timeout in minutes', type=int) p.add_argument('--verbose', '-v', action='count', default=0) - p.add_argument('--fuzzing-timeout', help='Fuzzing timeout in minutes.', type=int, default=0) - p.add_argument( - '-d', '--delta-debugging-mode', - choices=['none', 'violation', 'benchmark'], - default='none' - ) - p.add_argument("--seed", help="Seed to use for the random fuzzer", type=int, default=2001) - p.add_argument("--fuzzing-strategy", action=enum_action(FuzzOptions), default="GUIDED") - p.add_argument("--full-campaigns", help="Do not sample at all, just do full campaigns.", action='store_true') - p.add_argument("--hdd-only", help="Disable the delta debugger's CDG phase.", action='store_true') + p.add_argument('--iterations', '-i', type=int, default=1) args = p.parse_args() - random.seed(args.seed) - if args.verbose > 1: logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p') @@ -168,73 +187,63 @@ def main(): logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p') - benchmark: Benchmark = build_benchmark(args.benchmark) + model_location = importlib.resources.path("src.resources.configuration_spaces", f"{args.tool}_config.json") + + benchmark: Benchmark = build_benchmark(args.benchmark, args.tool) logger.info(f'Benchmark is {benchmark}') # Check for groundtruths - from importlib.resources import files - with as_file(files("src.resources.configuration_spaces").joinpath(f"{args.tool}_config.json")) as model_location,\ - as_file(files("src.resources.grammars").joinpath(f"{args.tool}_grammar.json")) as grammar,\ - as_file(files("src.resources.tools").joinpath(f"{args.tool}")) as tool_dir: - - files = os.listdir(tool_dir) - groundtruths = None - for f in files: - if args.benchmark.lower() in f.lower() and 'groundtruth' in f.lower(): - groundtruths = os.path.join(tool_dir, f) - break - - if groundtruths is not None: - logger.info(f'Using {groundtruths} as groundtruths.') - - results_location = Path('/results') / args.tool / args.benchmark - - Path(results_location).mkdir(exist_ok=True, parents=True) - runner = RunnerFactory.get_runner_for_tool(args.tool) - - if "dacapo" in args.benchmark.lower(): - runner.whole_program = True - # Set timeout. - if args.timeout is not None: - runner.timeout = args.timeout - - generator = FuzzGeneratorFactory.get_fuzz_generator_for_name(args.tool, model_location, grammar, - benchmark, args.fuzzing_strategy, - args.full_campaigns) - reader = ReaderFactory.get_reader_for_task_and_tool(args.task, args.tool) - checker = ViolationCheckerFactory.get_violation_checker_for_task(args.task, args.tool, - jobs=args.jobs, - ground_truths=groundtruths, - reader=reader, - output_folder=results_location / "violations") - - match args.delta_debugging_mode.lower(): - case 'violation': debugger = JavaViolationDeltaDebugger(runner, reader, checker, hdd_only=args.hdd_only) - case 'benchmark': debugger = JavaBenchmarkDeltaDebugger(runner, reader, checker, hdd_only=args.hdd_only) - case _: debugger = None - - t = ToolTester(generator, runner, debugger, results_location, - num_processes=args.jobs, fuzzing_timeout=args.fuzzing_timeout, - checker=checker, seed=args.seed) - t.main() + tool_dir = importlib.resources.path(f'src.resources.tools.{args.tool}', '') + files = os.listdir(tool_dir) + groundtruths = None + for f in files: + if args.benchmark.lower() in f.lower() and 'groundtruth' in f.lower(): + groundtruths = os.path.join(tool_dir, f) + break + + if groundtruths is not None: + logger.info(f'Using {groundtruths} as groundtruths.') + + results_location = Path('/results') / args.tool / args.benchmark + Path(results_location).mkdir(exist_ok=True, parents=True) + runner = RunnerFactory.get_runner_for_tool(args.tool) -def build_benchmark(benchmark: str) -> Benchmark: + if "dacapo" in args.benchmark.lower(): + runner.whole_program = True + # Set timeout. + if args.timeout is not None: + runner.timeout = args.timeout + + generator = FuzzGeneratorFactory.get_fuzz_generator_for_name(args.tool, model_location, benchmark) + reader = ReaderFactory.get_reader_for_task_and_tool(args.task, args.tool) + + t = ToolTester(generator, runner, reader, results_location, args.jobs, args.iterations) + t.main() + + +def get_file_type(tool: str) -> str: + match tool.lower(): + case "soot" | "wala" | "doop": return 'jar' + case "wala-js" | "tajs": return 'js' + case "flowdroid" | "droidsafe" | "amandroid": return 'apk' + + +def build_benchmark(benchmark: str, tool: str) -> Benchmark: # TODO: Check that benchmarks are loaded. If not, load from git. if not os.path.exists("/benchmarks"): - with as_file(importlib.resources.files("src.resources.benchmarks").joinpath(benchmark).joinpath("build.sh")) as build: - logging.info(f"Building benchmark....") - subprocess.run(build) - with as_file(importlib.resources.files("src.resources.benchmarks").joinpath(benchmark)) as benchmark_dir: - if os.path.exists(index_file := Path(benchmark_dir)/Path("index.json")): - return BenchmarkReader().read_benchmark(index_file) - else: - benchmark_list = [] - for root, dirs, files in os.walk("/benchmarks"): - benchmark_list.extend([os.path.abspath(os.path.join(root, f)) for f in files if - (f.endswith(".jar") or f.endswith(".apk") or f.endswith( - ".js"))]) # TODO more dynamic extensions? - return Benchmark([BenchmarkRecord(b) for b in benchmark_list]) + build = importlib.resources.path(f"src.resources.benchmarks.{benchmark}", "build.sh") + logging.info(f"Building benchmark....") + subprocess.run(build) + if os.path.exists(importlib.resources.path(f"src.resources.benchmarks.{benchmark}", "index.json")): + return BenchmarkReader().read_benchmark( + importlib.resources.path(f"src.resources.benchmarks.{benchmark}", "index.json")) + else: + benchmark_list = [] + for root, dirs, files in os.walk("/benchmarks"): + benchmark_list.extend([os.path.abspath(os.path.join(root, f)) for f in files if + (f.endswith(get_file_type(tool)))]) # TODO more dynamic extensions? + return Benchmark([BenchmarkRecord(b) for b in benchmark_list]) if __name__ == '__main__': diff --git a/src/ecstatic/readers/AmanDroidReader.py b/src/ecstatic/readers/AmanDroidReader.py new file mode 100644 index 00000000..42c96c78 --- /dev/null +++ b/src/ecstatic/readers/AmanDroidReader.py @@ -0,0 +1,44 @@ +# ECSTATIC: Extensible, Customizable STatic Analysis Tester Informed by Configuration +# +# Copyright (c) 2022. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +from typing import List, Tuple + +from src.ecstatic.readers.AbstractReader import AbstractReader + + +logger = logging.getLogger(__name__) +class AmanDroidReader(AbstractReader): + + def import_file(self, file): + flows: List[Tuple[str, str]] = [] + isFlow = False + with open(file) as f: + lines = f.readlines() + for line in lines: + if isFlow: + if 'Source:' in line: + source = line.split(': ', 1)[1] + elif 'Sink:' in line: + sink = line.split(': ', 1)[1] + flows.append((source, sink)) + isFlow = isFlow == False + else: + if 'TaintPath:' in line: + isFlow = isFlow == False + return flows + \ No newline at end of file diff --git a/src/ecstatic/readers/DroidSafeReader.py b/src/ecstatic/readers/DroidSafeReader.py new file mode 100644 index 00000000..d152bf6b --- /dev/null +++ b/src/ecstatic/readers/DroidSafeReader.py @@ -0,0 +1,37 @@ +# ECSTATIC: Extensible, Customizable STatic Analysis Tester Informed by Configuration +# +# Copyright (c) 2022. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +from typing import List, Tuple + +from src.ecstatic.readers.AbstractReader import AbstractReader + + +logger = logging.getLogger(__name__) +class DroidSafeReader(AbstractReader): + + def import_file(self, file): + flows: List[Tuple[str, str]] = [] + with open(file) as f: + lines = f.readlines() + for line in lines: + if line.startswith('FLOW:'): + source = line.rsplit('|', 1)[-1].split('<=')[1] + sink = line.rsplit('|', 1)[-1].split('<=')[0] + flows.append((source, sink)) + return flows + \ No newline at end of file diff --git a/src/ecstatic/readers/ReaderFactory.py b/src/ecstatic/readers/ReaderFactory.py index 960f9b60..697fe112 100644 --- a/src/ecstatic/readers/ReaderFactory.py +++ b/src/ecstatic/readers/ReaderFactory.py @@ -22,6 +22,11 @@ from src.ecstatic.readers.callgraph.DOOPCallGraphReader import DOOPCallGraphReader from src.ecstatic.readers.callgraph.SOOTCallGraphReader import SOOTCallGraphReader from src.ecstatic.readers.callgraph.WALACallGraphReader import WALACallGraphReader +from src.ecstatic.readers.callgraph.TAJSCallGraphReader import TAJSCallGraphReader +from src.ecstatic.readers.callgraph.WALAJSCallGraphReader import WALAJSCallGraphReader +from src.ecstatic.readers.DroidSafeReader import DroidSafeReader +from src.ecstatic.readers.AmanDroidReader import AmanDroidReader + def get_reader_for_task_and_tool(task: str, name: str, *args) -> Any: match task.lower(): @@ -29,10 +34,14 @@ def get_reader_for_task_and_tool(task: str, name: str, *args) -> Any: match name.lower(): case "soot": return SOOTCallGraphReader(*args) case "wala": return WALACallGraphReader(*args) + case "wala-js": return WALAJSCallGraphReader(*args) case "doop": return DOOPCallGraphReader(*args) + case "tajs": return TAJSCallGraphReader(*args) case _: raise NotImplementedError(f"No support for task {task} on tool {name}") case "taint": match name.lower(): case "flowdroid": return FlowDroidFlowReader(*args) + case "droidsafe": return DroidSafeReader(*args) + case "amandroid": return AmanDroidReader(*args) case _: raise NotImplementedError(f"No support for task {task} on tool {name}") case _: raise NotImplementedError(f"No support for task {task}.") diff --git a/src/ecstatic/readers/callgraph/TAJSCallGraphReader.py b/src/ecstatic/readers/callgraph/TAJSCallGraphReader.py new file mode 100644 index 00000000..d9036071 --- /dev/null +++ b/src/ecstatic/readers/callgraph/TAJSCallGraphReader.py @@ -0,0 +1,45 @@ +from src.ecstatic.readers.callgraph.AbstractCallGraphReader import AbstractCallGraphReader +from src.ecstatic.util.CGCallSite import CGCallSite +from src.ecstatic.util.CGTarget import CGTarget + +from abc import ABC +from typing import Tuple, List, Any + +class TAJSCallGraphReader(AbstractCallGraphReader): + def import_file(self, file): + callgraph: List[Tuple[Any, Any]] = [] + edges = [] + nodes = {} + with open(file) as f: + lines = f.readlines() + for i in lines: + # print(i) + if "->" in i: + parts = i.strip().split(" -> ") + edges.append((parts[0], parts[1])) + print("edge:", i) + else: + parts = [] + node = [] + try: + parts = i.strip().split(" ") + node = parts[2].strip(" label=").strip('"]').strip('"]}').split("\\n") + except: + parts = i.strip().split(" ") + nodes[parts[0]] = node + print("node:", i) + for j in edges: + callid, targetid = j + call_funct = nodes[callid][0] + if (nodes[callid] != ['
']): + call_location = nodes[callid][1] + else: + call_location = '' + target_funct = nodes[targetid][0] + target_location = nodes[targetid][1] + # add tuple of CGCallsite and CGTarget to list + callsite = CGCallSite(call_funct, call_location, '') + target = CGTarget(target_location, '') + print(call_funct, call_location, target_location) + callgraph.append((callsite, target)) + return callgraph \ No newline at end of file diff --git a/src/ecstatic/readers/callgraph/WALAJSCallGraphReader.py b/src/ecstatic/readers/callgraph/WALAJSCallGraphReader.py new file mode 100644 index 00000000..a81bc284 --- /dev/null +++ b/src/ecstatic/readers/callgraph/WALAJSCallGraphReader.py @@ -0,0 +1,30 @@ +# ECSTATIC: Extensible, Customizable STatic Analysis Tester Informed by Configuration +# +# Copyright (c) 2022. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from typing import Tuple, Any + +from src.ecstatic.readers.callgraph.AbstractCallGraphReader import AbstractCallGraphReader +from src.ecstatic.util.CGCallSite import CGCallSite +from src.ecstatic.util.CGTarget import CGTarget + + +class WALAJSCallGraphReader(AbstractCallGraphReader): + + def process_line(self, line: str) -> Tuple[Any, Any]: + match line.split("\t"): + case [caller_function, caller_line, caller_context, callee_target, callee_context]: + return CGCallSite(caller_function, caller_line, caller_context), CGTarget(callee_target, callee_context) + case _: raise ValueError(f"Could not read line {line}") \ No newline at end of file diff --git a/src/ecstatic/runners/AmanDroidRunner.py b/src/ecstatic/runners/AmanDroidRunner.py new file mode 100644 index 00000000..bba3e64b --- /dev/null +++ b/src/ecstatic/runners/AmanDroidRunner.py @@ -0,0 +1,76 @@ +# ECSTATIC: Extensible, Customizable STatic Analysis Tester Informed by Configuration +# +# Copyright (c) 2022. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public Licese for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import importlib +import logging +import os +import shutil +import subprocess +import uuid +from typing import Tuple, List + +from src.ecstatic.runners.CommandLineToolRunner import CommandLineToolRunner +from src.ecstatic.util.UtilClasses import BenchmarkRecord +from src.ecstatic.util import FuzzingJob + + +logger = logging.getLogger(__name__) + +class AmanDroidRunner (CommandLineToolRunner): + + def get_timeout_option(self) -> List[str]: + return [] + + def get_input_option(self, benchmark_record: BenchmarkRecord) -> List[str]: + return f"{benchmark_record.name}".split() + + def get_output_option(self, output_file: str) -> List[str]: + return f"-o {output_file}".split() + + def get_base_command(self) -> List[str]: + return "java -jar /amandroid/argus-saf-3.2.1-SNAPSHOT-assembly.jar t".split() + + def try_run_job(self, job: FuzzingJob, output_folder: str) -> Tuple[str, str]: + logging.info(f'Job configuration is {[(str(k), str(v)) for k, v in job.configuration.items()]}') + config_hash = self.dict_hash(job.configuration) + id = uuid.uuid1().hex + result_dir = f'/amandroid/{config_hash}_{id}/' + cmd = self.get_base_command() + cmd.extend(self.get_output_option(result_dir)) + cmd.extend(self.get_input_option(job.target)) + logging.info(f"Cmd is {cmd}") + print(f"Cmd is {cmd}") + + ps = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + logger.info(f"Stdout from command {' '.join(cmd)} is {ps.stdout}") + + try: + result_dir_full = os.path.join(result_dir, os.path.basename(job.target.name).replace('.apk', '')) + intermediate_file = os.path.join(result_dir_full, 'result/AppData.txt') + except UnboundLocalError: + raise RuntimeError(ps.stdout) + + output_file = self.get_output(output_folder, job) + shutil.move(intermediate_file, output_file) + logging.info(f'Moved {intermediate_file} to {output_file}') + + if not os.path.exists(output_file): + raise RuntimeError(ps.stdout) + return output_file, ps.stdout + + diff --git a/src/ecstatic/runners/DroidSafeRunner.py b/src/ecstatic/runners/DroidSafeRunner.py new file mode 100644 index 00000000..32ba745a --- /dev/null +++ b/src/ecstatic/runners/DroidSafeRunner.py @@ -0,0 +1,64 @@ +# ECSTATIC: Extensible, Customizable STatic Analysis Tester Informed by Configuration +# +# Copyright (c) 2022. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import importlib +import logging +import os +import shutil +import subprocess +import uuid +from typing import Tuple + +from src.ecstatic.runners.AbstractCommandLineToolRunner import AbstractCommandLineToolRunner +from src.ecstatic.util import FuzzingJob + + +logger = logging.getLogger(__name__) + + +class DroidSafeRunner(AbstractCommandLineToolRunner): + + def try_run_job(self, job: FuzzingJob, output_folder: str) -> Tuple[str, str]: + target_basedir = os.path.join(os.getenv('DROIDSAFE_SRC_HOME'), 'runs') + app_name = os.path.basename(job.target.name).replace('.apk', '') + config_hash = self.dict_hash(job.configuration) + id = uuid.uuid1().hex + target_dir = os.path.join(target_basedir, f'{config_hash}_{app_name}_{id}') + + droidsafe_shell = importlib.resources.path(f"src.resources.tools.droidsafe", "droidsafe.sh") + cmd = [droidsafe_shell] + cmd.append(job.target.name) + cmd.append(f'{config_hash}_{app_name}_{id}') + cmd.append(self.dict_to_config_str(job.configuration)) + logger.info(f'Running job with configuration {self.dict_hash(job.configuration)} on apk {job.target.name}') + ps = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + logger.info(f'Stdout for cmd {" ".join([str(c) for c in cmd])} was {ps.stdout}') + logger.info(f'Job on configuration {self.dict_hash(job.configuration)} on apk {job.target.name} done.') + + output_file = self.get_output(output_folder, job) + + try: + target_dir_gen = os.path.join(target_dir, 'droidsafe-gen') + intermediate_file = os.path.join(target_dir_gen, "info-flow-results.txt") + except UnboundLocalError: + raise RuntimeError(ps.stdout) + shutil.copyfile(intermediate_file, output_file) + logger.info(f'Copied {intermediate_file} to {output_file}') + + return output_file, ps.stdout + diff --git a/src/ecstatic/runners/RunnerFactory.py b/src/ecstatic/runners/RunnerFactory.py index de9cd576..c960c4d3 100644 --- a/src/ecstatic/runners/RunnerFactory.py +++ b/src/ecstatic/runners/RunnerFactory.py @@ -22,7 +22,12 @@ from src.ecstatic.runners.DOOPRunner import DOOPRunner from src.ecstatic.runners.FlowDroidRunner import FlowDroidRunner from src.ecstatic.runners.SOOTRunner import SOOTRunner +from src.ecstatic.runners.WALAJSRunner import WALAJSRunner from src.ecstatic.runners.WALARunner import WALARunner +from src.ecstatic.runners.TAJSRunner import TAJSRunner +from src.ecstatic.runners.DroidSafeRunner import DroidSafeRunner +from src.ecstatic.runners.AmanDroidRunner import AmanDroidRunner + logger = logging.getLogger(__name__) @@ -33,4 +38,8 @@ def get_runner_for_tool(name: str, *args) -> AbstractCommandLineToolRunner: case "wala": return WALARunner(*args) case "doop": return DOOPRunner(*args) case "flowdroid": return FlowDroidRunner(*args) + case "wala-js": return WALAJSRunner(*args) + case "tajs": return TAJSRunner(*args) + case "droidsafe": return DroidSafeRunner(*args) + case "amandroid": return AmanDroidRunner(*args) case _ : raise NotImplementedError(f"No support for runner for {name}") diff --git a/src/ecstatic/runners/TAJSRunner.py b/src/ecstatic/runners/TAJSRunner.py new file mode 100644 index 00000000..e3c4049c --- /dev/null +++ b/src/ecstatic/runners/TAJSRunner.py @@ -0,0 +1,84 @@ +import os.path +from typing import List, Dict + +from src.ecstatic.runners.CommandLineToolRunner import CommandLineToolRunner +from src.ecstatic.util.UtilClasses import BenchmarkRecord + + +class TAJSRunner (CommandLineToolRunner): + def get_timeout_option(self) -> List[str]: + if self.timeout is None: + return [] + else: + return f"-time-limit {self.timeout * 60}".split(" ") + + def get_whole_program(self) -> List[str]: + # shouldn't be necessary + return [] + + def get_input_option(self, benchmark_record: BenchmarkRecord) -> List[str]: + return f"{benchmark_record.name}".split() + + def get_output_option(self, output_file: str) -> List[str]: + # callgraph recieves file path argument + return f"-callgraph {output_file}".split() + + def get_task_option(self, task: str) -> List[str]: + # might need to add to this + if task == 'cg': + return [] + else: + raise NotImplementedError(f'TAJS does not support task {task}.') + + def dict_to_config_str(self, config_as_dict: Dict[str, str]) -> str: + """ + We need special handling of TAJS's options, because of unsound options and commands without level value + + + Parameters + ---------- + config_as_dict: The dictionary specifying the configuration. + Returns + ------- + The corresponding command-line string. + """ + #config_as_str = "" + #for k, v in config_as_dict.items(): + # k: Option + # v: Level + # for t in k.tags: + # if t.startswith('unsound'): + # config_as_str = config_as_str + f"-unsound -{k.name} " + #rest_of_config = "" + #for k, v in config_as_dict.items(): + # if len([t for t in k.tags if t.startswith('unsound')]) == 0: + # k: Option + # v: Level + # if isinstance(v.level_name, int) or \ + # v.level_name.lower() not in ['false', 'true']: + # rest_of_config += f'-{k.name} {v.level_name} ' + # elif v.level_name.lower() == 'true': + # rest_of_config += f'-{k.name} ' +# + # Compute string for the rest of the options which are not unsound. + # this part may not work + #return rest_of_config + config_as_str + config_as_str = ""; + for key in config_as_dict: + if key.startswith('isunsound_'): + #phase option, append like this -p 'phase' 'key':'value' + #format is "phase_'phase'_'key'" = VALUE + config_as_str = config_as_str + f"-unsound -{key[key.find('_')+1:]} " + else: + if(config_as_dict[key] == 'true'): + config_as_str = config_as_str + f"-{key} " + elif(config_as_dict[key] == 'false'): + config_as_str = config_as_str; #dumb options that are turned off by not giving them? + else: + config_as_str = config_as_str + f"-{key} {config_as_dict[key]} " + + return config_as_str; + + def get_base_command(self) -> List[str]: + return "java -jar /TAJS/dist/tajs-all.jar".split() + \ No newline at end of file diff --git a/src/ecstatic/runners/WALAJSRunner.py b/src/ecstatic/runners/WALAJSRunner.py new file mode 100644 index 00000000..16651320 --- /dev/null +++ b/src/ecstatic/runners/WALAJSRunner.py @@ -0,0 +1,32 @@ +# ECSTATIC: Extensible, Customizable STatic Analysis Tester Informed by Configuration +# +# Copyright (c) 2022. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from typing import List + +from src.ecstatic.runners.WALARunner import WALARunner +from src.ecstatic.util.UtilClasses import BenchmarkRecord + + +class WALAJSRunner(WALARunner): + def get_input_option(self, benchmark_record: BenchmarkRecord) -> List[str]: + return f"--scripts {benchmark_record.name}".split(" ") + + def get_output_option(self, output_file: str) -> List[str]: + return f"--cgoutput {output_file}".split(" ") + + + def get_base_command(self) -> List[str]: + return "java -jar /WalaJSCallgraph/target/jscallgraph-0.0.1-SNAPSHOT-jar-with-dependencies.jar".split(" ") \ No newline at end of file diff --git a/src/resources/configuration_spaces/amandroid_config.json b/src/resources/configuration_spaces/amandroid_config.json new file mode 100644 index 00000000..4e7cab3e --- /dev/null +++ b/src/resources/configuration_spaces/amandroid_config.json @@ -0,0 +1,20 @@ +{ + "name": "AmanDroid", + "options": [ + { + "name": "static_init", + "levels": [ + "FALSE", + "TRUE" + ], + "default": "FALSE", + "orders": [ + { + "left": "TRUE", + "order": "MPT", + "right": "FALSE" + } + ] + } + ] + } \ No newline at end of file diff --git a/src/resources/configuration_spaces/droidsafe_config.json b/src/resources/configuration_spaces/droidsafe_config.json new file mode 100644 index 00000000..81cef644 --- /dev/null +++ b/src/resources/configuration_spaces/droidsafe_config.json @@ -0,0 +1,50 @@ +{ + "name": "DroidSafe", + "options": [ + { + "name": "analyzestrings_unfiltered", + "levels": [ + "FALSE", + "TRUE" + ], + "default": "FALSE", + "orders": [ + { + "left": "TRUE", + "order": "MPT", + "right": "FALSE" + } + ] + }, + { + "name": "filetransforms", + "levels": [ + "FALSE", + "TRUE" + ], + "default": "FALSE", + "orders": [ + { + "left": "TRUE", + "order": "MST", + "right": "FALSE" + } + ] + }, + { + "name": "ignorenocontextflows", + "levels": [ + "TRUE", + "FALSE" + ], + "default": "FALSE", + "orders": [ + { + "left": "TRUE", + "order": "MST", + "right": "FALSE" + } + ] + } + ] + } \ No newline at end of file diff --git a/src/resources/grammars/amandroid.json b/src/resources/grammars/amandroid.json new file mode 100644 index 00000000..a34371e7 --- /dev/null +++ b/src/resources/grammars/amandroid.json @@ -0,0 +1,9 @@ +{ + "": [""], + "": ["