/* 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.analysis; import java.io.File; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; import org.apache.log4j.Logger; import de.rub.syssec.saaf.analysis.steps.CheckSimilartiyStep; import de.rub.syssec.saaf.analysis.steps.ParseSmaliStep; import de.rub.syssec.saaf.analysis.steps.ProgressHandler; import de.rub.syssec.saaf.analysis.steps.ProgressListener; import de.rub.syssec.saaf.analysis.steps.SetupFileSystemStep; import de.rub.syssec.saaf.analysis.steps.SetupLoggingStep; import de.rub.syssec.saaf.analysis.steps.SkipKnownAppStep; import de.rub.syssec.saaf.analysis.steps.Step; import de.rub.syssec.saaf.analysis.steps.ThrowRuntimeExceptions; import de.rub.syssec.saaf.analysis.steps.TrashOldAnalysisStep; import de.rub.syssec.saaf.analysis.steps.cfg.GenerateCFGStep; import de.rub.syssec.saaf.analysis.steps.cleanup.DeleteFilesStep; import de.rub.syssec.saaf.analysis.steps.decompile.DecompileToJavaStep; import de.rub.syssec.saaf.analysis.steps.extract.ExtractApkStep; import de.rub.syssec.saaf.analysis.steps.extract.FileCheckStep; import de.rub.syssec.saaf.analysis.steps.hash.GenerateFuzzyStep; import de.rub.syssec.saaf.analysis.steps.hash.GenerateHashesStep; import de.rub.syssec.saaf.analysis.steps.hash.Hash; import de.rub.syssec.saaf.analysis.steps.heuristic.HeuristicSearchStep; import de.rub.syssec.saaf.analysis.steps.metadata.CategorizePermissionsStep; import de.rub.syssec.saaf.analysis.steps.metadata.MatchAPICallsStep; import de.rub.syssec.saaf.analysis.steps.metadata.ParseMetaDataStep; import de.rub.syssec.saaf.analysis.steps.obfuscation.LengthBasedDetectObfuscationStep; import de.rub.syssec.saaf.analysis.steps.obfuscation.EntropyBasedDetectObfuscationStep; import de.rub.syssec.saaf.analysis.steps.reporting.GenerateReportStep; import de.rub.syssec.saaf.analysis.steps.slicing.SlicingStep; import de.rub.syssec.saaf.db.persistence.exceptions.InvalidEntityException; import de.rub.syssec.saaf.db.persistence.exceptions.PersistenceException; import de.rub.syssec.saaf.db.persistence.interfaces.EntityManagerFacade; import de.rub.syssec.saaf.misc.config.Config; import de.rub.syssec.saaf.misc.config.ConfigKeys; import de.rub.syssec.saaf.model.SAAFException; import de.rub.syssec.saaf.model.analysis.AnalysisException; import de.rub.syssec.saaf.model.analysis.AnalysisInterface; import de.rub.syssec.saaf.model.analysis.BTResultInterface; import de.rub.syssec.saaf.model.analysis.HResultInterface; import de.rub.syssec.saaf.model.application.ApplicationInterface; /** * This class takes care of all involved steps which are related to any analysis * operation of an APK. * * @author Hanno Lemoine <hanno.lemoine@gdata.de> * @author Johannes Hoffmann <johannes.hoffmann@rub.de> * */ public class Analysis implements AnalysisInterface { private ApplicationInterface app; private List<BTResultInterface> slicingResults; private List<HResultInterface> heuristicResults; private int heuristicValue; private Date creationTime = new Date(0); private Date startTime = creationTime; private Date stopTime = startTime; private Status status; private int analysisIdInDb = -1; // ID from the table in db private List<SAAFException> nonCriticalExceptions = new ArrayList<SAAFException>(); private List<SAAFException> criticalExceptions = new ArrayList<SAAFException>(); // the next 3 should be final and unmodifiable, but static init would be // very ugly private static final List<Step> PROCESSING_STEPS = new LinkedList<Step>(); private static final List<Step> ANALYSIS_STEPS = new LinkedList<Step>(); private static final List<Step> CLEANUP_STEPS = new LinkedList<Step>(); private boolean changed; private File reportFile = null; private ProgressHandler progressHandler; private static final Logger LOGGER = Logger.getLogger(Analysis.class); private static final boolean INIT_OK; /** * Static initializer. Sets up all the steps, these are always the same. */ static { boolean b = true; try { PROCESSING_STEPS.addAll(buildProcessingSteps()); ANALYSIS_STEPS.addAll(buildAnalysisSteps()); CLEANUP_STEPS.addAll(buildCleanupSteps()); } catch (PersistenceException e) { LOGGER.error("Could not build steps required for analysis!", e); b = false; } INIT_OK = b; } /** * Each analysis of each APK is handled by this class. * * @param app * the application to be analyzed * @throws AnalysisException * thrown if this class could not initialize */ public Analysis(ApplicationInterface app) throws AnalysisException { if (!INIT_OK) throw new AnalysisException( "Analysis initialization failed, see log!"); this.progressHandler = new ProgressHandler(); creationTime = Calendar.getInstance().getTime(); this.app = app; status = Status.NOT_STARTED; changed = true; } /** * Run all configured analysis steps. * * @throws PersistenceException * @throws AnalysisException */ public void run() throws PersistenceException, AnalysisException { LOGGER.debug("Preparing analysis of application " + app); LOGGER.debug("Setting up database connection"); EntityManagerFacade manager = Config.getInstance().getEntityManager(); LOGGER.debug("Configure the preprocessing and analysis steps"); logEnabledStepsToConfig(); startTime = Calendar.getInstance().getTime(); status = updateStatus(); LOGGER.info("Analysis for application " + app.getApplicationName() + " started\n\n"); try { // do what needs to be done so we can start analyzing doPreprocessing(); //check if preprocessing didn't result in skipping the apk if (status == Status.SKIPPED) { LOGGER.info("Further analysis steps for " + app.getApplicationName() + " are skipped."); } else { // run the different analyzes on the extracted apks' doAnalysis(); } stopTime = Calendar.getInstance().getTime(); status = updateStatus(); } catch (AnalysisException e) { // do not catch all Exceptions here handleCaughtException(e); } catch (NullPointerException e) { handleCaughtException(e); } catch (NoSuchElementException e) { handleCaughtException(e); } catch (ArrayIndexOutOfBoundsException e) { handleCaughtException(e); } finally { if (status != Status.SKIPPED) { if (Config.getInstance().getBooleanConfigValue(ConfigKeys.ANALYSIS_GENERATE_REPORT)) { doGenerateReport(); } try { // store the results int btrSize = 0; int hrSize = 0; if (getBTResults() != null) btrSize = getBTResults().size(); if (getHResults() != null) hrSize = getHResults().size(); LOGGER.info("Storing results. Backtracking: " + btrSize + " Heuristic: " + hrSize); // save Analysis with new status to DB manager.save(this); LOGGER.info("Results stored."); } catch (InvalidEntityException e) { LOGGER.error("Problem storing the exceptions for analysis " + this.app.getApplicationName()); } // cleanup doCleanUp(); } // close connections manager.shutdown(); } LOGGER.info("Analysis for application " + app.getApplicationName() + " completed\n\n"); } /** * @throws AnalysisException */ @Override public void doGenerateReport() throws AnalysisException { Step reporting = new GenerateReportStep( Config.getInstance(), true); reporting.process(this); } /** * @throws AnalysisException */ @Override public void doCleanUp() throws AnalysisException { for (Step step : CLEANUP_STEPS) { if (!step.process(this)) break; } } /** * @throws AnalysisException */ @Override public void doAnalysis() throws AnalysisException { int done = 0; this.progressHandler.notifyMax(PROCESSING_STEPS.size()); for (Step step : ANALYSIS_STEPS) { this.progressHandler.notifyProgress(step.getName()); if (!step.process(this)) { return; } this.progressHandler.notifyProgress(++done); } this.progressHandler.notifyFinsihed(); } /** * @throws AnalysisException */ @Override public void doPreprocessing() throws AnalysisException { int done = 0; this.progressHandler.notifyMax(PROCESSING_STEPS.size()); for (Step step : PROCESSING_STEPS) { this.progressHandler.notifyProgress(step.getName()); if (!step.process(this)) { status = Status.SKIPPED; return; } this.progressHandler.notifyProgress(++done); } } private void handleCaughtException(Exception e) { LOGGER.error("Analysis for " + app.getApplicationName() + " failed!", e); status = Status.FAILED; this.addCriticalException(e); } /** * Log the configured and therefore enabled steps. */ private void logEnabledStepsToConfig() { LOGGER.debug("Processing steps before analysis:"); for (Step step : PROCESSING_STEPS) { LOGGER.debug(step); } for (Step step : ANALYSIS_STEPS) { LOGGER.debug(step); } for (Step step : CLEANUP_STEPS) { LOGGER.debug(step); } } /** * Build analysis steps. These are the second steps run, after cleanup and * before processing, eg, Program Slicing. * * @return the steps * @throws PersistenceException */ private static List<Step> buildAnalysisSteps() throws PersistenceException { Config config = Config.getInstance(); EntityManagerFacade manager = config.getEntityManager(); List<Step> analysisSteps = new LinkedList<Step>(); analysisSteps.add(new TrashOldAnalysisStep(config, manager .getAnalysisManager(), config.getBooleanConfigValue(ConfigKeys.ANALYSIS_KEEP_ONLY_ONE))); analysisSteps.add(new EntropyBasedDetectObfuscationStep(config, true)); analysisSteps.add(new CategorizePermissionsStep(config, true)); analysisSteps.add(new HeuristicSearchStep(config, manager .gethPatternManager().readAll(), config.getBooleanConfigValue(ConfigKeys.ANALYSIS_DO_HEURISTIC))); analysisSteps.add(new ThrowRuntimeExceptions(config, false)); analysisSteps.add(new SlicingStep(config, manager.getBtPatternManager() .readAll(), config.getBooleanConfigValue(ConfigKeys.ANALYSIS_DO_BACKTRACK))); // similarity checking is not implemented (just a dummy class) analysisSteps.add(new CheckSimilartiyStep(config, false)); analysisSteps.add(new GenerateCFGStep(config, config.getBooleanConfigValue(ConfigKeys.ANALYSIS_GENERATE_CFG))); return analysisSteps; } /** * Build processing steps. These are the first steps run and set everything * up, eg, APK unpacking. * * @param manager2 * @return the steps * @throws PersistenceException */ private static List<Step> buildProcessingSteps() throws PersistenceException { Config config = Config.getInstance(); EntityManagerFacade manager = config.getEntityManager(); List<Step> processingSteps = new LinkedList<Step>(); processingSteps.add(new FileCheckStep(config, true)); processingSteps.add(new GenerateHashesStep(config, true)); processingSteps.add(new SkipKnownAppStep(config, manager .getAnalysisManager(),config.getBooleanConfigValue(ConfigKeys.ANALYSIS_SKIP_KNOWN_APP))); processingSteps.add(new SetupFileSystemStep(config, false, true)); processingSteps.add(new SetupLoggingStep(config, true)); processingSteps.add(new ThrowRuntimeExceptions(config, false)); processingSteps.add(new ExtractApkStep(config, true)); processingSteps.add(new ParseMetaDataStep(config, true)); processingSteps.add(new ParseSmaliStep(config, true)); processingSteps.add(new LengthBasedDetectObfuscationStep(config, true)); // processingSteps.add(new EntropyBasedDetectObfuscationStep(config, true)); processingSteps.add(new MatchAPICallsStep(config, true)); processingSteps.add(new DecompileToJavaStep(config, config.getBooleanConfigValue(ConfigKeys.ANALYSIS_GENERATE_JAVA))); processingSteps.add(new GenerateFuzzyStep(config, config.getBooleanConfigValue(ConfigKeys.ANALYSIS_GENERATE_FUZZYHASH))); return processingSteps; } /** * Build cleanup steps. These are run after all other steps. * * @return the steps */ private static List<Step> buildCleanupSteps() { List<Step> cleanupSteps = new LinkedList<Step>(); cleanupSteps.add(new DeleteFilesStep(Config.getInstance(),!Config.getInstance().getBooleanConfigValue(ConfigKeys.ANALYSIS_KEEP_FILES))); // delete // old // files // if // requested return cleanupSteps; } /** * Increment the status of the analysis, eg, from NOT_STARTED to RUNNING * unless already in FAILED or EXCEPTION state. * * @return the new status */ private Status updateStatus() { switch (status) { case NOT_STARTED: status = Status.RUNNING; break; case RUNNING: if (nonCriticalExceptions.isEmpty()) status = Status.FINISHED; else status = Status.FINISHED_WITH_EXCEPTION; break; case FINISHED: // status locked case SKIPPED: case FAILED: case FINISHED_WITH_EXCEPTION: break; default: LOGGER.warn("Unknown Status, will default to FAILED."); status = Status.FAILED; break; } setChanged(true); return status; } @Override public void setHeuristicValue(int heuristicValue) { this.heuristicValue = heuristicValue; setChanged(true); } @Override public int getId() { return analysisIdInDb; } /** * Set a new ID. Can only be used once. * * @param analysisIdInDb * the id which is used in the DB */ @Override public void setId(int id) { if (analysisIdInDb == -1) { analysisIdInDb = id; } } @Override public Status getStatus() { return status; } @Override public void setStatus(Status status) { this.status = status; setChanged(true); } @Override public ApplicationInterface getApp() { return app; } @Override public int getHeuristicValue() { return heuristicValue; } @Override public Date getStartTime() { return startTime; } @Override public Date getStopTime() { return stopTime; } @Override public Date getCreationTime() { return creationTime; } @Override public List<BTResultInterface> getBTResults() { return slicingResults; } @Override public List<HResultInterface> getHResults() { return heuristicResults; } /** * Get all exceptions that occurred during the analysis and were handled * locally. * * @return the nonCriticalExceptions */ @Override public List<SAAFException> getNonCriticalExceptions() { return nonCriticalExceptions; } /** * Set the exceptions that occurred during the analysis and were handled * locally. * * @param nonCriticalExceptions * the nonCriticalExceptions to set */ @Override public void setNonCriticalExceptions(List<SAAFException> backTrackExceptions) { this.nonCriticalExceptions = backTrackExceptions; setChanged(true); } @Override public void setApp(ApplicationInterface app) { this.app = app; setChanged(true); } @Override public void setBTResults(List<BTResultInterface> btResults) { this.slicingResults = btResults; setChanged(true); } @Override public void setHResults(List<HResultInterface> heuristicResults) { this.heuristicResults = heuristicResults; setChanged(true); } @Override public void setChanged(boolean changed) { this.changed = changed; } @Override public boolean isChanged() { return changed; } @Override public List<SAAFException> getCriticalExceptions() { return criticalExceptions; } @Override public void setCriticalExceptions(List<SAAFException> criticalExceptions) { this.criticalExceptions = criticalExceptions; } @Override public String toString() { return app.getApplicationName() + "_" + app.getMessageDigest(Hash.DEFAULT_DIGEST); } @Override public File getReportFile(){ return reportFile; } @Override public void setReportFile(File report) { this.reportFile=report; } @Override public void addNonCriticalException(Exception e) { nonCriticalExceptions.add(new SAAFException(e.getMessage(), e, this)); } @Override public void addCriticalException(Exception e) { criticalExceptions.add(new SAAFException(e.getMessage(), e, this)); } @Override public void addProgressListener(ProgressListener listener) { this.progressHandler.addProgressListener(listener); } }