/******************************************************************************* * Copyright (c) 2012 Secure Software Engineering Group at EC SPRIDE. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser Public License v2.1 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * * Contributors: Christian Fritz, Steven Arzt, Siegfried Rasthofer, Eric * Bodden, and others. ******************************************************************************/ package edu.psu.cse.siis.ic3; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import soot.Main; import soot.PackManager; import soot.Scene; import soot.SootClass; import soot.SootMethod; import soot.jimple.infoflow.android.AnalyzeJimpleClass; import soot.jimple.infoflow.android.data.AndroidMethod; import soot.jimple.infoflow.android.resources.ARSCFileParser; import soot.jimple.infoflow.android.resources.ARSCFileParser.AbstractResource; import soot.jimple.infoflow.android.resources.ARSCFileParser.StringResource; import soot.jimple.infoflow.android.resources.LayoutControl; import soot.jimple.infoflow.android.resources.LayoutFileParser; import soot.jimple.infoflow.data.SootMethodAndClass; import soot.jimple.infoflow.entryPointCreators.AndroidEntryPointCreator; import soot.options.Options; public class SetupApplication { private final Logger logger = LoggerFactory.getLogger(getClass()); private final Map<String, Set<SootMethodAndClass>> callbackMethods = new HashMap<String, Set<SootMethodAndClass>>(10000); private Set<String> entrypoints = null; private String appPackageName = ""; private final String apkFileLocation; private final String classDirectory; private final String androidClassPath; private AndroidEntryPointCreator entryPointCreator; public SetupApplication(String apkFileLocation, String classDirectory, String androidClassPath) { this.apkFileLocation = apkFileLocation; this.classDirectory = classDirectory; this.androidClassPath = androidClassPath; } /** * Gets the entry point creator used for generating the dummy main method emulating the Android * lifecycle and the callbacks. Make sure to call calculateSourcesSinksEntryPoints() first, or you * will get a null result. * * @return The entry point creator */ public AndroidEntryPointCreator getEntryPointCreator() { return entryPointCreator; } /** * Prints list of classes containing entry points to stdout */ public void printEntrypoints() { if (logger.isDebugEnabled()) { if (this.entrypoints == null) { logger.debug("Entry points not initialized"); } else { logger.debug("Classes containing entry points:"); for (String className : entrypoints) { logger.debug("\t" + className); } logger.debug("End of Entrypoints"); } } } /** * Calculates the sets of sources, modifiers, entry points, and callbacks methods for the given * APK file. * * @param sourceMethods The set of methods to be considered as sources * @param modifierMethods The set of methods to be considered as modifiers * @throws IOException Thrown if the given source/modifier file could not be read. */ public Map<String, Set<String>> calculateSourcesSinksEntrypoints( Set<AndroidMethod> sourceMethods, Set<AndroidMethod> modifierMethods, String packageName, Set<String> entryPointClasses) throws IOException { // To look for callbacks, we need to start somewhere. We use the Android // lifecycle methods for this purpose. this.appPackageName = packageName; this.entrypoints = entryPointClasses; boolean parseLayoutFile = !apkFileLocation.endsWith(".xml"); // Parse the resource file ARSCFileParser resParser = null; if (parseLayoutFile) { resParser = new ARSCFileParser(); resParser.parse(apkFileLocation); } AnalyzeJimpleClass jimpleClass = null; LayoutFileParser lfp = parseLayoutFile ? new LayoutFileParser(this.appPackageName, resParser) : null; boolean hasChanged = true; while (hasChanged) { hasChanged = false; soot.G.reset(); initializeSoot(); createMainMethod(); if (jimpleClass == null) { // Collect the callback interfaces implemented in the app's source code jimpleClass = new AnalyzeJimpleClass(entrypoints); jimpleClass.collectCallbackMethods(); // Find the user-defined sources in the layout XML files. This // only needs to be done once, but is a Soot phase. if (parseLayoutFile) { lfp.parseLayoutFile(apkFileLocation, entrypoints); } } else { jimpleClass.collectCallbackMethodsIncremental(); } // Run the soot-based operations PackManager.v().getPack("wjpp").apply(); PackManager.v().getPack("cg").apply(); PackManager.v().getPack("wjtp").apply(); // Collect the results of the soot-based phases for (Entry<String, Set<SootMethodAndClass>> entry : jimpleClass.getCallbackMethods() .entrySet()) { if (this.callbackMethods.containsKey(entry.getKey())) { if (this.callbackMethods.get(entry.getKey()).addAll(entry.getValue())) { hasChanged = true; } } else { this.callbackMethods.put(entry.getKey(), new HashSet<SootMethodAndClass>(entry.getValue())); hasChanged = true; } } } // Collect the XML-based callback methods for (Entry<String, Set<Integer>> lcentry : jimpleClass.getLayoutClasses().entrySet()) { final SootClass callbackClass = Scene.v().getSootClass(lcentry.getKey()); for (Integer classId : lcentry.getValue()) { AbstractResource resource = resParser.findResource(classId); if (resource instanceof StringResource) { final String layoutFileName = ((StringResource) resource).getValue(); // Add the callback methods for the given class Set<String> callbackMethods = lfp.getCallbackMethods().get(layoutFileName); if (callbackMethods != null) { for (String methodName : callbackMethods) { final String subSig = "void " + methodName + "(android.view.View)"; // The callback may be declared directly in the // class // or in one of the superclasses SootClass currentClass = callbackClass; while (true) { SootMethod callbackMethod = currentClass.getMethodUnsafe(subSig); if (callbackMethod != null) { addCallbackMethod(callbackClass.getName(), new AndroidMethod(callbackMethod)); break; } if (!currentClass.hasSuperclass()) { System.err.println("Callback method " + methodName + " not found in class " + callbackClass.getName()); break; } currentClass = currentClass.getSuperclass(); } } } // For user-defined views, we need to emulate their // callbacks Set<LayoutControl> controls = lfp.getUserControls().get(layoutFileName); if (controls != null) { for (LayoutControl lc : controls) { registerCallbackMethodsForView(callbackClass, lc); } } } else { System.err.println("Unexpected resource type for layout class"); } } } logger.info("Entry point calculation done."); // Clean up everything we no longer need soot.G.reset(); Map<String, Set<String>> result = new HashMap<>(this.callbackMethods.size()); for (Map.Entry<String, Set<SootMethodAndClass>> entry : this.callbackMethods.entrySet()) { Set<SootMethodAndClass> callbackSet = entry.getValue(); Set<String> callbackStrings = new HashSet<>(callbackSet.size()); for (SootMethodAndClass androidMethod : callbackSet) { callbackStrings.add(androidMethod.getSignature()); } result.put(entry.getKey(), callbackStrings); } entryPointCreator = createEntryPointCreator(); return result; } /** * Registers the callback methods in the given layout control so that they are included in the * dummy main method * * @param callbackClass The class with which to associate the layout callbacks * @param lc The layout control whose callbacks are to be associated with the given class */ private void registerCallbackMethodsForView(SootClass callbackClass, LayoutControl lc) { // Ignore system classes if (callbackClass.getName().startsWith("android.")) { return; } if (lc.getViewClass().getName().startsWith("android.")) { return; } // Check whether the current class is actually a view { SootClass sc = lc.getViewClass(); boolean isView = false; while (sc.hasSuperclass()) { if (sc.getName().equals("android.view.View")) { isView = true; break; } sc = sc.getSuperclass(); } if (!isView) { return; } } // There are also some classes that implement interesting callback // methods. // We model this as follows: Whenever the user overwrites a method in an // Android OS class, we treat it as a potential callback. SootClass sc = lc.getViewClass(); Set<String> systemMethods = new HashSet<String>(10000); for (SootClass parentClass : Scene.v().getActiveHierarchy().getSuperclassesOf(sc)) { if (parentClass.getName().startsWith("android.")) { for (SootMethod sm : parentClass.getMethods()) { if (!sm.isConstructor()) { systemMethods.add(sm.getSubSignature()); } } } } // Scan for methods that overwrite parent class methods for (SootMethod sm : sc.getMethods()) { if (!sm.isConstructor()) { if (systemMethods.contains(sm.getSubSignature())) { // This is a real callback method addCallbackMethod(callbackClass.getName(), new AndroidMethod(sm)); } } } } /** * Adds a method to the set of callback method * * @param layoutClass The layout class for which to register the callback * @param callbackMethod The callback method to register */ private void addCallbackMethod(String layoutClass, AndroidMethod callbackMethod) { Set<SootMethodAndClass> methods = this.callbackMethods.get(layoutClass); if (methods == null) { methods = new HashSet<SootMethodAndClass>(); this.callbackMethods.put(layoutClass, methods); } methods.add(new AndroidMethod(callbackMethod)); } /** * Creates the main method based on the current callback information, injects it into the Soot * scene. */ private void createMainMethod() { // Always update the entry point creator to reflect the newest set // of callback methods SootMethod entryPoint = createEntryPointCreator().createDummyMain(); Scene.v().setEntryPoints(Collections.singletonList(entryPoint)); if (Scene.v().containsClass(entryPoint.getDeclaringClass().getName())) { Scene.v().removeClass(entryPoint.getDeclaringClass()); } Scene.v().addClass(entryPoint.getDeclaringClass()); } /** * Initializes soot for running the soot-based phases of the application metadata analysis * * @return The entry point used for running soot */ public void initializeSoot() { Options.v().set_no_bodies_for_excluded(true); Options.v().set_allow_phantom_refs(true); Options.v().set_output_format(Options.output_format_none); Options.v().set_whole_program(true); Options.v().setPhaseOption("cg.spark", "on"); // Options.v().setPhaseOption("cg.spark", "geom-pta:true"); // Options.v().setPhaseOption("cg.spark", "geom-encoding:PtIns"); Options.v().set_ignore_resolution_errors(true); // Options.v().setPhaseOption("jb", "use-original-names:true"); Options.v() .set_soot_classpath(this.classDirectory + File.pathSeparator + this.androidClassPath); if (logger.isDebugEnabled()) { logger.debug("Android class path: " + this.androidClassPath); } // Options.v().set_android_jars(androidJar); // Options.v().set_src_prec(Options.src_prec_apk); Options.v().set_process_dir(new ArrayList<>(this.entrypoints)); // Options.v().set_app(true); Main.v().autoSetOptions(); Scene.v().loadNecessaryClasses(); // for (String className : this.entrypoints) { // SootClass c = Scene.v().forceResolve(className, SootClass.BODIES); // c.setApplicationClass(); // } // // SootMethod entryPoint = getEntryPointCreator().createDummyMain(); // Scene.v().setEntryPoints(Collections.singletonList(entryPoint)); // return entryPoint; } public AndroidEntryPointCreator createEntryPointCreator() { AndroidEntryPointCreator entryPointCreator = new AndroidEntryPointCreator(new ArrayList<String>(this.entrypoints)); Map<String, List<String>> callbackMethodSigs = new HashMap<String, List<String>>(); for (String className : this.callbackMethods.keySet()) { List<String> methodSigs = new ArrayList<String>(); callbackMethodSigs.put(className, methodSigs); for (SootMethodAndClass am : this.callbackMethods.get(className)) { methodSigs.add(am.getSignature()); } } entryPointCreator.setCallbackFunctions(callbackMethodSigs); return entryPointCreator; } }