Android 動態代理以及利用動態代理實現 ServiceHook

Shawn_Dut發表於2017-02-25

  這篇部落格主要介紹使用 InvocationHandler 這個介面來達到 hook 系統 service ,從而實現一些很有意思特殊功能的詳細步驟。

  轉載請註明出處:blog.csdn.net/self_study/…

  對技術感興趣的同鞋加群 544645972 一起交流。

Java 的動態代理

  首先我們要介紹的就是 Java 動態代理,Java 的動態代理涉及到兩個類:InvocationHandler 介面和 Proxy 類,下面我們會著重介紹一下這兩個類,並且結合例項來著重分析一下使用的正確姿勢等。在這之前簡單介紹一下 Java 中 class 檔案的生成和載入過程,Java 編譯器編譯好 Java 檔案之後會在磁碟中產生 .class 檔案。這種 .class 檔案是二進位制檔案,內容是隻有 JVM 虛擬機器才能識別的機器碼,JVM 虛擬機器讀取位元組碼檔案,取出二進位制資料,載入到記憶體中,解析 .class 檔案內的資訊,使用相對應的 ClassLoader 類載入器生成對應的 Class 物件:

Android 動態代理以及利用動態代理實現 ServiceHook
這裡寫圖片描述


.class 位元組碼檔案是根據 JVM 虛擬機器規範中規定的位元組碼組織規則生成的,具體的 .class 檔案格式介紹可以檢視部落格 深入理解Java Class檔案格式Java 虛擬機器規範

  通過上面我們知道 JVM 是通過位元組碼的二進位制資訊載入類的,那麼我們如果在執行期系統中,遵循 Java 編譯系統組織 .class 檔案的格式和結構,生成相應的二進位制資料,然後再把這個二進位制資料轉換成對應的類,這樣就可以在執行中動態生成一個我們想要的類了:

Android 動態代理以及利用動態代理實現 ServiceHook
這裡寫圖片描述

  Java 中有很多的框架可以在執行時根據 JVM 規範動態的生成對應的 .class 二進位制位元組碼,比如 ASM 和 Javassist 等,這裡就不詳細介紹了,感興趣的可以去查閱相關的資料。這裡我們就以動態代理模式為例來介紹一下我們要用到這兩個很重要的類,關於動態代理模式,我在 java/android 設計模式學習筆記(9)---代理模式中已經介紹過了,但是當時並沒有詳細分析過 InvocationHandler 介面和 Proxy 類,這裡就來詳細介紹一下。在代理模式那篇部落格中,我們提到了代理模式分為動態代理和靜態代理:

Android 動態代理以及利用動態代理實現 ServiceHook
這裡寫圖片描述


上面就是靜態代理模式的類圖,當在程式碼階段規定這種代理關係時,ProxySubject 類通過編譯器生成 .class 位元組碼檔案,當系統執行之前,這個 .class 檔案就已經存在了。動態代理模式的結構和上面的靜態代理模式的結構稍微有所不同,它引入了一個 InvocationHandler 介面和 Proxy 類。在靜態代理模式中,代理類 ProxySubject 中的方法,都指定地呼叫到特定 RealSubject 中對應的方法,ProxySubject 所做的事情無非是呼叫觸發 RealSubject 對應的方法;動態代理工作的基本模式就是將自己方法功能的實現交給 InvocationHandler 角色,外界對 Proxy 角色中每一個方法的呼叫,Proxy 角色都會交給 InvocationHandler 來處理,而 InvocationHandler 則呼叫 RealSubject 的方法,如下圖所示:
Android 動態代理以及利用動態代理實現 ServiceHook
這裡寫圖片描述

InvocationHandler 介面和 Proxy

  我們來分析一下動態代理模式中 ProxySubject 的生成步驟:

  1. 獲取 RealSubject 上的所有介面列表;
  2. 確定要生成的代理類的類名,系統預設生成的名字為:com.sun.proxy.$ProxyXXXX ;
  3. 根據需要實現的介面資訊,在程式碼中動態建立該 ProxySubject 類的位元組碼;
  4. 將對應的位元組碼轉換為對應的 Class 物件;
  5. 建立 InvocationHandler 的例項物件 h,用來處理 Proxy 角色的所有方法呼叫;
  6. 以建立的 h 物件為引數,例項化一個 Proxy 角色物件。
具體的程式碼為:

Subject.java

public interface Subject {
    String operation();
}複製程式碼

RealSubject.java

public class RealSubject implements Subject{
    @Override
    public String operation() {
        return "operation by subject";
    }
}複製程式碼

ProxySubject.java

public class ProxySubject implements InvocationHandler{
     protected Subject subject;
    public ProxySubject(Subject subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //do something before
        return method.invoke(subject, args);
    }
}複製程式碼

測試程式碼

Subject subject = new RealSubject();
ProxySubject proxy = new ProxySubject(subject);
Subject sub = (Subject) Proxy.newProxyInstance(subject.getClass().getClassLoader(),
        subject.getClass().getInterfaces(), proxy);
sub.operation();複製程式碼

以上就是動態代理模式的最簡單實現程式碼,JDK 通過使用 java.lang.reflect.Proxy 包來支援動態代理,我們來看看這個類的表述:

Proxy provides static methods for creating dynamic proxy classes and instances, and it is also the 
superclass of all dynamic proxy classes created by those methods.複製程式碼

一般情況下,我們使用下面的 newProxyInstance) 方法來生成動態代理:

Public methods
static Object newProxyInstance(ClassLoader loader, Class[]<?> interfaces, InvocationHandler h)
Returns an instance of a proxy class for the specified interfaces that dispatches method invocations to the specified invocation handler.

對應的引數為:

Parameters
loader ClassLoader: the class loader to define the proxy class
interfaces Class: the list of interfaces for the proxy class to implement
h InvocationHandler: the invocation handler to dispatch method invocations to

我們來仔細看看這個函式的實現:

public static Object newProxyInstance(ClassLoader loader, Class<?>[]interfaces,InvocationHandler h) throws IllegalArgumentException { 
    // 檢查 h 不為空,否則拋異常
    if (h == null) { 
        throw new NullPointerException(); 
    } 

    // 獲得與指定類裝載器和一組介面相關的代理類型別物件
    Class cl = getProxyClass(loader, interfaces); 

    // 通過反射獲取建構函式物件並生成代理類例項
    try { 
        Constructor cons = cl.getConstructor(constructorParams); 
        return (Object) cons.newInstance(new Object[] { h }); 
    } catch (NoSuchMethodException e) { throw new InternalError(e.toString()); 
    } catch (IllegalAccessException e) { throw new InternalError(e.toString()); 
    } catch (InstantiationException e) { throw new InternalError(e.toString()); 
    } catch (InvocationTargetException e) { throw new InternalError(e.toString()); 
    } 
}複製程式碼

Proxy 類的 getProxyClass 方法呼叫了 ProxyGenerator 的 generatorProxyClass 方法去生成動態類:

public static byte[] generateProxyClass(final String name, Class[] interfaces)複製程式碼

這個方法我們下面將會介紹到,這裡先略過,生成這個動態類的位元組碼之後,通過反射去生成這個動態類的物件,通過 Proxy 類的這個靜態函式生成了一個動態代理物件 sub 之後,呼叫 sub 代理物件的每一個方法,在程式碼內部,都是直接呼叫了 InvocationHandler 的 invoke 方法,而 invoke 方法根據代理類傳遞給自己的 method 引數來區分是什麼方法,我們來看看 InvocationHandler 類的介紹:

InvocationHandler is the interface implemented by the invocation handler of a proxy instance.

Each proxy instance has an associated invocation handler. When a method is invoked on a proxy 
instance, the method invocation is encoded and dispatched to the invoke method of its invocation handler.複製程式碼
Public methods
abstract Object invoke(Object proxy, Method method, Object[] args)
Processes a method invocation on a proxy instance and returns the result.

方法的引數和返回:

Parameters
proxy Object: the proxy instance that the method was invoked on
method Method: the Method instance corresponding to the interface method invoked on the proxy instance. The declaring class of the Method object will be the interface that the method was declared in, which may be a superinterface of the proxy interface that the proxy class inherits the method through.
args Object: an array of objects containing the values of the arguments passed in the method invocation on the proxy instance, or null if interface method takes no arguments. Arguments of primitive types are wrapped in instances of the appropriate primitive wrapper class, such as java.lang.Integer or java.lang.Boolean.
Returns
Object the value to return from the method invocation on the proxy instance. If the declared return type of the interface method is a primitive type, then the value returned by this method must be an instance of the corresponding primitive wrapper class; otherwise, it must be a type assignable to the declared return type. If the value returned by this method is null and the interface method's return type is primitive, then a NullPointerException will be thrown by the method invocation on the proxy instance. If the value returned by this method is otherwise not compatible with the interface method's declared return type as described above, a ClassCastException will be thrown by the method invocation on the proxy instance.

上面提到的一點需要特別注意的是,如果 Subject 類中定義的方法返回值為 8 種基本資料型別,那麼在 ProxySubject 類中必須要返回相應的基本型別包裝類,即 int 對應的返回為 Integer 等等,還需要注意的是如果此時返回 null,則會丟擲 NullPointerException,除此之外的其他情況下返回值的物件必須要和 Subject 類中定義方法的返回值一致,要不然會丟擲 ClassCastException。

生成原始碼分析

  那麼通過 Proxy 類的 newProxyInstance 方法動態生成的類是什麼樣子的呢,我們上面也提到了,JDK 為我們提供了一個方法 ProxyGenerator.generateProxyClass(String proxyName,class[] interfaces) 來產生動態代理類的位元組碼,這個類位於 sun.misc 包中,是屬於特殊的 jar 包,於是問題又來了,android studio 建立的 android 工程是沒法找到 ProxyGenerator 這個類的,這個類在 jre 目錄下,就算我把這個類相關的 .jar 包拷貝到工程裡面並且在 gradle 裡面引用它,雖然最後能夠找到這個類,但是編譯時又會出現很奇葩的問題,所以,沒辦法嘍,android studio 沒辦法建立普通的 java 工程,只能自己裝一個 intellij idea 或者求助相關的同事了。建立好 java 工程之後,使用下面這段程式碼就可以將生成的類匯出到指定路徑下面:

public static void generateClassFile(Class clazz,String proxyName)
{
    //根據類資訊和提供的代理類名稱,生成位元組碼  
    byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, clazz.getInterfaces());
    String paths = "D:\\"; // 這裡寫死路徑為 D 盤,可以根據實際需要去修改
    System.out.println(paths);
    FileOutputStream out = null;

    try {
        //保留到硬碟中  
        out = new FileOutputStream(paths+proxyName+".class");
        out.write(classFile);
        out.flush();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}複製程式碼

呼叫程式碼的方式為:

generateClassFile(ProxySubject.class, "ProxySubject");複製程式碼

最後就會在 D 盤(如果沒有修改路徑)的根目錄下面生成一個 ProxySubject.class 的檔案,使用 jd-gui 就可以開啟該檔案:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class ProxySubject
  extends Proxy
  implements Subject
{
  private static Method m1;
  private static Method m3;
  private static Method m2;
  private static Method m0;

  public ProxySubject(InvocationHandler paramInvocationHandler)
  {
    super(paramInvocationHandler);
  }

  public final boolean equals(Object paramObject)
  {
    try
    {
      return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }

  public final String operation()
  {
    try
    {
      return (String)this.h.invoke(this, m3, null);
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }

  public final String toString()
  {
    try
    {
      return (String)this.h.invoke(this, m2, null);
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }

  public final int hashCode()
  {
    try
    {
      return ((Integer)this.h.invoke(this, m0, null)).intValue();
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }

  static
  {
    try
    {
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
      m3 = Class.forName("Subject").getMethod("operation", new Class[0]);
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      return;
    }
    catch (NoSuchMethodException localNoSuchMethodException)
    {
      throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
    }
    catch (ClassNotFoundException localClassNotFoundException)
    {
      throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
    }
  }
}複製程式碼

可以觀察到這個生成的類繼承自 java.lang.reflect.Proxy,實現了 Subject 介面,我們在看看生成動態類的程式碼:

Subject sub = (Subject) Proxy.newProxyInstance(subject.getClass().getClassLoader(),
        subject.getClass().getInterfaces(), proxy);複製程式碼

可見這個動態生成類會實現 subject.getClass().getInterfaces() 中的所有介面,並且還有一點是類中所有的方法都是 final 的,而且該類也是 final ,所以該類不可繼承,最後就是所有的方法都會呼叫到 InvocationHandler 物件 h 的 invoke() 方法,這也就是為什麼最後會呼叫到 ProxySubject 類的 invoke() 方法了,畫一下它們的簡單類圖:

Android 動態代理以及利用動態代理實現 ServiceHook
這裡寫圖片描述


從這個類圖可以很清楚的看明白,動態生成的類 ProxySubject(同名,所以後面加上了 Dynamic)持有了實現 InvocationHandler 介面的 ProxySubject 類的物件 h,然後呼叫代理物件的 operation 方法時,就會呼叫到物件 h 的 invoke 方法中,invoke 方法中根據 method 的名字來區分到底是什麼方法,然後通過 method.invoke() 方法來呼叫具體物件的對應方法。

Android 中利用動態代理實現 ServiceHook

  通過上面對 InvocationHandler 的介紹,我們對這個介面應該有了大體的瞭解,但是在執行時動態生成的代理類有什麼作用呢,其實它的作用就是在呼叫真正業務之前或者之後插入一些額外的操作:

Android 動態代理以及利用動態代理實現 ServiceHook
這裡寫圖片描述


所以簡而言之,代理類的處理邏輯很簡單,就是在呼叫某個方法前及方法後插入一些額外的業務。而我們在 Android 中的實踐例子就是在真正呼叫系統的某個 Service 之前和之後選擇性的做一些自己特殊的處理,這種思想在外掛化框架上也是很重要的。那麼我們具體怎麼去實現 hook 系統的 Service ,在真正呼叫系統 Service 的時候附加上我們需要的業務呢,這就需要介紹 ServiceManager 這個類了。

ServiceManager 介紹以及 hook 的步驟

  第一步

  關於 ServiceManager 的詳細介紹在我的部落格:android IPC通訊(下)-AIDL 中已經介紹過了,這裡就不贅述了,強烈建議大家去看一下那篇部落格,我們這裡就著重看一下 ServiceManager 的 getService(String name) 方法:

public final class ServiceManager {
    private static final String TAG = "ServiceManager";

    private static IServiceManager sServiceManager;
    private static HashMap<String, IBinder> sCache = new HashMap<String, IBinder>();

    private static IServiceManager getIServiceManager() {
        if (sServiceManager != null) {
            return sServiceManager;
        }

        // Find the service manager
        sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());
        return sServiceManager;
    }

    /**
     * Returns a reference to a service with the given name.
     * 
     * @param name the name of the service to get
     * @return a reference to the service, or <code>null</code> if the service doesn't exist
     */
    public static IBinder getService(String name) {
        try {
            IBinder service = sCache.get(name);
            if (service != null) {
                return service;
            } else {
                return getIServiceManager().getService(name);
            }
        } catch (RemoteException e) {
            Log.e(TAG, "error in getService", e);
        }
        return null;
    }

    public static void addService(String name, IBinder service) {
    ...
    }
    ....
}複製程式碼

我們可以看到,getService 方法第一步會去 sCache 這個 map 中根據 Service 的名字獲取這個 Service 的 IBinder 物件,如果獲取到為空,則會通過 ServiceManagerNative 通過跨程式通訊獲取這個 Service 的 IBinder 物件,所以我們就以 sCache 這個 map 為切入點,反射該物件,然後修改該物件,由於系統的 android.os.ServiceManager 類是 @hide 的,所以只能使用反射,根據這個初步思路,寫下第一步的程式碼:

Class c_ServiceManager = Class.forName("android.os.ServiceManager");
if (c_ServiceManager == null) {
    return;
}

if (sCacheService == null) {
    try {
        Field sCache = c_ServiceManager.getDeclaredField("sCache");
        sCache.setAccessible(true);
        sCacheService = (Map<String, IBinder>) sCache.get(null);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
sCacheService.remove(serviceName);
sCacheService.put(serviceName, service);複製程式碼

反射 sCache 這個變數,移除系統 Service,然後將我們自己改造過的 Service put 進去,這樣就能實現當呼叫 ServiceManager 的 getService(String name) 方法的時候,返回的是我們改造過的 Service 而不是系統的原生 Service。

  第二步

  第一步知道了如何去將改造過後的 Service put 進系統的 ServiceManager 中,第二步就是去生成一個 hook Service 了,怎麼去生成呢?這就要用到我們上面介紹到的 InvocationHandler 類,我們先獲取原生的 Service ,然後通過 InvocationHandler 去構造一個 hook Service,最後通過第一步的步驟 put 進 sCache 這個變數即可,第二步程式碼:

public class ServiceHook implements InvocationHandler {
    private static final String TAG = "ServiceHook";

    private IBinder mBase;
    private Class<?> mStub;
    private Class<?> mInterface;
    private InvocationHandler mInvocationHandler;

    public ServiceHook(IBinder mBase, String iInterfaceName, boolean isStub, InvocationHandler InvocationHandler) {
        this.mBase = mBase;
        this.mInvocationHandler = InvocationHandler;

        try {
            this.mInterface = Class.forName(iInterfaceName);
            this.mStub = Class.forName(String.format("%s%s", iInterfaceName, isStub ? "$Stub" : ""));
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if ("queryLocalInterface".equals(method.getName())) {
            return Proxy.newProxyInstance(proxy.getClass().getClassLoader(), new Class[] { mInterface },
                    new HookHandler(mBase, mStub, mInvocationHandler));
        }

        Log.e(TAG, "ERROR!!!!! method:name = " + method.getName());
        return method.invoke(mBase, args);
    }

    private static class HookHandler implements InvocationHandler {
        private Object mBase;
        private InvocationHandler mInvocationHandler;

        public HookHandler(IBinder base, Class<?> stubClass,
                           InvocationHandler InvocationHandler) {
            mInvocationHandler = InvocationHandler;

            try {
                Method asInterface = stubClass.getDeclaredMethod("asInterface", IBinder.class);
                this.mBase = asInterface.invoke(null, base);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (mInvocationHandler != null) {
                return mInvocationHandler.invoke(mBase, method, args);
            }
            return method.invoke(mBase, args);
        }
    }
}複製程式碼

這裡我們以 ClipboardService 的呼叫程式碼為例:

IBinder clipboardService = ServiceManager.getService(Context.CLIPBOARD_SERVICE);
String IClipboard = "android.content.IClipboard";

if (clipboardService != null) {
    IBinder hookClipboardService =
            (IBinder) Proxy.newProxyInstance(clipboardService.getClass().getClassLoader(),
                            clipboardService.getClass().getInterfaces(),
                    new ServiceHook(clipboardService, IClipboard, true, new ClipboardHookHandler()));
    //呼叫第一步的方法
    ServiceManager.setService(Context.CLIPBOARD_SERVICE, hookClipboardService);
} else {
    Log.e(TAG, "ClipboardService hook failed!");
}複製程式碼

分析一下上面的這段程式碼,分析之前,先要看一下 ClipboardService 的相關類圖:

Android 動態代理以及利用動態代理實現 ServiceHook
這裡寫圖片描述

從這張類圖我們可以清晰的看見 ClipboardService 的相關繼承關係,接下來就是分析程式碼了:

  • 呼叫程式碼中,Proxy.newProxyInstance 函式的第二個引數需要注意,由於 ClipboardService 繼承自三個介面,所以這裡需要把所有的介面傳遞進去,但是如果將第二個引數變更為 new Class[] { IBinder.class } 其實也是沒有問題的(感興趣的可以試一下,第一個引數修改為 IBinder.class.getClassLoader(),第二個引數修改為 new Class[]{IBinder.class},也是可以的),因為實際使用的時候,我們只是用到了 IBinder 類的 queryLocalInterface 方法,其他的方法都沒有使用到,接下來我們就會說明 queryLocalInterface 這個函式的作用;
  • Proxy.newProxyInstance 函式的第三個引數為 ServcieHook 物件,所以當把這個 Service set 進 sCache 變數的時候,所有呼叫 ClipboardService 的操作都將呼叫到 ServiceHook 中的 invoke 方法中;
  • 接著我們來看看 ServiceHook 的 invoke 函式,很簡單,當函式為 queryLocalInterface 方法的時候返回一個 HookHandler 的物件,其他的情況直接呼叫 method.invoke 原生系統的 ClipboardService 功能,為什麼只處理 queryLocalInterface 方法呢,這個我在部落格:java/android 設計模式學習筆記(9)---代理模式 中分析 AMS 的時候已經提到了,asInterface 方法最終會呼叫到 queryLocalInterface 方法,queryLocalInterface 方法最後的返回結果會作為 asInterface 的結果而返回給 Service 的呼叫方,所以 queryLocalInterface 方法的最後返回的物件是會被外部直接呼叫的,這也解釋了為什麼呼叫程式碼中的第二個引數變更為 new Class[] { IBinder.class } 也是沒有問題,因為第一次呼叫到 queryLocalInterface 函式之後,後續的所有呼叫都到了 HookHandler 物件中,動態生成的物件中只需要有 IBinder 的 queryLocalInterface 方法即可,而不需要 IClipboard 介面的其他方法;
  • 接下來是 HookHandler 類,首先我們看看這個類的建構函式,第一個引數為系統的 ClipboardService,第二個引數為
Class.forName(String.format("%s%s", iInterfaceName, isStub ? "$Stub" : ""))//"android.content.IClipboard"複製程式碼

這個引數我們們對照上面的類圖,這個類為 ClipboardService 的父類,它裡面有一個 asInterface 的方法,通過反射 asInterface 方法然後將 IBinder 物件變成 IInterface 物件,為什麼要這麼做,可以去看看我的部落格: java/android 設計模式學習筆記(9)---代理模式 中的最後總結,通過 ServiceManager.getService 方法獲取一個 IBinder 物件,但是這個 IBinder 物件不能直接呼叫,必須要通過 asInterface 方法轉成對應的 IInterface 物件才可以使用,所以 mBase 物件其實是一個 IInterface 物件:

Android 動態代理以及利用動態代理實現 ServiceHook
這裡寫圖片描述


最後也證實了這個結果,為什麼是 Proxy 物件這就不用我解釋了吧;

  • 最後是 HookHandler 的 invoke 方法,這個方法呼叫到了 ClipboardHookHandler 物件,我們來看看這個類的實現:
  • public class ClipboardHook {
    
        private static final String TAG = ClipboardHook.class.getSimpleName();
    
        public static void hookService(Context context) {
            IBinder clipboardService = ServiceManager.getService(Context.CLIPBOARD_SERVICE);
            String IClipboard = "android.content.IClipboard";
    
            if (clipboardService != null) {
                IBinder hookClipboardService =
                        (IBinder) Proxy.newProxyInstance(IBinder.class.getClassLoader(),
                                new Class[]{IBinder.class},
                                new ServiceHook(clipboardService, IClipboard, true, new ClipboardHookHandler()));
                ServiceManager.setService(Context.CLIPBOARD_SERVICE, hookClipboardService);
            } else {
                Log.e(TAG, "ClipboardService hook failed!");
            }
        }
    
        public static class ClipboardHookHandler implements InvocationHandler {
    
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                String methodName = method.getName();
                int argsLength = args.length;
                //每次從本應用複製的文字,後面都加上分享的出處
                if ("setPrimaryClip".equals(methodName)) {
                    if (argsLength >= 2 && args[0] instanceof ClipData) {
                        ClipData data = (ClipData) args[0];
                        String text = data.getItemAt(0).getText().toString();
                        text += "this is shared from ServiceHook-----by Shawn_Dut";
                        args[0] = ClipData.newPlainText(data.getDescription().getLabel(), text);
                    }
                }
                return method.invoke(proxy, args);
            }
        }
    }複製程式碼

    所以 ClipboardHookHandler 類的 invoke 方法最終獲取到了要 hook 的 Service 的 IInterface 物件(即為 IClipboard.Proxy 物件,最後通過 Binder Driver 呼叫到了系統的 ClipboardService 中),呼叫函式的 Method 物件和引數列表物件,獲取到了這些之後,不用我說了,就可以盡情的去做一些額外的操作了,我這裡是在仿照知乎複製文字時,在後面加上類似的版權宣告。

    問題討論

      上面就是 ServiceHook 的詳細步驟了,瞭解它必須要對 InvocationHandler 有詳細的瞭解,並且還要去看一下 AOSP 原始碼,比如要去 hook ClipboardService ,那麼就要去先看看 ClipboardService 的原始碼,看看這個類中每個函式的名字和作用,引數列表中每個引數的順序和作用,而且有時候這還遠遠不夠,我們知道,隨著 Android 每個版本的更新,這些類可能也會被更新修改甚至刪除,很有可能對於新版本來說老的 hook 方法就不管用了,這時候必須要去了解最新的原始碼,看看更新修改的地方,針對新版本去重新制定 hook 的步驟,這是一點需要慎重對待考慮的地方。

    步驟

    Android 動態代理以及利用動態代理實現 ServiceHook
    這裡寫圖片描述

    原始碼

      此為我們公司某位大神程式碼,經過整理修改而出,不知道有沒有版權問題,哈哈哈,謝謝周杰大神,雖然已經不在公司,感謝感謝~~

      原始碼下載地址:github.com/zhaozepeng/…

      先來看看執行的效果:

      

    Android 動態代理以及利用動態代理實現 ServiceHook
    這裡寫圖片描述


      最後把所有的程式碼貼出來:

    ServiceManager.java

    public class ServiceManager {
    
        private static Method sGetServiceMethod;
        private static Map<String, IBinder> sCacheService;
        private static Class c_ServiceManager;
    
        static {
            try {
                c_ServiceManager = Class.forName("android.os.ServiceManager");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public static IBinder getService(String serviceName) {
            if (c_ServiceManager == null) {
                return null;
            }
    
            if (sGetServiceMethod == null) {
                try {
                    sGetServiceMethod = c_ServiceManager.getDeclaredMethod("getService", String.class);
                    sGetServiceMethod.setAccessible(true);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                }
            }
    
            if (sGetServiceMethod != null) {
                try {
                    return (IBinder) sGetServiceMethod.invoke(null, serviceName);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
    
            return null;
        }
    
        public static void setService(String serviceName, IBinder service) {
            if (c_ServiceManager == null) {
                return;
            }
    
            if (sCacheService == null) {
                try {
                    Field sCache = c_ServiceManager.getDeclaredField("sCache");
                    sCache.setAccessible(true);
                    sCacheService = (Map<String, IBinder>) sCache.get(null);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            sCacheService.remove(serviceName);
            sCacheService.put(serviceName, service);
        }
    }複製程式碼

    ServiceManager 這個類就是使用反射的方式去獲取對應 Service (這裡不能使用 Context.getSystemService 函式,因為它的返回不是 IBinder 物件,比如對於 ClipboardService,它就是 ClipboardManager 物件)和設定 service 到 sCache 變數中;

    ServiceHook.java

    public class ServiceHook implements InvocationHandler {
        private static final String TAG = "ServiceHook";
    
        private IBinder mBase;
        private Class<?> mStub;
        private Class<?> mInterface;
        private InvocationHandler mInvocationHandler;
    
        public ServiceHook(IBinder mBase, String iInterfaceName, boolean isStub, InvocationHandler InvocationHandler) {
            this.mBase = mBase;
            this.mInvocationHandler = InvocationHandler;
    
            try {
                this.mInterface = Class.forName(iInterfaceName);
                this.mStub = Class.forName(String.format("%s%s", iInterfaceName, isStub ? "$Stub" : ""));
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    
        @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if ("queryLocalInterface".equals(method.getName())) {
                return Proxy.newProxyInstance(proxy.getClass().getClassLoader(), new Class[] { mInterface },
                        new HookHandler(mBase, mStub, mInvocationHandler));
            }
    
            Log.e(TAG, "ERROR!!!!! method:name = " + method.getName());
            return method.invoke(mBase, args);
        }
    
        private static class HookHandler implements InvocationHandler {
            private Object mBase;
            private InvocationHandler mInvocationHandler;
    
            public HookHandler(IBinder base, Class<?> stubClass,
                               InvocationHandler InvocationHandler) {
                mInvocationHandler = InvocationHandler;
    
                try {
                    Method asInterface = stubClass.getDeclaredMethod("asInterface", IBinder.class);
                    this.mBase = asInterface.invoke(null, base);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
    
            @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if (mInvocationHandler != null) {
                    return mInvocationHandler.invoke(mBase, method, args);
                }
                return method.invoke(mBase, args);
            }
        }
    }複製程式碼

    這個類上面介紹的很詳細了,在這裡就不繼續介紹了;

    ClipboardHook.java

    public class ClipboardHook {
    
        private static final String TAG = ClipboardHook.class.getSimpleName();
    
        public static void hookService(Context context) {
            IBinder clipboardService = ServiceManager.getService(Context.CLIPBOARD_SERVICE);
            String IClipboard = "android.content.IClipboard";
    
            if (clipboardService != null) {
                IBinder hookClipboardService =
                        (IBinder) Proxy.newProxyInstance(IBinder.class.getClassLoader(),
                                new Class[]{IBinder.class},
                                new ServiceHook(clipboardService, IClipboard, true, new ClipboardHookHandler()));
                ServiceManager.setService(Context.CLIPBOARD_SERVICE, hookClipboardService);
            } else {
                Log.e(TAG, "ClipboardService hook failed!");
            }
        }
    
        public static class ClipboardHookHandler implements InvocationHandler {
    
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                String methodName = method.getName();
                int argsLength = args.length;
                //每次從本應用複製的文字,後面都加上分享的出處
                if ("setPrimaryClip".equals(methodName)) {
                    if (argsLength >= 2 && args[0] instanceof ClipData) {
                        ClipData data = (ClipData) args[0];
                        String text = data.getItemAt(0).getText().toString();
                        text += "this is shared from ServiceHook-----by Shawn_Dut";
                        args[0] = ClipData.newPlainText(data.getDescription().getLabel(), text);
                    }
                }
                return method.invoke(proxy, args);
            }
        }
    }複製程式碼

    這裡的 hook ClipboardService 使用的是,將複製的文字取出,在結尾處新增我們自定義的文字,使用者再貼上到其他地方的時候,就會出現我們新增上我們自定義文字後的文字了,還有需要注意的是這隻會影響應用程式的 ClipboardService,並不會影響主程式的相關 Service,因為不管怎麼 hook,修改的都是應用程式的 ServiceManager 裡面的 sCache 變數,應用程式的 ServiceManager 其實也就相當於一個 Binder Client,sCache 裡面獲取不到對應的 Service,它就會自動通過 Binder Driver 和 Binder Server (主程式的 ServiceManager)通訊去獲取對應的 Service,所以修改 Binder Client,其實是不會影響 Binder Server的,不明白的建議還是看看:android IPC通訊(下)-AIDL

    測試程式碼

    switch (v.getId()) {
        case R.id.btn_copy:
            String input = mEtInput.getText().toString().trim();
            if (TextUtils.isEmpty(input)) {
                Toast.makeText(this, "input不能為空", Toast.LENGTH_SHORT).show();
                return;
             }
    
            //複製
            ClipData clip = ClipData.newPlainText("simple text", mEtInput.getText().toString());
            clipboard.setPrimaryClip(clip);
            break;
        case R.id.btn_show_paste:
            //黏貼
            clip = clipboard.getPrimaryClip();
            if (clip != null && clip.getItemCount() > 0) {
                Toast.makeText(this, clip.getItemAt(0).getText(), Toast.LENGTH_SHORT).show();
            }
            break;
    }複製程式碼

    測試程式碼,我這裡就不需要說明了,Clipboard 的簡單使用而已。
      
      這裡只演示了 hook ClipboardService 的例子,其他的使用方式比如外掛化等等在這裡就不一一介紹了,感興趣的可以去網上查閱相關的資料。
      轉載請註明出處:blog.csdn.net/self_study/…

    引用

    blog.csdn.net/luanlouis/a…
    www.cnblogs.com/xiaoluo5013…
    blog.csdn.net/self_study/…
    paddy-w.iteye.com/blog/841798
    www.cnblogs.com/flyoung2008…

    相關文章