Fireyer 是為了校驗我們的虛擬化環境構建是否存在缺陷,可以保障我們的每次更新的產品質量,提升開發效率。
專案已開源:
☞ Github:https://www.github.com/iofomo/fireyer ☜
如果您也喜歡 Fireyer,別忘了給我們點個星。
1. 說明
fire
+ eyer
= Fireyer
(火眼),Fireyer
專案是我們在做虛擬化沙箱產品過程中的內部副產品。目的是為了校驗我們的虛擬化環境構建是否存在漏洞,在內部作為我們產品的黑白檢測工具應用,可以保障我們的每次更新的產品質量,提升開發效率。對於開發沙箱,虛擬化等相關場景產品的夥伴也可以提升開發效率,快速驗證功能穩定性。Fireyer
的檢測項還在不斷完善中,後續會持續同步更新。
由於我們的虛擬化產品是普通主流機型,因此Fireyer
主要用於在正常系統環境下,檢測應用被重打包(或重簽名),容器環境(免安裝載入執行),虛擬機器(將Android
系統變成普通應用)的通用個人手機場景。Fireyer
當前並不適用於定製ROM
,或刷入Magisk
,或ROOT
的環境檢測(當然由於技術的相關性,其中某些檢測項可能生效,但並非針對性用例),但也在我們後續的迭代計劃中。
2. 如何使用
Fireyer
專案的主要目的是為了提升我們產品的穩定性,並非為了應用的強對抗,只是為了保證正常的應用行為執行穩定。
我們自測的方法:
- 在正常的應用環境中,點選
單元測試【原始環境】
,Fireyer
會將執行完成的用例資料格式化儲存在系統的剪下板中備用。 - 在虛擬的測試環境中,點選
單元測試【虛擬環境】
,Fireyer
會從系統的剪下板中獲取測試資料,然後與當前執行用例結果進行對比,最終得到測試驗證的目的。
3. 系統呼叫實現
為了可以實現對inline
和got
表的攔截檢測,我們需要實現一些基本函式的系統呼叫,如:
int open(const char *pathname, int flags, ...);
int close(int fd);
int stat(const char* path, struct stat* buf);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t readlink(const char *path, char *buf, size_t bufsiz);
系統呼叫的方式如何實現呢,有個簡單的辦法就是將手機裡面的libc.so
庫匯出來(這裡匯出的64位的庫),然後用ida
開啟,檢視對應函式的實現,如open
的實現如下:
這樣我們得到openat
在64位系統上的系統呼叫的實現方式:
__attribute__((__naked__)) int svc_openat() {
__asm__ volatile("mov x15, x8\n"
"ldr x8, =0x38\n"
"svc #0\n"
"mov x8, x15\n"
"bx lr"
);
}
優勢:
透過自實現系統呼叫函式,可以在關鍵的地方和正常的函式呼叫進行對比,從而達到識別的目的,不管是基於got
表還是inline
的攔截。
對抗:
如何對抗該檢測,則可以使用應用級trace
攔截。
4. 代理攔截和檢測
攔截是利用Java
的Proxy
模組完成的,如:
package java.lang.reflect;
public class Proxy implements java.io.Serializable {
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);
}
代理後,原物件例項被更換為代理後的物件,當應用使用呼叫介面方法後,即可回撥。
普通的檢測方法:
package java.lang.reflect;
public class Proxy implements java.io.Serializable {
public static boolean isProxyClass(Class<?> cl) {
return Proxy.class.isAssignableFrom(cl) && proxyClassCache.containsValue(cl);
}
}
通常對方會自己呼叫native
方法實現建立代理物件,而不使用Proxy
類,如:
package java.lang.reflect;
public class Proxy implements java.io.Serializable {
private static native Class<?> generateProxy(String name, Class<?>[] interfaces,
ClassLoader loader, Method[] methods,
Class<?>[][] exceptions);
}
那我們依然可以透過對比該物件的類名進行識別,如:
// 正常類
android.view.IWindowSession$Stub$Proxy
// 代理後的類
android.view.IWindowSession$Stub$Proxy$Proxy
5. Binder攔截和檢測
很多時候我們與Service
的通訊可能被劫持,而攔截Binder
通訊最簡單的方法就是介面代理。由於Android
服務的Binder
通訊框架的資料解析和序列化都是基於介面:
/**
* /frameworks/base/core/java/android/app/IActivityManager.aidl
*/
interface IActivityManager {
// ...
}
/**
* /frameworks/base/core/java/android/content/pm/IPackageManager.aidl
*/
interface IPackageManager {
// ...
}
public interface Parcelable {
public interface Creator<T> {
public T createFromParcel(Parcel source);
public T[] newArray(int size);
}
}
1、我們可以獲取對應服務的Binder
物件,檢測是否已經被代理。
Object obj = ReflectUtils.getStaticFieldValue("android.app.ActivityManager", "IActivityManagerSingleton");
Object inst = ReflectUtils.getFieldValue(obj, "mInstance");
if (Proxy.isProxyClass(inst.getClass())) {
// TODO
}
2、可能面臨基於底層Binder
攔截的方案,如之前分享的開源專案:【Android】深入Binder底層攔截。
則整個解析不經過Java
層,上層無法檢測,但是底層解析有個很大的弊端就是對於複雜的Binder
通訊,如引數或返回值為Bundle
,Intent
,ApplicationInfo
,PackageInfo
時,解析邏輯非常複雜,要做到相容性好,通常會呼叫上層的程式碼進行解析。
6. 完整性檢測
6.1 簽名校驗
1、透過系統的PackageManagerService
提供的返回值(太簡單,非小白略過)。
PackageInfo pi = getContext().getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNING_CERTIFICATES);
pi.signingInfo;// TODO
2、透過解析本地檔案。(太簡單,非小白略過)。
PackageInfo pi = getContext().getPackageManager().getPackageArchiveInfo(mPackageInfo.applicationInfo.sourceDir, PackageManager.GET_SIGNING_CERTIFICATES);
pi.signingInfo;// TODO
以上兩種方法都可以透過介面代理方式替換SigningInfo.CREATOR
,來完成PackageInfo.signingInfo
的攔截和偽裝。
// source code
public final class SigningInfo implements Parcelable {
public static final @android.annotation.NonNull Parcelable.Creator<SigningInfo> CREATOR =
new Parcelable.Creator<SigningInfo>() {
@Override
public SigningInfo createFromParcel(Parcel source) {
return new SigningInfo(source);
}
@Override
public SigningInfo[] newArray(int size) {
return new SigningInfo[size];
}
};
}
6.2 屬性檢測
1、校驗Application
完整性。
<application
android:theme="@ref/0x7f120289" ----------------------------------------- 是否被替換
android:label="@ref/0x7f0d0001" ----------------------------------------- 是否被替換
android:icon="@ref/0x7f0d0001" ------------------------------------------ 是否被替換
android:name="com.demo.app.Application" --------------------------------- 是否被替換
android:persistent="false"
android:allowBackup="false"
android:debuggable="false" ---------------------------------------------- 是否被開啟
android:hardwareAccelerated="true"
android:largeHeap="true"
android:supportsRtl="false"
android:extractNativeLibs="true"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@ref/0x7f150051"
android:appComponentFactory="androidx.core.app.CoreComponentFactory" ---- 是否替換
android:requestLegacyExternalStorage="true"
android:allowNativeHeapPointerTagging="false"
android:preserveLegacyExternalStorage="true"
>
</application>
2、檢測permission
。
3、檢測四大元件:activity
、activity-alias
、service
、provider
、receiver
。
4、檢測meta-data
。
7. 執行環境
7.1 檢測隱藏API許可權
很多應用篡改目的是為了完成某些功能,時常涉及隱藏介面的呼叫(從9.0
後),會將一些模組的保護許可權解除,因此我們需要對一些常用的模組做檢測。
if (classFind("android.app.ActivityThread")) break;
// /libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
if (classFind("dalvik.system.DexPathList")) break;
// /frameworks/base/core/java/android/app/LoadedApk.java
if (classFind("android.app.LoadedApk")) break;
// /frameworks/base/core/java/android/app/IActivityManager.aidl
if (classFind("android.app.IActivityManager")) break;
// /frameworks/base/core/java/android/content/pm/IPackageManager.aidl
if (classFind("android.content.pm.IPackageManager")) break;
透過一些類的反射訪問(該類在Android
開發者網站上說明,原始碼有@hide
標註),可以確認當前執行環境的隱藏API
是否已經被解除。該方案很難被修復,如果完全無感知需要虛擬化框架在呼叫時設定隱藏API
策略,提前快取好目標class
,method
和field
,然後再恢復,但如此則虛擬化環境記憶體消耗和初始化效能則會受到很大影響。
7.2 檢測目錄
透過系統呼叫實現檢視當前私有目錄下是否存在未知檔案和目錄,某些虛擬化環境會在應用目錄提前存放了一些資料檔案。
7.3 檢測呼叫棧
在某些關鍵函式回撥中進行呼叫棧的檢測。
- 如:
AppComopentFactory
的初始化回撥。 - 如:
Application
的初始化回撥。 - 如:
ActivityThread$H
的callback
回撥。
檢測的方式:
- 直接上層的
Thread.dumpStack
獲取。虛擬化環境可以透過對native
的函式攔截偽裝。 - 透過低層
libunwind
庫獲取對應的函式名和庫資訊。虛擬化環境可以透過對getcontext
的攔截進行偽裝。
7.4 檢測執行緒
Java
層檢測:
public static void getAllThreadsInfo() {
Map<Thread, StackTraceElement[]> allThreads = Thread.getAllStackTraces();
for (Map.Entry<Thread, StackTraceElement[]> entry : allThreads.entrySet()) {
Thread thread = entry.getKey();
StackTraceElement[] stackTrace = entry.getValue();
// Got thread id and names
}
}
但某些實現會攔截native
層函式呼叫進行偽裝,因此我們需要遍歷執行緒目錄(使用自實現的系統呼叫函式訪問)
void getAllThreadsInfo() {
char threadName[128];
DIR* taskDir = opendir("/proc/self/task");
if (taskDir != nullptr) {
struct dirent* entry;
while ((entry = svc_readdir(taskDir)) != nullptr) {
if (entry->d_type == DT_DIR && strcmp(entry->d_name, ".") != 0 && strcmp(entry->d_name, "..") != 0) {
pid_t threadId = atoi(entry->d_name);
if (pthread_getname_np(pthread_t(threadId), threadName, sizeof(threadName)) == 0) {
// Got thread id and names
}
}
}
closedir(taskDir);
}
}
7.5 C程序檢測
增加採用C
程式命令的方式採集資訊。如:
ls ${dir}
。cat ${file}
。- 自己實現
c
程式對主程序進行資訊採集。
應對方案:
- 攔截程序
execve
函式,對呼叫c
程式命令的引數進行修正。 - 攔截程序
execve
函式,對即將fork
的子程序,向子程序的envp
環境變數注入預載入庫,從而實現對C
程式內部函式呼叫的攔截。
7.6 maps檢測
maps
檢測實現,使用系統呼叫函式對/proc/self/maps
中的內容進行校驗。
- 校驗
maps
是否有第三方庫的載入痕跡。 - 校驗
base.apk
路徑是否合法。 - 校驗
dex
庫是否被篡改。
該檢測可以被Trace
方案攔截,並對映至修正的新的maps
檔案,達到虛擬化偽裝的目的。
7.7 注入庫檢測
當前程序可能被載入了執行程式碼(如:dex
或lib
),因此我們透過查詢本程序的maps
進行識別(使用自實現的系統呼叫函式訪問)。
int fd = svc_open("proc/self/maps", "r");
if (0 <= fd) {
char buffer[1024];
svc_read(fd, buffer, sizeof(buffer);// 這裡迴圈讀取並檢測,是否包含非安裝目錄庫(如:/data/user)
svc_close(fd);
}
而對方可能會直接採用記憶體方式載入dex
或apk
,如:
/**
* /libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
**/
public final class DexPathList {
public static Element[] makeInMemoryDexElements(ByteBuffer[] dexFiles,
List<IOException> suppressedExceptions) {
Element[] elements = new Element[dexFiles.length];
int elementPos = 0;
for (ByteBuffer buf : dexFiles) {
try {
DexFile dex = new DexFile(new ByteBuffer[] { buf }, /* classLoader */ null,
/* dexElements */ null);
elements[elementPos++] = new Element(dex);
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + buf, suppressed);
suppressedExceptions.add(suppressed);
}
}
if (elementPos != elements.length) {
elements = Arrays.copyOf(elements, elementPos);
}
return elements;
}
}
同樣也會透過先在將lib
庫載入到記憶體,然後透過從記憶體載入lib
的方式實現,這樣在maps
中就不會留下的檔案目錄痕跡。
FILE* tempFile = tmpfile();
// TODO read lib file to tempFile
const char* tempFileName = fileno(tempFile);
void* libHandle = dlopen(tempFileName, RTLD_NOW);
if (libHandle != nullptr) {
// ...
dlclose(libHandle);
}
unlink(tempFileName);
以上情況,我們需要對maps
中的地址區間的內容進行進一步的識別。
7.8 Trace檢測
Trace
檢測實現,當前使用系統呼叫函式對/proc/self/status
中的TracerPid:
欄位進行簡單校驗。後面會有單獨的文章分享如何構建Trace
程序互相檢測實現。