安卓 反调试 环境检测点汇总 hook( 面具 xp ida frida )检测点 和 绕过策略

属于到处收集的资料和工作中碰到的,在此感谢文末出处链接的作者

我把他们分为三大部分,由浅至深,实际上检测点实在太多了,肯定不全的,如果有漏掉的麻烦大佬在评论区指出

Java

在java层检测的实际很容易就能找到,因为app反调试的反映只有几种

  • 闪退
  • 弹窗
  • 卡启动页

实际上甚至可以无视上面几种,无脑hook所有函数,再过滤掉启动必须的函数就能找到,java层能实现的,native层也能实现

0.检查应用列表
            String packageName = context.getPackageName();
            PackageManager pm = context.getPackageManager();
            List<PackageInfo> pkgs = pm.getInstalledPackages(0);

改面具名字就绕过了

1.应用主动申请root权限
Runtime.getRuntime().exec("su")

如果提权成功的话就是root了

2.获取系统build.prop属性(native也能检测)

如ro.build.tags/ro.build.type/ro.secure/ro.debuggable
如果开了系统debugger或root了,分别为 userdebugger 和test-keys
以下方案都能获取到

SystemPropertiesProxy.get(this, "ro.build.version.release")
android.os.Build.TAGS.contains("test-keys")

后者hook string的contains就行

3.是否存在XXX提权app或链接库例如magisk,su之类(native也能检测)
        String[] paths = {"/sbin/su",
                "/system/bin/su",
                "/system/xbin/su",
                "/data/local/xbin/su",
                "/data/local/bin/su",
                "/system/sd/xbin/su",
                "/system/bin/failsafe/su",
                "/data/local/su"};
        for (String path : paths) {
            file = new File(path);
            if (file.exists()) return true;//可以继续做可执行判断
        }
4.执行指令/which命令找su/busybox(native也能检测)
ArrayList<String> execResult = executeCommand(new String[] {"/system/xbin/which","su"});
ArrayList<String> execResult = executeCommand(new String[] {"busybox","df"});
5.写入文件到/data或者/etc之类(native也能检测)
FileOutputStream fout = new FileOutputStream(fileName);
byte [] bytes = message.getBytes();
fout.write(bytes);
fout.close();

这个估计还有很多类似的方法

6. 检测app本身是否debugger模式
(context.getApplicationInfo().flags
                & ApplicationInfo.FLAG_DEBUGGABLE) != 0
android.os.Debug.isDebuggerConnected()
7.扫描常用逆向应用端口
 InetAddress theAddress = InetAddress.getByName(host);
        try {
            Socket socket = new Socket(theAddress, port);
            flag = true;
        } catch (IOException e) {
        }
8.加载常用逆向应用的类
类名->de.robv.android.xposed.XposedHelpers/de.robv.android.xposed.XposedBridge
Object xpHelperObj = ClassLoader
                    .getSystemClassLoader()
                    .loadClass(类名)
                    .newInstance();
9.检查堆栈
 try {
            throw new Exception("gg");
        } catch (Exception e) {
            for (StackTraceElement stackTraceElement : e.getStackTrace()) {
                if (stackTraceElement.getClassName().contains("de.robv.android.xposed.XposedBridge")) return true;
            }
            return false;
        }

这里xp有个漏洞可以app反过来把xp关掉

10.扫描/proc/{pid}/maps 是否有hook相关so
    public boolean hasReadProcMaps(String paramString) {
        try {
            Object localObject = new HashSet();
            BufferedReader localBufferedReader =
                    new BufferedReader(new FileReader("/proc/" + Process.myPid() + "/maps"));
            for (; ; ) {
                String str = localBufferedReader.readLine();
                if (str == null) {
                    break;
                }
                if ((str.endsWith(".so")) || (str.endsWith(".jar"))) {
                    ((Set) localObject).add(str.substring(str.lastIndexOf(" ") + 1));
                }
            }
            localBufferedReader.close();
            localObject = ((Set) localObject).iterator();
            while (((Iterator) localObject).hasNext()) {
                boolean bool = ((String) ((Iterator) localObject).next()).contains(paramString);
                if (bool) {
                    return true;
                }
            }
        } catch (Exception fuck) {
        }
        return false;
    }

这个可以用来扫描frida的注入噢 ,这里还能扫描系统的其他目录,可以获取到很多信息,但是主要还是得Filereader~


java层的大概就这些了,实际ndk层的也差不多,但是实现方式更加曲折.
然而java层的其实很容易就找的出来,除非真的同时全用上了,又hook不上,又挂不上root,那就只能一层一层hook.
有个专搞aosp编译系统的朋友,直接发了个改过对应特征的系统,可以免root+hook直接随意翱翔于这一层甚至ndk层了

NDK

Frida

当然不是说只有这个,本文的其它也能检测frida,举一反三

https://github.com/b-mueller/frida-detection-demo
https://github.com/darvincisec/DetectFrida

Xposed

https://github.com/w568w/XposedDetectLib
https://github.com/KagurazakaHanabi/XposedHider
https://github.com/vvb2060/XposedDetector

1.端口检测
int num = 54321;

//检测常用的端口
void check()
{
    FILE* pfile=NULL;
    char buf[0x1000]={0};
    // 执行命令
    char* strCatTcp= "cat /proc/net/tcp |grep :5D8A";
    char* strNetstat="netstat |grep :23946";
    pfile=popen(strCatTcp,"r");
	int pid=getpid();
    if(NULL==pfile)
    {
        printf("打开命令失败!\n");
        return;
    }
    // 获取结果
    while(fgets(buf,sizeof(buf),pfile))
    {
        // 执行到这里,判定为调试状态
        printf("执行cat /proc/net/tcp |grep :5D8A的结果:\n");
        printf("%s",buf);
		int ret=kill(pid,SIGKILL);
    }
    pclose(pfile);
}

无限查端口并杀掉来防止调试
通过hook fopen或者popen可以绕过

2.进程名称检测
//进程名称检测
void coursecheck(){
	const int bufsize = 1024;
    char filename[bufsize];
    char line[bufsize];
    char name[bufsize];
    char nameline[bufsize];
    int pid = getpid();
    //先读取Tracepid的值
    sprintf(filename, "/proc/%d/status", pid);
    FILE *fd=fopen(filename,"r");
    if(fd!=NULL)
	{
        while(fgets(line,bufsize,fd))
		{
            if(strstr(line,"TracerPid")!=NULL)
            {
                int statue =atoi(&line[10]);
                if(statue!=0)
				{
                    sprintf(name,"/proc/%d/cmdline",statue);
                    FILE *fdname=fopen(name,"r");
                    if(fdname!= NULL)
					{
                        while(fgets(nameline,bufsize,fdname))
						{
                            if(strstr(nameline,"android_server")!=NULL)
							{
                                int ret=kill(pid,SIGKILL);
                            }
                        }
                    }
                    fclose(fdname);
                }
            }
        }
    }
    fclose(fd);
}

这个可以通过hook strstr来看看对比了啥,然后加个判断来绕过

2.5.进程名检测
void CheckParents()
{
///
// 设置buf
char strPpidCmdline[0x100] = { 0};
    snprintf(strPpidCmdline, sizeof(strPpidCmdline), "/proc/%d/cmdl
ine", getppid());
// 打开文件
int file = open(strPpidCmdline, O_RDONLY);
    if (file < 0) {
        LOGA("CheckParents open错误!\n");
        return;
    }
    // 文件内容读入内存
    memset(strPpidCmdline, 0, sizeof(strPpidCmdline));
ssize_t ret = read(file, strPpidCmdline, sizeof(strPpidCmdline));
    if (-1 == ret) {
        LOGA("CheckParents read错误!\n");
        return;
    }
// 没找到返回0
char sRet = strstr(strPpidCmdline, "zygote");
    if (NULL == sRet) {
        // 执行到这里,判定为调试状态
        LOGA("父进程cmdline没有zygote子串!\n");
        return;
    }
int i = 0;
    return;
}

有的时候不使用apk附加调试的方法进行逆向,而是写一个.out可执行文件直接加载so进行
调试,这样程序的父进程名和正常启动apk的父进程名是不一样的。
读取/proc/pid/cmdline,查看内容是否为zygote

2.75进程数量检测

APK线程检测
正常apk进程一般会有十几个线程在运行(比如会有jdwp线程),
自己写可执行文件加载so一般只有一个线程,
可以根据这个差异来进行调试环境检测

void CheckTaskCount()
{
char buf[0x100] = { 0};
    char * str="/proc/%d/task";
    snprintf(buf, sizeof(buf), str, getpid());
    // 打开目录:
    DIR * pdir = opendir(buf);
    if (!pdir) {
        perror("CheckTaskCount open() fail.\n");
        return;
    }
// 查看目录下文件个数:
struct dirent * pde=NULL;
int Count = 0;
    while ((pde = readdir(pdir))) {
        // 字符过滤
        if ((pde -> d_name[0] <= '9') && (pde -> d_name[0] >= '0')) {
            ++Count;
            LOGB("%d 线程名称:%s\n", Count, pde -> d_name);
        }
    }
    LOGB("线程个数为:%d", Count);
    if (1 >= Count) {
        // 此处判定为调试状态.
        LOGA("调试状态!\n");
    }
int i = 0;
    return;
}
3.检查时间差

这个跟time有关,hook所有时间的堆栈应该会有收获
五个能获取时间的api:
time()函数
time_t结构体
clock()函数
clock_t结构体
gettimeofday()函数
timeval结构
timezone结构
clock_gettime()函数
timespec结构
getrusage()函数
rusage结构

4.ptrace
void anti_ptrace(void)  
{  
    pid_t child;  
 
    // 创建子进程
    child = fork();  
    if (child) 
    {
        // 返回在父进程中
        wait(NULL);  
    }  
    else 
    {
        // 获取父进程的pid
        pid_t parent = getppid();  
        // ptrace附加父进程
        if (ptrace(PTRACE_ATTACH, parent, 0, 0) < 0)
        {
        while(1);  
        sleep(1);  
        } 
 
        // 释放附加的进程
        ptrace(PTRACE_DETACH, parent, 0, 0);  
        // 结束当前进程
        exit(0);  
    }  
}  

zygote通过fork()系统调用,fork出一个app

app内通过ptrace(PTRACE_TRACEME,0,0,0);将父进程zygote做为自己的tracer

这样其他进程就无法ptrace()到app进程了

一些进程文件中存储了进程信息,可以读取这些信息得知是否为调试状态。
第一种:
/proc/pid/status
/proc/pid/task/pid/status
TracerPid非0
statue字段中写入t(tracing stop)
第二种:
/proc/pid/stat
/proc/pid/task/pid/stat
第二个字段是t(T)
第三种:
/proc/pid/wchan
/proc/pid/task/pid/wchan
ptrace_stop

5. 断点调试指令检测
void checkbkpt(u8* addr,u32 size)
{
    //结果
    u32 uRet=0;
    //断点指令
    u8 armBkpt[4]={0xf0,0x01,0xf0,0xe7};
    u8 thumbBkpt[2]={0x10,0xde};
    int mode=(u32)addr%2;
    if(1==mode)
    {
        u8* start=(u8*)((u32)addr-1);
        u8* end=(u8*)((u32)start+size);
        while(1)
        {
            if(start>=end)
            {
                uRet=0;
                return;
            }
            if(0==memcmp(start,thumbBkpt,2))
            {
                uRet=1;
                break;
            }
            start=start+2;
        }
    } else{
        //arm
        u8* start=(u8*)addr;
        u8* end=(u8*)((u32)start+size);
        while (1)
        {
            if(start>=end)
            {
                uRet=0;
                return;
            }
            if(0==memcmp(start,armBkpt,4))
            {
                uRet=1;
                break;
            }
            start=start+4;
        }
        
    }
}

如果被下了断点的话,指令会被替换成(bkpt断点指令),那么在内存搜索一下不就完事了吗,注意arm和thumb指令有所区别

6.系统源码检测

bool checkSystem()
{
// 建立管道
int pipefd[2];
    if (-1 == pipe(pipefd)) {
        LOGA("pipe() error.\n");
        return false;
    }
// 创建子进程
pid_t pid = fork();
    LOGB("father pid is: %d\n", getpid());
    LOGB("child pid is: %d\n", pid);
    // for失败
    if (0 > pid) {
        LOGA("fork() error.\n");
        return false;
    }
// 子进程程序
int childTracePid = 0;
    if (0 == pid) {
int iRet = ptrace(PTRACE_TRACEME, 0, 0, 0);
        if (-1 == iRet) {
            LOGA("child ptrace failed.\n");
            exit(0);
        }
        LOGA("%s ptrace succeed.\n");
// 获取tracepid
char pathbuf[0x100] = { 0};
char readbuf[100] = { 0};
        sprintf(pathbuf, "/proc/%d/status", getpid());
int fd = openat(NULL, pathbuf, O_RDONLY);
        if (-1 == fd) {
            LOGA("openat failed.\n");
        }
        read(fd, readbuf, 100);
        close(fd);
        uint8_t * start = (uint8_t *) readbuf;
uint8_t des[100] = {
            0x54, 0x72, 0x61, 0x63, 0x65, 0x72, 0x5
0, 0x69, 0x64, 0x3A, 0x09};
int i = 100;
bool flag = false;
        while (--i) {
            if (0 == memcmp(start, des, 10)) {
                start = start + 11;
                childTracePid = atoi((char *)start);
                flag = true;
                break;
            } else {
                start = start + 1;
                flag = false;
            }
        }//while
        if (false == flag) {
            LOGA("get tracepid failed.\n");
            return false;
        }
        // 向管道写入数据
        close(pipefd[0]); // 关闭管道读端
        write(pipefd[1], (void*) & childTracePid, 4); // 向管道写端写入
        数据
        close(pipefd[1]); // 写完关闭管道写
        端
        LOGA("child succeed, Finish.\n");
        exit(0);
    }
    else {
        // 父进程程序
        LOGA("开始等待子进程.\n");
        waitpid(pid, NULL, NULL); // 等待子进程
        结束
int buf2 = 0;
        close(pipefd[1]); // 关闭写端
        read(pipefd[0], (void*) & buf2, 4); // 从读端读取
        数据到buf
        close(pipefd[0]); // 关闭读端
        LOGB("子进程传递的内容为:%d\n", buf2); // 输出内容
        // 判断子进程ptarce后的tracepid
        if (0 == buf2) {
            LOGA("源码被修改了.\n");
        } else {
            LOGA("源码没有被修改.\n");
        }
        return true;
    }
}

这个比较6,是检查自己附加之后traceid有没有变,没变就是改源码了

7.Inotify事件监控dump

void thread_watchDumpPagemap()
{
    LOGA("-------------------watchDump:Pagemap-------------------
        \n");
char dirName[NAME_MAX] = { 0};
    snprintf(dirName, NAME_MAX, "/proc/%d/pagemap", getpid());
int fd = inotify_init();
    if (fd < 0) {
        LOGA("inotify_init err.\n");
        return;
    }
int wd = inotify_add_watch(fd, dirName, IN_ALL_EVENTS);
    if (wd < 0) {
        LOGA("inotify_add_watch err.\n");
        close(fd);
        return;
    }
    const int buflen = sizeof(struct inotify_event) * 0x100;
char buf[buflen] = { 0};
fd_set readfds;
    while (1) {
        FD_ZERO(& readfds);
        FD_SET(fd, & readfds);
int iRet = select(fd + 1,& readfds, 0, 0, 0); // 此处阻塞
        LOGB("iRet的返回值:%d\n", iRet);
        if (-1 == iRet)
            break;
        if (iRet) {
            memset(buf, 0, buflen);
int len = read(fd, buf, buflen);
int i = 0;
            while (i < len) {
struct inotify_event * event = (struct inotify_even
                t *)& buf[i];
                LOGB("1 event mask的数值为:%d\n", event -> mask);
                if ((event -> mask == IN_OPEN)) {
                    // 此处判定为有true,执行崩溃.
                    LOGB("2 有人打开pagemap,第%d次.\n\n", i);
                    //__asm __volatile(".int 0x8c89fa98");
                }
                i += sizeof(struct inotify_event) + event -> len;
            }
            LOGA("-----3 退出小循环-----\n");
        }
    }
    inotify_rm_watch(fd, wd);
    close(fd);
    LOGA("-----4 退出大循环,关闭监视-----\n");
    return;
}
void smain()
{
// 监控/proc/pid/mem
pthread_t ptMem, t, ptPageMap;
int iRet = 0;
    // 监控/proc/pid/pagemap
    iRet = pthread_create(& ptPageMap, NULL, (PPP)thread_watchDumpPagema
p, NULL);
    if (0 != iRet) {
        LOGA("Create,thread_watchDumpPagemap,error!\n");
        return;
    }
    iRet = pthread_detach(ptPageMap);
    if (0 != iRet) {
        LOGA("pthread_detach,thread_watchDumpPagemap,error!\n");
        return;
    }
    LOGA("-------------------smain-------------------\n");
    LOGB("pid:%d\n", getpid());
    return;
}

通常壳会在程序运行前完成对text的解密,所以脱壳可以通过dd与gdb_gcore来dump
/proc/pid/mem或/proc/pid/pagemap,获取到解密后的代码内容。
可以通过Inotify系列api来监控mem或pagemap的打开或访问事件,
一旦发生时间就结束进程来阻止dump。

8.扫描目录文件适配关键词

interceptor
这个是我最近碰到的,实际原理就是root的权限比较大,可以扫描隔壁的文件夹,这个app就真的全扫了,然后匹配文字,属实🐮

当然以上都只是检测点冰山一角,很多内存中的异常都是检测点


9…基于信号的检测

signal() raise() 发送信号
利用IDA先截获信号特性的检测
IDA总是先于我们的应用程序截获信号
signal(SIGTRAP, myhandler); SIGTRAP:调试信号 myhandler:信号处理函数(自己实现的)
信号处理函数 可以设置一个全局变量
终止进程方式可以kill进程 或者 sleep()
先给程序设置signal 并实现信号处理函数
在关键函数里或者开一个线程 隔时 发送信号 即 raise()
若信号未收到 则程序被调试
信号 消息机制 被捕获就会消失 一次性

10./task/pid/status

扫描task目录下所有/task/pid/status中的Name字段寻找是否存在frida注入的特征,具体线程名为
gmain 、 gdbus 和 gum-js- loop ,一般情况下这三个线程在第11–13的位置,此外在frida运行脚本过
程中,还会存在一个Name字段为 pool-frida 的线程。

11.readlink

通过readlink查看 /proc/self/fd 和 /proc/self/task/pid/fd 下所有打开的文件,检测是否有frida
相关文件。

12.D-BUS

Frida是通过D-Bus协议进行通信的,所以可以遍历/proc/net/tcp文件,向每个开放的端口发送 D-Bus 的
认证消息 AUTH ,如果端口回复了 REJECT ,那么这个端口就是frida-server

绕过脚本参考

  • anti_fgets();
    anti_exit();
    anti_fork();
    anti_kill();
    anti_ptrace();
    https://github.com/deathmemory/FridaContainer/blob/011c205d5ef1372b697ed04a66f69b8fd75680ed/utils/android/Anti.ts

  • 签名校验之类
    https://github.com/axhlzy/Il2CppHookScripts/blob/862b39b695540008ff06ed2b15e1a1ec98b44fa3/Others/FTS/fts.js

系统

  • frida syscalls https://github.com/FrenchYeti/frida-syscall/blob/main/index.js
  • frida syscalls https://github.com/AeonLucid/frida-syscall-interceptor
  • 内联汇编https://www.jianshu.com/p/7f5081b9f000
  • io重定向http://www.yxfzedu.com/rs_show/1447

写的修改系统源码
https://blog.csdn.net/weixin_42453905/article/details/122462984?spm=1001.2014.3001.5501

参考文章
https://blog.csdn.net/lintax/article/details/70988565
https://www.jianshu.com/p/c37b1bdb4757

https://forum.butian.net/share/776
http://pwn4.fun/2017/04/15/Android%E9%80%86%E5%90%91%E4%B9%8B%E5%8F%8D%E8%B0%83%E8%AF%95/
https://coar.wang/article/42
https://www.freebuf.com/articles/mobile/291894.html
https://www.jianshu.com/p/55d7bb3ba5af

  • 4
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值