freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

【Fireyer】一款Android平台环境检测应用
2024-05-24 20:47:54

image

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会从系统的剪切板中获取测试数据,然后与当前运行用例结果进行对比,最终得到测试验证的目的。

image

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

image

这样我们得到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进程互相检测实现。

# android安全 # 移动应用安全 # 虚拟化
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
  • 0 文章数
  • 0 关注者