Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion base_image.dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ RUN apt-get update -y && apt-get upgrade -y && \
apt-get install -y cmake z3 python3.10 python3.10-dev python3-distutils python3-pip python3-apt python3.10-venv pkg-config libcairo2-dev libjpeg-dev libgif-dev \
openjdk-8-jdk git maven wget && \
DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends --assume-yes build-essential libpq-dev unzip
apt-get install -y --no-install-recommends --assume-yes build-essential libpq-dev unzip libcairo2-dev pkg-config python3.10-dev
RUN apt-get update && apt-get install -y nodejs npm

FROM python-build AS ecstatic-build
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 66 additions & 0 deletions src/ecstatic/debugging/JavaBenchmarkDeltaDebugger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 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 <https://www.gnu.org/licenses/>.

import logging
from typing import Iterable, Tuple

from src.ecstatic.debugging.AbstractDeltaDebugger import DeltaDebuggingPredicate, GroundTruth
from src.ecstatic.debugging.BenchmarkDeltaDebugger import BenchmarkDeltaDebugger
from src.ecstatic.debugging.JavaDeltaDebugger import JavaDeltaDebugger
from src.ecstatic.util.PartialOrder import PartialOrderType
from src.ecstatic.util.PotentialViolation import PotentialViolation

logger = logging.getLogger(__name__)


class JavaBenchmarkDeltaDebugger(BenchmarkDeltaDebugger, JavaDeltaDebugger):
def make_predicates(self, potential_violation: PotentialViolation) -> Iterable[Tuple[DeltaDebuggingPredicate, GroundTruth]]:
"""
Returns the predicates and ground truths for a potential violation. If the potential violation's main
partial order (i.e., in the case that there are two potential partial order violations, we want the partial
order that matches the order of the jobs, such that job1 has some relationship to job2).
:param potential_violation: The non-violation
:return:
"""
mp = potential_violation.get_main_partial_order()
if (not potential_violation.is_violation) and (len(potential_violation.expected_diffs) > 0) and \
mp.is_explicit():
match mp.type:
case PartialOrderType.MORE_SOUND_THAN:
# If it's a soundness partial order, then we are not sure how many of the additional edges
# produces by the more sound configuration
def predicate(pv: PotentialViolation):
return pv.expected_diffs <= potential_violation.expected_diffs
ground_truth = {
"partial_order": str(mp),
"left_preserve_at_least_one": [str(e) for e in potential_violation.expected_diffs]
}
yield predicate, ground_truth
case PartialOrderType.MORE_PRECISE_THAN:
# If it's a precision partial order, we create a new benchmark for each of the edges missing in the
# more precise result, since we can be confident they are all false positives.
for e in potential_violation.expected_diffs:
def predicate(pv: PotentialViolation):
return e in pv.expected_diffs
ground_truth = {
"partial_order": str(mp),
"left_preserve_all": [str(e)]
}
yield predicate, ground_truth
case _:
raise RuntimeError("Pattern matching on main partial order failed.")

1 change: 0 additions & 1 deletion src/ecstatic/dispatcher/DockerManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@

def build_image(tool: str, nocache: bool = False):
env = os.environ
os.environ["DOCKER_DEFAULT_PLATFORM"] = "linux/amd64"
if tool == 'base':
logger.info("Creating base image")
cmd = ['docker', 'build', '.', '-f', 'base_image.dockerfile', '-t', get_image_name(tool)]
Expand Down
44 changes: 44 additions & 0 deletions src/ecstatic/readers/AmanDroidReader.py
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

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

37 changes: 37 additions & 0 deletions src/ecstatic/readers/DroidSafeReader.py
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

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

9 changes: 9 additions & 0 deletions src/ecstatic/readers/ReaderFactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,26 @@
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():
case "cg":
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}.")
45 changes: 45 additions & 0 deletions src/ecstatic/readers/callgraph/TAJSCallGraphReader.py
Original file line number Diff line number Diff line change
@@ -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] != ['<main>']):
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
30 changes: 30 additions & 0 deletions src/ecstatic/readers/callgraph/WALAJSCallGraphReader.py
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
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}")
78 changes: 78 additions & 0 deletions src/ecstatic/runners/AmanDroidRunner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# 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 <https://www.gnu.org/licenses/>.


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]:
print("Amandroid does not support the timeout option. Sorry!")
exit(-1)
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


Loading