侧边栏壁纸
博主头像
Fup1p1 's Blog 博主等级

梦想是财富自由~

  • 累计撰写 38 篇文章
  • 累计创建 24 个标签
  • 累计收到 9 条评论

目 录CONTENT

文章目录

【Android】设备指纹检测与Hook框架检测(施工中)

Fup1p1
2023-10-09 / 0 评论 / 0 点赞 / 1811 阅读 / 0 字 / 正在检测是否收录...

ROOT检测

Java层检测

遍历文件名检测

	public class checkroot {
    	static String [] paths={
            "/system/app/Superuser.apk",
            "/sbin/su", "/sbin/magisk32",
            "/system/bin/su", "/system/xbin/su",
            "/data/local/xbin/su", "/data/local/bin/su",
            "/system/sd/xbin/su","/sbin/magisk64",
            "/system/bin/failsafe/su", "/data/local/su",
            "/su/bin/su","/data/adb/magisk",
            "/sbin/magisk","/sbin/magiskinit",
            "/sbin/magiskpolicy","/sbin/riru.prop","/bin/magisk","/bin/magiskpolicy",
            "/sbin/supolicy","/data/user/0/com.topjohnwu.magisk",
            "system/lib/libriru_edxp.so.s","system/lib/libriru_edxp.so",
            "system/lib/libsandhook.edxp.so","system/lib64/libriru_edxp.so",
            "system/lib/libsandhook.edxp.so.s","system/lib64/libriru_edxp.so",
            "system/lib64/libsandhook.edxp.so.s","lib/armeabi-v7a/libriru.so",
            "lib/armeabi-v7a/libriruhide.so","lib/armeabi-v7a/libriruloader.so",
            "lib/arm64-v8a/libriru.so","lib/arm64-v8a/libriruhide.so",
            "lib/arm64-v8a/libriruloader.so","lib/arm64-v8a/librirud.so",
            "/data/adb/modules/riru-core/allow_install_app",
            "/data/misc/riru/api_version","/data/misc/riru/version_code",
            "/data/misc/riru/version_name",
    };
    private static boolean checkrootfile(){
        //系统调用的过程中最终会调用一个方法,faccessat()
        for(String path:paths){
            boolean flag=new File(path).exists();
            if(flag) Log.i("Fup1p1:","Detected Root!!!(Java) :"+path);
        }
        return false;
    }
}

对抗思路

对于简单的直接hook,然后返回false即可。

因为其检测的文件名可能有很多,而且可能会对字符串进行加密,那么我们有什么办法去知道它检测了哪些文件名。

对于 File.exists() 方法,它的目的是检查某个文件是否存在。在 Linux 层面,判断文件是否存在常常是通过 access 或 faccessat 系统调用来完成的。access 用于确定调用进程是否有权限访问文件的路径,而不是实际打开它。
使用strace工具(一开始没有编译进去,是通过termux 的pkg进行下载的)

image

image-1696560930001
strace 显示的是真实的系统调用,可见libc.so最后调用内核的函数是faccessat。
我使用frida hook libc时,hook faccessat和access都行,也都能打印出文件路径。

抽空会添加aosp源码的分析,

function hook_libc(){

   var access_addr = Process.findModuleByName("libc.so").findExportByName("access");
   var access_create_addr = Module.findExportByName("libc.so", "faccessat");
   console.log("pthread_create_addr =>", access_create_addr);
   Interceptor.attach(access_create_addr, {
       onEnter: function(args){
        console.log("onEnter");
        console.log("faccessat args =>", args[0], args[1].readCString(), args[2], args[4]);
       },onLeave: function(retval){
           console.log(retval);
       }
   })

   Interceptor.attach(access_addr, {
    onEnter: function(args){
     console.log("onEnter");
     console.log("access args =>", args[0].readCString(), args[1], args[2], args[4]);
    },onLeave: function(retval){
        console.log(retval);
    }
})
}
hook_libc();

通过执行which su命令

private static boolean checkroot2(){
        //保存命令行执行后的结果
        Process process=null;
            try {
                process=Runtime.getRuntime().exec("which su");
                //process=Runtime.getRuntime().exec(new String[]{"which","su"});//不能将命令和参数放在一起,即不能 "which su",这样会被看做一个整体命令,会找不到   效果同上
                BufferedReader inp=new BufferedReader(new InputStreamReader(process.getInputStream()));
                StringBuilder output=new StringBuilder();
                String line=inp.readLine();
                inp.close();
                if(line!=null){
                    Log.i("Fup1p1", "checkroot2: "+ line);
                    return true;
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if(process!=null){
                    process.destroy();
                }
        }
        return false;
    };

通过检查Build类中信息

当Android操作系统的源代码进行编译时,它会使用不同的密钥来签名。
这些密钥分为“test-keys”和“release-keys”。
通常,官方的、经过OEM验证的固件/ROM都是使用“release-keys”进行签名的,许多自定义ROM或某些root工具可能会使用“test-keys”进行签名。这是因为它们是基于Android Open Source Project (AOSP)编译的,并使用默认的test-keys进行签名。

如果你编译过Android就知道一开始可以选择系统的版本。userdebug版本则是包含了额外的调试和日志记录功能的版本。通常,这个版本是为开发者和测试人员提供的。

如果一个设备运行的是userdebug版本,那么该设备可能已经被root或者至少更容易被root。
如果想要查看build类的信息,可以在system目录下以root权限执行 cat build.prop 或者 getprop
image-1696562036188

    private static boolean checkroot1(){
        String buildtag= Build.TAGS;
        String buildfinger=Build.FINGERPRINT;
        if(buildtag=="test-keys"||buildfinger.contains("userdebug"))
            return true;
        Log.i("Fup1p1:","build.TAGS:"+buildtag+" build.FINGERPRINT: "+buildfinger);
        return false;
    }

so层检测

extern "C" JNIEXPORT jboolean JNICALL Java_com_anti_root_checkroot_nativeDetect(JNIEnv* env,
                                                                                  jobject /* this */clazz,jobjectArray paths){

    int count=env->GetArrayLength(paths);
    bool flag=false;
    bool ret=false;
    for(int i=0;i<count;i++){
        auto j_filepath=env->GetObjectArrayElement(paths,i);
        auto filepath=env->GetStringUTFChars(static_cast<jstring>(j_filepath), 0);
        //LOGI("%s",filepath);
        ret=File::NativeDetected(filepath,true);
        if(ret)flag=true;
    }
    return flag;
}

bool File::NativeDetected(const char * path,bool usesyscall){
    if(usesyscall){//使用系统调用的方式,查找文件是否存在
        long ret= syscall(SYS_faccessat,AT_FDCWD,path,0);
        if(ret==0){
            LOGI("JNI syscall:%s",path);
            return true;
        }else{
            return false;
        }
        
    }else{
        struct stat buf{};
        //使用linux内核提供的方法
        if(access(path,F_OK)==0){
            LOGI("JNI access :%s",path);
            return true;
        };
        if(stat(path,&buf)==0){//linux 文件属性结构体
            LOGI("JNI stat :%s",path);
            return true;
        }
        if(fstat(open(path,O_PATH),&buf)==0){
            LOGI("JNI fstat :%s",path);
            return true;
        }
    };
    return false;
}

Xposed检测

Java层检测

通过遍历内存去查找敏感类(Android 9+)

参考珍惜大佬的博客
blog

主要通过系统的API(VMDebug.java中的getInstancesOfClasses)去获得获取ClassLoader所有的实例,包括其子类的实例,然后遍历这些ClassLoader,通过Class.forName去查看特定的类是否存在。

public class chooseutils {
    private static final Method startMethodTracingMethod;
    private static final Method stopMethodTracingMethod;
    private static final Method getMethodTracingModeMethod;
    private static final Method getRuntimeStatMethod;
    private static final Method getRuntimeStatsMethod;
    private static final Method countInstancesOfClassMethod;
    private static final Method countInstancesOfClassesMethod;

    private static  Method getInstancesOfClassesMethod ;

    static {
        try {
            Class<?> c = Class.forName("dalvik.system.VMDebug");
            //启动方法跟踪器  Integer.TYPE 和 int.class 是等价的
            startMethodTracingMethod = c.getDeclaredMethod("startMethodTracing", String.class, Integer.TYPE, Integer.TYPE, Boolean.TYPE, Integer.TYPE);
            //停止方法跟踪器
            stopMethodTracingMethod = c.getDeclaredMethod("stopMethodTracing");
            //获取方法跟踪器状态
            getMethodTracingModeMethod = c.getDeclaredMethod("getMethodTracingMode");
            getRuntimeStatMethod = c.getDeclaredMethod("getRuntimeStat", String.class);
            getRuntimeStatsMethod = c.getDeclaredMethod("getRuntimeStats");
            countInstancesOfClassMethod = c.getDeclaredMethod("countInstancesOfClass",Class.class, Boolean.TYPE);
            countInstancesOfClassesMethod = c.getDeclaredMethod("countInstancesOfClasses", Class[].class, Boolean.TYPE);

            if(android.os.Build.VERSION.SDK_INT>=28) {
                getInstancesOfClassesMethod = c.getDeclaredMethod("getInstancesOfClasses", Class[].class, Boolean.TYPE);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 根据Class获取当前进程全部的实例
     *
     * @param clazz 需要查找的Class
     * @return 当前进程的全部实例。
     */
    @TargetApi(28)
    public static<T> ArrayList<T> choose(Class<T> clazz) {
        return choose(clazz, false);
    }

    /**
     * 根据Class获取当前进程全部的实例
     *
     * @param clazz      需要查找的Class
     * @param assignable 是否包含子类的实例
     * @return 当前进程的全部实例。
     */
    @TargetApi(28)
    public static synchronized <T>  ArrayList<T> choose(Class<T> clazz, boolean assignable) {
        ArrayList<T> resut = null;
        try {
            //从类加载器  ClassLoader.class  中查找他的子实例
            Object[][] instancesOfClasses = getInstancesOfClasses(new Class[]{clazz}, assignable);
            if (instancesOfClasses != null) {
                resut = new ArrayList<>();
                for (Object[] instancesOfClass : instancesOfClasses) {
                    List<T> objects = (List<T>)Arrays.asList(instancesOfClass);
                    resut.addAll(objects);
                }
            }
        } catch (Throwable e) {
            Log.e("Fup1p1","ChooseUtils choose error ", e);
            e.printStackTrace();
        }
        return resut;
    }

    /**
     *
     * @param classes   ClassLoader 类对象
     * @param assignable 是否获取所有子类的实例
     * @return
     * @throws Exception
     */
    @TargetApi(28)
    private static Object[][] getInstancesOfClasses(Class<?>[] classes, boolean assignable)
            throws Exception {
        return (Object[][]) getInstancesOfClassesMethod.invoke(null, classes, assignable);
    }




    //下面是 VMdebug.java 类其他的方法调用
    public static void startMethodTracing(String filename, int bufferSize, int flags,
                                          boolean samplingEnabled, int intervalUs) throws Exception {
        startMethodTracingMethod.invoke(null, filename, bufferSize, flags, samplingEnabled,
                intervalUs);
    }

    public static void stopMethodTracing() throws Exception {
        stopMethodTracingMethod.invoke(null);
    }

    public static int getMethodTracingMode() throws Exception {
        return (int) getMethodTracingModeMethod.invoke(null);
    }

    /**
     *  String gc_count = VMDebug.getRuntimeStat("art.gc.gc-count");
     *  String gc_time = VMDebug.getRuntimeStat("art.gc.gc-time");
     *  String bytes_allocated = VMDebug.getRuntimeStat("art.gc.bytes-allocated");
     *  String bytes_freed = VMDebug.getRuntimeStat("art.gc.bytes-freed");
     *  String blocking_gc_count = VMDebug.getRuntimeStat("art.gc.blocking-gc-count");
     *  String blocking_gc_time = VMDebug.getRuntimeStat("art.gc.blocking-gc-time");
     *  String gc_count_rate_histogram = VMDebug.getRuntimeStat("art.gc.gc-count-rate-histogram");
     *  String blocking_gc_count_rate_histogram =VMDebug.getRuntimeStat("art.gc.gc-count-rate-histogram");
     */
    public static String getRuntimeStat(String statName) throws Exception {
        return (String) getRuntimeStatMethod.invoke(null, statName);
    }

    /**
     * 获取当前进程的状态信息
     */
    public static Map<String, String> getRuntimeStats() throws Exception {
        return (Map<String, String>) getRuntimeStatsMethod.invoke(null);
    }

    public static long countInstancesofClass(Class<?> c, boolean assignable) throws Exception {
        return (long) countInstancesOfClassMethod.invoke(null, new Object[]{c, assignable});
    }

    public static long[] countInstancesofClasses(Class<?>[] classes, boolean assignable)
            throws Exception {
        return (long[]) countInstancesOfClassesMethod.invoke(
                null, new Object[]{classes, assignable});
    }

}
public static boolean check(){
        new Handler().post(new Runnable() {
            @Override
            public void run() {
                ArrayList<ClassLoader>choose=chooseutils.choose(ClassLoader.class,true);//true的话,会获取ClassLoader所有的实例  包含其子类
                Log.i("Fup1p1","对象数量"+choose.size());
                for(ClassLoader classLoader:choose){
                    Class <?> clazz=null;

                    try {
                        clazz=Class.forName("de.robv.android.xposed.XposedBridge",false,classLoader);
                        /**
                        de.robv.android.xposed.XposedHelpers
                        de.robv.android.xposed.XposedBridge
                        xposed 框架加载 jar包的类加载器 PathClassLoader  直接通过 context 获取即可
                        edXposed 框架加载 jar包的类加载器 InMemoryDexClassLoader 需要通过内存漫游方式获取
                        **/
                        if(clazz!=null){
                            Log.i("Fup1p1","类加载器: "+classLoader+"   类:"+clazz);

                        }
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        return false;
    }

检测lsposed结果

类加载器: dalvik.system.InMemoryDexClassLoader[DexPathList[[dex file "InMemoryDexFile[cookie=[0, 486433538928]]"],nativeLibraryDirectories=[/system/lib64, /system_ext/lib64, /product/lib64]]]   类:class de.robv.android.xposed.XposedBridge
类加载器: LspModuleClassLoader[module=/data/app/~~hNYaAHiorVwigr4njmWJlA==/com.hhvvg.anytext-ax_F5ZvkSinxbbrD3qNZhw==/base.apk, J[DexPathList[[dex file "InMemoryDexFile[cookie=[0, 486433428272]]"],nativeLibraryDirectories=[/data/app/~~hNYaAHiorVwigr4njmWJlA==/com.hhvvg.anytext-ax_F5ZvkSinxbbrD3qNZhw==/base.apk!/lib/arm64-v8a, /system/lib64, /system_ext/lib64, /product/lib64]]]]   类:class de.robv.android.xposed.XposedBridge
类加载器: LspModuleClassLoader[module=/data/app/~~9fTDxZuTlm-EjiD5_kw-cw==/name.caiyao.sporteditor-tPqHFcfm1ac1n3jBaXSiXA==/base.apk, J[DexPathList[[dex file "InMemoryDexFile[cookie=[0, 486433424912]]"],nativeLibraryDirectories=[/data/app/~~9fTDxZuTlm-EjiD5_kw-cw==/name.caiyao.sporteditor-tPqHFcfm1ac1n3jBaXSiXA==/base.apk!/lib/arm64-v8a, /system/lib64, /system_ext/lib64, /product/lib64]]]]   类:class de.robv.android.xposed.XposedBridge
类加载器: LspModuleClassLoader[module=/data/app/~~rmNIJCpJAn8sTQ-wfzxxAA==/com.gqghj.vb2345qw-VmGVuvSAx7-uecDIFTmhGQ==/base.apk, J[DexPathList[[dex file "InMemoryDexFile[cookie=[0, 486433429840, 486433424016]]"],nativeLibraryDirectories=[/data/app/~~rmNIJCpJAn8sTQ-wfzxxAA==/com.gqghj.vb2345qw-VmGVuvSAx7-uecDIFTmhGQ==/base.apk!/lib/arm64-v8a, /system/lib64, /system_ext/lib64, /product/lib64]]]]   类:class de.robv.android.xposed.XposedBridge
类加载器: LspModuleClassLoader[module=/data/app/~~e0w5JQTXcWygOaNPsJf4ng==/com.example.xposed01-1JodEz8RnWM0F8FLV_lavw==/base.apk, J[DexPathList[[dex file "InMemoryDexFile[cookie=[0, 486433419984, 486433427824, 486433422000]]"],nativeLibraryDirectories=[/data/app/~~e0w5JQTXcWygOaNPsJf4ng==/com.example.xposed01-1JodEz8RnWM0F8FLV_lavw==/base.apk!/lib/arm64-v8a, /system/lib64, /system_ext/lib64, /product/lib64]]]]   类:class de.robv.android.xposed.XposedBridge
类加载器: dalvik.system.InMemoryDexClassLoader[DexPathList[[dex file "InMemoryDexFile[cookie=[486970569168, 486433590224]]"],nativeLibraryDirectories=[/system/lib64, /system_ext/lib64, /product/lib64]]]   类:class de.robv.android.xposed.XposedBridge
类加载器: dalvik.system.InMemoryDexClassLoader[DexPathList[[dex file "InMemoryDexFile[cookie=[0, 486433433200]]"],nativeLibraryDirectories=[/system/lib64, /system_ext/lib64, /product/lib64]]]   类:class de.robv.android.xposed.XposedBridge
类加载器: dalvik.system.InMemoryDexClassLoader[DexPathList[[dex file "InMemoryDexFile[cookie=[0, 486433429616]]"],nativeLibraryDirectories=[/system/lib64, /system_ext/lib64, /product/lib64]]]   类:class de.robv.android.xposed.XposedBridge
类加载器: dalvik.system.InMemoryDexClassLoader[DexPathList[[dex file "InMemoryDexFile[cookie=[0, 486433442160]]"],nativeLibraryDirectories=[/system/lib64, /system_ext/lib64, /product/lib64]]]   类:class de.robv.android.xposed.XposedBridge
类加载器: dalvik.system.InMemoryDexClassLoader[DexPathList[[dex file "InMemoryDexFile[cookie=[0, 486433444176]]"],nativeLibraryDirectories=[/system/lib64, /system_ext/lib64, /product/lib64]]]   类:class de.robv.android.xposed.XposedBridge

proc/self/maps 检测

就是通过打开文件,然后扫描关键字,通过扫描maps目录,去检查当前进程的内存映射关系,。

public class xpcheck {
    public static boolean checkmaps(){
        Boolean retval=false;
        try {
            int pid=android.os.Process.myPid();
            FileInputStream file1=new FileInputStream("/proc/self/maps");
            FileInputStream file2=new FileInputStream("/proc/self/smaps");
            FileInputStream file3=new FileInputStream("/proc/"+pid+"/maps");
            List<InputStream> list=new ArrayList<>();
            list.add(file1);
            list.add(file2);
            list.add(file3);
            for(int i=0;i< list.size();i++){
                InputStreamReader inputStreamReader=new InputStreamReader(list.get(i), StandardCharsets.UTF_8);
                BufferedReader bufferedReader=new BufferedReader(inputStreamReader);
                while(true){
                    String line= bufferedReader.readLine();
                    if(line==null)break;

                    if(line.contains("Hook")||line.contains("zygisk")||line.contains("magisk")||line.contains("edxp")||line.contains("lsp")){
                        Log.i("Fup1p1","CheckMaps : "+line);
                        retval=true;
                    }
                }
                bufferedReader.close();
                inputStreamReader.close();
            }
            file1.close();
            file2.close();
            file3.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return retval;
    }

通过调用栈去检查

当一个方法被Xposed钩子时,调用堆栈可能会显示一些与Xposed相关的类和方法。因此,一个常见的检测Xposed是否在运行的方法是检查调用堆栈。xposed和一些模块的开发者知道这种检测方式,因此他们可能会使用一些技巧来避免被检测,比如修改或隐藏相关的类和方法。我实际使用lsposed测试是检测不到的。

    public static boolean checkstacktrace(){
        StackTraceElement[] elements=Thread.currentThread().getStackTrace();
        for(StackTraceElement element:elements){
//            Log.i("Fup1p1","checkstacktrace: "+element.getClassName());
            if(element.getClassName().contains("xposed")||element.getClassName().contains("EdHooker")){
                Log.i("Fup1p1","checkstacktrace :"+element.getClassName());
                return true;
            }
        };
        return false;
    };

so层检测

proc/self/maps 检测,改为JNI层

extern "C" JNIEXPORT jboolean JNICALL Java_com_anti_utils_xpcheck_nativecheckmaps(JNIEnv* env,
                                                                            jobject /* this */clazz){
    jboolean flag=false;
    jboolean ret=false;
    string pid=std::to_string(getpid());
    string file1="/proc/"+pid+"/maps";
    string file2="/proc/self/maps";
    string file3="/proc/self/smaps";

    std::vector<string>list{file1,file2,file3};

    for(int i=0;i<list.size();i++){
        flag=File::getmaps(const_cast<char *>(list.at(i).c_str()));;//要转成char *
        if(flag){
            ret=flag;
        }
    }

    return ret;
}

bool File::getmaps(char * path){
    bool flag=false;
    int fd=open(path,O_RDONLY);
    if(fd==-1){
        LOGE("failed to open file: %s",path);
    }
    FILE *file= fdopen(fd,"r");//将文件描述词转成文件指针
    char* line= nullptr;
    size_t len = 0;
    while(getline(&line,&len,file)!=-1){
	//indexof为自实现的函数,功能和strstr一样
    if(indexof(line,"edxp")||indexof(line,"magisk")||indexof(line,"zygisk")||indexof(line,"Hook")||indexof(line,"lsp")||indexof(line,"frida")){
            LOGI("JNI nativemap: %s",line);
            flag= true;
        }
    }
    return flag;
}

在Native层通过反射去调用getStackTrace

extern "C" JNIEXPORT jboolean JNICALL Java_com_anti_utils_xpcheck_nativecheckstacktrace(JNIEnv* env,
                                                                                  jobject /* this */clazz){

    bool retval=false;
    //Thread.currentThread().getStackTrace();
    jclass threadclass=env->FindClass("java/lang/Thread");
    jmethodID currentthread=env->GetStaticMethodID(threadclass,"currentThread","()Ljava/lang/Thread;");
     jobject thread=env->CallStaticObjectMethod(threadclass,currentthread);

     jmethodID getstacktrace =env->GetMethodID(threadclass,"getStackTrace", "()[Ljava/lang/StackTraceElement;");
     auto stacktrace=(jobjectArray)env->CallObjectMethod(thread,getstacktrace);
     int length=env->GetArrayLength(stacktrace);

     jclass stacktraceelementclass=env->FindClass("java/lang/StackTraceElement");
     jmethodID getclass=env->GetMethodID(stacktraceelementclass,"getClassName","()Ljava/lang/String;");

     for(int i=0;i<length;i++){
         jobject straceelement=env->GetObjectArrayElement(stacktrace,i);
         auto classname=(jstring)env->CallObjectMethod(straceelement,getclass);
         const char * name=env->GetStringUTFChars(classname,0);

         if(indexof(name,"xposed")|| indexof(name,"EdHooker")){
             LOGI("JNI checkstacktrace :%s ",name);
             retval= true;
         }
         //当你在 JNI 中获得一个 Java 对象的引用,Java 虚拟机需要知道这个引用何时不再被使用,这样垃圾回收器就可以回收它。
         env->ReleaseStringUTFChars(classname, name); // Release string chars
         env->DeleteLocalRef(classname); // Release the local reference
         env->DeleteLocalRef(straceelement);
     }

     return retval;

}

对抗手段

对于so层的,可以hook一些libc 的函数,比如getline函数内部最后会调用read函数,直接hook read函数,修改buf数据就行。
而且对于openat函数,可以修改fd或者path(后者可能会导致长度的问题所以不推荐)。其中修改fd就,就是自己用open打开一个没有特征的文件,用这个文件的fd去替换原先的fd。

Build类入门

build类中记录了很多设备指纹的关键信息
还有一些信息Build类中没有,要通过函数去获取比如基带信息

我们可以通过反射去获得,但是要注意Android 9 +的系统对反射系统的类做了一些限制。
但是也有人完成了突破。
https://github.com/LSPosed/AndroidHiddenApiBypass

public static String getBaseband(){
        //public static String get(@NonNull String key, @Nullable String def)
        String retval=null;
        if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P){
            try {
                Class<?> clazz=Class.forName("android.os.SystemProperties") ;
                Object invoker= HiddenApiBypass.newInstance(clazz);
                Object result=HiddenApiBypass.invoke(clazz,invoker,"get","gsm.version.baseband", "no message");
                retval= (String) result;
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }else{
        try {
                //@SuppressLint("PrivateApi")
                Class<?> clazz=Class.forName("android.os.SystemProperties") ;
                Object invoker = clazz.newInstance();
                Method get=clazz.getDeclaredMethod("get",String.class,String.class);
                Object result=get.invoke(invoker,"gsm.version.baseband", "no message");
                Log.i("Fup1p1", (String) result);
                retval= (String) result;

            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
        return retval;
    }

获取cpu当前频率

public static String getCurCpuFreq(){
        String result="";
        String [] args={"cat","/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq"}; //第一个核心
        ProcessBuilder cmd = new ProcessBuilder(args);

        Process process= null;

        try {
            process = cmd.start();
            InputStream in= process.getInputStream();
            byte [] buf =new byte[16];
            while(in.read(buf)!=-1){
                result=result+ new String(buf);
            }
            in.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
        Log.i("Fup1p1","cpu0_cur_freq:"+result.trim());
        return result.trim();



    }

通过反射去获得手机序列号(Android 10以上无法实现)

开发者应该使用Build.getSerial()来获取设备序列号,但这也需要READ_PHONE_STATE权限,并且从Android 8.0(API 级别 26)开始,非系统应用需要用户明确同意才能授予这一权限。在Android 10及以上版本,只有具有特殊权限的应用(如设备所有者或设备策略控制器应用等)和具有系统权限的预装应用才能访问设备序列号

public static String getSerialNumbers() {
        String serial = "";
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {//9.0+
                serial = Build.getSerial();   //return service.getSerialForPackage(callingPackage, null); hook 方法实现,避免其返回null 或异常,返回硬件序列号
            } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {//8.0+
                serial = Build.SERIAL;
            } else {//8.0-
                Class<?> c = Class.forName("android.os.SystemProperties");
                Method get = c.getMethod("get", String.class);
                serial = (String) get.invoke(c, "ro.serialno");
            }
        } catch (Exception e) {
            e.printStackTrace();
            Log.e("e", "读取设备序列号异常:" + e.toString());
        }
        Log.i("Fup1p1","序列号: "+serial);
        return serial;
    }

Settings类入门

获取Android ID

在设备首次启动时,系统会随机生成一个64位的数字,并把这个数字以16进制字符串的形式保存下来 。 当设备被wipe后该值会被重置 (wipe:手机恢复出厂设置、刷机或其他类似操作),这就是Android ID。

Android 8之前,如果两个应用使用同一个用户权限运行,它们获取到的ANDROID_ID是相同的

Android 8 之后,如果两个应用是由同一个开发者签名,并且在同一用户的设备上以相同的用户身份(UID)运行,则它们将获得相同的 ANDROID_ID。但如果两个应用虽然有相同的签名,但被安装在不同用户的用户空间(如在多用户设备上),它们将获得不同的 ANDROID_ID,即使它们来自同一个开发者

app第一次启动时,先获取Android id,然后保存到本地数据库中,后续都从数据库中获取。

可以看看珍惜大佬的文章Link

    public static  String getAndroidID(Context context){
        String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
        Log.i("Fup1p1","Android_id: "+androidId);
        return androidId;
    }

我们可以hook getString 和 getStringForUser这两个函数
image-1698752670553

分析getStringForUser函数

可以发现最后会走到mMap.get(key),然后得到android_id
image
最后还保存了android的值并且返回。
image-1698817556090

获取sd卡的大小


//-----------------------------------------------方法一-------------------------------------------------
jclass clazz=env->FindClass("android/os/StatFs");
    //new StatFs(path);
    //通过反射去调用方法获得sd卡的大小
    jmethodID methd_id=env->GetMethodID(clazz,"<init>", "(Ljava/lang/String;)V");
    auto obj=env->NewObject(clazz,methd_id,env->NewStringUTF("/storage/emulated/0"));
    //getTotalBytes
    jmethodID mtd_gettotal=env->GetMethodID(clazz,"getTotalBytes","()J");

    auto num=env->CallLongMethod(obj,mtd_gettotal);

    LOGI("SD 大小: %ld",num);

//-----------------------------------------------方法二-------------------------------------------------


    //open() 函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程。
    char buffer[1024];
    FILE *fp= popen("stat -f /storage/emulated/0","r");//“r” 则文件指针连接到 command 的标准输出,“w” 则文件指针连接到 command 的标准输入
    if(fp!= nullptr){
        while (fgets(buffer, sizeof(buffer), fp) != nullptr) {
            LOGI("stat -f /storage/emulated/0 :%s",buffer);
        };
    }
    pclose(fp);


//-----------------------------------------------方法三-------------------------------------------------
    //#include <sys/statfs.h>
    struct statfs64 buf={};
    if(statfs64("/storage/emulated/0",&buf)== -1){
        LOGI(" statfs Failed!");
    }
    LOGI("statfs : %ld",buf.f_blocks);

包名检查

方法一: 通过执行pm list packages,来查看安装的所有包名,但是需要高权限,不然访问不到其他应用的包名。

方法二:直接通过API去得到已安装的包名。packageManager.getInstalledPackages

方法三、方法四:通过检查data/data/目录下的包名。但是Android10以上对权限的管理非常严格,在Android8还是可行的。

public class packagecheck {
    static String[] pack = {
            "topjohnwu",
            "magisk",
            "vvb2060",
            "xposed",
            "lsplant",
            "edxposed",
            "lsposed",
            "top.canyie.dreamland.manager",
            "me.weishu.exp",
            "hidemyapplist",
            "com.android.vendinf",
            "com.tsng.hidemyapplist",
            "cn.geektang.privacyspace",
            "moe.shizuku.redirectstorage"};
    public static boolean check(){
        boolean flag=false;
        String line;
        try {
            Process p = Runtime.getRuntime().exec("pm list packages");
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8));
            while ((line = bufferedReader.readLine()) != null) {
//                line = line.trim();
                if (line.startsWith("package")) {
                    line = line.substring(8);
                    for (int i = 0; i < pack.length; i++) {
                        if (line.contains(pack[i])) {
                            flag = true;
                            Log.i("Fup1p1", "可疑包名: " + line);
                        }
                    }
                }
            }

            // 等待进程执行完成
            p.waitFor();

            // 关闭输入流
            bufferedReader.close();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }

        return flag;
    }
    public static boolean check2(Context context){
        PackageManager packageManager = context.getPackageManager();
        List<PackageInfo> packageList = packageManager.getInstalledPackages(0);
        for (PackageInfo packageInfo : packageList) {
            for(int i=0;i<pack.length;i++){
                if(packageInfo.packageName.contains(pack[i])){
                    Log.i("Fup1p1","可疑的包名: "+packageInfo.packageName);
                }
            }
        }
        return false;
    }
    public static boolean checkPath(){//Android10以上不行
        String[] path ={"/data/data/","/data/user_de/0/","/data/misc/profiles/ref/","/storage/emulated/0/Android/data/",
                "/storage/emulated/0/Android/media/","/storage/emulated/0/Android/obb/"};
        String[] pkg ={
                "org.lsposed.lsplant",
                "com.topjohnwu.magisk",
                "io.github.vvb2060.magisk",
                "io.github.vvb2060.magisk.lite",
                "de.robv.android.xposed.installer",
                "org.meowcat.edxposed.manager",
                "org.lsposed.manager",
                "top.canyie.dreamland.manager",
                "me.weishu.exp",
                "com.android.vendinf",
                "com.tsng.hidemyapplist",
                "cn.geektang.privacyspace",
                "moe.shizuku.redirectstorage"
        };
        for (int i = 0; i < path.length; i++) {
            for (int j = 0; j < pkg.length; j++) {
                String pkgPath = path[i]+pkg[j];
//                Log.d("abcd", "path: "+pkgPath);
                if (new File(pkgPath).exists()){
                    Log.i("Fup1p1", "path: "+pkgPath);
                    return true;
                }
            }
        }
        return false;
    }
public static boolean nativecheckPath(){
    String[] path ={"/data/data/","/data/user_de/0/","/data/misc/profiles/ref/","/storage/emulated/0/Android/data/",
            "/storage/emulated/0/Android/media/","/storage/emulated/0/Android/obb/"};
    String[] pkg ={
            "org.lsposed.lsplant",
            "com.topjohnwu.magisk",
            "io.github.vvb2060.magisk",
            "io.github.vvb2060.magisk.lite",
            "de.robv.android.xposed.installer",
            "org.meowcat.edxposed.manager",
            "org.lsposed.manager",
            "top.canyie.dreamland.manager",
            "me.weishu.exp",
            "com.android.vendinf",
            "com.tsng.hidemyapplist",
            "cn.geektang.privacyspace",
            "moe.shizuku.redirectstorage"
    };
    for (int i = 0; i < path.length; i++) {
        for (int j = 0; j < pkg.length; j++) {
            String pkgPath = path[i]+pkg[j];
//                Log.d("abcd", "path: "+pkgPath);
            if (nativedetect(pkgPath)){
                Log.i("Fup1p1", "native check path: "+pkgPath);
                return true;
            }
        }
    }
    return false;
}
public static native boolean nativedetect(String str);
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_anti_check_packagecheck_nativedetect(JNIEnv *env, jclass clazz,jstring str) {
    const char * cpath=env->GetStringUTFChars(str, nullptr);
    if(syscall(SYS_faccessat,AT_FDCWD,cpath,0)>=0){
        return true;
    }else{
        struct stat buf{};
        //使用linux内核提供的方法
        if(access(cpath,F_OK)==0){
            LOGI("JNI access :%s",cpath);
            return true;
        };
        if(stat(cpath,&buf)==0){//linux 文件属性结构体
            LOGI("JNI stat :%s",cpath);
            return true;
        }
        if(fstat(open(cpath,O_PATH),&buf)==0){
            LOGI("JNI fstat :%s",cpath);
            return true;
        }
    }
    return false;

}

Hook 检查

原理挺简单的,就是比较内存和本地的libc.so的指令。怎么比较的呢,就是根据ELF的节区表,去找拥有可执行权限的节区,然后遍历节区的每一条指令,本文中是累加指令的值,然后比较。也可以使用CRC32 或者 MD5去校验都是可行的。

还要注意Android的版本问题,Android 10+开始就引入了APEX,libc.so被放到了apex目录下。system/lib 里的是通过链接,原目录还是在apex下,后文中有介绍

checkinlinehook.cpp

#include <sys/system_properties.h>
#include "checkinlinehook.h"
#include "File.h"
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <string>
#include <dlfcn.h>
using namespace std;
int checkMaps(sectionResult* section,const char * path);
int scanTextSegment(char *line, sectionResult *section, const char *pathLib,  bool b);
int getSdk(){
    char sdk_version[32]={0};
    __system_property_get("ro.build.version.sdk",sdk_version);
    int SDK_INT=-1;
    SDK_INT=stoi(sdk_version);
    return  SDK_INT;
}
#if defined(__LP64__)
typedef Elf64_Ehdr Elf_Ehdr;
typedef Elf64_Shdr Elf_Shdr;
typedef Elf64_Addr Elf_Addr;
typedef Elf64_Dyn Elf_Dyn;
typedef Elf64_Rela Elf_Rela;
typedef Elf64_Sym Elf_Sym;
typedef Elf64_Off Elf_Off;

#define ELF_R_SYM(i) ELF64_R_SYM(i)

#else
typedef Elf32_Ehdr Elf_Ehdr;
typedef Elf32_Shdr Elf_Shdr;
typedef Elf32_Addr Elf_Addr;
typedef Elf32_Dyn Elf_Dyn;
typedef Elf32_Rel Elf_Rela;
typedef Elf32_Sym Elf_Sym;
typedef Elf32_Off Elf_Off;

#define ELF_R_SYM(i) ELF32_R_SYM(i)
#endif
char * getlibc(){
    int SDK_INT=-1;
    SDK_INT=getSdk();
    if(SDK_INT==-1){
        LOGI("sdk search Erro!");
    }
#if defined(__aarch64__) //lib与lib64的区别
    char *libc;
    if (SDK_INT >= 30) {
            libc = (char *) "/apex/com.android.runtime/lib64/bionic/libc.so";
        } else if (SDK_INT >= 29) {
            libc = (char *) "/apex/com.android.runtime/lib64/bionic/libc.so";
        } else {
            libc = (char *) "/system/lib64/libc.so";
        }
#else
    char *libc;
    if (SDK_INT >= 30) {//Android 10(API 级别 29)及以上版本中,Google 引入了 APEX(Android Project Execution Environment)作为操作系统模块化的一部分,用以更新和改进系统核心组件,系统运行时和库已经逐渐迁移到 APEX 模块中.
        libc = (char *) "/apex/com.android.runtime/lib/bionic/libc.so";
    } else if (SDK_INT >= 29) {
        libc = (char *) "/apex/com.android.runtime/lib/bionic/libc.so";
    } else {
        libc = (char *) "/system/lib/libc.so";
    }
#endif
    return libc;
}
unsigned long addr;
//计算buf每个地址之间的和
static unsigned long checksum(void *buffer, size_t len){
    if(buffer==nullptr||len<100){
        return 0;

    }
    unsigned long seed=0;
    auto *buf=(uint8_t *)buffer;
    for(size_t i=0;i<len;i++){
        uint8_t *ptr=buf++;
        seed+=(unsigned long)(*ptr);

    };
    return seed;
}
bool checkInlinehook::checkinlinehook(){
    //计算本地so 文件 text plt段的指令,用于和内存中的指令比较。
    auto ret= compareTextSectionsCRC(getlibc());
    if(!ret.isCorrect){
        return false;
    }
    int ret1= checkMaps(&ret,getlibc());
    if(ret1>0)return true;
    return false;
}
sectionResult checkInlinehook::compareTextSectionsCRC(const char * path){
    Elf_Ehdr elf;
    Elf_Shdr shdr;
    int fd=-1;
    sectionResult  section={0};

    //打开libc.so
    fd=open(path,O_RDONLY);

    if(fd<0){
        LOGI("Failed to open file");
        return section;
    }

    read(fd,&elf,sizeof(Elf_Ehdr));

    int sectionCount=0; //用于区分 text段和plt段
    unsigned long offset[2]={0};//用于保存 需要的段的起始位置
    unsigned long c_size[2]={0};// 保存每个段的大小
    char * name;
    lseek(fd,(off_t)elf.e_shoff,SEEK_SET);//节头表的偏移


    //该文件中一共有多少个 section header
    for(int i=0;i<elf.e_shnum;i++){
        memset(&shdr,0,sizeof(Elf_Shdr));
        read(fd,&shdr,sizeof(Elf_Shdr));
        if(shdr.sh_flags&SHF_EXECINSTR){//sh_flags指示该section在进程执行时的特性   SHF_EXECINSTR(当前节包含可执行的机器指令),SHF_WRITE(当前节包含进程执行过程中可写的数据)...

            //拿到当前节头的偏移地址和偏移大小,然后最后和内存中的大小进行比较
            offset[sectionCount]=shdr.sh_offset;
            c_size[sectionCount]=shdr.sh_size;
            sectionCount++;
            if(sectionCount==2)break;
        }
    }

    if(sectionCount==0){
        LOGI("可执行节头信息获取失败");
        close(fd);
        return section;
    }


    section.sectionnum=sectionCount;
    section.startAddrinMem=0;
    for(int i=0;i<sectionCount;i++){
        lseek(fd,(off_t)offset[i],SEEK_SET);
        //fd 移动指针到fd指定的偏移位置  .text 的开始位置。

        auto buf= calloc(1,c_size[i]*sizeof(uint8_t));
        if(buf== nullptr){
            free(buf);
            buf= nullptr;
            return section;
        }

        read(fd,buf,c_size[i]);
        section.offset[i]=offset[i];
        section.memsize[i]=c_size[i];
        section.checksum[i]=checksum(buf,c_size[i]);
        LOGI("节头大小统计[%d]: %lu",i,section.checksum[i]);
        free(buf);
    }
    section.isCorrect= true;
    close(fd);
    return section;
}
int checkMaps(sectionResult* section,const char * path){
    if(section==nullptr)return false;
    auto mapspath=string("proc/").append(to_string(getpid())).append("/maps").c_str();
    FILE *maps= fopen(mapspath,"r");
    if(!maps){
        return false;
    }
    char line[256];
    int ret;
    bool firstaddr=true;
    while(fgets(line,sizeof(line),maps)){
        if(strstr(line,path)!=nullptr){
            ret= scanTextSegment(line, section, path, firstaddr);
            firstaddr=false;
            if(ret==1)break;
        }
    }
    fclose(maps);
    return ret;
}
/**

Android 8
70ffb60000-70ffc28000 r-xp 00000000 103:11 946       /system/lib64/libc.so
70ffc29000-70ffc2f000 r--p 000c8000 103:11 946       /system/lib64/libc.so
70ffc2f000-70ffc31000 rw-p 000ce000 103:11 946       /system/lib64/libc.so

Android 10
7753a1f000-7753a5f000 r--p 00000000 fd:00 331        /apex/com.android.runtime/lib64/bionic/libc.so
7753a5f000-7753b08000 --xp 00040000 fd:00 331        /apex/com.android.runtime/lib64/bionic/libc.so
7753b08000-7753b0b000 rw-p 000e9000 fd:00 331        /apex/com.android.runtime/lib64/bionic/libc.so
7753b0b000-7753b12000 r--p 000ec000 fd:00 331        /apex/com.android.runtime/lib64/bionic/libc.so


Android 11
748f916000-748f951000 r--p 00000000 07:88 33        /apex/com.android.runtime/lib64/bionic/libc.so
748f951000-748f9cc000 r-xp 0003b000 07:88 33        /apex/com.android.runtime/lib64/bionic/libc.so
748f9cc000-748f9d0000 r--p 000b6000 07:88 33        /apex/com.android.runtime/lib64/bionic/libc.so
748f9d0000-748f9d3000 rw-p 000b9000 07:88 33        /apex/com.android.runtime/lib64/bionic/libc.so
 * **/
int scanTextSegment(char* line, sectionResult *section,const char* pathLib,bool flag){
    unsigned long start;
    unsigned long end;
    char buf[32] = "";
    char path[256] = "";
    char tmp[128] = "";
    sscanf(line,"%lx-%lx %s %s %s %s",&start,&end,buf,tmp,tmp,tmp,path);
    if(getSdk()<29){//Android 10 +
        if(buf[2]=='x'){
            LOGI("line :%s",line);
            uint8_t *buffer;
            buffer=(uint8_t* )start;
            for(int i=0;i<section->sectionnum;i++){
                auto begin=(void *)(buffer+section->offset[i]);
                unsigned long size=section->memsize[i];
                unsigned long mapsSize=checksum(begin,size);
                LOGI("%s || 可执行段检查 内存so:%ld  <-->  本地so:%ld", pathLib, mapsSize, section->checksum[i]);
                if(mapsSize!=section->checksum[i]){
                    LOGI("可执行段被hook! %ld,%ld",mapsSize,section->checksum[i]);
                    return 1;
                }else{
                    LOGI("可执行段w未被hook! %ld,%ld",mapsSize,section->checksum[i]);
                }
            }
       }
    }

    else{

        if(flag){
           addr=start;
        }
        if(buf[2]=='x'){

                LOGI("line :%s",line);
//                uint8_t *buffer;
//                buffer=(uint8_t* )start;
                for(int i=0;i<section->sectionnum;i++){
                    auto begin=(void *)(addr+section->offset[i]);
                    unsigned long size=section->memsize[i];
                    unsigned long mapsSize=checksum(begin,size);
                    LOGI("%s || 可执行段检查 内存so:%ld  <-->  本地so:%ld", pathLib, mapsSize, section->checksum[i]);
                    if(mapsSize!=section->checksum[i]){
                        LOGI("可执行段被hook! %ld,%ld",mapsSize,section->checksum[i]);
                        return 1;
                    }else{
                        LOGI("可执行段w未被hook! %ld,%ld",mapsSize,section->checksum[i]);
                    }
                }
            }
    }

    return 0;
}

checkinlinehook.h

#pragma once
#include "utils.h"
#include <linux/elf.h>
#include <sys/mman.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct ExecSection{
    //目标节头数量
    int sectionnum;

    //用于保存目标节头在so中的偏移地址
    unsigned long offset[2];

    //用于保存目标节头数据的大小
    unsigned long memsize[2];

    //保存目标节头大小综合
    unsigned long checksum[2];
    unsigned long startAddrinMem;

    //是否完成
    bool isCorrect=false;
}sectionResult ;

class checkInlinehook{
public:
    static bool checkinlinehook();
    static sectionResult compareTextSectionsCRC(const char * path);
    static  bool checkSVChook();

};

实际应用,发现我们使用frida去hook libc.so时,是可以被检测到的。
image-1699363205142

枚举linker加载的so库

/system/bin/linker64 : 使用linker加载的所有so文件,都会将文件信息保存到 linker64 中的一个全局变量 __dl__ZL6solist 中,linker64因为不在系统白名单中,所以无法通过dlopen打开,我们可以通过解析静态文件,找到 __dl__ZL6solist 在内存中的位置,在遍历其中的 soinfo 结构体实现遍历通过linker加载的so文件

我们可以参考 riru源码项目的代码。

image-1699446328439

测试了一下,当使用frida hook上app后,输出的个数增加。
并且也输出了frida-agent-64.so 非常经典
image-1699446637851

frida 检测

遍历 proc/self/task/tid/status 去检查字符串
hluda 也没去掉这些特征。
image-1699491840187

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_anti_check_fridacheck_fridastatus(JNIEnv *env, jclass clazz) {
    DIR *dir=NULL;
    struct dirent *entry;
    char path[512];
    memset(path,0,sizeof(path));
    if((dir= opendir("/proc/self/task/"))==NULL){
        LOGE("打开失败");
    }else{
        entry= readdir(dir);
        while(entry!= nullptr){
            switch (entry->d_type) {
                case DT_DIR:
                    if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {//去除.和 ..
                        break;
                    }
                    sprintf(path,"%s/%s/status","proc/self/task",entry->d_name);
                    FILE *fp= fopen(path,"r");
                    char buf[1024];
                    if(fp== nullptr){
                        LOGI("open err");
                        fclose(fp);
                    }
                    while(fgets(buf,1024,fp)){
                        std::string  status=buf;
                        if(strstr(buf,"gmain")!= nullptr||strstr(buf,"gum-js-loop")!= nullptr||strstr(buf,"frida")!= nullptr||strstr(buf,"ggdbus")!= nullptr){
                            LOGI(" frida detected!!! %s  path :%s ",buf,path);
                        }
                    }
                    fclose(fp);


            }
            entry= readdir(dir);

        }
    }
    return false;
}
0

评论区