content: /*----------------------------------------------------------------------------+
   *|                                                                           |
   *|                          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;
    }
  
  }
  