/* 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.application; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumMap; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Vector; import org.apache.log4j.Logger; import de.rub.syssec.saaf.misc.FileList; import de.rub.syssec.saaf.misc.config.Config; import de.rub.syssec.saaf.model.APICall; import de.rub.syssec.saaf.model.application.ApplicationInterface; import de.rub.syssec.saaf.model.application.ClassInterface; import de.rub.syssec.saaf.model.application.ClassOrMethodNotFoundException; import de.rub.syssec.saaf.model.application.CodeLineInterface; import de.rub.syssec.saaf.model.application.DetectionLogicError; import de.rub.syssec.saaf.model.application.Digest; import de.rub.syssec.saaf.model.application.MethodInterface; import de.rub.syssec.saaf.model.application.SmaliClassError; import de.rub.syssec.saaf.model.application.manifest.ComponentInterface; import de.rub.syssec.saaf.model.application.manifest.ManifestInterface; /** * This class represents a whole Android application or APK file. */ public class Application implements ApplicationInterface { private static final Logger LOGGER = Logger.getLogger(Application.class); /** * The directory where .smali, .class and .java files are located */ private File bytecodeDirectory; private File appDirectory; /** * the directory where all unpacked files from the apk reside */ private File decompiledContentDir; private String applicationName; private String fileExtension; private File apkFile; private File manifestFile; private ManifestInterface manifest; // directory in which the content of the .apk file is extracted private File apkContentDir; private int smaliClassLabel = 0; private int id; /** * Directory where the imported app is located */ private File apkDirectory; private Config config; private HashMap<String,ClassInterface> smaliClassMap = new HashMap<String, ClassInterface >(); /** * This map stores all calculated message Digests for this application. */ private final EnumMap<Digest, String> digestMap = new EnumMap<Digest, String>(Digest.class); /** * Mapping of this apps codelines to apicalls if they match */ HashMap<CodeLineInterface, APICall> matchedCalls; List<CodeLineInterface> foundCalls = new ArrayList<CodeLineInterface>();//maybe change this to treeset and make it comparable /** * returns the calls which could be mapped to the permissions based on android permission map */ public HashMap<CodeLineInterface, APICall> getMatchedCalls(){ return matchedCalls; } @Override public void setMatchedCalls(HashMap<CodeLineInterface, APICall> calls) { this.matchedCalls = calls; } public List<CodeLineInterface> getFoundCalls(){ return foundCalls; } @Override public void setFoundCalls(List<CodeLineInterface> calls) { this.foundCalls=calls; } @Override public int getId() { return id; } @Override public void setId(int id) { this.id = id; } @Override public File getUnpackedDataDir() { return decompiledContentDir; } @Override public String getMessageDigest(Digest digestAlgorithm) { return digestMap.get(digestAlgorithm); } @Override public void setMessageDigest(Digest digestAlgorithm, String digest) { digestMap.put(digestAlgorithm, digest); } @Override public File getManifestFile() { return this.manifestFile; } @Override public ManifestInterface getManifest() { return this.manifest; } @Override public File getBytecodeDirectory() { return bytecodeDirectory; } @Override public File getApplicationDirectory() { return appDirectory; } @Override public File getApkFile() { return apkFile; } /** * TODO: Add support for temporary apps which are unpacked into a temp * directory and which are not inserted into the DB. * * @param apk * @param warnIfDuplicate * Show a info dialog if an app with the same hash is already in * the DB old param generateJava -> now set in * Config.GENERATE_JAVA If this is true java code is generated * out of the .class files * @throws Exception */ public Application(File apk, boolean warnIfDuplicate) { // Set up some names, files, etc this(); this.applicationName = apk.getName().substring(0, apk.getName().length() - 4); this.fileExtension = apk.getName().substring( apk.getName().length() - 3); this.apkFile = apk; } public Application() { this.config = Config.getInstance(); this.changed=true; } private FileList allSmaliClasss = null; private FileList allClassFiles = null; @Override public Vector<File> getAllRawSmaliFiles(boolean includeFilesFromAdPackages) { if (allSmaliClasss == null) allSmaliClasss = new FileList(bytecodeDirectory, FileList.SMALI_FILES); return allSmaliClasss.getAllFoundFiles(includeFilesFromAdPackages); } @Override public Vector<File> getAllClassFiles(boolean includeFilesFromAdPackages) { if (allClassFiles == null) allClassFiles = new FileList(bytecodeDirectory, FileList.CLASS_FILES); return allClassFiles.getAllFoundFiles(includeFilesFromAdPackages); } private boolean changed; @Override public ClassInterface getSmaliClass(File file) { ClassInterface sf = smaliClassMap.get(file.getAbsolutePath()); if (sf == null ){ boolean inAdFramework=false; inAdFramework = config.getAdChecker().containsAnAd(file); try { sf = new SmaliClass(file, this, smaliClassLabel++); sf.setInAdFramework(inAdFramework); smaliClassMap.put(file.getAbsolutePath(), sf); } catch (IOException e) { LOGGER.error("Could not create SmaliClass object", e); } catch (DetectionLogicError e) { LOGGER.error("Could not create SmaliClass object", e); } catch (SmaliClassError e) { LOGGER.error("Could not create SmaliClass object", e); } } return sf;//smaliClassMap.get(file.getAbsolutePath()); } @Override public LinkedList<ClassInterface> getAllSmaliClasss( boolean includeFilesFromAdPackages) { LinkedList<ClassInterface> sfList = new LinkedList<ClassInterface>(); for (File f : getAllRawSmaliFiles(includeFilesFromAdPackages)) { ClassInterface sf = getSmaliClass(f); if (sf == null) { boolean inAdFramework=false; inAdFramework = config.getAdChecker().containsAnAd(f); try { sf = new SmaliClass(f, this, smaliClassLabel++); sf.setInAdFramework(inAdFramework); smaliClassMap.put(f.getAbsolutePath(), sf); } catch (IOException e) { LOGGER.error("Could not create SmaliClass object", e); } catch (DetectionLogicError e) { LOGGER.error("Could not create SmaliClass object", e); } catch (SmaliClassError e) { LOGGER.error("Could not create SmaliClass object", e); } } if (sf != null){sfList.addLast(sf);} } return sfList; } @Override public void setAllSmaliClasss(HashMap<String, ClassInterface> smaliClassMap) { this.smaliClassMap = smaliClassMap; } @Override public String getApplicationName() { return applicationName; } @Override public int getNumberOfCodelines(boolean includeFilesFromAdPackages) { int nr = 0; LinkedList<ClassInterface> files = getAllSmaliClasss(includeFilesFromAdPackages); for (ClassInterface f : files) { nr += f.getLinesOfCode(); } return nr; } @Override public MethodInterface getMethodByClassAndName(String className, String methodName, byte[] parameterDeclaration, byte[] returnValue) throws ClassOrMethodNotFoundException { File f = new File(bytecodeDirectory, className + ".smali"); if (!f.exists()) { StringBuilder sb = new StringBuilder(); sb.append("Lost track, class unknown: "); sb.append(className); sb.append("->"); sb.append(methodName); sb.append("("); sb.append(new String(parameterDeclaration)); sb.append(")"); throw new ClassOrMethodNotFoundException(sb.toString()); } else { ClassInterface sf = getSmaliClass(f); for (MethodInterface m : sf.getMethods()) { if (m.getName().equals(methodName)) { if (parameterDeclaration != null && Arrays.equals(parameterDeclaration, m.getParameters()) && Arrays.equals(returnValue, m.getReturnValue())) { return m; } } } } StringBuilder sb = new StringBuilder(); sb.append("Lost track, method unknown: "); sb.append(className); sb.append("->"); sb.append(methodName); sb.append("("); sb.append(new String(parameterDeclaration)); sb.append(")"); throw new ClassOrMethodNotFoundException(sb.toString()); } @Override public String getFileExtension() { return fileExtension; } @Override public void setApplicationName(String name) { this.applicationName = name; } @Override public void setFileExtension(String extension) { this.fileExtension = extension; } @Override public void setManifest(ManifestInterface manifest) { this.manifest = manifest; } @Override public void setManifestFile(File manifestFile) { this.manifestFile=manifestFile; } /** * @return the apkDirectory */ @Override public File getApkDirectory() { return apkDirectory; } /** * @param apkDirectory the apkDirectory to set */ @Override public void setApkDirectory(File apkDirectory) { this.apkDirectory = apkDirectory; } /** * @return the decompiledContentDir */ @Override public File getDecompiledContentDir() { return decompiledContentDir; } /** * @param decompiledContentDir the decompiledContentDir to set */ @Override public void setDecompiledContentDir(File decompiledContentDir) { this.decompiledContentDir = decompiledContentDir; } /** * @param appDirectory the appDirectory to set */ @Override public void setApplicationDirectory(File appDirectory) { this.appDirectory = appDirectory; } /** * @param bytecodeDirectory the bytecodeDirectory to set */ @Override public void setBytecodeDirectory(File bytecodeDirectory) { this.bytecodeDirectory = bytecodeDirectory; } @Override public void setApkContentDir(File apkContentDir) { this.apkContentDir=apkContentDir; } @Override public File getApkContentDir() { return this.apkContentDir; } /** * @return the smaliClassLabel */ public int getSmaliClassLabel() { return smaliClassLabel; } /** * @param smaliClassLabel the smaliClassLabel to set */ @Override public void setSmaliClassLabel(int smaliClassLabel) { this.smaliClassLabel = smaliClassLabel; } @Override public void setChanged(boolean changed) { this.changed = changed; } @Override public boolean isChanged() { return changed; } @Override public String toString() { return "Application [applicationName=" + applicationName + "]"; } /** * @param app */ public static boolean isAPKFile(File apk) { FileInputStream fis = null; try { if (apk.length() <= 2) { LOGGER.info("File too small. Aborting."); return false; } if (!apk.canRead()) { LOGGER.info("File not readable. Aborting."); return false; } fis = new FileInputStream(apk); byte[] fileHead = new byte[8]; int read = fis.read(fileHead); if (read <= 2) { LOGGER.info("Could not read file: "+apk.getName()+". Aborting."); return false; } if ( fileHead[0] != 'P' || fileHead[1] != 'K' ) { LOGGER.info("Magic bytes do not match! Aborting."); return false; } } catch (IOException e) { LOGGER.info("Could not check file, aborting. Message: "+e.getMessage()); return false; } finally { if (fis != null) { try { fis.close(); } catch (Exception e) { /* ignore */ } } } return true; } @Override public ClassInterface getSmaliClass(ComponentInterface component) { String path = component.getName(); if(component.getName().startsWith(".")) { path = this.getManifest().getPackageName()+path; } path = path.replace('.', '/'); path = this.getDecompiledContentDir() + File.separator + "smali" + File.separator + path + ".smali"; return this.getSmaliClass(new File(path)); } }