hook原理
了解Hook
我們知道,在Android操作系統(tǒng)中系統(tǒng)維護著自己的一套事件分發(fā)機制。應(yīng)用程序扣汪,包括應(yīng)用觸發(fā)事件和后臺邏輯處理,也是根據(jù)事件流程一步步的向下執(zhí)行锨匆。而“鉤子”的意思崭别,就是在事件傳送到終點前截獲并監(jiān)控事件的傳輸,像個鉤子勾上事件一樣恐锣。并且能夠在勾上事件時茅主,處理一些自己特定的事件。如下圖所示:
動態(tài)代理
傳統(tǒng)的靜態(tài)代理模式需要為每一個需要代理的類寫一個代理類土榴,如果需要代理的類有幾百個那不是要累死诀姚?為了更優(yōu)雅地實現(xiàn)代理模式,JDK提供了動態(tài)代理方式玷禽,可以簡單理解為JVM可以在運行時幫我們動態(tài)生成一系列的代理類赫段,這樣我們就不需要手寫每一個靜態(tài)的代理類了。依然以購物為例矢赁,用動態(tài)代理實現(xiàn)如下:
public static void main(String[] args) {
Shopping people = new ShoppingImp();
System.out.println(Arrays.toString(people.doShopping(100)));
people = (Shopping) Proxy.newProxyInstance(Shopping.class.getClassLoader(),
people.getClass().getInterfaces(), new ShoppingHandler(people));
System.out.println(Arrays.toString(people.doShopping(100)));
}
Hook Android的startActivity方法
Android在啟動的時候會創(chuàng)建ActivityThread
, 這是一個單例的對象糯笙,而startActivity
實際上是Instrumentation
中的execStartActivity()
來實現(xiàn)的。所有我們只要替換掉ActivityThread
中的Instrumentation
的對象成我們自己的方法撩银。
創(chuàng)建代理類
public class ProxyInstrumentation extends Instrumentation {
private static final String TAG = "EvilInstrumentation";
// ActivityThread中原始的對象, 保存起來
Instrumentation mBase;
public ProxyInstrumentation(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, "sanfen到此一游8椤!额获!");
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都失效了.
// 由于這個方法是隱藏的,因此需要使用反射調(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(mBase, who,
contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
// 某該死的rom修改了 需要手動適配
throw new RuntimeException("do not support!!! pls adapt it");
}
}
}
通過反射修改ActivityThread
中的mInstrumentation
够庙。
public static void hookStartActivity(){
try {
// 先獲取到當(dāng)前的ActivityThread對象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 拿到原始的 mInstrumentation字段
Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
// 創(chuàng)建代理對象
Instrumentation evilInstrumentation = new ProxyInstrumentation(mInstrumentation);
// 偷梁換柱
mInstrumentationField.set(currentActivityThread, evilInstrumentation);
} catch (ClassNotFoundException
| NoSuchMethodException
| IllegalAccessException
| InvocationTargetException
| NoSuchFieldException e) {
e.printStackTrace();
}
}
執(zhí)行效果,在運行startActivty的時候打出了一段日志抄邀。
AndFix使用
AndFix采用native hook的方式耘眨,這套方案直接使用dalvik_replaceMethod替換class中方法的實現(xiàn)。由于它并沒有整體替換class, 而field在class中的相對地址在class加載時已確定撤摸,所以AndFix無法支持新增或者刪除filed的情況(通過替換init與clinit只可以修改field的數(shù)值)毅桃。
也正因如此褒纲,Andfix可以支持的補丁場景相對有限,僅僅可以使用它來修復(fù)特定問題钥飞。結(jié)合之前的發(fā)布流程莺掠,我們更希望補丁對開發(fā)者是不感知的,即他不需要清楚這個修改是對補丁版本還是正式發(fā)布版本(事實上我們也是使用git分支管理+cherry-pick方式)读宙。另一方面彻秆,使用native替換將會面臨比較復(fù)雜的兼容性問題。
引入andfix
在gradle中添加依賴
dependencies {
compile 'com.alipay.euler:andfix:0.5.0@aar'
}
在Application中初始化AndFix
public class AndFixApplication extends Application {
public static PatchManager mPatchManager;
@Override
public void onCreate() {
super.onCreate();
// 初始化patch管理類
mPatchManager = new PatchManager(this);
// 初始化patch版本
mPatchManager.init("1.0");
// String appVersion = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
// mPatchManager.init(appVersion);
// 加載已經(jīng)添加到PatchManager中的patch
mPatchManager.loadPatch();
}
}
生成patch包
為了方便演示结闸,我們設(shè)置點擊按鈕來加載patch
public class MainActivity extends AppCompatActivity {
private static final String APATCH_PATH = "/fix.apatch"; // 補丁文件名
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.load).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
update();
}
});
}
private void update() {
String patchFileStr = Environment.getExternalStorageDirectory().getAbsolutePath() + APATCH_PATH;
try {
AndFixApplication.mPatchManager.addPatch(patchFileStr);
} catch (IOException e) {
e.printStackTrace();
}
}
}
patch命令
- -f <new.apk> :新apk
- -t <old.apk> : 舊apk
- -o <output> : 輸出目錄(補丁文件的存放目錄)
- -k <keystore>: 打包所用的keystore
- -p <password>: keystore的密碼
- -a <alias>: keystore 用戶別名
- -e <alias password>: keystore 用戶別名密碼
sh apkpatch.sh -f app-release-2.0.apk -t app-release-1.0.apk -o output -k abc.keystore -p qwe123 -a abc.keystore -e qwe123
運行
load patch
#安裝應(yīng)用
adb install app-debug-1.0.apk
#將patch push到手機中
adb push fix.apatch /storage/emulated/0/fix.apatch
原理解析
.apatch實際是一個壓縮文件
Manifest-Version: 1.0
Patch-Name: app-debug-2
Created-Time: 12 May 2017 02:31:07 GMT
From-File: app-debug-2.0.apk
To-File: app-debug-1.0.apk
Patch-Classes: com.example.fensan.andfixdemo.MainActivity_CF
Created-By: 1.0 (ApkPatch)
這個Patch-CLasses標(biāo)志了哪些類有修改唇兑,這里會顯示完全的類名同時加上一個_CF后綴。AndFix首先會讀取這個文件里面的東西桦锄,保存在Patch類的一個對象里扎附,備用。
然后我們反編譯diff.dex來查看里面的類结耀,用jd-gui來查看:
可以看到這個dex里面只有一個class留夜,而且在我們所修改的方法上有一個"@MethodReplace"注解,在代碼中可以明顯的看到了我們加入的this.fix ="修復(fù)了"
這段代碼图甜!
源碼淺析
1. PatchManager
/**
* initialize
*
* @param appVersion
* App version
*/
public void init(String appVersion) {
//patch路徑的初始化
if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
Log.e(TAG, "patch dir create error.");
return;
} else if (!mPatchDir.isDirectory()) {// not directory
mPatchDir.delete();
return;
}
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
Context.MODE_PRIVATE);
String ver = sp.getString(SP_VERSION, null);
//針對apk版本的patch處理碍粥,如果版本不一致清除,版本一直則加載黑毅。
if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
cleanPatch();
sp.edit().putString(SP_VERSION, appVersion).commit();
} else {
//初始化本地的patch
initPatchs();
}
}
private void initPatchs() {
File[] files = mPatchDir.listFiles();
for (File file : files) {
addPatch(file);
}
}
在init()方法中嚼摩,主要對本地的patch進行處理,當(dāng)apk的版本與patch的版本一致矿瘦,就加載本地的patch枕面;版本不一致則清除。
接下來我們來看loadPatch()的代碼實現(xiàn):
/**
* load patch,call when application start
*
*/
public void loadPatch() {
mLoaders.put("*", mContext.getClassLoader());// wildcard
Set<String> patchNames;
List<String> classes;
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
//對本地的patch進行遍歷匪凡,并fix
for (String patchName : patchNames) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
classes);
}
}
}
loadPatch()中對本地的patch進行了遍歷膊畴,獲取每個patch的信息,逐一進行fix()病游,其中參數(shù)classes為patch中配置文件Patch.MF的Patch-Classes字段對應(yīng)的所有類唇跨,即為要修復(fù)的類。
接下來進入AndFixManager中衬衬。
AndFixManager
/**
* fix class
*
* @param clazz
* class
*/
private void fixClass(Class<?> clazz, ClassLoader classLoader) {
//得到類所有公用方法
Method[] methods = clazz.getDeclaredMethods();
MethodReplace methodReplace;
String clz;
String meth;
//枚舉方式买猖,通過注解找到需要替換的類
for (Method method : methods) {
//獲得打了@MethodRepalce標(biāo)簽的方法
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();
meth = methodReplace.method();
if (!isEmpty(clz) && !isEmpty(meth)) {
//需要替換的類,執(zhí)行下一步
replaceMethod(classLoader, clz, meth, method);
}
}
}
/**
* replace method
*
* @param classLoader classloader
* @param clz class
* @param meth name of target method
* @param method source method
*/
private void replaceMethod(ClassLoader classLoader, String clz,
String meth, Method method) {
try {
String key = clz + "@" + classLoader.toString();
//得到原apk中要替換的類
Class<?> clazz = mFixedClass.get(key);
//如果該類還沒有加載
if (clazz == null) {// class not load
Class<?> clzz = classLoader.loadClass(clz);
// initialize target class
// 初始化該類
clazz = AndFix.initTargetClass(clzz);
}
if (clazz != null) {// initialize class OK
mFixedClass.put(key, clazz);
Method src = clazz.getDeclaredMethod(meth,
method.getParameterTypes());
//開始進入native層進行方法的替換
AndFix.addReplaceMethod(src, method);
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
fixClass()方法中進行的過程就是從需要修復(fù)的類中定位到需要修復(fù)的方法滋尉。
replaceMethod() 定位到需要修復(fù)的方法以后玉控,進入AndFix進行方法的替換。
AndFix
AndFix是Java層進行方法替換的核心類狮惜,在該類中提供了Native層的接口高诺,加載了andfix.cpp碌识,主要進行了Native層的初始化,以及目標(biāo)修復(fù)類的替換工作虱而。
/**
* replace method's body
*
* @param src
* source method
* @param dest
* target method
*
*/
public static void addReplaceMethod(Method src, Method dest) {
try {
replaceMethod(src, dest);
initFields(dest.getDeclaringClass());
} catch (Throwable e) {
Log.e(TAG, "addReplaceMethod", e);
}
}
private static native void replaceMethod(Method dest, Method src);
Native層
Dalvik部分
extern jboolean __attribute__ ((visibility ("hidden"))) dalvik_setup(
JNIEnv* env, int apilevel) {
//Davik虛擬機實現(xiàn) 是在libdvm.so中
//dlopen()方法以指定模式打開動態(tài)鏈接庫筏餐,RTLD_NOW立即打開
void* dvm_hand = dlopen("libdvm.so", RTLD_NOW);
if (dvm_hand) {
//dvm_dlsym:通過句柄和連接符名稱獲取函數(shù)或變量名
dvmDecodeIndirectRef_fnPtr = dvm_dlsym(dvm_hand,
apilevel > 10 ?
"_Z20dvmDecodeIndirectRefP6ThreadP8_jobject" :
"dvmDecodeIndirectRef");
if (!dvmDecodeIndirectRef_fnPtr) {
return JNI_FALSE;
}
dvmThreadSelf_fnPtr = dvm_dlsym(dvm_hand,
apilevel > 10 ? "_Z13dvmThreadSelfv" : "dvmThreadSelf");
if (!dvmThreadSelf_fnPtr) {
return JNI_FALSE;
}
jclass clazz = env->FindClass("java/lang/reflect/Method");
jClassMethod = env->GetMethodID(clazz, "getDeclaringClass",
"()Ljava/lang/Class;");
return JNI_TRUE;
} else {
return JNI_FALSE;
}
}
該方法進行的操作主要是打開運行dalvik虛擬機的libdvm.so
,得到dvmDecodeIndirectRef_fnPtr牡拇、dvmThreadSelf_fnPtr
函數(shù)魁瞪,下面將用到這兩個函數(shù)獲取類對象。
接下來我們進入整個AndFix最核心的dalvik_replaceMethod()
方法中惠呼,在其中進行了對類方法指針的替換导俘,真正實現(xiàn)對方法的替換。
extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
//clazz為被替換的類
jobject clazz = env->CallObjectMethod(dest, jClassMethod);
//clz 為被替換的類對象
ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
dvmThreadSelf_fnPtr(), clazz);
//將類狀態(tài)設(shè)置為裝載完畢
clz->status = CLASS_INITIALIZED;
//得到指向新方法的指針
Method* meth = (Method*) env->FromReflectedMethod(src);
//得到指向需要修復(fù)的目標(biāo)方法的指針
Method* target = (Method*) env->FromReflectedMethod(dest);
LOGD("dalvikMethod: %s", meth->name);
//新方法指向目標(biāo)方法剔蹋,實現(xiàn)方法的替換
// meth->clazz = target->clazz;
meth->accessFlags |= ACC_PUBLIC;
meth->methodIndex = target->methodIndex;
meth->jniArgInfo = target->jniArgInfo;
meth->registersSize = target->registersSize;
meth->outsSize = target->outsSize;
meth->insSize = target->insSize;
meth->prototype = target->prototype;
meth->insns = target->insns;
meth->nativeFunc = target->nativeFunc;
}
ART
art部分根據(jù)版本號的不同旅薄,進行了不同的處理。
void replace_5_0(JNIEnv* env, jobject src, jobject dest) {
//獲得指向新的方法的指針
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
//獲得指向被替換的目標(biāo)方法的指針
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
//目標(biāo)方法的裝載器和方法中聲明類設(shè)置為新方法對應(yīng)值
dmeth->declaring_class_->class_loader_ =
smeth->declaring_class_->class_loader_;
dmeth->declaring_class_->clinit_thread_id_ =
smeth->declaring_class_->clinit_thread_id_;
dmeth->declaring_class_->status_ = smeth->declaring_class_->status_-1;
//新方法指向目標(biāo)方法滩租,實現(xiàn)方法的替換
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->access_flags_ = dmeth->access_flags_;
smeth->frame_size_in_bytes_ = dmeth->frame_size_in_bytes_;
smeth->dex_cache_initialized_static_storage_ =
dmeth->dex_cache_initialized_static_storage_;
smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
smeth->vmap_table_ = dmeth->vmap_table_;
smeth->core_spill_mask_ = dmeth->core_spill_mask_;
smeth->fp_spill_mask_ = dmeth->fp_spill_mask_;
smeth->mapping_table_ = dmeth->mapping_table_;
smeth->code_item_offset_ = dmeth->code_item_offset_;
smeth->entry_point_from_compiled_code_ =
dmeth->entry_point_from_compiled_code_;
smeth->entry_point_from_interpreter_ = dmeth->entry_point_from_interpreter_;
smeth->native_method_ = dmeth->native_method_;
smeth->method_index_ = dmeth->method_index_;
smeth->method_dex_index_ = dmeth->method_dex_index_;
LOGD("replace_5_0: %d , %d", smeth->entry_point_from_compiled_code_,
dmeth->entry_point_from_compiled_code_);
}
總體的過程總結(jié)如下:
- 初始化patch管理器赋秀,加載補独洹律想;
- 檢查手機是否支持,判斷ART绍弟、Dalvik技即;
- 進行md5,指紋的安全檢查
- 驗證補丁的配置樟遣,通過patch-classes字段得到要替換的所有類
- 通過注解從類中得到具體要替換的方法
- 修改方法的訪問權(quán)限為public
- 得到指向新方法和被替換目標(biāo)方法的指針而叼,將新方法指向目標(biāo)方法,完成方法的替換豹悬。
AndFix提供了一種Native層hook Java層代碼的思路葵陵,實現(xiàn)了動態(tài)的替換方法。在處理簡單沒有特別復(fù)雜的方法中有獨特的優(yōu)勢瞻佛,但因為在加載類時跳過了類裝載過程直接設(shè)置為初始化完畢脱篙,所以不支持新增靜態(tài)變量和方法。