背景
當下,資料就像水、電、空氣一樣無處不在,說它是“21世紀的生産資料”一點都不誇張,由此帶來的是,各行業對于資料的争奪熱火朝天。随着網際網路和資料的思維深入人心,一些灰色産業悄然興起,資料販子、爬蟲、外挂軟體等等也接踵而來,網際網路行業中各公司競争對手之間不僅業務競争十分激烈,黑科技的比拼也越發重要。随着移動網際網路的興起,爬蟲和外挂也從單一的網頁轉向了App,其中利用Android平台下Dalvik模式中的
Xposed Installer
和
Cydia Substrate
架構對App的函數進行Hook這一招,堪稱老牌經典。
接下來,本文将分别介紹針對這兩種架構的防護技術。
Xposed Installer
原理
Zygote
在Android系統中App程序都是由Zygote程序“孵化”出來的。Zygote程序在啟動時會建立一個虛拟機執行個體,每當它“孵化”一個新的應用程式程序時,都會将這個Dalvik虛拟機執行個體複制到新的App程序裡面去,進而使每個App程序都有一個獨立的Dalvik虛拟機執行個體。
Zygote程序在啟動的過程中,除了會建立一個虛拟機執行個體之外還會将
Java Rumtime
加載到程序中并注冊一些Android核心類的JNI(Java Native Interface,Java本地接口)方法。一個App程序被Zygote程序孵化出來的時候,不僅會獲得Zygote程序中的虛拟機執行個體拷貝,還會與Zygote程序一起共享
Java Rumtime
,也就是可以将
XposedBridge.jar
這個Jar包加載到每一個Android App程序中去。安裝
Xposed Installer
之後,系統
app_process
将被替換,然後利用Java的
Reflection
機制覆寫内置方法,實作功能劫持。下面我們來看一下細節。
Hook和Replace
Xposed Installer
架構中真正起作用的是對方法的Hook和Replace。在Android系統啟動的時候,Zygote程序加載
XposedBridge.jar
,将所有需要替換的Method通過
JNI
方法
hookMethodNative
指向Native方法
xposedCallHandler
,這個方法再通過調用
handleHookedMethod
這個Java方法來調用被劫持的方法轉入Hook邏輯。
上面提到的
hookMethodNative
是
XposedBridge.jar
中的私有的本地方法,它将一個方法對象作為傳入參數并修改Dalvik虛拟機中對于該方法的定義,把該方法的類型改變為Native并将其實作指向另外一個B方法。
換言之,當調用那個被Hook的A方法時,其實調用的是B方法,調用者是不知道的。在hookMethodNative的實作中,會調用
XposedBridge.jar
中的
handleHookedMethod
這個方法來傳遞參數。
handleHookedMethod
這個方法類似于一個統一排程的Dispatch例程,其對應的底層的C++函數是
xposedCallHandler
。而
handleHookedMethod
實作裡面會根據一個全局結構
hookedMethodCallbacks
來選擇相應的Hook函數并調用他們的
before
和
after
函數,當多子產品同時Hook一個方法的時候
Xposed
會自動根據
Module
的優先級來排序。
調用順序如下:A.before -> B.before -> original method -> B.after -> A.after。
檢測
在做Android App的安全防禦中檢測點衆多,
Xposed Installer
檢測是必不可少的一環。對于Xposed架構的防禦總體上分為兩層:Java層和Native層。
Java層檢測
需要說明的是,Java層的檢測基本隻能檢測出基礎的
Xposed Installer
架構,而不能防護其對App内方法的Hook,如果架構中帶有反檢測則Java層檢測大多不起作用。
下面列出Java層的檢測點,僅供參考。
① 通過PackageManager檢視安裝清單
最簡單的檢測,我們調用Android提供的
PackageManager
的API來周遊系統中App的安裝情況來辨識是否有安裝
Xposed Installer
相關的軟體包。
PackageManager packageManager = context.getPackageManager();
List applicationInfoList = packageManager.getInstalledApplications(PackageManager.GET_META_DATA);
for (ApplicationInfo applicationInfo: applicationInfoList) {
if (applicationInfo.packageName.equals("de.robv.android.xposed.installer")) {
// is Xposed TODO... }
}
通常情況下使用
Xposed Installer
架構都會屏蔽對其的檢測,即Hook掉
PackageManager的getInstalledApplications
方法的傳回值,以便過濾掉
de.robv.android.xposed.installer
來躲避這種檢測。
② 自造異常讀取棧
Xposed Installer
架構對每個由Zygote孵化的App程序都會介入,是以在程式方法異常棧中就會出現
Xposed
相關的“身影”,我們可以通過自造異常
Catch
來讀取異常堆棧的形式,用以檢查其中是否存在
Xposed
的調用方法。
try {
throw new Exception("blah");
} catch(Exception e) {
for (StackTraceElement stackTraceElement: e.getStackTrace()) {
// stackTraceElement.getClassName() stackTraceElement.getMethodName() 是否存 在Xposed
}
}
E/GEnvironment: no such table: preference (code 1): while compiling: SELECT keyguard_show_livewallpaper FROM preference
...
at com.meituan.test.extpackage.ExtPackageManager.checkUpdate(ExtPackageManager.java:127)
at com.meituan.test.MiFGService$1.run(MiFGService.java:41)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5072)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
...
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:609)
at de.robv.android.xposed.XposedBridge.main(XposedBridge.java:132) //發現Xposed子產品
at dalvik.system.NativeStart.main(Native Method)
③ 檢查關鍵Java方法被變為Native JNI方法
當一個Android App中的
Java
方法被莫名其妙地變成了
Native JNI
方法,則非常有可能被
Xposed Hook
了。由此可得,檢查關鍵方法是不是變成
Native JNI
方法,也可以檢測是否被Hook。
通過反射調用
Modifier.isNative(method.getModifiers())
方法可以校驗方法是不是
Native JNI
方法,Xposed同樣可以篡改
isNative
這個方法的傳回值。
④ 反射讀取XposedHelper類字段
通過反射周遊
XposedHelper
類中的
fieldCache
、
methodCache
、
constructorCache
變量,讀取HashMap緩存字段,如字段項的key中包含App中唯一或敏感方法等,即可認為有
Xposed
注入。
boolean methodCache = CheckHook(clsXposedHelper, "methodCache", keyWord);
private static boolean CheckHook(Object cls, String filedName, String str) {
boolean result = false;
String interName;
Set keySet;
try {
Field filed = cls.getClass().getDeclaredField(filedName);
filed.setAccessible(true);
keySet = filed.get(cls)).keySet();
if (!keySet.isEmpty()) {
for (Object aKeySet: keySet) {
interName = aKeySet.toString().toLowerCase();
if (interName.contains("meituan") || interName.contains("dianping") ) {
result = true;
break;
}
}
}
...
return result;
}
Native層檢測
由上文可知,無論在Java層做何種檢測,Xposed都可以通過Hook相關的API并傳回指定的結果來繞過檢測,隻要有方法就可以被Hook。如果僅在Java層檢測就顯得很徒勞,為了有效提搞檢測準确率,就須做到Java和Native層同時檢測。每個App在系統中都有對應的加載庫清單,這些加載庫清單在
/proc/
下對應的
pid/maps
檔案中描述,在Native層讀取
/proc/self/maps
檔案不失為檢測Xposed Installer的有效辦法之一。由于
Xposed Installer
通常隻能Hook Java層,是以在Native層使用C來解析
/proc/self/maps
檔案,搜檢App自身加載的庫中是否存在
XposedBridge.jar
、相關的Dex、Jar和So庫等檔案。
bool is_xposed()
{
bool rel = false;
FILE *fp = NULL;
char* filepath = "/proc/self/maps";
...
string xp_name = "XposedBridge.jar";
fp = fopen(filepath,"r"))
while (!feof(fp))
{
fgets(strLine,BUFFER_SIZE,fp);
origin_str = strLine;
str = trim(origin_str);
if (contain(str,xp_name))
{
rel = true; //檢測到Xposed子產品
break;
}
}
...
}
Cydia Substrate
原理
Cydia Substrate
注入Hook的一個典型流程如下圖所示,在Java層配置注入的關鍵So庫
libsubstrate.so
和
libsubstratedvm.so
。考慮到Java層檢測強度太低,Substrate的檢測主要在Native層來實作。
檢測
動态加載式檢測
讀取
/proc/self/maps
,列出了App中所有加載的檔案。
上圖為
Cydia Substrate
在Android 4.4上注入後的程序maps表,其中
libsubstrate.so
和
libsubstrate-dvm.so
兩個檔案為Substrate必載入檔案。通過
IDA Pro
分析對其分析。
先來看
libsubstrate-dvm.so
的導出表,共有9個函數導出。
當程序maps表中出現
libsubstrate-dvm.so
,可以嘗試去load該so檔案并調用
MSJavaHookMethod
方法,它會傳回該方法的位址即判定為惡意子產品(第三方程式)。
void* lookup_symbol(char* libraryname,char* symbolname)
{
void *imagehandle = dlopen(libraryname, RTLD_GLOBAL | RTLD_NOW);
if (imagehandle != NULL){
void * sym = dlsym(imagehandle, symbolname);
if (sym != NULL){
return sym; //發現Cydia Substrate相關子產品
}
...
}
該方式基于載入庫檔案的檔案名或檔案路徑和導出函數來判斷是否為惡意子產品,如果完全依賴此方式來判斷可能會誤判,但也不失為檢測方式的一個點。
基于方法特征碼檢測
特征碼即用來判斷某段資料屬于哪個計算機字段。在非Root環境下一般一個正常App在啟動時候,系統會排程相關大小的記憶體、空間給App使用,此時App的運作環境内産生的資料、記憶體、存儲等是獨立于其它App的(即獨立運作在沙箱中)。因為處于運作沙箱環境中的程序對沙箱的記憶體有最高讀寫權限,當我們的App程序被惡意子產品附加或注入時,就可以通過對目前程序的PID所對應的maps中加載的子產品進行合法校驗。這裡的子產品校驗我們可以采取對單個子產品内容取樣來判斷是否為惡意子產品,這種方式被定義為“基于方法的特征碼檢測”。
下面對一段程式段中
OpcodeSample
方法來提取特征碼。
方法原型:
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG, fmt, ##args)
void OpcodeSample(int a ,int b){
int c,d,e;
c = a + b;
d = a * b;
e = a / b;
LOGD("Hello It's c !%s\n", c);
LOGD("Hello It's d !%s\n", d);
LOGD("Hello It's e !%s\n", e);
return;
}
通過
IDA Pro
對其分析。
左側紅色方框代表為
OpcodeSample
方法的操作碼,右邊為操作碼對應ARM平台的指令集。我們要在左側的操作碼中取出一段作為
OpcodeSample
的定位特征碼,選用
__android_log_print
方法調用指令集上下文,來确定特征碼。
第一次取樣:"03 20 31 46 42 46 FF F7 ?? EA"
通過第一次取樣,查找結果有三處相似,再進一步分析。這次我們加入一個常量取樣:
第二次取樣:"7E 44 ?? ?? F8 44 03 20 31 46 42 46 FF F7 ?? EA"
繼而得出唯一特征碼,到此,我們對特征碼方法取樣有了初步的了解。下面來把它轉為實用的技能——動态加載式檢測+特征碼結合。
我們對
libsubstrate-dvm.so
中導出函數
MSJavaHookMethod
來精準定位。
IDA PRO
導出函數表如圖:
第三次取樣:"55 57 56 53 E8 CC 14 ?? ?? 81 C3 DB ?? ?? ?? 8D 64 ?? ?? 8B 83 F4 ?? ?? ??"
以上即為對
Cydia Substrate
的注入檢測識别,通過檢測
/proc/self/maps
下的加載
so
庫清單得到各個庫檔案絕度路徑,通過
fopen
函數将
so
庫的内容以16進制讀進來放在記憶體裡面進行規則比對,采用字元串模糊查找來檢測是否命中黑名單中的方法特征碼。
總結
在安全對抗領域,相比攻擊方,防守方曆來處于弱勢的一方。上文所提到的
Xposed Installer
和
Cydia Substrate
的檢測也僅僅是保障App安全的手段之一。App安全的防禦不應僅僅依賴于此,應該建構起整體的安全防禦閉環,盡可能在所有已知的可能攻擊點都追加檢測,再配合代碼加強,将防禦代碼隐藏。遺憾的是App防禦代碼隐藏再深也終究會被破解,僅僅依賴于用戶端的防禦顯然是不足的。移動網際網路領域的整體安全防禦應該是走端雲結合協作之道,共同防禦,方能在攻防對抗中占據優勢地位。
作者簡介
- 禮贊,美團安全工程師,2016年11月加入美團。專注于二進制、移動端攻防相關工作,現負責美團Android移動安全元件的建設工作。
- 毅然,美團技術專家,2016年初加入美團。緻力于美團配送App組的Android App crash解決工作、Android App性能優化、Android App反外挂、反爬蟲。目前主導負責美團配送Android App移動安全相關建設。
招聘資訊
美團集團安全部正在招募Web&二進制攻防、背景&系統開發、機器學習&算法等各路小夥伴。
我們想做的事情:
建構一套基于海量IDC環境下,橫跨網絡層、虛拟化層、Server 軟體層(核心态/使用者态)、語言執行虛拟機層(JVM/Zend/JavaScript V8)、Web應用層、資料通路層(DAL)的,基于大資料+機器學習的全自動人機識别與安全事件感覺系統。規模上對應美團全線業務的伺服器,技術棧覆寫了幾乎大多數雲環境下的網際網路應用,資料規模也将是很大的挑戰。
此外我們還關注全球網際網路領域在企業安全建設方面的最佳實踐,努力建構類似于Google的内置式安全架構和縱深防禦體系,對在安全和工程技術領域有所追求的同學來說應該是一個很好的機會。
如果你想加入我們,歡迎将履歷發至郵箱zhaoyan17#meituan.com,具體職位資訊點選“崗位招聘”檢視。
另外友情打個招聘:美團配送App團隊,負責美團騎手、美團衆包、美團跑腿等配送相關App的研發,涉及技術領域包括但不限于App的穩定性建設、App性能監控和優化、App動态化。對上述領域感興趣的請聯系 yulei10#meituan.com 。