/* 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.metadata; import java.io.File; import java.io.IOException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.log4j.Logger; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import de.rub.syssec.saaf.application.manifest.Manifest; import de.rub.syssec.saaf.application.manifest.components.Action; import de.rub.syssec.saaf.application.manifest.components.Activity; import de.rub.syssec.saaf.application.manifest.components.IntentFilter; import de.rub.syssec.saaf.application.manifest.components.IntentReceivingComponent; import de.rub.syssec.saaf.application.manifest.components.Receiver; import de.rub.syssec.saaf.application.manifest.components.Service; import de.rub.syssec.saaf.application.manifest.permissions.Permission; import de.rub.syssec.saaf.application.manifest.permissions.PermissionRequest; import de.rub.syssec.saaf.model.application.manifest.DuplicateEntryPointException; import de.rub.syssec.saaf.model.application.manifest.IntentFilterInterface; import de.rub.syssec.saaf.model.application.manifest.ManifestInterface; /** * This class reads the AndroidManifest.xml using DOM and XPath * * @author Tilman Bender <tilman.bender@rub.de> * @author Hanno Lemoine <hanno.lemoine@gdata.de> */ public class DOMManifestParser implements ManifestParser { private static final String ANDROID_ATTR_NS = "http://schemas.android.com/apk/res/android"; private static final Logger LOGGER = Logger .getLogger(DOMManifestParser.class); public DOMManifestParser(PermissionChecker checker) { } /* * (non-Javadoc) * * @see de.rub.syssec.saaf.nongui.ManifestParser#parse(java.io.File) */ @Override public ManifestInterface parse(File manifestFile) throws ManifestParserException { LOGGER.info("Analyzing Manifest: " + manifestFile.getAbsolutePath()); ManifestInterface parsedManifest = new Manifest(manifestFile); Document doc = buildDom(manifestFile); parseManifest(doc, parsedManifest); parseActivities(doc, parsedManifest); parseServices(doc, parsedManifest); parseReceivers(doc, parsedManifest); parsePermissions(doc, parsedManifest); LOGGER.info("found in " + parsedManifest.getPath() + ": " + parsedManifest.getNumberOfActivities() + " Activities" + ", " + parsedManifest.getNumberOfReceivers() + " Receivers" + ", " + parsedManifest.getNumberOfServices() + " Services" + " and " + parsedManifest.getNumberOfPermissions() + " Permissions"); LOGGER.info("Finished analyzing Manifest: " + manifestFile.getAbsolutePath()); return parsedManifest; } /** * @param manifestFile * @return * @throws ManifestParserException */ private Document buildDom(File manifestFile) throws ManifestParserException { DocumentBuilderFactory domFactory = DocumentBuilderFactory .newInstance(); domFactory.setNamespaceAware(true); DocumentBuilder builder; Document doc=null; try { builder = domFactory.newDocumentBuilder(); doc = builder.parse(manifestFile); } catch (ParserConfigurationException e) { throw new ManifestParserException(e); } catch (SAXException e) { //TODO Fix to handle (deliberately) malformed manifest //May cause all kinds of errors down the line (i.e. classes having no name) //Right now sanitizing the XML is not an option // // apparently something is wrong with the XML // // we did not try sanitizing the document, so lets see if that helps // LOGGER.warn("An exception occured during the parsing of "+manifestFile.getName()+" trying to tidy the XML."); // Tidy tidy = new Tidy(); // tidy.setXmlTags(true); // try { // File tidyManifest = new File(manifestFile.getParent() + File.separator + "Cleaned-AndroidManifest.xml"); // FileOutputStream tidyOutputStream = new FileOutputStream(tidyManifest); // tidy.parse(new FileInputStream(manifestFile), tidyOutputStream); // builder = domFactory.newDocumentBuilder(); // doc = builder.parse(new FileInputStream(tidyManifest)); // LOGGER.info("Cleaned manifest was wirtten to: "+tidyManifest.getAbsolutePath()); // } catch (FileNotFoundException e1) { // //ignore. If we got this far, this should not happen. // } catch (SAXException e1) { // throw new ManifestParserException(e1); // } catch (IOException e1) { // throw new ManifestParserException(e1); // } catch (ParserConfigurationException e1) { // throw new ManifestParserException(e1); // } throw new ManifestParserException(e); } catch (IOException e) { throw new ManifestParserException(e); } return doc; } /** * Parse the Manifest for basic tags, like 'manifest', 'uses-sdl', * 'application', etc., which only occur one time. * * @param doc * XML-document-object (INPUT). * @param stats * Manifest-Class to save results (OUTPUT). */ private void parseManifest(Document doc, ManifestInterface stats) { // ####### <manifest ..> NodeList manifests = doc.getElementsByTagName("manifest"); if (manifests.getLength() == 1) { Element manifestsNode = (Element) manifests.item(0); Attr packageName = manifestsNode.getAttributeNode("package"); if (packageName != null) { LOGGER.debug("package: " + packageName.getValue()); stats.setPackageName(packageName.getValue()); } Attr versionCode = manifestsNode .getAttributeNode("android:versionCode"); if (versionCode != null) { LOGGER.debug("version code: " + versionCode.getValue()); try { stats.setVersionCode(Integer.parseInt(versionCode .getValue())); } catch (NumberFormatException e) { LOGGER.warn("version code could not be parsed correctly"); } } Attr versionName = manifestsNode .getAttributeNode("android:versionName"); if (versionName != null) { LOGGER.debug("version name: " + versionName.getValue()); stats.setVersionName(versionName.getValue()); } } else { LOGGER.error("There is more than one <manifest> in the Manifests: " + manifests); } // ####### <uses-sdk ..> NodeList usesSDK = doc.getElementsByTagName("uses-sdk"); if (usesSDK.getLength() == 1) { Element usesSDKNode = (Element) usesSDK.item(0); Attr minSdkVersion = usesSDKNode.getAttributeNode("minSdkVersion"); if (minSdkVersion != null) { LOGGER.info("MinSdkVersion: " + minSdkVersion.getValue()); try { stats.setMinSdkVersion(Integer.parseInt(minSdkVersion .getValue())); } catch (NumberFormatException nfe) { LOGGER.warn("MinSdkVersion could not be parsed correctly"); } } } else if (usesSDK.getLength() > 0) { LOGGER.warn("There is more than one <uses-sdk> in the usesSDK: " + usesSDK.getLength()); } // ####### <application ..> NodeList application = doc.getElementsByTagName("application"); if (application.getLength() == 1) { Element applicationNode = (Element) application.item(0); Attr appLabel = applicationNode.getAttributeNode("android:label"); Attr appDebuggable = applicationNode .getAttributeNode("android:debuggable"); if (appDebuggable != null) { LOGGER.debug("appDebuggable: " + appDebuggable.getValue()); stats.setAppDebuggable(Boolean.parseBoolean(appDebuggable .getValue())); } if (appLabel != null) { LOGGER.debug(" appLabel: " + appLabel.getValue()); stats.setAppLabel(appLabel.getValue()); } } else { LOGGER.error("There is more than one <application> in the application: " + application); } } private void parseActivities(Document doc, ManifestInterface stats) { // count activities LOGGER.debug("Analyzing activity definitions..."); NodeList activities = doc.getElementsByTagName("activity"); //$NON-NLS-1$ for (int nodeIndex = 0; nodeIndex < activities.getLength(); nodeIndex++) { Element activityNode = (Element) activities.item(nodeIndex); Attr attr = activityNode .getAttributeNodeNS(ANDROID_ATTR_NS, "name"); if (attr != null) { Activity activity = new Activity(attr.getValue()); // look for intent-filters and add them parseIntentFilters(activityNode, activity); if (activity.isEntryPoint()) { try { stats.setDefaultActivity(activity); } catch (DuplicateEntryPointException e) { LOGGER.warn("The manifest defines a second entry point for the application"); } } stats.addActivity(activity); LOGGER.debug("Found activity: " + activity); } } LOGGER.debug("Finished analyzing activity definitions. " + stats.getNumberOfActivities() + " activities found"); } /** * Look for intent-filters as children of the given element and adds them to * the specified component. * * @param node * the DOM-Node whose descendants we will check for * intent-filters. * @param component * the component we will add the intent-filters to. * @throws NumberFormatException */ private void parseIntentFilters(Element node, IntentReceivingComponent component) throws NumberFormatException { Attr attr; NodeList filters = node.getElementsByTagName("intent-filter"); for (int filterIndex = 0; filterIndex < filters.getLength(); filterIndex++) { Element filterNode = (Element) filters.item(filterIndex); IntentFilter filter = new IntentFilter(); attr = filterNode.getAttributeNodeNS(ANDROID_ATTR_NS, "label"); if (attr != null) { filter.setLabel(attr.getValue()); } attr = filterNode.getAttributeNodeNS(ANDROID_ATTR_NS, "priority"); if (attr != null) { try { filter.setPriority(Integer.parseInt(attr.getValue())); } catch (NumberFormatException nfe) { LOGGER.warn("could not parse priority for intent"); } } parseFilterActions(filterNode, filter); if (filter.hasAction("android.intent.action.MAIN")) { component.setEntryPoint(true); } component.addIntentFilter(filter); } } private void parseFilterActions(Element node, IntentFilterInterface filter) { Attr attr; NodeList filters = node.getElementsByTagName("action"); for (int filterIndex = 0; filterIndex < filters.getLength(); filterIndex++) { Element filterNode = (Element) filters.item(filterIndex); attr = filterNode.getAttributeNodeNS(ANDROID_ATTR_NS, "name"); if (attr != null) { filter.addAction(new Action(attr.getValue())); } } } private void parseServices(Document doc, ManifestInterface stats) { // count activities LOGGER.debug("Analyzing service definitions..."); NodeList services = doc.getElementsByTagName("service"); //$NON-NLS-1$ for (int nodeIndex = 0; nodeIndex < services.getLength(); nodeIndex++) { Element serviceNode = (Element) services.item(nodeIndex); Attr attr = serviceNode.getAttributeNodeNS(ANDROID_ATTR_NS, "name"); Service service = new Service(); if (attr != null) { service.setName(attr.getValue()); } // look for intent-filters parseIntentFilters(serviceNode, service); stats.addService(service); LOGGER.debug("Found service: " + service); } LOGGER.debug("Finished analyzing service definitions. " + stats.getNumberOfServices() + " services found."); } private void parseReceivers(Document doc, ManifestInterface stats) { // count activities LOGGER.debug("Analyzing receiver definitions..."); NodeList receivers = doc.getElementsByTagName("receiver"); //$NON-NLS-1$ for (int nodeIndex = 0; nodeIndex < receivers.getLength(); nodeIndex++) { Element receiverNode = (Element) receivers.item(nodeIndex); Attr attr = receiverNode .getAttributeNodeNS(ANDROID_ATTR_NS, "name"); Receiver receiver = new Receiver(); if (attr != null) { receiver.setName(attr.getValue()); } // look for intent-filters parseIntentFilters(receiverNode, receiver); stats.addReceiver(receiver); LOGGER.debug("Found receiver: " + receiver); } LOGGER.debug("Finished analyzing receiver definitions. " + stats.getNumberOfReceivers() + " receivers found."); } private void parsePermissions(Document doc, ManifestInterface manifest) { NodeList requestedPermissions = doc .getElementsByTagName("uses-permission"); //$NON-NLS-1$ Element permission; Attr name; LOGGER.debug("Analyzing requested permissions..."); // iterate over all permissions requested in the manifest for (int permissionNr = 0; permissionNr < requestedPermissions .getLength(); permissionNr++) { permission = (Element) requestedPermissions.item(permissionNr); name = permission.getAttributeNodeNS(ANDROID_ATTR_NS, "name"); //default permission is created in case further parsing fails PermissionRequest request = new PermissionRequest(new Permission(name.getValue())); manifest.addPermissionRequest(request); LOGGER.debug("Requested permission: "+ request.getRequestedPermission()); } LOGGER.debug("Finished analyzing requested permissions. " + manifest.getNumberOfPermissions() + " permissions requested."); } }