1. 什么是 Hook
Hook 英文翻譯過來就是「鉤子」的意思,那我們?cè)谑裁磿r(shí)候使用這個(gè)「鉤子」呢皱坛?在 Android 操作系統(tǒng)中系統(tǒng)維護(hù)著自己的一套事件分發(fā)機(jī)制编曼。應(yīng)用程序,包括應(yīng)用觸發(fā)事件和后臺(tái)邏輯處理剩辟,也是根據(jù)事件流程一步步地向下執(zhí)行掐场。而「鉤子」的意思往扔,就是在事件傳送到終點(diǎn)前截獲并監(jiān)控事件的傳輸,像個(gè)鉤子鉤上事件一樣熊户,并且能夠在鉤上事件時(shí)萍膛,處理一些自己特定的事件。
Hook 的這個(gè)本領(lǐng)嚷堡,使它能夠?qū)⒆陨淼拇a「融入」被勾谆嚷蕖(Hook)的程序的進(jìn)程中,成為目標(biāo)進(jìn)程的一個(gè)部分蝌戒。API Hook 技術(shù)是一種用于改變 API 執(zhí)行結(jié)果的技術(shù)串塑,能夠?qū)⑾到y(tǒng)的 API 函數(shù)執(zhí)行重定向。在 Android 系統(tǒng)中使用了沙箱機(jī)制瓶颠,普通用戶程序的進(jìn)程空間都是獨(dú)立的拟赊,程序的運(yùn)行互不干擾刺桃。這就使我們希望通過一個(gè)程序改變其他程序的某些行為的想法不能直接實(shí)現(xiàn)粹淋,但是 Hook 的出現(xiàn)給我們開拓了解決此類問題的道路。當(dāng)然瑟慈,根據(jù) Hook 對(duì)象與 Hook 后處理的事件方式不同桃移,Hook 還分為不同的種類,比如消息 Hook葛碧、API Hook 等借杰。
- 使用 Java 反射實(shí)現(xiàn) API Hook
通過對(duì) Android 平臺(tái)的虛擬機(jī)注入與 Java 反射的方式,來改變 Android 虛擬機(jī)調(diào)用函數(shù)的方式(ClassLoader)进泼,從而達(dá)到 Java 函數(shù)重定向的目的蔗衡,這里我們將此類操作稱為 Java API Hook。
下面通過 Hook View 的 OnClickListener 來說明 Hook 的使用方法乳绕。
首先進(jìn)入 View 的 setOnClickListener 方法绞惦,我們看到 OnClickListener 對(duì)象被保存在了一個(gè)叫做 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 對(duì)象雁乡,注入自定義的操作。
private void hookOnClickListener(View view) {
try {
// 得到 View 的 ListenerInfo 對(duì)象
Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
getListenerInfo.setAccessible(true);
Object listenerInfo = getListenerInfo.invoke(view);
// 得到 原始的 OnClickListener 對(duì)象
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);
我們?cè)賮砜匆粋€(gè)很常見的例子 startActivity
下面我們Hook掉startActivity
這個(gè)方法,使得每次調(diào)用這個(gè)方法之前輸出一條日志侵浸;(當(dāng)然旺韭,這個(gè)輸入日志有點(diǎn)點(diǎn)弱,只是為了展示原理掏觉,如果你想可以替換參數(shù)区端,攔截這個(gè)startActivity
過程,使得調(diào)用它導(dǎo)致啟動(dòng)某個(gè)別的Activity澳腹,指鹿為馬V巍)
我們知道對(duì)于Context.startActivity,Context的實(shí)現(xiàn)實(shí)際上是ContextImpl;我們看ConetxtImpl類的startActivity方法:
@Override
public void startActivity(Intent intent, Bundle options) {
warnIfCallingFromSystemProcess();
if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
throw new AndroidRuntimeException(
"Calling startActivity() from outside of an Activity "
+ " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ " Is this really what you want?");
}
mMainThread.getInstrumentation().execStartActivity(
getOuterContext(), mMainThread.getApplicationThread(), null,
(Activity)null, intent, -1, options);
}
這里酱塔,實(shí)際上使用了ActivityThread
類的mInstrumentation
成員的execStartActivity
方法沥邻;注意到,ActivityThread
實(shí)際上是主線程羊娃,而主線程一個(gè)進(jìn)程只有一個(gè)唐全,因此這里是一個(gè)良好的Hook點(diǎn)。
接下來就是想要Hook掉我們的主線程對(duì)象蕊玷,也就是把這個(gè)主線程對(duì)象里面的mInstrumentation
給替換成我們修改過的代理對(duì)象邮利;要替換主線程對(duì)象里面的字段,首先我們得拿到主線程對(duì)象的引用垃帅,如何獲取呢延届?ActivityThread
類里面有一個(gè)靜態(tài)方法currentActivityThread
可以幫助我們拿到這個(gè)對(duì)象類;但是ActivityThread
是一個(gè)隱藏類挺智,我們需要用反射去獲取祷愉,代碼如下:
// 先獲取到當(dāng)前的ActivityThread對(duì)象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
拿到這個(gè)currentActivityThread
之后,我們需要修改它的mInstrumentation
這個(gè)字段為我們的代理對(duì)象赦颇,我們先實(shí)現(xiàn)這個(gè)代理對(duì)象二鳄,由于JDK動(dòng)態(tài)代理只支持接口,而這個(gè)Instrumentation
是一個(gè)類媒怯,沒辦法订讼,我們只有手動(dòng)寫靜態(tài)代理類,覆蓋掉原始的方法即可扇苞。(cglib
可以做到基于類的動(dòng)態(tài)代理欺殿,這里先不介紹)
public class EvilInstrumentation extends Instrumentation {
private static final String TAG = "EvilInstrumentation";
// ActivityThread中原始的對(duì)象, 保存起來
Instrumentation mBase;
public EvilInstrumentation(Instrumentation base) {
mBase = base;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
// Hook之前, XXX到此一游!
Log.d(TAG, "\n執(zhí)行了startActivity, 參數(shù)如下: \n" + "who = [" + who + "], " +
"\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
"\ntarget = [" + target + "], \nintent = [" + intent +
"], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");
// 開始調(diào)用原始的方法, 調(diào)不調(diào)用隨你,但是不調(diào)用的話, 所有的startActivity都失效了.
// 由于這個(gè)方法是隱藏的,因此需要使用反射調(diào)用;首先找到這個(gè)方法
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(mBase, who,
contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
// 某該死的rom修改了 需要手動(dòng)適配
throw new RuntimeException("do not support!!! pls adapt it");
}
}
}
Ok寄纵,有了代理對(duì)象,我們要做的就是偷梁換柱脖苏!代碼比較簡(jiǎn)單程拭,采用反射直接修改:
public static void attactContext() throws Exception{
// 先獲取到當(dāng)前的ActivityThread對(duì)象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Field currentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
currentActivityThreadField.setAccessible(true);
Object currentActivityThread = currentActivityThreadField.get(null);
// 拿到原始的 mInstrumentation字段
Field mInstrumentationField = activityThreadClass.getField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
// 創(chuàng)建代理對(duì)象
Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);
// 偷梁換柱
mInstrumentationField.set(currentActivityThread, evilInstrumentation);
}
好了,我們啟動(dòng)一個(gè)Activity測(cè)試一下棍潘,結(jié)果如下:
總結(jié)一下:
Hook 過程:
尋找 Hook 點(diǎn)恃鞋,原則是靜態(tài)變量或者單例對(duì)象,盡量 Hook public 的對(duì)象和方法亦歉。
選擇合適的代理方式恤浪,如果是接口可以用動(dòng)態(tài)代理。
偷梁換柱——用代理對(duì)象替換原始對(duì)象肴楷。
Android 的 API 版本比較多水由,方法和類可能不一樣,所以要做好 API 的兼容工作赛蔫。
舉個(gè)例子
Android10后添加了ActivityTaskManager
int result = ActivityTaskManager.getService().startActivity(whoThread,
who.getBasePackageName(), who.getAttributionTag(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()), token,
target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, null, options);
http://www.reibang.com/p/8632fdc86009
2. Xposed
通過替換 /system/bin/app_process 程序控制 Zygote 進(jìn)程砂客,使得 app_process 在啟動(dòng)過程中會(huì)加載 XposedBridge.jar 這個(gè) Jar 包,從而完成對(duì) Zygote 進(jìn)程及其創(chuàng)建的 Dalvik 虛擬機(jī)的劫持濒募。
Xposed 在開機(jī)的時(shí)候完成對(duì)所有的 Hook Function 的劫持鞭盟,在原 Function 執(zhí)行的前后加上自定義代碼。
現(xiàn)在安裝Xposed比較方便瑰剃,因?yàn)閄posed作者開發(fā)了一個(gè)Xposed Installer App,下載后按照提示傻瓜式安裝(前提是root手機(jī))筝野。其實(shí)它的安裝過程是這個(gè)樣子的:首先探測(cè)手機(jī)型號(hào)晌姚,然后按照手機(jī)版本下載不同的刷機(jī)包,最后把Xposed刷機(jī)包刷入手機(jī)重啟就好歇竟。刷機(jī)包下載 里面有所有版本的刷機(jī)包挥唠。
刷機(jī)包解壓打開里面的問件構(gòu)成是這個(gè)樣子的:
META-INF/ 里面有文件配置腳本 flash-script.sh 配置各個(gè)文件安裝位置。
system/bin/ 替換zygote進(jìn)程等文件
system/framework/XposedBridge.jar jar包位置
system/lib system/lib64 一些so文件所在位置
xposed.prop xposed版本說明文件
所以安裝Xposed的過程就上把上面這些文件放到手機(jī)里相同文件路徑下焕议。
通過查看文件安裝腳本發(fā)現(xiàn):
system/bin/下面的文件替換了app_process等文件宝磨,app_process就是zygote進(jìn)程文件。所以Xposed通過替換zygote進(jìn)程實(shí)現(xiàn)了控制手機(jī)上所有app進(jìn)程盅安。因?yàn)樗衋pp進(jìn)程都是由Zygote fork出來的唤锉。
Xposed的基本原理是修改了ART/Davilk虛擬機(jī),將需要hook的函數(shù)注冊(cè)為Native層函數(shù)别瞭。當(dāng)執(zhí)行到這一函數(shù)是虛擬機(jī)會(huì)優(yōu)先執(zhí)行Native層函數(shù)窿祥,然后再去執(zhí)行Java層函數(shù),這樣完成函數(shù)的hook蝙寨。如下圖:
通過讀Xposed源碼發(fā)現(xiàn)其啟動(dòng)過程:
- 手機(jī)啟動(dòng)時(shí)init進(jìn)程會(huì)啟動(dòng)zygote這個(gè)進(jìn)程晒衩。由于zygote進(jìn)程文件app_process已被替換嗤瞎,所以啟動(dòng)的時(shí)Xposed版的zygote進(jìn)程。
- Xposed_zygote進(jìn)程啟動(dòng)后會(huì)初始化一些so文件(system/lib system/lib64)听系,然后進(jìn)入XposedBridge.jar中的XposedBridge.main中初始化jar包完成對(duì)一些關(guān)鍵Android系統(tǒng)函數(shù)的hook贝奇。
- Hook則是利用修改過的虛擬機(jī)將函數(shù)注冊(cè)為native函數(shù)。
- 然后再返回zygote中完成原本zygote需要做的工作靠胜。
這只是在宏觀層面稍微介紹了下Xposed弃秆,要想詳細(xì)了解需要讀它的源碼了。下面兩篇寫的挺好髓帽,要想深入理解的可以看看菠赚。
Android基于Linux,第一個(gè)啟動(dòng)的進(jìn)程自然是init進(jìn)程郑藏,該進(jìn)程會(huì)
啟動(dòng)所有Android進(jìn)程的父進(jìn)程——Zygote(孵化)進(jìn)程衡查,該進(jìn)程的啟動(dòng)配置在
/init.rc腳本中,而Zygote進(jìn)程對(duì)應(yīng)的執(zhí)行文件是/system/bin/app_process必盖,
該文件完成類庫(kù)的加載以及一些函數(shù)的調(diào)用工作拌牲。在Zygote進(jìn)程創(chuàng)建后,
再fork出SystemServer進(jìn)程和其他進(jìn)程歌粥。
而Xposed Framework呢塌忽,就是用自己實(shí)現(xiàn)的app_process替換掉了系統(tǒng)原本
提供的app_process,加載一個(gè)額外的jar包失驶,然后入口從原來的:
com.android.internal.osZygoteInit.main()被替換成了:
de.robv.android.xposed.XposedBridge.main()土居,
然后創(chuàng)建的Zygote進(jìn)程就變成Hook的Zygote進(jìn)程了,而后面Fork出來的進(jìn)程
也是被Hook過的嬉探。這個(gè)Jar包在:
/data/data/de.rbov.android.xposed.installer/bin/XposedBridge.jar
原文鏈接:https://blog.csdn.net/coder_pig/article/details/80031285
Android Hook框架Xposed原理與源代碼分析
https://blog.csdn.net/wxyyxc1992/article/details/17320911