soot.jimple.infoflow.android.manifest.BaseProcessManifest Maven / Gradle / Ivy
package soot.jimple.infoflow.android.manifest;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.xmlpull.v1.XmlPullParserException;
import pxb.android.axml.AxmlVisitor;
import soot.jimple.infoflow.android.axml.AXmlAttribute;
import soot.jimple.infoflow.android.axml.AXmlDocument;
import soot.jimple.infoflow.android.axml.AXmlHandler;
import soot.jimple.infoflow.android.axml.AXmlNode;
import soot.jimple.infoflow.android.axml.ApkHandler;
import soot.jimple.infoflow.android.manifest.binary.BinaryAndroidApplication;
import soot.jimple.infoflow.android.manifest.containers.EagerComponentContainer;
import soot.jimple.infoflow.android.manifest.containers.EmptyComponentContainer;
import soot.jimple.infoflow.android.resources.ARSCFileParser;
/**
* This class provides easy access to all data of an AppManifest.
* Nodes and attributes of a parsed manifest can be changed. A new byte
* compressed manifest considering the changes can be generated.
*
* @author Steven Arzt
* @author Stefan Haas, Mario Schlipf
* @see App
* Manifest
*/
public abstract class BaseProcessManifest
implements IManifestHandler {
/**
* Factory class for creating component implementations
*
* @author Steven Arzt
*
*/
protected interface IComponentFactory {
/**
* Creates a new data object for an activity inside an Android app
*
* @param node The binary XML node that contains the activity definition
* @return The new activity object
*/
public A createActivity(AXmlNode node);
/**
* Creates a new data object for a broadcast receiver inside an Android app
*
* @param node The binary XML node that contains the broadcast receiver
* definition
* @return The new broadcast receiver object
*/
public B createBroadcastReceiver(AXmlNode node);
/**
* Creates a new data object for a content provider inside an Android app
*
* @param node The binary XML node that contains the content provider definition
* @return The new broadcast content provider object
*/
public C createContentProvider(AXmlNode node);
/**
* Creates a new data object for a service inside an Android app
*
* @param node The binary XML node that contains the service definition
* @return The new broadcast service object
*/
public S createService(AXmlNode node);
}
/**
* Handler for zip-like apk files
*/
protected ApkHandler apk = null;
/**
* Handler for android xml files
*/
protected AXmlHandler axml;
protected ARSCFileParser arscParser;
// android manifest data
protected AXmlNode manifest;
protected AXmlNode application;
// Components in the manifest file
protected List providers = null;
protected List services = null;
protected List activities = null;
protected List aliasActivities = null;
protected List receivers = null;
protected IComponentFactory factory = createComponentFactory();
/**
* Processes an AppManifest which is within the file identified by the given
* path.
*
* @param apkPath file path to an APK.
* @throws IOException if an I/O error occurs.
* @throws XmlPullParserException can occur due to a malformed manifest.
*/
public BaseProcessManifest(String apkPath) throws IOException, XmlPullParserException {
this(new File(apkPath));
}
/**
* Processes an AppManifest which is within the given {@link File}.
*
* @param apkFile the AppManifest within the given APK will be parsed.
* @throws IOException if an I/O error occurs.
* @throws XmlPullParserException can occur due to a malformed manifest.
* @see {@link BaseProcessManifest#ProcessManifest(InputStream)}
*/
public BaseProcessManifest(File apkFile) throws IOException, XmlPullParserException {
this(apkFile, ARSCFileParser.getInstance(apkFile));
}
/**
* Processes an AppManifest which is within the given {@link File}.
*
* @param apkFile the AppManifest within the given APK will be parsed.
* @param arscParser The parser for the Android resource database
* @throws IOException if an I/O error occurs.
* @throws XmlPullParserException can occur due to a malformed manifest.
* @see {@link BaseProcessManifest#ProcessManifest(InputStream)}
*/
public BaseProcessManifest(File apkFile, ARSCFileParser arscParser) throws IOException, XmlPullParserException {
if (!apkFile.exists())
throw new RuntimeException(
String.format("The given APK file %s does not exist", apkFile.getCanonicalPath()));
this.apk = new ApkHandler(apkFile);
this.arscParser = arscParser;
try (InputStream is = this.apk.getInputStream("AndroidManifest.xml")) {
if (is == null)
throw new FileNotFoundException(
String.format("The file %s does not contain an Android Manifest", apkFile.getAbsolutePath()));
this.handle(is);
}
}
/**
* Processes an AppManifest which is provided by the given {@link InputStream}.
*
* @param manifestIS InputStream for an AppManifest.
* @param arscParser The Android resource file parser
* @throws IOException if an I/O error occurs.
* @throws XmlPullParserException can occur due to a malformed manifest.
*/
public BaseProcessManifest(InputStream manifestIS, ARSCFileParser arscParser)
throws IOException, XmlPullParserException {
this.arscParser = arscParser;
this.handle(manifestIS);
}
/**
* Initialises the {@link BaseProcessManifest} by parsing the manifest provided
* by the given {@link InputStream}.
*
* @param manifestIS InputStream for an AppManifest.
* @throws IOException if an I/O error occurs.
* @throws XmlPullParserException can occur due to a malformed manifest.
*/
protected void handle(InputStream manifestIS) throws IOException, XmlPullParserException {
this.axml = new AXmlHandler(manifestIS);
// get manifest node
AXmlDocument document = this.axml.getDocument();
this.manifest = document.getRootNode();
if (!this.manifest.getTag().equals("manifest"))
throw new RuntimeException("Root node is not a manifest node");
// get application node
List applications = this.manifest.getChildrenWithTag("application");
if (applications.isEmpty())
throw new RuntimeException("Manifest contains no application node");
else if (applications.size() > 1)
throw new RuntimeException("Manifest contains more than one application node");
this.application = applications.get(0);
// Get components
this.providers = this.axml.getNodesWithTag("provider");
this.services = this.axml.getNodesWithTag("service");
this.activities = this.axml.getNodesWithTag("activity");
this.aliasActivities = this.axml.getNodesWithTag("activity-alias");
this.receivers = this.axml.getNodesWithTag("receiver");
}
/**
* Returns the handler which parsed and holds the manifest's data.
*
* @return Android XML handler
*/
public AXmlHandler getAXml() {
return this.axml;
}
/**
* Returns the handler which opened the APK file. If {@link BaseProcessManifest}
* was instanciated directly with an {@link InputStream} this will return
* null
.
*
* @return APK Handler
*/
public ApkHandler getApk() {
return this.apk;
}
/**
* The unique manifest
node of the AppManifest.
*
* @return manifest node
*/
public AXmlNode getManifest() {
return this.manifest;
}
@Override
public IAndroidApplication getApplication() {
return new BinaryAndroidApplication(this.application, this);
}
@Override
public IComponentContainer getContentProviders() {
if (this.providers == null)
return EmptyComponentContainer.get();
return new EagerComponentContainer<>(
this.providers.stream().map(p -> factory.createContentProvider(p)).collect(Collectors.toList()));
}
@Override
public IComponentContainer getServices() {
if (this.services == null)
return EmptyComponentContainer.get();
return new EagerComponentContainer<>(
services.stream().map(s -> factory.createService(s)).collect(Collectors.toList()));
}
/**
* Gets the type of the component identified by the given class name
*
* @param className The class name for which to get the component type
* @return The component type of the given class if this class has been
* registered as a component in the manifest file, otherwise null
*/
public ComponentType getComponentType(String className) {
for (AXmlNode node : this.activities)
if (node.getAttribute("name").getValue().equals(className))
return ComponentType.Activity;
for (AXmlNode node : this.services)
if (node.getAttribute("name").getValue().equals(className))
return ComponentType.Service;
for (AXmlNode node : this.receivers)
if (node.getAttribute("name").getValue().equals(className))
return ComponentType.BroadcastReceiver;
for (AXmlNode node : this.providers)
if (node.getAttribute("name").getValue().equals(className))
return ComponentType.ContentProvider;
return null;
}
@Override
public IComponentContainer getActivities() {
if (this.activities == null)
return EmptyComponentContainer.get();
return new EagerComponentContainer<>(
this.activities.stream().map(a -> factory.createActivity(a)).collect(Collectors.toList()));
}
/**
* Returns a list containing all nodes with tag activity-alias
*
* @return list with all alias activities
*/
public List getAliasActivities() {
return new ArrayList(this.aliasActivities);
}
@Override
public IComponentContainer getBroadcastReceivers() {
if (this.receivers == null)
return EmptyComponentContainer.get();
return new EagerComponentContainer<>(
receivers.stream().map(r -> factory.createBroadcastReceiver(r)).collect(Collectors.toList()));
}
/**
* Returns the provider
which has the given name
.
*
* @param name the provider's name
*
* @param name the provider's name
* @return provider with name
*/
public AXmlNode getProvider(String name) {
return this.getNodeWithName(this.providers, name);
}
/**
* Returns the service
which has the given name
.
*
* @param name the service's name
* @return service with name
*/
public AXmlNode getService(String name) {
return this.getNodeWithName(this.services, name);
}
/**
* Returns the activity
which has the given name
.
*
* @param name the activitie's name
* @return activitiy with name
*/
public AXmlNode getActivity(String name) {
return this.getNodeWithName(this.activities, name);
}
/**
* Returns the alias analysis
which has the given name
*
* @param name the alias activity's name
* @return alias activity with name
*/
public AXmlNode getAliasActivity(String name) {
return this.getNodeWithName(this.aliasActivities, name);
}
/**
* Returns the receiver
which has the given name
.
*
* @param name the receiver's name
* @return receiver with name
*/
public AXmlNode getReceiver(String name) {
return this.getNodeWithName(this.receivers, name);
}
/**
* Iterates over list
and checks which node has the given
* name
.
*
* @param list contains nodes.
* @param name the node's name.
* @return node with name
.
*/
protected AXmlNode getNodeWithName(List list, String name) {
for (AXmlNode node : list) {
Object attr = node.getAttributes().get("name");
if (attr != null && ((AXmlAttribute>) attr).getValue().equals(name))
return node;
}
return null;
}
/**
* Returns the target activity specified in the targetActivity
* attribute of the alias activity
*
* @param aliasActivity
* @return activity
*/
public AXmlNode getAliasActivityTarget(AXmlNode aliasActivity) {
if (BaseProcessManifest.isAliasActivity(aliasActivity)) {
AXmlAttribute> targetActivityAttribute = aliasActivity.getAttribute("targetActivity");
if (targetActivityAttribute != null) {
return this.getActivity((String) targetActivityAttribute.getValue());
}
}
return null;
}
/**
* Returns whether the given activity is an alias activity or not
*
* @param activity
* @return True if the activity is an alias activity, False otherwise
*/
public static boolean isAliasActivity(AXmlNode activity) {
return activity.getTag().equals("activity-alias");
}
public ArrayList getAllActivities() {
ArrayList allActivities = new ArrayList<>(this.activities);
allActivities.addAll(this.aliasActivities);
return allActivities;
}
/**
* Returns the Manifest as a compressed android xml byte array. This will
* consider all changes made to the manifest and application nodes respectively
* to their child nodes.
*
* @return byte compressed AppManifest
* @see AXmlHandler#toByteArray()
*/
public byte[] getOutput() {
return this.axml.toByteArray();
}
/**
* Gets the application's package name
*
* @return The package name of the application
*/
private String cache_PackageName = null;
public String getPackageName() {
if (cache_PackageName == null) {
AXmlAttribute> attr = this.manifest.getAttribute("package");
if (attr != null && attr.getValue() != null)
cache_PackageName = (String) attr.getValue();
}
return cache_PackageName;
}
/**
* Gets the version code of the application. This code is used to compare
* versions for updates.
*
* @return The version code of the application
*/
public int getVersionCode() {
AXmlAttribute> attr = this.manifest.getAttribute("versionCode");
return attr == null || attr.getValue() == null ? -1 : Integer.parseInt(attr.getValue().toString());
}
/**
* Gets the application's version name as it is displayed to the user
*
* @return The application#s version name as in pretty print
*/
public String getVersionName() {
AXmlAttribute> attr = this.manifest.getAttribute("versionName");
return attr == null || attr.getValue() == null ? null : attr.getValue().toString();
}
/**
* Gets the minimum SDK version on which this application is supposed to run
*
* @return The minimum SDK version on which this application is supposed to run
*/
public int getMinSdkVersion() {
List usesSdk = this.manifest.getChildrenWithTag("uses-sdk");
if (usesSdk == null || usesSdk.isEmpty())
return -1;
AXmlAttribute> attr = usesSdk.get(0).getAttribute("minSdkVersion");
if (attr == null || attr.getValue() == null)
return -1;
if (attr.getValue() instanceof Integer)
return (Integer) attr.getValue();
return Integer.parseInt(attr.getValue().toString());
}
/**
* Gets the target SDK version for which this application was developed
*
* @return The target SDK version for which this application was developed
*/
public int getTargetSdkVersion() {
List usesSdk = this.manifest.getChildrenWithTag("uses-sdk");
if (usesSdk == null || usesSdk.isEmpty())
return -1;
AXmlAttribute> attr = usesSdk.get(0).getAttribute("targetSdkVersion");
if (attr == null || attr.getValue() == null)
return -1;
if (attr.getValue() instanceof Integer)
return (Integer) attr.getValue();
return Integer.parseInt(attr.getValue().toString());
}
/**
* Gets the permissions this application requests
*
* @return The permissions requested by this application
* @return
*/
public Set getPermissions() {
List usesPerms = this.manifest.getChildrenWithTag("uses-permission");
Set permissions = new HashSet();
for (AXmlNode perm : usesPerms) {
AXmlAttribute> attr = perm.getAttribute("name");
if (attr != null)
permissions.add((String) attr.getValue());
else {
// The required "name" attribute is missing, so we collect all
// empty
// attributes as a best-effort solution for broken malware apps
for (AXmlAttribute> a : perm.getAttributes().values())
if (a.getType() == AxmlVisitor.TYPE_STRING && (a.getName() == null || a.getName().isEmpty()))
permissions.add((String) a.getValue());
}
}
return permissions;
}
/**
* Adds a new permission to the manifest.
*
* @param complete permission name e.g. "android.permission.INTERNET"
*/
public void addPermission(String permissionName) {
AXmlNode permission = new AXmlNode("uses-permission", null, manifest, "");
AXmlAttribute permissionNameAttr = new AXmlAttribute("name", permissionName,
AXmlHandler.ANDROID_NAMESPACE);
permission.addAttribute(permissionNameAttr);
}
/**
* Adds a new provider to the manifest
*
* @param node provider represented as an AXmlNode
*/
public void addProvider(AXmlNode node) {
if (providers.isEmpty())
providers = new ArrayList();
providers.add(node);
}
/**
* Adds a new receiver to the manifest
*
* @param node receiver represented as an AXmlNode
*/
public void addReceiver(AXmlNode node) {
if (receivers.isEmpty())
receivers = new ArrayList();
receivers.add(node);
}
/**
* Adds a new activity to the manifest
*
* @param node activity represented as an AXmlNode
*/
public void addActivity(AXmlNode node) {
if (activities.isEmpty())
activities = new ArrayList();
activities.add(node);
}
/**
* Adds a new service to the manifest
*
* @param node service represented as an AXmlNode
*/
public void addService(AXmlNode node) {
if (services.isEmpty())
services = new ArrayList();
services.add(node);
}
/**
* Closes this apk file and all resources associated with it
*/
@Override
public void close() {
if (this.apk != null)
this.apk.close();
}
/**
* Returns all activity nodes that are "launchable", i.e. that are called when
* the user clicks on the button in the launcher.
*
* @return
*/
public Set getLaunchableActivityNodes() {
Set allLaunchableActivities = new LinkedHashSet();
for (AXmlNode activity : this.getAllActivities()) {
for (AXmlNode activityChildren : activity.getChildren()) {
if (activityChildren.getTag().equals("intent-filter")) {
boolean actionFilter = false;
boolean categoryFilter = false;
for (AXmlNode intentFilter : activityChildren.getChildren()) {
if (intentFilter.getTag().equals("action") && intentFilter.getAttribute("name").getValue()
.toString().equals("android.intent.action.MAIN"))
actionFilter = true;
else if (intentFilter.getTag().equals("category") && intentFilter.getAttribute("name")
.getValue().toString().equals("android.intent.category.LAUNCHER"))
categoryFilter = true;
}
if (actionFilter && categoryFilter)
allLaunchableActivities.add(activity);
}
}
}
return allLaunchableActivities;
}
/**
* Generates a full class name from a short class name by appending the
* globally-defined package when necessary
*
* @param className The class name to expand
* @return The expanded class name for the given short name
*/
public String expandClassName(String className) {
String packageName = getPackageName();
if (className.startsWith("."))
return packageName + className;
else if (!className.contains("."))
return packageName + "." + className;
else
return className;
}
/**
* Gets the Android resource parser
*
* @return The Android resource parser
*/
public ARSCFileParser getArscParser() {
return arscParser;
}
/**
* Creates a factory that can instantiate data objects for Android components
*
* @return The new factory
*/
protected abstract IComponentFactory createComponentFactory();
}