【Fireyer】一款Android平臺環境檢測應用

iofomo發表於2024-05-24

Fireyer 是為了校驗我們的虛擬化環境構建是否存在缺陷,可以保障我們的每次更新的產品質量,提升開發效率。

專案已開源:

☞ Github:https://www.github.com/iofomo/fireyer ☜ 

如果您也喜歡 Fireyer,別忘了給我們點個星。

1. 說明

fire + eyer = Fireyer(火眼),Fireyer專案是我們在做虛擬化沙箱產品過程中的內部副產品。目的是為了校驗我們的虛擬化環境構建是否存在漏洞,在內部作為我們產品的黑白檢測工具應用,可以保障我們的每次更新的產品質量,提升開發效率。對於開發沙箱,虛擬化等相關場景產品的夥伴也可以提升開發效率,快速驗證功能穩定性。Fireyer的檢測項還在不斷完善中,後續會持續同步更新。

由於我們的虛擬化產品是普通主流機型,因此Fireyer主要用於在正常系統環境下,檢測應用被重打包(或重簽名),容器環境(免安裝載入執行),虛擬機器(將Android系統變成普通應用)的通用個人手機場景。Fireyer當前並不適用於定製ROM,或刷入Magisk,或ROOT的環境檢測(當然由於技術的相關性,其中某些檢測項可能生效,但並非針對性用例),但也在我們後續的迭代計劃中。

2. 如何使用

Fireyer專案的主要目的是為了提升我們產品的穩定性,並非為了應用的強對抗,只是為了保證正常的應用行為執行穩定。

我們自測的方法:

  1. 在正常的應用環境中,點選單元測試【原始環境】Fireyer會將執行完成的用例資料格式化儲存在系統的剪下板中備用。
  2. 在虛擬的測試環境中,點選單元測試【虛擬環境】Fireyer會從系統的剪下板中獲取測試資料,然後與當前執行用例結果進行對比,最終得到測試驗證的目的。

3. 系統呼叫實現

為了可以實現對inlinegot表的攔截檢測,我們需要實現一些基本函式的系統呼叫,如:

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. 代理攔截和檢測

攔截是利用JavaProxy模組完成的,如:

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通訊,如引數或返回值為BundleIntentApplicationInfoPackageInfo時,解析邏輯非常複雜,要做到相容性好,通常會呼叫上層的程式碼進行解析。

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、檢測四大元件:activityactivity-aliasserviceproviderreceiver

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策略,提前快取好目標classmethodfield,然後再恢復,但如此則虛擬化環境記憶體消耗和初始化效能則會受到很大影響。

7.2 檢測目錄

透過系統呼叫實現檢視當前私有目錄下是否存在未知檔案和目錄,某些虛擬化環境會在應用目錄提前存放了一些資料檔案。

7.3 檢測呼叫棧

在某些關鍵函式回撥中進行呼叫棧的檢測。

  1. 如:AppComopentFactory的初始化回撥。
  2. 如:Application的初始化回撥。
  3. 如:ActivityThread$Hcallback回撥。

檢測的方式:

  1. 直接上層的Thread.dumpStack獲取。虛擬化環境可以透過對native的函式攔截偽裝。
  2. 透過低層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程式命令的方式採集資訊。如:

  1. ls ${dir}
  2. cat ${file}
  3. 自己實現c程式對主程序進行資訊採集。

應對方案:

  1. 攔截程序execve函式,對呼叫c程式命令的引數進行修正。
  2. 攔截程序execve函式,對即將fork的子程序,向子程序的envp環境變數注入預載入庫,從而實現對C程式內部函式呼叫的攔截。

7.6 maps檢測

maps檢測實現,使用系統呼叫函式對/proc/self/maps中的內容進行校驗。

  1. 校驗maps是否有第三方庫的載入痕跡。
  2. 校驗base.apk路徑是否合法。
  3. 校驗dex庫是否被篡改。

該檢測可以被Trace方案攔截,並對映至修正的新的maps檔案,達到虛擬化偽裝的目的。

7.7 注入庫檢測

當前程序可能被載入了執行程式碼(如:dexlib),因此我們透過查詢本程序的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);
}

而對方可能會直接採用記憶體方式載入dexapk,如:

/**
 * /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程序互相檢測實現。

相關文章