一. 什么是 Hook
Hook 英文翻譯過來就是「鉤子」的意思,那我們在什么時候使用這個「鉤子」呢福侈?在 Android 操作系統(tǒng)中系統(tǒng)維護(hù)著自己的一套事件分發(fā)機(jī)制。應(yīng)用程序酝陈,包括應(yīng)用觸發(fā)事件和后臺邏輯處理营罢,也是根據(jù)事件流程一步步地向下執(zhí)行。而「鉤子」的意思仲智,就是在事件傳送到終點(diǎn)前截獲并監(jiān)控事件的傳輸买乃,像個鉤子鉤上事件一樣,并且能夠在鉤上事件時钓辆,處理一些自己特定的事件剪验。
Hook 的這個本領(lǐng)肴焊,使它能夠?qū)⒆陨淼拇a「融入」被勾住(Hook)的程序的進(jìn)程中功戚,成為目標(biāo)進(jìn)程的一個部分娶眷。API Hook 技術(shù)是一種用于改變 API 執(zhí)行結(jié)果的技術(shù),能夠?qū)⑾到y(tǒng)的 API 函數(shù)執(zhí)行重定向啸臀。在 Android 系統(tǒng)中使用了沙箱機(jī)制茂浮,普通用戶程序的進(jìn)程空間都是獨(dú)立的,程序的運(yùn)行互不干擾壳咕。這就使我們希望通過一個程序改變其他程序的某些行為的想法不能直接實(shí)現(xiàn)席揽,但是 Hook 的出現(xiàn)給我們開拓了解決此類問題的道路。
什么是沙箱機(jī)制
沙箱是一個虛擬系統(tǒng)程序谓厘,沙箱提供的環(huán)境相對于每一個運(yùn)行的程序的進(jìn)程空間都是獨(dú)立的幌羞,程序的運(yùn)行互不干擾,而且不會對現(xiàn)有的系統(tǒng)產(chǎn)生影響竟稳。
二属桦、Hook 分類
1、根據(jù)Android開發(fā)模式
Java層級的Hook他爸;
Native層級的Hook聂宾;
2、根 Hook 對象與 Hook 后處理事件方式
消息Hook诊笤;
API Hook系谐;
3、針對Hook的不同進(jìn)程上來說
全局Hook讨跟;
單個進(jìn)程Hook
四. API Hook 原理
通過對 Android 平臺的虛擬機(jī)注入與 Java 反射的方式纪他,來改變 Android 虛擬機(jī)調(diào)用函數(shù)的方式(ClassLoader),從而達(dá)到 Java 函數(shù)重定向的目的晾匠,這里我們將此類操作稱為 Java API Hook茶袒。
基礎(chǔ)知識
由此可見,Hook的基礎(chǔ)知識凉馆,是反射及基于反射的動態(tài)代理
- 反射
反射(Reflection)是什么呢薪寓?
反射有時候也被稱為內(nèi)省(Introspection)澜共,事實(shí)上向叉,反射,就是一種內(nèi)省的方式咳胃,Java不允許在運(yùn)行時改變程序結(jié)構(gòu)或類型變量的結(jié)構(gòu)植康,但它允許在運(yùn)行時去探知、加載展懈、調(diào)用在編譯期完全未知的class销睁,可以在運(yùn)行時加載該class供璧,生成實(shí)例對象(instance object),調(diào)用method冻记,或?qū)ield賦值睡毒。這種類似于“看透”了class的特性被稱為反射(Reflection),我們可以將反射直接理解為:可以看到自己在水中的倒影冗栗,這種操作與直接操作源代碼效果相同演顾,但靈活性高得多。
在之前學(xué)習(xí)熱更新的時候有介紹過反射隅居,詳見 Android熱更新二:理解Java反射 钠至。
- java 的動態(tài)代理
首先了解一些代理模式的定義。
為其他對象提供一種代理以控制這個對象的訪問胎源。
從代碼的角度來分棉钧,代理可以分為兩種:一種是靜態(tài)代理,另一種是動態(tài)代理涕蚤。
之前講設(shè)計(jì)模式的時候宪卿,也講過動態(tài)代理,詳見 Android常見設(shè)計(jì)模式五:代理模式万栅。
五佑钾、Hook Activity 的 startActivity
原理知道后,還是實(shí)戰(zhàn)來得舒暢烦粒,下面以startActivity啟動一個activity為例休溶,在Activity中啟動另一個Activity,首先我們需要了解activity的啟動流程,一步步跟蹤撒遣,發(fā)現(xiàn)最終在Activity.startActivityForResult中以如下方式啟動:
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
if (ar != null) {
mMainThread.sendActivityResult(
mToken, mEmbeddedID, requestCode, ar.getResultCode(),
ar.getResultData());
}
如果你對Activity啟動熟悉的話會發(fā)現(xiàn)邮偎,此處的mInstrumentation就是ActivityThread通過ativity.attach傳過來的,而ActivityThread一個app唯一的义黎,而mInstrumentation就是在ActivityThread創(chuàng)建后馬上創(chuàng)建的,此時豁跑,是不是感覺這個mInstrumentation符合hook點(diǎn)廉涕,ok,先hook一把
public static void hookCurrentThread(){
try {
Class<?> activityThreadCls = Class.forName("android.app.ActivityThread");
//1.獲取ActivityThread對象
//hook點(diǎn)艇拍,有public的方法或?qū)傩院桑瑑?yōu)先
Method currentActThreadMethod = activityThreadCls.getDeclaredMethod("currentActivityThread");
Object curThreadObj = currentActThreadMethod.invoke(null);
//獲取mInstrumentation
Field instrumentationField = curThreadObj.getClass().getDeclaredField("mInstrumentation");
instrumentationField.setAccessible(true);
instrumentationField.set(curThreadObj,new InstrumentProxy((Instrumentation) instrumentationField.get(curThreadObj)));
} catch (Exception e) {
e.printStackTrace();
}
其中InstrumentProxy如下:
public class InstrumentProxy extends Instrumentation{
private Instrumentation realObj;
public InstrumentProxy(Instrumentation obj){
this.realObj = obj;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
// 開始調(diào)用原始的方法, 調(diào)不調(diào)用隨你,但是不調(diào)用的話, 所有的startActivity都失效了.
// 由于這個方法是隱藏的,因此需要使用反射調(diào)用;首先找到這個方法
try {
Method execStartActivity = Instrumentation.class.getDeclaredMethod(
"execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class);
execStartActivity.setAccessible(true);
return (ActivityResult) execStartActivity.invoke(realObj, who,
contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
throw new RuntimeException("do not support!!! pls adapt it");
}
}
}
此處,先獲取ActivityThread中的mInstrumentation屬性卸夕,然后再采用靜態(tài)代理將其替換掉层释,這樣就hook住系統(tǒng)的方法,我們也就可以在InstrumentProxy中任意插樁快集。
倘若你沒有發(fā)現(xiàn)mInstrumentation符合hook點(diǎn)贡羔,你可以繼續(xù)跟蹤Instrumentation.execStartActivity方法廉白,里面有個非常明顯的hook點(diǎn):
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess();
int result = ActivityManagerNative.getDefault().startActivity(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
}
對,就是ActivityManagerNative.getDefault()乖寒,我們可以進(jìn)入ActivityManagerNative類猴蹂,發(fā)現(xiàn)getDefault方法實(shí)現(xiàn)如下:
static public IActivityManager getDefault() {
return gDefault.get();
}
其中g(shù)Default是個static屬性,完全符合hook要求,具體hook如下:
public static void hookAMNative(){
try {
Class<?> actManagerNativeCls = Class.forName("android.app.ActivityManagerNative");
//獲取gDefault
Field gDefaultField = actManagerNativeCls.getDeclaredField("gDefault");
gDefaultField.setAccessible(true);
Object gDefaultObj = gDefaultField.get(null);
// Method getField = gDefaultObj.getClass().getDeclaredMethod("get");
// Object activityImpl = getField.invoke(null);
Class<?> singleton = Class.forName("android.util.Singleton");
Field mInstanceField = singleton.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
Object activityImpl = mInstanceField.get(gDefaultObj);
// Method activityManagerMethod= actManagerNativeCls.getMethod("getDefault");
// Object actManagerImpl = activityManagerMethod.invoke(null);
Object actProxy = Proxy.newProxyInstance(activityImpl.getClass().getClassLoader(),
activityImpl.getClass().getInterfaces(),new ProxyHandler(activityImpl,null));
mInstanceField.set(gDefaultObj,actProxy);
} catch (Exception e) {
e.printStackTrace();
}
}
其中ProxyHandler如下:
public class ProxyHandler implements InvocationHandler{
private Object realObj;
public ProxyHandler(Object obj,Object d){
this.realObj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if("startActivity".equals(methodName)){
android.util.Log.e("hooktest",">>>>proxyhandler>>>startActivityBefore");
Object retur = method.invoke(realObj,args);
android.util.Log.e("hooktest",">>>>proxyhandler>>>startActivityAfter");
return retur;
}
return method.invoke(realObj,args);
}
}
采用動態(tài)代理的方法對IActivityManager進(jìn)行了接管楣嘁,同樣完成了startActivity的hook磅轻。
其實(shí)不選單例、靜態(tài)屬性或共有屬性逐虚,整個private的也是可以的聋溜,還以啟動startActivity為例,考慮到Activity是繼承ContextWrapper叭爱,而ContextWrapper中有個屬性mBase撮躁,如果我們能對mBase hook也是可以的,這樣就需要對ContextImpl來個代理就可以了涤伐,代碼可以如下:
public static void hookContextWrapper(ContextWrapper wrapper) {
try {
Field mBaseFiled;
Class<?> wrapperClass = ContextWrapper.class;
mBaseFiled = wrapperClass.getDeclaredField("mBase");
mBaseFiled.setAccessible(true);
mBaseFiled.set(wrapper,new HookWrapper((Context) mBaseFiled.get(wrapper)));
} catch (Exception e) {
e.printStackTrace();
}
}
這樣盡管可以馒胆,但是啟動activity就需要注意了,只要能走到mBase.startActivity(intent)接口才生效凝果,如果沒走它祝迂,就hook失效啰,所以hook點(diǎn)選擇很關(guān)鍵器净,盡管都hook到了東西型雳,但是是不是hook住了全部,還需要驗(yàn)證山害。
六纠俭、Hook View 的 OnClickListener
下面通過 Hook View 的 OnClickListener 來說明 Hook 的使用方法荞下。
首先進(jìn)入 View 的 setOnClickListener 方法灶轰,我們看到 OnClickListener 對象被保存在了一個叫做 ListenerInfo 的內(nèi)部類里,其中 mListenerInfo 是 View 的成員變量歇终。ListeneInfo 里面保存了 View 的各種監(jiān)聽事件权纤,比如 OnClickListener钓简、OnLongClickListener、OnKeyListener 等等汹想。
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
我們的目標(biāo)是 Hook OnClickListener外邓,所以就要在給 View 設(shè)置監(jiān)聽事件后,替換 OnClickListener 對象古掏,注入自定義的操作损话。
private void hookOnClickListener(View view) {
try {
// 得到 View 的 ListenerInfo 對象
Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
getListenerInfo.setAccessible(true);
Object listenerInfo = getListenerInfo.invoke(view);
// 得到 原始的 OnClickListener 對象
Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");
mOnClickListener.setAccessible(true);
View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo);
// 用自定義的 OnClickListener 替換原始的 OnClickListener
View.OnClickListener hookedOnClickListener = new HookedOnClickListener(originOnClickListener);
mOnClickListener.set(listenerInfo, hookedOnClickListener);
} catch (Exception e) {
log.warn("hook clickListener failed!", e);
}
}
class HookedOnClickListener implements View.OnClickListener {
private View.OnClickListener origin;
HookedOnClickListener(View.OnClickListener origin) {
this.origin = origin;
}
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "hook click", Toast.LENGTH_SHORT).show();
log.info("Before click, do what you want to to.");
if (origin != null) {
origin.onClick(v);
}
log.info("After click, do what you want to to.");
}
}
到這里,我們成功 Hook 了 OnClickListener槽唾,在點(diǎn)擊之前和點(diǎn)擊之后可以執(zhí)行某些操作丧枪,達(dá)到了我們的目的光涂。下面是調(diào)用的部分,在給 Button 設(shè)置 OnClickListener 后豪诲,執(zhí)行 Hook 操作顶捷。點(diǎn)擊按鈕后,日志的打印結(jié)果是:Before click → onClick → After click屎篱。
Button btnSend = (Button) findViewById(R.id.btn_send);
btnSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
log.info("onClick");
}
});
hookOnClickListener(btnSend);
七服赎、使用 Hook 攔截應(yīng)用內(nèi)的通知
當(dāng)應(yīng)用內(nèi)接入了眾多的 SDK,SDK 內(nèi)部會使用系統(tǒng)服務(wù) NotificationManager 發(fā)送通知交播,這就導(dǎo)致通知難以管理和控制≈芈牵現(xiàn)在我們就用 Hook 技術(shù)攔截部分通知,限制應(yīng)用內(nèi)的通知發(fā)送操作秦士。
發(fā)送通知使用的是 NotificationManager 的 notify 方法缺厉,我們跟隨 API 進(jìn)去看看。它會使用 INotificationManager 類型的對象隧土,并調(diào)用其 enqueueNotificationWithTag 方法完成通知的發(fā)送提针。
public void notify(String tag, int id, Notification notification)
{
INotificationManager service = getService();
…… // 省略部分代碼
try {
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
stripped, idOut, UserHandle.myUserId());
if (id != idOut[0]) {
Log.w(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]);
}
} catch (RemoteException e) {
}
}
private static INotificationManager sService;
/** @hide */
static public INotificationManager getService()
{
if (sService != null) {
return sService;
}
IBinder b = ServiceManager.getService("notification");
sService = INotificationManager.Stub.asInterface(b);
return sService;
}
INotificationManager 是跨進(jìn)程通信的 Binder 類,sService 是 NMS(NotificationManagerService) 在客戶端的代理曹傀,發(fā)送通知要委托給 sService辐脖,由它傳遞給 NMS,具體的原理在這里不再細(xì)究皆愉,感興趣的可以了解系統(tǒng)服務(wù)和應(yīng)用的通信過程嗜价。
我們發(fā)現(xiàn) sService 是個靜態(tài)成員變量,而且只會初始化一次幕庐。只要把 sService 替換成自定義的不就行了么久锥,確實(shí)如此。下面用到大量的 Java 反射和動態(tài)代理异剥,特別要注意代碼的書寫瑟由。
private void hookNotificationManager(Context context) {
try {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
// 得到系統(tǒng)的 sService
Method getService = NotificationManager.class.getDeclaredMethod("getService");
getService.setAccessible(true);
final Object sService = getService.invoke(notificationManager);
Class iNotiMngClz = Class.forName("android.app.INotificationManager");
// 動態(tài)代理 INotificationManager
Object proxyNotiMng = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{iNotiMngClz}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.debug("invoke(). method:{}", method);
if (args != null && args.length > 0) {
for (Object arg : args) {
log.debug("type:{}, arg:{}", arg != null ? arg.getClass() : null, arg);
}
}
// 操作交由 sService 處理,不攔截通知
// return method.invoke(sService, args);
// 攔截通知冤寿,什么也不做
return null;
// 或者是根據(jù)通知的 Tag 和 ID 進(jìn)行篩選
}
});
// 替換 sService
Field sServiceField = NotificationManager.class.getDeclaredField("sService");
sServiceField.setAccessible(true);
sServiceField.set(notificationManager, proxyNotiMng);
} catch (Exception e) {
log.warn("Hook NotificationManager failed!", e);
}
}
Hook 的時機(jī)還是盡量要早错妖,我們在 attachBaseContext 里面操作。
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
hookNotificationManager(newBase);
}
這樣我們就完成了對通知的攔截疚沐,可見 Hook 技術(shù)真的是非常強(qiáng)大,好多插件化的原理都是建立在 Hook 之上的潮模。
八亮蛔、結(jié)語
以上,我們知道擎厢,hook技術(shù)涉及到的知識點(diǎn)主要有反射究流、代理及android的一些底層知識辣吃,如果想要較好地掌握好hook相關(guān)的內(nèi)容,就需要花更多的時間去學(xué)習(xí)和總結(jié)芬探。
下面總結(jié)一下注意點(diǎn):
- Hook 的選擇點(diǎn):靜態(tài)變量和單例神得,因?yàn)橐坏﹦?chuàng)建對象,它們不容易變化偷仿,非常容易定位哩簿。
- Hook 過程:
尋找 Hook 點(diǎn),原則是靜態(tài)變量或者單例對象酝静,盡量 Hook public 的對象和方法节榜。
選擇合適的代理方式,如果是接口可以用動態(tài)代理别智。
偷梁換柱——用代理對象替換原始對象宗苍。
- Android 的 API 版本比較多,方法和類可能不一樣薄榛,所以要做好 API 的兼容工作讳窟。
另外,市面上游幾個比較成熟的Hook 方案敞恋,如果有需要大量使用此技術(shù)的不妨參考參考:
通過替換 /system/bin/app_process 程序控制 Zygote 進(jìn)程丽啡,使得 app_process 在啟動過程中會加載 XposedBridge.jar 這個 Jar 包,從而完成對 Zygote 進(jìn)程及其創(chuàng)建的 Dalvik 虛擬機(jī)的劫持耳舅。
Xposed 在開機(jī)的時候完成對所有的 Hook Function 的劫持碌上,在原 Function 執(zhí)行的前后加上自定義代碼。
-
Cydia Substrate 框架為蘋果用戶提供了越獄相關(guān)的服務(wù)框架浦徊,當(dāng)然也推出了 Android 版 馏予。Cydia Substrate 是一個代碼修改平臺,它可以修改任何進(jìn)程的代碼盔性。不管是用 Java 還是 C/C++(native代碼)編寫的霞丧,而 Xposed 只支持 Hook app_process 中的 Java 函數(shù)。
-
Legend 是 Android 免 Root 環(huán)境下的一個 Apk Hook 框架冕香,該框架代碼設(shè)計(jì)簡潔蛹尝,通用性高,適合逆向工程時一些 Hook 場景突那。大部分的功能都放到了 Java 層,這樣的兼容性就非常好构眯。
原理是這樣的愕难,直接構(gòu)造出新舊方法對應(yīng)的虛擬機(jī)數(shù)據(jù)結(jié)構(gòu),然后替換信息寫到內(nèi)存中即可。