【原創】Android5.1 Art Hook 技術分享
【原創】Android5.1 Art Hook 技術分享
Hi,大家好,很多次的在各種技術論壇上看到大牛的分享,學到了很多。本著共建社群,共享知識的目的,在這裡我和大家分享一下我最近研究到的關於Android5.1的ART HOOK方案。還是demo階段,請大家多多指正。可以加我QQ 313199058一起探討。
廢話不說,切入正題。
之前看過低端碼農關於ART HOOK的思路,有了啟發,大家可以先到他的部落格上看看他對於ART虛擬機器的理解以及他做的hook方案。
http://blog.csdn.net/l173864930/article/details/45035521
他做的是基於android 4.4的hook方案,但是是停留在僅僅打log的階段,在我後面的測試中發現這其實離真正的hook應用還相去甚遠。下面我羅列了一些我需要解決的問題。這些問題,低端碼農有解決過一些,有的沒有;xposed有解決過一些,有的沒有。
我們需要解決如下的幾個問題:
1. 如何hook到一個函式
2. 如何迴避在輸出呼叫棧時的虛擬機器崩潰
3. 如何處理引數
4. 如何處理返回值
5. 如何在不替換libart.so的情況下完成hook
6. 如何在不引用android原始碼標頭檔案及庫的情況下,用NDK直接編譯出我們的so
0x00
老調重彈,我先簡單介紹一下ART虛擬機器關於方法的呼叫方式。不同於dalvik虛擬機器,ART其實包含了兩種呼叫方式——解釋執行和機器碼執行,首先他沒有完全丟棄解釋執行的呼叫方式,因為有些情況下還是需要通過解釋執行完成一個函式的執行;接著ART不同於dalvik是因為引入了機器碼的執行方式,其實就是在dex opt的時候dex裡的一個函式體被優化成了組合語言編寫的機器碼,這樣執行效率當然高了。
下面看一下oatdump出的某函式片段
4: voidcom.example.atry.MainActivity.onClick(android.view.View) (dex_method_idx=18)
DEX CODE:
0x0000: const/4 v0, #+2
0x0001: const-wide/16 v2, #+5
0x0003: invoke-virtual {v4, v0, v2, v3}, voidcom.example.atry.MainActivity.nativeTest(int, long) // [email protected]
0x0006: return-void
OAT DATA:
frame_size_in_bytes: 64
core_spill_mask: 0x00008060 (r5, r6, r15)
fp_spill_mask: 0x00000000
vmap_table: 0xf722d58a (offset=0x0000258a)
v3/r5, v4/r6, v65535/r15
mapping_table: 0xf722d584 (offset=0x00002584)
gc_map: 0xf722d590 (offset=0x00002590)
CODE: 0xf722d51d (offset=0x0000251d size=104)...
0xf722d51c: f8d9c010 ldr.w r12, [r9, #16] ; stack_end_
0xf722d520: e92d4060 push {r5, r6, lr}
0xf722d524: f2ad0e34 subw lr, sp, #52
0xf722d528: 45e6 cmp lr, r12
0xf722d52a: f0c08024 bcc.w +72 (0xf722d576)
0xf722d52e: 46f5 mov sp, lr
0xf722d530: 9000 str r0, [sp, #0]
0xf722d532: 1c0e mov r6, r1
0xf722d534: 9212 str r2, [sp, #72]
0xf722d536: 2202 movs r2, #2
0xf722d538: 9208 str r2, [sp, #32]
0xf722d53a: 2305 movs r3, #5
0xf722d53c: f04f0c00 mov.w r12, ThumbExpand(0)
0xf722d540: e9cd3c0a
0xf722d544: 9b0b ldr r3, [sp, #44]
0xf722d546: 1c31 mov r1, r6
0xf722d548: f8d1e000 ldr.w lr, [r1, #0]
0xf722d54c: 9304 str r3, [sp, #16]
0xf722d54e: 9304 str r3, [sp, #16]
0xf722d550: f8dee034 ldr.w lr, [lr, #52]
0xf722d554: 9b0a ldr r3, [sp, #40]
0xf722d556: 2202 movs r2, #2
0xf722d558: f8de0544 ldr.w r0, [lr, #1348]
0xf722d55c: f8d0e028 ldr.w lr, [r0, #40]
0xf722d560: 47f0 blx lr
suspend point dex PC: 0x0003
GC map objects: v4 (r6), v5 ([sp + #72])
0xf722d562: 3c01 subs r4, #1
0xf722d564: f0008003 beq.w +6 (0xf722d56e)
0xf722d568: b00d add sp, sp, #52
0xf722d56a: e8bd8060 pop {r5, r6, pc}
0xf722d56e: f8d9e25c ldr.w lr, [r9, #604] ; pTestSuspend
0xf722d572: 47f0 blx lr
suspend point dex PC: 0x0006
0xf722d574: e7f8 b -16 (0xf722d568)
0xf722d576: f8dde008 ldr.w lr, [sp, #8]
0xf722d57a: b003 add sp, sp, #12
0xf722d57c: f8d9c274 ldr.w r12, [r9, #628] ; pThrowStackOverflow
0xf722d580: 4760 bx r12
0xf722d582: 0000 lsls r0, r0, #0
包含了smali程式碼和彙編程式碼。
由於ART是這種大雜燴的執行函式的方式,因此他就要確定一個函式是通過解釋執行來執行,還是通過機器碼來執行,所以在4.4版本的art的出現了bridge的概念,他可以被理解為解釋執行方式跳轉到機器碼執行方式或者機器碼執行方式跳轉到解釋執行方式的橋樑。舉例說明,就是本來a,b,c,d四個函式都是順序執行在機器碼執行的方式下,突然在呼叫e這個函式的時候發現需要跳轉到解釋執行的方式,這就需要一個bridge。
下面結合程式碼看一下,首先是art_method.h(忽略了無關程式碼)
class MANAGED ArtMethod : public Object {
…
protected:
Class* declaring_class_;
uint32_taccess_flags_;
uint32_t code_item_offset_;
const void* entry_point_from_compiled_code_;
EntryPointFromInterpreter*entry_point_from_interpreter_;
….
}
這裡entry_point_from_compiled_code_和entry_point_from_interpreter_就是2個bridge。一個是說從code(機器碼)轉來的,去哪裡由這個bridge決定,一個是說從interpreter轉來的,去哪裡由這個bridge決定。其實每次函式呼叫,呼叫者都是執行被呼叫者的bridge。舉例說明,如果一個函式是在機器碼執行流程裡,他呼叫下一個函式的時候會呼叫被呼叫者的成員介面entry_point_from_compiled_code_(意思是告訴被呼叫者,這是來自機器碼的執行流程),如果被呼叫者的這個介面被設為機器碼的執行入口,那麼被呼叫者就直接被執行了,也就是是說被呼叫者也是在機器碼執行流程中;否則,這個介面如果被設為一個解釋執行函式的入口函式,被呼叫者就會在解釋執行中被運行了。下面介紹的一個就是一個解釋執行的入口函式。
ENTRY art_quick_to_interpreter_bridge
SETUP_REF_AND_ARGS_CALLEE_SAVE_FRAME
mov r1, r9 @ pass Thread::Current
mov r2, sp @ pass SP
blx artQuickToInterpreterBridge @ (Method* method, Thread*, SP)
ldr r2, [r9,#THREAD_EXCEPTION_OFFSET] @ loadThread::Current()->exception_
ldr lr, [sp, #44] @ restore lr
add sp, #48 @ pop frame
.cfi_adjust_cfa_offset -48
cbnz r2, 1f @ success if no exception ispending
bx lr @ return on success
1:
DELIVER_PENDING_EXCEPTION
END art_quick_to_interpreter_bridge
可以看到最終呼叫到artQuickToInterpreterBridge中去了,在那裡就會對這個函式進行了解釋執行。
把之前的com.example.atry.MainActivity.onClick函式的內容再拿來分析一下(去掉無關程式碼)
4: voidcom.example.atry.MainActivity.onClick(android.view.View) (dex_method_idx=18)
DEX CODE:
0x0000: const/4 v0, #+2
0x0001: const-wide/16 v2, #+5
0x0003: invoke-virtual {v4, v0, v2, v3}, voidcom.example.atry.MainActivity.nativeTest(int, long) // [email protected]
0x0006: return-void
CODE: 0xf722d51d (offset=0x0000251d size=104)...
0xf722d51c: f8d9c010 ldr.w r12, [r9, #16] ; stack_end_
0xf722d520: e92d4060 push {r5, r6, lr}
0xf722d524: f2ad0e34 subw lr, sp, #52
0xf722d528: 45e6 cmp lr, r12
0xf722d52a: f0c08024 bcc.w +72 (0xf722d576)
//上面都是檢查是否呼叫函式層數太多,防止棧溢位。
0xf722d52e: 46f5 mov sp, lr
0xf722d530: 9000 str r0, [sp, #0]
0xf722d532: 1c0e mov r6, r1
0xf722d534: 9212 str r2, [sp, #72]
0xf722d536: 2202 movs r2, #2
0xf722d538: 9208 str r2, [sp, #32]
0xf722d53a: 2305 movs r3, #5
0xf722d53c: f04f0c00 mov.w r12, ThumbExpand(0)
0xf722d540: e9cd3c0a
0xf722d544: 9b0b ldr r3, [sp, #44]
0xf722d546: 1c31 mov r1, r6
0xf722d548: f8d1e000 ldr.w lr, [r1, #0]
0xf722d54c: 9304 str r3, [sp, #16]
0xf722d54e: 9304 str r3, [sp, #16]
0xf722d550: f8dee034 ldr.w lr, [lr, #52]
0xf722d554: 9b0a ldr r3, [sp, #40]
0xf722d556: 2202 movs r2, #2//上面都是在構造引數,準備呼叫下個函式
0xf722d558: f8de0544 ldr.w r0, [lr, #1348] //找到了被呼叫函式nativeTest
0xf722d55c: f8d0e028 ldr.w lr, [r0, #40]//取出被呼叫函式首地址偏移40的地址
0xf722d560: 47f0 blx lr//跳轉到偏移40處的地址
…
從上的程式碼可以看到,機器碼直接跳轉到被呼叫函式偏移40的位置,而那就是被呼叫函式的entry_point_from_compiled_code_介面。(4.4是偏移40,5.1偏移44)
機器碼執行的函式在初始化的時候會設定entry_point_from_compiled_code_為機器碼執行入口;而如果這個函式需要解釋執行,則entry_point_from_compiled_code_會被設為art_quick_to_interpreter_bridge(絕大多數的).
說了這麼多,就引出了第一個問題的答案,如何hook一個函式?我們可以把一個函式偏移40處的地址存的值設為我們自己寫的函式地址,這樣,一個函式的執行流程就被hook到了。
程式碼示例:
static jint hook_zposed_method(JNIEnv* env, jobjectthiz, jobject method) {
jmethodID methid = (*env)->FromReflectedMethod(env, method);
int artmeth = (int) methid;
int* quick_entry_32 = (int*) (artmeth + 40);
jint ptr = (jint)* quick_entry_32;
*quick_entry_32 = (int) (&art_quick_proxy);
/*
int* access_flag = (int*) (artmeth +METHOD_ACCESS_FLAG);
*access_flag = *access_flag | kAccNative;
int* mapping_table = (int*) (artmeth +METHOD_MAPPING_TABLE);
*mapping_table = 0;*/
return ptr;
}
art_quick_proxy就是我們自己寫的函式,事實證明在呼叫被hook函式的時候,呼叫的其實是art_quick_proxy。
0x01
下面我們談談如何迴避輸出呼叫棧時虛擬機器會crash的問題。
相信大多數做ART HOOk的朋友都遇到過這樣的問題,如果在呼叫流程中類似
Log.d(TAG, "test", new Exception());或者
e.printStackTrace();等函式被呼叫,
虛擬機器就會崩潰。崩潰的位置在StackVisitor::WalkStack函式裡。經過我的實驗發現,在呼叫上面型別的函式時,函式會回溯呼叫棧,如果發現在呼叫棧裡出現了沒有和dex對應的彙編指令(就是我們自己定義的跳轉函式等)就會報錯。
所以我們解決這個問題的辦法需要走2步:
1. 堅決杜絕在呼叫hook處理函式前,呼叫到輸出堆疊型別的函式;並且在呼叫到hook處理函式前要做好堆疊和暫存器的處理,保證在回溯的時候發現不了任何跳轉函式的足跡
2. 去掉dex和機器碼之間的mapping關係。在4.4上的art_method類裡是有一個mapping table的成員,我的做法是直接將其值為Null。但是還沒有在5.1上發現類似的成員,所以5.1上的hook可能出現類似的問題,不過既然知道了方向,想解決也不難。
下面我們看一下一個hook的示例
ENTRY art_quick_dispatcher
push {r4,r5, lr} @ sp -12
…
blx artQuickToDispatcher
pop {r4,r5, pc} @ success,r0andr1 hold the result
END art_quick_dispatcher
上面一段的意思就是說一個被hook的函式被呼叫後,其實先呼叫了art_quick_dispatcher這個函式,接著這個函式又呼叫了artQuickToDispatcher。
那麼這就出現了問題,如果原函式或者你自己寫的hook處理函式中出現了輸出呼叫棧的程式碼,那麼我們預期的呼叫關係會是:呼叫者->art_quick_dispatcher->artQuickToDispatcher->被呼叫者。可是art_quick_dispatcher和artQuickToDispatcher是沒有java程式碼與其對應的,結果就是虛擬機器直接崩潰。
所以我對這種程式碼做了一個升級。要達到的目的就是每次呼叫一個我們自己寫的函式,都是有java程式碼與其對應的。舉例說來就是上面的呼叫者->art_quick_dispatcher->artQuickToDispatcher->被呼叫者關係,其中的art_quick_dispatcher,artQuickToDispatcher都有真實的java程式碼對應。所以我就預寫了相關的java函式程式碼,然後取出其art_method的首地址,在hook後直接在彙編程式碼層進行呼叫。這樣對於虛擬機器來說就是正常的函式呼叫關係了。
呼叫形式如下:
ENTRY art_quick_proxy
push {r0-r7}
mov r7, lr
mov r0, #9//標號為9的函式是我預寫的java函式
bl exe_switch_entry//執行這個java函式,exe_switch_entry也是我寫的一段彙編程式碼
…
mov lr, r7
pop {r0-r7}
…
這裡儲存了所有暫存器和返回地址,在呼叫了想要呼叫的java函式後,所有暫存器和堆疊以及返回地址都恢復正常,這樣對於虛擬機器來說,就相當於正常的java呼叫。
取消mapping關係的方法,在4.4上我將art_method類mapping table置為Null,方法如下:
//下面的define都是4.4上的,5.1上沒有mappingtable,還沒有研究
#define METHOD_ACCESS_FLAG 20
#define METHOD_MAPPING_TABLE 60
#define kAccNative 0x0100
#define kAccStatic 0x0008
…
int* access_flag = (int*) (artmeth + METHOD_ACCESS_FLAG);
*access_flag = *access_flag | kAccNative;
int* mapping_table = (int*) (artmeth + METHOD_MAPPING_TABLE);
*mapping_table= 0;
…
0x02
如何處理引數
處理引數問題就和主流的引數處理方法一致了,我這裡就是遍歷堆疊,獲取引數,然後通過呼叫java函式對基本型別裝箱,最後由一個Object陣列的形式封裝所有的引數。下面具體介紹一下。
首先要介紹一下art虛擬機器上引數是如何傳遞的。在彙編層面,引數組織如下:
r0 = method
r1 = this
r2 = arg0
r3 = arg1
[sp] = N/A
[sp + 4] = N/A
[sp + 8] = N/A
[sp + 12] = N/A
[sp + 16] = arg2
需要注意的就是堆疊中的前4個儲存單元裡存的東西未知,不管是什麼,肯定是我們不需要的,但是又不建議妄自修改的東西。
然後我們可以從r2暫存器開始遍歷,取出所有的引數。基本引數的裝箱就是指將int, short等型別轉換為Integer, Short這樣的Object,java裡已經有這樣的函式供我們使用了:
Integer.valueOf(int);
Short.valueOf(short);
…
0x03
如何處理返回值
我想到的辦法就是,為每一個被hook的函式都分配一個與其返回值對應的hook處理函式,由於返回值型別是確定的(8種基本型別加Object),所有我列舉的構造了9種不同返回值的hook處理函式(直接java編寫)。
private static int onHookInt(Object artmethod, Object receiver,Object[] args) {
return (Integer) HookManager.onHooked(artmethod,receiver, args);
}
private static long onHookLong(Object artmethod, Objectreceiver, Object[] args) {
return (Long) HookManager.onHooked(artmethod,receiver, args);
}
private static double onHookDouble(Object artmethod,Object receiver, Object[] args) {
return (Double) HookManager.onHooked(artmethod,receiver, args);
}
private static char onHookChar(Object artmethod, Objectreceiver, Object[] args) {
return (Character) HookManager.onHooked(artmethod,receiver, args);
}
private static short onHookShort(Object artmethod, Objectreceiver, Object[] args) {
return (Short) HookManager.onHooked(artmethod,receiver, args);
}
private static float onHookFloat(Object artmethod, Objectreceiver, Object[] args) {
return (Float) HookManager.onHooked(artmethod,receiver, args);
}
private static Object onHookObject(Objectartmethod, Object receiver, Object[] args) {
return HookManager.onHooked(artmethod,receiver, args);
}
private static boolean onHookBoolean(Objectartmethod, Object receiver, Object[] args) {
return (Boolean) HookManager.onHooked(artmethod,receiver, args);
}
private static byte onHookByte(Object artmethod, Objectreceiver, Object[] args) {
return (Byte) HookManager.onHooked(artmethod,receiver, args);
}
而HookManager.onHooked返回的是Object型別,對於基本型別來說,我們只要對其拆箱就可以了。
0x04
結束
至此,關於android5.1上的hook就完成了,本文主要是為了解決前輩們做的hook demo遺留下來的一些問題,立志於對這一體系做一種補充,感謝大家。