【Fireyer】一款Android平台环境检测应用

查看 40|回复 2
作者:iofomo   

Fireyer 是为了校验我们的虚拟化环境构建是否存在缺陷,可以保障我们的每次更新的产品质量,提升开发效率。



topic.jpg (84.72 KB, 下载次数: 1)
下载附件
2024-5-28 10:58 上传

项目已开源:
☞ Github:https://www.github.com/iofomo/fireyer ☜ 
如果您也喜欢 Fireyer,别忘了给我们点个星。
1.  说明
fire + eyer = Fireyer(火眼),Fireyer项目是我们在做虚拟化沙箱产品过程中的内部副产品。目的是为了校验我们的虚拟化环境构建是否存在漏洞,在内部作为我们产品的黑白检测工具应用,可以保障我们的每次更新的产品质量,提升开发效率。对于开发沙箱,虚拟化等相关场景产品的伙伴也可以提升开发效率,快速验证功能稳定性。Fireyer的检测项还在不断完善中,后续会持续同步更新。
由于我们的虚拟化产品是普通主流机型,因此Fireyer主要用于在正常系统环境下,检测应用被重打包(或重签名),容器环境(免安装加载运行),虚拟机(将Android系统变成普通应用)的通用个人手机场景。Fireyer当前并不适用于定制ROM,或刷入Magisk,或ROOT的环境检测(当然由于技术的相关性,其中某些检测项可能生效,但并非针对性用例),但也在我们后续的迭代计划中。
2.  如何使用
Fireyer项目的主要目的是为了提升我们产品的稳定性,并非为了应用的强对抗,只是为了保证正常的应用行为运行稳定。
我们自测的方法:
[ol]
  • 在正常的应用环境中,点击单元测试【原始环境】,Fireyer会将运行完成的用例数据格式化保存在系统的剪切板中备用。
  • 在虚拟的测试环境中,点击单元测试【虚拟环境】,Fireyer会从系统的剪切板中获取测试数据,然后与当前运行用例结果进行对比,最终得到测试验证的目的。
    [/ol]


    02.jpg (89.17 KB, 下载次数: 2)
    下载附件
    2024-5-28 10:58 上传

    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的实现如下:


    01.jpg (139.91 KB, 下载次数: 0)
    下载附件
    2024-5-28 10:58 上传

    这样我们得到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 {
            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 CREATOR =
                new Parcelable.Creator() {
            @Override
            public SigningInfo createFromParcel(Parcel source) {
                return new SigningInfo(source);
            }
            @Override
            public SigningInfo[] newArray(int size) {
                return new SigningInfo[size];
            }
        };
    }
    6.2 属性检测
    1、校验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 检测调用栈
    在某些关键函数回调中进行调用栈的检测。
    [ol]
  • 如:AppComopentFactory的初始化回调。
  • 如:Application的初始化回调。
  • 如:ActivityThread$H的callback回调。
    [/ol]
    检测的方式:
    [ol]
  • 直接上层的Thread.dumpStack获取。虚拟化环境可以通过对native的函数拦截伪装。
  • 通过低层libunwind库获取对应的函数名和库信息。虚拟化环境可以通过对getcontext的拦截进行伪装。
    [/ol]
    7.4 检测线程
    Java层检测:
    public static void getAllThreadsInfo() {
        Map allThreads = Thread.getAllStackTraces();
        for (Map.Entry 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程序命令的方式采集信息。如:
    [ol]
  • ls ${dir}。
  • cat ${file}。
  • 自己实现c程序对主进程进行信息采集。
    [/ol]
    应对方案:
    [ol]
  • 拦截进程execve函数,对调用c程序命令的参数进行修正。
  • 拦截进程execve函数,对即将fork的子进程,向子进程的envp环境变量注入预加载库,从而实现对C程序内部函数调用的拦截。
    [/ol]
    7.6 maps检测
    maps检测实现,使用系统调用函数对/proc/self/maps中的内容进行校验。
    [ol]
  • 校验maps是否有第三方库的加载痕迹。
  • 校验base.apk路径是否合法。
  • 校验dex库是否被篡改。
    [/ol]
    该检测可以被Trace方案拦截,并映射至修正的新的maps文件,达到虚拟化伪装的目的。
    7.7 注入库检测
    当前进程可能被加载了执行代码(如:dex或lib),因此我们通过查找本进程的maps进行识别(使用自实现的系统调用函数访问)。
    int fd = svc_open("proc/self/maps", "r");
    if (0
    而对方可能会直接采用内存方式加载dex或apk,如:
    /**
    * /libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
    **/
    public final class DexPathList {
           public static Element[] makeInMemoryDexElements(ByteBuffer[] dexFiles,
                List[I] 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进程互相检测实现。

    函数, 环境

  • xixicoco   

    好家伙,都检测到了
    雨之幽   

    感谢分享
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部