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进行下载的)
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
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这两个函数
分析getStringForUser函数
可以发现最后会走到mMap.get(key),然后得到android_id
最后还保存了android的值并且返回。
获取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时,是可以被检测到的。
枚举linker加载的so库
/system/bin/linker64 : 使用linker加载的所有so文件,都会将文件信息保存到 linker64 中的一个全局变量 __dl__ZL6solist 中,linker64因为不在系统白名单中,所以无法通过dlopen打开,我们可以通过解析静态文件,找到 __dl__ZL6solist 在内存中的位置,在遍历其中的 soinfo 结构体实现遍历通过linker加载的so文件
我们可以参考 riru源码项目的代码。
测试了一下,当使用frida hook上app后,输出的个数增加。
并且也输出了frida-agent-64.so 非常经典
frida 检测
遍历 proc/self/task/tid/status 去检查字符串
hluda 也没去掉这些特征。
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;
}
评论区