如果您還沒有閱讀第一部分的內容,這篇文章不需往下讀,在閱讀第一部分後才能繼續下面的內容:Hook動態代理
基於上面的一篇博客,我們學習了代理的概念,以及如何尋找Hook點。本篇博客將繼續拓展前文,不過這次內容要深入很多,這些都是繼續學習插件化的基礎,爲了避免長篇的介紹代理這些枯燥的概念,我特意把它分開來講,難度一次提升,希望讀者能夠耐心閱讀。
之前我們解釋代理設計模式的時候,用的是小明打官司的例子,通過這個例子我們發現,如果一個對象需要代理,我們就需要定義很多個代理類,這對於大型項目的話,顯然有點不太現實(程序員也是人啊),所以,這裏又萌生動態代理的概念。
這種機制可以理解爲jvm運行時動態爲我們生成一個代理類,我們只需簡單的實現InvocationHandler接口即可
我們看下demo:
//定義我們自己的接口
interface MyInterface {
void foo();
}
interface MyInterface2 {
}
//實現類
static class MyObject implements MyInterface , MyInterface2{
@Override
public void foo() {
System.out.print("proxy foo");
}
}
//動態代理
static class MyInvocation implements InvocationHandler {
public MyInvocation() {
}
Object m_object;
public MyInvocation(Object object) {
m_object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//當調用foo函數之前我們打印一段haha字符串
if("foo".equals(method.getName())) {
System.out.println("haha");
}
return method.invoke(m_object, args);
}
}
public static void main(String[] args) {
MyObject myObject = new MyObject();
System.out.println(myObject);
//第一個參數是指定類加載器,第二個參數指定返回的代理對象要實現的接口
//第三個參數用於處理分發的函數調用,我們可以如本例一樣,在執行foo函數之前打印haha字符串
MyInterface myInterface = (MyInterface) Proxy.newProxyInstance(myObject.getClass().getClassLoader(),
new Class[]{MyInterface.class, MyInterface2.class}, new MyInvocation(myObject));
myInterface.foo();
}
還是很好理解的,所以我們可以開始本文主題了。
我們使用插件的目的,就是希望在無需重新編譯app的情況下,升級添加某些功能,但是,如果我們使用插件來達到我們的目的,我們並不能正常使用一些系統服務,比如:粘貼板,網絡管理器。這是不能忍的,既然我們使用插件,就應該和平時coding一樣,沒有任何差異——我們能夠調用任何我們能夠獲取的系統服務。所以,這一節我們就要解決hook系統服務的問題,保證我們插件能夠正常使用。
回想一下,如果我們平時需要調用系統服務,我們如何實現呢?
like this:
ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
我們本節就是打算hook粘貼板服務,試着讓粘貼板裏面一直有我們自定義的內容
Hook粘貼板服務
在開始之前,我們還是和第一節一樣,切進去看下,一步步的尋找hook點,還記得我們第一節尋找hook點的原則嗎——靜態或單例對象
我們切換到activity的getSystemService中:
@Override
public Object getSystemService(@ServiceName @NonNull String name) {
if (getBaseContext() == null) {
throw new IllegalStateException(
"System services not available to Activities before onCreate()");
}
if (WINDOW_SERVICE.equals(name)) {
return mWindowManager;
} else if (SEARCH_SERVICE.equals(name)) {
ensureSearchManager();
return mSearchManager;
}
return super.getSystemService(name);
}
這裏調用了Activity的函數,顯然,我們如果使用粘貼板服務的話,還需調用父類的getSystemService方法。切換到Activity的父類——ContextThemeWrapper去看
顯然我們又要到getBaseContext這個函數返回的對象去看。
不過可惜的是,它返回的只是一個base對象,從ContextThemeWrapper這個類名就不難看出,它只是一個包裝器,真正實現功能的肯定在另一個類中:
它的CTOR:
從我截的圖可以看出,初始化或者賦值mBase的話,只有通過構造函數或者attachBaseContext方法,不過,從第一節我們介紹ActivityThread的時候就提出了,activity只會在activity thread這個類中被實例化,所以,我們如果要找mBase被修改的位置,肯定要找到那個類(ActivityThread)並從中找線索
切換到ActivityThread.java
在這個類中有這樣一個函數:private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent)
他是用來轉載activity的,顯然,這裏是最有可能找到Activity產生實例的地方。
哦?這裏有個newActivity哦,切進去看下
確實,這裏是實例化Activity的,不過它並沒有調用有參數的構造函數,所以它的父類ContextThemeWrapper也就不會憑空賦一個值
那麼線索就只剩下attachBaseContext了,檢索下,我們又找到了這裏:
在Activity的attach這個函數中,調用了attachBaseContext,而這個顯然在ActivityThread這個類中出現過,我們再次切換到ActivityThread:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
Activity activity = null;
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
try {
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
if (localLOGV) Slog.v(TAG, "Performing launch of " + r);
if (localLOGV) Slog.v(
TAG, r + ": app=" + app
+ ", appName=" + app.getPackageName()
+ ", pkg=" + r.packageInfo.getPackageName()
+ ", comp=" + r.intent.getComponent().toShortString()
+ ", dir=" + r.packageInfo.getAppDir());
if (activity != null) {
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
+ r.activityInfo.name + " with config " + config);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor);
...
return activity;
}
createBaseContextForActivity的返回值,被作爲參數傳入到了Activity的attach。
我們在切入到createBaseContextForActivity中去查看:
private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) {
int displayId = Display.DEFAULT_DISPLAY;
try {
displayId = ActivityManagerNative.getDefault().getActivityDisplayId(r.token);
} catch (RemoteException e) {
}
ContextImpl appContext = ContextImpl.createActivityContext(
this, r.packageInfo, displayId, r.overrideConfig);
appContext.setOuterContext(activity);
Context baseContext = appContext;
final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
// For debugging purposes, if the activity's package name contains the value of
// the "debug.use-second-display" system property as a substring, then show
// its content on a secondary display if there is one.
String pkgName = SystemProperties.get("debug.second-display.pkg");
if (pkgName != null && !pkgName.isEmpty()
&& r.packageInfo.mPackageName.contains(pkgName)) {
for (int id : dm.getDisplayIds()) {
if (id != Display.DEFAULT_DISPLAY) {
Display display =
dm.getCompatibleDisplay(id, appContext.getDisplayAdjustments(id));
baseContext = appContext.createDisplayContext(display);
break;
}
}
}
return baseContext;
}
函數生成了一個ContextImpl對象,並且賦給baseContext,然後返回,哈哈,終於找到了Context這個接口真實的實現類!ContextImpl。
我們先總結一下剛剛到底發生了什麼:
我們從Activity的getSystemService一直跟蹤到了它的父類ContextThemeWrapper,想再往下找的時候,發現ContextThemeWrapper只是一個包裝器,真實的實現不在這裏,所以我們又要找到真實的實現位置。一路跟蹤,最後找到了ContextImpl
我們現在切換到ContextImpl中:
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
看來又要看看SystemServiceRegistry了,不過還好,在SystemServiceRegistry裏面我們找到了想要的內容:
哇哦,這裏都是調用了ServiceManager的Binder,然後 I*Manager.*.asInterface作爲接口返回,最後再生成具體的java平臺類!
我們看下ServiceManager.java:
public static IBinder getService(String name) {
50 try {
51 IBinder service = sCache.get(name);//靜態變量哦!
52 if (service != null) {
53 return service;
54 } else {
55 return getIServiceManager().getService(name);
56 }
57 } catch (RemoteException e) {
58 Log.e(TAG, "error in getService", e);
59 }
60 return null;
61 }
哇塞,看到沒sCache,從命名就可以看出它是一個靜態變量,回憶之前我們尋找hook點時候指出,最好的hook點就是靜態或者單例對象,畢竟他們只有一個,且相對不會變化。那麼思路來了,如果我們能夠生成一自定義的ibinder對象,然後存放到sCache中,不久能夠Hook系統服務了嗎?簡直完美啊!
但是值得注意的是,由於粘貼板服務和我們的app不在同一個進程裏面,這就意味着,及時我們hook了遠程的粘貼板服務,我們並不能修改當前app中binder代理的行爲。這裏需要讀者熟悉android的aidl多進程通信。如果不太懂,就去閱讀下aidl相關的文章
所以我們還得看下本地服務代理的代碼:
101行獲得了遠程服務的binder,之後,通過IClipboard.Stub.asInterface函數,我們獲得了遠程服務的接口。之後就能像調用本地代碼一樣調用遠程服務了。
我們再到102行函數裏查看:
26 public static android.content.IClipboard asInterface(android.os.IBinder obj)
27 {
28 if ((obj==null)) {
29 return null;
30 }
//調用IBinder的queryLocalInterface函數,獲得服務操作的接口
31 android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
//如果不爲空 那麼就返回
32 if (((iin!=null)&&(iin instanceof android.content.IClipboard))) {
33 return ((android.content.IClipboard)iin);
34 }
//否則創建一個服務代理對象
35 return new android.content.IClipboard.Stub.Proxy(obj);
36 }
顯然我們一定不能讓31行的iin爲空,因爲爲空它會返回系統自己定義的服務代理對象。那個對象的行爲我們無法修改。我們只能是的31永遠不爲空,而返回的對象正好可以是我們定製過的對象!
思路來了,首先我們hook ServiceManger中的sCache所關聯ClipboardService的IBinder對象,使之在調用queryLocalInterface的時候返回一個永遠不爲空的android.os.IInterface對象,我們修改那個返回對象的行爲就達到hook ClipboardService的目的!
代碼:
public class HookApplication extends Application {
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
//獲得ServiceManger的class
Class<?> serviceManager = Class.forName("android.os.ServiceManager", false, getClassLoader());
//找到getService靜態方法
Method getService = serviceManager.getDeclaredMethod("getService", String.class);
//獲得原始的binder對象
IBinder rawBinder = (IBinder) getService.invoke(null, CLIPBOARD_SERVICE);
//定製我們自己的IBinder對象
ProxyBinder proxyBinder = new ProxyBinder(rawBinder, this);
IBinder proxy = (IBinder) Proxy.newProxyInstance(getClassLoader(), new Class[]{IBinder.class},
proxyBinder);
//注入到ServiceManager的sCache中
Field field = serviceManager.getDeclaredField("sCache");
field.setAccessible(true);
Map<String, IBinder> map = (Map<String, IBinder>) field.get(null);
map.put(CLIPBOARD_SERVICE, proxy);
} catch (ClassNotFoundException | IllegalAccessException | NoSuchMethodException
| InvocationTargetException | NoSuchFieldException e) {
e.printStackTrace();
}
}
}
ProxyBinder的源碼:
/**
* Created by chan on 16/4/10.
*/
public class ProxyBinder implements InvocationHandler {
private Class<?> m_iClipboardInterfaceClz;
private Object m_iClipboardInterface;
private Object m_base;
private Context m_context;
public ProxyBinder(Object base, Context context)
throws ClassNotFoundException, NoSuchMethodException,
InvocationTargetException,
IllegalAccessException {
m_base = base;
m_context = context;
//獲得IClipboard的class
m_iClipboardInterfaceClz = Class.forName("android.content.IClipboard",
false, context.getClassLoader());
//獲得服務的Stub 這是aidl內容 需讀者熟悉aidl
Class<?> clipboardServiceStub = Class.forName("android.content.IClipboard$Stub",
false, context.getClassLoader());
//獲取stub的asInterface靜態方法
Method asInterface = clipboardServiceStub.getDeclaredMethod("asInterface", IBinder.class);
//獲得android.content.IClipboard實例 這是系統默認的實例
m_iClipboardInterface = asInterface.invoke(null, base);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//修改queryLocalInterface的行爲
//我們動態的修改爲我們自定義的IBinder對象
if ("queryLocalInterface".equals(method.getName())) {
return Proxy.newProxyInstance(m_context.getClassLoader(),
new Class[]{IBinder.class, m_iClipboardInterfaceClz, IInterface.class},
new BinderHook(m_iClipboardInterface));
}
//其他的操作都是調用跟原來一樣的操作
return method.invoke(m_base, args);
}
}
BinderHook.java:
/**
* Created by chan on 16/4/10.
*/
public class BinderHook implements InvocationHandler {
private Object m_base;
public BinderHook(Object iClipboardInterface) {
m_base = iClipboardInterface;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//當調用getPrimaryClip的時候 我們始終返回我們自己定義的一段字符串
if ("getPrimaryClip".equals(method.getName())) {
return ClipData.newPlainText(null, "I am crazy");
}
//使得客戶端一直默認粘貼板中有東西
if ("getPrimaryClip".equals(method.getName())) {
return true;
}
//其它的操作都還是一樣調用遠程的服務接口
return method.invoke(m_base, args);
}
}
demo代碼:
findViewById(R.id.id_start).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ClipData.Item item = m_clipboardManager.getPrimaryClip().getItemAt(0);
Toast.makeText(MainActivity.this, item.getText(), Toast.LENGTH_SHORT).show();
}
});
效果: