??Hello大家好,好久沒有出文章了,不知道大家想我了沒有,本來這一篇是要寫《Flutter原理篇:硬核-從Platform到Dart通信原理分析之iOS篇》的吁恍,但是中間在工作中遇到一些積累的東西,特別想把他記錄下來播演,而且關于這個主題我其實很早以前就想寫了冀瓦,一方面當時積累不夠多不足以全面的來書寫這個話題,二來沒有機緣巧合的時間所以每每想起時只有作罷写烤,所以直到今天才有了今天這篇文章翼闽,喜歡Flutter的小伙伴要有點耐心再稍等一下??,好了回到這篇文章的這個話題來顶霞,我們今天來聊一聊App開發(fā)涉及或者接觸到的用到的Hook技術肄程,我會以我視角來帶大家去看看這些常用的Hook技術。
??首先聲明选浑,本人并非是做底層架構出身蓝厌,也并非是從事App安全相關的工作,之所以對于Hook有這么多的了解只不過是興趣使然古徒,再加上本人有扎實的計算機底層的基礎拓提,對于一些Hook技術可以去弄明白他的緣由而已,另外這篇文章也為了我也是為了我對于Hook這個主題做一個自我的總結隧膘,以后可能都不會多花時間去涉及這一塊的內容了代态,好的讓我們愉快的開始吧。
??本著該文是雜談屬性的又一篇“杰作”疹吃,熟悉我博客的小伙伴就知道這一篇肯定也是輕松愉快的文章蹦疑,代碼量肯定不大,大家只需放松心態(tài)即可萨驶,我們還是從《Android App Hook》開始來談談歉摧,《iOS App Hook》我們放到下一篇來講解,那么大家知道Android主要是基于Java語言的開發(fā)的腔呜,熟悉Java語言的小伙伴都知道叁温,Java本身就是一門帶動態(tài)特性的靜態(tài)語言,所以我們單從Java應用層就有很多的Hook技術可以用來玩的核畴。
??首先使用Java語言很容易的寫出一種簡單的一般稱作靜態(tài)代理模式的Hook膝但,如下:
/**
* 售票服務接口
*/
public interface TicketService {
//售票
public void sellTicket();
}
/**
* 售票服務接口實現(xiàn)類,車站
*/
public class Station implements TicketService {
@Override
public void sellTicket() {
System.out.println("\n\t售票.....\n");
}
}
/**
* 車票代售點
*
*/
public class StationProxy implements TicketService {
private Station station;
public StationProxy(Station station){
this.station = station;
}
@Override
public void sellTicket() {
// 1.做真正業(yè)務前谤草,提示信息
this.showAlertInfo("××××您正在使用車票代售點進行購票跟束,每張票將會收取5元手續(xù)費莺奸!××××");
// 2.調用真實業(yè)務邏輯
station.sellTicket();
// 3.后處理
this.showAlertInfo("××××歡迎您的光臨,再見泳炉!××××\n");
}
}
很簡單吧憾筏,這種在原本業(yè)務邏輯前面加上一些判斷處理的方式,我們稱之為AOP的編程也就是面向切面的編程花鹅。
??說完了靜態(tài)代理,那么就不得不提到Java還有一種動態(tài)代理的模式枫浙,他不需要手動創(chuàng)建代理對象刨肃,但是可以完全實現(xiàn)靜態(tài)代理功能,最常見的就是JDK自帶的InvocationHandler類箩帚,使用方法很簡單
static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
返回一個指定接口的代理類實例真友,該接口可以將方法調用指派到指定的調用處理程序。
Object invoke(Object proxy,Method method,Object[] args)
在代理實例上處理方法調用并返回結果紧帕。
??這樣一來你就不需要自己實現(xiàn)很多的代理對象盔然,通過這個可以自動的創(chuàng)建一個代理對象來實現(xiàn)AOP的Hook,動態(tài)代理的實現(xiàn)主要依賴于java.lang.reflect.InvocationHandler接口與java.lang.reflect.Proxy類是嗜,其實現(xiàn)原理是基于java的反射技術來實現(xiàn)的愈案。
??我們知道Java語言是編譯成字節(jié)碼解釋執(zhí)行的,那么有沒有一種可能使得我們在編譯的時候自動生成一些代碼來進行Hook呢鹅搪,答案是有的站绪,比如我們做Android開發(fā)很常見的APT技術,我們常見使用這個的框架為ButterKnife丽柿,ARouter等等恢准,簡單的來講這些框架通過APT技術實現(xiàn)的。
??我們先來看看APT是什么甫题,APT(Annotation Processing Tool)是一種處理注釋的工具,它對源代碼文件進行檢測找出其中的Annotation馁筐,根據(jù)注解自動生成代碼。 Annotation處理器在處理Annotation時可以根據(jù)源文件中的Annotation生成額外的源文件和其它的文件(文件具體內容由Annotation處理器的編寫者決定)坠非,APT還會編譯生成的源文件和原來的源文件敏沉,將它們一起生成class文件
簡單的來講通過APT技術,即注解處理器在編譯時掃描并處理注解麻顶,注解處理器可以在編譯時生成額外的.java文件赦抖,在程序運行的時候調用相關方法,可以達到減少重復代碼的效果辅肾。它的好處:提高開發(fā)效率队萤,使得項目更容易維護和擴展,同時幾乎不影響性能矫钓。
例如ARouter動態(tài)生成的代碼如下:
public class ARouter$$Root$$app implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("test", ARouter$$Group$$test.class);
}
}
大家具體想了解ARouter與APT的原理要尔,可以去看看我這篇博客 Android App模塊化篇
?? 既然Java是編譯成字節(jié)碼執(zhí)行的舍杜,那么有沒有一種可能使得他在執(zhí)行的時候去修改字節(jié)碼達到Hook的目的呢,答案是有的最常見的就是javassist赵辕,他可以直接編輯和生成Java的字節(jié)碼既绩,達到另一種動態(tài)的特性,例如:
package com.example.javassist;
public class Hello {
public static void say() {
System.out.println("hello world!");
}
}
package com.example.javassist;
import javassist.*;
public class Javassist {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.example.javassist.Hello");
CtMethod personFly = cc.getDeclaredMethod("say");
personFly.insertBefore("System.out.println(\"執(zhí)行方法之前\");");
personFly.insertAfter("System.out.println(\"執(zhí)行方法之后\");");
cc.toClass();
Hello.say();
}
}
執(zhí)行方法之前
hello world!
執(zhí)行方法之后
??上面就是通過javassist動態(tài)操作字節(jié)碼去生成的方法还惠,以此來達到了動態(tài)AOP的效果饲握,具有類似功能的還有ASM框架,作用類似蚕键,這里就不細講了救欧。
??上面更多的是從Java的運行原理去分析的,我們再從一些常見的框架去看看锣光,還有哪些Hook的特性呢笆怠,首先介紹的就是DroidPlugin這個框架了,這個是一個比較老的Android插件化框架了誊爹,通過Hook了Android的四大組件蹬刷,Binder機制等等來實現(xiàn)插件化的功能的,那么他的基本原理是怎么實現(xiàn)的呢
這個框架的Hook有兩個要素:
- 第一要找到一個最合適Hook的點频丘,既不影響額外的功能办成,又能很要的解決問題,
- 第二去動態(tài)的修改他里面的變量或者屬性等等椎镣,達到Hook的目的诈火,至于修改的方法其實就是我們上面介紹過的通過Java的動態(tài)代理以及反射去去實現(xiàn),
給大家舉一個例子吧:
??我們想Hook住系統(tǒng)的剪貼板內容替換為 "you are hooked”状答,一共要分幾步呢:第一首先Hook住sCache這個變量使得我們查找CLIPBOARD_SERVICE這個剪貼板服務的時候返回我們自己生成的一個BinderProxyHookHandler動態(tài)代理
// Hook 掉這個Binder代理對象的 queryLocalInterface 方法
// 然后在 queryLocalInterface 返回一個IInterface對象, hook掉我們感興趣的方法即可.
IBinder hookedBinder = (IBinder) Proxy.newProxyInstance(serviceManager.getClassLoader(),
new Class<?>[] { IBinder.class },
new BinderProxyHookHandler(rawBinder));
// 把這個hook過的Binder代理對象放進ServiceManager的cache里面
// 以后查詢的時候 會優(yōu)先查詢緩存里面的Binder, 這樣就會使用被我們修改過的Binder了
Field cacheField = serviceManager.getDeclaredField("sCache");
cacheField.setAccessible(true);
Map<String, IBinder> cache = (Map) cacheField.get(null);
cache.put(CLIPBOARD_SERVICE, hookedBinder);
然后在這個IBinder的對象也就是BinderProxyHookHandler的queryLocalInterface方法里面返回是我們生成的一個實現(xiàn)了IInterface冷守,IClipboard接口的BinderHookHandler對象,這里使用的是動態(tài)代理機制
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("queryLocalInterface".equals(method.getName())) {
Log.d(TAG, "hook queryLocalInterface");
// 這里直接返回真正被Hook掉的Service接口
// 這里的 queryLocalInterface 就不是原本的意思了
// 我們肯定不會真的返回一個本地接口, 因為我們接管了 asInterface方法的作用
// 因此必須是一個完整的 asInterface 過的 IInterface對象, 既要處理本地對象,也要處理代理對象
// 這只是一個Hook點而已, 它原始的含義已經(jīng)被我們重定義了; 因為我們會永遠確保這個方法不返回null
// 讓 IClipboard.Stub.asInterface 永遠走到if語句的else分支里面
return Proxy.newProxyInstance(proxy.getClass().getClassLoader(),
// asInterface 的時候會檢測是否是特定類型的接口然后進行強制轉換
// 因此這里的動態(tài)代理生成的類型信息的類型必須是正確的
new Class[] { IBinder.class, IInterface.class, this.iinterface },
new BinderHookHandler(base, stub));
}
Log.d(TAG, "method:" + method.getName());
return method.invoke(base, args);
}
最后在返回實現(xiàn)了IClipboard的對象BinderHookHandler去查找具體方法的時候Hook住getPrimaryClip方法使其返回固定的內容即可惊科,這里也是使用的動態(tài)代理機制
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 把剪切版的內容替換為 "you are hooked"
if ("getPrimaryClip".equals(method.getName())) {
Log.d(TAG, "hook getPrimaryClip");
return ClipData.newPlainText(null, "you are hooked");
}
// 欺騙系統(tǒng),使之認為剪切版上一直有內容
if ("hasPrimaryClip".equals(method.getName())) {
return true;
}
return method.invoke(base, args);
}
關于DroidPlugin的內容還有很多拍摇,網(wǎng)絡上有一個高手叫 weishu 專門寫了幾篇分析這個框架的文章可以給大家分享一下 http://weishu.me/,大家感興趣的話可以去看看他的博客
??接下來我們再來談談QQ空間的熱修復方案 MultiDex馆截,他的Hook方式相對來說比較好理解充活,主要是利用了Android的動態(tài)的類加載機制(可以運行時加載)把需要Hook的內容提前放到前面使用Class Loader加載使得運行的方法被替換了效果,QQ空間熱修復方案正是基于ClassLoader的這個原理蜡娶,把修復后的類打包到一個dex(path.dex)中去混卵,然后把這個dex插入到Elements的最前面去
private static void injectAboveEqualApiLevel14(Context context, String dexPath, String dexClassName)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//得到當前的PathClassLoader
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
//將老的dexElements和pathDexElements進行組合生出新的dexElements
Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
getDexElements(getPathList(
new DexClassLoader(dexPath, context.getDir("dex", 0).getAbsolutePath(), dexPath, context.getClassLoader()))));
//拿到DexPathList對象
Object a2 = getPathList(pathClassLoader);
//將DexPathList實例中的dexElements成員替換為合并后的dexElements
setField(a2, a2.getClass(), "dexElements", a);
//加載指定的類
pathClassLoader.loadClass(dexClassName);
}
??好了,上面介紹的都是基于應用層的Hook了已經(jīng)是五花八門的窖张,但是僅僅只是一半而已幕随,另一半基于底層的Hook也很精彩,比如接下來要介紹的Andfix框架宿接,AndFix不同于QQ空間超級補丁技術和微信Tinker通過增加或替換整個DEX的方案赘淮,提供了一種運行時在Native修改Filed指針的方式辕录,實現(xiàn)方法的替換,達到即時生效無需重啟梢卸,對應用無性能消耗的目的走诞;
??說白了就是利用Android虛擬機執(zhí)行的機制,是把要被替換的方法變?yōu)镹ative方法蛤高,因為由于虛擬機的執(zhí)行順序如果是JAVA方法會被解釋執(zhí)行(解釋執(zhí)行里面的字節(jié)碼指令)蚣旱,如果是Native方法那么就會直接調用(Method 結構體里面的nativeFunc就是函數(shù)的地址),這里再覆蓋他的nativeFunc為自定義的dalvik_dispatcher方法戴陡,這里面最核心的就是dvmCallMethod_fnPtr這個指針函數(shù)的調用姻锁,
dvmCallMethod_fnPtr(self, (Method*) jInvokeMethod,
dvmCreateReflectMethodObject_fnPtr(meth), &result, thisObj,argArray);
這里通過這個JVM函數(shù)動態(tài)調用了JAVA的invoke的反射方法,方法體就是替換的函數(shù)內容dvmCreateReflectMethodObject_fnPtr(meth), 總結就是讓被替換方法走Native的方式然后在JNI層去Hook住nativeFun猜欺,然后操作JVM的調用函數(shù)去調用反射的方式去執(zhí)行一個新的JAVA方法,由于是“雜談”的緣故不會講解得很細拷窜,大家想看具體的Andfix的話可以去看看我的這篇博客 AndFix各個版本的改動以及原理
??既然到了底層我們再來看看Android 底層還有哪些Hook的好東西呢开皿,下一個要介紹的就是基于Got的hook,比較可惜的是在Android里面沒有找到一個成熟的框架篮昧,大家都是手寫去實現(xiàn)的赋荆,他的是怎樣進行Hook的呢
??首先通過解析elf格式,分析Section header table找出靜態(tài)的.got表的位置懊昨,并在內存中找到相應的.got表位置窄潭,這個時候內存中.got表保存著導入函數(shù)的地址,讀取目標函數(shù)地址酵颁,與.got表每一項函數(shù)入口地址進行匹配嫉你,找到的話就直接替換新的函數(shù)地址
GotHook涉及內容比較多,給大家推薦一篇文章 http://www.reibang.com/p/43ef7ddd0081
??GotHook在Android中應用較少躏惋,而且并不是全能的幽污,網(wǎng)絡上很多的博客以及up主的視頻都提到了主要他是用來Hook外部方法的,但是其實這種說法是不正確的簿姨,這里沒有針對那些博主的以及up主(因為網(wǎng)絡上很多博客博主自己都沒弄明白Got Hook的原理)距误,因為大家知道C語言里面方法默認就是外部方法,難道都是可以被Got Hook的嗎扁位,顯然是不可能的准潭,熟悉Got Hook的都知道這種應該是在動態(tài)庫里面Hook才會有效果的,所以應該是在動態(tài)庫里面的強符號方法才可以被Hook
??現(xiàn)在Got Hook已經(jīng)很底層了域仇,那還有沒有更底層的Hook呢刑然,還真有那就是稱之為終極Hook的inline Hook,目前據(jù)了解支持inline Hook的框架有Cydia Substrate殉簸,Dobby這兩個框架闰集,老實講這種Hook的作用范圍非常廣沽讹,不限于Android底層,只要是C/C++語言相關的他都可以Hook武鲁,而且?guī)缀鯖]有限制條件爽雄,那么他是怎么Hook的呢,先來看一張圖
??它屬于是運行時指令替換沐鼠,假設目標方法內有ins0, ins1, ins2三條指令挚瘟,首先將起始指令(實際上是前2條指令)替換為等長的跳轉指令jump_ins,jump_ins負責跳轉到hook方法執(zhí)行饲梭,而hook操作后乘盖,往往還需要保留調用原方法的能力以保證功能可用性,所以hook方法內還有一個跳轉指令來調回原方法繼續(xù)執(zhí)行(jump ins1)憔涉,調回前需要先補充執(zhí)行目標方法已被替換的原始指令(圖中ins0)订框,保證原方法完整性。綜上兜叨,inline hook需要完成的工作就是圖中綠色的部分穿扳,即跳轉指令的替換、補充執(zhí)行原指令国旷、跳回原方法繼續(xù)執(zhí)行這三步矛物。
這個原理也是比較復雜,具體給大家分享一個鏈接 http://www.reibang.com/p/2684e251124d
??好了已經(jīng)介紹了很多種跪但,下面再來給大家最后介紹一種Hook最為結束的開胃菜吧履羞,那就是通過ptrace的Hook,這種方式比較少見一來是實現(xiàn)比較復雜屡久,二來穩(wěn)定性也不是最高的忆首,而且需要root的權限去實現(xiàn),所以用得很少涂身,但是我們還是來看看他是怎么實現(xiàn)的吧
他的原理就是利用ptrace附加進程進行調試雄卷,利用ARM匯編底層修改替換寄存器來實現(xiàn)Hook,可以簡單的分為以下步驟:
- 1.先找到被注入進程的pid
- 2.附加當前進程到被注入進程
- 3.保存原寄存器的值
- 4.找到需要Hook的系統(tǒng)調用函數(shù)
- 5.修改目標進程寄存器
- 6.執(zhí)行目標函數(shù)調用
- 7.恢復寄存器的值
- 8.分離附加進程
原理也是比較復雜的蛤售,細節(jié)大家可以看看我這兩篇博客 :
已經(jīng)講得很到位了悴能,絕對能幫助到大家了
??介紹得差不多了揣钦,這篇博客也是把我這幾年接觸到的Hook基本上都寫出來了,因為這些對于我來說在工作中都不是直接主要的技能漠酿,所以以后可能都不會再去寫關于Hook的內容了冯凹,這個也是為什么副標題叫“絕口不再提”的原因,一開始是憑著興趣入門摸索,一腔熱血宇姚,沒有任何人指導也堅持到了現(xiàn)在匈庭,今天也算是為了自己這么多年的做的總結吧
??好了對于Android的Hook我們就介紹到這里了,上面的這些Hook技術都是我在工作中積累的內容了浑劳,如果能幫助到也是喜歡Hook技術的你的話那么我很是欣慰了阱持,如果你還喜歡的話請幫忙點贊加關注,你的一鍵三連是我持續(xù)寫作的動力魔熏。
?? 在2022年最后一天這篇博客還是發(fā)表出來了衷咽,最后祝大家2023年元旦節(jié)心想事成,天天開心??···