/* 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.steps.heuristic;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import org.apache.log4j.Logger;
import de.rub.syssec.saaf.misc.config.Config;
import de.rub.syssec.saaf.misc.config.ConfigKeys;
import de.rub.syssec.saaf.model.analysis.AnalysisInterface;
import de.rub.syssec.saaf.model.analysis.HPatternInterface;
import de.rub.syssec.saaf.model.analysis.HResultInterface;
import de.rub.syssec.saaf.model.analysis.PatternType;
import de.rub.syssec.saaf.model.application.ClassInterface;
import de.rub.syssec.saaf.model.application.CodeLineInterface;
import de.rub.syssec.saaf.model.application.MethodInterface;
import de.rub.syssec.saaf.model.application.instruction.InstructionInterface;
import de.rub.syssec.saaf.model.application.instruction.InstructionType;
import de.rub.syssec.saaf.model.application.manifest.ManifestInterface;
/**
* Class to quickly check an APK against certain things.
* <p>
* Two examples would be the search for invocations of certain methods and
* whether some String can be found in the smali files.
* </p>
*
* TODO: This class needs a lot of love.
*
* @author Hanno Lemoine <hanno.lemoine@gdata.de>
* @author Johannes Hoffmann <johannes.hofffmann@rub.de>
*
*/
public class Heuristic {
private static final boolean DEBUG = Boolean.parseBoolean(System
.getProperty("debug.heuristic", "false"));
private static final String DEBUG_FILE = System.getProperty(
"debug.heuristic.file", "perf.log.bak");
private static final Logger LOGGER = Logger.getLogger(Heuristic.class);
private List<HPatternInterface> patterns;
public Heuristic(List<HPatternInterface> patterns) {
this.patterns = patterns;
}
/**
* Check the APK against all set patterns.
*
* @param ana
* the analysis holding representing the APK
* @return the results
*/
public LinkedList<HResultInterface> check(AnalysisInterface ana) {
LinkedList<HResultInterface> hResults = new LinkedList<HResultInterface>();
if (this.patterns.isEmpty()) {
LOGGER.error("The list of heuristic patterns is empty. Not checking anything.");
return new LinkedList<HResultInterface>();
}
// sort Pattern by PatternType
LinkedList<HPatternInterface> manifestPattern = new LinkedList<HPatternInterface>();
LinkedList<HPatternInterface> invokePattern = new LinkedList<HPatternInterface>();
LinkedList<HPatternInterface> smaliPattern = new LinkedList<HPatternInterface>();
LinkedList<HPatternInterface> methodPattern = new LinkedList<HPatternInterface>();
LinkedList<HPatternInterface> superclassPattern = new LinkedList<HPatternInterface>();
LinkedList<HPatternInterface> patchedCodePattern = new LinkedList<HPatternInterface>();
for (HPatternInterface pattern : patterns) {
if (pattern.isActive()) {
switch (pattern.getSearchin()) {
case MANIFEST:
manifestPattern.add(pattern);
break;
case INVOKE:
invokePattern.add(pattern);
break;
case SMALI:
smaliPattern.add(pattern);
break;
case METHOD_MOD:
methodPattern.add(pattern);
break;
case SUPERCLASS:
superclassPattern.add(pattern);
break;
case PATCHED_CODE:
patchedCodePattern.add(pattern);
break;
default:
LOGGER.warn("Unknown pattern type detected, ignoring.");
}
}
}
Date d1 = new Date(System.currentTimeMillis());
Date d2 = d1;
Date d3 = d1;
Date d4 = d1;
Date d5 = d1;
Date d6 = d1;
Date d7 = d1;
Date d8 = d1;
Config conf = Config.getInstance();
// check for MANIFEST-Pattern
if (DEBUG)
d1 = Calendar.getInstance().getTime();
if (conf.getBooleanConfigValue(ConfigKeys.HEURISTIC_PATTERN_MANIFEST)) {
LOGGER.debug("Start HeuristicSearch for " + manifestPattern.size()
+ " Manifest-Pattern.");
hResults.addAll(checkManifest(ana, manifestPattern));
}
// check for INVOKE-Pattern (only invokes given in a special format)
if (DEBUG)
d2 = Calendar.getInstance().getTime();
if (conf.getBooleanConfigValue(ConfigKeys.HEURISTIC_PATTERN_INVOKE)) {
LOGGER.debug("Start HeuristicSearch for " + invokePattern.size()
+ " INVOKE-Pattern.");
hResults.addAll(checkInvoke(ana, invokePattern));
}
// check for SMALI-Pattern (search only text in SMALI Files)
if (DEBUG)
d3 = Calendar.getInstance().getTime();
if (conf.getBooleanConfigValue(ConfigKeys.HEURISTIC_PATTERN_SMALI)) {
LOGGER.debug("Start HeuristicSearch for " + smaliPattern.size()
+ " SMALI-Pattern.");
hResults.addAll(checkSmali(ana, smaliPattern));
}
if (DEBUG)
d4 = Calendar.getInstance().getTime();
if (conf.getBooleanConfigValue(ConfigKeys.HEURISTIC_PATTERN_METHOD_MOD)) {
LOGGER.debug("Start HeuristicSearch for " + methodPattern.size()
+ " METHOD_MOD-Pattern.");
hResults.addAll(checkMethodDeclaration(ana, methodPattern));
}
if (DEBUG)
d5 = Calendar.getInstance().getTime();
if (conf.getBooleanConfigValue(ConfigKeys.HEURISTIC_PATTERN_SUPERCLASS)) {
LOGGER.debug("Start HeuristicSearch for "
+ superclassPattern.size() + " SUPERCLASS-Pattern.");
hResults.addAll(checkInheritance(ana, superclassPattern));
}
if (DEBUG)
d6 = Calendar.getInstance().getTime();
if (conf.getBooleanConfigValue(ConfigKeys.HEURISTIC_SEARCH_PATCHED_CODE)) {
LOGGER.debug("Start HeuristicSearch for "
+ patchedCodePattern.size() + " PATCHED_CODE-Pattern.");
hResults.addAll(searchPatchedCode(ana, patchedCodePattern));
}
if (DEBUG)
d7 = Calendar.getInstance().getTime();
int sumHValue = sumHValue(hResults);
ana.setHeuristicValue(sumHValue);
LOGGER.debug("Finished HeuristicSearch" + " for Application "
+ ana.getApp().getApplicationName() + " with "
+ hResults.size() + " Results"
+ " and summed HeuristicResult = " + sumHValue);
if (DEBUG)
d8 = Calendar.getInstance().getTime();
if (DEBUG) {
LOGGER.debug(ana.getApp().getApplicationName()
+ " Manifest-Pattern time: " + diffTime(d1, d2));
LOGGER.debug(ana.getApp().getApplicationName()
+ " INVOKE-Pattern time: " + diffTime(d2, d3));
LOGGER.debug(ana.getApp().getApplicationName()
+ " SMALI-Pattern time: " + diffTime(d3, d4));
LOGGER.debug(ana.getApp().getApplicationName()
+ " METHOD_MOD-Pattern time: " + diffTime(d4, d5));
LOGGER.debug(ana.getApp().getApplicationName()
+ " SUPERCLASS-Pattern time: " + diffTime(d5, d6));
LOGGER.debug(ana.getApp().getApplicationName()
+ " PATCHED_CODE-Pattern time: " + diffTime(d6, d7));
LOGGER.debug(ana.getApp().getApplicationName()
+ " Sum&Convert Results time: " + diffTime(d7, d8));
LOGGER.debug(ana.getApp().getApplicationName()
+ " Total time: " + diffTime(d1, d8));
}
// if (DEBUG) {
// // Achtung es hängt sehr von der Reihenfolge der Aufrufe ab
// // WENN checkINVOKE nach checkSMALI aufgerufen wird, braucht es im
// // Schnitt nur 1 ms ansonsten 30ms
// // TODO: Test mit größerem Sample und mehr INVOKE Pattern
// String name = "INV";
// Writer fw = null;
// try {
// fw = new FileWriter(DEBUG_FILE, true);
// fw.write("Performance: \t " + name + "\t" + d1.toString()
// + "\t" + (d2.getTime() - d1.getTime()) + "\t"
// + (d3.getTime() - d2.getTime()) + "\t"
// + (d4.getTime() - d3.getTime()) + "\t"
// + (d5.getTime() - d4.getTime()) + "\t"
// + (d5.getTime() - d1.getTime()) + "\n");
// } catch (IOException e) {
// LOGGER.info("Could not create file.");
// } finally {
// if (fw != null) try { fw.close(); } catch (IOException ignore) { }
// }
// }
return hResults;
}
/**
* Checks an APK against all patterns related to the Manifest file.
*
* @param ana
* the analysis representing the APK
* @param pattern
* the pattern
* @return all found results
*/
private LinkedList<HResultInterface> checkManifest(AnalysisInterface ana,
LinkedList<HPatternInterface> pattern) {
LinkedList<HResultInterface> hResults = new LinkedList<HResultInterface>();
ManifestInterface manifest = ana.getApp().getManifest();
if (manifest != null) {
for (HPatternInterface hPat : pattern) {
boolean patternMatched = true;
String[] patterns = hPat.getPattern().split("\\s+");
for (int z = 0; z < patterns.length; z++) {
if (patterns[z].trim().equals("noActivity")) {
patternMatched &= manifest.hasNoActivities();
} else if (patterns[z].trim().equals("priorityBR")) {
patternMatched &= manifest.hasPriorityBR();
} else {
patternMatched &= manifest.hasPermission("android."
+ patterns[z].trim());
}
if (!patternMatched) {
/*
* b/c all checks are ANDed with patternMatched we can
* bail out if it is false at any time
*/
break;
}
}
if (patternMatched) {
hResults.add(new HResult(ana, hPat));
}
}
}else{
LOGGER.warn("Could not find a Manifest. Skipping manifest patterns.");
}
return hResults;
}
/**
* Checks an APK against all patterns related to invoke opcodes.
*
* @param ana
* the analysis representing the APK
* @param pattern
* the pattern
* @return all found results
*/
private LinkedList<HResultInterface> checkInvoke(AnalysisInterface ana,
LinkedList<HPatternInterface> pattern) {
LinkedList<HResultInterface> hResults = new LinkedList<HResultInterface>();
for (HPatternInterface hPat : pattern) {
hResults.addAll(findInvokePattern(ana, hPat));
}
return hResults;
}
/**
* Checks an APK against all patterns related to smali bytecode.
*
* @param ana
* the analysis representing the APK
* @param pattern
* the pattern
* @return all found results
*/
private LinkedList<HResult> checkSmali(AnalysisInterface ana,
LinkedList<HPatternInterface> pattern) {
LinkedList<HResult> hResults = new LinkedList<HResult>();
LinkedList<ClassInterface> appFiles = ana.getApp().getAllSmaliClasss(
Config.getInstance().getBooleanConfigValue(
ConfigKeys.ANALYSIS_INCLUDE_AD_FRAMEWORKS));
for (ClassInterface sf : appFiles) {
LinkedList<CodeLineInterface> codeLines = sf.getAllCodeLines();
for (CodeLineInterface cl : codeLines) {
for (HPatternInterface hPat : pattern) {
if (cl.contains(hPat.getPattern().getBytes()))
hResults.add(new HResult(ana, hPat, cl));
}
}
}
return hResults;
}
/**
* Search for methods which contain dead code. This might be an indicator
* for a patched program as the compiler would normally not generate such
* code. See {@link BasicBlockInterface.hasDeadCode()} and {@link
* MethodInterface.hasUnlinkedBBs()} for more information about this.
*
* @param ana
* the analysis representing the APK
* @param pattern
* pattern the pattern
* @return all found results
*/
private LinkedList<HResult> searchPatchedCode(AnalysisInterface ana,
LinkedList<HPatternInterface> pattern) {
LinkedList<HResult> hResults = new LinkedList<HResult>();
LinkedList<ClassInterface> appFiles = ana.getApp().getAllSmaliClasss(
Config.getInstance().getBooleanConfigValue(
ConfigKeys.ANALYSIS_INCLUDE_AD_FRAMEWORKS));
for (ClassInterface sf : appFiles) {
for (MethodInterface method : sf.getMethods()) {
if (method.isProbablyPatched()) {
for (HPatternInterface hPat : pattern) {
hResults.add(new HResult(ana, hPat, method
.getCodeLines().getFirst()));
}
}
}
}
return hResults;
}
/**
* Searches for a pattern in all method declarations. Used, eg, to search
* for 'native' methods.
*
* @param ana
* ana the analysis representing the APK
* @param pattern
* pattern the pattern
* @return all found results
*/
private LinkedList<HResultInterface> checkMethodDeclaration(
AnalysisInterface ana, LinkedList<HPatternInterface> pattern) {
LinkedList<HResultInterface> hResults = new LinkedList<HResultInterface>();
LinkedList<ClassInterface> appFiles = ana.getApp().getAllSmaliClasss(
Config.getInstance().getBooleanConfigValue(
ConfigKeys.ANALYSIS_INCLUDE_AD_FRAMEWORKS));
for (ClassInterface sf : appFiles) {
for (HPatternInterface hPat : pattern) {
for (MethodInterface m : sf.getEmptyMethods()) {
if (m.getCodeLines().getFirst()
.contains(hPat.getPattern().getBytes())) {
hResults.add(new HResult(ana, hPat, m.getCodeLines()
.getFirst()));
}
}
}
}
return hResults;
}
/**
* This method checks against pattern which check whether some method is
* invoked in the APK or not.
*
* @param ana
* the analysis representing the APK
* @param pattern
* pattern the pattern
* @return all found results
*/
private LinkedList<HResultInterface> findInvokePattern(
AnalysisInterface ana, HPatternInterface pattern) {
LinkedList<HResultInterface> hResults = new LinkedList<HResultInterface>();
// Where to search
LinkedList<ClassInterface> appFiles = ana.getApp().getAllSmaliClasss(
Config.getInstance().getBooleanConfigValue(
ConfigKeys.ANALYSIS_INCLUDE_AD_FRAMEWORKS));
// What to search
if (pattern.getSearchin() == PatternType.INVOKE) {
byte[][] cm = new byte[2][];
String patternString = pattern.getPattern();
String[] pat = patternString.split("->");
cm[0] = pat[0].getBytes(); // Java package with class
cm[1] = pat[1].getBytes(); // Java method
for (ClassInterface sf : appFiles) {
LinkedList<CodeLineInterface> codeLines = sf.getAllCodeLines();
for (CodeLineInterface cl : codeLines) {
InstructionInterface i = cl.getInstruction();
if (i.getType() == InstructionType.INVOKE
|| i.getType() == InstructionType.INVOKE_STATIC) {
if (Arrays
.equals(i.getCalledClassAndMethod()[0], cm[0])
&& Arrays.equals(
i.getCalledClassAndMethod()[1], cm[1])) {
// we found the method!
hResults.add(new HResult(ana, pattern, cl));
}
}
}
}
}
return hResults;
}
/**
* Search for all classes which extend some given other class (the
* superclass).
*
* @param ana
* the analysis representing the APK
* @param pattern
* pattern the pattern
* @return all found results
*/
private LinkedList<HResultInterface> checkInheritance(
AnalysisInterface analysis, LinkedList<HPatternInterface> pattern) {
LinkedList<HResultInterface> hResults = new LinkedList<HResultInterface>();
LinkedList<ClassInterface> appFiles = analysis.getApp()
.getAllSmaliClasss(
Config.getInstance().getBooleanConfigValue(
ConfigKeys.ANALYSIS_INCLUDE_AD_FRAMEWORKS));
for (HPatternInterface p : pattern) {
String superClass = p.getPattern(); // search for this super class
for (ClassInterface sf : appFiles) {
if (superClass.equalsIgnoreCase(sf.getSuperClass())) {
// use the first codeline as it refers to the class which
// extends the superclass in question
hResults.add(new HResult(analysis, p, sf.getAllCodeLines()
.getFirst()));
}
}
}
return hResults;
}
/**
* TODO: Heuristic values are currently set to 0. This method will therefore
* also only return 0.
*
* @param hResults
* @return
*/
private int sumHValue(LinkedList<HResultInterface> hResults) {
// int x = 0;
// for (HResultInterface hResult : hResults) {
// x += hResult.getPattern().getHvalue();
// }
// return x;
return 0;
}
private String diffTime(Date d1, Date d2) {
Date tmp;
long diff, ms, s, m;
if (d2.before(d1)) {
tmp = d2;
d2 = d1;
d1 = tmp;
}
diff = d2.getTime() - d1.getTime();
ms = diff % 1000;
diff /= 1000;
s = diff % 60;
diff /= 60;
m = diff % 60;
diff /= 60;
return String.format("%02d:%02d:%02d.%04d", diff, m, s, ms);// h:mm:ss.ms
}
}