mirror of
https://github.com/lifting-bits/mcsema
synced 2026-06-08 15:31:09 +00:00
2d55d02cc3
* API improvements. Must be used with the api_improvements branch of both Remill and McSema fixes for x86 and running the lifted code with klee * Update dockerfile to clone anvill * update remill commit id * Add python3 to dockerfile * update python3 * disable abi script * Updated cmake to find anvill * Update main.cpp * update find_package for anvill * WIP:updated prebuild cfg * update prebuild cfg files * enable abi build for testsuite * Fix memory leak * install missing package for testcases * frontend: Reflect cfg file changes in dyninst frontend. * frontend: Update local files copyrights to reflect overall change to agplv3. * fix failing testcases * update test cfgs * Fix test failure with local state pointer * set the flag to use local state_ptr in default mode Co-authored-by: kumarak <iit.akshay@gmail.com> Co-authored-by: Lukas Korencik <xkorenc1@fi.muni.cz>
644 lines
20 KiB
Python
Executable File
644 lines
20 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Copyright (c) 2020 Trail of Bits, Inc.
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import argparse
|
|
import difflib
|
|
import filecmp
|
|
import operator
|
|
import os
|
|
import queue
|
|
import threading
|
|
import shutil
|
|
import subprocess
|
|
from subprocess import CalledProcessError
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
import colors
|
|
import result_data
|
|
import util
|
|
|
|
llvm_version = 8
|
|
|
|
lift = None
|
|
bin_dir = "bin"
|
|
libmcsema = None
|
|
recompiled_dir = "recompiled"
|
|
tags_dir = 'tags'
|
|
|
|
batches = []
|
|
shared_libs = None
|
|
abi_lib_dir = None
|
|
|
|
def get_recompiled_name(name):
|
|
return name
|
|
|
|
# Print results of tests
|
|
def print_results(t_cases):
|
|
for key, val in sorted(t_cases.items(), key = operator.itemgetter(0)):
|
|
val.print(0)
|
|
for key, val in sorted(t_cases.items(), key = operator.itemgetter(0)):
|
|
val.print_ces()
|
|
|
|
def log_results(result, into):
|
|
for entry in result.failures:
|
|
what = entry[0]
|
|
into[what.name].cases[what._testMethodName] = result_data.FAIL
|
|
|
|
for entry in result.errors:
|
|
what = entry[0]
|
|
# Do not overwrite timeouts
|
|
if into[what.name].cases[what._testMethodName] != result_data.TIMEOUT:
|
|
into[what.name].cases[what._testMethodName] = result_data.ERROR
|
|
|
|
|
|
# Fill global variables
|
|
# TODO: Rework, this is really ugly
|
|
def check_arguments(args):
|
|
if not os.path.isfile(args.lift):
|
|
print("{} passed to --lift is not a valid file".format(args.lift))
|
|
sys.exit (1)
|
|
global lift
|
|
lift = args.lift
|
|
|
|
if not os.path.isfile(args.runtime_lib):
|
|
print ("{} passed to --runtime_lib is not a valid file".format(args.runtime_lib))
|
|
sys.exit (1)
|
|
global libmcsema
|
|
libmcsema = args.runtime_lib
|
|
|
|
if not os.path.isdir(args.bin_dir):
|
|
print("{} passed to --bin_dir is not a valid directory".format(args.bin_dir))
|
|
sys.exit (1)
|
|
bin_dir = os.path.abspath(args.bin_dir)
|
|
|
|
if not os.path.isdir(args.shared_lib_dir):
|
|
print("{} passed to --shared_lib_dir is not a valid directory".format(args.shared_lib_dir))
|
|
sys.exit (1)
|
|
|
|
global shared_libs
|
|
shared_libs = []
|
|
for lib_name in os.listdir(args.shared_lib_dir):
|
|
shared_libs.append(os.path.join(args.shared_lib_dir, lib_name ))
|
|
|
|
print("\n > Shared libraries: ")
|
|
print(shared_libs)
|
|
|
|
if not os.path.isdir(args.abi_lib_dir):
|
|
print ("{} passed to --abi_lib_dir is not a valid directory".format(args.abi_lib_dir))
|
|
sys.exit (1)
|
|
|
|
global abi_lib_dir
|
|
abi_lib_dir = ''
|
|
for lib_name in os.listdir(args.abi_lib_dir):
|
|
if lib_name.endswith(".bc"):
|
|
abi_lib_dir += os.path.join(args.abi_lib_dir, lib_name + ',')
|
|
|
|
print("\n > --abi_libraries files:")
|
|
print(abi_lib_dir)
|
|
|
|
# TODO: This whole snippet can be done using argparser and some actions
|
|
if args.batch is not None:
|
|
for batch in args.batch:
|
|
batch_dir = batch + "_cfg"
|
|
if not os.path.isdir(batch_dir):
|
|
print("{} passed to --batch is not a valid directory".format(batch))
|
|
sys.exit(1)
|
|
batches.append(batch_dir)
|
|
|
|
if args.batch_dir is not None:
|
|
for batch in args.batch_dir:
|
|
if not os.path.isdir(batch):
|
|
print("{} passed to --batch_dir is not a valid directory".format(batch))
|
|
sys.exit(1)
|
|
batches.append(batch_dir)
|
|
|
|
if not batches:
|
|
print("No batch is selected, exiting")
|
|
sys.exit(0)
|
|
|
|
print(batches)
|
|
|
|
|
|
def exec_and_log_fail(args):
|
|
try:
|
|
pipes = subprocess.Popen(args, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
|
|
std_out, std_err = pipes.communicate(timeout = 180)
|
|
ret_code = pipes.returncode
|
|
except subprocess.TimeoutExpired as e:
|
|
pipes.terminate()
|
|
print("Timeout!")
|
|
return False
|
|
|
|
if ret_code:
|
|
print("** stdout:")
|
|
print(std_out)
|
|
print("** stderr:")
|
|
print(std_err)
|
|
return False
|
|
return True
|
|
|
|
class Config:
|
|
class Result:
|
|
SUCCESS = 0
|
|
LIFT_FAIL = 1
|
|
RECOMPILE_FAIL = 2
|
|
|
|
defaults = [ '-os', 'linux', '-arch', 'amd64' ]
|
|
togglable = {
|
|
'abi_libraries' : ['-abi_libraries', abi_lib_dir],
|
|
}
|
|
|
|
# Since strings are immutable, hotfix is needed once abi_lib_dir is set
|
|
def _fix_excludes(self):
|
|
flag, opt = self.togglable['abi_libraries']
|
|
self.togglable['abi_libraries'] = (flag, abi_lib_dir)
|
|
|
|
def __init__(self, name, src, cfg_path):
|
|
self.name = name
|
|
self.binary = os.path.join(bin_dir, name)
|
|
self.config = src.rsplit('.', 2)[1]
|
|
self.id = self.name + '.' + self.config
|
|
self.cfg = cfg_path
|
|
self.lift_args = []
|
|
self.exclude_args = []
|
|
self.tags = []
|
|
self._fix_excludes()
|
|
self._parse_config(src)
|
|
|
|
def _tags(self, line):
|
|
self.tags = line.split(' ')[1:]
|
|
|
|
def _lift_opts(self, line):
|
|
tokens = line.split(' ')[1:]
|
|
# LIFT_OPTS are empty
|
|
if not tokens:
|
|
return
|
|
|
|
exclude = []
|
|
for opt in tokens:
|
|
if not opt:
|
|
break
|
|
elif opt[0] == '+':
|
|
self.lift_args.append(opt[1:])
|
|
elif opt[0] != '!':
|
|
self.lift_args.append(opt)
|
|
else:
|
|
exclude.append(opt[1:])
|
|
|
|
for key, val in Config.togglable.items():
|
|
if key in exclude:
|
|
continue
|
|
self.lift_args += val
|
|
|
|
|
|
def _parse_config(self, src):
|
|
print(' > Parsing', src)
|
|
with open(os.path.join(tags_dir, src), 'r') as cfg:
|
|
for line in cfg:
|
|
line = line.rstrip('\n')
|
|
header = line.split(' ', 1)[0]
|
|
|
|
header_dispatch = {
|
|
'TAGS:' : Config._tags,
|
|
'LIFT_OPTS:' : Config._lift_opts,
|
|
}
|
|
|
|
if header not in header_dispatch:
|
|
raise Exception("Unknown header {}".format(header))
|
|
header_dispatch[header](self, line)
|
|
|
|
def lift(self, test_dir):
|
|
print(" > Lifting", self.name + self.config)
|
|
|
|
self.bc = os.path.join(test_dir, '.'.join([self.name, self.config, 'bc']))
|
|
self.recompiled = os.path.join(test_dir, self.name + '.' + self.config)
|
|
|
|
args = [lift] + self.defaults + self.lift_args + \
|
|
['-output', self.bc, '-cfg', self.cfg]
|
|
print(args)
|
|
if not exec_and_log_fail(args):
|
|
return Config.Result.LIFT_FAIL
|
|
|
|
return self.recompile()
|
|
|
|
def recompile(self):
|
|
compiler = None
|
|
if 'c' in self.tags:
|
|
compiler = "clang-{}"
|
|
elif 'cpp' in self.tags:
|
|
compiler = "clang++-{}"
|
|
else:
|
|
print(" > Cannot decide on compiler when recompiling",\
|
|
self.name + '.' + self.config)
|
|
return Config.Result.RECOMPILE_FAIL
|
|
|
|
compiler = compiler.format(llvm_version)
|
|
args = [compiler, self.bc, '-o', self.recompiled, \
|
|
libmcsema, '-lpthread', '-lm', '-ldl'] + shared_libs
|
|
|
|
if not exec_and_log_fail(args):
|
|
return Config.Result.RECOMPILE_FAIL
|
|
return Config.Result.SUCCESS
|
|
|
|
|
|
def parallel_lift(*t_args):
|
|
todo, test_dir, results = t_args
|
|
while not todo.empty():
|
|
try:
|
|
config = todo.get()
|
|
except queue.Empty:
|
|
return
|
|
|
|
res = config.lift(test_dir)
|
|
# TODO: More fine grained log
|
|
if res != Config.Result.SUCCESS:
|
|
results[config.id] = result_data.TCData(config.id,config.recompiled, config.binary)
|
|
|
|
def get_configs(directory, allowed_tags, batched):
|
|
result = []
|
|
for f in os.listdir(directory):
|
|
name = util.strip_whole_config(f)
|
|
if not name or name not in batched:
|
|
continue
|
|
c = Config(name, f, batched[name])
|
|
|
|
if allowed_tags is None:
|
|
result.append(c)
|
|
elif any(x in allowed_tags for x in c.tags):
|
|
result.append(c)
|
|
return result
|
|
|
|
class TestDetails:
|
|
def __init__(self, cmd):
|
|
self.cmd = cmd
|
|
self.files = None
|
|
self.stdin = None
|
|
self.f_stdin = None
|
|
self.check = []
|
|
|
|
def set_files(self, files):
|
|
self.files = files
|
|
return self
|
|
|
|
def set_check(self, check):
|
|
self.check = check
|
|
return self
|
|
|
|
def set_f_stdin(self, f_stdin):
|
|
self.f_stdin = f_stdin
|
|
return self
|
|
|
|
def set_stdin(self, stdin):
|
|
self.stdin = stdin
|
|
return self
|
|
|
|
def files(self, files):
|
|
if self.files is not None:
|
|
raise Exception("Incorrect format of test case")
|
|
self.files = files
|
|
|
|
def stdin(self, stdin):
|
|
if stdin[0] != 'F':
|
|
self.stdin = stdin.encode()
|
|
return
|
|
self.f_stdin = os.path.abspath(stdin[1:])
|
|
|
|
class TestCase:
|
|
def __init__(self, src=None):
|
|
self.details = []
|
|
if src is not None:
|
|
self._parse(src)
|
|
|
|
def _parse(self, src):
|
|
dispatch = {
|
|
'STDIN:' : TestDetails.stdin,
|
|
'FILES:' : TestDetails.files,
|
|
}
|
|
with open(src, 'r') as f:
|
|
for line in f:
|
|
line = line.rstrip('\n')
|
|
head = line.split(' ', 1)[0]
|
|
if head in dispatch:
|
|
dispatch[head](self.details[-1], line.split(' ', 1)[1])
|
|
else:
|
|
self.details.append(TestDetails(line.split(' ')))
|
|
# .test was present but empty
|
|
if not self.details:
|
|
self.details.append(TestDetails(['']))
|
|
|
|
|
|
class Runner:
|
|
def __init__(self, config, test_case):
|
|
self.config = config
|
|
self.test_case = test_case
|
|
|
|
def set_up(self):
|
|
self.t_bin = tempfile.mkdtemp(dir=os.getcwd(), prefix='bin_t')
|
|
self.t_recompiled = tempfile.mkdtemp(dir=os.getcwd(), prefix='recompiled_t')
|
|
self.sawed_cwd = os.getcwd()
|
|
|
|
cfg = self.config
|
|
os.symlink(os.path.abspath(cfg.binary), os.path.join(self.t_bin, cfg.name))
|
|
os.symlink(cfg.recompiled, os.path.join(self.t_recompiled, cfg.name))
|
|
|
|
def tear_down(self):
|
|
os.chdir(self.sawed_cwd)
|
|
shutil.rmtree(self.t_recompiled)
|
|
shutil.rmtree(self.t_bin)
|
|
|
|
def copy_files(self, detail):
|
|
if detail.files is not None:
|
|
for f in detail.files:
|
|
basename = os.path.basename(f)
|
|
full_name = f
|
|
r = os.path.join(self.t_recompiled, basename)
|
|
b = os.path.join(self.t_bin, basename)
|
|
shutil.copyfile(full_name, r)
|
|
shutil.copyfile(full_name, b)
|
|
|
|
def compare(self, expected, actual, files):
|
|
e_out, e_err, e_ret = expected
|
|
a_out, a_err, a_ret = actual
|
|
|
|
correct = True
|
|
counterexample = ''
|
|
# TODO: This cannot be more hacky than this
|
|
try:
|
|
if a_out != e_out:
|
|
counterexample += 'stdout:\n'
|
|
counterexample += '\tExpected:\n'
|
|
counterexample += e_out.decode() + '\n'
|
|
counterexample += '\tGot:\n'
|
|
counterexample += a_out.decode() + '\n'
|
|
correct = correct and False
|
|
if a_err != e_err:
|
|
counterexample += 'stderr:'+ '\n'
|
|
counterexample += '\tExpected:'+ '\n'
|
|
counterexample += e_err.decode()+ '\n'
|
|
counterexample += '\tGot:'+ '\n'
|
|
counterexample += a_err.decode()+ '\n'
|
|
correct = correct and False
|
|
except UnicodeDecodeError as e:
|
|
correct = False
|
|
counterexample += str(e) + '\n'
|
|
|
|
if e_ret != e_ret:
|
|
counterexample += "Return code: " + str(e_ret) + ' != ' + str(a_ret) + '\n'
|
|
correct = correct and False
|
|
|
|
for name in files:
|
|
base_name = os.path.basename(name)
|
|
actual = os.path.join(self.t_recompiled, base_name)
|
|
expected = os.path.join(self.t_bin, base_name)
|
|
try:
|
|
if not filecmp.cmp(expected, actual):
|
|
counterexample += 'Files do not match: ' + base_name + '\n'
|
|
correct = correct and False
|
|
except FileNotFoundError as e:
|
|
counterexample += 'File {} not found/produced! '.format(base_name)
|
|
correct = correct and False
|
|
|
|
result = result_data.RUN if correct else result_data.FAIL
|
|
return (result, counterexample)
|
|
|
|
def open_stdin(self, t_dir, args, stdin):
|
|
with open(stdin, 'rb') as f:
|
|
r = f.read()
|
|
return self.exec_(t_dir, args, r)
|
|
|
|
def exec_(self, t_dir, args, stdin):
|
|
os.chdir(t_dir)
|
|
try:
|
|
pipes = subprocess.Popen(
|
|
args, stdout=subprocess.PIPE,
|
|
stderr = subprocess.PIPE,
|
|
stdin = subprocess.PIPE)
|
|
out, err = pipes.communicate(stdin, timeout=5)
|
|
ret = pipes.returncode
|
|
|
|
os.chdir(self.sawed_cwd)
|
|
return (out, err, ret)
|
|
except subprocess.TimeoutExpired as e:
|
|
pipes.terminate()
|
|
raise e
|
|
|
|
|
|
def exec(self, detail, files):
|
|
# To avoid calling system-wide installed binaries
|
|
filename = './' + self.config.name
|
|
|
|
_exec = Runner.exec_ if detail.f_stdin is None else Runner.open_stdin
|
|
stdin = detail.f_stdin if detail.f_stdin is not None else detail.stdin
|
|
|
|
try:
|
|
print(detail.cmd)
|
|
expected = _exec(self, self.t_bin, [filename] + detail.cmd, stdin)
|
|
actual = _exec(self, self.t_recompiled, [filename] + detail.cmd, stdin)
|
|
return self.compare(expected, actual, files)
|
|
except subprocess.TimeoutExpired as e:
|
|
return result_data.TIMEOUT, "Timeout"
|
|
except FileNotFoundError as e:
|
|
return result_data.ERROR, "File not found"
|
|
|
|
def run(self, detail):
|
|
self.set_up()
|
|
self.copy_files(detail)
|
|
result, ce = self.exec(detail, detail.check)
|
|
self.tear_down()
|
|
return (result, ce)
|
|
|
|
|
|
def evaluate(self, results):
|
|
c = self.config
|
|
results[c.id] = result_data.TCData(c.id, c.recompiled, c.binary)
|
|
for tc in self.test_case:
|
|
for d in tc.details:
|
|
result, ce = self.run(d)
|
|
results[c.id].cases[' '.join(d.cmd)] = result
|
|
results[c.id].total += 1
|
|
if result == result_data.RUN:
|
|
results[c.id].success += 1
|
|
else:
|
|
results[c.id].ces[' '.join(d.cmd)] = ce
|
|
|
|
class Tester:
|
|
|
|
def __init__(self, configs, test_def_dir):
|
|
# Config -> TestCase
|
|
self.cases = {}
|
|
|
|
for c in configs:
|
|
self.cases[c] = []
|
|
|
|
for f in os.listdir(test_def_dir):
|
|
basename, ext = os.path.splitext(f)
|
|
if ext != '.test':
|
|
continue
|
|
|
|
tc = TestCase(os.path.join(test_def_dir, f))
|
|
for key, val in self.cases.items():
|
|
if key.name == basename:
|
|
val.append(tc)
|
|
|
|
for key, val in g_complex_test.items():
|
|
for k, v in self.cases.items():
|
|
if k.name == key:
|
|
tc = TestCase()
|
|
tc.details = val
|
|
v.append(tc)
|
|
|
|
def run(self, results):
|
|
for key, val in self.cases.items():
|
|
print(key.id, 'number of tests:', len(val))
|
|
r = Runner(key, val)
|
|
r.evaluate(results)
|
|
|
|
|
|
g_complex_test = {
|
|
"gzip" :
|
|
[
|
|
TestDetails(['--help']),
|
|
TestDetails(['--version']),
|
|
TestDetails(['-asda']),
|
|
TestDetails(['-f', './data.txt']).set_files(['inputs/data.txt']). \
|
|
set_check(['data.txt.gz']),
|
|
TestDetails(['-l', './dec_data.txt.gz']).set_files(['inputs/dec_data.txt.gz']),
|
|
TestDetails(['-df', './dec_data.txt.gz']).set_files(['inputs/dec_data.txt.gz']).\
|
|
set_check(['dec_data.txt']),
|
|
TestDetails(['']),
|
|
],
|
|
|
|
"cat":
|
|
[
|
|
TestDetails(['--help']),
|
|
TestDetails(['--version']),
|
|
TestDetails(['data.txt']).set_files(['inputs/data.txt']),
|
|
TestDetails(['-n', 'data.txt']).set_files(['inputs/data.txt']),
|
|
],
|
|
|
|
"echo":
|
|
[
|
|
TestDetails(['--help']),
|
|
TestDetails(['--version']),
|
|
TestDetails(['Hello Wordl!']),
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
# Right now batches are combined, maybe it would make sense to separate batches from each other
|
|
# that can be useful when comparing performance of frontends
|
|
def main():
|
|
arg_parser = argparse.ArgumentParser(
|
|
formatter_class = argparse.RawDescriptionHelpFormatter)
|
|
|
|
arg_parser.add_argument('--lift',
|
|
help = "Path to the mcsema-lift binary",
|
|
required = True)
|
|
|
|
arg_parser.add_argument('--runtime_lib',
|
|
help = "Runtime library for lifted bitcode \
|
|
(e.g. libmcsema-rt64-6.0.a",
|
|
required = True)
|
|
|
|
arg_parser.add_argument('--shared_lib_dir',
|
|
help = "Directory that contains shared libraries used by original binaries",
|
|
default = "shared_libs",
|
|
required = False)
|
|
|
|
|
|
arg_parser.add_argument('--bin_dir',
|
|
help = "Directory that contains original binaries",
|
|
default = "bin",
|
|
required = False)
|
|
|
|
|
|
arg_parser.add_argument('--batch',
|
|
help = "Batch names",
|
|
nargs = '+',
|
|
required = False)
|
|
|
|
arg_parser.add_argument('--batch_dir',
|
|
help = 'Directories with cfgs',
|
|
nargs = '+',
|
|
required = False)
|
|
|
|
arg_parser.add_argument('--abi_lib_dir',
|
|
help = "Directory that contains bitcode types of external functions",
|
|
required = True)
|
|
|
|
arg_parser.add_argument('--dry_run',
|
|
help = 'Do not run any tests',
|
|
required = False)
|
|
|
|
arg_parser.add_argument('--save_log',
|
|
help = "Name of file to save result in json format",
|
|
required = False)
|
|
|
|
arg_parser.add_argument('--jobs',
|
|
help = "Number of threads to use",
|
|
default = 1,
|
|
required = False)
|
|
|
|
arg_parser.add_argument('--tags',
|
|
help = "Test only these tags from batch",
|
|
required = False)
|
|
|
|
args, command_args = arg_parser.parse_known_args()
|
|
check_arguments(args)
|
|
|
|
|
|
# Create directory to store recompiled binaries
|
|
# TemporaryDirectory() is not used, since we may want to have a look at recompiled
|
|
# code, maybe we want some --preserve option
|
|
test_dir = tempfile.mkdtemp(dir=os.getcwd())
|
|
|
|
# basename -> path
|
|
batched = {}
|
|
for batch in batches:
|
|
for f in os.listdir(batch):
|
|
if os.path.isfile(os.path.join(batch, f)):
|
|
basename, ext = os.path.splitext(f)
|
|
batched[basename] = os.path.join(batch, f)
|
|
|
|
|
|
configs = get_configs(tags_dir, args.tags, batched)
|
|
_todo = queue.Queue()
|
|
for c in configs:
|
|
_todo.put(c)
|
|
|
|
threads = []
|
|
results = {}
|
|
for i in range(int(args.jobs)):
|
|
t = threading.Thread(target=parallel_lift, args=(_todo, test_dir, results))
|
|
t.start()
|
|
threads.append(t)
|
|
for t in threads:
|
|
t.join()
|
|
|
|
Tester(configs, tags_dir).run(results)
|
|
print_results(results)
|
|
|
|
return
|
|
|
|
if __name__ == '__main__':
|
|
main()
|