Java tutorial
/*----------------------------------------------------------------------------+ *| | *| Android's Hooker | *| | *+---------------------------------------------------------------------------+ *| Copyright (C) 2011 Georges Bossert and Dimitri Kirchner | *| 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/>. | *+---------------------------------------------------------------------------+ *| @url : http://www.amossys.fr | *| @contact : android-hooker@amossys.fr | *| @sponsors : Amossys, http://www.amossys.fr | *+---------------------------------------------------------------------------+ */ package com.amossys.hooker.hookers; import java.lang.reflect.GenericDeclaration; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.util.Calendar; import java.util.HashMap; import java.util.Map; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import android.content.Context; import com.amossys.hooker.ApplicationConfig; import com.amossys.hooker.SubstrateMain; import com.amossys.hooker.common.InterceptEvent; import com.amossys.hooker.exceptions.HookerInitializationException; import com.amossys.hooker.hookers.interfaces.HookerListener; import com.amossys.hooker.service.InstrumentationServiceConnection; import com.saurik.substrate.MS; import com.saurik.substrate.MS.MethodPointer; /** * Hooker, a class extended by all the hookers * * @author Georges Bossert * */ public abstract class Hooker { /** * Abstract methods (child must implement) */ public abstract void attach(); /** * Attributes */ // name of the hooker, (also its category) private String name; // package name of the application we are in memory private String packageName; // start time of the hooker private final long startTimestamp = Calendar.getInstance().getTime().getTime(); // Service connection instance to communicate with the collecting service private final InstrumentationServiceConnection serviceConnection = new InstrumentationServiceConnection(); /** * Create a new hooker given a name and a place to store events * * @param name: the name of the hooker */ public Hooker(String name) { this.name = name; } /** * Send event to the service by means of the service connection and sets its IDXP * * @param event: the event to send */ public void sendEventToCollectService(InterceptEvent event, Context appContext) { if (appContext != null && !serviceConnection.isBoundToTheService()) { serviceConnection.doBindService(appContext); } this.serviceConnection.sendEvent(event); } protected void insertEvent(InterceptEvent event, Context appContext) { if (event == null) { return; } // Compute relative timestamp long relativeTimestamp = event.getTimestamp() - this.startTimestamp; event.setRelativeTimestamp(relativeTimestamp); if (SubstrateMain.NETWORK_MODE || SubstrateMain.FILE_MODE) { this.sendEventToCollectService(event, appContext); } if (SubstrateMain.DEBUG_MODE) { SubstrateMain.log(event.toString()); } } protected String getStringRepresentationOfAttribute(Object arg) { String argValue = null; /** * If the class of the argument (or one of its father) redefined the toString method, we use it, * if not, we use ToStringBuilder.reflectionToString() to infer it */ try { if (arg.getClass().getMethod("toString").getDeclaringClass().equals(Object.class)) { argValue = ToStringBuilder.reflectionToString(arg, ToStringStyle.SHORT_PREFIX_STYLE); } else { argValue = arg.toString(); } } catch (NoSuchMethodException e) { SubstrateMain.log("The attribute of object " + arg + " has no toString method defined.", e); } return argValue; } /** * Hook the specified methods and create an event for each calls of it. * * @param listener * @param className * @param methods * @throws HookerInitializationException */ protected void hookMethods(final HookerListener listener, final String className, final Map<String, Integer> methods) throws HookerInitializationException { hookMethodsWithOutputs(listener, className, methods, null); } protected void hookMethodsWithOutputs(final HookerListener listener, final String className, final Map<String, Integer> methods, final Map<String, Object> outputs) throws HookerInitializationException { final String hookerName = this.getHookerName(); MS.hookClassLoad(className, new MS.ClassLoadHook() { @SuppressWarnings({ "unchecked", "rawtypes" }) public void classLoaded(Class<?> resources) { /** * Based on the name of the method, we retrieve all the possibilities */ Map<GenericDeclaration, String> methodsToHook = new HashMap<GenericDeclaration, String>(); boolean found; for (String methodName : methods.keySet()) { found = false; /** * Checks if the requested method is a constructor or not */ if (className.substring(className.lastIndexOf('.') + 1).equals(methodName)) { found = true; for (int iConstructor = 0; iConstructor < resources .getConstructors().length; iConstructor++) { methodsToHook.put(resources.getConstructors()[iConstructor], methodName); } } else { for (Method m : resources.getMethods()) { if (m.getName().equals(methodName)) { found = true; methodsToHook.put(m, methodName); } } } if (!found) { SubstrateMain.log(new StringBuilder("No method found with name ").append(className) .append(":").append(methodName).toString()); } } for (final GenericDeclaration pMethod : methodsToHook.keySet()) { final String methodName = methodsToHook.get(pMethod); if (SubstrateMain.DEBUG_MODE) { SubstrateMain.log(new StringBuilder("Hooking method ").append(className).append(":") .append(methodName).toString()); } // To debug Substrate if you have a stacktrace // for (Class param : ((Method) pMethod).getParameterTypes()) { // SubstrateMain.log(" Param: " + param.getSimpleName()); // } final int intrusiveLevelFinal = methods.get(methodName); final MS.MethodPointer<Object, Object> old = new MethodPointer<Object, Object>(); MS.hookMethod_(resources, (Member) pMethod, new MS.MethodHook() { public Object invoked(final Object resources, final Object... args) throws Throwable { if (ApplicationConfig.isFiltered() || ApplicationConfig.getPackageName() == null) { return old.invoke(resources, args); } if (isSelfHooking((Member) pMethod)) { SubstrateMain.log( "Self hooking detected on method '" + ((Member) pMethod).getName() + "'."); return old.invoke(resources, args); } final String packName = ApplicationConfig.getPackageName(); final Context appContext = ApplicationConfig.getContext(); InterceptEvent event = null; if (packName != null && appContext != null) { // Open the connection to the service if not yet bound if (!serviceConnection.isBoundToTheService()) { serviceConnection.doBindService(appContext); } // Create the intercept event for this hook event = new InterceptEvent(hookerName, intrusiveLevelFinal, System.identityHashCode(resources), packName, className, methodName); //TODO: We should also save the parameters value before the call. // it requires to clone the parameters // and save them in the event if their value is different after the call /** * If specified, we execute the before method of the provided listener */ if (listener != null) { listener.before(className, pMethod, resources, event); } } /** * We invoke the original method and capture the result */ Object result = old.invoke(resources, args); // if requested we modify the output value of the invocation if (outputs != null && outputs.containsKey(methodName)) { if (result == null || outputs.get(methodName) == null || result.getClass().isAssignableFrom(outputs.get(methodName).getClass())) { result = outputs.get(methodName); } else { SubstrateMain.log("Cannot replace method " + methodName + " output with " + outputs.get(methodName) + ": types are incompatible.", false); } } // Store the result in the event (if available) if (event != null && appContext != null) { // Register the parameters of the method call in the event if (args != null) { for (Object arg : args) { if (arg != null) { String argValue = getStringRepresentationOfAttribute(arg); event.addParameter(arg.getClass().getName(), argValue); } else { event.addParameter(null, null); } } } // if the invocation returned something we store it in the event if (result != null) { String strResult = getStringRepresentationOfAttribute(result); event.setReturns(result.getClass().getName(), strResult); } else { event.setReturns(null, null); } /** * if specified, we execute the after method of the provided listener */ if (listener != null) { listener.after(className, pMethod, resources, event); } insertEvent(event, appContext); } return result; } /** * Computes if we are self hooking ourselves. To do so, we generate a stack trace to retrieve * the caller list of the current invocation and check no Hooker appears after the second entry of the stack trace. * @param pMethod * @param pMethod * @return true if we are self-hooking */ private boolean isSelfHooking(Member pMethod) { boolean selfHooking = false; StackTraceElement[] stackTrace = new Throwable().getStackTrace(); if (stackTrace.length > 2) { for (int i = 2; i < stackTrace.length; i++) { if (stackTrace[i].getClassName().startsWith(Hooker.class.getName())) { selfHooking = true; break; } } } return selfHooking; } }, old); } } }); } /** * @return the name */ public String getHookerName() { return name; } public String getPackageName() { return this.packageName; } }