/* SAAF: A static analyzer for APK files.
* Copyright (C) 2013 syssec.rub.de
*
* 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 <http://www.gnu.org/licenses/>.
*/
package de.rub.syssec.saaf;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.util.Properties;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.apache.commons.cli.UnrecognizedOptionException;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.jf.util.ConsoleUtil;
import org.jf.util.SmaliHelpFormatter;
import de.rub.syssec.saaf.db.DatabaseHelper;
import de.rub.syssec.saaf.db.datasources.DataSourceException;
import de.rub.syssec.saaf.db.persistence.exceptions.InvalidEntityException;
import de.rub.syssec.saaf.db.persistence.exceptions.PersistenceException;
import de.rub.syssec.saaf.gui.MainWindow;
import de.rub.syssec.saaf.misc.config.Config;
import de.rub.syssec.saaf.misc.config.ConfigKeys;
import de.rub.syssec.saaf.model.APICalls;
public class Main {
private static final Logger LOGGER = Logger.getLogger(Main.class);
private static final String VERSION_PROPERTIES = "de/rub/syssec/saaf/version.properties";
public static String title;
private static Options basicOptions = new Options();
private static Options headlessOptions = new Options();
private static Options guiOptions = new Options();
private static Options reportDbLogOptions = new Options();
private static Options options = new Options();
private static Properties props;
private static File apkPath;
private static int exitcode = 0;
public static void main(String[] args) throws Exception {
// make sure log4j configuration is read before doing anything else
updateLog4jConfiguration(false, false);
try {
//Setup the commandline
buildOptions();
Config conf = Config.getInstance();
//Parse the arguments and adjust configuration accordingly
processCommandline(args);
//Check if the adjusted configuration contains any errors/inconsistencies
conf.validate();
//prepare database and filesystem
prepare(conf);
//should we just watch a folder for incoming apks?
if(conf.getBooleanConfigValue(ConfigKeys.DAEMON_ENABLED))
{
String watched = conf.getConfigValue(ConfigKeys.DAEMON_DIRECTORY);
long interval = conf.getIntConfigValue(ConfigKeys.DAEMON_POLLING_INTERVAL,5000);
FolderWatcher watcher = new FolderWatcher(watched,interval);
watcher.startWatching();
}
// Create GUI?
else if (conf.getBooleanConfigValue(
ConfigKeys.ANALYSIS_IS_HEADLESS)) {
// no GUI
exitcode = Headless.startAnalysis(apkPath);
} else {
// yes, I want a GUI
// Schedule a job for the event-dispatching thread:
// creating and showing this Application's GUI.
// TODO: use apk_path in gui mode!
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
MainWindow m = new MainWindow();
m.createAndShowGUI();
}
});
}
} catch (Exception e) {
LOGGER.error("An error occured", e);
System.exit(1);
}
exit();
}
/**
* Reads the config file and prepares database and filessystem accordingly.
*
* @param conf
* @throws PersistenceException
* @throws SQLException
* @throws IOException
* @throws InvalidEntityException
* @throws DataSourceException
*/
private static void prepare(Config conf) throws PersistenceException,
SQLException, IOException, InvalidEntityException,
DataSourceException {
if (conf.getBooleanConfigValue(ConfigKeys.ANALYSIS_DROP_DB_AND_FILES)) {
if (!conf.getBooleanConfigValue(ConfigKeys.DATABASE_DISABLED)) {
LOGGER.info("Dropping database tables...");
DatabaseHelper dbh = new DatabaseHelper(conf);
dbh.dropTables();
dbh.getConnection().close();
}
LOGGER.info("Deleting directories...");
FileUtils.deleteDirectory(new File(conf
.getConfigValue(ConfigKeys.DIRECTORY_APPS)));
File f = new File(
conf.getConfigValue(ConfigKeys.DIRECTORY_APPS));// necessary?
f.mkdirs();// necessary?
FileUtils.deleteDirectory(new File(conf
.getConfigValue(ConfigKeys.DIRECTORY_APPS)));
f = new File(conf.getConfigValue(ConfigKeys.DIRECTORY_BYTECODE));// necessary?
f.mkdirs();// necessary?
}
APICalls.readAPICalls();
if (!conf.getBooleanConfigValue(ConfigKeys.DATABASE_DISABLED)) {
LOGGER.info("Checking DB and creating tables if necessary...");
DatabaseHelper dbh = new DatabaseHelper(conf);
dbh.createDatabaseSchema(); // New DB Layout
dbh.populateTables();
dbh.getConnection().close();
LOGGER.info("DB check successful.");
}
if (!conf.getBooleanConfigValue(ConfigKeys.ANALYSIS_KEEP_FILES)) {
// Add a shutdown hook which ensures cleanup if the VM exists.
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
LOGGER.debug("Running ShutdownHook: Deleting directories as -k/--keep was not requested.");
File dir = new File(Config.getInstance()
.getConfigValue(ConfigKeys.DIRECTORY_APPS,
"apps"));
FileUtils.deleteQuietly(dir);
dir.mkdir();
dir = new File(Config.getInstance().getConfigValue(
ConfigKeys.DIRECTORY_BYTECODE, "bytecode"));
FileUtils.deleteQuietly(dir);
dir.mkdir();
LOGGER.debug("ShutdownHook finished.");
}
});
}
}
/**
* Parses the commandline and sets config parameters accordingly.
*
* @param args
* @return
* @throws ParseException
*/
private static void processCommandline(String[] args)
throws ParseException {
CommandLineParser parser = new PosixParser();
CommandLine cmdLine = null;
apkPath = null;
try {
cmdLine = parser.parse(options, args);
} catch (UnrecognizedOptionException e) {
System.out.println("Found an unrecognized option( "
+ e.getMessage()
+ " ), the following possibilities are supported: ");
usage();
exit();
}
String[] remainingArgs = cmdLine.getArgs();
switch (remainingArgs.length) {
case 0:
apkPath = null;
break;
case 1:
apkPath = new File(remainingArgs[0]);
break;
default:
LOGGER.error("You can't insert more than one Path!");
for (int i = 0; i < remainingArgs.length; i++) {
System.err.print("Unknown arguments: " + remainingArgs[i]
+ "\n");
}
usage(true);
exit();
}
if (cmdLine.hasOption(props.getProperty("options.version.short"))) {
version();
exit();
}
if (cmdLine.hasOption(props.getProperty("options.help.short"))) {
usage();
exit();
}
if (cmdLine.hasOption(props.getProperty("options.nobt.short"))
&& cmdLine.hasOption(props
.getProperty("options.noheuristic.short"))) {
LOGGER.error("You diabled quick checks as well as program slicing, this is currently not supported.");
// usage();
exit();
}
parseOptions(cmdLine);
}
private static void exit() {
System.exit(exitcode);
}
/**
* Import properties from outsourced file The values for the
* properties/options are outsourced in a ASCII-File, so we have to import
* them first.
*
* @author Tilman Bender
* @return commandline properties
*/
private static Properties parseVersionProperties() {
InputStream in = Main.class.getClassLoader().getResourceAsStream(
VERSION_PROPERTIES);
Properties props = null;
if (in != null) {
props = new Properties();
try {
props.load(in);
} catch (IOException e) {
System.out
.println("Could not load commandline properties. Exiting");
exit();
}
} else {
System.out
.println("Could not load commandline properties. Exiting");
exit();
}
return props;
}
public static Properties getProperties() {
return props;
}
/**
* Creates the options for the appache.common.cli parser
*
* @author Hanno Lemoine <hanno.lemoine@gdata.de>
* @param props
*/
private static void buildOptions() {
props = parseVersionProperties();
if (props != null) {
title = props.getProperty("software.name");
// VERSION = props.getProperty("software.version");
} else {
System.out
.println("Could not load commandline properties. Exiting.");
exit();
}
basicOptions.addOption(props.getProperty("options.version.short"),
props.getProperty("options.version.long"), false,
props.getProperty("options.version.descr"));
basicOptions.addOption(props.getProperty("options.help.short"),
props.getProperty("options.help.long"), false,
props.getProperty("options.help.descr"));
basicOptions.addOption(props.getProperty("options.drop.short"),
props.getProperty("options.drop.long"), false,
props.getProperty("options.drop.descr"));
basicOptions.addOption(props.getProperty("options.color.short"),
props.getProperty("options.color.long"), false,
props.getProperty("options.color.descr"));
basicOptions.addOption(props.getProperty("options.colorinverse.short"),
props.getProperty("options.colorinverse.long"), false,
props.getProperty("options.colorinverse.descr"));
basicOptions.addOption(props.getProperty("options.genjava.short"),
props.getProperty("options.genjava.long"), false,
props.getProperty("options.genjava.descr"));
basicOptions.addOption(props.getProperty("options.fuzzy.short"),
props.getProperty("options.fuzzy.long"), false,
props.getProperty("options.fuzzy.descr"));
// basicOptions.addOption("NgJava", "no-gJava", false,
// "do not generate Java source code.");
headlessOptions.addOption(props.getProperty("options.headless.short"),
props.getProperty("options.headless.long"), false,
props.getProperty("options.headless.descr"));
headlessOptions.addOption(props.getProperty("options.skip.short"),
props.getProperty("options.skip.long"), false,
props.getProperty("options.skip.descr"));
headlessOptions.addOption(props.getProperty("options.keep.short"),
props.getProperty("options.keep.long"), false,
props.getProperty("options.keep.descr"));
headlessOptions.addOption(
props.getProperty("options.noheuristic.short"),
props.getProperty("options.noheuristic.long"), false,
props.getProperty("options.noheuristic.descr"));
headlessOptions.addOption(props.getProperty("options.nobt.short"),
props.getProperty("options.nobt.long"), false,
props.getProperty("options.nobt.descr"));
headlessOptions.addOption(props.getProperty("options.cfg.short"),
props.getProperty("options.cfg.long"), false,
props.getProperty("options.cfg.descr"));
headlessOptions.addOption(
props.getProperty("options.hl.recursive.short"),
props.getProperty("options.hl.recursive.long"), false,
props.getProperty("options.hl.recursive.descr"));
headlessOptions.addOption(
props.getProperty("options.hl.filelist.short"),
props.getProperty("options.hl.filelist.long"), false,
props.getProperty("options.hl.filelist.descr"));
headlessOptions.addOption(
props.getProperty("options.hl.singlethreaded.short"),
props.getProperty("options.hl.singlethreaded.long"), false,
props.getProperty("options.hl.singlethreaded.descr"));
// option for running as daemon that watches a folder
headlessOptions.addOption(props.getProperty("options.daemon.short"),
props.getProperty("options.daemon.long"), true,
props.getProperty("options.daemon.descr"));
// TODO does not work yet
// headlessOptions.addOption("sc", false, "do Similarity Check");
// headlessOptions.addOption("Nsc", "no-sc", false,
// "do not do Similarity Check");
// TODO: Check if ignore-errors is still used
// headlessOptions.addOption("ie", "ignore-errors", false,
// "do not stop, if an analysis of an apk has crashed");
// TODO: @Hanno: Check if this still works!
// headlessOptions.addOption("doa", "del-old-analyses", false,
// "delete all existing analyses in the database, before making a new one.");
reportDbLogOptions.addOption(props.getProperty("options.nodb.short"),
props.getProperty("options.nodb.long"), false,
props.getProperty("options.nodb.descr"));
reportDbLogOptions.addOption(props.getProperty("options.report.short"),
props.getProperty("options.report.long"), true,
props.getProperty("options.report.descr"));
reportDbLogOptions.addOption(
props.getProperty("options.rtemplate.short"),
props.getProperty("options.rtemplate.long"), true,
props.getProperty("options.rtemplate.descr"));
reportDbLogOptions.addOption(props.getProperty("options.log.short"),
props.getProperty("options.log.long"), true,
props.getProperty("options.log.descr"));
reportDbLogOptions.getOption(props.getProperty("options.log.short"))
.setArgName("file");
guiOptions.addOption(props.getProperty("options.gui.short"),
props.getProperty("options.gui.long"), false,
props.getProperty("options.gui.descr"));
// include all sub-options in "options"
for (Object option : basicOptions.getOptions()) {
options.addOption((Option) option);
}
for (Object option : headlessOptions.getOptions()) {
options.addOption((Option) option);
}
for (Object option : guiOptions.getOptions()) {
options.addOption((Option) option);
}
for (Object option : reportDbLogOptions.getOptions()) {
options.addOption((Option) option);
}
}
/**
* Prints the usage/help message.
*
* @author Hanno Lemoine <Hanno.Lemoine@gdata.de> Thanks to Ben Gruver
* (JesusFreke)
*/
private static void usage(boolean printHeadlessGuiOptions) {
SmaliHelpFormatter formatter = new SmaliHelpFormatter();
int consoleWidth = ConsoleUtil.getConsoleWidth();
formatter.setWidth(consoleWidth);
PrintWriter writer = new PrintWriter(System.out);
writer.write("SAAF Copyright (C) 2013 syssec.rub.de\n");
writer.write("This program comes with ABSOLUTELY NO WARRANTY.\n");
writer.write("This is free software, and you are welcome to redistribute it\n");
writer.write("under certain conditions.");
writer.write("\n\n#########################################\n");
writer.write("# SAAF: A static analyzer for APK files #\n");
writer.write("#########################################\n");
writer.write("\nUsage: java -jar saaf.jar [options] [file/directory]");
writer.write("\nIf no options are set, SAAF will start in GUI mode.\n");
writer.write("\nBasic Options:\n");
formatter.printOptions(writer, consoleWidth, basicOptions, 1, 3);
if (printHeadlessGuiOptions) {
writer.write("\nHeadless Options:\n");
formatter.printOptions(writer, consoleWidth, headlessOptions, 1, 3);
writer.write("\nGUI Options:\n");
formatter.printOptions(writer, consoleWidth, guiOptions, 1, 3);
writer.write("\nReport, DB and Log Options:\n");
formatter.printOptions(writer, consoleWidth, reportDbLogOptions, 1,
3);
}
writer.flush();
// Do not close writer, otherwise System.out would be dead.
}
private static void usage() {
usage(true);
}
/**
* Prints the version message.
*/
private static void version() {
System.out.println(props.getProperty("software.name") + " "
+ props.getProperty("software.version")
+ props.getProperty("software.descr"));
exit();
}
private static void parseOptions(CommandLine cmdLine) {
Config conf = Config.getInstance();
if (cmdLine.hasOption(props.getProperty("options.headless.short"))) {
conf.setBooleanConfigValue(ConfigKeys.ANALYSIS_IS_HEADLESS, true);
}
if (cmdLine.hasOption(props.getProperty("options.color.long"))) {
conf.setBooleanConfigValue(ConfigKeys.LOGGING_USE_COLOR, true);
conf.setBooleanConfigValue(ConfigKeys.LOGGING_USE_INVERSE_COLOR,
false);
}
if (cmdLine.hasOption(props.getProperty("options.colorinverse.short"))) {
conf.setBooleanConfigValue(ConfigKeys.LOGGING_USE_COLOR, true);
conf.setBooleanConfigValue(ConfigKeys.LOGGING_USE_INVERSE_COLOR,
true);
}
updateLog4jConfiguration(
conf.getBooleanConfigValue(ConfigKeys.LOGGING_USE_COLOR),
conf.getBooleanConfigValue(ConfigKeys.LOGGING_USE_INVERSE_COLOR));
if (cmdLine.hasOption(props.getProperty("options.genjava.short"))) {
conf.setBooleanConfigValue(ConfigKeys.ANALYSIS_GENERATE_JAVA, true);
}
if (cmdLine.hasOption(props.getProperty("options.drop.short"))) {
conf.setBooleanConfigValue(ConfigKeys.ANALYSIS_DROP_DB_AND_FILES,
true);
}
if (cmdLine.hasOption(props.getProperty("options.skip.short"))) {
conf.setBooleanConfigValue(ConfigKeys.ANALYSIS_SKIP_KNOWN_APP, true);
}
if (cmdLine.hasOption(props.getProperty("options.hl.recursive.short"))) {
conf.setBooleanConfigValue(ConfigKeys.RECURSIVE_DIR_ANALYSIS, true);
}
if (cmdLine.hasOption(props.getProperty("options.hl.filelist.short"))) {
conf.setBooleanConfigValue(ConfigKeys.USE_FILE_LIST, true);
}
if (cmdLine.hasOption(props
.getProperty("options.hl.singlethreaded.short"))) {
conf.setBooleanConfigValue(ConfigKeys.MULTITHREADING_ENABLED, false);
}
if (cmdLine.hasOption(props.getProperty("options.nodb.short"))) {
conf.setBooleanConfigValue(ConfigKeys.DATABASE_DISABLED, true);
}
// feature #35: run without database
if (conf.getBooleanConfigValue(ConfigKeys.DATABASE_DISABLED)) {
// disable database backend
conf.setBooleanConfigValue(ConfigKeys.DATABASE_DISABLED, true);
// if we are headless (i.e. no other way to see the results
if (cmdLine.hasOption(props.getProperty("options.headless.short"))) {
// automatically turn on reporting
conf.setBooleanConfigValue(ConfigKeys.ANALYSIS_GENERATE_REPORT,
true);
}
}
if (cmdLine.hasOption(props.getProperty("options.gui.short"))
&& cmdLine.hasOption(props
.getProperty("options.headless.short"))) {
LOGGER.error("You have to decide if GUI or HEADLESS mode. Both is "
+ "not possible!");
exit();
}
if (cmdLine.hasOption(props.getProperty("options.daemon.short"))) {
conf.setBooleanConfigValue(ConfigKeys.ANALYSIS_IS_HEADLESS, true);
conf.setBooleanConfigValue(ConfigKeys.DAEMON_ENABLED, true);
String watched = cmdLine.getOptionValue(props
.getProperty("options.daemon.short"));
conf.setConfigValue(ConfigKeys.DAEMON_DIRECTORY, watched);
}
if (cmdLine.hasOption(props.getProperty("options.headless.short"))) {
conf.setBooleanConfigValue(ConfigKeys.ANALYSIS_IS_HEADLESS, true);
}
if (cmdLine.getOptions().length <= 0
|| cmdLine.hasOption(props.getProperty("options.gui.short"))) {
conf.setBooleanConfigValue(ConfigKeys.ANALYSIS_IS_HEADLESS, false);
}
// What to analyze?
if (cmdLine.hasOption(props.getProperty("options.noheuristic.short"))) {
conf.setBooleanConfigValue(ConfigKeys.ANALYSIS_DO_HEURISTIC, false);
}
if (cmdLine.hasOption(props.getProperty("options.nobt.short"))) {
conf.setBooleanConfigValue(ConfigKeys.ANALYSIS_DO_BACKTRACK, false);
}
// if (cmdLine.hasOption("no-sc"))
// Config.DO_SIMILARITY = false;
// if (cmdLine.hasOption("sc"))
// Config.DO_SIMILARITY = true;
// How to analyze?
// if (cmdLine.hasOption("ignore-errors"))
// Config.QUIT_ON_ERROR = false;
// if (cmdLine.hasOption("skip-known-apps"))
// Config.SKIP_KNOWN_APP = true;
// if (cmdLine.hasOption("del-old-analyses"))
// Config.HOLD_ONLY_ONE_ANA_PER_APP = true;
if (cmdLine.hasOption(props.getProperty("options.report.short"))) {
conf.setBooleanConfigValue(ConfigKeys.ANALYSIS_GENERATE_REPORT,
true);
String reportPath = cmdLine.getOptionValue(props
.getProperty("options.report.short"));
conf.setConfigValue(ConfigKeys.DIRECTORY_REPORTS, reportPath);
}
if (cmdLine.hasOption(props.getProperty("options.rtemplate.short"))) {
String templateName = cmdLine.getOptionValue(props
.getProperty("options.rtemplate.short"));
conf.setConfigValue(ConfigKeys.REPORTING_TEMPLATE_GROUP_DEFAULT,
templateName);
}
if (cmdLine.hasOption(props.getProperty("options.log.short"))) {
conf.setBooleanConfigValue(ConfigKeys.LOGGING_CREATE_SEPERATE, true);
String logpath = cmdLine.getOptionValue(props
.getProperty("options.log.short"));
conf.setConfigValue(ConfigKeys.LOGGING_FILE_PATH, logpath);
}
if (cmdLine.hasOption(props.getProperty("options.cfg.short"))) {
conf.setBooleanConfigValue(ConfigKeys.ANALYSIS_GENERATE_CFG, true);
}
if (cmdLine.hasOption(props.getProperty("options.fuzzy.short"))) {
conf.setBooleanConfigValue(ConfigKeys.ANALYSIS_GENERATE_FUZZYHASH,
true);
}
if (cmdLine.hasOption(props.getProperty("options.keep.short"))) {
conf.setBooleanConfigValue(ConfigKeys.ANALYSIS_KEEP_FILES, true);
}
}
/**
* Load or update log4j properties from 'conf/log4j.properties' file. If
* useColor is set, the Appender Layout is overwritten with the
* ANSIColorLayout.
*
* @author Hanno Lemoine <hanno.lemoine@gdata.de>
* @param useColor
* Boolean to activate ANSIColorLayout.
* @param inverseColor
* set if you have a white background
*/
private static void updateLog4jConfiguration(Boolean useColor,
Boolean inverseColor) {
Properties log4j_props = new Properties();
try {
log4j_props.load(new FileInputStream("conf/log4j.properties"));
} catch (IOException e) {
System.err
.println("Error: Cannot laod log4j configuration file. Exiting.");
exit();
}
if (useColor) {
log4j_props.setProperty("log4j.appender.A1.layout",
"de.rub.syssec.saaf.misc.log4j.ANSIColorLayout");
}
if (useColor && inverseColor) {
// final String cWhite = "\u001B[1;37m";
final String cBlack = "\u001B[0;30m";
final String cGray = "\u001B[1;30m";
final String cRed = "\u001B[0;31m";
final String cYel = "\u001B[1;33m";
final String cCya = "\u001B[0;36m";
log4j_props.setProperty("log4j.appender.A1.layout.all", cBlack);
log4j_props.setProperty("log4j.appender.A1.layout.fatal", cRed);
log4j_props.setProperty("log4j.appender.A1.layout.error", cRed);
log4j_props.setProperty("log4j.appender.A1.layout.warn", cYel);
log4j_props.setProperty("log4j.appender.A1.layout.info", cGray);
log4j_props.setProperty("log4j.appender.A1.layout.debug", cCya);
log4j_props
.setProperty("log4j.appender.A1.layout.stacktrace", cRed);
log4j_props.setProperty("log4j.appender.A1.layout.defaultcolor",
cBlack);
}
LogManager.resetConfiguration();
PropertyConfigurator.configure(log4j_props);
}
}